From 44d8c2b82481cc5558aafb4410a5e6f468e749b6 Mon Sep 17 00:00:00 2001 From: Yuji Sugiura <6259812+leaysgur@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:00:59 +0900 Subject: [PATCH] feat(formatter_htmlish): Implement HTML-ish formatter `Html` variant (#28272) Part of #28263 --- .gitattributes | 4 + .github/CODEOWNERS | 1 + Cargo.lock | 39 +- Cargo.toml | 1 + crates/oxc_formatter_htmlish/AGENTS.md | 53 ++ crates/oxc_formatter_htmlish/Cargo.toml | 39 + crates/oxc_formatter_htmlish/DIVERGENCES.md | 789 ++++++++++++++++++ crates/oxc_formatter_htmlish/build.rs | 15 + .../examples/htmlish_formatter.rs | 61 ++ crates/oxc_formatter_htmlish/src/context.rs | 33 + crates/oxc_formatter_htmlish/src/format.rs | 228 +++++ crates/oxc_formatter_htmlish/src/lib.rs | 16 + crates/oxc_formatter_htmlish/src/options.rs | 82 ++ .../src/print/attribute.rs | 320 +++++++ .../oxc_formatter_htmlish/src/print/block.rs | 138 +++ .../src/print/children.rs | 292 +++++++ .../src/print/element.rs | 222 +++++ crates/oxc_formatter_htmlish/src/print/mod.rs | 136 +++ crates/oxc_formatter_htmlish/src/print/tag.rs | 211 +++++ .../oxc_formatter_htmlish/src/print/text.rs | 117 +++ crates/oxc_formatter_htmlish/src/rules.rs | 37 + .../oxc_formatter_htmlish/src/tree/build.rs | 684 +++++++++++++++ .../oxc_formatter_htmlish/src/tree/display.rs | 73 ++ crates/oxc_formatter_htmlish/src/tree/mod.rs | 317 +++++++ .../oxc_formatter_htmlish/src/tree/names.rs | 586 +++++++++++++ .../tests/conformance.rs | 74 ++ ...-block-name-with-line-break.component.html | 6 + ...k-name-with-line-break.component.html.snap | 35 + ...angular-glued-block-markers.component.html | 5 + ...ar-glued-block-markers.component.html.snap | 34 + .../attribute-name-case.component.html | 2 + .../attribute-name-case.component.html.snap | 21 + .../fixtures/angular/blocks.component.html | 24 + .../angular/blocks.component.html.snap | 107 +++ .../bracketed-attribute.component.html | 1 + .../bracketed-attribute.component.html.snap | 18 + .../angular/empty-block.component.html | 1 + .../angular/empty-block.component.html.snap | 32 + .../tests/fixtures/fingerprint.rs | 144 ++++ .../tests/fixtures/html/basic.html | 1 + .../tests/fixtures/html/basic.html.snap | 22 + .../tests/fixtures/html/cdata-as-text.html | 18 + .../fixtures/html/cdata-as-text.html.snap | 69 ++ .../tests/fixtures/html/cdata-as-written.html | 1 + .../fixtures/html/cdata-as-written.html.snap | 25 + .../fixtures/html/class-ascii-whitespace.html | 2 + .../html/class-ascii-whitespace.html.snap | 21 + .../tests/fixtures/html/crlf/options.json | 4 + .../tests/fixtures/html/crlf/verbatim.html | 27 + .../fixtures/html/crlf/verbatim.html.snap | 174 ++++ .../fixtures/html/dialog-out-of-flow.html | 7 + .../html/dialog-out-of-flow.html.snap | 46 + .../html/doctype-html-in-one-pass.html | 2 + .../html/doctype-html-in-one-pass.html.snap | 21 + .../fixtures/html/doctype-in-xhtml.xhtml | 2 + .../fixtures/html/doctype-in-xhtml.xhtml.snap | 29 + .../html/end-tag-indentation-as-printed.html | 12 + .../end-tag-indentation-as-printed.html.snap | 49 ++ .../html/end-tag-with-space-is-comment.html | 1 + .../end-tag-with-space-is-comment.html.snap | 18 + .../html/htm-closing-tag-as-written.html | 2 + .../html/htm-closing-tag-as-written.html.snap | 21 + .../html/ignore/cdata-merged-text.html | 2 + .../html/ignore/cdata-merged-text.html.snap | 21 + .../tests/fixtures/html/ignore/options.json | 1 + .../html/ignored-closing-end-once.html | 4 + .../html/ignored-closing-end-once.html.snap | 31 + .../html/line-break-before-borrowed-end.html | 4 + .../line-break-before-borrowed-end.html.snap | 25 + .../fixtures/html/lowercase-known-names.html | 8 + .../html/lowercase-known-names.html.snap | 45 + .../fixtures/html/merged-inline-element.html | 6 + .../html/merged-inline-element.html.snap | 29 + .../fixtures/html/nul/nul-as-written.html | Bin 0 -> 36 bytes .../html/nul/nul-as-written.html.snap | Bin 0 -> 430 bytes .../fixtures/html/omitted-end-tag-at-eof.html | 4 + .../html/omitted-end-tag-at-eof.html.snap | 27 + .../tests/fixtures/html/omitted-end-tag.html | 6 + .../fixtures/html/omitted-end-tag.html.snap | 33 + .../html/pre-content-without-end-tag.html | 14 + .../pre-content-without-end-tag.html.snap | 73 ++ .../fixtures/html/prefixed-script-style.html | 3 + .../html/prefixed-script-style.html.snap | 24 + .../html/prettier-ignore-attribute.html | 8 + .../html/prettier-ignore-attribute.html.snap | 45 + .../fixtures/html/script-display-none.html | 13 + .../html/script-display-none.html.snap | 118 +++ .../html/self-closing-with-end-tag.html | 2 + .../html/self-closing-with-end-tag.html.snap | 29 + .../tests/fixtures/html/self-closing.html | 13 + .../fixtures/html/self-closing.html.snap | 58 ++ .../fixtures/html/srcset-zero-density.html | 2 + .../html/srcset-zero-density.html.snap | 21 + .../line-break-before-borrowed-end.html | 3 + .../line-break-before-borrowed-end.html.snap | 20 + .../tests/fixtures/html/strict/options.json | 1 + .../pre-after-borrowed-closing-end.html | 3 + .../pre-after-borrowed-closing-end.html.snap | 24 + .../fixtures/html/strict/self-closing.html | 4 + .../html/strict/self-closing.html.snap | 23 + .../html/svg-text-content-inline.html | 4 + .../html/svg-text-content-inline.html.snap | 56 ++ .../tests/fixtures/html/tag-name-case.xhtml | 1 + .../fixtures/html/tag-name-case.xhtml.snap | 18 + .../fixtures/html/text-display-width.html | 1 + .../html/text-display-width.html.snap | 18 + .../html/unquoted-value-with-slash.html | 2 + .../html/unquoted-value-with-slash.html.snap | 21 + .../end-tag-indentation-as-printed.html | 7 + .../end-tag-indentation-as-printed.html.snap | 36 + .../tests/fixtures/html/use-tabs/options.json | 1 + .../fixtures/html/whitespace-only-pre.html | 2 + .../html/whitespace-only-pre.html.snap | 21 + .../html/written-names-and-end-tags.html | 5 + .../html/written-names-and-end-tags.html.snap | 32 + .../tests/fixtures/mod.rs | 67 ++ .../tests/fixtures/options.rs | 36 + .../tests/fixtures/vue/doctype-case.vue | 2 + .../tests/fixtures/vue/doctype-case.vue.snap | 29 + .../fixtures/vue/vue-element-gap-kind.vue | 10 + .../vue/vue-element-gap-kind.vue.snap | 52 ++ crates/oxc_formatter_htmlish/tests/mod.rs | 1 + .../snapshots/conformance__prettier-html.snap | 15 + 123 files changed, 6750 insertions(+), 6 deletions(-) create mode 100644 crates/oxc_formatter_htmlish/AGENTS.md create mode 100644 crates/oxc_formatter_htmlish/Cargo.toml create mode 100644 crates/oxc_formatter_htmlish/DIVERGENCES.md create mode 100644 crates/oxc_formatter_htmlish/build.rs create mode 100644 crates/oxc_formatter_htmlish/examples/htmlish_formatter.rs create mode 100644 crates/oxc_formatter_htmlish/src/context.rs create mode 100644 crates/oxc_formatter_htmlish/src/format.rs create mode 100644 crates/oxc_formatter_htmlish/src/lib.rs create mode 100644 crates/oxc_formatter_htmlish/src/options.rs create mode 100644 crates/oxc_formatter_htmlish/src/print/attribute.rs create mode 100644 crates/oxc_formatter_htmlish/src/print/block.rs create mode 100644 crates/oxc_formatter_htmlish/src/print/children.rs create mode 100644 crates/oxc_formatter_htmlish/src/print/element.rs create mode 100644 crates/oxc_formatter_htmlish/src/print/mod.rs create mode 100644 crates/oxc_formatter_htmlish/src/print/tag.rs create mode 100644 crates/oxc_formatter_htmlish/src/print/text.rs create mode 100644 crates/oxc_formatter_htmlish/src/rules.rs create mode 100644 crates/oxc_formatter_htmlish/src/tree/build.rs create mode 100644 crates/oxc_formatter_htmlish/src/tree/display.rs create mode 100644 crates/oxc_formatter_htmlish/src/tree/mod.rs create mode 100644 crates/oxc_formatter_htmlish/src/tree/names.rs create mode 100644 crates/oxc_formatter_htmlish/tests/conformance.rs create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/angular/angular-block-name-with-line-break.component.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/angular/angular-block-name-with-line-break.component.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/angular/angular-glued-block-markers.component.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/angular/angular-glued-block-markers.component.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/angular/attribute-name-case.component.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/angular/attribute-name-case.component.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/angular/blocks.component.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/angular/blocks.component.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/angular/bracketed-attribute.component.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/angular/bracketed-attribute.component.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/angular/empty-block.component.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/angular/empty-block.component.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/fingerprint.rs create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/basic.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/basic.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/cdata-as-text.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/cdata-as-text.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/cdata-as-written.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/cdata-as-written.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/class-ascii-whitespace.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/class-ascii-whitespace.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/crlf/options.json create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/crlf/verbatim.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/crlf/verbatim.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/dialog-out-of-flow.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/dialog-out-of-flow.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/doctype-html-in-one-pass.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/doctype-html-in-one-pass.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/doctype-in-xhtml.xhtml create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/doctype-in-xhtml.xhtml.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/end-tag-indentation-as-printed.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/end-tag-indentation-as-printed.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/end-tag-with-space-is-comment.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/end-tag-with-space-is-comment.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/htm-closing-tag-as-written.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/htm-closing-tag-as-written.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/ignore/cdata-merged-text.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/ignore/cdata-merged-text.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/ignore/options.json create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/ignored-closing-end-once.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/ignored-closing-end-once.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/line-break-before-borrowed-end.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/line-break-before-borrowed-end.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/lowercase-known-names.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/lowercase-known-names.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/merged-inline-element.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/merged-inline-element.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/nul/nul-as-written.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/nul/nul-as-written.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/omitted-end-tag-at-eof.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/omitted-end-tag-at-eof.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/omitted-end-tag.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/omitted-end-tag.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/pre-content-without-end-tag.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/pre-content-without-end-tag.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/prefixed-script-style.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/prefixed-script-style.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/prettier-ignore-attribute.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/prettier-ignore-attribute.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/script-display-none.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/script-display-none.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/self-closing-with-end-tag.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/self-closing-with-end-tag.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/self-closing.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/self-closing.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/srcset-zero-density.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/srcset-zero-density.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/strict/line-break-before-borrowed-end.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/strict/line-break-before-borrowed-end.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/strict/options.json create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/strict/pre-after-borrowed-closing-end.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/strict/pre-after-borrowed-closing-end.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/strict/self-closing.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/strict/self-closing.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/svg-text-content-inline.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/svg-text-content-inline.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/tag-name-case.xhtml create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/tag-name-case.xhtml.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/text-display-width.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/text-display-width.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/unquoted-value-with-slash.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/unquoted-value-with-slash.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/use-tabs/end-tag-indentation-as-printed.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/use-tabs/end-tag-indentation-as-printed.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/use-tabs/options.json create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/whitespace-only-pre.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/whitespace-only-pre.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/written-names-and-end-tags.html create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/html/written-names-and-end-tags.html.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/mod.rs create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/options.rs create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/vue/doctype-case.vue create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/vue/doctype-case.vue.snap create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/vue/vue-element-gap-kind.vue create mode 100644 crates/oxc_formatter_htmlish/tests/fixtures/vue/vue-element-gap-kind.vue.snap create mode 100644 crates/oxc_formatter_htmlish/tests/mod.rs create mode 100644 crates/oxc_formatter_htmlish/tests/snapshots/conformance__prettier-html.snap diff --git a/.gitattributes b/.gitattributes index f566f3343d..6168438f90 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,6 +5,10 @@ crates/oxc_formatter_json/tests/fixtures/** text=auto eol=lf crates/oxc_formatter_json/tests/fixtures/jsonc/crlf/** text=auto eol=crlf crates/oxc_formatter_graphql/tests/fixtures/** text=auto eol=lf crates/oxc_formatter_graphql/tests/fixtures/graphql/crlf/** text=auto eol=crlf +crates/oxc_formatter_htmlish/tests/fixtures/** text=auto eol=lf +crates/oxc_formatter_htmlish/tests/fixtures/html/crlf/** text=auto eol=crlf +# NUL bytes would make git read these as binary +crates/oxc_formatter_htmlish/tests/fixtures/html/nul/** text eol=lf diff crates/oxc_formatter_yaml/tests/fixtures/** text=auto eol=lf crates/oxc_formatter_yaml/tests/fixtures/yaml/crlf/** text=auto eol=crlf crates/oxc_formatter_markdown/tests/fixtures/** text=auto eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7291cd5f3c..18fb8cb4d8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -31,6 +31,7 @@ pnpm-lock.yaml /crates/oxc_formatter_core @leaysgur /crates/oxc_formatter_json @leaysgur /crates/oxc_formatter_graphql @leaysgur +/crates/oxc_formatter_htmlish @leaysgur /crates/oxc_formatter_css @leaysgur /crates/oxc_formatter_yaml @leaysgur /crates/oxc_formatter_markdown @leaysgur diff --git a/Cargo.lock b/Cargo.lock index 558dd7e918..ff61273c36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,7 +280,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -682,7 +682,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1517,7 +1517,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1703,6 +1703,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "oxc-htmlish-parser" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "909fd32d29f076e5129a27244801bed7de5ce33a974310c710f8777dee016495" +dependencies = [ + "memchr", + "oxc_allocator", +] + [[package]] name = "oxc-markdown-parser" version = "0.0.3" @@ -2123,6 +2133,23 @@ dependencies = [ "pico-args", ] +[[package]] +name = "oxc_formatter_htmlish" +version = "0.75.0" +dependencies = [ + "cow-utils", + "insta", + "memchr", + "oxc-htmlish-parser", + "oxc_allocator", + "oxc_diagnostics", + "oxc_formatter_core", + "oxc_formatter_tests", + "oxc_span", + "pico-args", + "zmij_ecma", +] + [[package]] name = "oxc_formatter_json" version = "0.75.0" @@ -3379,7 +3406,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3798,7 +3825,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4332,7 +4359,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 4e58d94ea0..483647a5ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -242,6 +242,7 @@ num-bigint = "0.5.0" # Big integers num-traits = "0.2.19" # Numeric traits oxc-css-parser = "0.0.18" # CSS/SCSS/Less parser (raffia 0.12.3 fork: adds `template_placeholder` typed placeholders for css-in-js + valid-syntax coverage fixes (see crates/oxc_formatter_css/AGENTS.md)) oxc-graphql-parser = "0.0.9" # GraphQL parser (direct AST; apollo-parser 0.8.6 fork aligned with graphql-js 17 syntax) +oxc-htmlish-parser = "0.0.1" # HTML / Vue / Angular parser (loose, span-faithful AST) oxc-markdown-parser = "0.0.3" # CommonMark + GFM parser (span-faithful, style-preserving typed AST) oxc-toml = "0.15.1" # TOML parser and formatter oxc-yaml-parser = "0.0.7" # YAML 1.2 parser (comment-preserving typed AST, node naming from `yaml-unist-parser`) diff --git a/crates/oxc_formatter_htmlish/AGENTS.md b/crates/oxc_formatter_htmlish/AGENTS.md new file mode 100644 index 0000000000..e2db192406 --- /dev/null +++ b/crates/oxc_formatter_htmlish/AGENTS.md @@ -0,0 +1,53 @@ +# Coding agent guides for `crates/oxc_formatter_htmlish` + +Follow @../oxc_formatter_core/FORMATTER_POLICY.md , this file holds only the htmlish-specific rules. +Known divergences live in DIVERGENCES.md. + +## Overview + +Prettier compatible formatter for HTML and the template languages built on it (Vue, Angular, ...), on `oxc_formatter_core`. + +### Parser + +Parses with [`oxc-htmlish-parser`](https://github.com/oxc-project/oxc-htmlish-parser). +Parse behavior and its own divergences are that repo's `AGENTS.md` / `DIVERGENCES.md`. + +### Dialects + +What a dialect changes in the shared rules is a `DialectRules` field (`src/rules.rs`), resolved once per document; +a dialect adds a field there, not a `Dialect` check at the use site. + +### Preprocess + +The facts Prettier's `print-preprocess` computes (CSS display, whitespace sensitivity, kept children, borrowed tag markers) +are a table keyed by `NodeId` (`src/tree/`). +The AST is never rewritten. + +### Tags + +An end tag the source omits is never added (DIVERGENCES.md#unclosed-at-end, DIVERGENCES.md#omitted-end-tag). +A normalization is applied only where it is known safe for every reader, as in `oxc_formatter_css` (its AGENTS.md "Case"): +a void element prints `/>`, a doctype `` but in XHTML, which an XML parser may read (DIVERGENCES.md#doctype-case), +and the tag and attribute names Prettier knows print lowercase in a `.html` file only (DIVERGENCES.md#tag-name-case). + +The borrow rules (`src/print/tag.rs`) are shaped like Prettier's `print/tag.js` against the policy, kept deliberately (why: its module doc). + +## Verification + +```sh +cargo run -p oxc_formatter_htmlish --example htmlish_formatter [filename] +DUMP_IR=1 cargo run -p oxc_formatter_htmlish --example htmlish_formatter [filename] + +# The oracle. The parser follows the file name as the example's variant does (`*.component.html` is angular). +node apps/oxfmt/node_modules/prettier/bin/prettier.cjs [filename] +``` + +### Prettier conformance + +`tests/conformance.rs` runs `tests/format/html`. +Files whose output depends on embedded formatting (`b
+ + ++ ab +
+ + ++ a + + b +
+``` + +```html + +x
`). + +Prettier lays `script` out as a block and puts it on its own line, which turns `a` and `b` glued around it into `a b`: +it did so for glued scripts between blocks (`b` puts the `script` on its own line). + /// A run that renders nothing keeps its gaps as written, or a line break printed next to a block would spread next time. + /// A gap's two sides agree on spaces and sensitivity, so a pass each way carries it. + fn spread_spaces_past_hidden(&mut self, parent: NodeId) { + let mut next = self.tree[parent].first_child; + while let Some(id) = next { + next = self.tree[id].next; + let facts = &self.tree[id]; + if self.is_hidden(id) && facts.has_leading_spaces && facts.leading_sensitive { + self.facts(id).has_trailing_spaces = true; + if let Some(next) = next { + self.facts(next).has_leading_spaces = true; + } + } + } + let mut prev = self.tree[parent].last_child; + while let Some(id) = prev { + prev = self.tree[id].prev; + let facts = &self.tree[id]; + if self.is_hidden(id) && facts.has_trailing_spaces && facts.trailing_sensitive { + self.facts(id).has_leading_spaces = true; + if let Some(prev) = prev { + self.facts(prev).has_trailing_spaces = true; + } + } + } + } + + /// Where known names print lowercase (`DialectRules::lowercase_known_names`): + /// marks foreign content, under which they keep their case, and a tag name written in another case. + fn mark_name_case(&mut self, id: NodeId, parent: NodeId) { + let t = &self.tree; + let Some(el) = t.element(id) else { return }; + let foreign = t[parent].foreign || el.namespace != Namespace::Html; + let uppercase = |name: Span| name.slice(t.source).bytes().any(|b| b.is_ascii_uppercase()); + let lowercase_name = (uppercase(el.name) + || el.end_tag.is_some_and(|tag| uppercase(tag.name))) + && t.lowercases_names(id, el); + let facts = self.facts(id); + facts.foreign = foreign; + facts.lowercase_name = lowercase_name; + } + + /// A node's display, also for one in a block whose children are not built yet. + fn display_of(&self, id: NodeId) -> Display { + if self.tree[id].parent.is_some() { self.tree[id].display } else { self.display(id) } + } + + /// The nearest box: `id`, or the nearest ancestor that is not a block. + fn flow_parent(&self, mut id: NodeId) -> NodeId { + while self.tree.is_block(id) { + id = self.tree.parent(id); + } + id + } +} + +fn is_whitespace_text(node: &Node<'_>) -> bool { + matches!(&node.kind, NodeKind::Text(text) if text.is_whitespace_only()) +} + +/// CSS `white-space: pre*`. +fn is_pre_like(el: &Element<'_>) -> bool { + el.namespace != Namespace::Svg + && matches!(el.key, "pre" | "textarea" | "listing" | "plaintext" | "xmp") +} + +/// A void element, or one written `/>` without an end tag. +/// +/// HTML ignores the `/` of `123`: the first child prints its parent's opening `>`. + pub parent_opening_end: bool, + /// `
123`: the previous sibling's closing `>`. + pub prev_closing_end: bool, + /// ``: the last child's closing `>`, before the node's own end tag. + pub last_child_closing_end: bool, + /// `123`: the last text prints its parent's ``: the text prints its next sibling's ` bool {
+ self.pre_like || self.script_like
+ }
+
+ /// Whitespace before the node is significant and there is none.
+ pub fn glued_before(&self) -> bool {
+ self.leading_sensitive && !self.has_leading_spaces
+ }
+
+ /// Whitespace after the node is significant and there is none.
+ pub fn glued_after(&self) -> bool {
+ self.trailing_sensitive && !self.has_trailing_spaces
+ }
+
+ /// Exactly one child prints.
+ pub fn only_child(&self) -> Option x x
+ x
+
+ x
+
+ x
+
+ x
+ Open the panel Open the panel Open the panel Open the panel x x x x a a a a a/> a/> a/> a/> CDATA: here. a b c CDATA: here. a b c CDATA: here. a b c CDATA: here. a b c axcydze and qrst axcydze and qrst axcydze and qrst axcydze and qrst para
+ para
+ para
+ para
+ ab xz ab aaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb ab xz ab aaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+ ab
+
+ xz
+
+ ab
+
+ aaaaaaaaaaaaaaaaaaaaaaaa
+
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+
+ ab
+
+ xz
+
+ ab
+
+ aaaaaaaaaaaaaaaaaaaaaaaa
+
+ bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+ b b b b x after x after x after x after a
+ b
+ a
+ b
+ a b a b Text Text Text Text xyz x xyz x xyz x xyz x {{ a }} {{ a }} {{ a }} {{ a }} a long enough first element here and a second one that does not fit a long enough first element here and a second one that does not fit
+ a long enough first element here and a second one that does not fit
+
+ a long enough first element here and a second one that does not fit
+
+ line 1
+ line 2
+
+
+
+
+
+ line 1
+ line 2
+
+
+
+
+
+ line 1
+ line 2
+
+
+
+
+
+ line 1
+ line 2
+
+
+
+
+
+ line 1
+ line 2
+
+
+
+
+
b
b
b
b
+
+
+
+
+
+
+
+
+x unclosed
diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/pre-content-without-end-tag.html.snap b/crates/oxc_formatter_htmlish/tests/fixtures/html/pre-content-without-end-tag.html.snap
new file mode 100644
index 0000000000..3d8cc95035
--- /dev/null
+++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/pre-content-without-end-tag.html.snap
@@ -0,0 +1,73 @@
+---
+source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
+---
+==================== Input ====================
+
+x unclosed
+
+==================== Output ====================
+------------------
+{ printWidth: 80 }
+------------------
+
+
+
+
+x unclosed
+
+-------------------
+{ printWidth: 100 }
+-------------------
+
+
+
+
+x unclosed
+
+===================== End =====================
diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/prefixed-script-style.html b/crates/oxc_formatter_htmlish/tests/fixtures/html/prefixed-script-style.html
new file mode 100644
index 0000000000..8cf7e65d22
--- /dev/null
+++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/prefixed-script-style.html
@@ -0,0 +1,3 @@
+
+
+
+
+
+
+
+
+
+
+







x
+a
+x
diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/strict/pre-after-borrowed-closing-end.html.snap b/crates/oxc_formatter_htmlish/tests/fixtures/html/strict/pre-after-borrowed-closing-end.html.snap
new file mode 100644
index 0000000000..c2160750d3
--- /dev/null
+++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/strict/pre-after-borrowed-closing-end.html.snap
@@ -0,0 +1,24 @@
+---
+source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
+---
+==================== Input ====================
+ax
+a
+x
+
+==================== Output ====================
+-------------------------------------------------------
+{ htmlWhitespaceSensitivity: "strict", printWidth: 80 }
+-------------------------------------------------------
+ax
a
+x
+
+--------------------------------------------------------
+{ htmlWhitespaceSensitivity: "strict", printWidth: 100 }
+--------------------------------------------------------
+ax
a
+x
+
+===================== End =====================
diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/strict/self-closing.html b/crates/oxc_formatter_htmlish/tests/fixtures/html/strict/self-closing.html
new file mode 100644
index 0000000000..046b478744
--- /dev/null
+++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/strict/self-closing.html
@@ -0,0 +1,4 @@
+
+
+a
+
diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/strict/self-closing.html.snap b/crates/oxc_formatter_htmlish/tests/fixtures/html/strict/self-closing.html.snap
new file mode 100644
index 0000000000..a7e42fe278
--- /dev/null
+++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/strict/self-closing.html.snap
@@ -0,0 +1,23 @@
+---
+source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
+---
+==================== Input ====================
+
+
+a
+
+
+==================== Output ====================
+-------------------------------------------------------
+{ htmlWhitespaceSensitivity: "strict", printWidth: 80 }
+-------------------------------------------------------
+
+a
+
+--------------------------------------------------------
+{ htmlWhitespaceSensitivity: "strict", printWidth: 100 }
+--------------------------------------------------------
+
+a
+
+===================== End =====================
diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/svg-text-content-inline.html b/crates/oxc_formatter_htmlish/tests/fixtures/html/svg-text-content-inline.html
new file mode 100644
index 0000000000..25cea0ac93
--- /dev/null
+++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/svg-text-content-inline.html
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/svg-text-content-inline.html.snap b/crates/oxc_formatter_htmlish/tests/fixtures/html/svg-text-content-inline.html.snap
new file mode 100644
index 0000000000..e299798c9f
--- /dev/null
+++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/svg-text-content-inline.html.snap
@@ -0,0 +1,56 @@
+---
+source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
+---
+==================== Input ====================
+
+
+
+
+
+==================== Output ====================
+------------------
+{ printWidth: 80 }
+------------------
+
+
+
+
+
+-------------------
+{ printWidth: 100 }
+-------------------
+
+
+
+
+
+===================== End =====================
diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/tag-name-case.xhtml b/crates/oxc_formatter_htmlish/tests/fixtures/html/tag-name-case.xhtml
new file mode 100644
index 0000000000..eae65a0d6c
--- /dev/null
+++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/tag-name-case.xhtml
@@ -0,0 +1 @@
+
diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/unquoted-value-with-slash.html.snap b/crates/oxc_formatter_htmlish/tests/fixtures/html/unquoted-value-with-slash.html.snap
new file mode 100644
index 0000000000..6d811996f0
--- /dev/null
+++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/unquoted-value-with-slash.html.snap
@@ -0,0 +1,21 @@
+---
+source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
+---
+==================== Input ====================
+favorite website
+
+
+==================== Output ====================
+------------------
+{ printWidth: 80 }
+------------------
+favorite website
+
+
+-------------------
+{ printWidth: 100 }
+-------------------
+favorite website
+
+
+===================== End =====================
diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/use-tabs/end-tag-indentation-as-printed.html b/crates/oxc_formatter_htmlish/tests/fixtures/html/use-tabs/end-tag-indentation-as-printed.html
new file mode 100644
index 0000000000..ee7e85803f
--- /dev/null
+++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/use-tabs/end-tag-indentation-as-printed.html
@@ -0,0 +1,7 @@
+
+
diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/whitespace-only-pre.html.snap b/crates/oxc_formatter_htmlish/tests/fixtures/html/whitespace-only-pre.html.snap
new file mode 100644
index 0000000000..24beb94f84
--- /dev/null
+++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/whitespace-only-pre.html.snap
@@ -0,0 +1,21 @@
+---
+source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
+---
+==================== Input ====================
+
+
+
+==================== Output ====================
+------------------
+{ printWidth: 80 }
+------------------
+
+
+
+-------------------
+{ printWidth: 100 }
+-------------------
+
+
+
+===================== End =====================
diff --git a/crates/oxc_formatter_htmlish/tests/fixtures/html/written-names-and-end-tags.html b/crates/oxc_formatter_htmlish/tests/fixtures/html/written-names-and-end-tags.html
new file mode 100644
index 0000000000..6ee774b611
--- /dev/null
+++ b/crates/oxc_formatter_htmlish/tests/fixtures/html/written-names-and-end-tags.html
@@ -0,0 +1,5 @@
+
+
+ reactive() stores.
+
+
+ reactive() stores.
+
+
+ reactive() stores.
+
+
+ reactive() stores.
+