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
« 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
5from markdown_it import MarkdownIt
6from markdown_it.rules_block import StateBlock
8from mdit_py_plugins.utils import is_code_block
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>`_.
16 .. code-block:: md
18 :name *markup*:
19 :name1: body content
20 :name2: paragraph 1
22 paragraph 2
23 :name3:
24 paragraph 1
26 paragraph 2
28 A field name may consist of any characters except colons (":").
29 Inline markup is parsed in field names.
31 The field name is followed by whitespace and the field body.
32 The field body may be empty or contain multiple body elements.
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 )
47def parseNameMarker(state: StateBlock, startLine: int) -> Tuple[int, str]:
48 """Parse field name: `:name:`
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]
56 # marker should have at least 3 chars (colon + character + colon)
57 if pos + 2 >= maximum:
58 return -1, ""
60 # first character should be ':'
61 if state.src[pos] != ":":
62 return -1, ""
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
76 if not found_close:
77 return -1, ""
79 # get name
80 name_text = state.src[pos + 1 : pos + name_length]
82 # name should contain at least one character
83 if not name_text.strip():
84 return -1, ""
86 return pos + name_length + 1, name_text
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
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
103 if is_code_block(state, startLine):
104 return False
106 posAfterName, name_text = parseNameMarker(state, startLine)
107 if posAfterName < 0:
108 return False
110 # For validation mode we can terminate immediately
111 if silent:
112 return True
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]
119 # iterate list items
120 nextLine = startLine
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)
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 )
142 # find indent to start of body on first line
143 while pos < maximum:
144 ch = state.src[pos]
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
155 pos += 1
157 contentStart = pos
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 )
175 _line += 1
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)
188 # Run subparser on the field body
189 token = state.push("fieldlist_body_open", "dd", 1)
190 token.map = [startLine, startLine]
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 )
207 state.tShift[startLine] = contentStart - diff - state.bMarks[startLine]
208 state.sCount[startLine] = first_line_body_indent - diff
209 state.blkIndent = block_indent
211 state.md.block.tokenize(state, startLine, endLine)
213 state.push("fieldlist_body_close", "dd", -1)
215 nextLine = startLine = state.line
216 token.map[1] = nextLine
218 if nextLine >= endLine:
219 break
221 contentStart = state.bMarks[startLine]
223 # Try to check if list is terminated or continued.
224 if state.sCount[nextLine] < state.blkIndent:
225 break
227 if is_code_block(state, startLine):
228 break
230 # get next field item
231 posAfterName, name_text = parseNameMarker(state, startLine)
232 if posAfterName < 0:
233 break
235 # Finalize list
236 token = state.push("field_list_close", "dl", -1)
237 listLines[1] = nextLine
238 state.line = nextLine
240 return True
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