mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(formatter_htmlish): Implement HTML-ish formatter Html variant (#28272)
Part of #28263
This commit is contained in:
1 parent
c62fbbb088
commit
44d8c2b824
123 files changed
+6750
-6
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Generated
+33
-6
@@ -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]]
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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 `<!doctype html>` 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 (`<script>` / `<style>` bodies, `style` / `on*` attributes, a YAML front matter body) are ignored here and covered by oxfmt's E2E conformance;
|
||||
the ignore list names the reason for each.
|
||||
|
||||
Every failing file is accounted for by a DIVERGENCES.md entry's `Conformance:` line; there are no unclassified failures.
|
||||
|
||||
Fixtures and the example read `.vue` as Vue, `.xhtml` / `.xht` as XHTML and `*.component.html` as Angular.
|
||||
@@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "oxc_formatter_htmlish"
|
||||
version = "0.75.0"
|
||||
authors.workspace = true
|
||||
categories.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
include = ["/src"]
|
||||
keywords.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
description.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
cow-utils = { workspace = true }
|
||||
zmij_ecma = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
oxc-htmlish-parser = { workspace = true }
|
||||
oxc_allocator = { workspace = true }
|
||||
oxc_diagnostics = { workspace = true }
|
||||
oxc_formatter_core = { workspace = true }
|
||||
oxc_span = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true }
|
||||
oxc_formatter_tests = { workspace = true, features = ["conformance"] }
|
||||
pico-args = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
oxc_formatter_tests = { workspace = true }
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
@@ -0,0 +1,789 @@
|
||||
# Known divergences
|
||||
|
||||
Admission reasons and rules: see `crates/oxc_formatter_core/FORMATTER_POLICY.md` "Known divergences".
|
||||
|
||||
## whitespace-only-pre
|
||||
|
||||
- Why: semantics (whitespace in `pre` / `textarea` is content)
|
||||
- Pin: `tests/fixtures/html/whitespace-only-pre.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<pre> </pre>
|
||||
<textarea> </textarea>
|
||||
|
||||
<!-- ours -->
|
||||
<pre> </pre>
|
||||
<textarea> </textarea>
|
||||
|
||||
<!-- prettier -->
|
||||
<pre></pre>
|
||||
<textarea> </textarea>
|
||||
```
|
||||
|
||||
A whitespace-only `<pre>` or `<textarea>` keeps its content.
|
||||
Prettier drops a lone whitespace-only text in `pre` (as in every element) and collapses one in `textarea` to a single space,
|
||||
which changes what `pre` renders and what `textarea` submits.
|
||||
|
||||
## doctype-case
|
||||
|
||||
- Why: uniform-rule (same construct, same output: the doctype of a `.html` file, which Prettier lowercases)
|
||||
- Pin: `tests/fixtures/vue/doctype-case.vue`
|
||||
|
||||
```vue
|
||||
<!-- input -->
|
||||
<!DOCTYPE html>
|
||||
<html><body><p>{{ a }}</p></body></html>
|
||||
|
||||
<!-- ours -->
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<p>{{ a }}</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!-- prettier -->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<p>{{ a }}</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
`<!DOCTYPE html>` prints `<!doctype html>` in every HTML document but XHTML (`.xhtml`, `.xht`), where an XML parser needs `<!DOCTYPE`:
|
||||
a Vue page, html-in-js and an HTML code block too, and `.hta` / `.inc` files in oxfmt.
|
||||
Prettier lowercases it in a file named `.html` / `.htm` only, as it decides by file name.
|
||||
The `html` in any doctype prints lowercase (`<!DOCTYPE html PUBLIC …>`), as in Prettier.
|
||||
|
||||
## doctype-html-in-one-pass
|
||||
|
||||
- Why: invariant
|
||||
- Pin: `tests/fixtures/html/doctype-html-in-one-pass.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<!DOCTYPE HTML>
|
||||
|
||||
<!-- ours, and Prettier's second pass -->
|
||||
<!doctype html>
|
||||
|
||||
<!-- prettier -->
|
||||
<!DOCTYPE html>
|
||||
```
|
||||
|
||||
Prettier's output is not a fixpoint: it lowercases the `HTML` and the `<!DOCTYPE` in different passes.
|
||||
`<!DOCTYPE HTML>` prints `<!doctype html>` in one.
|
||||
|
||||
## cdata-as-written
|
||||
|
||||
- Why: uniform-rule (a section's content prints as written, as a comment's does: in HTML content it is a bogus comment to browsers; prettier/prettier#20256, prettier/prettier#16147)
|
||||
- Pin: `tests/fixtures/html/cdata-as-written.html`
|
||||
|
||||
```html
|
||||
<!-- input (printWidth: 20) -->
|
||||
<p>text <![CDATA[aaaa bbbb cccc dddd eeee]]> more</p>
|
||||
|
||||
<!-- ours -->
|
||||
<p>
|
||||
text
|
||||
<![CDATA[aaaa bbbb cccc dddd eeee]]>
|
||||
more
|
||||
</p>
|
||||
|
||||
<!-- prettier -->
|
||||
<p>
|
||||
text <![CDATA[aaaa
|
||||
bbbb cccc dddd
|
||||
eeee]]> more
|
||||
</p>
|
||||
```
|
||||
|
||||
A CDATA section is one unbreakable word, together with any text touching it; the text around it wraps as usual.
|
||||
Prettier merges it into the surrounding text and re-wraps its words, changing what it holds:
|
||||
in HTML the section is comment data, and in SVG / MathML its text, where a re-wrap only moves collapsible whitespace.
|
||||
Prettier `main` keeps it as written (prettier/prettier#20256), unreleased at the pin.
|
||||
|
||||
## class-ascii-whitespace
|
||||
|
||||
- Why: semantics (class names are separated by ASCII whitespace only)
|
||||
- Pin: `tests/fixtures/html/class-ascii-whitespace.html`
|
||||
|
||||
```html
|
||||
<!-- input (the space in `a b` is U+00A0) -->
|
||||
<div class="a b c"></div>
|
||||
|
||||
<!-- ours -->
|
||||
<div class="a b c"></div>
|
||||
|
||||
<!-- prettier: `a` and `b` are now two classes -->
|
||||
<div class="a b c"></div>
|
||||
```
|
||||
|
||||
A class list collapses and trims ASCII whitespace; a no-break space stays part of a class name.
|
||||
Prettier collapses and trims with JavaScript's `\s`, which splits `a b` written with a literal U+00A0 (one class) into two classes.
|
||||
|
||||
## end-tag-indentation-as-printed
|
||||
|
||||
- Why: uniform-rule (same construct, same output: Prettier's output for the same layout indented with spaces; prettier/prettier#17048)
|
||||
- Pin: `tests/fixtures/html/end-tag-indentation-as-printed.html`, `tests/fixtures/html/use-tabs/end-tag-indentation-as-printed.html`
|
||||
|
||||
```html
|
||||
<!-- input (useTabs) -->
|
||||
<div>
|
||||
<div>
|
||||
<!--
|
||||
comment
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ours -->
|
||||
<div>
|
||||
<div>
|
||||
<!--
|
||||
comment
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- prettier -->
|
||||
<div>
|
||||
<div>
|
||||
<!--
|
||||
comment
|
||||
--></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
When the last child is a comment or `pre` text ending in a line break plus the element's indentation, the end tag follows it on the same line.
|
||||
That indentation is counted in the printer's indent unit: a tab per level with `useTabs`, `indentWidth` spaces otherwise.
|
||||
Prettier counts `tabWidth` × depth characters, tabs or spaces alike.
|
||||
With `useTabs`, a comment closing one level deeper than its element then counts as being at the element's indentation and the end tag joins it,
|
||||
while the same layout written with spaces keeps the end tag on its own line.
|
||||
Without `useTabs`, a tab-indented tail of that many characters joins likewise.
|
||||
|
||||
## srcset-zero-density
|
||||
|
||||
- Why: semantics (`0x` is a density the HTML srcset parser accepts; without it the candidate defaults to `1x`)
|
||||
- Pin: `tests/fixtures/html/srcset-zero-density.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<img srcset="a.png 0x, b.png 2x">
|
||||
|
||||
<!-- ours -->
|
||||
<img srcset="a.png 0x, b.png 2x" />
|
||||
|
||||
<!-- prettier -->
|
||||
<img srcset="a.png, b.png 2x" />
|
||||
```
|
||||
|
||||
A `0x` (or `-0x`) descriptor is kept, printed as `0x`.
|
||||
Authors must write a density above zero, but the HTML srcset parser reads `0x` as density 0 (infinite natural dimensions), not as the default `1x`.
|
||||
Prettier's srcset parser treats a zero density as absent and drops it.
|
||||
|
||||
## script-display-none
|
||||
|
||||
- Why: semantics (whitespace added on both sides of a `script` renders between its inline neighbors)
|
||||
- Pin: `tests/fixtures/html/script-display-none.html`
|
||||
- Conformance: `html/tags/tags.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<p><span>a</span><script src="a.js"></script><span>b</span></p>
|
||||
|
||||
<!-- ours -->
|
||||
<p>
|
||||
<span>a</span
|
||||
><script src="a.js"></script
|
||||
><span>b</span>
|
||||
</p>
|
||||
|
||||
<!-- prettier -->
|
||||
<p>
|
||||
<span>a</span>
|
||||
<script src="a.js"></script>
|
||||
<span>b</span>
|
||||
</p>
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<div>Price: <span>10</span> <meta itemprop="priceCurrency" content="USD">USD, and some more text that wraps the line</div>
|
||||
|
||||
<!-- ours -->
|
||||
<div>
|
||||
Price: <span>10</span> <meta itemprop="priceCurrency" content="USD" /> USD,
|
||||
and some more text that wraps the line
|
||||
</div>
|
||||
|
||||
<!-- prettier -->
|
||||
<div>
|
||||
Price: <span>10</span> <meta itemprop="priceCurrency" content="USD" />USD, and
|
||||
some more text that wraps the line
|
||||
</div>
|
||||
```
|
||||
|
||||
An element with the UA stylesheet's `display: none` (`script`, `style`, `link`, `meta`, `title`, ...) renders nothing:
|
||||
the whitespace on both its sides is one run between the content around it, judged as if the element were not there.
|
||||
- Between inline content, a run without whitespace stays glued (`>` borrowed);
|
||||
one with whitespace may break at any of its gaps, as it renders one space either way.
|
||||
- Next to a block or a parent's edge the run renders nothing and breaks freely,
|
||||
though a gap between two non-blocks stays on the line where it fits (`<p>x<meta /></p>`).
|
||||
|
||||
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 (`</script><script>` borrowing `>`, prettier/prettier#6393, prettier/prettier#6423),
|
||||
which passing hidden elements prints the same way without changing what renders.
|
||||
It keeps the whitespace around the other hidden elements as written,
|
||||
so a glued one borrows `>` even where nothing renders, and a spaced run breaks only at the space.
|
||||
A `script` / `style` body renders nothing, so its end tag takes its own line even where the next sibling borrows its `>`.
|
||||
Prettier's other block overrides for boxless elements (`param`, `source`, `track`) are kept:
|
||||
they sit inside `object` / `video` / `audio`, whose children render only as fallback content.
|
||||
|
||||
## svg-text-content-inline
|
||||
|
||||
- Why: semantics (whitespace added between a `<text>`'s children renders: `H<tspan>2</tspan>O` reads "H 2 O")
|
||||
- Pin: `tests/fixtures/html/svg-text-content-inline.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<svg><text>H<tspan baseline-shift="sub">2</tspan>O</text></svg>
|
||||
|
||||
<!-- ours -->
|
||||
<svg>
|
||||
<text>H<tspan baseline-shift="sub">2</tspan>O</text>
|
||||
</svg>
|
||||
|
||||
<!-- prettier -->
|
||||
<svg>
|
||||
<text>
|
||||
H
|
||||
<tspan baseline-shift="sub">2</tspan>
|
||||
O
|
||||
</text>
|
||||
</svg>
|
||||
```
|
||||
|
||||
An SVG `<text>` lays out its content as a line of text, so its descendants (`tspan`, `textPath`, `a`, ...) are inline:
|
||||
whitespace between them is kept or omitted as written, as between HTML inline elements.
|
||||
`<text>` itself stays a block, whose leading and trailing whitespace collapses away.
|
||||
Prettier lays out every SVG element but `<svg>` as a block (prettier/prettier#17713, prettier/prettier#10121).
|
||||
With `htmlWhitespaceSensitivity: "ignore"` they stay blocks, as HTML inline elements do.
|
||||
|
||||
## dialog-out-of-flow
|
||||
|
||||
- Why: semantics (whitespace added on both sides of a `dialog` renders between its inline neighbors)
|
||||
- Pin: `tests/fixtures/html/dialog-out-of-flow.html`
|
||||
- Conformance: `html/tags/tags2.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<main><b>a</b><dialog></dialog><b>b</b></main>
|
||||
|
||||
<!-- ours -->
|
||||
<main><b>a</b><dialog></dialog><b>b</b></main>
|
||||
|
||||
<!-- prettier -->
|
||||
<main>
|
||||
<b>a</b>
|
||||
<dialog></dialog>
|
||||
<b>b</b>
|
||||
</main>
|
||||
```
|
||||
|
||||
A `dialog` takes no room in its line: the UA stylesheet hides it while closed (`dialog:not([open])`)
|
||||
and positions it absolutely while open, so whitespace around it is kept or omitted as written, as around a `script` (DIVERGENCES.md#script-display-none).
|
||||
Its content has a box of its own, so it lays out as an inline-block's: line breaks inside its tags are free.
|
||||
Prettier reads `dialog` as a block from the UA stylesheet's `display: block` and skips the `:not([open])` rule.
|
||||
Between blocks, where it usually sits, the output is the same.
|
||||
|
||||
## nul-as-written
|
||||
|
||||
- Why: semantics (Prettier drops everything after a NUL between tags)
|
||||
- Pin: `tests/fixtures/html/nul/nul-as-written.html`
|
||||
|
||||
```html
|
||||
<!-- input (`␀` is U+0000) -->
|
||||
<p a>b</p>
|
||||
<p>c␀d</p>
|
||||
<p >e</p>
|
||||
|
||||
<!-- ours -->
|
||||
<p a>b</p>
|
||||
<p>c␀d</p>
|
||||
<p>e</p>
|
||||
|
||||
<!-- prettier -->
|
||||
<p a>b</p>
|
||||
<p>c</p>
|
||||
```
|
||||
|
||||
A NUL prints as written, as in every oxc formatter (CSS, Markdown, YAML and JS strings keep it too).
|
||||
HTML makes it a parse error that browsers ignore in text (or replace with U+FFFD elsewhere), so the output reads as the input does.
|
||||
|
||||
## pre-after-borrowed-closing-end
|
||||
|
||||
- Why: semantics (Prettier prints the borrowed `>` twice; the second is text; prettier/prettier#16184)
|
||||
- Pin: `tests/fixtures/html/strict/pre-after-borrowed-closing-end.html`
|
||||
|
||||
```html
|
||||
<!-- input (htmlWhitespaceSensitivity: strict) -->
|
||||
<b>a</b><pre><b>x</b></pre>
|
||||
|
||||
<!-- ours -->
|
||||
<b>a</b><pre><b>x</b></pre>
|
||||
|
||||
<!-- prettier -->
|
||||
<b>a</b>><pre><b>x</b></pre>
|
||||
```
|
||||
|
||||
A `pre` holding an element (printed as written) that borrows its previous sibling's `>` prints it once.
|
||||
Prettier prints it again, adding a `>` to the rendered text.
|
||||
Prettier `main` prints it once (prettier/prettier#20215), unreleased at the pin.
|
||||
|
||||
## ignored-closing-end-once
|
||||
|
||||
- Why: semantics (Prettier prints the borrowed `>` twice; the second is text)
|
||||
- Pin: `tests/fixtures/html/ignored-closing-end-once.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<time><!-- prettier-ignore -->
|
||||
<mark>
|
||||
<input />
|
||||
</mark></time>
|
||||
|
||||
<!-- ours -->
|
||||
<time
|
||||
><!-- prettier-ignore -->
|
||||
<mark>
|
||||
<input />
|
||||
</mark
|
||||
></time>
|
||||
|
||||
<!-- prettier: a `>` text after </mark> -->
|
||||
<time
|
||||
><!-- prettier-ignore -->
|
||||
<mark>
|
||||
<input />
|
||||
</mark>
|
||||
></time>
|
||||
```
|
||||
|
||||
A `prettier-ignore`d last child whose closing `>` its parent borrows prints its source without that `>`.
|
||||
Prettier strips it only when the next sibling borrows it, so the parent prints it a second time.
|
||||
|
||||
## line-break-before-borrowed-end
|
||||
|
||||
- Why: invariant
|
||||
- Pin: `tests/fixtures/html/line-break-before-borrowed-end.html`, `tests/fixtures/html/strict/line-break-before-borrowed-end.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<div><progress><progress>x</progress><wbr />
|
||||
amet,
|
||||
|
||||
</progress><abbr></abbr></div>
|
||||
|
||||
<!-- ours, and Prettier's second pass -->
|
||||
<div>
|
||||
<progress><progress>x</progress><wbr /> amet,</progress><abbr></abbr>
|
||||
</div>
|
||||
|
||||
<!-- prettier -->
|
||||
<div>
|
||||
<progress>
|
||||
<progress>x</progress><wbr />
|
||||
amet,</progress
|
||||
><abbr></abbr>
|
||||
</div>
|
||||
```
|
||||
|
||||
Prettier's output is not a fixpoint: its second pass joins the lines the first broke.
|
||||
A line break after a last child, before an end tag whose `>` is borrowed, does not count as one
|
||||
(for an only child on its own line, or a hard line before a node with line breaks around it).
|
||||
No printer can keep it there: the end tag follows on the same line, after a space where whitespace matters and right after the child otherwise.
|
||||
Prettier counts it from the source, then its next pass no longer sees it and joins the lines.
|
||||
Where that layout stays for other reasons, Prettier's first pass is stable and differs from ours.
|
||||
|
||||
## text-display-width
|
||||
|
||||
- Why: uniform-rule (text width is the display width; `oxc_formatter_core`'s FORMATTER_POLICY.md "Text width"; prettier/prettier#20081)
|
||||
- Pin: `tests/fixtures/html/text-display-width.html`
|
||||
|
||||
```html
|
||||
<!-- input (print width 80; `{60 × U+00A0 U+200C}` stands for the characters) -->
|
||||
<div>{60 × U+00A0 U+200C}</div>
|
||||
|
||||
<!-- ours -->
|
||||
<div>{60 × U+00A0 U+200C}</div>
|
||||
|
||||
<!-- prettier -->
|
||||
<div>
|
||||
{60 × U+00A0 U+200C}
|
||||
</div>
|
||||
```
|
||||
|
||||
Zero-width characters count as 0 toward the print width, so the text fits next to its tags.
|
||||
Prettier counts the format characters (`U+200B..U+200F`, `U+FEFF`) as 1 and breaks.
|
||||
An email template pads its preview text this way, NBSP alternating with zero-width characters.
|
||||
|
||||
## unclosed-at-end
|
||||
|
||||
- Why: semantics (an end tag added where a fragment ends changes the page it is joined into; prettier/prettier#17761, prettier/prettier#5665)
|
||||
- Pin: `tests/fixtures/html/omitted-end-tag-at-eof.html`
|
||||
- Conformance: `html/basics/broken-html.html`, `html/prettier_ignore/unclosed2.html`
|
||||
|
||||
```html
|
||||
<!-- input (header.html, closed by footer.html) -->
|
||||
<html>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>Site</header>
|
||||
|
||||
<!-- ours -->
|
||||
<html>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>Site</header>
|
||||
|
||||
<!-- prettier -->
|
||||
<html>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>Site</header>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
An element left open at the end gets no end tag: it is a fragment that something else closes,
|
||||
html-in-js joined with other templates (Prettier turns `` html`<div>` `` into `` html`<div></div>` ``),
|
||||
or a partial `.html` file included before its closing counterpart (a header for a footer).
|
||||
Prettier closes it where the fragment ends, which changes the page it is joined into.
|
||||
|
||||
## omitted-end-tag
|
||||
|
||||
- Why: uniform-rule (a normalization is applied only where it is known safe for every reader: `oxc_formatter_css`'s AGENTS.md "Case")
|
||||
- Pin: `tests/fixtures/html/omitted-end-tag.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<ul>
|
||||
<li>one
|
||||
<li>two
|
||||
</ul>
|
||||
|
||||
<!-- ours -->
|
||||
<ul>
|
||||
<li>one
|
||||
<li>two
|
||||
</ul>
|
||||
|
||||
<!-- prettier -->
|
||||
<ul>
|
||||
<li>one</li>
|
||||
<li>two</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
An end tag the source omits where HTML closes the element (`</li>`, `</p>`) is not added.
|
||||
To a browser reading the page the end tag changes nothing, but in a template the place HTML closes it may sit in a branch:
|
||||
|
||||
```html
|
||||
{% if x %}<p>a{% else %}<p>b{% endif %}<div>
|
||||
<!-- an end tag added before the second <p> lands in the else branch: a stray </p>, which makes an empty p -->
|
||||
```
|
||||
|
||||
## pre-content-without-end-tag
|
||||
|
||||
- Why: semantics (whitespace in `pre`-like elements is content; prettier/prettier#16205)
|
||||
- Pin: `tests/fixtures/html/pre-content-without-end-tag.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<div><pre />
|
||||
<b>x</b></div>
|
||||
<pre><b>x</b> unclosed
|
||||
|
||||
<!-- ours -->
|
||||
<div>
|
||||
<pre />
|
||||
<b>x</b></div>
|
||||
<pre><b>x</b> unclosed
|
||||
|
||||
<!-- prettier -->
|
||||
<div>
|
||||
<pre />
|
||||
<b>x</b>
|
||||
</div>
|
||||
<pre>>
|
||||
```
|
||||
|
||||
A `pre`, `listing`, `plaintext` or `xmp` holding markup without its end tag keeps its content as written, up to where HTML closes it.
|
||||
`<pre />` reads as one: HTML ignores the `/` and the content up to an ancestor's end tag is inside it.
|
||||
Prettier reads `<pre />` as self-closing and re-indents that content, adding whitespace to what `pre` renders;
|
||||
a `pre` left open at the end loses its content to a stray `>`.
|
||||
|
||||
## tag-name-case
|
||||
|
||||
- Why: semantics (consumers other than an HTML parser tell names apart by case; prettier/prettier#16898)
|
||||
- Pin: `tests/fixtures/html/tag-name-case.xhtml`
|
||||
|
||||
```html
|
||||
<!-- input (an .xhtml file) -->
|
||||
<DIV CLASS="a"><P>Text</P></DIV>
|
||||
|
||||
<!-- ours -->
|
||||
<DIV CLASS="a"><P>Text</P></DIV>
|
||||
|
||||
<!-- prettier -->
|
||||
<div class="a"><p>Text</p></div>
|
||||
```
|
||||
|
||||
```js
|
||||
// input
|
||||
html`<A someThing="bla"></A>`;
|
||||
|
||||
// ours
|
||||
html`<A someThing="bla"></A>`;
|
||||
|
||||
// prettier
|
||||
html`<a someThing="bla"></a>`;
|
||||
```
|
||||
|
||||
Tag and attribute names keep their case where a reader other than an HTML parser may read them: embedded HTML and XHTML.
|
||||
In a `.html` file, whose reader is an HTML parser, the names Prettier knows print lowercase as it does (`src/tree/names.rs`).
|
||||
Prettier lowercases them in embedded HTML and XHTML too, by the language alone:
|
||||
- a framework's `html` tagged template is read by its own runtime, which tells `<A>` (a router component) from the element `<a>`;
|
||||
- a code block labelled `html` often holds a template of another language (a Vue `<Button>`);
|
||||
- an XML parser reads XHTML's names case-sensitively.
|
||||
|
||||
Vue and Angular keep every name as written, as Prettier does.
|
||||
|
||||
## prefixed-script-style
|
||||
|
||||
- Why: uniform-rule (same construct, same output: `<html:p>` and every other prefixed name, which Prettier lays out as an unknown element)
|
||||
- Pin: `tests/fixtures/html/prefixed-script-style.html`
|
||||
- Conformance: `html/basics/with-colon.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<div><html:style>.a{color:red}</html:style></div>
|
||||
<div>x<html:p>p</html:p>y</div>
|
||||
|
||||
<!-- ours -->
|
||||
<div><html:style>.a{color:red}</html:style></div>
|
||||
<div>x<html:p>p</html:p>y</div>
|
||||
|
||||
<!-- prettier -->
|
||||
<div>
|
||||
<html:style
|
||||
>.a{color:red}</html:style
|
||||
>
|
||||
</div>
|
||||
<div>x<html:p>p</html:p>y</div>
|
||||
```
|
||||
|
||||
In HTML, an element whose name has a `prefix:` is an unknown element, as browsers read it (the parser's DIVERGENCES.md, "Namespace prefixes"):
|
||||
`<html:script>` and `<html:style>` too, whose content is text.
|
||||
Prettier lays out every prefixed name so, but sees `script` / `style` through the prefix and puts them on their own lines.
|
||||
A `prefix:` comes from XML namespaces; only an XML parser (XHTML, XUL, a standalone SVG) reads `html:script` as a script.
|
||||
Angular's compiler reads a prefix as a namespace (`svg:rect`), and so does the Angular dialect.
|
||||
|
||||
## unquoted-value-with-slash
|
||||
|
||||
- Why: semantics (Prettier ends an unquoted value at `/`: `src=a.png/` becomes `src="a.png"`, another URL)
|
||||
- Pin: `tests/fixtures/html/unquoted-value-with-slash.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<img src=a.png/>
|
||||
|
||||
<!-- ours -->
|
||||
<img src="a.png/" />
|
||||
|
||||
<!-- prettier -->
|
||||
<img src="a.png" />
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<a href=https://www.mozilla.org/>favorite website</a>
|
||||
|
||||
<!-- ours -->
|
||||
<a href="https://www.mozilla.org/">favorite website</a>
|
||||
|
||||
<!-- prettier: error, Opening tag "a" not terminated -->
|
||||
```
|
||||
|
||||
In HTML, an unquoted attribute value runs to whitespace or `>`, as browsers read it (the parser's DIVERGENCES.md, "Unquoted attribute values").
|
||||
Prettier's parser also ends it at `/`: a value ending in `/` loses it and the tag self-closes,
|
||||
and a `/` inside the value (a URL) leaves the rest of the tag unreadable, a parse error
|
||||
(in a Markdown code block, the block is left as written).
|
||||
|
||||
## htm-closing-tag-as-written
|
||||
|
||||
- Why: uniform-rule (same construct, same output: an htm end tag Prettier prints as written outside a merged text)
|
||||
- Pin: `tests/fixtures/html/htm-closing-tag-as-written.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<span>a<b>x<//>c</span>
|
||||
|
||||
<!-- ours -->
|
||||
<span>a<b>x<//>c</span>
|
||||
|
||||
<!-- prettier -->
|
||||
<span>a<b>x</b>c</span>
|
||||
```
|
||||
|
||||
htm's `<//>`, an end tag closing the innermost element, prints as written.
|
||||
Prettier prints it as written too, except for an element it merges into the text around it, whose end tag it rebuilds from the element's name.
|
||||
|
||||
## end-tag-with-space-is-comment
|
||||
|
||||
- Why: semantics (HTML reads `</ div>` as a comment, so the element stays open)
|
||||
- Pin: `tests/fixtures/html/end-tag-with-space-is-comment.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<div>a</ div>b
|
||||
|
||||
<!-- ours -->
|
||||
<div>a</ div>b
|
||||
|
||||
<!-- prettier -->
|
||||
<div>a</div>
|
||||
b
|
||||
```
|
||||
|
||||
In HTML, `</` without a name right after it is a bogus comment, as in browsers, and prints as written.
|
||||
Vue's and Angular's compilers read `</ div>` as an end tag, and so do those dialects.
|
||||
Prettier reads it as an end tag and prints `</div>`, which closes the `div` there and moves what follows out of it.
|
||||
|
||||
## vue-element-gap-kind
|
||||
|
||||
- Why: semantics (Vue's `whitespace: 'condense'` drops whitespace with a line break between two elements and keeps a same-line one as a space)
|
||||
- Pin: `tests/fixtures/vue/vue-element-gap-kind.vue`
|
||||
|
||||
```vue
|
||||
<!-- input -->
|
||||
<template>
|
||||
<ul>
|
||||
<li>
|
||||
<router-link to="/using-store">/using-store</router-link> — <b>universal</b>
|
||||
<code>reactive()</code> stores.
|
||||
</li>
|
||||
</ul>
|
||||
<p><span>a long enough first element here</span> <span>and a second one that does not fit</span></p>
|
||||
</template>
|
||||
|
||||
<!-- ours -->
|
||||
<template>
|
||||
<ul>
|
||||
<li>
|
||||
<router-link to="/using-store">/using-store</router-link> —
|
||||
<b>universal</b>
|
||||
<code>reactive()</code> stores.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<span>a long enough first element here</span> <span
|
||||
>and a second one that does not fit</span
|
||||
>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<!-- prettier -->
|
||||
<template>
|
||||
<ul>
|
||||
<li>
|
||||
<router-link to="/using-store">/using-store</router-link> —
|
||||
<b>universal</b> <code>reactive()</code> stores.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
<span>a long enough first element here</span>
|
||||
<span>and a second one that does not fit</span>
|
||||
</p>
|
||||
</template>
|
||||
```
|
||||
|
||||
In Vue, whitespace between two sibling elements keeps its kind where it renders (between inline elements):
|
||||
a line break stays a line break and a same-line space stays a space that never breaks.
|
||||
Vue's compiler drops whitespace containing a line break between elements and condenses the rest to a space,
|
||||
so Prettier's output renders `universal reactive()` (a space the source did not render) and `hereand` (losing one).
|
||||
Where the render layer ignores the gap (next to a block), and at text edges, the layout is Prettier's.
|
||||
|
||||
## angular-glued-block-markers
|
||||
|
||||
- Why: semantics (whitespace added around a block marker renders between the content it joins; prettier/prettier#16577, prettier/prettier#19020)
|
||||
- Pin: `tests/fixtures/angular/angular-glued-block-markers.component.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
<p>a@if (x) {b}c</p>
|
||||
<span>@if (test) {text}</span>
|
||||
|
||||
<!-- ours -->
|
||||
<p>a@if (x) {b}c</p>
|
||||
<span>@if (test) {text}</span>
|
||||
|
||||
<!-- prettier -->
|
||||
<p>
|
||||
a
|
||||
@if (x) {
|
||||
b
|
||||
}
|
||||
c
|
||||
</p>
|
||||
<span>
|
||||
@if (test) {
|
||||
text
|
||||
}
|
||||
</span>
|
||||
```
|
||||
|
||||
An Angular control flow block makes no box: its markers (`@if (x) {`, `}`) render nothing,
|
||||
so the whitespace on both sides of a marker is one run between the content around it.
|
||||
Where that run is significant and empty, the markers stay glued to their neighbors;
|
||||
otherwise a block is laid out on its own lines, as Prettier does.
|
||||
Prettier always breaks around a block, which turns `abc` into `a b c` (and adds spaces inside the `span`).
|
||||
|
||||
## angular-block-name-with-line-break
|
||||
|
||||
- Why: semantics (Angular reads `@else⏎ if` as an unknown block, a compile error, and `@else if` as an else-if branch)
|
||||
- Pin: `tests/fixtures/angular/angular-block-name-with-line-break.component.html`
|
||||
|
||||
```html
|
||||
<!-- input -->
|
||||
@if (a) {
|
||||
x
|
||||
} @else
|
||||
if (b) {
|
||||
y
|
||||
}
|
||||
|
||||
<!-- ours -->
|
||||
@if (a) {
|
||||
x
|
||||
}
|
||||
@else
|
||||
if (b) {
|
||||
y
|
||||
}
|
||||
|
||||
<!-- prettier -->
|
||||
@if (a) {
|
||||
x
|
||||
} @else if (b) {
|
||||
y
|
||||
}
|
||||
```
|
||||
|
||||
A block name holding a line break prints as written, on its own line, as an unknown block: Angular's compiler reports `Unrecognized block @else`.
|
||||
Prettier collapses the name to `else if` and joins it to the previous block, turning the error into an else-if branch.
|
||||
Whitespace without a line break (`@else if`) is collapsed, as Angular reads it.
|
||||
@@ -0,0 +1,15 @@
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use oxc_formatter_tests::{GenerateConfig, generate_tests};
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let dest_path = PathBuf::from(out_dir).join("generated_tests.rs");
|
||||
|
||||
let config = GenerateConfig { extensions: &["html", "vue", "xhtml"] };
|
||||
|
||||
generate_tests(&dest_path, Path::new("tests/fixtures"), &config).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#![expect(clippy::print_stdout)]
|
||||
//! Format a file and print the result.
|
||||
//!
|
||||
//! ```bash
|
||||
//! cargo run -p oxc_formatter_htmlish --example htmlish_formatter [filename]
|
||||
//! cargo run -p oxc_formatter_htmlish --example htmlish_formatter -- --print-width 100 [filename]
|
||||
//! cargo run -p oxc_formatter_htmlish --example htmlish_formatter -- --use-tabs --bracket-same-line \
|
||||
//! --single-attribute-per-line --html-whitespace-sensitivity ignore [filename]
|
||||
//! DUMP_IR=1 cargo run -p oxc_formatter_htmlish --example htmlish_formatter [filename]
|
||||
//! ```
|
||||
|
||||
use std::fs;
|
||||
|
||||
use pico_args::Arguments;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_formatter_core::{IndentStyle, LineWidth};
|
||||
use oxc_formatter_htmlish::{HtmlishFormatOptions, HtmlishVariant, WhitespaceSensitivity};
|
||||
|
||||
fn main() -> Result<(), String> {
|
||||
let mut args = Arguments::from_env();
|
||||
let print_width = args.opt_value_from_str::<&'static str, u16>("--print-width").unwrap_or(None);
|
||||
let use_tabs = args.contains("--use-tabs");
|
||||
let bracket_same_line = args.contains("--bracket-same-line");
|
||||
let single_attribute_per_line = args.contains("--single-attribute-per-line");
|
||||
let whitespace_sensitivity = match args
|
||||
.opt_value_from_str::<&'static str, String>("--html-whitespace-sensitivity")
|
||||
.unwrap_or(None)
|
||||
.as_deref()
|
||||
{
|
||||
Some("strict") => WhitespaceSensitivity::Strict,
|
||||
Some("ignore") => WhitespaceSensitivity::Ignore,
|
||||
_ => WhitespaceSensitivity::Css,
|
||||
};
|
||||
let name = args.free_from_str().unwrap_or_else(|_| "test.html".to_string());
|
||||
let source_text = fs::read_to_string(&name).map_err(|_| format!("Missing '{name}'"))?;
|
||||
let variant = match std::path::Path::new(&name).extension().and_then(|ext| ext.to_str()) {
|
||||
Some("vue") => HtmlishVariant::Vue,
|
||||
Some("xhtml" | "xht") => HtmlishVariant::Xhtml,
|
||||
_ if name.ends_with(".component.html") => HtmlishVariant::Angular,
|
||||
_ => HtmlishVariant::Html,
|
||||
};
|
||||
let options = HtmlishFormatOptions {
|
||||
line_width: LineWidth::try_from(print_width.unwrap_or(80)).unwrap(),
|
||||
indent_style: if use_tabs { IndentStyle::Tab } else { IndentStyle::Space },
|
||||
whitespace_sensitivity,
|
||||
bracket_same_line,
|
||||
single_attribute_per_line,
|
||||
variant,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let allocator = Allocator::new();
|
||||
let formatted = oxc_formatter_htmlish::format(&allocator, &source_text, options)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if std::env::var("DUMP_IR").is_ok() {
|
||||
println!("{:#?}", formatted.document());
|
||||
}
|
||||
print!("{}", formatted.print().map_err(|error| error.to_string())?.into_code());
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
use oxc_formatter_core::{FormatContext, SourceText};
|
||||
|
||||
use crate::{options::HtmlishFormatOptions, tree::Tree};
|
||||
|
||||
/// Formatting context for htmlish.
|
||||
pub struct HtmlishFormatContext<'a> {
|
||||
options: HtmlishFormatOptions,
|
||||
source_text: SourceText<'a>,
|
||||
tree: &'a Tree<'a>,
|
||||
}
|
||||
|
||||
impl<'a> HtmlishFormatContext<'a> {
|
||||
pub fn new(options: HtmlishFormatOptions, tree: &'a Tree<'a>) -> Self {
|
||||
Self { options, source_text: SourceText::new(tree.source()), tree }
|
||||
}
|
||||
|
||||
/// The preprocess table, with the arena lifetime so it outlives borrows of the formatter.
|
||||
pub fn tree(&self) -> &'a Tree<'a> {
|
||||
self.tree
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatContext for HtmlishFormatContext<'_> {
|
||||
type Options = HtmlishFormatOptions;
|
||||
|
||||
fn options(&self) -> &Self::Options {
|
||||
&self.options
|
||||
}
|
||||
|
||||
fn source_code(&self) -> &str {
|
||||
&self.source_text
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_formatter_core::{
|
||||
Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, Formatter, InputKind,
|
||||
builders::{empty_line, hard_line_break, text},
|
||||
spec::{FrontMatter, blank_front_matter, parse_front_matter, split_bom},
|
||||
write, write_front_matter,
|
||||
};
|
||||
use oxc_htmlish_parser::{
|
||||
Dialect, Parser,
|
||||
ast::{NodeId, Root},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
context::HtmlishFormatContext,
|
||||
options::HtmlishFormatOptions,
|
||||
print::{self, HtmlishFormatter},
|
||||
tree::Tree,
|
||||
};
|
||||
|
||||
/// Parse `source_text` as `options.variant` and build its formatter IR.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an [`OxcDiagnostic`] when the parser reports broken structure.
|
||||
pub fn format<'a>(
|
||||
allocator: &'a Allocator,
|
||||
source_text: &str,
|
||||
options: HtmlishFormatOptions,
|
||||
) -> Result<Formatted<'a, HtmlishFormatContext<'a>>, OxcDiagnostic> {
|
||||
// A service-less session: front matter stays as-is.
|
||||
// Hosts that install services (oxfmt) use [`format_with_session`].
|
||||
format_with_session(
|
||||
&FormatSession::new(allocator, InputKind::PhysicalFile),
|
||||
source_text,
|
||||
options,
|
||||
)
|
||||
}
|
||||
|
||||
/// Like [`format()`], but on a caller-supplied [`FormatSession`]:
|
||||
/// the root whose session carries the host's `SessionServices` (front matter dispatch, Tailwind sorting).
|
||||
///
|
||||
/// # Errors
|
||||
/// Same as [`format()`].
|
||||
pub fn format_with_session<'a>(
|
||||
session: &FormatSession<'a>,
|
||||
source_text: &str,
|
||||
options: HtmlishFormatOptions,
|
||||
) -> Result<Formatted<'a, HtmlishFormatContext<'a>>, OxcDiagnostic> {
|
||||
debug_assert!(
|
||||
session.input_kind() == InputKind::PhysicalFile,
|
||||
"format_with_session is the physical-root entry; embedded inputs go through format_to_ir"
|
||||
);
|
||||
let parsed = parse_for_format(session.allocator(), source_text, &options)?;
|
||||
|
||||
let context = build_context(session.allocator(), &parsed, options, true);
|
||||
// A root `Document` owns a fresh Tailwind class scope
|
||||
let session = session.with_new_tailwind_scope();
|
||||
let mut state = FormatState::new_with_session(context, session.clone());
|
||||
// Pre-allocate: measured on 771 HTML files (markup_fmt, biome, deno, Prettier tests, large spec / wikipedia pages),
|
||||
// 0.75x source bytes plus a 1024-element floor avoids reallocation for 97.8% of the corpus.
|
||||
let capacity = (parsed.source.len() * 3 / 4).max(1024);
|
||||
let mut buffer = Formatter::with_capacity(capacity, &mut state);
|
||||
|
||||
write!(&mut buffer, FormatHtmlishRoot { parsed: &parsed });
|
||||
|
||||
let elements = buffer.into_vec();
|
||||
let context = state.into_context();
|
||||
|
||||
let ir = Document::new(elements, session.take_sorted_tailwind_classes());
|
||||
|
||||
Ok(Formatted::new(ir, context))
|
||||
}
|
||||
|
||||
/// [`parse_for_format`] output: the AST, plus the envelope [`format()`] prints around it.
|
||||
pub struct ParsedHtmlish<'a> {
|
||||
pub root: &'a Root<'a>,
|
||||
/// The arena source every span indexes into, `\r` normalized away and the front matter blanked.
|
||||
pub source: &'a str,
|
||||
/// The leading `---` / `+++` block (it is not markup), sliced from the unblanked source.
|
||||
pub front_matter: Option<FrontMatter<'a>>,
|
||||
has_bom: bool,
|
||||
}
|
||||
|
||||
/// Parse `source_text` the way the formatter does, for callers that inspect the AST
|
||||
/// (e.g. a harness comparing what the source held against the formatted output).
|
||||
///
|
||||
/// [`format()`] goes through this too, so what a caller sees is exactly what gets formatted.
|
||||
/// Owns the BOM split and `\r` normalization.
|
||||
///
|
||||
/// # Errors
|
||||
/// Same as [`format()`].
|
||||
pub fn parse_for_format<'a>(
|
||||
allocator: &'a Allocator,
|
||||
source_text: &str,
|
||||
options: &HtmlishFormatOptions,
|
||||
) -> Result<ParsedHtmlish<'a>, OxcDiagnostic> {
|
||||
let (has_bom, stripped) = split_bom(source_text);
|
||||
parse_root(allocator, stripped, has_bom, options.variant.to_dialect()).map_err(|mut err| {
|
||||
oxc_formatter_core::rebase_labels(&mut err.labels, source_text, stripped);
|
||||
err
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse `source_text` and build the formatter IR for embedding into another
|
||||
/// formatter's document (dispatcher path).
|
||||
///
|
||||
/// Unlike [`format()`], this:
|
||||
/// - allocates from the session's shared arena and `GroupId` space, so the IR lives as long as the parent's document
|
||||
/// - emits neither a BOM nor the trailing newline
|
||||
///
|
||||
/// # Errors
|
||||
/// Same as [`format()`], and when a fragment starts with front matter.
|
||||
pub fn format_to_ir<'a>(
|
||||
session: &FormatSession<'a>,
|
||||
source_text: &str,
|
||||
options: HtmlishFormatOptions,
|
||||
) -> Result<EmbeddedIr<'a>, OxcDiagnostic> {
|
||||
let allocator = session.allocator();
|
||||
// `FormatSession::dispatch` never hands a BOM-headed input to an embedded part
|
||||
let parsed = parse_root(allocator, source_text, false, options.variant.to_dialect())?;
|
||||
if parsed.front_matter.is_some() && !session.input_kind().owns_front_matter() {
|
||||
// A fragment never acquires file envelope semantics
|
||||
return Err(OxcDiagnostic::error(
|
||||
"Front matter in an HTML fragment; the part is preserved as-is",
|
||||
));
|
||||
}
|
||||
|
||||
let context = build_context(allocator, &parsed, options, false);
|
||||
let mut state = FormatState::new_with_session(context, session.clone());
|
||||
let mut buffer = Formatter::new(&mut state);
|
||||
|
||||
write!(&mut buffer, FormatHtmlishEmbedded { parsed: &parsed });
|
||||
|
||||
Ok(EmbeddedIr { ir: buffer.into_vec() })
|
||||
}
|
||||
|
||||
/// Parse the source into the AST, bailing out on any diagnostic.
|
||||
///
|
||||
/// Copies the source into the arena so every slice taken from it carries `'a`.
|
||||
/// Entries own the BOM strip; this layer assumes BOM-free input (see [`split_bom`]).
|
||||
fn parse_root<'a>(
|
||||
allocator: &'a Allocator,
|
||||
source_text: &str,
|
||||
has_bom: bool,
|
||||
dialect: Dialect,
|
||||
) -> Result<ParsedHtmlish<'a>, OxcDiagnostic> {
|
||||
// The printer slices verbatim text from the source, and the IR forbids `\r`.
|
||||
let unblanked: &'a str =
|
||||
allocator.alloc_str(&oxc_formatter_core::normalize_newlines(source_text, ['\r']));
|
||||
// Blanked byte for byte, so every span still indexes the same source.
|
||||
let front_matter = parse_front_matter(unblanked);
|
||||
let source: &'a str = match &front_matter {
|
||||
Some(fm) => allocator.alloc_str(&blank_front_matter(unblanked, fm.raw.len())),
|
||||
None => unblanked,
|
||||
};
|
||||
let root = Parser::new(allocator, source, dialect).parse().map_err(|diagnostic| {
|
||||
let mut err = OxcDiagnostic::error(format!("Syntax error: {diagnostic}"))
|
||||
.with_label(oxc_span::Span::new(diagnostic.span.start, diagnostic.span.end));
|
||||
oxc_formatter_core::restore_crlf_labels(&mut err.labels, source_text);
|
||||
err
|
||||
})?;
|
||||
Ok(ParsedHtmlish { root: allocator.alloc(root), source, front_matter, has_bom })
|
||||
}
|
||||
|
||||
/// The context over the preprocess table [`Tree::build`] computes from the AST.
|
||||
fn build_context<'a>(
|
||||
allocator: &'a Allocator,
|
||||
parsed: &ParsedHtmlish<'a>,
|
||||
options: HtmlishFormatOptions,
|
||||
standalone: bool,
|
||||
) -> HtmlishFormatContext<'a> {
|
||||
let tree = allocator.alloc(Tree::build(
|
||||
allocator,
|
||||
parsed.root,
|
||||
parsed.source,
|
||||
options.whitespace_sensitivity,
|
||||
options.variant,
|
||||
standalone,
|
||||
));
|
||||
HtmlishFormatContext::new(options, tree)
|
||||
}
|
||||
|
||||
/// Emits the front matter and the nodes with the final newline.
|
||||
struct FormatHtmlishRoot<'b, 'a> {
|
||||
parsed: &'b ParsedHtmlish<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Format<'a, HtmlishFormatContext<'a>> for FormatHtmlishRoot<'_, 'a> {
|
||||
fn fmt(&self, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
if self.parsed.has_bom {
|
||||
write!(f, text("\u{feff}"));
|
||||
}
|
||||
|
||||
write_document(self.parsed, f);
|
||||
|
||||
// Prettier prints an empty document as the empty string, not `\n`.
|
||||
let has_body = f.context().tree()[NodeId::DOCUMENT].first_child.is_some();
|
||||
if self.parsed.front_matter.is_some() || has_body {
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Emits the front matter and the nodes only;
|
||||
/// no BOM, no final newline (the parent document owns the surrounding layout).
|
||||
struct FormatHtmlishEmbedded<'b, 'a> {
|
||||
parsed: &'b ParsedHtmlish<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Format<'a, HtmlishFormatContext<'a>> for FormatHtmlishEmbedded<'_, 'a> {
|
||||
fn fmt(&self, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
write_document(self.parsed, f);
|
||||
}
|
||||
}
|
||||
|
||||
/// The front matter (a blank line after it when a body follows) and the nodes.
|
||||
fn write_document<'a>(parsed: &ParsedHtmlish<'a>, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
let has_body = f.context().tree()[NodeId::DOCUMENT].first_child.is_some();
|
||||
if let Some(fm) = &parsed.front_matter {
|
||||
write_front_matter(fm, &["yaml", "toml"], f);
|
||||
if has_body {
|
||||
write!(f, empty_line());
|
||||
}
|
||||
}
|
||||
if has_body {
|
||||
print::write_root(f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! HTML (and HTML-based template language) formatter built on top of `oxc_formatter_core`.
|
||||
//!
|
||||
//! Parses with `oxc-htmlish-parser` and prints Prettier-compatible output.
|
||||
|
||||
mod context;
|
||||
mod format;
|
||||
mod options;
|
||||
mod print;
|
||||
mod rules;
|
||||
mod tree;
|
||||
|
||||
pub use crate::{
|
||||
context::HtmlishFormatContext,
|
||||
format::{ParsedHtmlish, format, format_to_ir, format_with_session, parse_for_format},
|
||||
options::{HtmlishFormatOptions, HtmlishVariant, WhitespaceSensitivity},
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
use oxc_formatter_core::{
|
||||
CoreFormatOptions, FormatOptions, IndentStyle, IndentWidth, LineEnding, LineWidth,
|
||||
};
|
||||
use oxc_htmlish_parser::Dialect;
|
||||
|
||||
/// HTML-based template language variant.
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
|
||||
pub enum HtmlishVariant {
|
||||
/// Prettier's `parser: html` equivalent.
|
||||
#[default]
|
||||
Html,
|
||||
/// Prettier's `parser: vue` equivalent.
|
||||
Vue,
|
||||
/// Prettier's `parser: angular` equivalent.
|
||||
Angular,
|
||||
/// Prettier's `parser: html` on an XHTML file (`.xhtml`, `.xht`), which an XML parser may read:
|
||||
/// read and printed as HTML, but the doctype keeps its case.
|
||||
Xhtml,
|
||||
}
|
||||
|
||||
impl HtmlishVariant {
|
||||
pub(crate) fn to_dialect(self) -> Dialect {
|
||||
match self {
|
||||
Self::Html | Self::Xhtml => Dialect::Html,
|
||||
Self::Vue => Dialect::Vue,
|
||||
Self::Angular => Dialect::Angular,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Format options for htmlish (HTML and the template languages built on it).
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
|
||||
pub struct HtmlishFormatOptions {
|
||||
pub indent_style: IndentStyle,
|
||||
pub indent_width: IndentWidth,
|
||||
pub line_width: LineWidth,
|
||||
pub line_ending: LineEnding,
|
||||
pub variant: HtmlishVariant,
|
||||
/// Mirrors Prettier's `htmlWhitespaceSensitivity`.
|
||||
pub whitespace_sensitivity: WhitespaceSensitivity,
|
||||
/// Put the `>` of a multi-line start tag at the end of the last line. Mirrors Prettier's `bracketSameLine`.
|
||||
pub bracket_same_line: bool,
|
||||
/// Mirrors Prettier's `singleAttributePerLine`.
|
||||
pub single_attribute_per_line: bool,
|
||||
}
|
||||
|
||||
/// Which whitespace between elements is significant.
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
|
||||
pub enum WhitespaceSensitivity {
|
||||
/// As the element's default CSS `display` says.
|
||||
#[default]
|
||||
Css,
|
||||
/// Every element is inline: all whitespace is significant.
|
||||
Strict,
|
||||
/// Every element is a block: no whitespace is significant.
|
||||
Ignore,
|
||||
}
|
||||
|
||||
impl FormatOptions for HtmlishFormatOptions {
|
||||
fn indent_style(&self) -> IndentStyle {
|
||||
self.indent_style
|
||||
}
|
||||
|
||||
fn indent_width(&self) -> IndentWidth {
|
||||
self.indent_width
|
||||
}
|
||||
|
||||
fn line_width(&self) -> LineWidth {
|
||||
self.line_width
|
||||
}
|
||||
|
||||
fn line_ending(&self) -> LineEnding {
|
||||
self.line_ending
|
||||
}
|
||||
|
||||
fn apply_core(&mut self, core: CoreFormatOptions) {
|
||||
self.indent_style = core.indent_style;
|
||||
self.indent_width = core.indent_width;
|
||||
self.line_width = core.line_width;
|
||||
self.line_ending = core.line_ending;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
//! Attributes: the list in the start tag (line breaks, `prettier-ignore-attribute`),
|
||||
//! each one's name as written, then the value, which a few attributes get re-printed by their own grammar.
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use cow_utils::CowUtils;
|
||||
|
||||
use oxc_formatter_core::{
|
||||
Format, arena_cow_str,
|
||||
builders::{
|
||||
group, hard_line_break, if_group_breaks, if_group_fits_on_line, indent, soft_block_indent,
|
||||
soft_line_break, soft_line_break_or_space, space, text,
|
||||
},
|
||||
spec::is_suppression_marker,
|
||||
write,
|
||||
};
|
||||
use oxc_htmlish_parser::ast::{Attribute, NodeId, NodeKind};
|
||||
|
||||
use super::{HtmlishFormatter, format_with, tag::opening_end_borrowed};
|
||||
use crate::{
|
||||
context::HtmlishFormatContext,
|
||||
tree::{Tree, collapse_whitespace},
|
||||
};
|
||||
|
||||
/// `<!-- prettier-ignore-attribute -->` right before the element: the attribute names it lists (all when none),
|
||||
/// which print as written.
|
||||
fn ignored_attributes<'a>(t: &Tree<'a>, id: NodeId) -> Option<&'a str> {
|
||||
let NodeKind::Comment(comment) = t.kind(t[id].prev?) else { return None };
|
||||
if comment.bogus {
|
||||
return None;
|
||||
}
|
||||
let content = comment.content.slice(t.source()).trim_ascii();
|
||||
let (marker, names) =
|
||||
content.split_once(|c: char| c.is_ascii_whitespace()).unwrap_or((content, ""));
|
||||
marker.strip_suffix("-attribute").is_some_and(is_suppression_marker).then_some(names)
|
||||
}
|
||||
|
||||
pub fn write_attributes<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
let item = &t[id];
|
||||
let Some(el) = t.element(id) else { return };
|
||||
// `<br />`
|
||||
let slash = el.self_closing || el.void;
|
||||
if el.attributes.is_empty() {
|
||||
if slash {
|
||||
write!(f, space());
|
||||
}
|
||||
return;
|
||||
}
|
||||
let options = *f.options();
|
||||
|
||||
let force_not_to_break = t.tag(id) == "script"
|
||||
&& matches!(el.attributes.as_slice(), [only] if t.attribute_is(only.name.slice(t.source()), "src"))
|
||||
&& item.first_child.is_none();
|
||||
let per_line = options.single_attribute_per_line && el.attributes.len() > 1;
|
||||
let ignored = ignored_attributes(t, id);
|
||||
let is_ignored = |name: &str| {
|
||||
ignored.is_some_and(|names| {
|
||||
names.is_empty() || names.split_ascii_whitespace().any(|n| t.attribute_is(name, n))
|
||||
})
|
||||
};
|
||||
|
||||
write!(
|
||||
f,
|
||||
indent(&format_with(|f| {
|
||||
if force_not_to_break {
|
||||
write!(f, space());
|
||||
} else {
|
||||
write!(f, soft_line_break_or_space());
|
||||
}
|
||||
for (i, attribute) in el.attributes.iter().enumerate() {
|
||||
if i > 0 {
|
||||
if per_line {
|
||||
write!(f, hard_line_break());
|
||||
} else {
|
||||
write!(f, soft_line_break_or_space());
|
||||
}
|
||||
}
|
||||
let name = attribute.name.slice(t.source());
|
||||
if is_ignored(name) {
|
||||
write!(f, text(attribute.span.slice(t.source())));
|
||||
} else {
|
||||
write_attribute(t, id, attribute, f);
|
||||
}
|
||||
}
|
||||
}))
|
||||
);
|
||||
|
||||
let hug_end = opening_end_borrowed(t, id)
|
||||
|| (item.self_closing && t[t.parent(id)].borrows.last_child_closing_end)
|
||||
|| force_not_to_break;
|
||||
if hug_end || options.bracket_same_line {
|
||||
if slash {
|
||||
write!(f, space());
|
||||
}
|
||||
} else if slash {
|
||||
write!(f, soft_line_break_or_space());
|
||||
} else {
|
||||
write!(f, soft_line_break());
|
||||
}
|
||||
}
|
||||
|
||||
fn write_attribute<'a>(
|
||||
t: &'a Tree<'a>,
|
||||
element: NodeId,
|
||||
attribute: &Attribute<'a>,
|
||||
f: &mut HtmlishFormatter<'_, 'a>,
|
||||
) {
|
||||
let source = t.source();
|
||||
let name = attribute.name.slice(source);
|
||||
let is = |known| t.attribute_is(name, known);
|
||||
write!(f, text(t.attribute_name(element, name)));
|
||||
let Some(value) = attribute.value else { return };
|
||||
let value = value.span.slice(source);
|
||||
write!(f, "=");
|
||||
|
||||
let tag = t.tag(element);
|
||||
// A class list that collapses to nothing prints as written;
|
||||
// the names are split at ASCII whitespace only (DIVERGENCES.md#class-ascii-whitespace).
|
||||
if is("class") && !value.contains("{{") && !value.trim_ascii().is_empty() {
|
||||
let names = collapse_whitespace(f.allocator(), unescaped(value, f));
|
||||
write!(f, ["\"", text(quoted_piece(names, f)), "\""]);
|
||||
} else if is("srcset")
|
||||
&& matches!(tag, "img" | "source")
|
||||
&& let Some(candidates) = parse_srcset(unescaped(value, f))
|
||||
{
|
||||
write_srcset(&candidates, f);
|
||||
} else if is("allow") && tag == "iframe" && !value.contains("{{") {
|
||||
write_permissions_policy(unescaped(value, f), f);
|
||||
} else {
|
||||
write!(f, AttributeValue(value));
|
||||
}
|
||||
}
|
||||
|
||||
/// A quoted attribute value: `"` unless the value holds more `"` than `'`, the chosen quote escaped.
|
||||
/// Quote entities are read back first, so the choice does not depend on how the source quoted.
|
||||
pub struct AttributeValue<'a>(pub &'a str);
|
||||
|
||||
impl<'a> Format<'a, HtmlishFormatContext<'a>> for AttributeValue<'a> {
|
||||
fn fmt(&self, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
let raw = unescaped(self.0, f);
|
||||
let doubles = raw.bytes().filter(|&b| b == b'"').count();
|
||||
let singles = raw.bytes().filter(|&b| b == b'\'').count();
|
||||
let (quote, escaped) = if doubles > singles { ("'", "'") } else { ("\"", """) };
|
||||
let printed = arena_cow_str(&raw.cow_replace(quote, escaped), f);
|
||||
write!(f, [text(quote), text(printed), text(quote)]);
|
||||
}
|
||||
}
|
||||
|
||||
/// `'` and `"` read back.
|
||||
fn unescaped<'a>(value: &'a str, f: &HtmlishFormatter<'_, 'a>) -> &'a str {
|
||||
if !value.contains('&') {
|
||||
return value;
|
||||
}
|
||||
let apos = value.cow_replace("'", "'");
|
||||
match apos.cow_replace(""", "\"") {
|
||||
Cow::Borrowed(_) => arena_cow_str(&apos, f),
|
||||
Cow::Owned(both) => f.allocator().alloc_str(&both),
|
||||
}
|
||||
}
|
||||
|
||||
/// `"` re-escaped for a value printed in `"`.
|
||||
fn quoted_piece<'a>(piece: &'a str, f: &HtmlishFormatter<'_, 'a>) -> &'a str {
|
||||
arena_cow_str(&piece.cow_replace('"', """), f)
|
||||
}
|
||||
|
||||
/// Prettier's `printExpand`: `content` on its own indented lines when the group breaks.
|
||||
fn write_expanded<'a>(
|
||||
content: &impl Format<'a, HtmlishFormatContext<'a>>,
|
||||
f: &mut HtmlishFormatter<'_, 'a>,
|
||||
) {
|
||||
write!(f, ["\"", group(&soft_block_indent(content)), "\""]);
|
||||
}
|
||||
|
||||
struct Candidate<'a> {
|
||||
url: &'a str,
|
||||
/// The descriptor's number as printed, and its unit (`w` / `h` / `x`).
|
||||
descriptor: Option<(String, &'a str)>,
|
||||
}
|
||||
|
||||
/// One candidate per line when long, descriptors aligned on their decimal point.
|
||||
fn write_srcset<'a>(candidates: &[Candidate<'a>], f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
let max_url = candidates.iter().map(|c| c.url.len()).max().unwrap_or(0);
|
||||
let left = |number: &str| number.find('.').unwrap_or(number.len());
|
||||
let max_left =
|
||||
candidates.iter().filter_map(|c| c.descriptor.as_ref().map(|d| left(&d.0))).max();
|
||||
let max_left = max_left.unwrap_or(0);
|
||||
let padding: &'a str = f.allocator().alloc_str(&" ".repeat(max_url + 1 + max_left));
|
||||
write_expanded(
|
||||
&format_with(|f| {
|
||||
for (i, c) in candidates.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, [",", soft_line_break_or_space()]);
|
||||
}
|
||||
let url = quoted_piece(c.url, f);
|
||||
write!(f, text(url));
|
||||
if let Some((number, unit)) = &c.descriptor {
|
||||
let padding = &padding[..max_url - c.url.len() + 1 + max_left - left(number)];
|
||||
let number = f.allocator().alloc_str(number);
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
if_group_breaks(&text(padding)),
|
||||
if_group_fits_on_line(&space()),
|
||||
text(number),
|
||||
text(unit)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
f,
|
||||
);
|
||||
}
|
||||
|
||||
/// `@prettier/parse-srcset`: `None` for what it rejects (a bad descriptor, or no candidate),
|
||||
/// and for mixed descriptor kinds, which Prettier does not print either.
|
||||
fn parse_srcset(input: &str) -> Option<Vec<Candidate<'_>>> {
|
||||
let is_ws = |c: char| c.is_ascii_whitespace();
|
||||
let mut candidates = vec![];
|
||||
let mut kind = None;
|
||||
let mut rest = input;
|
||||
loop {
|
||||
rest = rest.trim_start_matches(|c| is_ws(c) || c == ',');
|
||||
if rest.is_empty() {
|
||||
break;
|
||||
}
|
||||
let url_end = rest.find(is_ws).unwrap_or(rest.len());
|
||||
let mut url = &rest[..url_end];
|
||||
rest = &rest[url_end..];
|
||||
let descriptors = if url.ends_with(',') {
|
||||
url = url.trim_end_matches(',');
|
||||
""
|
||||
} else {
|
||||
// Descriptors up to the next `,` outside parentheses.
|
||||
rest = rest.trim_ascii_start();
|
||||
let mut depth = 0;
|
||||
let mut end = rest.len();
|
||||
for (i, c) in rest.char_indices() {
|
||||
match c {
|
||||
'(' => depth += 1,
|
||||
')' if depth > 0 => depth -= 1,
|
||||
',' if depth == 0 => {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let descriptors = &rest[..end];
|
||||
rest = rest.get(end + 1..).unwrap_or("");
|
||||
descriptors
|
||||
};
|
||||
let mut descriptor = None;
|
||||
for d in descriptors.split_ascii_whitespace() {
|
||||
let unit = d.chars().last()?;
|
||||
let value = &d[..d.len() - unit.len_utf8()];
|
||||
let parsed = match unit {
|
||||
'w' | 'h' if value.bytes().all(|b| b.is_ascii_digit()) => {
|
||||
value.parse::<f64>().ok().filter(|&v| v > 0.0)
|
||||
}
|
||||
// A zero density is kept (DIVERGENCES.md#srcset-zero-density).
|
||||
'x' if is_floating_point(value) => value.parse::<f64>().ok().filter(|&v| v >= 0.0),
|
||||
_ => None,
|
||||
}?;
|
||||
let parsed = zmij_ecma::Buffer::new().format(parsed).to_owned();
|
||||
// One descriptor per candidate and one kind per list:
|
||||
// a second is an error to Prettier or a mix it rejects.
|
||||
if descriptor.is_some() || kind.is_some_and(|kind| kind != unit) {
|
||||
return None;
|
||||
}
|
||||
kind = Some(unit);
|
||||
descriptor = Some((parsed, &d[d.len() - unit.len_utf8()..]));
|
||||
}
|
||||
candidates.push(Candidate { url, descriptor });
|
||||
}
|
||||
(!candidates.is_empty()).then_some(candidates)
|
||||
}
|
||||
|
||||
/// `/^-?(?:[0-9]+|[0-9]*\.[0-9]+)(?:[eE][+-]?[0-9]+)?$/`
|
||||
fn is_floating_point(value: &str) -> bool {
|
||||
let value = value.strip_prefix('-').unwrap_or(value);
|
||||
let (mantissa, exponent) = match value.find(['e', 'E']) {
|
||||
Some(i) => (&value[..i], Some(&value[i + 1..])),
|
||||
None => (value, None),
|
||||
};
|
||||
let digits = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit());
|
||||
let mantissa_ok = match mantissa.split_once('.') {
|
||||
Some((int, frac)) => (int.is_empty() || digits(int)) && digits(frac),
|
||||
None => digits(mantissa),
|
||||
};
|
||||
let exponent_ok = exponent.is_none_or(|e| digits(e.strip_prefix(['+', '-']).unwrap_or(e)));
|
||||
mantissa_ok && exponent_ok
|
||||
}
|
||||
|
||||
/// `allow` (permissions policy): one directive per line when long, `;` between them.
|
||||
fn write_permissions_policy<'a>(value: &'a str, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
let directives: Vec<&'a str> = value
|
||||
.split(';')
|
||||
.map(|token| collapse_whitespace(f.allocator(), token))
|
||||
.filter(|directive| !directive.is_empty())
|
||||
.map(|directive| quoted_piece(directive, f))
|
||||
.collect();
|
||||
if directives.is_empty() {
|
||||
write!(f, "\"\"");
|
||||
return;
|
||||
}
|
||||
write_expanded(
|
||||
&format_with(|f| {
|
||||
for (i, directive) in directives.iter().enumerate() {
|
||||
write!(f, text(directive));
|
||||
if i + 1 == directives.len() {
|
||||
write!(f, if_group_breaks(&text(";")));
|
||||
} else {
|
||||
write!(f, [";", soft_line_break_or_space()]);
|
||||
}
|
||||
}
|
||||
}),
|
||||
f,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Angular control flow blocks: transparent, their markers print no box (`@if (a) {` … `}`).
|
||||
|
||||
use oxc_formatter_core::{
|
||||
builders::{hard_line_break, indent, text},
|
||||
write,
|
||||
};
|
||||
use oxc_htmlish_parser::ast::{Block, NodeId, NodeKind};
|
||||
|
||||
use super::{
|
||||
HtmlishFormatter,
|
||||
children::{Between, write_children},
|
||||
format_with,
|
||||
};
|
||||
use oxc_allocator::{Allocator, StringBuilder};
|
||||
|
||||
use crate::tree::{Tree, collapse_whitespace};
|
||||
|
||||
/// Next to a block marker: glued where the run is significant and empty (DIVERGENCES.md#angular-glued-block-markers),
|
||||
/// a space before a block that continues the previous one (`} @else {`), a line break otherwise.
|
||||
pub fn between_block(t: &Tree<'_>, prev: NodeId, next: NodeId) -> Between {
|
||||
let (p, n) = (&t[prev], &t[next]);
|
||||
// The run continues into a block's content past its marker.
|
||||
let spaces = p.has_trailing_spaces
|
||||
|| n.has_leading_spaces
|
||||
|| (t.is_block(prev) && p.last_child.is_some_and(|last| t[last].has_trailing_spaces))
|
||||
|| (t.is_block(next) && n.first_child.is_some_and(|first| t[first].has_leading_spaces));
|
||||
if n.leading_sensitive && !spaces {
|
||||
Between::Nothing
|
||||
} else if continues_block(t, prev, next) {
|
||||
Between::Space
|
||||
} else {
|
||||
Between::Hard
|
||||
}
|
||||
}
|
||||
|
||||
/// `next` is a block that continues `prev` (`@if` … `@else`), printed after its `}`.
|
||||
fn continues_block(t: &Tree<'_>, prev: NodeId, next: NodeId) -> bool {
|
||||
let (NodeKind::Block(prev), NodeKind::Block(next)) = (t.kind(prev), t.kind(next)) else {
|
||||
return false;
|
||||
};
|
||||
let next = block_name(t, next);
|
||||
match block_name(t, prev) {
|
||||
"if" | "else if" => matches!(next, "else if" | "else"),
|
||||
"for" => next == "empty",
|
||||
"defer" | "placeholder" | "loading" | "error" => {
|
||||
matches!(next, "placeholder" | "loading" | "error")
|
||||
}
|
||||
"boundary" => next == "error",
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The block's name, whitespace collapsed unless it spans lines (`else\n if` is no `else if`, DIVERGENCES.md#angular-block-name-with-line-break).
|
||||
fn block_name<'a>(t: &'a Tree<'a>, block: &Block<'_>) -> &'a str {
|
||||
let name = block.name.slice(t.source());
|
||||
if name.contains('\n') { name } else { t.collapsed(block.name) }
|
||||
}
|
||||
|
||||
pub fn write_block<'a>(
|
||||
t: &'a Tree<'a>,
|
||||
id: NodeId,
|
||||
block: &Block<'a>,
|
||||
f: &mut HtmlishFormatter<'_, 'a>,
|
||||
) {
|
||||
let item = &t[id];
|
||||
let name = block_name(t, block);
|
||||
let default_never = name == "default never";
|
||||
write!(f, ["@", text(name)]);
|
||||
if !block.parameters.is_empty() {
|
||||
// `@default never(state)`: Prettier glues the parameters to `never`.
|
||||
write!(f, if default_never { "(" } else { " (" });
|
||||
for (i, parameter) in block.parameters.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, "; ");
|
||||
}
|
||||
// As written until expressions are formatted, on one line.
|
||||
let parameter =
|
||||
collapse_whitespace_outside_quotes(f.allocator(), parameter.slice(t.source()));
|
||||
write!(f, text(parameter));
|
||||
}
|
||||
write!(f, ")");
|
||||
}
|
||||
if block.close.is_none() {
|
||||
// `@default never;`; a fall-through `@case (a)` has nothing more.
|
||||
if default_never {
|
||||
write!(f, ";");
|
||||
}
|
||||
return;
|
||||
}
|
||||
write!(f, " {");
|
||||
if let (Some(first), Some(last)) = (item.first_child, item.last_child) {
|
||||
let glued_before = t[first].glued_before() && !item.has_leading_spaces;
|
||||
let glued_after = t[last].glued_after() && !item.has_trailing_spaces;
|
||||
write!(
|
||||
f,
|
||||
indent(&format_with(|f| {
|
||||
if !glued_before {
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
write_children(t, id, false, f);
|
||||
}))
|
||||
);
|
||||
if !glued_after {
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
} else if item.next.is_some_and(|next| continues_block(t, id, next)) {
|
||||
// `@if (a) {⏎} @else {`, as Prettier prints a closing `}` before the next block.
|
||||
write!(f, hard_line_break());
|
||||
}
|
||||
write!(f, "}");
|
||||
}
|
||||
|
||||
/// [`collapse_whitespace`] outside string literals (`'`, `"`, `` ` ``, a `\\` escaping the next character).
|
||||
fn collapse_whitespace_outside_quotes<'a>(allocator: &'a Allocator, text: &'a str) -> &'a str {
|
||||
if !text.contains(['\'', '"', '`']) {
|
||||
return collapse_whitespace(allocator, text);
|
||||
}
|
||||
let mut out = StringBuilder::new_in(allocator);
|
||||
let (mut quote, mut escaped, mut space) = (None, false, false);
|
||||
for c in text.trim_ascii().chars() {
|
||||
if quote.is_none() && c.is_ascii_whitespace() {
|
||||
space = true;
|
||||
continue;
|
||||
}
|
||||
if std::mem::take(&mut space) {
|
||||
out.push(' ');
|
||||
}
|
||||
out.push(c);
|
||||
match (c, quote) {
|
||||
_ if std::mem::take(&mut escaped) => {}
|
||||
('\\', _) => escaped = true,
|
||||
(c, Some(q)) if c == q => quote = None,
|
||||
('\'' | '"' | '`', None) => quote = Some(c),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out.into_str()
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
//! What separates siblings (Prettier's `printChildren`), and the line break facts it reads from the source.
|
||||
|
||||
use oxc_formatter_core::{
|
||||
Format, GroupId,
|
||||
builders::{
|
||||
empty_line, expand_parent, group, hard_line_break, if_group_fits_on_line, soft_line_break,
|
||||
soft_line_break_or_space, space,
|
||||
},
|
||||
write,
|
||||
};
|
||||
use oxc_htmlish_parser::ast::{NodeId, NodeKind};
|
||||
|
||||
use super::{
|
||||
HtmlishFormatter, block::between_block, format_with, tag::closing_end_borrowed,
|
||||
tag::closing_start_borrowed, write_child,
|
||||
};
|
||||
use crate::{context::HtmlishFormatContext, tree::Tree};
|
||||
|
||||
/// What separates two siblings (Prettier's `printBetweenLine`).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Between {
|
||||
Nothing,
|
||||
Soft,
|
||||
Line,
|
||||
/// A space that never breaks (the compiler layer keeps a same-line gap).
|
||||
Space,
|
||||
Hard,
|
||||
}
|
||||
|
||||
impl<'a> Format<'a, HtmlishFormatContext<'a>> for Between {
|
||||
fn fmt(&self, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
match self {
|
||||
Self::Nothing => {}
|
||||
Self::Soft => write!(f, soft_line_break()),
|
||||
Self::Line => write!(f, soft_line_break_or_space()),
|
||||
Self::Space => write!(f, space()),
|
||||
Self::Hard => write!(f, hard_line_break()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn between(t: &Tree<'_>, prev: NodeId, next: NodeId) -> Between {
|
||||
if t.is_block(prev) || t.is_block(next) {
|
||||
return between_block(t, prev, next);
|
||||
}
|
||||
let (p, n) = (&t[prev], &t[next]);
|
||||
if p.text_like && n.text_like {
|
||||
if p.trailing_sensitive {
|
||||
if p.has_trailing_spaces {
|
||||
return if prefer_hard_line_as_leading(t, next) {
|
||||
Between::Hard
|
||||
} else {
|
||||
Between::Line
|
||||
};
|
||||
}
|
||||
return Between::Nothing;
|
||||
}
|
||||
return if prefer_hard_line_as_leading(t, next) { Between::Hard } else { Between::Soft };
|
||||
}
|
||||
|
||||
if t[next].borrows.prev_closing_end
|
||||
&& t.element(prev).is_some()
|
||||
&& p.only_child().is_some_and(|only| t.is_interpolation(only))
|
||||
&& p.has_leading_spaces
|
||||
{
|
||||
return Between::Nothing;
|
||||
}
|
||||
|
||||
if (t[prev].borrows.next_opening_start
|
||||
&& (n.prettier_ignore
|
||||
// 123<a
|
||||
// ~
|
||||
// ><b>
|
||||
|| n.first_child.is_some()
|
||||
// 123<!--
|
||||
// ~
|
||||
// -->
|
||||
|| n.self_closing
|
||||
// 123<span
|
||||
// ~
|
||||
// attr
|
||||
|| t.element(next).is_some_and(|el| !el.attributes.is_empty())))
|
||||
// <img
|
||||
// src="long"
|
||||
// ~
|
||||
// />123
|
||||
|| (t.element(prev).is_some() && p.self_closing && t[next].borrows.prev_closing_end)
|
||||
{
|
||||
return Between::Nothing;
|
||||
}
|
||||
|
||||
// <div>{{ x }}<!-- comment --></div>
|
||||
// ~
|
||||
if matches!(t.kind(next), NodeKind::Comment(_)) && n.leading_sensitive && !n.has_leading_spaces
|
||||
{
|
||||
return Between::Soft;
|
||||
}
|
||||
|
||||
// Past a hidden element, insensitive whitespace between two non-blocks is free to stay on the line,
|
||||
// as written (DIVERGENCES.md#script-display-none): `<p>x<meta /></p>`, `<link /> <link />`
|
||||
if !n.leading_sensitive
|
||||
&& (t.is_transparent(prev) || t.is_transparent(next))
|
||||
&& p.display.sides_sensitive()
|
||||
&& n.display.sides_sensitive()
|
||||
&& !prefer_hard_line_as_leading(t, next)
|
||||
{
|
||||
return if n.has_leading_spaces { Between::Line } else { Between::Soft };
|
||||
}
|
||||
|
||||
if !n.leading_sensitive
|
||||
|| prefer_hard_line_as_leading(t, next)
|
||||
// Want to write us a letter? Use our<a
|
||||
// ><b><a>mailing address</a></b></a
|
||||
// ~
|
||||
// >.
|
||||
|| (t[next].borrows.prev_closing_end
|
||||
&& p.last_child.is_some_and(|last| {
|
||||
t[last].borrows.parent_closing_start && closing_start_borrowed(t, last)
|
||||
}))
|
||||
{
|
||||
return Between::Hard;
|
||||
}
|
||||
|
||||
if !n.has_leading_spaces {
|
||||
return Between::Soft;
|
||||
}
|
||||
// The compiler layer: between two elements, whitespace that renders keeps its kind (DIVERGENCES.md#vue-element-gap-kind).
|
||||
if t.rules().keeps_element_gap_kind && t.element(prev).is_some() && t.element(next).is_some() {
|
||||
return if has_trailing_line_break(t, prev) { Between::Hard } else { Between::Space };
|
||||
}
|
||||
Between::Line
|
||||
}
|
||||
|
||||
pub fn write_children<'a>(
|
||||
t: &'a Tree<'a>,
|
||||
id: NodeId,
|
||||
force_break: bool,
|
||||
f: &mut HtmlishFormatter<'_, 'a>,
|
||||
) {
|
||||
if force_break {
|
||||
write!(f, expand_parent());
|
||||
for child in t.children(id) {
|
||||
if let Some(prev) = t[child].prev {
|
||||
write_between(between(t, prev, child), force_next_empty_line(t, prev), f);
|
||||
}
|
||||
write_child(t, child, f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Each pair's separator and blank-line fact, computed once and carried to the next child.
|
||||
let mut carried = (Between::Nothing, false);
|
||||
let mut prev_group_id: Option<GroupId> = None;
|
||||
for child in t.children(id) {
|
||||
let node = &t[child];
|
||||
let (next_between, next_blank) = match node.next {
|
||||
Some(next) => (between(t, child, next), force_next_empty_line(t, child)),
|
||||
None => (Between::Nothing, false),
|
||||
};
|
||||
let (prev_between, prev_blank) =
|
||||
std::mem::replace(&mut carried, (next_between, next_blank));
|
||||
|
||||
if node.text_like {
|
||||
if node.prev.is_some_and(|prev| t[prev].text_like) {
|
||||
write_between(prev_between, prev_blank, f);
|
||||
}
|
||||
write_child(t, child, f);
|
||||
continue;
|
||||
}
|
||||
|
||||
// A line break on a side is printed outside the child's group; a softer separator inside it.
|
||||
let breaks =
|
||||
|between, blank| between != Between::Nothing && (blank || between == Between::Hard);
|
||||
let prev_breaks = breaks(prev_between, prev_blank);
|
||||
let leading = match node.prev {
|
||||
// After a `Space` (the previous group's trailing), no break before the next child.
|
||||
Some(prev)
|
||||
if !matches!(prev_between, Between::Nothing | Between::Space) && !prev_breaks =>
|
||||
{
|
||||
if t[prev].text_like {
|
||||
Some(Leading::Between(prev_between))
|
||||
} else {
|
||||
prev_group_id.map(Leading::IfPrevFlat)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let next_breaks = breaks(next_between, next_blank);
|
||||
let trailing = if next_breaks { Between::Nothing } else { next_between };
|
||||
|
||||
if prev_breaks {
|
||||
write_between(prev_between, prev_blank, f);
|
||||
}
|
||||
let group_id = f.state().group_id("htmlChild");
|
||||
write!(
|
||||
f,
|
||||
group(&format_with(|f| {
|
||||
match leading {
|
||||
Some(Leading::Between(between)) => write!(f, between),
|
||||
Some(Leading::IfPrevFlat(prev_id)) => write!(
|
||||
f,
|
||||
if_group_fits_on_line(&soft_line_break()).with_group_id(Some(prev_id))
|
||||
),
|
||||
None => {}
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
group(&format_with(|f| {
|
||||
write_child(t, child, f);
|
||||
write!(f, trailing);
|
||||
}))
|
||||
.with_group_id(Some(group_id))
|
||||
);
|
||||
}))
|
||||
);
|
||||
prev_group_id = Some(group_id);
|
||||
if next_breaks && node.next.is_some_and(|next| t[next].text_like) {
|
||||
write_between(next_between, next_blank, f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A separator between siblings; a kept blank line is `empty_line`
|
||||
/// (Prettier's two `hardline`s, which would collapse into one here).
|
||||
fn write_between(between: Between, blank: bool, f: &mut HtmlishFormatter<'_, '_>) {
|
||||
match between {
|
||||
Between::Nothing => {}
|
||||
_ if blank => write!(f, empty_line()),
|
||||
_ => write!(f, between),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Leading {
|
||||
Between(Between),
|
||||
/// A soft line break printed only while the previous sibling's group stays flat.
|
||||
IfPrevFlat(GroupId),
|
||||
}
|
||||
|
||||
fn force_next_empty_line(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
t[id].next.is_some_and(|next| t.line_breaks(t.source_end(id), t[next].span.start) >= 2)
|
||||
}
|
||||
|
||||
fn prefer_hard_line_as_leading(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
prefer_hard_line_as_surrounding(t, id)
|
||||
|| t[id].prev.is_some_and(|prev| prefer_hard_line_as_trailing(t, prev))
|
||||
|| has_surrounding_line_break(t, id)
|
||||
}
|
||||
|
||||
fn prefer_hard_line_as_trailing(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
prefer_hard_line_as_surrounding(t, id) || t.tag(id) == "br" || has_surrounding_line_break(t, id)
|
||||
}
|
||||
|
||||
fn prefer_hard_line_as_surrounding(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
match t.kind(id) {
|
||||
NodeKind::ConditionalComment(_) | NodeKind::Comment(_) => true,
|
||||
NodeKind::Element(_) => matches!(t.tag(id), "script" | "select"),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_surrounding_line_break(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
has_leading_line_break(t, id) && has_trailing_line_break(t, id)
|
||||
}
|
||||
|
||||
pub fn has_leading_line_break(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
let item = &t[id];
|
||||
if !item.has_leading_spaces {
|
||||
return false;
|
||||
}
|
||||
if let Some(prev) = item.prev {
|
||||
return t.line_breaks(t.source_end(prev), item.span.start) > 0;
|
||||
}
|
||||
let parent = t.parent(id);
|
||||
parent == NodeId::DOCUMENT || t.line_breaks(t.content_start(parent), item.span.start) > 0
|
||||
}
|
||||
|
||||
pub fn has_trailing_line_break(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
let item = &t[id];
|
||||
if !item.has_trailing_spaces {
|
||||
return false;
|
||||
}
|
||||
if let Some(next) = item.next {
|
||||
return t.line_breaks(t.source_end(id), t[next].span.start) > 0;
|
||||
}
|
||||
let parent = t.parent(id);
|
||||
parent == NodeId::DOCUMENT
|
||||
// A line break before a borrowed `>` cannot be printed (`b </p\n>`, `b</p\n>`),
|
||||
// so it is not read either: the next pass sees what this one did (DIVERGENCES.md#line-break-before-borrowed-end).
|
||||
|| (!closing_end_borrowed(t, parent)
|
||||
&& t.end_tag_start(parent).is_some_and(|end| t.line_breaks(t.source_end(id), end) > 0))
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
//! Elements (and conditional comments): the tags around the children, or the content as written.
|
||||
|
||||
use oxc_formatter_core::{
|
||||
Format,
|
||||
builders::{
|
||||
dedent_to_root, expand_parent, group, if_group_breaks, indent, indent_if_group_breaks,
|
||||
soft_line_break, soft_line_break_or_space, space, text,
|
||||
},
|
||||
write,
|
||||
};
|
||||
use oxc_htmlish_parser::ast::{NodeId, NodeKind};
|
||||
|
||||
use super::{
|
||||
HtmlishFormatter,
|
||||
children::{has_leading_line_break, has_trailing_line_break, write_children},
|
||||
format_with,
|
||||
tag::{
|
||||
closing_end_borrowed, closing_end_marker, closing_start_borrowed, closing_start_marker,
|
||||
opening_end_borrowed, opening_end_marker, write_closing_tag, write_opening_tag,
|
||||
},
|
||||
};
|
||||
use crate::{
|
||||
HtmlishFormatOptions,
|
||||
context::HtmlishFormatContext,
|
||||
tree::{Display, Tree},
|
||||
};
|
||||
|
||||
pub fn write_element<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
let item = &t[id];
|
||||
|
||||
if should_preserve_content(t, id) {
|
||||
// The opening tag prints the borrowed prefix, once (DIVERGENCES.md#pre-after-borrowed-closing-end).
|
||||
write!(f, group(&format_with(|f| write_opening_tag(t, id, f))));
|
||||
write!(f, text(node_content(t, id)));
|
||||
write_closing_tag(t, id, f);
|
||||
return;
|
||||
}
|
||||
|
||||
let attr_group_id = f.state().group_id("htmlAttributes");
|
||||
let print_tag = |f: &mut HtmlishFormatter<'_, 'a>,
|
||||
body: &dyn Format<'a, HtmlishFormatContext<'a>>| {
|
||||
write!(
|
||||
f,
|
||||
group(&format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
group(&format_with(|f| write_opening_tag(t, id, f)))
|
||||
.with_group_id(Some(attr_group_id))
|
||||
);
|
||||
write!(f, body);
|
||||
write_closing_tag(t, id, f);
|
||||
}))
|
||||
);
|
||||
};
|
||||
|
||||
let (Some(first), Some(last)) = (item.first_child, item.last_child) else {
|
||||
let dangling = item.has_dangling_spaces && item.dangling_sensitive;
|
||||
print_tag(
|
||||
f,
|
||||
&format_with(|f| {
|
||||
if dangling {
|
||||
write!(f, soft_line_break_or_space());
|
||||
}
|
||||
}),
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let hug = item.only_child().is_some()
|
||||
&& t.is_interpolation(first)
|
||||
&& t[first].leading_sensitive
|
||||
&& !t[first].has_leading_spaces
|
||||
&& t[last].trailing_sensitive
|
||||
&& !t[last].has_trailing_spaces;
|
||||
|
||||
let line_before = format_with(|f| {
|
||||
if hug {
|
||||
write!(f, if_group_breaks(&soft_line_break()).with_group_id(Some(attr_group_id)));
|
||||
} else if t[first].has_leading_spaces && t[first].leading_sensitive {
|
||||
write!(f, soft_line_break_or_space());
|
||||
} else if t.is_text(first) && item.pre_like {
|
||||
write!(f, dedent_to_root(&soft_line_break()));
|
||||
} else {
|
||||
write!(f, soft_line_break());
|
||||
}
|
||||
});
|
||||
|
||||
let line_after = format_with(|f| {
|
||||
if closing_end_borrowed(t, id) {
|
||||
if item.script_like {
|
||||
// The body renders nothing, so its end tag takes its own line (DIVERGENCES.md#script-display-none).
|
||||
write!(f, soft_line_break());
|
||||
} else if t[last].has_trailing_spaces && t[last].trailing_sensitive {
|
||||
write!(f, space());
|
||||
}
|
||||
} else if item.pre_like && t[last].borrows.parent_closing_start {
|
||||
// `<pre>` whose last text prints `</pre`: a line break would be content.
|
||||
} else if hug {
|
||||
write!(f, if_group_breaks(&soft_line_break()).with_group_id(Some(attr_group_id)));
|
||||
} else if t[last].has_trailing_spaces && t[last].trailing_sensitive {
|
||||
write!(f, soft_line_break_or_space());
|
||||
} else if t[last].ends_in_pre {
|
||||
// `<div><pre />⏎<b>x</b></div>`: the unclosed `pre` ends here, so a line break would be its content
|
||||
// (DIVERGENCES.md#pre-content-without-end-tag).
|
||||
} else if !ends_at_indentation(t, id, last, *f.options()) {
|
||||
write!(f, soft_line_break());
|
||||
}
|
||||
});
|
||||
|
||||
let force_break = force_break_children(t, id);
|
||||
let children = format_with(|f| {
|
||||
write!(f, line_before);
|
||||
write_children(t, id, force_break, f);
|
||||
});
|
||||
print_tag(
|
||||
f,
|
||||
&format_with(|f| {
|
||||
if force_break || force_break_content(t, id) {
|
||||
write!(f, expand_parent());
|
||||
}
|
||||
if hug {
|
||||
write!(f, indent_if_group_breaks(&children, attr_group_id));
|
||||
} else {
|
||||
write!(f, indent(&children));
|
||||
}
|
||||
write!(f, line_after);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// The last child is a comment or `pre` text ending in a line break plus exactly the element's indentation,
|
||||
/// counted in the indent unit (DIVERGENCES.md#end-tag-indentation-as-printed): the end tag already sits where it would be printed.
|
||||
fn ends_at_indentation(
|
||||
t: &Tree<'_>,
|
||||
id: NodeId,
|
||||
last: NodeId,
|
||||
options: HtmlishFormatOptions,
|
||||
) -> bool {
|
||||
let value = match t.kind(last) {
|
||||
NodeKind::Comment(comment) => comment.content.slice(t.source()),
|
||||
NodeKind::Text(_) | NodeKind::Cdata if t[id].pre_like => t[last].span.slice(t.source()),
|
||||
_ => return false,
|
||||
};
|
||||
let Some((_, tail)) = value.rsplit_once('\n') else { return false };
|
||||
let (unit, per_level) = if options.indent_style.is_tab() {
|
||||
(b'\t', 1)
|
||||
} else {
|
||||
(b' ', usize::from(options.indent_width.value()))
|
||||
};
|
||||
if !tail.bytes().all(|b| b == unit) {
|
||||
return false;
|
||||
}
|
||||
// The element's own indentation: one level per ancestor below the document.
|
||||
let mut depth = 0;
|
||||
let mut ancestor = t.parent(id);
|
||||
while ancestor != NodeId::DOCUMENT {
|
||||
depth += 1;
|
||||
ancestor = t.parent(ancestor);
|
||||
}
|
||||
tail.len() == per_level * depth
|
||||
}
|
||||
|
||||
/// Children Prettier prints as written between the tags.
|
||||
fn should_preserve_content(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
let item = &t[id];
|
||||
match t.kind(id) {
|
||||
NodeKind::ConditionalComment(comment) => {
|
||||
!comment.parsed
|
||||
// `<!--[if lt IE 9]><html><![endif]-->`: an unclosed element inside.
|
||||
|| item.last_child.is_some_and(|last| t.is_unclosed(last))
|
||||
}
|
||||
// `<pre>` with markup inside.
|
||||
_ => {
|
||||
item.pre_like
|
||||
&& t.children(id).any(|child| !(t.is_text(child) || t.is_interpolation(child)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The source between the tags, adjusted for markers the edges borrow (Prettier's `getNodeContent`).
|
||||
fn node_content<'a>(t: &'a Tree<'a>, id: NodeId) -> &'a str {
|
||||
let item = &t[id];
|
||||
let mut start = t.content_start(id) as usize;
|
||||
// An unclosed element's content runs to where the parser closed it.
|
||||
let mut end = t.end_tag_start(id).unwrap_or(item.span.end) as usize;
|
||||
if opening_end_borrowed(t, id) {
|
||||
start -= opening_end_marker(t, id).len();
|
||||
}
|
||||
if closing_start_borrowed(t, id) {
|
||||
end += closing_start_marker(t, id).len();
|
||||
} else if t[id].borrows.last_child_closing_end {
|
||||
end -= closing_end_marker(t, item.last_child.expect("borrowed by the last child")).len();
|
||||
}
|
||||
&t.source()[start..end]
|
||||
}
|
||||
|
||||
fn force_break_children(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
let item = &t[id];
|
||||
t.element(id).is_some()
|
||||
&& item.first_child.is_some()
|
||||
&& (matches!(t.tag(id), "html" | "head" | "ul" | "ol" | "select")
|
||||
|| item.display == Display::Table)
|
||||
}
|
||||
|
||||
/// Besides `force_break_children`: the content of `body`, `script`, `style`, nested markup, or a lone child on its own lines.
|
||||
fn force_break_content(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
let item = &t[id];
|
||||
if t.element(id).is_some()
|
||||
&& item.first_child.is_some()
|
||||
&& (item.script_like
|
||||
|| t.tag(id) == "body"
|
||||
|| t.children(id)
|
||||
.any(|child| t.children(child).any(|grandchild| !t.is_text(grandchild))))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
item.only_child().is_some_and(|only| {
|
||||
!t.is_text(only)
|
||||
&& has_leading_line_break(t, only)
|
||||
&& (!t[only].trailing_sensitive || has_trailing_line_break(t, only))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
//! The printer: dispatch by node kind; elements, text and blocks get their own module,
|
||||
//! besides the separators between siblings (`children`), the tag markers (`tag`) and attributes (`attribute`).
|
||||
|
||||
use oxc_formatter_core::{
|
||||
Format, Formatter,
|
||||
builders::{FormatWith, group, soft_space_or_block_indent, space, text, token},
|
||||
write,
|
||||
};
|
||||
use oxc_htmlish_parser::ast::{NodeId, NodeKind};
|
||||
|
||||
use crate::{context::HtmlishFormatContext, tree::Tree};
|
||||
|
||||
mod attribute;
|
||||
mod block;
|
||||
mod children;
|
||||
mod element;
|
||||
mod tag;
|
||||
mod text;
|
||||
|
||||
use block::write_block;
|
||||
use children::write_children;
|
||||
use element::write_element;
|
||||
use tag::{
|
||||
after_html_word, closing_end_borrowed, closing_end_marker, closing_tag_suffix,
|
||||
opening_start_borrowed, opening_start_marker, opening_tag_prefix, write_closing_tag_end,
|
||||
write_opening_tag_start,
|
||||
};
|
||||
use text::{write_dedented_lines, write_text};
|
||||
|
||||
pub type HtmlishFormatter<'buf, 'a> = Formatter<'buf, 'a, HtmlishFormatContext<'a>>;
|
||||
|
||||
/// `Format` impl for `&'static str` specialized to `HtmlishFormatContext`.
|
||||
impl<'a> Format<'a, HtmlishFormatContext<'a>> for &'static str {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
write!(f, token(self));
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a re-entrant closure in a [`FormatWith`], its context pinned to [`HtmlishFormatContext`].
|
||||
#[inline]
|
||||
pub const fn format_with<'a, T>(formatter: T) -> FormatWith<T>
|
||||
where
|
||||
T: Fn(&mut HtmlishFormatter<'_, 'a>),
|
||||
{
|
||||
FormatWith::new(formatter)
|
||||
}
|
||||
|
||||
pub fn write_root(f: &mut HtmlishFormatter<'_, '_>) {
|
||||
let t = f.context().tree();
|
||||
write!(f, group(&format_with(|f| write_children(t, NodeId::DOCUMENT, false, f))));
|
||||
}
|
||||
|
||||
fn write_child<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
if !t[id].prettier_ignore {
|
||||
write_node(t, id, f);
|
||||
return;
|
||||
}
|
||||
let mut start = t[id].span.start as usize;
|
||||
if opening_start_borrowed(t, id) {
|
||||
start += opening_start_marker(t, id).len();
|
||||
}
|
||||
let mut end = ignored_end(t, id) as usize;
|
||||
// Whoever borrows the closing `>` prints it, the parent included (DIVERGENCES.md#ignored-closing-end-once).
|
||||
if closing_end_borrowed(t, id) {
|
||||
end -= closing_end_marker(t, id).len();
|
||||
}
|
||||
let raw = t.source()[start..end].trim_ascii_end();
|
||||
write!(f, [opening_tag_prefix(t, id), text(raw), closing_tag_suffix(t, id)]);
|
||||
}
|
||||
|
||||
/// Prettier's `getEndLocation`: an unclosed element extends to its last child.
|
||||
fn ignored_end(t: &Tree<'_>, id: NodeId) -> u32 {
|
||||
let end = t.source_end(id);
|
||||
match t.kind(id) {
|
||||
NodeKind::Element(el) if el.end_tag.is_none() => {
|
||||
t[id].last_child.map_or(end, |last| end.max(ignored_end(t, last)))
|
||||
}
|
||||
_ => end,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_node<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
let item = &t[id];
|
||||
match t.kind(id) {
|
||||
NodeKind::Element(_) | NodeKind::ConditionalComment(_) => write_element(t, id, f),
|
||||
NodeKind::ConditionalStart(_) | NodeKind::ConditionalEnd => {
|
||||
write_opening_tag_start(t, id, f);
|
||||
write_closing_tag_end(t, id, f);
|
||||
}
|
||||
NodeKind::Text(_) | NodeKind::Cdata => write_text(t, id, f),
|
||||
// `{{ }}`; the expression keeps its lines until it is formatted.
|
||||
NodeKind::Interpolation(interpolation) => {
|
||||
write_opening_tag_start(t, id, f);
|
||||
let expression = interpolation.expression.slice(t.source());
|
||||
if !expression.trim_ascii().is_empty() {
|
||||
write!(
|
||||
f,
|
||||
soft_space_or_block_indent(&format_with(|f| write_dedented_lines(
|
||||
expression, f
|
||||
)))
|
||||
);
|
||||
}
|
||||
write_closing_tag_end(t, id, f);
|
||||
}
|
||||
NodeKind::DocType(doctype) => {
|
||||
let value = t.collapsed(doctype.value);
|
||||
write!(
|
||||
f,
|
||||
group(&format_with(|f| {
|
||||
write_opening_tag_start(t, id, f);
|
||||
write!(f, space());
|
||||
// Its leading `html` lowercased (`<!DOCTYPE HTML PUBLIC …>`)
|
||||
if let Some(rest) = after_html_word(value) {
|
||||
write!(f, [text("html"), text(rest)]);
|
||||
} else {
|
||||
write!(f, text(value));
|
||||
}
|
||||
}))
|
||||
);
|
||||
write_closing_tag_end(t, id, f);
|
||||
}
|
||||
// Comments are printed as written, a `</ div>` too (DIVERGENCES.md#end-tag-with-space-is-comment),
|
||||
// and an omitted element's end tag (`</body>`).
|
||||
NodeKind::Comment(_) | NodeKind::EndTag(_) => write!(
|
||||
f,
|
||||
[
|
||||
opening_tag_prefix(t, id),
|
||||
text(item.span.slice(t.source())),
|
||||
closing_tag_suffix(t, id)
|
||||
]
|
||||
),
|
||||
NodeKind::Block(block) => write_block(t, id, block, f),
|
||||
NodeKind::Document => unreachable!("the root is written by `write_root`"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
//! Tags and their markers, and the "borrow" rules that move a marker to a neighbor
|
||||
//! so that a line break can sit inside a tag where whitespace would be significant:
|
||||
//!
|
||||
//! ```html
|
||||
//! <span>text</span
|
||||
//! ><b>
|
||||
//! ```
|
||||
//!
|
||||
//! The borrower side is [`Borrows`](crate::tree::Borrows), computed with the tree;
|
||||
//! the owner side here (`*_borrowed`) omits the marker.
|
||||
//!
|
||||
//! The rules end up shaped like Prettier's `print/tag.js`, not ported from it:
|
||||
//! which neighbor prints a marker is the output itself, and matching that output leaves no other structure.
|
||||
|
||||
use oxc_formatter_core::{Format, builders::text, write};
|
||||
|
||||
use super::{HtmlishFormatter, attribute::write_attributes};
|
||||
use oxc_htmlish_parser::{
|
||||
Span,
|
||||
ast::{NodeId, NodeKind},
|
||||
};
|
||||
|
||||
use crate::{context::HtmlishFormatContext, tree::Tree};
|
||||
|
||||
/// A tag marker, written as up to four pieces (`<` + name, `<!--[if ` + condition + `]><!--><` + name, ...).
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Marker<'a>([&'a str; 4]);
|
||||
|
||||
impl<'a> Marker<'a> {
|
||||
const fn one(piece: &'a str) -> Self {
|
||||
Self([piece, "", "", ""])
|
||||
}
|
||||
|
||||
const fn two(piece: &'a str, name: &'a str) -> Self {
|
||||
Self([piece, name, "", ""])
|
||||
}
|
||||
|
||||
pub fn len(self) -> usize {
|
||||
self.0.iter().map(|piece| piece.len()).sum()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Format<'a, HtmlishFormatContext<'a>> for Marker<'a> {
|
||||
fn fmt(&self, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
for piece in self.0 {
|
||||
if !piece.is_empty() {
|
||||
write!(f, text(piece));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// Owner side
|
||||
|
||||
/// The node's closing `>` is printed by its next sibling or its parent.
|
||||
pub fn closing_end_borrowed(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
match t[id].next {
|
||||
Some(next) => t[next].borrows.prev_closing_end,
|
||||
None => t[t.parent(id)].borrows.last_child_closing_end,
|
||||
}
|
||||
}
|
||||
|
||||
/// The node's `<name` is printed by its previous sibling.
|
||||
pub fn opening_start_borrowed(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
t[id].prev.is_some_and(|prev| t[prev].borrows.next_opening_start)
|
||||
}
|
||||
|
||||
/// The node's opening `>` is printed by its first child.
|
||||
pub fn opening_end_borrowed(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
t[id].first_child.is_some_and(|first| t[first].borrows.parent_opening_end)
|
||||
}
|
||||
|
||||
/// The node's `</name` is printed by its last child.
|
||||
pub fn closing_start_borrowed(t: &Tree<'_>, id: NodeId) -> bool {
|
||||
t[id].last_child.is_some_and(|last| t[last].borrows.parent_closing_start)
|
||||
}
|
||||
|
||||
// ---
|
||||
// Markers
|
||||
|
||||
pub fn opening_start_marker<'a>(t: &'a Tree<'a>, id: NodeId) -> Marker<'a> {
|
||||
match t.kind(id) {
|
||||
NodeKind::ConditionalComment(comment) => {
|
||||
Marker::two("<!--[if ", t.collapsed(comment.condition))
|
||||
}
|
||||
NodeKind::ConditionalStart(start) => Marker::two("<!--[if ", t.collapsed(start.condition)),
|
||||
NodeKind::ConditionalEnd => Marker::one("<!--<!"),
|
||||
NodeKind::Interpolation(_) => Marker::one("{{"),
|
||||
// `<!doctype html>` but where an XML parser may read it, otherwise as written
|
||||
// (DIVERGENCES.md#doctype-case, DIVERGENCES.md#doctype-html-in-one-pass)
|
||||
NodeKind::DocType(doctype) => {
|
||||
if t.rules().lowercase_doctype
|
||||
&& after_html_word(doctype.value.slice(t.source()).trim_ascii()) == Some("")
|
||||
{
|
||||
Marker::one("<!doctype")
|
||||
} else {
|
||||
let start = t[id].span.start;
|
||||
Marker::one(Span::new(start, start + 9).slice(t.source()))
|
||||
}
|
||||
}
|
||||
NodeKind::Element(_) => match t[id].condition.map(|start| t.kind(start)) {
|
||||
Some(NodeKind::ConditionalStart(start)) => {
|
||||
Marker(["<!--[if ", t.collapsed(start.condition), "]><!--><", t.name(id)])
|
||||
}
|
||||
_ => Marker::two("<", t.name(id)),
|
||||
},
|
||||
_ => Marker::two("<", t.name(id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn opening_end_marker<'a>(t: &'a Tree<'a>, id: NodeId) -> Marker<'a> {
|
||||
match t.kind(id) {
|
||||
NodeKind::ConditionalComment(_) => Marker::one("]>"),
|
||||
NodeKind::Element(_) if t[id].condition.is_some() => Marker::one("><!--<![endif]-->"),
|
||||
NodeKind::Element(el) if el.self_closing => Marker::one("/>"),
|
||||
_ => Marker::one(">"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn closing_start_marker<'a>(t: &'a Tree<'a>, id: NodeId) -> Marker<'a> {
|
||||
match t.kind(id) {
|
||||
NodeKind::ConditionalComment(_) => Marker::one("<!"),
|
||||
// The end tag as written, its name lowercase with the start tag's (DIVERGENCES.md#tag-name-case);
|
||||
// an omitted one is never added (DIVERGENCES.md#omitted-end-tag).
|
||||
// What HTML ignores after the name (`</div foo>`) is kept too, and so is htm's `<//>`.
|
||||
NodeKind::Element(el) => el.end_tag.map_or(Marker::default(), |tag| {
|
||||
let name = t.printed_name(id, el, tag.name.slice(t.source()));
|
||||
let ignored = tag
|
||||
.ignored
|
||||
.map_or("", |ignored| Span::new(tag.name.end, ignored.end).slice(t.source()));
|
||||
Marker(["</", name, ignored, ""])
|
||||
}),
|
||||
_ => Marker::two("</", t.name(id)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn closing_end_marker<'a>(t: &'a Tree<'a>, id: NodeId) -> Marker<'a> {
|
||||
let item = &t[id];
|
||||
match t.kind(id) {
|
||||
NodeKind::ConditionalComment(_) | NodeKind::ConditionalEnd => Marker::one("[endif]-->"),
|
||||
NodeKind::ConditionalStart(_) => Marker::one("]><!-->"),
|
||||
NodeKind::Interpolation(_) => Marker::one("}}"),
|
||||
// A void element prints `/>` too
|
||||
NodeKind::Element(_) if item.self_closing => Marker::one("/>"),
|
||||
NodeKind::Element(el) if el.end_tag.is_none() => Marker::default(),
|
||||
_ => Marker::one(">"),
|
||||
}
|
||||
}
|
||||
|
||||
/// What follows a leading `html` word in a doctype's value (`HTML PUBLIC …`), which HTML reads case-insensitively.
|
||||
pub fn after_html_word(value: &str) -> Option<&str> {
|
||||
let rest = value.get(4..).filter(|_| value[..4].eq_ignore_ascii_case("html"))?;
|
||||
(!rest.starts_with(|c: char| c.is_ascii_alphanumeric() || c == '_')).then_some(rest)
|
||||
}
|
||||
|
||||
// ---
|
||||
// Prefixes and suffixes: what a node prints for its neighbors
|
||||
|
||||
pub fn opening_tag_prefix<'a>(t: &'a Tree<'a>, id: NodeId) -> Marker<'a> {
|
||||
if t[id].borrows.parent_opening_end {
|
||||
opening_end_marker(t, t.parent(id))
|
||||
} else if t[id].borrows.prev_closing_end {
|
||||
closing_end_marker(t, t[id].prev.expect("borrowed from the previous sibling"))
|
||||
} else {
|
||||
Marker::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn closing_tag_suffix<'a>(t: &'a Tree<'a>, id: NodeId) -> Marker<'a> {
|
||||
if t[id].borrows.parent_closing_start {
|
||||
closing_start_marker(t, t.parent(id))
|
||||
} else if t[id].borrows.next_opening_start {
|
||||
opening_start_marker(t, t[id].next.expect("borrowed from the next sibling"))
|
||||
} else {
|
||||
Marker::default()
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
// Tags
|
||||
|
||||
pub fn write_opening_tag_start<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
if !opening_start_borrowed(t, id) {
|
||||
write!(f, [opening_tag_prefix(t, id), opening_start_marker(t, id)]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_opening_tag<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
write_opening_tag_start(t, id, f);
|
||||
write_attributes(t, id, f);
|
||||
if !t[id].self_closing && !opening_end_borrowed(t, id) {
|
||||
write!(f, opening_end_marker(t, id));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_closing_tag<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
if !t[id].self_closing && !closing_start_borrowed(t, id) {
|
||||
if t[id].borrows.last_child_closing_end {
|
||||
write!(f, closing_end_marker(t, t[id].last_child.expect("borrowed by the last child")));
|
||||
}
|
||||
write!(f, closing_start_marker(t, id));
|
||||
}
|
||||
write_closing_tag_end(t, id, f);
|
||||
}
|
||||
|
||||
pub fn write_closing_tag_end<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
if !closing_end_borrowed(t, id) {
|
||||
write!(f, [closing_end_marker(t, id), closing_tag_suffix(t, id)]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Text: filled words, or lines as written where whitespace is content.
|
||||
|
||||
use oxc_formatter_core::{
|
||||
builders::{exact_line_breaks, soft_line_break_or_space, text},
|
||||
write,
|
||||
};
|
||||
use oxc_htmlish_parser::ast::NodeId;
|
||||
|
||||
use super::{
|
||||
HtmlishFormatter, format_with,
|
||||
tag::{closing_tag_suffix, opening_tag_prefix},
|
||||
};
|
||||
use crate::tree::Tree;
|
||||
|
||||
pub fn write_text<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
let parent = &t[t.parent(id)];
|
||||
let value = t.text(id);
|
||||
let prefix = opening_tag_prefix(t, id);
|
||||
let suffix = closing_tag_suffix(t, id);
|
||||
|
||||
if parent.pre_like {
|
||||
write!(f, [prefix, text(value), suffix]);
|
||||
return;
|
||||
}
|
||||
if parent.script_like {
|
||||
write!(f, prefix);
|
||||
write_dedented_lines(value, f);
|
||||
write!(f, suffix);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut words = words(value, t[id].has_cdata).peekable();
|
||||
let mut fill = f.fill();
|
||||
let mut first = true;
|
||||
while let Some(word) = words.next() {
|
||||
let last = words.peek().is_none();
|
||||
fill.entry(
|
||||
&soft_line_break_or_space(),
|
||||
&format_with(|f| {
|
||||
if first {
|
||||
write!(f, prefix);
|
||||
}
|
||||
write!(f, text(word));
|
||||
if last {
|
||||
write!(f, suffix);
|
||||
}
|
||||
}),
|
||||
);
|
||||
first = false;
|
||||
}
|
||||
fill.finish();
|
||||
}
|
||||
|
||||
/// Writes `value`'s lines, trimmed and dedented (Prettier's `htmlTrimPreserveIndentation` + `dedentString`),
|
||||
/// joined by line breaks: every blank line is kept, and prints without indentation.
|
||||
pub fn write_dedented_lines<'a>(value: &'a str, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||
let mut breaks = 0;
|
||||
for line in dedent(trim_preserving_indentation(value)) {
|
||||
if line.is_empty() {
|
||||
breaks += 1;
|
||||
continue;
|
||||
}
|
||||
if breaks > 0 {
|
||||
write!(f, exact_line_breaks(breaks));
|
||||
}
|
||||
write!(f, text(line));
|
||||
breaks = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Whitespace-separated words; with `cdata`, a CDATA section is part of one word, whatever it holds
|
||||
/// (DIVERGENCES.md#cdata-as-written).
|
||||
fn words(value: &str, cdata: bool) -> impl Iterator<Item = &str> {
|
||||
let bytes = value.as_bytes();
|
||||
let mut pos = 0;
|
||||
std::iter::from_fn(move || {
|
||||
pos += bytes[pos..].iter().take_while(|b| b.is_ascii_whitespace()).count();
|
||||
if pos == bytes.len() {
|
||||
return None;
|
||||
}
|
||||
let start = pos;
|
||||
while pos < bytes.len() && !bytes[pos].is_ascii_whitespace() {
|
||||
pos = if cdata && bytes[pos..].starts_with(b"<![CDATA[") {
|
||||
memchr::memmem::find(&bytes[pos..], b"]]>").map_or(bytes.len(), |end| pos + end + 3)
|
||||
} else {
|
||||
pos + 1
|
||||
};
|
||||
}
|
||||
Some(&value[start..pos])
|
||||
})
|
||||
}
|
||||
|
||||
/// Prettier's `htmlTrimPreserveIndentation`: trailing whitespace and leading blank lines go.
|
||||
fn trim_preserving_indentation(value: &str) -> &str {
|
||||
let value = value.trim_ascii_end();
|
||||
let mut start = 0;
|
||||
while let Some(newline) = value[start..].find('\n') {
|
||||
if value[start..start + newline].trim_ascii().is_empty() {
|
||||
start += newline + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
&value[start..]
|
||||
}
|
||||
|
||||
/// Prettier's `dedentString`: the lines with their common indentation removed.
|
||||
fn dedent(value: &str) -> impl Iterator<Item = &str> {
|
||||
let indentation = |line: &str| line.len() - line.trim_ascii_start().len();
|
||||
let min = value
|
||||
.split('\n')
|
||||
.filter(|line| !line.trim_ascii_start().is_empty())
|
||||
.map(indentation)
|
||||
.min()
|
||||
.unwrap_or(0);
|
||||
value.split('\n').map(move |line| line.get(min..).unwrap_or(""))
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
use oxc_htmlish_parser::Dialect;
|
||||
|
||||
use crate::HtmlishVariant;
|
||||
|
||||
/// What the variant changes in the shared rules, resolved once per document.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct DialectRules {
|
||||
/// Known attribute names match case-insensitively (Prettier's HTML parser lowercases them;
|
||||
/// a Vue file read as HTML too, Angular never).
|
||||
pub names_ignore_case: bool,
|
||||
/// Vue's compiler drops whitespace with a line break between two elements and keeps a same-line one as a space.
|
||||
pub keeps_element_gap_kind: bool,
|
||||
/// Angular: control flow blocks can occur (checked before looking at a node's kind).
|
||||
pub has_blocks: bool,
|
||||
/// `<!DOCTYPE html>` prints `<!doctype html>`: everywhere but XHTML, where an XML parser needs `<!DOCTYPE`
|
||||
/// (DIVERGENCES.md#doctype-case).
|
||||
pub lowercase_doctype: bool,
|
||||
/// A known HTML element's name and its known attributes print lowercase, as Prettier's HTML parser reads them:
|
||||
/// in a `.html` file, whose reader is an HTML parser; as written where embedded, in XHTML, Vue and Angular
|
||||
/// (DIVERGENCES.md#tag-name-case).
|
||||
pub lowercase_known_names: bool,
|
||||
}
|
||||
|
||||
impl DialectRules {
|
||||
/// `sfc`: a Vue file read as a single-file component, not as HTML.
|
||||
/// `standalone`: a file of its own, not a part embedded in another language.
|
||||
pub fn new(variant: HtmlishVariant, sfc: bool, standalone: bool) -> Self {
|
||||
let dialect = variant.to_dialect();
|
||||
Self {
|
||||
names_ignore_case: dialect == Dialect::Html || (dialect == Dialect::Vue && !sfc),
|
||||
keeps_element_gap_kind: dialect == Dialect::Vue,
|
||||
has_blocks: dialect == Dialect::Angular,
|
||||
lowercase_doctype: variant != HtmlishVariant::Xhtml,
|
||||
lowercase_known_names: standalone && variant == HtmlishVariant::Html,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,684 @@
|
||||
use oxc_allocator::{Allocator, ArenaVec, StringBuilder};
|
||||
use oxc_formatter_core::spec::is_suppression_marker;
|
||||
use oxc_htmlish_parser::{
|
||||
Span,
|
||||
ast::{Element, Namespace, Node, NodeId, NodeKind, Root, Whitespace},
|
||||
};
|
||||
|
||||
use super::{
|
||||
Borrows, Display, Facts, Tree,
|
||||
display::{display_comment, ua_display},
|
||||
};
|
||||
use crate::{HtmlishVariant, options::WhitespaceSensitivity, rules::DialectRules};
|
||||
|
||||
impl<'a> Tree<'a> {
|
||||
pub fn build(
|
||||
allocator: &'a Allocator,
|
||||
root: &'a Root<'a>,
|
||||
source: &'a str,
|
||||
sensitivity: WhitespaceSensitivity,
|
||||
variant: HtmlishVariant,
|
||||
standalone: bool,
|
||||
) -> Self {
|
||||
let facts = ArenaVec::from_iter_in(
|
||||
root.nodes.iter().map(|node| Facts { span: node.span, ..Facts::default() }),
|
||||
&allocator,
|
||||
);
|
||||
let mut builder = Builder {
|
||||
tree: Self {
|
||||
allocator,
|
||||
rules: DialectRules::new(variant, root.sfc, standalone),
|
||||
root,
|
||||
source,
|
||||
facts,
|
||||
merged_texts: ArenaVec::new_in(&allocator),
|
||||
},
|
||||
sensitivity,
|
||||
};
|
||||
builder.facts(NodeId::DOCUMENT).display = builder.display(NodeId::DOCUMENT);
|
||||
builder.children(NodeId::DOCUMENT);
|
||||
builder.facts(NodeId::DOCUMENT).borrows = builder.borrows(NodeId::DOCUMENT);
|
||||
// Merges push in post-order (children first)
|
||||
builder.tree.merged_texts.sort_unstable_by_key(|(text, _)| *text);
|
||||
builder.tree
|
||||
}
|
||||
}
|
||||
|
||||
struct Builder<'a> {
|
||||
tree: Tree<'a>,
|
||||
sensitivity: WhitespaceSensitivity,
|
||||
}
|
||||
|
||||
impl Builder<'_> {
|
||||
fn facts(&mut self, id: NodeId) -> &mut Facts {
|
||||
&mut self.tree.facts[id.index()]
|
||||
}
|
||||
|
||||
fn children(&mut self, parent: NodeId) {
|
||||
let root = self.tree.root;
|
||||
let whitespace_sensitive = self.tree[self.flow_parent(parent)].whitespace_sensitive();
|
||||
// The end comment a conditional start tag absorbed
|
||||
let absorbed_end = self.tree[parent].condition.and(root[parent].first_child);
|
||||
let nodes = || {
|
||||
std::iter::successors(root[parent].first_child, move |&id| next_child(root, parent, id))
|
||||
.filter(move |&id| Some(id) != absorbed_end)
|
||||
.map(|id| (id, &root[id]))
|
||||
};
|
||||
|
||||
// Prettier drops a lone whitespace-only text even where whitespace is content;
|
||||
// a whitespace-sensitive parent keeps it (DIVERGENCES.md#whitespace-only-pre), except `script` / `style`.
|
||||
let mut first_two = nodes();
|
||||
let (first, second) = (first_two.next(), first_two.next());
|
||||
let only_whitespace = (!whitespace_sensitive || self.tree[parent].script_like)
|
||||
&& second.is_none()
|
||||
&& first.is_some_and(|(_, node)| is_whitespace_text(node));
|
||||
if first.is_none() || only_whitespace {
|
||||
let facts = self.facts(parent);
|
||||
facts.has_dangling_spaces = only_whitespace;
|
||||
facts.dangling_sensitive = facts.display.edges_sensitive() && !facts.script_like;
|
||||
return;
|
||||
}
|
||||
|
||||
let mut prev: Option<NodeId> = None;
|
||||
let mut pending_space = false;
|
||||
let mut any_cdata = false;
|
||||
let mut any_hidden = false;
|
||||
for (id, node) in nodes() {
|
||||
if let Some(element) = self.absorbing_element(id) {
|
||||
let facts = self.facts(element);
|
||||
facts.condition = Some(id);
|
||||
facts.span.start = node.span.start;
|
||||
continue;
|
||||
}
|
||||
let (leading, trailing, whitespace_only) = match &node.kind {
|
||||
NodeKind::Text(text) if !whitespace_sensitive => (
|
||||
text.leading != Whitespace::None,
|
||||
text.trailing != Whitespace::None,
|
||||
text.is_whitespace_only(),
|
||||
),
|
||||
// `<pre />⏎`: the line break HTML drops after the start tag is whitespace after a tag that reads self-closing
|
||||
NodeKind::Element(el)
|
||||
if !whitespace_sensitive && el.leading_lf && closes_at_start_tag(node) =>
|
||||
{
|
||||
(false, true, false)
|
||||
}
|
||||
_ => (false, false, false),
|
||||
};
|
||||
if leading && let Some(prev) = prev {
|
||||
self.facts(prev).has_trailing_spaces = true;
|
||||
}
|
||||
if whitespace_only {
|
||||
pending_space = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
self.node(id, whitespace_sensitive);
|
||||
if matches!(node.kind, NodeKind::Cdata) {
|
||||
any_cdata = true;
|
||||
self.facts(id).has_cdata = true;
|
||||
}
|
||||
let facts = self.facts(id);
|
||||
facts.parent = Some(parent);
|
||||
facts.prev = prev;
|
||||
facts.has_leading_spaces = leading || pending_space;
|
||||
facts.has_trailing_spaces = trailing;
|
||||
match prev {
|
||||
Some(prev) => {
|
||||
if let NodeKind::Comment(comment) = &root[prev].kind
|
||||
&& !comment.bogus
|
||||
{
|
||||
let content = comment.content.slice(self.tree.source);
|
||||
let facts = self.facts(id);
|
||||
facts.prettier_ignore = is_suppression_marker(content);
|
||||
if let Some(display) = display_comment(content) {
|
||||
facts.display = display;
|
||||
}
|
||||
}
|
||||
self.facts(prev).next = Some(id);
|
||||
}
|
||||
None => self.facts(parent).first_child = Some(id),
|
||||
}
|
||||
any_hidden |=
|
||||
self.tree[id].display == Display::None && matches!(node.kind, NodeKind::Element(_));
|
||||
if self.tree.rules.lowercase_known_names {
|
||||
self.mark_name_case(id, parent);
|
||||
}
|
||||
prev = Some(id);
|
||||
pending_space = trailing;
|
||||
}
|
||||
self.facts(parent).last_child = prev;
|
||||
// Inside a hidden parent (`head`) no whitespace renders: nothing to look past
|
||||
let past_hidden = any_hidden && self.tree[parent].display != Display::None;
|
||||
// Runs meet the nodes that render around them, past hidden ones (Angular's blocks look past them themselves):
|
||||
// the one before is the last seen, the one ahead is looked up again on reaching it, so each child is read once
|
||||
let look_past = past_hidden && !self.tree.rules.has_blocks;
|
||||
let mut before: Option<NodeId> = None;
|
||||
let mut ahead =
|
||||
if look_past { self.flow_past(self.tree[parent].first_child, true) } else { None };
|
||||
|
||||
// A side is sensitive when both nodes facing each other say so
|
||||
let mut prev_trailing = false;
|
||||
let mut next = self.tree[parent].first_child;
|
||||
while let Some(id) = next {
|
||||
let (prev, after) = (self.tree[id].prev, self.tree[id].next);
|
||||
let (leading, trailing) = if look_past {
|
||||
let own = (ahead == Some(id)).then_some(id);
|
||||
if own.is_some() {
|
||||
ahead = self.flow_past(after, true);
|
||||
}
|
||||
let sides = (
|
||||
self.side_sensitive_through(id, parent, own.or(ahead), before, true),
|
||||
self.side_sensitive_through(id, parent, own.or(before), ahead, false),
|
||||
);
|
||||
before = own.or(before);
|
||||
sides
|
||||
} else {
|
||||
(
|
||||
self.side_sensitive(id, parent, prev, true),
|
||||
self.side_sensitive(id, parent, after, false),
|
||||
)
|
||||
};
|
||||
if let Some(prev) = prev {
|
||||
self.facts(prev).trailing_sensitive &= leading;
|
||||
}
|
||||
let facts = self.facts(id);
|
||||
facts.leading_sensitive = leading && (prev.is_none() || prev_trailing);
|
||||
facts.trailing_sensitive = trailing;
|
||||
prev_trailing = trailing;
|
||||
next = after;
|
||||
}
|
||||
if past_hidden {
|
||||
self.spread_spaces_past_hidden(parent);
|
||||
}
|
||||
let mut next = self.tree[parent].first_child;
|
||||
while let Some(id) = next {
|
||||
next = self.tree[id].next;
|
||||
if printed_first_child(&root[id]).is_some() {
|
||||
self.children(id);
|
||||
}
|
||||
let facts = &self.tree[id];
|
||||
let ends_in_text_like = facts.last_child.map_or_else(
|
||||
|| facts.text_like || self.tree.is_interpolation(id),
|
||||
|last| self.tree[last].ends_in_text_like,
|
||||
);
|
||||
let ends_in_pre = self.tree.is_unclosed(id)
|
||||
&& (facts.pre_like
|
||||
|| facts.last_child.is_some_and(|last| self.tree[last].ends_in_pre));
|
||||
let facts = self.facts(id);
|
||||
facts.ends_in_text_like = ends_in_text_like;
|
||||
facts.ends_in_pre = ends_in_pre;
|
||||
}
|
||||
|
||||
if any_cdata {
|
||||
self.merge_cdata(parent);
|
||||
}
|
||||
self.merge_simple_elements(parent);
|
||||
|
||||
let mut next = self.tree[parent].first_child;
|
||||
while let Some(id) = next {
|
||||
self.facts(id).borrows = self.borrows(id);
|
||||
next = self.tree[id].next;
|
||||
}
|
||||
}
|
||||
|
||||
/// Needs the node's final siblings and its children's facts.
|
||||
fn borrows(&self, id: NodeId) -> Borrows {
|
||||
let t = &self.tree;
|
||||
let item = &t[id];
|
||||
// A block's markers are no tags: nothing borrows them, and a block borrows nothing
|
||||
if t.is_block(id) {
|
||||
return Borrows::default();
|
||||
}
|
||||
let in_block = item.parent.is_some_and(|parent| t.is_block(parent));
|
||||
Borrows {
|
||||
parent_opening_end: item.prev.is_none() && item.glued_before() && !in_block,
|
||||
prev_closing_end: item.prev.is_some_and(|prev| {
|
||||
!matches!(t.kind(prev), NodeKind::DocType(_))
|
||||
&& !t.is_block(prev)
|
||||
&& !t[prev].text_like
|
||||
}) && item.glued_before(),
|
||||
last_child_closing_end: item.last_child.is_some_and(|last| {
|
||||
t[last].glued_after() && !t[last].ends_in_text_like && !t.is_block(last)
|
||||
}) && !item.pre_like,
|
||||
parent_closing_start: item.next.is_none()
|
||||
&& item.glued_after()
|
||||
&& item.ends_in_text_like
|
||||
&& !in_block,
|
||||
next_opening_start: item
|
||||
.next
|
||||
.is_some_and(|next| !t[next].text_like && !t.is_block(next))
|
||||
&& item.text_like
|
||||
&& item.glued_after(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `<!--[if IE]><!--><html><!--<![endif]-->`: for the start comment, the element whose start tag absorbs it
|
||||
/// together with the end comment, its first child (Prettier's `mergeIfConditionalStartEndCommentIntoElementOpeningTag`).
|
||||
fn absorbing_element(&self, id: NodeId) -> Option<NodeId> {
|
||||
let root = self.tree.root;
|
||||
let start = &root[id];
|
||||
if !matches!(start.kind, NodeKind::ConditionalStart(_)) {
|
||||
return None;
|
||||
}
|
||||
let element = start.next?;
|
||||
let NodeKind::Element(el) = &root[element].kind else { return None };
|
||||
let end = printed_first_child(&root[element])?;
|
||||
(root[element].span.start == start.span.end
|
||||
&& matches!(root[end].kind, NodeKind::ConditionalEnd)
|
||||
&& root[end].span.start == el.start_tag.end)
|
||||
.then_some(element)
|
||||
}
|
||||
|
||||
/// Texts and CDATA sections next to each other print as one text (Prettier's `mergeCdataIntoText`);
|
||||
/// the source between them, whitespace included, is the merged value.
|
||||
fn merge_cdata(&mut self, parent: NodeId) {
|
||||
let mut next = self.tree[parent].first_child;
|
||||
while let Some(id) = next {
|
||||
next = self.tree[id].next;
|
||||
let Some(after) = next else { break };
|
||||
// The parser never puts two texts side by side, so a pair here always holds a section
|
||||
if self.tree.is_text(id) && self.tree.is_text(after) {
|
||||
self.absorb(parent, id, after);
|
||||
// The merged text may absorb the next one too
|
||||
next = Some(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `keep` extends over its siblings up to `last`, which leave the printed links.
|
||||
fn absorb(&mut self, parent: NodeId, keep: NodeId, last: NodeId) {
|
||||
let last = self.tree[last];
|
||||
let facts = self.facts(keep);
|
||||
facts.span.end = last.span.end;
|
||||
facts.trailing_sensitive = last.trailing_sensitive;
|
||||
facts.has_trailing_spaces = last.has_trailing_spaces;
|
||||
facts.has_cdata |= last.has_cdata;
|
||||
facts.next = last.next;
|
||||
match last.next {
|
||||
Some(after_next) => self.facts(after_next).prev = Some(keep),
|
||||
None => self.facts(parent).last_child = Some(keep),
|
||||
}
|
||||
}
|
||||
|
||||
/// `a<b>x</b>c` prints as one text (Prettier's `mergeSimpleElementIntoText`):
|
||||
/// the text before absorbs the element and the text after.
|
||||
fn merge_simple_elements(&mut self, parent: NodeId) {
|
||||
let mut next = self.tree[parent].first_child;
|
||||
while let Some(text) = next {
|
||||
// Prettier rebuilds the text from the parts (`<b >` prints `<b>`);
|
||||
// the source serves when it reads the same.
|
||||
// A chain's text is rebuilt once (`a<b >x</b>c<i>y</i>d`).
|
||||
// The end tag keeps its written name, htm's `<//>` included (DIVERGENCES.md#htm-closing-tag-as-written),
|
||||
// but where known names print lowercase (DIVERGENCES.md#tag-name-case).
|
||||
let mut rebuilt: Option<StringBuilder<'_>> = None;
|
||||
while let Some(id) = self.tree[text].next.filter(|&id| self.is_simple_element(id)) {
|
||||
let t = &self.tree;
|
||||
// `is_simple_element` checked each of these
|
||||
let (el, source) = (t.element(id).expect("an element"), t.source);
|
||||
let child = t[id].first_child.expect("one child");
|
||||
let after = t[id].next.expect("a text after");
|
||||
let end_tag = el.end_tag.expect("an end tag");
|
||||
let (written, written_end) = (el.name.slice(source), end_tag.name.slice(source));
|
||||
let (name, end_name) =
|
||||
(t.printed_name(id, el, written), t.printed_name(id, el, written_end));
|
||||
let plain = el.start_tag.slice(source).len() == written.len() + 2
|
||||
&& end_tag.span.slice(source).len() == written_end.len() + 3
|
||||
&& name == written
|
||||
&& end_name == written_end;
|
||||
if !plain || rebuilt.is_some() {
|
||||
let value = rebuilt.get_or_insert_with(|| {
|
||||
StringBuilder::from_str_in(t[text].span.slice(source), t.allocator)
|
||||
});
|
||||
for piece in [
|
||||
"<",
|
||||
name,
|
||||
">",
|
||||
t[child].span.slice(source),
|
||||
"</",
|
||||
end_name,
|
||||
">",
|
||||
t[after].span.slice(source),
|
||||
] {
|
||||
value.push_str(piece);
|
||||
}
|
||||
}
|
||||
let child_has_cdata = t[child].has_cdata;
|
||||
self.absorb(parent, text, after);
|
||||
self.facts(text).has_cdata |= child_has_cdata;
|
||||
}
|
||||
if let Some(value) = rebuilt {
|
||||
self.tree.merged_texts.push((text, value.into_str()));
|
||||
}
|
||||
next = self.tree[text].next;
|
||||
}
|
||||
}
|
||||
|
||||
/// An inline element with one word inside, glued to texts on both sides.
|
||||
fn is_simple_element(&self, id: NodeId) -> bool {
|
||||
let t = &self.tree;
|
||||
let (item, source) = (&t[id], t.source);
|
||||
let Some(el) = t.element(id) else { return false };
|
||||
let is_text = |id: NodeId| t.is_text(id);
|
||||
item.glued_before()
|
||||
&& item.glued_after()
|
||||
&& item.prev.is_some_and(is_text)
|
||||
&& item.next.is_some_and(is_text)
|
||||
&& el.attributes.is_empty()
|
||||
&& el.end_tag.is_some_and(|tag| tag.ignored.is_none())
|
||||
&& item.only_child().is_some_and(|child| {
|
||||
is_text(child)
|
||||
&& !t[child].has_leading_spaces
|
||||
&& !t[child].has_trailing_spaces
|
||||
&& !t[child].span.slice(source).bytes().any(|b| b.is_ascii_whitespace())
|
||||
})
|
||||
}
|
||||
|
||||
/// The facts of a node that does not depend on its siblings.
|
||||
fn node(&mut self, id: NodeId, in_whitespace_sensitive: bool) {
|
||||
let node = &self.tree.root[id];
|
||||
let (mut self_closing, mut pre_like, mut script_like) = (true, false, false);
|
||||
match &node.kind {
|
||||
NodeKind::Text(text) if !in_whitespace_sensitive => self.facts(id).span = text.content,
|
||||
NodeKind::Interpolation(_) | NodeKind::ConditionalComment(_) | NodeKind::Block(_) => {
|
||||
self_closing = false;
|
||||
}
|
||||
NodeKind::Element(el) => {
|
||||
self_closing = closes_at_start_tag(node);
|
||||
pre_like = is_pre_like(el);
|
||||
script_like = matches!(el.key, "script" | "style");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// CDATA is text to Prettier (`mergeCdataIntoText`),
|
||||
// printed as written here (DIVERGENCES.md#cdata-as-written).
|
||||
let text_like =
|
||||
matches!(node.kind, NodeKind::Text(_) | NodeKind::Comment(_) | NodeKind::Cdata);
|
||||
let display = self.display(id);
|
||||
let facts = self.facts(id);
|
||||
facts.self_closing = self_closing;
|
||||
facts.pre_like = pre_like;
|
||||
facts.script_like = script_like;
|
||||
facts.text_like = text_like;
|
||||
facts.display = display;
|
||||
}
|
||||
|
||||
/// The display from the element's table and the option;
|
||||
/// a `<!-- display: -->` comment overrides it in `children`.
|
||||
fn display(&self, id: NodeId) -> Display {
|
||||
let Some(el) = self.tree.element(id) else {
|
||||
// Text, comments and the document: only the option applies
|
||||
return match self.sensitivity {
|
||||
WhitespaceSensitivity::Ignore => Display::Block,
|
||||
_ => Display::Inline,
|
||||
};
|
||||
};
|
||||
if el.namespace == Namespace::Svg {
|
||||
return if el.key == "svg" {
|
||||
Display::InlineBlock
|
||||
} else if self.sensitivity != WhitespaceSensitivity::Ignore && self.in_svg_text(id) {
|
||||
// Whitespace between a `<text>`'s children renders (DIVERGENCES.md#svg-text-content-inline)
|
||||
Display::Inline
|
||||
} else {
|
||||
Display::Block
|
||||
};
|
||||
}
|
||||
match self.sensitivity {
|
||||
WhitespaceSensitivity::Strict => Display::Inline,
|
||||
WhitespaceSensitivity::Ignore => Display::Block,
|
||||
WhitespaceSensitivity::Css => ua_display(el.key),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an SVG element is inside a `<text>`, 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)
|
||||
.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:
|
||||
/// the leading side looks at the previous sibling as `neighbor`, the trailing side at the next one.
|
||||
fn side_sensitive(
|
||||
&self,
|
||||
id: NodeId,
|
||||
parent_id: NodeId,
|
||||
neighbor: Option<NodeId>,
|
||||
leading: bool,
|
||||
) -> bool {
|
||||
if self.tree.rules.has_blocks {
|
||||
return self.side_sensitive_around_blocks(id, parent_id, neighbor, leading);
|
||||
}
|
||||
let display = neighbor.map(|n| self.tree[n].display);
|
||||
self.side_sensitive_with(id, id, parent_id, neighbor, display, leading)
|
||||
}
|
||||
|
||||
/// Blocks are transparent: a whitespace run continues past their markers, and past hidden elements.
|
||||
fn side_sensitive_around_blocks(
|
||||
&self,
|
||||
id: NodeId,
|
||||
parent_id: NodeId,
|
||||
neighbor: Option<NodeId>,
|
||||
leading: bool,
|
||||
) -> bool {
|
||||
// Across a block, the run meets the content at its near edge, past empty blocks
|
||||
let neighbor = self.flow_past(neighbor, !leading);
|
||||
self.side_sensitive_through(
|
||||
id,
|
||||
parent_id,
|
||||
self.flow_past(Some(id), leading),
|
||||
neighbor,
|
||||
leading,
|
||||
)
|
||||
}
|
||||
|
||||
/// `flow_item` is the node the run meets on `id`'s side (`None` past the parent's edge), `neighbor` the one across it.
|
||||
fn side_sensitive_through(
|
||||
&self,
|
||||
id: NodeId,
|
||||
parent_id: NodeId,
|
||||
flow_item: Option<NodeId>,
|
||||
neighbor: Option<NodeId>,
|
||||
leading: bool,
|
||||
) -> bool {
|
||||
let t = &self.tree;
|
||||
// An empty block passes the run: its sides are those of the content around it
|
||||
let (flow_item, neighbor, leading) = match (flow_item, neighbor) {
|
||||
(Some(flow_item), neighbor) => (flow_item, neighbor, leading),
|
||||
(None, Some(neighbor)) => (neighbor, None, !leading),
|
||||
(None, None) => (id, None, leading),
|
||||
};
|
||||
// At the content's edge, the run is the one the block's own side faces, decided outside it
|
||||
if neighbor.is_none() && t.is_block(parent_id) {
|
||||
let block = &t[parent_id];
|
||||
return if leading { block.leading_sensitive } else { block.trailing_sensitive };
|
||||
}
|
||||
// The rules are the nearest box's
|
||||
let display = neighbor.map(|n| self.display_of(n));
|
||||
self.side_sensitive_with(
|
||||
id,
|
||||
flow_item,
|
||||
self.flow_parent(parent_id),
|
||||
neighbor,
|
||||
display,
|
||||
leading,
|
||||
)
|
||||
}
|
||||
|
||||
/// `flow_item` is the node the run meets on `id`'s side (`id` itself, unless it is a block),
|
||||
/// `display` the neighbor's.
|
||||
fn side_sensitive_with(
|
||||
&self,
|
||||
id: NodeId,
|
||||
flow_item: NodeId,
|
||||
parent_id: NodeId,
|
||||
neighbor: Option<NodeId>,
|
||||
display: Option<Display>,
|
||||
leading: bool,
|
||||
) -> bool {
|
||||
let t = &self.tree;
|
||||
let (item, parent) = (&t[id], &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) {
|
||||
true
|
||||
} else if parent.display == Display::None {
|
||||
false
|
||||
} else if parent.pre_like {
|
||||
true
|
||||
} else if let Some(display) = display {
|
||||
display.sides_sensitive()
|
||||
} else {
|
||||
!(parent_id == NodeId::DOCUMENT
|
||||
|| item.pre_like
|
||||
|| parent.script_like
|
||||
|| !parent.display.edges_sensitive())
|
||||
};
|
||||
// The line break HTML drops after `<pre>` is not content
|
||||
if leading
|
||||
&& sensitive
|
||||
&& neighbor.is_none()
|
||||
&& t.element(parent_id).is_some_and(|el| el.ignores_first_lf)
|
||||
{
|
||||
return t.is_interpolation(id);
|
||||
}
|
||||
sensitive
|
||||
}
|
||||
|
||||
/// The node a whitespace run meets at `id`'s start (`start`) or end, looking into blocks:
|
||||
/// a block's first (last) child that is not whitespace-only;
|
||||
/// `None` for a block without content or a hidden element, which the run passes.
|
||||
fn flow_edge(&self, id: NodeId, start: bool) -> Option<NodeId> {
|
||||
if self.is_hidden(id) {
|
||||
return None;
|
||||
}
|
||||
if !self.tree.is_block(id) {
|
||||
return Some(id);
|
||||
}
|
||||
let mut edges = self
|
||||
.tree
|
||||
.root
|
||||
.children(id)
|
||||
.filter(|(_, node)| !is_whitespace_text(node))
|
||||
.filter_map(|(child, _)| self.flow_edge(child, start));
|
||||
if start { edges.next() } else { edges.last() }
|
||||
}
|
||||
|
||||
/// The node a whitespace run meets from `sibling` on, going forward (`forward`) or back, past empty blocks.
|
||||
fn flow_past(&self, sibling: Option<NodeId>, forward: bool) -> Option<NodeId> {
|
||||
let t = &self.tree;
|
||||
std::iter::successors(sibling, |&id| if forward { t[id].next } else { t[id].prev })
|
||||
.find_map(|id| self.flow_edge(id, forward))
|
||||
}
|
||||
|
||||
/// An element with `display: none` renders nothing, so the whitespace on both its sides is one run
|
||||
/// between the content around it (DIVERGENCES.md#script-display-none).
|
||||
/// Not `Tree::is_transparent`: a block's children are read before they are built.
|
||||
#[inline]
|
||||
fn is_hidden(&self, id: NodeId) -> bool {
|
||||
self.display_of(id) == Display::None && self.tree.element(id).is_some()
|
||||
}
|
||||
|
||||
/// Where hidden siblings join gaps into one run, whitespace in one gap lets every gap of the run take it:
|
||||
/// the run renders the one space either way (`a</span> <script></script><span>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 `<my-el />` and an ancestor's end tag closes it, but the tag reads as self-closing (as Prettier reads it):
|
||||
/// its content follows it as siblings (`next_child`).
|
||||
/// The text prints the same either way; whitespace inside such an element is judged as whitespace between siblings,
|
||||
/// so `<div><my-el />⏎</div>` drops the `⏎` HTML puts in `my-el`, collapsible at the end of a block and never rendered.
|
||||
/// Except in `<pre />`, whose content keeps its whitespace: re-indented as siblings, it would render differently
|
||||
/// (DIVERGENCES.md#pre-content-without-end-tag).
|
||||
fn closes_at_start_tag(node: &Node<'_>) -> bool {
|
||||
let NodeKind::Element(el) = &node.kind else { return false };
|
||||
el.void
|
||||
|| (el.self_closing
|
||||
&& el.end_tag.is_none()
|
||||
&& (!is_pre_like(el) || node.first_child.is_none()))
|
||||
}
|
||||
|
||||
/// The parser's first child, unless the content follows the element as siblings (`next_child`).
|
||||
fn printed_first_child(node: &Node<'_>) -> Option<NodeId> {
|
||||
node.first_child.filter(|_| !closes_at_start_tag(node))
|
||||
}
|
||||
|
||||
/// The node after `id` among `parent`'s children,
|
||||
/// with the content of an element whose start tag closes it following it as siblings.
|
||||
fn next_child(root: &Root<'_>, parent: NodeId, mut id: NodeId) -> Option<NodeId> {
|
||||
if closes_at_start_tag(&root[id])
|
||||
&& let Some(first) = root[id].first_child
|
||||
{
|
||||
return Some(first);
|
||||
}
|
||||
loop {
|
||||
if let Some(next) = root[id].next {
|
||||
return Some(next);
|
||||
}
|
||||
id = root[id].parent.filter(|&up| up != parent)?;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/// The CSS `display` an element gets from the UA stylesheet (Prettier's table, `constants.evaluate.js`).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Display {
|
||||
#[default]
|
||||
Inline,
|
||||
Block,
|
||||
InlineBlock,
|
||||
ListItem,
|
||||
TableCell,
|
||||
/// Every other `table*` value.
|
||||
Table,
|
||||
None,
|
||||
/// `contents`, `ruby`, or a value from a `<!-- display: -->` comment.
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
fn block_like(self) -> bool {
|
||||
matches!(self, Self::Block | Self::ListItem | Self::Table | Self::TableCell)
|
||||
}
|
||||
|
||||
/// Whitespace at the start of the first child / the end of the last child / of an empty element is significant.
|
||||
pub fn edges_sensitive(self) -> bool {
|
||||
!self.block_like() && self != Self::InlineBlock
|
||||
}
|
||||
|
||||
/// Whitespace between this and a sibling is significant.
|
||||
pub fn sides_sensitive(self) -> bool {
|
||||
!self.block_like()
|
||||
}
|
||||
}
|
||||
|
||||
/// `<!-- display: block -->` (Prettier's `/^\s*display:\s*([a-z]+)\s*$/`).
|
||||
pub fn display_comment(content: &str) -> Option<Display> {
|
||||
let value = content.trim().strip_prefix("display:")?.trim_start();
|
||||
if value.is_empty() || !value.bytes().all(|b| b.is_ascii_lowercase()) {
|
||||
return None;
|
||||
}
|
||||
Some(match value {
|
||||
"block" => Display::Block,
|
||||
"inline" => Display::Inline,
|
||||
"table" => Display::Table,
|
||||
"none" => Display::None,
|
||||
_ => Display::Other,
|
||||
})
|
||||
}
|
||||
|
||||
/// The UA stylesheet's `display` for an HTML element, as Prettier tabulates it.
|
||||
pub fn ua_display(tag: &str) -> Display {
|
||||
match tag {
|
||||
// `script` as the UA has it;
|
||||
// Prettier lays it out as a block (DIVERGENCES.md#script-display-none).
|
||||
"area" | "base" | "basefont" | "datalist" | "head" | "link" | "meta" | "noembed"
|
||||
| "noframes" | "rp" | "script" | "style" | "title" => Display::None,
|
||||
"param" | "html" | "body" | "address" | "blockquote" | "center" | "div" | "figure"
|
||||
| "figcaption" | "footer" | "form" | "header" | "hr" | "legend" | "listing" | "main"
|
||||
| "p" | "plaintext" | "pre" | "search" | "xmp" | "article" | "aside" | "h1" | "h2"
|
||||
| "h3" | "h4" | "h5" | "h6" | "hgroup" | "nav" | "section" | "dir" | "dd" | "dl" | "dt"
|
||||
| "menu" | "ol" | "ul" | "fieldset" | "details" | "summary" | "option" | "optgroup"
|
||||
| "source" | "track" => Display::Block,
|
||||
"slot" | "ruby" | "rt" => Display::Other,
|
||||
"li" => Display::ListItem,
|
||||
"table" | "caption" | "colgroup" | "col" | "thead" | "tbody" | "tfoot" | "tr" => {
|
||||
Display::Table
|
||||
}
|
||||
"td" | "th" => Display::TableCell,
|
||||
// `dialog`: out of flow (`display: none` closed, `position: absolute` open), with a box for its content;
|
||||
// Prettier lays it out as a block (DIVERGENCES.md#dialog-out-of-flow).
|
||||
"input" | "button" | "marquee" | "select" | "meter" | "progress" | "object" | "video"
|
||||
| "audio" | "dialog" => Display::InlineBlock,
|
||||
_ => Display::Inline,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
//! The facts Prettier's `print-preprocess` attaches to its tree, as a table keyed by the parser's [`NodeId`].
|
||||
//!
|
||||
//! The AST is not rewritten; names, kinds and the tag table's verdict are read from it.
|
||||
//! What the table adds:
|
||||
//! - the nodes that print, linked by `parent` / `prev` / `next` / `first_child` / `last_child`:
|
||||
//! a whitespace-only text of a whitespace-insensitive parent is skipped (it becomes `has_*_spaces` of its neighbors),
|
||||
//! and the content of an element that reads as self-closing (`<my-el />`) follows it as siblings;
|
||||
//! - CSS display and whitespace sensitivity, which depend on the options;
|
||||
//! - facts the printer asks repeatedly (self-closing, `ends_in_text_like`, `prettier-ignore`).
|
||||
|
||||
mod build;
|
||||
mod display;
|
||||
mod names;
|
||||
|
||||
use oxc_allocator::{Allocator, ArenaVec, StringBuilder};
|
||||
use oxc_htmlish_parser::{
|
||||
Span,
|
||||
ast::{Element, Namespace, NodeId, NodeKind, Root},
|
||||
};
|
||||
|
||||
use crate::rules::DialectRules;
|
||||
|
||||
pub use display::Display;
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Facts {
|
||||
/// Prettier's `sourceSpan`: a text in a whitespace-insensitive parent is trimmed.
|
||||
pub span: Span,
|
||||
/// The parent, siblings and children that print.
|
||||
pub parent: Option<NodeId>,
|
||||
pub prev: Option<NodeId>,
|
||||
pub next: Option<NodeId>,
|
||||
pub first_child: Option<NodeId>,
|
||||
pub last_child: Option<NodeId>,
|
||||
pub display: Display,
|
||||
/// Text and comments: printed without tags whose markers could be borrowed.
|
||||
pub text_like: bool,
|
||||
/// 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.
|
||||
/// Children keep their whitespace and indentation.
|
||||
pub pre_like: bool,
|
||||
/// `script` / `style`: children keep their whitespace.
|
||||
pub script_like: bool,
|
||||
/// 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:
|
||||
/// 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.
|
||||
pub has_cdata: bool,
|
||||
/// The previous sibling is `<!-- prettier-ignore -->`.
|
||||
pub prettier_ignore: bool,
|
||||
/// An SVG / MathML element, or one under it (`DialectRules::lowercase_known_names` only).
|
||||
pub foreign: bool,
|
||||
/// The tag name prints lowercase, written otherwise in the start or end tag (`Tree::printed_name`).
|
||||
pub lowercase_name: bool,
|
||||
pub has_leading_spaces: bool,
|
||||
pub has_trailing_spaces: bool,
|
||||
pub leading_sensitive: bool,
|
||||
pub trailing_sensitive: bool,
|
||||
/// An element without children had only whitespace inside.
|
||||
pub has_dangling_spaces: bool,
|
||||
pub dangling_sensitive: bool,
|
||||
/// An element whose start tag absorbs `<!--[if X]><!-->` right before it and `<!--<![endif]-->` right after it
|
||||
/// (`<!--[if X]><!--><html><!--<![endif]-->`): those two comments, which do not print on their own.
|
||||
/// The start comment; the end comment is the element's first child.
|
||||
pub condition: Option<NodeId>,
|
||||
pub borrows: Borrows,
|
||||
}
|
||||
|
||||
/// The tag markers a node prints for its neighbors (the borrow rules in `print/tag.rs`);
|
||||
/// the owners omit them (`*_borrowed`).
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct Borrows {
|
||||
/// `<p\n >123`: the first child prints its parent's opening `>`.
|
||||
pub parent_opening_end: bool,
|
||||
/// `<p></p\n>123`: the previous sibling's closing `>`.
|
||||
pub prev_closing_end: bool,
|
||||
/// `<p\n ><a></a\n ></p\n>`: the last child's closing `>`, before the node's own end tag.
|
||||
pub last_child_closing_end: bool,
|
||||
/// `123</p\n>`: the last text prints its parent's `</p`.
|
||||
pub parent_closing_start: bool,
|
||||
/// `123<p\n>`: the text prints its next sibling's `<p`.
|
||||
pub next_opening_start: bool,
|
||||
}
|
||||
|
||||
impl Facts {
|
||||
/// Children keep their whitespace.
|
||||
pub fn whitespace_sensitive(&self) -> 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<NodeId> {
|
||||
self.first_child.filter(|_| self.first_child == self.last_child)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Tree<'a> {
|
||||
allocator: &'a Allocator,
|
||||
rules: DialectRules,
|
||||
root: &'a Root<'a>,
|
||||
source: &'a str,
|
||||
facts: ArenaVec<'a, Facts>,
|
||||
/// Texts whose merged value differs from their source
|
||||
/// (an element merged in had whitespace in a tag), sorted by `NodeId`.
|
||||
merged_texts: ArenaVec<'a, (NodeId, &'a str)>,
|
||||
}
|
||||
|
||||
impl std::ops::Index<NodeId> for Tree<'_> {
|
||||
type Output = Facts;
|
||||
|
||||
fn index(&self, id: NodeId) -> &Facts {
|
||||
&self.facts[id.index()]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Tree<'a> {
|
||||
pub fn source(&self) -> &'a str {
|
||||
self.source
|
||||
}
|
||||
|
||||
pub fn kind(&self, id: NodeId) -> &'a NodeKind<'a> {
|
||||
&self.root[id].kind
|
||||
}
|
||||
|
||||
pub fn rules(&self) -> DialectRules {
|
||||
self.rules
|
||||
}
|
||||
|
||||
/// An Angular control flow block: transparent, its markers print no box (`@if (a) {` … `}`).
|
||||
pub fn is_block(&self, id: NodeId) -> bool {
|
||||
self.rules.has_blocks && matches!(self.kind(id), NodeKind::Block(_))
|
||||
}
|
||||
|
||||
/// A `display: none` element in a parent that renders: the whitespace on both its sides is one run
|
||||
/// (DIVERGENCES.md#script-display-none).
|
||||
#[inline]
|
||||
pub fn is_transparent(&self, id: NodeId) -> bool {
|
||||
self[id].display == Display::None
|
||||
&& self.element(id).is_some()
|
||||
&& self[self.parent(id)].display != Display::None
|
||||
}
|
||||
|
||||
pub fn is_interpolation(&self, id: NodeId) -> bool {
|
||||
matches!(self.kind(id), NodeKind::Interpolation(_))
|
||||
}
|
||||
|
||||
/// A text or a CDATA section, which Prettier merges into text (`mergeCdataIntoText`).
|
||||
pub fn is_text(&self, id: NodeId) -> bool {
|
||||
matches!(self.kind(id), NodeKind::Text(_) | NodeKind::Cdata)
|
||||
}
|
||||
|
||||
pub fn element(&self, id: NodeId) -> Option<&'a Element<'a>> {
|
||||
match &self.root[id].kind {
|
||||
NodeKind::Element(el) => Some(el),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The tag name to print (DIVERGENCES.md#tag-name-case); empty for other nodes.
|
||||
#[inline]
|
||||
pub fn name(&self, id: NodeId) -> &'a str {
|
||||
self.element(id).map_or("", |el| self.printed_name(id, el, el.name.slice(self.source)))
|
||||
}
|
||||
|
||||
/// A start or end tag's name `written`, or the element's lowercase name where it prints so (DIVERGENCES.md#tag-name-case);
|
||||
/// htm's `<//>` closes as written.
|
||||
#[inline]
|
||||
pub fn printed_name(&self, id: NodeId, el: &Element<'a>, written: &'a str) -> &'a str {
|
||||
if self[id].lowercase_name && written != "/" { el.key } else { written }
|
||||
}
|
||||
|
||||
/// An attribute's name written `written` to print: as written, or Prettier's known name in lowercase
|
||||
/// (DIVERGENCES.md#tag-name-case).
|
||||
#[inline]
|
||||
pub fn attribute_name(&self, element: NodeId, written: &'a str) -> &'a str {
|
||||
if !self.rules.lowercase_known_names || !written.bytes().any(|b| b.is_ascii_uppercase()) {
|
||||
return written;
|
||||
}
|
||||
match self.element(element) {
|
||||
Some(el) if self.lowercases_names(element, el) => {
|
||||
names::known_attribute(el.key, written).unwrap_or(written)
|
||||
}
|
||||
_ => written,
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the element's known names print lowercase: a known HTML element outside foreign content.
|
||||
/// `svg` / `math` themselves are known; under them Prettier keeps every name, `foreignObject`'s HTML too.
|
||||
pub fn lowercases_names(&self, id: NodeId, el: &Element<'a>) -> bool {
|
||||
!self[id].parent.is_some_and(|parent| self[parent].foreign)
|
||||
&& (el.namespace == Namespace::Html || matches!(el.key, "svg" | "math"))
|
||||
&& names::is_known_element(el.key)
|
||||
}
|
||||
|
||||
/// Whether an attribute written `written` is the one named `name` (lowercase).
|
||||
/// Prettier lowercases only the attributes it knows for the element; any name here is close enough.
|
||||
pub fn attribute_is(&self, written: &str, name: &str) -> bool {
|
||||
if self.rules.names_ignore_case {
|
||||
written.eq_ignore_ascii_case(name)
|
||||
} else {
|
||||
written == name
|
||||
}
|
||||
}
|
||||
|
||||
/// A text's value: its source, unless a merge rewrote it.
|
||||
pub fn text(&self, id: NodeId) -> &'a str {
|
||||
match self.merged_texts.binary_search_by_key(&id, |(text, _)| *text) {
|
||||
Ok(index) => self.merged_texts[index].1,
|
||||
Err(_) => self[id].span.slice(self.source),
|
||||
}
|
||||
}
|
||||
|
||||
/// A conditional comment's condition or a doctype's value, whitespace collapsed.
|
||||
pub fn collapsed(&self, span: Span) -> &'a str {
|
||||
collapse_whitespace(self.allocator, span.slice(self.source))
|
||||
}
|
||||
|
||||
/// The name the HTML tables are keyed by (the parser's, per dialect); empty for other nodes.
|
||||
pub fn tag(&self, id: NodeId) -> &'a str {
|
||||
self.element(id).map_or("", |el| el.key)
|
||||
}
|
||||
|
||||
pub fn parent(&self, id: NodeId) -> NodeId {
|
||||
self[id].parent.expect("only the document has no parent")
|
||||
}
|
||||
|
||||
/// The printed children, in order.
|
||||
pub fn children(&self, id: NodeId) -> impl Iterator<Item = NodeId> + '_ {
|
||||
std::iter::successors(self[id].first_child, move |&child| self[child].next)
|
||||
}
|
||||
|
||||
/// Where the content between the tags starts (Prettier's `startSourceSpan.end`).
|
||||
pub fn content_start(&self, id: NodeId) -> u32 {
|
||||
if self[id].condition.is_some()
|
||||
&& let Some(end) = self.root[id].first_child
|
||||
{
|
||||
return self.root[end].span.end;
|
||||
}
|
||||
match &self.root[id].kind {
|
||||
NodeKind::Element(el) => el.start_tag.end,
|
||||
NodeKind::ConditionalComment(comment) => comment.content.start,
|
||||
NodeKind::Block(block) => block.open.end,
|
||||
_ => self[id].span.start,
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the end tag starts, when there is one (Prettier's `endSourceSpan.start`).
|
||||
pub fn end_tag_start(&self, id: NodeId) -> Option<u32> {
|
||||
match &self.root[id].kind {
|
||||
NodeKind::Element(el) => el.end_tag.map(|tag| tag.span.start),
|
||||
NodeKind::ConditionalComment(comment) => Some(comment.content.end),
|
||||
NodeKind::Block(block) => block.close.map(|close| close.start),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// An element without an end tag (closed by an ancestor's, a sibling's or the end of input)
|
||||
/// that does not read as self-closing.
|
||||
pub fn is_unclosed(&self, id: NodeId) -> bool {
|
||||
self.element(id).is_some_and(|el| el.end_tag.is_none()) && !self[id].self_closing
|
||||
}
|
||||
|
||||
/// Where Prettier's `sourceSpan` of the node ends:
|
||||
/// an element without an end tag ends at its start tag, children aside.
|
||||
pub fn source_end(&self, id: NodeId) -> u32 {
|
||||
match &self.root[id].kind {
|
||||
NodeKind::Element(el) if el.end_tag.is_none() => el.start_tag.end,
|
||||
_ => self[id].span.end,
|
||||
}
|
||||
}
|
||||
|
||||
/// Line breaks in `[from, to)`, counted up to 2:
|
||||
/// Prettier compares line numbers, so a gap is on the same line, the next line, or further.
|
||||
pub fn line_breaks(&self, from: u32, to: u32) -> usize {
|
||||
if from >= to {
|
||||
return 0;
|
||||
}
|
||||
memchr::memchr_iter(b'\n', &self.source.as_bytes()[from as usize..to as usize])
|
||||
.take(2)
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
/// `text` trimmed, with its whitespace runs collapsed to single spaces; borrowed when already so.
|
||||
pub fn collapse_whitespace<'a>(allocator: &'a Allocator, text: &'a str) -> &'a str {
|
||||
let text = text.trim_ascii();
|
||||
let collapsed = text
|
||||
.as_bytes()
|
||||
.windows(2)
|
||||
.all(|pair| !pair[1].is_ascii_whitespace() || (pair[1] == b' ' && pair[0] != b' '));
|
||||
if collapsed {
|
||||
return text;
|
||||
}
|
||||
let mut out = StringBuilder::new_in(allocator);
|
||||
for (i, word) in text.split_ascii_whitespace().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(' ');
|
||||
}
|
||||
out.push_str(word);
|
||||
}
|
||||
out.into_str()
|
||||
}
|
||||
@@ -0,0 +1,586 @@
|
||||
//! The names Prettier's HTML parser lowercases (DIVERGENCES.md#tag-name-case):
|
||||
//! `@prettier/html-tags` 2.2.0 and `html-element-attributes` 3.5.0, as the pin bundles them.
|
||||
//! Read only for a name holding an uppercase letter, so the tables are sorted slices.
|
||||
|
||||
const ELEMENTS: &[&str] = &[
|
||||
"a",
|
||||
"abbr",
|
||||
"acronym",
|
||||
"address",
|
||||
"applet",
|
||||
"area",
|
||||
"article",
|
||||
"aside",
|
||||
"audio",
|
||||
"b",
|
||||
"base",
|
||||
"basefont",
|
||||
"bdi",
|
||||
"bdo",
|
||||
"bgsound",
|
||||
"big",
|
||||
"blink",
|
||||
"blockquote",
|
||||
"body",
|
||||
"br",
|
||||
"button",
|
||||
"canvas",
|
||||
"caption",
|
||||
"center",
|
||||
"cite",
|
||||
"code",
|
||||
"col",
|
||||
"colgroup",
|
||||
"command",
|
||||
"content",
|
||||
"data",
|
||||
"datalist",
|
||||
"dd",
|
||||
"del",
|
||||
"details",
|
||||
"dfn",
|
||||
"dialog",
|
||||
"dir",
|
||||
"div",
|
||||
"dl",
|
||||
"dt",
|
||||
"em",
|
||||
"embed",
|
||||
"fencedframe",
|
||||
"fieldset",
|
||||
"figcaption",
|
||||
"figure",
|
||||
"font",
|
||||
"footer",
|
||||
"form",
|
||||
"frame",
|
||||
"frameset",
|
||||
"geolocation",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"head",
|
||||
"header",
|
||||
"hgroup",
|
||||
"hr",
|
||||
"html",
|
||||
"i",
|
||||
"iframe",
|
||||
"image",
|
||||
"img",
|
||||
"input",
|
||||
"ins",
|
||||
"isindex",
|
||||
"kbd",
|
||||
"keygen",
|
||||
"label",
|
||||
"legend",
|
||||
"li",
|
||||
"link",
|
||||
"listing",
|
||||
"main",
|
||||
"map",
|
||||
"mark",
|
||||
"marquee",
|
||||
"math",
|
||||
"menu",
|
||||
"menuitem",
|
||||
"meta",
|
||||
"meter",
|
||||
"multicol",
|
||||
"nav",
|
||||
"nextid",
|
||||
"nobr",
|
||||
"noembed",
|
||||
"noframes",
|
||||
"noscript",
|
||||
"object",
|
||||
"ol",
|
||||
"optgroup",
|
||||
"option",
|
||||
"output",
|
||||
"p",
|
||||
"param",
|
||||
"picture",
|
||||
"plaintext",
|
||||
"pre",
|
||||
"progress",
|
||||
"q",
|
||||
"rb",
|
||||
"rbc",
|
||||
"rp",
|
||||
"rt",
|
||||
"rtc",
|
||||
"ruby",
|
||||
"s",
|
||||
"samp",
|
||||
"script",
|
||||
"search",
|
||||
"section",
|
||||
"select",
|
||||
"selectedcontent",
|
||||
"shadow",
|
||||
"slot",
|
||||
"small",
|
||||
"source",
|
||||
"spacer",
|
||||
"span",
|
||||
"strike",
|
||||
"strong",
|
||||
"style",
|
||||
"sub",
|
||||
"summary",
|
||||
"sup",
|
||||
"svg",
|
||||
"table",
|
||||
"tbody",
|
||||
"td",
|
||||
"template",
|
||||
"textarea",
|
||||
"tfoot",
|
||||
"th",
|
||||
"thead",
|
||||
"time",
|
||||
"title",
|
||||
"tr",
|
||||
"track",
|
||||
"tt",
|
||||
"u",
|
||||
"ul",
|
||||
"var",
|
||||
"video",
|
||||
"wbr",
|
||||
"xmp",
|
||||
];
|
||||
|
||||
const GLOBAL_ATTRIBUTES: &[&str] = &[
|
||||
"accesskey",
|
||||
"autocapitalize",
|
||||
"autocorrect",
|
||||
"autofocus",
|
||||
"class",
|
||||
"contenteditable",
|
||||
"dir",
|
||||
"draggable",
|
||||
"enterkeyhint",
|
||||
"exportparts",
|
||||
"hidden",
|
||||
"id",
|
||||
"inert",
|
||||
"inputmode",
|
||||
"is",
|
||||
"itemid",
|
||||
"itemprop",
|
||||
"itemref",
|
||||
"itemscope",
|
||||
"itemtype",
|
||||
"lang",
|
||||
"nonce",
|
||||
"part",
|
||||
"popover",
|
||||
"slot",
|
||||
"spellcheck",
|
||||
"style",
|
||||
"tabindex",
|
||||
"title",
|
||||
"translate",
|
||||
"writingsuggestions",
|
||||
];
|
||||
|
||||
/// By element, each list sorted.
|
||||
const ELEMENT_ATTRIBUTES: &[(&str, &[&str])] = &[
|
||||
(
|
||||
"a",
|
||||
&[
|
||||
"charset",
|
||||
"coords",
|
||||
"download",
|
||||
"href",
|
||||
"hreflang",
|
||||
"name",
|
||||
"ping",
|
||||
"referrerpolicy",
|
||||
"rel",
|
||||
"rev",
|
||||
"shape",
|
||||
"target",
|
||||
"type",
|
||||
],
|
||||
),
|
||||
(
|
||||
"applet",
|
||||
&[
|
||||
"align", "alt", "archive", "code", "codebase", "height", "hspace", "name", "object",
|
||||
"vspace", "width",
|
||||
],
|
||||
),
|
||||
(
|
||||
"area",
|
||||
&[
|
||||
"alt",
|
||||
"coords",
|
||||
"download",
|
||||
"href",
|
||||
"hreflang",
|
||||
"nohref",
|
||||
"ping",
|
||||
"referrerpolicy",
|
||||
"rel",
|
||||
"shape",
|
||||
"target",
|
||||
"type",
|
||||
],
|
||||
),
|
||||
("audio", &["autoplay", "controls", "crossorigin", "loop", "muted", "preload", "src"]),
|
||||
("base", &["href", "target"]),
|
||||
("basefont", &["color", "face", "size"]),
|
||||
("blockquote", &["cite"]),
|
||||
("body", &["alink", "background", "bgcolor", "link", "text", "vlink"]),
|
||||
("br", &["clear"]),
|
||||
(
|
||||
"button",
|
||||
&[
|
||||
"command",
|
||||
"commandfor",
|
||||
"disabled",
|
||||
"form",
|
||||
"formaction",
|
||||
"formenctype",
|
||||
"formmethod",
|
||||
"formnovalidate",
|
||||
"formtarget",
|
||||
"name",
|
||||
"popovertarget",
|
||||
"popovertargetaction",
|
||||
"type",
|
||||
"value",
|
||||
],
|
||||
),
|
||||
("canvas", &["height", "width"]),
|
||||
("caption", &["align"]),
|
||||
("col", &["align", "char", "charoff", "span", "valign", "width"]),
|
||||
("colgroup", &["align", "char", "charoff", "span", "valign", "width"]),
|
||||
("data", &["value"]),
|
||||
("del", &["cite", "datetime"]),
|
||||
("details", &["name", "open"]),
|
||||
("dialog", &["closedby", "open"]),
|
||||
("dir", &["compact"]),
|
||||
("div", &["align"]),
|
||||
("dl", &["compact"]),
|
||||
("embed", &["height", "src", "type", "width"]),
|
||||
("fieldset", &["disabled", "form", "name"]),
|
||||
("font", &["color", "face", "size"]),
|
||||
(
|
||||
"form",
|
||||
&[
|
||||
"accept",
|
||||
"accept-charset",
|
||||
"action",
|
||||
"autocomplete",
|
||||
"enctype",
|
||||
"method",
|
||||
"name",
|
||||
"novalidate",
|
||||
"target",
|
||||
],
|
||||
),
|
||||
(
|
||||
"frame",
|
||||
&[
|
||||
"frameborder",
|
||||
"longdesc",
|
||||
"marginheight",
|
||||
"marginwidth",
|
||||
"name",
|
||||
"noresize",
|
||||
"scrolling",
|
||||
"src",
|
||||
],
|
||||
),
|
||||
("frameset", &["cols", "rows"]),
|
||||
("h1", &["align"]),
|
||||
("h2", &["align"]),
|
||||
("h3", &["align"]),
|
||||
("h4", &["align"]),
|
||||
("h5", &["align"]),
|
||||
("h6", &["align"]),
|
||||
("head", &["profile"]),
|
||||
("hr", &["align", "noshade", "size", "width"]),
|
||||
("html", &["manifest", "version"]),
|
||||
(
|
||||
"iframe",
|
||||
&[
|
||||
"align",
|
||||
"allow",
|
||||
"allowfullscreen",
|
||||
"allowpaymentrequest",
|
||||
"allowusermedia",
|
||||
"frameborder",
|
||||
"height",
|
||||
"loading",
|
||||
"longdesc",
|
||||
"marginheight",
|
||||
"marginwidth",
|
||||
"name",
|
||||
"referrerpolicy",
|
||||
"sandbox",
|
||||
"scrolling",
|
||||
"src",
|
||||
"srcdoc",
|
||||
"width",
|
||||
],
|
||||
),
|
||||
(
|
||||
"img",
|
||||
&[
|
||||
"align",
|
||||
"alt",
|
||||
"border",
|
||||
"crossorigin",
|
||||
"decoding",
|
||||
"fetchpriority",
|
||||
"height",
|
||||
"hspace",
|
||||
"ismap",
|
||||
"loading",
|
||||
"longdesc",
|
||||
"name",
|
||||
"referrerpolicy",
|
||||
"sizes",
|
||||
"src",
|
||||
"srcset",
|
||||
"usemap",
|
||||
"vspace",
|
||||
"width",
|
||||
],
|
||||
),
|
||||
(
|
||||
"input",
|
||||
&[
|
||||
"accept",
|
||||
"align",
|
||||
"alpha",
|
||||
"alt",
|
||||
"autocomplete",
|
||||
"checked",
|
||||
"colorspace",
|
||||
"dirname",
|
||||
"disabled",
|
||||
"form",
|
||||
"formaction",
|
||||
"formenctype",
|
||||
"formmethod",
|
||||
"formnovalidate",
|
||||
"formtarget",
|
||||
"height",
|
||||
"ismap",
|
||||
"list",
|
||||
"max",
|
||||
"maxlength",
|
||||
"min",
|
||||
"minlength",
|
||||
"multiple",
|
||||
"name",
|
||||
"pattern",
|
||||
"placeholder",
|
||||
"popovertarget",
|
||||
"popovertargetaction",
|
||||
"readonly",
|
||||
"required",
|
||||
"size",
|
||||
"src",
|
||||
"step",
|
||||
"type",
|
||||
"usemap",
|
||||
"value",
|
||||
"width",
|
||||
],
|
||||
),
|
||||
("ins", &["cite", "datetime"]),
|
||||
("isindex", &["prompt"]),
|
||||
("label", &["for", "form"]),
|
||||
("legend", &["align"]),
|
||||
("li", &["type", "value"]),
|
||||
(
|
||||
"link",
|
||||
&[
|
||||
"as",
|
||||
"blocking",
|
||||
"charset",
|
||||
"color",
|
||||
"crossorigin",
|
||||
"disabled",
|
||||
"fetchpriority",
|
||||
"href",
|
||||
"hreflang",
|
||||
"imagesizes",
|
||||
"imagesrcset",
|
||||
"integrity",
|
||||
"media",
|
||||
"referrerpolicy",
|
||||
"rel",
|
||||
"rev",
|
||||
"sizes",
|
||||
"target",
|
||||
"type",
|
||||
],
|
||||
),
|
||||
("map", &["name"]),
|
||||
("menu", &["compact"]),
|
||||
("meta", &["charset", "content", "http-equiv", "media", "name", "scheme"]),
|
||||
("meter", &["high", "low", "max", "min", "optimum", "value"]),
|
||||
(
|
||||
"object",
|
||||
&[
|
||||
"align",
|
||||
"archive",
|
||||
"border",
|
||||
"classid",
|
||||
"codebase",
|
||||
"codetype",
|
||||
"data",
|
||||
"declare",
|
||||
"form",
|
||||
"height",
|
||||
"hspace",
|
||||
"name",
|
||||
"standby",
|
||||
"type",
|
||||
"typemustmatch",
|
||||
"usemap",
|
||||
"vspace",
|
||||
"width",
|
||||
],
|
||||
),
|
||||
("ol", &["compact", "reversed", "start", "type"]),
|
||||
("optgroup", &["disabled", "label"]),
|
||||
("option", &["disabled", "label", "selected", "value"]),
|
||||
("output", &["for", "form", "name"]),
|
||||
("p", &["align"]),
|
||||
("param", &["name", "type", "value", "valuetype"]),
|
||||
("pre", &["width"]),
|
||||
("progress", &["max", "value"]),
|
||||
("q", &["cite"]),
|
||||
(
|
||||
"script",
|
||||
&[
|
||||
"async",
|
||||
"blocking",
|
||||
"charset",
|
||||
"crossorigin",
|
||||
"defer",
|
||||
"fetchpriority",
|
||||
"integrity",
|
||||
"language",
|
||||
"nomodule",
|
||||
"referrerpolicy",
|
||||
"src",
|
||||
"type",
|
||||
],
|
||||
),
|
||||
("select", &["autocomplete", "disabled", "form", "multiple", "name", "required", "size"]),
|
||||
("slot", &["name"]),
|
||||
("source", &["height", "media", "sizes", "src", "srcset", "type", "width"]),
|
||||
("style", &["blocking", "media", "type"]),
|
||||
(
|
||||
"table",
|
||||
&[
|
||||
"align",
|
||||
"bgcolor",
|
||||
"border",
|
||||
"cellpadding",
|
||||
"cellspacing",
|
||||
"frame",
|
||||
"rules",
|
||||
"summary",
|
||||
"width",
|
||||
],
|
||||
),
|
||||
("tbody", &["align", "char", "charoff", "valign"]),
|
||||
(
|
||||
"td",
|
||||
&[
|
||||
"abbr", "align", "axis", "bgcolor", "char", "charoff", "colspan", "headers", "height",
|
||||
"nowrap", "rowspan", "scope", "valign", "width",
|
||||
],
|
||||
),
|
||||
(
|
||||
"template",
|
||||
&[
|
||||
"shadowrootclonable",
|
||||
"shadowrootcustomelementregistry",
|
||||
"shadowrootdelegatesfocus",
|
||||
"shadowrootmode",
|
||||
"shadowrootserializable",
|
||||
],
|
||||
),
|
||||
(
|
||||
"textarea",
|
||||
&[
|
||||
"autocomplete",
|
||||
"cols",
|
||||
"dirname",
|
||||
"disabled",
|
||||
"form",
|
||||
"maxlength",
|
||||
"minlength",
|
||||
"name",
|
||||
"placeholder",
|
||||
"readonly",
|
||||
"required",
|
||||
"rows",
|
||||
"wrap",
|
||||
],
|
||||
),
|
||||
("tfoot", &["align", "char", "charoff", "valign"]),
|
||||
(
|
||||
"th",
|
||||
&[
|
||||
"abbr", "align", "axis", "bgcolor", "char", "charoff", "colspan", "headers", "height",
|
||||
"nowrap", "rowspan", "scope", "valign", "width",
|
||||
],
|
||||
),
|
||||
("thead", &["align", "char", "charoff", "valign"]),
|
||||
("time", &["datetime"]),
|
||||
("tr", &["align", "bgcolor", "char", "charoff", "valign"]),
|
||||
("track", &["default", "kind", "label", "src", "srclang"]),
|
||||
("ul", &["compact", "type"]),
|
||||
(
|
||||
"video",
|
||||
&[
|
||||
"autoplay",
|
||||
"controls",
|
||||
"crossorigin",
|
||||
"height",
|
||||
"loop",
|
||||
"muted",
|
||||
"playsinline",
|
||||
"poster",
|
||||
"preload",
|
||||
"src",
|
||||
"width",
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
/// An HTML element Prettier knows, by its lowercase name.
|
||||
pub fn is_known_element(lowercase: &str) -> bool {
|
||||
ELEMENTS.binary_search(&lowercase).is_ok()
|
||||
}
|
||||
|
||||
/// The lowercase name of an attribute Prettier knows on `element` (lowercase), written `written` in any case:
|
||||
/// a global one, but only on an element the attribute table lists.
|
||||
pub fn known_attribute(element: &str, written: &str) -> Option<&'static str> {
|
||||
let i = ELEMENT_ATTRIBUTES.binary_search_by_key(&element, |(name, _)| name).ok()?;
|
||||
find(GLOBAL_ATTRIBUTES, written).or_else(|| find(ELEMENT_ATTRIBUTES[i].1, written))
|
||||
}
|
||||
|
||||
fn find(names: &[&'static str], written: &str) -> Option<&'static str> {
|
||||
names
|
||||
.binary_search_by(|name| name.bytes().cmp(written.bytes().map(|b| b.to_ascii_lowercase())))
|
||||
.ok()
|
||||
.map(|i| names[i])
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//! Prettier conformance for HTML.
|
||||
//!
|
||||
//! Compares output against the Prettier suite's `tests/format/html` snapshots via `oxc_formatter_tests::conformance`;
|
||||
//! the failure report is pinned with `insta`.
|
||||
//!
|
||||
//! Debug a specific test: `PRETTIER_FILTER=<substring> cargo test -p oxc_formatter_htmlish --test conformance -- --nocapture`
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_formatter_core::LineWidth;
|
||||
use oxc_formatter_htmlish::{HtmlishFormatOptions, format};
|
||||
use oxc_formatter_tests::{
|
||||
OptionSet,
|
||||
conformance::{ConformanceConfig, run_conformance},
|
||||
};
|
||||
|
||||
#[path = "fixtures/options.rs"]
|
||||
mod options;
|
||||
use options::apply_htmlish_options;
|
||||
|
||||
const CONFIG: ConformanceConfig = ConformanceConfig {
|
||||
language: "html",
|
||||
fixture_roots: &["html"],
|
||||
exact_parser: Some("html"),
|
||||
ignore: &[
|
||||
// Cursor tracking is an editor feature, not formatting.
|
||||
"html/cursor/",
|
||||
// Embedded languages need the dispatcher; covered by oxfmt's E2E conformance.
|
||||
"html/css/",
|
||||
"html/js/",
|
||||
"html/multiparser/",
|
||||
"html/script/",
|
||||
"html/svg/embeded/",
|
||||
"html/bracket-same-line/embed.html",
|
||||
"html/prettier_ignore/issue-15738.html",
|
||||
"html/basics/html5-boilerplate.html",
|
||||
"html/case/case.html",
|
||||
// `<script type="text/x-handlebars-template">` is formatted as Glimmer.
|
||||
"html/handlebars-venerable/",
|
||||
// The front matter's YAML body.
|
||||
"html/yaml/yaml.html",
|
||||
// `style="…"` is formatted as CSS, `on*="…"` as JS.
|
||||
"html/attributes/style.html",
|
||||
"html/attributes/event-attributes.html",
|
||||
"html/attributes/no-semi/",
|
||||
"html/doctype_declarations/xhtml1.1.html",
|
||||
"html/tags/marquee.html",
|
||||
"html/tags/menu.html",
|
||||
"html/whitespace/fill.html",
|
||||
"html/whitespace/nested-inline-without-whitespace.html",
|
||||
// Pragma support (`@format` insertion / require).
|
||||
"html/pragma/",
|
||||
],
|
||||
skip_spec: None,
|
||||
};
|
||||
|
||||
fn format_html(_path: &Path, source_text: &str, spec: &OptionSet) -> Option<String> {
|
||||
// Prettier's default `printWidth` is 80 (oxc defaults to 100).
|
||||
let mut options = HtmlishFormatOptions {
|
||||
line_width: LineWidth::try_from(80).unwrap(),
|
||||
..HtmlishFormatOptions::default()
|
||||
};
|
||||
apply_htmlish_options(&mut options, spec);
|
||||
let allocator = Allocator::default();
|
||||
let formatted = format(&allocator, source_text, options).ok()?;
|
||||
Some(formatted.print().ok()?.into_code())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prettier_conformance() {
|
||||
let Some(report) = run_conformance(&CONFIG, format_html) else { return };
|
||||
insta::assert_snapshot!("prettier-html", report);
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
@if (a) {
|
||||
x
|
||||
} @else
|
||||
if (b) {
|
||||
y
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
@if (a) {
|
||||
x
|
||||
} @else
|
||||
if (b) {
|
||||
y
|
||||
}
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
@if (a) {
|
||||
x
|
||||
}
|
||||
@else
|
||||
if (b) {
|
||||
y
|
||||
}
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
@if (a) {
|
||||
x
|
||||
}
|
||||
@else
|
||||
if (b) {
|
||||
y
|
||||
}
|
||||
|
||||
===================== End =====================
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
<p>a@if (x) {b}c</p>
|
||||
<span>@if (test) {text}</span>
|
||||
<p><b>a</b>@for (i of items; track i) {<i>{{ i }}</i>}<b>z</b></p>
|
||||
<span>a@if (x) {}b</span>
|
||||
<span>a@case (1)@case (2) {b}c</span>
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<p>a@if (x) {b}c</p>
|
||||
<span>@if (test) {text}</span>
|
||||
<p><b>a</b>@for (i of items; track i) {<i>{{ i }}</i>}<b>z</b></p>
|
||||
<span>a@if (x) {}b</span>
|
||||
<span>a@case (1)@case (2) {b}c</span>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<p>a@if (x) {b}c</p>
|
||||
<span>@if (test) {text}</span>
|
||||
<p>
|
||||
<b>a</b>@for (i of items; track i) {<i>{{ i }}</i>}<b>z</b>
|
||||
</p>
|
||||
<span>a@if (x) {}b</span>
|
||||
<span>a@case (1)@case (2) {b}c</span>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<p>a@if (x) {b}c</p>
|
||||
<span>@if (test) {text}</span>
|
||||
<p>
|
||||
<b>a</b>@for (i of items; track i) {<i>{{ i }}</i>}<b>z</b>
|
||||
</p>
|
||||
<span>a@if (x) {}b</span>
|
||||
<span>a@case (1)@case (2) {b}c</span>
|
||||
|
||||
===================== End =====================
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<div CLASS="a b" class="c d"></div>
|
||||
<iframe ALLOW="a; b"></iframe>
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div CLASS="a b" class="c d"></div>
|
||||
<iframe ALLOW="a; b"></iframe>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div CLASS="a b" class="c d"></div>
|
||||
<iframe ALLOW="a; b"></iframe>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div CLASS="a b" class="c d"></div>
|
||||
<iframe ALLOW="a; b"></iframe>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,24 @@
|
||||
<ul>
|
||||
@for (item of items; track item.id) {
|
||||
<li>{{ item.name }}</li>
|
||||
} @empty {
|
||||
<li>none</li>
|
||||
}
|
||||
</ul>
|
||||
@if (a) { <p>x</p> }
|
||||
@else { <p>y</p> }
|
||||
@if (a) {}
|
||||
@if (isDev) {} @else if (test) {}
|
||||
<p>text @if (x) { more } end</p>
|
||||
@switch (mode) {
|
||||
@case ("a")
|
||||
@case ("b") {
|
||||
<p>ab</p>
|
||||
}
|
||||
@default never;
|
||||
}
|
||||
@defer (on viewport) {
|
||||
<x-chart />
|
||||
} @placeholder (minimum 500ms) {
|
||||
<p>loading</p>
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<ul>
|
||||
@for (item of items; track item.id) {
|
||||
<li>{{ item.name }}</li>
|
||||
} @empty {
|
||||
<li>none</li>
|
||||
}
|
||||
</ul>
|
||||
@if (a) { <p>x</p> }
|
||||
@else { <p>y</p> }
|
||||
@if (a) {}
|
||||
@if (isDev) {} @else if (test) {}
|
||||
<p>text @if (x) { more } end</p>
|
||||
@switch (mode) {
|
||||
@case ("a")
|
||||
@case ("b") {
|
||||
<p>ab</p>
|
||||
}
|
||||
@default never;
|
||||
}
|
||||
@defer (on viewport) {
|
||||
<x-chart />
|
||||
} @placeholder (minimum 500ms) {
|
||||
<p>loading</p>
|
||||
}
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<ul>
|
||||
@for (item of items; track item.id) {
|
||||
<li>{{ item.name }}</li>
|
||||
} @empty {
|
||||
<li>none</li>
|
||||
}
|
||||
</ul>
|
||||
@if (a) {
|
||||
<p>x</p>
|
||||
} @else {
|
||||
<p>y</p>
|
||||
}
|
||||
@if (a) {}
|
||||
@if (isDev) {
|
||||
} @else if (test) {}
|
||||
<p>
|
||||
text
|
||||
@if (x) {
|
||||
more
|
||||
}
|
||||
end
|
||||
</p>
|
||||
@switch (mode) {
|
||||
@case ("a")
|
||||
@case ("b") {
|
||||
<p>ab</p>
|
||||
}
|
||||
@default never;
|
||||
}
|
||||
@defer (on viewport) {
|
||||
<x-chart />
|
||||
} @placeholder (minimum 500ms) {
|
||||
<p>loading</p>
|
||||
}
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<ul>
|
||||
@for (item of items; track item.id) {
|
||||
<li>{{ item.name }}</li>
|
||||
} @empty {
|
||||
<li>none</li>
|
||||
}
|
||||
</ul>
|
||||
@if (a) {
|
||||
<p>x</p>
|
||||
} @else {
|
||||
<p>y</p>
|
||||
}
|
||||
@if (a) {}
|
||||
@if (isDev) {
|
||||
} @else if (test) {}
|
||||
<p>
|
||||
text
|
||||
@if (x) {
|
||||
more
|
||||
}
|
||||
end
|
||||
</p>
|
||||
@switch (mode) {
|
||||
@case ("a")
|
||||
@case ("b") {
|
||||
<p>ab</p>
|
||||
}
|
||||
@default never;
|
||||
}
|
||||
@defer (on viewport) {
|
||||
<x-chart />
|
||||
} @placeholder (minimum 500ms) {
|
||||
<p>loading</p>
|
||||
}
|
||||
|
||||
===================== End =====================
|
||||
+1
@@ -0,0 +1 @@
|
||||
<div [class.[&>svg]:w-4]="x" [a]="b">y</div>
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div [class.[&>svg]:w-4]="x" [a]="b">y</div>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div [class.[&>svg]:w-4]="x" [a]="b">y</div>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div [class.[&>svg]:w-4]="x" [a]="b">y</div>
|
||||
|
||||
===================== End =====================
|
||||
+1
@@ -0,0 +1 @@
|
||||
<div>@switch (a) {@case (1)@case (2) {x}}</div>
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div>@switch (a) {@case (1)@case (2) {x}}</div>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div>
|
||||
@switch (a) {
|
||||
@case (1)
|
||||
@case (2) {
|
||||
x
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div>
|
||||
@switch (a) {
|
||||
@case (1)
|
||||
@case (2) {
|
||||
x
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,144 @@
|
||||
//! What formatting must not change, as one string per document:
|
||||
//! elements (name, attributes, end tag presence; names in any case where known ones print lowercase),
|
||||
//! comments as written, a doctype in any case,
|
||||
//! attribute values with their quote entities read back (a class list as its names, `srcset` as its URLs, `allow` as its directives),
|
||||
//! and text as its words (whitespace runs may be re-flowed), except where whitespace is content:
|
||||
//! `pre` as written, a `script` / `style` body as its lines (re-indented, blank ones at its ends dropped).
|
||||
|
||||
use std::{borrow::Cow, fmt::Write as _};
|
||||
|
||||
use cow_utils::CowUtils;
|
||||
|
||||
use oxc_htmlish_parser::ast::{Namespace, NodeId, NodeKind, Root};
|
||||
|
||||
/// `fold_names`: a `.html` file, where known names print lowercase (DIVERGENCES.md#tag-name-case).
|
||||
pub fn fingerprint(source: &str, root: &Root<'_>, fold_names: bool) -> String {
|
||||
let mut out = String::new();
|
||||
walk(source, root, NodeId::DOCUMENT, Text::Words, fold_names, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// How a text keeps its whitespace.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Text {
|
||||
Words,
|
||||
Lines,
|
||||
AsWritten,
|
||||
}
|
||||
|
||||
fn walk(
|
||||
source: &str,
|
||||
root: &Root<'_>,
|
||||
parent: NodeId,
|
||||
text: Text,
|
||||
fold_names: bool,
|
||||
out: &mut String,
|
||||
) {
|
||||
for (id, node) in root.children(parent) {
|
||||
match &node.kind {
|
||||
NodeKind::Element(el) => {
|
||||
let name = fold(el.name.slice(source), fold_names);
|
||||
write!(out, "<{name}").unwrap();
|
||||
for attribute in &el.attributes {
|
||||
let attribute_name = attribute.name.slice(source);
|
||||
write!(out, " {}", fold(attribute_name, fold_names)).unwrap();
|
||||
let Some(value) = attribute.value else { continue };
|
||||
let value = value.span.slice(source).cow_replace(""", "\"");
|
||||
let value = value.cow_replace("'", "'");
|
||||
let is = |name: &str| attribute_name.eq_ignore_ascii_case(name);
|
||||
out.push('=');
|
||||
if is("class") {
|
||||
for name in value.split_ascii_whitespace() {
|
||||
write!(out, " {name:?}").unwrap();
|
||||
}
|
||||
} else if is("srcset") {
|
||||
// The URLs; descriptors are re-printed as numbers.
|
||||
for candidate in value.split(',') {
|
||||
if let Some(url) = candidate.split_ascii_whitespace().next() {
|
||||
write!(out, " {url:?}").unwrap();
|
||||
}
|
||||
}
|
||||
} else if is("allow") {
|
||||
for directive in value.split(';') {
|
||||
let words: Vec<&str> = directive.split_ascii_whitespace().collect();
|
||||
if !words.is_empty() {
|
||||
write!(out, " {:?}", words.join(" ")).unwrap();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
write!(out, "{value:?}").unwrap();
|
||||
}
|
||||
}
|
||||
out.push('>');
|
||||
// The formatter's `pre_like` and `script_like`, restated rather than shared
|
||||
// so that the check stays independent of the code under test.
|
||||
let text = if el.namespace != Namespace::Svg
|
||||
&& matches!(el.key, "pre" | "textarea" | "listing" | "plaintext" | "xmp")
|
||||
{
|
||||
Text::AsWritten
|
||||
} else if matches!(el.key, "script" | "style") {
|
||||
Text::Lines
|
||||
} else {
|
||||
text
|
||||
};
|
||||
walk(source, root, id, text, fold_names, out);
|
||||
if el.end_tag.is_some() {
|
||||
write!(out, "</{name}>").unwrap();
|
||||
}
|
||||
}
|
||||
NodeKind::Text(_) => {
|
||||
let value = node.span.slice(source);
|
||||
match text {
|
||||
Text::AsWritten => write!(out, "{value:?}").unwrap(),
|
||||
Text::Lines => {
|
||||
for line in value.trim_ascii().lines() {
|
||||
write!(out, "{:?}", line.trim_ascii()).unwrap();
|
||||
}
|
||||
}
|
||||
Text::Words => {
|
||||
for word in value.split_ascii_whitespace() {
|
||||
write!(out, " {word}").unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
NodeKind::ConditionalComment(comment) => {
|
||||
write!(out, "[cc {}]", words(comment.condition.slice(source))).unwrap();
|
||||
if comment.parsed {
|
||||
walk(source, root, id, text, fold_names, out);
|
||||
} else {
|
||||
write!(out, "{:?}", comment.content.slice(source)).unwrap();
|
||||
}
|
||||
}
|
||||
// The expression's tokens may be re-flowed until it is formatted.
|
||||
NodeKind::Interpolation(interpolation) => {
|
||||
write!(out, "{{{{{}}}}}", words(interpolation.expression.slice(source))).unwrap();
|
||||
}
|
||||
NodeKind::Block(block) => {
|
||||
write!(out, "[@{}", words(block.name.slice(source))).unwrap();
|
||||
for parameter in &block.parameters {
|
||||
write!(out, " ({})", words(parameter.slice(source))).unwrap();
|
||||
}
|
||||
out.push('{');
|
||||
walk(source, root, id, text, fold_names, out);
|
||||
out.push_str(if block.close.is_some() { "}]" } else { "]" });
|
||||
}
|
||||
// HTML reads a doctype ASCII case-insensitively
|
||||
NodeKind::DocType(_) => {
|
||||
write!(out, "[{:?}]", words(&node.span.slice(source).cow_to_ascii_lowercase()))
|
||||
.unwrap();
|
||||
}
|
||||
_ => write!(out, "[{:?}]", node.span.slice(source)).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `name`, ASCII-lowercased with `fold_names`.
|
||||
fn fold(name: &str, fold_names: bool) -> Cow<'_, str> {
|
||||
if fold_names { name.cow_to_ascii_lowercase() } else { Cow::Borrowed(name) }
|
||||
}
|
||||
|
||||
/// `text`'s words, one space apart.
|
||||
fn words(text: &str) -> String {
|
||||
text.split_ascii_whitespace().collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<div><p>Hello <b>world</b>!</p></div>
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div><p>Hello <b>world</b>!</p></div>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div>
|
||||
<p>Hello <b>world</b>!</p>
|
||||
</div>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div>
|
||||
<p>Hello <b>world</b>!</p>
|
||||
</div>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,18 @@
|
||||
<div><p><![CDATA[x]]></p></div>
|
||||
<div><p><![CDATA[x]]> here</p></div>
|
||||
<div>
|
||||
<![CDATA[x]]>
|
||||
</div>
|
||||
<div>
|
||||
<![CDATA[x]]> here
|
||||
</div>
|
||||
<pre class="aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccc"><![CDATA[x]]> a
|
||||
b</pre>
|
||||
<div>
|
||||
<pre><![CDATA[x]]> a
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<pre>a <![CDATA[x]]>
|
||||
</pre>
|
||||
</div>
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div><p><![CDATA[x]]></p></div>
|
||||
<div><p><![CDATA[x]]> here</p></div>
|
||||
<div>
|
||||
<![CDATA[x]]>
|
||||
</div>
|
||||
<div>
|
||||
<![CDATA[x]]> here
|
||||
</div>
|
||||
<pre class="aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccc"><![CDATA[x]]> a
|
||||
b</pre>
|
||||
<div>
|
||||
<pre><![CDATA[x]]> a
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<pre>a <![CDATA[x]]>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div><p><![CDATA[x]]></p></div>
|
||||
<div><p><![CDATA[x]]> here</p></div>
|
||||
<div><![CDATA[x]]></div>
|
||||
<div><![CDATA[x]]> here</div>
|
||||
<pre
|
||||
class="aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccc"
|
||||
>
|
||||
<![CDATA[x]]> a
|
||||
b</pre>
|
||||
<div>
|
||||
<pre>
|
||||
<![CDATA[x]]> a
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<pre>
|
||||
a <![CDATA[x]]>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div><p><![CDATA[x]]></p></div>
|
||||
<div><p><![CDATA[x]]> here</p></div>
|
||||
<div><![CDATA[x]]></div>
|
||||
<div><![CDATA[x]]> here</div>
|
||||
<pre class="aaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb cccccccccccccccccccccccccccc">
|
||||
<![CDATA[x]]> a
|
||||
b</pre>
|
||||
<div>
|
||||
<pre>
|
||||
<![CDATA[x]]> a
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<pre>
|
||||
a <![CDATA[x]]>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1 @@
|
||||
<p>text <![CDATA[aaaa bbbb cccc dddd eeee ffff gggg hhhh iiii jjjj kkkk llll mmmm nnnn oooo pppp]]> more</p>
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<p>text <![CDATA[aaaa bbbb cccc dddd eeee ffff gggg hhhh iiii jjjj kkkk llll mmmm nnnn oooo pppp]]> more</p>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<p>
|
||||
text
|
||||
<![CDATA[aaaa bbbb cccc dddd eeee ffff gggg hhhh iiii jjjj kkkk llll mmmm nnnn oooo pppp]]>
|
||||
more
|
||||
</p>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<p>
|
||||
text <![CDATA[aaaa bbbb cccc dddd eeee ffff gggg hhhh iiii jjjj kkkk llll mmmm nnnn oooo pppp]]>
|
||||
more
|
||||
</p>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,2 @@
|
||||
<div class="a b c"></div>
|
||||
<div class=" a "></div>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div class="a b c"></div>
|
||||
<div class=" a "></div>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div class="a b c"></div>
|
||||
<div class=" a"></div>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div class="a b c"></div>
|
||||
<div class=" a"></div>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,4 @@
|
||||
[
|
||||
{ "endOfLine": "lf" },
|
||||
{ "endOfLine": "crlf" }
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
<div>
|
||||
<!-- prettier-ignore -->
|
||||
<div class="a" >
|
||||
kept as written
|
||||
</div>
|
||||
<pre>
|
||||
line 1
|
||||
line 2
|
||||
</pre>
|
||||
<!--
|
||||
multi-line
|
||||
comment
|
||||
-->
|
||||
<script>
|
||||
const a = 1
|
||||
|
||||
foo( a )
|
||||
</script>
|
||||
<style>
|
||||
a { color : red }
|
||||
</style>
|
||||
<p title="first
|
||||
second">x</p>
|
||||
<textarea>
|
||||
value
|
||||
</textarea>
|
||||
</div>
|
||||
@@ -0,0 +1,174 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div>
|
||||
<!-- prettier-ignore -->
|
||||
<div class="a" >
|
||||
kept as written
|
||||
</div>
|
||||
<pre>
|
||||
line 1
|
||||
line 2
|
||||
</pre>
|
||||
<!--
|
||||
multi-line
|
||||
comment
|
||||
-->
|
||||
<script>
|
||||
const a = 1
|
||||
|
||||
foo( a )
|
||||
</script>
|
||||
<style>
|
||||
a { color : red }
|
||||
</style>
|
||||
<p title="first
|
||||
second">x</p>
|
||||
<textarea>
|
||||
value
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
==================== Output ====================
|
||||
-----------------------------------
|
||||
{ endOfLine: "lf", printWidth: 80 }
|
||||
-----------------------------------
|
||||
<div>
|
||||
<!-- prettier-ignore -->
|
||||
<div class="a" >
|
||||
kept as written
|
||||
</div>
|
||||
<pre>
|
||||
line 1
|
||||
line 2
|
||||
</pre>
|
||||
<!--
|
||||
multi-line
|
||||
comment
|
||||
-->
|
||||
<script>
|
||||
const a = 1
|
||||
|
||||
foo( a )
|
||||
</script>
|
||||
<style>
|
||||
a { color : red }
|
||||
</style>
|
||||
<p
|
||||
title="first
|
||||
second"
|
||||
>
|
||||
x
|
||||
</p>
|
||||
<textarea>
|
||||
value
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
------------------------------------
|
||||
{ endOfLine: "lf", printWidth: 100 }
|
||||
------------------------------------
|
||||
<div>
|
||||
<!-- prettier-ignore -->
|
||||
<div class="a" >
|
||||
kept as written
|
||||
</div>
|
||||
<pre>
|
||||
line 1
|
||||
line 2
|
||||
</pre>
|
||||
<!--
|
||||
multi-line
|
||||
comment
|
||||
-->
|
||||
<script>
|
||||
const a = 1
|
||||
|
||||
foo( a )
|
||||
</script>
|
||||
<style>
|
||||
a { color : red }
|
||||
</style>
|
||||
<p
|
||||
title="first
|
||||
second"
|
||||
>
|
||||
x
|
||||
</p>
|
||||
<textarea>
|
||||
value
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
-------------------------------------
|
||||
{ endOfLine: "crlf", printWidth: 80 }
|
||||
-------------------------------------
|
||||
<div>
|
||||
<!-- prettier-ignore -->
|
||||
<div class="a" >
|
||||
kept as written
|
||||
</div>
|
||||
<pre>
|
||||
line 1
|
||||
line 2
|
||||
</pre>
|
||||
<!--
|
||||
multi-line
|
||||
comment
|
||||
-->
|
||||
<script>
|
||||
const a = 1
|
||||
|
||||
foo( a )
|
||||
</script>
|
||||
<style>
|
||||
a { color : red }
|
||||
</style>
|
||||
<p
|
||||
title="first
|
||||
second"
|
||||
>
|
||||
x
|
||||
</p>
|
||||
<textarea>
|
||||
value
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
--------------------------------------
|
||||
{ endOfLine: "crlf", printWidth: 100 }
|
||||
--------------------------------------
|
||||
<div>
|
||||
<!-- prettier-ignore -->
|
||||
<div class="a" >
|
||||
kept as written
|
||||
</div>
|
||||
<pre>
|
||||
line 1
|
||||
line 2
|
||||
</pre>
|
||||
<!--
|
||||
multi-line
|
||||
comment
|
||||
-->
|
||||
<script>
|
||||
const a = 1
|
||||
|
||||
foo( a )
|
||||
</script>
|
||||
<style>
|
||||
a { color : red }
|
||||
</style>
|
||||
<p
|
||||
title="first
|
||||
second"
|
||||
>
|
||||
x
|
||||
</p>
|
||||
<textarea>
|
||||
value
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,7 @@
|
||||
<main><b>a</b><dialog></dialog><b>b</b></main>
|
||||
<p>Open the <dialog open>settings</dialog> panel</p>
|
||||
<body>
|
||||
<div>before</div>
|
||||
<dialog id="d"><form method="dialog"><p>Are you sure?</p><button>OK</button></form></dialog>
|
||||
<div>after</div>
|
||||
</body>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<main><b>a</b><dialog></dialog><b>b</b></main>
|
||||
<p>Open the <dialog open>settings</dialog> panel</p>
|
||||
<body>
|
||||
<div>before</div>
|
||||
<dialog id="d"><form method="dialog"><p>Are you sure?</p><button>OK</button></form></dialog>
|
||||
<div>after</div>
|
||||
</body>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<main><b>a</b><dialog></dialog><b>b</b></main>
|
||||
<p>Open the <dialog open>settings</dialog> panel</p>
|
||||
<body>
|
||||
<div>before</div>
|
||||
<dialog id="d">
|
||||
<form method="dialog">
|
||||
<p>Are you sure?</p>
|
||||
<button>OK</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<div>after</div>
|
||||
</body>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<main><b>a</b><dialog></dialog><b>b</b></main>
|
||||
<p>Open the <dialog open>settings</dialog> panel</p>
|
||||
<body>
|
||||
<div>before</div>
|
||||
<dialog id="d">
|
||||
<form method="dialog">
|
||||
<p>Are you sure?</p>
|
||||
<button>OK</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<div>after</div>
|
||||
</body>
|
||||
|
||||
===================== End =====================
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<!DOCTYPE HTML>
|
||||
<p>x</p>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<!DOCTYPE HTML>
|
||||
<p>x</p>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<!doctype html>
|
||||
<p>x</p>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<!doctype html>
|
||||
<p>x</p>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"><body><p>a<br/>b</p></body></html>
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"><body><p>a<br/>b</p></body></html>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<body>
|
||||
<p>a<br />b</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<body>
|
||||
<p>a<br />b</p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
===================== End =====================
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<div>
|
||||
<div>
|
||||
<!--
|
||||
comment
|
||||
-->
|
||||
</div>
|
||||
<div>
|
||||
<!--
|
||||
comment
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div>
|
||||
<div>
|
||||
<!--
|
||||
comment
|
||||
-->
|
||||
</div>
|
||||
<div>
|
||||
<!--
|
||||
comment
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div>
|
||||
<div>
|
||||
<!--
|
||||
comment
|
||||
-->
|
||||
</div>
|
||||
<div>
|
||||
<!--
|
||||
comment
|
||||
--></div>
|
||||
</div>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div>
|
||||
<div>
|
||||
<!--
|
||||
comment
|
||||
-->
|
||||
</div>
|
||||
<div>
|
||||
<!--
|
||||
comment
|
||||
--></div>
|
||||
</div>
|
||||
|
||||
===================== End =====================
|
||||
+1
@@ -0,0 +1 @@
|
||||
<div>a</ div>b
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div>a</ div>b
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div>a</ div>b
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div>a</ div>b
|
||||
|
||||
===================== End =====================
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<div><p>a<//></div>
|
||||
<span>a<b>x<//>c</span>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div><p>a<//></div>
|
||||
<span>a<b>x<//>c</span>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div><p>a<//></div>
|
||||
<span>a<b>x<//>c</span>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div><p>a<//></div>
|
||||
<span>a<b>x<//>c</span>
|
||||
|
||||
===================== End =====================
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<p>CDATA: <![CDATA[text]]> here.</p>
|
||||
<p>a <![CDATA[x]]> b <![CDATA[y]]> c</p>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<p>CDATA: <![CDATA[text]]> here.</p>
|
||||
<p>a <![CDATA[x]]> b <![CDATA[y]]> c</p>
|
||||
|
||||
==================== Output ====================
|
||||
-------------------------------------------------------
|
||||
{ htmlWhitespaceSensitivity: "ignore", printWidth: 80 }
|
||||
-------------------------------------------------------
|
||||
<p>CDATA: <![CDATA[text]]> here.</p>
|
||||
<p>a <![CDATA[x]]> b <![CDATA[y]]> c</p>
|
||||
|
||||
--------------------------------------------------------
|
||||
{ htmlWhitespaceSensitivity: "ignore", printWidth: 100 }
|
||||
--------------------------------------------------------
|
||||
<p>CDATA: <![CDATA[text]]> here.</p>
|
||||
<p>a <![CDATA[x]]> b <![CDATA[y]]> c</p>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1 @@
|
||||
[{ "htmlWhitespaceSensitivity": "ignore" }]
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<time><!-- prettier-ignore -->
|
||||
<mark>
|
||||
<input />
|
||||
</mark></time>
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<time><!-- prettier-ignore -->
|
||||
<mark>
|
||||
<input />
|
||||
</mark></time>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<time
|
||||
><!-- prettier-ignore -->
|
||||
<mark>
|
||||
<input />
|
||||
</mark
|
||||
></time>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<time
|
||||
><!-- prettier-ignore -->
|
||||
<mark>
|
||||
<input />
|
||||
</mark
|
||||
></time>
|
||||
|
||||
===================== End =====================
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<div><progress><progress>x</progress><wbr />
|
||||
amet,
|
||||
|
||||
</progress><abbr></abbr></div>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div><progress><progress>x</progress><wbr />
|
||||
amet,
|
||||
|
||||
</progress><abbr></abbr></div>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div>
|
||||
<progress><progress>x</progress><wbr /> amet,</progress><abbr></abbr>
|
||||
</div>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div>
|
||||
<progress><progress>x</progress><wbr /> amet,</progress><abbr></abbr>
|
||||
</div>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,8 @@
|
||||
<SPAN CLASS="a" ID="b">x</SPAN>
|
||||
<DIV CLASS="a" ALIGN="c" xml:Lang="en">x</DIV>
|
||||
<SVG VIEWBOX="0 0 1 1"><RECT WIDTH="1"/><foreignObject><DIV CLASS="z">y</DIV></foreignObject></SVG>
|
||||
<MY-EL CLASS="a"></MY-EL>
|
||||
<INPUT AUTOCOMPLETE="off" Type="text">
|
||||
<MATH><MI>x</MI></MATH>
|
||||
<DIV>x<//>
|
||||
<div>a<B>x</B>c</div>
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<SPAN CLASS="a" ID="b">x</SPAN>
|
||||
<DIV CLASS="a" ALIGN="c" xml:Lang="en">x</DIV>
|
||||
<SVG VIEWBOX="0 0 1 1"><RECT WIDTH="1"/><foreignObject><DIV CLASS="z">y</DIV></foreignObject></SVG>
|
||||
<MY-EL CLASS="a"></MY-EL>
|
||||
<INPUT AUTOCOMPLETE="off" Type="text">
|
||||
<MATH><MI>x</MI></MATH>
|
||||
<DIV>x<//>
|
||||
<div>a<B>x</B>c</div>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<span CLASS="a" ID="b">x</span>
|
||||
<div class="a" align="c" xml:Lang="en">x</div>
|
||||
<svg VIEWBOX="0 0 1 1">
|
||||
<RECT WIDTH="1" />
|
||||
<foreignObject><DIV CLASS="z">y</DIV></foreignObject>
|
||||
</svg>
|
||||
<MY-EL CLASS="a"></MY-EL>
|
||||
<input autocomplete="off" type="text" />
|
||||
<math><MI>x</MI></math>
|
||||
<div>x<//>
|
||||
<div>a<b>x</b>c</div>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<span CLASS="a" ID="b">x</span>
|
||||
<div class="a" align="c" xml:Lang="en">x</div>
|
||||
<svg VIEWBOX="0 0 1 1">
|
||||
<RECT WIDTH="1" />
|
||||
<foreignObject><DIV CLASS="z">y</DIV></foreignObject>
|
||||
</svg>
|
||||
<MY-EL CLASS="a"></MY-EL>
|
||||
<input autocomplete="off" type="text" />
|
||||
<math><MI>x</MI></math>
|
||||
<div>x<//>
|
||||
<div>a<b>x</b>c</div>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,6 @@
|
||||
<div>
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<b >x</b
|
||||
>c<i>y</i>d<em
|
||||
>z</em>e
|
||||
</div>
|
||||
<p>a<b >x</b>c<i>y</i>d<b >z</b>e and <i>q</i>r<u >s</u>t</p>
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div>
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<b >x</b
|
||||
>c<i>y</i>d<em
|
||||
>z</em>e
|
||||
</div>
|
||||
<p>a<b >x</b>c<i>y</i>d<b >z</b>e and <i>q</i>r<u >s</u>t</p>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div>
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<b>x</b>c<i>y</i>d<em>z</em>e
|
||||
</div>
|
||||
<p>a<b>x</b>c<i>y</i>d<b>z</b>e and <i>q</i>r<u>s</u>t</p>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div>
|
||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<b>x</b>c<i>y</i>d<em>z</em>e
|
||||
</div>
|
||||
<p>a<b>x</b>c<i>y</i>d<b>z</b>e and <i>q</i>r<u>s</u>t</p>
|
||||
|
||||
===================== End =====================
|
||||
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1,4 @@
|
||||
<html>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>Site</header>
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<html>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>Site</header>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<html>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>Site</header>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<html>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>Site</header>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,6 @@
|
||||
<ul>
|
||||
<li>one
|
||||
<li>two
|
||||
</ul>
|
||||
<p>para
|
||||
<div>block</div>
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<ul>
|
||||
<li>one
|
||||
<li>two
|
||||
</ul>
|
||||
<p>para
|
||||
<div>block</div>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<ul>
|
||||
<li>one
|
||||
<li>two
|
||||
</ul>
|
||||
<p>para
|
||||
<div>block</div>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<ul>
|
||||
<li>one
|
||||
<li>two
|
||||
</ul>
|
||||
<p>para
|
||||
<div>block</div>
|
||||
|
||||
===================== End =====================
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<div><ul><li><pre />
|
||||
<b>x</b></ul></div>
|
||||
<ul><li><pre />
|
||||
<b>x</b></ul>
|
||||
<div><pre />
|
||||
x y</div>
|
||||
<div><pre />
|
||||
<b>x</b></div>
|
||||
<span><listing />
|
||||
<i>y</i>
|
||||
</span>
|
||||
<div><xmp />
|
||||
<b>x</b></div>
|
||||
<pre><b>x</b> unclosed
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div><ul><li><pre />
|
||||
<b>x</b></ul></div>
|
||||
<ul><li><pre />
|
||||
<b>x</b></ul>
|
||||
<div><pre />
|
||||
x y</div>
|
||||
<div><pre />
|
||||
<b>x</b></div>
|
||||
<span><listing />
|
||||
<i>y</i>
|
||||
</span>
|
||||
<div><xmp />
|
||||
<b>x</b></div>
|
||||
<pre><b>x</b> unclosed
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div>
|
||||
<ul>
|
||||
<li>
|
||||
<pre />
|
||||
<b>x</b></ul>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<pre />
|
||||
<b>x</b></ul>
|
||||
<div><pre />x y</div>
|
||||
<div>
|
||||
<pre />
|
||||
<b>x</b></div>
|
||||
<span>
|
||||
<listing />
|
||||
<i>y</i>
|
||||
</span>
|
||||
<div>
|
||||
<xmp />
|
||||
<b>x</b></div>
|
||||
<pre><b>x</b> unclosed
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div>
|
||||
<ul>
|
||||
<li>
|
||||
<pre />
|
||||
<b>x</b></ul>
|
||||
</div>
|
||||
<ul>
|
||||
<li>
|
||||
<pre />
|
||||
<b>x</b></ul>
|
||||
<div><pre />x y</div>
|
||||
<div>
|
||||
<pre />
|
||||
<b>x</b></div>
|
||||
<span>
|
||||
<listing />
|
||||
<i>y</i>
|
||||
</span>
|
||||
<div>
|
||||
<xmp />
|
||||
<b>x</b></div>
|
||||
<pre><b>x</b> unclosed
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,3 @@
|
||||
<div><html:style>.a{color:red}</html:style></div>
|
||||
<div>x<html:script>let a = 1</html:script>y</div>
|
||||
<div>x<html:p>p</html:p>y</div>
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div><html:style>.a{color:red}</html:style></div>
|
||||
<div>x<html:script>let a = 1</html:script>y</div>
|
||||
<div>x<html:p>p</html:p>y</div>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div><html:style>.a{color:red}</html:style></div>
|
||||
<div>x<html:script>let a = 1</html:script>y</div>
|
||||
<div>x<html:p>p</html:p>y</div>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div><html:style>.a{color:red}</html:style></div>
|
||||
<div>x<html:script>let a = 1</html:script>y</div>
|
||||
<div>x<html:p>p</html:p>y</div>
|
||||
|
||||
===================== End =====================
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<!-- prettier-ignore-attribute -->
|
||||
<div class="a b"
|
||||
title="x
|
||||
y" id="z"></div>
|
||||
<!-- prettier-ignore-attribute class -->
|
||||
<div class="a b" CLASS="a b" title="x"></div>
|
||||
<!-- oxfmt-ignore-attribute -->
|
||||
<div class="a b"></div>
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<!-- prettier-ignore-attribute -->
|
||||
<div class="a b"
|
||||
title="x
|
||||
y" id="z"></div>
|
||||
<!-- prettier-ignore-attribute class -->
|
||||
<div class="a b" CLASS="a b" title="x"></div>
|
||||
<!-- oxfmt-ignore-attribute -->
|
||||
<div class="a b"></div>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<!-- prettier-ignore-attribute -->
|
||||
<div
|
||||
class="a b"
|
||||
title="x
|
||||
y"
|
||||
id="z"
|
||||
></div>
|
||||
<!-- prettier-ignore-attribute class -->
|
||||
<div class="a b" CLASS="a b" title="x"></div>
|
||||
<!-- oxfmt-ignore-attribute -->
|
||||
<div class="a b"></div>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<!-- prettier-ignore-attribute -->
|
||||
<div
|
||||
class="a b"
|
||||
title="x
|
||||
y"
|
||||
id="z"
|
||||
></div>
|
||||
<!-- prettier-ignore-attribute class -->
|
||||
<div class="a b" CLASS="a b" title="x"></div>
|
||||
<!-- oxfmt-ignore-attribute -->
|
||||
<div class="a b"></div>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,13 @@
|
||||
<p><span>a</span><script src="a.js"></script><span>b</span></p>
|
||||
<div>text<script src="a.js"></script>more text that is long enough to wrap around the line width limit</div>
|
||||
<div>
|
||||
<span>a</span>
|
||||
<script src="a.js"></script>
|
||||
<span>b</span>
|
||||
</div>
|
||||
<p>x<script>var y=1</script>z</p>
|
||||
<p><span>a</span><style>a{color:red}</style><span>b</span></p>
|
||||
<div>x</div><script>a()</script><script src="b.js"></script><div>y</div>
|
||||
<div>x</div><link rel="stylesheet" href="a.css"><meta name="a" content="b"><div>y</div>
|
||||
<p><span>aaaaaaaaaaaaaaaaaaaaaaaa</span> <script src="a.js"></script><span>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</span></p>
|
||||
<div>Price: <span>10</span> <meta itemprop="priceCurrency" content="USD">USD, and some more text that wraps the line</div>
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<p><span>a</span><script src="a.js"></script><span>b</span></p>
|
||||
<div>text<script src="a.js"></script>more text that is long enough to wrap around the line width limit</div>
|
||||
<div>
|
||||
<span>a</span>
|
||||
<script src="a.js"></script>
|
||||
<span>b</span>
|
||||
</div>
|
||||
<p>x<script>var y=1</script>z</p>
|
||||
<p><span>a</span><style>a{color:red}</style><span>b</span></p>
|
||||
<div>x</div><script>a()</script><script src="b.js"></script><div>y</div>
|
||||
<div>x</div><link rel="stylesheet" href="a.css"><meta name="a" content="b"><div>y</div>
|
||||
<p><span>aaaaaaaaaaaaaaaaaaaaaaaa</span> <script src="a.js"></script><span>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</span></p>
|
||||
<div>Price: <span>10</span> <meta itemprop="priceCurrency" content="USD">USD, and some more text that wraps the line</div>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<p>
|
||||
<span>a</span
|
||||
><script src="a.js"></script
|
||||
><span>b</span>
|
||||
</p>
|
||||
<div>
|
||||
text<script src="a.js"></script
|
||||
>more text that is long enough to wrap around the line width limit
|
||||
</div>
|
||||
<div>
|
||||
<span>a</span>
|
||||
<script src="a.js"></script>
|
||||
<span>b</span>
|
||||
</div>
|
||||
<p>
|
||||
x<script>
|
||||
var y=1
|
||||
</script
|
||||
>z
|
||||
</p>
|
||||
<p>
|
||||
<span>a</span
|
||||
><style>
|
||||
a{color:red}
|
||||
</style
|
||||
><span>b</span>
|
||||
</p>
|
||||
<div>x</div>
|
||||
<script>
|
||||
a()
|
||||
</script>
|
||||
<script src="b.js"></script>
|
||||
<div>y</div>
|
||||
<div>x</div>
|
||||
<link rel="stylesheet" href="a.css" /><meta name="a" content="b" />
|
||||
<div>y</div>
|
||||
<p>
|
||||
<span>aaaaaaaaaaaaaaaaaaaaaaaa</span>
|
||||
<script src="a.js"></script>
|
||||
<span>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</span>
|
||||
</p>
|
||||
<div>
|
||||
Price: <span>10</span> <meta itemprop="priceCurrency" content="USD" /> USD,
|
||||
and some more text that wraps the line
|
||||
</div>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<p>
|
||||
<span>a</span
|
||||
><script src="a.js"></script
|
||||
><span>b</span>
|
||||
</p>
|
||||
<div>
|
||||
text<script src="a.js"></script
|
||||
>more text that is long enough to wrap around the line width limit
|
||||
</div>
|
||||
<div>
|
||||
<span>a</span>
|
||||
<script src="a.js"></script>
|
||||
<span>b</span>
|
||||
</div>
|
||||
<p>
|
||||
x<script>
|
||||
var y=1
|
||||
</script
|
||||
>z
|
||||
</p>
|
||||
<p>
|
||||
<span>a</span
|
||||
><style>
|
||||
a{color:red}
|
||||
</style
|
||||
><span>b</span>
|
||||
</p>
|
||||
<div>x</div>
|
||||
<script>
|
||||
a()
|
||||
</script>
|
||||
<script src="b.js"></script>
|
||||
<div>y</div>
|
||||
<div>x</div>
|
||||
<link rel="stylesheet" href="a.css" /><meta name="a" content="b" />
|
||||
<div>y</div>
|
||||
<p>
|
||||
<span>aaaaaaaaaaaaaaaaaaaaaaaa</span>
|
||||
<script src="a.js"></script>
|
||||
<span>bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</span>
|
||||
</p>
|
||||
<div>
|
||||
Price: <span>10</span> <meta itemprop="priceCurrency" content="USD" /> USD, and some more text
|
||||
that wraps the line
|
||||
</div>
|
||||
|
||||
===================== End =====================
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
<div />after</div>
|
||||
<section><div />a<p>b</p></section>
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<div />after</div>
|
||||
<section><div />a<p>b</p></section>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<div />after</div>
|
||||
<section>
|
||||
<div />
|
||||
a
|
||||
<p>b</p>
|
||||
</section>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<div />after</div>
|
||||
<section>
|
||||
<div />
|
||||
a
|
||||
<p>b</p>
|
||||
</section>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,13 @@
|
||||
<span><i class="a" /><i class="b" /></span>
|
||||
<div><my-el />
|
||||
</div>
|
||||
<div><my-el />
|
||||
<p>x</p>
|
||||
</div>
|
||||
<span><my-el /><!-- c --></span>
|
||||
<div><a-el /><b-el />text</div>
|
||||
<!--[if IE]><!--><my-el /><!--<![endif]-->
|
||||
<ul><li><my-el /></li></ul>
|
||||
<!-- prettier-ignore -->
|
||||
<my-el />
|
||||
<p>after</p>
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<span><i class="a" /><i class="b" /></span>
|
||||
<div><my-el />
|
||||
</div>
|
||||
<div><my-el />
|
||||
<p>x</p>
|
||||
</div>
|
||||
<span><my-el /><!-- c --></span>
|
||||
<div><a-el /><b-el />text</div>
|
||||
<!--[if IE]><!--><my-el /><!--<![endif]-->
|
||||
<ul><li><my-el /></li></ul>
|
||||
<!-- prettier-ignore -->
|
||||
<my-el />
|
||||
<p>after</p>
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<span><i class="a" /><i class="b" /></span>
|
||||
<div><my-el /></div>
|
||||
<div>
|
||||
<my-el />
|
||||
<p>x</p>
|
||||
</div>
|
||||
<span><my-el /><!-- c --></span>
|
||||
<div><a-el /><b-el />text</div>
|
||||
<!--[if IE]><!--><my-el /><!--<![endif]-->
|
||||
<ul>
|
||||
<li><my-el /></li>
|
||||
</ul>
|
||||
<!-- prettier-ignore -->
|
||||
<my-el />
|
||||
<p>after</p>
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<span><i class="a" /><i class="b" /></span>
|
||||
<div><my-el /></div>
|
||||
<div>
|
||||
<my-el />
|
||||
<p>x</p>
|
||||
</div>
|
||||
<span><my-el /><!-- c --></span>
|
||||
<div><a-el /><b-el />text</div>
|
||||
<!--[if IE]><!--><my-el /><!--<![endif]-->
|
||||
<ul>
|
||||
<li><my-el /></li>
|
||||
</ul>
|
||||
<!-- prettier-ignore -->
|
||||
<my-el />
|
||||
<p>after</p>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,2 @@
|
||||
<img srcset="a.png 0x, b.png 2x">
|
||||
<img srcset="a.png -0x, b.png 1e21x">
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<img srcset="a.png 0x, b.png 2x">
|
||||
<img srcset="a.png -0x, b.png 1e21x">
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
<img srcset="a.png 0x, b.png 2x" />
|
||||
<img srcset="a.png 0x, b.png 1e+21x" />
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
<img srcset="a.png 0x, b.png 2x" />
|
||||
<img srcset="a.png 0x, b.png 1e+21x" />
|
||||
|
||||
===================== End =====================
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
<p><b>a</b>
|
||||
b
|
||||
</p><i></i>
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<p><b>a</b>
|
||||
b
|
||||
</p><i></i>
|
||||
|
||||
==================== Output ====================
|
||||
-------------------------------------------------------
|
||||
{ htmlWhitespaceSensitivity: "strict", printWidth: 80 }
|
||||
-------------------------------------------------------
|
||||
<p><b>a</b> b </p><i></i>
|
||||
|
||||
--------------------------------------------------------
|
||||
{ htmlWhitespaceSensitivity: "strict", printWidth: 100 }
|
||||
--------------------------------------------------------
|
||||
<p><b>a</b> b </p><i></i>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1 @@
|
||||
[{ "htmlWhitespaceSensitivity": "strict" }]
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
<b>a</b><pre><b>x</b></pre>
|
||||
<b>a</b><pre>
|
||||
<b>x</b></pre>
|
||||
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<b>a</b><pre><b>x</b></pre>
|
||||
<b>a</b><pre>
|
||||
<b>x</b></pre>
|
||||
|
||||
==================== Output ====================
|
||||
-------------------------------------------------------
|
||||
{ htmlWhitespaceSensitivity: "strict", printWidth: 80 }
|
||||
-------------------------------------------------------
|
||||
<b>a</b><pre><b>x</b></pre> <b>a</b
|
||||
><pre>
|
||||
<b>x</b></pre>
|
||||
|
||||
--------------------------------------------------------
|
||||
{ htmlWhitespaceSensitivity: "strict", printWidth: 100 }
|
||||
--------------------------------------------------------
|
||||
<b>a</b><pre><b>x</b></pre> <b>a</b
|
||||
><pre>
|
||||
<b>x</b></pre>
|
||||
|
||||
===================== End =====================
|
||||
@@ -0,0 +1,4 @@
|
||||
<span><pre />
|
||||
</span>
|
||||
<span>a<pre />
|
||||
</span>
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
---
|
||||
source: crates/oxc_formatter_htmlish/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
<span><pre />
|
||||
</span>
|
||||
<span>a<pre />
|
||||
</span>
|
||||
|
||||
==================== Output ====================
|
||||
-------------------------------------------------------
|
||||
{ htmlWhitespaceSensitivity: "strict", printWidth: 80 }
|
||||
-------------------------------------------------------
|
||||
<span><pre /></span>
|
||||
<span>a<pre /></span>
|
||||
|
||||
--------------------------------------------------------
|
||||
{ htmlWhitespaceSensitivity: "strict", printWidth: 100 }
|
||||
--------------------------------------------------------
|
||||
<span><pre /></span>
|
||||
<span>a<pre /></span>
|
||||
|
||||
===================== End =====================
|
||||
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user