diff --git a/crates/oxc_formatter_htmlish/DIVERGENCES.md b/crates/oxc_formatter_htmlish/DIVERGENCES.md index d0b2565b25..f807ff852b 100644 --- a/crates/oxc_formatter_htmlish/DIVERGENCES.md +++ b/crates/oxc_formatter_htmlish/DIVERGENCES.md @@ -274,6 +274,36 @@ whitespace between them is kept or omitted as written, as between HTML inline el Prettier lays out every SVG element but `` as a block (prettier/prettier#17713, prettier/prettier#10121). With `htmlWhitespaceSensitivity: "ignore"` they stay blocks, as HTML inline elements do. +## svg-xml-space-preserve + +- Why: semantics (whitespace in an SVG `` under `xml:space="preserve"` renders as written; prettier/prettier#10121) +- Pin: `tests/fixtures/html/svg-xml-space-preserve.html` + +```html + + a b c + + + + a b c + + + + + + a + b + c + + +``` + +An SVG `` (or an element in one) under `xml:space="preserve"`, on it or an SVG ancestor, keeps its content as written, as a `pre` does. +Browsers render every space of it, and a line break with its indentation as spaces. +They differ beyond the element itself: Gecko applies an ancestor's `preserve` and keeps it under a `xml:space="default"`, Blink does neither, +so any `preserve` above counts. +Prettier lays the content out as blocks, adding and removing whitespace that renders. + ## dialog-out-of-flow - Why: semantics (whitespace added on both sides of a `dialog` renders between its inline neighbors) diff --git a/crates/oxc_formatter_htmlish/src/tree/build.rs b/crates/oxc_formatter_htmlish/src/tree/build.rs index a7408b9a26..358d62be01 100644 --- a/crates/oxc_formatter_htmlish/src/tree/build.rs +++ b/crates/oxc_formatter_htmlish/src/tree/build.rs @@ -384,7 +384,7 @@ impl Builder<'_> { } NodeKind::Element(el) => { self_closing = closes_at_start_tag(node); - pre_like = is_pre_like(el); + pre_like = is_pre_like(el) || self.preserves_svg_text(id, el); script_like = matches!(el.key, "script" | "style"); } _ => {} @@ -429,11 +429,31 @@ impl Builder<'_> { } } + /// An SVG ``, or an element in one, under `xml:space="preserve"` on it or an SVG ancestor: + /// its whitespace renders as written (DIVERGENCES.md#svg-xml-space-preserve). + fn preserves_svg_text(&self, id: NodeId, el: &Element<'_>) -> bool { + let source = self.tree.source; + el.namespace == Namespace::Svg + && (el.key == "text" || self.in_svg_text(id)) + && self.svg_elements(Some(id)).any(|el| { + el.attributes.iter().any(|attribute| { + self.tree.attribute_is(attribute.name.slice(source), "xml:space") + && attribute + .value + .is_some_and(|value| value.span.slice(source) == "preserve") + }) + }) + } + /// Whether an SVG element is inside a ``, whose content lays out as inline text. fn in_svg_text(&self, id: NodeId) -> bool { - std::iter::successors(self.tree.root[id].parent, |&p| self.tree.root[p].parent) + self.svg_elements(self.tree.root[id].parent).any(|el| el.key == "text") + } + + /// The SVG elements from `from` up, until one that is not. + fn svg_elements(&self, from: Option) -> impl Iterator> { + std::iter::successors(from, |&p| self.tree.root[p].parent) .map_while(|p| self.tree.element(p).filter(|el| el.namespace == Namespace::Svg)) - .any(|el| el.key == "text") } /// Whether whitespace on one side of the node is significant, before the neighbor's side is combined in: @@ -495,7 +515,7 @@ impl Builder<'_> { leading: bool, ) -> bool { let t = &self.tree; - let (item, parent) = (&t[id], &t[parent_id]); + let parent = &t[parent_id]; let text_or_interpolation = |id: NodeId| t.is_text(id) || t.is_interpolation(id); let sensitive = if text_or_interpolation(flow_item) && neighbor.is_some_and(text_or_interpolation) { @@ -507,8 +527,9 @@ impl Builder<'_> { } else if let Some(neighbor) = neighbor { self.display_of(neighbor).sides_sensitive() } else { + // An HTML `pre`'s own edges, not those of all pre-like content !(parent_id == NodeId::DOCUMENT - || item.pre_like + || t.element(id).is_some_and(is_pre_like) || parent.script_like || !parent.display.edges_sensitive()) }; @@ -619,7 +640,7 @@ fn is_whitespace_text(node: &Node<'_>) -> bool { matches!(&node.kind, NodeKind::Text(text) if text.is_whitespace_only()) } -/// CSS `white-space: pre*`. +/// An HTML element whose default style is `white-space: pre*`. fn is_pre_like(el: &Element<'_>) -> bool { el.namespace != Namespace::Svg && matches!(el.key, "pre" | "textarea" | "listing" | "plaintext" | "xmp") diff --git a/crates/oxc_formatter_htmlish/src/tree/mod.rs b/crates/oxc_formatter_htmlish/src/tree/mod.rs index 6c71a6840e..556ddc6dbe 100644 --- a/crates/oxc_formatter_htmlish/src/tree/mod.rs +++ b/crates/oxc_formatter_htmlish/src/tree/mod.rs @@ -38,7 +38,8 @@ pub struct Facts { /// No children and no end tag: the start tag closes the node (Prettier's `isSelfClosing`). /// Text-like nodes count too. pub self_closing: bool, - /// CSS `white-space: pre*`: `pre`, `textarea`, `listing`, `plaintext`, `xmp`, outside SVG. + /// CSS `white-space: pre*`: `pre`, `textarea`, `listing`, `plaintext`, `xmp`, outside SVG; + /// an SVG ``, or an element in one, under `xml:space="preserve"` (DIVERGENCES.md#svg-xml-space-preserve). /// Children keep their whitespace and indentation. pub pre_like: bool, /// `script` / `style`: children keep their whitespace. @@ -46,7 +47,7 @@ pub struct Facts { /// The deepest last descendant is text-like, or the node itself is /// (an interpolation ends in its text). pub ends_in_text_like: bool, - /// The node is a `pre` without an end tag, or ends in one through its last children without end tags: + /// The node is pre-like without an end tag, or ends in one through its last children without end tags: /// what follows it up to the end tag that closes them is `pre` content. pub ends_in_pre: bool, /// A CDATA section, or a text that merged one: its sections are never re-wrapped. diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/svg-xml-space-preserve.html b/crates/oxc_formatter_htmlish/tests/fixtures/html/svg-xml-space-preserve.html new file mode 100644 index 0000000000..00d9c92c0e --- /dev/null +++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/svg-xml-space-preserve.html @@ -0,0 +1,5 @@ + a b c + a + b + a b c +a b c diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/svg-xml-space-preserve.html.snap b/crates/oxc_formatter_htmlish/tests/fixtures/html/svg-xml-space-preserve.html.snap new file mode 100644 index 0000000000..5e86129a4e --- /dev/null +++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/svg-xml-space-preserve.html.snap @@ -0,0 +1,54 @@ +--- +source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs +--- +==================== Input ==================== + a b c + a + b + a b c +a b c + +==================== Output ==================== +------------------ +{ printWidth: 80 } +------------------ + + a b c + + + + a + b + + + + a b c + + + + a b c + + + +------------------- +{ printWidth: 100 } +------------------- + a b c + + + a + b + + + + a b c + + + + a b c + + + +===================== End =====================