346 lines
9.4 KiB
Python
346 lines
9.4 KiB
Python
|
# Lists
|
||
|
import logging
|
||
|
|
||
|
from ..common.utils import isStrSpace
|
||
|
from .state_block import StateBlock
|
||
|
|
||
|
LOGGER = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
# Search `[-+*][\n ]`, returns next pos after marker on success
|
||
|
# or -1 on fail.
|
||
|
def skipBulletListMarker(state: StateBlock, startLine: int) -> int:
|
||
|
pos = state.bMarks[startLine] + state.tShift[startLine]
|
||
|
maximum = state.eMarks[startLine]
|
||
|
|
||
|
try:
|
||
|
marker = state.src[pos]
|
||
|
except IndexError:
|
||
|
return -1
|
||
|
pos += 1
|
||
|
|
||
|
if marker not in ("*", "-", "+"):
|
||
|
return -1
|
||
|
|
||
|
if pos < maximum:
|
||
|
ch = state.src[pos]
|
||
|
|
||
|
if not isStrSpace(ch):
|
||
|
# " -test " - is not a list item
|
||
|
return -1
|
||
|
|
||
|
return pos
|
||
|
|
||
|
|
||
|
# Search `\d+[.)][\n ]`, returns next pos after marker on success
|
||
|
# or -1 on fail.
|
||
|
def skipOrderedListMarker(state: StateBlock, startLine: int) -> int:
|
||
|
start = state.bMarks[startLine] + state.tShift[startLine]
|
||
|
pos = start
|
||
|
maximum = state.eMarks[startLine]
|
||
|
|
||
|
# List marker should have at least 2 chars (digit + dot)
|
||
|
if pos + 1 >= maximum:
|
||
|
return -1
|
||
|
|
||
|
ch = state.src[pos]
|
||
|
pos += 1
|
||
|
|
||
|
ch_ord = ord(ch)
|
||
|
# /* 0 */ /* 9 */
|
||
|
if ch_ord < 0x30 or ch_ord > 0x39:
|
||
|
return -1
|
||
|
|
||
|
while True:
|
||
|
# EOL -> fail
|
||
|
if pos >= maximum:
|
||
|
return -1
|
||
|
|
||
|
ch = state.src[pos]
|
||
|
pos += 1
|
||
|
|
||
|
# /* 0 */ /* 9 */
|
||
|
ch_ord = ord(ch)
|
||
|
if ch_ord >= 0x30 and ch_ord <= 0x39:
|
||
|
# List marker should have no more than 9 digits
|
||
|
# (prevents integer overflow in browsers)
|
||
|
if pos - start >= 10:
|
||
|
return -1
|
||
|
|
||
|
continue
|
||
|
|
||
|
# found valid marker
|
||
|
if ch in (")", "."):
|
||
|
break
|
||
|
|
||
|
return -1
|
||
|
|
||
|
if pos < maximum:
|
||
|
ch = state.src[pos]
|
||
|
|
||
|
if not isStrSpace(ch):
|
||
|
# " 1.test " - is not a list item
|
||
|
return -1
|
||
|
|
||
|
return pos
|
||
|
|
||
|
|
||
|
def markTightParagraphs(state: StateBlock, idx: int) -> None:
|
||
|
level = state.level + 2
|
||
|
|
||
|
i = idx + 2
|
||
|
length = len(state.tokens) - 2
|
||
|
while i < length:
|
||
|
if state.tokens[i].level == level and state.tokens[i].type == "paragraph_open":
|
||
|
state.tokens[i + 2].hidden = True
|
||
|
state.tokens[i].hidden = True
|
||
|
i += 2
|
||
|
i += 1
|
||
|
|
||
|
|
||
|
def list_block(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
|
||
|
LOGGER.debug("entering list: %s, %s, %s, %s", state, startLine, endLine, silent)
|
||
|
|
||
|
isTerminatingParagraph = False
|
||
|
tight = True
|
||
|
|
||
|
if state.is_code_block(startLine):
|
||
|
return False
|
||
|
|
||
|
# Special case:
|
||
|
# - item 1
|
||
|
# - item 2
|
||
|
# - item 3
|
||
|
# - item 4
|
||
|
# - this one is a paragraph continuation
|
||
|
if (
|
||
|
state.listIndent >= 0
|
||
|
and state.sCount[startLine] - state.listIndent >= 4
|
||
|
and state.sCount[startLine] < state.blkIndent
|
||
|
):
|
||
|
return False
|
||
|
|
||
|
# limit conditions when list can interrupt
|
||
|
# a paragraph (validation mode only)
|
||
|
# Next list item should still terminate previous list item
|
||
|
#
|
||
|
# This code can fail if plugins use blkIndent as well as lists,
|
||
|
# but I hope the spec gets fixed long before that happens.
|
||
|
#
|
||
|
if (
|
||
|
silent
|
||
|
and state.parentType == "paragraph"
|
||
|
and state.sCount[startLine] >= state.blkIndent
|
||
|
):
|
||
|
isTerminatingParagraph = True
|
||
|
|
||
|
# Detect list type and position after marker
|
||
|
posAfterMarker = skipOrderedListMarker(state, startLine)
|
||
|
if posAfterMarker >= 0:
|
||
|
isOrdered = True
|
||
|
start = state.bMarks[startLine] + state.tShift[startLine]
|
||
|
markerValue = int(state.src[start : posAfterMarker - 1])
|
||
|
|
||
|
# If we're starting a new ordered list right after
|
||
|
# a paragraph, it should start with 1.
|
||
|
if isTerminatingParagraph and markerValue != 1:
|
||
|
return False
|
||
|
else:
|
||
|
posAfterMarker = skipBulletListMarker(state, startLine)
|
||
|
if posAfterMarker >= 0:
|
||
|
isOrdered = False
|
||
|
else:
|
||
|
return False
|
||
|
|
||
|
# If we're starting a new unordered list right after
|
||
|
# a paragraph, first line should not be empty.
|
||
|
if (
|
||
|
isTerminatingParagraph
|
||
|
and state.skipSpaces(posAfterMarker) >= state.eMarks[startLine]
|
||
|
):
|
||
|
return False
|
||
|
|
||
|
# We should terminate list on style change. Remember first one to compare.
|
||
|
markerChar = state.src[posAfterMarker - 1]
|
||
|
|
||
|
# For validation mode we can terminate immediately
|
||
|
if silent:
|
||
|
return True
|
||
|
|
||
|
# Start list
|
||
|
listTokIdx = len(state.tokens)
|
||
|
|
||
|
if isOrdered:
|
||
|
token = state.push("ordered_list_open", "ol", 1)
|
||
|
if markerValue != 1:
|
||
|
token.attrs = {"start": markerValue}
|
||
|
|
||
|
else:
|
||
|
token = state.push("bullet_list_open", "ul", 1)
|
||
|
|
||
|
token.map = listLines = [startLine, 0]
|
||
|
token.markup = markerChar
|
||
|
|
||
|
#
|
||
|
# Iterate list items
|
||
|
#
|
||
|
|
||
|
nextLine = startLine
|
||
|
prevEmptyEnd = False
|
||
|
terminatorRules = state.md.block.ruler.getRules("list")
|
||
|
|
||
|
oldParentType = state.parentType
|
||
|
state.parentType = "list"
|
||
|
|
||
|
while nextLine < endLine:
|
||
|
pos = posAfterMarker
|
||
|
maximum = state.eMarks[nextLine]
|
||
|
|
||
|
initial = offset = (
|
||
|
state.sCount[nextLine]
|
||
|
+ posAfterMarker
|
||
|
- (state.bMarks[startLine] + state.tShift[startLine])
|
||
|
)
|
||
|
|
||
|
while pos < maximum:
|
||
|
ch = state.src[pos]
|
||
|
|
||
|
if ch == "\t":
|
||
|
offset += 4 - (offset + state.bsCount[nextLine]) % 4
|
||
|
elif ch == " ":
|
||
|
offset += 1
|
||
|
else:
|
||
|
break
|
||
|
|
||
|
pos += 1
|
||
|
|
||
|
contentStart = pos
|
||
|
|
||
|
# trimming space in "- \n 3" case, indent is 1 here
|
||
|
indentAfterMarker = 1 if contentStart >= maximum else offset - initial
|
||
|
|
||
|
# If we have more than 4 spaces, the indent is 1
|
||
|
# (the rest is just indented code block)
|
||
|
if indentAfterMarker > 4:
|
||
|
indentAfterMarker = 1
|
||
|
|
||
|
# " - test"
|
||
|
# ^^^^^ - calculating total length of this thing
|
||
|
indent = initial + indentAfterMarker
|
||
|
|
||
|
# Run subparser & write tokens
|
||
|
token = state.push("list_item_open", "li", 1)
|
||
|
token.markup = markerChar
|
||
|
token.map = itemLines = [startLine, 0]
|
||
|
if isOrdered:
|
||
|
token.info = state.src[start : posAfterMarker - 1]
|
||
|
|
||
|
# change current state, then restore it after parser subcall
|
||
|
oldTight = state.tight
|
||
|
oldTShift = state.tShift[startLine]
|
||
|
oldSCount = state.sCount[startLine]
|
||
|
|
||
|
# - example list
|
||
|
# ^ listIndent position will be here
|
||
|
# ^ blkIndent position will be here
|
||
|
#
|
||
|
oldListIndent = state.listIndent
|
||
|
state.listIndent = state.blkIndent
|
||
|
state.blkIndent = indent
|
||
|
|
||
|
state.tight = True
|
||
|
state.tShift[startLine] = contentStart - state.bMarks[startLine]
|
||
|
state.sCount[startLine] = offset
|
||
|
|
||
|
if contentStart >= maximum and state.isEmpty(startLine + 1):
|
||
|
# workaround for this case
|
||
|
# (list item is empty, list terminates before "foo"):
|
||
|
# ~~~~~~~~
|
||
|
# -
|
||
|
#
|
||
|
# foo
|
||
|
# ~~~~~~~~
|
||
|
state.line = min(state.line + 2, endLine)
|
||
|
else:
|
||
|
# NOTE in list.js this was:
|
||
|
# state.md.block.tokenize(state, startLine, endLine, True)
|
||
|
# but tokeniz does not take the final parameter
|
||
|
state.md.block.tokenize(state, startLine, endLine)
|
||
|
|
||
|
# If any of list item is tight, mark list as tight
|
||
|
if (not state.tight) or prevEmptyEnd:
|
||
|
tight = False
|
||
|
|
||
|
# Item become loose if finish with empty line,
|
||
|
# but we should filter last element, because it means list finish
|
||
|
prevEmptyEnd = (state.line - startLine) > 1 and state.isEmpty(state.line - 1)
|
||
|
|
||
|
state.blkIndent = state.listIndent
|
||
|
state.listIndent = oldListIndent
|
||
|
state.tShift[startLine] = oldTShift
|
||
|
state.sCount[startLine] = oldSCount
|
||
|
state.tight = oldTight
|
||
|
|
||
|
token = state.push("list_item_close", "li", -1)
|
||
|
token.markup = markerChar
|
||
|
|
||
|
nextLine = startLine = state.line
|
||
|
itemLines[1] = nextLine
|
||
|
|
||
|
if nextLine >= endLine:
|
||
|
break
|
||
|
|
||
|
contentStart = state.bMarks[startLine]
|
||
|
|
||
|
#
|
||
|
# Try to check if list is terminated or continued.
|
||
|
#
|
||
|
if state.sCount[nextLine] < state.blkIndent:
|
||
|
break
|
||
|
|
||
|
if state.is_code_block(startLine):
|
||
|
break
|
||
|
|
||
|
# fail if terminating block found
|
||
|
terminate = False
|
||
|
for terminatorRule in terminatorRules:
|
||
|
if terminatorRule(state, nextLine, endLine, True):
|
||
|
terminate = True
|
||
|
break
|
||
|
|
||
|
if terminate:
|
||
|
break
|
||
|
|
||
|
# fail if list has another type
|
||
|
if isOrdered:
|
||
|
posAfterMarker = skipOrderedListMarker(state, nextLine)
|
||
|
if posAfterMarker < 0:
|
||
|
break
|
||
|
start = state.bMarks[nextLine] + state.tShift[nextLine]
|
||
|
else:
|
||
|
posAfterMarker = skipBulletListMarker(state, nextLine)
|
||
|
if posAfterMarker < 0:
|
||
|
break
|
||
|
|
||
|
if markerChar != state.src[posAfterMarker - 1]:
|
||
|
break
|
||
|
|
||
|
# Finalize list
|
||
|
if isOrdered:
|
||
|
token = state.push("ordered_list_close", "ol", -1)
|
||
|
else:
|
||
|
token = state.push("bullet_list_close", "ul", -1)
|
||
|
|
||
|
token.markup = markerChar
|
||
|
|
||
|
listLines[1] = nextLine
|
||
|
state.line = nextLine
|
||
|
|
||
|
state.parentType = oldParentType
|
||
|
|
||
|
# mark paragraphs tight if needed
|
||
|
if tight:
|
||
|
markTightParagraphs(state, listTokIdx)
|
||
|
|
||
|
return True
|