Skip to content

Commit

Permalink
fix(format/html): enforce whitespace sensitivity for siblings in Html…
Browse files Browse the repository at this point in the history
…ElementList (#5025)

Co-authored-by: Emanuele Stoppa <[email protected]>
  • Loading branch information
dyc3 and ematipico authored Feb 3, 2025
1 parent ea84e0c commit feda489
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 9 deletions.
41 changes: 32 additions & 9 deletions crates/biome_html_formatter/src/html/lists/element_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ use std::cell::RefCell;
use crate::{
comments::HtmlComments,
prelude::*,
utils::children::{
html_split_children, is_meaningful_html_text, HtmlChild, HtmlChildrenIterator, HtmlSpace,
utils::{
children::{
html_split_children, is_meaningful_html_text, HtmlChild, HtmlChildrenIterator,
HtmlSpace,
},
metadata::is_element_whitespace_sensitive,
},
};
use biome_formatter::{best_fitting, prelude::*, CstFormatContext, FormatRuleWithOptions};
Expand Down Expand Up @@ -171,6 +175,9 @@ impl FormatHtmlElementList {
next_child,
AnyHtmlElement::HtmlSelfClosingElement(_)
) || word.is_single_character(),
is_next_element_whitespace_sensitive: is_element_whitespace_sensitive(
f, next_child,
),
}),

