Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/markdown_it/rules_block/list.py: 99%
175 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:07 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:07 +0000
1# Lists
2import logging
4from ..common.utils import isSpace
5from .state_block import StateBlock
7LOGGER = logging.getLogger(__name__)
10# Search `[-+*][\n ]`, returns next pos after marker on success
11# or -1 on fail.
12def skipBulletListMarker(state: StateBlock, startLine: int):
13 pos = state.bMarks[startLine] + state.tShift[startLine]
14 maximum = state.eMarks[startLine]
16 marker = state.srcCharCode[pos]
17 pos += 1
18 # Check bullet /* * */ /* - */ /* + */
19 if marker != 0x2A and marker != 0x2D and marker != 0x2B:
20 return -1
22 if pos < maximum:
23 ch = state.srcCharCode[pos]
25 if not isSpace(ch):
26 # " -test " - is not a list item
27 return -1
29 return pos
32# Search `\d+[.)][\n ]`, returns next pos after marker on success
33# or -1 on fail.
34def skipOrderedListMarker(state: StateBlock, startLine: int):
35 start = state.bMarks[startLine] + state.tShift[startLine]
36 pos = start
37 maximum = state.eMarks[startLine]
39 # List marker should have at least 2 chars (digit + dot)
40 if pos + 1 >= maximum:
41 return -1
43 ch = state.srcCharCode[pos]
44 pos += 1
46 # /* 0 */ /* 9 */
47 if ch < 0x30 or ch > 0x39:
48 return -1
50 while True:
51 # EOL -> fail
52 if pos >= maximum:
53 return -1
55 ch = state.srcCharCode[pos]
56 pos += 1
58 # /* 0 */ /* 9 */
59 if ch >= 0x30 and ch <= 0x39:
60 # List marker should have no more than 9 digits
61 # (prevents integer overflow in browsers)
62 if pos - start >= 10:
63 return -1
65 continue
67 # found valid marker: /* ) */ /* . */
68 if ch == 0x29 or ch == 0x2E:
69 break
71 return -1
73 if pos < maximum:
74 ch = state.srcCharCode[pos]
76 if not isSpace(ch):
77 # " 1.test " - is not a list item
78 return -1
80 return pos
83def markTightParagraphs(state: StateBlock, idx: int):
84 level = state.level + 2
86 i = idx + 2
87 length = len(state.tokens) - 2
88 while i < length:
89 if state.tokens[i].level == level and state.tokens[i].type == "paragraph_open":
90 state.tokens[i + 2].hidden = True
91 state.tokens[i].hidden = True
92 i += 2
93 i += 1
96def list_block(state: StateBlock, startLine: int, endLine: int, silent: bool):
97 LOGGER.debug("entering list: %s, %s, %s, %s", state, startLine, endLine, silent)
99 isTerminatingParagraph = False
100 tight = True
102 # if it's indented more than 3 spaces, it should be a code block
103 if state.sCount[startLine] - state.blkIndent >= 4:
104 return False
106 # Special case:
107 # - item 1
108 # - item 2
109 # - item 3
110 # - item 4
111 # - this one is a paragraph continuation
112 if (
113 state.listIndent >= 0
114 and state.sCount[startLine] - state.listIndent >= 4
115 and state.sCount[startLine] < state.blkIndent
116 ):
117 return False
119 # limit conditions when list can interrupt
120 # a paragraph (validation mode only)
121 if silent and state.parentType == "paragraph":
122 # Next list item should still terminate previous list item
123 #
124 # This code can fail if plugins use blkIndent as well as lists,
125 # but I hope the spec gets fixed long before that happens.
126 #
127 if state.tShift[startLine] >= state.blkIndent:
128 isTerminatingParagraph = True
130 # Detect list type and position after marker
131 posAfterMarker = skipOrderedListMarker(state, startLine)
132 if posAfterMarker >= 0:
133 isOrdered = True
134 start = state.bMarks[startLine] + state.tShift[startLine]
135 markerValue = int(state.src[start : posAfterMarker - 1])
137 # If we're starting a new ordered list right after
138 # a paragraph, it should start with 1.
139 if isTerminatingParagraph and markerValue != 1:
140 return False
141 else:
142 posAfterMarker = skipBulletListMarker(state, startLine)
143 if posAfterMarker >= 0:
144 isOrdered = False
145 else:
146 return False
148 # If we're starting a new unordered list right after
149 # a paragraph, first line should not be empty.
150 if isTerminatingParagraph:
151 if state.skipSpaces(posAfterMarker) >= state.eMarks[startLine]:
152 return False
154 # We should terminate list on style change. Remember first one to compare.
155 markerCharCode = state.srcCharCode[posAfterMarker - 1]
157 # For validation mode we can terminate immediately
158 if silent:
159 return True
161 # Start list
162 listTokIdx = len(state.tokens)
164 if isOrdered:
165 token = state.push("ordered_list_open", "ol", 1)
166 if markerValue != 1:
167 token.attrs = {"start": markerValue}
169 else:
170 token = state.push("bullet_list_open", "ul", 1)
172 token.map = listLines = [startLine, 0]
173 token.markup = chr(markerCharCode)
175 #
176 # Iterate list items
177 #
179 nextLine = startLine
180 prevEmptyEnd = False
181 terminatorRules = state.md.block.ruler.getRules("list")
183 oldParentType = state.parentType
184 state.parentType = "list"
186 while nextLine < endLine:
187 pos = posAfterMarker
188 maximum = state.eMarks[nextLine]
190 initial = offset = (
191 state.sCount[nextLine]
192 + posAfterMarker
193 - (state.bMarks[startLine] + state.tShift[startLine])
194 )
196 while pos < maximum:
197 ch = state.srcCharCode[pos]
199 if ch == 0x09: # \t
200 offset += 4 - (offset + state.bsCount[nextLine]) % 4
201 elif ch == 0x20: # \s
202 offset += 1
203 else:
204 break
206 pos += 1
208 contentStart = pos
210 if contentStart >= maximum:
211 # trimming space in "- \n 3" case, indent is 1 here
212 indentAfterMarker = 1
213 else:
214 indentAfterMarker = offset - initial
216 # If we have more than 4 spaces, the indent is 1
217 # (the rest is just indented code block)
218 if indentAfterMarker > 4:
219 indentAfterMarker = 1
221 # " - test"
222 # ^^^^^ - calculating total length of this thing
223 indent = initial + indentAfterMarker
225 # Run subparser & write tokens
226 token = state.push("list_item_open", "li", 1)
227 token.markup = chr(markerCharCode)
228 token.map = itemLines = [startLine, 0]
229 if isOrdered:
230 token.info = state.src[start : posAfterMarker - 1]
232 # change current state, then restore it after parser subcall
233 oldTight = state.tight
234 oldTShift = state.tShift[startLine]
235 oldSCount = state.sCount[startLine]
237 # - example list
238 # ^ listIndent position will be here
239 # ^ blkIndent position will be here
240 #
241 oldListIndent = state.listIndent
242 state.listIndent = state.blkIndent
243 state.blkIndent = indent
245 state.tight = True
246 state.tShift[startLine] = contentStart - state.bMarks[startLine]
247 state.sCount[startLine] = offset
249 if contentStart >= maximum and state.isEmpty(startLine + 1):
250 # workaround for this case
251 # (list item is empty, list terminates before "foo"):
252 # ~~~~~~~~
253 # -
254 #
255 # foo
256 # ~~~~~~~~
257 state.line = min(state.line + 2, endLine)
258 else:
259 # NOTE in list.js this was:
260 # state.md.block.tokenize(state, startLine, endLine, True)
261 # but tokeniz does not take the final parameter
262 state.md.block.tokenize(state, startLine, endLine)
264 # If any of list item is tight, mark list as tight
265 if (not state.tight) or prevEmptyEnd:
266 tight = False
268 # Item become loose if finish with empty line,
269 # but we should filter last element, because it means list finish
270 prevEmptyEnd = (state.line - startLine) > 1 and state.isEmpty(state.line - 1)
272 state.blkIndent = state.listIndent
273 state.listIndent = oldListIndent
274 state.tShift[startLine] = oldTShift
275 state.sCount[startLine] = oldSCount
276 state.tight = oldTight
278 token = state.push("list_item_close", "li", -1)
279 token.markup = chr(markerCharCode)
281 nextLine = startLine = state.line
282 itemLines[1] = nextLine
284 if nextLine >= endLine:
285 break
287 contentStart = state.bMarks[startLine]
289 #
290 # Try to check if list is terminated or continued.
291 #
292 if state.sCount[nextLine] < state.blkIndent:
293 break
295 # if it's indented more than 3 spaces, it should be a code block
296 if state.sCount[startLine] - state.blkIndent >= 4:
297 break
299 # fail if terminating block found
300 terminate = False
301 for terminatorRule in terminatorRules:
302 if terminatorRule(state, nextLine, endLine, True):
303 terminate = True
304 break
306 if terminate:
307 break
309 # fail if list has another type
310 if isOrdered:
311 posAfterMarker = skipOrderedListMarker(state, nextLine)
312 if posAfterMarker < 0:
313 break
314 start = state.bMarks[nextLine] + state.tShift[nextLine]
315 else:
316 posAfterMarker = skipBulletListMarker(state, nextLine)
317 if posAfterMarker < 0:
318 break
320 if markerCharCode != state.srcCharCode[posAfterMarker - 1]:
321 break
323 # Finalize list
324 if isOrdered:
325 token = state.push("ordered_list_close", "ol", -1)
326 else:
327 token = state.push("bullet_list_close", "ul", -1)
329 token.markup = chr(markerCharCode)
331 listLines[1] = nextLine
332 state.line = nextLine
334 state.parentType = oldParentType
336 # mark paragraphs tight if needed
337 if tight:
338 markTightParagraphs(state, listTokIdx)
340 return True