diff --git a/crates/biome_markdown_formatter/tests/specs/prettier/markdown/list/issue-17652.md.snap b/crates/biome_markdown_formatter/tests/specs/prettier/markdown/list/issue-17652.md.snap index 2236f67c06f7..368783892080 100644 --- a/crates/biome_markdown_formatter/tests/specs/prettier/markdown/list/issue-17652.md.snap +++ b/crates/biome_markdown_formatter/tests/specs/prettier/markdown/list/issue-17652.md.snap @@ -79,21 +79,3 @@ info: markdown/list/issue-17652.md 1. Another 2. List ``` - -# Errors -``` -issue-17652.md:11:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Expected an ordered list item - - 9 │ 2. List - 10 │ - > 11 │ 1. Some text, and code block below, with newline after code block - │ ^^ - 12 │ - 13 │ 1. Another - - i Ordered list items start with a number followed by `.` or `)` at the beginning of a line - - -``` diff --git a/crates/biome_markdown_parser/src/parser.rs b/crates/biome_markdown_parser/src/parser.rs index 9d90991803ae..556c0c7fdae7 100644 --- a/crates/biome_markdown_parser/src/parser.rs +++ b/crates/biome_markdown_parser/src/parser.rs @@ -73,6 +73,8 @@ pub(crate) struct MarkdownParserState { pub(crate) list_item_indents: Vec, /// Recorded quote marker indents keyed by quote node range. pub(crate) quote_indents: Vec, + /// Whether the most recently parsed list ended with a blank line. + pub(crate) last_list_ends_with_blank: bool, /// Virtual line start override for container prefixes (e.g., block quotes). pub(crate) virtual_line_start: Option, /// Flag to unwind quote parsing when nesting exceeds the maximum depth. @@ -192,6 +194,14 @@ impl<'source> MarkdownParser<'source> { self.state.quote_indents.push(QuoteIndent { range, indent }); } + pub(crate) fn set_last_list_ends_with_blank(&mut self, value: bool) { + self.state.last_list_ends_with_blank = value; + } + + pub(crate) fn take_last_list_ends_with_blank(&mut self) -> bool { + std::mem::take(&mut self.state.last_list_ends_with_blank) + } + /// Re-lex the current token using LinkDefinition context. /// This makes whitespace produce separate tokens for destination/title parsing. pub(crate) fn re_lex_link_definition(&mut self) { diff --git a/crates/biome_markdown_parser/src/syntax/list.rs b/crates/biome_markdown_parser/src/syntax/list.rs index a215f9e1b602..f52677e67152 100644 --- a/crates/biome_markdown_parser/src/syntax/list.rs +++ b/crates/biome_markdown_parser/src/syntax/list.rs @@ -87,6 +87,14 @@ fn compute_marker_indent(p: &MarkdownParser) -> usize { return p.line_start_leading_indent(); } + // If the current token still contains the preserved pre-marker + // indentation, measure to the first non-whitespace character on the + // line. Measuring to the current token start would incorrectly report + // column 0 for nested list markers. + if p.at(MD_TEXTUAL_LITERAL) && is_whitespace_only(p.cur_text()) { + return p.line_start_leading_indent(); + } + // Virtual line start: compute actual column from source text. // The leading whitespace was skipped as trivia, but we need the // real column for indented code block detection in nested lists. @@ -311,7 +319,7 @@ struct ListItemBlankInfo { fn skip_blank_lines_between_items( p: &mut MarkdownParser, - has_item_after_blank_lines: fn(&mut MarkdownParser) -> bool, + has_item_after_blank_lines: impl Fn(&mut MarkdownParser) -> bool, is_tight: &mut bool, last_item_ends_with_blank: &mut bool, ) { @@ -361,7 +369,7 @@ fn parse_list_element_common( marker_state: &mut Option, current_marker: FMarker, parse_item: FParse, - has_item_after_blank_lines: fn(&mut MarkdownParser) -> bool, + has_item_after_blank_lines: impl Fn(&mut MarkdownParser) -> bool, is_tight: &mut bool, last_item_ends_with_blank: &mut bool, ) -> ParsedSyntax @@ -486,7 +494,7 @@ impl ParseNodeList for BulletList { &mut self.marker_kind, current_bullet_marker, parse_bullet, - has_bullet_item_after_blank_lines, + |p| has_bullet_item_after_blank_lines_at_indent(p, self.marker_indent), &mut self.is_tight, &mut self.last_item_ends_with_blank, ) @@ -512,34 +520,10 @@ impl ParseNodeList for BulletList { |p, _marker_kind| { let next_is_bullet_at_indent = p.lookahead(|p| { p.bump(NEWLINE); - // Count indent before marker (tabs expand to next tab stop) - let mut indent = 0usize; - while p.at(MD_TEXTUAL_LITERAL) { - let text = p.cur_text(); - if text == " " { - indent += 1; - p.bump(MD_TEXTUAL_LITERAL); - } else if text == "\t" { - indent += TAB_STOP_SPACES - (indent % TAB_STOP_SPACES); - p.bump(MD_TEXTUAL_LITERAL); - } else { - break; - } - } - // Check indent matches this list's marker indent - let indent_ok = if marker_indent == 0 { - indent <= MAX_BLOCK_PREFIX_INDENT - } else { - indent >= marker_indent && indent <= marker_indent + MAX_BLOCK_PREFIX_INDENT - }; - if !indent_ok { - return false; - } - if p.at(T![-]) || p.at(T![*]) || p.at(T![+]) { - p.bump(p.cur()); - return marker_followed_by_whitespace_or_eol(p); - } - false + // Don't set virtual_line_start here — it would cause + // list_item_within_indent to zero out base_indent, + // making the inner list claim items at outer indent. + at_bullet_list_item_with_base_indent(p, marker_indent) }); if next_is_bullet_at_indent { Some(false) @@ -572,6 +556,7 @@ impl ParseNodeList for BulletList { let range = completed.range(p); p.record_list_tightness(range, self.is_tight); + p.set_last_list_ends_with_blank(self.last_item_ends_with_blank); completed } } @@ -831,14 +816,17 @@ struct OrderedList { last_item_ends_with_blank: bool, /// The delimiter for this ordered list (`.` or `)`). marker_delim: Option, + /// The indentation level of the list marker (0 for top-level). + marker_indent: usize, } impl OrderedList { - fn new() -> Self { + fn new(marker_indent: usize) -> Self { Self { is_tight: true, last_item_ends_with_blank: false, marker_delim: None, + marker_indent, } } } @@ -855,13 +843,19 @@ impl ParseNodeList for OrderedList { &mut self.marker_delim, current_ordered_delim, parse_ordered_bullet, - has_ordered_item_after_blank_lines, + |p| has_ordered_item_after_blank_lines_at_indent(p, self.marker_indent), &mut self.is_tight, &mut self.last_item_ends_with_blank, ) } fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { + let marker_indent = self.marker_indent; + + if p.at_line_start() && at_blank_line_start(p) { + return !has_ordered_item_after_blank_lines_at_indent(p, marker_indent); + } + is_at_list_end_common( p, self.marker_delim, @@ -871,12 +865,7 @@ impl ParseNodeList for OrderedList { |p, marker_delim| { let next_is_ordered = p.lookahead(|p| { p.bump(NEWLINE); - skip_leading_whitespace_tokens(p); - if p.at(MD_ORDERED_LIST_MARKER) { - p.bump(MD_ORDERED_LIST_MARKER); - return marker_followed_by_whitespace_or_eol(p); - } - false + at_order_list_item_with_base_indent(p, marker_indent) }); if next_is_ordered { if let (Some(current_delim), Some(next_delim)) = @@ -887,7 +876,10 @@ impl ParseNodeList for OrderedList { } return Some(false); } - Some(!has_ordered_item_after_blank_lines(p)) + Some(!has_ordered_item_after_blank_lines_at_indent( + p, + marker_indent, + )) }, ) } @@ -909,6 +901,7 @@ impl ParseNodeList for OrderedList { let completed = m.complete(p, Self::LIST_KIND); let range = completed.range(p); p.record_list_tightness(range, self.is_tight); + p.set_last_list_ends_with_blank(self.last_item_ends_with_blank); completed } } @@ -974,7 +967,8 @@ pub(crate) fn parse_order_list_item(p: &mut MarkdownParser) -> ParsedSyntax { p.state_mut().list_nesting_depth += 1; // Use ParseNodeList to parse the list with proper recovery - let mut list_helper = OrderedList::new(); + let marker_indent = compute_marker_indent(p); + let mut list_helper = OrderedList::new(marker_indent); list_helper.parse_list(p); // Decrement list depth @@ -1844,6 +1838,10 @@ fn parse_first_line_blocks( false }; } else { + if p.take_last_list_ends_with_blank() { + state.has_blank_line = true; + state.last_was_blank = true; + } state.last_block_was_paragraph = false; } state.first_line = false; @@ -2014,10 +2012,6 @@ fn check_continuation_indent( let is_indent_code_block = allow_indent_code_block && indent >= state.required_indent + INDENT_CODE_BLOCK_SPACES; if !is_indent_code_block { - // Sufficient indentation - skip it and continue - // (emitting indent tokens here is not possible because MdIndentToken - // is not a valid child of MdBlockList — leave as trivia) - p.skip_line_indent(state.required_indent); let prev_virtual = p.state().virtual_line_start; p.state_mut().virtual_line_start = Some(p.cur_range().start()); @@ -2042,6 +2036,12 @@ fn check_continuation_indent( }; } + // Consume the structural indent as trivia so that block + // detection (at_bullet_list_item, etc.) works on the + // de-indented content. See §5.2 continuation lines. + p.skip_line_indent(state.required_indent); + p.set_virtual_line_start(); + return ContinuationResult { action: LoopAction::FallThrough, restore: VirtualLineRestore::Restore(prev_virtual), @@ -2065,6 +2065,16 @@ fn check_continuation_indent( }; } + // Once a nested item's continuation drops back to its marker column, + // the line belongs to the parent item rather than lazily extending + // the nested paragraph. + if state.marker_indent > 0 && indent <= state.marker_indent { + return ContinuationResult { + action: LoopAction::Break, + restore: VirtualLineRestore::None, + }; + } + // Lazy continuation per CommonMark §5.2 if !state.last_block_was_paragraph { return ContinuationResult { @@ -2104,6 +2114,10 @@ fn parse_continuation_block( } else { false }; + if p.take_last_list_ends_with_blank() { + state.has_blank_line = true; + state.last_was_blank = true; + } if let VirtualLineRestore::Restore(prev_virtual) = restore { p.state_mut().virtual_line_start = prev_virtual; @@ -2513,13 +2527,12 @@ fn consume_blank_line(p: &mut MarkdownParser) { /// they just make it "loose". This function peeks ahead across blank lines /// to see if another bullet item follows. fn has_bullet_item_after_blank_lines(p: &mut MarkdownParser) -> bool { - has_list_item_after_blank_lines(p, |p| { - if p.at(T![-]) || p.at(T![*]) || p.at(T![+]) { - p.bump(p.cur()); - marker_followed_by_whitespace_or_eol(p) - } else { - false - } + has_list_item_after_blank_lines_at_indent(p, 0, |p| { + let prev_virtual = p.state().virtual_line_start; + p.state_mut().virtual_line_start = Some(p.cur_range().start()); + let result = at_bullet_list_item_with_base_indent(p, 0); + p.state_mut().virtual_line_start = prev_virtual; + result }) } @@ -2530,12 +2543,11 @@ fn has_bullet_item_after_blank_lines_at_indent( expected_indent: usize, ) -> bool { has_list_item_after_blank_lines_at_indent(p, expected_indent, |p| { - if p.at(T![-]) || p.at(T![*]) || p.at(T![+]) { - p.bump(p.cur()); - marker_followed_by_whitespace_or_eol(p) - } else { - false - } + let prev_virtual = p.state().virtual_line_start; + p.state_mut().virtual_line_start = Some(p.cur_range().start()); + let result = at_bullet_list_item_with_base_indent(p, expected_indent); + p.state_mut().virtual_line_start = prev_virtual; + result }) } @@ -2598,58 +2610,25 @@ where /// they just make it "loose". This function peeks ahead across blank lines /// to see if another ordered item follows. fn has_ordered_item_after_blank_lines(p: &mut MarkdownParser) -> bool { - has_list_item_after_blank_lines(p, |p| p.at(MD_ORDERED_LIST_MARKER)) + has_list_item_after_blank_lines_at_indent(p, 0, |p| { + let prev_virtual = p.state().virtual_line_start; + p.state_mut().virtual_line_start = Some(p.cur_range().start()); + let result = at_order_list_item_with_base_indent(p, 0); + p.state_mut().virtual_line_start = prev_virtual; + result + }) } -fn has_list_item_after_blank_lines(p: &mut MarkdownParser, has_marker: F) -> bool -where - F: Fn(&mut MarkdownParser) -> bool, -{ - p.lookahead(|p| { - // Skip all blank lines - loop { - // Skip whitespace on current line - while p.at(MD_TEXTUAL_LITERAL) { - let text = p.cur_text(); - if text == " " || text == "\t" { - p.bump(MD_TEXTUAL_LITERAL); - } else { - break; - } - } - - // If at NEWLINE, consume it and continue checking - if p.at(NEWLINE) { - p.bump(NEWLINE); - continue; - } - - // Reached non-blank content or EOF - break; - } - - // Check for marker directly (avoid nested lookahead issues) - // Skip leading indent (up to 3 spaces for list items) - let mut indent = 0; - while p.at(MD_TEXTUAL_LITERAL) { - let text = p.cur_text(); - if text == " " { - indent += 1; - p.bump(MD_TEXTUAL_LITERAL); - } else if text == "\t" { - indent += TAB_STOP_SPACES - (indent % TAB_STOP_SPACES); - p.bump(MD_TEXTUAL_LITERAL); - } else { - break; - } - } - - // More than 3 spaces indent = indented code block, not a list item - if indent > MAX_BLOCK_PREFIX_INDENT { - return false; - } - - has_marker(p) +fn has_ordered_item_after_blank_lines_at_indent( + p: &mut MarkdownParser, + expected_indent: usize, +) -> bool { + has_list_item_after_blank_lines_at_indent(p, expected_indent, |p| { + let prev_virtual = p.state().virtual_line_start; + p.state_mut().virtual_line_start = Some(p.cur_range().start()); + let result = at_order_list_item_with_base_indent(p, expected_indent); + p.state_mut().virtual_line_start = prev_virtual; + result }) } diff --git a/crates/biome_markdown_parser/src/syntax/mod.rs b/crates/biome_markdown_parser/src/syntax/mod.rs index 018a77ad8a5d..3a07a96d725a 100644 --- a/crates/biome_markdown_parser/src/syntax/mod.rs +++ b/crates/biome_markdown_parser/src/syntax/mod.rs @@ -935,8 +935,9 @@ fn break_for_setext_after_inline_newline( false } -/// Returns `true` when list-item indentation exposes a block interrupt or -/// nested list marker that must terminate the current paragraph. +/// Break the paragraph when the next line is a block interrupt at +/// `required_indent`, or when a nested item's continuation drops to +/// the parent's indent level (§5.2 ownership, depth >= 2). fn break_for_list_interrupt_after_inline_newline( p: &mut MarkdownParser, required_indent: usize, @@ -1008,6 +1009,22 @@ fn handle_inline_newline(p: &mut MarkdownParser, has_content: bool) -> InlineNew return InlineNewlineAction::Break; } + // Inside a nested list item (depth >= 2), break the paragraph when the + // continuation line's indent drops to or below the marker column. + // Such lines belong to a parent item, not lazy continuation. + // We only check nesting depth, not marker_indent alone, because a + // top-level list with leading whitespace (e.g. ` 1. text`) still + // allows lazy continuation at indent 0 per CommonMark §5.2. + if required_indent > 0 && p.state().list_nesting_depth >= 2 { + let marker_indent = p.state().list_item_marker_indent; + if marker_indent > 0 { + let indent = p.line_start_leading_indent(); + if indent < required_indent && indent <= marker_indent { + return InlineNewlineAction::Break; + } + } + } + // Check for block-level constructs that can interrupt paragraphs. // Textual fence tokens (e.g. "```") may not be caught by line_starts_with_fence // because the lexer emits them as MD_TEXTUAL_LITERAL in inline context. diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/list_continuation_edge_cases.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/list_continuation_edge_cases.md.snap index 286cfa6b53b5..11eb43510b60 100644 --- a/crates/biome_markdown_parser/tests/md_test_suite/ok/list_continuation_edge_cases.md.snap +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/list_continuation_edge_cases.md.snap @@ -307,8 +307,15 @@ MdDocument { md_bullet_list: MdBulletList [ MdBullet { prefix: MdListMarkerPrefix { - pre_marker_indent: MdIndentTokenList [], - marker: MINUS@285..288 "-" [Skipped(" "), Skipped(" ")] [], + pre_marker_indent: MdIndentTokenList [ + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@285..286 " " [] [], + }, + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@286..287 " " [] [], + }, + ], + marker: MINUS@287..288 "-" [] [], post_marker_space_token: missing (optional), content_indent: MdIndentTokenList [], }, @@ -339,18 +346,6 @@ MdDocument { MdTextual { value_token: MD_TEXTUAL_LITERAL@322..323 "\n" [] [], }, - MdTextual { - value_token: MD_TEXTUAL_LITERAL@323..324 " " [] [], - }, - MdTextual { - value_token: MD_TEXTUAL_LITERAL@324..325 " " [] [], - }, - MdTextual { - value_token: MD_TEXTUAL_LITERAL@325..365 "outer continuation at parent indentation" [] [], - }, - MdTextual { - value_token: MD_TEXTUAL_LITERAL@365..366 "\n" [] [], - }, ], hard_line: missing (optional), }, @@ -358,6 +353,17 @@ MdDocument { }, ], }, + MdParagraph { + list: MdInlineItemList [ + MdTextual { + value_token: MD_TEXTUAL_LITERAL@323..365 "outer continuation at parent indentation" [Skipped(" "), Skipped(" ")] [], + }, + MdTextual { + value_token: MD_TEXTUAL_LITERAL@365..366 "\n" [] [], + }, + ], + hard_line: missing (optional), + }, MdNewline { value_token: NEWLINE@366..367 "\n" [] [], }, @@ -653,17 +659,21 @@ MdDocument { 1: MD_TEXTUAL@284..285 0: MD_TEXTUAL_LITERAL@284..285 "\n" [] [] 1: (empty) - 1: MD_BULLET_LIST_ITEM@285..366 - 0: MD_BULLET_LIST@285..366 - 0: MD_BULLET@285..366 + 1: MD_BULLET_LIST_ITEM@285..323 + 0: MD_BULLET_LIST@285..323 + 0: MD_BULLET@285..323 0: MD_LIST_MARKER_PREFIX@285..288 - 0: MD_INDENT_TOKEN_LIST@285..285 - 1: MINUS@285..288 "-" [Skipped(" "), Skipped(" ")] [] + 0: MD_INDENT_TOKEN_LIST@285..287 + 0: MD_INDENT_TOKEN@285..286 + 0: MD_INDENT_CHAR@285..286 " " [] [] + 1: MD_INDENT_TOKEN@286..287 + 0: MD_INDENT_CHAR@286..287 " " [] [] + 1: MINUS@287..288 "-" [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@288..288 - 1: MD_BLOCK_LIST@288..366 - 0: MD_PARAGRAPH@288..366 - 0: MD_INLINE_ITEM_LIST@288..366 + 1: MD_BLOCK_LIST@288..323 + 0: MD_PARAGRAPH@288..323 + 0: MD_INLINE_ITEM_LIST@288..323 0: MD_TEXTUAL@288..299 0: MD_TEXTUAL_LITERAL@288..299 " inner item" [] [] 1: MD_TEXTUAL@299..300 @@ -680,16 +690,15 @@ MdDocument { 0: MD_TEXTUAL_LITERAL@304..322 "inner continuation" [] [] 7: MD_TEXTUAL@322..323 0: MD_TEXTUAL_LITERAL@322..323 "\n" [] [] - 8: MD_TEXTUAL@323..324 - 0: MD_TEXTUAL_LITERAL@323..324 " " [] [] - 9: MD_TEXTUAL@324..325 - 0: MD_TEXTUAL_LITERAL@324..325 " " [] [] - 10: MD_TEXTUAL@325..365 - 0: MD_TEXTUAL_LITERAL@325..365 "outer continuation at parent indentation" [] [] - 11: MD_TEXTUAL@365..366 - 0: MD_TEXTUAL_LITERAL@365..366 "\n" [] [] 1: (empty) - 2: MD_NEWLINE@366..367 + 2: MD_PARAGRAPH@323..366 + 0: MD_INLINE_ITEM_LIST@323..366 + 0: MD_TEXTUAL@323..365 + 0: MD_TEXTUAL_LITERAL@323..365 "outer continuation at parent indentation" [Skipped(" "), Skipped(" ")] [] + 1: MD_TEXTUAL@365..366 + 0: MD_TEXTUAL_LITERAL@365..366 "\n" [] [] + 1: (empty) + 3: MD_NEWLINE@366..367 0: NEWLINE@366..367 "\n" [] [] 1: MD_BULLET@367..385 0: MD_LIST_MARKER_PREFIX@367..368 diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/list_indentation.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/list_indentation.md.snap index 49e9c20f0d8c..a426f22fc824 100644 --- a/crates/biome_markdown_parser/tests/md_test_suite/ok/list_indentation.md.snap +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/list_indentation.md.snap @@ -733,8 +733,15 @@ MdDocument { md_bullet_list: MdBulletList [ MdBullet { prefix: MdListMarkerPrefix { - pre_marker_indent: MdIndentTokenList [], - marker: MINUS@712..715 "-" [Skipped(" "), Skipped(" ")] [], + pre_marker_indent: MdIndentTokenList [ + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@712..713 " " [] [], + }, + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@713..714 " " [] [], + }, + ], + marker: MINUS@714..715 "-" [] [], post_marker_space_token: missing (optional), content_indent: MdIndentTokenList [], }, @@ -765,18 +772,6 @@ MdDocument { MdTextual { value_token: MD_TEXTUAL_LITERAL@741..742 "\n" [] [], }, - MdTextual { - value_token: MD_TEXTUAL_LITERAL@742..743 " " [] [], - }, - MdTextual { - value_token: MD_TEXTUAL_LITERAL@743..744 " " [] [], - }, - MdTextual { - value_token: MD_TEXTUAL_LITERAL@744..759 "outer continued" [] [], - }, - MdTextual { - value_token: MD_TEXTUAL_LITERAL@759..760 "\n" [] [], - }, ], hard_line: missing (optional), }, @@ -784,6 +779,17 @@ MdDocument { }, ], }, + MdParagraph { + list: MdInlineItemList [ + MdTextual { + value_token: MD_TEXTUAL_LITERAL@742..759 "outer continued" [Skipped(" "), Skipped(" ")] [], + }, + MdTextual { + value_token: MD_TEXTUAL_LITERAL@759..760 "\n" [] [], + }, + ], + hard_line: missing (optional), + }, ], }, ], @@ -1309,17 +1315,21 @@ MdDocument { 1: MD_TEXTUAL@711..712 0: MD_TEXTUAL_LITERAL@711..712 "\n" [] [] 1: (empty) - 1: MD_BULLET_LIST_ITEM@712..760 - 0: MD_BULLET_LIST@712..760 - 0: MD_BULLET@712..760 + 1: MD_BULLET_LIST_ITEM@712..742 + 0: MD_BULLET_LIST@712..742 + 0: MD_BULLET@712..742 0: MD_LIST_MARKER_PREFIX@712..715 - 0: MD_INDENT_TOKEN_LIST@712..712 - 1: MINUS@712..715 "-" [Skipped(" "), Skipped(" ")] [] + 0: MD_INDENT_TOKEN_LIST@712..714 + 0: MD_INDENT_TOKEN@712..713 + 0: MD_INDENT_CHAR@712..713 " " [] [] + 1: MD_INDENT_TOKEN@713..714 + 0: MD_INDENT_CHAR@713..714 " " [] [] + 1: MINUS@714..715 "-" [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@715..715 - 1: MD_BLOCK_LIST@715..760 - 0: MD_PARAGRAPH@715..760 - 0: MD_INLINE_ITEM_LIST@715..760 + 1: MD_BLOCK_LIST@715..742 + 0: MD_PARAGRAPH@715..742 + 0: MD_INLINE_ITEM_LIST@715..742 0: MD_TEXTUAL@715..721 0: MD_TEXTUAL_LITERAL@715..721 " inner" [] [] 1: MD_TEXTUAL@721..722 @@ -1336,15 +1346,14 @@ MdDocument { 0: MD_TEXTUAL_LITERAL@726..741 "inner continued" [] [] 7: MD_TEXTUAL@741..742 0: MD_TEXTUAL_LITERAL@741..742 "\n" [] [] - 8: MD_TEXTUAL@742..743 - 0: MD_TEXTUAL_LITERAL@742..743 " " [] [] - 9: MD_TEXTUAL@743..744 - 0: MD_TEXTUAL_LITERAL@743..744 " " [] [] - 10: MD_TEXTUAL@744..759 - 0: MD_TEXTUAL_LITERAL@744..759 "outer continued" [] [] - 11: MD_TEXTUAL@759..760 - 0: MD_TEXTUAL_LITERAL@759..760 "\n" [] [] 1: (empty) + 2: MD_PARAGRAPH@742..760 + 0: MD_INLINE_ITEM_LIST@742..760 + 0: MD_TEXTUAL@742..759 + 0: MD_TEXTUAL_LITERAL@742..759 "outer continued" [Skipped(" "), Skipped(" ")] [] + 1: MD_TEXTUAL@759..760 + 0: MD_TEXTUAL_LITERAL@759..760 "\n" [] [] + 1: (empty) 39: MD_NEWLINE@760..761 0: NEWLINE@760..761 "\n" [] [] 40: MD_PARAGRAPH@761..789 diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/list_tightness.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/list_tightness.md.snap index 1a74fe3fb5b4..7d70f5c4af79 100644 --- a/crates/biome_markdown_parser/tests/md_test_suite/ok/list_tightness.md.snap +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/list_tightness.md.snap @@ -670,8 +670,15 @@ MdDocument { md_bullet_list: MdBulletList [ MdBullet { prefix: MdListMarkerPrefix { - pre_marker_indent: MdIndentTokenList [], - marker: MINUS@547..550 "-" [Skipped(" "), Skipped(" ")] [], + pre_marker_indent: MdIndentTokenList [ + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@547..548 " " [] [], + }, + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@548..549 " " [] [], + }, + ], + marker: MINUS@549..550 "-" [] [], post_marker_space_token: missing (optional), content_indent: MdIndentTokenList [], }, @@ -813,8 +820,15 @@ MdDocument { md_bullet_list: MdBulletList [ MdBullet { prefix: MdListMarkerPrefix { - pre_marker_indent: MdIndentTokenList [], - marker: MINUS@631..634 "-" [Skipped(" "), Skipped(" ")] [], + pre_marker_indent: MdIndentTokenList [ + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@631..632 " " [] [], + }, + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@632..633 " " [] [], + }, + ], + marker: MINUS@633..634 "-" [] [], post_marker_space_token: missing (optional), content_indent: MdIndentTokenList [], }, @@ -1271,8 +1285,12 @@ MdDocument { 0: MD_BULLET_LIST@547..572 0: MD_BULLET@547..560 0: MD_LIST_MARKER_PREFIX@547..550 - 0: MD_INDENT_TOKEN_LIST@547..547 - 1: MINUS@547..550 "-" [Skipped(" "), Skipped(" ")] [] + 0: MD_INDENT_TOKEN_LIST@547..549 + 0: MD_INDENT_TOKEN@547..548 + 0: MD_INDENT_CHAR@547..548 " " [] [] + 1: MD_INDENT_TOKEN@548..549 + 0: MD_INDENT_CHAR@548..549 " " [] [] + 1: MINUS@549..550 "-" [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@550..550 1: MD_BLOCK_LIST@550..560 @@ -1364,8 +1382,12 @@ MdDocument { 0: MD_BULLET_LIST@631..655 0: MD_BULLET@631..643 0: MD_LIST_MARKER_PREFIX@631..634 - 0: MD_INDENT_TOKEN_LIST@631..631 - 1: MINUS@631..634 "-" [Skipped(" "), Skipped(" ")] [] + 0: MD_INDENT_TOKEN_LIST@631..633 + 0: MD_INDENT_TOKEN@631..632 + 0: MD_INDENT_CHAR@631..632 " " [] [] + 1: MD_INDENT_TOKEN@632..633 + 0: MD_INDENT_CHAR@632..633 " " [] [] + 1: MINUS@633..634 "-" [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@634..634 1: MD_BLOCK_LIST@634..643 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 45ce015ed547..1d71ce638a3f 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 @@ -178,8 +178,15 @@ MdDocument { md_bullet_list: MdBulletList [ MdBullet { prefix: MdListMarkerPrefix { - pre_marker_indent: MdIndentTokenList [], - marker: MINUS@115..118 "-" [Skipped(" "), Skipped(" ")] [], + pre_marker_indent: MdIndentTokenList [ + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@115..116 " " [] [], + }, + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@116..117 " " [] [], + }, + ], + marker: MINUS@117..118 "-" [] [], post_marker_space_token: missing (optional), content_indent: MdIndentTokenList [], }, @@ -365,8 +372,12 @@ MdDocument { 0: MD_BULLET_LIST@115..148 0: MD_BULLET@115..130 0: MD_LIST_MARKER_PREFIX@115..118 - 0: MD_INDENT_TOKEN_LIST@115..115 - 1: MINUS@115..118 "-" [Skipped(" "), Skipped(" ")] [] + 0: MD_INDENT_TOKEN_LIST@115..117 + 0: MD_INDENT_TOKEN@115..116 + 0: MD_INDENT_CHAR@115..116 " " [] [] + 1: MD_INDENT_TOKEN@116..117 + 0: MD_INDENT_CHAR@116..117 " " [] [] + 1: MINUS@117..118 "-" [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@118..118 1: MD_BLOCK_LIST@118..130 diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/nested_bullet_indent_tokens.md b/crates/biome_markdown_parser/tests/md_test_suite/ok/nested_bullet_indent_tokens.md new file mode 100644 index 000000000000..eb03c4dfb7b8 --- /dev/null +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/nested_bullet_indent_tokens.md @@ -0,0 +1,2 @@ +* ff + + ff diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/nested_bullet_indent_tokens.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/nested_bullet_indent_tokens.md.snap new file mode 100644 index 000000000000..a7766a83491f --- /dev/null +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/nested_bullet_indent_tokens.md.snap @@ -0,0 +1,128 @@ +--- +source: crates/biome_markdown_parser/tests/spec_test.rs +assertion_line: 131 +expression: snapshot +--- + +## Input + +``` +* ff + + ff + +``` + + +## AST + +``` +MdDocument { + bom_token: missing (optional), + value: MdBlockList [ + MdBulletListItem { + md_bullet_list: MdBulletList [ + MdBullet { + prefix: MdListMarkerPrefix { + pre_marker_indent: MdIndentTokenList [], + marker: STAR@0..1 "*" [] [], + post_marker_space_token: missing (optional), + content_indent: MdIndentTokenList [], + }, + content: MdBlockList [ + MdParagraph { + list: MdInlineItemList [ + MdTextual { + value_token: MD_TEXTUAL_LITERAL@1..4 " ff" [] [], + }, + MdTextual { + value_token: MD_TEXTUAL_LITERAL@4..5 "\n" [] [], + }, + ], + hard_line: missing (optional), + }, + MdBulletListItem { + md_bullet_list: MdBulletList [ + MdBullet { + prefix: MdListMarkerPrefix { + pre_marker_indent: MdIndentTokenList [ + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@5..6 " " [] [], + }, + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@6..7 " " [] [], + }, + ], + marker: PLUS@7..8 "+" [] [], + post_marker_space_token: missing (optional), + content_indent: MdIndentTokenList [], + }, + content: MdBlockList [ + MdParagraph { + list: MdInlineItemList [ + MdTextual { + value_token: MD_TEXTUAL_LITERAL@8..11 " ff" [] [], + }, + MdTextual { + value_token: MD_TEXTUAL_LITERAL@11..12 "\n" [] [], + }, + ], + hard_line: missing (optional), + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + eof_token: EOF@12..12 "" [] [], +} +``` + +## CST + +``` +0: MD_DOCUMENT@0..12 + 0: (empty) + 1: MD_BLOCK_LIST@0..12 + 0: MD_BULLET_LIST_ITEM@0..12 + 0: MD_BULLET_LIST@0..12 + 0: MD_BULLET@0..12 + 0: MD_LIST_MARKER_PREFIX@0..1 + 0: MD_INDENT_TOKEN_LIST@0..0 + 1: STAR@0..1 "*" [] [] + 2: (empty) + 3: MD_INDENT_TOKEN_LIST@1..1 + 1: MD_BLOCK_LIST@1..12 + 0: MD_PARAGRAPH@1..5 + 0: MD_INLINE_ITEM_LIST@1..5 + 0: MD_TEXTUAL@1..4 + 0: MD_TEXTUAL_LITERAL@1..4 " ff" [] [] + 1: MD_TEXTUAL@4..5 + 0: MD_TEXTUAL_LITERAL@4..5 "\n" [] [] + 1: (empty) + 1: MD_BULLET_LIST_ITEM@5..12 + 0: MD_BULLET_LIST@5..12 + 0: MD_BULLET@5..12 + 0: MD_LIST_MARKER_PREFIX@5..8 + 0: MD_INDENT_TOKEN_LIST@5..7 + 0: MD_INDENT_TOKEN@5..6 + 0: MD_INDENT_CHAR@5..6 " " [] [] + 1: MD_INDENT_TOKEN@6..7 + 0: MD_INDENT_CHAR@6..7 " " [] [] + 1: PLUS@7..8 "+" [] [] + 2: (empty) + 3: MD_INDENT_TOKEN_LIST@8..8 + 1: MD_BLOCK_LIST@8..12 + 0: MD_PARAGRAPH@8..12 + 0: MD_INLINE_ITEM_LIST@8..12 + 0: MD_TEXTUAL@8..11 + 0: MD_TEXTUAL_LITERAL@8..11 " ff" [] [] + 1: MD_TEXTUAL@11..12 + 0: MD_TEXTUAL_LITERAL@11..12 "\n" [] [] + 1: (empty) + 2: EOF@12..12 "" [] [] + +``` diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/nested_list_interrupt_after_newline.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/nested_list_interrupt_after_newline.md.snap index ba168613c2da..8ee6efcdf58b 100644 --- a/crates/biome_markdown_parser/tests/md_test_suite/ok/nested_list_interrupt_after_newline.md.snap +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/nested_list_interrupt_after_newline.md.snap @@ -1,5 +1,6 @@ --- source: crates/biome_markdown_parser/tests/spec_test.rs +assertion_line: 131 expression: snapshot --- @@ -43,8 +44,15 @@ MdDocument { md_bullet_list: MdBulletList [ MdBullet { prefix: MdListMarkerPrefix { - pre_marker_indent: MdIndentTokenList [], - marker: MINUS@6..9 "-" [Skipped(" "), Skipped(" ")] [], + pre_marker_indent: MdIndentTokenList [ + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@6..7 " " [] [], + }, + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@7..8 " " [] [], + }, + ], + marker: MINUS@8..9 "-" [] [], post_marker_space_token: missing (optional), content_indent: MdIndentTokenList [], }, @@ -99,8 +107,12 @@ MdDocument { 0: MD_BULLET_LIST@6..14 0: MD_BULLET@6..14 0: MD_LIST_MARKER_PREFIX@6..9 - 0: MD_INDENT_TOKEN_LIST@6..6 - 1: MINUS@6..9 "-" [Skipped(" "), Skipped(" ")] [] + 0: MD_INDENT_TOKEN_LIST@6..8 + 0: MD_INDENT_TOKEN@6..7 + 0: MD_INDENT_CHAR@6..7 " " [] [] + 1: MD_INDENT_TOKEN@7..8 + 0: MD_INDENT_CHAR@7..8 " " [] [] + 1: MINUS@8..9 "-" [] [] 2: (empty) 3: MD_INDENT_TOKEN_LIST@9..9 1: MD_BLOCK_LIST@9..14 diff --git a/crates/biome_markdown_parser/tests/spec_test.rs b/crates/biome_markdown_parser/tests/spec_test.rs index 1b8d6d1f1692..99338d6b95e5 100644 --- a/crates/biome_markdown_parser/tests/spec_test.rs +++ b/crates/biome_markdown_parser/tests/spec_test.rs @@ -209,4 +209,24 @@ pub fn quick_test() { "- foo\n ---\n", "
    \n
  • \n

    foo

    \n
  • \n
\n", ); + test_example( + 10001, + " - foo\n - bar\n\t - baz\n", + "
    \n
  • foo\n
      \n
    • bar\n
        \n
      • baz
      • \n
      \n
    • \n
    \n
  • \n
\n", + ); + test_example( + 10002, + "1. A paragraph\n with two lines.\n\n indented code\n\n > A block quote.\n", + "
    \n
  1. \n

    A paragraph\nwith two lines.

    \n
    indented code\n
    \n
    \n

    A block quote.

    \n
    \n
  2. \n
\n", + ); + test_example( + 10003, + "- a\n - b\n - c\n\n- d\n - e\n - f\n", + "
    \n
  • \n

    a

    \n
      \n
    • b
    • \n
    • c
    • \n
    \n
  • \n
  • \n

    d

    \n
      \n
    • e
    • \n
    • f
    • \n
    \n
  • \n
\n", + ); + test_example( + 10004, + "- 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
    • \n
    \n

    outer continuation at parent indentation

    \n
  • \n
  • \n

    next outer item

    \n
  • \n
\n", + ); }