Some(HtmlChild::Newline | HtmlChild::Whitespace | HtmlChild::EmptyLine) => {
Expand Down Expand Up @@ -275,15 +282,20 @@ impl FormatHtmlElementList {
let line_mode = match children_iter.peek() {
Some(HtmlChild::Word(word)) => {
// Break if the current or next element is a self closing element
// ```javascript
// ```html
// <pre className="h-screen overflow-y-scroll" />adefg
// ```
// Becomes
// ```javascript
// ```html
// <pre className="h-screen overflow-y-scroll" />
// adefg
// ```
if matches!(non_text, AnyHtmlElement::HtmlSelfClosingElement(_))
let is_current_whitespace_sensitive =
is_element_whitespace_sensitive(f, non_text);
if is_current_whitespace_sensitive {
// we can't add any whitespace if the element is whitespace sensitive
None
} else if matches!(non_text, AnyHtmlElement::HtmlSelfClosingElement(_))
&& !word.is_single_character()
{
Some(LineMode::Hard)
Expand Down Expand Up @@ -481,7 +493,10 @@ enum WordSeparator {
/// </div>
/// );
/// ```
EndOfText { is_soft_line_break: bool },
EndOfText {
is_soft_line_break: bool,
is_next_element_whitespace_sensitive: bool,
},
}

impl WordSeparator {
Expand All @@ -491,6 +506,7 @@ impl WordSeparator {
self,
WordSeparator::EndOfText {
is_soft_line_break: false,
is_next_element_whitespace_sensitive: _
}
)
}
Expand All @@ -500,16 +516,23 @@ impl Format<HtmlFormatContext> for WordSeparator {
fn fmt(&self, f: &mut Formatter<HtmlFormatContext>) -> FormatResult<()> {
match self {
WordSeparator::BetweenWords => soft_line_break_or_space().fmt(f),
WordSeparator::EndOfText { is_soft_line_break } => {
WordSeparator::EndOfText {
is_soft_line_break,
is_next_element_whitespace_sensitive,
} => {
// If the next element is whitespace sensitive, we can't insert any whitespace.
if *is_next_element_whitespace_sensitive {
return Ok(());
}
if *is_soft_line_break {
soft_line_break().fmt(f)
}
// ```javascript
// ```html
// <div>ab<br/></div>
// ```
// Becomes
//
// ```javascript
// ```html
// <div>
// ab
// <br />
Expand Down
31 changes: 31 additions & 0 deletions crates/biome_html_formatter/src/utils/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
use biome_html_syntax::AnyHtmlElement;
use biome_rowan::AstNode;

use crate::HtmlFormatter;

/// HTML tags that have an "inline" layout by default.
///
/// In HTML, The inline layout treats the element as if it were a single line of text. This means that the element does not start on a new line, and only takes up as much width as necessary.
Expand All @@ -12,3 +17,29 @@ pub const HTML_INLINE_TAGS: &[&str] = &[
// TODO: this is incomplete. derive this from the HTML spec.
"b", "i", "u", "span", "a", "strong", "em", "small", "big",
];

/// Whether an element should be considered whitespace sensitive, considering the element's tag name and the
/// formatter's whitespace sensitivity options.
pub(crate) fn is_element_whitespace_sensitive(f: &HtmlFormatter, 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 is_inline_element = if let Ok(name) = name {
let Ok(Some(tag_name)) = name.trim_trivia().map(|t| t.value_token()).transpose() else {
return false;
};
HTML_INLINE_TAGS
.iter()
.any(|tag| tag_name.text().eq_ignore_ascii_case(tag))
} else {
false
};

let sensitivity = f.options().whitespace_sensitivity();
sensitivity.is_css() && is_inline_element || sensitivity.is_strict()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Lorem ipsum<span />dolor sit amet, consectetur adipiscing elit. Sed eu diam vel
sem congue pulvinar at vitae purus. Morbi volutpat arcu massa, eu laoreet eros
feugiat ac.<b
attribute="really long for this example"
actually="so long it should break and become multiline"
>Etiam sit amet turpis blandit, volutpat magna nec, luctus justo. Nam nec
augue mauris. Nullam sit amet blandit massa, at finibus felis. Nunc ut
vestibulum nulla.</b
>Donec maximus euismod egestas. Sed tempus semper efficitur. Suspendisse maximus
ut risus vel sollicitudin. Maecenas eu bibendum lorem.<span
attribute="really long for this example"
actually="so long it should break and become multiline"
/>Sed porttitor commodo commodo. Morbi luctus consequat maximus. Vestibulum
viverra libero quis lacus euismod, ut consequat ante convallis.
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
source: crates/biome_formatter_test/src/snapshot_builder.rs
info: long-inline-elements.html
---
# Input

```html
Lorem ipsum<span />dolor sit amet, consectetur adipiscing elit. Sed eu diam vel
sem congue pulvinar at vitae purus. Morbi volutpat arcu massa, eu laoreet eros
feugiat ac.<b
attribute="really long for this example"
actually="so long it should break and become multiline"
>Etiam sit amet turpis blandit, volutpat magna nec, luctus justo. Nam nec
augue mauris. Nullam sit amet blandit massa, at finibus felis. Nunc ut
vestibulum nulla.</b
>Donec maximus euismod egestas. Sed tempus semper efficitur. Suspendisse maximus
ut risus vel sollicitudin. Maecenas eu bibendum lorem.<span
attribute="really long for this example"
actually="so long it should break and become multiline"
/>Sed porttitor commodo commodo. Morbi luctus consequat maximus. Vestibulum
viverra libero quis lacus euismod, ut consequat ante convallis.
```
=============================
# 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
-----
```html
Lorem ipsum<span />dolor sit amet, consectetur adipiscing elit. Sed eu diam vel
sem congue pulvinar at vitae purus. Morbi volutpat arcu massa, eu laoreet eros
feugiat ac.<b
attribute="really long for this example"
actually="so long it should break and become multiline"
>Etiam sit amet turpis blandit, volutpat magna nec, luctus justo. Nam nec
augue mauris. Nullam sit amet blandit massa, at finibus felis. Nunc ut
vestibulum nulla.</b
>Donec maximus euismod egestas. Sed tempus semper efficitur. Suspendisse maximus
ut risus vel sollicitudin. Maecenas eu bibendum lorem.<span
attribute="really long for this example"
actually="so long it should break and become multiline"
/>Sed porttitor commodo commodo. Morbi luctus consequat maximus. Vestibulum
viverra libero quis lacus euismod, ut consequat ante convallis.
```

0 comments on commit feda489

Please sign in to comment.