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

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

2from __future__ import annotations 

3 

4from math import floor 

5from typing import TYPE_CHECKING, Any, Callable, Sequence 

6 

7from markdown_it import MarkdownIt 

8from markdown_it.rules_block import StateBlock 

9 

10from mdit_py_plugins.utils import is_code_block 

11 

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 

16 

17 

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>`__. 

27 

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

29 

30 .. code-block:: md 

31 

32 :::: name 

33 ::: name 

34 *markdown* 

35 ::: 

36 :::: 

37 

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 

42 

43 """ 

44 

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

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

47 

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) 

58 

59 return self.renderToken(tokens, idx, _options, env) # type: ignore 

60 

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 

67 

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 

73 

74 auto_closed = False 

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

76 maximum = state.eMarks[startLine] 

77 

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 

82 

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 

93 

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

95 if marker_count < min_markers: 

96 return False 

97 pos -= (pos - start) % marker_len 

98 

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 

104 

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

106 if silent: 

107 return True 

108 

109 # Search for the end of the block 

110 nextLine = startLine 

111 

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 

118 

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

120 maximum = state.eMarks[nextLine] 

121 

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 

127 

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

129 continue 

130 

131 if is_code_block(state, nextLine): 

132 continue 

133 

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 

143 

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 

147 

148 # make sure tail has spaces only 

149 pos -= (pos - start) % marker_len 

150 pos = state.skipSpaces(pos) 

151 

152 if pos < maximum: 

153 continue 

154 

155 # found! 

156 auto_closed = True 

157 break 

158 

159 old_parent = state.parentType 

160 old_line_max = state.lineMax 

161 state.parentType = "container" 

162 

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

164 state.lineMax = nextLine 

165 

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] 

171 

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

173 

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

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

176 token.block = True 

177 

178 state.parentType = old_parent 

179 state.lineMax = old_line_max 

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

181 

182 return True 

183 

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)