Skip to content

Commit 27177ca

Browse files
authored
perf(format/html): replace string comparisons with token set checks (#11355)
1 parent ff992a1 commit 27177ca

13 files changed

Lines changed: 588 additions & 206 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed the HTML formatter incorrectly applying native HTML element formatting to PascalCase component names such as `<Ul>` and `<Body>` in Vue, Svelte, and Astro files.
6+
7+
```diff
8+
-<Body>
9+
- <div>content</div>
10+
-</Body>
11+
+<Body><div>content</div></Body>
12+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Fixed the HTML formatter incorrectly applying SVG block formatting to unknown elements whose names matched SVG element names.
6+
7+
```diff
8+
-<foreignobject>
9+
- <div>content</div>
10+
-</foreignobject>
11+
+<foreignobject><div>content</div></foreignobject>
12+
```
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Improved performance of the HTML formatter for documents that contain many HTML-native or SVG-native tags.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Removed special HTML formatter handling for the obsolete `<listing>` element.

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

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
use crate::html::lists::element_list::{FormatHtmlElementListOptions, HtmlChildListLayout};
2-
use crate::utils::css_display::{CssDisplay, get_css_display, get_css_display_from_tag};
2+
use crate::utils::css_display::{CssDisplay, get_css_display_from_tag};
33
use crate::utils::metadata::{get_css_whitespace, get_element_css_display};
44
use crate::verbatim::{format_html_leading_comments, format_html_leading_comments_for_block};
55
use crate::{html::lists::element_list::FormatHtmlElementList, prelude::*};
66
use biome_formatter::{CstFormatContext, FormatRefWithRule, FormatRuleWithOptions, write};
77
use biome_html_syntax::{
88
AnyHtmlContent, AnyHtmlElement, AnyHtmlTagName, HtmlElement, HtmlElementFields,
99
HtmlElementList, HtmlRoot, HtmlSelfClosingElement,
10-
HtmlSyntaxKind::{self, AUDIO_KW, OBJECT_KW, TEMPLATE_KW, VIDEO_KW},
10+
HtmlSyntaxKind::{
11+
self, AUDIO_KW, BODY_KW, HEAD_KW, HTML_KW, OBJECT_KW, OL_KW, SCRIPT_KW, SELECT_KW,
12+
STYLE_KW, TEMPLATE_KW, UL_KW, VIDEO_KW,
13+
},
1114
HtmlSyntaxToken,
1215
};
1316
use biome_parser::{TokenSet, token_set};
14-
use biome_string_case::StrLikeExtension;
1517

1618
use super::{
1719
closing_element::{FormatHtmlClosingElement, FormatHtmlClosingElementOptions},
@@ -25,15 +27,17 @@ use super::{
2527
/// browser user-agent CSS.
2628
///
2729
/// See also: <https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/white-space>
28-
fn is_verbatim_tag(tag_name: &str) -> bool {
29-
tag_name.eq_ignore_ascii_case("script")
30-
|| tag_name.eq_ignore_ascii_case("style")
30+
fn is_verbatim_tag(tag_name: &AnyHtmlTagName) -> bool {
31+
matches!(tag_name.tag_name_kind(), Some(SCRIPT_KW | STYLE_KW))
3132
|| get_css_whitespace(tag_name).preserves_content()
3233
}
3334

3435
const STRUCTURAL_FALLBACK_ELEMENTS: TokenSet<HtmlSyntaxKind> =
3536
token_set!(AUDIO_KW, OBJECT_KW, VIDEO_KW);
3637

38+
const FORCE_BREAK_CHILDREN_ELEMENTS: TokenSet<HtmlSyntaxKind> =
39+
token_set!(HTML_KW, HEAD_KW, UL_KW, OL_KW, SELECT_KW);
40+
3741
#[derive(Debug, Clone, Default)]
3842
pub(crate) struct FormatHtmlElement {
3943
/// Whether the closing `>` should be borrowed by an adjacent sibling element.
@@ -94,8 +98,9 @@ impl FormatNodeRule<HtmlElement> for FormatHtmlElement {
9498
// Instead of:
9599
// <!-- comment --> <div>...</div>
96100
let css_display = node
97-
.tag_name()
98-
.map_or(CssDisplay::Block, |tag| get_css_display(&tag));
101+
.name()
102+
.as_ref()
103+
.map_or(CssDisplay::Block, get_css_display_from_tag);
99104

100105
if css_display.is_block_like() {
101106
format_html_leading_comments_for_block(node.syntax()).fmt(f)
@@ -142,12 +147,12 @@ impl FormatHtmlElement {
142147
closing_element,
143148
} = node.as_fields();
144149

145-
let should_force_break_content = should_force_break_content(node);
146-
147150
let closing_element = closing_element?;
148151
let opening_element = opening_element?;
149152
let tag_name = opening_element.name()?;
150153
let css_display = get_css_display_from_tag(&tag_name);
154+
let should_force_break_content =
155+
should_force_break_content(node, tag_name.tag_name_kind(), css_display);
151156
let is_element_internally_whitespace_sensitive =
152157
css_display.is_internally_whitespace_sensitive(f);
153158
let is_root_element_list = node
@@ -179,14 +184,7 @@ impl FormatHtmlElement {
179184
AnyHtmlElement::AnyHtmlContent(AnyHtmlContent::HtmlEmbeddedContent(_))
180185
)
181186
});
182-
let should_be_verbatim = has_embedded_content
183-
|| match tag_name {
184-
AnyHtmlTagName::HtmlComponentName(_) | AnyHtmlTagName::HtmlMemberName(_) => false,
185-
AnyHtmlTagName::HtmlTagName(tag_name) => tag_name
186-
.value_token()
187-
.as_ref()
188-
.is_ok_and(|tag_name_token| is_verbatim_tag(tag_name_token.text_trimmed())),
189-
};
187+
let should_be_verbatim = has_embedded_content || is_verbatim_tag(&tag_name);
190188

191189
let should_format_embedded_nodes = if f.context().should_delegate_fmt_embedded_nodes() {
192190
// Only delegate for supported <script> or <style> content
@@ -345,33 +343,33 @@ impl FormatHtmlElement {
345343
/// This is equivalent to Prettier's `function forceBreakChildren()`.
346344
///
347345
/// Prettier source: src/language-html/utilities/index.js:271-278
348-
fn should_force_break_children(tag_name: &str) -> bool {
349-
let tag_lower = tag_name.to_ascii_lowercase_cow();
350-
351-
// These elements always break children
352-
if matches!(tag_lower.as_ref(), "html" | "head" | "ul" | "ol" | "select") {
346+
fn should_force_break_children(
347+
tag_name_kind: Option<HtmlSyntaxKind>,
348+
css_display: CssDisplay,
349+
) -> bool {
350+
if tag_name_kind.is_some_and(|kind| FORCE_BREAK_CHILDREN_ELEMENTS.contains(kind)) {
353351
return true;
354352
}
355353

356354
// Table-related elements (except table-cell) break children
357-
let display = get_css_display(&tag_lower);
358-
display.is_table_like() && !matches!(display, CssDisplay::TableCell)
355+
css_display.is_table_like() && !matches!(css_display, CssDisplay::TableCell)
359356
}
360357

361358
/// Determines if the content of an element should be forcefully broken into multiple lines.
362359
///
363360
/// This is equivalent to Prettier's `function forceBreakContent()`.
364-
fn should_force_break_content(node: &HtmlElement) -> bool {
365-
let Some(tag_name) = node.tag_name() else {
366-
return false;
367-
};
368-
if should_force_break_children(&tag_name) {
361+
fn should_force_break_content(
362+
node: &HtmlElement,
363+
tag_name_kind: Option<HtmlSyntaxKind>,
364+
css_display: CssDisplay,
365+
) -> bool {
366+
if should_force_break_children(tag_name_kind, css_display) {
369367
return true;
370368
}
371369

372370
// prettier also considers `<script>` and `<style>` here, but we handle those elsewhere.
373371
// if its a `<body>` or if the grandchildren contain non-text nodes
374-
if !node.children().is_empty() && tag_name.eq_ignore_ascii_case("body")
372+
if !node.children().is_empty() && tag_name_kind == Some(BODY_KW)
375373
|| node.children().iter().any(|child| {
376374
if let Some(element) = child.as_html_element() {
377375
has_non_text_child(&element.children())

0 commit comments

Comments
 (0)