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