diff --git a/.changeset/evil-fans-live.md b/.changeset/evil-fans-live.md new file mode 100644 index 000000000000..c30d0bddb8f2 --- /dev/null +++ b/.changeset/evil-fans-live.md @@ -0,0 +1,10 @@ +--- +"@biomejs/biome": patch +--- + +Fixed [#8584](https://github.com/biomejs/biome/issues/8584): The HTML formatter will preserve whitespace after some elements and embedded expressions, which more closely aligns with Prettier's behavior. + +```diff +-

Hello, {framework}and Svelte!

++

Hello, {framework} and Svelte!

+``` 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 3e22c7bcf863..76a19aa8e46c 100644 --- a/crates/biome_html_formatter/src/html/lists/element_list.rs +++ b/crates/biome_html_formatter/src/html/lists/element_list.rs @@ -10,7 +10,7 @@ use crate::{ HtmlChild, HtmlChildrenIterator, HtmlSpace, html_split_children, is_meaningful_html_text, }, - metadata::is_element_whitespace_sensitive_from_element, + metadata::{is_element_whitespace_sensitive_from_element, is_inline_element_from_element}, }, }; use biome_formatter::{CstFormatContext, FormatRuleWithOptions, GroupId, best_fitting, prelude::*}; @@ -505,7 +505,14 @@ impl FormatHtmlElementList { } else { let mut memoized = non_text.format().memoized(); - force_multiline = memoized.inspect(f)?.will_break(); + // Only set force_multiline based on will_break() for non-inline elements. + // Inline elements (like , , ) may have expanded variants that + // contain hard breaks, but we don't want to force the parent to multiline + // just because they could potentially break - they should flow with text. + let is_inline = is_inline_element_from_element(non_text); + if !is_inline { + force_multiline = memoized.inspect(f)?.will_break(); + } flat.write(&format_args![memoized, format_separator], f); if let Some(format_separator) = format_separator { @@ -560,7 +567,12 @@ impl FormatHtmlElementList { for child in list { match child { AnyHtmlElement::HtmlElement(_) | AnyHtmlElement::HtmlSelfClosingElement(_) => { - meta.any_tag = true + // Only consider block-level elements (non-inline) as "any_tag" for the + // purpose of forcing multiline layout. Inline elements like , , + // should flow with the text and not force line breaks. + if !is_inline_element_from_element(&child) { + meta.any_tag = true; + } } AnyHtmlElement::AnyHtmlContent(AnyHtmlContent::HtmlContent(text)) => { meta.meaningful_text = meta.meaningful_text diff --git a/crates/biome_html_formatter/src/utils/children.rs b/crates/biome_html_formatter/src/utils/children.rs index 41c052cd8831..3c878191fd3c 100644 --- a/crates/biome_html_formatter/src/utils/children.rs +++ b/crates/biome_html_formatter/src/utils/children.rs @@ -396,6 +396,31 @@ where } else { builder.entry(HtmlChild::NonText(child.clone())); } + + // Check for trailing whitespace, and preserve it if + // - its embedded expression content + // - its an element + // This preserves spaces between expressions/elements and following text content. + if matches!( + &child, + AnyHtmlElement::AnyHtmlContent(_) + | AnyHtmlElement::HtmlElement(_) + | AnyHtmlElement::HtmlSelfClosingElement(_) + ) && let Some(last_token) = child.syntax().last_token() + && last_token.has_trailing_whitespace() + { + // Check if trailing trivia contains a newline + let has_newline = last_token + .trailing_trivia() + .pieces() + .any(|piece| piece.is_newline()); + if has_newline { + builder.entry(HtmlChild::Newline); + } else { + builder.entry(HtmlChild::Whitespace); + } + } + prev_child_was_content = false; } } diff --git a/crates/biome_html_formatter/src/utils/metadata.rs b/crates/biome_html_formatter/src/utils/metadata.rs index 7774fff5cdfc..f8bdf39ae74a 100644 --- a/crates/biome_html_formatter/src/utils/metadata.rs +++ b/crates/biome_html_formatter/src/utils/metadata.rs @@ -809,6 +809,22 @@ pub(crate) fn is_inline_element(tag_name: &HtmlTagName) -> bool { .any(|tag| tag_name.text_trimmed().eq_ignore_ascii_case(tag)) } +/// Checks if an element is an inline element based on its tag name. +pub(crate) fn is_inline_element_from_element(element: &AnyHtmlElement) -> bool { + let name = match element { + AnyHtmlElement::HtmlElement(element) => { + element.opening_element().and_then(|element| element.name()) + } + AnyHtmlElement::HtmlSelfClosingElement(element) => element.name(), + _ => return false, + }; + let Ok(name) = name else { + return false; + }; + + is_inline_element(&name) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/biome_html_formatter/tests/specs/html/elements/inline/mixed-block-inline.html.snap b/crates/biome_html_formatter/tests/specs/html/elements/inline/mixed-block-inline.html.snap index 00abe2f600b3..53621cc0a0c9 100644 --- a/crates/biome_html_formatter/tests/specs/html/elements/inline/mixed-block-inline.html.snap +++ b/crates/biome_html_formatter/tests/specs/html/elements/inline/mixed-block-inline.html.snap @@ -1,7 +1,6 @@ --- source: crates/biome_formatter_test/src/snapshot_builder.rs info: elements/inline/mixed-block-inline.html -snapshot_kind: text --- # Input @@ -30,11 +29,8 @@ Self close void elements: never ```html - hello foo foo -
foo
- foo foo foo barbarbar foo - foo foo -
foo
+ hello foo foo
foo
foo foo foo + barbarbar foo foo foo
foo
foo foo ``` diff --git a/crates/biome_html_formatter/tests/specs/html/elements/inline/tags-hug-content-longer-w-attr.html.snap b/crates/biome_html_formatter/tests/specs/html/elements/inline/tags-hug-content-longer-w-attr.html.snap index ee6cf186292d..40f34953d7d4 100644 --- a/crates/biome_html_formatter/tests/specs/html/elements/inline/tags-hug-content-longer-w-attr.html.snap +++ b/crates/biome_html_formatter/tests/specs/html/elements/inline/tags-hug-content-longer-w-attr.html.snap @@ -1,7 +1,6 @@ --- source: crates/biome_formatter_test/src/snapshot_builder.rs info: elements/inline/tags-hug-content-longer-w-attr.html -snapshot_kind: text --- # Input @@ -30,7 +29,7 @@ Self close void elements: never ```html asdfasdf foo bar things much morelonger things lorem ipsum or + >asdfasdf foo bar things much more longer things lorem ipsum or something idk i put pineapple in strombolis ``` diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/each_nested.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/each_nested.svelte.snap index 0a6b4b326826..1db774eb961a 100644 --- a/crates/biome_html_formatter/tests/specs/html/svelte/each_nested.svelte.snap +++ b/crates/biome_html_formatter/tests/specs/html/svelte/each_nested.svelte.snap @@ -52,8 +52,6 @@ Self close void elements: never {/each} {#each matrix as row} - {#each row as cell} - {cell} - {/each} + {#each row as cell}{cell}{/each} {/each} ``` diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/each_with_destructuring.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/each_with_destructuring.svelte.snap index 6449115b6fe5..a9bb8158c96e 100644 --- a/crates/biome_html_formatter/tests/specs/html/svelte/each_with_destructuring.svelte.snap +++ b/crates/biome_html_formatter/tests/specs/html/svelte/each_with_destructuring.svelte.snap @@ -48,7 +48,7 @@ Self close void elements: never {/each} {#each users as { name, email }, i} -
{i}: {name}({email})
+
{i}: {name} ({email})
{/each} {#each products as [id, title]} diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/whitespace/issue-8584-w-newline.svelte b/crates/biome_html_formatter/tests/specs/html/svelte/whitespace/issue-8584-w-newline.svelte new file mode 100644 index 000000000000..ebb3a7eab8c6 --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/svelte/whitespace/issue-8584-w-newline.svelte @@ -0,0 +1,8 @@ + + +

+ Hello, {framework} + and Svelte! +

diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/whitespace/issue-8584-w-newline.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/whitespace/issue-8584-w-newline.svelte.snap new file mode 100644 index 000000000000..1d3e82ed8645 --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/svelte/whitespace/issue-8584-w-newline.svelte.snap @@ -0,0 +1,53 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: svelte/whitespace/issue-8584-w-newline.svelte +--- +# Input + +```svelte + + +

+ Hello, {framework} + and Svelte! +

+ +``` + + +============================= + +# 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 + + +

+ Hello, {framework} + and Svelte! +

+``` + + + +## Unimplemented nodes/tokens + +"\n\tconst framework = \"Astro\";\n" => 8..37 diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/whitespace/issue-8584.svelte b/crates/biome_html_formatter/tests/specs/html/svelte/whitespace/issue-8584.svelte new file mode 100644 index 000000000000..f2f4a0a78e3c --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/svelte/whitespace/issue-8584.svelte @@ -0,0 +1,5 @@ + + +

Hello, {framework} and Svelte!

diff --git a/crates/biome_html_formatter/tests/specs/html/svelte/whitespace/issue-8584.svelte.snap b/crates/biome_html_formatter/tests/specs/html/svelte/whitespace/issue-8584.svelte.snap new file mode 100644 index 000000000000..5a6a5dca609e --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/svelte/whitespace/issue-8584.svelte.snap @@ -0,0 +1,47 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: svelte/whitespace/issue-8584.svelte +--- +# Input + +```svelte + + +

Hello, {framework} and Svelte!

+ +``` + + +============================= + +# 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 + + +

Hello, {framework} and Svelte!

+``` + + + +## Unimplemented nodes/tokens + +"\n\tconst framework = \"Astro\";\n" => 8..37 diff --git a/crates/biome_html_formatter/tests/specs/html/whitespace/preserve-newline-after-element.html b/crates/biome_html_formatter/tests/specs/html/whitespace/preserve-newline-after-element.html new file mode 100644 index 000000000000..8abcb2cfd877 --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/whitespace/preserve-newline-after-element.html @@ -0,0 +1,4 @@ +

+ Foo + Bar +

diff --git a/crates/biome_html_formatter/tests/specs/html/whitespace/preserve-newline-after-element.html.snap b/crates/biome_html_formatter/tests/specs/html/whitespace/preserve-newline-after-element.html.snap new file mode 100644 index 000000000000..02f7d5e52aaf --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/whitespace/preserve-newline-after-element.html.snap @@ -0,0 +1,39 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: whitespace/preserve-newline-after-element.html +--- +# Input + +```html +

+ Foo + Bar +

+ +``` + + +============================= + +# 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 +----- + +```html +

+ Foo + Bar +

+``` diff --git a/crates/biome_html_formatter/tests/specs/html/whitespace/preserve-space-after-element.html b/crates/biome_html_formatter/tests/specs/html/whitespace/preserve-space-after-element.html new file mode 100644 index 000000000000..2fdad6193134 --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/whitespace/preserve-space-after-element.html @@ -0,0 +1,3 @@ +

+ Foo Bar +

diff --git a/crates/biome_html_formatter/tests/specs/html/whitespace/preserve-space-after-element.html.snap b/crates/biome_html_formatter/tests/specs/html/whitespace/preserve-space-after-element.html.snap new file mode 100644 index 000000000000..0d4f732e9127 --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/html/whitespace/preserve-space-after-element.html.snap @@ -0,0 +1,35 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: whitespace/preserve-space-after-element.html +--- +# Input + +```html +

+ Foo Bar +

+ +``` + + +============================= + +# 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 +----- + +```html +

Foo Bar

+``` diff --git a/crates/biome_html_formatter/tests/specs/prettier/html/aurelia/basic.html.snap b/crates/biome_html_formatter/tests/specs/prettier/html/aurelia/basic.html.snap new file mode 100644 index 000000000000..4d5c1e296ccc --- /dev/null +++ b/crates/biome_html_formatter/tests/specs/prettier/html/aurelia/basic.html.snap @@ -0,0 +1,31 @@ +--- +source: crates/biome_formatter_test/src/snapshot_builder.rs +info: html/aurelia/basic.html +--- +# Input + +```html + + +``` + + +# Prettier differences + +```diff +--- Prettier ++++ Biome +@@ -1,3 +1 @@ +- ++ +``` + +# Output + +```html + +``` diff --git a/crates/biome_html_formatter/tests/specs/prettier/html/case/case.html.snap b/crates/biome_html_formatter/tests/specs/prettier/html/case/case.html.snap index ed04cfe88396..e682cba9f41c 100644 --- a/crates/biome_html_formatter/tests/specs/prettier/html/case/case.html.snap +++ b/crates/biome_html_formatter/tests/specs/prettier/html/case/case.html.snap @@ -36,14 +36,14 @@ info: html/case/case.html -@@ -7,17 +7,13 @@ +@@ -7,17 +7,12 @@

- Hello world!
+- This is HTML5 Boilerplate. + Hello world! -+
- This is HTML5 Boilerplate. ++
This is HTML5 Boilerplate.