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