Skip to content

Commit 52bb565

Browse files
authored
test(markdown_parser): add CST list invariants (#10369)
1 parent 8d0adfb commit 52bb565

3 files changed

Lines changed: 139 additions & 17 deletions

File tree

crates/biome_markdown_parser/src/syntax/list.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,35 @@ fn line_indent_from_current(p: &MarkdownParser) -> usize {
13191319
column - start_col
13201320
}
13211321

1322+
/// Measure indentation between the current virtual line start and cursor.
1323+
///
1324+
/// This is needed after consuming a quote prefix: the lexer can leave the
1325+
/// remaining indentation bundled with the following marker token, so token
1326+
/// walking alone cannot recover the already-consumed quote-prefix boundary.
1327+
fn virtual_line_indent_before_current(p: &MarkdownParser) -> Option<usize> {
1328+
let virtual_start: usize = p.state().virtual_line_start?.into();
1329+
let current: usize = p.cur_range().start().into();
1330+
if virtual_start >= current {
1331+
return None;
1332+
}
1333+
1334+
let source = p.source().source_text();
1335+
let text = source.get(virtual_start..current)?;
1336+
if !text.chars().all(|c| c == ' ' || c == '\t') {
1337+
return None;
1338+
}
1339+
1340+
let mut column = 0usize;
1341+
for c in text.chars() {
1342+
match c {
1343+
' ' => column += 1,
1344+
'\t' => column += TAB_STOP_SPACES - (column % TAB_STOP_SPACES),
1345+
_ => return None,
1346+
}
1347+
}
1348+
Some(column)
1349+
}
1350+
13221351
fn quote_only_line_indent_at_current(p: &MarkdownParser, depth: usize) -> Option<usize> {
13231352
if depth == 0 {
13241353
return None;
@@ -2369,6 +2398,17 @@ fn emit_current_line_indent_list_bytes(p: &mut MarkdownParser, mut byte_count: u
23692398
let list_m = p.start();
23702399
while byte_count > 0 && p.at(MD_TEXTUAL_LITERAL) {
23712400
let text = p.cur_text();
2401+
if !text.is_empty()
2402+
&& text.starts_with([' ', '\t'])
2403+
&& !text.chars().all(|c| c == ' ' || c == '\t')
2404+
{
2405+
let old_range = p.cur_range();
2406+
p.re_lex(MarkdownReLexContext::ListPostMarker);
2407+
if p.cur_range() == old_range {
2408+
break;
2409+
}
2410+
continue;
2411+
}
23722412
if !text.chars().all(|c| c == ' ' || c == '\t') {
23732413
break;
23742414
}
@@ -2593,8 +2633,11 @@ fn check_continuation_indent(
25932633
};
25942634
}
25952635

2596-
let indent = line_indent_from_current(p);
2597-
2636+
let indent = line_indent_from_current(p).max(
2637+
virtual_line_indent_before_current(p)
2638+
.filter(|_| line_started_with_quote_prefix)
2639+
.unwrap_or(0),
2640+
);
25982641
if indent < state.marker_indent {
25992642
// Below the marker indent. Lazy continuation is still allowed for
26002643
// plain-text lines per CommonMark §5.2; list item starts and block
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use biome_markdown_parser::parse_markdown;
2+
use biome_markdown_syntax::{MdContinuationIndent, MdListMarkerPrefix, MdOrderedListItem};
3+
use biome_rowan::{AstNode, AstNodeList};
4+
5+
fn indent_len(indent: impl AstNodeList) -> usize {
6+
indent.len()
7+
}
8+
9+
fn continuation_indents(input: &str) -> Vec<usize> {
10+
let parsed = parse_markdown(input);
11+
12+
parsed
13+
.syntax()
14+
.descendants()
15+
.filter_map(MdContinuationIndent::cast)
16+
.map(|indent| indent_len(indent.indent()))
17+
.collect()
18+
}
19+
20+
fn marker_prefix(input: &str, marker_text: &str) -> MdListMarkerPrefix {
21+
let parsed = parse_markdown(input);
22+
23+
parsed
24+
.syntax()
25+
.descendants()
26+
.filter_map(MdListMarkerPrefix::cast)
27+
.find(|prefix| {
28+
prefix
29+
.marker()
30+
.is_ok_and(|marker| marker.text_trimmed() == marker_text)
31+
})
32+
.unwrap_or_else(|| panic!("expected ordered marker prefix {marker_text:?} in {input:?}"))
33+
}
34+
35+
fn ordered_list_item_count(input: &str) -> usize {
36+
let parsed = parse_markdown(input);
37+
38+
parsed
39+
.syntax()
40+
.descendants()
41+
.filter_map(MdOrderedListItem::cast)
42+
.count()
43+
}
44+
45+
#[test]
46+
fn nested_ordered_marker_keeps_parent_continuation_indent() {
47+
let input = "+ outer\n 1. nested\n";
48+
49+
assert_eq!(continuation_indents(input), [3]);
50+
assert_eq!(
51+
indent_len(marker_prefix(input, "1.").pre_marker_indent()),
52+
0
53+
);
54+
}
55+
56+
#[test]
57+
fn nested_bullet_marker_keeps_parent_continuation_indent() {
58+
let input = "+ outer\n - nested\n";
59+
60+
assert_eq!(continuation_indents(input), [3]);
61+
assert_eq!(indent_len(marker_prefix(input, "-").pre_marker_indent()), 0);
62+
}
63+
64+
#[test]
65+
fn non_one_ordered_marker_does_not_interrupt_paragraph_continuation() {
66+
let input = "+ outer\n 2. still paragraph\n";
67+
68+
assert_eq!(ordered_list_item_count(input), 0);
69+
}
70+
71+
#[test]
72+
fn quote_prefixed_nested_ordered_marker_does_not_steal_parent_indent() {
73+
let input = "> + outer\n> 1. nested\n";
74+
75+
assert_eq!(
76+
indent_len(marker_prefix(input, "1.").pre_marker_indent()),
77+
0
78+
);
79+
}

crates/biome_markdown_parser/tests/md_test_suite/ok/block_quote_ordered_sublist_after_list_item.md.snap

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,17 +52,17 @@ MdDocument {
5252
post_marker_space_token: MD_QUOTE_POST_MARKER_SPACE@12..13 " " [] [],
5353
},
5454
MdContinuationIndent {
55-
indent: MdIndentTokenList [],
55+
indent: MdIndentTokenList [
56+
MdIndentToken {
57+
md_indent_char_token: MD_INDENT_CHAR@13..15 " " [] [],
58+
},
59+
],
5660
},
5761
MdOrderedListItem {
5862
md_bullet_list: MdBulletList [
5963
MdBullet {
6064
prefix: MdListMarkerPrefix {
61-
pre_marker_indent: MdIndentTokenList [
62-
MdIndentToken {
63-
md_indent_char_token: MD_INDENT_CHAR@13..15 " " [] [],
64-
},
65-
],
65+
pre_marker_indent: MdIndentTokenList [],
6666
marker: MD_ORDERED_LIST_MARKER@15..17 "1." [] [],
6767
post_marker_space_token: MD_LIST_POST_MARKER_SPACE@17..18 " " [] [],
6868
content_indent: MdIndentTokenList [],
@@ -126,15 +126,15 @@ MdDocument {
126126
0: MD_QUOTE_INDENT_LIST@11..11
127127
1: R_ANGLE@11..12 ">" [] []
128128
2: MD_QUOTE_POST_MARKER_SPACE@12..13 " " [] []
129-
2: MD_CONTINUATION_INDENT@13..13
130-
0: MD_INDENT_TOKEN_LIST@13..13
131-
3: MD_ORDERED_LIST_ITEM@13..24
132-
0: MD_BULLET_LIST@13..24
133-
0: MD_BULLET@13..24
134-
0: MD_LIST_MARKER_PREFIX@13..18
135-
0: MD_INDENT_TOKEN_LIST@13..15
136-
0: MD_INDENT_TOKEN@13..15
137-
0: MD_INDENT_CHAR@13..15 " " [] []
129+
2: MD_CONTINUATION_INDENT@13..15
130+
0: MD_INDENT_TOKEN_LIST@13..15
131+
0: MD_INDENT_TOKEN@13..15
132+
0: MD_INDENT_CHAR@13..15 " " [] []
133+
3: MD_ORDERED_LIST_ITEM@15..24
134+
0: MD_BULLET_LIST@15..24
135+
0: MD_BULLET@15..24
136+
0: MD_LIST_MARKER_PREFIX@15..18
137+
0: MD_INDENT_TOKEN_LIST@15..15
138138
1: MD_ORDERED_LIST_MARKER@15..17 "1." [] []
139139
2: MD_LIST_POST_MARKER_SPACE@17..18 " " [] []
140140
3: MD_INDENT_TOKEN_LIST@18..18

0 commit comments

Comments
 (0)