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