Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/mdit_py_plugins/field_list/__init__.py: 99%

122 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:15 +0000

1"""Field list plugin""" 

2from contextlib import contextmanager 

3from typing import Iterator, Optional, Tuple 

4 

5from markdown_it import MarkdownIt 

6from markdown_it.rules_block import StateBlock 

7 

8from mdit_py_plugins.utils import is_code_block 

9 

10 

11def fieldlist_plugin(md: MarkdownIt) -> None: 

12 """Field lists are mappings from field names to field bodies, based on the 

13 `reStructureText syntax 

14 <https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#field-lists>`_. 

15 

16 .. code-block:: md 

17 

18 :name *markup*: 

19 :name1: body content 

20 :name2: paragraph 1 

21 

22 paragraph 2 

23 :name3: 

24 paragraph 1 

25 

26 paragraph 2 

27 

28 A field name may consist of any characters except colons (":"). 

29 Inline markup is parsed in field names. 

30 

31 The field name is followed by whitespace and the field body. 

32 The field body may be empty or contain multiple body elements. 

33 

34 Since the field marker may be quite long, 

35 the second and subsequent lines of the field body do not have to 

36 line up with the first line, but they must be indented relative to the 

37 field name marker, and they must line up with each other. 

38 """ 

39 md.block.ruler.before( 

40 "paragraph", 

41 "fieldlist", 

42 _fieldlist_rule, 

43 {"alt": ["paragraph", "reference", "blockquote"]}, 

44 ) 

45 

46 

47def parseNameMarker(state: StateBlock, startLine: int) -> Tuple[int, str]: 

48 """Parse field name: `:name:` 

49 

50 :returns: position after name marker, name text 

51 """ 

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

53 pos = start 

54 maximum = state.eMarks[startLine] 

55 

56 # marker should have at least 3 chars (colon + character + colon) 

57 if pos + 2 >= maximum: 

58 return -1, "" 

59 

60 # first character should be ':' 

61 if state.src[pos] != ":": 

62 return -1, "" 

63 

64 # scan name length 

65 name_length = 1 

66 found_close = False 

67 for ch in state.src[pos + 1 :]: 

68 if ch == "\n": 

69 break 

70 if ch == ":": 

71 # TODO backslash escapes 

72 found_close = True 

73 break 

74 name_length += 1 

75 

76 if not found_close: 

77 return -1, "" 

78 

79 # get name 

80 name_text = state.src[pos + 1 : pos + name_length] 

81 

82 # name should contain at least one character 

83 if not name_text.strip(): 

84 return -1, "" 

85 

86 return pos + name_length + 1, name_text 

87 

88 

89@contextmanager 

90def set_parent_type(state: StateBlock, name: str) -> Iterator[None]: 

91 """Temporarily set parent type to `name`""" 

92 oldParentType = state.parentType 

93 state.parentType = name 

94 yield 

95 state.parentType = oldParentType 

96 

97 

98def _fieldlist_rule( 

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

100) -> bool: 

101 # adapted from markdown_it/rules_block/list.py::list_block 

102 

103 if is_code_block(state, startLine): 

104 return False 

105 

106 posAfterName, name_text = parseNameMarker(state, startLine) 

107 if posAfterName < 0: 

108 return False 

109 

110 # For validation mode we can terminate immediately 

111 if silent: 

112 return True 

113 

114 # start field list 

115 token = state.push("field_list_open", "dl", 1) 

116 token.attrSet("class", "field-list") 

117 token.map = listLines = [startLine, 0] 

118 

119 # iterate list items 

120 nextLine = startLine 

121 

122 with set_parent_type(state, "fieldlist"): 

123 while nextLine < endLine: 

124 # create name tokens 

125 token = state.push("fieldlist_name_open", "dt", 1) 

126 token.map = [startLine, startLine] 

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

128 token.map = [startLine, startLine] 

129 token.content = name_text 

130 token.children = [] 

131 token = state.push("fieldlist_name_close", "dt", -1) 

132 

133 # set indent positions 

134 pos = posAfterName 

135 maximum: int = state.eMarks[nextLine] 

