///|
/// How sibling list items are recognized: by a regexp (dlist) or by a marker.
priv enum SiblingTrait {
Pattern(@regex.Regex)
Marker(String)
}
///|
fn list_rx_for(ctx : Context) -> @regex.Regex {
match ctx {
Ulist => unordered_list_rx
Olist => ordered_list_rx
Dlist => description_list_rx
_ => callout_list_rx
}
}
///|
fn parse_list(
reader : Reader,
list_type : Context,
parent : Node,
style : String?,
start? : String?,
) -> Node {
let start_i = match start {
Some(Some(s)) => Some(@rb.to_i(s))
_ => None
}
let list_block = match start_i {
Some(s) if s != 1 =>
Node::new_list(
parent,
list_type,
attributes=Attributes::from_array([("start", Int(s))]),
)
_ => Node::new_list(parent, list_type)
}
let list_rx = list_rx_for(list_type)
while reader.has_more_lines() {
guard list_rx.find(reader.peek_line().unwrap_or("")) is Some(m) else {
break
}
match parse_list_item(reader, list_block, m, Marker(m.at(1)), style) {
Item(li) => list_block.blocks.push(li)
Pair(_) => ()
}
if reader.skip_blank_lines() is None {
break
}
}
list_block
}
///|
fn parse_description_list(
reader : Reader,
m : @regex.MatchData,
parent : Node,
) -> Node {
let list_block = Node::new_list(parent, Dlist)
let sibling_pattern = description_list_sibling_rx.get(m.at(2)).unwrap()
let mut current_pair = match
parse_list_item(reader, list_block, m, Pattern(sibling_pattern), None) {
Pair(p) => p
Item(_) => abort("unexpected")
}
list_block.dlist_items.push(current_pair)
while reader.has_more_lines() {
guard sibling_pattern.find(reader.peek_line().unwrap_or("")) is Some(m2) else {
break
}
let next_pair = match
parse_list_item(reader, list_block, m2, Pattern(sibling_pattern), None) {
Pair(p) => p
Item(_) => abort("unexpected")
}
if current_pair.desc is Some(_) {
list_block.dlist_items.push(next_pair)
current_pair = next_pair
} else {
current_pair.terms.push(next_pair.terms[0])
current_pair.desc = next_pair.desc
}
}
list_block
}
///|
fn parse_callout_list(
reader : Reader,
m : @regex.MatchData?,
parent : Node,
callouts : Callouts,
) -> Node {
let list_block = Node::new_list(parent, Colist)
let mut next_index = 1
let mut autonum = 0
let mut m = m
while true {
let cur = match m {
Some(mm) => mm
None =>
match reader.peek_line() {
Some(l) =>
match callout_list_rx.find(l) {
Some(mm) => {
reader.mark() |> ignore
mm
}
None => break
}
None => break
}
}
let mut num = cur.at(1)
if num == "." {
autonum += 1
num = autonum.to_string()
}
if num != next_index.to_string() {
log_warn(
"callout list item index: expected \{next_index}, got \{num}",
source_location=reader.cursor_at_mark(),
)
}
match parse_list_item(reader, list_block, cur, Marker("<1>"), None) {
Item(list_item) => {
list_block.blocks.push(list_item)
let coids = callouts.callout_ids(list_block.blocks.length())
if coids == "" {
log_warn(
"no callout found for <\{list_block.blocks.length()}>",
source_location=reader.cursor_at_mark(),
)
} else {
list_item.attributes.set_str("coids", coids)
}
}
Pair(_) => ()
}
next_index += 1
m = None
}
callouts.next_list()
list_block
}
///|
priv enum ListItemResult {
Item(Node)
Pair(DlistItem)
}
///|
fn parse_list_item(
reader : Reader,
list_block : Node,
m : @regex.MatchData,
sibling_trait : SiblingTrait,
style : String?,
) -> ListItemResult {
let list_type = list_block.context
let mut dlist = false
let mut has_text = true
let mut sourcemap_assignment_deferred = false
let mut list_term : Node? = None
let mut sibling_trait = sibling_trait
let list_item = if list_type == Dlist {
dlist = true
let term_text = m.at(1)
let lt = Node::new_list_item(list_block, text=term_text)
list_term = Some(lt)
if term_text.has_prefix("[[") {
match leading_inline_anchor_rx.find(term_text) {
Some(am) =>
catalog_inline_anchor(
am.at(1),
match am.group(2) {
Some(r) => Some(r)
None => Some(@rb.lstrip(am.post_match()))
},
lt,
Some(reader.cursor()),
lt.document(),
)
None => ()
}
}
has_text = m.has(3)
let li = Node::new_list_item(list_block, text?=m.group(3))
if list_block.document().sourcemap() {
lt.source_location = Some(reader.cursor())
if has_text {
li.source_location = lt.source_location
} else {
sourcemap_assignment_deferred = true
}
}
li
} else {
has_text = true
let item_text = m.at(2)
let li = Node::new_list_item(list_block, text=item_text)
if list_block.document().sourcemap() {
li.source_location = Some(reader.cursor())
}
match list_type {
Ulist => {
li.marker = match sibling_trait {
Marker(s) => Some(s)
_ => None
}
if item_text.has_prefix("[") {
if style == Some("bibliography") {
match inline_biblio_anchor_rx.find(item_text) {
Some(am) =>
catalog_inline_biblio_anchor(am.at(1), am.group(2), li, reader)
None => ()
}
} else if item_text.has_prefix("[[") {
match leading_inline_anchor_rx.find(item_text) {
Some(am) =>
catalog_inline_anchor(
am.at(1),
am.group(2),
li,
Some(reader.cursor()),
li.document(),
)
None => ()
}
} else if item_text.has_prefix("[ ] ") ||
item_text.has_prefix("[x] ") ||
item_text.has_prefix("[*] ") {
list_block.attributes.set_str("checklist-option", "")
li.attributes.set_str("checkbox", "")
if !item_text.has_prefix("[ ") {
li.attributes.set_str("checked", "")
}
li.set_text(Some(@rb.from(item_text, 4)))
}
}
}
Olist => {
let mut ordinal = list_block.blocks.length()
let first = ordinal == 0
let mut validate = true
let marker0 = match sibling_trait {
Marker(s) => s
_ => ""
}
match list_block.attributes.get("start") {
Some(start) if start.truthy() => ordinal += start.to_i() - 1
_ =>
if first {
let start = resolve_ordered_list_start(marker0)
if start != 1 {
list_block.attributes.set("start", Int(start))
ordinal += start - 1
validate = false
}
}
}
let (marker, implicit_style) = resolve_ordered_list_marker(
marker0,
ordinal=Some(ordinal),
validate~,
reader=Some(reader),
)
sibling_trait = Marker(marker)
li.marker = Some(marker)
if first && style is None {
list_block.symbolic_style = implicit_style is Some(_)
list_block.style = Some(
match implicit_style {
Some(s) => s
None =>
match marker.length() - 1 {
0 => "arabic"
1 => "loweralpha"
2 => "lowerroman"
3 => "upperalpha"
4 => "upperroman"
_ => "arabic"
}
},
)
}
if item_text.has_prefix("[[") {
match leading_inline_anchor_rx.find(item_text) {
Some(am) =>
catalog_inline_anchor(
am.at(1),
am.group(2),
li,
Some(reader.cursor()),
li.document(),
)
None => ()
}
}
}
_ => {
li.marker = match sibling_trait {
Marker(s) => Some(s)
_ => None
}
if item_text.has_prefix("[[") {
match leading_inline_anchor_rx.find(item_text) {
Some(am) =>
catalog_inline_anchor(
am.at(1),
am.group(2),
li,
Some(reader.cursor()),
li.document(),
)
None => ()
}
}
}
}
li
}
reader.shift() |> ignore
let block_cursor = reader.cursor()
let (item_lines, item_marks) = read_lines_for_list_item(
reader, list_type, sibling_trait, has_text,
)
let list_item_reader = Reader::new_marked(
item_lines,
item_marks,
cursor=block_cursor,
)
if list_item_reader.has_more_lines() {
if sourcemap_assignment_deferred {
list_item.source_location = Some(block_cursor)
}
let comment_lines = list_item_reader.skip_line_comments()
let mut content_adjacent = false
match list_item_reader.peek_line() {
Some(subsequent_line) => {
if !comment_lines.is_empty() {
list_item_reader.unshift_lines(comment_lines)
}
if subsequent_line != "" {
content_adjacent = true
if !dlist {
has_text = false
}
}
}
None => ()
}
match
next_block(
list_item_reader,
list_item,
Attributes::new(),
text_only=!has_text,
list_type~,
) {
Some(b) => list_item.blocks.push(b)
None => ()
}
while list_item_reader.has_more_lines() {
match
next_block(list_item_reader, list_item, Attributes::new(), list_type~) {
Some(b) => list_item.blocks.push(b)
None => ()
}
}
if content_adjacent &&
(match list_item.blocks.get(0) {
Some(fb) => fb.context == Paragraph
None => false
}) {
list_item.fold_first()
}
}
if dlist {
Pair({
terms: [list_term.unwrap()],
desc: if list_item.has_text() || list_item.has_blocks() {
Some(list_item)
} else {
None
},
})
} else {
Item(list_item)
}
}
///|
/// Reads the lines belonging to a list item (Ruby `read_lines_for_list_item`).
/// List continuation markers are tracked in a parallel array (Ruby uses
/// string identity via a marker module).
fn read_lines_for_list_item(
reader : Reader,
list_type : Context,
sibling_trait : SiblingTrait,
has_text : Bool,
) -> (Array[String], Array[Bool]) {
let buffer : Array[String] = []
let is_marker : Array[Bool] = []
let push = fn(line : String, marker : Bool) {
buffer.push(line)
is_marker.push(marker)
}
let concat = fn(lines : Array[String]) {
for l in lines {
buffer.push(l)
is_marker.push(false)
}
}
let mut has_text = has_text
let mut continuation = 0 // 0 inactive, 1 active, 2 frozen
let mut within_nested_list = false
let mut detached_continuation : Int? = None
let dlist = list_type == Dlist
let mut this_line : String? = None
let mut this_marker = false
while reader.has_more_lines() {
let mut line = reader.read_line().unwrap()
this_line = Some(line)
this_marker = line == "+" || reader.last_line_marked()
if is_sibling_list_item(line, list_type, sibling_trait) {
break
}
let mut line_is_marker = this_marker
let prev_idx = buffer.length() - 1
let prev_line : String? = if prev_idx >= 0 {
Some(buffer[prev_idx])
} else {
None
}
let prev_is_marker = prev_idx >= 0 && is_marker[prev_idx]
if prev_is_marker {
if continuation == 0 {
continuation = 1
has_text = true
if !within_nested_list {
buffer[prev_idx] = ""
is_marker[prev_idx] = true
}
}
if line_is_marker {
if continuation != 2 {
continuation = 2
push(line, true)
}
this_line = None
continue
}
}
match is_delimited_block(line) {
Some(dm) => {
if continuation != 1 {
break
}
push(line, line_is_marker)
concat(
reader.read_lines_until(
terminator=dm.terminator,
read_last_line=true,
context=None,
),
)
continuation = 0
}
None =>
if dlist &&
continuation != 1 &&
line.has_prefix("[") &&
block_attribute_line_rx.matches(line) {
let block_attribute_lines = [line]
let mut interrupt = false
while reader.peek_line() is Some(next_line) {
if is_delimited_block(next_line) is Some(_) {
interrupt = true
} else if next_line == "" ||
(
next_line.has_prefix("[") &&
block_attribute_line_rx.matches(next_line)
) {
block_attribute_lines.push(reader.read_line().unwrap())
continue
} else if any_list_rx.matches(next_line) &&
!is_sibling_list_item(next_line, list_type, sibling_trait) {
concat(block_attribute_lines)
} else {
interrupt = true
}
break
}
if interrupt {
this_line = None
reader.unshift_lines(block_attribute_lines)
break
}
} else if continuation == 1 && line != "" {
if literal_paragraph_rx.matches(line) {
reader.unshift_line(line)
if dlist {
concat(
reader.read_lines_until(
preserve_last_line=true,
break_on_blank_lines=true,
break_on_list_continuation=true,
break_if=l => {
is_sibling_list_item(l, list_type, sibling_trait)
},
),
)
} else {
concat(
reader.read_lines_until(
preserve_last_line=true,
break_on_blank_lines=true,
break_on_list_continuation=true,
),
)
}
continuation = 0
} else if (line.has_prefix(".") && block_title_rx.matches(line)) ||
(line.has_prefix("[") && block_attribute_line_rx.matches(line)) ||
(line.has_prefix(":") && attribute_entry_rx.matches(line)) {
push(line, line_is_marker)
} else {
let candidates = if within_nested_list {
[Dlist]
} else {
[Ulist, Olist, Dlist]
}
for ctx in candidates {
match list_rx_for(ctx).find(line) {
Some(nm) => {
within_nested_list = true
if ctx == Dlist && nm.at(3) == "" {
has_text = false
}
break
}
None => ()
}
}
push(line, line_is_marker)
continuation = 0
}
} else if prev_line == Some("") {
if line == "" {
if reader.skip_blank_lines() is None {
this_line = None
break
}
match reader.read_line() {
Some(l) => {
line = l
this_line = Some(l)
line_is_marker = l == "+" || reader.last_line_marked()
this_marker = line_is_marker
}
None => {
this_line = None
break
}
}
if is_sibling_list_item(line, list_type, sibling_trait) {
break
}
}
if line == "+" {
detached_continuation = Some(buffer.length())
push(line, true)
} else if has_text {
if is_sibling_list_item(line, list_type, sibling_trait) {
break
}
let mut nested : Context? = None
let mut nested_m : @regex.MatchData? = None
for ctx in [Ulist, Olist, Dlist] {
match list_rx_for(ctx).find(line) {
Some(nm) => {
nested = Some(ctx)
nested_m = Some(nm)
break
}
None => ()
}
}
match nested {
Some(ctx) => {
push(line, line_is_marker)
within_nested_list = true
if ctx == Dlist && nested_m.unwrap().at(3) == "" {
has_text = false
}
}
None =>
if literal_paragraph_rx.matches(line) {
reader.unshift_line(line)
if dlist {
concat(
reader.read_lines_until(
preserve_last_line=true,
break_on_blank_lines=true,
break_on_list_continuation=true,
break_if=l => {
is_sibling_list_item(l, list_type, sibling_trait)
},
),
)
} else {
concat(
reader.read_lines_until(
preserve_last_line=true,
break_on_blank_lines=true,
break_on_list_continuation=true,
),
)
}
} else {
break
}
}
} else {
if !within_nested_list {
buffer.pop() |> ignore
is_marker.pop() |> ignore
}
push(line, line_is_marker)
has_text = true
}
} else if line_is_marker {
has_text = true
push(line, true)
} else {
if line != "" {
has_text = true
let candidates = if within_nested_list {
[Dlist]
} else {
[Ulist, Olist, Dlist]
}
for ctx in candidates {
match list_rx_for(ctx).find(line) {
Some(nm) => {
within_nested_list = true
if ctx == Dlist && nm.at(3) == "" {
has_text = false
}
break
}
None => ()
}
}
}
push(line, false)
}
}
this_line = None
}
match this_line {
Some(l) => reader.unshift_marked(l, this_marker && l != "+")
None => ()
}
match detached_continuation {
Some(i) => {
buffer[i] = ""
is_marker[i] = true
}
None => ()
}
while !buffer.is_empty() {
let last = buffer.length() - 1
if is_marker[last] {
buffer.pop() |> ignore
is_marker.pop() |> ignore
break
} else if buffer[last] == "" {
buffer.pop() |> ignore
is_marker.pop() |> ignore
} else {
break
}
}
(buffer, is_marker)
}
///|
fn resolve_list_marker(list_type : Context, marker : String) -> String {
match list_type {
Ulist => marker
Olist => resolve_ordered_list_marker(marker).0
_ => "<1>"
}
}
///|
fn ordered_list_style_of(marker : String) -> String? {
for s in ["arabic", "loweralpha", "lowerroman", "upperalpha", "upperroman"] {
if ordered_list_marker_rx_map.get(s).unwrap().matches(marker) {
return Some(s)
}
}
None
}
///|
fn resolve_ordered_list_marker(
marker : String,
ordinal? : Int?,
validate? : Bool = false,
reader? : Reader?,
) -> (String, String?) {
if marker.has_prefix(".") {
return (marker, None)
}
let style = ordered_list_style_of(marker)
let ordinal_v = ordinal.unwrap_or(None)
let mut expected = ""
let mut actual = ""
let new_marker = match style {
Some("arabic") => {
if validate {
expected = (ordinal_v.unwrap_or(0) + 1).to_string()
actual = @rb.to_i(marker).to_string()
}
"1."
}
Some("loweralpha") => {
if validate {
expected = (97 + ordinal_v.unwrap_or(0)).unsafe_to_char().to_string()
actual = @rb.chop(marker)
}
"a."
}
Some("upperalpha") => {
if validate {
expected = (65 + ordinal_v.unwrap_or(0)).unsafe_to_char().to_string()
actual = @rb.chop(marker)
}
"A."
}
Some("lowerroman") => {
if validate {
expected = @rb.downcase(@rb.int_to_roman(ordinal_v.unwrap_or(0) + 1))
actual = @rb.chop(marker)
}
"i)"
}
Some("upperroman") => {
if validate {
expected = @rb.int_to_roman(ordinal_v.unwrap_or(0) + 1)
actual = @rb.chop(marker)
}
"I)"
}
_ => marker
}
match ordinal_v {
Some(_) => {
if validate && expected != actual {
match reader.unwrap_or(None) {
Some(r) =>
log_warn(
"list item index: expected \{expected}, got \{actual}",
source_location=r.cursor(),
)
None =>
log_warn("list item index: expected \{expected}, got \{actual}")
}
}
(new_marker, style)
}
None => (new_marker, None)
}
}
///|
fn resolve_ordered_list_start(marker : String) -> Int {
if marker.has_prefix(".") {
return 1
}
match ordered_list_style_of(marker) {
Some("arabic") => @rb.to_i(marker)
Some("loweralpha") => @rb.chop(marker)[0].to_int() - 96
Some("upperalpha") => @rb.chop(marker)[0].to_int() - 64
Some("lowerroman") => roman_to_int(@rb.upcase(@rb.chop(marker)))
Some("upperroman") => roman_to_int(@rb.chop(marker))
_ => 1
}
}
///|
fn is_sibling_list_item(
line : String,
list_type : Context,
sibling_trait : SiblingTrait,
) -> Bool {
match sibling_trait {
Pattern(rx) => rx.matches(line)
Marker(s) =>
match list_rx_for(list_type).find(line) {
Some(m) => s == resolve_list_marker(list_type, m.at(1))
None => false
}
}
}