diff --git a/.changeset/ninety-rice-like.md b/.changeset/ninety-rice-like.md new file mode 100644 index 000000000000..07319338b8ab --- /dev/null +++ b/.changeset/ninety-rice-like.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Added proper parsing for spread attributes `{...props}` in Svelte and Astro files. diff --git a/crates/biome_cli/tests/cases/handle_astro_files.rs b/crates/biome_cli/tests/cases/handle_astro_files.rs index c9c5f59da998..3c55bf6a2e41 100644 --- a/crates/biome_cli/tests/cases/handle_astro_files.rs +++ b/crates/biome_cli/tests/cases/handle_astro_files.rs @@ -770,6 +770,7 @@ fn embedded_bindings_are_tracked_correctly() { import { Component } from "./component.svelte"; let hello = "Hello World"; let array = []; +let props = []; --- @@ -777,6 +778,7 @@ let array = []; {notDefined} { array.map(item => ({item})) } + "# .as_bytes(), diff --git a/crates/biome_cli/tests/cases/handle_svelte_files.rs b/crates/biome_cli/tests/cases/handle_svelte_files.rs index fd31ae5e23c5..b803107c8d6f 100644 --- a/crates/biome_cli/tests/cases/handle_svelte_files.rs +++ b/crates/biome_cli/tests/cases/handle_svelte_files.rs @@ -561,6 +561,7 @@ fn embedded_bindings_are_tracked_correctly() { import { Component } from "./component.svelte"; let hello = "Hello World"; let array = []; +let props = []; @@ -569,6 +570,7 @@ let array = []; {#each array as item} {/each} + "# .as_bytes(), diff --git a/crates/biome_cli/tests/snapshots/main_cases_handle_astro_files/embedded_bindings_are_tracked_correctly.snap b/crates/biome_cli/tests/snapshots/main_cases_handle_astro_files/embedded_bindings_are_tracked_correctly.snap index 651c7678cf0f..d6fddd3c4a5c 100644 --- a/crates/biome_cli/tests/snapshots/main_cases_handle_astro_files/embedded_bindings_are_tracked_correctly.snap +++ b/crates/biome_cli/tests/snapshots/main_cases_handle_astro_files/embedded_bindings_are_tracked_correctly.snap @@ -20,6 +20,7 @@ expression: redactor(content) import { Component } from "./component.svelte"; let hello = "Hello World"; let array = []; +let props = []; --- @@ -27,6 +28,7 @@ let array = []; {notDefined} { array.map(item => ({item})) } + ``` diff --git a/crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/embedded_bindings_are_tracked_correctly.snap b/crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/embedded_bindings_are_tracked_correctly.snap index a4b4c3701102..9550d0017295 100644 --- a/crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/embedded_bindings_are_tracked_correctly.snap +++ b/crates/biome_cli/tests/snapshots/main_cases_handle_svelte_files/embedded_bindings_are_tracked_correctly.snap @@ -20,6 +20,7 @@ expression: redactor(content) import { Component } from "./component.svelte"; let hello = "Hello World"; let array = []; +let props = []; @@ -28,6 +29,7 @@ let array = []; {#each array as item} {/each} + ``` diff --git a/crates/biome_html_factory/src/generated/node_factory.rs b/crates/biome_html_factory/src/generated/node_factory.rs index 2ff3ca5c4bc4..56bba86ffee2 100644 --- a/crates/biome_html_factory/src/generated/node_factory.rs +++ b/crates/biome_html_factory/src/generated/node_factory.rs @@ -349,6 +349,22 @@ pub fn html_single_text_expression( ], )) } +pub fn html_spread_attribute( + l_curly_token: SyntaxToken, + dotdotdot_token: SyntaxToken, + argument: HtmlTextExpression, + r_curly_token: SyntaxToken, +) -> HtmlSpreadAttribute { + HtmlSpreadAttribute::unwrap_cast(SyntaxNode::new_detached( + HtmlSyntaxKind::HTML_SPREAD_ATTRIBUTE, + [ + Some(SyntaxElement::Token(l_curly_token)), + Some(SyntaxElement::Token(dotdotdot_token)), + Some(SyntaxElement::Node(argument.into_syntax())), + Some(SyntaxElement::Token(r_curly_token)), + ], + )) +} pub fn html_string(value_token: SyntaxToken) -> HtmlString { HtmlString::unwrap_cast(SyntaxNode::new_detached( HtmlSyntaxKind::HTML_STRING, diff --git a/crates/biome_html_factory/src/generated/syntax_factory.rs b/crates/biome_html_factory/src/generated/syntax_factory.rs index cae37e3be3ec..2f695a4f5b1e 100644 --- a/crates/biome_html_factory/src/generated/syntax_factory.rs +++ b/crates/biome_html_factory/src/generated/syntax_factory.rs @@ -611,6 +611,46 @@ impl SyntaxFactory for HtmlSyntaxFactory { } slots.into_node(HTML_SINGLE_TEXT_EXPRESSION, children) } + HTML_SPREAD_ATTRIBUTE => { + 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 ! [...] + { + 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( + HTML_SPREAD_ATTRIBUTE.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(HTML_SPREAD_ATTRIBUTE, children) + } HTML_STRING => { 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 3010205ee9db..bbefa8c4aa1b 100644 --- a/crates/biome_html_formatter/src/generated.rs +++ b/crates/biome_html_formatter/src/generated.rs @@ -682,6 +682,44 @@ impl IntoFormat for biome_html_syntax::HtmlSingleTextExpressi ) } } +impl FormatRule + for crate::html::auxiliary::spread_attribute::FormatHtmlSpreadAttribute +{ + type Context = HtmlFormatContext; + #[inline(always)] + fn fmt( + &self, + node: &biome_html_syntax::HtmlSpreadAttribute, + f: &mut HtmlFormatter, + ) -> FormatResult<()> { + FormatNodeRule::::fmt(self, node, f) + } +} +impl AsFormat for biome_html_syntax::HtmlSpreadAttribute { + type Format<'a> = FormatRefWithRule< + 'a, + biome_html_syntax::HtmlSpreadAttribute, + crate::html::auxiliary::spread_attribute::FormatHtmlSpreadAttribute, + >; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule::new( + self, + crate::html::auxiliary::spread_attribute::FormatHtmlSpreadAttribute::default(), + ) + } +} +impl IntoFormat for biome_html_syntax::HtmlSpreadAttribute { + type Format = FormatOwnedWithRule< + biome_html_syntax::HtmlSpreadAttribute, + crate::html::auxiliary::spread_attribute::FormatHtmlSpreadAttribute, + >; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule::new( + self, + crate::html::auxiliary::spread_attribute::FormatHtmlSpreadAttribute::default(), + ) + } +} impl FormatRule for crate::html::auxiliary::string::FormatHtmlString { diff --git a/crates/biome_html_formatter/src/html/any/attribute.rs b/crates/biome_html_formatter/src/html/any/attribute.rs index b7f2a967d2e3..2eef1d2f93d2 100644 --- a/crates/biome_html_formatter/src/html/any/attribute.rs +++ b/crates/biome_html_formatter/src/html/any/attribute.rs @@ -14,6 +14,7 @@ impl FormatRule for FormatAnyHtmlAttribute { AnyHtmlAttribute::HtmlBogusAttribute(node) => node.format().fmt(f), AnyHtmlAttribute::HtmlDoubleTextExpression(node) => node.format().fmt(f), AnyHtmlAttribute::HtmlSingleTextExpression(node) => node.format().fmt(f), + AnyHtmlAttribute::HtmlSpreadAttribute(node) => node.format().fmt(f), AnyHtmlAttribute::SvelteAttachAttribute(node) => node.format().fmt(f), } } diff --git a/crates/biome_html_formatter/src/html/auxiliary/mod.rs b/crates/biome_html_formatter/src/html/auxiliary/mod.rs index efa516139811..5ab437627397 100644 --- a/crates/biome_html_formatter/src/html/auxiliary/mod.rs +++ b/crates/biome_html_formatter/src/html/auxiliary/mod.rs @@ -16,6 +16,7 @@ pub(crate) mod opening_element; pub(crate) mod root; pub(crate) mod self_closing_element; pub(crate) mod single_text_expression; +pub(crate) mod spread_attribute; pub(crate) mod string; pub(crate) mod tag_name; pub(crate) mod text_expression; diff --git a/crates/biome_html_formatter/src/html/auxiliary/spread_attribute.rs b/crates/biome_html_formatter/src/html/auxiliary/spread_attribute.rs new file mode 100644 index 000000000000..bf80eb93d6bd --- /dev/null +++ b/crates/biome_html_formatter/src/html/auxiliary/spread_attribute.rs @@ -0,0 +1,25 @@ +use crate::prelude::*; +use biome_formatter::write; +use biome_html_syntax::{HtmlSpreadAttribute, HtmlSpreadAttributeFields}; +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatHtmlSpreadAttribute; +impl FormatNodeRule for FormatHtmlSpreadAttribute { + fn fmt_fields(&self, node: &HtmlSpreadAttribute, f: &mut HtmlFormatter) -> FormatResult<()> { + let HtmlSpreadAttributeFields { + l_curly_token, + dotdotdot_token, + argument, + r_curly_token, + } = node.as_fields(); + + write!( + f, + [ + l_curly_token.format(), + dotdotdot_token.format(), + argument.format(), + r_curly_token.format() + ] + ) + } +} diff --git a/crates/biome_html_formatter/src/html/lists/attribute_list.rs b/crates/biome_html_formatter/src/html/lists/attribute_list.rs index 0507e9f3888d..3d4e56b26a4f 100644 --- a/crates/biome_html_formatter/src/html/lists/attribute_list.rs +++ b/crates/biome_html_formatter/src/html/lists/attribute_list.rs @@ -77,6 +77,9 @@ impl FormatRule for FormatHtmlAttributeList { AnyHtmlAttribute::AnySvelteDirective(attr) => { attr.format().fmt(f) } + AnyHtmlAttribute::HtmlSpreadAttribute(attr) => { + attr.format().fmt(f) + } }) })) .finish()?; diff --git a/crates/biome_html_parser/src/syntax/astro.rs b/crates/biome_html_parser/src/syntax/astro.rs index 458a111fbdee..fc3db10bb11d 100644 --- a/crates/biome_html_parser/src/syntax/astro.rs +++ b/crates/biome_html_parser/src/syntax/astro.rs @@ -1,13 +1,16 @@ use crate::parser::HtmlParser; -use crate::syntax::parse_error::expected_closed_fence; +use crate::syntax::HtmlSyntaxFeatures::Astro; +use crate::syntax::parse_error::{expected_closed_fence, expected_expression}; +use crate::syntax::{TextExpression, parse_single_text_expression}; use crate::token_source::HtmlLexContext; use biome_html_syntax::HtmlSyntaxKind::{ - ASTRO_EMBEDDED_CONTENT, ASTRO_FRONTMATTER_ELEMENT, FENCE, HTML_LITERAL, + ASTRO_EMBEDDED_CONTENT, ASTRO_FRONTMATTER_ELEMENT, FENCE, HTML_LITERAL, HTML_SPREAD_ATTRIBUTE, }; use biome_html_syntax::T; -use biome_parser::Parser; +use biome_parser::parsed_syntax::ParsedSyntax::Present; use biome_parser::prelude::ParsedSyntax; use biome_parser::prelude::ParsedSyntax::Absent; +use biome_parser::{Parser, SyntaxFeature}; pub(crate) fn parse_astro_fence(p: &mut HtmlParser) -> ParsedSyntax { if !p.at(T![---]) { @@ -39,3 +42,36 @@ pub(crate) fn parse_astro_embedded(p: &mut HtmlParser) -> ParsedSyntax { ParsedSyntax::Present(m.complete(p, ASTRO_EMBEDDED_CONTENT)) } + +/// Parses a spread attribute or a single text expression. +pub(crate) fn parse_astro_spread_or_expression(p: &mut HtmlParser) -> ParsedSyntax { + if !Astro.is_supported(p) { + return Absent; + } + + if !p.at(T!['{']) { + return Absent; + } + + let checkpoint = p.checkpoint(); + let m = p.start(); + + // We bump using svelte context because it's faster to lex a possible ..., which is also + // only consumable when using the Svelte context + p.bump_with_context(T!['{'], HtmlLexContext::Svelte); + + if p.at(T![...]) { + p.bump_with_context(T![...], HtmlLexContext::single_expression()); + TextExpression::new_single() + .parse_element(p) + .or_add_diagnostic(p, expected_expression); + + p.expect_with_context(T!['}'], HtmlLexContext::InsideTag); + + Present(m.complete(p, HTML_SPREAD_ATTRIBUTE)) + } else { + p.rewind(checkpoint); + m.abandon(p); + parse_single_text_expression(p, HtmlLexContext::InsideTag) + } +} diff --git a/crates/biome_html_parser/src/syntax/mod.rs b/crates/biome_html_parser/src/syntax/mod.rs index 03b64e9b4b09..2b6f97510352 100644 --- a/crates/biome_html_parser/src/syntax/mod.rs +++ b/crates/biome_html_parser/src/syntax/mod.rs @@ -5,11 +5,12 @@ mod vue; use crate::parser::HtmlParser; use crate::syntax::HtmlSyntaxFeatures::{Astro, DoubleTextExpressions, SingleTextExpressions, Vue}; -use crate::syntax::astro::parse_astro_fence; +use crate::syntax::astro::{parse_astro_fence, parse_astro_spread_or_expression}; use crate::syntax::parse_error::*; use crate::syntax::svelte::{ is_at_svelte_directive_start, is_at_svelte_keyword, parse_attach_attribute, parse_svelte_at_block, parse_svelte_directive, parse_svelte_hash_block, + parse_svelte_spread_or_expression, }; use crate::syntax::vue::{ parse_vue_directive, parse_vue_v_bind_shorthand_directive, parse_vue_v_on_shorthand_directive, @@ -461,9 +462,12 @@ fn parse_attribute(p: &mut HtmlParser) -> ParsedSyntax { parse_vue_v_slot_shorthand_directive, |p, m| disabled_vue(p, m.range(p)), ), + T!['{'] if SingleTextExpressions.is_supported(p) => parse_svelte_spread_or_expression(p), + T!['{'] if Astro.is_supported(p) => parse_astro_spread_or_expression(p), + // Keep previous behaviour so that invalid documents are still parsed. T!['{'] => SingleTextExpressions.parse_exclusive_syntax( p, - |p| parse_single_text_expression(p, HtmlLexContext::InsideTag), + |p| parse_svelte_spread_or_expression(p), |p: &HtmlParser<'_>, m: &CompletedMarker| disabled_svelte(p, m.range(p)), ), T!["{@"] => SingleTextExpressions.parse_exclusive_syntax( @@ -472,8 +476,7 @@ fn parse_attribute(p: &mut HtmlParser) -> ParsedSyntax { |p: &HtmlParser<'_>, m: &CompletedMarker| disabled_svelte(p, m.range(p)), ), _ if p.cur_text().starts_with("v-") => { - HtmlSyntaxFeatures::Vue - .parse_exclusive_syntax(p, parse_vue_directive, |p, m| disabled_vue(p, m.range(p))) + Vue.parse_exclusive_syntax(p, parse_vue_directive, |p, m| disabled_vue(p, m.range(p))) } _ if is_at_svelte_directive_start(p) => { SingleTextExpressions.parse_exclusive_syntax(p, parse_svelte_directive, |p, m| { @@ -486,10 +489,8 @@ fn parse_attribute(p: &mut HtmlParser) -> ParsedSyntax { if p.at(T![=]) { parse_attribute_initializer(p).ok(); - Present(m.complete(p, HTML_ATTRIBUTE)) - } else { - Present(m.complete(p, HTML_ATTRIBUTE)) } + Present(m.complete(p, HTML_ATTRIBUTE)) } } } @@ -669,7 +670,7 @@ pub(crate) fn parse_single_text_expression( p: &mut HtmlParser, context: HtmlLexContext, ) -> ParsedSyntax { - if !HtmlSyntaxFeatures::SingleTextExpressions.is_supported(p) { + if !SingleTextExpressions.is_supported(p) { return Absent; } diff --git a/crates/biome_html_parser/src/syntax/svelte.rs b/crates/biome_html_parser/src/syntax/svelte.rs index b25f0822f107..94837cad73fe 100644 --- a/crates/biome_html_parser/src/syntax/svelte.rs +++ b/crates/biome_html_parser/src/syntax/svelte.rs @@ -1,23 +1,25 @@ use crate::parser::HtmlParser; +use crate::syntax::HtmlSyntaxFeatures::SingleTextExpressions; use crate::syntax::parse_error::{ expected_child_or_block, expected_expression, expected_name, expected_svelte_closing_block, expected_svelte_property, expected_text_expression, expected_valid_directive, }; use crate::syntax::{ - parse_attribute_initializer, parse_html_element, parse_single_text_expression_content, + TextExpression, parse_attribute_initializer, parse_html_element, parse_single_text_expression, + parse_single_text_expression_content, }; use crate::token_source::{HtmlLexContext, HtmlReLexContext, RestrictedExpressionStopAt}; use biome_html_syntax::HtmlSyntaxKind::{ - EOF, HTML_BOGUS_ELEMENT, HTML_ELEMENT_LIST, HTML_LITERAL, IDENT, SVELTE_ANIMATE_DIRECTIVE, - SVELTE_ATTACH_ATTRIBUTE, SVELTE_AWAIT_BLOCK, SVELTE_AWAIT_CATCH_BLOCK, - SVELTE_AWAIT_CATCH_CLAUSE, SVELTE_AWAIT_CLAUSES_LIST, SVELTE_AWAIT_CLOSING_BLOCK, - SVELTE_AWAIT_OPENING_BLOCK, SVELTE_AWAIT_THEN_BLOCK, SVELTE_AWAIT_THEN_CLAUSE, - SVELTE_BIND_DIRECTIVE, SVELTE_BINDING_ASSIGNMENT_BINDING_LIST, SVELTE_BINDING_LIST, - SVELTE_BOGUS_BLOCK, SVELTE_CLASS_DIRECTIVE, SVELTE_CONST_BLOCK, SVELTE_CURLY_DESTRUCTURED_NAME, - SVELTE_DEBUG_BLOCK, SVELTE_DIRECTIVE_MODIFIER, SVELTE_DIRECTIVE_MODIFIER_LIST, - SVELTE_DIRECTIVE_VALUE, SVELTE_EACH_AS_KEYED_ITEM, SVELTE_EACH_BLOCK, - SVELTE_EACH_CLOSING_BLOCK, SVELTE_EACH_INDEX, SVELTE_EACH_KEY, SVELTE_EACH_KEYED_ITEM, - SVELTE_EACH_OPENING_BLOCK, SVELTE_ELSE_CLAUSE, SVELTE_ELSE_IF_CLAUSE, + EOF, HTML_BOGUS_ELEMENT, HTML_ELEMENT_LIST, HTML_LITERAL, HTML_SPREAD_ATTRIBUTE, IDENT, + SVELTE_ANIMATE_DIRECTIVE, SVELTE_ATTACH_ATTRIBUTE, SVELTE_AWAIT_BLOCK, + SVELTE_AWAIT_CATCH_BLOCK, SVELTE_AWAIT_CATCH_CLAUSE, SVELTE_AWAIT_CLAUSES_LIST, + SVELTE_AWAIT_CLOSING_BLOCK, SVELTE_AWAIT_OPENING_BLOCK, SVELTE_AWAIT_THEN_BLOCK, + SVELTE_AWAIT_THEN_CLAUSE, SVELTE_BIND_DIRECTIVE, SVELTE_BINDING_ASSIGNMENT_BINDING_LIST, + SVELTE_BINDING_LIST, SVELTE_BOGUS_BLOCK, SVELTE_CLASS_DIRECTIVE, SVELTE_CONST_BLOCK, + SVELTE_CURLY_DESTRUCTURED_NAME, SVELTE_DEBUG_BLOCK, SVELTE_DIRECTIVE_MODIFIER, + SVELTE_DIRECTIVE_MODIFIER_LIST, SVELTE_DIRECTIVE_VALUE, SVELTE_EACH_AS_KEYED_ITEM, + SVELTE_EACH_BLOCK, SVELTE_EACH_CLOSING_BLOCK, SVELTE_EACH_INDEX, SVELTE_EACH_KEY, + SVELTE_EACH_KEYED_ITEM, SVELTE_EACH_OPENING_BLOCK, SVELTE_ELSE_CLAUSE, SVELTE_ELSE_IF_CLAUSE, SVELTE_ELSE_IF_CLAUSE_LIST, SVELTE_HTML_BLOCK, SVELTE_IF_BLOCK, SVELTE_IF_CLOSING_BLOCK, SVELTE_IF_OPENING_BLOCK, SVELTE_IN_DIRECTIVE, SVELTE_KEY_BLOCK, SVELTE_KEY_CLOSING_BLOCK, SVELTE_KEY_OPENING_BLOCK, SVELTE_LITERAL, SVELTE_NAME, SVELTE_OUT_DIRECTIVE, @@ -30,7 +32,7 @@ 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}; +use biome_parser::{Marker, Parser, SyntaxFeature, TokenSet, token_set}; use biome_rowan::TextRange; use std::ops::Sub; @@ -212,7 +214,7 @@ fn parse_each_as_keyed_item(p: &mut HtmlParser) -> ParsedSyntax { parse_square_destructured_name(p) } else { // Parse name (required) - parse_name(p) + parse_svelte_name(p) } .or_add_diagnostic(p, |p, range| { p.err_builder("Expected a binding pattern after 'as'", range) @@ -283,7 +285,7 @@ fn parse_each_index(p: &mut HtmlParser) -> ParsedSyntax { // Parse the index let m = p.start(); p.bump_with_context(T![,], HtmlLexContext::Svelte); - parse_name(p).or_add_diagnostic(p, |p, range| { + parse_svelte_name(p).or_add_diagnostic(p, |p, range| { p.err_builder("Expected an index binding after ','", range) }); Present(m.complete(p, SVELTE_EACH_INDEX)) @@ -354,6 +356,39 @@ fn parse_each_opening_block(p: &mut HtmlParser, parent_marker: Marker) -> (Parse } // #endregion +/// Parses a spread attribute or a single text expression. +pub(crate) fn parse_svelte_spread_or_expression(p: &mut HtmlParser) -> ParsedSyntax { + if !SingleTextExpressions.is_supported(p) { + return Absent; + } + + if !p.at(T!['{']) { + return Absent; + } + + let checkpoint = p.checkpoint(); + let m = p.start(); + + // We bump using svelte context because it's faster to lex a possible ..., which is also + // only consumable when using the Svelte context + p.bump_with_context(T!['{'], HtmlLexContext::Svelte); + + if p.at(T![...]) { + p.bump_with_context(T![...], HtmlLexContext::single_expression()); + + TextExpression::new_single() + .parse_element(p) + .or_add_diagnostic(p, expected_expression); + + p.expect_with_context(T!['}'], HtmlLexContext::InsideTag); + Present(m.complete(p, HTML_SPREAD_ATTRIBUTE)) + } else { + p.rewind(checkpoint); + m.abandon(p); + parse_single_text_expression(p, HtmlLexContext::InsideTag) + } +} + // #region await parse functions fn parse_await_block(p: &mut HtmlParser, parent_marker: Marker) -> ParsedSyntax { @@ -899,7 +934,7 @@ impl ParseSeparatedList for BindingList { const LIST_KIND: Self::Kind = SVELTE_BINDING_LIST; fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { - parse_name(p) + parse_svelte_name(p) } fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool { @@ -928,7 +963,7 @@ impl ParseSeparatedList for BindingList { } /// Parses a Svelte name -fn parse_name(p: &mut HtmlParser) -> ParsedSyntax { +fn parse_svelte_name(p: &mut HtmlParser) -> ParsedSyntax { if !p.at(IDENT) && !is_at_svelte_keyword(p) { return Absent; } @@ -951,7 +986,7 @@ fn parse_rest_name(p: &mut HtmlParser) -> ParsedSyntax { } let m = p.start(); p.bump_with_context(T![...], HtmlLexContext::Svelte); - parse_name(p).or_add_diagnostic(p, |p, range| { + parse_svelte_name(p).or_add_diagnostic(p, |p, range| { p.err_builder("Expected a valid Svelte name after '...'", range) }); @@ -1055,7 +1090,7 @@ impl ParseSeparatedList for SvelteBindingAssignmentBindingList { if p.at(T![...]) { parse_rest_name(p) } else { - parse_name(p) + parse_svelte_name(p) } } @@ -1140,7 +1175,7 @@ fn parse_directive_value(p: &mut HtmlParser, context_after_colon: HtmlLexContext } else if context_after_colon == HtmlLexContext::SvelteBindingLiteral { parse_binding_literal(p).or_add_diagnostic(p, expected_svelte_property); } else { - parse_name(p).or_add_diagnostic(p, expected_name); + parse_svelte_name(p).or_add_diagnostic(p, expected_name); } ModifiersList.parse_list(p); @@ -1183,7 +1218,7 @@ impl ParseNodeList for ModifiersList { fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax { let m = p.start(); p.expect_with_context(T![|], HtmlLexContext::Svelte); - parse_name(p).or_add_diagnostic(p, |p, range| { + parse_svelte_name(p).or_add_diagnostic(p, |p, range| { p.err_builder("Expected a valid Svelte modifier name", range) }); Present(m.complete(p, SVELTE_DIRECTIVE_MODIFIER)) diff --git a/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro b/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro new file mode 100644 index 000000000000..5e0ae6752ffb --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro @@ -0,0 +1,2 @@ + + diff --git a/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro.snap b/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro.snap new file mode 100644 index 000000000000..cb7975395ff6 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/astro/spread.astro.snap @@ -0,0 +1,89 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```astro + + + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@0..1 "<" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@7..8 "{" [] [], + dotdotdot_token: DOT3@8..11 "..." [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@11..43 "props >\n\n" [] [], + }, + r_curly_token: missing (required), + }, + ], + slash_token: missing (optional), + r_angle_token: missing (required), + }, + ], + eof_token: EOF@43..43 "" [] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..43 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..43 + 0: HTML_SELF_CLOSING_ELEMENT@0..43 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_TAG_NAME@1..7 + 0: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@7..43 + 0: HTML_SPREAD_ATTRIBUTE@7..43 + 0: L_CURLY@7..8 "{" [] [] + 1: DOT3@8..11 "..." [] [] + 2: HTML_TEXT_EXPRESSION@11..43 + 0: HTML_LITERAL@11..43 "props >\n\n" [] [] + 3: (empty) + 3: (empty) + 4: (empty) + 4: EOF@43..43 "" [] [] + +``` + +## Diagnostics + +``` +spread.astro:3:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `}` but instead the file ends + + 1 │ + 2 │ + > 3 │ + │ + + i the file ends here + + 1 │ + 2 │ + > 3 │ + │ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte b/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte new file mode 100644 index 000000000000..b1a2cba138b4 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte @@ -0,0 +1,2 @@ + + diff --git a/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte.snap b/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte.snap new file mode 100644 index 000000000000..57932e17da63 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/error/svelte/spread.svelte.snap @@ -0,0 +1,125 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```svelte + + + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@0..1 "<" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@7..8 "{" [] [], + dotdotdot_token: DOT3@8..11 "..." [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@11..11 "" [] [], + }, + r_curly_token: R_CURLY@11..13 "}" [] [Whitespace(" ")], + }, + ], + slash_token: SLASH@13..14 "/" [] [], + r_angle_token: R_ANGLE@14..15 ">" [] [], + }, + HtmlElement { + opening_element: HtmlOpeningElement { + l_angle_token: L_ANGLE@15..17 "<" [Newline("\n")] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@17..27 "Component" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@27..28 "{" [] [], + dotdotdot_token: DOT3@28..31 "..." [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@31..40 "props />\n" [] [], + }, + r_curly_token: missing (required), + }, + ], + r_angle_token: missing (required), + }, + children: HtmlElementList [], + closing_element: missing (required), + }, + ], + eof_token: EOF@40..40 "" [] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..40 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..40 + 0: HTML_SELF_CLOSING_ELEMENT@0..15 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_TAG_NAME@1..7 + 0: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@7..13 + 0: HTML_SPREAD_ATTRIBUTE@7..13 + 0: L_CURLY@7..8 "{" [] [] + 1: DOT3@8..11 "..." [] [] + 2: HTML_TEXT_EXPRESSION@11..11 + 0: HTML_LITERAL@11..11 "" [] [] + 3: R_CURLY@11..13 "}" [] [Whitespace(" ")] + 3: SLASH@13..14 "/" [] [] + 4: R_ANGLE@14..15 ">" [] [] + 1: HTML_ELEMENT@15..40 + 0: HTML_OPENING_ELEMENT@15..40 + 0: L_ANGLE@15..17 "<" [Newline("\n")] [] + 1: HTML_COMPONENT_NAME@17..27 + 0: HTML_LITERAL@17..27 "Component" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@27..40 + 0: HTML_SPREAD_ATTRIBUTE@27..40 + 0: L_CURLY@27..28 "{" [] [] + 1: DOT3@28..31 "..." [] [] + 2: HTML_TEXT_EXPRESSION@31..40 + 0: HTML_LITERAL@31..40 "props />\n" [] [] + 3: (empty) + 3: (empty) + 1: HTML_ELEMENT_LIST@40..40 + 2: (empty) + 4: EOF@40..40 "" [] [] + +``` + +## Diagnostics + +``` +spread.svelte:3:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × expected `}` but instead the file ends + + 1 │ + 2 │ + > 3 │ + │ + + i the file ends here + + 1 │ + 2 │ + > 3 │ + │ + +``` diff --git a/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro b/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro new file mode 100644 index 000000000000..0fc2e72c58b6 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro @@ -0,0 +1,2 @@ + + diff --git a/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro.snap b/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro.snap new file mode 100644 index 000000000000..e60f34126fe1 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/astro/spread.astro.snap @@ -0,0 +1,99 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```astro + + + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@0..1 "<" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@7..8 "{" [] [], + dotdotdot_token: DOT3@8..11 "..." [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@11..16 "props" [] [], + }, + r_curly_token: R_CURLY@16..18 "}" [] [Whitespace(" ")], + }, + ], + slash_token: missing (optional), + r_angle_token: R_ANGLE@18..19 ">" [] [], + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@19..21 "<" [Newline("\n")] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@21..31 "Component" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@31..32 "{" [] [], + dotdotdot_token: DOT3@32..35 "..." [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@35..40 "props" [] [], + }, + r_curly_token: R_CURLY@40..42 "}" [] [Whitespace(" ")], + }, + ], + slash_token: SLASH@42..43 "/" [] [], + r_angle_token: R_ANGLE@43..44 ">" [] [], + }, + ], + eof_token: EOF@44..45 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..45 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..44 + 0: HTML_SELF_CLOSING_ELEMENT@0..19 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_TAG_NAME@1..7 + 0: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@7..18 + 0: HTML_SPREAD_ATTRIBUTE@7..18 + 0: L_CURLY@7..8 "{" [] [] + 1: DOT3@8..11 "..." [] [] + 2: HTML_TEXT_EXPRESSION@11..16 + 0: HTML_LITERAL@11..16 "props" [] [] + 3: R_CURLY@16..18 "}" [] [Whitespace(" ")] + 3: (empty) + 4: R_ANGLE@18..19 ">" [] [] + 1: HTML_SELF_CLOSING_ELEMENT@19..44 + 0: L_ANGLE@19..21 "<" [Newline("\n")] [] + 1: HTML_COMPONENT_NAME@21..31 + 0: HTML_LITERAL@21..31 "Component" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@31..42 + 0: HTML_SPREAD_ATTRIBUTE@31..42 + 0: L_CURLY@31..32 "{" [] [] + 1: DOT3@32..35 "..." [] [] + 2: HTML_TEXT_EXPRESSION@35..40 + 0: HTML_LITERAL@35..40 "props" [] [] + 3: R_CURLY@40..42 "}" [] [Whitespace(" ")] + 3: SLASH@42..43 "/" [] [] + 4: R_ANGLE@43..44 ">" [] [] + 4: EOF@44..45 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/shorthand-spread-props.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/shorthand-spread-props.svelte.snap index 385241c54392..d0a083d92d1e 100644 --- a/crates/biome_html_parser/tests/html_specs/ok/svelte/shorthand-spread-props.svelte.snap +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/shorthand-spread-props.svelte.snap @@ -25,10 +25,11 @@ HtmlRoot { value_token: HTML_LITERAL@1..8 "button" [] [Whitespace(" ")], }, attributes: HtmlAttributeList [ - HtmlSingleTextExpression { + HtmlSpreadAttribute { l_curly_token: L_CURLY@8..9 "{" [] [], - expression: HtmlTextExpression { - html_literal_token: HTML_LITERAL@9..17 "...props" [] [], + dotdotdot_token: DOT3@9..12 "..." [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@12..17 "props" [] [], }, r_curly_token: R_CURLY@17..18 "}" [] [], }, @@ -68,11 +69,12 @@ HtmlRoot { 1: HTML_TAG_NAME@1..8 0: HTML_LITERAL@1..8 "button" [] [Whitespace(" ")] 2: HTML_ATTRIBUTE_LIST@8..18 - 0: HTML_SINGLE_TEXT_EXPRESSION@8..18 + 0: HTML_SPREAD_ATTRIBUTE@8..18 0: L_CURLY@8..9 "{" [] [] - 1: HTML_TEXT_EXPRESSION@9..17 - 0: HTML_LITERAL@9..17 "...props" [] [] - 2: R_CURLY@17..18 "}" [] [] + 1: DOT3@9..12 "..." [] [] + 2: HTML_TEXT_EXPRESSION@12..17 + 0: HTML_LITERAL@12..17 "props" [] [] + 3: R_CURLY@17..18 "}" [] [] 3: R_ANGLE@18..19 ">" [] [] 1: HTML_ELEMENT_LIST@19..27 0: HTML_CONTENT@19..27 diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte b/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte new file mode 100644 index 000000000000..6095f7ff303d --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte @@ -0,0 +1,3 @@ + + + diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte.snap new file mode 100644 index 000000000000..81925398af89 --- /dev/null +++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/spread.svelte.snap @@ -0,0 +1,131 @@ +--- +source: crates/biome_html_parser/tests/spec_test.rs +expression: snapshot +--- +## Input + +```svelte + + + + +``` + + +## AST + +``` +HtmlRoot { + bom_token: missing (optional), + frontmatter: missing (optional), + directive: missing (optional), + html: HtmlElementList [ + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@0..1 "<" [] [], + name: HtmlTagName { + value_token: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@7..8 "{" [] [], + dotdotdot_token: DOT3@8..11 "..." [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@11..14 "foo" [] [], + }, + r_curly_token: R_CURLY@14..16 "}" [] [Whitespace(" ")], + }, + ], + slash_token: SLASH@16..17 "/" [] [], + r_angle_token: R_ANGLE@17..18 ">" [] [], + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@18..20 "<" [Newline("\n")] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@20..30 "Component" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@30..31 "{" [] [], + dotdotdot_token: DOT3@31..34 "..." [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@34..37 "foo" [] [], + }, + r_curly_token: R_CURLY@37..39 "}" [] [Whitespace(" ")], + }, + ], + slash_token: SLASH@39..40 "/" [] [], + r_angle_token: R_ANGLE@40..41 ">" [] [], + }, + HtmlSelfClosingElement { + l_angle_token: L_ANGLE@41..43 "<" [Newline("\n")] [], + name: HtmlComponentName { + value_token: HTML_LITERAL@43..53 "Component" [] [Whitespace(" ")], + }, + attributes: HtmlAttributeList [ + HtmlSpreadAttribute { + l_curly_token: L_CURLY@53..54 "{" [] [], + dotdotdot_token: DOT3@54..57 "..." [] [], + argument: HtmlTextExpression { + html_literal_token: HTML_LITERAL@57..70 "foo ? [] : []" [] [], + }, + r_curly_token: R_CURLY@70..72 "}" [] [Whitespace(" ")], + }, + ], + slash_token: SLASH@72..73 "/" [] [], + r_angle_token: R_ANGLE@73..74 ">" [] [], + }, + ], + eof_token: EOF@74..75 "" [Newline("\n")] [], +} +``` + +## CST + +``` +0: HTML_ROOT@0..75 + 0: (empty) + 1: (empty) + 2: (empty) + 3: HTML_ELEMENT_LIST@0..74 + 0: HTML_SELF_CLOSING_ELEMENT@0..18 + 0: L_ANGLE@0..1 "<" [] [] + 1: HTML_TAG_NAME@1..7 + 0: HTML_LITERAL@1..7 "input" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@7..16 + 0: HTML_SPREAD_ATTRIBUTE@7..16 + 0: L_CURLY@7..8 "{" [] [] + 1: DOT3@8..11 "..." [] [] + 2: HTML_TEXT_EXPRESSION@11..14 + 0: HTML_LITERAL@11..14 "foo" [] [] + 3: R_CURLY@14..16 "}" [] [Whitespace(" ")] + 3: SLASH@16..17 "/" [] [] + 4: R_ANGLE@17..18 ">" [] [] + 1: HTML_SELF_CLOSING_ELEMENT@18..41 + 0: L_ANGLE@18..20 "<" [Newline("\n")] [] + 1: HTML_COMPONENT_NAME@20..30 + 0: HTML_LITERAL@20..30 "Component" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@30..39 + 0: HTML_SPREAD_ATTRIBUTE@30..39 + 0: L_CURLY@30..31 "{" [] [] + 1: DOT3@31..34 "..." [] [] + 2: HTML_TEXT_EXPRESSION@34..37 + 0: HTML_LITERAL@34..37 "foo" [] [] + 3: R_CURLY@37..39 "}" [] [Whitespace(" ")] + 3: SLASH@39..40 "/" [] [] + 4: R_ANGLE@40..41 ">" [] [] + 2: HTML_SELF_CLOSING_ELEMENT@41..74 + 0: L_ANGLE@41..43 "<" [Newline("\n")] [] + 1: HTML_COMPONENT_NAME@43..53 + 0: HTML_LITERAL@43..53 "Component" [] [Whitespace(" ")] + 2: HTML_ATTRIBUTE_LIST@53..72 + 0: HTML_SPREAD_ATTRIBUTE@53..72 + 0: L_CURLY@53..54 "{" [] [] + 1: DOT3@54..57 "..." [] [] + 2: HTML_TEXT_EXPRESSION@57..70 + 0: HTML_LITERAL@57..70 "foo ? [] : []" [] [] + 3: R_CURLY@70..72 "}" [] [Whitespace(" ")] + 3: SLASH@72..73 "/" [] [] + 4: R_ANGLE@73..74 ">" [] [] + 4: EOF@74..75 "" [Newline("\n")] [] + +``` diff --git a/crates/biome_html_parser/tests/quick_test.rs b/crates/biome_html_parser/tests/quick_test.rs index 3c74e90185a0..019edf11a486 100644 --- a/crates/biome_html_parser/tests/quick_test.rs +++ b/crates/biome_html_parser/tests/quick_test.rs @@ -5,7 +5,7 @@ use biome_test_utils::has_bogus_nodes_or_empty_slots; #[ignore] #[test] pub fn quick_test() { - let code = r#"

Multiple shorthand

+ let code = r#"

Multiple shorthand

diff --git a/crates/biome_html_syntax/src/generated/kind.rs b/crates/biome_html_syntax/src/generated/kind.rs index b18ba3bbfd5c..b0b4f72f5ac1 100644 --- a/crates/biome_html_syntax/src/generated/kind.rs +++ b/crates/biome_html_syntax/src/generated/kind.rs @@ -95,6 +95,7 @@ pub enum HtmlSyntaxKind { HTML_DOUBLE_TEXT_EXPRESSION, HTML_SINGLE_TEXT_EXPRESSION, HTML_TEXT_EXPRESSION, + HTML_SPREAD_ATTRIBUTE, ASTRO_FRONTMATTER_ELEMENT, ASTRO_EMBEDDED_CONTENT, SVELTE_DEBUG_BLOCK, diff --git a/crates/biome_html_syntax/src/generated/macros.rs b/crates/biome_html_syntax/src/generated/macros.rs index d96f6e40037f..bab2c31d41ae 100644 --- a/crates/biome_html_syntax/src/generated/macros.rs +++ b/crates/biome_html_syntax/src/generated/macros.rs @@ -89,6 +89,10 @@ macro_rules! map_syntax_node { let $pattern = unsafe { $crate::HtmlSingleTextExpression::new_unchecked(node) }; $body } + $crate::HtmlSyntaxKind::HTML_SPREAD_ATTRIBUTE => { + let $pattern = unsafe { $crate::HtmlSpreadAttribute::new_unchecked(node) }; + $body + } $crate::HtmlSyntaxKind::HTML_STRING => { let $pattern = unsafe { $crate::HtmlString::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 6a4f7e5ac019..d44346324b0f 100644 --- a/crates/biome_html_syntax/src/generated/nodes.rs +++ b/crates/biome_html_syntax/src/generated/nodes.rs @@ -825,6 +825,56 @@ pub struct HtmlSingleTextExpressionFields { pub r_curly_token: SyntaxResult, } #[derive(Clone, PartialEq, Eq, Hash)] +pub struct HtmlSpreadAttribute { + pub(crate) syntax: SyntaxNode, +} +impl HtmlSpreadAttribute { + #[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) -> HtmlSpreadAttributeFields { + HtmlSpreadAttributeFields { + l_curly_token: self.l_curly_token(), + dotdotdot_token: self.dotdotdot_token(), + argument: self.argument(), + r_curly_token: self.r_curly_token(), + } + } + pub fn l_curly_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 0usize) + } + pub fn dotdotdot_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 1usize) + } + pub fn argument(&self) -> SyntaxResult { + support::required_node(&self.syntax, 2usize) + } + pub fn r_curly_token(&self) -> SyntaxResult { + support::required_token(&self.syntax, 3usize) + } +} +impl Serialize for HtmlSpreadAttribute { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.as_fields().serialize(serializer) + } +} +#[derive(Serialize)] +pub struct HtmlSpreadAttributeFields { + pub l_curly_token: SyntaxResult, + pub dotdotdot_token: SyntaxResult, + pub argument: SyntaxResult, + pub r_curly_token: SyntaxResult, +} +#[derive(Clone, PartialEq, Eq, Hash)] pub struct HtmlString { pub(crate) syntax: SyntaxNode, } @@ -3376,6 +3426,7 @@ pub enum AnyHtmlAttribute { HtmlBogusAttribute(HtmlBogusAttribute), HtmlDoubleTextExpression(HtmlDoubleTextExpression), HtmlSingleTextExpression(HtmlSingleTextExpression), + HtmlSpreadAttribute(HtmlSpreadAttribute), SvelteAttachAttribute(SvelteAttachAttribute), } impl AnyHtmlAttribute { @@ -3415,6 +3466,12 @@ impl AnyHtmlAttribute { _ => None, } } + pub fn as_html_spread_attribute(&self) -> Option<&HtmlSpreadAttribute> { + match &self { + Self::HtmlSpreadAttribute(item) => Some(item), + _ => None, + } + } pub fn as_svelte_attach_attribute(&self) -> Option<&SvelteAttachAttribute> { match &self { Self::SvelteAttachAttribute(item) => Some(item), @@ -4908,6 +4965,65 @@ impl From for SyntaxElement { n.syntax.into() } } +impl AstNode for HtmlSpreadAttribute { + type Language = Language; + const KIND_SET: SyntaxKindSet = + SyntaxKindSet::from_raw(RawSyntaxKind(HTML_SPREAD_ATTRIBUTE as u16)); + fn can_cast(kind: SyntaxKind) -> bool { + kind == HTML_SPREAD_ATTRIBUTE + } + 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 HtmlSpreadAttribute { + 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("HtmlSpreadAttribute") + .field( + "l_curly_token", + &support::DebugSyntaxResult(self.l_curly_token()), + ) + .field( + "dotdotdot_token", + &support::DebugSyntaxResult(self.dotdotdot_token()), + ) + .field("argument", &support::DebugSyntaxResult(self.argument())) + .field( + "r_curly_token", + &support::DebugSyntaxResult(self.r_curly_token()), + ) + .finish() + } else { + f.debug_struct("HtmlSpreadAttribute").finish() + }; + DEPTH.set(current_depth); + result + } +} +impl From for SyntaxNode { + fn from(n: HtmlSpreadAttribute) -> Self { + n.syntax + } +} +impl From for SyntaxElement { + fn from(n: HtmlSpreadAttribute) -> Self { + n.syntax.into() + } +} impl AstNode for HtmlString { type Language = Language; const KIND_SET: SyntaxKindSet = @@ -8007,6 +8123,11 @@ impl From for AnyHtmlAttribute { Self::HtmlSingleTextExpression(node) } } +impl From for AnyHtmlAttribute { + fn from(node: HtmlSpreadAttribute) -> Self { + Self::HtmlSpreadAttribute(node) + } +} impl From for AnyHtmlAttribute { fn from(node: SvelteAttachAttribute) -> Self { Self::SvelteAttachAttribute(node) @@ -8020,6 +8141,7 @@ impl AstNode for AnyHtmlAttribute { .union(HtmlBogusAttribute::KIND_SET) .union(HtmlDoubleTextExpression::KIND_SET) .union(HtmlSingleTextExpression::KIND_SET) + .union(HtmlSpreadAttribute::KIND_SET) .union(SvelteAttachAttribute::KIND_SET); fn can_cast(kind: SyntaxKind) -> bool { match kind { @@ -8027,6 +8149,7 @@ impl AstNode for AnyHtmlAttribute { | HTML_BOGUS_ATTRIBUTE | HTML_DOUBLE_TEXT_EXPRESSION | HTML_SINGLE_TEXT_EXPRESSION + | HTML_SPREAD_ATTRIBUTE | SVELTE_ATTACH_ATTRIBUTE => true, k if AnySvelteDirective::can_cast(k) => true, k if AnyVueDirective::can_cast(k) => true, @@ -8043,6 +8166,7 @@ impl AstNode for AnyHtmlAttribute { HTML_SINGLE_TEXT_EXPRESSION => { Self::HtmlSingleTextExpression(HtmlSingleTextExpression { syntax }) } + HTML_SPREAD_ATTRIBUTE => Self::HtmlSpreadAttribute(HtmlSpreadAttribute { syntax }), SVELTE_ATTACH_ATTRIBUTE => { Self::SvelteAttachAttribute(SvelteAttachAttribute { syntax }) } @@ -8067,6 +8191,7 @@ impl AstNode for AnyHtmlAttribute { Self::HtmlBogusAttribute(it) => it.syntax(), Self::HtmlDoubleTextExpression(it) => it.syntax(), Self::HtmlSingleTextExpression(it) => it.syntax(), + Self::HtmlSpreadAttribute(it) => it.syntax(), Self::SvelteAttachAttribute(it) => it.syntax(), Self::AnySvelteDirective(it) => it.syntax(), Self::AnyVueDirective(it) => it.syntax(), @@ -8078,6 +8203,7 @@ impl AstNode for AnyHtmlAttribute { Self::HtmlBogusAttribute(it) => it.into_syntax(), Self::HtmlDoubleTextExpression(it) => it.into_syntax(), Self::HtmlSingleTextExpression(it) => it.into_syntax(), + Self::HtmlSpreadAttribute(it) => it.into_syntax(), Self::SvelteAttachAttribute(it) => it.into_syntax(), Self::AnySvelteDirective(it) => it.into_syntax(), Self::AnyVueDirective(it) => it.into_syntax(), @@ -8093,6 +8219,7 @@ impl std::fmt::Debug for AnyHtmlAttribute { Self::HtmlBogusAttribute(it) => std::fmt::Debug::fmt(it, f), Self::HtmlDoubleTextExpression(it) => std::fmt::Debug::fmt(it, f), Self::HtmlSingleTextExpression(it) => std::fmt::Debug::fmt(it, f), + Self::HtmlSpreadAttribute(it) => std::fmt::Debug::fmt(it, f), Self::SvelteAttachAttribute(it) => std::fmt::Debug::fmt(it, f), } } @@ -8106,6 +8233,7 @@ impl From for SyntaxNode { AnyHtmlAttribute::HtmlBogusAttribute(it) => it.into_syntax(), AnyHtmlAttribute::HtmlDoubleTextExpression(it) => it.into_syntax(), AnyHtmlAttribute::HtmlSingleTextExpression(it) => it.into_syntax(), + AnyHtmlAttribute::HtmlSpreadAttribute(it) => it.into_syntax(), AnyHtmlAttribute::SvelteAttachAttribute(it) => it.into_syntax(), } } @@ -9650,6 +9778,11 @@ impl std::fmt::Display for HtmlSingleTextExpression { std::fmt::Display::fmt(self.syntax(), f) } } +impl std::fmt::Display for HtmlSpreadAttribute { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self.syntax(), f) + } +} impl std::fmt::Display for HtmlString { 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 34e63b00384b..64a24326c6fa 100644 --- a/crates/biome_html_syntax/src/generated/nodes_mut.rs +++ b/crates/biome_html_syntax/src/generated/nodes_mut.rs @@ -357,6 +357,32 @@ impl HtmlSingleTextExpression { ) } } +impl HtmlSpreadAttribute { + pub fn with_l_curly_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(0usize..=0usize, once(Some(element.into()))), + ) + } + pub fn with_dotdotdot_token(self, element: SyntaxToken) -> Self { + Self::unwrap_cast( + self.syntax + .splice_slots(1usize..=1usize, once(Some(element.into()))), + ) + } + pub fn with_argument(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 HtmlString { pub fn with_value_token(self, element: SyntaxToken) -> Self { Self::unwrap_cast( diff --git a/xtask/codegen/html.ungram b/xtask/codegen/html.ungram index 90348abd385a..d8802387d973 100644 --- a/xtask/codegen/html.ungram +++ b/xtask/codegen/html.ungram @@ -185,6 +185,7 @@ AnyHtmlAttribute = | HtmlDoubleTextExpression | HtmlSingleTextExpression | SvelteAttachAttribute + | HtmlSpreadAttribute | AnySvelteDirective | AnyVueDirective | HtmlBogusAttribute @@ -208,7 +209,6 @@ AnyHtmlAttributeInitializer = | HtmlSingleTextExpression - // ================================== // Svelte // ================================== @@ -593,7 +593,13 @@ SvelteDirectiveModifier = '|' name: SvelteName - +// +// ^^^^^^^^^^ +HtmlSpreadAttribute = + '{' + '...' + argument: HtmlTextExpression + '}' // Keep it different just for svelte diff --git a/xtask/codegen/src/html_kinds_src.rs b/xtask/codegen/src/html_kinds_src.rs index 0aa942ea6d14..ffba3bdfdfb9 100644 --- a/xtask/codegen/src/html_kinds_src.rs +++ b/xtask/codegen/src/html_kinds_src.rs @@ -89,6 +89,7 @@ pub const HTML_KINDS_SRC: KindsSrc = KindsSrc { "HTML_DOUBLE_TEXT_EXPRESSION", "HTML_SINGLE_TEXT_EXPRESSION", "HTML_TEXT_EXPRESSION", + "HTML_SPREAD_ATTRIBUTE", // Astro nodes "ASTRO_FRONTMATTER_ELEMENT", "ASTRO_EMBEDDED_CONTENT",