Skip to content

Commit 50aa415

Browse files
authored
fix(format/html): fix case where comments cause invalid html (#10363)
1 parent c8b5e2a commit 50aa415

50 files changed

Lines changed: 1233 additions & 134 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/cyan-mangos-sit.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed HTML formatting for a case where comments could cause the formatter to split up a closing tag, which would cause the resulting HTML to be syntactically invalid.
6+
7+
Input:
8+
```html
9+
<span><!-- 1
10+
--><span>a</span><!-- 2
11+
--><span>b</span><!-- 3
12+
--></span>
13+
```
14+
15+
Output:
16+
```diff
17+
<span
18+
><!-- 1
19+
- --> <span>a</span<!-- 2
20+
- --> ><span>b</span><!-- 3
21+
+ --><span>a</span><!-- 2
22+
+ --><span>b</span><!-- 3
23+
--></span
24+
>
25+
```

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_html_formatter/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ harness = false
1515
name = "html_formatter"
1616

1717
[dependencies]
18+
biome_console = { workspace = true }
1819
biome_deserialize = { workspace = true }
1920
biome_deserialize_macros = { workspace = true }
2021
biome_diagnostics_categories = { workspace = true }

crates/biome_html_formatter/src/html/auxiliary/element.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ pub(crate) struct FormatHtmlElement {
3838
/// A `>` token borrowed from the previous sibling element's closing tag.
3939
/// This is printed at the start of this element's opening tag group.
4040
borrowed_sibling_r_angle: Option<HtmlSyntaxToken>,
41+
/// Whether comments adjacent to this element are formatted by the containing child list.
42+
comments_as_children: bool,
4143
}
4244

4345
#[derive(Debug, Clone, Default)]
@@ -46,6 +48,8 @@ pub(crate) struct FormatHtmlElementOptions {
4648
pub closing_r_angle_borrowed: bool,
4749
/// A `>` token borrowed from the previous sibling element's closing tag.
4850
pub borrowed_sibling_r_angle: Option<HtmlSyntaxToken>,
51+
/// Whether comments adjacent to this element are formatted by the containing child list.
52+
pub comments_as_children: bool,
4953
}
5054

5155
impl FormatRuleWithOptions<HtmlElement> for FormatHtmlElement {
@@ -54,6 +58,7 @@ impl FormatRuleWithOptions<HtmlElement> for FormatHtmlElement {
5458
fn with_options(mut self, options: Self::Options) -> Self {
5559
self.closing_r_angle_borrowed = options.closing_r_angle_borrowed;
5660
self.borrowed_sibling_r_angle = options.borrowed_sibling_r_angle;
61+
self.comments_as_children = options.comments_as_children;
5762
self
5863
}
5964
}
@@ -72,6 +77,11 @@ impl FormatNodeRule<HtmlElement> for FormatHtmlElement {
7277
}
7378

7479
fn fmt_leading_comments(&self, node: &HtmlElement, f: &mut HtmlFormatter) -> FormatResult<()> {
80+
if self.comments_as_children {
81+
// handled by element list formatter
82+
return Ok(());
83+
}
84+
7585
// For block elements, use hard line break after leading comments even if
7686
// there's no newline in the source. This ensures proper formatting like:
7787
// <!-- comment -->
@@ -90,6 +100,11 @@ impl FormatNodeRule<HtmlElement> for FormatHtmlElement {
90100
}
91101

92102
fn fmt_trailing_comments(&self, node: &HtmlElement, f: &mut HtmlFormatter) -> FormatResult<()> {
103+
if self.comments_as_children {
104+
// handled by element list formatter
105+
return Ok(());
106+
}
107+
93108
// If there is leading whitespace before a leading comment, we need to preserve it because it's probably indentation.
94109
// See prettier test case: crates/biome_html_formatter/tests/specs/prettier/html/comments/hidden.html
95110
// The current implementation for `biome_formatter::FormatTrailingComments` actually has a ton of js specific behavior that we don't want in the html formatter.

crates/biome_html_formatter/src/html/auxiliary/self_closing_element.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,32 @@
11
use crate::{
2-
html::lists::attribute_list::FormatHtmlAttributeListOptions, prelude::*,
2+
html::lists::attribute_list::FormatHtmlAttributeListOptions,
3+
prelude::*,
34
utils::metadata::should_lowercase_html_tag,
5+
verbatim::{format_html_leading_comments, format_html_trailing_comments},
46
};
5-
use biome_formatter::write;
7+
use biome_formatter::{FormatRuleWithOptions, write};
68
use biome_html_syntax::{HtmlSelfClosingElement, HtmlSelfClosingElementFields};
79
#[derive(Debug, Clone, Default)]
8-
pub(crate) struct FormatHtmlSelfClosingElement;
10+
pub(crate) struct FormatHtmlSelfClosingElement {
11+
/// Whether comments adjacent to this element are formatted by the containing child list.
12+
comments_as_children: bool,
13+
}
14+
15+
#[derive(Debug, Clone, Default)]
16+
pub(crate) struct FormatHtmlSelfClosingElementOptions {
17+
/// Whether comments adjacent to this element are formatted by the containing child list.
18+
pub comments_as_children: bool,
19+
}
20+
21+
impl FormatRuleWithOptions<HtmlSelfClosingElement> for FormatHtmlSelfClosingElement {
22+
type Options = FormatHtmlSelfClosingElementOptions;
23+
24+
fn with_options(mut self, options: Self::Options) -> Self {
25+
self.comments_as_children = options.comments_as_children;
26+
self
27+
}
28+
}
29+
930
impl FormatNodeRule<HtmlSelfClosingElement> for FormatHtmlSelfClosingElement {
1031
fn fmt_fields(&self, node: &HtmlSelfClosingElement, f: &mut HtmlFormatter) -> FormatResult<()> {
1132
let HtmlSelfClosingElementFields {
@@ -93,4 +114,30 @@ impl FormatNodeRule<HtmlSelfClosingElement> for FormatHtmlSelfClosingElement {
93114

94115
Ok(())
95116
}
117+
118+
fn fmt_leading_comments(
119+
&self,
120+
_node: &HtmlSelfClosingElement,
121+
_f: &mut HtmlFormatter,
122+
) -> FormatResult<()> {
123+
if self.comments_as_children {
124+
// handled by element list formatter
125+
return Ok(());
126+
}
127+
128+
format_html_leading_comments(_node.syntax()).fmt(_f)
129+
}
130+
131+
fn fmt_trailing_comments(
132+
&self,
133+
_node: &HtmlSelfClosingElement,
134+
_f: &mut HtmlFormatter,
135+
) -> FormatResult<()> {
136+
if self.comments_as_children {
137+
// handled by element list formatter
138+
return Ok(());
139+
}
140+
141+
format_html_trailing_comments(_node.syntax()).fmt(_f)
142+
}
96143
}

crates/biome_html_formatter/src/html/auxiliary/single_text_expression.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ impl FormatNodeRule<HtmlSingleTextExpression> for FormatHtmlSingleTextExpression
3636
)
3737
}
3838
}
39+
40+
fn fmt_leading_comments(
41+
&self,
42+
_node: &HtmlSingleTextExpression,
43+
_f: &mut HtmlFormatter,
44+
) -> FormatResult<()> {
45+
// handled by element list formatter
46+
Ok(())
47+
}
48+
49+
fn fmt_trailing_comments(
50+
&self,
51+
_node: &HtmlSingleTextExpression,
52+
_f: &mut HtmlFormatter,
53+
) -> FormatResult<()> {
54+
// handled by element list formatter
55+
Ok(())
56+
}
3957
}
4058

4159
impl FormatRuleWithOptions<HtmlSingleTextExpression> for FormatHtmlSingleTextExpression {

crates/biome_html_formatter/src/html/lists/element_list.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,14 +182,20 @@
182182
//! ```
183183
184184
use crate::{
185-
html::auxiliary::element::{FormatHtmlElement, FormatHtmlElementOptions},
185+
html::auxiliary::{
186+
element::{FormatHtmlElement, FormatHtmlElementOptions},
187+
self_closing_element::{FormatHtmlSelfClosingElement, FormatHtmlSelfClosingElementOptions},
188+
},
186189
prelude::*,
187190
utils::{
188-
children::{HtmlChild, HtmlChildrenIterator, html_split_children},
191+
children::{
192+
DisplayHtmlChildSequence, HtmlChild, HtmlChildrenIterator, html_split_children,
193+
},
189194
css_display::CssDisplay,
190195
metadata::get_element_css_display,
191196
},
192197
};
198+
use biome_console::{ColorMode, ConsoleExt, EnvConsole, markup};
193199
use biome_formatter::{FormatRuleWithOptions, GroupId, prelude::*};
194200
use biome_formatter::{format_args, write};
195201
use biome_html_syntax::{
@@ -369,12 +375,23 @@ impl FormatHtmlElementList {
369375
// Split children into HtmlChild variants
370376
let children = html_split_children(
371377
list.iter(),
378+
list.parent::<HtmlElement>()
379+
.and_then(|p| p.opening_element().ok())
380+
.and_then(|opening| opening.r_angle_token().ok())
381+
.as_ref(),
372382
list.parent::<HtmlElement>()
373383
.and_then(|p| p.closing_element().ok())
374384
.as_ref(),
375385
f,
376386
)?;
377387

388+
#[cfg(debug_assertions)]
389+
if std::env::var("DEBUG_HTML_FORMATTER_CHILDREN").is_ok() {
390+
EnvConsole::new(ColorMode::Auto).error(markup! {
391+
{DisplayHtmlChildSequence::new(&children)}
392+
});
393+
}
394+
378395
let children_meta = self.children_meta(&children);
379396

380397
// Trim trailing new lines from children
@@ -594,6 +611,15 @@ impl FormatHtmlElementList {
594611
// HTML comment
595612
HtmlChild::Comment(comment) => {
596613
write!(f, [comment])?;
614+
615+
let next = children_iter.peek();
616+
if let Some(HtmlChild::NonText(next_non_text)) = next {
617+
// when a comment is followed by a block element, we need to add a line break.
618+
let next_css_display = get_element_css_display(next_non_text);
619+
if !next_css_display.is_externally_whitespace_sensitive(f) {
620+
write!(f, [hard_line_break()])?;
621+
}
622+
}
597623
}
598624

599625
// Whitespace between children
@@ -650,6 +676,8 @@ impl FormatHtmlElementList {
650676
&& !self.is_container_whitespace_sensitive
651677
{
652678
// If the container is not whitespace sensitive, we trim trailing whitespace
679+
} else if is_first_child && !self.is_container_whitespace_sensitive {
680+
// if the container is not whitespace sensitive, we trim leading whitespace
653681
} else {
654682
write!(f, [space()])?;
655683
}
@@ -1026,12 +1054,23 @@ fn format_element_with_borrowing(
10261054
&FormatHtmlElement::default().with_options(FormatHtmlElementOptions {
10271055
closing_r_angle_borrowed,
10281056
borrowed_sibling_r_angle: borrowed_sibling_r_angle.clone(),
1057+
comments_as_children: true,
10291058
}),
10301059
html_element,
10311060
f,
10321061
)
1062+
} else if let AnyHtmlElement::HtmlSelfClosingElement(self_closing_element) = element {
1063+
FormatNodeRule::fmt(
1064+
&FormatHtmlSelfClosingElement::default().with_options(
1065+
FormatHtmlSelfClosingElementOptions {
1066+
comments_as_children: true,
1067+
},
1068+
),
1069+
self_closing_element,
1070+
f,
1071+
)
10331072
} else {
1034-
// For other element types (self-closing, etc.), use default formatting
1073+
// For other element types, use default formatting.
10351074
write!(f, [element.format()])
10361075
}
10371076
})

crates/biome_html_formatter/src/svelte/auxiliary/await_block.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,13 @@ impl FormatNodeRule<SvelteAwaitBlock> for FormatSvelteAwaitBlock {
2020
]
2121
)
2222
}
23+
24+
fn fmt_leading_comments(
25+
&self,
26+
_node: &SvelteAwaitBlock,
27+
_f: &mut HtmlFormatter,
28+
) -> FormatResult<()> {
29+
// handled by element list formatter
30+
Ok(())
31+
}
2332
}

crates/biome_html_formatter/src/svelte/auxiliary/each_block.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,13 @@ impl FormatNodeRule<SvelteEachBlock> for FormatSvelteEachBlock {
2525

2626
write!(f, [closing_block.format()])
2727
}
28+
29+
fn fmt_leading_comments(
30+
&self,
31+
_node: &SvelteEachBlock,
32+
_f: &mut HtmlFormatter,
33+
) -> FormatResult<()> {
34+
// handled by element list formatter
35+
Ok(())
36+
}
2837
}

crates/biome_html_formatter/src/svelte/auxiliary/html_block.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,22 @@ impl FormatNodeRule<SvelteHtmlBlock> for FormatSvelteHtmlBlock {
2323
]
2424
)
2525
}
26+
27+
fn fmt_leading_comments(
28+
&self,
29+
_node: &SvelteHtmlBlock,
30+
_f: &mut HtmlFormatter,
31+
) -> FormatResult<()> {
32+
// handled by element list formatter
33+
Ok(())
34+
}
35+
36+
fn fmt_trailing_comments(
37+
&self,
38+
_node: &SvelteHtmlBlock,
39+
_f: &mut HtmlFormatter,
40+
) -> FormatResult<()> {
41+
// handled by element list formatter
42+
Ok(())
43+
}
2644
}

0 commit comments

Comments
 (0)