Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/mdit_py_plugins/admon/index.py: 95%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

132 statements  

1# Process admonitions and pass to cb. 

2 

3from __future__ import annotations 

4 

5from collections.abc import Callable, Sequence 

6from contextlib import suppress 

7import re 

8from typing import TYPE_CHECKING 

9 

10from markdown_it import MarkdownIt 

11from markdown_it.rules_block import StateBlock 

12 

13from mdit_py_plugins.utils import is_code_block 

14 

15if TYPE_CHECKING: 

16 from markdown_it.renderer import RendererProtocol 

17 from markdown_it.token import Token 

18 from markdown_it.utils import EnvType, OptionsDict 

19 

20 

21def _get_multiple_tags(params: str) -> tuple[list[str], str]: 

22 """Check for multiple tags when the title is double quoted.""" 

23 re_tags = re.compile(r'^\s*(?P<tokens>[^"]+)\s+"(?P<title>.*)"\S*$') 

24 match = re_tags.match(params) 

25 if match: 

26 tags = match["tokens"].strip().split(" ") 

27 return [tag.lower() for tag in tags], match["title"] 

28 raise ValueError("No match found for parameters") 

29 

30 

31def _get_tag(_params: str) -> tuple[list[str], str]: 

32 """Separate the tag name from the admonition title.""" 

33 params = _params.strip() 

34 if not params: 

35 return [""], "" 

36 

37 with suppress(ValueError): 

38 return _get_multiple_tags(params) 

39 

40 tag, *_title = params.split(" ") 

41 joined = " ".join(_title) 

42 

43 title = "" 

44 if not joined: 

45 title = tag.title() 

46 elif joined != '""': # Specifically check for no title 

47 title = joined 

48 return [tag.lower()], title 

49 

50 

51def _validate(params: str) -> bool: 

52 """Validate the presence of the tag name after the marker.""" 

53 tag = params.strip().split(" ", 1)[-1] or "" 

54 return bool(tag) 

55 

56 

57MARKER_LEN = 3 # Regardless of extra characters, block indent stays the same 

58MARKERS = ("!!!", "???", "???+") 

59MARKER_CHARS = {_m[0] for _m in MARKERS} 

60MAX_MARKER_LEN = max(len(_m) for _m in MARKERS) 

61 

62 

63def _extra_classes(markup: str) -> list[str]: 

64 """Return the list of additional classes based on the markup.""" 

65 if markup.startswith("?"): 

66 if markup.endswith("+"): 

67 return ["is-collapsible collapsible-open"] 

68 return ["is-collapsible collapsible-closed"] 

69 return [] 

70 

71 

