diff --git a/crates/biome_markdown_formatter/src/markdown/any/inline.rs b/crates/biome_markdown_formatter/src/markdown/any/inline.rs index 92ad268475b4..6fb02427588b 100644 --- a/crates/biome_markdown_formatter/src/markdown/any/inline.rs +++ b/crates/biome_markdown_formatter/src/markdown/any/inline.rs @@ -12,6 +12,7 @@ impl FormatRule for FormatAnyMdInline { AnyMdInline::MdEntityReference(node) => node.format().fmt(f), AnyMdInline::MdHardLine(node) => node.format().fmt(f), AnyMdInline::MdHtmlBlock(node) => node.format().fmt(f), + AnyMdInline::MdIndentToken(node) => node.format().fmt(f), AnyMdInline::MdInlineCode(node) => node.format().fmt(f), AnyMdInline::MdInlineEmphasis(node) => node.format().fmt(f), AnyMdInline::MdInlineHtml(node) => node.format().fmt(f), diff --git a/crates/biome_markdown_formatter/tests/specs/prettier/markdown/blockquote/code.md.snap b/crates/biome_markdown_formatter/tests/specs/prettier/markdown/blockquote/code.md.snap index 093bba36f044..dbe69bb92d7e 100644 --- a/crates/biome_markdown_formatter/tests/specs/prettier/markdown/blockquote/code.md.snap +++ b/crates/biome_markdown_formatter/tests/specs/prettier/markdown/blockquote/code.md.snap @@ -1,7 +1,9 @@ --- source: crates/biome_formatter_test/src/snapshot_builder.rs +assertion_line: 212 info: markdown/blockquote/code.md --- + # Input ```md @@ -67,31 +69,6 @@ info: markdown/blockquote/code.md >``` ``` -# Errors -``` -code.md:2:2 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Unterminated fenced code block, expected closing triple backticks (```). - - 1 │ > NOTE: To use `unobtrusive`, `unobtrusive/import`, `unobtrusive/react`, and `unobtrusive/flowtype` together, your eslint config would look like this: - > 2 │ >```json - │ ^^^ - 3 │ >{ - 4 │ > "extends": [ - - i code block started here - - 1 │ > NOTE: To use `unobtrusive`, `unobtrusive/import`, `unobtrusive/react`, and `unobtrusive/flowtype` together, your eslint config would look like this: - > 2 │ >```json - │ ^^^ - 3 │ >{ - 4 │ > "extends": [ - - i Add closing triple backticks (```) at the start of a new line. - - -``` - # Lines exceeding max width of 80 characters ``` 1: > NOTE: To use `unobtrusive`, `unobtrusive/import`, `unobtrusive/react`, and `unobtrusive/flowtype` together, your eslint config would look like this: diff --git a/crates/biome_markdown_formatter/tests/specs/prettier/markdown/blockquote/ignore-code.md.snap b/crates/biome_markdown_formatter/tests/specs/prettier/markdown/blockquote/ignore-code.md.snap index f594c74b5da2..6ebb688306e3 100644 --- a/crates/biome_markdown_formatter/tests/specs/prettier/markdown/blockquote/ignore-code.md.snap +++ b/crates/biome_markdown_formatter/tests/specs/prettier/markdown/blockquote/ignore-code.md.snap @@ -1,7 +1,9 @@ --- source: crates/biome_formatter_test/src/snapshot_builder.rs +assertion_line: 212 info: markdown/blockquote/ignore-code.md --- + # Input ```md @@ -142,112 +144,3 @@ info: markdown/blockquote/ignore-code.md > b = 2 > ```` ``` - -# Errors -``` -ignore-code.md:1:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Unterminated fenced code block, expected closing triple backticks (```). - - > 1 │ > ````md - │ ^^^^ - 2 │ > - 3 │ > ```js - - i code block started here - - > 1 │ > ````md - │ ^^^^ - 2 │ > - 3 │ > ```js - - i Add closing triple backticks (```) at the start of a new line. - -ignore-code.md:8:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Unterminated fenced code block, expected closing triple backticks (```). - - 6 │ > ```` - 7 │ - > 8 │ > ```md - │ ^^^ - 9 │ > - 10 │ > - This is a long long - - i code block started here - - 6 │ > ```` - 7 │ - > 8 │ > ```md - │ ^^^ - 9 │ > - 10 │ > - This is a long long - - i Add closing triple backticks (```) at the start of a new line. - -ignore-code.md:16:5 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Unterminated fenced code block, expected closing triple backticks (```). - - 15 │ > - test - > 16 │ > ```md - │ ^^^ - 17 │ > - 18 │ > - This is a long long - - i code block started here - - 15 │ > - test - > 16 │ > ```md - │ ^^^ - 17 │ > - 18 │ > - This is a long long - - i Add closing triple backticks (```) at the start of a new line. - -ignore-code.md:32:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Unterminated fenced code block, expected closing triple backticks (```). - - 30 │ ```` - 31 │ - > 32 │ > ````md - │ ^^^^ - 33 │ > > ```md - 34 │ > > - - i code block started here - - 30 │ ```` - 31 │ - > 32 │ > ````md - │ ^^^^ - 33 │ > > ```md - 34 │ > > - - i Add closing triple backticks (```) at the start of a new line. - -ignore-code.md:48:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - - × Unterminated fenced code block, expected closing triple backticks (```). - - 46 │ >· - 47 │ - > 48 │ > ````js - │ ^^^^ - 49 │ > // biome-ignore format: prettier ignore - 50 │ > const x = 1, - - i code block started here - - 46 │ >· - 47 │ - > 48 │ > ````js - │ ^^^^ - 49 │ > // biome-ignore format: prettier ignore - 50 │ > const x = 1, - - i Add closing triple backticks (```) at the start of a new line. - - -``` diff --git a/crates/biome_markdown_parser/src/syntax/fenced_code_block.rs b/crates/biome_markdown_parser/src/syntax/fenced_code_block.rs index 4ea73ad4ee6b..6cedce534bfc 100644 --- a/crates/biome_markdown_parser/src/syntax/fenced_code_block.rs +++ b/crates/biome_markdown_parser/src/syntax/fenced_code_block.rs @@ -33,6 +33,7 @@ use biome_parser::{ }; use crate::syntax::parse_error::unterminated_fenced_code; +use crate::syntax::quote::try_bump_quote_marker; use crate::syntax::{MAX_BLOCK_PREFIX_INDENT, TAB_STOP_SPACES}; /// Minimum number of fence characters required per CommonMark §4.5. @@ -276,70 +277,190 @@ fn parse_code_content( // Consume all tokens until we see the matching closing fence or EOF while !p.at(T![EOF]) { - if at_line_start && quote_depth > 0 { - let prev_virtual = p.state().virtual_line_start; - p.state_mut().virtual_line_start = Some(p.cur_range().start()); - p.skip_line_indent(MAX_BLOCK_PREFIX_INDENT); - p.state_mut().virtual_line_start = prev_virtual; - - let mut ok = true; - for _ in 0..quote_depth { - if p.at(MD_TEXTUAL_LITERAL) && p.cur_text().starts_with('>') { - p.force_relex_regular(); - } + match prepare_next_code_content_token( + p, + is_tilde_fence, + fence_len, + fence_indent, + quote_depth, + &mut at_line_start, + ) { + CodeContentTokenAction::Break => break, + CodeContentTokenAction::Skip => {} + CodeContentTokenAction::Consume => { + bump_code_textual(p); + at_line_start = false; + } + } + } - if p.at(T![>]) { - p.parse_as_skipped_trivia_tokens(|p| p.bump(T![>])); - } else if p.at(MD_TEXTUAL_LITERAL) && p.cur_text() == ">" { - p.parse_as_skipped_trivia_tokens(|p| p.bump_remap(T![>])); - } else { - ok = false; - break; - } + m.complete(p, MD_INLINE_ITEM_LIST); +} - if p.at(MD_TEXTUAL_LITERAL) { - let text = p.cur_text(); - if text == " " || text == "\t" { - p.parse_as_skipped_trivia_tokens(|p| p.bump(MD_TEXTUAL_LITERAL)); - } - } - } +enum CodeContentTokenAction { + Break, + Skip, + Consume, +} - if !ok { - break; - } - at_line_start = false; - } +/// Prepare the next token inside fenced code block content. +/// +/// Handles quote prefix consumption, newlines, closing fence detection, +/// and indent stripping. Returns an action telling the caller how to +/// proceed in the content loop. +fn prepare_next_code_content_token( + p: &mut MarkdownParser, + is_tilde_fence: bool, + fence_len: usize, + fence_indent: usize, + quote_depth: usize, + at_line_start: &mut bool, +) -> CodeContentTokenAction { + if *at_line_start && quote_depth > 0 && !consume_quote_prefixes_in_code_content(p, quote_depth) + { + return CodeContentTokenAction::Break; + } - if p.at(NEWLINE) { - // Preserve newlines as code content and reset virtual line start. - let text_m = p.start(); - p.bump_remap(MD_TEXTUAL_LITERAL); - text_m.complete(p, MD_TEXTUAL); - p.set_virtual_line_start(); - at_line_start = true; - continue; - } + if consume_code_newline(p) { + *at_line_start = true; + return CodeContentTokenAction::Skip; + } + + if at_closing_fence(p, is_tilde_fence, fence_len) { + return CodeContentTokenAction::Break; + } + if *at_line_start && fence_indent > 0 { + skip_fenced_content_indent(p, fence_indent); if at_closing_fence(p, is_tilde_fence, fence_len) { - break; + return CodeContentTokenAction::Break; } + } - if at_line_start && fence_indent > 0 { - skip_fenced_content_indent(p, fence_indent); - if at_closing_fence(p, is_tilde_fence, fence_len) { - break; + // Prefix/indent consumption above can advance directly to EOF. + if p.at(T![EOF]) { + return CodeContentTokenAction::Break; + } + + CodeContentTokenAction::Consume +} + +/// Consume all expected `>` quote prefixes for the current line inside a +/// fenced code block. +/// +/// Uses a lookahead preflight to verify all `quote_depth` prefixes are +/// present before consuming any. This prevents partial consumption from +/// stealing outer blockquote markers when an inner prefix is missing +/// (e.g., `> hello` inside a depth-2 blockquote would consume the outer +/// `>` but fail on the missing inner `>`, corrupting outer parsing). +/// +/// Returns `true` if all prefixes were consumed successfully, `false` if +/// any prefix is missing — the caller should break out of the content loop. +fn consume_quote_prefixes_in_code_content(p: &mut MarkdownParser, quote_depth: usize) -> bool { + // Preflight: verify all prefixes exist before consuming any. + let all_present = p.lookahead(|p| { + p.skip_line_indent(MAX_BLOCK_PREFIX_INDENT); + for _ in 0..quote_depth { + if p.at(MD_TEXTUAL_LITERAL) && p.cur_text().starts_with('>') { + p.force_relex_regular(); + } + if p.at(T![>]) { + p.bump(T![>]); + } else if p.at(MD_TEXTUAL_LITERAL) && p.cur_text() == ">" { + p.bump(MD_TEXTUAL_LITERAL); + } else { + return false; + } + // Skip optional post-marker space + if p.at(MD_TEXTUAL_LITERAL) { + let text = p.cur_text(); + if text == " " || text == "\t" { + p.bump(MD_TEXTUAL_LITERAL); + } } } + true + }); - // Consume the token as code content (including NEWLINE tokens) - let text_m = p.start(); - p.bump_remap(MD_TEXTUAL_LITERAL); - text_m.complete(p, MD_TEXTUAL); - at_line_start = false; + if !all_present { + return false; } - m.complete(p, MD_INLINE_ITEM_LIST); + let prev_virtual = p.state().virtual_line_start; + p.state_mut().virtual_line_start = Some(p.cur_range().start()); + p.skip_line_indent(MAX_BLOCK_PREFIX_INDENT); + p.state_mut().virtual_line_start = prev_virtual; + + for _ in 0..quote_depth { + let consumed = consume_quote_prefix_in_code_content(p); + debug_assert!(consumed, "preflight verified all prefixes present"); + } + + p.set_virtual_line_start(); + true +} + +/// Consume a single `> ` quote prefix (marker + optional trailing space) +/// inside fenced code block content, emitting it as an `MdQuotePrefix` CST node. +/// +/// Returns `true` if the prefix was consumed, `false` if the current token +/// is not a quote marker. +fn consume_quote_prefix_in_code_content(p: &mut MarkdownParser) -> bool { + if p.at(MD_TEXTUAL_LITERAL) && p.cur_text().starts_with('>') { + p.force_relex_regular(); + } + + if !(p.at(T![>]) || (p.at(MD_TEXTUAL_LITERAL) && p.cur_text() == ">")) { + return false; + } + + let prefix_m = p.start(); + + // Empty pre-marker indent list (initial indent handled by skip_line_indent). + let indent_list_m = p.start(); + indent_list_m.complete(p, MD_QUOTE_INDENT_LIST); + + let marker_bumped = try_bump_quote_marker(p); + debug_assert!( + marker_bumped, + "consume_quote_prefix_in_code_content: quote marker not found after guard confirmed `>` \ + token — check that force_relex_regular and the guard condition are in sync" + ); + if !marker_bumped { + prefix_m.abandon(p); + return false; + } + + // Optional post-marker space + if p.at(MD_TEXTUAL_LITERAL) { + let text = p.cur_text(); + if text == " " || text == "\t" { + p.bump_remap(MD_QUOTE_POST_MARKER_SPACE); + } + } + + prefix_m.complete(p, MD_QUOTE_PREFIX); + true +} + +fn consume_code_newline(p: &mut MarkdownParser) -> bool { + if !p.at(NEWLINE) { + return false; + } + + // Preserve newlines as code content and reset virtual line start. + let text_m = p.start(); + p.bump_remap(MD_TEXTUAL_LITERAL); + text_m.complete(p, MD_TEXTUAL); + p.set_virtual_line_start(); + true +} + +/// Bump the current token as code textual content (`MdTextual` node). +fn bump_code_textual(p: &mut MarkdownParser) { + let text_m = p.start(); + p.bump_remap(MD_TEXTUAL_LITERAL); + text_m.complete(p, MD_TEXTUAL); } pub(crate) fn info_string_has_backtick(p: &mut MarkdownParser) -> bool { @@ -390,7 +511,9 @@ fn skip_fenced_content_indent(p: &mut MarkdownParser, indent: usize) { } consumed += width; - p.parse_as_skipped_trivia_tokens(|p| p.bump(MD_TEXTUAL_LITERAL)); + let char_m = p.start(); + p.bump_remap(MD_INDENT_CHAR); + char_m.complete(p, MD_INDENT_TOKEN); } } @@ -399,9 +522,12 @@ fn line_has_closing_fence(p: &MarkdownParser, is_tilde_fence: bool, fence_len: u return false; }; - let line_start = find_line_start(&source[..start]); + let line_start: usize = match p.state().virtual_line_start { + Some(virtual_start) => virtual_start.into(), + None => find_line_start(&source[..start]), + }; - if !is_whitespace_prefix(source, start, line_start) { + if line_start != start && !is_whitespace_prefix(source, start, line_start) { return false; } diff --git a/crates/biome_markdown_parser/src/syntax/quote.rs b/crates/biome_markdown_parser/src/syntax/quote.rs index 49ee3043bb18..9d7e71ea5de5 100644 --- a/crates/biome_markdown_parser/src/syntax/quote.rs +++ b/crates/biome_markdown_parser/src/syntax/quote.rs @@ -128,54 +128,101 @@ fn emit_quote_prefix_node(p: &mut MarkdownParser) -> bool { /// Emit one quote prefix token sequence: [indent?] `>` [optional space/tab]. /// /// Returns whether a post-marker separator was consumed. -fn emit_quote_prefix_tokens(p: &mut MarkdownParser, use_virtual_line_start: bool) -> Option { - let saved_virtual = if use_virtual_line_start { +pub(crate) fn emit_quote_prefix_tokens( + p: &mut MarkdownParser, + use_virtual_line_start: bool, +) -> Option { + let saved_virtual = save_virtual_line_start_if_needed(p, use_virtual_line_start); + + emit_quote_pre_marker_indents(p); + + restore_virtual_line_start_if_needed(p, saved_virtual); + + if !try_bump_quote_marker(p) { + return None; + } + + let has_indented_code = at_quote_indented_code_start(p); + let marker_space = emit_post_marker_space(p, has_indented_code); + Some(marker_space) +} + +/// Save virtual line start position if requested, returning the previous value. +fn save_virtual_line_start_if_needed( + p: &mut MarkdownParser, + use_virtual_line_start: bool, +) -> Option> { + if use_virtual_line_start { let prev = p.state().virtual_line_start; p.state_mut().virtual_line_start = Some(p.cur_range().start()); Some(prev) } else { None - }; + } +} + +/// Restore virtual line start to saved value. +fn restore_virtual_line_start_if_needed( + p: &mut MarkdownParser, + saved_virtual: Option>, +) { + if let Some(prev) = saved_virtual { + p.state_mut().virtual_line_start = prev; + } +} +/// Emit pre-marker indentation (0-3 spaces/tabs) as MD_QUOTE_INDENT nodes. +fn emit_quote_pre_marker_indents(p: &mut MarkdownParser) { // Direct bounded scan (0-3 cols per CommonMark §5.1): simpler than ParseNodeList // here because we immediately validate `>` and keep this path no-recovery. let indent_list_m = p.start(); let mut consumed = 0usize; + while p.at(MD_TEXTUAL_LITERAL) { let text = p.cur_text(); - if text.is_empty() || !text.chars().all(|c| c == ' ' || c == '\t') { + if !is_whitespace_only(text) { break; } - // Tabs expand to tab-stop width (CommonMark §2.2). - let indent: usize = text - .chars() - .map(|c| if c == '\t' { TAB_STOP_SPACES } else { 1 }) - .sum(); + + let indent = calculate_indent_width(text); if consumed + indent > MAX_BLOCK_PREFIX_INDENT { break; } + consumed += indent; let indent_m = p.start(); p.bump_remap(MD_QUOTE_PRE_MARKER_INDENT); indent_m.complete(p, MD_QUOTE_INDENT); } + indent_list_m.complete(p, MD_QUOTE_INDENT_LIST); +} - if let Some(prev) = saved_virtual { - p.state_mut().virtual_line_start = prev; - } +/// Check if text contains only spaces and tabs. +fn is_whitespace_only(text: &str) -> bool { + !text.is_empty() && text.chars().all(|c| c == ' ' || c == '\t') +} + +/// Calculate indent width accounting for tab expansion (CommonMark §2.2). +fn calculate_indent_width(text: &str) -> usize { + text.chars() + .map(|c| if c == '\t' { TAB_STOP_SPACES } else { 1 }) + .sum() +} +/// Try to consume a `>` marker token. +/// +/// Returns `true` if a marker was consumed, `false` otherwise. +pub(crate) fn try_bump_quote_marker(p: &mut MarkdownParser) -> bool { if p.at(T![>]) { p.bump(T![>]); + true } else if p.at(MD_TEXTUAL_LITERAL) && p.cur_text() == ">" { p.bump_remap(T![>]); + true } else { - return None; + false } - - let has_indented_code = at_quote_indented_code_start(p); - let marker_space = emit_post_marker_space(p, has_indented_code); - Some(marker_space) } /// Consume the optional space after `>` as an `MD_QUOTE_POST_MARKER_SPACE` token. @@ -220,66 +267,73 @@ impl QuoteBlockList { line_started_with_prefix: true, // First line implicitly has prefix } } -} - -impl ParseNodeList for QuoteBlockList { - type Kind = MarkdownSyntaxKind; - type Parser<'source> = MarkdownParser<'source>; - - const LIST_KIND: Self::Kind = MD_BLOCK_LIST; - - fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { - if p.state().quote_depth_exceeded { - p.state_mut().quote_depth_exceeded = false; - return Absent; - } + /// Handle line start and quote prefix detection. + /// + /// Returns `false` if parsing should stop (no prefix found when required). + fn handle_line_start_prefix(&mut self, p: &mut MarkdownParser) -> bool { self.line_started_with_prefix = self.first_line; + if !self.first_line && !p.at(NEWLINE) && (p.at_line_start() || p.has_preceding_line_break()) { if has_quote_prefix(p, self.depth) { consume_quote_prefix(p, self.depth); self.line_started_with_prefix = true; } else { - return Absent; + return false; } } + self.first_line = false; + true + } - if p.at(NEWLINE) { - if !self.line_started_with_prefix && line_has_quote_prefix_at_current(p, self.depth) { - self.line_started_with_prefix = true; - } - if (p.at_blank_line() || has_empty_line_before(p) || self.last_block_was_paragraph) - && !self.line_started_with_prefix - { - return Absent; - } - if !self.line_started_with_prefix { - let has_next_prefix = p.lookahead(|p| { - p.bump(NEWLINE); - has_quote_prefix(p, self.depth) - }); - if !has_next_prefix { - return Absent; - } - } - let text_m = p.start(); - p.bump(NEWLINE); - return Present(text_m.complete(p, MD_NEWLINE)); + /// Handle newline parsing with lazy continuation checks. + fn handle_newline(&mut self, p: &mut MarkdownParser) -> ParsedSyntax { + // Update prefix status if we can see a prefix at current position + if !self.line_started_with_prefix && line_has_quote_prefix_at_current(p, self.depth) { + self.line_started_with_prefix = true; } - if at_quote_indented_code_start(p) { - let parsed = parse_quote_indented_code_block(p, self.depth); - self.last_block_was_paragraph = false; - return parsed; + // Check if lazy continuation should stop + if self.should_stop_lazy_continuation(p) { + return Absent; } + // If no prefix on this line, check if next line has one + if !self.line_started_with_prefix && !self.next_line_has_prefix(p) { + return Absent; + } + + let text_m = p.start(); + p.bump(NEWLINE); + Present(text_m.complete(p, MD_NEWLINE)) + } + + /// Check if lazy continuation should stop. + fn should_stop_lazy_continuation(&self, p: &MarkdownParser) -> bool { + (p.at_blank_line() || has_empty_line_before(p) || self.last_block_was_paragraph) + && !self.line_started_with_prefix + } + + /// Check if the next line (after newline) has a quote prefix. + fn next_line_has_prefix(&self, p: &mut MarkdownParser) -> bool { + p.lookahead(|p| { + p.bump(NEWLINE); + has_quote_prefix(p, self.depth) + }) + } + + /// Parse a block element with virtual line start set. + fn parse_block_with_virtual_line_start(&mut self, p: &mut MarkdownParser) -> ParsedSyntax { // Treat content after '>' as column 0 for block parsing (fence detection). let prev_virtual = p.state().virtual_line_start; p.state_mut().virtual_line_start = Some(p.cur_range().start()); + let parsed = parse_any_block_with_indent_code_policy(p, true); + p.state_mut().virtual_line_start = prev_virtual; + if let Present(ref marker) = parsed { self.last_block_was_paragraph = is_paragraph_like(marker.kind(p)); } else { @@ -288,6 +342,36 @@ impl ParseNodeList for QuoteBlockList { parsed } +} + +impl ParseNodeList for QuoteBlockList { + type Kind = MarkdownSyntaxKind; + type Parser<'source> = MarkdownParser<'source>; + + const LIST_KIND: Self::Kind = MD_BLOCK_LIST; + + fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { + if p.state().quote_depth_exceeded { + p.state_mut().quote_depth_exceeded = false; + return Absent; + } + + if !self.handle_line_start_prefix(p) { + return Absent; + } + + if p.at(NEWLINE) { + return self.handle_newline(p); + } + + if at_quote_indented_code_start(p) { + let parsed = parse_quote_indented_code_block(p, self.depth); + self.last_block_was_paragraph = false; + return parsed; + } + + self.parse_block_with_virtual_line_start(p) + } fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { p.at(T![EOF]) || p.state().quote_depth_exceeded @@ -322,35 +406,71 @@ pub(crate) fn line_has_quote_prefix_at_current(p: &MarkdownParser, depth: usize) let line_start = source[..start].rfind('\n').map_or(0, |idx| idx + 1); let mut idx = line_start; + + // Skip leading indentation (0-3 spaces/tabs) + if !skip_line_leading_indent(source.as_bytes(), &mut idx, start) { + return false; + } + + // Check for required number of quote markers + check_quote_markers_at_position(source.as_bytes(), &mut idx, start, depth) +} + +/// Skip leading indentation on a line, respecting MAX_BLOCK_PREFIX_INDENT. +/// +/// Returns `false` if indentation exceeds the limit. +fn skip_line_leading_indent(source: &[u8], idx: &mut usize, limit: usize) -> bool { let mut indent = 0usize; - while idx < start { - match source.as_bytes()[idx] { + + while *idx < limit { + match source[*idx] { b' ' => { indent += 1; - idx += 1; + *idx += 1; } b'\t' => { indent += TAB_STOP_SPACES; - idx += 1; + *idx += 1; } _ => break, } + if indent > MAX_BLOCK_PREFIX_INDENT { return false; } } + true +} + +/// Check if the required number of quote markers (`>`) exist at the current position. +fn check_quote_markers_at_position( + source: &[u8], + idx: &mut usize, + limit: usize, + depth: usize, +) -> bool { for _ in 0..depth { - if idx >= start || source.as_bytes()[idx] != b'>' { + if !try_consume_quote_marker(source, idx, limit) { return false; } - idx += 1; - if idx < start { - let c = source.as_bytes()[idx]; - if c == b' ' || c == b'\t' { - idx += 1; - } - } + } + true +} + +/// Try to consume a single quote marker (`>`) with optional trailing space/tab. +/// +/// Returns `false` if no marker is found. +fn try_consume_quote_marker(source: &[u8], idx: &mut usize, limit: usize) -> bool { + if *idx >= limit || source[*idx] != b'>' { + return false; + } + + *idx += 1; + + // Skip optional space or tab after `>` + if *idx < limit && (source[*idx] == b' ' || source[*idx] == b'\t') { + *idx += 1; } true @@ -389,37 +509,52 @@ fn parse_quote_indented_code_block(p: &mut MarkdownParser, depth: usize) -> Pars } if p.at(NEWLINE) { - let text_m = p.start(); - p.bump_remap(MD_TEXTUAL_LITERAL); - text_m.complete(p, MD_TEXTUAL); - - if p.at(T![EOF]) { - break; - } - - if !has_quote_prefix(p, depth) { - break; - } - consume_quote_prefix(p, depth); - - if p.at(NEWLINE) { - continue; - } - if !at_quote_indented_code_start(p) { + if !parse_code_block_newline(p, depth) { break; } - continue; + } else { + parse_code_block_textual(p); } - - let text_m = p.start(); - p.bump_remap(MD_TEXTUAL_LITERAL); - text_m.complete(p, MD_TEXTUAL); } content.complete(p, MD_INLINE_ITEM_LIST); Present(m.complete(p, MD_INDENT_CODE_BLOCK)) } +/// Parse a newline in an indented code block within a quote. +/// +/// Returns `false` if the code block should end (no more indented lines). +fn parse_code_block_newline(p: &mut MarkdownParser, depth: usize) -> bool { + let text_m = p.start(); + p.bump_remap(MD_TEXTUAL_LITERAL); + text_m.complete(p, MD_TEXTUAL); + + if p.at(T![EOF]) { + return false; + } + + if !has_quote_prefix(p, depth) { + return false; + } + + consume_quote_prefix(p, depth); + + // Blank lines (consecutive newlines) are allowed in indented code + if p.at(NEWLINE) { + return true; + } + + // Next line must still be indented to continue the code block + at_quote_indented_code_start(p) +} + +/// Parse a single textual token in an indented code block. +fn parse_code_block_textual(p: &mut MarkdownParser) { + let text_m = p.start(); + p.bump_remap(MD_TEXTUAL_LITERAL); + text_m.complete(p, MD_TEXTUAL); +} + pub(crate) fn skip_optional_marker_space(p: &mut MarkdownParser, preserve_tab: bool) -> bool { if !p.at(MD_TEXTUAL_LITERAL) { return false; diff --git a/crates/biome_markdown_parser/src/to_html.rs b/crates/biome_markdown_parser/src/to_html.rs index 981cc0c3e5e4..10aa9d72994d 100644 --- a/crates/biome_markdown_parser/src/to_html.rs +++ b/crates/biome_markdown_parser/src/to_html.rs @@ -1699,6 +1699,9 @@ fn extract_alt_text_inline(inline: &AnyMdInline, ctx: &HtmlRenderContext, out: & AnyMdInline::MdQuotePrefix(_) => { // Quote prefixes don't contribute text to alt attributes } + AnyMdInline::MdIndentToken(_) => { + // Indent tokens don't contribute text to alt attributes + } } } diff --git a/crates/biome_markdown_parser/tests/md_test_suite/error/fenced_code_blockquote_eof_after_prefix.md b/crates/biome_markdown_parser/tests/md_test_suite/error/fenced_code_blockquote_eof_after_prefix.md new file mode 100644 index 000000000000..2e03faf21f4d --- /dev/null +++ b/crates/biome_markdown_parser/tests/md_test_suite/error/fenced_code_blockquote_eof_after_prefix.md @@ -0,0 +1,2 @@ +> ``` +> \ No newline at end of file diff --git a/crates/biome_markdown_parser/tests/md_test_suite/error/fenced_code_blockquote_eof_after_prefix.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/error/fenced_code_blockquote_eof_after_prefix.md.snap new file mode 100644 index 000000000000..588569e8f135 --- /dev/null +++ b/crates/biome_markdown_parser/tests/md_test_suite/error/fenced_code_blockquote_eof_after_prefix.md.snap @@ -0,0 +1,95 @@ +--- +source: crates/biome_markdown_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +``` +> ``` +> +``` + + +## AST + +``` +MdDocument { + bom_token: missing (optional), + value: MdBlockList [ + MdQuote { + prefix: MdQuotePrefix { + pre_marker_indent: MdQuoteIndentList [], + marker_token: R_ANGLE@0..1 ">" [] [], + post_marker_space_token: MD_QUOTE_POST_MARKER_SPACE@1..2 " " [] [], + }, + content: MdBlockList [ + MdFencedCodeBlock { + l_fence: TRIPLE_BACKTICK@2..5 "```" [] [], + code_list: MdCodeNameList [], + content: MdInlineItemList [ + MdTextual { + value_token: MD_TEXTUAL_LITERAL@5..6 "\n" [] [], + }, + MdQuotePrefix { + pre_marker_indent: MdQuoteIndentList [], + marker_token: R_ANGLE@6..7 ">" [] [], + post_marker_space_token: MD_QUOTE_POST_MARKER_SPACE@7..8 " " [] [], + }, + ], + r_fence: missing (required), + }, + ], + }, + ], + eof_token: EOF@8..8 "" [] [], +} +``` + +## CST + +``` +0: MD_DOCUMENT@0..8 + 0: (empty) + 1: MD_BLOCK_LIST@0..8 + 0: MD_QUOTE@0..8 + 0: MD_QUOTE_PREFIX@0..2 + 0: MD_QUOTE_INDENT_LIST@0..0 + 1: R_ANGLE@0..1 ">" [] [] + 2: MD_QUOTE_POST_MARKER_SPACE@1..2 " " [] [] + 1: MD_BLOCK_LIST@2..8 + 0: MD_FENCED_CODE_BLOCK@2..8 + 0: TRIPLE_BACKTICK@2..5 "```" [] [] + 1: MD_CODE_NAME_LIST@5..5 + 2: MD_INLINE_ITEM_LIST@5..8 + 0: MD_TEXTUAL@5..6 + 0: MD_TEXTUAL_LITERAL@5..6 "\n" [] [] + 1: MD_QUOTE_PREFIX@6..8 + 0: MD_QUOTE_INDENT_LIST@6..6 + 1: R_ANGLE@6..7 ">" [] [] + 2: MD_QUOTE_POST_MARKER_SPACE@7..8 " " [] [] + 3: (empty) + 2: EOF@8..8 "" [] [] + +``` + +## Diagnostics + +``` +fenced_code_blockquote_eof_after_prefix.md:1:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unterminated fenced code block, expected closing triple backticks (```). + + > 1 │ > ``` + │ ^^^ + 2 │ >· + + i code block started here + + > 1 │ > ``` + │ ^^^ + 2 │ >· + + i Add closing triple backticks (```) at the start of a new line. + +``` diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/fenced_code_advanced.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/fenced_code_advanced.md.snap index 9ef15e62f4b4..9c18231d6139 100644 --- a/crates/biome_markdown_parser/tests/md_test_suite/ok/fenced_code_advanced.md.snap +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/fenced_code_advanced.md.snap @@ -378,8 +378,14 @@ MdDocument { MdTextual { value_token: MD_TEXTUAL_LITERAL@341..342 "\n" [] [], }, + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@342..343 " " [] [], + }, + MdIndentToken { + md_indent_char_token: MD_INDENT_CHAR@343..344 " " [] [], + }, MdTextual { - value_token: MD_TEXTUAL_LITERAL@342..353 "code line" [Skipped(" "), Skipped(" ")] [], + value_token: MD_TEXTUAL_LITERAL@344..353 "code line" [] [], }, MdTextual { value_token: MD_TEXTUAL_LITERAL@353..354 "\n" [] [], @@ -618,9 +624,13 @@ MdDocument { 2: MD_INLINE_ITEM_LIST@341..354 0: MD_TEXTUAL@341..342 0: MD_TEXTUAL_LITERAL@341..342 "\n" [] [] - 1: MD_TEXTUAL@342..353 - 0: MD_TEXTUAL_LITERAL@342..353 "code line" [Skipped(" "), Skipped(" ")] [] - 2: MD_TEXTUAL@353..354 + 1: MD_INDENT_TOKEN@342..343 + 0: MD_INDENT_CHAR@342..343 " " [] [] + 2: MD_INDENT_TOKEN@343..344 + 0: MD_INDENT_CHAR@343..344 " " [] [] + 3: MD_TEXTUAL@344..353 + 0: MD_TEXTUAL_LITERAL@344..353 "code line" [] [] + 4: MD_TEXTUAL@353..354 0: MD_TEXTUAL_LITERAL@353..354 "\n" [] [] 3: TRIPLE_BACKTICK@354..359 "```" [Skipped(" "), Skipped(" ")] [] 26: MD_NEWLINE@359..360 diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/fenced_code_in_blockquote.md b/crates/biome_markdown_parser/tests/md_test_suite/ok/fenced_code_in_blockquote.md new file mode 100644 index 000000000000..ec7a35ef2e00 --- /dev/null +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/fenced_code_in_blockquote.md @@ -0,0 +1,7 @@ +> ```rust +> fn main() {} +> ``` + +> > ``` +> > nested +> > ``` diff --git a/crates/biome_markdown_parser/tests/md_test_suite/ok/fenced_code_in_blockquote.md.snap b/crates/biome_markdown_parser/tests/md_test_suite/ok/fenced_code_in_blockquote.md.snap new file mode 100644 index 000000000000..343c19da5373 --- /dev/null +++ b/crates/biome_markdown_parser/tests/md_test_suite/ok/fenced_code_in_blockquote.md.snap @@ -0,0 +1,228 @@ +--- +source: crates/biome_markdown_parser/tests/spec_test.rs +expression: snapshot +--- + +## Input + +``` +> ```rust +> fn main() {} +> ``` + +> > ``` +> > nested +> > ``` + +``` + + +## AST + +``` +MdDocument { + bom_token: missing (optional), + value: MdBlockList [ + MdQuote { + prefix: MdQuotePrefix { + pre_marker_indent: MdQuoteIndentList [], + marker_token: R_ANGLE@0..1 ">" [] [], + post_marker_space_token: MD_QUOTE_POST_MARKER_SPACE@1..2 " " [] [], + }, + content: MdBlockList [ + MdFencedCodeBlock { + l_fence: TRIPLE_BACKTICK@2..5 "```" [] [], + code_list: MdCodeNameList [ + MdTextual { + value_token: MD_TEXTUAL_LITERAL@5..9 "rust" [] [], + }, + ], + content: MdInlineItemList [ + MdTextual { + value_token: MD_TEXTUAL_LITERAL@9..10 "\n" [] [], + }, + MdQuotePrefix { + pre_marker_indent: MdQuoteIndentList [], + marker_token: R_ANGLE@10..11 ">" [] [], + post_marker_space_token: MD_QUOTE_POST_MARKER_SPACE@11..12 " " [] [], + }, + MdTextual { + value_token: MD_TEXTUAL_LITERAL@12..19 "fn main" [] [], + }, + MdTextual { + value_token: MD_TEXTUAL_LITERAL@19..20 "(" [] [], + }, + MdTextual { + value_token: MD_TEXTUAL_LITERAL@20..21 ")" [] [], + }, + MdTextual { + value_token: MD_TEXTUAL_LITERAL@21..24 " {}" [] [], + }, + MdTextual { + value_token: MD_TEXTUAL_LITERAL@24..25 "\n" [] [], + }, + MdQuotePrefix { + pre_marker_indent: MdQuoteIndentList [], + marker_token: R_ANGLE@25..26 ">" [] [], + post_marker_space_token: MD_QUOTE_POST_MARKER_SPACE@26..27 " " [] [], + }, + ], + r_fence: TRIPLE_BACKTICK@27..30 "```" [] [], + }, + MdNewline { + value_token: NEWLINE@30..31 "\n" [] [], + }, + ], + }, + MdNewline { + value_token: NEWLINE@31..32 "\n" [] [], + }, + MdQuote { + prefix: MdQuotePrefix { + pre_marker_indent: MdQuoteIndentList [], + marker_token: R_ANGLE@32..33 ">" [] [], + post_marker_space_token: MD_QUOTE_POST_MARKER_SPACE@33..34 " " [] [], + }, + content: MdBlockList [ + MdQuote { + prefix: MdQuotePrefix { + pre_marker_indent: MdQuoteIndentList [], + marker_token: R_ANGLE@34..35 ">" [] [], + post_marker_space_token: MD_QUOTE_POST_MARKER_SPACE@35..36 " " [] [], + }, + content: MdBlockList [ + MdFencedCodeBlock { + l_fence: TRIPLE_BACKTICK@36..39 "```" [] [], + code_list: MdCodeNameList [], + content: MdInlineItemList [ + MdTextual { + value_token: MD_TEXTUAL_LITERAL@39..40 "\n" [] [], + }, + MdQuotePrefix { + pre_marker_indent: MdQuoteIndentList [], + marker_token: R_ANGLE@40..41 ">" [] [], + post_marker_space_token: MD_QUOTE_POST_MARKER_SPACE@41..42 " " [] [], + }, + MdQuotePrefix { + pre_marker_indent: MdQuoteIndentList [], + marker_token: R_ANGLE@42..43 ">" [] [], + post_marker_space_token: MD_QUOTE_POST_MARKER_SPACE@43..44 " " [] [], + }, + MdTextual { + value_token: MD_TEXTUAL_LITERAL@44..50 "nested" [] [], + }, + MdTextual { + value_token: MD_TEXTUAL_LITERAL@50..51 "\n" [] [], + }, + MdQuotePrefix { + pre_marker_indent: MdQuoteIndentList [], + marker_token: R_ANGLE@51..52 ">" [] [], + post_marker_space_token: MD_QUOTE_POST_MARKER_SPACE@52..53 " " [] [], + }, + MdQuotePrefix { + pre_marker_indent: MdQuoteIndentList [], + marker_token: R_ANGLE@53..54 ">" [] [], + post_marker_space_token: MD_QUOTE_POST_MARKER_SPACE@54..55 " " [] [], + }, + ], + r_fence: TRIPLE_BACKTICK@55..58 "```" [] [], + }, + MdNewline { + value_token: NEWLINE@58..59 "\n" [] [], + }, + ], + }, + ], + }, + ], + eof_token: EOF@59..59 "" [] [], +} +``` + +## CST + +``` +0: MD_DOCUMENT@0..59 + 0: (empty) + 1: MD_BLOCK_LIST@0..59 + 0: MD_QUOTE@0..31 + 0: MD_QUOTE_PREFIX@0..2 + 0: MD_QUOTE_INDENT_LIST@0..0 + 1: R_ANGLE@0..1 ">" [] [] + 2: MD_QUOTE_POST_MARKER_SPACE@1..2 " " [] [] + 1: MD_BLOCK_LIST@2..31 + 0: MD_FENCED_CODE_BLOCK@2..30 + 0: TRIPLE_BACKTICK@2..5 "```" [] [] + 1: MD_CODE_NAME_LIST@5..9 + 0: MD_TEXTUAL@5..9 + 0: MD_TEXTUAL_LITERAL@5..9 "rust" [] [] + 2: MD_INLINE_ITEM_LIST@9..27 + 0: MD_TEXTUAL@9..10 + 0: MD_TEXTUAL_LITERAL@9..10 "\n" [] [] + 1: MD_QUOTE_PREFIX@10..12 + 0: MD_QUOTE_INDENT_LIST@10..10 + 1: R_ANGLE@10..11 ">" [] [] + 2: MD_QUOTE_POST_MARKER_SPACE@11..12 " " [] [] + 2: MD_TEXTUAL@12..19 + 0: MD_TEXTUAL_LITERAL@12..19 "fn main" [] [] + 3: MD_TEXTUAL@19..20 + 0: MD_TEXTUAL_LITERAL@19..20 "(" [] [] + 4: MD_TEXTUAL@20..21 + 0: MD_TEXTUAL_LITERAL@20..21 ")" [] [] + 5: MD_TEXTUAL@21..24 + 0: MD_TEXTUAL_LITERAL@21..24 " {}" [] [] + 6: MD_TEXTUAL@24..25 + 0: MD_TEXTUAL_LITERAL@24..25 "\n" [] [] + 7: MD_QUOTE_PREFIX@25..27 + 0: MD_QUOTE_INDENT_LIST@25..25 + 1: R_ANGLE@25..26 ">" [] [] + 2: MD_QUOTE_POST_MARKER_SPACE@26..27 " " [] [] + 3: TRIPLE_BACKTICK@27..30 "```" [] [] + 1: MD_NEWLINE@30..31 + 0: NEWLINE@30..31 "\n" [] [] + 1: MD_NEWLINE@31..32 + 0: NEWLINE@31..32 "\n" [] [] + 2: MD_QUOTE@32..59 + 0: MD_QUOTE_PREFIX@32..34 + 0: MD_QUOTE_INDENT_LIST@32..32 + 1: R_ANGLE@32..33 ">" [] [] + 2: MD_QUOTE_POST_MARKER_SPACE@33..34 " " [] [] + 1: MD_BLOCK_LIST@34..59 + 0: MD_QUOTE@34..59 + 0: MD_QUOTE_PREFIX@34..36 + 0: MD_QUOTE_INDENT_LIST@34..34 + 1: R_ANGLE@34..35 ">" [] [] + 2: MD_QUOTE_POST_MARKER_SPACE@35..36 " " [] [] + 1: MD_BLOCK_LIST@36..59 + 0: MD_FENCED_CODE_BLOCK@36..58 + 0: TRIPLE_BACKTICK@36..39 "```" [] [] + 1: MD_CODE_NAME_LIST@39..39 + 2: MD_INLINE_ITEM_LIST@39..55 + 0: MD_TEXTUAL@39..40 + 0: MD_TEXTUAL_LITERAL@39..40 "\n" [] [] + 1: MD_QUOTE_PREFIX@40..42 + 0: MD_QUOTE_INDENT_LIST@40..40 + 1: R_ANGLE@40..41 ">" [] [] + 2: MD_QUOTE_POST_MARKER_SPACE@41..42 " " [] [] + 2: MD_QUOTE_PREFIX@42..44 + 0: MD_QUOTE_INDENT_LIST@42..42 + 1: R_ANGLE@42..43 ">" [] [] + 2: MD_QUOTE_POST_MARKER_SPACE@43..44 " " [] [] + 3: MD_TEXTUAL@44..50 + 0: MD_TEXTUAL_LITERAL@44..50 "nested" [] [] + 4: MD_TEXTUAL@50..51 + 0: MD_TEXTUAL_LITERAL@50..51 "\n" [] [] + 5: MD_QUOTE_PREFIX@51..53 + 0: MD_QUOTE_INDENT_LIST@51..51 + 1: R_ANGLE@51..52 ">" [] [] + 2: MD_QUOTE_POST_MARKER_SPACE@52..53 " " [] [] + 6: MD_QUOTE_PREFIX@53..55 + 0: MD_QUOTE_INDENT_LIST@53..53 + 1: R_ANGLE@53..54 ">" [] [] + 2: MD_QUOTE_POST_MARKER_SPACE@54..55 " " [] [] + 3: TRIPLE_BACKTICK@55..58 "```" [] [] + 1: MD_NEWLINE@58..59 + 0: NEWLINE@58..59 "\n" [] [] + 2: EOF@59..59 "" [] [] + +``` diff --git a/crates/biome_markdown_parser/tests/spec_test.rs b/crates/biome_markdown_parser/tests/spec_test.rs index 796833429571..ee937f3ed803 100644 --- a/crates/biome_markdown_parser/tests/spec_test.rs +++ b/crates/biome_markdown_parser/tests/spec_test.rs @@ -193,4 +193,9 @@ pub fn quick_test() { "![a & b < c](url)\n", "

\"a

\n", ); + test_example( + 9992, + "> ```\n> hello\n> ```\n", + "
\n
hello\n
\n
\n", + ); } diff --git a/crates/biome_markdown_syntax/src/generated/nodes.rs b/crates/biome_markdown_syntax/src/generated/nodes.rs index 65255547e295..f81d76aedadc 100644 --- a/crates/biome_markdown_syntax/src/generated/nodes.rs +++ b/crates/biome_markdown_syntax/src/generated/nodes.rs @@ -1711,6 +1711,7 @@ pub enum AnyMdInline { MdEntityReference(MdEntityReference), MdHardLine(MdHardLine), MdHtmlBlock(MdHtmlBlock), + MdIndentToken(MdIndentToken), MdInlineCode(MdInlineCode), MdInlineEmphasis(MdInlineEmphasis), MdInlineHtml(MdInlineHtml), @@ -1748,6 +1749,12 @@ impl AnyMdInline { _ => None, } } + pub fn as_md_indent_token(&self) -> Option<&MdIndentToken> { + match &self { + Self::MdIndentToken(item) => Some(item), + _ => None, + } + } pub fn as_md_inline_code(&self) -> Option<&MdInlineCode> { match &self { Self::MdInlineCode(item) => Some(item), @@ -4118,6 +4125,11 @@ impl From for AnyMdInline { Self::MdHtmlBlock(node) } } +impl From for AnyMdInline { + fn from(node: MdIndentToken) -> Self { + Self::MdIndentToken(node) + } +} impl From for AnyMdInline { fn from(node: MdInlineCode) -> Self { Self::MdInlineCode(node) @@ -4179,6 +4191,7 @@ impl AstNode for AnyMdInline { .union(MdEntityReference::KIND_SET) .union(MdHardLine::KIND_SET) .union(MdHtmlBlock::KIND_SET) + .union(MdIndentToken::KIND_SET) .union(MdInlineCode::KIND_SET) .union(MdInlineEmphasis::KIND_SET) .union(MdInlineHtml::KIND_SET) @@ -4197,6 +4210,7 @@ impl AstNode for AnyMdInline { | MD_ENTITY_REFERENCE | MD_HARD_LINE | MD_HTML_BLOCK + | MD_INDENT_TOKEN | MD_INLINE_CODE | MD_INLINE_EMPHASIS | MD_INLINE_HTML @@ -4216,6 +4230,7 @@ impl AstNode for AnyMdInline { MD_ENTITY_REFERENCE => Self::MdEntityReference(MdEntityReference { syntax }), MD_HARD_LINE => Self::MdHardLine(MdHardLine { syntax }), MD_HTML_BLOCK => Self::MdHtmlBlock(MdHtmlBlock { syntax }), + MD_INDENT_TOKEN => Self::MdIndentToken(MdIndentToken { syntax }), MD_INLINE_CODE => Self::MdInlineCode(MdInlineCode { syntax }), MD_INLINE_EMPHASIS => Self::MdInlineEmphasis(MdInlineEmphasis { syntax }), MD_INLINE_HTML => Self::MdInlineHtml(MdInlineHtml { syntax }), @@ -4237,6 +4252,7 @@ impl AstNode for AnyMdInline { Self::MdEntityReference(it) => it.syntax(), Self::MdHardLine(it) => it.syntax(), Self::MdHtmlBlock(it) => it.syntax(), + Self::MdIndentToken(it) => it.syntax(), Self::MdInlineCode(it) => it.syntax(), Self::MdInlineEmphasis(it) => it.syntax(), Self::MdInlineHtml(it) => it.syntax(), @@ -4256,6 +4272,7 @@ impl AstNode for AnyMdInline { Self::MdEntityReference(it) => it.into_syntax(), Self::MdHardLine(it) => it.into_syntax(), Self::MdHtmlBlock(it) => it.into_syntax(), + Self::MdIndentToken(it) => it.into_syntax(), Self::MdInlineCode(it) => it.into_syntax(), Self::MdInlineEmphasis(it) => it.into_syntax(), Self::MdInlineHtml(it) => it.into_syntax(), @@ -4277,6 +4294,7 @@ impl std::fmt::Debug for AnyMdInline { Self::MdEntityReference(it) => std::fmt::Debug::fmt(it, f), Self::MdHardLine(it) => std::fmt::Debug::fmt(it, f), Self::MdHtmlBlock(it) => std::fmt::Debug::fmt(it, f), + Self::MdIndentToken(it) => std::fmt::Debug::fmt(it, f), Self::MdInlineCode(it) => std::fmt::Debug::fmt(it, f), Self::MdInlineEmphasis(it) => std::fmt::Debug::fmt(it, f), Self::MdInlineHtml(it) => std::fmt::Debug::fmt(it, f), @@ -4298,6 +4316,7 @@ impl From for SyntaxNode { AnyMdInline::MdEntityReference(it) => it.into_syntax(), AnyMdInline::MdHardLine(it) => it.into_syntax(), AnyMdInline::MdHtmlBlock(it) => it.into_syntax(), + AnyMdInline::MdIndentToken(it) => it.into_syntax(), AnyMdInline::MdInlineCode(it) => it.into_syntax(), AnyMdInline::MdInlineEmphasis(it) => it.into_syntax(), AnyMdInline::MdInlineHtml(it) => it.into_syntax(), diff --git a/xtask/codegen/markdown.ungram b/xtask/codegen/markdown.ungram index 0101c8ac1288..026d1acecd80 100644 --- a/xtask/codegen/markdown.ungram +++ b/xtask/codegen/markdown.ungram @@ -231,6 +231,7 @@ AnyMdInline = | MdSoftBreak | MdTextual | MdQuotePrefix + | MdIndentToken // *italic*