spoiler.py 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. import re
  2. from typing import TYPE_CHECKING, Match
  3. if TYPE_CHECKING:
  4. from ..block_parser import BlockParser
  5. from ..core import BaseRenderer, BlockState, InlineState
  6. from ..inline_parser import InlineParser
  7. from ..markdown import Markdown
  8. __all__ = ["spoiler"]
  9. _BLOCK_SPOILER_START = re.compile(r"^ {0,3}! ?", re.M)
  10. _BLOCK_SPOILER_MATCH = re.compile(r"^( {0,3}![^\n]*\n)+$")
  11. INLINE_SPOILER_PATTERN = r">!\s*(?P<spoiler_text>.+?)\s*!<"
  12. def parse_block_spoiler(block: "BlockParser", m: Match[str], state: "BlockState") -> int:
  13. text, end_pos = block.extract_block_quote(m, state)
  14. if not text.endswith("\n"):
  15. # ensure it endswith \n to make sure
  16. # _BLOCK_SPOILER_MATCH.match works
  17. text += "\n"
  18. depth = state.depth()
  19. if not depth and _BLOCK_SPOILER_MATCH.match(text):
  20. text = _BLOCK_SPOILER_START.sub("", text)
  21. tok_type = "block_spoiler"
  22. else:
  23. tok_type = "block_quote"
  24. # scan children state
  25. child = state.child_state(text)
  26. if state.depth() >= block.max_nested_level - 1:
  27. rules = list(block.block_quote_rules)
  28. rules.remove("block_quote")
  29. else:
  30. rules = block.block_quote_rules
  31. block.parse(child, rules)
  32. token = {"type": tok_type, "children": child.tokens}
  33. if end_pos:
  34. state.prepend_token(token)
  35. return end_pos
  36. state.append_token(token)
  37. return state.cursor
  38. def parse_inline_spoiler(inline: "InlineParser", m: Match[str], state: "InlineState") -> int:
  39. text = m.group("spoiler_text")
  40. new_state = state.copy()
  41. new_state.src = text
  42. children = inline.render(new_state)
  43. state.append_token({"type": "inline_spoiler", "children": children})
  44. return m.end()
  45. def render_block_spoiler(renderer: "BaseRenderer", text: str) -> str:
  46. return '<div class="spoiler">\n' + text + "</div>\n"
  47. def render_inline_spoiler(renderer: "BaseRenderer", text: str) -> str:
  48. return '<span class="spoiler">' + text + "</span>"
  49. def spoiler(md: "Markdown") -> None:
  50. """A mistune plugin to support block and inline spoiler. The
  51. syntax is inspired by stackexchange:
  52. .. code-block:: text
  53. Block level spoiler looks like block quote, but with `>!`:
  54. >! this is spoiler
  55. >!
  56. >! the content will be hidden
  57. Inline spoiler is surrounded by `>!` and `!<`, such as >! hide me !<.
  58. :param md: Markdown instance
  59. """
  60. # reset block quote parser with block spoiler parser
  61. md.block.register("block_quote", None, parse_block_spoiler)
  62. md.inline.register("inline_spoiler", INLINE_SPOILER_PATTERN, parse_inline_spoiler)
  63. if md.renderer and md.renderer.NAME == "html":
  64. md.renderer.register("block_spoiler", render_block_spoiler)
  65. md.renderer.register("inline_spoiler", render_inline_spoiler)