72def admonition(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool: 

73 if is_code_block(state, startLine): 

74 return False 

75 

76 start = state.bMarks[startLine] + state.tShift[startLine] 

77 maximum = state.eMarks[startLine] 

78 

79 # Check out the first character quickly, which should filter out most of non-containers 

80 if state.src[start] not in MARKER_CHARS: 

81 return False 

82 

83 # Check out the rest of the marker string 

84 marker = "" 

85 marker_len = MAX_MARKER_LEN 

86 while marker_len > 0: 

87 marker_pos = start + marker_len 

88 markup = state.src[start:marker_pos] 

89 if markup in MARKERS: 

90 marker = markup 

91 break 

92 marker_len -= 1 

93 else: 

94 return False 

95 

96 params = state.src[marker_pos:maximum] 

97 

98 if not _validate(params): 

99 return False 

100 

101 # Since start is found, we can report success here in validation mode 

102 if silent: 

103 return True 

104 

105 old_parent = state.parentType 

106 old_line_max = state.lineMax 

107 old_indent = state.blkIndent 

108 

109 blk_start = marker_pos 

110 while blk_start < maximum and state.src[blk_start] == " ": 

111 blk_start += 1 

112 

113 state.parentType = "admonition" 

114 # Correct block indentation when extra marker characters are present 

115 marker_alignment_correction = MARKER_LEN - len(marker) 

116 state.blkIndent += blk_start - start + marker_alignment_correction 

117 

118 was_empty = False 

119 

120 # Search for the end of the block 

121 next_line = startLine 

122 while True: 

123 next_line += 1 

124 if next_line >= endLine: 

125 # unclosed block should be autoclosed by end of document. 

126 # also block seems to be autoclosed by end of parent 

127 break 

128 pos = state.bMarks[next_line] + state.tShift[next_line] 

129 maximum = state.eMarks[next_line] 

130 is_empty = state.sCount[next_line] < state.blkIndent 

131 

132 # two consecutive empty lines autoclose the block 

133 if is_empty and was_empty: 

134 break 

135 was_empty = is_empty 

136 

137 if pos < maximum and state.sCount[next_line] < state.blkIndent: 

138 # non-empty line with negative indent should stop the block: 

139 # - !!! 

140 # test 

141 break 

142 

143 # this will prevent lazy continuations from ever going past our end marker 

144 state.lineMax = next_line 

145 

146 tags, title = _get_tag(params) 

147 tag = tags[0] 

148 

149 token = state.push("admonition_open", "div", 1) 

150 token.markup = markup 

151 token.block = True 

152 token.attrs = {"class": " ".join(["admonition", *tags, *_extra_classes(markup)])} 

153 token.meta = {"tag": tag} 

154 token.content = title 

155 token.info = params 

156 token.map = [startLine, next_line] 

157 

158 if title: 

159 title_markup = f"{markup} {tag}" 

160 token = state.push("admonition_title_open", "p", 1) 

161 token.markup = title_markup 

162 token.attrs = {"class": "admonition-title"} 

163 token.map = [startLine, startLine + 1] 

164 

165 token = state.push("inline", "", 0) 

166 token.content = title 

167 token.map = [startLine, startLine + 1] 

168 token.children = [] 

169 

170 token = state.push("admonition_title_close", "p", -1) 

171 

172 state.md.block.tokenize(state, startLine + 1, next_line) 

173 

174 token = state.push("admonition_close", "div", -1) 

175 token.markup = markup 

176 token.block = True 

177 

178 state.parentType = old_parent 

179 state.lineMax = old_line_max 

180 state.blkIndent = old_indent 

181 state.line = next_line 

182 

183 return True 

184 

185 

186def admon_plugin(md: MarkdownIt, render: None | Callable[..., str] = None) -> None: 

187 """Plugin to use 

188 `python-markdown style admonitions 

189 <https://python-markdown.github.io/extensions/admonition>`_. 

190 

191 .. code-block:: md 

192 

193 !!! note 

194 *content* 

195 

196 `And mkdocs-style collapsible blocks 

197 <https://squidfunk.github.io/mkdocs-material/reference/admonitions/#collapsible-blocks>`_. 

198 

199 .. code-block:: md 

200 

201 ???+ note 

202 *content* 

203 

204 Note, this is ported from 

205 `markdown-it-admon 

206 <https://github.com/commenthol/markdown-it-admon>`_. 

207 """ 

208 

209 def renderDefault( 

210 self: RendererProtocol, 

211 tokens: Sequence[Token], 

212 idx: int, 

213 _options: OptionsDict, 

214 env: EnvType, 

215 ) -> str: 

216 return self.renderToken(tokens, idx, _options, env) # type: ignore[attr-defined,no-any-return] 

217 

218 render = render or renderDefault 

219 

220 md.add_render_rule("admonition_open", render) 

221 md.add_render_rule("admonition_close", render) 

222 md.add_render_rule("admonition_title_open", render) 

223 md.add_render_rule("admonition_title_close", render) 

224 

225 md.block.ruler.before( 

226 "fence", 

227 "admonition", 

228 admonition, 

229 {"alt": ["paragraph", "reference", "blockquote", "list"]}, 

230 )