diff --git a/.changeset/happy-cats-drive.md b/.changeset/happy-cats-drive.md new file mode 100644 index 000000000000..8bccf9850ebd --- /dev/null +++ b/.changeset/happy-cats-drive.md @@ -0,0 +1,5 @@ +--- +"@biomejs/biome": patch +--- + +Fixed [#8605](https://github.com/biomejs/biome/issues/8605): Text expressions in some template languages (`{{ expr }}` or `{ expr }`) at the top level of an HTML document no longer causes panicking. diff --git a/crates/biome_html_factory/src/generated/node_factory.rs b/crates/biome_html_factory/src/generated/node_factory.rs index 84c558f2a850..65e856d2775a 100644 --- a/crates/biome_html_factory/src/generated/node_factory.rs +++ b/crates/biome_html_factory/src/generated/node_factory.rs @@ -66,6 +66,20 @@ impl HtmlAttributeBuilder { )) } } +pub fn html_attribute_double_text_expression( + l_double_curly_token: SyntaxToken, + expression: HtmlTextExpression, + r_double_curly_token: SyntaxToken, +) -> HtmlAttributeDoubleTextExpression { + HtmlAttributeDoubleTextExpression::unwrap_cast(SyntaxNode::new_detached( + HtmlSyntaxKind::HTML_ATTRIBUTE_DOUBLE_TEXT_EXPRESSION, + [ + Some(SyntaxElement::Token(l_double_curly_token)), + Some(SyntaxElement::Node(expression.into_syntax())), + Some(SyntaxElement::Token(r_double_curly_token)), + ], + )) +} pub fn html_attribute_initializer_clause( eq_token: SyntaxToken, value: AnyHtmlAttributeInitializer, @@ -84,6 +98,20 @@ pub fn html_attribute_name(value_token: SyntaxToken) -> HtmlAttributeName { [Some(SyntaxElement::Token(value_token))], )) } +pub fn html_attribute_single_text_expression( + l_curly_token: SyntaxToken, + expression: HtmlTextExpression, + r_curly_token: SyntaxToken, +) -> HtmlAttributeSingleTextExpression { + HtmlAttributeSingleTextExpression::unwrap_cast(SyntaxNode::new_detached( + HtmlSyntaxKind::HTML_ATTRIBUTE_SINGLE_TEXT_EXPRESSION, + [ + Some(SyntaxElement::Token(l_curly_token)), + Some(SyntaxElement::Node(expression.into_syntax())), + Some(SyntaxElement::Token(r_curly_token)), + ], + )) +} pub fn html_cdata_section( cdata_start_token: SyntaxToken, content_token: SyntaxToken, diff --git a/crates/biome_html_factory/src/generated/syntax_factory.rs b/crates/biome_html_factory/src/generated/syntax_factory.rs index efe1463d5f9a..73a28cc20f9b 100644 --- a/crates/biome_html_factory/src/generated/syntax_factory.rs +++ b/crates/biome_html_factory/src/generated/syntax_factory.rs @@ -102,6 +102,39 @@ impl SyntaxFactory for HtmlSyntaxFactory { } slots.into_node(HTML_ATTRIBUTE, children) } + HTML_ATTRIBUTE_DOUBLE_TEXT_EXPRESSION => { + 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 + && HtmlTextExpression::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T!["}}"] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + HTML_ATTRIBUTE_DOUBLE_TEXT_EXPRESSION.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(HTML_ATTRIBUTE_DOUBLE_TEXT_EXPRESSION, children) + } HTML_ATTRIBUTE_INITIALIZER_CLAUSE => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<2usize> = RawNodeSlots::default(); @@ -147,6 +180,39 @@ impl SyntaxFactory for HtmlSyntaxFactory { } slots.into_node(HTML_ATTRIBUTE_NAME, children) } + HTML_ATTRIBUTE_SINGLE_TEXT_EXPRESSION => { + 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 + && HtmlTextExpression::can_cast(element.kind()) + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if let Some(element) = ¤t_element + && element.kind() == T!['}'] + { + slots.mark_present(); + current_element = elements.next(); + } + slots.next_slot(); + if current_element.is_some() { + return RawSyntaxNode::new( + HTML_ATTRIBUTE_SINGLE_TEXT_EXPRESSION.to_bogus(), + children.into_iter().map(Some), + ); + } + slots.into_node(HTML_ATTRIBUTE_SINGLE_TEXT_EXPRESSION, children) + } HTML_CDATA_SECTION => { let mut elements = (&children).into_iter(); let mut slots: RawNodeSlots<3usize> = RawNodeSlots::default(); diff --git a/crates/biome_html_formatter/src/generated.rs b/crates/biome_html_formatter/src/generated.rs index 196ebbe7f7b7..6245f837066b 100644 --- a/crates/biome_html_formatter/src/generated.rs +++ b/crates/biome_html_formatter/src/generated.rs @@ -120,6 +120,19 @@ impl IntoFormat for biome_html_syntax::HtmlAttribute { ) } } +impl FormatRule < biome_html_syntax :: HtmlAttributeDoubleTextExpression > for crate :: html :: auxiliary :: attribute_double_text_expression :: FormatHtmlAttributeDoubleTextExpression { type Context = HtmlFormatContext ; # [inline (always)] fn fmt (& self , node : & biome_html_syntax :: HtmlAttributeDoubleTextExpression , f : & mut HtmlFormatter) -> FormatResult < () > { FormatNodeRule :: < biome_html_syntax :: HtmlAttributeDoubleTextExpression > :: fmt (self , node , f) } } +impl AsFormat for biome_html_syntax::HtmlAttributeDoubleTextExpression { + type Format < 'a > = FormatRefWithRule < 'a , biome_html_syntax :: HtmlAttributeDoubleTextExpression , crate :: html :: auxiliary :: attribute_double_text_expression :: FormatHtmlAttributeDoubleTextExpression > ; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule :: new (self , crate :: html :: auxiliary :: attribute_double_text_expression :: FormatHtmlAttributeDoubleTextExpression :: default ()) + } +} +impl IntoFormat for biome_html_syntax::HtmlAttributeDoubleTextExpression { + type Format = FormatOwnedWithRule < biome_html_syntax :: HtmlAttributeDoubleTextExpression , crate :: html :: auxiliary :: attribute_double_text_expression :: FormatHtmlAttributeDoubleTextExpression > ; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule :: new (self , crate :: html :: auxiliary :: attribute_double_text_expression :: FormatHtmlAttributeDoubleTextExpression :: default ()) + } +} impl FormatRule for crate::html::auxiliary::attribute_initializer_clause::FormatHtmlAttributeInitializerClause { @@ -190,6 +203,19 @@ impl IntoFormat for biome_html_syntax::HtmlAttributeName { ) } } +impl FormatRule < biome_html_syntax :: HtmlAttributeSingleTextExpression > for crate :: html :: auxiliary :: attribute_single_text_expression :: FormatHtmlAttributeSingleTextExpression { type Context = HtmlFormatContext ; # [inline (always)] fn fmt (& self , node : & biome_html_syntax :: HtmlAttributeSingleTextExpression , f : & mut HtmlFormatter) -> FormatResult < () > { FormatNodeRule :: < biome_html_syntax :: HtmlAttributeSingleTextExpression > :: fmt (self , node , f) } } +impl AsFormat for biome_html_syntax::HtmlAttributeSingleTextExpression { + type Format < 'a > = FormatRefWithRule < 'a , biome_html_syntax :: HtmlAttributeSingleTextExpression , crate :: html :: auxiliary :: attribute_single_text_expression :: FormatHtmlAttributeSingleTextExpression > ; + fn format(&self) -> Self::Format<'_> { + FormatRefWithRule :: new (self , crate :: html :: auxiliary :: attribute_single_text_expression :: FormatHtmlAttributeSingleTextExpression :: default ()) + } +} +impl IntoFormat for biome_html_syntax::HtmlAttributeSingleTextExpression { + type Format = FormatOwnedWithRule < biome_html_syntax :: HtmlAttributeSingleTextExpression , crate :: html :: auxiliary :: attribute_single_text_expression :: FormatHtmlAttributeSingleTextExpression > ; + fn into_format(self) -> Self::Format { + FormatOwnedWithRule :: new (self , crate :: html :: auxiliary :: attribute_single_text_expression :: FormatHtmlAttributeSingleTextExpression :: default ()) + } +} impl FormatRule for crate::html::auxiliary::cdata_section::FormatHtmlCdataSection { diff --git a/crates/biome_html_formatter/src/html/any/attribute.rs b/crates/biome_html_formatter/src/html/any/attribute.rs index 561f0dcb2765..a807c7c59488 100644 --- a/crates/biome_html_formatter/src/html/any/attribute.rs +++ b/crates/biome_html_formatter/src/html/any/attribute.rs @@ -10,9 +10,9 @@ impl FormatRule for FormatAnyHtmlAttribute { match node { AnyHtmlAttribute::AnyVueDirective(node) => node.format().fmt(f), AnyHtmlAttribute::HtmlAttribute(node) => node.format().fmt(f), + AnyHtmlAttribute::HtmlAttributeDoubleTextExpression(node) => node.format().fmt(f), + AnyHtmlAttribute::HtmlAttributeSingleTextExpression(node) => node.format().fmt(f), AnyHtmlAttribute::HtmlBogusAttribute(node) => node.format().fmt(f), - AnyHtmlAttribute::HtmlDoubleTextExpression(node) => node.format().fmt(f), - AnyHtmlAttribute::HtmlSingleTextExpression(node) => node.format().fmt(f), AnyHtmlAttribute::SvelteAttachAttribute(node) => node.format().fmt(f), } } diff --git a/crates/biome_html_formatter/src/html/any/attribute_initializer.rs b/crates/biome_html_formatter/src/html/any/attribute_initializer.rs index 79709c0a9cbe..9c163f33bebc 100644 --- a/crates/biome_html_formatter/src/html/any/attribute_initializer.rs +++ b/crates/biome_html_formatter/src/html/any/attribute_initializer.rs @@ -8,7 +8,9 @@ impl FormatRule for FormatAnyHtmlAttributeInitializ type Context = HtmlFormatContext; fn fmt(&self, node: &AnyHtmlAttributeInitializer, f: &mut HtmlFormatter) -> FormatResult<()> { match node { - AnyHtmlAttributeInitializer::HtmlSingleTextExpression(node) => node.format().fmt(f), + AnyHtmlAttributeInitializer::HtmlAttributeSingleTextExpression(node) => { + node.format().fmt(f) + } AnyHtmlAttributeInitializer::HtmlString(node) => node.format().fmt(f), } } diff --git a/crates/biome_html_formatter/src/html/auxiliary/attribute_double_text_expression.rs b/crates/biome_html_formatter/src/html/auxiliary/attribute_double_text_expression.rs new file mode 100644 index 000000000000..c8d8b90faded --- /dev/null +++ b/crates/biome_html_formatter/src/html/auxiliary/attribute_double_text_expression.rs @@ -0,0 +1,33 @@ +use crate::prelude::*; +use biome_formatter::write; +use biome_html_syntax::{ + HtmlAttributeDoubleTextExpression, HtmlAttributeDoubleTextExpressionFields, +}; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatHtmlAttributeDoubleTextExpression; + +impl FormatNodeRule for FormatHtmlAttributeDoubleTextExpression { + fn fmt_fields( + &self, + node: &HtmlAttributeDoubleTextExpression, + f: &mut HtmlFormatter, + ) -> FormatResult<()> { + let HtmlAttributeDoubleTextExpressionFields { + l_double_curly_token, + expression, + r_double_curly_token, + } = node.as_fields(); + + write!( + f, + [ + l_double_curly_token.format(), + space(), + expression.format(), + space(), + r_double_curly_token.format(), + ] + ) + } +} diff --git a/crates/biome_html_formatter/src/html/auxiliary/attribute_single_text_expression.rs b/crates/biome_html_formatter/src/html/auxiliary/attribute_single_text_expression.rs new file mode 100644 index 000000000000..b901919845b1 --- /dev/null +++ b/crates/biome_html_formatter/src/html/auxiliary/attribute_single_text_expression.rs @@ -0,0 +1,31 @@ +use crate::prelude::*; +use biome_formatter::write; +use biome_html_syntax::{ + HtmlAttributeSingleTextExpression, HtmlAttributeSingleTextExpressionFields, +}; + +#[derive(Debug, Clone, Default)] +pub(crate) struct FormatHtmlAttributeSingleTextExpression; + +impl FormatNodeRule for FormatHtmlAttributeSingleTextExpression { + fn fmt_fields( + &self, + node: &HtmlAttributeSingleTextExpression, + f: &mut HtmlFormatter, + ) -> FormatResult<()> { + let HtmlAttributeSingleTextExpressionFields { + l_curly_token, + expression, + r_curly_token, + } = node.as_fields(); + + write!( + f, + [ + l_curly_token.format(), + expression.format(), + r_curly_token.format() + ] + ) + } +} diff --git a/crates/biome_html_formatter/src/html/auxiliary/mod.rs b/crates/biome_html_formatter/src/html/auxiliary/mod.rs index 260ed313e467..b955937453f8 100644 --- a/crates/biome_html_formatter/src/html/auxiliary/mod.rs +++ b/crates/biome_html_formatter/src/html/auxiliary/mod.rs @@ -1,8 +1,10 @@ //! This is a generated file. Don't modify it by hand! Run 'cargo codegen formatter' to re-generate the file. pub(crate) mod attribute; +pub(crate) mod attribute_double_text_expression; pub(crate) mod attribute_initializer_clause; pub(crate) mod attribute_name; +pub(crate) mod attribute_single_text_expression; pub(crate) mod cdata_section; pub(crate) mod closing_element; pub(crate) mod content; diff --git a/crates/biome_html_formatter/src/html/lists/attribute_list.rs b/crates/biome_html_formatter/src/html/lists/attribute_list.rs index 611abde8975a..321867123e58 100644 --- a/crates/biome_html_formatter/src/html/lists/attribute_list.rs +++ b/crates/biome_html_formatter/src/html/lists/attribute_list.rs @@ -60,10 +60,10 @@ impl FormatRule for FormatHtmlAttributeList { tag_name: self.tag_name.clone(), }) .fmt(f), - AnyHtmlAttribute::HtmlDoubleTextExpression(attr) => { + AnyHtmlAttribute::HtmlAttributeDoubleTextExpression(attr) => { attr.format().fmt(f) } - AnyHtmlAttribute::HtmlSingleTextExpression(attr) => { + AnyHtmlAttribute::HtmlAttributeSingleTextExpression(attr) => { attr.format().fmt(f) } AnyHtmlAttribute::HtmlBogusAttribute(attr) => { 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 76a19aa8e46c..ce70f153b50b 100644 --- a/crates/biome_html_formatter/src/html/lists/element_list.rs +++ b/crates/biome_html_formatter/src/html/lists/element_list.rs @@ -245,6 +245,9 @@ impl FormatHtmlElementList { // Some(WordSeparator::Lines(2)) } + // Don't add a newline before a bogus element as it may break the layout + Some(HtmlChild::NonText(AnyHtmlElement::HtmlBogusElement(_))) => None, + // Last word or last word before an element without any whitespace in between Some(HtmlChild::NonText(next_child)) => Some(WordSeparator::EndOfText { is_soft_line_break: !matches!( diff --git a/crates/biome_html_formatter/src/utils/children.rs b/crates/biome_html_formatter/src/utils/children.rs index 3c878191fd3c..975a3f506018 100644 --- a/crates/biome_html_formatter/src/utils/children.rs +++ b/crates/biome_html_formatter/src/utils/children.rs @@ -398,14 +398,16 @@ where } // Check for trailing whitespace, and preserve it if - // - its embedded expression content - // - its an element + // - it's embedded expression content + // - it's an element + // - it's a bogus element // This preserves spaces between expressions/elements and following text content. if matches!( &child, AnyHtmlElement::AnyHtmlContent(_) | AnyHtmlElement::HtmlElement(_) | AnyHtmlElement::HtmlSelfClosingElement(_) + | AnyHtmlElement::HtmlBogusElement(_) ) && let Some(last_token) = child.syntax().last_token() && last_token.has_trailing_whitespace() { diff --git a/crates/biome_html_formatter/tests/specs/html/interpolation/interpolation.html.snap b/crates/biome_html_formatter/tests/specs/html/interpolation/interpolation.html.snap index 4fdc257476b4..22ae535f571d 100644 --- a/crates/biome_html_formatter/tests/specs/html/interpolation/interpolation.html.snap +++ b/crates/biome_html_formatter/tests/specs/html/interpolation/interpolation.html.snap @@ -31,9 +31,7 @@ Self close void elements: never ----- ```html -
- {{ $interpolation }} -
+
{{ $interpolation }}
``` ## Output 1 @@ -51,7 +49,5 @@ Self close void elements: never ----- ```html -
- {{ $interpolation }} -
+
{{ $interpolation }}
``` diff --git a/crates/biome_html_formatter/tests/specs/prettier/html/interpolation/example.html.snap b/crates/biome_html_formatter/tests/specs/prettier/html/interpolation/example.html.snap index 123a226c2353..e93e5aba267c 100644 --- a/crates/biome_html_formatter/tests/specs/prettier/html/interpolation/example.html.snap +++ b/crates/biome_html_formatter/tests/specs/prettier/html/interpolation/example.html.snap @@ -40,16 +40,15 @@ x => { ```diff --- Prettier +++ Biome -@@ -1,8 +1,26 @@ -- --
+@@ -1,8 +1,31 @@ + +
- Fuga magnam facilis. Voluptatem quaerat porro.{{ x => { const hello = 'world' - return hello; } }} Magni consectetur in et molestias neque esse voluptatibus - voluptas. {{ some_variable }} Eum quia nihil nulla esse. Dolorem asperiores - vero est error {{ preserve invalid interpolation }} reprehenderit voluptates - minus {{console.log( short_interpolation )}} nemo. --
-+
Fuga magnam facilis. Voluptatem quaerat porro.{{ ++ Fuga magnam facilis. Voluptatem quaerat porro.{{ + + +x => { @@ -59,14 +58,16 @@ x => { + + + -+}} Magni consectetur in et molestias neque esse voluptatibus voluptas. {{ ++}} Magni consectetur in et molestias neque esse voluptatibus voluptas. ++ {{ + + + some_variable + + + -+}} Eum quia nihil nulla esse. Dolorem asperiores vero est error {{ ++}} Eum quia nihil nulla esse. Dolorem asperiores vero est error ++ {{ + + preserve + @@ -74,13 +75,16 @@ x => { + + interpolation + -+}} reprehenderit voluptates minus {{console.log( short_interpolation )}} nemo.
++}} reprehenderit voluptates minus {{console.log( short_interpolation )}} nemo. +
``` # Output ```html -
Fuga magnam facilis. Voluptatem quaerat porro.{{ + +
+ Fuga magnam facilis. Voluptatem quaerat porro.{{ x => { @@ -90,14 +94,16 @@ x => { -}} Magni consectetur in et molestias neque esse voluptatibus voluptas. {{ +}} Magni consectetur in et molestias neque esse voluptatibus voluptas. + {{ some_variable -}} Eum quia nihil nulla esse. Dolorem asperiores vero est error {{ +}} Eum quia nihil nulla esse. Dolorem asperiores vero est error + {{ preserve @@ -105,7 +111,8 @@ x => { interpolation -}} reprehenderit voluptates minus {{console.log( short_interpolation )}} nemo.
+}} reprehenderit voluptates minus {{console.log( short_interpolation )}} nemo. +
``` # Errors @@ -174,9 +181,3 @@ example.html:26:35 parse ━━━━━━━━━━━━━━━━━━ ``` - -# Lines exceeding max width of 80 characters -``` - 1:
Fuga magnam facilis. Voluptatem quaerat porro.{{ - 26: }} reprehenderit voluptates minus {{console.log( short_interpolation )}} nemo.
-``` diff --git a/crates/biome_html_formatter/tests/specs/prettier/vue/html-vue/hello-world.html.snap b/crates/biome_html_formatter/tests/specs/prettier/vue/html-vue/hello-world.html.snap index a4dfb188c87c..2b15625d878f 100644 --- a/crates/biome_html_formatter/tests/specs/prettier/vue/html-vue/hello-world.html.snap +++ b/crates/biome_html_formatter/tests/specs/prettier/vue/html-vue/hello-world.html.snap @@ -36,21 +36,20 @@ info: vue/html-vue/hello-world.html ```diff --- Prettier +++ Biome -@@ -1,5 +1,5 @@ +@@ -1,22 +1,20 @@ - + My first Vue app -@@ -7,16 +7,16 @@ + -
+-
- {{ message }} -
-+ {{ message }} -+
++
{{ message }}
-
- {{ message }} -
+
{{ message }}