use crate::options::ParserOptions;
use crate::syntax::SyntaxKind;
use rowan::GreenNodeBuilder;
use crate::parser::utils::container_stack::{Container, ContainerStack, leading_indent};
use crate::parser::utils::helpers::strip_newline;
use crate::parser::utils::list_item_buffer::ListItemBuffer;
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ListMarker {
Bullet(char),
Ordered(OrderedMarker),
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum OrderedMarker {
Decimal {
number: String,
style: ListDelimiter,
},
Hash,
LowerAlpha {
letter: char,
style: ListDelimiter,
},
UpperAlpha {
letter: char,
style: ListDelimiter,
},
LowerRoman {
numeral: String,
style: ListDelimiter,
},
UpperRoman {
numeral: String,
style: ListDelimiter,
},
Example {
label: Option<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ListDelimiter {
Period,
RightParen,
Parens,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ListMarkerMatch {
pub(crate) marker: ListMarker,
pub(crate) marker_len: usize,
pub(crate) spaces_after_cols: usize,
pub(crate) spaces_after_bytes: usize,
}
#[derive(Debug, Clone, Copy)]
pub(in crate::parser) struct ListItemEmissionInput<'a> {
pub content: &'a str,
pub marker_len: usize,
pub spaces_after_cols: usize,
pub spaces_after_bytes: usize,
pub indent_cols: usize,
pub indent_bytes: usize,
}
fn try_parse_roman_numeral(text: &str, uppercase: bool) -> Option<(String, usize)> {
let valid_chars = if uppercase { "IVXLCDM" } else { "ivxlcdm" };
let count = text
.chars()
.take_while(|c| valid_chars.contains(*c))
.count();
if count == 0 {
return None;
}
let numeral = &text[..count];
let numeral_upper = numeral.to_uppercase();
let has_only_roman_chars = numeral_upper.chars().all(|c| "IVXLCDM".contains(c));
if !has_only_roman_chars {
return None;
}
if count == 1 {
let ch = numeral_upper.chars().next().unwrap();
if !matches!(ch, 'I' | 'V' | 'X') {
return None;
}
}
if numeral_upper.contains("IIII")
|| numeral_upper.contains("XXXX")
|| numeral_upper.contains("CCCC")
|| numeral_upper.contains("VV")
|| numeral_upper.contains("LL")
|| numeral_upper.contains("DD")
{
return None;
}
let chars: Vec<char> = numeral_upper.chars().collect();
for i in 0..chars.len().saturating_sub(1) {
let curr = chars[i];
let next = chars[i + 1];
let curr_val = match curr {
'I' => 1,
'V' => 5,
'X' => 10,
'L' => 50,
'C' => 100,
'D' => 500,
'M' => 1000,
_ => return None,
};
let next_val = match next {
'I' => 1,
'V' => 5,
'X' => 10,
'L' => 50,
'C' => 100,
'D' => 500,
'M' => 1000,
_ => return None,
};
if curr_val < next_val {
match (curr, next) {
('I', 'V') | ('I', 'X') => {} ('X', 'L') | ('X', 'C') => {} ('C', 'D') | ('C', 'M') => {} _ => return None, }
}
}
Some((numeral.to_string(), count))
}
pub(crate) fn try_parse_list_marker(line: &str, config: &ParserOptions) -> Option<ListMarkerMatch> {
let (_indent_cols, indent_bytes) = leading_indent(line);
let trimmed = &line[indent_bytes..];
if let Some(ch) = trimmed.chars().next()
&& matches!(ch, '*' | '+' | '-')
{
let after_marker = &trimmed[1..];
let trimmed_after = after_marker.trim_start();
let is_task = trimmed_after.starts_with('[')
&& trimmed_after.len() >= 3
&& matches!(
trimmed_after.chars().nth(1),
Some(' ') | Some('x') | Some('X')
)
&& trimmed_after.chars().nth(2) == Some(']');
if after_marker.starts_with(' ')
|| after_marker.starts_with('\t')
|| after_marker.is_empty()
|| is_task
{
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
return Some(ListMarkerMatch {
marker: ListMarker::Bullet(ch),
marker_len: 1,
spaces_after_cols,
spaces_after_bytes,
});
}
}
if config.extensions.fancy_lists
&& let Some(after_marker) = trimmed.strip_prefix("#.")
&& (after_marker.starts_with(' ')
|| after_marker.starts_with('\t')
|| after_marker.is_empty())
{
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
return Some(ListMarkerMatch {
marker: ListMarker::Ordered(OrderedMarker::Hash),
marker_len: 2,
spaces_after_cols,
spaces_after_bytes,
});
}
if config.extensions.example_lists
&& let Some(rest) = trimmed.strip_prefix("(@")
{
let label_end = rest
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '-')
.count();
if rest.len() > label_end && rest.chars().nth(label_end) == Some(')') {
let label = if label_end > 0 {
Some(rest[..label_end].to_string())
} else {
None
};
let after_marker = &rest[label_end + 1..];
if after_marker.starts_with(' ')
|| after_marker.starts_with('\t')
|| after_marker.is_empty()
{
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
let marker_len = 2 + label_end + 1; return Some(ListMarkerMatch {
marker: ListMarker::Ordered(OrderedMarker::Example { label }),
marker_len,
spaces_after_cols,
spaces_after_bytes,
});
}
}
}
if let Some(rest) = trimmed.strip_prefix('(') {
if config.extensions.fancy_lists {
let digit_count = rest.chars().take_while(|c| c.is_ascii_digit()).count();
if digit_count > 0
&& rest.len() > digit_count
&& rest.chars().nth(digit_count) == Some(')')
{
let number = &rest[..digit_count];
let after_marker = &rest[digit_count + 1..];
if after_marker.starts_with(' ')
|| after_marker.starts_with('\t')
|| after_marker.is_empty()
{
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
let marker_len = 2 + digit_count;
return Some(ListMarkerMatch {
marker: ListMarker::Ordered(OrderedMarker::Decimal {
number: number.to_string(),
style: ListDelimiter::Parens,
}),
marker_len,
spaces_after_cols,
spaces_after_bytes,
});
}
}
}
if config.extensions.fancy_lists {
if let Some((numeral, len)) = try_parse_roman_numeral(rest, false)
&& rest.len() > len
&& rest.chars().nth(len) == Some(')')
{
let after_marker = &rest[len + 1..];
if after_marker.starts_with(' ')
|| after_marker.starts_with('\t')
|| after_marker.is_empty()
{
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
return Some(ListMarkerMatch {
marker: ListMarker::Ordered(OrderedMarker::LowerRoman {
numeral,
style: ListDelimiter::Parens,
}),
marker_len: len + 2,
spaces_after_cols,
spaces_after_bytes,
});
}
}
if let Some((numeral, len)) = try_parse_roman_numeral(rest, true)
&& rest.len() > len
&& rest.chars().nth(len) == Some(')')
{
let after_marker = &rest[len + 1..];
if after_marker.starts_with(' ')
|| after_marker.starts_with('\t')
|| after_marker.is_empty()
{
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
return Some(ListMarkerMatch {
marker: ListMarker::Ordered(OrderedMarker::UpperRoman {
numeral,
style: ListDelimiter::Parens,
}),
marker_len: len + 2,
spaces_after_cols,
spaces_after_bytes,
});
}
}
if let Some(ch) = rest.chars().next()
&& ch.is_ascii_lowercase()
&& rest.len() > 1
&& rest.chars().nth(1) == Some(')')
{
let after_marker = &rest[2..];
if after_marker.starts_with(' ')
|| after_marker.starts_with('\t')
|| after_marker.is_empty()
{
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
return Some(ListMarkerMatch {
marker: ListMarker::Ordered(OrderedMarker::LowerAlpha {
letter: ch,
style: ListDelimiter::Parens,
}),
marker_len: 3,
spaces_after_cols,
spaces_after_bytes,
});
}
}
if let Some(ch) = rest.chars().next()
&& ch.is_ascii_uppercase()
&& rest.len() > 1
&& rest.chars().nth(1) == Some(')')
{
let after_marker = &rest[2..];
if after_marker.starts_with(' ')
|| after_marker.starts_with('\t')
|| after_marker.is_empty()
{
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
return Some(ListMarkerMatch {
marker: ListMarker::Ordered(OrderedMarker::UpperAlpha {
letter: ch,
style: ListDelimiter::Parens,
}),
marker_len: 3,
spaces_after_cols,
spaces_after_bytes,
});
}
}
}
}
let digit_count = trimmed.chars().take_while(|c| c.is_ascii_digit()).count();
if digit_count > 0 && trimmed.len() > digit_count {
let number = &trimmed[..digit_count];
let delim = trimmed.chars().nth(digit_count);
let (style, marker_len) = match delim {
Some('.') => (ListDelimiter::Period, digit_count + 1),
Some(')') => (ListDelimiter::RightParen, digit_count + 1),
_ => return None,
};
if style == ListDelimiter::RightParen && !config.extensions.fancy_lists {
return None;
}
let after_marker = &trimmed[marker_len..];
if after_marker.starts_with(' ')
|| after_marker.starts_with('\t')
|| after_marker.is_empty()
{
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
return Some(ListMarkerMatch {
marker: ListMarker::Ordered(OrderedMarker::Decimal {
number: number.to_string(),
style,
}),
marker_len,
spaces_after_cols,
spaces_after_bytes,
});
}
}
if config.extensions.fancy_lists {
if let Some((numeral, len)) = try_parse_roman_numeral(trimmed, false)
&& trimmed.len() > len
&& let Some(delim) = trimmed.chars().nth(len)
&& (delim == '.' || delim == ')')
{
let style = if delim == '.' {
ListDelimiter::Period
} else {
ListDelimiter::RightParen
};
let marker_len = len + 1;
let after_marker = &trimmed[marker_len..];
if after_marker.starts_with(' ')
|| after_marker.starts_with('\t')
|| after_marker.is_empty()
{
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
return Some(ListMarkerMatch {
marker: ListMarker::Ordered(OrderedMarker::LowerRoman { numeral, style }),
marker_len,
spaces_after_cols,
spaces_after_bytes,
});
}
}
if let Some((numeral, len)) = try_parse_roman_numeral(trimmed, true)
&& trimmed.len() > len
&& let Some(delim) = trimmed.chars().nth(len)
&& (delim == '.' || delim == ')')
{
let style = if delim == '.' {
ListDelimiter::Period
} else {
ListDelimiter::RightParen
};
let marker_len = len + 1;
let after_marker = &trimmed[marker_len..];
if after_marker.starts_with(' ')
|| after_marker.starts_with('\t')
|| after_marker.is_empty()
{
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
return Some(ListMarkerMatch {
marker: ListMarker::Ordered(OrderedMarker::UpperRoman { numeral, style }),
marker_len,
spaces_after_cols,
spaces_after_bytes,
});
}
}
if let Some(ch) = trimmed.chars().next()
&& ch.is_ascii_lowercase()
&& trimmed.len() > 1
&& let Some(delim) = trimmed.chars().nth(1)
&& (delim == '.' || delim == ')')
{
let style = if delim == '.' {
ListDelimiter::Period
} else {
ListDelimiter::RightParen
};
let marker_len = 2;
let after_marker = &trimmed[marker_len..];
if after_marker.starts_with(' ')
|| after_marker.starts_with('\t')
|| after_marker.is_empty()
{
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
return Some(ListMarkerMatch {
marker: ListMarker::Ordered(OrderedMarker::LowerAlpha { letter: ch, style }),
marker_len,
spaces_after_cols,
spaces_after_bytes,
});
}
}
if let Some(ch) = trimmed.chars().next()
&& ch.is_ascii_uppercase()
&& trimmed.len() > 1
&& let Some(delim) = trimmed.chars().nth(1)
&& (delim == '.' || delim == ')')
{
let style = if delim == '.' {
ListDelimiter::Period
} else {
ListDelimiter::RightParen
};
let marker_len = 2;
let after_marker = &trimmed[marker_len..];
let min_spaces = if delim == '.' { 2 } else { 1 };
let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
if (after_marker.starts_with(' ') || after_marker.starts_with('\t'))
&& spaces_after_cols >= min_spaces
{
return Some(ListMarkerMatch {
marker: ListMarker::Ordered(OrderedMarker::UpperAlpha { letter: ch, style }),
marker_len,
spaces_after_cols,
spaces_after_bytes,
});
}
}
}
None
}
pub(crate) fn markers_match(a: &ListMarker, b: &ListMarker) -> bool {
match (a, b) {
(ListMarker::Bullet(_), ListMarker::Bullet(_)) => true,
(ListMarker::Ordered(OrderedMarker::Hash), ListMarker::Ordered(OrderedMarker::Hash)) => {
true
}
(
ListMarker::Ordered(OrderedMarker::Decimal { style: s1, .. }),
ListMarker::Ordered(OrderedMarker::Decimal { style: s2, .. }),
) => s1 == s2,
(
ListMarker::Ordered(OrderedMarker::LowerAlpha { style: s1, .. }),
ListMarker::Ordered(OrderedMarker::LowerAlpha { style: s2, .. }),
) => s1 == s2,
(
ListMarker::Ordered(OrderedMarker::UpperAlpha { style: s1, .. }),
ListMarker::Ordered(OrderedMarker::UpperAlpha { style: s2, .. }),
) => s1 == s2,
(
ListMarker::Ordered(OrderedMarker::LowerRoman { style: s1, .. }),
ListMarker::Ordered(OrderedMarker::LowerRoman { style: s2, .. }),
) => s1 == s2,
(
ListMarker::Ordered(OrderedMarker::UpperRoman { style: s1, .. }),
ListMarker::Ordered(OrderedMarker::UpperRoman { style: s2, .. }),
) => s1 == s2,
(
ListMarker::Ordered(OrderedMarker::Example { .. }),
ListMarker::Ordered(OrderedMarker::Example { .. }),
) => true, _ => false,
}
}
pub(in crate::parser) fn emit_list_item(
builder: &mut GreenNodeBuilder<'static>,
item: &ListItemEmissionInput<'_>,
) -> (usize, String) {
builder.start_node(SyntaxKind::LIST_ITEM.into());
if item.indent_bytes > 0 {
builder.token(
SyntaxKind::WHITESPACE.into(),
&item.content[..item.indent_bytes],
);
}
let marker_text = &item.content[item.indent_bytes..item.indent_bytes + item.marker_len];
builder.token(SyntaxKind::LIST_MARKER.into(), marker_text);
if item.spaces_after_bytes > 0 {
let space_start = item.indent_bytes + item.marker_len;
let space_end = space_start + item.spaces_after_bytes;
if space_end <= item.content.len() {
builder.token(
SyntaxKind::WHITESPACE.into(),
&item.content[space_start..space_end],
);
}
}
let content_col = item.indent_cols + item.marker_len + item.spaces_after_cols;
let content_start = item.indent_bytes + item.marker_len + item.spaces_after_bytes;
let text_to_buffer = if content_start < item.content.len() {
let rest = &item.content[content_start..];
if (rest.starts_with("[ ]") || rest.starts_with("[x]") || rest.starts_with("[X]"))
&& rest
.as_bytes()
.get(3)
.is_some_and(|b| (*b as char).is_whitespace())
{
builder.token(SyntaxKind::TASK_CHECKBOX.into(), &rest[..3]);
rest[3..].to_string()
} else {
rest.to_string()
}
} else {
String::new()
};
(content_col, text_to_buffer)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::options::ParserOptions;
#[test]
fn detects_bullet_markers() {
let config = ParserOptions::default();
assert!(try_parse_list_marker("* item", &config).is_some());
assert!(try_parse_list_marker("*\titem", &config).is_some());
}
#[test]
fn detects_fancy_alpha_markers() {
let mut config = ParserOptions::default();
config.extensions.fancy_lists = true;
assert!(
try_parse_list_marker("a. item", &config).is_some(),
"a. should parse"
);
assert!(
try_parse_list_marker("b. item", &config).is_some(),
"b. should parse"
);
assert!(
try_parse_list_marker("c. item", &config).is_some(),
"c. should parse"
);
assert!(
try_parse_list_marker("a) item", &config).is_some(),
"a) should parse"
);
assert!(
try_parse_list_marker("b) item", &config).is_some(),
"b) should parse"
);
}
}
#[test]
fn markers_match_fancy_lists() {
use ListDelimiter::*;
use ListMarker::*;
use OrderedMarker::*;
let a_period = Ordered(LowerAlpha {
letter: 'a',
style: Period,
});
let b_period = Ordered(LowerAlpha {
letter: 'b',
style: Period,
});
assert!(
markers_match(&a_period, &b_period),
"a. and b. should match"
);
let i_period = Ordered(LowerRoman {
numeral: "i".to_string(),
style: Period,
});
let ii_period = Ordered(LowerRoman {
numeral: "ii".to_string(),
style: Period,
});
assert!(
markers_match(&i_period, &ii_period),
"i. and ii. should match"
);
let a_paren = Ordered(LowerAlpha {
letter: 'a',
style: RightParen,
});
assert!(
!markers_match(&a_period, &a_paren),
"a. and a) should not match"
);
}
#[test]
fn detects_complex_roman_numerals() {
let mut config = ParserOptions::default();
config.extensions.fancy_lists = true;
assert!(
try_parse_list_marker("iv. item", &config).is_some(),
"iv. should parse"
);
assert!(
try_parse_list_marker("v. item", &config).is_some(),
"v. should parse"
);
assert!(
try_parse_list_marker("vi. item", &config).is_some(),
"vi. should parse"
);
assert!(
try_parse_list_marker("vii. item", &config).is_some(),
"vii. should parse"
);
assert!(
try_parse_list_marker("viii. item", &config).is_some(),
"viii. should parse"
);
assert!(
try_parse_list_marker("ix. item", &config).is_some(),
"ix. should parse"
);
assert!(
try_parse_list_marker("x. item", &config).is_some(),
"x. should parse"
);
}
#[test]
fn detects_example_list_markers() {
let mut config = ParserOptions::default();
config.extensions.example_lists = true;
assert!(
try_parse_list_marker("(@) item", &config).is_some(),
"(@) should parse"
);
assert!(
try_parse_list_marker("(@foo) item", &config).is_some(),
"(@foo) should parse"
);
assert!(
try_parse_list_marker("(@my_label) item", &config).is_some(),
"(@my_label) should parse"
);
assert!(
try_parse_list_marker("(@test-123) item", &config).is_some(),
"(@test-123) should parse"
);
let disabled_config = ParserOptions {
extensions: crate::options::Extensions {
example_lists: false,
..Default::default()
},
..Default::default()
};
assert!(
try_parse_list_marker("(@) item", &disabled_config).is_none(),
"(@) should not parse when extension disabled"
);
}
#[test]
fn deep_ordered_prefers_nearest_enclosing_indent_over_nearest_below() {
use crate::parser::utils::container_stack::{Container, ContainerStack};
let marker = ListMarker::Ordered(OrderedMarker::LowerRoman {
numeral: "ii".to_string(),
style: ListDelimiter::Period,
});
let mut containers = ContainerStack::new();
containers.push(Container::List {
marker: marker.clone(),
base_indent_cols: 8,
has_blank_between_items: false,
});
containers.push(Container::ListItem {
content_col: 11,
buffer: crate::parser::utils::list_item_buffer::ListItemBuffer::new(),
});
containers.push(Container::List {
marker,
base_indent_cols: 6,
has_blank_between_items: false,
});
assert_eq!(
find_matching_list_level(
&containers,
&ListMarker::Ordered(OrderedMarker::LowerRoman {
numeral: "iii".to_string(),
style: ListDelimiter::Period,
}),
7
),
Some(0)
);
}
#[test]
fn deep_ordered_matches_exact_indent_when_available() {
use crate::parser::utils::container_stack::{Container, ContainerStack};
let marker = ListMarker::Ordered(OrderedMarker::LowerRoman {
numeral: "ii".to_string(),
style: ListDelimiter::Period,
});
let mut containers = ContainerStack::new();
containers.push(Container::List {
marker: marker.clone(),
base_indent_cols: 8,
has_blank_between_items: false,
});
containers.push(Container::List {
marker,
base_indent_cols: 6,
has_blank_between_items: false,
});
assert_eq!(
find_matching_list_level(
&containers,
&ListMarker::Ordered(OrderedMarker::LowerRoman {
numeral: "iii".to_string(),
style: ListDelimiter::Period,
}),
6
),
Some(1)
);
}
#[test]
fn parses_nested_bullet_list_from_single_marker() {
use crate::parse;
use crate::syntax::SyntaxKind;
let config = ParserOptions::default();
for (input, desc) in [("- *\n", "- *"), ("- +\n", "- +"), ("- -\n", "- -")] {
let tree = parse(input, Some(config.clone()));
assert_eq!(
tree.kind(),
SyntaxKind::DOCUMENT,
"{desc}: root should be DOCUMENT"
);
let outer_list = tree
.children()
.find(|n| n.kind() == SyntaxKind::LIST)
.unwrap_or_else(|| panic!("{desc}: should have outer LIST node"));
let outer_item = outer_list
.children()
.find(|n| n.kind() == SyntaxKind::LIST_ITEM)
.unwrap_or_else(|| panic!("{desc}: should have outer LIST_ITEM"));
let nested_list = outer_item
.children()
.find(|n| n.kind() == SyntaxKind::LIST)
.unwrap_or_else(|| {
panic!(
"{desc}: outer LIST_ITEM should contain nested LIST, got: {:?}",
outer_item.children().map(|n| n.kind()).collect::<Vec<_>>()
)
});
let nested_item = nested_list
.children()
.find(|n| n.kind() == SyntaxKind::LIST_ITEM)
.unwrap_or_else(|| panic!("{desc}: nested LIST should have LIST_ITEM"));
let has_plain = nested_item
.children()
.any(|n| n.kind() == SyntaxKind::PLAIN);
assert!(
!has_plain,
"{desc}: nested LIST_ITEM should not have PLAIN node (should be empty)"
);
}
}
pub(in crate::parser) fn in_list(containers: &ContainerStack) -> bool {
containers
.stack
.iter()
.any(|c| matches!(c, Container::List { .. }))
}
pub(in crate::parser) fn in_blockquote_list(containers: &ContainerStack) -> bool {
let mut seen_blockquote = false;
for c in &containers.stack {
if matches!(c, Container::BlockQuote { .. }) {
seen_blockquote = true;
}
if seen_blockquote && matches!(c, Container::List { .. }) {
return true;
}
}
false
}
pub(in crate::parser) fn find_matching_list_level(
containers: &ContainerStack,
marker: &ListMarker,
indent_cols: usize,
) -> Option<usize> {
let mut best_match: Option<(usize, usize, bool)> = None;
let is_deep_ordered = matches!(marker, ListMarker::Ordered(_)) && indent_cols >= 4;
let mut best_above_match: Option<(usize, usize)> = None;
for (i, c) in containers.stack.iter().enumerate().rev() {
if let Container::List {
marker: list_marker,
base_indent_cols,
..
} = c
&& markers_match(marker, list_marker)
{
let matches = if indent_cols >= 4 && *base_indent_cols >= 4 {
match (marker, list_marker) {
(ListMarker::Ordered(_), ListMarker::Ordered(_)) => {
indent_cols.abs_diff(*base_indent_cols) <= 3
}
_ => indent_cols >= *base_indent_cols && indent_cols <= base_indent_cols + 3,
}
} else if indent_cols >= 4 || *base_indent_cols >= 4 {
match (marker, list_marker) {
(ListMarker::Ordered(_), ListMarker::Ordered(_)) => {
indent_cols.abs_diff(*base_indent_cols) <= 3
}
_ => false,
}
} else {
indent_cols.abs_diff(*base_indent_cols) <= 3
};
if matches {
let distance = indent_cols.abs_diff(*base_indent_cols);
let base_leq_indent = *base_indent_cols <= indent_cols;
if is_deep_ordered
&& matches!(
(marker, list_marker),
(ListMarker::Ordered(_), ListMarker::Ordered(_))
)
&& *base_indent_cols >= indent_cols
{
let delta = *base_indent_cols - indent_cols;
if best_above_match.is_none_or(|(_, best_delta)| delta < best_delta) {
best_above_match = Some((i, delta));
}
}
if let Some((_, best_dist, best_base_leq)) = best_match {
if distance < best_dist
|| (distance == best_dist && base_leq_indent && !best_base_leq)
{
best_match = Some((i, distance, base_leq_indent));
}
} else {
best_match = Some((i, distance, base_leq_indent));
}
if distance == 0 {
return Some(i);
}
}
}
}
if let Some((index, _)) = best_above_match {
return Some(index);
}
best_match.map(|(i, _, _)| i)
}
pub(in crate::parser) fn start_nested_list(
containers: &mut ContainerStack,
builder: &mut GreenNodeBuilder<'static>,
marker: &ListMarker,
item: &ListItemEmissionInput<'_>,
indent_to_emit: Option<&str>,
) {
if let Some(indent_str) = indent_to_emit {
builder.token(SyntaxKind::WHITESPACE.into(), indent_str);
}
builder.start_node(SyntaxKind::LIST.into());
containers.push(Container::List {
marker: marker.clone(),
base_indent_cols: item.indent_cols,
has_blank_between_items: false,
});
let (content_col, text_to_buffer) = emit_list_item(builder, item);
let mut buffer = ListItemBuffer::new();
if !text_to_buffer.is_empty() {
buffer.push_text(text_to_buffer);
}
containers.push(Container::ListItem {
content_col,
buffer,
});
}
pub(in crate::parser) fn is_content_nested_bullet_marker(
content: &str,
marker_len: usize,
spaces_after_bytes: usize,
) -> Option<char> {
let (_, indent_bytes) = leading_indent(content);
let content_start = indent_bytes + marker_len + spaces_after_bytes;
if content_start >= content.len() {
return None;
}
let remaining = &content[content_start..];
let (text_part, _) = strip_newline(remaining);
let trimmed = text_part.trim();
if trimmed.len() == 1 {
let ch = trimmed.chars().next().unwrap();
if matches!(ch, '*' | '+' | '-') {
return Some(ch);
}
}
None
}
pub(in crate::parser) fn add_list_item_with_nested_empty_list(
containers: &mut ContainerStack,
builder: &mut GreenNodeBuilder<'static>,
item: &ListItemEmissionInput<'_>,
nested_marker: char,
) {
builder.start_node(SyntaxKind::LIST_ITEM.into());
if item.indent_bytes > 0 {
builder.token(
SyntaxKind::WHITESPACE.into(),
&item.content[..item.indent_bytes],
);
}
let marker_text = &item.content[item.indent_bytes..item.indent_bytes + item.marker_len];
builder.token(SyntaxKind::LIST_MARKER.into(), marker_text);
if item.spaces_after_bytes > 0 {
let space_start = item.indent_bytes + item.marker_len;
let space_end = space_start + item.spaces_after_bytes;
if space_end <= item.content.len() {
builder.token(
SyntaxKind::WHITESPACE.into(),
&item.content[space_start..space_end],
);
}
}
builder.start_node(SyntaxKind::LIST.into());
builder.start_node(SyntaxKind::LIST_ITEM.into());
builder.token(SyntaxKind::LIST_MARKER.into(), &nested_marker.to_string());
let content_start = item.indent_bytes + item.marker_len + item.spaces_after_bytes;
if content_start < item.content.len() {
let remaining = &item.content[content_start..];
if remaining.len() > 1 {
let (_, newline_str) = strip_newline(&remaining[1..]);
if !newline_str.is_empty() {
builder.token(SyntaxKind::NEWLINE.into(), newline_str);
}
}
}
builder.finish_node(); builder.finish_node();
let content_col = item.indent_cols + item.marker_len + item.spaces_after_cols;
containers.push(Container::ListItem {
content_col,
buffer: ListItemBuffer::new(),
});
}
pub(in crate::parser) fn add_list_item(
containers: &mut ContainerStack,
builder: &mut GreenNodeBuilder<'static>,
item: &ListItemEmissionInput<'_>,
) {
let (content_col, text_to_buffer) = emit_list_item(builder, item);
log::debug!(
"add_list_item: content={:?}, text_to_buffer={:?}",
item.content,
text_to_buffer
);
let mut buffer = ListItemBuffer::new();
if !text_to_buffer.is_empty() {
buffer.push_text(text_to_buffer);
}
containers.push(Container::ListItem {
content_col,
buffer,
});
}