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",