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