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