Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/mdit_py_plugins/admon/index.py: 96%
116 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 admonitions and pass to cb.
2from __future__ import annotations
4from typing import TYPE_CHECKING, Callable, Sequence
6from markdown_it import MarkdownIt
7from markdown_it.rules_block import StateBlock
9from mdit_py_plugins.utils import is_code_block
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
17def _get_tag(params: str) -> tuple[str, str]:
18 """Separate the tag name from the admonition title."""
19 if not params.strip():
20 return "", ""
22 tag, *_title = params.strip().split(" ")
23 joined = " ".join(_title)
25 title = ""
26 if not joined:
27 title = tag.title()
28 elif joined != '""':
29 title = joined
30 return tag.lower(), title
33def _validate(params: str) -> bool:
34 """Validate the presence of the tag name after the marker."""
35 tag = params.strip().split(" ", 1)[-1] or ""
36 return bool(tag)
39MARKER_LEN = 3 # Regardless of extra characters, block indent stays the same
40MARKERS = ("!!!", "???", "???+")
41MARKER_CHARS = {_m[0] for _m in MARKERS}
42MAX_MARKER_LEN = max(len(_m) for _m in MARKERS)
45def _extra_classes(markup: str) -> list[str]:
46 """Return the list of additional classes based on the markup."""
47 if markup.startswith("?"):
48 if markup.endswith("+"):
49 return ["is-collapsible collapsible-open"]
50 return ["is-collapsible collapsible-closed"]
51 return []
54def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
55 if is_code_block(state, startLine):
56 return False
58 start = state.bMarks[startLine] + state.tShift[startLine]
59 maximum = state.eMarks[startLine]
61 # Check out the first character quickly, which should filter out most of non-containers
62 if state.src[start] not in MARKER_CHARS:
63 return False
65 # Check out the rest of the marker string
66 marker = ""
67 marker_len = MAX_MARKER_LEN
68 while marker_len > 0:
69 marker_pos = start + marker_len
70 markup = state.src[start:marker_pos]
71 if markup in MARKERS:
72 marker = markup
73 break
74 marker_len -= 1
75 else:
76 return False
78 params = state.src[marker_pos:maximum]
80 if not _validate(params):
81 return False
83 # Since start is found, we can report success here in validation mode
84 if silent:
85 return True
87 old_parent = state.parentType
88 old_line_max = state.lineMax
89 old_indent = state.blkIndent
91 blk_start = marker_pos
92 while blk_start < maximum and state.src[blk_start] == " ":
93 blk_start += 1
95 state.parentType = "admonition"
96 # Correct block indentation when extra marker characters are present
97 marker_alignment_correction = MARKER_LEN - len(marker)
98 state.blkIndent += blk_start - start + marker_alignment_correction
100 was_empty = False
102 # Search for the end of the block
103 next_line = startLine
104 while True:
105 next_line += 1
106 if next_line >= endLine:
107 # unclosed block should be autoclosed by end of document.
108 # also block seems to be autoclosed by end of parent
109 break
110 pos = state.bMarks[next_line] + state.tShift[next_line]
111 maximum = state.eMarks[next_line]
112 is_empty = state.sCount[next_line] < state.blkIndent
114 # two consecutive empty lines autoclose the block
115 if is_empty and was_empty:
116 break
117 was_empty = is_empty
119 if pos < maximum and state.sCount[next_line] < state.blkIndent:
120 # non-empty line with negative indent should stop the block:
121 # - !!!
122 # test
123 break
125 # this will prevent lazy continuations from ever going past our end marker
126 state.lineMax = next_line
128 tag, title = _get_tag(params)
130 token = state.push("admonition_open", "div", 1)
131 token.markup = markup
132 token.block = True
133 token.attrs = {"class": " ".join(["admonition", tag, *_extra_classes(markup)])}
134 token.meta = {"tag": tag}
135 token.content = title
136 token.info = params
137 token.map = [startLine, next_line]
139 if title:
140 title_markup = f"{markup} {tag}"
141 token = state.push("admonition_title_open", "p", 1)
142 token.markup = title_markup
143 token.attrs = {"class": "admonition-title"}
144 token.map = [startLine, startLine + 1]
146 token = state.push("inline", "", 0)
147 token.content = title
148 token.map = [startLine, startLine + 1]
149 token.children = []
151 token = state.push("admonition_title_close", "p", -1)
153 state.md.block.tokenize(state, startLine + 1, next_line)
155 token = state.push("admonition_close", "div", -1)
156 token.markup = markup
157 token.block = True
159 state.parentType = old_parent
160 state.lineMax = old_line_max
161 state.blkIndent = old_indent
162 state.line = next_line
164 return True
167def admon_plugin(md: MarkdownIt, render: None | Callable[..., str] = None) -> None:
168 """Plugin to use
169 `python-markdown style admonitions
170 <https://python-markdown.github.io/extensions/admonition>`_.
172 .. code-block:: md
174 !!! note
175 *content*
177 `And mkdocs-style collapsible blocks
178 <https://squidfunk.github.io/mkdocs-material/reference/admonitions/#collapsible-blocks>`_.
180 .. code-block:: md
182 ???+ note
183 *content*
185 Note, this is ported from
186 `markdown-it-admon
187 <https://github.com/commenthol/markdown-it-admon>`_.
188 """
190 def renderDefault(
191 self: RendererProtocol,
192 tokens: Sequence[Token],
193 idx: int,
194 _options: OptionsDict,
195 env: EnvType,
196 ) -> str:
197 return self.renderToken(tokens, idx, _options, env) # type: ignore
199 render = render or renderDefault
201 md.add_render_rule("admonition_open", render)
202 md.add_render_rule("admonition_close", render)
203 md.add_render_rule("admonition_title_open", render)
204 md.add_render_rule("admonition_title_close", render)
206 md.block.ruler.before(
207 "fence",
208 "admonition",
209 admonition,
210 {"alt": ["paragraph", "reference", "blockquote", "list"]},
211 )