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