diff --git a/.changeset/light-toys-check.md b/.changeset/light-toys-check.md new file mode 100644 index 000000000000..ffda5b3523e9 --- /dev/null +++ b/.changeset/light-toys-check.md @@ -0,0 +1,12 @@ +--- +"@biomejs/biome": patch +--- + +Added support Svelte syntax `{#key}`. Biome now is able to parse and format the Svelte syntax [`{#key}`](https://svelte.dev/docs/svelte/key): + +```diff +-{#key expression}
{/key} ++{#key expression} ++
++{/key} +``` diff --git a/crates/biome_html_factory/src/generated/node_factory.rs b/crates/biome_html_factory/src/generated/node_factory.rs index a0c6018a025a..be4731c12db6 100644 --- a/crates/biome_html_factory/src/generated/node_factory.rs +++ b/crates/biome_html_factory/src/generated/node_factory.rs @@ -363,6 +363,50 @@ pub fn svelte_debug_block( ], )) } +pub fn svelte_key_block( + opening_block: SvelteKeyOpeningBlock, + children: HtmlElementList, + closing_block: SvelteKeyClosingBlock, +) -> SvelteKeyBlock { + SvelteKeyBlock::unwrap_cast(SyntaxNode::new_detached( + HtmlSyntaxKind::SVELTE_KEY_BLOCK, + [ + Some(SyntaxElement::Node(opening_block.into_syntax())), + Some(SyntaxElement::Node(children.into_syntax())), + Some(SyntaxElement::Node(closing_block.into_syntax())), + ], + )) +} +pub fn svelte_key_closing_block( + sv_curly_slash_token: SyntaxToken, + key_token: SyntaxToken, + r_curly_token: SyntaxToken, +) -> SvelteKeyClosingBlock { + SvelteKeyClosingBlock::unwrap_cast(SyntaxNode::new_detached( + HtmlSyntaxKind::SVELTE_KEY_CLOSING_BLOCK, + [ + Some(SyntaxElement::Token(sv_curly_slash_token)), + Some(SyntaxElement::Token(key_token)), + Some(SyntaxElement::Token(r_curly_token)), + ], + )) +} +pub fn svelte_key_opening_block( + sv_curly_hash_token: SyntaxToken, + key_token: SyntaxToken, + expression: HtmlTextExpression, + r_curly_token: SyntaxToken, +) -> SvelteKeyOpeningBlock { + SvelteKeyOpeningBlock::unwrap_cast(SyntaxNode::new_detached( + HtmlSyntaxKind::SVELTE_KEY_OPENING_BLOCK, + [ + Some(SyntaxElement::Token(sv_curly_hash_token)), + Some(SyntaxElement::Token(key_token)), + Some(SyntaxElement::Node(expression.into_syntax())), + Some(SyntaxElement::Token(r_curly_token)), + ], + )) +} pub fn svelte_name(svelte_ident_token: SyntaxToken) -> SvelteName { SvelteName::unwrap_cast(SyntaxNode::new_detached( HtmlSyntaxKind::SVELTE_NAME, diff --git a/crates/biome_html_factory/src/generated/syntax_factory.rs b/crates/biome_html_factory/src/generated/syntax_factory.rs index 6527c0d76455..de78d2e04a7f 100644 --- a/crates/biome_html_factory/src/generated/syntax_factory.rs +++ b/crates/biome_html_factory/src/generated/syntax_factory.rs @@ -652,6 +652,112 @@ impl SyntaxFactory for HtmlSyntaxFactory { } slots.into_node(SVELTE_DEBUG_BLOCK, children) } + SVELTE_KEY_BLOCK => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<3usize> = RawNodeSlots::default(); + let mut current_element = elements.next(); + if let Some(element) = ¤t_element + && SvelteKeyOpeningBlock::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && HtmlElementList::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && SvelteKeyClosingBlock::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + SVELTE_KEY_BLOCK.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(SVELTE_KEY_BLOCK, children) + } + SVELTE_KEY_CLOSING_BLOCK => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<3usize> = RawNodeSlots::default(); + let mut current_element = elements.next(); + if let Some(element) = ¤t_element + && element.kind() == T!["{/"] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T![key] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T!['}'] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + SVELTE_KEY_CLOSING_BLOCK.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(SVELTE_KEY_CLOSING_BLOCK, children) + } + SVELTE_KEY_OPENING_BLOCK => { + let mut elements = (&children).into_iter(); + let mut slots: RawNodeSlots<4usize> = RawNodeSlots::default(); + let mut current_element = elements.next(); + if let Some(element) = ¤t_element + && element.kind() == T!["{#"] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T![key] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && HtmlTextExpression::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T!['}'] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + SVELTE_KEY_OPENING_BLOCK.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(SVELTE_KEY_OPENING_BLOCK, children) + } SVELTE_NAME => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<1usize> = RawNodeSlots::default(); diff --git a/crates/biome_html_formatter/src/generated.rs b/crates/biome_html_formatter/src/generated.rs index 870a562cfa3b..120639cdc377 100644 --- a/crates/biome_html_formatter/src/generated.rs +++ b/crates/biome_html_formatter/src/generated.rs @@ -754,6 +754,120 @@ impl IntoFormat for biome_html_syntax::SvelteDebugBlock { ) } } +impl FormatRule + for crate::svelte::auxiliary::key_block::FormatSvelteKeyBlock +{ + type Context = HtmlFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_html_syntax::SvelteKeyBlock, + f: &mut HtmlFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_html_syntax::SvelteKeyBlock { + type Format<'a> = FormatRefWithRule< + 'a, + biome_html_syntax::SvelteKeyBlock, + crate::svelte::auxiliary::key_block::FormatSvelteKeyBlock, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::svelte::auxiliary::key_block::FormatSvelteKeyBlock::default(), + ) + } +} +impl IntoFormat for biome_html_syntax::SvelteKeyBlock { + type Format = FormatOwnedWithRule< + biome_html_syntax::SvelteKeyBlock, + crate::svelte::auxiliary::key_block::FormatSvelteKeyBlock, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::svelte::auxiliary::key_block::FormatSvelteKeyBlock::default(), + ) + } +} +impl FormatRule + for crate::svelte::auxiliary::key_closing_block::FormatSvelteKeyClosingBlock +{ + type Context = HtmlFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_html_syntax::SvelteKeyClosingBlock, + f: &mut HtmlFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_html_syntax::SvelteKeyClosingBlock { + type Format<'a> = FormatRefWithRule< + 'a, + biome_html_syntax::SvelteKeyClosingBlock, + crate::svelte::auxiliary::key_closing_block::FormatSvelteKeyClosingBlock, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::svelte::auxiliary::key_closing_block::FormatSvelteKeyClosingBlock::default(), + ) + } +} +impl IntoFormat for biome_html_syntax::SvelteKeyClosingBlock { + type Format = FormatOwnedWithRule< + biome_html_syntax::SvelteKeyClosingBlock, + crate::svelte::auxiliary::key_closing_block::FormatSvelteKeyClosingBlock, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::svelte::auxiliary::key_closing_block::FormatSvelteKeyClosingBlock::default(), + ) + } +} +impl FormatRule + for crate::svelte::auxiliary::key_opening_block::FormatSvelteKeyOpeningBlock +{ + type Context = HtmlFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_html_syntax::SvelteKeyOpeningBlock, + f: &mut HtmlFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_html_syntax::SvelteKeyOpeningBlock { + type Format<'a> = FormatRefWithRule< + 'a, + biome_html_syntax::SvelteKeyOpeningBlock, + crate::svelte::auxiliary::key_opening_block::FormatSvelteKeyOpeningBlock, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::svelte::auxiliary::key_opening_block::FormatSvelteKeyOpeningBlock::default(), + ) + } +} +impl IntoFormat for biome_html_syntax::SvelteKeyOpeningBlock { + type Format = FormatOwnedWithRule< + biome_html_syntax::SvelteKeyOpeningBlock, + crate::svelte::auxiliary::key_opening_block::FormatSvelteKeyOpeningBlock, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::svelte::auxiliary::key_opening_block::FormatSvelteKeyOpeningBlock::default(), + ) + } +} impl FormatRule for crate::svelte::auxiliary::name::FormatSvelteName { diff --git a/crates/biome_html_formatter/src/html/auxiliary/text_expression.rs b/crates/biome_html_formatter/src/html/auxiliary/text_expression.rs index 27be4ef2aeac..7a28f88c1c65 100644 --- a/crates/biome_html_formatter/src/html/auxiliary/text_expression.rs +++ b/crates/biome_html_formatter/src/html/auxiliary/text_expression.rs @@ -4,6 +4,6 @@ use biome_html_syntax::HtmlTextExpression; pub(crate) struct FormatHtmlTextExpression; impl FormatNodeRule for FormatHtmlTextExpression { fn fmt_fields(&self, node: &HtmlTextExpression, f: &mut HtmlFormatter) -> FormatResult<()> { - format_html_verbatim_node(node.syntax()).fmt(f) + format_verbatim_skipped(node.syntax()).fmt(f) } } diff --git a/crates/biome_html_formatter/src/html/lists/element_list.rs b/crates/biome_html_formatter/src/html/lists/element_list.rs index de96d885a41d..0894d676f137 100644 --- a/crates/biome_html_formatter/src/html/lists/element_list.rs +++ b/crates/biome_html_formatter/src/html/lists/element_list.rs @@ -23,7 +23,7 @@ use tag::GroupMode; #[derive(Debug, Clone, Default)] pub(crate) struct FormatHtmlElementList { layout: HtmlChildListLayout, - /// Whether or not the parent element that encapsulates this element list is whitespace sensitive. + /// Whether the parent element that encapsulates this element list is whitespace sensitive. is_element_whitespace_sensitive: bool, borrowed_tokens: BorrowedTokens, diff --git a/crates/biome_html_formatter/src/svelte/any/block.rs b/crates/biome_html_formatter/src/svelte/any/block.rs index 1c9b0882bf0f..79139b39cad7 100644 --- a/crates/biome_html_formatter/src/svelte/any/block.rs +++ b/crates/biome_html_formatter/src/svelte/any/block.rs @@ -10,6 +10,7 @@ impl FormatRule for FormatAnySvelteBlock { match node { AnySvelteBlock::SvelteBogusBlock(node) => node.format().fmt(f), AnySvelteBlock::SvelteDebugBlock(node) => node.format().fmt(f), + AnySvelteBlock::SvelteKeyBlock(node) => node.format().fmt(f), } } } diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/key_block.rs b/crates/biome_html_formatter/src/svelte/auxiliary/key_block.rs new file mode 100644 index 000000000000..a063230bef47 --- /dev/null +++ b/crates/biome_html_formatter/src/svelte/auxiliary/key_block.rs @@ -0,0 +1,49 @@ +use crate::html::lists::element_list::{FormatChildrenResult, FormatHtmlElementList}; +use crate::prelude::*; +use biome_formatter::{format_args, write}; +use biome_html_syntax::{SvelteKeyBlock, SvelteKeyBlockFields}; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatSvelteKeyBlock; +impl FormatNodeRule for FormatSvelteKeyBlock { + fn fmt_fields(&self, node: &SvelteKeyBlock, f: &mut HtmlFormatter) -> FormatResult<()> { + let SvelteKeyBlockFields { + opening_block, + children, + closing_block, + } = node.as_fields(); + + write!(f, [opening_block.format(),])?; + // The order here is important. First, we must check if we can delegate the formatting + // of embedded nodes, then we check if we should format them verbatim. + let format_children = FormatHtmlElementList::default().fmt_children(&children, f)?; + let attr_group_id = f.group_id("element-attr-group-id"); + + match format_children { + FormatChildrenResult::ForceMultiline(multiline) => { + write!(f, [multiline])?; + } + FormatChildrenResult::BestFitting { + flat_children, + expanded_children, + } => { + let expanded_children = expanded_children.memoized(); + write!( + f, + [ + // If the attribute group breaks, prettier always breaks the children as well. + &if_group_breaks(&expanded_children).with_group_id(Some(attr_group_id)), + // If the attribute group does NOT break, print whatever fits best for the children. + &if_group_fits_on_line(&best_fitting![ + format_args![flat_children], + format_args![expanded_children], + ]) + .with_group_id(Some(attr_group_id)), + ] + )?; + } + } + + write!(f, [closing_block.format()]) + } +} diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/key_closing_block.rs b/crates/biome_html_formatter/src/svelte/auxiliary/key_closing_block.rs new file mode 100644 index 000000000000..ddd59549587d --- /dev/null +++ b/crates/biome_html_formatter/src/svelte/auxiliary/key_closing_block.rs @@ -0,0 +1,24 @@ +use crate::prelude::*; +use biome_formatter::write; +use biome_html_syntax::{SvelteKeyClosingBlock, SvelteKeyClosingBlockFields}; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatSvelteKeyClosingBlock; +impl FormatNodeRule for FormatSvelteKeyClosingBlock { + fn fmt_fields(&self, node: &SvelteKeyClosingBlock, f: &mut HtmlFormatter) -> FormatResult<()> { + let SvelteKeyClosingBlockFields { + key_token, + r_curly_token, + sv_curly_slash_token, + } = node.as_fields(); + + write!( + f, + [ + sv_curly_slash_token.format(), + key_token.format(), + r_curly_token.format(), + ] + ) + } +} diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/key_opening_block.rs b/crates/biome_html_formatter/src/svelte/auxiliary/key_opening_block.rs new file mode 100644 index 000000000000..217fb89d8f7b --- /dev/null +++ b/crates/biome_html_formatter/src/svelte/auxiliary/key_opening_block.rs @@ -0,0 +1,27 @@ +use crate::prelude::*; +use biome_formatter::write; +use biome_html_syntax::{SvelteKeyOpeningBlock, SvelteKeyOpeningBlockFields}; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatSvelteKeyOpeningBlock; +impl FormatNodeRule for FormatSvelteKeyOpeningBlock { + fn fmt_fields(&self, node: &SvelteKeyOpeningBlock, f: &mut HtmlFormatter) -> FormatResult<()> { + let SvelteKeyOpeningBlockFields { + key_token, + r_curly_token, + expression, + sv_curly_hash_token, + } = node.as_fields(); + + write!( + f, + [ + sv_curly_hash_token.format(), + key_token.format(), + space(), + expression.format(), + r_curly_token.format(), + ] + ) + } +} diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/mod.rs b/crates/biome_html_formatter/src/svelte/auxiliary/mod.rs index 05a91f84246f..dca13324601c 100644 --- a/crates/biome_html_formatter/src/svelte/auxiliary/mod.rs +++ b/crates/biome_html_formatter/src/svelte/auxiliary/mod.rs @@ -1,4 +1,7 @@ //! This is a generated file. Don't modify it by hand! Run 'cargo codegen formatter' to re-generate the file. pub(crate) mod debug_block; +pub(crate) mod key_block; +pub(crate) mod key_closing_block; +pub(crate) mod key_opening_block; pub(crate) mod name; diff --git a/crates/biome_html_formatter/tests/specs/html/component-frameworks/svelte-component-casing.svelte.snap b/crates/biome_html_formatter/tests/specs/html/component-frameworks/svelte-component-casing.svelte.snap index 3f59eaa71e44..00e178b7cea3 100644 --- a/crates/biome_html_formatter/tests/specs/html/component-frameworks/svelte-component-casing.svelte.snap +++ b/crates/biome_html_formatter/tests/specs/html/component-frameworks/svelte-component-casing.svelte.snap @@ -61,4 +61,3 @@ Self close void elements: never ## Unimplemented nodes/tokens "\n import Button from './Button.svelte';\n import TextInput from './TextInput.svelte';\n import Select from './Select.svelte';\n" => 18..145 -"['a', 'b']" => 241..251 diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/debug.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/debug.svelte.snap index 05d49eec4991..d4284605562b 100644 --- a/crates/biome_html_formatter/tests/specs/html/svelte/debug.svelte.snap +++ b/crates/biome_html_formatter/tests/specs/html/svelte/debug.svelte.snap @@ -1,6 +1,6 @@ --- source: crates/biome_formatter_test/src/snapshot_builder.rs -info: astro/debug.svelte +info: svelte/debug.svelte --- # Input diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/key.svelte b/crates/biome_html_formatter/tests/specs/html/svelte/key.svelte new file mode 100644 index 000000000000..d69fde23881e --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/svelte/key.svelte @@ -0,0 +1,3 @@ +{#key expression} +
+{/key} diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/key.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/key.svelte.snap new file mode 100644 index 000000000000..bd1b12f96932 --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/svelte/key.svelte.snap @@ -0,0 +1,37 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: svelte/key.svelte +--- +# Input + +```svelte +{#key expression} +
+{/key} + +``` + + +============================= + +# Outputs + +## Output 1 + +----- +Indent style: Tab +Indent width: 2 +Line ending: LF +Line width: 80 +Attribute Position: Auto +Bracket same line: false +Whitespace sensitivity: css +Indent script and style: false +Self close void elements: never +----- + +```svelte +{#key expression} +
+{/key} +``` diff --git a/crates/biome_html_formatter/tests/specs/html/text_expressions/expressions.vue.snap b/crates/biome_html_formatter/tests/specs/html/text_expressions/expressions.vue.snap index e26b7e824664..c7c1c5ff7250 100644 --- a/crates/biome_html_formatter/tests/specs/html/text_expressions/expressions.vue.snap +++ b/crates/biome_html_formatter/tests/specs/html/text_expressions/expressions.vue.snap @@ -40,9 +40,3 @@ Self close void elements: never } }} ``` - - - -## Unimplemented nodes/tokens - -" x => {\n\treturn \"hello\"\n} " => 14..40 diff --git a/crates/biome_html_formatter/tests/specs/prettier/html/front-matter/empty2.html.snap b/crates/biome_html_formatter/tests/specs/prettier/html/front-matter/empty2.html.snap index e9477afb9082..ab29229871db 100644 --- a/crates/biome_html_formatter/tests/specs/prettier/html/front-matter/empty2.html.snap +++ b/crates/biome_html_formatter/tests/specs/prettier/html/front-matter/empty2.html.snap @@ -64,6 +64,7 @@ empty2.html:5:1 parse ━━━━━━━━━━━━━━━━━━━ - element - text + - closing block ``` diff --git a/crates/biome_html_parser/src/lexer/mod.rs b/crates/biome_html_parser/src/lexer/mod.rs index 20a8c890b69b..e7dad2689a4b 100644 --- a/crates/biome_html_parser/src/lexer/mod.rs +++ b/crates/biome_html_parser/src/lexer/mod.rs @@ -3,7 +3,7 @@ mod tests; use crate::token_source::{HtmlEmbeddedLanguage, HtmlLexContext, TextExpressionKind}; use biome_html_syntax::HtmlSyntaxKind::{ COMMENT, DEBUG_KW, DOCTYPE_KW, EOF, ERROR_TOKEN, HTML_KW, HTML_LITERAL, HTML_STRING_LITERAL, - NEWLINE, SVELTE_IDENT, TOMBSTONE, UNICODE_BOM, WHITESPACE, + KEY_KW, NEWLINE, SVELTE_IDENT, TOMBSTONE, UNICODE_BOM, WHITESPACE, }; use biome_html_syntax::{HtmlSyntaxKind, T, TextLen, TextSize}; use biome_parser::diagnostic::ParseDiagnostic; @@ -256,22 +256,17 @@ impl<'src> HtmlLexer<'src> { } } - // TODO: keep this function, and enhance svelte_expression until we don't need it anymore /// Consumes tokens within a single text expression ('{...}') while tracking nested /// brackets until the matching closing bracket is found. fn consume_single_text_expression(&mut self) -> HtmlSyntaxKind { let mut brackets_stack = 0; - if self.prev_byte() == Some(b'{') { - brackets_stack += 1; - } while let Some(current) = self.current_byte() { match current { - b',' if brackets_stack == 0 => break, b'}' => { - brackets_stack -= 1; if brackets_stack == 0 { break; } else { + brackets_stack -= 1; self.advance(1); } } @@ -429,6 +424,7 @@ impl<'src> HtmlLexer<'src> { b"html" | b"HTML" if context.is_doctype() => HTML_KW, buffer if context.is_svelte() => match buffer { b"debug" if self.current_kind == T!["{@"] => DEBUG_KW, + b"key" if self.current_kind == T!["{#"] || self.current_kind == T!["{/"] => KEY_KW, _ => SVELTE_IDENT, }, _ => HTML_LITERAL, @@ -1022,6 +1018,7 @@ impl<'src> LexerWithCheckpoint<'src> for HtmlLexer<'src> { } } } + struct QuotesSeen { single: u16, double: u16, diff --git a/crates/biome_html_parser/src/syntax/mod.rs b/crates/biome_html_parser/src/syntax/mod.rs index 47dcf987a28c..4dc7df5bb8e6 100644 --- a/crates/biome_html_parser/src/syntax/mod.rs +++ b/crates/biome_html_parser/src/syntax/mod.rs @@ -5,7 +5,7 @@ mod svelte; use crate::parser::HtmlParser; use crate::syntax::astro::parse_astro_fence; use crate::syntax::parse_error::*; -use crate::syntax::svelte::parse_svelte_at_block; +use crate::syntax::svelte::{parse_svelte_at_block, parse_svelte_hash_block}; use crate::token_source::{HtmlEmbeddedLanguage, HtmlLexContext, TextExpressionKind}; use biome_html_syntax::HtmlSyntaxKind::*; use biome_html_syntax::{HtmlSyntaxKind, T}; @@ -210,6 +210,39 @@ fn parse_closing_tag(p: &mut HtmlParser) -> ParsedSyntax { Present(m.complete(p, HTML_CLOSING_ELEMENT)) } +pub(crate) fn parse_html_element(p: &mut HtmlParser) -> ParsedSyntax { + match p.cur() { + T![" parse_cdata_section(p), + T![<] => parse_element(p), + T!["{{"] => HtmlSyntaxFeatures::DoubleTextExpressions.parse_exclusive_syntax( + p, + |p| parse_double_text_expression(p, HtmlLexContext::Regular), + |p, m| disabled_interpolation(p, m.range(p)), + ), + T!["{@"] => parse_svelte_at_block(p), + T!["{#"] => parse_svelte_hash_block(p), + T!['{'] => parse_single_text_expression(p, HtmlLexContext::Regular).or_else(|| { + let m = p.start(); + p.bump_remap(HTML_LITERAL); + Present(m.complete(p, HTML_CONTENT)) + }), + T!["}}"] | T!['}'] => { + // The closing text expression should be handled by other functions. + // If we're here, we assume that text expressions are enabled and + // we remap to HTML_LITERAL + let m = p.start(); + p.bump_remap(HTML_LITERAL); + Present(m.complete(p, HTML_CONTENT)) + } + HTML_LITERAL => { + let m = p.start(); + p.bump_with_context(HTML_LITERAL, HtmlLexContext::Regular); + Present(m.complete(p, HTML_CONTENT)) + } + _ => Absent, + } +} + #[derive(Default)] struct ElementList; @@ -219,35 +252,7 @@ impl ParseNodeList for ElementList { const LIST_KIND: Self::Kind = HTML_ELEMENT_LIST; fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { - match p.cur() { - T![" parse_cdata_section(p), - T![<] => parse_element(p), - T!["{{"] => HtmlSyntaxFeatures::DoubleTextExpressions.parse_exclusive_syntax( - p, - |p| parse_double_text_expression(p, HtmlLexContext::Regular), - |p, m| disabled_interpolation(p, m.range(p)), - ), - T!["{@"] => parse_svelte_at_block(p), - T!['{'] => parse_single_text_expression(p, HtmlLexContext::Regular).or_else(|| { - let m = p.start(); - p.bump_remap(HTML_LITERAL); - Present(m.complete(p, HTML_CONTENT)) - }), - T!["}}"] | T!['}'] => { - // The closing text expression should be handled by other functions. - // If we're here, we assume that text expressions are enabled and - // we remap to HTML_LITERAL - let m = p.start(); - p.bump_remap(HTML_LITERAL); - Present(m.complete(p, HTML_CONTENT)) - } - HTML_LITERAL => { - let m = p.start(); - p.bump_with_context(HTML_LITERAL, HtmlLexContext::Regular); - Present(m.complete(p, HTML_CONTENT)) - } - _ => Absent, - } + parse_html_element(p) } fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { @@ -283,7 +288,7 @@ impl ParseNodeList for AttributeList { } fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { - p.at(T![>]) || p.at(T![/]) || p.at(EOF) + p.at(T![>]) || p.at(T![/]) || p.at(EOF) || p.at(T!['}']) } fn recover( @@ -460,10 +465,7 @@ fn parse_double_text_expression(p: &mut HtmlParser, context: HtmlLexContext) -> let checkpoint = p.checkpoint(); let m = p.start(); let opening_range = p.cur_range(); - p.bump_with_context( - T!["{{"], - HtmlLexContext::TextExpression(TextExpressionKind::Double), - ); + p.bump_with_context(T!["{{"], HtmlLexContext::double_expression()); TextExpression::new_double().parse_element(p).ok(); @@ -510,10 +512,7 @@ pub(crate) fn parse_single_text_expression( let m = p.start(); let opening_range = p.cur_range(); - p.bump_with_context( - T!['{'], - HtmlLexContext::TextExpression(TextExpressionKind::Single), - ); + p.bump_with_context(T!['{'], HtmlLexContext::single_expression()); TextExpression::new_single().parse_element(p).ok(); @@ -564,7 +563,6 @@ impl TextExpression { } let m = p.start(); - match self.kind { TextExpressionKind::Single => { if p.at(T!["}}"]) { @@ -572,8 +570,11 @@ impl TextExpression { HTML_LITERAL, HtmlLexContext::TextExpression(self.kind), ); - } else { + } else if !p.at(T!['}']) { p.bump_remap_with_context(HTML_LITERAL, HtmlLexContext::InsideTag); + } else { + m.abandon(p); + return Absent; } } TextExpressionKind::Double => { diff --git a/crates/biome_html_parser/src/syntax/parse_error.rs b/crates/biome_html_parser/src/syntax/parse_error.rs index f49ad0f91dfa..340d325b9997 100644 --- a/crates/biome_html_parser/src/syntax/parse_error.rs +++ b/crates/biome_html_parser/src/syntax/parse_error.rs @@ -33,7 +33,7 @@ pub(crate) fn expected_text_expression( } pub(crate) fn expected_child(p: &HtmlParser, range: TextRange) -> ParseDiagnostic { - expect_one_of(&["element", "text"], range).into_diagnostic(p) + expect_one_of(&["element", "text", "closing block"], range).into_diagnostic(p) } pub(crate) fn expected_closed_fence(p: &HtmlParser, range: TextRange) -> ParseDiagnostic { @@ -94,5 +94,5 @@ pub(crate) fn closing_tag_should_not_have_attributes( } pub(crate) fn expected_svelte_closing_block(p: &HtmlParser, range: TextRange) -> ParseDiagnostic { - p.err_builder("Expected an identifier.", range) + p.err_builder("Expected a closing block, instead found none.", range) } diff --git a/crates/biome_html_parser/src/syntax/svelte.rs b/crates/biome_html_parser/src/syntax/svelte.rs index 8eb73c40167d..62a49fc08e0b 100644 --- a/crates/biome_html_parser/src/syntax/svelte.rs +++ b/crates/biome_html_parser/src/syntax/svelte.rs @@ -1,20 +1,111 @@ use crate::parser::HtmlParser; -use crate::syntax::parse_error::expected_svelte_closing_block; +use crate::syntax::parse_error::{expected_child, expected_svelte_closing_block}; +use crate::syntax::{TextExpression, parse_html_element}; use crate::token_source::HtmlLexContext; use biome_html_syntax::HtmlSyntaxKind::{ - EOF, SVELTE_BINDING_LIST, SVELTE_BOGUS_BLOCK, SVELTE_DEBUG_BLOCK, SVELTE_IDENT, SVELTE_NAME, + EOF, HTML_BOGUS_ELEMENT, HTML_ELEMENT_LIST, SVELTE_BINDING_LIST, SVELTE_BOGUS_BLOCK, + SVELTE_DEBUG_BLOCK, SVELTE_IDENT, SVELTE_KEY_BLOCK, SVELTE_KEY_CLOSING_BLOCK, + SVELTE_KEY_OPENING_BLOCK, SVELTE_NAME, }; use biome_html_syntax::{HtmlSyntaxKind, T}; -use biome_parser::parse_lists::ParseSeparatedList; +use biome_parser::parse_lists::{ParseNodeList, ParseSeparatedList}; use biome_parser::parse_recovery::{ParseRecoveryTokenSet, RecoveryResult}; use biome_parser::prelude::ParsedSyntax; use biome_parser::prelude::ParsedSyntax::{Absent, Present}; use biome_parser::{Marker, Parser, TokenSet, token_set}; +pub(crate) fn parse_svelte_hash_block(p: &mut HtmlParser) -> ParsedSyntax { + if !p.at(T!["{#"]) { + return Absent; + } + // NOTE: use or_else chain here to parse + // other possible hash blocks + parse_key_block(p) +} + +pub(crate) fn parse_key_block(p: &mut HtmlParser) -> ParsedSyntax { + if !p.at(T!["{#"]) { + return Absent; + } + + let m = p.start(); + + let completed = parse_opening_block(p, T![key], SVELTE_KEY_OPENING_BLOCK).ok(); + + SvelteElementList.parse_list(p); + + parse_closing_block(p, T![key], SVELTE_KEY_CLOSING_BLOCK).or_add_diagnostic(p, |p, range| { + let diagnostic = expected_svelte_closing_block(p, range); + if let Some(completed) = completed { + diagnostic.with_detail(completed.range(p), "This is where the block started.") + } else { + diagnostic + } + }); + + Present(m.complete(p, SVELTE_KEY_BLOCK)) +} + +/// Parses a `{# expression }` block. +/// +/// `node` is the name of the node to emit +pub(crate) fn parse_opening_block( + p: &mut HtmlParser, + keyword: HtmlSyntaxKind, + node: HtmlSyntaxKind, +) -> ParsedSyntax { + if !p.at(T!["{#"]) { + return Absent; + } + let m = p.start(); + let checkpoint = p.checkpoint(); + p.bump_with_context(T!["{#"], HtmlLexContext::Svelte); + + if !p.at(keyword) { + m.abandon(p); + p.rewind(checkpoint); + return Absent; + } + p.bump_with_context(keyword, HtmlLexContext::Svelte); + TextExpression::new_single() + .parse_element(p) + .or_add_diagnostic(p, |p, range| { + p.err_builder( + "Expected an expression, instead none was found.", + range.sub_start(m.start()), + ) + }); + + p.expect_with_context(T!['}'], HtmlLexContext::InsideTag); + + Present(m.complete(p, node)) +} + +/// Parses a `{/ }` block. +/// +/// `node` is the name of the node to emit +pub(crate) fn parse_closing_block( + p: &mut HtmlParser, + keyword: HtmlSyntaxKind, + node: HtmlSyntaxKind, +) -> ParsedSyntax { + if !p.at(T!["{/"]) { + return Absent; + } + let m = p.start(); + p.bump_with_context(T!["{/"], HtmlLexContext::Svelte); + + p.expect_with_context(keyword, HtmlLexContext::Svelte); + + p.expect_with_context(T!['}'], HtmlLexContext::InsideTag); + + Present(m.complete(p, node)) +} + pub(crate) fn parse_svelte_at_block(p: &mut HtmlParser) -> ParsedSyntax { if !p.at(T!["{@"]) { return Absent; - }; + } let m = p.start(); p.bump_with_context(T!["{@"], HtmlLexContext::Svelte); @@ -95,3 +186,35 @@ fn parse_name(p: &mut HtmlParser) -> ParsedSyntax { Present(m.complete(p, SVELTE_NAME)) } + +#[derive(Default)] +struct SvelteElementList; + +impl ParseNodeList for SvelteElementList { + type Kind = HtmlSyntaxKind; + type Parser<'source> = HtmlParser<'source>; + const LIST_KIND: Self::Kind = HTML_ELEMENT_LIST; + + fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { + parse_html_element(p) + } + + fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { + let at_l_angle0 = p.at(T![<]); + let at_slash1 = p.nth_at(1, T![/]); + let at_eof = p.at(EOF); + at_l_angle0 && at_slash1 || at_eof || p.at(T!["{/"]) + } + + fn recover( + &mut self, + p: &mut Self::Parser<'_>, + parsed_element: ParsedSyntax, + ) -> RecoveryResult { + parsed_element.or_recover_with_token_set( + p, + &ParseRecoveryTokenSet::new(HTML_BOGUS_ELEMENT, token_set![T![<], T![>], T!["{/"]]), + expected_child, + ) + } +} diff --git a/crates/biome_html_parser/src/token_source.rs b/crates/biome_html_parser/src/token_source.rs index 49c8fb7d271b..900a18a767cd 100644 --- a/crates/biome_html_parser/src/token_source.rs +++ b/crates/biome_html_parser/src/token_source.rs @@ -55,6 +55,16 @@ pub(crate) enum HtmlLexContext { AstroFencedCodeBlock, } +impl HtmlLexContext { + pub fn single_expression() -> Self { + Self::TextExpression(TextExpressionKind::Single) + } + + pub fn double_expression() -> Self { + Self::TextExpression(TextExpressionKind::Double) + } +} + #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord)] pub(crate) enum TextExpressionKind { // {{ expr }} diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/debug-trailing-comma.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/debug-trailing-comma.svelte.snap index 43f6530b8362..4541a0a9a502 100644 --- a/crates/biome_html_parser/tests/html_specs/error/svelte/debug-trailing-comma.svelte.snap +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/debug-trailing-comma.svelte.snap @@ -61,7 +61,7 @@ HtmlRoot { ``` debug-trailing-comma.svelte:1:19 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × Expected an identifier. + × Expected a closing block, instead found none. > 1 │ {@debug something,} │ ^ diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/debug.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/debug.svelte.snap index 57f1cf6f032d..9eaa3e5736fc 100644 --- a/crates/biome_html_parser/tests/html_specs/error/svelte/debug.svelte.snap +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/debug.svelte.snap @@ -87,7 +87,7 @@ HtmlRoot { ``` debug.svelte:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - × Expected an identifier. + × Expected a closing block, instead found none. 1 │ {@debug > 2 │ {@debug something} diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/key_missing_close.svelte b/crates/biome_html_parser/tests/html_specs/error/svelte/key_missing_close.svelte new file mode 100644 index 000000000000..753cf37759f8 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/key_missing_close.svelte @@ -0,0 +1,2 @@ +{#key expression} + something diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/key_missing_close.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/key_missing_close.svelte.snap new file mode 100644 index 000000000000..67621afa7dbc --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/key_missing_close.svelte.snap @@ -0,0 +1,85 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```svelte +{#key expression} + something + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + SvelteKeyBlock { + opening_block: SvelteKeyOpeningBlock { + sv_curly_hash_token: SV_CURLY_HASH@0..2 "{#" [] [], + key_token: KEY_KW@2..6 "key" [] [Whitespace(" ")], + expression: HtmlTextExpression { + html_literal_token: HTML_LITERAL@6..16 "expression" [] [], + }, + r_curly_token: R_CURLY@16..17 "}" [] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@17..28 "something" [Newline("\n"), Whitespace("\t")] [], + }, + ], + closing_block: missing (required), + }, + ], + eof_token: EOF@28..29 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..29 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..28 + 0: SVELTE_KEY_BLOCK@0..28 + 0: SVELTE_KEY_OPENING_BLOCK@0..17 + 0: SV_CURLY_HASH@0..2 "{#" [] [] + 1: KEY_KW@2..6 "key" [] [Whitespace(" ")] + 2: HTML_TEXT_EXPRESSION@6..16 + 0: HTML_LITERAL@6..16 "expression" [] [] + 3: R_CURLY@16..17 "}" [] [] + 1: HTML_ELEMENT_LIST@17..28 + 0: HTML_CONTENT@17..28 + 0: HTML_LITERAL@17..28 "something" [Newline("\n"), Whitespace("\t")] [] + 2: (empty) + 4: EOF@28..29 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +key_missing_close.svelte:3:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected a closing block, instead found none. + + 1 │ {#key expression} + 2 │ something + > 3 │ + │ + + i This is where the block started. + + > 1 │ {#key expression} + │ ^^^^^^^^^^^^^^^^^ + 2 │ something + 3 │ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/key_missing_expression.svelte b/crates/biome_html_parser/tests/html_specs/error/svelte/key_missing_expression.svelte new file mode 100644 index 000000000000..44c4e14ab12a --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/key_missing_expression.svelte @@ -0,0 +1,3 @@ +{#key } + something +{/key} diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/key_missing_expression.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/key_missing_expression.svelte.snap new file mode 100644 index 000000000000..cbb77da26140 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/key_missing_expression.svelte.snap @@ -0,0 +1,83 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```svelte +{#key } + something +{/key} + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + SvelteKeyBlock { + opening_block: SvelteKeyOpeningBlock { + sv_curly_hash_token: SV_CURLY_HASH@0..2 "{#" [] [], + key_token: KEY_KW@2..6 "key" [] [Whitespace(" ")], + expression: missing (required), + r_curly_token: R_CURLY@6..7 "}" [] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@7..18 "something" [Newline("\n"), Whitespace("\t")] [], + }, + ], + closing_block: SvelteKeyClosingBlock { + sv_curly_slash_token: SV_CURLY_SLASH@18..21 "{/" [Newline("\n")] [], + key_token: KEY_KW@21..24 "key" [] [], + r_curly_token: R_CURLY@24..25 "}" [] [], + }, + }, + ], + eof_token: EOF@25..26 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..26 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..25 + 0: SVELTE_KEY_BLOCK@0..25 + 0: SVELTE_KEY_OPENING_BLOCK@0..7 + 0: SV_CURLY_HASH@0..2 "{#" [] [] + 1: KEY_KW@2..6 "key" [] [Whitespace(" ")] + 2: (empty) + 3: R_CURLY@6..7 "}" [] [] + 1: HTML_ELEMENT_LIST@7..18 + 0: HTML_CONTENT@7..18 + 0: HTML_LITERAL@7..18 "something" [Newline("\n"), Whitespace("\t")] [] + 2: SVELTE_KEY_CLOSING_BLOCK@18..25 + 0: SV_CURLY_SLASH@18..21 "{/" [Newline("\n")] [] + 1: KEY_KW@21..24 "key" [] [] + 2: R_CURLY@24..25 "}" [] [] + 4: EOF@25..26 "" [Newline("\n")] [] + +``` + +## Diagnostics + +``` +key_missing_expression.svelte:1:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Expected an expression, instead none was found. + + > 1 │ {#key } + │ ^ + 2 │ something + 3 │ {/key} + +``` diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/key.svelte b/crates/biome_html_parser/tests/html_specs/ok/svelte/key.svelte new file mode 100644 index 000000000000..d145bc102654 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/key.svelte @@ -0,0 +1,7 @@ +{#key expression} + something +{/key} + +{#key key} + something +{/key} diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/key.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/key.svelte.snap new file mode 100644 index 000000000000..11d244487827 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/key.svelte.snap @@ -0,0 +1,110 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```svelte +{#key expression} + something +{/key} + +{#key key} + something +{/key} + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + SvelteKeyBlock { + opening_block: SvelteKeyOpeningBlock { + sv_curly_hash_token: SV_CURLY_HASH@0..2 "{#" [] [], + key_token: KEY_KW@2..6 "key" [] [Whitespace(" ")], + expression: HtmlTextExpression { + html_literal_token: HTML_LITERAL@6..16 "expression" [] [], + }, + r_curly_token: R_CURLY@16..17 "}" [] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@17..28 "something" [Newline("\n"), Whitespace("\t")] [], + }, + ], + closing_block: SvelteKeyClosingBlock { + sv_curly_slash_token: SV_CURLY_SLASH@28..31 "{/" [Newline("\n")] [], + key_token: KEY_KW@31..34 "key" [] [], + r_curly_token: R_CURLY@34..35 "}" [] [], + }, + }, + SvelteKeyBlock { + opening_block: SvelteKeyOpeningBlock { + sv_curly_hash_token: SV_CURLY_HASH@35..39 "{#" [Newline("\n"), Newline("\n")] [], + key_token: KEY_KW@39..43 "key" [] [Whitespace(" ")], + expression: HtmlTextExpression { + html_literal_token: HTML_LITERAL@43..46 "key" [] [], + }, + r_curly_token: R_CURLY@46..47 "}" [] [], + }, + children: HtmlElementList [ + HtmlContent { + value_token: HTML_LITERAL@47..58 "something" [Newline("\n"), Whitespace("\t")] [], + }, + ], + closing_block: SvelteKeyClosingBlock { + sv_curly_slash_token: SV_CURLY_SLASH@58..61 "{/" [Newline("\n")] [], + key_token: KEY_KW@61..64 "key" [] [], + r_curly_token: R_CURLY@64..65 "}" [] [], + }, + }, + ], + eof_token: EOF@65..66 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..66 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..65 + 0: SVELTE_KEY_BLOCK@0..35 + 0: SVELTE_KEY_OPENING_BLOCK@0..17 + 0: SV_CURLY_HASH@0..2 "{#" [] [] + 1: KEY_KW@2..6 "key" [] [Whitespace(" ")] + 2: HTML_TEXT_EXPRESSION@6..16 + 0: HTML_LITERAL@6..16 "expression" [] [] + 3: R_CURLY@16..17 "}" [] [] + 1: HTML_ELEMENT_LIST@17..28 + 0: HTML_CONTENT@17..28 + 0: HTML_LITERAL@17..28 "something" [Newline("\n"), Whitespace("\t")] [] + 2: SVELTE_KEY_CLOSING_BLOCK@28..35 + 0: SV_CURLY_SLASH@28..31 "{/" [Newline("\n")] [] + 1: KEY_KW@31..34 "key" [] [] + 2: R_CURLY@34..35 "}" [] [] + 1: SVELTE_KEY_BLOCK@35..65 + 0: SVELTE_KEY_OPENING_BLOCK@35..47 + 0: SV_CURLY_HASH@35..39 "{#" [Newline("\n"), Newline("\n")] [] + 1: KEY_KW@39..43 "key" [] [Whitespace(" ")] + 2: HTML_TEXT_EXPRESSION@43..46 + 0: HTML_LITERAL@43..46 "key" [] [] + 3: R_CURLY@46..47 "}" [] [] + 1: HTML_ELEMENT_LIST@47..58 + 0: HTML_CONTENT@47..58 + 0: HTML_LITERAL@47..58 "something" [Newline("\n"), Whitespace("\t")] [] + 2: SVELTE_KEY_CLOSING_BLOCK@58..65 + 0: SV_CURLY_SLASH@58..61 "{/" [Newline("\n")] [] + 1: KEY_KW@61..64 "key" [] [] + 2: R_CURLY@64..65 "}" [] [] + 4: EOF@65..66 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_html_parser/tests/spec_test.rs b/crates/biome_html_parser/tests/spec_test.rs index 8cd3853bae03..25f34bcb6f06 100644 --- a/crates/biome_html_parser/tests/spec_test.rs +++ b/crates/biome_html_parser/tests/spec_test.rs @@ -142,7 +142,11 @@ pub fn run(test_case: &str, _snapshot_name: &str, test_directory: &str, outcome_ #[ignore] #[test] pub fn quick_test() { - let code = r#"{@debug something, something, something} + let code = r#" +{#key expression} +
+{/key} + "#; let root = parse_html(code, (&HtmlFileSource::svelte()).into()); diff --git a/crates/biome_html_syntax/src/generated/kind.rs b/crates/biome_html_syntax/src/generated/kind.rs index abf41fbd3280..efd18f796df1 100644 --- a/crates/biome_html_syntax/src/generated/kind.rs +++ b/crates/biome_html_syntax/src/generated/kind.rs @@ -36,6 +36,7 @@ pub enum HtmlSyntaxKind { DOCTYPE_KW, HTML_KW, DEBUG_KW, + KEY_KW, HTML_STRING_LITERAL, HTML_LITERAL, ERROR_TOKEN, @@ -68,6 +69,9 @@ pub enum HtmlSyntaxKind { ASTRO_FRONTMATTER_ELEMENT, ASTRO_EMBEDDED_CONTENT, SVELTE_DEBUG_BLOCK, + SVELTE_KEY_BLOCK, + SVELTE_KEY_OPENING_BLOCK, + SVELTE_KEY_CLOSING_BLOCK, SVELTE_BINDING_LIST, SVELTE_NAME, HTML_BOGUS, @@ -121,6 +125,7 @@ impl HtmlSyntaxKind { "doctype" => DOCTYPE_KW, "html" => HTML_KW, "debug" => DEBUG_KW, + "key" => KEY_KW, _ => return None, }; Some(kw) @@ -151,6 +156,7 @@ impl HtmlSyntaxKind { DOCTYPE_KW => "doctype", HTML_KW => "html", DEBUG_KW => "debug", + KEY_KW => "key", EOF => "EOF", HTML_STRING_LITERAL => "string literal", _ => return None, @@ -160,4 +166,4 @@ impl HtmlSyntaxKind { } #[doc = r" Utility macro for creating a SyntaxKind through simple macro syntax"] #[macro_export] -macro_rules ! T { [<] => { $ crate :: HtmlSyntaxKind :: L_ANGLE } ; [>] => { $ crate :: HtmlSyntaxKind :: R_ANGLE } ; [/] => { $ crate :: HtmlSyntaxKind :: SLASH } ; [=] => { $ crate :: HtmlSyntaxKind :: EQ } ; [!] => { $ crate :: HtmlSyntaxKind :: BANG } ; [-] => { $ crate :: HtmlSyntaxKind :: MINUS } ; [" { $ crate :: HtmlSyntaxKind :: CDATA_START } ; ["]]>"] => { $ crate :: HtmlSyntaxKind :: CDATA_END } ; [---] => { $ crate :: HtmlSyntaxKind :: FENCE } ; ['{'] => { $ crate :: HtmlSyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: HtmlSyntaxKind :: R_CURLY } ; ["{{"] => { $ crate :: HtmlSyntaxKind :: L_DOUBLE_CURLY } ; ["}}"] => { $ crate :: HtmlSyntaxKind :: R_DOUBLE_CURLY } ; ["{@"] => { $ crate :: HtmlSyntaxKind :: SV_CURLY_AT } ; ["{#"] => { $ crate :: HtmlSyntaxKind :: SV_CURLY_HASH } ; ["{/"] => { $ crate :: HtmlSyntaxKind :: SV_CURLY_SLASH } ; ["{:"] => { $ crate :: HtmlSyntaxKind :: SV_CURLY_COLON } ; [,] => { $ crate :: HtmlSyntaxKind :: COMMA } ; [null] => { $ crate :: HtmlSyntaxKind :: NULL_KW } ; [true] => { $ crate :: HtmlSyntaxKind :: TRUE_KW } ; [false] => { $ crate :: HtmlSyntaxKind :: FALSE_KW } ; [doctype] => { $ crate :: HtmlSyntaxKind :: DOCTYPE_KW } ; [html] => { $ crate :: HtmlSyntaxKind :: HTML_KW } ; [debug] => { $ crate :: HtmlSyntaxKind :: DEBUG_KW } ; [ident] => { $ crate :: HtmlSyntaxKind :: IDENT } ; [EOF] => { $ crate :: HtmlSyntaxKind :: EOF } ; [UNICODE_BOM] => { $ crate :: HtmlSyntaxKind :: UNICODE_BOM } ; [#] => { $ crate :: HtmlSyntaxKind :: HASH } ; } +macro_rules ! T { [<] => { $ crate :: HtmlSyntaxKind :: L_ANGLE } ; [>] => { $ crate :: HtmlSyntaxKind :: R_ANGLE } ; [/] => { $ crate :: HtmlSyntaxKind :: SLASH } ; [=] => { $ crate :: HtmlSyntaxKind :: EQ } ; [!] => { $ crate :: HtmlSyntaxKind :: BANG } ; [-] => { $ crate :: HtmlSyntaxKind :: MINUS } ; [" { $ crate :: HtmlSyntaxKind :: CDATA_START } ; ["]]>"] => { $ crate :: HtmlSyntaxKind :: CDATA_END } ; [---] => { $ crate :: HtmlSyntaxKind :: FENCE } ; ['{'] => { $ crate :: HtmlSyntaxKind :: L_CURLY } ; ['}'] => { $ crate :: HtmlSyntaxKind :: R_CURLY } ; ["{{"] => { $ crate :: HtmlSyntaxKind :: L_DOUBLE_CURLY } ; ["}}"] => { $ crate :: HtmlSyntaxKind :: R_DOUBLE_CURLY } ; ["{@"] => { $ crate :: HtmlSyntaxKind :: SV_CURLY_AT } ; ["{#"] => { $ crate :: HtmlSyntaxKind :: SV_CURLY_HASH } ; ["{/"] => { $ crate :: HtmlSyntaxKind :: SV_CURLY_SLASH } ; ["{:"] => { $ crate :: HtmlSyntaxKind :: SV_CURLY_COLON } ; [,] => { $ crate :: HtmlSyntaxKind :: COMMA } ; [null] => { $ crate :: HtmlSyntaxKind :: NULL_KW } ; [true] => { $ crate :: HtmlSyntaxKind :: TRUE_KW } ; [false] => { $ crate :: HtmlSyntaxKind :: FALSE_KW } ; [doctype] => { $ crate :: HtmlSyntaxKind :: DOCTYPE_KW } ; [html] => { $ crate :: HtmlSyntaxKind :: HTML_KW } ; [debug] => { $ crate :: HtmlSyntaxKind :: DEBUG_KW } ; [key] => { $ crate :: HtmlSyntaxKind :: KEY_KW } ; [ident] => { $ crate :: HtmlSyntaxKind :: IDENT } ; [EOF] => { $ crate :: HtmlSyntaxKind :: EOF } ; [UNICODE_BOM] => { $ crate :: HtmlSyntaxKind :: UNICODE_BOM } ; [#] => { $ crate :: HtmlSyntaxKind :: HASH } ; } diff --git a/crates/biome_html_syntax/src/generated/macros.rs b/crates/biome_html_syntax/src/generated/macros.rs index 104c566738e0..910a70e3a2bf 100644 --- a/crates/biome_html_syntax/src/generated/macros.rs +++ b/crates/biome_html_syntax/src/generated/macros.rs @@ -97,6 +97,18 @@ macro_rules! map_syntax_node { let $pattern = unsafe { $crate::SvelteDebugBlock::new_unchecked(node) }; $body } + $crate::HtmlSyntaxKind::SVELTE_KEY_BLOCK => { + let $pattern = unsafe { $crate::SvelteKeyBlock::new_unchecked(node) }; + $body + } + $crate::HtmlSyntaxKind::SVELTE_KEY_CLOSING_BLOCK => { + let $pattern = unsafe { $crate::SvelteKeyClosingBlock::new_unchecked(node) }; + $body + } + $crate::HtmlSyntaxKind::SVELTE_KEY_OPENING_BLOCK => { + let $pattern = unsafe { $crate::SvelteKeyOpeningBlock::new_unchecked(node) }; + $body + } $crate::HtmlSyntaxKind::SVELTE_NAME => { let $pattern = unsafe { $crate::SvelteName::new_unchecked(node) }; $body diff --git a/crates/biome_html_syntax/src/generated/nodes.rs b/crates/biome_html_syntax/src/generated/nodes.rs index 188d31eeada9..2a31f24a4e39 100644 --- a/crates/biome_html_syntax/src/generated/nodes.rs +++ b/crates/biome_html_syntax/src/generated/nodes.rs @@ -900,6 +900,146 @@ pub struct SvelteDebugBlockFields { pub r_curly_token: SyntaxResult, } #[derive(Clone, PartialEq, Eq, Hash)] +pub struct SvelteKeyBlock { + pub(crate) syntax: SyntaxNode, +} +impl SvelteKeyBlock { + #[doc = r" Create an AstNode from a SyntaxNode without checking its kind"] + #[doc = r""] + #[doc = r" # Safety"] + #[doc = r" This function must be guarded with a call to [AstNode::can_cast]"] + #[doc = r" or a match on [SyntaxNode::kind]"] + #[inline] + pub const unsafe fn new_unchecked(syntax: SyntaxNode) -> Self { + Self { syntax } + } + pub fn as_fields(&self) -> SvelteKeyBlockFields { + SvelteKeyBlockFields { + opening_block: self.opening_block(), + children: self.children(), + closing_block: self.closing_block(), + } + } + pub fn opening_block(&self) -> SyntaxResult { + support::required_node(&self.syntax, 0usize) + } + pub fn children(&self) -> HtmlElementList { + support::list(&self.syntax, 1usize) + } + pub fn closing_block(&self) -> SyntaxResult { + support::required_node(&self.syntax, 2usize) + } +} +impl Serialize for SvelteKeyBlock { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.as_fields().serialize(serializer) + } +} +#[derive(Serialize)] +pub struct SvelteKeyBlockFields { + pub opening_block: SyntaxResult, + pub children: HtmlElementList, + pub closing_block: SyntaxResult, +} +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct SvelteKeyClosingBlock { + pub(crate) syntax: SyntaxNode, +} +impl SvelteKeyClosingBlock { + #[doc = r" Create an AstNode from a SyntaxNode without checking its kind"] + #[doc = r""] + #[doc = r" # Safety"] + #[doc = r" This function must be guarded with a call to [AstNode::can_cast]"] + #[doc = r" or a match on [SyntaxNode::kind]"] + #[inline] + pub const unsafe fn new_unchecked(syntax: SyntaxNode) -> Self { + Self { syntax } + } + pub fn as_fields(&self) -> SvelteKeyClosingBlockFields { + SvelteKeyClosingBlockFields { + sv_curly_slash_token: self.sv_curly_slash_token(), + key_token: self.key_token(), + r_curly_token: self.r_curly_token(), + } + } + pub fn sv_curly_slash_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 0usize) + } + pub fn key_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 1usize) + } + pub fn r_curly_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 2usize) + } +} +impl Serialize for SvelteKeyClosingBlock { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.as_fields().serialize(serializer) + } +} +#[derive(Serialize)] +pub struct SvelteKeyClosingBlockFields { + pub sv_curly_slash_token: SyntaxResult, + pub key_token: SyntaxResult, + pub r_curly_token: SyntaxResult, +} +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct SvelteKeyOpeningBlock { + pub(crate) syntax: SyntaxNode, +} +impl SvelteKeyOpeningBlock { + #[doc = r" Create an AstNode from a SyntaxNode without checking its kind"] + #[doc = r""] + #[doc = r" # Safety"] + #[doc = r" This function must be guarded with a call to [AstNode::can_cast]"] + #[doc = r" or a match on [SyntaxNode::kind]"] + #[inline] + pub const unsafe fn new_unchecked(syntax: SyntaxNode) -> Self { + Self { syntax } + } + pub fn as_fields(&self) -> SvelteKeyOpeningBlockFields { + SvelteKeyOpeningBlockFields { + sv_curly_hash_token: self.sv_curly_hash_token(), + key_token: self.key_token(), + expression: self.expression(), + r_curly_token: self.r_curly_token(), + } + } + pub fn sv_curly_hash_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 0usize) + } + pub fn key_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 1usize) + } + pub fn expression(&self) -> SyntaxResult { + support::required_node(&self.syntax, 2usize) + } + pub fn r_curly_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 3usize) + } +} +impl Serialize for SvelteKeyOpeningBlock { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.as_fields().serialize(serializer) + } +} +#[derive(Serialize)] +pub struct SvelteKeyOpeningBlockFields { + pub sv_curly_hash_token: SyntaxResult, + pub key_token: SyntaxResult, + pub expression: SyntaxResult, + pub r_curly_token: SyntaxResult, +} +#[derive(Clone, PartialEq, Eq, Hash)] pub struct SvelteName { pub(crate) syntax: SyntaxNode, } @@ -1108,6 +1248,7 @@ impl AnyHtmlTextExpression { pub enum AnySvelteBlock { SvelteBogusBlock(SvelteBogusBlock), SvelteDebugBlock(SvelteDebugBlock), + SvelteKeyBlock(SvelteKeyBlock), } impl AnySvelteBlock { pub fn as_svelte_bogus_block(&self) -> Option<&SvelteBogusBlock> { @@ -1122,6 +1263,12 @@ impl AnySvelteBlock { _ => None, } } + pub fn as_svelte_key_block(&self) -> Option<&SvelteKeyBlock> { + match &self { + Self::SvelteKeyBlock(item) => Some(item), + _ => None, + } + } } impl AstNode for AstroEmbeddedContent { type Language = Language; @@ -2219,6 +2366,172 @@ impl From for SyntaxElement { n.syntax.into() } } +impl AstNode for SvelteKeyBlock { + type Language = Language; + const KIND_SET: SyntaxKindSet = + SyntaxKindSet::from_raw(RawSyntaxKind(SVELTE_KEY_BLOCK as u16)); + fn can_cast(kind: SyntaxKind) -> bool { + kind == SVELTE_KEY_BLOCK + } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } + fn into_syntax(self) -> SyntaxNode { + self.syntax + } +} +impl std::fmt::Debug for SvelteKeyBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + thread_local! { static DEPTH : std :: cell :: Cell < u8 > = const { std :: cell :: Cell :: new (0) } }; + let current_depth = DEPTH.get(); + let result = if current_depth < 16 { + DEPTH.set(current_depth + 1); + f.debug_struct("SvelteKeyBlock") + .field( + "opening_block", + &support::DebugSyntaxResult(self.opening_block()), + ) + .field("children", &self.children()) + .field( + "closing_block", + &support::DebugSyntaxResult(self.closing_block()), + ) + .finish() + } else { + f.debug_struct("SvelteKeyBlock").finish() + }; + DEPTH.set(current_depth); + result + } +} +impl From for SyntaxNode { + fn from(n: SvelteKeyBlock) -> Self { + n.syntax + } +} +impl From for SyntaxElement { + fn from(n: SvelteKeyBlock) -> Self { + n.syntax.into() + } +} +impl AstNode for SvelteKeyClosingBlock { + type Language = Language; + const KIND_SET: SyntaxKindSet = + SyntaxKindSet::from_raw(RawSyntaxKind(SVELTE_KEY_CLOSING_BLOCK as u16)); + fn can_cast(kind: SyntaxKind) -> bool { + kind == SVELTE_KEY_CLOSING_BLOCK + } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } + fn into_syntax(self) -> SyntaxNode { + self.syntax + } +} +impl std::fmt::Debug for SvelteKeyClosingBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + thread_local! { static DEPTH : std :: cell :: Cell < u8 > = const { std :: cell :: Cell :: new (0) } }; + let current_depth = DEPTH.get(); + let result = if current_depth < 16 { + DEPTH.set(current_depth + 1); + f.debug_struct("SvelteKeyClosingBlock") + .field( + "sv_curly_slash_token", + &support::DebugSyntaxResult(self.sv_curly_slash_token()), + ) + .field("key_token", &support::DebugSyntaxResult(self.key_token())) + .field( + "r_curly_token", + &support::DebugSyntaxResult(self.r_curly_token()), + ) + .finish() + } else { + f.debug_struct("SvelteKeyClosingBlock").finish() + }; + DEPTH.set(current_depth); + result + } +} +impl From for SyntaxNode { + fn from(n: SvelteKeyClosingBlock) -> Self { + n.syntax + } +} +impl From for SyntaxElement { + fn from(n: SvelteKeyClosingBlock) -> Self { + n.syntax.into() + } +} +impl AstNode for SvelteKeyOpeningBlock { + type Language = Language; + const KIND_SET: SyntaxKindSet = + SyntaxKindSet::from_raw(RawSyntaxKind(SVELTE_KEY_OPENING_BLOCK as u16)); + fn can_cast(kind: SyntaxKind) -> bool { + kind == SVELTE_KEY_OPENING_BLOCK + } + fn cast(syntax: SyntaxNode) -> Option { + if Self::can_cast(syntax.kind()) { + Some(Self { syntax }) + } else { + None + } + } + fn syntax(&self) -> &SyntaxNode { + &self.syntax + } + fn into_syntax(self) -> SyntaxNode { + self.syntax + } +} +impl std::fmt::Debug for SvelteKeyOpeningBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + thread_local! { static DEPTH : std :: cell :: Cell < u8 > = const { std :: cell :: Cell :: new (0) } }; + let current_depth = DEPTH.get(); + let result = if current_depth < 16 { + DEPTH.set(current_depth + 1); + f.debug_struct("SvelteKeyOpeningBlock") + .field( + "sv_curly_hash_token", + &support::DebugSyntaxResult(self.sv_curly_hash_token()), + ) + .field("key_token", &support::DebugSyntaxResult(self.key_token())) + .field("expression", &support::DebugSyntaxResult(self.expression())) + .field( + "r_curly_token", + &support::DebugSyntaxResult(self.r_curly_token()), + ) + .finish() + } else { + f.debug_struct("SvelteKeyOpeningBlock").finish() + }; + DEPTH.set(current_depth); + result + } +} +impl From for SyntaxNode { + fn from(n: SvelteKeyOpeningBlock) -> Self { + n.syntax + } +} +impl From for SyntaxElement { + fn from(n: SvelteKeyOpeningBlock) -> Self { + n.syntax.into() + } +} impl AstNode for SvelteName { type Language = Language; const KIND_SET: SyntaxKindSet = @@ -2764,17 +3077,27 @@ impl From for AnySvelteBlock { Self::SvelteDebugBlock(node) } } +impl From for AnySvelteBlock { + fn from(node: SvelteKeyBlock) -> Self { + Self::SvelteKeyBlock(node) + } +} impl AstNode for AnySvelteBlock { type Language = Language; - const KIND_SET: SyntaxKindSet = - SvelteBogusBlock::KIND_SET.union(SvelteDebugBlock::KIND_SET); + const KIND_SET: SyntaxKindSet = SvelteBogusBlock::KIND_SET + .union(SvelteDebugBlock::KIND_SET) + .union(SvelteKeyBlock::KIND_SET); fn can_cast(kind: SyntaxKind) -> bool { - matches!(kind, SVELTE_BOGUS_BLOCK | SVELTE_DEBUG_BLOCK) + matches!( + kind, + SVELTE_BOGUS_BLOCK | SVELTE_DEBUG_BLOCK | SVELTE_KEY_BLOCK + ) } fn cast(syntax: SyntaxNode) -> Option { let res = match syntax.kind() { SVELTE_BOGUS_BLOCK => Self::SvelteBogusBlock(SvelteBogusBlock { syntax }), SVELTE_DEBUG_BLOCK => Self::SvelteDebugBlock(SvelteDebugBlock { syntax }), + SVELTE_KEY_BLOCK => Self::SvelteKeyBlock(SvelteKeyBlock { syntax }), _ => return None, }; Some(res) @@ -2783,12 +3106,14 @@ impl AstNode for AnySvelteBlock { match self { Self::SvelteBogusBlock(it) => &it.syntax, Self::SvelteDebugBlock(it) => &it.syntax, + Self::SvelteKeyBlock(it) => &it.syntax, } } fn into_syntax(self) -> SyntaxNode { match self { Self::SvelteBogusBlock(it) => it.syntax, Self::SvelteDebugBlock(it) => it.syntax, + Self::SvelteKeyBlock(it) => it.syntax, } } } @@ -2797,6 +3122,7 @@ impl std::fmt::Debug for AnySvelteBlock { match self { Self::SvelteBogusBlock(it) => std::fmt::Debug::fmt(it, f), Self::SvelteDebugBlock(it) => std::fmt::Debug::fmt(it, f), + Self::SvelteKeyBlock(it) => std::fmt::Debug::fmt(it, f), } } } @@ -2805,6 +3131,7 @@ impl From for SyntaxNode { match n { AnySvelteBlock::SvelteBogusBlock(it) => it.into(), AnySvelteBlock::SvelteDebugBlock(it) => it.into(), + AnySvelteBlock::SvelteKeyBlock(it) => it.into(), } } } @@ -2949,6 +3276,21 @@ impl std::fmt::Display for SvelteDebugBlock { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for SvelteKeyBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +impl std::fmt::Display for SvelteKeyClosingBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} +impl std::fmt::Display for SvelteKeyOpeningBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for SvelteName { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Display::fmt(self.syntax(), f) diff --git a/crates/biome_html_syntax/src/generated/nodes_mut.rs b/crates/biome_html_syntax/src/generated/nodes_mut.rs index e313d156b95e..5836c7ab5280 100644 --- a/crates/biome_html_syntax/src/generated/nodes_mut.rs +++ b/crates/biome_html_syntax/src/generated/nodes_mut.rs @@ -379,6 +379,72 @@ impl SvelteDebugBlock { ) } } +impl SvelteKeyBlock { + pub fn with_opening_block(self, element: SvelteKeyOpeningBlock) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(0usize..=0usize, once(Some(element.into_syntax().into()))), + ) + } + pub fn with_children(self, element: HtmlElementList) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(1usize..=1usize, once(Some(element.into_syntax().into()))), + ) + } + pub fn with_closing_block(self, element: SvelteKeyClosingBlock) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(2usize..=2usize, once(Some(element.into_syntax().into()))), + ) + } +} +impl SvelteKeyClosingBlock { + pub fn with_sv_curly_slash_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(0usize..=0usize, once(Some(element.into()))), + ) + } + pub fn with_key_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(1usize..=1usize, once(Some(element.into()))), + ) + } + pub fn with_r_curly_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(2usize..=2usize, once(Some(element.into()))), + ) + } +} +impl SvelteKeyOpeningBlock { + pub fn with_sv_curly_hash_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(0usize..=0usize, once(Some(element.into()))), + ) + } + pub fn with_key_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(1usize..=1usize, once(Some(element.into()))), + ) + } + pub fn with_expression(self, element: HtmlTextExpression) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(2usize..=2usize, once(Some(element.into_syntax().into()))), + ) + } + pub fn with_r_curly_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(3usize..=3usize, once(Some(element.into()))), + ) + } +} impl SvelteName { pub fn with_svelte_ident_token(self, element: SyntaxToken) -> Self { Self::unwrap_cast( diff --git a/xtask/codegen/html.ungram b/xtask/codegen/html.ungram index 594025cb8d6c..fe5c7964f999 100644 --- a/xtask/codegen/html.ungram +++ b/xtask/codegen/html.ungram @@ -190,15 +190,40 @@ AnyHtmlAttributeInitializer = AnySvelteBlock = SvelteDebugBlock + | SvelteKeyBlock | SvelteBogusBlock // {@debug} +// ^^^^^^^^ SvelteDebugBlock = '{@' 'debug' bindings: SvelteBindingList '}' +// {#key ...} ... {/key} +// ^^^^^^^^^^^^^^^^^^^^^ +SvelteKeyBlock = + opening_block: SvelteKeyOpeningBlock + children: HtmlElementList + closing_block: SvelteKeyClosingBlock + +// {#key ...} ... {/key} +// ^^^^^^^^^^ +SvelteKeyOpeningBlock = + '{#' + 'key' + expression: HtmlTextExpression + '}' + +// {#key ...} ... {/key} +// ^^^^^^ +SvelteKeyClosingBlock = + '{/' + 'key' + '}' + + SvelteBindingList = (SvelteName (',' SvelteName)*) SvelteName = 'svelte_ident' diff --git a/xtask/codegen/src/html_kinds_src.rs b/xtask/codegen/src/html_kinds_src.rs index db99472b5359..f8938602608f 100644 --- a/xtask/codegen/src/html_kinds_src.rs +++ b/xtask/codegen/src/html_kinds_src.rs @@ -21,7 +21,7 @@ pub const HTML_KINDS_SRC: KindsSrc = KindsSrc { ("{:", "SV_CURLY_COLON"), (",", "COMMA"), ], - keywords: &["null", "true", "false", "doctype", "html", "debug"], + keywords: &["null", "true", "false", "doctype", "html", "debug", "key"], literals: &["HTML_STRING_LITERAL", "HTML_LITERAL"], tokens: &[ "ERROR_TOKEN", @@ -58,6 +58,9 @@ pub const HTML_KINDS_SRC: KindsSrc = KindsSrc { "ASTRO_EMBEDDED_CONTENT", // Svelte nodes "SVELTE_DEBUG_BLOCK", + "SVELTE_KEY_BLOCK", + "SVELTE_KEY_OPENING_BLOCK", + "SVELTE_KEY_CLOSING_BLOCK", "SVELTE_BINDING_LIST", "SVELTE_NAME", // Bogus nodes