diff --git a/crates/biome_markdown_parser/src/parser.rs b/crates/biome_markdown_parser/src/parser.rs index 7f4348e6372b..a1ba88d682cc 100644 --- a/crates/biome_markdown_parser/src/parser.rs +++ b/crates/biome_markdown_parser/src/parser.rs @@ -63,6 +63,12 @@ pub(crate) struct MarkdownParserState { /// Indentation column where the current list marker starts. /// Used to detect sibling list items after blank lines. pub(crate) list_item_marker_indent: usize, + /// The bullet marker kind of the parent list (e.g. `-`, `*`, `+`). + /// Used to detect marker changes at blank-line boundaries. + pub(crate) list_item_marker_kind: Option, + /// The ordered list delimiter of the parent list (`.` or `)`). + /// Used to detect delimiter changes at blank-line boundaries. + pub(crate) list_item_ordered_delim: Option, /// Emphasis parsing context for the current inline item list. pub(crate) emphasis_context: Option, /// Normalized link reference definitions collected in a prepass. diff --git a/crates/biome_markdown_parser/src/syntax/list.rs b/crates/biome_markdown_parser/src/syntax/list.rs index a254e8ca71c1..1282cb9b71e0 100644 --- a/crates/biome_markdown_parser/src/syntax/list.rs +++ b/crates/biome_markdown_parser/src/syntax/list.rs @@ -486,7 +486,16 @@ impl ParseNodeList for BulletList { const LIST_KIND: Self::Kind = MD_BULLET_LIST; fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { - parse_list_element_common( + // Detect marker before setting state so first-item content parsing + // knows its parent marker kind for boundary detection. + if self.marker_kind.is_none() { + self.marker_kind = current_bullet_marker(p); + } + let prev_marker_kind = p.state().list_item_marker_kind; + let prev_ordered_delim = p.state().list_item_ordered_delim; + p.state_mut().list_item_marker_kind = self.marker_kind; + p.state_mut().list_item_ordered_delim = None; + let result = parse_list_element_common( p, &mut self.marker_kind, current_bullet_marker, @@ -494,7 +503,10 @@ impl ParseNodeList for BulletList { |p| has_bullet_item_after_blank_lines_at_indent(p, self.marker_indent), &mut self.is_tight, &mut self.last_item_ends_with_blank, - ) + ); + p.state_mut().list_item_marker_kind = prev_marker_kind; + p.state_mut().list_item_ordered_delim = prev_ordered_delim; + result } fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { @@ -651,6 +663,30 @@ fn marker_changes_after_blank_lines( matches!(next, Some(next) if current != next) } +/// Check if the ordered delimiter after blank lines differs from the current list's delimiter. +fn delim_changes_after_blank_lines(p: &mut MarkdownParser, marker_delim: Option) -> bool { + let Some(current) = marker_delim else { + return false; + }; + let next = p.lookahead(|p| { + // Skip blank lines (same pattern as marker_changes_after_blank_lines) + loop { + if !p.at(NEWLINE) { + break; + } + p.bump(NEWLINE); + while p.at(MD_TEXTUAL_LITERAL) && p.cur_text().chars().all(|c| c == ' ' || c == '\t') { + p.bump(MD_TEXTUAL_LITERAL); + } + if !p.at(NEWLINE) { + break; + } + } + current_ordered_delim(p) + }); + matches!(next, Some(next) if current != next) +} + /// Error builder for bullet list recovery fn expected_bullet(p: &MarkdownParser, range: TextRange) -> ParseDiagnostic { p.err_builder("Expected a list item", range) @@ -896,7 +932,16 @@ impl ParseNodeList for OrderedList { const LIST_KIND: Self::Kind = MD_BULLET_LIST; fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { - parse_list_element_common( + // Detect delimiter before setting state so first-item content parsing + // knows its parent ordered delim for boundary detection. + if self.marker_delim.is_none() { + self.marker_delim = current_ordered_delim(p); + } + let prev_marker_kind = p.state().list_item_marker_kind; + let prev_ordered_delim = p.state().list_item_ordered_delim; + p.state_mut().list_item_marker_kind = None; + p.state_mut().list_item_ordered_delim = self.marker_delim; + let result = parse_list_element_common( p, &mut self.marker_delim, current_ordered_delim, @@ -904,14 +949,28 @@ impl ParseNodeList for OrderedList { |p| has_ordered_item_after_blank_lines_at_indent(p, self.marker_indent), &mut self.is_tight, &mut self.last_item_ends_with_blank, - ) + ); + p.state_mut().list_item_marker_kind = prev_marker_kind; + p.state_mut().list_item_ordered_delim = prev_ordered_delim; + result } fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { let marker_indent = self.marker_indent; + let marker_delim = self.marker_delim; if p.at_line_start() && at_blank_line_start(p) { - return !has_ordered_item_after_blank_lines_at_indent(p, marker_indent); + // Check if there's an ordered item after blank lines + let has_item = has_ordered_item_after_blank_lines_at_indent(p, marker_indent); + if has_item { + // Per CommonMark §5.3, a delimiter change across blank lines + // starts a new list. + if delim_changes_after_blank_lines(p, marker_delim) { + return true; + } + return false; + } + return true; } is_at_list_end_common( @@ -926,18 +985,27 @@ impl ParseNodeList for OrderedList { at_order_list_item_with_base_indent(p, marker_indent) }); if next_is_ordered { - if let (Some(current_delim), Some(next_delim)) = - (marker_delim, current_ordered_delim(p)) + // Check delimiter after bumping past the NEWLINE + let next_delim = p.lookahead(|p| { + p.bump(NEWLINE); + current_ordered_delim(p) + }); + if let (Some(current_delim), Some(next_delim)) = (marker_delim, next_delim) && current_delim != next_delim { return Some(true); } return Some(false); } - Some(!has_ordered_item_after_blank_lines_at_indent( - p, - marker_indent, - )) + // Check if there's an ordered item after blank lines + let has_item = has_ordered_item_after_blank_lines_at_indent(p, marker_indent); + if has_item { + if delim_changes_after_blank_lines(p, marker_delim) { + return Some(true); + } + return Some(false); + } + Some(true) }, ) } @@ -1369,6 +1437,8 @@ struct ListItemLoopState { first_line: bool, required_indent: usize, marker_indent: usize, + parent_marker_kind: Option, + parent_ordered_delim: Option, } impl ListItemLoopState { @@ -1380,6 +1450,8 @@ impl ListItemLoopState { first_line: true, required_indent: p.state().list_item_required_indent, marker_indent: p.state().list_item_marker_indent, + parent_marker_kind: p.state().list_item_marker_kind, + parent_ordered_delim: p.state().list_item_ordered_delim, } } @@ -1523,7 +1595,13 @@ fn blank_line_phase_non_quote_classify( return None; } - let action = classify_blank_line(p, state.required_indent, state.marker_indent); + let action = classify_blank_line( + p, + state.required_indent, + state.marker_indent, + state.parent_marker_kind, + state.parent_ordered_delim, + ); let is_blank = list_newline_is_blank_line(p); let result = apply_blank_line_action(p, state, action, is_blank); Some(BlankLineOutcome::resolved(result)) @@ -1613,9 +1691,22 @@ fn blank_line_phase_after_prefix( } let marker_line_break = state.first_line; let action = if quote_depth > 0 { - classify_blank_line_in_quote(p, state.required_indent, state.marker_indent, quote_depth) + classify_blank_line_in_quote( + p, + state.required_indent, + state.marker_indent, + quote_depth, + state.parent_marker_kind, + state.parent_ordered_delim, + ) } else { - classify_blank_line(p, state.required_indent, state.marker_indent) + classify_blank_line( + p, + state.required_indent, + state.marker_indent, + state.parent_marker_kind, + state.parent_ordered_delim, + ) }; let result = apply_blank_line_action_with_prefix( p, @@ -2172,7 +2263,50 @@ fn parse_continuation_block( ) { let is_blank_line = p.at_blank_line(); if is_blank_line { - state.record_blank(); + // Don't record as blank if the blank line is actually the boundary + // before a different-marker list (CommonMark §5.3). The blank line + // belongs to the inter-list gap, not this item. + let is_marker_boundary = is_blank_line + && (state.parent_marker_kind.is_some() || state.parent_ordered_delim.is_some()) + && { + let mk = state.parent_marker_kind; + let od = state.parent_ordered_delim; + let mi = state.marker_indent; + p.lookahead(|p| { + // Skip blank lines (including whitespace-only tokens between newlines) + while p.at_blank_line() { + p.bump(NEWLINE); + while p.at(MD_TEXTUAL_LITERAL) && is_whitespace_only(p.cur_text()) { + p.bump(MD_TEXTUAL_LITERAL); + } + } + let next_is_bullet = at_bullet_list_item_with_base_indent(p, mi); + let next_is_ordered = at_order_list_item_with_base_indent(p, mi); + if let Some(cur) = mk { + // Parent is bullet list + if next_is_ordered { + return true; // bullet → ordered + } + if next_is_bullet { + let next = current_bullet_marker(p); + return matches!(next, Some(nxt) if cur != nxt); + } + } else if let Some(cur_delim) = od { + // Parent is ordered list + if next_is_bullet { + return true; // ordered → bullet + } + if next_is_ordered { + let next_delim = current_ordered_delim(p); + return matches!(next_delim, Some(nxt) if cur_delim != nxt); + } + } + false + }) + }; + if !is_marker_boundary { + state.record_blank(); + } } else { state.last_was_blank = false; } @@ -2186,7 +2320,8 @@ fn parse_continuation_block( } else { false }; - if p.take_last_list_ends_with_blank() { + let last_list_blank = p.take_last_list_ends_with_blank(); + if last_list_blank { state.has_blank_line = true; state.last_was_blank = true; } @@ -2363,6 +2498,8 @@ fn classify_blank_line( p: &mut MarkdownParser, required_indent: usize, marker_indent: usize, + parent_marker_kind: Option, + parent_ordered_delim: Option, ) -> BlankLineAction { p.lookahead(|p| { // Skip ALL consecutive blank lines (not just one). @@ -2416,10 +2553,34 @@ fn classify_blank_line( } // If next non-blank line starts a new list item, this is a blank line between items. - if indent <= marker_indent + MAX_BLOCK_PREFIX_INDENT - && (at_bullet_list_item_with_base_indent(p, marker_indent) - || at_order_list_item_with_base_indent(p, marker_indent)) + let next_is_bullet = at_bullet_list_item_with_base_indent(p, marker_indent); + let next_is_ordered = at_order_list_item_with_base_indent(p, marker_indent); + if indent <= marker_indent + MAX_BLOCK_PREFIX_INDENT && (next_is_bullet || next_is_ordered) { + // Per CommonMark §5.3, a marker/type change means a new list starts. + // The blank line is a list boundary, not an item boundary — don't + // count it toward looseness of the current list. + if let Some(current) = parent_marker_kind { + // Parent is a bullet list + if next_is_ordered { + // Bullet → ordered: different list type + return BlankLineAction::EndItemBeforeBlank; + } + let next = current_bullet_marker(p); + if matches!(next, Some(next) if current != next) { + return BlankLineAction::EndItemBeforeBlank; + } + } else if let Some(current_delim) = parent_ordered_delim { + // Parent is an ordered list + if next_is_bullet { + // Ordered → bullet: different list type + return BlankLineAction::EndItemBeforeBlank; + } + let next_delim = current_ordered_delim(p); + if matches!(next_delim, Some(next) if current_delim != next) { + return BlankLineAction::EndItemBeforeBlank; + } + } // The first "blank line" is just the item-ending newline. // Only report actual blank lines if more than 1 was found. if blank_lines_found > 1 { @@ -2437,6 +2598,8 @@ fn classify_blank_line_in_quote( required_indent: usize, marker_indent: usize, quote_depth: usize, + parent_marker_kind: Option, + parent_ordered_delim: Option, ) -> BlankLineAction { p.lookahead(|p| { loop { @@ -2500,6 +2663,31 @@ fn classify_blank_line_in_quote( } if indent <= marker_indent + MAX_BLOCK_PREFIX_INDENT { + let next_is_bullet = at_bullet_list_item_with_base_indent(p, marker_indent); + let next_is_ordered = at_order_list_item_with_base_indent(p, marker_indent); + + if next_is_bullet || next_is_ordered { + // Per CommonMark §5.3, a marker/type change means a new list starts. + if let Some(current) = parent_marker_kind { + if next_is_ordered { + return BlankLineAction::EndItemBeforeBlank; + } + let next = current_bullet_marker(p); + if matches!(next, Some(next) if current != next) { + return BlankLineAction::EndItemBeforeBlank; + } + } else if let Some(current_delim) = parent_ordered_delim { + if next_is_bullet { + return BlankLineAction::EndItemBeforeBlank; + } + let next_delim = current_ordered_delim(p); + if matches!(next_delim, Some(next) if current_delim != next) { + return BlankLineAction::EndItemBeforeBlank; + } + } + return BlankLineAction::EndItemAfterBlank; + } + let is_list_marker = p.lookahead(|p| { skip_leading_whitespace_tokens(p); diff --git a/crates/biome_markdown_parser/src/to_html.rs b/crates/biome_markdown_parser/src/to_html.rs index b974f6f2dc3e..613118d0b3ca 100644 --- a/crates/biome_markdown_parser/src/to_html.rs +++ b/crates/biome_markdown_parser/src/to_html.rs @@ -2006,4 +2006,84 @@ mod tests { ); assert_eq!(html, "

foo\\

\n"); } + + #[test] + fn test_tight_list_marker_split() { + // Two tight lists separated by blank line with different markers + let input = "- foo\n- bar\n\n* baz\n"; + let parsed = parse_markdown(input); + let html = document_to_html( + &parsed.tree(), + parsed.list_tightness(), + parsed.list_item_indents(), + parsed.quote_indents(), + ); + assert_eq!( + html, + "
    \n
  • foo
  • \n
  • bar
  • \n
\n
    \n
  • baz
  • \n
\n" + ); + } + + #[test] + fn test_tight_list_basic() { + let input = "- foo\n- bar\n"; + let parsed = parse_markdown(input); + let html = document_to_html( + &parsed.tree(), + parsed.list_tightness(), + parsed.list_item_indents(), + parsed.quote_indents(), + ); + assert_eq!(html, "
    \n
  • foo
  • \n
  • bar
  • \n
\n"); + } + + #[test] + fn test_loose_list_same_marker() { + let input = "- foo\n\n- bar\n"; + let parsed = parse_markdown(input); + let html = document_to_html( + &parsed.tree(), + parsed.list_tightness(), + parsed.list_item_indents(), + parsed.quote_indents(), + ); + assert_eq!( + html, + "
    \n
  • \n

    foo

    \n
  • \n
  • \n

    bar

    \n
  • \n
\n" + ); + } + + #[test] + fn test_ordered_delim_split_tight() { + // Different ordered delimiters across blank line → separate tight lists + let input = "1. First\n2. Second\n\n1) Third\n2) Fourth\n"; + let parsed = parse_markdown(input); + let html = document_to_html( + &parsed.tree(), + parsed.list_tightness(), + parsed.list_item_indents(), + parsed.quote_indents(), + ); + assert_eq!( + html, + "
    \n
  1. First
  2. \n
  3. Second
  4. \n
