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

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

122 statements  

1"""Field list plugin""" 

2 

3from contextlib import contextmanager 

4from typing import Iterator, Optional, Tuple 

5 

6from markdown_it import MarkdownIt 

7from markdown_it.rules_block import StateBlock 

8 

9from mdit_py_plugins.utils import is_code_block 

10 

11 

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

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

14 `reStructureText syntax 

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

16 

17 .. code-block:: md 

18 

19 :name *markup*: 

20 :name1: body content 

21 :name2: paragraph 1 

22 

23 paragraph 2 

24 :name3: 

25 paragraph 1 

26 

27 paragraph 2 

28 

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

30 Inline markup is parsed in field names. 

31 

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

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

34 

35 Since the field marker may be quite long, 

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

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

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

39 """ 

40 md.block.ruler.before( 

41 "paragraph", 

42 "fieldlist", 

43 _fieldlist_rule, 

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

45 ) 

46 

47 

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

49 """Parse field name: `:name:` 

50 

51 :returns: position after name marker, name text 

52 """ 

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

54 pos = start 

55 maximum = state.eMarks[startLine] 

56 

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

58 if pos + 2 >= maximum: 

59 return -1, "" 

60 

61 # first character should be ':' 

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

63 return -1, "" 

64 

65 # scan name length 

66 name_length = 1 

67 found_close = False 

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

69 if ch == "\n": 

70 break 

71 if ch == ":": 

72 # TODO backslash escapes 

73 found_close = True 

74 break 

75 name_length += 1 

76 

77 if not found_close: 

78 return -1, "" 

79 

80 # get name 

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

82 

83 # name should contain at least one character 

84 if not name_text.strip(): 

85 return -1, "" 

86 

87 return pos + name_length + 1, name_text 

88 

89 

90@contextmanager 

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

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

93 oldParentType = state.parentType 

94 state.parentType = name 

95 yield 

96 state.parentType = oldParentType 

97 

98 

99def _fieldlist_rule( 

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

101) -> bool: 

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

103 

104 if is_code_block(state, startLine): 

105 return False 

106 

107 posAfterName, name_text = parseNameMarker(state, startLine) 

108 if posAfterName < 0: 

109 return False 

110 

111 # For validation mode we can terminate immediately 

112 if silent: 

113 return True 

114 

115 # start field list 

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

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

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

119 

120 # iterate list items 

121 nextLine = startLine 

122 

123 with set_parent_type(state, "fieldlist"): 

124 while nextLine < endLine: 

125 # create name tokens 

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

127 token.map = [startLine, startLine] 

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

129 token.map = [startLine, startLine] 

130 token.content = name_text 

131 token.children = [] 

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

133 

134 # set indent positions 

135 pos = posAfterName 

136 maximum: int = state.eMarks[nextLine] 

137 first_line_body_indent = ( 

138 state.sCount[nextLine] 

139 + posAfterName 

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

141 ) 

142 

143 # find indent to start of body on first line 

144 while pos < maximum: 

145 ch = state.src[pos] 

146 

147 if ch == "\t": 

148 first_line_body_indent += ( 

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

150 ) 

151 elif ch == " ": 

152 first_line_body_indent += 1 

153 else: 

154 break 

155 

156 pos += 1 

157 

158 contentStart = pos 

159 

160 # to figure out the indent of the body, 

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

162 block_indent: Optional[int] = None 

163 _line = startLine + 1 

164 while _line < endLine: 

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

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

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

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

169 break 

170 block_indent = ( 

171 state.tShift[_line] 

172 if block_indent is None 

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

174 ) 

175 

176 _line += 1 

177 

178 has_first_line = contentStart < maximum 

179 if block_indent is None: # no body content 

180 if not has_first_line: # noqa: SIM108 

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

182 block_indent = 2 

183 else: 

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

185 block_indent = first_line_body_indent 

186 else: 

187 block_indent = min(block_indent, first_line_body_indent) 

188 

189 # Run subparser on the field body 

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

191 token.map = [startLine, startLine] 

192 

193 with temp_state_changes(state, startLine): 

194 diff = 0 

195 if has_first_line and block_indent < first_line_body_indent: 

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

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

198 # between the first line indent and the block indent 

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

200 # so that src indexes still match 

201 diff = first_line_body_indent - block_indent 

202 state.src = ( 

203 state.src[: contentStart - diff] 

204 + " " * diff 

205 + state.src[contentStart:] 

206 ) 

207 

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

209 state.sCount[startLine] = first_line_body_indent - diff 

210 state.blkIndent = block_indent 

211 

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

213 

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

215 

216 nextLine = startLine = state.line 

217 token.map[1] = nextLine 

218 

219 if nextLine >= endLine: 

220 break 

221 

222 contentStart = state.bMarks[startLine] 

223 

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

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

226 break 

227 

228 if is_code_block(state, startLine): 

229 break 

230 

231 # get next field item 

232 posAfterName, name_text = parseNameMarker(state, startLine) 

233 if posAfterName < 0: 

234 break 

235 

236 # Finalize list 

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

238 listLines[1] = nextLine 

239 state.line = nextLine 

240 

241 return True 

242 

243 

244@contextmanager 

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

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

247 oldTShift = state.tShift[startLine] 

248 oldSCount = state.sCount[startLine] 

249 oldBlkIndent = state.blkIndent 

250 oldSrc = state.src 

251 yield 

252 state.blkIndent = oldBlkIndent 

253 state.tShift[startLine] = oldTShift 

254 state.sCount[startLine] = oldSCount 

255 state.src = oldSrc