136 first_line_body_indent = ( 

137 state.sCount[nextLine] 

138 + posAfterName 

139 - (state.bMarks[startLine] + state.tShift[startLine]) 

140 ) 

141 

142 # find indent to start of body on first line 

143 while pos < maximum: 

144 ch = state.src[pos] 

145 

146 if ch == "\t": 

147 first_line_body_indent += ( 

148 4 - (first_line_body_indent + state.bsCount[nextLine]) % 4 

149 ) 

150 elif ch == " ": 

151 first_line_body_indent += 1 

152 else: 

153 break 

154 

155 pos += 1 

156 

157 contentStart = pos 

158 

159 # to figure out the indent of the body, 

160 # we look at all non-empty, indented lines and find the minimum indent 

161 block_indent: Optional[int] = None 

162 _line = startLine + 1 

163 while _line < endLine: 

164 # if start_of_content < end_of_content, then non-empty line 

165 if (state.bMarks[_line] + state.tShift[_line]) < state.eMarks[_line]: 

166 if state.tShift[_line] <= 0: 

167 # the line has no indent, so it's the end of the field 

168 break 

169 block_indent = ( 

170 state.tShift[_line] 

171 if block_indent is None 

172 else min(block_indent, state.tShift[_line]) 

173 ) 

174 

175 _line += 1 

176 

177 has_first_line = contentStart < maximum 

178 if block_indent is None: # no body content 

179 if not has_first_line: # noqa SIM108 

180 # no body or first line, so just use default 

181 block_indent = 2 

182 else: 

183 # only a first line, so use it's indent 

184 block_indent = first_line_body_indent 

185 else: 

186 block_indent = min(block_indent, first_line_body_indent) 

187 

188 # Run subparser on the field body 

189 token = state.push("fieldlist_body_open", "dd", 1) 

190 token.map = [startLine, startLine] 

191 

192 with temp_state_changes(state, startLine): 

193 diff = 0 

194 if has_first_line and block_indent < first_line_body_indent: 

195 # this is a hack to get the first line to render correctly 

196 # we temporarily "shift" it to the left by the difference 

197 # between the first line indent and the block indent 

198 # and replace the "hole" left with space, 

199 # so that src indexes still match 

200 diff = first_line_body_indent - block_indent 

201 state.src = ( 

202 state.src[: contentStart - diff] 

203 + " " * diff 

204 + state.src[contentStart:] 

205 ) 

206 

207 state.tShift[startLine] = contentStart - diff - state.bMarks[startLine] 

208 state.sCount[startLine] = first_line_body_indent - diff 

209 state.blkIndent = block_indent 

210 

211 state.md.block.tokenize(state, startLine, endLine) 

212 

213 state.push("fieldlist_body_close", "dd", -1) 

214 

215 nextLine = startLine = state.line 

216 token.map[1] = nextLine 

217 

218 if nextLine >= endLine: 

219 break 

220 

221 contentStart = state.bMarks[startLine] 

222 

223 # Try to check if list is terminated or continued. 

224 if state.sCount[nextLine] < state.blkIndent: 

225 break 

226 

227 if is_code_block(state, startLine): 

228 break 

229 

230 # get next field item 

231 posAfterName, name_text = parseNameMarker(state, startLine) 

232 if posAfterName < 0: 

233 break 

234 

235 # Finalize list 

236 token = state.push("field_list_close", "dl", -1) 

237 listLines[1] = nextLine 

238 state.line = nextLine 

239 

240 return True 

241 

242 

243@contextmanager 

244def temp_state_changes(state: StateBlock, startLine: int) -> Iterator[None]: 

245 """Allow temporarily changing certain state attributes.""" 

246 oldTShift = state.tShift[startLine] 

247 oldSCount = state.sCount[startLine] 

248 oldBlkIndent = state.blkIndent 

249 oldSrc = state.src 

250 yield 

251 state.blkIndent = oldBlkIndent 

252 state.tShift[startLine] = oldTShift 

253 state.sCount[startLine] = oldSCount 

254 state.src = oldSrc