\n
    \n
  1. Third
  2. \n
  3. Fourth
  4. \n
\n" + ); + } + + #[test] + fn test_cross_type_split_tight() { + // Bullet → ordered across blank line → separate tight lists + let input = "- bullet\n\n1. ordered\n"; + let parsed = parse_markdown(input); + let html = document_to_html( + &parsed.tree(), + parsed.list_tightness(), + parsed.list_item_indents(), + parsed.quote_indents(), + ); + assert_eq!( + html, + "
    \n
  • bullet
  • \n
\n
    \n
  1. ordered
  2. \n
\n" + ); + } } diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/blockquote_inside_list.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/blockquote_inside_list.md.snap index 2acffba8e45e..c0a3e8009a38 100644 --- a/crates/biome_markdown_parser/tests/md_test_suite/ok/blockquote_inside_list.md.snap +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/blockquote_inside_list.md.snap @@ -148,13 +148,13 @@ MdDocument { }, ], }, - MdNewline { - value_token: NEWLINE@54..55 "\n" [] [], - }, ], }, ], }, + MdNewline { + value_token: NEWLINE@54..55 "\n" [] [], + }, MdOrderedListItem { md_bullet_list: MdBulletList [ MdBullet { @@ -224,8 +224,8 @@ MdDocument { 0: MD_DOCUMENT@0..84 0: (empty) 1: MD_BLOCK_LIST@0..84 - 0: MD_BULLET_LIST_ITEM@0..55 - 0: MD_BULLET_LIST@0..55 + 0: MD_BULLET_LIST_ITEM@0..54 + 0: MD_BULLET_LIST@0..54 0: MD_BULLET@0..28 0: MD_LIST_MARKER_PREFIX@0..1 0: MD_INDENT_TOKEN_LIST@0..0 @@ -273,13 +273,13 @@ MdDocument { 1: (empty) 3: MD_NEWLINE@27..28 0: NEWLINE@27..28 "\n" [] [] - 1: MD_BULLET@28..55 + 1: MD_BULLET@28..54 0: MD_LIST_MARKER_PREFIX@28..29 0: MD_INDENT_TOKEN_LIST@28..28 1: MINUS@28..29 "-" [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@29..29 - 1: MD_BLOCK_LIST@29..55 + 1: MD_BLOCK_LIST@29..54 0: MD_PARAGRAPH@29..37 0: MD_INLINE_ITEM_LIST@29..37 0: MD_TEXTUAL@29..36 @@ -306,9 +306,9 @@ MdDocument { 1: MD_TEXTUAL@53..54 0: MD_TEXTUAL_LITERAL@53..54 "\n" [] [] 1: (empty) - 3: MD_NEWLINE@54..55 - 0: NEWLINE@54..55 "\n" [] [] - 1: MD_ORDERED_LIST_ITEM@55..84 + 1: MD_NEWLINE@54..55 + 0: NEWLINE@54..55 "\n" [] [] + 2: MD_ORDERED_LIST_ITEM@55..84 0: MD_BULLET_LIST@55..84 0: MD_BULLET@55..84 0: MD_LIST_MARKER_PREFIX@55..57 diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/bullet_list.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/bullet_list.md.snap index d7e4102fd135..cab7677d7efd 100644 --- a/crates/biome_markdown_parser/tests/md_test_suite/ok/bullet_list.md.snap +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/bullet_list.md.snap @@ -88,13 +88,13 @@ MdDocument { ], hard_line: missing (optional), }, - MdNewline { - value_token: NEWLINE@35..36 "\n" [] [], - }, ], }, ], }, + MdNewline { + value_token: NEWLINE@35..36 "\n" [] [], + }, MdBulletListItem { md_bullet_list: MdBulletList [ MdBullet { @@ -137,13 +137,13 @@ MdDocument { ], hard_line: missing (optional), }, - MdNewline { - value_token: NEWLINE@72..73 "\n" [] [], - }, ], }, ], }, + MdNewline { + value_token: NEWLINE@72..73 "\n" [] [], + }, MdBulletListItem { md_bullet_list: MdBulletList [ MdBullet { @@ -201,8 +201,8 @@ MdDocument { 0: MD_DOCUMENT@0..107 0: (empty) 1: MD_BLOCK_LIST@0..107 - 0: MD_BULLET_LIST_ITEM@0..36 - 0: MD_BULLET_LIST@0..36 + 0: MD_BULLET_LIST_ITEM@0..35 + 0: MD_BULLET_LIST@0..35 0: MD_BULLET@0..11 0: MD_LIST_MARKER_PREFIX@0..1 0: MD_INDENT_TOKEN_LIST@0..0 @@ -231,13 +231,13 @@ MdDocument { 1: MD_TEXTUAL@21..22 0: MD_TEXTUAL_LITERAL@21..22 "\n" [] [] 1: (empty) - 2: MD_BULLET@22..36 + 2: MD_BULLET@22..35 0: MD_LIST_MARKER_PREFIX@22..23 0: MD_INDENT_TOKEN_LIST@22..22 1: MINUS@22..23 "-" [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@23..23 - 1: MD_BLOCK_LIST@23..36 + 1: MD_BLOCK_LIST@23..35 0: MD_PARAGRAPH@23..35 0: MD_INLINE_ITEM_LIST@23..35 0: MD_TEXTUAL@23..34 @@ -245,10 +245,10 @@ MdDocument { 1: MD_TEXTUAL@34..35 0: MD_TEXTUAL_LITERAL@34..35 "\n" [] [] 1: (empty) - 1: MD_NEWLINE@35..36 - 0: NEWLINE@35..36 "\n" [] [] - 1: MD_BULLET_LIST_ITEM@36..73 - 0: MD_BULLET_LIST@36..73 + 1: MD_NEWLINE@35..36 + 0: NEWLINE@35..36 "\n" [] [] + 2: MD_BULLET_LIST_ITEM@36..72 + 0: MD_BULLET_LIST@36..72 0: MD_BULLET@36..57 0: MD_LIST_MARKER_PREFIX@36..37 0: MD_INDENT_TOKEN_LIST@36..36 @@ -263,13 +263,13 @@ MdDocument { 1: MD_TEXTUAL@56..57 0: MD_TEXTUAL_LITERAL@56..57 "\n" [] [] 1: (empty) - 1: MD_BULLET@57..73 + 1: MD_BULLET@57..72 0: MD_LIST_MARKER_PREFIX@57..58 0: MD_INDENT_TOKEN_LIST@57..57 1: STAR@57..58 "*" [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@58..58 - 1: MD_BLOCK_LIST@58..73 + 1: MD_BLOCK_LIST@58..72 0: MD_PARAGRAPH@58..72 0: MD_INLINE_ITEM_LIST@58..72 0: MD_TEXTUAL@58..71 @@ -277,9 +277,9 @@ MdDocument { 1: MD_TEXTUAL@71..72 0: MD_TEXTUAL_LITERAL@71..72 "\n" [] [] 1: (empty) - 1: MD_NEWLINE@72..73 - 0: NEWLINE@72..73 "\n" [] [] - 2: MD_BULLET_LIST_ITEM@73..107 + 3: MD_NEWLINE@72..73 + 0: NEWLINE@72..73 "\n" [] [] + 4: MD_BULLET_LIST_ITEM@73..107 0: MD_BULLET_LIST@73..107 0: MD_BULLET@73..87 0: MD_LIST_MARKER_PREFIX@73..74 diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/header_in_list.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/header_in_list.md.snap index 8655ba529d32..ebee4cdf6dc8 100644 --- a/crates/biome_markdown_parser/tests/md_test_suite/ok/header_in_list.md.snap +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/header_in_list.md.snap @@ -142,18 +142,14 @@ MdDocument { }, after: MdHashList [], }, - MdNewline { - value_token: NEWLINE@39..40 "\n" [] [], - }, ], }, - ], - }, - MdNewline { - value_token: NEWLINE@40..41 "\n" [] [], - }, - MdBulletListItem { - md_bullet_list: MdBulletList [ + MdNewline { + value_token: NEWLINE@39..40 "\n" [] [], + }, + MdNewline { + value_token: NEWLINE@40..41 "\n" [] [], + }, MdBullet { prefix: MdListMarkerPrefix { pre_marker_indent: MdIndentTokenList [], @@ -184,13 +180,9 @@ MdDocument { }, ], }, - ], - }, - MdNewline { - value_token: NEWLINE@55..56 "\n" [] [], - }, - MdBulletListItem { - md_bullet_list: MdBulletList [ + MdNewline { + value_token: NEWLINE@55..56 "\n" [] [], + }, MdBullet { prefix: MdListMarkerPrefix { pre_marker_indent: MdIndentTokenList [], @@ -253,13 +245,13 @@ MdDocument { }, ], }, - MdNewline { - value_token: NEWLINE@93..94 "\n" [] [], - }, ], }, ], }, + MdNewline { + value_token: NEWLINE@93..94 "\n" [] [], + }, MdNewline { value_token: NEWLINE@94..95 "\n" [] [], }, @@ -338,8 +330,8 @@ MdDocument { 0: MD_DOCUMENT@0..138 0: (empty) 1: MD_BLOCK_LIST@0..138 - 0: MD_BULLET_LIST_ITEM@0..40 - 0: MD_BULLET_LIST@0..40 + 0: MD_BULLET_LIST_ITEM@0..93 + 0: MD_BULLET_LIST@0..93 0: MD_BULLET@0..8 0: MD_LIST_MARKER_PREFIX@0..2 0: MD_INDENT_TOKEN_LIST@0..0 @@ -395,13 +387,13 @@ MdDocument { 1: (empty) 4: MD_NEWLINE@26..27 0: NEWLINE@26..27 "\n" [] [] - 2: MD_BULLET@27..40 + 2: MD_BULLET@27..39 0: MD_LIST_MARKER_PREFIX@27..29 0: MD_INDENT_TOKEN_LIST@27..27 1: MINUS@27..28 "-" [] [] 2: MD_LIST_POST_MARKER_SPACE@28..29 " " [] [] 3: MD_INDENT_TOKEN_LIST@29..29 - 1: MD_BLOCK_LIST@29..40 + 1: MD_BLOCK_LIST@29..39 0: MD_HEADER@29..39 0: MD_INDENT_TOKEN_LIST@29..29 1: MD_HASH_LIST@29..31 @@ -413,13 +405,11 @@ MdDocument { 0: MD_TEXTUAL_LITERAL@31..39 " Level 2" [] [] 1: (empty) 3: MD_HASH_LIST@39..39 - 1: MD_NEWLINE@39..40 - 0: NEWLINE@39..40 "\n" [] [] - 1: MD_NEWLINE@40..41 - 0: NEWLINE@40..41 "\n" [] [] - 2: MD_BULLET_LIST_ITEM@41..55 - 0: MD_BULLET_LIST@41..55 - 0: MD_BULLET@41..55 + 3: MD_NEWLINE@39..40 + 0: NEWLINE@39..40 "\n" [] [] + 4: MD_NEWLINE@40..41 + 0: NEWLINE@40..41 "\n" [] [] + 5: MD_BULLET@41..55 0: MD_LIST_MARKER_PREFIX@41..43 0: MD_INDENT_TOKEN_LIST@41..41 1: STAR@41..42 "*" [] [] @@ -439,11 +429,9 @@ MdDocument { 3: MD_HASH_LIST@54..54 1: MD_NEWLINE@54..55 0: NEWLINE@54..55 "\n" [] [] - 3: MD_NEWLINE@55..56 - 0: NEWLINE@55..56 "\n" [] [] - 4: MD_BULLET_LIST_ITEM@56..94 - 0: MD_BULLET_LIST@56..94 - 0: MD_BULLET@56..73 + 6: MD_NEWLINE@55..56 + 0: NEWLINE@55..56 "\n" [] [] + 7: MD_BULLET@56..73 0: MD_LIST_MARKER_PREFIX@56..58 0: MD_INDENT_TOKEN_LIST@56..56 1: MINUS@56..57 "-" [] [] @@ -463,15 +451,15 @@ MdDocument { 3: MD_HASH_LIST@72..72 1: MD_NEWLINE@72..73 0: NEWLINE@72..73 "\n" [] [] - 1: MD_NEWLINE@73..74 + 8: MD_NEWLINE@73..74 0: NEWLINE@73..74 "\n" [] [] - 2: MD_BULLET@74..94 + 9: MD_BULLET@74..93 0: MD_LIST_MARKER_PREFIX@74..76 0: MD_INDENT_TOKEN_LIST@74..74 1: MINUS@74..75 "-" [] [] 2: MD_LIST_POST_MARKER_SPACE@75..76 " " [] [] 3: MD_INDENT_TOKEN_LIST@76..76 - 1: MD_BLOCK_LIST@76..94 + 1: MD_BLOCK_LIST@76..93 0: MD_HEADER@76..93 0: MD_INDENT_TOKEN_LIST@76..76 1: MD_HASH_LIST@76..77 @@ -485,11 +473,11 @@ MdDocument { 3: MD_HASH_LIST@91..93 0: MD_HASH@91..93 0: HASH@91..93 "#" [Whitespace(" ")] [] - 1: MD_NEWLINE@93..94 - 0: NEWLINE@93..94 "\n" [] [] - 5: MD_NEWLINE@94..95 + 1: MD_NEWLINE@93..94 + 0: NEWLINE@93..94 "\n" [] [] + 2: MD_NEWLINE@94..95 0: NEWLINE@94..95 "\n" [] [] - 6: MD_ORDERED_LIST_ITEM@95..137 + 3: MD_ORDERED_LIST_ITEM@95..137 0: MD_BULLET_LIST@95..137 0: MD_BULLET@95..116 0: MD_LIST_MARKER_PREFIX@95..98 @@ -529,7 +517,7 @@ MdDocument { 0: MD_TEXTUAL_LITERAL@121..137 " Ordered level 2" [] [] 1: (empty) 3: MD_HASH_LIST@137..137 - 7: MD_NEWLINE@137..138 + 4: MD_NEWLINE@137..138 0: NEWLINE@137..138 "\n" [] [] 2: EOF@138..138 "" [] [] diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/list_marker_trailing_spaces.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/list_marker_trailing_spaces.md.snap index efe81a3f1f30..e8cbbc229230 100644 --- a/crates/biome_markdown_parser/tests/md_test_suite/ok/list_marker_trailing_spaces.md.snap +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/list_marker_trailing_spaces.md.snap @@ -98,13 +98,13 @@ MdDocument { ], hard_line: missing (optional), }, - MdNewline { - value_token: NEWLINE@22..23 "\n" [] [], - }, ], }, ], }, + MdNewline { + value_token: NEWLINE@22..23 "\n" [] [], + }, MdBulletListItem { md_bullet_list: MdBulletList [ MdBullet { @@ -191,8 +191,8 @@ MdDocument { 0: MD_DOCUMENT@0..39 0: (empty) 1: MD_BLOCK_LIST@0..39 - 0: MD_ORDERED_LIST_ITEM@0..23 - 0: MD_BULLET_LIST@0..23 + 0: MD_ORDERED_LIST_ITEM@0..22 + 0: MD_BULLET_LIST@0..22 0: MD_BULLET@0..5 0: MD_LIST_MARKER_PREFIX@0..2 0: MD_INDENT_TOKEN_LIST@0..0 @@ -227,13 +227,13 @@ MdDocument { 1: MD_BLOCK_LIST@13..17 0: MD_NEWLINE@13..17 0: NEWLINE@13..17 " \n" [] [] - 3: MD_BULLET@17..23 + 3: MD_BULLET@17..22 0: MD_LIST_MARKER_PREFIX@17..19 0: MD_INDENT_TOKEN_LIST@17..17 1: MD_ORDERED_LIST_MARKER@17..19 "2." [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@19..19 - 1: MD_BLOCK_LIST@19..23 + 1: MD_BLOCK_LIST@19..22 0: MD_PARAGRAPH@19..22 0: MD_INLINE_ITEM_LIST@19..22 0: MD_TEXTUAL@19..21 @@ -241,9 +241,9 @@ MdDocument { 1: MD_TEXTUAL@21..22 0: MD_TEXTUAL_LITERAL@21..22 "\n" [] [] 1: (empty) - 1: MD_NEWLINE@22..23 - 0: NEWLINE@22..23 "\n" [] [] - 1: MD_BULLET_LIST_ITEM@23..39 + 1: MD_NEWLINE@22..23 + 0: NEWLINE@22..23 "\n" [] [] + 2: MD_BULLET_LIST_ITEM@23..39 0: MD_BULLET_LIST@23..39 0: MD_BULLET@23..28 0: MD_LIST_MARKER_PREFIX@23..27 diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/multiline_list.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/multiline_list.md.snap index 1d71ce638a3f..bf4fc92194a0 100644 --- a/crates/biome_markdown_parser/tests/md_test_suite/ok/multiline_list.md.snap +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/multiline_list.md.snap @@ -82,13 +82,13 @@ MdDocument { ], hard_line: missing (optional), }, - MdNewline { - value_token: NEWLINE@47..48 "\n" [] [], - }, ], }, ], }, + MdNewline { + value_token: NEWLINE@47..48 "\n" [] [], + }, MdOrderedListItem { md_bullet_list: MdBulletList [ MdBullet { @@ -146,13 +146,13 @@ MdDocument { ], hard_line: missing (optional), }, - MdNewline { - value_token: NEWLINE@98..99 "\n" [] [], - }, ], }, ], }, + MdNewline { + value_token: NEWLINE@98..99 "\n" [] [], + }, MdBulletListItem { md_bullet_list: MdBulletList [ MdBullet { @@ -270,8 +270,8 @@ MdDocument { 0: MD_DOCUMENT@0..165 0: (empty) 1: MD_BLOCK_LIST@0..165 - 0: MD_BULLET_LIST_ITEM@0..48 - 0: MD_BULLET_LIST@0..48 + 0: MD_BULLET_LIST_ITEM@0..47 + 0: MD_BULLET_LIST@0..47 0: MD_BULLET@0..33 0: MD_LIST_MARKER_PREFIX@0..1 0: MD_INDENT_TOKEN_LIST@0..0 @@ -294,13 +294,13 @@ MdDocument { 5: MD_TEXTUAL@32..33 0: MD_TEXTUAL_LITERAL@32..33 "\n" [] [] 1: (empty) - 1: MD_BULLET@33..48 + 1: MD_BULLET@33..47 0: MD_LIST_MARKER_PREFIX@33..34 0: MD_INDENT_TOKEN_LIST@33..33 1: MINUS@33..34 "-" [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@34..34 - 1: MD_BLOCK_LIST@34..48 + 1: MD_BLOCK_LIST@34..47 0: MD_PARAGRAPH@34..47 0: MD_INLINE_ITEM_LIST@34..47 0: MD_TEXTUAL@34..46 @@ -308,10 +308,10 @@ MdDocument { 1: MD_TEXTUAL@46..47 0: MD_TEXTUAL_LITERAL@46..47 "\n" [] [] 1: (empty) - 1: MD_NEWLINE@47..48 - 0: NEWLINE@47..48 "\n" [] [] - 1: MD_ORDERED_LIST_ITEM@48..99 - 0: MD_BULLET_LIST@48..99 + 1: MD_NEWLINE@47..48 + 0: NEWLINE@47..48 "\n" [] [] + 2: MD_ORDERED_LIST_ITEM@48..98 + 0: MD_BULLET_LIST@48..98 0: MD_BULLET@48..82 0: MD_LIST_MARKER_PREFIX@48..50 0: MD_INDENT_TOKEN_LIST@48..48 @@ -336,13 +336,13 @@ MdDocument { 6: MD_TEXTUAL@81..82 0: MD_TEXTUAL_LITERAL@81..82 "\n" [] [] 1: (empty) - 1: MD_BULLET@82..99 + 1: MD_BULLET@82..98 0: MD_LIST_MARKER_PREFIX@82..84 0: MD_INDENT_TOKEN_LIST@82..82 1: MD_ORDERED_LIST_MARKER@82..84 "2." [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@84..84 - 1: MD_BLOCK_LIST@84..99 + 1: MD_BLOCK_LIST@84..98 0: MD_PARAGRAPH@84..98 0: MD_INLINE_ITEM_LIST@84..98 0: MD_TEXTUAL@84..97 @@ -350,9 +350,9 @@ MdDocument { 1: MD_TEXTUAL@97..98 0: MD_TEXTUAL_LITERAL@97..98 "\n" [] [] 1: (empty) - 1: MD_NEWLINE@98..99 - 0: NEWLINE@98..99 "\n" [] [] - 2: MD_BULLET_LIST_ITEM@99..165 + 3: MD_NEWLINE@98..99 + 0: NEWLINE@98..99 "\n" [] [] + 4: MD_BULLET_LIST_ITEM@99..165 0: MD_BULLET_LIST@99..165 0: MD_BULLET@99..148 0: MD_LIST_MARKER_PREFIX@99..100 diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/ordered_list.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/ordered_list.md.snap index 34ed681b8244..02bfa1b8a334 100644 --- a/crates/biome_markdown_parser/tests/md_test_suite/ok/ordered_list.md.snap +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/ordered_list.md.snap @@ -85,13 +85,13 @@ MdDocument { ], hard_line: missing (optional), }, - MdNewline { - value_token: NEWLINE@43..44 "\n" [] [], - }, ], }, ], }, + MdNewline { + value_token: NEWLINE@43..44 "\n" [] [], + }, MdOrderedListItem { md_bullet_list: MdBulletList [ MdBullet { @@ -149,8 +149,8 @@ MdDocument { 0: MD_DOCUMENT@0..81 0: (empty) 1: MD_BLOCK_LIST@0..81 - 0: MD_ORDERED_LIST_ITEM@0..44 - 0: MD_BULLET_LIST@0..44 + 0: MD_ORDERED_LIST_ITEM@0..43 + 0: MD_BULLET_LIST@0..43 0: MD_BULLET@0..14 0: MD_LIST_MARKER_PREFIX@0..2 0: MD_INDENT_TOKEN_LIST@0..0 @@ -179,13 +179,13 @@ MdDocument { 1: MD_TEXTUAL@28..29 0: MD_TEXTUAL_LITERAL@28..29 "\n" [] [] 1: (empty) - 2: MD_BULLET@29..44 + 2: MD_BULLET@29..43 0: MD_LIST_MARKER_PREFIX@29..31 0: MD_INDENT_TOKEN_LIST@29..29 1: MD_ORDERED_LIST_MARKER@29..31 "3." [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@31..31 - 1: MD_BLOCK_LIST@31..44 + 1: MD_BLOCK_LIST@31..43 0: MD_PARAGRAPH@31..43 0: MD_INLINE_ITEM_LIST@31..43 0: MD_TEXTUAL@31..42 @@ -193,9 +193,9 @@ MdDocument { 1: MD_TEXTUAL@42..43 0: MD_TEXTUAL_LITERAL@42..43 "\n" [] [] 1: (empty) - 1: MD_NEWLINE@43..44 - 0: NEWLINE@43..44 "\n" [] [] - 1: MD_ORDERED_LIST_ITEM@44..81 + 1: MD_NEWLINE@43..44 + 0: NEWLINE@43..44 "\n" [] [] + 2: MD_ORDERED_LIST_ITEM@44..81 0: MD_BULLET_LIST@44..81 0: MD_BULLET@44..65 0: MD_LIST_MARKER_PREFIX@44..46 diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/thematic_break_in_list.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/thematic_break_in_list.md.snap index f32e6a51dbce..6c9002d13de3 100644 --- a/crates/biome_markdown_parser/tests/md_test_suite/ok/thematic_break_in_list.md.snap +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/thematic_break_in_list.md.snap @@ -118,18 +118,14 @@ MdDocument { }, ], }, - MdNewline { - value_token: NEWLINE@21..22 "\n" [] [], - }, ], }, - ], - }, - MdNewline { - value_token: NEWLINE@22..23 "\n" [] [], - }, - MdBulletListItem { - md_bullet_list: MdBulletList [ + MdNewline { + value_token: NEWLINE@21..22 "\n" [] [], + }, + MdNewline { + value_token: NEWLINE@22..23 "\n" [] [], + }, MdBullet { prefix: MdListMarkerPrefix { pre_marker_indent: MdIndentTokenList [], @@ -156,13 +152,9 @@ MdDocument { }, ], }, - ], - }, - MdNewline { - value_token: NEWLINE@29..30 "\n" [] [], - }, - MdBulletListItem { - md_bullet_list: MdBulletList [ + MdNewline { + value_token: NEWLINE@29..30 "\n" [] [], + }, MdBullet { prefix: MdListMarkerPrefix { pre_marker_indent: MdIndentTokenList [], @@ -348,15 +340,15 @@ MdDocument { 0: NEWLINE@14..15 "\n" [] [] 5: MD_NEWLINE@15..16 0: NEWLINE@15..16 "\n" [] [] - 6: MD_BULLET_LIST_ITEM@16..22 - 0: MD_BULLET_LIST@16..22 - 0: MD_BULLET@16..22 + 6: MD_BULLET_LIST_ITEM@16..62 + 0: MD_BULLET_LIST@16..62 + 0: MD_BULLET@16..21 0: MD_LIST_MARKER_PREFIX@16..18 0: MD_INDENT_TOKEN_LIST@16..16 1: MINUS@16..17 "-" [] [] 2: MD_LIST_POST_MARKER_SPACE@17..18 " " [] [] 3: MD_INDENT_TOKEN_LIST@18..18 - 1: MD_BLOCK_LIST@18..22 + 1: MD_BLOCK_LIST@18..21 0: MD_THEMATIC_BREAK_BLOCK@18..21 0: MD_THEMATIC_BREAK_PART_LIST@18..21 0: MD_THEMATIC_BREAK_CHAR@18..19 @@ -365,13 +357,11 @@ MdDocument { 0: UNDERSCORE@19..20 "_" [] [] 2: MD_THEMATIC_BREAK_CHAR@20..21 0: UNDERSCORE@20..21 "_" [] [] - 1: MD_NEWLINE@21..22 - 0: NEWLINE@21..22 "\n" [] [] - 7: MD_NEWLINE@22..23 - 0: NEWLINE@22..23 "\n" [] [] - 8: MD_BULLET_LIST_ITEM@23..29 - 0: MD_BULLET_LIST@23..29 - 0: MD_BULLET@23..29 + 1: MD_NEWLINE@21..22 + 0: NEWLINE@21..22 "\n" [] [] + 2: MD_NEWLINE@22..23 + 0: NEWLINE@22..23 "\n" [] [] + 3: MD_BULLET@23..29 0: MD_LIST_MARKER_PREFIX@23..25 0: MD_INDENT_TOKEN_LIST@23..23 1: STAR@23..24 "*" [] [] @@ -388,11 +378,9 @@ MdDocument { 0: MINUS@27..28 "-" [] [] 1: MD_NEWLINE@28..29 0: NEWLINE@28..29 "\n" [] [] - 9: MD_NEWLINE@29..30 - 0: NEWLINE@29..30 "\n" [] [] - 10: MD_BULLET_LIST_ITEM@30..62 - 0: MD_BULLET_LIST@30..62 - 0: MD_BULLET@30..37 + 4: MD_NEWLINE@29..30 + 0: NEWLINE@29..30 "\n" [] [] + 5: MD_BULLET@30..37 0: MD_LIST_MARKER_PREFIX@30..32 0: MD_INDENT_TOKEN_LIST@30..30 1: MINUS@30..31 "-" [] [] @@ -411,9 +399,9 @@ MdDocument { 0: STAR@35..36 "*" [] [] 1: MD_NEWLINE@36..37 0: NEWLINE@36..37 "\n" [] [] - 1: MD_NEWLINE@37..38 + 6: MD_NEWLINE@37..38 0: NEWLINE@37..38 "\n" [] [] - 2: MD_BULLET@38..45 + 7: MD_BULLET@38..45 0: MD_LIST_MARKER_PREFIX@38..40 0: MD_INDENT_TOKEN_LIST@38..38 1: MINUS@38..39 "-" [] [] @@ -432,9 +420,9 @@ MdDocument { 0: UNDERSCORE@43..44 "_" [] [] 1: MD_NEWLINE@44..45 0: NEWLINE@44..45 "\n" [] [] - 3: MD_NEWLINE@45..46 + 8: MD_NEWLINE@45..46 0: NEWLINE@45..46 "\n" [] [] - 4: MD_BULLET@46..54 + 9: MD_BULLET@46..54 0: MD_LIST_MARKER_PREFIX@46..48 0: MD_INDENT_TOKEN_LIST@46..46 1: MINUS@46..47 "-" [] [] @@ -455,9 +443,9 @@ MdDocument { 0: STAR@52..53 "*" [] [] 1: MD_NEWLINE@53..54 0: NEWLINE@53..54 "\n" [] [] - 5: MD_NEWLINE@54..55 + 10: MD_NEWLINE@54..55 0: NEWLINE@54..55 "\n" [] [] - 6: MD_BULLET@55..62 + 11: MD_BULLET@55..62 0: MD_LIST_MARKER_PREFIX@55..57 0: MD_INDENT_TOKEN_LIST@55..55 1: MINUS@55..56 "-" [] [] @@ -476,7 +464,7 @@ MdDocument { 0: UNDERSCORE@60..61 "_" [] [] 4: MD_THEMATIC_BREAK_CHAR@61..62 0: UNDERSCORE@61..62 "_" [] [] - 11: MD_NEWLINE@62..63 + 7: MD_NEWLINE@62..63 0: NEWLINE@62..63 "\n" [] [] 2: EOF@63..63 "" [] [] diff --git a/crates/biome_markdown_parser/tests/spec_test.rs b/crates/biome_markdown_parser/tests/spec_test.rs index 1589cbf45da9..8634dc86c4f5 100644 --- a/crates/biome_markdown_parser/tests/spec_test.rs +++ b/crates/biome_markdown_parser/tests/spec_test.rs @@ -325,17 +325,29 @@ pub fn quick_test() { "- outer item\n - inner item\n inner continuation\n outer continuation at parent indentation\n\n- next outer item\n", "
    \n
  • \n

    outer item

    \n
      \n
    • inner item\ninner continuation\nouter continuation at parent indentation
    • \n
    \n
  • \n
  • \n

    next outer item

    \n
  • \n
