///| Bullet and ordered list parsing (split from block_parser.mbt).
///|
/// A parsed list marker: its width in characters and the list it starts.
priv struct ListMarker {
width : Int
ordered : Bool
bullet : Char
start : Int
delim : Char
}
///|
/// Read the list marker at `pos`, if there is one.
///
/// `interrupts_paragraph` applies the extra restrictions the spec puts on a
/// list that starts in the middle of a paragraph: it must be numbered 1 and
/// may not be empty.
fn BlockParser::parse_list_marker(
self : BlockParser,
pos : Int,
interrupts_paragraph : Bool,
) -> ListMarker? {
let line = self.line
let len = self.line_len
guard pos < len else { return None }
let c = line.unsafe_get(pos)
let mut i = pos
let mut ordered = false
let mut bullet = '-'
let mut number = 1
let mut delim = '.'
if c == '*' || c == '-' || c == '+' {
bullet = c.unsafe_to_char()
i = i + 1
} else if c >= '0' && c <= '9' {
ordered = true
let mut digits = 0
number = 0
while i < len && line.unsafe_get(i) >= '0' && line.unsafe_get(i) <= '9' {
number = number * 10 + (line.unsafe_get(i).to_int() - '0'.to_int())
digits = digits + 1
i = i + 1
}
guard digits <= 9 else { return None }
guard i < len else { return None }
let d = line.unsafe_get(i)
guard d == '.' || d == ')' else { return None }
delim = d.unsafe_to_char()
i = i + 1
} else {
return None
}
guard i >= len || is_space_or_tab(line.unsafe_get(i)) else { return None }
if interrupts_paragraph {
if ordered && number != 1 {
return None
}
// An empty list item cannot interrupt a paragraph.
let mut j = i
while j < len && is_space_or_tab(line.unsafe_get(j)) {
j = j + 1
}
if j >= len {
return None
}
}
Some({ width: i - pos, ordered, bullet, start: number, delim })
}
///|
fn lists_match(list : Node, marker : ListMarker) -> Bool {
list.ordered == marker.ordered &&
(if marker.ordered {
list.delim == marker.delim
} else {
list.marker == marker.bullet
})
}
///|
/// Open a list item, and the list around it when this starts a new one.
fn BlockParser::try_list_item(self : BlockParser, container : Node) -> Node? {
// A `-`, `*` or `_` run that reads as a thematic break wins over a list.
if self.would_be_thematic_break() {
return None
}
let interrupts = container.kind == NodeKind::ParagraphNode
guard self.parse_list_marker(self.next_nonspace, interrupts) is Some(marker) else {
return None
}
let marker_offset = self.indent
let item_start = self.line_start + self.next_nonspace
self.advance_offset(self.next_nonspace + marker.width - self.offset, false)
// Measure the whitespace after the marker: one to four spaces set the
// content column; anything else (or nothing) falls back to a single space.
let save_offset = self.offset
let save_column = self.column
let save_tab = self.partially_consumed_tab
while self.column - save_column <= 5 &&
is_space_or_tab(self.peek_line(self.offset)) {
self.advance_offset(1, true)
}
let spaces = self.column - save_column
let padding = if spaces >= 5 || spaces < 1 || self.offset >= self.line_len {
self.offset = save_offset
self.column = save_column
self.partially_consumed_tab = save_tab
if is_space_or_tab(self.peek_line(self.offset)) {
self.advance_offset(1, true)
}
marker.width + 1
} else {
marker.width + spaces
}
let mut list = container
if container.kind != NodeKind::ListNode || !lists_match(container, marker) {
list = self.add_child(container, NodeKind::ListNode, item_start)
list.ordered = marker.ordered
list.marker = marker.bullet
list.delim = marker.delim
list.list_start = marker.start
}
list.end = self.line_end
let item = self.add_child(list, NodeKind::ItemNode, item_start)
item.ordered = marker.ordered
item.marker = marker.bullet
item.delim = marker.delim
item.item_marker_offset = marker_offset
item.item_padding = padding
item.end = self.line_end
self.take_task_checkbox(item)
Some(item)
}
///|
/// Would the current line read as a thematic break? `- - -` is a break, not a
/// list holding a list.
fn BlockParser::would_be_thematic_break(self : BlockParser) -> Bool {
let c = self.peek_line(self.next_nonspace)
guard c == '*' || c == '-' || c == '_' else { return false }
let mut i = self.next_nonspace
let mut count = 0
while i < self.line_len {
let ch = self.line.unsafe_get(i)
if ch == c {
count = count + 1
} else if !is_space_or_tab(ch) {
return false
}
i = i + 1
}
count >= 3
}
///|
/// GFM task list items: `[ ]`, `[x]` or `[X]` right after the marker.
fn BlockParser::take_task_checkbox(self : BlockParser, item : Node) -> Unit {
let i = self.offset
guard self.peek_line(i) == '[' else { return }
let mark = self.peek_line(i + 1)
guard mark == ' ' || mark == 'x' || mark == 'X' else { return }
guard self.peek_line(i + 2) == ']' else { return }
guard i + 3 >= self.line_len || is_space_or_tab(self.peek_line(i + 3)) else {
return
}
item.checked = Some(mark != ' ')
self.advance_offset(3, false)
if is_space_or_tab(self.peek_line(self.offset)) {
self.advance_offset(1, true)
}
}