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

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

103 statements  

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)