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