colon_fence.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. from __future__ import annotations
  2. from typing import TYPE_CHECKING, Sequence
  3. from markdown_it import MarkdownIt
  4. from markdown_it.common.utils import escapeHtml, unescapeAll
  5. from markdown_it.rules_block import StateBlock
  6. from mdit_py_plugins.utils import is_code_block
  7. if TYPE_CHECKING:
  8. from markdown_it.renderer import RendererProtocol
  9. from markdown_it.token import Token
  10. from markdown_it.utils import EnvType, OptionsDict
  11. def colon_fence_plugin(md: MarkdownIt) -> None:
  12. """This plugin directly mimics regular fences, but with `:` colons.
  13. Example::
  14. :::name
  15. contained text
  16. :::
  17. """
  18. md.block.ruler.before(
  19. "fence",
  20. "colon_fence",
  21. _rule,
  22. {"alt": ["paragraph", "reference", "blockquote", "list", "footnote_def"]},
  23. )
  24. md.add_render_rule("colon_fence", _render)
  25. def _rule(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
  26. if is_code_block(state, startLine):
  27. return False
  28. haveEndMarker = False
  29. pos = state.bMarks[startLine] + state.tShift[startLine]
  30. maximum = state.eMarks[startLine]
  31. if pos + 3 > maximum:
  32. return False
  33. marker = state.src[pos]
  34. if marker != ":":
  35. return False
  36. # scan marker length
  37. mem = pos
  38. pos = _skipCharsStr(state, pos, marker)
  39. length = pos - mem
  40. if length < 3:
  41. return False
  42. markup = state.src[mem:pos]
  43. params = state.src[pos:maximum]
  44. # Since start is found, we can report success here in validation mode
  45. if silent:
  46. return True
  47. # search end of block
  48. nextLine = startLine
  49. while True:
  50. nextLine += 1
  51. if nextLine >= endLine:
  52. # unclosed block should be autoclosed by end of document.
  53. # also block seems to be autoclosed by end of parent
  54. break
  55. pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
  56. maximum = state.eMarks[nextLine]
  57. if pos < maximum and state.sCount[nextLine] < state.blkIndent:
  58. # non-empty line with negative indent should stop the list:
  59. # - ```
  60. # test
  61. break
  62. if state.src[pos] != marker:
  63. continue
  64. if is_code_block(state, nextLine):
  65. continue
  66. pos = _skipCharsStr(state, pos, marker)
  67. # closing code fence must be at least as long as the opening one
  68. if pos - mem < length:
  69. continue
  70. # make sure tail has spaces only
  71. pos = state.skipSpaces(pos)
  72. if pos < maximum:
  73. continue
  74. haveEndMarker = True
  75. # found!
  76. break
  77. # If a fence has heading spaces, they should be removed from its inner block
  78. length = state.sCount[startLine]
  79. state.line = nextLine + (1 if haveEndMarker else 0)
  80. token = state.push("colon_fence", "code", 0)
  81. token.info = params
  82. token.content = state.getLines(startLine + 1, nextLine, length, True)
  83. token.markup = markup
  84. token.map = [startLine, state.line]
  85. return True
  86. def _skipCharsStr(state: StateBlock, pos: int, ch: str) -> int:
  87. """Skip character string from given position."""
  88. # TODO this can be replaced with StateBlock.skipCharsStr in markdown-it-py 3.0.0
  89. while True:
  90. try:
  91. current = state.src[pos]
  92. except IndexError:
  93. break
  94. if current != ch:
  95. break
  96. pos += 1
  97. return pos
  98. def _render(
  99. self: RendererProtocol,
  100. tokens: Sequence[Token],
  101. idx: int,
  102. options: OptionsDict,
  103. env: EnvType,
  104. ) -> str:
  105. token = tokens[idx]
  106. info = unescapeAll(token.info).strip() if token.info else ""
  107. content = escapeHtml(token.content)
  108. block_name = ""
  109. if info:
  110. block_name = info.split()[0]
  111. return (
  112. "<pre><code"
  113. + (f' class="block-{block_name}" ' if block_name else "")
  114. + ">"
  115. + content
  116. + "</code></pre>\n"
  117. )