index.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. """Process block-level custom containers."""
  2. from __future__ import annotations
  3. from math import floor
  4. from typing import TYPE_CHECKING, Any, Callable, Sequence
  5. from markdown_it import MarkdownIt
  6. from markdown_it.rules_block import StateBlock
  7. from mdit_py_plugins.utils import is_code_block
  8. if TYPE_CHECKING:
  9. from markdown_it.renderer import RendererProtocol
  10. from markdown_it.token import Token
  11. from markdown_it.utils import EnvType, OptionsDict
  12. def container_plugin(
  13. md: MarkdownIt,
  14. name: str,
  15. marker: str = ":",
  16. validate: None | Callable[[str, str], bool] = None,
  17. render: None | Callable[..., str] = None,
  18. ) -> None:
  19. """Plugin ported from
  20. `markdown-it-container <https://github.com/markdown-it/markdown-it-container>`__.
  21. It is a plugin for creating block-level custom containers:
  22. .. code-block:: md
  23. :::: name
  24. ::: name
  25. *markdown*
  26. :::
  27. ::::
  28. :param name: the name of the container to parse
  29. :param marker: the marker character to use
  30. :param validate: func(marker, param) -> bool, default matches against the name
  31. :param render: render func
  32. """
  33. def validateDefault(params: str, *args: Any) -> bool:
  34. return params.strip().split(" ", 2)[0] == name
  35. def renderDefault(
  36. self: RendererProtocol,
  37. tokens: Sequence[Token],
  38. idx: int,
  39. _options: OptionsDict,
  40. env: EnvType,
  41. ) -> str:
  42. # add a class to the opening tag
  43. if tokens[idx].nesting == 1:
  44. tokens[idx].attrJoin("class", name)
  45. return self.renderToken(tokens, idx, _options, env) # type: ignore[attr-defined,no-any-return]
  46. min_markers = 3
  47. marker_str = marker
  48. marker_char = marker_str[0]
  49. marker_len = len(marker_str)
  50. validate = validate or validateDefault
  51. render = render or renderDefault
  52. def container_func(
  53. state: StateBlock, startLine: int, endLine: int, silent: bool
  54. ) -> bool:
  55. if is_code_block(state, startLine):
  56. return False
  57. auto_closed = False
  58. start = state.bMarks[startLine] + state.tShift[startLine]
  59. maximum = state.eMarks[startLine]
  60. # Check out the first character quickly,
  61. # this should filter out most of non-containers
  62. if marker_char != state.src[start]:
  63. return False
  64. # Check out the rest of the marker string
  65. pos = start + 1
  66. while pos <= maximum:
  67. try:
  68. character = state.src[pos]
  69. except IndexError:
  70. break
  71. if marker_str[(pos - start) % marker_len] != character:
  72. break
  73. pos += 1
  74. marker_count = floor((pos - start) / marker_len)
  75. if marker_count < min_markers:
  76. return False
  77. pos -= (pos - start) % marker_len
  78. markup = state.src[start:pos]
  79. params = state.src[pos:maximum]
  80. assert validate is not None
  81. if not validate(params, markup):
  82. return False
  83. # Since start is found, we can report success here in validation mode
  84. if silent:
  85. return True
  86. # Search for the end of the block
  87. nextLine = startLine
  88. while True:
  89. nextLine += 1
  90. if nextLine >= endLine:
  91. # unclosed block should be autoclosed by end of document.
  92. # also block seems to be autoclosed by end of parent
  93. break
  94. start = state.bMarks[nextLine] + state.tShift[nextLine]
  95. maximum = state.eMarks[nextLine]
  96. if start < maximum and state.sCount[nextLine] < state.blkIndent:
  97. # non-empty line with negative indent should stop the list:
  98. # - ```
  99. # test
  100. break
  101. if marker_char != state.src[start]:
  102. continue
  103. if is_code_block(state, nextLine):
  104. continue
  105. pos = start + 1
  106. while pos <= maximum:
  107. try:
  108. character = state.src[pos]
  109. except IndexError:
  110. break
  111. if marker_str[(pos - start) % marker_len] != character:
  112. break
  113. pos += 1
  114. # closing code fence must be at least as long as the opening one
  115. if floor((pos - start) / marker_len) < marker_count:
  116. continue
  117. # make sure tail has spaces only
  118. pos -= (pos - start) % marker_len
  119. pos = state.skipSpaces(pos)
  120. if pos < maximum:
  121. continue
  122. # found!
  123. auto_closed = True
  124. break
  125. old_parent = state.parentType
  126. old_line_max = state.lineMax
  127. state.parentType = "container"
  128. # this will prevent lazy continuations from ever going past our end marker
  129. state.lineMax = nextLine
  130. token = state.push(f"container_{name}_open", "div", 1)
  131. token.markup = markup
  132. token.block = True
  133. token.info = params
  134. token.map = [startLine, nextLine]
  135. state.md.block.tokenize(state, startLine + 1, nextLine)
  136. token = state.push(f"container_{name}_close", "div", -1)
  137. token.markup = state.src[start:pos]
  138. token.block = True
  139. state.parentType = old_parent
  140. state.lineMax = old_line_max
  141. state.line = nextLine + (1 if auto_closed else 0)
  142. return True
  143. md.block.ruler.before(
  144. "fence",
  145. "container_" + name,
  146. container_func,
  147. {"alt": ["paragraph", "reference", "blockquote", "list"]},
  148. )
  149. md.add_render_rule(f"container_{name}_open", render)
  150. md.add_render_rule(f"container_{name}_close", render)