diff --git a/.changeset/spicy-actors-leave.md b/.changeset/spicy-actors-leave.md
new file mode 100644
index 000000000000..254c0520c9b4
--- /dev/null
+++ b/.changeset/spicy-actors-leave.md
@@ -0,0 +1,22 @@
+---
+"@biomejs/biome": patch
+---
+
+Added support for the Svelte syntax `{#if}{/if}`. The Biome HTML parser is now able to parse and format the [`{#if}{/if} blocks`](https://svelte.dev/docs/svelte/if):
+
+```diff
+
+{#if porridge.temperature > 100}
+-
too hot!
++ too hot!
+{:else if 80 > porridge.temperature}
+-too cold!
++ too cold!
+{:else if 100 > porridge.temperature}
+-too too cold!
++ too too cold!
+{:else}
+-just right!
++ just right!
+{/if}
+```
diff --git a/crates/biome_html_factory/src/generated/node_factory.rs b/crates/biome_html_factory/src/generated/node_factory.rs
index 380ca76961c0..964e9f19e4d5 100644
--- a/crates/biome_html_factory/src/generated/node_factory.rs
+++ b/crates/biome_html_factory/src/generated/node_factory.rs
@@ -395,6 +395,42 @@ pub fn svelte_debug_block(
],
))
}
+pub fn svelte_else_clause(
+ sv_curly_colon_token: SyntaxToken,
+ else_token: SyntaxToken,
+ r_curly_token: SyntaxToken,
+ children: HtmlElementList,
+) -> SvelteElseClause {
+ SvelteElseClause::unwrap_cast(SyntaxNode::new_detached(
+ HtmlSyntaxKind::SVELTE_ELSE_CLAUSE,
+ [
+ Some(SyntaxElement::Token(sv_curly_colon_token)),
+ Some(SyntaxElement::Token(else_token)),
+ Some(SyntaxElement::Token(r_curly_token)),
+ Some(SyntaxElement::Node(children.into_syntax())),
+ ],
+ ))
+}
+pub fn svelte_else_if_clause(
+ sv_curly_colon_token: SyntaxToken,
+ else_token: SyntaxToken,
+ if_token: SyntaxToken,
+ expression: HtmlTextExpression,
+ r_curly_token: SyntaxToken,
+ children: HtmlElementList,
+) -> SvelteElseIfClause {
+ SvelteElseIfClause::unwrap_cast(SyntaxNode::new_detached(
+ HtmlSyntaxKind::SVELTE_ELSE_IF_CLAUSE,
+ [
+ Some(SyntaxElement::Token(sv_curly_colon_token)),
+ Some(SyntaxElement::Token(else_token)),
+ Some(SyntaxElement::Token(if_token)),
+ Some(SyntaxElement::Node(expression.into_syntax())),
+ Some(SyntaxElement::Token(r_curly_token)),
+ Some(SyntaxElement::Node(children.into_syntax())),
+ ],
+ ))
+}
pub fn svelte_html_block(
sv_curly_at_token: SyntaxToken,
html_token: SyntaxToken,
@@ -411,6 +447,74 @@ pub fn svelte_html_block(
],
))
}
+pub fn svelte_if_block(
+ opening_block: SvelteIfOpeningBlock,
+ else_if_clauses: SvelteElseIfClauseList,
+ closing_block: SvelteIfClosingBlock,
+) -> SvelteIfBlockBuilder {
+ SvelteIfBlockBuilder {
+ opening_block,
+ else_if_clauses,
+ closing_block,
+ else_clause: None,
+ }
+}
+pub struct SvelteIfBlockBuilder {
+ opening_block: SvelteIfOpeningBlock,
+ else_if_clauses: SvelteElseIfClauseList,
+ closing_block: SvelteIfClosingBlock,
+ else_clause: Option,
+}
+impl SvelteIfBlockBuilder {
+ pub fn with_else_clause(mut self, else_clause: SvelteElseClause) -> Self {
+ self.else_clause = Some(else_clause);
+ self
+ }
+ pub fn build(self) -> SvelteIfBlock {
+ SvelteIfBlock::unwrap_cast(SyntaxNode::new_detached(
+ HtmlSyntaxKind::SVELTE_IF_BLOCK,
+ [
+ Some(SyntaxElement::Node(self.opening_block.into_syntax())),
+ Some(SyntaxElement::Node(self.else_if_clauses.into_syntax())),
+ self.else_clause
+ .map(|token| SyntaxElement::Node(token.into_syntax())),
+ Some(SyntaxElement::Node(self.closing_block.into_syntax())),
+ ],
+ ))
+ }
+}
+pub fn svelte_if_closing_block(
+ sv_curly_slash_token: SyntaxToken,
+ if_token: SyntaxToken,
+ r_curly_token: SyntaxToken,
+) -> SvelteIfClosingBlock {
+ SvelteIfClosingBlock::unwrap_cast(SyntaxNode::new_detached(
+ HtmlSyntaxKind::SVELTE_IF_CLOSING_BLOCK,
+ [
+ Some(SyntaxElement::Token(sv_curly_slash_token)),
+ Some(SyntaxElement::Token(if_token)),
+ Some(SyntaxElement::Token(r_curly_token)),
+ ],
+ ))
+}
+pub fn svelte_if_opening_block(
+ sv_curly_hash_token: SyntaxToken,
+ if_token: SyntaxToken,
+ expression: HtmlTextExpression,
+ r_curly_token: SyntaxToken,
+ children: HtmlElementList,
+) -> SvelteIfOpeningBlock {
+ SvelteIfOpeningBlock::unwrap_cast(SyntaxNode::new_detached(
+ HtmlSyntaxKind::SVELTE_IF_OPENING_BLOCK,
+ [
+ Some(SyntaxElement::Token(sv_curly_hash_token)),
+ Some(SyntaxElement::Token(if_token)),
+ Some(SyntaxElement::Node(expression.into_syntax())),
+ Some(SyntaxElement::Token(r_curly_token)),
+ Some(SyntaxElement::Node(children.into_syntax())),
+ ],
+ ))
+}
pub fn svelte_key_block(
opening_block: SvelteKeyOpeningBlock,
children: HtmlElementList,
@@ -522,6 +626,18 @@ where
}),
))
}
+pub fn svelte_else_if_clause_list(items: I) -> SvelteElseIfClauseList
+where
+ I: IntoIterator- ,
+ I::IntoIter: ExactSizeIterator,
+{
+ SvelteElseIfClauseList::unwrap_cast(SyntaxNode::new_detached(
+ HtmlSyntaxKind::SVELTE_ELSE_IF_CLAUSE_LIST,
+ items
+ .into_iter()
+ .map(|item| Some(item.into_syntax().into())),
+ ))
+}
pub fn astro_bogus_frontmatter(slots: I) -> AstroBogusFrontmatter
where
I: IntoIterator
- >,
diff --git a/crates/biome_html_factory/src/generated/syntax_factory.rs b/crates/biome_html_factory/src/generated/syntax_factory.rs
index 93632b9d45ef..a6239ef575d1 100644
--- a/crates/biome_html_factory/src/generated/syntax_factory.rs
+++ b/crates/biome_html_factory/src/generated/syntax_factory.rs
@@ -732,6 +732,100 @@ impl SyntaxFactory for HtmlSyntaxFactory {
}
slots.into_node(SVELTE_DEBUG_BLOCK, children)
}
+ SVELTE_ELSE_CLAUSE => {
+ 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![else]
+ {
+ 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
+ && HtmlElementList::can_cast(element.kind())
+ {
+ slots.mark_present();
+ current_element = elements.next();
+ }
+ slots.next_slot();
+ if current_element.is_some() {
+ return RawSyntaxNode::new(
+ SVELTE_ELSE_CLAUSE.to_bogus(),
+ children.into_iter().map(Some),
+ );
+ }
+ slots.into_node(SVELTE_ELSE_CLAUSE, children)
+ }
+ SVELTE_ELSE_IF_CLAUSE => {
+ let mut elements = (&children).into_iter();
+ let mut slots: RawNodeSlots<6usize> = 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![else]
+ {
+ slots.mark_present();
+ current_element = elements.next();
+ }
+ slots.next_slot();
+ if let Some(element) = ¤t_element
+ && element.kind() == T![if]
+ {
+ 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 let Some(element) = ¤t_element
+ && HtmlElementList::can_cast(element.kind())
+ {
+ slots.mark_present();
+ current_element = elements.next();
+ }
+ slots.next_slot();
+ if current_element.is_some() {
+ return RawSyntaxNode::new(
+ SVELTE_ELSE_IF_CLAUSE.to_bogus(),
+ children.into_iter().map(Some),
+ );
+ }
+ slots.into_node(SVELTE_ELSE_IF_CLAUSE, children)
+ }
SVELTE_HTML_BLOCK => {
let mut elements = (&children).into_iter();
let mut slots: RawNodeSlots<4usize> = RawNodeSlots::default();
@@ -772,6 +866,126 @@ impl SyntaxFactory for HtmlSyntaxFactory {
}
slots.into_node(SVELTE_HTML_BLOCK, children)
}
+ SVELTE_IF_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
+ && SvelteIfOpeningBlock::can_cast(element.kind())
+ {
+ slots.mark_present();
+ current_element = elements.next();
+ }
+ slots.next_slot();
+ if let Some(element) = ¤t_element
+ && SvelteElseIfClauseList::can_cast(element.kind())
+ {
+ slots.mark_present();
+ current_element = elements.next();
+ }
+ slots.next_slot();
+ if let Some(element) = ¤t_element
+ && SvelteElseClause::can_cast(element.kind())
+ {
+ slots.mark_present();
+ current_element = elements.next();
+ }
+ slots.next_slot();
+ if let Some(element) = ¤t_element
+ && SvelteIfClosingBlock::can_cast(element.kind())
+ {
+ slots.mark_present();
+ current_element = elements.next();
+ }
+ slots.next_slot();
+ if current_element.is_some() {
+ return RawSyntaxNode::new(
+ SVELTE_IF_BLOCK.to_bogus(),
+ children.into_iter().map(Some),
+ );
+ }
+ slots.into_node(SVELTE_IF_BLOCK, children)
+ }
+ SVELTE_IF_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![if]
+ {
+ 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_IF_CLOSING_BLOCK.to_bogus(),
+ children.into_iter().map(Some),
+ );
+ }
+ slots.into_node(SVELTE_IF_CLOSING_BLOCK, children)
+ }
+ SVELTE_IF_OPENING_BLOCK => {
+ let mut elements = (&children).into_iter();
+ let mut slots: RawNodeSlots<5usize> = 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![if]
+ {
+ 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 let Some(element) = ¤t_element
+ && HtmlElementList::can_cast(element.kind())
+ {
+ slots.mark_present();
+ current_element = elements.next();
+ }
+ slots.next_slot();
+ if current_element.is_some() {
+ return RawSyntaxNode::new(
+ SVELTE_IF_OPENING_BLOCK.to_bogus(),
+ children.into_iter().map(Some),
+ );
+ }
+ slots.into_node(SVELTE_IF_OPENING_BLOCK, children)
+ }
SVELTE_KEY_BLOCK => {
let mut elements = (&children).into_iter();
let mut slots: RawNodeSlots<3usize> = RawNodeSlots::default();
@@ -950,6 +1164,9 @@ impl SyntaxFactory for HtmlSyntaxFactory {
T ! [,],
false,
),
+ SVELTE_ELSE_IF_CLAUSE_LIST => {
+ Self::make_node_list_syntax(kind, children, SvelteElseIfClause::can_cast)
+ }
_ => unreachable!("Is {:?} a token?", kind),
}
}
diff --git a/crates/biome_html_formatter/src/generated.rs b/crates/biome_html_formatter/src/generated.rs
index e41b9b02bd68..6bfe30833010 100644
--- a/crates/biome_html_formatter/src/generated.rs
+++ b/crates/biome_html_formatter/src/generated.rs
@@ -830,6 +830,82 @@ impl IntoFormat for biome_html_syntax::SvelteDebugBlock {
)
}
}
+impl FormatRule
+ for crate::svelte::auxiliary::else_clause::FormatSvelteElseClause
+{
+ type Context = HtmlFormatContext;
+ #[inline(always)]
+ fn fmt(
+ &self,
+ node: &biome_html_syntax::SvelteElseClause,
+ f: &mut HtmlFormatter,
+ ) -> FormatResult<()> {
+ FormatNodeRule::::fmt(self, node, f)
+ }
+}
+impl AsFormat for biome_html_syntax::SvelteElseClause {
+ type Format<'a> = FormatRefWithRule<
+ 'a,
+ biome_html_syntax::SvelteElseClause,
+ crate::svelte::auxiliary::else_clause::FormatSvelteElseClause,
+ >;
+ fn format(&self) -> Self::Format<'_> {
+ FormatRefWithRule::new(
+ self,
+ crate::svelte::auxiliary::else_clause::FormatSvelteElseClause::default(),
+ )
+ }
+}
+impl IntoFormat for biome_html_syntax::SvelteElseClause {
+ type Format = FormatOwnedWithRule<
+ biome_html_syntax::SvelteElseClause,
+ crate::svelte::auxiliary::else_clause::FormatSvelteElseClause,
+ >;
+ fn into_format(self) -> Self::Format {
+ FormatOwnedWithRule::new(
+ self,
+ crate::svelte::auxiliary::else_clause::FormatSvelteElseClause::default(),
+ )
+ }
+}
+impl FormatRule
+ for crate::svelte::auxiliary::else_if_clause::FormatSvelteElseIfClause
+{
+ type Context = HtmlFormatContext;
+ #[inline(always)]
+ fn fmt(
+ &self,
+ node: &biome_html_syntax::SvelteElseIfClause,
+ f: &mut HtmlFormatter,
+ ) -> FormatResult<()> {
+ FormatNodeRule::::fmt(self, node, f)
+ }
+}
+impl AsFormat for biome_html_syntax::SvelteElseIfClause {
+ type Format<'a> = FormatRefWithRule<
+ 'a,
+ biome_html_syntax::SvelteElseIfClause,
+ crate::svelte::auxiliary::else_if_clause::FormatSvelteElseIfClause,
+ >;
+ fn format(&self) -> Self::Format<'_> {
+ FormatRefWithRule::new(
+ self,
+ crate::svelte::auxiliary::else_if_clause::FormatSvelteElseIfClause::default(),
+ )
+ }
+}
+impl IntoFormat for biome_html_syntax::SvelteElseIfClause {
+ type Format = FormatOwnedWithRule<
+ biome_html_syntax::SvelteElseIfClause,
+ crate::svelte::auxiliary::else_if_clause::FormatSvelteElseIfClause,
+ >;
+ fn into_format(self) -> Self::Format {
+ FormatOwnedWithRule::new(
+ self,
+ crate::svelte::auxiliary::else_if_clause::FormatSvelteElseIfClause::default(),
+ )
+ }
+}
impl FormatRule
for crate::svelte::auxiliary::html_block::FormatSvelteHtmlBlock
{
@@ -868,6 +944,120 @@ impl IntoFormat for biome_html_syntax::SvelteHtmlBlock {
)
}
}
+impl FormatRule
+ for crate::svelte::auxiliary::if_block::FormatSvelteIfBlock
+{
+ type Context = HtmlFormatContext;
+ #[inline(always)]
+ fn fmt(
+ &self,
+ node: &biome_html_syntax::SvelteIfBlock,
+ f: &mut HtmlFormatter,
+ ) -> FormatResult<()> {
+ FormatNodeRule::::fmt(self, node, f)
+ }
+}
+impl AsFormat for biome_html_syntax::SvelteIfBlock {
+ type Format<'a> = FormatRefWithRule<
+ 'a,
+ biome_html_syntax::SvelteIfBlock,
+ crate::svelte::auxiliary::if_block::FormatSvelteIfBlock,
+ >;
+ fn format(&self) -> Self::Format<'_> {
+ FormatRefWithRule::new(
+ self,
+ crate::svelte::auxiliary::if_block::FormatSvelteIfBlock::default(),
+ )
+ }
+}
+impl IntoFormat for biome_html_syntax::SvelteIfBlock {
+ type Format = FormatOwnedWithRule<
+ biome_html_syntax::SvelteIfBlock,
+ crate::svelte::auxiliary::if_block::FormatSvelteIfBlock,
+ >;
+ fn into_format(self) -> Self::Format {
+ FormatOwnedWithRule::new(
+ self,
+ crate::svelte::auxiliary::if_block::FormatSvelteIfBlock::default(),
+ )
+ }
+}
+impl FormatRule
+ for crate::svelte::auxiliary::if_closing_block::FormatSvelteIfClosingBlock
+{
+ type Context = HtmlFormatContext;
+ #[inline(always)]
+ fn fmt(
+ &self,
+ node: &biome_html_syntax::SvelteIfClosingBlock,
+ f: &mut HtmlFormatter,
+ ) -> FormatResult<()> {
+ FormatNodeRule::::fmt(self, node, f)
+ }
+}
+impl AsFormat for biome_html_syntax::SvelteIfClosingBlock {
+ type Format<'a> = FormatRefWithRule<
+ 'a,
+ biome_html_syntax::SvelteIfClosingBlock,
+ crate::svelte::auxiliary::if_closing_block::FormatSvelteIfClosingBlock,
+ >;
+ fn format(&self) -> Self::Format<'_> {
+ FormatRefWithRule::new(
+ self,
+ crate::svelte::auxiliary::if_closing_block::FormatSvelteIfClosingBlock::default(),
+ )
+ }
+}
+impl IntoFormat for biome_html_syntax::SvelteIfClosingBlock {
+ type Format = FormatOwnedWithRule<
+ biome_html_syntax::SvelteIfClosingBlock,
+ crate::svelte::auxiliary::if_closing_block::FormatSvelteIfClosingBlock,
+ >;
+ fn into_format(self) -> Self::Format {
+ FormatOwnedWithRule::new(
+ self,
+ crate::svelte::auxiliary::if_closing_block::FormatSvelteIfClosingBlock::default(),
+ )
+ }
+}
+impl FormatRule
+ for crate::svelte::auxiliary::if_opening_block::FormatSvelteIfOpeningBlock
+{
+ type Context = HtmlFormatContext;
+ #[inline(always)]
+ fn fmt(
+ &self,
+ node: &biome_html_syntax::SvelteIfOpeningBlock,
+ f: &mut HtmlFormatter,
+ ) -> FormatResult<()> {
+ FormatNodeRule::::fmt(self, node, f)
+ }
+}
+impl AsFormat for biome_html_syntax::SvelteIfOpeningBlock {
+ type Format<'a> = FormatRefWithRule<
+ 'a,
+ biome_html_syntax::SvelteIfOpeningBlock,
+ crate::svelte::auxiliary::if_opening_block::FormatSvelteIfOpeningBlock,
+ >;
+ fn format(&self) -> Self::Format<'_> {
+ FormatRefWithRule::new(
+ self,
+ crate::svelte::auxiliary::if_opening_block::FormatSvelteIfOpeningBlock::default(),
+ )
+ }
+}
+impl IntoFormat for biome_html_syntax::SvelteIfOpeningBlock {
+ type Format = FormatOwnedWithRule<
+ biome_html_syntax::SvelteIfOpeningBlock,
+ crate::svelte::auxiliary::if_opening_block::FormatSvelteIfOpeningBlock,
+ >;
+ fn into_format(self) -> Self::Format {
+ FormatOwnedWithRule::new(
+ self,
+ crate::svelte::auxiliary::if_opening_block::FormatSvelteIfOpeningBlock::default(),
+ )
+ }
+}
impl FormatRule
for crate::svelte::auxiliary::key_block::FormatSvelteKeyBlock
{
@@ -1129,6 +1319,31 @@ impl IntoFormat for biome_html_syntax::SvelteBindingList {
)
}
}
+impl AsFormat for biome_html_syntax::SvelteElseIfClauseList {
+ type Format<'a> = FormatRefWithRule<
+ 'a,
+ biome_html_syntax::SvelteElseIfClauseList,
+ crate::svelte::lists::else_if_clause_list::FormatSvelteElseIfClauseList,
+ >;
+ fn format(&self) -> Self::Format<'_> {
+ FormatRefWithRule::new(
+ self,
+ crate::svelte::lists::else_if_clause_list::FormatSvelteElseIfClauseList::default(),
+ )
+ }
+}
+impl IntoFormat for biome_html_syntax::SvelteElseIfClauseList {
+ type Format = FormatOwnedWithRule<
+ biome_html_syntax::SvelteElseIfClauseList,
+ crate::svelte::lists::else_if_clause_list::FormatSvelteElseIfClauseList,
+ >;
+ fn into_format(self) -> Self::Format {
+ FormatOwnedWithRule::new(
+ self,
+ crate::svelte::lists::else_if_clause_list::FormatSvelteElseIfClauseList::default(),
+ )
+ }
+}
impl FormatRule
for crate::astro::bogus::bogus_frontmatter::FormatAstroBogusFrontmatter
{
diff --git a/crates/biome_html_formatter/src/html/auxiliary/element.rs b/crates/biome_html_formatter/src/html/auxiliary/element.rs
index 16f89af1e8c5..3d3adf81982d 100644
--- a/crates/biome_html_formatter/src/html/auxiliary/element.rs
+++ b/crates/biome_html_formatter/src/html/auxiliary/element.rs
@@ -143,6 +143,7 @@ impl FormatNodeRule for FormatHtmlElement {
FormatChildrenResult::BestFitting {
flat_children,
expanded_children,
+ group_id: _,
} => {
let expanded_children = expanded_children.memoized();
write!(
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 0894d676f137..add8c62f0cf0 100644
--- a/crates/biome_html_formatter/src/html/lists/element_list.rs
+++ b/crates/biome_html_formatter/src/html/lists/element_list.rs
@@ -13,7 +13,7 @@ use crate::{
metadata::is_element_whitespace_sensitive_from_element,
},
};
-use biome_formatter::{CstFormatContext, FormatRuleWithOptions, best_fitting, prelude::*};
+use biome_formatter::{CstFormatContext, FormatRuleWithOptions, GroupId, best_fitting, prelude::*};
use biome_formatter::{VecBuffer, format_args, write};
use biome_html_syntax::{
AnyHtmlContent, AnyHtmlElement, HtmlClosingElement, HtmlClosingElementFields, HtmlElementList,
@@ -27,6 +27,15 @@ pub(crate) struct FormatHtmlElementList {
is_element_whitespace_sensitive: bool,
borrowed_tokens: BorrowedTokens,
+
+ group: Option,
+}
+
+impl FormatHtmlElementList {
+ pub(crate) fn with_group_id(mut self, group: GroupId) -> Self {
+ self.group = Some(group);
+ self
+ }
}
pub(crate) struct FormatHtmlElementListOptions {
@@ -81,6 +90,7 @@ impl FormatRule for FormatHtmlElementList {
FormatChildrenResult::BestFitting {
flat_children,
expanded_children,
+ group_id: _,
} => {
write!(f, [best_fitting![flat_children, expanded_children]])?;
}
@@ -120,9 +130,42 @@ pub(crate) enum FormatChildrenResult {
BestFitting {
flat_children: FormatFlatChildren,
expanded_children: FormatMultilineChildren,
+ group_id: Option,
},
}
+impl Format for FormatChildrenResult {
+ fn fmt(&self, f: &mut Formatter) -> FormatResult<()> {
+ match self {
+ Self::ForceMultiline(multiline) => {
+ write!(f, [multiline])
+ }
+ Self::BestFitting {
+ flat_children,
+ expanded_children,
+ group_id,
+ } => {
+ let group_id = group_id.unwrap_or(f.group_id("element-attr-group-id"));
+
+ 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(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(group_id)),
+ ]
+ )
+ }
+ }
+ }
+}
+
impl FormatHtmlElementList {
pub(crate) fn fmt_children(
&self,
@@ -492,6 +535,7 @@ impl FormatHtmlElementList {
Ok(FormatChildrenResult::BestFitting {
flat_children: flat.finish()?,
expanded_children: multiline.finish()?,
+ group_id: self.group,
})
}
}
diff --git a/crates/biome_html_formatter/src/lib.rs b/crates/biome_html_formatter/src/lib.rs
index 97fb5607a561..e8ba234b77af 100644
--- a/crates/biome_html_formatter/src/lib.rs
+++ b/crates/biome_html_formatter/src/lib.rs
@@ -308,7 +308,6 @@ impl IntoFormat for HtmlSyntaxToken {
}
/// Formatting specific [Iterator] extensions
-#[expect(dead_code)]
pub(crate) trait FormattedIterExt {
/// Converts every item to an object that knows how to format it.
fn formatted(self) -> FormattedIter
diff --git a/crates/biome_html_formatter/src/svelte/any/block.rs b/crates/biome_html_formatter/src/svelte/any/block.rs
index 719cdf4bf5e4..469abf858f68 100644
--- a/crates/biome_html_formatter/src/svelte/any/block.rs
+++ b/crates/biome_html_formatter/src/svelte/any/block.rs
@@ -12,6 +12,7 @@ impl FormatRule for FormatAnySvelteBlock {
AnySvelteBlock::SvelteConstBlock(node) => node.format().fmt(f),
AnySvelteBlock::SvelteDebugBlock(node) => node.format().fmt(f),
AnySvelteBlock::SvelteHtmlBlock(node) => node.format().fmt(f),
+ AnySvelteBlock::SvelteIfBlock(node) => node.format().fmt(f),
AnySvelteBlock::SvelteKeyBlock(node) => node.format().fmt(f),
AnySvelteBlock::SvelteRenderBlock(node) => node.format().fmt(f),
}
diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/else_clause.rs b/crates/biome_html_formatter/src/svelte/auxiliary/else_clause.rs
new file mode 100644
index 000000000000..661a2778511c
--- /dev/null
+++ b/crates/biome_html_formatter/src/svelte/auxiliary/else_clause.rs
@@ -0,0 +1,33 @@
+use crate::html::lists::element_list::FormatHtmlElementList;
+use crate::prelude::*;
+use biome_formatter::write;
+use biome_html_syntax::{SvelteElseClause, SvelteElseClauseFields};
+
+#[derive(Debug, Clone, Default)]
+pub(crate) struct FormatSvelteElseClause;
+impl FormatNodeRule for FormatSvelteElseClause {
+ fn fmt_fields(&self, node: &SvelteElseClause, f: &mut HtmlFormatter) -> FormatResult<()> {
+ let SvelteElseClauseFields {
+ r_curly_token,
+ children,
+ else_token,
+ sv_curly_colon_token,
+ } = node.as_fields();
+
+ write!(
+ f,
+ [
+ sv_curly_colon_token.format(),
+ else_token.format(),
+ r_curly_token.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()
+ .with_group_id(f.group_id("svelte-else-group"))
+ .fmt_children(&children, f)?;
+
+ write!(f, [format_children, hard_line_break(),])
+ }
+}
diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/else_if_clause.rs b/crates/biome_html_formatter/src/svelte/auxiliary/else_if_clause.rs
new file mode 100644
index 000000000000..c435c6a38fac
--- /dev/null
+++ b/crates/biome_html_formatter/src/svelte/auxiliary/else_if_clause.rs
@@ -0,0 +1,39 @@
+use crate::html::lists::element_list::FormatHtmlElementList;
+use crate::prelude::*;
+use biome_formatter::write;
+use biome_html_syntax::{SvelteElseIfClause, SvelteElseIfClauseFields};
+
+#[derive(Debug, Clone, Default)]
+pub(crate) struct FormatSvelteElseIfClause;
+impl FormatNodeRule for FormatSvelteElseIfClause {
+ fn fmt_fields(&self, node: &SvelteElseIfClause, f: &mut HtmlFormatter) -> FormatResult<()> {
+ let SvelteElseIfClauseFields {
+ r_curly_token,
+ expression,
+ children,
+ if_token,
+ sv_curly_colon_token,
+ else_token,
+ } = node.as_fields();
+
+ write!(
+ f,
+ [
+ sv_curly_colon_token.format(),
+ else_token.format(),
+ space(),
+ if_token.format(),
+ expression.format(),
+ r_curly_token.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()
+ .with_group_id(f.group_id("svelte-else-if-group"))
+ .fmt_children(&children, f)?;
+
+ write!(f, [format_children, hard_line_break(),])
+ }
+}
diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/if_block.rs b/crates/biome_html_formatter/src/svelte/auxiliary/if_block.rs
new file mode 100644
index 000000000000..5156eca19dc0
--- /dev/null
+++ b/crates/biome_html_formatter/src/svelte/auxiliary/if_block.rs
@@ -0,0 +1,25 @@
+use crate::prelude::*;
+use biome_formatter::write;
+use biome_html_syntax::{SvelteIfBlock, SvelteIfBlockFields};
+#[derive(Debug, Clone, Default)]
+pub(crate) struct FormatSvelteIfBlock;
+impl FormatNodeRule for FormatSvelteIfBlock {
+ fn fmt_fields(&self, node: &SvelteIfBlock, f: &mut HtmlFormatter) -> FormatResult<()> {
+ let SvelteIfBlockFields {
+ opening_block,
+ closing_block,
+ else_clause,
+ else_if_clauses,
+ } = node.as_fields();
+
+ write!(
+ f,
+ [
+ opening_block.format(),
+ else_if_clauses.format(),
+ else_clause.format(),
+ closing_block.format()
+ ]
+ )
+ }
+}
diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/if_closing_block.rs b/crates/biome_html_formatter/src/svelte/auxiliary/if_closing_block.rs
new file mode 100644
index 000000000000..e6bfdd37f575
--- /dev/null
+++ b/crates/biome_html_formatter/src/svelte/auxiliary/if_closing_block.rs
@@ -0,0 +1,24 @@
+use crate::prelude::*;
+use biome_formatter::write;
+use biome_html_syntax::{SvelteIfClosingBlock, SvelteIfClosingBlockFields};
+
+#[derive(Debug, Clone, Default)]
+pub(crate) struct FormatSvelteIfClosingBlock;
+impl FormatNodeRule for FormatSvelteIfClosingBlock {
+ fn fmt_fields(&self, node: &SvelteIfClosingBlock, f: &mut HtmlFormatter) -> FormatResult<()> {
+ let SvelteIfClosingBlockFields {
+ if_token,
+ r_curly_token,
+ sv_curly_slash_token,
+ } = node.as_fields();
+
+ write!(
+ f,
+ [
+ sv_curly_slash_token.format(),
+ if_token.format(),
+ r_curly_token.format(),
+ ]
+ )
+ }
+}
diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/if_opening_block.rs b/crates/biome_html_formatter/src/svelte/auxiliary/if_opening_block.rs
new file mode 100644
index 000000000000..1fc9593f223e
--- /dev/null
+++ b/crates/biome_html_formatter/src/svelte/auxiliary/if_opening_block.rs
@@ -0,0 +1,36 @@
+use crate::html::lists::element_list::FormatHtmlElementList;
+use crate::prelude::*;
+use biome_formatter::write;
+use biome_html_syntax::{SvelteIfOpeningBlock, SvelteIfOpeningBlockFields};
+
+#[derive(Debug, Clone, Default)]
+pub(crate) struct FormatSvelteIfOpeningBlock;
+impl FormatNodeRule for FormatSvelteIfOpeningBlock {
+ fn fmt_fields(&self, node: &SvelteIfOpeningBlock, f: &mut HtmlFormatter) -> FormatResult<()> {
+ let SvelteIfOpeningBlockFields {
+ children,
+ r_curly_token,
+ if_token,
+ expression,
+ sv_curly_hash_token,
+ } = node.as_fields();
+
+ write!(
+ f,
+ [
+ sv_curly_hash_token.format(),
+ if_token.format(),
+ expression.format(),
+ r_curly_token.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()
+ .with_group_id(f.group_id("svelte-if-group"))
+ .fmt_children(&children, f)?;
+
+ write!(f, [format_children, hard_line_break()])
+ }
+}
diff --git a/crates/biome_html_formatter/src/svelte/auxiliary/key_block.rs b/crates/biome_html_formatter/src/svelte/auxiliary/key_block.rs
index a063230bef47..24afdc8aa15f 100644
--- a/crates/biome_html_formatter/src/svelte/auxiliary/key_block.rs
+++ b/crates/biome_html_formatter/src/svelte/auxiliary/key_block.rs
@@ -1,6 +1,6 @@
-use crate::html::lists::element_list::{FormatChildrenResult, FormatHtmlElementList};
+use crate::html::lists::element_list::FormatHtmlElementList;
use crate::prelude::*;
-use biome_formatter::{format_args, write};
+use biome_formatter::write;
use biome_html_syntax::{SvelteKeyBlock, SvelteKeyBlockFields};
#[derive(Debug, Clone, Default)]
@@ -16,34 +16,10 @@ impl FormatNodeRule for FormatSvelteKeyBlock {
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");
+ let format_children = FormatHtmlElementList::default()
+ .with_group_id(f.group_id("svelte-key-group"))
+ .fmt_children(&children, f)?;
- 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()])
+ write!(f, [format_children, closing_block.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
index 217fb89d8f7b..6ab4174912a4 100644
--- a/crates/biome_html_formatter/src/svelte/auxiliary/key_opening_block.rs
+++ b/crates/biome_html_formatter/src/svelte/auxiliary/key_opening_block.rs
@@ -18,7 +18,6 @@ impl FormatNodeRule for FormatSvelteKeyOpeningBlock {
[
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 4578b146e1c0..2d93e9dd71d2 100644
--- a/crates/biome_html_formatter/src/svelte/auxiliary/mod.rs
+++ b/crates/biome_html_formatter/src/svelte/auxiliary/mod.rs
@@ -3,7 +3,12 @@
pub(crate) mod attach_attribute;
pub(crate) mod const_block;
pub(crate) mod debug_block;
+pub(crate) mod else_clause;
+pub(crate) mod else_if_clause;
pub(crate) mod html_block;
+pub(crate) mod if_block;
+pub(crate) mod if_closing_block;
+pub(crate) mod if_opening_block;
pub(crate) mod key_block;
pub(crate) mod key_closing_block;
pub(crate) mod key_opening_block;
diff --git a/crates/biome_html_formatter/src/svelte/lists/else_if_clause_list.rs b/crates/biome_html_formatter/src/svelte/lists/else_if_clause_list.rs
new file mode 100644
index 000000000000..85bba4cfecfb
--- /dev/null
+++ b/crates/biome_html_formatter/src/svelte/lists/else_if_clause_list.rs
@@ -0,0 +1,10 @@
+use crate::prelude::*;
+use biome_html_syntax::SvelteElseIfClauseList;
+#[derive(Debug, Clone, Default)]
+pub(crate) struct FormatSvelteElseIfClauseList;
+impl FormatRule for FormatSvelteElseIfClauseList {
+ type Context = HtmlFormatContext;
+ fn fmt(&self, node: &SvelteElseIfClauseList, f: &mut HtmlFormatter) -> FormatResult<()> {
+ f.join().entries(node.iter().formatted()).finish()
+ }
+}
diff --git a/crates/biome_html_formatter/src/svelte/lists/mod.rs b/crates/biome_html_formatter/src/svelte/lists/mod.rs
index b9e5882be0d5..706761390719 100644
--- a/crates/biome_html_formatter/src/svelte/lists/mod.rs
+++ b/crates/biome_html_formatter/src/svelte/lists/mod.rs
@@ -1,3 +1,4 @@
//! This is a generated file. Don't modify it by hand! Run 'cargo codegen formatter' to re-generate the file.
pub(crate) mod binding_list;
+pub(crate) mod else_if_clause_list;
diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/if.svelte b/crates/biome_html_formatter/tests/specs/html/svelte/if.svelte
new file mode 100644
index 000000000000..fa087adbcac0
--- /dev/null
+++ b/crates/biome_html_formatter/tests/specs/html/svelte/if.svelte
@@ -0,0 +1,4 @@
+{#if answer === 42}
+
what was the question?
+{/if}
+
diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/if.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/if.svelte.snap
new file mode 100644
index 000000000000..06c626477d80
--- /dev/null
+++ b/crates/biome_html_formatter/tests/specs/html/svelte/if.svelte.snap
@@ -0,0 +1,38 @@
+---
+source: crates/biome_formatter_test/src/snapshot_builder.rs
+info: svelte/if.svelte
+---
+# Input
+
+```svelte
+{#if answer === 42}
+ what was the question?
+{/if}
+
+
+```
+
+
+=============================
+
+# 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
+{#if answer === 42}
+ what was the question?
+{/if}
+```
diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/if_else.svelte b/crates/biome_html_formatter/tests/specs/html/svelte/if_else.svelte
new file mode 100644
index 000000000000..5a2d87432148
--- /dev/null
+++ b/crates/biome_html_formatter/tests/specs/html/svelte/if_else.svelte
@@ -0,0 +1,6 @@
+
+{#if porridge.temperature > 100}
+ too hot!
+{:else}
+ too cold!
+{/if}
diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/if_else.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/if_else.svelte.snap
new file mode 100644
index 000000000000..1a4431589c96
--- /dev/null
+++ b/crates/biome_html_formatter/tests/specs/html/svelte/if_else.svelte.snap
@@ -0,0 +1,43 @@
+---
+source: crates/biome_formatter_test/src/snapshot_builder.rs
+info: svelte/if_else.svelte
+---
+# Input
+
+```svelte
+
+{#if porridge.temperature > 100}
+ too hot!
+{:else}
+ too cold!
+{/if}
+
+```
+
+
+=============================
+
+# 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
+
+{#if porridge.temperature > 100}
+ too hot!
+{:else}
+ too cold!
+{/if}
+```
diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/if_else_if_else.svelte b/crates/biome_html_formatter/tests/specs/html/svelte/if_else_if_else.svelte
new file mode 100644
index 000000000000..4195249c8217
--- /dev/null
+++ b/crates/biome_html_formatter/tests/specs/html/svelte/if_else_if_else.svelte
@@ -0,0 +1,10 @@
+
+{#if porridge.temperature > 100}
+ too hot!
+{:else if 80 > porridge.temperature}
+ too cold!
+{:else if 100 > porridge.temperature}
+ too too cold!
+{:else}
+ just right!
+{/if}
diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/if_else_if_else.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/if_else_if_else.svelte.snap
new file mode 100644
index 000000000000..e511f0bbee49
--- /dev/null
+++ b/crates/biome_html_formatter/tests/specs/html/svelte/if_else_if_else.svelte.snap
@@ -0,0 +1,51 @@
+---
+source: crates/biome_formatter_test/src/snapshot_builder.rs
+info: svelte/if_else_if_else.svelte
+---
+# Input
+
+```svelte
+
+{#if porridge.temperature > 100}
+ too hot!
+{:else if 80 > porridge.temperature}
+ too cold!
+{:else if 100 > porridge.temperature}
+ too too cold!
+{:else}
+ just right!
+{/if}
+
+```
+
+
+=============================
+
+# 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
+
+{#if porridge.temperature > 100}
+ too hot!
+{:else if 80 > porridge.temperature}
+ too cold!
+{:else if 100 > porridge.temperature}
+ too too cold!
+{:else}
+ just right!
+{/if}
+```
diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/if_nested.svelte b/crates/biome_html_formatter/tests/specs/html/svelte/if_nested.svelte
new file mode 100644
index 000000000000..b56c5b4f04a0
--- /dev/null
+++ b/crates/biome_html_formatter/tests/specs/html/svelte/if_nested.svelte
@@ -0,0 +1,9 @@
+{#if answer === 42}
+ what was the question?
+ {#if answer === 42}
+ what was the question?
+ {#if answer === 42}
+ what was the question?
+ {/if}
+ {/if}
+{/if}
diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/if_nested.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/if_nested.svelte.snap
new file mode 100644
index 000000000000..63865657cb0f
--- /dev/null
+++ b/crates/biome_html_formatter/tests/specs/html/svelte/if_nested.svelte.snap
@@ -0,0 +1,49 @@
+---
+source: crates/biome_formatter_test/src/snapshot_builder.rs
+info: svelte/if_nested.svelte
+---
+# Input
+
+```svelte
+{#if answer === 42}
+ what was the question?
+ {#if answer === 42}
+ what was the question?
+ {#if answer === 42}
+ what was the question?
+ {/if}
+ {/if}
+{/if}
+
+```
+
+
+=============================
+
+# 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
+{#if answer === 42}
+ what was the question?
+ {#if answer === 42}
+ what was the question?
+ {#if answer === 42}
+ what was the question?
+ {/if}
+ {/if}
+{/if}
+```
diff --git a/crates/biome_html_parser/src/lexer/mod.rs b/crates/biome_html_parser/src/lexer/mod.rs
index 926acfb65ea8..8ce26afbab46 100644
--- a/crates/biome_html_parser/src/lexer/mod.rs
+++ b/crates/biome_html_parser/src/lexer/mod.rs
@@ -1,14 +1,16 @@
mod tests;
-use crate::token_source::{HtmlEmbeddedLanguage, HtmlLexContext, TextExpressionKind};
+use crate::token_source::{
+ HtmlEmbeddedLanguage, HtmlLexContext, HtmlReLexContext, TextExpressionKind,
+};
use biome_html_syntax::HtmlSyntaxKind::{
- ATTACH_KW, COMMENT, CONST_KW, DEBUG_KW, DOCTYPE_KW, EOF, ERROR_TOKEN, HTML_KW, HTML_LITERAL,
- HTML_STRING_LITERAL, KEY_KW, NEWLINE, RENDER_KW, SVELTE_IDENT, TOMBSTONE, UNICODE_BOM,
- WHITESPACE,
+ ATTACH_KW, COMMENT, CONST_KW, DEBUG_KW, DOCTYPE_KW, ELSE_KW, EOF, ERROR_TOKEN, HTML_KW,
+ HTML_LITERAL, HTML_STRING_LITERAL, IF_KW, KEY_KW, NEWLINE, RENDER_KW, SVELTE_IDENT, TOMBSTONE,
+ UNICODE_BOM, WHITESPACE,
};
use biome_html_syntax::{HtmlSyntaxKind, T, TextLen, TextSize};
use biome_parser::diagnostic::ParseDiagnostic;
-use biome_parser::lexer::{Lexer, LexerCheckpoint, LexerWithCheckpoint, TokenFlags};
+use biome_parser::lexer::{Lexer, LexerCheckpoint, LexerWithCheckpoint, ReLexer, TokenFlags};
use biome_rowan::SyntaxKind;
use biome_unicode_table::Dispatch::{BSL, QOT, UNI};
use biome_unicode_table::lookup_byte;
@@ -17,22 +19,14 @@ use std::ops::{Add, AddAssign};
pub(crate) struct HtmlLexer<'src> {
/// Source text
source: &'src str,
-
/// The start byte position in the source text of the next token.
position: usize,
-
current_kind: HtmlSyntaxKind,
-
current_start: TextSize,
-
diagnostics: Vec,
-
current_flags: TokenFlags,
-
preceding_line_break: bool,
-
after_newline: bool,
-
unicode_bom_length: usize,
}
@@ -100,9 +94,6 @@ impl<'src> HtmlLexer<'src> {
_ if (self.current_kind != T![<] && is_attribute_name_byte(current)) => {
self.consume_identifier(current, IdentifierContext::None)
}
- _ if is_at_svelte_start_identifier(current) => {
- self.consume_identifier(current, IdentifierContext::Svelte)
- }
_ => self.consume_unexpected_character(),
}
}
@@ -423,25 +414,17 @@ impl<'src> HtmlLexer<'src> {
match &buffer[..len] {
b"doctype" | b"DOCTYPE" if !context.is_svelte() => DOCTYPE_KW,
b"html" | b"HTML" if context.is_doctype() => HTML_KW,
- buffer if context.is_svelte() => {
- if self.current_kind == T!["{@"] {
- match buffer {
- b"debug" => DEBUG_KW,
- b"attach" => ATTACH_KW,
- b"const" => CONST_KW,
- b"render" => RENDER_KW,
- b"html" => HTML_KW,
- _ => SVELTE_IDENT,
- }
- } else if self.current_kind == T!["{#"] || self.current_kind == T!["{/"] {
- match buffer {
- b"key" => KEY_KW,
- _ => SVELTE_IDENT,
- }
- } else {
- SVELTE_IDENT
- }
- }
+ buffer if context.is_svelte() => match buffer {
+ b"debug" => DEBUG_KW,
+ b"attach" => ATTACH_KW,
+ b"const" => CONST_KW,
+ b"render" => RENDER_KW,
+ b"html" => HTML_KW,
+ b"key" => KEY_KW,
+ b"if" => IF_KW,
+ b"else" => ELSE_KW,
+ _ => SVELTE_IDENT,
+ },
_ => HTML_LITERAL,
}
}
@@ -869,7 +852,7 @@ impl<'src> Lexer<'src> for HtmlLexer<'src> {
const WHITESPACE: Self::Kind = WHITESPACE;
type Kind = HtmlSyntaxKind;
type LexContext = HtmlLexContext;
- type ReLexContext = ();
+ type ReLexContext = HtmlReLexContext;
fn source(&self) -> &'src str {
self.source
@@ -975,6 +958,30 @@ impl<'src> Lexer<'src> for HtmlLexer<'src> {
}
}
+impl<'src> ReLexer<'src> for HtmlLexer<'src> {
+ fn re_lex(&mut self, context: Self::ReLexContext) -> Self::Kind {
+ let old_position = self.position;
+ self.position = u32::from(self.current_start) as usize;
+
+ let re_lexed_kind = match self.current_byte() {
+ Some(current) => match context {
+ HtmlReLexContext::Svelte => self.consume_svelte(current),
+ HtmlReLexContext::SingleTextExpression => self.consume_single_text_expression(),
+ },
+ None => EOF,
+ };
+
+ if self.current() == re_lexed_kind {
+ // Didn't re-lex anything. Return existing token again
+ self.position = old_position;
+ } else {
+ self.current_kind = re_lexed_kind;
+ }
+
+ re_lexed_kind
+ }
+}
+
fn is_tag_name_byte(byte: u8) -> bool {
// Canonical HTML tag names are specified to be case-insensitive and alphanumeric.
// https://html.spec.whatwg.org/#elements-2
diff --git a/crates/biome_html_parser/src/lexer/tests.rs b/crates/biome_html_parser/src/lexer/tests.rs
index ffae14853385..9e0e5491f08e 100644
--- a/crates/biome_html_parser/src/lexer/tests.rs
+++ b/crates/biome_html_parser/src/lexer/tests.rs
@@ -394,14 +394,14 @@ fn svelte_keywords() {
SV_CURLY_AT: 2,
DEBUG_KW: 5,
WHITESPACE: 1,
- SVELTE_IDENT: 5,
+ DEBUG_KW: 5,
);
assert_lex!(
HtmlLexContext::Svelte,
" debug ",
WHITESPACE: 2,
- SVELTE_IDENT: 5,
+ DEBUG_KW: 5,
WHITESPACE: 2,
)
}
diff --git a/crates/biome_html_parser/src/parser.rs b/crates/biome_html_parser/src/parser.rs
index 66261d3154b6..60c7525276ba 100644
--- a/crates/biome_html_parser/src/parser.rs
+++ b/crates/biome_html_parser/src/parser.rs
@@ -1,4 +1,6 @@
-use crate::token_source::{HtmlTokenSource, HtmlTokenSourceCheckpoint, TextExpressionKind};
+use crate::token_source::{
+ HtmlReLexContext, HtmlTokenSource, HtmlTokenSourceCheckpoint, TextExpressionKind,
+};
use biome_html_factory::HtmlSyntaxFactory;
use biome_html_syntax::{
HtmlFileSource, HtmlLanguage, HtmlSyntaxKind, HtmlTextExpressions, HtmlVariant,
@@ -65,6 +67,12 @@ impl<'source> HtmlParser<'source> {
// scoped properties that aren't only dependent on checkpoints and
// should be reset manually when the scope of their use is exited.
}
+
+ /// Re-lexes the current token in the specified context. Returns the kind
+ /// of the re-lexed token (can be the same as before if the context doesn't make a difference for the current token)
+ pub fn re_lex(&mut self, context: HtmlReLexContext) -> HtmlSyntaxKind {
+ self.source_mut().re_lex(context)
+ }
}
pub struct HtmlParserCheckpoint {
diff --git a/crates/biome_html_parser/src/syntax/mod.rs b/crates/biome_html_parser/src/syntax/mod.rs
index 31b082af0a06..21b51f912298 100644
--- a/crates/biome_html_parser/src/syntax/mod.rs
+++ b/crates/biome_html_parser/src/syntax/mod.rs
@@ -9,7 +9,9 @@ use crate::syntax::parse_error::*;
use crate::syntax::svelte::{
parse_attach_attribute, parse_svelte_at_block, parse_svelte_hash_block,
};
-use crate::token_source::{HtmlEmbeddedLanguage, HtmlLexContext, TextExpressionKind};
+use crate::token_source::{
+ HtmlEmbeddedLanguage, HtmlLexContext, HtmlReLexContext, TextExpressionKind,
+};
use biome_html_syntax::HtmlSyntaxKind::*;
use biome_html_syntax::{HtmlSyntaxKind, T};
use biome_parser::Parser;
@@ -568,6 +570,21 @@ impl TextExpression {
}
}
+fn parse_single_text_expression_content(p: &mut HtmlParser) -> ParsedSyntax {
+ if p.at(EOF) || p.at(T![<]) || p.at(T!['}']) || p.cur_text().trim().is_empty() {
+ return Absent;
+ }
+ let m = p.start();
+
+ if p.at(SVELTE_IDENT) {
+ p.re_lex(HtmlReLexContext::SingleTextExpression);
+ } else {
+ p.bump_remap(HTML_LITERAL);
+ }
+
+ Present(m.complete(p, HTML_TEXT_EXPRESSION))
+}
+
impl TextExpression {
fn parse_element(&mut self, p: &mut HtmlParser) -> ParsedSyntax {
if p.at(EOF) || p.at(T![<]) {
@@ -583,7 +600,7 @@ impl TextExpression {
HtmlLexContext::TextExpression(self.kind),
);
} else if !p.at(T!['}']) {
- p.bump_remap_with_context(HTML_LITERAL, HtmlLexContext::InsideTag);
+ p.bump_remap(HTML_LITERAL);
} else {
m.abandon(p);
return Absent;
diff --git a/crates/biome_html_parser/src/syntax/parse_error.rs b/crates/biome_html_parser/src/syntax/parse_error.rs
index 4119b5b449f3..0be3b4fc766b 100644
--- a/crates/biome_html_parser/src/syntax/parse_error.rs
+++ b/crates/biome_html_parser/src/syntax/parse_error.rs
@@ -37,6 +37,10 @@ pub(crate) fn expected_text_expression(p: &HtmlParser, range: TextRange) -> Pars
}
pub(crate) fn expected_child(p: &HtmlParser, range: TextRange) -> ParseDiagnostic {
+ expect_one_of(&["element", "text"], range).into_diagnostic(p)
+}
+
+pub(crate) fn expected_child_or_block(p: &HtmlParser, range: TextRange) -> ParseDiagnostic {
expect_one_of(&["element", "text", "closing block"], range).into_diagnostic(p)
}
diff --git a/crates/biome_html_parser/src/syntax/svelte.rs b/crates/biome_html_parser/src/syntax/svelte.rs
index 7f86d645000c..4648b8f26273 100644
--- a/crates/biome_html_parser/src/syntax/svelte.rs
+++ b/crates/biome_html_parser/src/syntax/svelte.rs
@@ -1,14 +1,15 @@
use crate::parser::HtmlParser;
use crate::syntax::parse_error::{
- expected_child, expected_svelte_closing_block, expected_text_expression,
+ expected_child_or_block, expected_svelte_closing_block, expected_text_expression,
};
-use crate::syntax::{TextExpression, parse_html_element};
-use crate::token_source::HtmlLexContext;
+use crate::syntax::{parse_html_element, parse_single_text_expression_content};
+use crate::token_source::{HtmlLexContext, HtmlReLexContext};
use biome_html_syntax::HtmlSyntaxKind::{
EOF, HTML_BOGUS_ELEMENT, HTML_ELEMENT_LIST, SVELTE_ATTACH_ATTRIBUTE, SVELTE_BINDING_LIST,
- SVELTE_BOGUS_BLOCK, SVELTE_CONST_BLOCK, SVELTE_DEBUG_BLOCK, SVELTE_HTML_BLOCK, SVELTE_IDENT,
- SVELTE_KEY_BLOCK, SVELTE_KEY_CLOSING_BLOCK, SVELTE_KEY_OPENING_BLOCK, SVELTE_NAME,
- SVELTE_RENDER_BLOCK,
+ SVELTE_BOGUS_BLOCK, SVELTE_CONST_BLOCK, SVELTE_DEBUG_BLOCK, SVELTE_ELSE_CLAUSE,
+ SVELTE_ELSE_IF_CLAUSE, SVELTE_ELSE_IF_CLAUSE_LIST, SVELTE_HTML_BLOCK, SVELTE_IDENT,
+ SVELTE_IF_BLOCK, SVELTE_IF_CLOSING_BLOCK, SVELTE_IF_OPENING_BLOCK, SVELTE_KEY_BLOCK,
+ SVELTE_KEY_CLOSING_BLOCK, SVELTE_KEY_OPENING_BLOCK, SVELTE_NAME, SVELTE_RENDER_BLOCK,
};
use biome_html_syntax::{HtmlSyntaxKind, T};
use biome_parser::parse_lists::{ParseNodeList, ParseSeparatedList};
@@ -16,37 +17,135 @@ 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 std::ops::Sub;
pub(crate) fn parse_svelte_hash_block(p: &mut HtmlParser) -> ParsedSyntax {
if !p.at(T!["{#"]) {
return Absent;
}
+ let m = p.start();
+ p.bump_with_context(T!["{#"], HtmlLexContext::Svelte);
+ match p.cur() {
+ T![key] => parse_key_block(p, m),
+ T![if] => parse_if_block(p, m),
+ _ => {
+ m.abandon(p);
+ 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!["{#"]) {
+pub(crate) fn parse_key_block(p: &mut HtmlParser, parent_marker: Marker) -> ParsedSyntax {
+ let result = parse_opening_block(p, T![key], SVELTE_KEY_OPENING_BLOCK, parent_marker);
+ if result.is_absent() {
+ return Absent;
+ }
+ let m = result.precede(p);
+
+ SvelteElementList::new().parse_list(p);
+
+ parse_closing_block(p, T![key], SVELTE_KEY_CLOSING_BLOCK).or_add_diagnostic(p, |p, range| {
+ expected_svelte_closing_block(p, range)
+ .with_detail(range.sub(m.start()), "This is where the block started.")
+ });
+
+ Present(m.complete(p, SVELTE_KEY_BLOCK))
+}
+
+pub(crate) fn parse_if_block(p: &mut HtmlParser, parent_marker: Marker) -> ParsedSyntax {
+ if !p.at(T![if]) {
+ parent_marker.abandon(p);
return Absent;
}
+ let result = parse_if_opening_block(p, parent_marker);
+ let m = result.precede(p);
+
+ SvelteElseIfClauseLit.parse_list(p);
+
+ parse_else_clause(p).ok();
+
+ parse_closing_block(p, T![if], SVELTE_IF_CLOSING_BLOCK).or_add_diagnostic(p, |p, range| {
+ expected_svelte_closing_block(p, range)
+ .with_detail(range.sub(m.start()), "This is where the block started.")
+ });
+
+ Present(m.complete(p, SVELTE_IF_BLOCK))
+}
+
+fn parse_if_opening_block(p: &mut HtmlParser, parent_marker: Marker) -> ParsedSyntax {
+ if !p.at(T![if]) {
+ parent_marker.abandon(p);
+ return Absent;
+ }
+
+ p.bump_with_context(T![if], HtmlLexContext::single_expression());
+
+ parse_single_text_expression_content(p).or_add_diagnostic(p, |p, range| {
+ p.err_builder(
+ "Expected an expression, instead none was found.",
+ range.sub_start(parent_marker.start()),
+ )
+ });
+
+ p.expect(T!['}']);
+
+ SvelteElementList::new()
+ .with_stop_at_curly_colon()
+ .parse_list(p);
+
+ Present(parent_marker.complete(p, SVELTE_IF_OPENING_BLOCK))
+}
+
+/// Parses `{:else if expression} ...`
+pub(crate) fn parse_else_if_clause(p: &mut HtmlParser) -> ParsedSyntax {
+ if !p.at(T!["{:"]) {
+ return Absent;
+ }
let m = p.start();
+ let checkpoint = p.checkpoint();
- let completed = parse_opening_block(p, T![key], SVELTE_KEY_OPENING_BLOCK).ok();
+ p.bump_with_context(T!["{:"], HtmlLexContext::Svelte);
- SvelteElementList.parse_list(p);
+ p.expect_with_context(T![else], HtmlLexContext::Svelte);
- 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
- }
+ if p.at(T!['}']) {
+ // It's an `{:else}` block, we rewind the parsing and exit early
+ m.abandon(p);
+ p.rewind(checkpoint);
+ return Absent;
+ }
+ p.expect_with_context(T![if], HtmlLexContext::single_expression());
+
+ parse_single_text_expression_content(p).or_add_diagnostic(p, |p, range| {
+ p.err_builder(
+ "Expected an expression, instead none was found.",
+ range.sub_start(m.start()),
+ )
});
- Present(m.complete(p, SVELTE_KEY_BLOCK))
+ p.expect(T!['}']);
+
+ SvelteElementList::new()
+ .with_stop_at_curly_colon()
+ .parse_list(p);
+
+ Present(m.complete(p, SVELTE_ELSE_IF_CLAUSE))
+}
+
+/// Parses `{:else} ...`
+pub(crate) fn parse_else_clause(p: &mut HtmlParser) -> ParsedSyntax {
+ if !p.at(T!["{:"]) {
+ return Absent;
+ }
+ let m = p.start();
+ p.bump_with_context(T!["{:"], HtmlLexContext::Svelte);
+ p.expect(T![else]);
+ p.expect(T!['}']);
+ SvelteElementList::new().parse_list(p);
+ Present(m.complete(p, SVELTE_ELSE_CLAUSE))
}
/// Parses a `{# expression }` block.
@@ -56,28 +155,20 @@ pub(crate) fn parse_opening_block(
p: &mut HtmlParser,
keyword: HtmlSyntaxKind,
node: HtmlSyntaxKind,
+ m: Marker,
) -> 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.bump_with_context(keyword, HtmlLexContext::single_expression());
+ parse_single_text_expression_content(p).or_add_diagnostic(p, |p, range| {
+ p.err_builder(
+ "Expected an expression, instead none was found.",
+ range.sub_start(m.start()),
+ )
+ });
p.expect(T!['}']);
@@ -142,9 +233,7 @@ pub(crate) fn parse_html_block(p: &mut HtmlParser, marker: Marker) -> ParsedSynt
}
p.bump_with_context(T![html], HtmlLexContext::single_expression());
- TextExpression::new_single()
- .parse_element(p)
- .or_add_diagnostic(p, expected_text_expression);
+ parse_single_text_expression_content(p).or_add_diagnostic(p, expected_text_expression);
p.expect(T!['}']);
@@ -157,9 +246,7 @@ pub(crate) fn parse_render_block(p: &mut HtmlParser, marker: Marker) -> ParsedSy
}
p.bump_with_context(T![render], HtmlLexContext::single_expression());
- TextExpression::new_single()
- .parse_element(p)
- .or_add_diagnostic(p, expected_text_expression);
+ parse_single_text_expression_content(p).or_add_diagnostic(p, expected_text_expression);
p.expect(T!['}']);
@@ -174,9 +261,7 @@ pub(crate) fn parse_attach_attribute(p: &mut HtmlParser) -> ParsedSyntax {
p.bump_with_context(T!["{@"], HtmlLexContext::Svelte);
p.expect_with_context(T![attach], HtmlLexContext::single_expression());
- TextExpression::new_single()
- .parse_element(p)
- .or_add_diagnostic(p, expected_text_expression);
+ parse_single_text_expression_content(p).or_add_diagnostic(p, expected_text_expression);
p.expect_with_context(T!['}'], HtmlLexContext::InsideTag);
@@ -189,9 +274,7 @@ pub(crate) fn parse_const_block(p: &mut HtmlParser, marker: Marker) -> ParsedSyn
}
p.bump_with_context(T![const], HtmlLexContext::single_expression());
- TextExpression::new_single()
- .parse_element(p)
- .or_add_diagnostic(p, expected_text_expression);
+ parse_single_text_expression_content(p).or_add_diagnostic(p, expected_text_expression);
p.expect(T!['}']);
@@ -246,17 +329,27 @@ impl ParseSeparatedList for BindingList {
}
fn parse_name(p: &mut HtmlParser) -> ParsedSyntax {
- if !p.at(SVELTE_IDENT) {
- return Absent;
- }
let m = p.start();
- p.bump_with_context(SVELTE_IDENT, HtmlLexContext::Svelte);
+ p.bump_remap_with_context(SVELTE_IDENT, HtmlLexContext::Svelte);
Present(m.complete(p, SVELTE_NAME))
}
#[derive(Default)]
-struct SvelteElementList;
+struct SvelteElementList {
+ /// If `true`, the list parsing stops at `{:` too when calling [ParseNodeList::is_at_list_end]
+ stop_at_curly_colon: bool,
+}
+
+impl SvelteElementList {
+ pub fn new() -> Self {
+ Self::default()
+ }
+ pub fn with_stop_at_curly_colon(mut self) -> Self {
+ self.stop_at_curly_colon = true;
+ self
+ }
+}
impl ParseNodeList for SvelteElementList {
type Kind = HtmlSyntaxKind;
@@ -271,7 +364,10 @@ impl ParseNodeList for SvelteElementList {
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!["{/"])
+ at_l_angle0 && at_slash1
+ || at_eof
+ || p.at(T!["{/"])
+ || (self.stop_at_curly_colon && p.at(T!["{:"]))
}
fn recover(
@@ -282,7 +378,58 @@ impl ParseNodeList for SvelteElementList {
parsed_element.or_recover_with_token_set(
p,
&ParseRecoveryTokenSet::new(HTML_BOGUS_ELEMENT, token_set![T![<], T![>], T!["{/"]]),
- expected_child,
+ expected_child_or_block,
+ )
+ }
+}
+
+#[derive(Debug)]
+struct SvelteElseIfClauseLit;
+
+impl ParseNodeList for SvelteElseIfClauseLit {
+ type Kind = HtmlSyntaxKind;
+ type Parser<'source> = HtmlParser<'source>;
+ const LIST_KIND: Self::Kind = SVELTE_ELSE_IF_CLAUSE_LIST;
+
+ fn parse_element(&mut self, p: &mut Self::Parser<'_>) -> ParsedSyntax {
+ parse_else_if_clause(p)
+ }
+
+ fn is_at_list_end(&self, p: &mut Self::Parser<'_>) -> bool {
+ let closing = p.at(T!["{/"]);
+ if closing {
+ return true;
+ }
+ // Here we need to get creative. At the moment svelte keywords are correctly lexed
+ // only when we use the `Svelte` context. To retrieve them, we use the relex
+ // feature to bump two tokens, and relex them with the proper context.
+ // Once we retrieved the relexed tokens, we rewind the parser.
+ let curly_colon = p.at(T!["{:"]);
+ let checkpoint = p.checkpoint();
+ p.bump_any();
+ p.re_lex(HtmlReLexContext::Svelte);
+ let at_else = p.at(T![else]);
+ p.bump_any();
+ p.re_lex(HtmlReLexContext::Svelte);
+ let at_if = p.at(T![if]);
+
+ let condition = curly_colon && at_else && !at_if;
+ p.rewind(checkpoint);
+ condition
+ }
+
+ fn recover(
+ &mut self,
+ p: &mut Self::Parser<'_>,
+ parsed_element: ParsedSyntax,
+ ) -> RecoveryResult {
+ parsed_element.or_recover_with_token_set(
+ p,
+ &ParseRecoveryTokenSet::new(
+ SVELTE_BOGUS_BLOCK,
+ token_set![T![<], T![>], T!["{/"], T!["{:"]],
+ ),
+ expected_child_or_block,
)
}
}
diff --git a/crates/biome_html_parser/src/token_source.rs b/crates/biome_html_parser/src/token_source.rs
index 900a18a767cd..66bf944759c4 100644
--- a/crates/biome_html_parser/src/token_source.rs
+++ b/crates/biome_html_parser/src/token_source.rs
@@ -97,6 +97,12 @@ impl LexContext for HtmlLexContext {
}
}
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub(crate) enum HtmlReLexContext {
+ Svelte,
+ SingleTextExpression,
+}
+
pub(crate) type HtmlTokenSourceCheckpoint = TokenSourceCheckpoint;
impl<'source> HtmlTokenSource<'source> {
@@ -158,6 +164,10 @@ impl<'source> HtmlTokenSource<'source> {
self.trivia_list.truncate(checkpoint.trivia_len as usize);
self.lexer.rewind(checkpoint.lexer_checkpoint);
}
+
+ pub fn re_lex(&mut self, mode: HtmlReLexContext) -> HtmlSyntaxKind {
+ self.lexer.re_lex(mode)
+ }
}
impl TokenSource for HtmlTokenSource<'_> {
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 4541a0a9a502..d8ae22d2301d 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
@@ -26,9 +26,11 @@ HtmlRoot {
svelte_ident_token: SVELTE_IDENT@8..17 "something" [] [],
},
COMMA@17..18 "," [] [],
- missing element,
+ SvelteName {
+ svelte_ident_token: SVELTE_IDENT@18..19 "}" [] [],
+ },
],
- r_curly_token: R_CURLY@18..19 "}" [] [],
+ r_curly_token: missing (required),
},
],
eof_token: EOF@19..20 "" [Newline("\n")] [],
@@ -46,12 +48,13 @@ HtmlRoot {
0: SVELTE_DEBUG_BLOCK@0..19
0: SV_CURLY_AT@0..2 "{@" [] []
1: DEBUG_KW@2..8 "debug" [] [Whitespace(" ")]
- 2: SVELTE_BINDING_LIST@8..18
+ 2: SVELTE_BINDING_LIST@8..19
0: SVELTE_NAME@8..17
0: SVELTE_IDENT@8..17 "something" [] []
1: COMMA@17..18 "," [] []
- 2: (empty)
- 3: R_CURLY@18..19 "}" [] []
+ 2: SVELTE_NAME@18..19
+ 0: SVELTE_IDENT@18..19 "}" [] []
+ 3: (empty)
4: EOF@19..20 "" [Newline("\n")] []
```
@@ -59,12 +62,18 @@ HtmlRoot {
## Diagnostics
```
-debug-trailing-comma.svelte:1:19 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+debug-trailing-comma.svelte:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- × Expected a closing block, instead found none.
+ × expected `}` but instead the file ends
+
+ 1 │ {@debug something,}
+ > 2 │
+ │
+
+ i the file ends here
- > 1 │ {@debug something,}
- │ ^
- 2 │
+ 1 │ {@debug something,}
+ > 2 │
+ │
```
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 9eaa3e5736fc..cd1bf42a420b 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
@@ -23,13 +23,15 @@ HtmlRoot {
SvelteDebugBlock {
sv_curly_at_token: SV_CURLY_AT@0..2 "{@" [] [],
debug_token: DEBUG_KW@2..7 "debug" [] [],
- bindings: SvelteBindingList [],
- r_curly_token: missing (required),
- },
- SvelteDebugBlock {
- sv_curly_at_token: SV_CURLY_AT@7..10 "{@" [Newline("\n")] [],
- debug_token: DEBUG_KW@10..16 "debug" [] [Whitespace(" ")],
bindings: SvelteBindingList [
+ SvelteName {
+ svelte_ident_token: SVELTE_IDENT@7..10 "{@" [Newline("\n")] [],
+ },
+ missing separator,
+ SvelteName {
+ svelte_ident_token: SVELTE_IDENT@10..16 "debug" [] [Whitespace(" ")],
+ },
+ missing separator,
SvelteName {
svelte_ident_token: SVELTE_IDENT@16..25 "something" [] [],
},
@@ -59,19 +61,20 @@ HtmlRoot {
1: (empty)
2: (empty)
3: HTML_ELEMENT_LIST@0..41
- 0: SVELTE_DEBUG_BLOCK@0..7
+ 0: SVELTE_DEBUG_BLOCK@0..26
0: SV_CURLY_AT@0..2 "{@" [] []
1: DEBUG_KW@2..7 "debug" [] []
- 2: SVELTE_BINDING_LIST@7..7
- 3: (empty)
- 1: SVELTE_DEBUG_BLOCK@7..26
- 0: SV_CURLY_AT@7..10 "{@" [Newline("\n")] []
- 1: DEBUG_KW@10..16 "debug" [] [Whitespace(" ")]
- 2: SVELTE_BINDING_LIST@16..25
- 0: SVELTE_NAME@16..25
+ 2: SVELTE_BINDING_LIST@7..25
+ 0: SVELTE_NAME@7..10
+ 0: SVELTE_IDENT@7..10 "{@" [Newline("\n")] []
+ 1: (empty)
+ 2: SVELTE_NAME@10..16
+ 0: SVELTE_IDENT@10..16 "debug" [] [Whitespace(" ")]
+ 3: (empty)
+ 4: SVELTE_NAME@16..25
0: SVELTE_IDENT@16..25 "something" [] []
3: R_CURLY@25..26 "}" [] []
- 2: SVELTE_DEBUG_BLOCK@26..41
+ 1: SVELTE_DEBUG_BLOCK@26..41
0: SV_CURLY_AT@26..29 "{@" [Newline("\n")] []
1: DEBUG_KW@29..35 "debug" [] [Whitespace(" ")]
2: SVELTE_BINDING_LIST@35..40
@@ -85,14 +88,28 @@ HtmlRoot {
## Diagnostics
```
-debug.svelte:2:1 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+debug.svelte:2:3 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- × Expected a closing block, instead found none.
+ × expected `,` but instead found `debug`
1 │ {@debug
> 2 │ {@debug something}
- │ ^^
+ │ ^^^^^
3 │ {@debug debug}
4 │
+ i Remove debug
+
+debug.svelte:2:9 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+
+ × expected `,` but instead found `something`
+
+ 1 │ {@debug
+ > 2 │ {@debug something}
+ │ ^^^^^^^^^
+ 3 │ {@debug debug}
+ 4 │
+
+ i Remove 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
index 67621afa7dbc..f11c50004a0c 100644
--- 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
@@ -22,9 +22,9 @@ HtmlRoot {
SvelteKeyBlock {
opening_block: SvelteKeyOpeningBlock {
sv_curly_hash_token: SV_CURLY_HASH@0..2 "{#" [] [],
- key_token: KEY_KW@2..6 "key" [] [Whitespace(" ")],
+ key_token: KEY_KW@2..5 "key" [] [],
expression: HtmlTextExpression {
- html_literal_token: HTML_LITERAL@6..16 "expression" [] [],
+ html_literal_token: HTML_LITERAL@5..16 " expression" [] [],
},
r_curly_token: R_CURLY@16..17 "}" [] [],
},
@@ -51,9 +51,9 @@ HtmlRoot {
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" [] []
+ 1: KEY_KW@2..5 "key" [] []
+ 2: HTML_TEXT_EXPRESSION@5..16
+ 0: HTML_LITERAL@5..16 " expression" [] []
3: R_CURLY@16..17 "}" [] []
1: HTML_ELEMENT_LIST@17..28
0: HTML_CONTENT@17..28
@@ -77,9 +77,9 @@ key_missing_close.svelte:3:1 parse ━━━━━━━━━━━━━━━
i This is where the block started.
- > 1 │ {#key expression}
- │ ^^^^^^^^^^^^^^^^^
+ 1 │ {#key expression}
2 │ something
- 3 │
+ > 3 │
+ │
```
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
index cbb77da26140..9eada158d155 100644
--- 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
@@ -23,11 +23,17 @@ HtmlRoot {
SvelteKeyBlock {
opening_block: SvelteKeyOpeningBlock {
sv_curly_hash_token: SV_CURLY_HASH@0..2 "{#" [] [],
- key_token: KEY_KW@2..6 "key" [] [Whitespace(" ")],
+ key_token: KEY_KW@2..5 "key" [] [],
expression: missing (required),
- r_curly_token: R_CURLY@6..7 "}" [] [],
+ r_curly_token: missing (required),
},
children: HtmlElementList [
+ HtmlContent {
+ value_token: HTML_LITERAL@5..6 " " [] [],
+ },
+ HtmlContent {
+ value_token: HTML_LITERAL@6..7 "}" [] [],
+ },
HtmlContent {
value_token: HTML_LITERAL@7..18 "something" [Newline("\n"), Whitespace("\t")] [],
},
@@ -52,13 +58,17 @@ HtmlRoot {
2: (empty)
3: HTML_ELEMENT_LIST@0..25
0: SVELTE_KEY_BLOCK@0..25
- 0: SVELTE_KEY_OPENING_BLOCK@0..7
+ 0: SVELTE_KEY_OPENING_BLOCK@0..5
0: SV_CURLY_HASH@0..2 "{#" [] []
- 1: KEY_KW@2..6 "key" [] [Whitespace(" ")]
+ 1: KEY_KW@2..5 "key" [] []
2: (empty)
- 3: R_CURLY@6..7 "}" [] []
- 1: HTML_ELEMENT_LIST@7..18
- 0: HTML_CONTENT@7..18
+ 3: (empty)
+ 1: HTML_ELEMENT_LIST@5..18
+ 0: HTML_CONTENT@5..6
+ 0: HTML_LITERAL@5..6 " " [] []
+ 1: HTML_CONTENT@6..7
+ 0: HTML_LITERAL@6..7 "}" [] []
+ 2: 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")] []
@@ -71,12 +81,12 @@ HtmlRoot {
## Diagnostics
```
-key_missing_expression.svelte:1:7 parse ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
+key_missing_expression.svelte:1:6 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/debug.svelte b/crates/biome_html_parser/tests/html_specs/ok/svelte/debug.svelte
index 49658fd11910..c45338eaa273 100644
--- a/crates/biome_html_parser/tests/html_specs/ok/svelte/debug.svelte
+++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/debug.svelte
@@ -1,3 +1,4 @@
{@debug}
{@debug something}
+{@debug debug}
{@debug something, something, something}
diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/debug.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/debug.svelte.snap
index 2706db993e59..0887f3cffe57 100644
--- a/crates/biome_html_parser/tests/html_specs/ok/svelte/debug.svelte.snap
+++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/debug.svelte.snap
@@ -7,6 +7,7 @@ expression: snapshot
```svelte
{@debug}
{@debug something}
+{@debug debug}
{@debug something, something, something}
```
@@ -41,32 +42,42 @@ HtmlRoot {
debug_token: DEBUG_KW@30..36 "debug" [] [Whitespace(" ")],
bindings: SvelteBindingList [
SvelteName {
- svelte_ident_token: SVELTE_IDENT@36..45 "something" [] [],
+ svelte_ident_token: SVELTE_IDENT@36..41 "debug" [] [],
},
- COMMA@45..47 "," [] [Whitespace(" ")],
+ ],
+ r_curly_token: R_CURLY@41..42 "}" [] [],
+ },
+ SvelteDebugBlock {
+ sv_curly_at_token: SV_CURLY_AT@42..45 "{@" [Newline("\n")] [],
+ debug_token: DEBUG_KW@45..51 "debug" [] [Whitespace(" ")],
+ bindings: SvelteBindingList [
+ SvelteName {
+ svelte_ident_token: SVELTE_IDENT@51..60 "something" [] [],
+ },
+ COMMA@60..62 "," [] [Whitespace(" ")],
SvelteName {
- svelte_ident_token: SVELTE_IDENT@47..56 "something" [] [],
+ svelte_ident_token: SVELTE_IDENT@62..71 "something" [] [],
},
- COMMA@56..58 "," [] [Whitespace(" ")],
+ COMMA@71..73 "," [] [Whitespace(" ")],
SvelteName {
- svelte_ident_token: SVELTE_IDENT@58..67 "something" [] [],
+ svelte_ident_token: SVELTE_IDENT@73..82 "something" [] [],
},
],
- r_curly_token: R_CURLY@67..68 "}" [] [],
+ r_curly_token: R_CURLY@82..83 "}" [] [],
},
],
- eof_token: EOF@68..69 "" [Newline("\n")] [],
+ eof_token: EOF@83..84 "" [Newline("\n")] [],
}
```
## CST
```
-0: HTML_ROOT@0..69
+0: HTML_ROOT@0..84
0: (empty)
1: (empty)
2: (empty)
- 3: HTML_ELEMENT_LIST@0..68
+ 3: HTML_ELEMENT_LIST@0..83
0: SVELTE_DEBUG_BLOCK@0..8
0: SV_CURLY_AT@0..2 "{@" [] []
1: DEBUG_KW@2..7 "debug" [] []
@@ -79,19 +90,26 @@ HtmlRoot {
0: SVELTE_NAME@17..26
0: SVELTE_IDENT@17..26 "something" [] []
3: R_CURLY@26..27 "}" [] []
- 2: SVELTE_DEBUG_BLOCK@27..68
+ 2: SVELTE_DEBUG_BLOCK@27..42
0: SV_CURLY_AT@27..30 "{@" [Newline("\n")] []
1: DEBUG_KW@30..36 "debug" [] [Whitespace(" ")]
- 2: SVELTE_BINDING_LIST@36..67
- 0: SVELTE_NAME@36..45
- 0: SVELTE_IDENT@36..45 "something" [] []
- 1: COMMA@45..47 "," [] [Whitespace(" ")]
- 2: SVELTE_NAME@47..56
- 0: SVELTE_IDENT@47..56 "something" [] []
- 3: COMMA@56..58 "," [] [Whitespace(" ")]
- 4: SVELTE_NAME@58..67
- 0: SVELTE_IDENT@58..67 "something" [] []
- 3: R_CURLY@67..68 "}" [] []
- 4: EOF@68..69 "" [Newline("\n")] []
+ 2: SVELTE_BINDING_LIST@36..41
+ 0: SVELTE_NAME@36..41
+ 0: SVELTE_IDENT@36..41 "debug" [] []
+ 3: R_CURLY@41..42 "}" [] []
+ 3: SVELTE_DEBUG_BLOCK@42..83
+ 0: SV_CURLY_AT@42..45 "{@" [Newline("\n")] []
+ 1: DEBUG_KW@45..51 "debug" [] [Whitespace(" ")]
+ 2: SVELTE_BINDING_LIST@51..82
+ 0: SVELTE_NAME@51..60
+ 0: SVELTE_IDENT@51..60 "something" [] []
+ 1: COMMA@60..62 "," [] [Whitespace(" ")]
+ 2: SVELTE_NAME@62..71
+ 0: SVELTE_IDENT@62..71 "something" [] []
+ 3: COMMA@71..73 "," [] [Whitespace(" ")]
+ 4: SVELTE_NAME@73..82
+ 0: SVELTE_IDENT@73..82 "something" [] []
+ 3: R_CURLY@82..83 "}" [] []
+ 4: EOF@83..84 "" [Newline("\n")] []
```
diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/if.svelte b/crates/biome_html_parser/tests/html_specs/ok/svelte/if.svelte
new file mode 100644
index 000000000000..e769b4dedd24
--- /dev/null
+++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/if.svelte
@@ -0,0 +1,13 @@
+{#if answer === 42}
+ what was the question?
+{/if}
+
+{#if answer === 42}
+ what was the question?
+ {#if answer === 42}
+ what was the question?
+ {#if answer === 42}
+ what was the question?
+ {/if}
+ {/if}
+{/if}
diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/if.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/if.svelte.snap
new file mode 100644
index 000000000000..1972bce6560f
--- /dev/null
+++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/if.svelte.snap
@@ -0,0 +1,336 @@
+---
+source: crates/biome_html_parser/tests/spec_test.rs
+expression: snapshot
+---
+## Input
+
+```svelte
+{#if answer === 42}
+ what was the question?
+{/if}
+
+{#if answer === 42}
+ what was the question?
+ {#if answer === 42}
+ what was the question?
+ {#if answer === 42}
+ what was the question?
+ {/if}
+ {/if}
+{/if}
+
+```
+
+
+## AST
+
+```
+HtmlRoot {
+ bom_token: missing (optional),
+ frontmatter: missing (optional),
+ directive: missing (optional),
+ html: HtmlElementList [
+ SvelteIfBlock {
+ opening_block: SvelteIfOpeningBlock {
+ sv_curly_hash_token: SV_CURLY_HASH@0..2 "{#" [] [],
+ if_token: IF_KW@2..4 "if" [] [],
+ expression: HtmlTextExpression {
+ html_literal_token: HTML_LITERAL@4..18 " answer === 42" [] [],
+ },
+ r_curly_token: R_CURLY@18..19 "}" [] [],
+ children: HtmlElementList [
+ HtmlElement {
+ opening_element: HtmlOpeningElement {
+ l_angle_token: L_ANGLE@19..22 "<" [Newline("\n"), Whitespace("\t")] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@22..23 "p" [] [],
+ },
+ attributes: HtmlAttributeList [],
+ r_angle_token: R_ANGLE@23..24 ">" [] [],
+ },
+ children: HtmlElementList [
+ HtmlContent {
+ value_token: HTML_LITERAL@24..46 "what was the question?" [] [],
+ },
+ ],
+ closing_element: HtmlClosingElement {
+ l_angle_token: L_ANGLE@46..47 "<" [] [],
+ slash_token: SLASH@47..48 "/" [] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@48..49 "p" [] [],
+ },
+ r_angle_token: R_ANGLE@49..50 ">" [] [],
+ },
+ },
+ ],
+ },
+ else_if_clauses: SvelteElseIfClauseList [],
+ else_clause: missing (optional),
+ closing_block: SvelteIfClosingBlock {
+ sv_curly_slash_token: SV_CURLY_SLASH@50..53 "{/" [Newline("\n")] [],
+ if_token: IF_KW@53..55 "if" [] [],
+ r_curly_token: R_CURLY@55..56 "}" [] [],
+ },
+ },
+ SvelteIfBlock {
+ opening_block: SvelteIfOpeningBlock {
+ sv_curly_hash_token: SV_CURLY_HASH@56..60 "{#" [Newline("\n"), Newline("\n")] [],
+ if_token: IF_KW@60..62 "if" [] [],
+ expression: HtmlTextExpression {
+ html_literal_token: HTML_LITERAL@62..76 " answer === 42" [] [],
+ },
+ r_curly_token: R_CURLY@76..77 "}" [] [],
+ children: HtmlElementList [
+ HtmlElement {
+ opening_element: HtmlOpeningElement {
+ l_angle_token: L_ANGLE@77..80 "<" [Newline("\n"), Whitespace("\t")] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@80..81 "p" [] [],
+ },
+ attributes: HtmlAttributeList [],
+ r_angle_token: R_ANGLE@81..82 ">" [] [],
+ },
+ children: HtmlElementList [
+ HtmlContent {
+ value_token: HTML_LITERAL@82..104 "what was the question?" [] [],
+ },
+ ],
+ closing_element: HtmlClosingElement {
+ l_angle_token: L_ANGLE@104..105 "<" [] [],
+ slash_token: SLASH@105..106 "/" [] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@106..107 "p" [] [],
+ },
+ r_angle_token: R_ANGLE@107..108 ">" [] [],
+ },
+ },
+ SvelteIfBlock {
+ opening_block: SvelteIfOpeningBlock {
+ sv_curly_hash_token: SV_CURLY_HASH@108..112 "{#" [Newline("\n"), Whitespace("\t")] [],
+ if_token: IF_KW@112..114 "if" [] [],
+ expression: HtmlTextExpression {
+ html_literal_token: HTML_LITERAL@114..128 " answer === 42" [] [],
+ },
+ r_curly_token: R_CURLY@128..129 "}" [] [],
+ children: HtmlElementList [
+ HtmlElement {
+ opening_element: HtmlOpeningElement {
+ l_angle_token: L_ANGLE@129..134 "<" [Newline("\n"), Whitespace(" \t")] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@134..135 "p" [] [],
+ },
+ attributes: HtmlAttributeList [],
+ r_angle_token: R_ANGLE@135..136 ">" [] [],
+ },
+ children: HtmlElementList [
+ HtmlContent {
+ value_token: HTML_LITERAL@136..158 "what was the question?" [] [],
+ },
+ ],
+ closing_element: HtmlClosingElement {
+ l_angle_token: L_ANGLE@158..159 "<" [] [],
+ slash_token: SLASH@159..160 "/" [] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@160..161 "p" [] [],
+ },
+ r_angle_token: R_ANGLE@161..162 ">" [] [],
+ },
+ },
+ SvelteIfBlock {
+ opening_block: SvelteIfOpeningBlock {
+ sv_curly_hash_token: SV_CURLY_HASH@162..168 "{#" [Newline("\n"), Whitespace(" \t")] [],
+ if_token: IF_KW@168..170 "if" [] [],
+ expression: HtmlTextExpression {
+ html_literal_token: HTML_LITERAL@170..184 " answer === 42" [] [],
+ },
+ r_curly_token: R_CURLY@184..185 "}" [] [],
+ children: HtmlElementList [
+ HtmlElement {
+ opening_element: HtmlOpeningElement {
+ l_angle_token: L_ANGLE@185..192 "<" [Newline("\n"), Whitespace(" \t")] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@192..193 "p" [] [],
+ },
+ attributes: HtmlAttributeList [],
+ r_angle_token: R_ANGLE@193..194 ">" [] [],
+ },
+ children: HtmlElementList [
+ HtmlContent {
+ value_token: HTML_LITERAL@194..216 "what was the question?" [] [],
+ },
+ ],
+ closing_element: HtmlClosingElement {
+ l_angle_token: L_ANGLE@216..217 "<" [] [],
+ slash_token: SLASH@217..218 "/" [] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@218..219 "p" [] [],
+ },
+ r_angle_token: R_ANGLE@219..220 ">" [] [],
+ },
+ },
+ ],
+ },
+ else_if_clauses: SvelteElseIfClauseList [],
+ else_clause: missing (optional),
+ closing_block: SvelteIfClosingBlock {
+ sv_curly_slash_token: SV_CURLY_SLASH@220..227 "{/" [Newline("\n"), Whitespace(" ")] [],
+ if_token: IF_KW@227..229 "if" [] [],
+ r_curly_token: R_CURLY@229..230 "}" [] [],
+ },
+ },
+ ],
+ },
+ else_if_clauses: SvelteElseIfClauseList [],
+ else_clause: missing (optional),
+ closing_block: SvelteIfClosingBlock {
+ sv_curly_slash_token: SV_CURLY_SLASH@230..235 "{/" [Newline("\n"), Whitespace(" ")] [],
+ if_token: IF_KW@235..237 "if" [] [],
+ r_curly_token: R_CURLY@237..238 "}" [] [],
+ },
+ },
+ ],
+ },
+ else_if_clauses: SvelteElseIfClauseList [],
+ else_clause: missing (optional),
+ closing_block: SvelteIfClosingBlock {
+ sv_curly_slash_token: SV_CURLY_SLASH@238..241 "{/" [Newline("\n")] [],
+ if_token: IF_KW@241..243 "if" [] [],
+ r_curly_token: R_CURLY@243..244 "}" [] [],
+ },
+ },
+ ],
+ eof_token: EOF@244..245 "" [Newline("\n")] [],
+}
+```
+
+## CST
+
+```
+0: HTML_ROOT@0..245
+ 0: (empty)
+ 1: (empty)
+ 2: (empty)
+ 3: HTML_ELEMENT_LIST@0..244
+ 0: SVELTE_IF_BLOCK@0..56
+ 0: SVELTE_IF_OPENING_BLOCK@0..50
+ 0: SV_CURLY_HASH@0..2 "{#" [] []
+ 1: IF_KW@2..4 "if" [] []
+ 2: HTML_TEXT_EXPRESSION@4..18
+ 0: HTML_LITERAL@4..18 " answer === 42" [] []
+ 3: R_CURLY@18..19 "}" [] []
+ 4: HTML_ELEMENT_LIST@19..50
+ 0: HTML_ELEMENT@19..50
+ 0: HTML_OPENING_ELEMENT@19..24
+ 0: L_ANGLE@19..22 "<" [Newline("\n"), Whitespace("\t")] []
+ 1: HTML_TAG_NAME@22..23
+ 0: HTML_LITERAL@22..23 "p" [] []
+ 2: HTML_ATTRIBUTE_LIST@23..23
+ 3: R_ANGLE@23..24 ">" [] []
+ 1: HTML_ELEMENT_LIST@24..46
+ 0: HTML_CONTENT@24..46
+ 0: HTML_LITERAL@24..46 "what was the question?" [] []
+ 2: HTML_CLOSING_ELEMENT@46..50
+ 0: L_ANGLE@46..47 "<" [] []
+ 1: SLASH@47..48 "/" [] []
+ 2: HTML_TAG_NAME@48..49
+ 0: HTML_LITERAL@48..49 "p" [] []
+ 3: R_ANGLE@49..50 ">" [] []
+ 1: SVELTE_ELSE_IF_CLAUSE_LIST@50..50
+ 2: (empty)
+ 3: SVELTE_IF_CLOSING_BLOCK@50..56
+ 0: SV_CURLY_SLASH@50..53 "{/" [Newline("\n")] []
+ 1: IF_KW@53..55 "if" [] []
+ 2: R_CURLY@55..56 "}" [] []
+ 1: SVELTE_IF_BLOCK@56..244
+ 0: SVELTE_IF_OPENING_BLOCK@56..238
+ 0: SV_CURLY_HASH@56..60 "{#" [Newline("\n"), Newline("\n")] []
+ 1: IF_KW@60..62 "if" [] []
+ 2: HTML_TEXT_EXPRESSION@62..76
+ 0: HTML_LITERAL@62..76 " answer === 42" [] []
+ 3: R_CURLY@76..77 "}" [] []
+ 4: HTML_ELEMENT_LIST@77..238
+ 0: HTML_ELEMENT@77..108
+ 0: HTML_OPENING_ELEMENT@77..82
+ 0: L_ANGLE@77..80 "<" [Newline("\n"), Whitespace("\t")] []
+ 1: HTML_TAG_NAME@80..81
+ 0: HTML_LITERAL@80..81 "p" [] []
+ 2: HTML_ATTRIBUTE_LIST@81..81
+ 3: R_ANGLE@81..82 ">" [] []
+ 1: HTML_ELEMENT_LIST@82..104
+ 0: HTML_CONTENT@82..104
+ 0: HTML_LITERAL@82..104 "what was the question?" [] []
+ 2: HTML_CLOSING_ELEMENT@104..108
+ 0: L_ANGLE@104..105 "<" [] []
+ 1: SLASH@105..106 "/" [] []
+ 2: HTML_TAG_NAME@106..107
+ 0: HTML_LITERAL@106..107 "p" [] []
+ 3: R_ANGLE@107..108 ">" [] []
+ 1: SVELTE_IF_BLOCK@108..238
+ 0: SVELTE_IF_OPENING_BLOCK@108..230
+ 0: SV_CURLY_HASH@108..112 "{#" [Newline("\n"), Whitespace("\t")] []
+ 1: IF_KW@112..114 "if" [] []
+ 2: HTML_TEXT_EXPRESSION@114..128
+ 0: HTML_LITERAL@114..128 " answer === 42" [] []
+ 3: R_CURLY@128..129 "}" [] []
+ 4: HTML_ELEMENT_LIST@129..230
+ 0: HTML_ELEMENT@129..162
+ 0: HTML_OPENING_ELEMENT@129..136
+ 0: L_ANGLE@129..134 "<" [Newline("\n"), Whitespace(" \t")] []
+ 1: HTML_TAG_NAME@134..135
+ 0: HTML_LITERAL@134..135 "p" [] []
+ 2: HTML_ATTRIBUTE_LIST@135..135
+ 3: R_ANGLE@135..136 ">" [] []
+ 1: HTML_ELEMENT_LIST@136..158
+ 0: HTML_CONTENT@136..158
+ 0: HTML_LITERAL@136..158 "what was the question?" [] []
+ 2: HTML_CLOSING_ELEMENT@158..162
+ 0: L_ANGLE@158..159 "<" [] []
+ 1: SLASH@159..160 "/" [] []
+ 2: HTML_TAG_NAME@160..161
+ 0: HTML_LITERAL@160..161 "p" [] []
+ 3: R_ANGLE@161..162 ">" [] []
+ 1: SVELTE_IF_BLOCK@162..230
+ 0: SVELTE_IF_OPENING_BLOCK@162..220
+ 0: SV_CURLY_HASH@162..168 "{#" [Newline("\n"), Whitespace(" \t")] []
+ 1: IF_KW@168..170 "if" [] []
+ 2: HTML_TEXT_EXPRESSION@170..184
+ 0: HTML_LITERAL@170..184 " answer === 42" [] []
+ 3: R_CURLY@184..185 "}" [] []
+ 4: HTML_ELEMENT_LIST@185..220
+ 0: HTML_ELEMENT@185..220
+ 0: HTML_OPENING_ELEMENT@185..194
+ 0: L_ANGLE@185..192 "<" [Newline("\n"), Whitespace(" \t")] []
+ 1: HTML_TAG_NAME@192..193
+ 0: HTML_LITERAL@192..193 "p" [] []
+ 2: HTML_ATTRIBUTE_LIST@193..193
+ 3: R_ANGLE@193..194 ">" [] []
+ 1: HTML_ELEMENT_LIST@194..216
+ 0: HTML_CONTENT@194..216
+ 0: HTML_LITERAL@194..216 "what was the question?" [] []
+ 2: HTML_CLOSING_ELEMENT@216..220
+ 0: L_ANGLE@216..217 "<" [] []
+ 1: SLASH@217..218 "/" [] []
+ 2: HTML_TAG_NAME@218..219
+ 0: HTML_LITERAL@218..219 "p" [] []
+ 3: R_ANGLE@219..220 ">" [] []
+ 1: SVELTE_ELSE_IF_CLAUSE_LIST@220..220
+ 2: (empty)
+ 3: SVELTE_IF_CLOSING_BLOCK@220..230
+ 0: SV_CURLY_SLASH@220..227 "{/" [Newline("\n"), Whitespace(" ")] []
+ 1: IF_KW@227..229 "if" [] []
+ 2: R_CURLY@229..230 "}" [] []
+ 1: SVELTE_ELSE_IF_CLAUSE_LIST@230..230
+ 2: (empty)
+ 3: SVELTE_IF_CLOSING_BLOCK@230..238
+ 0: SV_CURLY_SLASH@230..235 "{/" [Newline("\n"), Whitespace(" ")] []
+ 1: IF_KW@235..237 "if" [] []
+ 2: R_CURLY@237..238 "}" [] []
+ 1: SVELTE_ELSE_IF_CLAUSE_LIST@238..238
+ 2: (empty)
+ 3: SVELTE_IF_CLOSING_BLOCK@238..244
+ 0: SV_CURLY_SLASH@238..241 "{/" [Newline("\n")] []
+ 1: IF_KW@241..243 "if" [] []
+ 2: R_CURLY@243..244 "}" [] []
+ 4: EOF@244..245 "" [Newline("\n")] []
+
+```
diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/if_else.svelte b/crates/biome_html_parser/tests/html_specs/ok/svelte/if_else.svelte
new file mode 100644
index 000000000000..5a2d87432148
--- /dev/null
+++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/if_else.svelte
@@ -0,0 +1,6 @@
+
+{#if porridge.temperature > 100}
+ too hot!
+{:else}
+ too cold!
+{/if}
diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/if_else.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/if_else.svelte.snap
new file mode 100644
index 000000000000..463d5554994b
--- /dev/null
+++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/if_else.svelte.snap
@@ -0,0 +1,162 @@
+---
+source: crates/biome_html_parser/tests/spec_test.rs
+expression: snapshot
+---
+## Input
+
+```svelte
+
+{#if porridge.temperature > 100}
+ too hot!
+{:else}
+ too cold!
+{/if}
+
+```
+
+
+## AST
+
+```
+HtmlRoot {
+ bom_token: missing (optional),
+ frontmatter: missing (optional),
+ directive: missing (optional),
+ html: HtmlElementList [
+ SvelteIfBlock {
+ opening_block: SvelteIfOpeningBlock {
+ sv_curly_hash_token: SV_CURLY_HASH@0..21 "{#" [Comments(""), Newline("\n")] [],
+ if_token: IF_KW@21..23 "if" [] [],
+ expression: HtmlTextExpression {
+ html_literal_token: HTML_LITERAL@23..50 " porridge.temperature > 100" [] [],
+ },
+ r_curly_token: R_CURLY@50..51 "}" [] [],
+ children: HtmlElementList [
+ HtmlElement {
+ opening_element: HtmlOpeningElement {
+ l_angle_token: L_ANGLE@51..54 "<" [Newline("\n"), Whitespace("\t")] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@54..55 "p" [] [],
+ },
+ attributes: HtmlAttributeList [],
+ r_angle_token: R_ANGLE@55..56 ">" [] [],
+ },
+ children: HtmlElementList [
+ HtmlContent {
+ value_token: HTML_LITERAL@56..64 "too hot!" [] [],
+ },
+ ],
+ closing_element: HtmlClosingElement {
+ l_angle_token: L_ANGLE@64..65 "<" [] [],
+ slash_token: SLASH@65..66 "/" [] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@66..67 "p" [] [],
+ },
+ r_angle_token: R_ANGLE@67..68 ">" [] [],
+ },
+ },
+ ],
+ },
+ else_if_clauses: SvelteElseIfClauseList [],
+ else_clause: SvelteElseClause {
+ sv_curly_colon_token: SV_CURLY_COLON@68..71 "{:" [Newline("\n")] [],
+ else_token: ELSE_KW@71..75 "else" [] [],
+ r_curly_token: R_CURLY@75..76 "}" [] [],
+ children: HtmlElementList [
+ HtmlElement {
+ opening_element: HtmlOpeningElement {
+ l_angle_token: L_ANGLE@76..79 "<" [Newline("\n"), Whitespace("\t")] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@79..80 "p" [] [],
+ },
+ attributes: HtmlAttributeList [],
+ r_angle_token: R_ANGLE@80..81 ">" [] [],
+ },
+ children: HtmlElementList [
+ HtmlContent {
+ value_token: HTML_LITERAL@81..90 "too cold!" [] [],
+ },
+ ],
+ closing_element: HtmlClosingElement {
+ l_angle_token: L_ANGLE@90..91 "<" [] [],
+ slash_token: SLASH@91..92 "/" [] [],
+ name: HtmlTagName {
+ value_token: HTML_LITERAL@92..93 "p" [] [],
+ },
+ r_angle_token: R_ANGLE@93..94 ">" [] [],
+ },
+ },
+ ],
+ },
+ closing_block: SvelteIfClosingBlock {
+ sv_curly_slash_token: SV_CURLY_SLASH@94..97 "{/" [Newline("\n")] [],
+ if_token: IF_KW@97..99 "if" [] [],
+ r_curly_token: R_CURLY@99..100 "}" [] [],
+ },
+ },
+ ],
+ eof_token: EOF@100..101 "" [Newline("\n")] [],
+}
+```
+
+## CST
+
+```
+0: HTML_ROOT@0..101
+ 0: (empty)
+ 1: (empty)
+ 2: (empty)
+ 3: HTML_ELEMENT_LIST@0..100
+ 0: SVELTE_IF_BLOCK@0..100
+ 0: SVELTE_IF_OPENING_BLOCK@0..68
+ 0: SV_CURLY_HASH@0..21 "{#" [Comments(""), Newline("\n")] []
+ 1: IF_KW@21..23 "if" [] []
+ 2: HTML_TEXT_EXPRESSION@23..50
+ 0: HTML_LITERAL@23..50 " porridge.temperature > 100" [] []
+ 3: R_CURLY@50..51 "}" [] []
+ 4: HTML_ELEMENT_LIST@51..68
+ 0: HTML_ELEMENT@51..68
+ 0: HTML_OPENING_ELEMENT@51..56
+ 0: L_ANGLE@51..54 "<" [Newline("\n"), Whitespace("\t")] []
+ 1: HTML_TAG_NAME@54..55
+ 0: HTML_LITERAL@54..55 "p" [] []
+ 2: HTML_ATTRIBUTE_LIST@55..55
+ 3: R_ANGLE@55..56 ">" [] []
+ 1: HTML_ELEMENT_LIST@56..64
+ 0: HTML_CONTENT@56..64
+ 0: HTML_LITERAL@56..64 "too hot!" [] []
+ 2: HTML_CLOSING_ELEMENT@64..68
+ 0: L_ANGLE@64..65 "<" [] []
+ 1: SLASH@65..66 "/" [] []
+ 2: HTML_TAG_NAME@66..67
+ 0: HTML_LITERAL@66..67 "p" [] []
+ 3: R_ANGLE@67..68 ">" [] []
+ 1: SVELTE_ELSE_IF_CLAUSE_LIST@68..68
+ 2: SVELTE_ELSE_CLAUSE@68..94
+ 0: SV_CURLY_COLON@68..71 "{:" [Newline("\n")] []
+ 1: ELSE_KW@71..75 "else" [] []
+ 2: R_CURLY@75..76 "}" [] []
+ 3: HTML_ELEMENT_LIST@76..94
+ 0: HTML_ELEMENT@76..94
+ 0: HTML_OPENING_ELEMENT@76..81
+ 0: L_ANGLE@76..79 "<" [Newline("\n"), Whitespace("\t")] []
+ 1: HTML_TAG_NAME@79..80
+ 0: HTML_LITERAL@79..80 "p" [] []
+ 2: HTML_ATTRIBUTE_LIST@80..80
+ 3: R_ANGLE@80..81 ">" [] []
+ 1: HTML_ELEMENT_LIST@81..90
+ 0: HTML_CONTENT@81..90
+ 0: HTML_LITERAL@81..90 "too cold!" [] []
+ 2: HTML_CLOSING_ELEMENT@90..94
+ 0: L_ANGLE@90..91 "<" [] []
+ 1: SLASH@91..92 "/" [] []
+ 2: HTML_TAG_NAME@92..93
+ 0: HTML_LITERAL@92..93 "p" [] []
+ 3: R_ANGLE@93..94 ">" [] []
+ 3: SVELTE_IF_CLOSING_BLOCK@94..100
+ 0: SV_CURLY_SLASH@94..97 "{/" [Newline("\n")] []
+ 1: IF_KW@97..99 "if" [] []
+ 2: R_CURLY@99..100 "}" [] []
+ 4: EOF@100..101 "" [Newline("\n")] []
+
+```
diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/if_else_if_else.svelte b/crates/biome_html_parser/tests/html_specs/ok/svelte/if_else_if_else.svelte
new file mode 100644
index 000000000000..0bb196f22e1f
--- /dev/null
+++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/if_else_if_else.svelte
@@ -0,0 +1,8 @@
+
+{#if porridge.temperature > 100}
+ too hot!
+{:else if 80 > porridge.temperature}
+ too cold!
+{:else}
+ just right!
+{/if}
diff --git a/crates/biome_html_parser/tests/html_specs/ok/svelte/if_else_if_else.svelte.snap b/crates/biome_html_parser/tests/html_specs/ok/svelte/if_else_if_else.svelte.snap
new file mode 100644
index 000000000000..8da67f3fa672
--- /dev/null
+++ b/crates/biome_html_parser/tests/html_specs/ok/svelte/if_else_if_else.svelte.snap
@@ -0,0 +1,223 @@
+---
+source: crates/biome_html_parser/tests/spec_test.rs
+expression: snapshot
+---
+## Input
+
+```svelte
+
+{#if porridge.temperature > 100}
+ too hot!
+{:else if 80 > porridge.temperature}
+ too cold!
+{:else}
+ just right!
+{/if}
+
+```
+
+
+## AST
+
+```
+HtmlRoot {
+ bom_token: missing (optional),
+ frontmatter: missing (optional),
+ directive: missing (optional),
+ html: HtmlElementList [
+ SvelteIfBlock {
+ opening_block: SvelteIfOpeningBlock {
+ sv_curly_hash_token: SV_CURLY_HASH@0..31 "{#" [Comments("