Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

100 statements  

1"""Process block-level custom containers.""" 

2 

3from __future__ import annotations 

4 

5from math import floor 

6from typing import TYPE_CHECKING, Any, Callable, Sequence 

7 

8from markdown_it import MarkdownIt 

9from markdown_it.rules_block import StateBlock 

10 

11from mdit_py_plugins.utils import is_code_block 

12 

13if TYPE_CHECKING: 

14 from markdown_it.renderer import RendererProtocol 

15 from markdown_it.token import Token 

16 from markdown_it.utils import EnvType, OptionsDict 

17 

18 

19def container_plugin( 

20 md: MarkdownIt, 

21 name: str, 

22 marker: str = ":", 

23 validate: None | Callable[[str, str], bool] = None, 

24 render: None | Callable[..., str] = None, 

25) -> None: 

26 """Plugin ported from 

27 `markdown-it-container <https://github.com/markdown-it/markdown-it-container>`__. 

28 

29 It is a plugin for creating block-level custom containers: 

30 

31 .. code-block:: md 

32 

33 :::: name 

34 ::: name 

35 *markdown* 

36 ::: 

37 :::: 

38 

39 :param name: the name of the container to parse 

40 :param marker: the marker character to use 

41 :param validate: func(marker, param) -> bool, default matches against the name 

42 :param render: render func 

43 

44 """ 

45 

46 def validateDefault(params: str, *args: Any) -> bool: 

47 return params.strip().split(" ", 2)[0] == name 

48 

49 def renderDefault( 

50 self: RendererProtocol, 

51 tokens: Sequence[Token], 

52 idx: int, 

53 _options: OptionsDict, 

54 env: EnvType, 

55 ) -> str: 

56 # add a class to the opening tag 

57 if tokens[idx].nesting == 1: 

58 tokens[idx].attrJoin("class", name) 

59 

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

61 

62 min_markers = 3 

63 marker_str = marker 

64 marker_char = marker_str[0] 

65 marker_len = len(marker_str) 

66 validate = validate or validateDefault 

67 render = render or renderDefault 

68 

69 def container_func( 

70 state: StateBlock, startLine: int, endLine: int, silent: bool 

71 ) -> bool: 

72 if is_code_block(state, startLine): 

73 return False 

74 

75 auto_closed = False 

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

77 maximum = state.eMarks[startLine] 

78 

79 # Check out the first character quickly, 

80 # this should filter out most of non-containers 

81 if marker_char != state.src[start]: 

82 return False 

83 

84 # Check out the rest of the marker string 

85 pos = start + 1 

86 while pos <= maximum: 

87 try: 

88 character = state.src[pos] 

89 except IndexError: 

90 break 

91 if marker_str[(pos - start) % marker_len] != character: 

92 break 

93 pos += 1 

94 

95 marker_count = floor((pos - start) / marker_len) 

96 if marker_count < min_markers: 

97 return False 

98 pos -= (pos - start) % marker_len 

99 

100 markup = state.src[start:pos] 

101 params = state.src[pos:maximum] 

102 assert validate is not None 

103 if not validate(params, markup): 

104 return False 

105 

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

107 if silent: 

108 return True 

109 

110 # Search for the end of the block 

111 nextLine = startLine 

112 

113 while True: 

114 nextLine += 1 

115 if nextLine >= endLine: 

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

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

118 break 

119 

120 start = state.bMarks[nextLine] + state.tShift[nextLine] 

121 maximum = state.eMarks[nextLine] 

122 

123 if start < maximum and state.sCount[nextLine] < state.blkIndent: 

124 # non-empty line with negative indent should stop the list: 

125 # - ``` 

126 # test 

127 break 

128 

129 if marker_char != state.src[start]: 

130 continue 

131 

132 if is_code_block(state, nextLine): 

133 continue 

134 

135 pos = start + 1 

136 while pos <= maximum: 

137 try: 

138 character = state.src[pos] 

139 except IndexError: 

140 break 

141 if marker_str[(pos - start) % marker_len] != character: 

142 break 

143 pos += 1 

144 

145 # closing code fence must be at least as long as the opening one 

146 if floor((pos - start) / marker_len) < marker_count: 

147 continue 

148 

149 # make sure tail has spaces only 

150 pos -= (pos - start) % marker_len 

151 pos = state.skipSpaces(pos) 

152 

153 if pos < maximum: 

154 continue 

155 

156 # found! 

157 auto_closed = True 

158 break 

159 

160 old_parent = state.parentType 

161 old_line_max = state.lineMax 

162 state.parentType = "container" 

163 

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

165 state.lineMax = nextLine 

166 

167 token = state.push(f"container_{name}_open", "div", 1) 

168 token.markup = markup 

169 token.block = True 

170 token.info = params 

171 token.map = [startLine, nextLine] 

172 

173 state.md.block.tokenize(state, startLine + 1, nextLine) 

174 

175 token = state.push(f"container_{name}_close", "div", -1) 

176 token.markup = state.src[start:pos] 

177 token.block = True 

178 

179 state.parentType = old_parent 

180 state.lineMax = old_line_max 

181 state.line = nextLine + (1 if auto_closed else 0) 

182 

183 return True 

184 

185 md.block.ruler.before( 

186 "fence", 

187 "container_" + name, 

188 container_func, 

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

190 ) 

191 md.add_render_rule(f"container_{name}_open", render) 

192 md.add_render_rule(f"container_{name}_close", render)