\n", ); - // Mixed ordered delimiters across blank lines produce separate lists + // Mixed ordered delimiters across blank lines produce separate tight lists test_example( 10005, "1. one\n\n2) two\n", - "
    \n
  1. \n

    one

    \n
  2. \n
\n
    \n
  1. \n

    two

    \n
  2. \n
\n", + "
    \n
  1. one
  2. \n
\n
    \n
  1. two
  2. \n
\n", ); - // Mixed bullet markers across blank lines produce separate lists + // Mixed bullet markers across blank lines produce separate tight lists test_example( 10006, "- one\n\n+ two\n", - "
    \n
  • \n

    one

    \n
  • \n
\n
    \n
  • \n

    two

    \n
  • \n
\n", + "
    \n
  • one
  • \n
\n
    \n
  • two
  • \n
\n", + ); + // Bullet → ordered across blank lines produce separate lists + test_example( + 10012, + "- bullet\n\n1. ordered\n", + "
    \n
  • bullet
  • \n
\n
    \n
  1. ordered
  2. \n
\n", + ); + // Ordered → bullet across blank lines produce separate lists + test_example( + 10013, + "1. ordered\n\n- bullet\n", + "
    \n
  1. ordered
  2. \n
\n
    \n
  • bullet
  • \n
\n", ); // Nested list items separated by blank lines stay in the same nested list. test_example( @@ -393,4 +405,11 @@ pub fn quick_test() { "
\n

Foo

\n
\n", ); test_example(20003, "> ---\n", "
\n
\n
\n"); + + // Single-item lists split by marker change should be tight + test_example( + 20008, + "* item one\n\n- item two\n", + "
    \n
  • item one
  • \n
\n
    \n
  • item two
  • \n
\n", + ); }