Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ninety-rice-like.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@biomejs/biome": patch
---

Added proper parsing for spread attributes `{...props}` in Svelte and Astro files.
2 changes: 2 additions & 0 deletions crates/biome_cli/tests/cases/handle_astro_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -770,13 +770,15 @@ fn embedded_bindings_are_tracked_correctly() {
import { Component } from "./component.svelte";
let hello = "Hello World";
let array = [];
let props = [];
---

<html>
<span>{hello}</span>
<span>{notDefined}</span>
{ array.map(item => (<span>{item}</span>)) }
<Component />
<input {...props}>
</html>
"#
.as_bytes(),
Expand Down
2 changes: 2 additions & 0 deletions crates/biome_cli/tests/cases/handle_svelte_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ fn embedded_bindings_are_tracked_correctly() {
import { Component } from "./component.svelte";
let hello = "Hello World";
let array = [];
let props = [];
</script>

<html>
Expand All @@ -569,6 +570,7 @@ let array = [];
{#each array as item}
{/each}
<Component />
<input {...props} />
</html>
"#
.as_bytes(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ expression: redactor(content)
import { Component } from "./component.svelte";
let hello = "Hello World";
let array = [];
let props = [];
---

<html>
<span>{hello}</span>
<span>{notDefined}</span>
{ array.map(item => (<span>{item}</span>)) }
<Component />
<input {...props}>
</html>

```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ expression: redactor(content)
import { Component } from "./component.svelte";
let hello = "Hello World";
let array = [];
let props = [];
</script>

<html>
Expand All @@ -28,6 +29,7 @@ let array = [];
{#each array as item}
{/each}
<Component />
<input {...props} />
</html>

```
Expand Down
16 changes: 16 additions & 0 deletions crates/biome_html_factory/src/generated/node_factory.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

40 changes: 40 additions & 0 deletions crates/biome_html_factory/src/generated/syntax_factory.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions crates/biome_html_formatter/src/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,44 @@ impl IntoFormat<HtmlFormatContext> for biome_html_syntax::HtmlSingleTextExpressi
)
}
}
impl FormatRule<biome_html_syntax::HtmlSpreadAttribute>
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::<biome_html_syntax::HtmlSpreadAttribute>::fmt(self, node, f)
}
}
impl AsFormat<HtmlFormatContext> 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<HtmlFormatContext> 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<biome_html_syntax::HtmlString>
for crate::html::auxiliary::string::FormatHtmlString
{
Expand Down
1 change: 1 addition & 0 deletions crates/biome_html_formatter/src/html/any/attribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ impl FormatRule<AnyHtmlAttribute> 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),
}
}
Expand Down
1 change: 1 addition & 0 deletions crates/biome_html_formatter/src/html/auxiliary/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
25 changes: 25 additions & 0 deletions crates/biome_html_formatter/src/html/auxiliary/spread_attribute.rs
Original file line number Diff line number Diff line change
@@ -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<HtmlSpreadAttribute> 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()
]
)
}
}
3 changes: 3 additions & 0 deletions crates/biome_html_formatter/src/html/lists/attribute_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ impl FormatRule<HtmlAttributeList> for FormatHtmlAttributeList {
AnyHtmlAttribute::AnySvelteDirective(attr) => {
attr.format().fmt(f)
}
AnyHtmlAttribute::HtmlSpreadAttribute(attr) => {
attr.format().fmt(f)
}
})
}))
.finish()?;
Expand Down
42 changes: 39 additions & 3 deletions crates/biome_html_parser/src/syntax/astro.rs
Original file line number Diff line number Diff line change
@@ -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![---]) {
Expand Down Expand Up @@ -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)
}
}
17 changes: 9 additions & 8 deletions crates/biome_html_parser/src/syntax/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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| {
Expand All @@ -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))
}
}
}
Expand Down Expand Up @@ -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;
}

Expand Down
Loading