codekingpro/portable-devtools
114k
1# Lists2import logging3 4from ..common.utils import isStrSpace5from .state_block import StateBlock6 7LOGGER = logging.getLogger(__name__)8 9 10# Search `[-+*][\n ]`, returns next pos after marker on success11# or -1 on fail.12def skipBulletListMarker(state: StateBlock, startLine: int) -> int:13 pos = state.bMarks[startLine] + state.tShift[startLine]14 maximum = state.eMarks[startLine]15 16 try:17 marker = state.src[pos]18 except IndexError:19 return -120 pos += 121 22 if marker not in ("*", "-", "+"):23 return -124 25 if pos < maximum:26 ch = state.src[pos]27 28 if not isStrSpace(ch):29 # " -test " - is not a list item30 return -131 32 return pos33 34 35# Search `\d+[.)][\n ]`, returns next pos after marker on success36# or -1 on fail.37def skipOrderedListMarker(state: StateBlock, startLine: int) -> int:38 start = state.bMarks[startLine] + state.tShift[startLine]39 pos = start40 maximum = state.eMarks[startLine]41 42 # List marker should have at least 2 chars (digit + dot)43 if pos + 1 >= maximum:44 return -145 46 ch = state.src[pos]47 pos += 148 49 ch_ord = ord(ch)50 # /* 0 */ /* 9 */51 if ch_ord < 0x30 or ch_ord > 0x39:52 return -153 54 while True:55 # EOL -> fail56 if pos >= maximum:57 return -158 59 ch = state.src[pos]60 pos += 161 62 # /* 0 */ /* 9 */63 ch_ord = ord(ch)64 if ch_ord >= 0x30 and ch_ord <= 0x39:65 # List marker should have no more than 9 digits66 # (prevents integer overflow in browsers)67 if pos - start >= 10:68 return -169 70 continue71 72 # found valid marker73 if ch in (")", "."):74 break75 76 return -177 78 if pos < maximum:79 ch = state.src[pos]80 81 if not isStrSpace(ch):82 # " 1.test " - is not a list item83 return -184 85 return pos86 87 88def markTightParagraphs(state: StateBlock, idx: int) -> None:89 level = state.level + 290 91 i = idx + 292 length = len(state.tokens) - 293 while i < length:94 if state.tokens[i].level == level and state.tokens[i].type == "paragraph_open":95 state.tokens[i + 2].hidden = True96 state.tokens[i].hidden = True97 i += 298 i += 199 100 101def list_block(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:102 LOGGER.debug("entering list: %s, %s, %s, %s", state, startLine, endLine, silent)103 104 isTerminatingParagraph = False105 tight = True106 107 if state.is_code_block(startLine):108 return False109 110 # Special case:111 # - item 1112 # - item 2113 # - item 3114 # - item 4115 # - this one is a paragraph continuation116 if (117 state.listIndent >= 0118 and state.sCount[startLine] - state.listIndent >= 4119 and state.sCount[startLine] < state.blkIndent120 ):121 return False122 123 # limit conditions when list can interrupt124 # a paragraph (validation mode only)125 # Next list item should still terminate previous list item126 #127 # This code can fail if plugins use blkIndent as well as lists,128 # but I hope the spec gets fixed long before that happens.129 #130 if (131 silent132 and state.parentType == "paragraph"133 and state.sCount[startLine] >= state.blkIndent134 ):135 isTerminatingParagraph = True136 137 # Detect list type and position after marker138 posAfterMarker = skipOrderedListMarker(state, startLine)139 if posAfterMarker >= 0:140 isOrdered = True141 start = state.bMarks[startLine] + state.tShift[startLine]142 markerValue = int(state.src[start : posAfterMarker - 1])143 144 # If we're starting a new ordered list right after145 # a paragraph, it should start with 1.146 if isTerminatingParagraph and markerValue != 1:147 return False148 else:149 posAfterMarker = skipBulletListMarker(state, startLine)150 if posAfterMarker >= 0:151 isOrdered = False152 else:153 return False154 155 # If we're starting a new unordered list right after156 # a paragraph, first line should not be empty.157 if (158 isTerminatingParagraph159 and state.skipSpaces(posAfterMarker) >= state.eMarks[startLine]160 ):161 return False162 163 # We should terminate list on style change. Remember first one to compare.164 markerChar = state.src[posAfterMarker - 1]165 166 # For validation mode we can terminate immediately167 if silent:168 return True169 170 # Start list171 listTokIdx = len(state.tokens)172 173 if isOrdered:174 token = state.push("ordered_list_open", "ol", 1)175 if markerValue != 1:176 token.attrs = {"start": markerValue}177 178 else:179 token = state.push("bullet_list_open", "ul", 1)180 181 token.map = listLines = [startLine, 0]182 token.markup = markerChar183 184 #185 # Iterate list items186 #187 188 nextLine = startLine189 prevEmptyEnd = False190 terminatorRules = state.md.block.ruler.getRules("list")191 192 oldParentType = state.parentType193 state.parentType = "list"194 195 while nextLine < endLine:196 pos = posAfterMarker197 maximum = state.eMarks[nextLine]198 199 initial = offset = (200 state.sCount[nextLine]201 + posAfterMarker202 - (state.bMarks[startLine] + state.tShift[startLine])203 )204 205 while pos < maximum:206 ch = state.src[pos]207 208 if ch == "\t":209 offset += 4 - (offset + state.bsCount[nextLine]) % 4210 elif ch == " ":211 offset += 1212 else:213 break214 215 pos += 1216 217 contentStart = pos218 219 # trimming space in "- \n 3" case, indent is 1 here220 indentAfterMarker = 1 if contentStart >= maximum else offset - initial221 222 # If we have more than 4 spaces, the indent is 1223 # (the rest is just indented code block)224 if indentAfterMarker > 4:225 indentAfterMarker = 1226 227 # " - test"228 # ^^^^^ - calculating total length of this thing229 indent = initial + indentAfterMarker230 231 # Run subparser & write tokens232 token = state.push("list_item_open", "li", 1)233 token.markup = markerChar234 token.map = itemLines = [startLine, 0]235 if isOrdered:236 token.info = state.src[start : posAfterMarker - 1]237 238 # Detect GFM task checkbox: `[ ] ` or `[x] `/`[X] ` at content start239 checkboxLen = 0240 if state.md.options.get("tasklists", False) and contentStart < maximum:241 checked = _detect_task_checkbox(state.src, contentStart, maximum)242 if checked is not None:243 token.meta = {"checked": checked}244 # Advance content past the checkbox: `[x]` (3 chars) + whitespace.245 # `_detect_task_checkbox` already guarantees a whitespace char at246 # pos+3, so we always consume 4 characters.247 checkboxLen = 4248 249 # change current state, then restore it after parser subcall250 oldTight = state.tight251 oldBMark = state.bMarks[startLine]252 oldTShift = state.tShift[startLine]253 oldSCount = state.sCount[startLine]254 255 # - example list256 # ^ listIndent position will be here257 # ^ blkIndent position will be here258 #259 oldListIndent = state.listIndent260 state.listIndent = state.blkIndent261 state.blkIndent = indent262 263 state.tight = True264 state.tShift[startLine] = contentStart - state.bMarks[startLine]265 state.sCount[startLine] = offset266 267 # If we detected a checkbox, advance bMarks past it so that268 # getLines() doesn't include the checkbox text in the content.269 if checkboxLen:270 state.bMarks[startLine] = contentStart + checkboxLen271 state.tShift[startLine] = 0272 273 if contentStart >= maximum and state.isEmpty(startLine + 1):274 # workaround for this case275 # (list item is empty, list terminates before "foo"):276 # ~~~~~~~~277 # -278 #279 # foo280 # ~~~~~~~~281 state.line = min(state.line + 2, endLine)282 else:283 # NOTE in list.js this was:284 # state.md.block.tokenize(state, startLine, endLine, True)285 # but tokeniz does not take the final parameter286 state.md.block.tokenize(state, startLine, endLine)287 288 # If any of list item is tight, mark list as tight289 if (not state.tight) or prevEmptyEnd:290 tight = False291 292 # Item become loose if finish with empty line,293 # but we should filter last element, because it means list finish294 prevEmptyEnd = (state.line - startLine) > 1 and state.isEmpty(state.line - 1)295 296 state.blkIndent = state.listIndent297 state.listIndent = oldListIndent298 if checkboxLen:299 state.bMarks[startLine] = oldBMark300 state.tShift[startLine] = oldTShift301 state.sCount[startLine] = oldSCount302 state.tight = oldTight303 304 token = state.push("list_item_close", "li", -1)305 token.markup = markerChar306 307 nextLine = startLine = state.line308 itemLines[1] = nextLine309 310 if nextLine >= endLine:311 break312 313 contentStart = state.bMarks[startLine]314 315 #316 # Try to check if list is terminated or continued.317 #318 if state.sCount[nextLine] < state.blkIndent:319 break320 321 if state.is_code_block(startLine):322 break323 324 # fail if terminating block found325 terminate = False326 for terminatorRule in terminatorRules:327 if terminatorRule(state, nextLine, endLine, True):328 terminate = True329 break330 331 if terminate:332 break333 334 # fail if list has another type335 if isOrdered:336 posAfterMarker = skipOrderedListMarker(state, nextLine)337 if posAfterMarker < 0:338 break339 start = state.bMarks[nextLine] + state.tShift[nextLine]340 else:341 posAfterMarker = skipBulletListMarker(state, nextLine)342 if posAfterMarker < 0:343 break344 345 if markerChar != state.src[posAfterMarker - 1]:346 break347 348 # Finalize list349 350 # If any direct list item has a task checkbox, add class to the list351 if state.md.options.get("tasklists", False):352 containsTask = False353 level = state.tokens[listTokIdx].level354 for j in range(listTokIdx + 1, len(state.tokens)):355 tok = state.tokens[j]356 if (357 tok.level == level + 1358 and tok.type == "list_item_open"359 and tok.meta360 and "checked" in tok.meta361 ):362 tok.attrJoin("class", "task-list-item")363 containsTask = True364 if containsTask:365 state.tokens[listTokIdx].attrJoin("class", "contains-task-list")366 367 if isOrdered:368 token = state.push("ordered_list_close", "ol", -1)369 else:370 token = state.push("bullet_list_close", "ul", -1)371 372 token.markup = markerChar373 374 listLines[1] = nextLine375 state.line = nextLine376 377 state.parentType = oldParentType378 379 # mark paragraphs tight if needed380 if tight:381 markTightParagraphs(state, listTokIdx)382 383 return True384 385 386def _detect_task_checkbox(src: str, pos: int, maximum: int) -> bool | None:387 """Detect ``[ ]``, ``[x]``, or ``[X]`` at *pos*, followed by whitespace.388 389 Returns ``True`` (checked), ``False`` (unchecked), or ``None`` (no match).390 """391 # Need at least 4 chars: `[`, char, `]`, whitespace392 if pos + 4 > maximum:393 return None394 if src[pos] != "[":395 return None396 inner = src[pos + 1]397 if src[pos + 2] != "]":398 return None399 if inner == " ":400 checked = False401 elif inner in ("x", "X"):402 checked = True403 else:404 return None405 # After `]`, must have whitespace406 if src[pos + 3] not in (" ", "\t"):407 return None408 return checked409 