mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(oxfmt)!: format parser:html files by oxc_formatter_htmlish (#28273)
Fixes #28263 - Before this PR: `.html` and also html-in-{js,md,etc} are formatted by Prettier. - Because of that, we had to run JS, which became an overhead. - After this PR: All of them are formatted natively by `oxc_formatter_htmlish`. - No overhead. At the same time, since we are fixing Prettier bugs and inconsistent behavior, please refer to DIVERGENCES.md for the differences.
This commit is contained in:
1 parent
44d8c2b824
commit
296623d868
86 files changed
+11121
-577
No files matched your search
@@ -325,13 +325,13 @@ jobs:
|
|||||||
- uses: $/.github/actions/check-changes
|
- uses: $/.github/actions/check-changes
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
paths: apps/oxfmt/,apps/shared/,npm/oxfmt/,crates/oxc_formatter/,crates/oxc_formatter_core/,crates/oxc_formatter_css/,crates/oxc_formatter_graphql/,crates/oxc_formatter_json/,crates/oxc_formatter_yaml/,crates/oxc_formatter_markdown/,crates/oxc_formatter_toml/,crates/oxc_config/,pnpm-lock.yaml,package.json
|
paths: apps/oxfmt/,apps/shared/,npm/oxfmt/,crates/oxc_formatter/,crates/oxc_formatter_core/,crates/oxc_formatter_css/,crates/oxc_formatter_graphql/,crates/oxc_formatter_json/,crates/oxc_formatter_yaml/,crates/oxc_formatter_markdown/,crates/oxc_formatter_htmlish/,crates/oxc_formatter_toml/,crates/oxc_config/,pnpm-lock.yaml,package.json
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
pr-number: ${{ github.event.pull_request.number }}
|
pr-number: ${{ github.event.pull_request.number }}
|
||||||
- uses: $/.github/actions/check-changes
|
- uses: $/.github/actions/check-changes
|
||||||
id: filter-conformance
|
id: filter-conformance
|
||||||
with:
|
with:
|
||||||
paths: apps/oxfmt/,npm/oxfmt/,crates/oxc_formatter/,crates/oxc_formatter_core/,crates/oxc_formatter_css/,crates/oxc_formatter_graphql/,crates/oxc_formatter_json/,crates/oxc_formatter_yaml/,crates/oxc_formatter_markdown/,crates/oxc_formatter_toml/,pnpm-lock.yaml,package.json
|
paths: apps/oxfmt/,npm/oxfmt/,crates/oxc_formatter/,crates/oxc_formatter_core/,crates/oxc_formatter_css/,crates/oxc_formatter_graphql/,crates/oxc_formatter_json/,crates/oxc_formatter_yaml/,crates/oxc_formatter_markdown/,crates/oxc_formatter_htmlish/,crates/oxc_formatter_toml/,pnpm-lock.yaml,package.json
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
pr-number: ${{ github.event.pull_request.number }}
|
pr-number: ${{ github.event.pull_request.number }}
|
||||||
- uses: oxc-project/setup-node@f46a72f95efdc55273fcd042d61c84e723b2892c # v1.4.1
|
- uses: oxc-project/setup-node@f46a72f95efdc55273fcd042d61c84e723b2892c # v1.4.1
|
||||||
@@ -510,7 +510,7 @@ jobs:
|
|||||||
- uses: $/.github/actions/check-changes
|
- uses: $/.github/actions/check-changes
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
paths: apps/oxfmt/,apps/shared/,npm/oxfmt/,crates/oxc_formatter/,crates/oxc_formatter_core/,crates/oxc_formatter_css/,crates/oxc_formatter_graphql/,crates/oxc_formatter_json/,crates/oxc_formatter_yaml/,crates/oxc_formatter_markdown/,crates/oxc_formatter_toml/,crates/oxc_config/,pnpm-lock.yaml,package.json
|
paths: apps/oxfmt/,apps/shared/,npm/oxfmt/,crates/oxc_formatter/,crates/oxc_formatter_core/,crates/oxc_formatter_css/,crates/oxc_formatter_graphql/,crates/oxc_formatter_json/,crates/oxc_formatter_yaml/,crates/oxc_formatter_markdown/,crates/oxc_formatter_htmlish/,crates/oxc_formatter_toml/,crates/oxc_config/,pnpm-lock.yaml,package.json
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
pr-number: ${{ github.event.pull_request.number }}
|
pr-number: ${{ github.event.pull_request.number }}
|
||||||
- uses: samypr100/setup-dev-drive@562171fe8df7401fdf582d766cd3b6ab80d5b1a0 # v4.1.0
|
- uses: samypr100/setup-dev-drive@562171fe8df7401fdf582d766cd3b6ab80d5b1a0 # v4.1.0
|
||||||
|
|||||||
Generated
+1
@@ -2933,6 +2933,7 @@ dependencies = [
|
|||||||
"oxc_formatter_core",
|
"oxc_formatter_core",
|
||||||
"oxc_formatter_css",
|
"oxc_formatter_css",
|
||||||
"oxc_formatter_graphql",
|
"oxc_formatter_graphql",
|
||||||
|
"oxc_formatter_htmlish",
|
||||||
"oxc_formatter_json",
|
"oxc_formatter_json",
|
||||||
"oxc_formatter_markdown",
|
"oxc_formatter_markdown",
|
||||||
"oxc_formatter_toml",
|
"oxc_formatter_toml",
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ oxc_formatter = { path = "crates/oxc_formatter" } # Code formatting
|
|||||||
oxc_formatter_core = { path = "crates/oxc_formatter_core" } # Language-agnostic formatter core
|
oxc_formatter_core = { path = "crates/oxc_formatter_core" } # Language-agnostic formatter core
|
||||||
oxc_formatter_css = { path = "crates/oxc_formatter_css" } # CSS formatter
|
oxc_formatter_css = { path = "crates/oxc_formatter_css" } # CSS formatter
|
||||||
oxc_formatter_graphql = { path = "crates/oxc_formatter_graphql" } # GraphQL formatter
|
oxc_formatter_graphql = { path = "crates/oxc_formatter_graphql" } # GraphQL formatter
|
||||||
|
oxc_formatter_htmlish = { path = "crates/oxc_formatter_htmlish" } # HTML (and HTML-based template languages) formatter
|
||||||
oxc_formatter_json = { path = "crates/oxc_formatter_json" } # JSON formatter
|
oxc_formatter_json = { path = "crates/oxc_formatter_json" } # JSON formatter
|
||||||
oxc_formatter_markdown = { path = "crates/oxc_formatter_markdown" } # Markdown formatter
|
oxc_formatter_markdown = { path = "crates/oxc_formatter_markdown" } # Markdown formatter
|
||||||
oxc_formatter_tests = { path = "crates/oxc_formatter_tests" } # Formatter test infrastructure (fixture codegen + harness)
|
oxc_formatter_tests = { path = "crates/oxc_formatter_tests" } # Formatter test infrastructure (fixture codegen + harness)
|
||||||
|
|||||||
@@ -99,16 +99,16 @@ Embedded languages (e.g. css-in-js, CSS front matter YAML) go through the `Forma
|
|||||||
|
|
||||||
Routing is ONE table (`route_embedded` in `src/core/language.rs`, which also maps the LSP `languageId`):
|
Routing is ONE table (`route_embedded` in `src/core/language.rs`, which also maps the LSP `languageId`):
|
||||||
|
|
||||||
- `Native` languages (js/ts/css/graphql/yaml/json/markdown/toml/...) get a Rust branch
|
- `Native` languages (js/ts/css/graphql/yaml/json/markdown/toml/html/...) get a Rust branch
|
||||||
- `Prettier` set (html/angular/vue/svelte/astro/handlebars/mdx) goes to the Prettier Doc→IR channel (`embed/prettier_doc.rs`)
|
- `Prettier` set (angular/vue/svelte/astro/handlebars/mdx) goes to the Prettier Doc→IR channel (`embed/prettier_doc.rs`)
|
||||||
- everything else is deliberately preserved
|
- everything else is deliberately preserved
|
||||||
|
|
||||||
Language-pair data crosses as `DispatchRequest::parent_context` markers the dispatcher translates for the child, so no language crate depends on another.
|
Language-pair data crosses as `DispatchRequest::parent_context` markers the dispatcher translates for the child, so no language crate depends on another.
|
||||||
|
|
||||||
Vocabulary: "fallback" = the dispatcher's optional `PrettierDocFallback` slot (a build/root may not install one).
|
Vocabulary: "fallback" = the dispatcher's optional `PrettierDocFallback` slot (a build/root may not install one).
|
||||||
The pure Rust build runs fallback-less, so non-native embeds (html-in-js, custom front matter) deliberately stay verbatim.
|
The pure Rust build runs fallback-less, so non-native embeds (angular-in-js, custom front matter) deliberately stay verbatim.
|
||||||
|
|
||||||
The roots install `SessionServices`, all via `embed/services.rs::for_root` (one definition per build; the napi one takes the `ExternalServices` transport, and adds the Prettier fallback / string embedder / Tailwind sorter to the registry dispatcher): the JS/TS, CSS and Markdown file roots (`core/format.rs`, `PhysicalFile` sessions, both builds) and the Vue/Svelte/Astro `<script>` and Astro frontmatter root (`api/text_to_doc_api.rs`, `VirtualDocument` session, napi only).
|
The roots install `SessionServices`, all via `embed/services.rs::for_root` (one definition per build; the napi one takes the `ExternalServices` transport, and adds the Prettier fallback / string embedder / Tailwind sorter to the registry dispatcher): the JS/TS, CSS, Markdown and HTML file roots (`core/format.rs`, `PhysicalFile` sessions, both builds) and the Vue/Svelte/Astro `<script>` and Astro frontmatter root (`api/text_to_doc_api.rs`, `VirtualDocument` session, napi only).
|
||||||
|
|
||||||
Which languages may dispatch AT ALL from a given host is the host crate's own gate (e.g. `oxc_formatter_css` dispatches only `yaml`/`toml` front matter, `oxc_formatter_markdown` every fenced code block's language); the shared `route_embedded()` table then decides who serves the language.
|
Which languages may dispatch AT ALL from a given host is the host crate's own gate (e.g. `oxc_formatter_css` dispatches only `yaml`/`toml` front matter, `oxc_formatter_markdown` every fenced code block's language); the shared `route_embedded()` table then decides who serves the language.
|
||||||
Adding a dispatch call to a host crate is therefore a routing decision (check `route_embedded()` and the embedded conformance when doing so. A root needing a bespoke service set would assemble the `SessionServices` struct literally), none does today.
|
Adding a dispatch call to a host crate is therefore a routing decision (check `route_embedded()` and the embedded conformance when doing so. A root needing a bespoke service set would assemble the `SessionServices` struct literally), none does today.
|
||||||
@@ -120,12 +120,11 @@ Per-language options are NOT built up front: `ResolvedDispatchConfig` maps them
|
|||||||
A separate string-out channel (the session's `string_embedder` service, NOT the dispatcher) carries JSDoc's string-in/string-out consumer:
|
A separate string-out channel (the session's `string_embedder` service, NOT the dispatcher) carries JSDoc's string-in/string-out consumer:
|
||||||
|
|
||||||
- JSDoc fenced code blocks: the host (`oxc_formatter`) embeds only the `prettier-plugin-jsdoc` set (css/less/scss/json/yaml/html), then routing follows ONE rule, the same `route_embedded` table
|
- JSDoc fenced code blocks: the host (`oxc_formatter`) embeds only the `prettier-plugin-jsdoc` set (css/less/scss/json/yaml/html), then routing follows ONE rule, the same `route_embedded` table
|
||||||
- a `Native` fence language formats through `FormatSession::dispatch_to_string` via a thin string adapter (`embed/jsdoc_fence.rs::format_native_fence`, EVERY build, the pure Rust build wires it via `services::for_root`)
|
- a `Native` fence language (the whole set) formats through `FormatSession::dispatch_to_string` via a thin string adapter (`embed/jsdoc_fence.rs`, EVERY build, wired by `services::for_root`)
|
||||||
- html fences stay on the Prettier string path (`embed/prettier_string.rs`, napi only; its Doc→IR conversion has unrepresentable cases);
|
|
||||||
- everything else stays verbatim
|
- everything else stays verbatim
|
||||||
- the embedder carries the caller's effective print width; both branches honor it (native via `PrintWidth` override, Prettier via `printWidth` in the options JSON), so a fence prints at the same width a JS/TS snippet in the same position would (see `upstream-jsdoc-bugs.md` #11 for the deliberate divergence from upstream's flat `printWidth - 4`)
|
- the embedder carries the caller's effective print width (a `PrintWidth` override), so a fence prints at the same width a JS/TS snippet in the same position would (see `upstream-jsdoc-bugs.md` #11 for the deliberate divergence from upstream's flat `printWidth - 4`)
|
||||||
|
|
||||||
NOTE: The string-out channel outlives the html rewrite; its full exit criterion is owned by `oxc_formatter_core`'s AGENTS.md (domain (4)).
|
NOTE: The string-out channel outlives the html rewrite; its exit criterion is owned by `oxc_formatter_core`'s AGENTS.md (domain (4)).
|
||||||
The half owned here: JSDoc's string-out is NOT structural, fences can move to IR-out (session dispatch inside the comment IR).
|
The half owned here: JSDoc's string-out is NOT structural, fences can move to IR-out (session dispatch inside the comment IR).
|
||||||
The printer already has the per-line prefix mechanism the `*` continuation needs (`prefix_align`); what remains is JSDoc emitting its output as lines plus embedded IR instead of one string, deferred for verification time, not by design.
|
The printer already has the per-line prefix mechanism the `*` continuation needs (`prefix_align`); what remains is JSDoc emitting its output as lines plus embedded IR instead of one string, deferred for verification time, not by design.
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ oxc_formatter = { workspace = true }
|
|||||||
oxc_formatter_core = { workspace = true }
|
oxc_formatter_core = { workspace = true }
|
||||||
oxc_formatter_css = { workspace = true }
|
oxc_formatter_css = { workspace = true }
|
||||||
oxc_formatter_graphql = { workspace = true }
|
oxc_formatter_graphql = { workspace = true }
|
||||||
|
oxc_formatter_htmlish = { workspace = true }
|
||||||
oxc_formatter_json = { workspace = true }
|
oxc_formatter_json = { workspace = true }
|
||||||
oxc_formatter_markdown = { workspace = true }
|
oxc_formatter_markdown = { workspace = true }
|
||||||
oxc_formatter_toml = { workspace = true }
|
oxc_formatter_toml = { workspace = true }
|
||||||
|
|||||||
@@ -304,3 +304,114 @@ title: Home
|
|||||||
A front matter block whose body starts with an indented delimiter (` ---`, ` ...`, ` +++`) stays verbatim in CSS and Markdown (`write_front_matter` in `oxc_formatter_core`).
|
A front matter block whose body starts with an indented delimiter (` ---`, ` ...`, ` +++`) stays verbatim in CSS and Markdown (`write_front_matter` in `oxc_formatter_core`).
|
||||||
The indented delimiter does not close the block, but the body is trimmed before formatting, so it prints at the start of a line.
|
The indented delimiter does not close the block, but the body is trimmed before formatting, so it prints at the start of a line.
|
||||||
On the next parse (Prettier's and ours), the block closes there and the rest of the body moves into the document.
|
On the next parse (Prettier's and ours), the block closes there and the rest of the body moves into the document.
|
||||||
|
|
||||||
|
## script-end-in-embedded-template
|
||||||
|
|
||||||
|
- Why: semantics (the first `</script` ends an HTML `<script>`, the rest of the code becomes markup)
|
||||||
|
- Pin: `conformance/fixtures/edge-cases/html/script-end-in-gql-template.html`
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- input -->
|
||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
field # <\/script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- ours -->
|
||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
field # <\/script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- prettier -->
|
||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
field # </script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
An HTML `<script>` body never prints `</script`: one the formatting produces (an embedded template part printed from its cooked value, html-in-js or gql-in-js) prints as `<\/script`.
|
||||||
|
Prettier keeps it in html-in-js, css-in-js and markdown-in-js, but a gql-in-js part prints the cooked `</script`, which closes the `<script>` there.
|
||||||
|
|
||||||
|
## placeholder-case-in-child
|
||||||
|
|
||||||
|
- Why: semantics (Prettier prints a lowercased placeholder in place of the `${}` expression)
|
||||||
|
- Pin: `conformance/fixtures/edge-cases/html-in-js/placeholder-case-in-child.js`
|
||||||
|
|
||||||
|
```js
|
||||||
|
// input
|
||||||
|
const a = html`<style>a { ${b}: ${c} }</style>`;
|
||||||
|
|
||||||
|
// ours
|
||||||
|
const a = html`<style>
|
||||||
|
a {
|
||||||
|
${b}: ${c};
|
||||||
|
}
|
||||||
|
</style>`;
|
||||||
|
|
||||||
|
// prettier
|
||||||
|
const a = html`<style>
|
||||||
|
a {
|
||||||
|
prettier_html_placeholder_0_0_in_js: ${c};
|
||||||
|
}
|
||||||
|
</style>`;
|
||||||
|
```
|
||||||
|
|
||||||
|
An html-in-js placeholder is found in the child's output ASCII case-insensitively (`HTML_PLACEHOLDER_PREFIX` in `oxc_formatter`):
|
||||||
|
CSS lowercases a property name, and the placeholder standing in it with it.
|
||||||
|
Prettier looks for the placeholder as it wrote it, so the lowercased one stays in the output and `${b}` is lost.
|
||||||
|
|
||||||
|
## pre-like-template-expression
|
||||||
|
|
||||||
|
- Why: uniform-rule (same construct, same output: an expression outside `pre`-like content, whose lines indent from the line its `${` starts on; prettier/prettier#20355)
|
||||||
|
- Pin: `conformance/fixtures/edge-cases/html-in-js/pre-like-template-expression.js`
|
||||||
|
- Conformance: `edge-cases/html-in-js/template-expression-indent.js`, `externals/prettier/js/embeded/indention/19518.js`
|
||||||
|
|
||||||
|
```js
|
||||||
|
// input
|
||||||
|
html`
|
||||||
|
<form>
|
||||||
|
<textarea>
|
||||||
|
${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</textarea>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// ours
|
||||||
|
html`
|
||||||
|
<form>
|
||||||
|
<textarea>
|
||||||
|
${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</textarea>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// prettier
|
||||||
|
html`
|
||||||
|
<form>
|
||||||
|
<textarea>
|
||||||
|
${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</textarea>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
```
|
||||||
|
|
||||||
|
A `${}` expression in `pre`-like content (`pre`, `textarea`, ...) indents its lines from the whitespace its line starts with,
|
||||||
|
as an expression elsewhere does from the line its `${` starts on.
|
||||||
|
A line of that content prints from the root, so the expression aligns there (`oxc_formatter_htmlish`'s `write_pre_like_text`);
|
||||||
|
one on the line of an element before it (`<pre><b>x</b>${`) indents from that line, as elsewhere.
|
||||||
|
Prettier 3.9.6 aligned an expression starting its line so; since 3.9.7 (prettier/prettier#19725, for idempotency) it indents it by the elements around the content instead,
|
||||||
|
which leaves `})` right of the `${` it closes.
|
||||||
|
Reformatting the expression changes no content: its source is code, not text in the element.
|
||||||
@@ -92,6 +92,24 @@ const sources = [
|
|||||||
repo: "mdn/content/files/en-us/web/css/guides",
|
repo: "mdn/content/files/en-us/web/css/guides",
|
||||||
version: "5137b45128dcf07ac636da68184f00aab30ec1cc",
|
version: "5137b45128dcf07ac636da68184f00aab30ec1cc",
|
||||||
},
|
},
|
||||||
|
// html
|
||||||
|
{
|
||||||
|
name: "vite",
|
||||||
|
repo: "vitejs/vite/playground",
|
||||||
|
version: "v8.3.5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mdn-learning-area",
|
||||||
|
repo: "mdn/learning-area",
|
||||||
|
// No tags; pin to a commit (2026-10 main)
|
||||||
|
version: "21973263ba859ff3477919087b488157f9d618a6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mdn-dom-examples",
|
||||||
|
repo: "mdn/dom-examples",
|
||||||
|
// No tags; pin to a commit (2026-10 main)
|
||||||
|
version: "03ae1ffb325a9202d3a64f26ab5b8361392a5368",
|
||||||
|
},
|
||||||
// jsdoc
|
// jsdoc
|
||||||
{
|
{
|
||||||
name: "svelte",
|
name: "svelte",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
const a = html`<style>a { ${b}: ${c} }</style>`;
|
||||||
|
const d = html`<style>${e} { color: red }</style><p>${f}</p>`;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// prettier/prettier#20355
|
||||||
|
html`
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<form>
|
||||||
|
<textarea>
|
||||||
|
${Match.valueTags(form, {
|
||||||
|
EmptyForm: () => '',
|
||||||
|
InvalidForm: () => '',
|
||||||
|
CompletedForm: () => '',
|
||||||
|
})}</textarea>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const indented = html`
|
||||||
|
<form>
|
||||||
|
<pre>
|
||||||
|
${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</pre>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const afterText = html`<pre>
|
||||||
|
a ${foo({
|
||||||
|
x: 1,
|
||||||
|
})} b</pre>`;
|
||||||
|
|
||||||
|
const sameLine = html`<pre>
|
||||||
|
${a({
|
||||||
|
x: 1,
|
||||||
|
})} ${b({
|
||||||
|
y: 2,
|
||||||
|
})}</pre>`;
|
||||||
|
|
||||||
|
const afterElement = html`<div><pre><b>x</b>${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</pre></div>`;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
field # <\/script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
@@ -230,6 +230,30 @@ const categories: Category[] = [
|
|||||||
// mdn's GitHub alerts and macros would fail on known divergences (`line-shapes`) in bulk.
|
// mdn's GitHub alerts and macros would fail on known divergences (`line-shapes`) in bulk.
|
||||||
optionSets: [{ printWidth: 80, bracketSameLine: true }, { printWidth: 100 }],
|
optionSets: [{ printWidth: 80, bracketSameLine: true }, { printWidth: 100 }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "html",
|
||||||
|
fixtures: [
|
||||||
|
{
|
||||||
|
dir: join(EXTERNALS_DIR, "prettier", "html"),
|
||||||
|
ext: ".html",
|
||||||
|
// Parser error messages, cursor offsets and pragma insertion are not formatter concerns
|
||||||
|
excludes: ["/_errors_/", "html/cursor/", "html/pragma/"],
|
||||||
|
},
|
||||||
|
{ dir: join(FIXTURES_DIR, "edge-cases", "html") },
|
||||||
|
],
|
||||||
|
// Real-world pages: demos and playgrounds, dense with `<script>` / `<style>`
|
||||||
|
projects: [
|
||||||
|
// vite's `invalid*.html` are deliberately broken, both sides error
|
||||||
|
{ dir: join(EXTERNALS_DIR, "vite"), ext: ".html", excludes: ["html/invalid"] },
|
||||||
|
{ dir: join(EXTERNALS_DIR, "mdn-learning-area"), ext: ".html" },
|
||||||
|
{ dir: join(EXTERNALS_DIR, "mdn-dom-examples"), ext: ".html" },
|
||||||
|
],
|
||||||
|
optionSets: [
|
||||||
|
{ printWidth: 80 },
|
||||||
|
{ printWidth: 100, htmlWhitespaceSensitivity: "ignore", singleAttributePerLine: true },
|
||||||
|
{ printWidth: 120, htmlWhitespaceSensitivity: "strict", bracketSameLine: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "graphql",
|
name: "graphql",
|
||||||
fixtures: [],
|
fixtures: [],
|
||||||
|
|||||||
@@ -74,12 +74,20 @@
|
|||||||
|
|
||||||
## html-in-js
|
## html-in-js
|
||||||
|
|
||||||
### Option 1: 174/198 (87.88%)
|
### Option 1: 172/200 (86.00%)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"printWidth":80}
|
{"printWidth":80}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- [edge-cases/html-in-js/placeholder-case-in-child.js](diffs/html-in-js/edge-cases__html-in-js__placeholder-case-in-child.js.md)
|
||||||
|
- apps/oxfmt/DIVERGENCES.md#placeholder-case-in-child
|
||||||
|
- [edge-cases/html-in-js/pre-like-template-expression.js](diffs/html-in-js/edge-cases__html-in-js__pre-like-template-expression.js.md)
|
||||||
|
- apps/oxfmt/DIVERGENCES.md#pre-like-template-expression
|
||||||
|
- [edge-cases/html-in-js/template-expression-indent.js](diffs/html-in-js/edge-cases__html-in-js__template-expression-indent.js.md)
|
||||||
|
- apps/oxfmt/DIVERGENCES.md#pre-like-template-expression
|
||||||
|
- [externals/prettier/js/embeded/indention/19518.js](diffs/html-in-js/externals__prettier__js__embeded__indention__19518.js.md)
|
||||||
|
- apps/oxfmt/DIVERGENCES.md#pre-like-template-expression
|
||||||
- [externals/webawesome/badge/badge.ts](diffs/html-in-js/externals__webawesome__badge__badge.ts.md)
|
- [externals/webawesome/badge/badge.ts](diffs/html-in-js/externals__webawesome__badge__badge.ts.md)
|
||||||
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
|
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
|
||||||
- [externals/webawesome/button/button.ts](diffs/html-in-js/externals__webawesome__button__button.ts.md)
|
- [externals/webawesome/button/button.ts](diffs/html-in-js/externals__webawesome__button__button.ts.md)
|
||||||
@@ -129,12 +137,20 @@
|
|||||||
- [externals/webawesome/textarea/textarea.ts](diffs/html-in-js/externals__webawesome__textarea__textarea.ts.md)
|
- [externals/webawesome/textarea/textarea.ts](diffs/html-in-js/externals__webawesome__textarea__textarea.ts.md)
|
||||||
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
|
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
|
||||||
|
|
||||||
### Option 2: 188/198 (94.95%)
|
### Option 2: 186/200 (93.00%)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore"}
|
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- [edge-cases/html-in-js/placeholder-case-in-child.js](diffs/html-in-js/edge-cases__html-in-js__placeholder-case-in-child.js.md)
|
||||||
|
- apps/oxfmt/DIVERGENCES.md#placeholder-case-in-child
|
||||||
|
- [edge-cases/html-in-js/pre-like-template-expression.js](diffs/html-in-js/edge-cases__html-in-js__pre-like-template-expression.js.md)
|
||||||
|
- apps/oxfmt/DIVERGENCES.md#pre-like-template-expression
|
||||||
|
- [edge-cases/html-in-js/template-expression-indent.js](diffs/html-in-js/edge-cases__html-in-js__template-expression-indent.js.md)
|
||||||
|
- apps/oxfmt/DIVERGENCES.md#pre-like-template-expression
|
||||||
|
- [externals/prettier/js/embeded/indention/19518.js](diffs/html-in-js/externals__prettier__js__embeded__indention__19518.js.md)
|
||||||
|
- apps/oxfmt/DIVERGENCES.md#pre-like-template-expression
|
||||||
- [externals/webawesome/button/button.ts](diffs/html-in-js/externals__webawesome__button__button.ts.md)
|
- [externals/webawesome/button/button.ts](diffs/html-in-js/externals__webawesome__button__button.ts.md)
|
||||||
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
|
- crates/oxc_formatter/DIVERGENCES.md#union-annotation-flat-retry
|
||||||
- [externals/webawesome/callout/callout.ts](diffs/html-in-js/externals__webawesome__callout__callout.ts.md)
|
- [externals/webawesome/callout/callout.ts](diffs/html-in-js/externals__webawesome__callout__callout.ts.md)
|
||||||
@@ -318,7 +334,7 @@
|
|||||||
|
|
||||||
## markdown-mdn
|
## markdown-mdn
|
||||||
|
|
||||||
### Option 1: 360/362 (99.45%)
|
### Option 1: 357/362 (98.62%)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"printWidth":80,"bracketSameLine":true}
|
{"printWidth":80,"bracketSameLine":true}
|
||||||
@@ -328,8 +344,14 @@
|
|||||||
- crates/oxc_formatter_css/DIVERGENCES.md#value-semicolon-glue
|
- crates/oxc_formatter_css/DIVERGENCES.md#value-semicolon-glue
|
||||||
- [externals/mdn-css-guides/values_and_units/textual_data_types/index.md](diffs/markdown-mdn/externals__mdn-css-guides__values_and_units__textual_data_types__index.md.md)
|
- [externals/mdn-css-guides/values_and_units/textual_data_types/index.md](diffs/markdown-mdn/externals__mdn-css-guides__values_and_units__textual_data_types__index.md.md)
|
||||||
- crates/oxc_formatter_css/DIVERGENCES.md#prelude-string-requote
|
- crates/oxc_formatter_css/DIVERGENCES.md#prelude-string-requote
|
||||||
|
- [externals/mdn-learn/css_layout/multiple-column_layout/index.md](diffs/markdown-mdn/externals__mdn-learn__css_layout__multiple-column_layout__index.md.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#doctype-case
|
||||||
|
- [externals/mdn-learn/css_layout/positioning/index.md](diffs/markdown-mdn/externals__mdn-learn__css_layout__positioning__index.md.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#doctype-case
|
||||||
|
- [externals/mdn-learn/structuring_content/basic_html_syntax/index.md](diffs/markdown-mdn/externals__mdn-learn__structuring_content__basic_html_syntax__index.md.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#unquoted-value-with-slash
|
||||||
|
|
||||||
### Option 2: 360/362 (99.45%)
|
### Option 2: 357/362 (98.62%)
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"printWidth":100}
|
{"printWidth":100}
|
||||||
@@ -339,6 +361,69 @@
|
|||||||
- crates/oxc_formatter_css/DIVERGENCES.md#value-semicolon-glue
|
- crates/oxc_formatter_css/DIVERGENCES.md#value-semicolon-glue
|
||||||
- [externals/mdn-css-guides/values_and_units/textual_data_types/index.md](diffs/markdown-mdn/externals__mdn-css-guides__values_and_units__textual_data_types__index.md.md)
|
- [externals/mdn-css-guides/values_and_units/textual_data_types/index.md](diffs/markdown-mdn/externals__mdn-css-guides__values_and_units__textual_data_types__index.md.md)
|
||||||
- crates/oxc_formatter_css/DIVERGENCES.md#prelude-string-requote
|
- crates/oxc_formatter_css/DIVERGENCES.md#prelude-string-requote
|
||||||
|
- [externals/mdn-learn/css_layout/multiple-column_layout/index.md](diffs/markdown-mdn/externals__mdn-learn__css_layout__multiple-column_layout__index.md.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#doctype-case
|
||||||
|
- [externals/mdn-learn/css_layout/positioning/index.md](diffs/markdown-mdn/externals__mdn-learn__css_layout__positioning__index.md.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#doctype-case
|
||||||
|
- [externals/mdn-learn/structuring_content/basic_html_syntax/index.md](diffs/markdown-mdn/externals__mdn-learn__structuring_content__basic_html_syntax__index.md.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#unquoted-value-with-slash
|
||||||
|
|
||||||
|
## html
|
||||||
|
|
||||||
|
### Option 1: 617/623 (99.04%)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"printWidth":80}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [edge-cases/html/script-end-in-gql-template.html](diffs/html/edge-cases__html__script-end-in-gql-template.html.md)
|
||||||
|
- apps/oxfmt/DIVERGENCES.md#script-end-in-embedded-template
|
||||||
|
- [externals/prettier/html/basics/broken-html.html](diffs/html/externals__prettier__html__basics__broken-html.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#unclosed-at-end
|
||||||
|
- [externals/prettier/html/basics/with-colon.html](diffs/html/externals__prettier__html__basics__with-colon.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#prefixed-script-style
|
||||||
|
- [externals/prettier/html/prettier_ignore/unclosed2.html](diffs/html/externals__prettier__html__prettier_ignore__unclosed2.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#unclosed-at-end
|
||||||
|
- [externals/prettier/html/tags/tags2.html](diffs/html/externals__prettier__html__tags__tags2.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#dialog-out-of-flow
|
||||||
|
- [externals/vite/html/valid.html](diffs/html/externals__vite__html__valid.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#unquoted-value-with-slash
|
||||||
|
|
||||||
|
### Option 2: 618/623 (99.20%)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore","singleAttributePerLine":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [edge-cases/html/script-end-in-gql-template.html](diffs/html/edge-cases__html__script-end-in-gql-template.html.md)
|
||||||
|
- apps/oxfmt/DIVERGENCES.md#script-end-in-embedded-template
|
||||||
|
- [externals/prettier/html/basics/broken-html.html](diffs/html/externals__prettier__html__basics__broken-html.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#unclosed-at-end
|
||||||
|
- [externals/prettier/html/basics/with-colon.html](diffs/html/externals__prettier__html__basics__with-colon.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#prefixed-script-style
|
||||||
|
- [externals/prettier/html/prettier_ignore/unclosed2.html](diffs/html/externals__prettier__html__prettier_ignore__unclosed2.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#unclosed-at-end
|
||||||
|
- [externals/vite/html/valid.html](diffs/html/externals__vite__html__valid.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#unquoted-value-with-slash
|
||||||
|
|
||||||
|
### Option 3: 617/623 (99.04%)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"printWidth":120,"htmlWhitespaceSensitivity":"strict","bracketSameLine":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [edge-cases/html/script-end-in-gql-template.html](diffs/html/edge-cases__html__script-end-in-gql-template.html.md)
|
||||||
|
- apps/oxfmt/DIVERGENCES.md#script-end-in-embedded-template
|
||||||
|
- [externals/prettier/html/basics/broken-html.html](diffs/html/externals__prettier__html__basics__broken-html.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#unclosed-at-end
|
||||||
|
- [externals/prettier/html/basics/with-colon.html](diffs/html/externals__prettier__html__basics__with-colon.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#prefixed-script-style
|
||||||
|
- [externals/prettier/html/prettier_ignore/unclosed2.html](diffs/html/externals__prettier__html__prettier_ignore__unclosed2.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#unclosed-at-end
|
||||||
|
- [externals/prettier/html/script/script.html](diffs/html/externals__prettier__html__script__script.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#script-display-none
|
||||||
|
- [externals/vite/html/valid.html](diffs/html/externals__vite__html__valid.html.md)
|
||||||
|
- crates/oxc_formatter_htmlish/DIVERGENCES.md#unquoted-value-with-slash
|
||||||
|
|
||||||
## graphql
|
## graphql
|
||||||
|
|
||||||
|
|||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
# edge-cases/html-in-js/placeholder-case-in-child.js
|
||||||
|
|
||||||
|
## Option 1
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":80}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
const a = html`<style>
|
||||||
|
a {
|
||||||
|
- prettier_html_placeholder_0_13_in_js: ${c};
|
||||||
|
+ ${b}: ${c};
|
||||||
|
}
|
||||||
|
</style>`;
|
||||||
|
const d = html`<style>
|
||||||
|
${e} {
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
const a = html`<style>
|
||||||
|
a {
|
||||||
|
${b}: ${c};
|
||||||
|
}
|
||||||
|
</style>`;
|
||||||
|
const d = html`<style>
|
||||||
|
${e} {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<p>${f}</p>`;
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
const a = html`<style>
|
||||||
|
a {
|
||||||
|
prettier_html_placeholder_0_13_in_js: ${c};
|
||||||
|
}
|
||||||
|
</style>`;
|
||||||
|
const d = html`<style>
|
||||||
|
${e} {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<p>${f}</p>`;
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Option 2
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore"}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -1,8 +1,8 @@
|
||||||
|
const a = html`
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
- prettier_html_placeholder_0_2989_in_js: ${c};
|
||||||
|
+ ${b}: ${c};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
const d = html`
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
const a = html`
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
${b}: ${c};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
const d = html`
|
||||||
|
<style>
|
||||||
|
${e} {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<p>${f}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
const a = html`
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
prettier_html_placeholder_0_2989_in_js: ${c};
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
const d = html`
|
||||||
|
<style>
|
||||||
|
${e} {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<p>${f}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
`````
|
||||||
+327
@@ -0,0 +1,327 @@
|
|||||||
|
# edge-cases/html-in-js/pre-like-template-expression.js
|
||||||
|
|
||||||
|
## Option 1
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":80}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -4,12 +4,12 @@
|
||||||
|
<body>
|
||||||
|
<form>
|
||||||
|
<textarea>
|
||||||
|
${Match.valueTags(form, {
|
||||||
|
- EmptyForm: () => "",
|
||||||
|
- InvalidForm: () => "",
|
||||||
|
- CompletedForm: () => "",
|
||||||
|
- })}</textarea>
|
||||||
|
+ EmptyForm: () => "",
|
||||||
|
+ InvalidForm: () => "",
|
||||||
|
+ CompletedForm: () => "",
|
||||||
|
+})}</textarea>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
@@ -17,17 +17,17 @@
|
||||||
|
const indented = html`
|
||||||
|
<form>
|
||||||
|
<pre>
|
||||||
|
${foo({
|
||||||
|
- x: 1,
|
||||||
|
- })}</pre>
|
||||||
|
+ x: 1,
|
||||||
|
+ })}</pre>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const afterText = html`<pre>
|
||||||
|
a ${foo({
|
||||||
|
- x: 1,
|
||||||
|
- })} b</pre>`;
|
||||||
|
+ x: 1,
|
||||||
|
+})} b</pre>`;
|
||||||
|
|
||||||
|
const sameLine = html`<pre>
|
||||||
|
${a({
|
||||||
|
x: 1,
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
// prettier/prettier#20355
|
||||||
|
html`
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<form>
|
||||||
|
<textarea>
|
||||||
|
${Match.valueTags(form, {
|
||||||
|
EmptyForm: () => "",
|
||||||
|
InvalidForm: () => "",
|
||||||
|
CompletedForm: () => "",
|
||||||
|
})}</textarea>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const indented = html`
|
||||||
|
<form>
|
||||||
|
<pre>
|
||||||
|
${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</pre>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const afterText = html`<pre>
|
||||||
|
a ${foo({
|
||||||
|
x: 1,
|
||||||
|
})} b</pre>`;
|
||||||
|
|
||||||
|
const sameLine = html`<pre>
|
||||||
|
${a({
|
||||||
|
x: 1,
|
||||||
|
})} ${b({
|
||||||
|
y: 2,
|
||||||
|
})}</pre>`;
|
||||||
|
|
||||||
|
const afterElement = html`<div>
|
||||||
|
<pre><b>x</b>${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</pre>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
// prettier/prettier#20355
|
||||||
|
html`
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<form>
|
||||||
|
<textarea>
|
||||||
|
${Match.valueTags(form, {
|
||||||
|
EmptyForm: () => "",
|
||||||
|
InvalidForm: () => "",
|
||||||
|
CompletedForm: () => "",
|
||||||
|
})}</textarea>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const indented = html`
|
||||||
|
<form>
|
||||||
|
<pre>
|
||||||
|
${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</pre>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const afterText = html`<pre>
|
||||||
|
a ${foo({
|
||||||
|
x: 1,
|
||||||
|
})} b</pre>`;
|
||||||
|
|
||||||
|
const sameLine = html`<pre>
|
||||||
|
${a({
|
||||||
|
x: 1,
|
||||||
|
})} ${b({
|
||||||
|
y: 2,
|
||||||
|
})}</pre>`;
|
||||||
|
|
||||||
|
const afterElement = html`<div>
|
||||||
|
<pre><b>x</b>${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</pre>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Option 2
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore"}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -4,12 +4,12 @@
|
||||||
|
<body>
|
||||||
|
<form>
|
||||||
|
<textarea>
|
||||||
|
${Match.valueTags(form, {
|
||||||
|
- EmptyForm: () => "",
|
||||||
|
- InvalidForm: () => "",
|
||||||
|
- CompletedForm: () => "",
|
||||||
|
- })}</textarea>
|
||||||
|
+ EmptyForm: () => "",
|
||||||
|
+ InvalidForm: () => "",
|
||||||
|
+ CompletedForm: () => "",
|
||||||
|
+})}</textarea>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
@@ -17,27 +17,27 @@
|
||||||
|
const indented = html`
|
||||||
|
<form>
|
||||||
|
<pre>
|
||||||
|
${foo({
|
||||||
|
- x: 1,
|
||||||
|
- })}</pre>
|
||||||
|
+ x: 1,
|
||||||
|
+ })}</pre>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const afterText = html`
|
||||||
|
<pre>
|
||||||
|
a ${foo({
|
||||||
|
- x: 1,
|
||||||
|
- })} b</pre>
|
||||||
|
+ x: 1,
|
||||||
|
+})} b</pre>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const sameLine = html`
|
||||||
|
<pre>
|
||||||
|
${a({
|
||||||
|
- x: 1,
|
||||||
|
- })} ${b({
|
||||||
|
- y: 2,
|
||||||
|
- })}</pre>
|
||||||
|
+ x: 1,
|
||||||
|
+ })} ${b({
|
||||||
|
+ y: 2,
|
||||||
|
+ })}</pre>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const afterElement = html`
|
||||||
|
<div>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
// prettier/prettier#20355
|
||||||
|
html`
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<form>
|
||||||
|
<textarea>
|
||||||
|
${Match.valueTags(form, {
|
||||||
|
EmptyForm: () => "",
|
||||||
|
InvalidForm: () => "",
|
||||||
|
CompletedForm: () => "",
|
||||||
|
})}</textarea>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const indented = html`
|
||||||
|
<form>
|
||||||
|
<pre>
|
||||||
|
${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</pre>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const afterText = html`
|
||||||
|
<pre>
|
||||||
|
a ${foo({
|
||||||
|
x: 1,
|
||||||
|
})} b</pre>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const sameLine = html`
|
||||||
|
<pre>
|
||||||
|
${a({
|
||||||
|
x: 1,
|
||||||
|
})} ${b({
|
||||||
|
y: 2,
|
||||||
|
})}</pre>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const afterElement = html`
|
||||||
|
<div>
|
||||||
|
<pre><b>x</b>${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
// prettier/prettier#20355
|
||||||
|
html`
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<form>
|
||||||
|
<textarea>
|
||||||
|
${Match.valueTags(form, {
|
||||||
|
EmptyForm: () => "",
|
||||||
|
InvalidForm: () => "",
|
||||||
|
CompletedForm: () => "",
|
||||||
|
})}</textarea>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const indented = html`
|
||||||
|
<form>
|
||||||
|
<pre>
|
||||||
|
${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</pre>
|
||||||
|
</form>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const afterText = html`
|
||||||
|
<pre>
|
||||||
|
a ${foo({
|
||||||
|
x: 1,
|
||||||
|
})} b</pre>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const sameLine = html`
|
||||||
|
<pre>
|
||||||
|
${a({
|
||||||
|
x: 1,
|
||||||
|
})} ${b({
|
||||||
|
y: 2,
|
||||||
|
})}</pre>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const afterElement = html`
|
||||||
|
<div>
|
||||||
|
<pre><b>x</b>${foo({
|
||||||
|
x: 1,
|
||||||
|
})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
`````
|
||||||
+285
@@ -0,0 +1,285 @@
|
|||||||
|
# edge-cases/html-in-js/template-expression-indent.js
|
||||||
|
|
||||||
|
## Option 1
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":80}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -34,11 +34,11 @@
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
${JSON.stringify({
|
||||||
|
- a: 1,
|
||||||
|
- b: 2,
|
||||||
|
- })}</pre>
|
||||||
|
+ a: 1,
|
||||||
|
+ b: 2,
|
||||||
|
+})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
_ = html`
|
||||||
|
<div>
|
||||||
|
${
|
||||||
|
a + //
|
||||||
|
b
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// prettier/prettier#19518: nested embeds were not idempotent
|
||||||
|
const t = html`
|
||||||
|
<ol>
|
||||||
|
${items.map(
|
||||||
|
(entry) => html`
|
||||||
|
<li>
|
||||||
|
${
|
||||||
|
entry.children
|
||||||
|
? html`
|
||||||
|
<ol>
|
||||||
|
${entry.children.map(
|
||||||
|
(child) => html`<li>${child.title}</li>`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`
|
||||||
|
: entry.title
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function foo() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
${JSON.stringify({
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = html`
|
||||||
|
${{
|
||||||
|
c: y,
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
_ = html`
|
||||||
|
<div>
|
||||||
|
${
|
||||||
|
a + //
|
||||||
|
b
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// prettier/prettier#19518: nested embeds were not idempotent
|
||||||
|
const t = html`
|
||||||
|
<ol>
|
||||||
|
${items.map(
|
||||||
|
(entry) => html`
|
||||||
|
<li>
|
||||||
|
${
|
||||||
|
entry.children
|
||||||
|
? html`
|
||||||
|
<ol>
|
||||||
|
${entry.children.map(
|
||||||
|
(child) => html`<li>${child.title}</li>`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`
|
||||||
|
: entry.title
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function foo() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
${JSON.stringify({
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = html`
|
||||||
|
${{
|
||||||
|
c: y,
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Option 2
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore"}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -36,11 +36,11 @@
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
${JSON.stringify({
|
||||||
|
- a: 1,
|
||||||
|
- b: 2,
|
||||||
|
- })}</pre>
|
||||||
|
+ a: 1,
|
||||||
|
+ b: 2,
|
||||||
|
+})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
_ = html`
|
||||||
|
<div>
|
||||||
|
${
|
||||||
|
a + //
|
||||||
|
b
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// prettier/prettier#19518: nested embeds were not idempotent
|
||||||
|
const t = html`
|
||||||
|
<ol>
|
||||||
|
${items.map(
|
||||||
|
(entry) => html`
|
||||||
|
<li>
|
||||||
|
${
|
||||||
|
entry.children
|
||||||
|
? html`
|
||||||
|
<ol>
|
||||||
|
${entry.children.map(
|
||||||
|
(child) => html`
|
||||||
|
<li>${child.title}</li>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`
|
||||||
|
: entry.title
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function foo() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
${JSON.stringify({
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = html`
|
||||||
|
${{
|
||||||
|
c: y,
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
_ = html`
|
||||||
|
<div>
|
||||||
|
${
|
||||||
|
a + //
|
||||||
|
b
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// prettier/prettier#19518: nested embeds were not idempotent
|
||||||
|
const t = html`
|
||||||
|
<ol>
|
||||||
|
${items.map(
|
||||||
|
(entry) => html`
|
||||||
|
<li>
|
||||||
|
${
|
||||||
|
entry.children
|
||||||
|
? html`
|
||||||
|
<ol>
|
||||||
|
${entry.children.map(
|
||||||
|
(child) => html`
|
||||||
|
<li>${child.title}</li>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`
|
||||||
|
: entry.title
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function foo() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
${JSON.stringify({
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = html`
|
||||||
|
${{
|
||||||
|
c: y,
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
`````
|
||||||
+245
@@ -0,0 +1,245 @@
|
|||||||
|
# externals/prettier/js/embeded/indention/19518.js
|
||||||
|
|
||||||
|
## Option 1
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":80}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -24,11 +24,11 @@
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
${JSON.stringify({
|
||||||
|
- a: 1,
|
||||||
|
- b: 2,
|
||||||
|
- })}</pre>
|
||||||
|
+ a: 1,
|
||||||
|
+ b: 2,
|
||||||
|
+})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
const t = html`
|
||||||
|
<ol>
|
||||||
|
${items.map(
|
||||||
|
(entry) => html`
|
||||||
|
<li>
|
||||||
|
${
|
||||||
|
entry.children
|
||||||
|
? html`
|
||||||
|
<ol>
|
||||||
|
${entry.children.map(
|
||||||
|
(child) => html`<li>${child.title}</li>`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`
|
||||||
|
: entry.title
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function foo() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
${JSON.stringify({
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = html`
|
||||||
|
${{
|
||||||
|
c: y,
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
const t = html`
|
||||||
|
<ol>
|
||||||
|
${items.map(
|
||||||
|
(entry) => html`
|
||||||
|
<li>
|
||||||
|
${
|
||||||
|
entry.children
|
||||||
|
? html`
|
||||||
|
<ol>
|
||||||
|
${entry.children.map(
|
||||||
|
(child) => html`<li>${child.title}</li>`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`
|
||||||
|
: entry.title
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function foo() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
${JSON.stringify({
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = html`
|
||||||
|
${{
|
||||||
|
c: y,
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Option 2
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore"}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -26,11 +26,11 @@
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
${JSON.stringify({
|
||||||
|
- a: 1,
|
||||||
|
- b: 2,
|
||||||
|
- })}</pre>
|
||||||
|
+ a: 1,
|
||||||
|
+ b: 2,
|
||||||
|
+})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
const t = html`
|
||||||
|
<ol>
|
||||||
|
${items.map(
|
||||||
|
(entry) => html`
|
||||||
|
<li>
|
||||||
|
${
|
||||||
|
entry.children
|
||||||
|
? html`
|
||||||
|
<ol>
|
||||||
|
${entry.children.map(
|
||||||
|
(child) => html`
|
||||||
|
<li>${child.title}</li>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`
|
||||||
|
: entry.title
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function foo() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
${JSON.stringify({
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = html`
|
||||||
|
${{
|
||||||
|
c: y,
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````js
|
||||||
|
const t = html`
|
||||||
|
<ol>
|
||||||
|
${items.map(
|
||||||
|
(entry) => html`
|
||||||
|
<li>
|
||||||
|
${
|
||||||
|
entry.children
|
||||||
|
? html`
|
||||||
|
<ol>
|
||||||
|
${entry.children.map(
|
||||||
|
(child) => html`
|
||||||
|
<li>${child.title}</li>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`
|
||||||
|
: entry.title
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
`,
|
||||||
|
)}
|
||||||
|
</ol>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function foo() {
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
${JSON.stringify({
|
||||||
|
a: 1,
|
||||||
|
b: 2,
|
||||||
|
})}</pre>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = html`
|
||||||
|
${{
|
||||||
|
c: y,
|
||||||
|
}}
|
||||||
|
`;
|
||||||
|
|
||||||
|
`````
|
||||||
+151
@@ -0,0 +1,151 @@
|
|||||||
|
# edge-cases/html/script-end-in-gql-template.html
|
||||||
|
|
||||||
|
## Option 1
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":80}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
- field # </script>
|
||||||
|
+ field # <\/script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
field # <\/script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
field # </script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Option 2
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore","singleAttributePerLine":true}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
- field # </script>
|
||||||
|
+ field # <\/script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
field # <\/script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
field # </script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Option 3
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":120,"htmlWhitespaceSensitivity":"strict","bracketSameLine":true}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -1,7 +1,7 @@
|
||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
- field # </script>
|
||||||
|
+ field # <\/script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
field # <\/script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<script>
|
||||||
|
const query = gql`
|
||||||
|
query {
|
||||||
|
field # </script>
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
`````
|
||||||
+157
@@ -0,0 +1,157 @@
|
|||||||
|
# externals/prettier/html/basics/broken-html.html
|
||||||
|
|
||||||
|
## Option 1
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":80}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -5,5 +5,5 @@
|
||||||
|
linebreak
|
||||||
|
`<`
|
||||||
|
extra space(s)
|
||||||
|
-->
|
||||||
|
-<div><span> < </span></div>
|
||||||
|
+<div><span> <
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!--
|
||||||
|
#7241,
|
||||||
|
reproduction:
|
||||||
|
two different element,
|
||||||
|
linebreak
|
||||||
|
`<`
|
||||||
|
extra space(s)
|
||||||
|
-->
|
||||||
|
<div><span> <
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!--
|
||||||
|
#7241,
|
||||||
|
reproduction:
|
||||||
|
two different element,
|
||||||
|
linebreak
|
||||||
|
`<`
|
||||||
|
extra space(s)
|
||||||
|
-->
|
||||||
|
<div><span> < </span></div>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Option 2
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore","singleAttributePerLine":true}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -5,5 +5,5 @@
|
||||||
|
linebreak
|
||||||
|
`<`
|
||||||
|
extra space(s)
|
||||||
|
-->
|
||||||
|
-<div><span><</span></div>
|
||||||
|
+<div><span><
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!--
|
||||||
|
#7241,
|
||||||
|
reproduction:
|
||||||
|
two different element,
|
||||||
|
linebreak
|
||||||
|
`<`
|
||||||
|
extra space(s)
|
||||||
|
-->
|
||||||
|
<div><span><
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!--
|
||||||
|
#7241,
|
||||||
|
reproduction:
|
||||||
|
two different element,
|
||||||
|
linebreak
|
||||||
|
`<`
|
||||||
|
extra space(s)
|
||||||
|
-->
|
||||||
|
<div><span><</span></div>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Option 3
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":120,"htmlWhitespaceSensitivity":"strict","bracketSameLine":true}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -5,5 +5,5 @@
|
||||||
|
linebreak
|
||||||
|
`<`
|
||||||
|
extra space(s)
|
||||||
|
-->
|
||||||
|
-<div><span> < </span></div>
|
||||||
|
+<div><span> <
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!--
|
||||||
|
#7241,
|
||||||
|
reproduction:
|
||||||
|
two different element,
|
||||||
|
linebreak
|
||||||
|
`<`
|
||||||
|
extra space(s)
|
||||||
|
-->
|
||||||
|
<div><span> <
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!--
|
||||||
|
#7241,
|
||||||
|
reproduction:
|
||||||
|
two different element,
|
||||||
|
linebreak
|
||||||
|
`<`
|
||||||
|
extra space(s)
|
||||||
|
-->
|
||||||
|
<div><span> < </span></div>
|
||||||
|
|
||||||
|
`````
|
||||||
+1476
File diff suppressed because it is too large.
Load diff
+133
@@ -0,0 +1,133 @@
|
|||||||
|
# externals/prettier/html/prettier_ignore/unclosed2.html
|
||||||
|
|
||||||
|
## Option 1
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":80}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -1,5 +1,5 @@
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<p>
|
||||||
|
# Hello world!
|
||||||
|
|
||||||
|
-<div></div>
|
||||||
|
+<div>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<p>
|
||||||
|
# Hello world!
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<p>
|
||||||
|
# Hello world!
|
||||||
|
|
||||||
|
<div></div>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Option 2
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore","singleAttributePerLine":true}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -1,5 +1,5 @@
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<p>
|
||||||
|
# Hello world!
|
||||||
|
|
||||||
|
-<div></div>
|
||||||
|
+<div>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<p>
|
||||||
|
# Hello world!
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<p>
|
||||||
|
# Hello world!
|
||||||
|
|
||||||
|
<div></div>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Option 3
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":120,"htmlWhitespaceSensitivity":"strict","bracketSameLine":true}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -1,5 +1,5 @@
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<p>
|
||||||
|
# Hello world!
|
||||||
|
|
||||||
|
-<div> </div>
|
||||||
|
+<div>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<p>
|
||||||
|
# Hello world!
|
||||||
|
|
||||||
|
<div>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!-- prettier-ignore -->
|
||||||
|
<p>
|
||||||
|
# Hello world!
|
||||||
|
|
||||||
|
<div> </div>
|
||||||
|
|
||||||
|
`````
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
# externals/prettier/html/script/script.html
|
||||||
|
|
||||||
|
## Option 3
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":120,"htmlWhitespaceSensitivity":"strict","bracketSameLine":true}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -7,9 +7,10 @@
|
||||||
|
<script type="importmap">
|
||||||
|
{ "json": true }
|
||||||
|
</script>
|
||||||
|
<script type="systemjs-importmap">
|
||||||
|
- { "json": true }</script
|
||||||
|
+ { "json": true }
|
||||||
|
+</script
|
||||||
|
><script type="invalid">
|
||||||
|
{ "json":false }
|
||||||
|
</script>
|
||||||
|
<script type="text/html">
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{ "json": true }
|
||||||
|
</script>
|
||||||
|
<script type="application/json">
|
||||||
|
{ "json": true }
|
||||||
|
</script>
|
||||||
|
<script type="importmap">
|
||||||
|
{ "json": true }
|
||||||
|
</script>
|
||||||
|
<script type="systemjs-importmap">
|
||||||
|
{ "json": true }
|
||||||
|
</script
|
||||||
|
><script type="invalid">
|
||||||
|
{ "json":false }
|
||||||
|
</script>
|
||||||
|
<script type="text/html">
|
||||||
|
<div>
|
||||||
|
<p>foo</p>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script async="" id="" src="/_next/static/development/pages/_app.js?ts=1565732195968"></script
|
||||||
|
><script></script>
|
||||||
|
|
||||||
|
<!-- #8147 -->
|
||||||
|
<script lang="vbscript">
|
||||||
|
Function hello()
|
||||||
|
End Function
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="unknown"></script>
|
||||||
|
|
||||||
|
<script type="speculationrules">
|
||||||
|
{
|
||||||
|
"prerender": [{ "source": "list", "urls": ["https://a.test/foo"] }]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{ "json": true }
|
||||||
|
</script>
|
||||||
|
<script type="application/json">
|
||||||
|
{ "json": true }
|
||||||
|
</script>
|
||||||
|
<script type="importmap">
|
||||||
|
{ "json": true }
|
||||||
|
</script>
|
||||||
|
<script type="systemjs-importmap">
|
||||||
|
{ "json": true }</script
|
||||||
|
><script type="invalid">
|
||||||
|
{ "json":false }
|
||||||
|
</script>
|
||||||
|
<script type="text/html">
|
||||||
|
<div>
|
||||||
|
<p>foo</p>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script async="" id="" src="/_next/static/development/pages/_app.js?ts=1565732195968"></script
|
||||||
|
><script></script>
|
||||||
|
|
||||||
|
<!-- #8147 -->
|
||||||
|
<script lang="vbscript">
|
||||||
|
Function hello()
|
||||||
|
End Function
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="unknown"></script>
|
||||||
|
|
||||||
|
<script type="speculationrules">
|
||||||
|
{
|
||||||
|
"prerender": [{ "source": "list", "urls": ["https://a.test/foo"] }]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
`````
|
||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
# externals/prettier/html/tags/tags2.html
|
||||||
|
|
||||||
|
## Option 1
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":80}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -12,11 +12,10 @@
|
||||||
|
after
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
- before
|
||||||
|
- <dialog open>dialog long long long long long long long long</dialog>
|
||||||
|
- after
|
||||||
|
+ before<dialog open>dialog long long long long long long long long</dialog
|
||||||
|
+ >after
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
before<object data="horse.wav">
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<div>
|
||||||
|
before<noscript>noscript long long long long long long long long</noscript
|
||||||
|
>after
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
before
|
||||||
|
<details>
|
||||||
|
<summary>summary long long long long</summary>
|
||||||
|
details
|
||||||
|
</details>
|
||||||
|
after
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
before<dialog open>dialog long long long long long long long long</dialog
|
||||||
|
>after
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
before<object data="horse.wav">
|
||||||
|
<param name="autoplay" value="true" />
|
||||||
|
<param name="autoplay" value="true" /></object
|
||||||
|
>after
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
before<meter
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
low=".4"
|
||||||
|
high=".7"
|
||||||
|
optimum=".5"
|
||||||
|
value=".2"
|
||||||
|
></meter
|
||||||
|
>after
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>before<progress value=".5" max="1"></progress>after</div>
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<div>
|
||||||
|
before<noscript>noscript long long long long long long long long</noscript
|
||||||
|
>after
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
before
|
||||||
|
<details>
|
||||||
|
<summary>summary long long long long</summary>
|
||||||
|
details
|
||||||
|
</details>
|
||||||
|
after
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
before
|
||||||
|
<dialog open>dialog long long long long long long long long</dialog>
|
||||||
|
after
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
before<object data="horse.wav">
|
||||||
|
<param name="autoplay" value="true" />
|
||||||
|
<param name="autoplay" value="true" /></object
|
||||||
|
>after
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
before<meter
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
low=".4"
|
||||||
|
high=".7"
|
||||||
|
optimum=".5"
|
||||||
|
value=".2"
|
||||||
|
></meter
|
||||||
|
>after
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>before<progress value=".5" max="1"></progress>after</div>
|
||||||
|
|
||||||
|
`````
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
# externals/vite/html/valid.html
|
||||||
|
|
||||||
|
## Option 1
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":80}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -1,1 +1,20 @@
|
||||||
|
-ERROR
|
||||||
|
\ No newline at end of file
|
||||||
|
+<!-- accepted HTML parsing errors, browsers accept these -->
|
||||||
|
+<div id="duplicated-attrs" style="color: green" style="color: red">
|
||||||
|
+ Accept duplicated attribute
|
||||||
|
+</div>
|
||||||
|
+
|
||||||
|
+<!-- accepted HTML constructs -->
|
||||||
|
+<datalist>
|
||||||
|
+ <option value="option-without-closing-tag">
|
||||||
|
+</datalist>
|
||||||
|
+
|
||||||
|
+<div id="no-quotes-on-attr">No quotes on Attr</div>
|
||||||
|
+<script type="module" src="/valid.js"></script>
|
||||||
|
+
|
||||||
|
+<svg>
|
||||||
|
+ <!-- attr with prefix -->
|
||||||
|
+ <use xlink:href="/sprite.svg#svg"></use>
|
||||||
|
+</svg>
|
||||||
|
+
|
||||||
|
+<!-- allow self closing on non-void elements -->
|
||||||
|
+<self-close-non-void />
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!-- accepted HTML parsing errors, browsers accept these -->
|
||||||
|
<div id="duplicated-attrs" style="color: green" style="color: red">
|
||||||
|
Accept duplicated attribute
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- accepted HTML constructs -->
|
||||||
|
<datalist>
|
||||||
|
<option value="option-without-closing-tag">
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<div id="no-quotes-on-attr">No quotes on Attr</div>
|
||||||
|
<script type="module" src="/valid.js"></script>
|
||||||
|
|
||||||
|
<svg>
|
||||||
|
<!-- attr with prefix -->
|
||||||
|
<use xlink:href="/sprite.svg#svg"></use>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- allow self closing on non-void elements -->
|
||||||
|
<self-close-non-void />
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
ERROR
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Option 2
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":100,"htmlWhitespaceSensitivity":"ignore","singleAttributePerLine":true}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -1,1 +1,27 @@
|
||||||
|
-ERROR
|
||||||
|
\ No newline at end of file
|
||||||
|
+<!-- accepted HTML parsing errors, browsers accept these -->
|
||||||
|
+<div
|
||||||
|
+ id="duplicated-attrs"
|
||||||
|
+ style="color: green"
|
||||||
|
+ style="color: red"
|
||||||
|
+>
|
||||||
|
+ Accept duplicated attribute
|
||||||
|
+</div>
|
||||||
|
+
|
||||||
|
+<!-- accepted HTML constructs -->
|
||||||
|
+<datalist>
|
||||||
|
+ <option value="option-without-closing-tag">
|
||||||
|
+</datalist>
|
||||||
|
+
|
||||||
|
+<div id="no-quotes-on-attr">No quotes on Attr</div>
|
||||||
|
+<script
|
||||||
|
+ type="module"
|
||||||
|
+ src="/valid.js"
|
||||||
|
+></script>
|
||||||
|
+
|
||||||
|
+<svg>
|
||||||
|
+ <!-- attr with prefix -->
|
||||||
|
+ <use xlink:href="/sprite.svg#svg"></use>
|
||||||
|
+</svg>
|
||||||
|
+
|
||||||
|
+<!-- allow self closing on non-void elements -->
|
||||||
|
+<self-close-non-void />
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!-- accepted HTML parsing errors, browsers accept these -->
|
||||||
|
<div
|
||||||
|
id="duplicated-attrs"
|
||||||
|
style="color: green"
|
||||||
|
style="color: red"
|
||||||
|
>
|
||||||
|
Accept duplicated attribute
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- accepted HTML constructs -->
|
||||||
|
<datalist>
|
||||||
|
<option value="option-without-closing-tag">
|
||||||
|
</datalist>
|
||||||
|
|
||||||
|
<div id="no-quotes-on-attr">No quotes on Attr</div>
|
||||||
|
<script
|
||||||
|
type="module"
|
||||||
|
src="/valid.js"
|
||||||
|
></script>
|
||||||
|
|
||||||
|
<svg>
|
||||||
|
<!-- attr with prefix -->
|
||||||
|
<use xlink:href="/sprite.svg#svg"></use>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- allow self closing on non-void elements -->
|
||||||
|
<self-close-non-void />
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
ERROR
|
||||||
|
`````
|
||||||
|
|
||||||
|
## Option 3
|
||||||
|
|
||||||
|
`````json
|
||||||
|
{"printWidth":120,"htmlWhitespaceSensitivity":"strict","bracketSameLine":true}
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Diff
|
||||||
|
|
||||||
|
`````diff
|
||||||
|
===================================================================
|
||||||
|
--- prettier
|
||||||
|
+++ oxfmt
|
||||||
|
@@ -1,1 +1,16 @@
|
||||||
|
-ERROR
|
||||||
|
\ No newline at end of file
|
||||||
|
+<!-- accepted HTML parsing errors, browsers accept these -->
|
||||||
|
+<div id="duplicated-attrs" style="color: green" style="color: red">Accept duplicated attribute</div>
|
||||||
|
+
|
||||||
|
+<!-- accepted HTML constructs -->
|
||||||
|
+<datalist> <option value="option-without-closing-tag"> </datalist>
|
||||||
|
+
|
||||||
|
+<div id="no-quotes-on-attr">No quotes on Attr</div>
|
||||||
|
+<script type="module" src="/valid.js"></script>
|
||||||
|
+
|
||||||
|
+<svg>
|
||||||
|
+ <!-- attr with prefix -->
|
||||||
|
+ <use xlink:href="/sprite.svg#svg"></use>
|
||||||
|
+</svg>
|
||||||
|
+
|
||||||
|
+<!-- allow self closing on non-void elements -->
|
||||||
|
+<self-close-non-void />
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Actual (oxfmt)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
<!-- accepted HTML parsing errors, browsers accept these -->
|
||||||
|
<div id="duplicated-attrs" style="color: green" style="color: red">Accept duplicated attribute</div>
|
||||||
|
|
||||||
|
<!-- accepted HTML constructs -->
|
||||||
|
<datalist> <option value="option-without-closing-tag"> </datalist>
|
||||||
|
|
||||||
|
<div id="no-quotes-on-attr">No quotes on Attr</div>
|
||||||
|
<script type="module" src="/valid.js"></script>
|
||||||
|
|
||||||
|
<svg>
|
||||||
|
<!-- attr with prefix -->
|
||||||
|
<use xlink:href="/sprite.svg#svg"></use>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<!-- allow self closing on non-void elements -->
|
||||||
|
<self-close-non-void />
|
||||||
|
|
||||||
|
`````
|
||||||
|
|
||||||
|
### Expected (prettier)
|
||||||
|
|
||||||
|
`````html
|
||||||
|
ERROR
|
||||||
|
`````
|
||||||
+1599
File diff suppressed because it is too large.
Load diff
+2849
File diff suppressed because it is too large.
Load diff
+1967
File diff suppressed because it is too large.
Load diff
Vendored
+4
-4
@@ -44,7 +44,7 @@ export declare const enum Severity {
|
|||||||
* # Panics
|
* # Panics
|
||||||
* Panics if the current working directory cannot be determined.
|
* Panics if the current working directory cannot be determined.
|
||||||
*/
|
*/
|
||||||
export declare function format(filename: string, sourceText: string, options: any | undefined | null, formatFileCb: (options: Record<string, any>, code: string) => Promise<{ ok: true; code: string; } | { ok: false; error: string }>, formatEmbeddedCb: (options: Record<string, any>, code: string) => Promise<string | null>, formatEmbeddedDocCb: (options: Record<string, any>, code: string) => Promise<string | null>, sortTailwindClassesCb: (options: Record<string, any>, classes: string[]) => Promise<string[] | null>): Promise<FormatResult>
|
export declare function format(filename: string, sourceText: string, options: any | undefined | null, formatFileCb: (options: Record<string, any>, code: string) => Promise<{ ok: true; code: string; } | { ok: false; error: string }>, formatEmbeddedDocCb: (options: Record<string, any>, code: string) => Promise<string | null>, sortTailwindClassesCb: (options: Record<string, any>, classes: string[]) => Promise<string[] | null>): Promise<FormatResult>
|
||||||
|
|
||||||
export interface FormatResult {
|
export interface FormatResult {
|
||||||
/** The formatted code. */
|
/** The formatted code. */
|
||||||
@@ -59,7 +59,7 @@ export interface FormatResult {
|
|||||||
* This API is specialized for JS/TS snippets embedded in non-JS files.
|
* This API is specialized for JS/TS snippets embedded in non-JS files.
|
||||||
* Unlike `format()`, it is called only for js-in-xxx `textToDoc()` flow.
|
* Unlike `format()`, it is called only for js-in-xxx `textToDoc()` flow.
|
||||||
*/
|
*/
|
||||||
export declare function jsTextToDoc(sourceExt: string, sourceText: string, oxfmtPluginOptionsJson: string, parentContext: string, formatFileCb: (options: Record<string, any>, code: string) => Promise<{ ok: true; code: string; } | { ok: false; error: string }>, formatEmbeddedCb: (options: Record<string, any>, code: string) => Promise<string | null>, formatEmbeddedDocCb: (options: Record<string, any>, code: string) => Promise<string | null>, sortTailwindClassesCb: (options: Record<string, any>, classes: string[]) => Promise<string[] | null>): Promise<string | null>
|
export declare function jsTextToDoc(sourceExt: string, sourceText: string, oxfmtPluginOptionsJson: string, parentContext: string, formatFileCb: (options: Record<string, any>, code: string) => Promise<{ ok: true; code: string; } | { ok: false; error: string }>, formatEmbeddedDocCb: (options: Record<string, any>, code: string) => Promise<string | null>, sortTailwindClassesCb: (options: Record<string, any>, classes: string[]) => Promise<string[] | null>): Promise<string | null>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NAPI based JS CLI entry point.
|
* NAPI based JS CLI entry point.
|
||||||
@@ -70,11 +70,11 @@ export declare function jsTextToDoc(sourceExt: string, sourceText: string, oxfmt
|
|||||||
* 2. `load_js_config_cb`: Callback to load JS/TS config files
|
* 2. `load_js_config_cb`: Callback to load JS/TS config files
|
||||||
* 3. `init_external_services_cb`: Callback to initialize external services (JS worker pool)
|
* 3. `init_external_services_cb`: Callback to initialize external services (JS worker pool)
|
||||||
* 4. `format_file_cb`: Callback to format files
|
* 4. `format_file_cb`: Callback to format files
|
||||||
* 5. `format_embedded_cb`: Callback to format embedded code in templates
|
* 5. `format_embedded_doc_cb`: Callback to format embedded code in templates as a Prettier Doc
|
||||||
* 6. `sort_tailwindcss_classes_cb`: Callback to sort Tailwind classes
|
* 6. `sort_tailwindcss_classes_cb`: Callback to sort Tailwind classes
|
||||||
*
|
*
|
||||||
* Returns a tuple of `[mode, exitCode]`:
|
* Returns a tuple of `[mode, exitCode]`:
|
||||||
* - `mode`: If main logic will run in JS side, use this to indicate which mode
|
* - `mode`: If main logic will run in JS side, use this to indicate which mode
|
||||||
* - `exitCode`: If main logic already ran in Rust side, return the exit code
|
* - `exitCode`: If main logic already ran in Rust side, return the exit code
|
||||||
*/
|
*/
|
||||||
export declare function runCli(args: Array<string>, loadJsConfigCb: (path: string) => Promise<any>, initExternalServicesCb: (numThreads: number) => Promise<void>, formatFileCb: (options: Record<string, any>, code: string) => Promise<{ ok: true; code: string; } | { ok: false; error: string }>, formatEmbeddedCb: (options: Record<string, any>, code: string) => Promise<string | null>, formatEmbeddedDocCb: (options: Record<string, any>, code: string) => Promise<string | null>, sortTailwindcssClassesCb: (options: Record<string, any>, classes: string[]) => Promise<string[] | null>): Promise<[string, number | undefined | null]>
|
export declare function runCli(args: Array<string>, loadJsConfigCb: (path: string) => Promise<any>, initExternalServicesCb: (numThreads: number) => Promise<void>, formatFileCb: (options: Record<string, any>, code: string) => Promise<{ ok: true; code: string; } | { ok: false; error: string }>, formatEmbeddedDocCb: (options: Record<string, any>, code: string) => Promise<string | null>, sortTailwindcssClassesCb: (options: Record<string, any>, classes: string[]) => Promise<string[] | null>): Promise<[string, number | undefined | null]>
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
// `oxfmt` CLI - Worker Thread Entry Point
|
// `oxfmt` CLI - Worker Thread Entry Point
|
||||||
|
|
||||||
// Re-exports core functions for use in `worker_threads`
|
// Re-exports core functions for use in `worker_threads`
|
||||||
export {
|
export { formatEmbeddedDoc, formatFile, sortTailwindClasses } from "./libs/apis";
|
||||||
formatEmbeddedCode,
|
|
||||||
formatEmbeddedDoc,
|
|
||||||
formatFile,
|
|
||||||
sortTailwindClasses,
|
|
||||||
} from "./libs/apis";
|
|
||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
initExternalServices,
|
initExternalServices,
|
||||||
disposeExternalServices,
|
disposeExternalServices,
|
||||||
formatFile,
|
formatFile,
|
||||||
formatEmbeddedCode,
|
|
||||||
formatEmbeddedDoc,
|
formatEmbeddedDoc,
|
||||||
sortTailwindClasses,
|
sortTailwindClasses,
|
||||||
} from "./cli/worker-proxy";
|
} from "./cli/worker-proxy";
|
||||||
@@ -47,7 +46,6 @@ void (async () => {
|
|||||||
process.env.VP_VERSION ? loadVitePlusConfig : loadJsConfig,
|
process.env.VP_VERSION ? loadVitePlusConfig : loadJsConfig,
|
||||||
initExternalServices,
|
initExternalServices,
|
||||||
formatFile,
|
formatFile,
|
||||||
formatEmbeddedCode,
|
|
||||||
formatEmbeddedDoc,
|
formatEmbeddedDoc,
|
||||||
sortTailwindClasses,
|
sortTailwindClasses,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { toFormatFileResult, toNullable } from "../libs/napi-callbacks";
|
|||||||
import type { FormatFileResult } from "../libs/napi-callbacks";
|
import type { FormatFileResult } from "../libs/napi-callbacks";
|
||||||
import type {
|
import type {
|
||||||
FormatFileParam,
|
FormatFileParam,
|
||||||
FormatEmbeddedCodeParam,
|
|
||||||
FormatEmbeddedDocParam,
|
FormatEmbeddedDocParam,
|
||||||
SortTailwindClassesArgs,
|
SortTailwindClassesArgs,
|
||||||
} from "../libs/apis";
|
} from "../libs/apis";
|
||||||
@@ -65,19 +64,6 @@ export function formatFile(
|
|||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
export function formatEmbeddedCode(
|
|
||||||
options: FormatEmbeddedCodeParam["options"],
|
|
||||||
code: string,
|
|
||||||
): Promise<string | null> {
|
|
||||||
return toNullable(
|
|
||||||
getPool().then((pool) =>
|
|
||||||
pool.run({ options, code } satisfies FormatEmbeddedCodeParam, {
|
|
||||||
name: "formatEmbeddedCode",
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatEmbeddedDoc(
|
export function formatEmbeddedDoc(
|
||||||
options: FormatEmbeddedDocParam["options"],
|
options: FormatEmbeddedDocParam["options"],
|
||||||
code: string,
|
code: string,
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
// napi-JS `oxfmt` API entry point
|
// napi-JS `oxfmt` API entry point
|
||||||
|
|
||||||
import {
|
import { formatFile, formatEmbeddedDoc, sortTailwindClasses } from "./libs/apis";
|
||||||
formatFile,
|
|
||||||
formatEmbeddedCode,
|
|
||||||
formatEmbeddedDoc,
|
|
||||||
sortTailwindClasses,
|
|
||||||
} from "./libs/apis";
|
|
||||||
import { toFormatFileResult, toNullable } from "./libs/napi-callbacks";
|
import { toFormatFileResult, toNullable } from "./libs/napi-callbacks";
|
||||||
// Types are auto-generated from the JSON Schema.
|
// Types are auto-generated from the JSON Schema.
|
||||||
import type {
|
import type {
|
||||||
@@ -95,7 +90,6 @@ export async function format(fileName: string, sourceText: string, options?: For
|
|||||||
sourceText,
|
sourceText,
|
||||||
options ?? {},
|
options ?? {},
|
||||||
(options, code) => toFormatFileResult(formatFile({ options, code })),
|
(options, code) => toFormatFileResult(formatFile({ options, code })),
|
||||||
(options, code) => toNullable(formatEmbeddedCode({ options, code })),
|
|
||||||
(options, code) => toNullable(formatEmbeddedDoc({ options, code })),
|
(options, code) => toNullable(formatEmbeddedDoc({ options, code })),
|
||||||
(options, classes) => toNullable(sortTailwindClasses({ options, classes })),
|
(options, classes) => toNullable(sortTailwindClasses({ options, classes })),
|
||||||
);
|
);
|
||||||
@@ -117,7 +111,6 @@ export async function jsTextToDoc(
|
|||||||
oxfmtPluginOptionsJson,
|
oxfmtPluginOptionsJson,
|
||||||
parentContext,
|
parentContext,
|
||||||
() => toFormatFileResult(Promise.reject("formatFile is unavailable for jsTextToDoc")),
|
() => toFormatFileResult(Promise.reject("formatFile is unavailable for jsTextToDoc")),
|
||||||
(options, code) => toNullable(formatEmbeddedCode({ options, code })),
|
|
||||||
(options, code) => toNullable(formatEmbeddedDoc({ options, code })),
|
(options, code) => toNullable(formatEmbeddedDoc({ options, code })),
|
||||||
(options, classes) => toNullable(sortTailwindClasses({ options, classes })),
|
(options, classes) => toNullable(sortTailwindClasses({ options, classes })),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,14 +50,14 @@ async function loadPrettier(): Promise<typeof import("prettier")> {
|
|||||||
// there should be no side effects on other calls that don't set these fields.
|
// there should be no side effects on other calls that don't set these fields.
|
||||||
// @ts-expect-error: Use internal API
|
// @ts-expect-error: Use internal API
|
||||||
const { formatOptionsHiddenDefaults } = prettier.__internal;
|
const { formatOptionsHiddenDefaults } = prettier.__internal;
|
||||||
// For every embedded call (html-in-js, xxx-in-md), as Prettier's `textToDoc()` always sets it.
|
// For every embedded call (angular-in-js, xxx-in-md), as Prettier's `textToDoc()` always sets it.
|
||||||
// For html-family children, this prevents attribute level formatting from running.
|
// For html-family children, this prevents attribute level formatting from running.
|
||||||
// (e.g., CSS in `style="..."` attributes, JS in `onclick="..."` event handlers)
|
// (e.g., CSS in `style="..."` attributes, JS in `onclick="..."` event handlers)
|
||||||
// This does NOT affect `<style>`/`<script>` tags, they are always formatted.
|
// This does NOT affect `<style>`/`<script>` tags, they are always formatted.
|
||||||
// Ideally we'd only block JS attributes while allowing CSS attributes (because no nesting is possible in CSS),
|
// Ideally we'd only block JS attributes while allowing CSS attributes (because no nesting is possible in CSS),
|
||||||
// but Prettier's `!options.parentParser` check is all-or-nothing.
|
// but Prettier's `!options.parentParser` check is all-or-nothing.
|
||||||
formatOptionsHiddenDefaults.parentParser = null;
|
formatOptionsHiddenDefaults.parentParser = null;
|
||||||
// For html(angular)-in-js: Capture `htmlHasMultipleRootElements` from the HTML AST root during `__debug.printToDoc()`.
|
// For angular-in-js: Capture `htmlHasMultipleRootElements` from the HTML AST root during `__debug.printToDoc()`.
|
||||||
// This is used to decide whether to wrap content with `indent`.
|
// This is used to decide whether to wrap content with `indent`.
|
||||||
// Without this, we'd need either:
|
// Without this, we'd need either:
|
||||||
// - double parse AST
|
// - double parse AST
|
||||||
@@ -129,35 +129,6 @@ function hasTsxScriptBlock(sourceText: string): boolean {
|
|||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
export type FormatEmbeddedCodeParam = {
|
|
||||||
code: string;
|
|
||||||
options: Options;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format non-js code snippets into formatted string.
|
|
||||||
* Used for formatting code fences within JSDoc.
|
|
||||||
*
|
|
||||||
* @returns Formatted code snippet
|
|
||||||
*/
|
|
||||||
export async function formatEmbeddedCode({
|
|
||||||
code,
|
|
||||||
options,
|
|
||||||
}: FormatEmbeddedCodeParam): Promise<string> {
|
|
||||||
const prettier = CACHES.prettier ?? (await loadPrettier());
|
|
||||||
|
|
||||||
// Enable Tailwind CSS plugin for embedded code (e.g., html`...` in JS) if needed
|
|
||||||
if ("_useTailwindPlugin" in options) await setupTailwindPlugin(options);
|
|
||||||
|
|
||||||
// NOTE: This will throw if:
|
|
||||||
// - Specified parser is not available
|
|
||||||
// - Or, code has syntax errors
|
|
||||||
// In such cases, Rust side will fallback to original code
|
|
||||||
return prettier.format(code, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---
|
|
||||||
|
|
||||||
export type FormatEmbeddedDocParam = {
|
export type FormatEmbeddedDocParam = {
|
||||||
code: string;
|
code: string;
|
||||||
options: Options;
|
options: Options;
|
||||||
@@ -180,7 +151,7 @@ export async function formatEmbeddedDoc({
|
|||||||
if ("_useSveltePlugin" in options) await setupSveltePlugin(options);
|
if ("_useSveltePlugin" in options) await setupSveltePlugin(options);
|
||||||
// Add `astro` parser for ` ```astro ` code blocks in Markdown
|
// Add `astro` parser for ` ```astro ` code blocks in Markdown
|
||||||
if ("_useAstroPlugin" in options) await setupAstroPlugin(options);
|
if ("_useAstroPlugin" in options) await setupAstroPlugin(options);
|
||||||
// Enable Tailwind CSS plugin for embedded code (e.g., html`...` in JS) if needed
|
// Enable Tailwind CSS plugin for embedded code (e.g., Angular `@Component({ template })` in JS) if needed
|
||||||
if ("_useTailwindPlugin" in options) await setupTailwindPlugin(options);
|
if ("_useTailwindPlugin" in options) await setupTailwindPlugin(options);
|
||||||
|
|
||||||
const metadata: Record<string, unknown> = {};
|
const metadata: Record<string, unknown> = {};
|
||||||
@@ -189,8 +160,8 @@ export async function formatEmbeddedDoc({
|
|||||||
// see the comment in `loadPrettier()` for rationale. Any truthy value works
|
// see the comment in `loadPrettier()` for rationale. Any truthy value works
|
||||||
options.parentParser = "OXFMT";
|
options.parentParser = "OXFMT";
|
||||||
|
|
||||||
// html(angular)-in-js specific options
|
// angular-in-js specific options
|
||||||
if (options.parser === "html" || options.parser === "angular") {
|
if (options.parser === "angular") {
|
||||||
// https://github.com/prettier/prettier/blob/90983f40dce5e20beea4e5618b5e0426a6a7f4f0/src/language-js/embed/html.js#L42-L44
|
// https://github.com/prettier/prettier/blob/90983f40dce5e20beea4e5618b5e0426a6a7f4f0/src/language-js/embed/html.js#L42-L44
|
||||||
options.__onHtmlRoot = (root: { children?: unknown[] }) =>
|
options.__onHtmlRoot = (root: { children?: unknown[] }) =>
|
||||||
(metadata.htmlHasMultipleRootElements = (root.children?.length ?? 0) > 1);
|
(metadata.htmlHasMultipleRootElements = (root.children?.length ?? 0) > 1);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* Prettier plugin that uses `oxc_formatter` for (j|t)s-in-xxx part.
|
* Prettier plugin that uses `oxc_formatter` for (j|t)s-in-xxx part.
|
||||||
*
|
*
|
||||||
* When Prettier formats Vue/HTML (which can embed JS/TS code inside) files,
|
* When Prettier formats Vue/Angular (which can embed JS/TS code inside) files,
|
||||||
* it calls the `embed()` function for each block.
|
* it calls the `embed()` function for each block.
|
||||||
*
|
*
|
||||||
* By default, it uses the `babel` or `typescript` parser and `estree` printer.
|
* By default, it uses the `babel` or `typescript` parser and `estree` printer.
|
||||||
|
|||||||
@@ -5,9 +5,8 @@ use serde_json::Value;
|
|||||||
use oxc_napi::OxcError;
|
use oxc_napi::OxcError;
|
||||||
|
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
ExternalServices, FormatResult, JsFormatEmbeddedCb, JsFormatEmbeddedDocCb, JsFormatFileCb,
|
ExternalServices, FormatResult, JsFormatEmbeddedDocCb, JsFormatFileCb, JsSortTailwindClassesCb,
|
||||||
JsSortTailwindClassesCb, ResolveOutcome, SourceFormatter, classify_file, resolve_for_api,
|
ResolveOutcome, SourceFormatter, classify_file, resolve_for_api, utils,
|
||||||
utils,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ApiFormatResult {
|
pub struct ApiFormatResult {
|
||||||
@@ -27,7 +26,6 @@ pub fn run(
|
|||||||
source_text: String,
|
source_text: String,
|
||||||
options: Option<Value>,
|
options: Option<Value>,
|
||||||
format_file_cb: JsFormatFileCb,
|
format_file_cb: JsFormatFileCb,
|
||||||
format_embedded_cb: JsFormatEmbeddedCb,
|
|
||||||
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
||||||
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
|
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
|
||||||
) -> ApiFormatResult {
|
) -> ApiFormatResult {
|
||||||
@@ -45,12 +43,8 @@ pub fn run(
|
|||||||
let cwd = env::current_dir().expect("Failed to get current working directory");
|
let cwd = env::current_dir().expect("Failed to get current working directory");
|
||||||
let num_of_threads = 1;
|
let num_of_threads = 1;
|
||||||
|
|
||||||
let external_services = ExternalServices::new(
|
let external_services =
|
||||||
format_file_cb,
|
ExternalServices::new(format_file_cb, format_embedded_doc_cb, sort_tailwind_classes_cb);
|
||||||
format_embedded_cb,
|
|
||||||
format_embedded_doc_cb,
|
|
||||||
sort_tailwind_classes_cb,
|
|
||||||
);
|
|
||||||
let _cleanup = external_services.cleanup_guard();
|
let _cleanup = external_services.cleanup_guard();
|
||||||
|
|
||||||
let filepath = utils::normalize_relative_path(&cwd, Path::new(filename));
|
let filepath = utils::normalize_relative_path(&cwd, Path::new(filename));
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ use oxc_span::SourceType;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::{
|
core::{
|
||||||
ExternalServices, JsFormatEmbeddedCb, JsFormatEmbeddedDocCb, JsFormatFileCb,
|
ExternalServices, JsFormatEmbeddedDocCb, JsFormatFileCb, JsSortTailwindClassesCb,
|
||||||
JsSortTailwindClassesCb,
|
|
||||||
embed::{self, dispatcher::ResolvedDispatchConfig},
|
embed::{self, dispatcher::ResolvedDispatchConfig},
|
||||||
options::{ValidatedOptions, to_oxc_formatter, validate},
|
options::{ValidatedOptions, to_oxc_formatter, validate},
|
||||||
oxfmtrc::FormatConfig,
|
oxfmtrc::FormatConfig,
|
||||||
@@ -50,7 +49,6 @@ pub fn run(
|
|||||||
oxfmt_plugin_options_json: &str,
|
oxfmt_plugin_options_json: &str,
|
||||||
parent_context: &str,
|
parent_context: &str,
|
||||||
format_file_cb: JsFormatFileCb,
|
format_file_cb: JsFormatFileCb,
|
||||||
format_embedded_cb: JsFormatEmbeddedCb,
|
|
||||||
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
||||||
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
|
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
@@ -88,7 +86,6 @@ pub fn run(
|
|||||||
source_text,
|
source_text,
|
||||||
oxfmt_plugin_options_json,
|
oxfmt_plugin_options_json,
|
||||||
format_file_cb,
|
format_file_cb,
|
||||||
format_embedded_cb,
|
|
||||||
format_embedded_doc_cb,
|
format_embedded_doc_cb,
|
||||||
sort_tailwind_classes_cb,
|
sort_tailwind_classes_cb,
|
||||||
)?
|
)?
|
||||||
@@ -116,7 +113,6 @@ fn run_full(
|
|||||||
source_text: &str,
|
source_text: &str,
|
||||||
oxfmt_plugin_options_json: &str,
|
oxfmt_plugin_options_json: &str,
|
||||||
format_file_cb: JsFormatFileCb,
|
format_file_cb: JsFormatFileCb,
|
||||||
format_embedded_cb: JsFormatEmbeddedCb,
|
|
||||||
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
||||||
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
|
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
|
||||||
) -> Option<Value> {
|
) -> Option<Value> {
|
||||||
@@ -124,12 +120,8 @@ fn run_full(
|
|||||||
// so no `cwd` is threaded through here.
|
// so no `cwd` is threaded through here.
|
||||||
let (config, validated, parent_filepath) = parse_payload(oxfmt_plugin_options_json);
|
let (config, validated, parent_filepath) = parse_payload(oxfmt_plugin_options_json);
|
||||||
|
|
||||||
let external_services = ExternalServices::new(
|
let external_services =
|
||||||
format_file_cb,
|
ExternalServices::new(format_file_cb, format_embedded_doc_cb, sort_tailwind_classes_cb);
|
||||||
format_embedded_cb,
|
|
||||||
format_embedded_doc_cb,
|
|
||||||
sort_tailwind_classes_cb,
|
|
||||||
);
|
|
||||||
let _cleanup = external_services.cleanup_guard();
|
let _cleanup = external_services.cleanup_guard();
|
||||||
|
|
||||||
// Per-language options (and the Prettier options JSON with the Tailwind payload)
|
// Per-language options (and the Prettier options JSON with the Tailwind payload)
|
||||||
|
|||||||
@@ -549,14 +549,14 @@ mod tests_slow_path_validation {
|
|||||||
let resolver = resolver_from_json(serde_json::json!({
|
let resolver = resolver_from_json(serde_json::json!({
|
||||||
"printWidth": 80,
|
"printWidth": 80,
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{ "files": ["*.html"], "options": { "printWidth": 1000 } }
|
{ "files": ["*.vue"], "options": { "printWidth": 1000 } }
|
||||||
]
|
]
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Slow path triggers because the override matches.
|
// Slow path triggers because the override matches.
|
||||||
let path = Path::new("index.html");
|
let path = Path::new("App.vue");
|
||||||
let err =
|
let err =
|
||||||
resolver.resolve(path, FormatStrategy::Prettier(PrettierLanguage::Html)).unwrap_err();
|
resolver.resolve(path, FormatStrategy::Prettier(PrettierLanguage::Vue)).unwrap_err();
|
||||||
assert!(err.contains("printWidth"), "expected printWidth validation error, got: {err}");
|
assert!(err.contains("printWidth"), "expected printWidth validation error, got: {err}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,19 @@ use std::sync::{Arc, OnceLock};
|
|||||||
|
|
||||||
use tracing::{debug, debug_span};
|
use tracing::{debug, debug_span};
|
||||||
|
|
||||||
use oxc_formatter::{CssInJsTemplate, JsEmbeddedIn, JsFormatOptions, MarkdownInJsTemplate};
|
use oxc_formatter::{
|
||||||
|
CssInJsTemplate, HtmlInJsTemplate, JsEmbeddedIn, JsFormatOptions, MarkdownInJsTemplate,
|
||||||
|
};
|
||||||
use oxc_formatter_core::{
|
use oxc_formatter_core::{
|
||||||
DispatchRequest, DispatchResponse, EmbeddedIr, FormatDispatcher, FormatSession,
|
DispatchRequest, DispatchResponse, EmbeddedIr, FormatDispatcher, FormatSession,
|
||||||
};
|
};
|
||||||
use oxc_formatter_core::{FormatOptions, PrinterOptions};
|
use oxc_formatter_core::{FormatOptions, PrinterOptions};
|
||||||
use oxc_formatter_css::{CssFormatOptions, CssVariant};
|
use oxc_formatter_css::{CssEmbeddedIn, CssFormatOptions, CssVariant};
|
||||||
use oxc_formatter_graphql::GraphqlFormatOptions;
|
use oxc_formatter_graphql::GraphqlFormatOptions;
|
||||||
|
use oxc_formatter_htmlish::{
|
||||||
|
CssInHtmlStyleAttribute, HtmlishEmbeddedIn, HtmlishFormatOptions, HtmlishVariant,
|
||||||
|
JsInHtmlEventHandler,
|
||||||
|
};
|
||||||
use oxc_formatter_json::{JsonFormatOptions, JsonVariant};
|
use oxc_formatter_json::{JsonFormatOptions, JsonVariant};
|
||||||
use oxc_formatter_markdown::{MarkdownFormatOptions, XxxInMarkdownCodeBlock};
|
use oxc_formatter_markdown::{MarkdownFormatOptions, XxxInMarkdownCodeBlock};
|
||||||
use oxc_formatter_toml::TomlFormatOptions;
|
use oxc_formatter_toml::TomlFormatOptions;
|
||||||
@@ -24,8 +30,8 @@ use crate::core::{
|
|||||||
language::{Route, route_embedded},
|
language::{Route, route_embedded},
|
||||||
options::{
|
options::{
|
||||||
ValidatedOptions, to_oxc_formatter, to_oxc_formatter_css, to_oxc_formatter_graphql,
|
ValidatedOptions, to_oxc_formatter, to_oxc_formatter_css, to_oxc_formatter_graphql,
|
||||||
to_oxc_formatter_json, to_oxc_formatter_markdown, to_oxc_formatter_toml,
|
to_oxc_formatter_htmlish, to_oxc_formatter_json, to_oxc_formatter_markdown,
|
||||||
to_oxc_formatter_yaml,
|
to_oxc_formatter_toml, to_oxc_formatter_yaml,
|
||||||
},
|
},
|
||||||
oxfmtrc::FormatConfig,
|
oxfmtrc::FormatConfig,
|
||||||
support::{NativeLanguage, PrettierLanguage},
|
support::{NativeLanguage, PrettierLanguage},
|
||||||
@@ -56,13 +62,14 @@ pub struct ResolvedDispatchConfig {
|
|||||||
json: [OnceLock<JsonFormatOptions>; 4],
|
json: [OnceLock<JsonFormatOptions>; 4],
|
||||||
markdown: OnceLock<MarkdownFormatOptions>,
|
markdown: OnceLock<MarkdownFormatOptions>,
|
||||||
toml: OnceLock<TomlFormatOptions>,
|
toml: OnceLock<TomlFormatOptions>,
|
||||||
|
htmlish: OnceLock<HtmlishFormatOptions>,
|
||||||
/// The options handed to Prettier; see [`PrettierOptions`].
|
/// The options handed to Prettier; see [`PrettierOptions`].
|
||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
prettier: PrettierOptions,
|
prettier: PrettierOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The lazily-built options JSON handed to Prettier (+ plugins),
|
/// The lazily-built options JSON handed to Prettier (+ plugins),
|
||||||
/// consumed by the Doc→IR / string paths and the Tailwind sorter.
|
/// consumed by the Doc→IR path and the Tailwind sorter.
|
||||||
/// `path` is an ingredient, not a sibling datum: it becomes the JSON's `filepath` at last
|
/// `path` is an ingredient, not a sibling datum: it becomes the JSON's `filepath` at last
|
||||||
/// (see [`crate::core::options::build_prettier_options`]).
|
/// (see [`crate::core::options::build_prettier_options`]).
|
||||||
///
|
///
|
||||||
@@ -90,6 +97,7 @@ impl ResolvedDispatchConfig {
|
|||||||
json: [OnceLock::new(), OnceLock::new(), OnceLock::new(), OnceLock::new()],
|
json: [OnceLock::new(), OnceLock::new(), OnceLock::new(), OnceLock::new()],
|
||||||
markdown: OnceLock::new(),
|
markdown: OnceLock::new(),
|
||||||
toml: OnceLock::new(),
|
toml: OnceLock::new(),
|
||||||
|
htmlish: OnceLock::new(),
|
||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
prettier: PrettierOptions::default(),
|
prettier: PrettierOptions::default(),
|
||||||
}
|
}
|
||||||
@@ -177,6 +185,13 @@ impl ResolvedDispatchConfig {
|
|||||||
*self.toml.get_or_init(|| to_oxc_formatter_toml(&self.config, self.validated.core))
|
*self.toml.get_or_init(|| to_oxc_formatter_toml(&self.config, self.validated.core))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn htmlish_options(&self, variant: HtmlishVariant) -> HtmlishFormatOptions {
|
||||||
|
let options = self
|
||||||
|
.htmlish
|
||||||
|
.get_or_init(|| to_oxc_formatter_htmlish(&self.config, self.validated.core));
|
||||||
|
HtmlishFormatOptions { variant, ..options.clone() }
|
||||||
|
}
|
||||||
|
|
||||||
/// Printer options from the shared resolved core bundle;
|
/// Printer options from the shared resolved core bundle;
|
||||||
/// the fence adapter ([`super::jsdoc_fence`]) derives its per-fence options from these
|
/// the fence adapter ([`super::jsdoc_fence`]) derives its per-fence options from these
|
||||||
/// (width overridden to the fence's effective width).
|
/// (width overridden to the fence's effective width).
|
||||||
@@ -227,7 +242,7 @@ impl ResolvedDispatchConfig {
|
|||||||
|
|
||||||
/// Fallback invoked for [`Route::Prettier`] languages.
|
/// Fallback invoked for [`Route::Prettier`] languages.
|
||||||
/// Same shape as `FormatDispatcher`, plus the routed language
|
/// Same shape as `FormatDispatcher`, plus the routed language
|
||||||
/// (the Doc path consumes `parent_context` only for html(angular)-in-js's [`oxc_formatter::HtmlInJsTemplate`], not `input_kind`;
|
/// (the Doc path consumes `parent_context` only for angular-in-js's [`HtmlInJsTemplate`], not `input_kind`;
|
||||||
/// re-examine if it ever serves envelope-bearing inputs).
|
/// re-examine if it ever serves envelope-bearing inputs).
|
||||||
///
|
///
|
||||||
/// Assembled only in napi builds ([`super::prettier_doc`]);
|
/// Assembled only in napi builds ([`super::prettier_doc`]);
|
||||||
@@ -257,7 +272,12 @@ pub fn build_dispatcher(
|
|||||||
NativeLanguage::Js(source_type) => {
|
NativeLanguage::Js(source_type) => {
|
||||||
let embedded_in = request
|
let embedded_in = request
|
||||||
.parent_context_as::<XxxInMarkdownCodeBlock>()
|
.parent_context_as::<XxxInMarkdownCodeBlock>()
|
||||||
.map(|_| JsEmbeddedIn::MarkdownCodeBlock);
|
.map(|_| JsEmbeddedIn::MarkdownCodeBlock)
|
||||||
|
.or_else(|| {
|
||||||
|
request
|
||||||
|
.parent_context_as::<JsInHtmlEventHandler>()
|
||||||
|
.map(|_| JsEmbeddedIn::HtmlEventHandler)
|
||||||
|
});
|
||||||
oxc_formatter::format_to_ir(
|
oxc_formatter::format_to_ir(
|
||||||
session,
|
session,
|
||||||
text,
|
text,
|
||||||
@@ -274,17 +294,21 @@ pub fn build_dispatcher(
|
|||||||
NativeLanguage::Css(variant) => {
|
NativeLanguage::Css(variant) => {
|
||||||
// css-in-js (typed `CssInJsTemplate` context) is always parsed as SCSS with `${}` placeholder markers.
|
// css-in-js (typed `CssInJsTemplate` context) is always parsed as SCSS with `${}` placeholder markers.
|
||||||
// Any other caller gets the strict standalone grammar with the fence/request language's variant.
|
// Any other caller gets the strict standalone grammar with the fence/request language's variant.
|
||||||
let (variant, template_placeholders) =
|
let (variant, embedded_in) = if request
|
||||||
if request.parent_context_as::<CssInJsTemplate>().is_some() {
|
.parent_context_as::<CssInJsTemplate>()
|
||||||
(CssVariant::Scss, true)
|
.is_some()
|
||||||
} else {
|
{
|
||||||
(variant, false)
|
(CssVariant::Scss, Some(CssEmbeddedIn::CssInJsTemplate))
|
||||||
};
|
} else if request.parent_context_as::<CssInHtmlStyleAttribute>().is_some() {
|
||||||
|
(variant, Some(CssEmbeddedIn::HtmlStyleAttribute))
|
||||||
|
} else {
|
||||||
|
(variant, None)
|
||||||
|
};
|
||||||
oxc_formatter_css::format_to_ir(
|
oxc_formatter_css::format_to_ir(
|
||||||
session,
|
session,
|
||||||
text,
|
text,
|
||||||
dispatch_config.css_options(variant),
|
dispatch_config.css_options(variant),
|
||||||
template_placeholders,
|
embedded_in,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
NativeLanguage::Yaml => oxc_formatter_yaml::format_to_ir(
|
NativeLanguage::Yaml => oxc_formatter_yaml::format_to_ir(
|
||||||
@@ -316,6 +340,28 @@ pub fn build_dispatcher(
|
|||||||
text,
|
text,
|
||||||
dispatch_config.toml_options(),
|
dispatch_config.toml_options(),
|
||||||
),
|
),
|
||||||
|
NativeLanguage::Html(variant) => {
|
||||||
|
// Embedded in a JS template literal or a Markdown code block;
|
||||||
|
// `None` is a part formatted on its own (a JSDoc code block).
|
||||||
|
// An HTML parent added later needs its marker here, or reads as standalone.
|
||||||
|
let embedded_in = request
|
||||||
|
.parent_context_as::<HtmlInJsTemplate>()
|
||||||
|
.map(|c| HtmlishEmbeddedIn {
|
||||||
|
indent_multiple_roots: c.indent_multiple_roots,
|
||||||
|
placeholder: Some(c.placeholder),
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
request
|
||||||
|
.parent_context_as::<XxxInMarkdownCodeBlock>()
|
||||||
|
.map(|_| HtmlishEmbeddedIn::default())
|
||||||
|
});
|
||||||
|
oxc_formatter_htmlish::format_to_ir(
|
||||||
|
session,
|
||||||
|
text,
|
||||||
|
dispatch_config.htmlish_options(variant),
|
||||||
|
embedded_in,
|
||||||
|
)
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -418,6 +464,7 @@ mod tests {
|
|||||||
"markdown",
|
"markdown",
|
||||||
"md",
|
"md",
|
||||||
"toml",
|
"toml",
|
||||||
|
"html",
|
||||||
] {
|
] {
|
||||||
let text = match language {
|
let text = match language {
|
||||||
"graphql" | "gql" => "{ a }",
|
"graphql" | "gql" => "{ a }",
|
||||||
@@ -425,6 +472,7 @@ mod tests {
|
|||||||
"yaml" | "yml" => "a: 1",
|
"yaml" | "yml" => "a: 1",
|
||||||
"json" | "jsonc" | "json5" => "{ \"a\": 1 }",
|
"json" | "jsonc" | "json5" => "{ \"a\": 1 }",
|
||||||
"markdown" | "md" => "# a",
|
"markdown" | "md" => "# a",
|
||||||
|
"html" => "<p> a </p>",
|
||||||
_ => "a = 1",
|
_ => "a = 1",
|
||||||
};
|
};
|
||||||
let response = session.dispatch(DispatchRequest {
|
let response = session.dispatch(DispatchRequest {
|
||||||
@@ -476,7 +524,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
for (language, text) in [("html", "<div></div>"), ("ini", "a = 1")] {
|
for (language, text) in [("vue", "<template></template>"), ("ini", "a = 1")] {
|
||||||
let response = session.dispatch(DispatchRequest {
|
let response = session.dispatch(DispatchRequest {
|
||||||
language,
|
language,
|
||||||
text,
|
text,
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
//! Native-fence string adapter:
|
//! Native-fence string adapter:
|
||||||
//! a JSDoc fenced code block whose language has a Rust formatter branch
|
//! a JSDoc fenced code block whose language has a Rust formatter branch
|
||||||
//! formats through the dispatch registry, in EVERY build.
|
//! formats through the dispatch registry, in EVERY build; any other fence stays verbatim.
|
||||||
//!
|
//!
|
||||||
//! This is the build-independent half of the string-out channel;
|
//! This is the whole string-out channel; [`super::services`] installs it as the session's string embedder.
|
||||||
//! the profiles in [`super::services`] wire it into the session's string embedder
|
|
||||||
//! (the napi one via [`super::prettier_string`], which routes native fences here
|
|
||||||
//! before its Prettier string paths; the pure one directly, non-native fences stay verbatim).
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -14,11 +11,31 @@ use tracing::debug_span;
|
|||||||
use oxc_allocator::Allocator;
|
use oxc_allocator::Allocator;
|
||||||
use oxc_formatter_core::{
|
use oxc_formatter_core::{
|
||||||
DispatchRequest, FormatDispatcher, FormatSession, InputKind, PrintWidth, SessionServices,
|
DispatchRequest, FormatDispatcher, FormatSession, InputKind, PrintWidth, SessionServices,
|
||||||
TailwindSorter,
|
StringEmbedder, TailwindSorter,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::dispatcher::ResolvedDispatchConfig;
|
use super::dispatcher::ResolvedDispatchConfig;
|
||||||
|
|
||||||
|
/// The session's string embedder: [`format_native_fence`] over `fence_dispatcher`,
|
||||||
|
/// a fallback-less registry (a native fence never falls back to Prettier).
|
||||||
|
/// `sort_tailwind` is the root's pre-bound sorter, for the `@apply` classes a CSS fence collects.
|
||||||
|
pub fn build_string_embedder(
|
||||||
|
fence_dispatcher: FormatDispatcher,
|
||||||
|
dispatch_config: Arc<ResolvedDispatchConfig>,
|
||||||
|
sort_tailwind: Option<TailwindSorter>,
|
||||||
|
) -> StringEmbedder {
|
||||||
|
Arc::new(move |language: &str, code: &str, print_width: usize| {
|
||||||
|
format_native_fence(
|
||||||
|
language,
|
||||||
|
code,
|
||||||
|
print_width,
|
||||||
|
&fence_dispatcher,
|
||||||
|
&dispatch_config,
|
||||||
|
sort_tailwind.as_ref(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Format a JSDoc fenced code block through the native dispatch registry:
|
/// Format a JSDoc fenced code block through the native dispatch registry:
|
||||||
/// a string-in/string-out adapter over `FormatSession::dispatch_to_string`.
|
/// a string-in/string-out adapter over `FormatSession::dispatch_to_string`.
|
||||||
///
|
///
|
||||||
@@ -28,9 +45,9 @@ use super::dispatcher::ResolvedDispatchConfig;
|
|||||||
/// - `Err` keeps the fence verbatim, covering both the deliberate keep
|
/// - `Err` keeps the fence verbatim, covering both the deliberate keep
|
||||||
/// (`Ok(None)`; a failed native language never re-routes to Prettier) and operational errors
|
/// (`Ok(None)`; a failed native language never re-routes to Prettier) and operational errors
|
||||||
/// - The session-less `StringEmbedder` contract forces a fresh root session per fence,
|
/// - The session-less `StringEmbedder` contract forces a fresh root session per fence,
|
||||||
/// so `dispatch_depth` resets at this string boundary (inert today: no native fence language re-dispatches).
|
/// so `dispatch_depth` resets at this string boundary (inert today: the fence prints to a string, its placeholders never meet the parent's).
|
||||||
/// Threading the parent session through the callback is the eventual fix.
|
/// Threading the parent session through the callback is the eventual fix.
|
||||||
pub fn format_native_fence(
|
fn format_native_fence(
|
||||||
language: &str,
|
language: &str,
|
||||||
code: &str,
|
code: &str,
|
||||||
print_width: usize,
|
print_width: usize,
|
||||||
|
|||||||
@@ -9,9 +9,8 @@
|
|||||||
//! `ResolvedDispatchConfig` (lazy per-language options) + `build_dispatcher` with a Rust branch per `NativeLanguage`;
|
//! `ResolvedDispatchConfig` (lazy per-language options) + `build_dispatcher` with a Rust branch per `NativeLanguage`;
|
||||||
//! IR integrates into the parent's arena / `GroupId` space
|
//! IR integrates into the parent's arena / `GroupId` space
|
||||||
//! - [`services`] (every build): the root `SessionServices` assembly (`for_root`, one definition per build)
|
//! - [`services`] (every build): the root `SessionServices` assembly (`for_root`, one definition per build)
|
||||||
//! - [`jsdoc_fence`] (every build): the JSDoc native-fence string adapter over the registry
|
//! - [`jsdoc_fence`] (every build): the string-out channel, the JSDoc native-fence adapter over the registry
|
||||||
//! - [`prettier_doc`] (napi only): Prettier Doc→IR path for the `Route::Prettier` set
|
//! - [`prettier_doc`] (napi only): Prettier Doc→IR path for the `Route::Prettier` set
|
||||||
//! - [`prettier_string`] (napi only): the Prettier string paths of the string-out channel (html JSDoc fences; results re-embed line-by-line)
|
|
||||||
|
|
||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -23,24 +22,15 @@ pub mod dispatcher;
|
|||||||
pub mod jsdoc_fence;
|
pub mod jsdoc_fence;
|
||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
pub mod prettier_doc;
|
pub mod prettier_doc;
|
||||||
#[cfg(feature = "napi")]
|
|
||||||
pub mod prettier_string;
|
|
||||||
pub mod services;
|
pub mod services;
|
||||||
|
|
||||||
// --- Cross-module callback types ---
|
// --- Cross-module callback types ---
|
||||||
//
|
//
|
||||||
// These describe the shape of the napi-wrapped callbacks the orchestration builders consume.
|
// These describe the shape of the napi-wrapped callbacks the orchestration builders consume.
|
||||||
// NOTE: They live here (not in `external_services`),
|
// NOTE: They live here (not in `external_services`),
|
||||||
// so the `prettier_doc` / `prettier_string` factories stay independent of the napi boundary.
|
// so the `prettier_doc` factory stays independent of the napi boundary.
|
||||||
// `external_services` is the producer of these types via its `wrap_*` functions, and orchestration is the consumer.
|
// `external_services` is the producer of these types via its `wrap_*` functions, and orchestration is the consumer.
|
||||||
|
|
||||||
/// Callback function type for formatting embedded code with config.
|
|
||||||
/// Takes (options, code) and returns formatted code or an error.
|
|
||||||
/// The `options` Value is owned and includes `parser` set by the caller.
|
|
||||||
#[cfg(feature = "napi")]
|
|
||||||
pub type FormatEmbeddedWithConfigCallback =
|
|
||||||
Arc<dyn Fn(Value, &str) -> Result<String, String> + Send + Sync>;
|
|
||||||
|
|
||||||
/// Callback function type for formatting embedded code via the Doc IR path.
|
/// Callback function type for formatting embedded code via the Doc IR path.
|
||||||
/// Takes (options, text) and returns a Doc JSON string or an error.
|
/// Takes (options, text) and returns a Doc JSON string or an error.
|
||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! Sends the text to JS `printToDoc()`, then converts the returned Doc JSON into formatter IR
|
//! Sends the text to JS `printToDoc()`, then converts the returned Doc JSON into formatter IR
|
||||||
//! that integrates into the parent's arena / `GroupId` space.
|
//! that integrates into the parent's arena / `GroupId` space.
|
||||||
//! Fills the dispatcher's optional [`PrettierDocFallback`] slot;
|
//! Fills the dispatcher's optional [`PrettierDocFallback`] slot;
|
||||||
//! the string twin is [`super::prettier_string`].
|
//! the JSDoc fences' string channel is native only ([`super::jsdoc_fence`]).
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ pub fn build_prettier_fallback(
|
|||||||
session.group_id_builder(),
|
session.group_id_builder(),
|
||||||
)?;
|
)?;
|
||||||
from_prettier_doc::postprocess(&mut ir, allocator);
|
from_prettier_doc::postprocess(&mut ir, allocator);
|
||||||
// html(angular)-in-js: indent the content when more than one node prints at the root
|
// angular-in-js: what the native HTML formatter does on the same request
|
||||||
let indent = request
|
let indent = request
|
||||||
.parent_context_as::<HtmlInJsTemplate>()
|
.parent_context_as::<HtmlInJsTemplate>()
|
||||||
.is_some_and(|c| c.indent_multiple_roots)
|
.is_some_and(|c| c.indent_multiple_roots)
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
//! The napi string embedder: fence routing plus the Prettier string paths of the string-out channel.
|
|
||||||
//!
|
|
||||||
//! JSDoc fenced code blocks (` ```css `, ` ```yaml `, …) reach this channel through the session's `StringEmbedder` contract.
|
|
||||||
//!
|
|
||||||
//! Fence routing follows ONE rule, the shared routing table ([`route_embedded`]):
|
|
||||||
//! a `Route::Native` language formats through the dispatcher via [`super::jsdoc_fence::format_native_fence`] (the build-independent adapter);
|
|
||||||
//! the `Route::Prettier` set stays on the Prettier string path
|
|
||||||
//! (their Doc→IR conversion has unrepresentable cases, so forcing them through the dispatcher would regress to verbatim;
|
|
||||||
//! the wall falls with the HTML Rust port).
|
|
||||||
//!
|
|
||||||
//! Unlike the [`super::dispatcher`] contract (`PreserveOriginal` vs `Err`),
|
|
||||||
//! this channel has one failure meaning: `Err` keeps the input verbatim.
|
|
||||||
//! A failed native fence is never re-routed to Prettier, and a JSDoc fence surfaces no diagnostics (it lives inside a comment).
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use tracing::{debug, debug_span};
|
|
||||||
|
|
||||||
use oxc_formatter_core::{StringEmbedder, TailwindSorter};
|
|
||||||
|
|
||||||
use crate::core::{
|
|
||||||
embed::{FormatEmbeddedWithConfigCallback, dispatcher, jsdoc_fence},
|
|
||||||
language::{Route, route_embedded},
|
|
||||||
options::inject_string_embed_options,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Build the napi build's string embedder installed on the session.
|
|
||||||
///
|
|
||||||
/// Dispatches by language identifier: the native registry when available, otherwise Prettier via `format_embedded`.
|
|
||||||
/// The JSDoc fenced consumer sends only its embedded set (`EMBEDDED_FENCE_LANGUAGES` in `oxc_formatter`).
|
|
||||||
///
|
|
||||||
/// `sort_tailwind` is the SAME pre-bound sorter as the session's Tailwind service
|
|
||||||
/// (options JSON already applied by `services::for_root`), for the `@apply` classes a CSS fence collects.
|
|
||||||
pub fn build_string_embedder(
|
|
||||||
format_embedded: FormatEmbeddedWithConfigCallback,
|
|
||||||
sort_tailwind: Option<TailwindSorter>,
|
|
||||||
dispatch_config: Arc<dispatcher::ResolvedDispatchConfig>,
|
|
||||||
) -> StringEmbedder {
|
|
||||||
// Fence dispatchers never take the Prettier fallback (native fences never fall back),
|
|
||||||
// so one is invariant across the callback's lifetime: build it once, not per fence.
|
|
||||||
let fence_dispatcher = dispatcher::build_dispatcher(Arc::clone(&dispatch_config), None);
|
|
||||||
Arc::new(move |language: &str, code: &str, print_width: usize| {
|
|
||||||
let prettier_language = match route_embedded(language) {
|
|
||||||
// Native branch (JSDoc fenced code blocks): through the dispatcher, never Prettier.
|
|
||||||
Route::Native(_) => {
|
|
||||||
return jsdoc_fence::format_native_fence(
|
|
||||||
language,
|
|
||||||
code,
|
|
||||||
print_width,
|
|
||||||
&fence_dispatcher,
|
|
||||||
&dispatch_config,
|
|
||||||
sort_tailwind.as_ref(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Route::Prettier(prettier_language) => prettier_language,
|
|
||||||
// NOTE: Do not return `Ok(original)` here.
|
|
||||||
// We need to keep unsupported content as-is.
|
|
||||||
Route::Unsupported => return Err(format!("Unsupported language: {language}")),
|
|
||||||
};
|
|
||||||
let parser_name = prettier_language.parser();
|
|
||||||
debug_span!("oxfmt::external::format_embedded", parser = parser_name).in_scope(|| {
|
|
||||||
let Some(mut options) = dispatch_config.prettier_options_for(prettier_language) else {
|
|
||||||
return Err(format!("No plugin enabled for parser: {parser_name}"));
|
|
||||||
};
|
|
||||||
// The same effective width and line ending the native branch prints with
|
|
||||||
inject_string_embed_options(&mut options, print_width);
|
|
||||||
(format_embedded)(options, code)
|
|
||||||
.map(|mut code| {
|
|
||||||
// Remove trailing newline added by Prettier without allocation.
|
|
||||||
// For embedded code, we never want trailing newlines, regardless of options.
|
|
||||||
let trimmed_len = code.trim_end().len();
|
|
||||||
code.truncate(trimmed_len);
|
|
||||||
code
|
|
||||||
})
|
|
||||||
.inspect_err(|err| {
|
|
||||||
debug!("Failed to format embedded code for parser '{parser_name}': {err}");
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -9,8 +9,6 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use oxc_formatter_core::SessionServices;
|
use oxc_formatter_core::SessionServices;
|
||||||
#[cfg(not(feature = "napi"))]
|
|
||||||
use oxc_formatter_core::StringEmbedder;
|
|
||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
use oxc_formatter_core::TailwindSorter;
|
use oxc_formatter_core::TailwindSorter;
|
||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
@@ -32,18 +30,11 @@ pub fn for_root(dispatch_config: &Arc<ResolvedDispatchConfig>) -> SessionService
|
|||||||
// so the root's doubles as the fence adapter's.
|
// so the root's doubles as the fence adapter's.
|
||||||
let dispatcher = dispatch_config.root_dispatcher(None);
|
let dispatcher = dispatch_config.root_dispatcher(None);
|
||||||
let string_embedder = dispatcher.as_ref().map(|dispatcher| {
|
let string_embedder = dispatcher.as_ref().map(|dispatcher| {
|
||||||
let fence_dispatcher = Arc::clone(dispatcher);
|
super::jsdoc_fence::build_string_embedder(
|
||||||
let dispatch_config = Arc::clone(dispatch_config);
|
Arc::clone(dispatcher),
|
||||||
Arc::new(move |language: &str, code: &str, print_width: usize| {
|
Arc::clone(dispatch_config),
|
||||||
super::jsdoc_fence::format_native_fence(
|
None,
|
||||||
language,
|
)
|
||||||
code,
|
|
||||||
print_width,
|
|
||||||
&fence_dispatcher,
|
|
||||||
&dispatch_config,
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
}) as StringEmbedder
|
|
||||||
});
|
});
|
||||||
SessionServices { dispatcher, string_embedder, tailwind_sorter: None }
|
SessionServices { dispatcher, string_embedder, tailwind_sorter: None }
|
||||||
}
|
}
|
||||||
@@ -54,7 +45,7 @@ pub fn for_root(dispatch_config: &Arc<ResolvedDispatchConfig>) -> SessionService
|
|||||||
/// The options (including `filepath`) are captured in the closures and passed to JS on each call;
|
/// The options (including `filepath`) are captured in the closures and passed to JS on each call;
|
||||||
/// per-language options are mapped lazily at dispatch time (see [`ResolvedDispatchConfig`]).
|
/// per-language options are mapped lazily at dispatch time (see [`ResolvedDispatchConfig`]).
|
||||||
///
|
///
|
||||||
/// This is the assembly point only: closure factories live in `embed::{prettier_string, dispatcher, prettier_doc}`,
|
/// This is the assembly point only: closure factories live in `embed::{jsdoc_fence, dispatcher, prettier_doc}`,
|
||||||
/// and the napi callback `Arc`s come from the transport ([`ExternalServices`]).
|
/// and the napi callback `Arc`s come from the transport ([`ExternalServices`]).
|
||||||
///
|
///
|
||||||
/// NOTE: Tailwind data paths
|
/// NOTE: Tailwind data paths
|
||||||
@@ -75,7 +66,7 @@ pub fn for_root(dispatch_config: &Arc<ResolvedDispatchConfig>) -> SessionService
|
|||||||
/// Goes through the string embedder's fence adapter, which returns a formatted string (not parent-integrated IR),
|
/// Goes through the string embedder's fence adapter, which returns a formatted string (not parent-integrated IR),
|
||||||
/// so there is no parent index space to join:
|
/// so there is no parent index space to join:
|
||||||
/// `FormatSession::dispatch_to_string` collects the child's classes in a fresh scope and sorts it before printing.
|
/// `FormatSession::dispatch_to_string` collects the child's classes in a fresh scope and sorts it before printing.
|
||||||
/// The `prettier_string::build_string_embedder` factory receives the sorter for this case.
|
/// The `jsdoc_fence::build_string_embedder` factory receives the sorter for this case.
|
||||||
///
|
///
|
||||||
/// All four paths use the SAME `sort_tailwindcss_classes` napi callback; only the wiring differs.
|
/// All four paths use the SAME `sort_tailwindcss_classes` napi callback; only the wiring differs.
|
||||||
/// Moving sorting to the wrong layer (e.g. sorting inside embedded CSS instead of at the root) would double-sort or drop classes.
|
/// Moving sorting to the wrong layer (e.g. sorting inside embedded CSS instead of at the root) would double-sort or drop classes.
|
||||||
@@ -105,11 +96,12 @@ pub fn for_root(
|
|||||||
}) as TailwindSorter
|
}) as TailwindSorter
|
||||||
});
|
});
|
||||||
|
|
||||||
let string_embedder = needs_embedded.then(|| {
|
// Fences never take the Prettier fallback (a native fence never falls back)
|
||||||
super::prettier_string::build_string_embedder(
|
let string_embedder = dispatch_config.root_dispatcher(None).map(|fence_dispatcher| {
|
||||||
Arc::clone(&external_services.format_embedded),
|
super::jsdoc_fence::build_string_embedder(
|
||||||
sorter.clone(),
|
fence_dispatcher,
|
||||||
Arc::clone(dispatch_config),
|
Arc::clone(dispatch_config),
|
||||||
|
sorter.clone(),
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
//! [`ExternalServices`] wraps the JS callbacks as `Arc<dyn Fn>`s (private `wrap_*` helpers), serving:
|
//! [`ExternalServices`] wraps the JS callbacks as `Arc<dyn Fn>`s (private `wrap_*` helpers), serving:
|
||||||
//! - [`ExternalServices::init`]: JS-side worker-pool setup
|
//! - [`ExternalServices::init`]: JS-side worker-pool setup
|
||||||
//! - [`ExternalServices::format_file`]: Tier 3/4 whole-file delegation (`core::format`)
|
//! - [`ExternalServices::format_file`]: Tier 3/4 whole-file delegation (`core::format`)
|
||||||
//! - the embed callback `Arc`s (`format_embedded` / `format_embedded_doc` / `sort_tailwindcss_classes`),
|
//! - the embed callback `Arc`s (`format_embedded_doc` / `sort_tailwindcss_classes`),
|
||||||
//! consumed by the `core::embed::services` profiles
|
//! consumed by the `core::embed::services` profiles
|
||||||
|
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
@@ -17,10 +17,7 @@ use napi::{
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tracing::debug_span;
|
use tracing::debug_span;
|
||||||
|
|
||||||
use crate::core::embed::{
|
use crate::core::embed::{FormatEmbeddedDocWithConfigCallback, TailwindWithConfigCallback};
|
||||||
FormatEmbeddedDocWithConfigCallback, FormatEmbeddedWithConfigCallback,
|
|
||||||
TailwindWithConfigCallback,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Callback function type for formatting whole files (Tier 3/4 delegation).
|
/// Callback function type for formatting whole files (Tier 3/4 delegation).
|
||||||
/// Takes (options, code) and returns formatted code or an error.
|
/// Takes (options, code) and returns formatted code or an error.
|
||||||
@@ -59,22 +56,6 @@ pub type JsFormatFileCb = ThreadsafeFunction<
|
|||||||
false,
|
false,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
/// Type alias for the callback function signature.
|
|
||||||
/// Takes (options, code) as arguments and returns formatted code or null on error.
|
|
||||||
/// The `options` object includes `parser` field set by Rust side.
|
|
||||||
pub type JsFormatEmbeddedCb = ThreadsafeFunction<
|
|
||||||
// Input arguments
|
|
||||||
FnArgs<(Value, String)>, // (options, code)
|
|
||||||
// Return type (what JS function returns)
|
|
||||||
Promise<Option<String>>,
|
|
||||||
// Arguments (repeated)
|
|
||||||
FnArgs<(Value, String)>,
|
|
||||||
// Error status
|
|
||||||
Status,
|
|
||||||
// CalleeHandled
|
|
||||||
false,
|
|
||||||
>;
|
|
||||||
|
|
||||||
/// Type alias for the Doc-path callback function signature.
|
/// Type alias for the Doc-path callback function signature.
|
||||||
/// Takes (options, code) as arguments and returns a Doc JSON string or null on error.
|
/// Takes (options, code) as arguments and returns a Doc JSON string or null on error.
|
||||||
/// The `options` object includes `parser` field set by Rust side.
|
/// The `options` object includes `parser` field set by Rust side.
|
||||||
@@ -110,7 +91,6 @@ pub type JsSortTailwindClassesCb = ThreadsafeFunction<
|
|||||||
struct TsfnHandles {
|
struct TsfnHandles {
|
||||||
init: Arc<RwLock<Option<JsInitExternalServicesCb>>>,
|
init: Arc<RwLock<Option<JsInitExternalServicesCb>>>,
|
||||||
format_file: Arc<RwLock<Option<JsFormatFileCb>>>,
|
format_file: Arc<RwLock<Option<JsFormatFileCb>>>,
|
||||||
format_embedded: Arc<RwLock<Option<JsFormatEmbeddedCb>>>,
|
|
||||||
format_embedded_doc: Arc<RwLock<Option<JsFormatEmbeddedDocCb>>>,
|
format_embedded_doc: Arc<RwLock<Option<JsFormatEmbeddedDocCb>>>,
|
||||||
sort_tailwind: Arc<RwLock<Option<JsSortTailwindClassesCb>>>,
|
sort_tailwind: Arc<RwLock<Option<JsSortTailwindClassesCb>>>,
|
||||||
}
|
}
|
||||||
@@ -126,7 +106,6 @@ impl Drop for CleanupGuard {
|
|||||||
let handles = &self.0;
|
let handles = &self.0;
|
||||||
let _ = handles.init.write().unwrap().take();
|
let _ = handles.init.write().unwrap().take();
|
||||||
let _ = handles.format_file.write().unwrap().take();
|
let _ = handles.format_file.write().unwrap().take();
|
||||||
let _ = handles.format_embedded.write().unwrap().take();
|
|
||||||
let _ = handles.format_embedded_doc.write().unwrap().take();
|
let _ = handles.format_embedded_doc.write().unwrap().take();
|
||||||
let _ = handles.sort_tailwind.write().unwrap().take();
|
let _ = handles.sort_tailwind.write().unwrap().take();
|
||||||
}
|
}
|
||||||
@@ -144,7 +123,6 @@ pub struct ExternalServices {
|
|||||||
handles: TsfnHandles,
|
handles: TsfnHandles,
|
||||||
pub init: InitExternalServicesCallback,
|
pub init: InitExternalServicesCallback,
|
||||||
pub format_file: FormatFileWithConfigCallback,
|
pub format_file: FormatFileWithConfigCallback,
|
||||||
pub format_embedded: FormatEmbeddedWithConfigCallback,
|
|
||||||
pub format_embedded_doc: FormatEmbeddedDocWithConfigCallback,
|
pub format_embedded_doc: FormatEmbeddedDocWithConfigCallback,
|
||||||
pub sort_tailwindcss_classes: TailwindWithConfigCallback,
|
pub sort_tailwindcss_classes: TailwindWithConfigCallback,
|
||||||
}
|
}
|
||||||
@@ -154,7 +132,6 @@ impl std::fmt::Debug for ExternalServices {
|
|||||||
f.debug_struct("ExternalServices")
|
f.debug_struct("ExternalServices")
|
||||||
.field("init", &"<callback>")
|
.field("init", &"<callback>")
|
||||||
.field("format_file", &"<callback>")
|
.field("format_file", &"<callback>")
|
||||||
.field("format_embedded", &"<callback>")
|
|
||||||
.field("format_embedded_doc", &"<callback>")
|
.field("format_embedded_doc", &"<callback>")
|
||||||
.field("sort_tailwindcss_classes", &"<callback>")
|
.field("sort_tailwindcss_classes", &"<callback>")
|
||||||
.finish()
|
.finish()
|
||||||
@@ -169,14 +146,12 @@ impl ExternalServices {
|
|||||||
/// crashes on Node.js exit when V8 cleans up global handles.
|
/// crashes on Node.js exit when V8 cleans up global handles.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
format_file_cb: JsFormatFileCb,
|
format_file_cb: JsFormatFileCb,
|
||||||
format_embedded_cb: JsFormatEmbeddedCb,
|
|
||||||
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
||||||
sort_tailwindcss_classes_cb: JsSortTailwindClassesCb,
|
sort_tailwindcss_classes_cb: JsSortTailwindClassesCb,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// Wrap TSFNs in Arc<RwLock<Option<...>>> so they can be explicitly dropped
|
// Wrap TSFNs in Arc<RwLock<Option<...>>> so they can be explicitly dropped
|
||||||
let init_handle = Arc::new(RwLock::new(None));
|
let init_handle = Arc::new(RwLock::new(None));
|
||||||
let format_file_handle = Arc::new(RwLock::new(Some(format_file_cb)));
|
let format_file_handle = Arc::new(RwLock::new(Some(format_file_cb)));
|
||||||
let format_embedded_handle = Arc::new(RwLock::new(Some(format_embedded_cb)));
|
|
||||||
let format_embedded_doc_handle = Arc::new(RwLock::new(Some(format_embedded_doc_cb)));
|
let format_embedded_doc_handle = Arc::new(RwLock::new(Some(format_embedded_doc_cb)));
|
||||||
let sort_tailwind_handle = Arc::new(RwLock::new(Some(sort_tailwindcss_classes_cb)));
|
let sort_tailwind_handle = Arc::new(RwLock::new(Some(sort_tailwindcss_classes_cb)));
|
||||||
|
|
||||||
@@ -184,21 +159,18 @@ impl ExternalServices {
|
|||||||
let handles = TsfnHandles {
|
let handles = TsfnHandles {
|
||||||
init: Arc::clone(&init_handle),
|
init: Arc::clone(&init_handle),
|
||||||
format_file: Arc::clone(&format_file_handle),
|
format_file: Arc::clone(&format_file_handle),
|
||||||
format_embedded: Arc::clone(&format_embedded_handle),
|
|
||||||
format_embedded_doc: Arc::clone(&format_embedded_doc_handle),
|
format_embedded_doc: Arc::clone(&format_embedded_doc_handle),
|
||||||
sort_tailwind: Arc::clone(&sort_tailwind_handle),
|
sort_tailwind: Arc::clone(&sort_tailwind_handle),
|
||||||
};
|
};
|
||||||
|
|
||||||
let rust_init = wrap_init_external_services(init_handle);
|
let rust_init = wrap_init_external_services(init_handle);
|
||||||
let rust_format_file = wrap_format_file(format_file_handle);
|
let rust_format_file = wrap_format_file(format_file_handle);
|
||||||
let rust_format_embedded = wrap_format_embedded(Arc::clone(&format_embedded_handle));
|
|
||||||
let rust_format_embedded_doc = wrap_format_embedded_doc(format_embedded_doc_handle);
|
let rust_format_embedded_doc = wrap_format_embedded_doc(format_embedded_doc_handle);
|
||||||
let rust_tailwind = wrap_sort_tailwind_classes(sort_tailwind_handle);
|
let rust_tailwind = wrap_sort_tailwind_classes(sort_tailwind_handle);
|
||||||
Self {
|
Self {
|
||||||
handles,
|
handles,
|
||||||
init: rust_init,
|
init: rust_init,
|
||||||
format_file: rust_format_file,
|
format_file: rust_format_file,
|
||||||
format_embedded: rust_format_embedded,
|
|
||||||
format_embedded_doc: rust_format_embedded_doc,
|
format_embedded_doc: rust_format_embedded_doc,
|
||||||
sort_tailwindcss_classes: rust_tailwind,
|
sort_tailwindcss_classes: rust_tailwind,
|
||||||
}
|
}
|
||||||
@@ -240,13 +212,11 @@ impl ExternalServices {
|
|||||||
handles: TsfnHandles {
|
handles: TsfnHandles {
|
||||||
init: Arc::new(RwLock::new(None)),
|
init: Arc::new(RwLock::new(None)),
|
||||||
format_file: Arc::new(RwLock::new(None)),
|
format_file: Arc::new(RwLock::new(None)),
|
||||||
format_embedded: Arc::new(RwLock::new(None)),
|
|
||||||
format_embedded_doc: Arc::new(RwLock::new(None)),
|
format_embedded_doc: Arc::new(RwLock::new(None)),
|
||||||
sort_tailwind: Arc::new(RwLock::new(None)),
|
sort_tailwind: Arc::new(RwLock::new(None)),
|
||||||
},
|
},
|
||||||
init: Arc::new(|_| Err("Dummy init called".to_string())),
|
init: Arc::new(|_| Err("Dummy init called".to_string())),
|
||||||
format_file: Arc::new(|_, _| Err("Dummy format_file called".to_string())),
|
format_file: Arc::new(|_, _| Err("Dummy format_file called".to_string())),
|
||||||
format_embedded: Arc::new(|_, _| Err("Dummy format_embedded called".to_string())),
|
|
||||||
format_embedded_doc: Arc::new(|_, _: &str| {
|
format_embedded_doc: Arc::new(|_, _: &str| {
|
||||||
Err("Dummy format_embedded_doc called".to_string())
|
Err("Dummy format_embedded_doc called".to_string())
|
||||||
}),
|
}),
|
||||||
@@ -343,34 +313,6 @@ fn parse_format_file_result(result: Value) -> Result<String, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wrap JS `formatEmbeddedCode` callback as a normal Rust function.
|
|
||||||
/// The `options` Value is received with `parser` already set by the caller.
|
|
||||||
fn wrap_format_embedded(
|
|
||||||
cb_handle: Arc<RwLock<Option<JsFormatEmbeddedCb>>>,
|
|
||||||
) -> FormatEmbeddedWithConfigCallback {
|
|
||||||
Arc::new(move |options: Value, code: &str| {
|
|
||||||
let guard = cb_handle.read().unwrap();
|
|
||||||
let Some(cb) = guard.as_ref() else {
|
|
||||||
return Err("JS callback unavailable (environment shutting down)".to_string());
|
|
||||||
};
|
|
||||||
let result = block_on(async {
|
|
||||||
let status = cb.call_async(FnArgs::from((options, code.to_string()))).await;
|
|
||||||
match status {
|
|
||||||
Ok(promise) => match promise.await {
|
|
||||||
Ok(Some(formatted_code)) => Ok(formatted_code),
|
|
||||||
Ok(None) => Err("Embedded formatting failed".to_string()),
|
|
||||||
// JS side never rejects; it returns `null` on error instead.
|
|
||||||
// `Err` here would only come from a napi-rs internal failure.
|
|
||||||
Err(err) => Err(err.reason),
|
|
||||||
},
|
|
||||||
Err(err) => Err(err.reason),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
drop(guard);
|
|
||||||
result
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Wrap JS `formatEmbeddedDoc` callback as a normal Rust function.
|
/// Wrap JS `formatEmbeddedDoc` callback as a normal Rust function.
|
||||||
/// The `options` Value is received with `parser` already set by the caller.
|
/// The `options` Value is received with `parser` already set by the caller.
|
||||||
fn wrap_format_embedded_doc(
|
fn wrap_format_embedded_doc(
|
||||||
|
|||||||
@@ -149,6 +149,15 @@ impl SourceFormatter {
|
|||||||
oxc_formatter_markdown::format_with_session(&session, source_text, options)
|
oxc_formatter_markdown::format_with_session(&session, source_text, options)
|
||||||
.and_then(|formatted| print(formatted, &path))
|
.and_then(|formatted| print(formatted, &path))
|
||||||
}
|
}
|
||||||
|
FormatStrategy::Native(NativeLanguage::Html(variant)) => {
|
||||||
|
let allocator = self.allocator_pool.get();
|
||||||
|
let dispatch_config =
|
||||||
|
ResolvedDispatchConfig::for_root(Arc::clone(&config), validated, &path);
|
||||||
|
let session = self.root_session(&allocator, &dispatch_config);
|
||||||
|
let options = dispatch_config.htmlish_options(variant);
|
||||||
|
oxc_formatter_htmlish::format_with_session(&session, source_text, options)
|
||||||
|
.and_then(|formatted| print(formatted, &path))
|
||||||
|
}
|
||||||
FormatStrategy::Native(NativeLanguage::Toml) => {
|
FormatStrategy::Native(NativeLanguage::Toml) => {
|
||||||
oxc_formatter_toml::format(source_text, to_oxc_formatter_toml(&config, core))
|
oxc_formatter_toml::format(source_text, to_oxc_formatter_toml(&config, core))
|
||||||
}
|
}
|
||||||
@@ -249,14 +258,14 @@ impl SourceFormatter {
|
|||||||
language: PrettierLanguage,
|
language: PrettierLanguage,
|
||||||
config: &FormatConfig,
|
config: &FormatConfig,
|
||||||
) -> Result<String, OxcDiagnostic> {
|
) -> Result<String, OxcDiagnostic> {
|
||||||
use PrettierLanguage::{Angular, Astro, Glimmer, Html, Svelte, Vue};
|
use PrettierLanguage::{Angular, Astro, Glimmer, Svelte, Vue};
|
||||||
|
|
||||||
let mut prettier_options = to_prettier(config);
|
let mut prettier_options = to_prettier(config);
|
||||||
inject_parser(&mut prettier_options, language.parser());
|
inject_parser(&mut prettier_options, language.parser());
|
||||||
inject_filepath(&mut prettier_options, path);
|
inject_filepath(&mut prettier_options, path);
|
||||||
|
|
||||||
// CSS/SCSS/Less also benefit, but they are formatted by `oxc_formatter_css`
|
// CSS/SCSS/Less also benefit, but they are formatted by `oxc_formatter_css`
|
||||||
if matches!(language, Html | Vue | Angular | Glimmer | Svelte | Astro) {
|
if matches!(language, Vue | Angular | Glimmer | Svelte | Astro) {
|
||||||
inject_tailwind_plugin_payload(&mut prettier_options, config);
|
inject_tailwind_plugin_payload(&mut prettier_options, config);
|
||||||
}
|
}
|
||||||
// Languages that embed JS/TS code.
|
// Languages that embed JS/TS code.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use schemars::JsonSchema;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use oxc_formatter_css::CssVariant;
|
use oxc_formatter_css::CssVariant;
|
||||||
|
use oxc_formatter_htmlish::HtmlishVariant;
|
||||||
use oxc_formatter_json::JsonVariant;
|
use oxc_formatter_json::JsonVariant;
|
||||||
use oxc_span::{FileExtension, SourceType};
|
use oxc_span::{FileExtension, SourceType};
|
||||||
|
|
||||||
@@ -159,7 +160,7 @@ impl Language {
|
|||||||
Self::Yaml => Route::Native(NativeLanguage::Yaml),
|
Self::Yaml => Route::Native(NativeLanguage::Yaml),
|
||||||
Self::Markdown => Route::Native(NativeLanguage::Markdown),
|
Self::Markdown => Route::Native(NativeLanguage::Markdown),
|
||||||
Self::Toml => Route::Native(NativeLanguage::Toml),
|
Self::Toml => Route::Native(NativeLanguage::Toml),
|
||||||
Self::Html => Route::Prettier(PrettierLanguage::Html),
|
Self::Html => Route::Native(NativeLanguage::Html(HtmlishVariant::Html)),
|
||||||
Self::AngularHtml => Route::Prettier(PrettierLanguage::Angular),
|
Self::AngularHtml => Route::Prettier(PrettierLanguage::Angular),
|
||||||
Self::Vue => Route::Prettier(PrettierLanguage::Vue),
|
Self::Vue => Route::Prettier(PrettierLanguage::Vue),
|
||||||
Self::Svelte => Route::Prettier(PrettierLanguage::Svelte),
|
Self::Svelte => Route::Prettier(PrettierLanguage::Svelte),
|
||||||
@@ -192,7 +193,6 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn prettier_fences_and_lsp_ids() {
|
fn prettier_fences_and_lsp_ids() {
|
||||||
for (fence, parser) in [
|
for (fence, parser) in [
|
||||||
("html", "html"),
|
|
||||||
("angular", "angular"),
|
("angular", "angular"),
|
||||||
("angular-html", "angular"),
|
("angular-html", "angular"),
|
||||||
("vue", "vue"),
|
("vue", "vue"),
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ pub use support::classify_file;
|
|||||||
|
|
||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
pub use external_services::{
|
pub use external_services::{
|
||||||
ExternalServices, JsFormatEmbeddedCb, JsFormatEmbeddedDocCb, JsFormatFileCb,
|
ExternalServices, JsFormatEmbeddedDocCb, JsFormatFileCb, JsInitExternalServicesCb,
|
||||||
JsInitExternalServicesCb, JsSortTailwindClassesCb,
|
JsSortTailwindClassesCb,
|
||||||
};
|
};
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
//! - [`to_oxc_formatter_graphql()`]: `oxc_formatter_graphql::GraphqlFormatOptions` for GraphQL formatting
|
//! - [`to_oxc_formatter_graphql()`]: `oxc_formatter_graphql::GraphqlFormatOptions` for GraphQL formatting
|
||||||
//! - [`to_oxc_formatter_yaml()`]: `oxc_formatter_yaml::YamlFormatOptions` for YAML formatting
|
//! - [`to_oxc_formatter_yaml()`]: `oxc_formatter_yaml::YamlFormatOptions` for YAML formatting
|
||||||
//! - [`to_oxc_formatter_markdown()`]: `oxc_formatter_markdown::MarkdownFormatOptions` for Markdown formatting
|
//! - [`to_oxc_formatter_markdown()`]: `oxc_formatter_markdown::MarkdownFormatOptions` for Markdown formatting
|
||||||
|
//! - [`to_oxc_formatter_htmlish()`]: `oxc_formatter_htmlish::HtmlishFormatOptions` for HTML formatting
|
||||||
//! - [`to_oxc_formatter_toml()`]: `TomlFormatOptions` for `oxc_formatter_toml`
|
//! - [`to_oxc_formatter_toml()`]: `TomlFormatOptions` for `oxc_formatter_toml`
|
||||||
//! - `to_prettier`(NAPI-only): Prettier-compatible JSON, plus `inject_*` helpers for
|
//! - `to_prettier`(NAPI-only): Prettier-compatible JSON, plus `inject_*` helpers for
|
||||||
//! layering in `parser` / `filepath` / plugin payloads at the format step
|
//! layering in `parser` / `filepath` / plugin payloads at the format step
|
||||||
@@ -19,6 +20,7 @@ mod to_core_options;
|
|||||||
mod to_oxc_formatter;
|
mod to_oxc_formatter;
|
||||||
mod to_oxc_formatter_css;
|
mod to_oxc_formatter_css;
|
||||||
mod to_oxc_formatter_graphql;
|
mod to_oxc_formatter_graphql;
|
||||||
|
mod to_oxc_formatter_htmlish;
|
||||||
mod to_oxc_formatter_json;
|
mod to_oxc_formatter_json;
|
||||||
mod to_oxc_formatter_markdown;
|
mod to_oxc_formatter_markdown;
|
||||||
mod to_oxc_formatter_toml;
|
mod to_oxc_formatter_toml;
|
||||||
@@ -30,6 +32,7 @@ mod validate;
|
|||||||
pub use to_oxc_formatter::to_oxc_formatter;
|
pub use to_oxc_formatter::to_oxc_formatter;
|
||||||
pub use to_oxc_formatter_css::to_oxc_formatter_css;
|
pub use to_oxc_formatter_css::to_oxc_formatter_css;
|
||||||
pub use to_oxc_formatter_graphql::to_oxc_formatter_graphql;
|
pub use to_oxc_formatter_graphql::to_oxc_formatter_graphql;
|
||||||
|
pub use to_oxc_formatter_htmlish::to_oxc_formatter_htmlish;
|
||||||
pub use to_oxc_formatter_json::{to_oxc_formatter_json, to_sort_package_json};
|
pub use to_oxc_formatter_json::{to_oxc_formatter_json, to_sort_package_json};
|
||||||
pub use to_oxc_formatter_markdown::to_oxc_formatter_markdown;
|
pub use to_oxc_formatter_markdown::to_oxc_formatter_markdown;
|
||||||
pub use to_oxc_formatter_toml::to_oxc_formatter_toml;
|
pub use to_oxc_formatter_toml::to_oxc_formatter_toml;
|
||||||
@@ -37,7 +40,6 @@ pub use to_oxc_formatter_yaml::to_oxc_formatter_yaml;
|
|||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
pub use to_prettier::{
|
pub use to_prettier::{
|
||||||
build_prettier_options, inject_filepath, inject_opt_in_plugin_payloads,
|
build_prettier_options, inject_filepath, inject_opt_in_plugin_payloads,
|
||||||
inject_oxfmt_plugin_payload, inject_parser, inject_string_embed_options,
|
inject_oxfmt_plugin_payload, inject_parser, inject_tailwind_plugin_payload, to_prettier,
|
||||||
inject_tailwind_plugin_payload, to_prettier,
|
|
||||||
};
|
};
|
||||||
pub use validate::{ValidatedOptions, validate};
|
pub use validate::{ValidatedOptions, validate};
|
||||||
@@ -8,8 +8,6 @@ use oxc_formatter::{
|
|||||||
};
|
};
|
||||||
use oxc_formatter_core::{CoreFormatOptions, FormatOptions};
|
use oxc_formatter_core::{CoreFormatOptions, FormatOptions};
|
||||||
|
|
||||||
#[cfg(feature = "napi")]
|
|
||||||
use super::super::oxfmtrc::SortTailwindcssUserConfig;
|
|
||||||
use super::super::oxfmtrc::{
|
use super::super::oxfmtrc::{
|
||||||
ArrowParensConfig, CommentLineStrategyConfig, FormatConfig, HtmlWhitespaceSensitivityConfig,
|
ArrowParensConfig, CommentLineStrategyConfig, FormatConfig, HtmlWhitespaceSensitivityConfig,
|
||||||
ImportModifierConfig, ImportSelectorConfig, JsdocUserConfig, LineWrappingStyleConfig,
|
ImportModifierConfig, ImportSelectorConfig, JsdocUserConfig, LineWrappingStyleConfig,
|
||||||
@@ -124,9 +122,7 @@ pub fn to_oxc_formatter(
|
|||||||
// so enabling it without the JS-side sorter would apply half the feature
|
// so enabling it without the JS-side sorter would apply half the feature
|
||||||
// (normalized but unsorted classes).
|
// (normalized but unsorted classes).
|
||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
if let Some(tw_config) =
|
if let Some(tw_config) = config.tailwind_config() {
|
||||||
config.sort_tailwindcss.clone().and_then(SortTailwindcssUserConfig::into_config)
|
|
||||||
{
|
|
||||||
// `config` / `stylesheet` / `preserve_duplicates` are JS-sorter-only
|
// `config` / `stylesheet` / `preserve_duplicates` are JS-sorter-only
|
||||||
// and travel through `to_prettier::inject_tailwind_plugin_payload`,
|
// and travel through `to_prettier::inject_tailwind_plugin_payload`,
|
||||||
// not the Rust formatter options.
|
// not the Rust formatter options.
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
use oxc_formatter_core::{CoreFormatOptions, FormatOptions};
|
||||||
|
use oxc_formatter_htmlish::{HtmlishFormatOptions, WhitespaceSensitivity};
|
||||||
|
|
||||||
|
use super::super::oxfmtrc::{FormatConfig, HtmlWhitespaceSensitivityConfig};
|
||||||
|
|
||||||
|
/// Convert `FormatConfig` into `HtmlishFormatOptions` for `oxc_formatter_htmlish`.
|
||||||
|
///
|
||||||
|
/// Prettier's `html` language consumes the shared layout options plus
|
||||||
|
/// `htmlWhitespaceSensitivity`, `bracketSameLine` and `singleAttributePerLine`.
|
||||||
|
///
|
||||||
|
/// NOTE: Pure field translation:
|
||||||
|
/// `core` comes pre-validated from the config-resolution gate (`validate()`), so this cannot fail.
|
||||||
|
pub fn to_oxc_formatter_htmlish(
|
||||||
|
config: &FormatConfig,
|
||||||
|
core_options: CoreFormatOptions,
|
||||||
|
) -> HtmlishFormatOptions {
|
||||||
|
let mut options = HtmlishFormatOptions::default();
|
||||||
|
options.apply_core(core_options);
|
||||||
|
|
||||||
|
// [Prettier] htmlWhitespaceSensitivity: "css" | "strict" | "ignore"
|
||||||
|
if let Some(sensitivity) = config.html_whitespace_sensitivity {
|
||||||
|
options.whitespace_sensitivity = match sensitivity {
|
||||||
|
HtmlWhitespaceSensitivityConfig::Css => WhitespaceSensitivity::Css,
|
||||||
|
HtmlWhitespaceSensitivityConfig::Strict => WhitespaceSensitivity::Strict,
|
||||||
|
HtmlWhitespaceSensitivityConfig::Ignore => WhitespaceSensitivity::Ignore,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// [Prettier] bracketSameLine: boolean
|
||||||
|
if let Some(bracket_same_line) = config.bracket_same_line {
|
||||||
|
options.bracket_same_line = bracket_same_line;
|
||||||
|
}
|
||||||
|
// [Prettier] singleAttributePerLine: boolean
|
||||||
|
if let Some(single_attribute_per_line) = config.single_attribute_per_line {
|
||||||
|
options.single_attribute_per_line = single_attribute_per_line;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [Oxfmt] sortTailwindcss: collect `class` (and `attributes`) classes for batch sorting.
|
||||||
|
// napi only, like the JS / CSS mappers: the sorter itself is JS-side.
|
||||||
|
#[cfg(feature = "napi")]
|
||||||
|
{
|
||||||
|
options.sort_tailwindcss =
|
||||||
|
config.tailwind_config().map(|tw_config| tw_config.attributes.unwrap_or_default());
|
||||||
|
}
|
||||||
|
|
||||||
|
options
|
||||||
|
}
|
||||||
@@ -10,8 +10,7 @@ use super::super::oxfmtrc::{
|
|||||||
ArrowParensConfig, AstroCompressHtmlConfig, AstroConfig, AstroUserConfig,
|
ArrowParensConfig, AstroCompressHtmlConfig, AstroConfig, AstroUserConfig,
|
||||||
EmbeddedLanguageFormattingConfig, EndOfLineConfig, FormatConfig,
|
EmbeddedLanguageFormattingConfig, EndOfLineConfig, FormatConfig,
|
||||||
HtmlWhitespaceSensitivityConfig, ObjectWrapConfig, OperatorPositionConfig, ProseWrapConfig,
|
HtmlWhitespaceSensitivityConfig, ObjectWrapConfig, OperatorPositionConfig, ProseWrapConfig,
|
||||||
QuotePropsConfig, SortTailwindcssUserConfig, SvelteConfig, SvelteUserConfig,
|
QuotePropsConfig, SvelteConfig, SvelteUserConfig, TrailingCommaConfig,
|
||||||
TrailingCommaConfig,
|
|
||||||
};
|
};
|
||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
use super::super::support::PrettierLanguage;
|
use super::super::support::PrettierLanguage;
|
||||||
@@ -172,15 +171,6 @@ pub fn inject_parser(opts: &mut Value, parser_name: &str) {
|
|||||||
as_object_mut(opts).insert("parser".to_string(), Value::String(parser_name.to_string()));
|
as_object_mut(opts).insert("parser".to_string(), Value::String(parser_name.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inject the keys for a string re-embedded into the parent's IR, overriding the configured ones:
|
|
||||||
/// - `printWidth`: the effective width at the embedding position
|
|
||||||
/// - `endOfLine`: `lf`, since the IR only takes `\n`
|
|
||||||
pub fn inject_string_embed_options(opts: &mut Value, print_width: usize) {
|
|
||||||
let opts = as_object_mut(opts);
|
|
||||||
opts.insert("printWidth".to_string(), Value::from(print_width));
|
|
||||||
opts.insert("endOfLine".to_string(), Value::from("lf"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Inject `filepath` key.
|
/// Inject `filepath` key.
|
||||||
///
|
///
|
||||||
/// Some plugins (Tailwind sorter, etc.) depend on it.
|
/// Some plugins (Tailwind sorter, etc.) depend on it.
|
||||||
@@ -198,8 +188,7 @@ pub fn inject_filepath(opts: &mut Value, path: &Path) {
|
|||||||
///
|
///
|
||||||
/// See: <https://github.com/tailwindlabs/prettier-plugin-tailwindcss#options>
|
/// See: <https://github.com/tailwindlabs/prettier-plugin-tailwindcss#options>
|
||||||
pub fn inject_tailwind_plugin_payload(opts: &mut Value, config: &FormatConfig) {
|
pub fn inject_tailwind_plugin_payload(opts: &mut Value, config: &FormatConfig) {
|
||||||
let Some(tw) = config.sort_tailwindcss.clone().and_then(SortTailwindcssUserConfig::into_config)
|
let Some(tw) = config.tailwind_config() else {
|
||||||
else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let map = as_object_mut(opts);
|
let map = as_object_mut(opts);
|
||||||
|
|||||||
@@ -229,15 +229,16 @@ pub struct FormatConfig {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub prose_wrap: Option<ProseWrapConfig>,
|
pub prose_wrap: Option<ProseWrapConfig>,
|
||||||
|
|
||||||
// ============================================================================================
|
|
||||||
// Prettier compatible options and only used by Prettier
|
|
||||||
// ============================================================================================
|
|
||||||
/// Specify the global whitespace sensitivity.
|
/// Specify the global whitespace sensitivity.
|
||||||
///
|
///
|
||||||
/// - Languages: HTML, Angular, Vue, Handlebars, Svelte
|
/// - Languages: HTML, Angular, Vue, Handlebars, Svelte
|
||||||
/// - Default: `"css"`
|
/// - Default: `"css"`
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub html_whitespace_sensitivity: Option<HtmlWhitespaceSensitivityConfig>,
|
pub html_whitespace_sensitivity: Option<HtmlWhitespaceSensitivityConfig>,
|
||||||
|
|
||||||
|
// ============================================================================================
|
||||||
|
// Prettier compatible options and only used by Prettier
|
||||||
|
// ============================================================================================
|
||||||
/// Whether or not to indent the code inside `<script>` and `<style>` tags in Vue files.
|
/// Whether or not to indent the code inside `<script>` and `<style>` tags in Vue files.
|
||||||
///
|
///
|
||||||
/// - Languages: Vue
|
/// - Languages: Vue
|
||||||
@@ -356,6 +357,11 @@ impl FormatConfig {
|
|||||||
matches!(self.astro, Some(AstroUserConfig::Bool(true) | AstroUserConfig::Object(_)))
|
matches!(self.astro, Some(AstroUserConfig::Bool(true) | AstroUserConfig::Object(_)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The `sortTailwindcss` settings, `None` when sorting is off.
|
||||||
|
pub fn tailwind_config(&self) -> Option<SortTailwindcssConfig> {
|
||||||
|
self.sort_tailwindcss.clone().and_then(SortTailwindcssUserConfig::into_config)
|
||||||
|
}
|
||||||
|
|
||||||
/// Whether Tailwind class sorting is enabled by this config.
|
/// Whether Tailwind class sorting is enabled by this config.
|
||||||
///
|
///
|
||||||
/// Enabled when `sortTailwindcss` is set to `true` or an object;
|
/// Enabled when `sortTailwindcss` is set to `true` or an object;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::path::Path;
|
|||||||
use phf::phf_set;
|
use phf::phf_set;
|
||||||
|
|
||||||
use oxc_formatter_css::CssVariant;
|
use oxc_formatter_css::CssVariant;
|
||||||
|
use oxc_formatter_htmlish::HtmlishVariant;
|
||||||
use oxc_formatter_json::JsonVariant;
|
use oxc_formatter_json::JsonVariant;
|
||||||
use oxc_span::SourceType;
|
use oxc_span::SourceType;
|
||||||
|
|
||||||
@@ -56,6 +57,12 @@ pub fn classify_file(path: &Path) -> Option<FormatStrategy> {
|
|||||||
if MARKDOWN_FILENAMES.contains(file_name) || MARKDOWN_EXTENSIONS.contains(ext) {
|
if MARKDOWN_FILENAMES.contains(file_name) || MARKDOWN_EXTENSIONS.contains(ext) {
|
||||||
return Some(FormatStrategy::Native(NativeLanguage::Markdown));
|
return Some(FormatStrategy::Native(NativeLanguage::Markdown));
|
||||||
}
|
}
|
||||||
|
// Angular templates share the extension, and stay with Prettier
|
||||||
|
if let Some(variant) = html_variant(ext)
|
||||||
|
&& !file_name.ends_with(".component.html")
|
||||||
|
{
|
||||||
|
return Some(FormatStrategy::Native(NativeLanguage::Html(variant)));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
if let Some(language) = prettier_language(file_name, ext) {
|
if let Some(language) = prettier_language(file_name, ext) {
|
||||||
@@ -129,6 +136,7 @@ pub enum NativeLanguage {
|
|||||||
Json(JsonVariant),
|
Json(JsonVariant),
|
||||||
Markdown,
|
Markdown,
|
||||||
Toml,
|
Toml,
|
||||||
|
Html(HtmlishVariant),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NativeLanguage {
|
impl NativeLanguage {
|
||||||
@@ -142,6 +150,7 @@ impl NativeLanguage {
|
|||||||
Self::Json(_) => "json",
|
Self::Json(_) => "json",
|
||||||
Self::Markdown => "markdown",
|
Self::Markdown => "markdown",
|
||||||
Self::Toml => "toml",
|
Self::Toml => "toml",
|
||||||
|
Self::Html(_) => "html",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,7 +164,6 @@ impl NativeLanguage {
|
|||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum PrettierLanguage {
|
pub enum PrettierLanguage {
|
||||||
Mdx,
|
Mdx,
|
||||||
Html,
|
|
||||||
Angular,
|
Angular,
|
||||||
Vue,
|
Vue,
|
||||||
/// Formatted only when `prettier-plugin-svelte` is enabled (`svelte` config key).
|
/// Formatted only when `prettier-plugin-svelte` is enabled (`svelte` config key).
|
||||||
@@ -178,7 +186,6 @@ impl PrettierLanguage {
|
|||||||
pub fn parser(self) -> &'static str {
|
pub fn parser(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Self::Mdx => "mdx",
|
Self::Mdx => "mdx",
|
||||||
Self::Html => "html",
|
|
||||||
Self::Angular => "angular",
|
Self::Angular => "angular",
|
||||||
Self::Vue => "vue",
|
Self::Vue => "vue",
|
||||||
Self::Svelte => "svelte",
|
Self::Svelte => "svelte",
|
||||||
@@ -383,6 +390,17 @@ static MARKDOWN_EXTENSIONS: phf::Set<&'static str> = phf_set! {
|
|||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
|
fn html_variant(ext: &str) -> Option<HtmlishVariant> {
|
||||||
|
match ext {
|
||||||
|
"html" | "hta" | "htm" | "inc" => Some(HtmlishVariant::Html),
|
||||||
|
// An XML parser may read XHTML
|
||||||
|
"xhtml" | "xht" => Some(HtmlishVariant::Xhtml),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
/// Returns the [`PrettierLanguage`] for the file, if supported.
|
/// Returns the [`PrettierLanguage`] for the file, if supported.
|
||||||
/// See also `prettier --support-info | jq '.languages[]'`
|
/// See also `prettier --support-info | jq '.languages[]'`
|
||||||
#[cfg(feature = "napi")]
|
#[cfg(feature = "napi")]
|
||||||
@@ -391,7 +409,6 @@ fn prettier_language(file_name: &str, ext: &str) -> Option<PrettierLanguage> {
|
|||||||
return Some(PrettierLanguage::Angular);
|
return Some(PrettierLanguage::Angular);
|
||||||
}
|
}
|
||||||
Some(match ext {
|
Some(match ext {
|
||||||
"html" | "hta" | "htm" | "inc" | "xht" | "xhtml" => PrettierLanguage::Html,
|
|
||||||
"vue" => PrettierLanguage::Vue,
|
"vue" => PrettierLanguage::Vue,
|
||||||
// Formatting is gated by `ResolveOutcome::MissingPlugin` (requires `svelte` / `astro` config),
|
// Formatting is gated by `ResolveOutcome::MissingPlugin` (requires `svelte` / `astro` config),
|
||||||
// classified here so that each caller can surface a friendly error or skip.
|
// classified here so that each caller can surface a friendly error or skip.
|
||||||
@@ -522,11 +539,11 @@ mod tests {
|
|||||||
("package.json", None),
|
("package.json", None),
|
||||||
("composer.json", None),
|
("composer.json", None),
|
||||||
("config.importmap", None),
|
("config.importmap", None),
|
||||||
// HTML
|
// HTML files are routed to `oxc_formatter_htmlish` in `classify_file`
|
||||||
("index.html", Some("html")),
|
// and excluded from this map.
|
||||||
("page.htm", Some("html")),
|
("index.html", None),
|
||||||
("template.xhtml", Some("html")),
|
("page.htm", None),
|
||||||
// Angular (must be detected before HTML)
|
// Angular
|
||||||
("app.component.html", Some("angular")),
|
("app.component.html", Some("angular")),
|
||||||
// MJML
|
// MJML
|
||||||
("email.mjml", Some("mjml")),
|
("email.mjml", Some("mjml")),
|
||||||
@@ -686,6 +703,28 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_html_files_route_to_oxc_formatter_htmlish() {
|
||||||
|
for (file_name, expected) in [
|
||||||
|
("index.html", HtmlishVariant::Html),
|
||||||
|
("page.htm", HtmlishVariant::Html),
|
||||||
|
("app.hta", HtmlishVariant::Html),
|
||||||
|
("header.inc", HtmlishVariant::Html),
|
||||||
|
("template.xhtml", HtmlishVariant::Xhtml),
|
||||||
|
("page.xht", HtmlishVariant::Xhtml),
|
||||||
|
] {
|
||||||
|
let result = classify_file(Path::new(file_name));
|
||||||
|
assert!(
|
||||||
|
matches!(result, Some(FormatStrategy::Native(NativeLanguage::Html(variant))) if variant == expected),
|
||||||
|
"`{file_name}` should be routed to oxc_formatter_htmlish as {expected:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Angular templates stay with Prettier (and are not formatted in the pure Rust build)
|
||||||
|
let result = classify_file(Path::new("app.component.html"));
|
||||||
|
assert!(!matches!(result, Some(FormatStrategy::Native(_))));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_toml_files() {
|
fn test_toml_files() {
|
||||||
// Files that should be detected as TOML
|
// Files that should be detected as TOML
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ use crate::{
|
|||||||
api::{format_api, text_to_doc_api},
|
api::{format_api, text_to_doc_api},
|
||||||
cli::{MigrateSource, Mode, StdinRunner, WalkRunner, format_command, init_rayon},
|
cli::{MigrateSource, Mode, StdinRunner, WalkRunner, format_command, init_rayon},
|
||||||
core::{
|
core::{
|
||||||
ExternalServices, JsFormatEmbeddedCb, JsFormatEmbeddedDocCb, JsFormatFileCb,
|
ExternalServices, JsFormatEmbeddedDocCb, JsFormatFileCb, JsInitExternalServicesCb,
|
||||||
JsInitExternalServicesCb, JsLoadJsConfigCb, JsSortTailwindClassesCb,
|
JsLoadJsConfigCb, JsSortTailwindClassesCb, create_js_config_loader, utils,
|
||||||
create_js_config_loader, utils,
|
|
||||||
},
|
},
|
||||||
lsp::run_lsp,
|
lsp::run_lsp,
|
||||||
};
|
};
|
||||||
@@ -25,7 +24,7 @@ use crate::{
|
|||||||
/// 2. `load_js_config_cb`: Callback to load JS/TS config files
|
/// 2. `load_js_config_cb`: Callback to load JS/TS config files
|
||||||
/// 3. `init_external_services_cb`: Callback to initialize external services (JS worker pool)
|
/// 3. `init_external_services_cb`: Callback to initialize external services (JS worker pool)
|
||||||
/// 4. `format_file_cb`: Callback to format files
|
/// 4. `format_file_cb`: Callback to format files
|
||||||
/// 5. `format_embedded_cb`: Callback to format embedded code in templates
|
/// 5. `format_embedded_doc_cb`: Callback to format embedded code in templates as a Prettier Doc
|
||||||
/// 6. `sort_tailwindcss_classes_cb`: Callback to sort Tailwind classes
|
/// 6. `sort_tailwindcss_classes_cb`: Callback to sort Tailwind classes
|
||||||
///
|
///
|
||||||
/// Returns a tuple of `[mode, exitCode]`:
|
/// Returns a tuple of `[mode, exitCode]`:
|
||||||
@@ -44,8 +43,6 @@ pub async fn run_cli(
|
|||||||
)]
|
)]
|
||||||
format_file_cb: JsFormatFileCb,
|
format_file_cb: JsFormatFileCb,
|
||||||
#[napi(ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string | null>")]
|
#[napi(ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string | null>")]
|
||||||
format_embedded_cb: JsFormatEmbeddedCb,
|
|
||||||
#[napi(ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string | null>")]
|
|
||||||
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
||||||
#[napi(
|
#[napi(
|
||||||
ts_arg_type = "(options: Record<string, any>, classes: string[]) => Promise<string[] | null>"
|
ts_arg_type = "(options: Record<string, any>, classes: string[]) => Promise<string[] | null>"
|
||||||
@@ -83,13 +80,9 @@ pub async fn run_cli(
|
|||||||
|
|
||||||
// Otherwise, handle modes that require Rust side processing
|
// Otherwise, handle modes that require Rust side processing
|
||||||
|
|
||||||
let external_services = ExternalServices::new(
|
let external_services =
|
||||||
format_file_cb,
|
ExternalServices::new(format_file_cb, format_embedded_doc_cb, sort_tailwindcss_classes_cb)
|
||||||
format_embedded_cb,
|
.with_init_cb(init_external_services_cb);
|
||||||
format_embedded_doc_cb,
|
|
||||||
sort_tailwindcss_classes_cb,
|
|
||||||
)
|
|
||||||
.with_init_cb(init_external_services_cb);
|
|
||||||
let _cleanup = external_services.cleanup_guard();
|
let _cleanup = external_services.cleanup_guard();
|
||||||
let js_config_loader = create_js_config_loader(load_js_config_cb);
|
let js_config_loader = create_js_config_loader(load_js_config_cb);
|
||||||
|
|
||||||
@@ -149,8 +142,6 @@ pub async fn format(
|
|||||||
)]
|
)]
|
||||||
format_file_cb: JsFormatFileCb,
|
format_file_cb: JsFormatFileCb,
|
||||||
#[napi(ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string | null>")]
|
#[napi(ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string | null>")]
|
||||||
format_embedded_cb: JsFormatEmbeddedCb,
|
|
||||||
#[napi(ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string | null>")]
|
|
||||||
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
||||||
#[napi(
|
#[napi(
|
||||||
ts_arg_type = "(options: Record<string, any>, classes: string[]) => Promise<string[] | null>"
|
ts_arg_type = "(options: Record<string, any>, classes: string[]) => Promise<string[] | null>"
|
||||||
@@ -162,7 +153,6 @@ pub async fn format(
|
|||||||
source_text,
|
source_text,
|
||||||
options,
|
options,
|
||||||
format_file_cb,
|
format_file_cb,
|
||||||
format_embedded_cb,
|
|
||||||
format_embedded_doc_cb,
|
format_embedded_doc_cb,
|
||||||
sort_tailwind_classes_cb,
|
sort_tailwind_classes_cb,
|
||||||
);
|
);
|
||||||
@@ -189,8 +179,6 @@ pub async fn js_text_to_doc(
|
|||||||
)]
|
)]
|
||||||
format_file_cb: JsFormatFileCb,
|
format_file_cb: JsFormatFileCb,
|
||||||
#[napi(ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string | null>")]
|
#[napi(ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string | null>")]
|
||||||
format_embedded_cb: JsFormatEmbeddedCb,
|
|
||||||
#[napi(ts_arg_type = "(options: Record<string, any>, code: string) => Promise<string | null>")]
|
|
||||||
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
||||||
#[napi(
|
#[napi(
|
||||||
ts_arg_type = "(options: Record<string, any>, classes: string[]) => Promise<string[] | null>"
|
ts_arg_type = "(options: Record<string, any>, classes: string[]) => Promise<string[] | null>"
|
||||||
@@ -205,7 +193,6 @@ pub async fn js_text_to_doc(
|
|||||||
&oxfmt_plugin_options_json,
|
&oxfmt_plugin_options_json,
|
||||||
&parent_context,
|
&parent_context,
|
||||||
format_file_cb,
|
format_file_cb,
|
||||||
format_embedded_cb,
|
|
||||||
format_embedded_doc_cb,
|
format_embedded_doc_cb,
|
||||||
sort_tailwind_classes_cb,
|
sort_tailwind_classes_cb,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe("JSDoc", () => {
|
|||||||
* \`\`\`
|
* \`\`\`
|
||||||
*
|
*
|
||||||
* \`\`\`html
|
* \`\`\`html
|
||||||
* <div class="body" > </ div>
|
* <div class="body" > </div>
|
||||||
* \`\`\`
|
* \`\`\`
|
||||||
*/
|
*/
|
||||||
`.trim();
|
`.trim();
|
||||||
@@ -181,9 +181,8 @@ describe("JSDoc", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should pass the fence's effective width to the Prettier string path too", async () => {
|
it("should pass the fence's effective width to an html fence too", async () => {
|
||||||
// Same 93-char boundary as the native case, exercised through the Prettier
|
// Same 93-char boundary as the native case, exercised through an html fence:
|
||||||
// branch (html fences go to Prettier with `printWidth` injected):
|
|
||||||
// the 96-char element must break, the 93-char one must not.
|
// the 96-char element must break, the 93-char one must not.
|
||||||
const source = `
|
const source = `
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,11 +23,36 @@ pub struct CssInJsTemplate;
|
|||||||
/// which would need escaping in the template.
|
/// which would need escaping in the template.
|
||||||
pub struct MarkdownInJsTemplate;
|
pub struct MarkdownInJsTemplate;
|
||||||
|
|
||||||
|
/// Prettier uses `PRETTIER_HTML_PLACEHOLDER_{index}_{counter}_IN_JS` as placeholders, the counter global.
|
||||||
|
/// Ours is `PRETTIER_HTML_PLACEHOLDER_{depth}_{index}_IN_JS`, the session's dispatch depth:
|
||||||
|
/// an html-in-js nested in this one (in its `<script>`) is deeper, so neither takes the other's placeholders,
|
||||||
|
/// and the same input always gets the same placeholders.
|
||||||
|
///
|
||||||
|
/// A child may change its case (CSS lowercases a property name: `${b}: 1px` in a `<style>`),
|
||||||
|
/// so it is found ASCII case-insensitively in the child's IR.
|
||||||
|
/// The shape stays Prettier's because angular-in-js still formats through Prettier,
|
||||||
|
/// which recognizes `/^PRETTIER_HTML_PLACEHOLDER_\d+_\d+_IN_JS$/` (unquoted values, parens around an identifier).
|
||||||
|
/// TODO: Once Angular formats natively, the name is ours to choose,
|
||||||
|
/// e.g. one no child changes the case of, and the case-insensitive search can go.
|
||||||
|
pub const HTML_PLACEHOLDER_PREFIX: &str = "PRETTIER_HTML_PLACEHOLDER_";
|
||||||
|
pub const HTML_PLACEHOLDER_SUFFIX: &str = "_IN_JS";
|
||||||
|
|
||||||
|
/// `name` is an html-in-js placeholder, of any depth.
|
||||||
|
/// One in a `<script>` stands in for an expression of any precedence (`f((${a + b}) / 2)`),
|
||||||
|
/// so the parens around it in the source stay (Prettier's `parentheses/identifier.js`).
|
||||||
|
pub fn is_html_placeholder(name: &str) -> bool {
|
||||||
|
name.strip_prefix(HTML_PLACEHOLDER_PREFIX)
|
||||||
|
.and_then(|id| id.strip_suffix(HTML_PLACEHOLDER_SUFFIX))
|
||||||
|
.is_some_and(|id| !id.is_empty() && id.bytes().all(|b| b.is_ascii_digit() || b == b'_'))
|
||||||
|
}
|
||||||
|
|
||||||
/// Parent→child context for HTML/Angular dispatched from a JS template literal (html-in-js).
|
/// Parent→child context for HTML/Angular dispatched from a JS template literal (html-in-js).
|
||||||
pub struct HtmlInJsTemplate {
|
pub struct HtmlInJsTemplate {
|
||||||
/// The backticks hug the content (no whitespace on both ends, whitespace sensitivity not `ignore`):
|
/// The backticks hug the content (no whitespace on both ends, whitespace sensitivity not `ignore`):
|
||||||
/// the child indents its content when more than one node prints at the root.
|
/// the child indents its content when more than one node prints at the root.
|
||||||
pub indent_multiple_roots: bool,
|
pub indent_multiple_roots: bool,
|
||||||
|
/// The prefix and suffix around the id of the placeholder standing in for a `${}` expression.
|
||||||
|
pub placeholder: (&'static str, &'static str),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a dispatched JS/TS program is embedded in, for the print rules that depend on it.
|
/// What a dispatched JS/TS program is embedded in, for the print rules that depend on it.
|
||||||
@@ -38,4 +63,7 @@ pub enum JsEmbeddedIn {
|
|||||||
/// A Markdown code block (js-in-markdown):
|
/// A Markdown code block (js-in-markdown):
|
||||||
/// a program of a single JSX expression statement prints without its semicolon.
|
/// a program of a single JSX expression statement prints without its semicolon.
|
||||||
MarkdownCodeBlock,
|
MarkdownCodeBlock,
|
||||||
|
/// An HTML event handler attribute's value (`onclick="..."`):
|
||||||
|
/// a single expression statement prints without its semicolon, and strings in `'` (the attribute's quote is `"`).
|
||||||
|
HtmlEventHandler,
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ use oxc_formatter_core::{FormatElement, SourceText};
|
|||||||
use oxc_span::{GetSpan, SourceType, Span};
|
use oxc_span::{GetSpan, SourceType, Span};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::{options::JsFormatOptions, utils::assignment_like::AssignmentLikeLayout};
|
use crate::{JsEmbeddedIn, options::JsFormatOptions, utils::assignment_like::AssignmentLikeLayout};
|
||||||
|
|
||||||
use super::Comments;
|
use super::Comments;
|
||||||
|
|
||||||
@@ -103,6 +103,8 @@ pub struct JsFormatContext<'ast> {
|
|||||||
/// Set only for an embedded program (see [`crate::format_to_ir`]):
|
/// Set only for an embedded program (see [`crate::format_to_ir`]):
|
||||||
/// its sole statement prints without its semicolon.
|
/// its sole statement prints without its semicolon.
|
||||||
embedding_omits_semicolon: bool,
|
embedding_omits_semicolon: bool,
|
||||||
|
/// What an embedded program is embedded in (see [`crate::format_to_ir`]).
|
||||||
|
embedded_in: Option<JsEmbeddedIn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for JsFormatContext<'_> {
|
impl std::fmt::Debug for JsFormatContext<'_> {
|
||||||
@@ -148,6 +150,7 @@ impl<'ast> JsFormatContext<'ast> {
|
|||||||
quote_needed_stack: Vec::new(),
|
quote_needed_stack: Vec::new(),
|
||||||
tailwind_context_stack: Vec::new(),
|
tailwind_context_stack: Vec::new(),
|
||||||
embedding_omits_semicolon: false,
|
embedding_omits_semicolon: false,
|
||||||
|
embedded_in: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +244,16 @@ impl<'ast> JsFormatContext<'ast> {
|
|||||||
self.embedding_omits_semicolon
|
self.embedding_omits_semicolon
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_embedded_in(&mut self, embedded_in: Option<JsEmbeddedIn>) {
|
||||||
|
self.embedded_in = embedded_in;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An HTML attribute's value (see [`JsEmbeddedIn::HtmlEventHandler`]):
|
||||||
|
/// strings print in `'`, whatever they hold, as the attribute's quote is `"`.
|
||||||
|
pub(crate) fn in_html_attribute(&self) -> bool {
|
||||||
|
self.embedded_in == Some(JsEmbeddedIn::HtmlEventHandler)
|
||||||
|
}
|
||||||
|
|
||||||
/// Push a Tailwind context entry onto the stack.
|
/// Push a Tailwind context entry onto the stack.
|
||||||
/// Call this when entering a JSXAttribute or CallExpression with Tailwind class context.
|
/// Call this when entering a JSXAttribute or CallExpression with Tailwind class context.
|
||||||
pub fn push_tailwind_context(&mut self, entry: TailwindContextEntry) {
|
pub fn push_tailwind_context(&mut self, entry: TailwindContextEntry) {
|
||||||
|
|||||||
@@ -756,7 +756,7 @@ const EMBEDDED_FENCE_LANGUAGES: [&str; 6] = ["css", "less", "scss", "json", "yam
|
|||||||
/// formatted in-process via [`format_jsdoc_js_snippet`] (`oxc_formatter`'s own parser/formatter).
|
/// formatted in-process via [`format_jsdoc_js_snippet`] (`oxc_formatter`'s own parser/formatter).
|
||||||
/// - Non-JS/TS fenced code of [`EMBEDDED_FENCE_LANGUAGES`]:
|
/// - Non-JS/TS fenced code of [`EMBEDDED_FENCE_LANGUAGES`]:
|
||||||
/// when the session carries a string embedder, routed through it (the string channel);
|
/// when the session carries a string embedder, routed through it (the string channel);
|
||||||
/// absent one (Rust-only conformance, or the language has no native branch), preserved verbatim.
|
/// absent one (Rust-only conformance) or on its failure, preserved verbatim.
|
||||||
///
|
///
|
||||||
/// NOTE: `@example` fences diverge here, see `tag_formatters::format_example_fenced_block`.
|
/// NOTE: `@example` fences diverge here, see `tag_formatters::format_example_fenced_block`.
|
||||||
fn format_code_value<'a>(
|
fn format_code_value<'a>(
|
||||||
|
|||||||
@@ -136,13 +136,18 @@ pub fn format_to_ir<'a>(
|
|||||||
|
|
||||||
// Special cases for js-in-xxx:
|
// Special cases for js-in-xxx:
|
||||||
// - Single JSX in Markdown code block: omit the semicolon
|
// - Single JSX in Markdown code block: omit the semicolon
|
||||||
let embedding_omits_semicolon = embedded_in == Some(JsEmbeddedIn::MarkdownCodeBlock)
|
// - Single expression in an HTML event handler attribute: omit the semicolon
|
||||||
&& program.directives.is_empty()
|
let embedding_omits_semicolon = program.directives.is_empty()
|
||||||
&& matches!(
|
&& match program.body.as_slice() {
|
||||||
program.body.as_slice(),
|
[Statement::ExpressionStatement(statement)] => match embedded_in {
|
||||||
[Statement::ExpressionStatement(statement)] if statement.expression.is_jsx()
|
Some(JsEmbeddedIn::MarkdownCodeBlock) => statement.expression.is_jsx(),
|
||||||
);
|
Some(JsEmbeddedIn::HtmlEventHandler) => true,
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
context.set_embedding_omits_semicolon(embedding_omits_semicolon);
|
context.set_embedding_omits_semicolon(embedding_omits_semicolon);
|
||||||
|
context.set_embedded_in(embedded_in);
|
||||||
|
|
||||||
let (ir, _) = formatter::build_ir(
|
let (ir, _) = formatter::build_ir(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use oxc_span::GetSpan;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast_nodes::{AstNode, AstNodes},
|
ast_nodes::{AstNode, AstNodes},
|
||||||
|
embed_context::is_html_placeholder,
|
||||||
formatter::{JsFormatter, JsFormatterExt as _},
|
formatter::{JsFormatter, JsFormatterExt as _},
|
||||||
print::{BinaryLikeOperator, should_flatten, unary_argument_takes_comment_parens},
|
print::{BinaryLikeOperator, should_flatten, unary_argument_takes_comment_parens},
|
||||||
utils::{expression::ExpressionLeftSide, typecast::is_cast_target},
|
utils::{expression::ExpressionLeftSide, typecast::is_cast_target},
|
||||||
@@ -73,6 +74,11 @@ impl NeedsParentheses<'_> for AstNode<'_, '_, IdentifierReference<'_>> {
|
|||||||
if f.comments().is_marked_as_type_cast_node(self) {
|
if f.comments().is_marked_as_type_cast_node(self) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// An html-in-js placeholder (reaching JS through HTML's `<script>` or `on*` attributes)
|
||||||
|
// stands in for an expression of any precedence, as Prettier's `parentheses/identifier.js` does
|
||||||
|
if is_html_placeholder(self.name.as_str()) {
|
||||||
|
return is_parenthesized_in_source(self, f);
|
||||||
|
}
|
||||||
|
|
||||||
// `(type) satisfies never;` and similar keyword-at-statement-start cast cases
|
// `(type) satisfies never;` and similar keyword-at-statement-start cast cases
|
||||||
// <https://github.com/prettier/prettier/blob/7584432401a47a26943dd7a9ca9a8e032ead7285/src/language-js/parentheses/identifier.js#L84-L107>
|
// <https://github.com/prettier/prettier/blob/7584432401a47a26943dd7a9ca9a8e032ead7285/src/language-js/parentheses/identifier.js#L84-L107>
|
||||||
@@ -136,6 +142,47 @@ impl NeedsParentheses<'_> for AstNode<'_, '_, IdentifierReference<'_>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the source wraps `node` in parentheses of its own (the AST keeps none),
|
||||||
|
/// besides those of the syntax around it (a sole call argument, an `if` test, ...).
|
||||||
|
fn is_parenthesized_in_source(
|
||||||
|
node: &AstNode<'_, '_, IdentifierReference<'_>>,
|
||||||
|
f: &JsFormatter<'_, '_>,
|
||||||
|
) -> bool {
|
||||||
|
let bytes = f.source_text().as_str().as_bytes();
|
||||||
|
let (mut start, mut end) = (node.span.start as usize, node.span.end as usize);
|
||||||
|
let mut pairs = 0;
|
||||||
|
loop {
|
||||||
|
let before = bytes[..start].iter().rposition(|b| !b.is_ascii_whitespace());
|
||||||
|
let after = bytes[end..].iter().position(|b| !b.is_ascii_whitespace()).map(|i| end + i);
|
||||||
|
match (before, after) {
|
||||||
|
(Some(before), Some(after)) if bytes[before] == b'(' && bytes[after] == b')' => {
|
||||||
|
pairs += 1;
|
||||||
|
(start, end) = (before, after + 1);
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let span = node.span;
|
||||||
|
let syntax_parens = match node.parent() {
|
||||||
|
AstNodes::CallExpression(call) => {
|
||||||
|
matches!(call.arguments.as_slice(), [only] if only.span() == span)
|
||||||
|
}
|
||||||
|
AstNodes::NewExpression(new) => {
|
||||||
|
matches!(new.arguments.as_slice(), [only] if only.span() == span)
|
||||||
|
}
|
||||||
|
AstNodes::ImportExpression(import) => {
|
||||||
|
import.source.span() == span && import.options.is_none()
|
||||||
|
}
|
||||||
|
AstNodes::IfStatement(statement) => statement.test.span() == span,
|
||||||
|
AstNodes::WhileStatement(statement) => statement.test.span() == span,
|
||||||
|
AstNodes::DoWhileStatement(statement) => statement.test.span() == span,
|
||||||
|
AstNodes::SwitchStatement(statement) => statement.discriminant.span() == span,
|
||||||
|
AstNodes::WithStatement(statement) => statement.object.span() == span,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
pairs > usize::from(syntax_parens)
|
||||||
|
}
|
||||||
|
|
||||||
impl NeedsParentheses<'_> for AstNode<'_, '_, BooleanLiteral> {
|
impl NeedsParentheses<'_> for AstNode<'_, '_, BooleanLiteral> {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn needs_parentheses(&self, _f: &JsFormatter<'_, '_>) -> bool {
|
fn needs_parentheses(&self, _f: &JsFormatter<'_, '_>) -> bool {
|
||||||
|
|||||||
@@ -104,8 +104,10 @@ pub(super) fn format_css_doc<'a>(
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|el| match el {
|
.map(|el| match el {
|
||||||
FormatElement::EmbedPlaceholder(_) => 1,
|
FormatElement::EmbedPlaceholder(_) => 1,
|
||||||
FormatElement::Text { text, .. } => {
|
FormatElement::Text { text, .. }
|
||||||
super::count_placeholders(text, PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX)
|
if super::find_ignore_ascii_case(text.as_str(), PLACEHOLDER_PREFIX).is_some() =>
|
||||||
|
{
|
||||||
|
super::split_on_placeholders(text, PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX).len() / 2
|
||||||
}
|
}
|
||||||
_ => 0,
|
_ => 0,
|
||||||
})
|
})
|
||||||
@@ -132,7 +134,10 @@ pub(super) fn format_css_doc<'a>(
|
|||||||
// Same scan as html.rs: `split_on_placeholders` yields alternating
|
// Same scan as html.rs: `split_on_placeholders` yields alternating
|
||||||
// [literal, index, literal, ...] (invalid matches fold into the literal),
|
// [literal, index, literal, ...] (invalid matches fold into the literal),
|
||||||
// so even parts are text and odd parts are a placeholder.
|
// so even parts are text and odd parts are a placeholder.
|
||||||
FormatElement::Text { text, .. } if text.contains(PLACEHOLDER_PREFIX) => {
|
FormatElement::Text { text, .. }
|
||||||
|
if super::find_ignore_ascii_case(text.as_str(), PLACEHOLDER_PREFIX)
|
||||||
|
.is_some() =>
|
||||||
|
{
|
||||||
let parts = super::split_on_placeholders(
|
let parts = super::split_on_placeholders(
|
||||||
text.as_str(),
|
text.as_str(),
|
||||||
PLACEHOLDER_PREFIX,
|
PLACEHOLDER_PREFIX,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use oxc_formatter_core::{
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ast_nodes::AstNode,
|
ast_nodes::AstNode,
|
||||||
embed_context::HtmlInJsTemplate,
|
embed_context::{HTML_PLACEHOLDER_PREFIX, HTML_PLACEHOLDER_SUFFIX, HtmlInJsTemplate},
|
||||||
format_args,
|
format_args,
|
||||||
formatter::prelude::*,
|
formatter::prelude::*,
|
||||||
print::template::{
|
print::template::{
|
||||||
@@ -15,17 +15,11 @@ use crate::{
|
|||||||
write,
|
write,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prettier uses `PRETTIER_HTML_PLACEHOLDER_{index}_{counter}_IN_JS` as placeholders.
|
/// Format an HTML(Angular)-in-JS template literal via the dispatcher with placeholder replacement.
|
||||||
// We use a fixed counter of 0 since we don't have nested embeds.
|
|
||||||
const PLACEHOLDER_PREFIX: &str = "PRETTIER_HTML_PLACEHOLDER_";
|
|
||||||
const PLACEHOLDER_SUFFIX: &str = "_IN_JS";
|
|
||||||
const COUNTER: &str = "0";
|
|
||||||
|
|
||||||
/// Format an HTML(Angular)-in-JS template literal via the Doc->IR path with placeholder replacement.
|
|
||||||
///
|
///
|
||||||
/// Uses `.cooked` values (unlike CSS which uses `.raw`), joins quasis with
|
/// Uses `.cooked` values (unlike CSS which uses `.raw`), joins quasis with
|
||||||
/// `PRETTIER_HTML_PLACEHOLDER_{N}_0_IN_JS` markers, formats via the given `embedded_language`,
|
/// `PRETTIER_HTML_PLACEHOLDER_{depth}_{N}_IN_JS` markers, formats via the given `embedded_language`,
|
||||||
/// then replaces placeholder occurrences in the resulting IR with `${expr}` Docs.
|
/// then replaces placeholder occurrences in the resulting IR with `${expr}` IR.
|
||||||
///
|
///
|
||||||
/// Supports both html-in-js and angular-in-js (`@Component({ template })`).
|
/// Supports both html-in-js and angular-in-js (`@Component({ template })`).
|
||||||
pub(super) fn format_html_doc<'a>(
|
pub(super) fn format_html_doc<'a>(
|
||||||
@@ -68,17 +62,17 @@ pub(super) fn format_html_doc<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Phase 1: Build joined text using .cooked with HTML placeholders
|
// Phase 1: Build joined text using .cooked with HTML placeholders
|
||||||
// quasis[0].cooked + "PRETTIER_HTML_PLACEHOLDER_0_0_IN_JS" + quasis[1].cooked + ...
|
// quasis[0].cooked + "PRETTIER_HTML_PLACEHOLDER_{depth}_0_IN_JS" + quasis[1].cooked + ...
|
||||||
let allocator = f.allocator();
|
let allocator = f.allocator();
|
||||||
|
let prefix = std::format!("{HTML_PLACEHOLDER_PREFIX}{}_", f.session().dispatch_depth());
|
||||||
|
let prefix = prefix.as_str();
|
||||||
let joined = {
|
let joined = {
|
||||||
let mut sb = ArenaStringBuilder::new_in(allocator);
|
let mut sb = ArenaStringBuilder::new_in(allocator);
|
||||||
for (idx, quasi_elem) in quasis.iter().enumerate() {
|
for (idx, quasi_elem) in quasis.iter().enumerate() {
|
||||||
if idx > 0 {
|
if idx > 0 {
|
||||||
sb.push_str(PLACEHOLDER_PREFIX);
|
sb.push_str(prefix);
|
||||||
let _ = std::fmt::Write::write_fmt(&mut sb, std::format_args!("{}", idx - 1));
|
let _ = std::fmt::Write::write_fmt(&mut sb, std::format_args!("{}", idx - 1));
|
||||||
sb.push('_');
|
sb.push_str(HTML_PLACEHOLDER_SUFFIX);
|
||||||
sb.push_str(COUNTER);
|
|
||||||
sb.push_str(PLACEHOLDER_SUFFIX);
|
|
||||||
}
|
}
|
||||||
let Some(cooked) = quasi_elem.value.cooked.as_ref() else {
|
let Some(cooked) = quasi_elem.value.cooked.as_ref() else {
|
||||||
return false;
|
return false;
|
||||||
@@ -103,7 +97,7 @@ pub(super) fn format_html_doc<'a>(
|
|||||||
// Formatting consumes comment state,
|
// Formatting consumes comment state,
|
||||||
// so discovering an unusable placeholder layout afterwards would make the verbatim fallback unsafe.
|
// so discovering an unusable placeholder layout afterwards would make the verbatim fallback unsafe.
|
||||||
let mut next_placeholder = 0;
|
let mut next_placeholder = 0;
|
||||||
if !placeholders_are_sequential(&ir, &mut next_placeholder)
|
if !placeholders_are_sequential(&ir, prefix, &mut next_placeholder)
|
||||||
|| next_placeholder != expressions.len()
|
|| next_placeholder != expressions.len()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -128,8 +122,8 @@ pub(super) fn format_html_doc<'a>(
|
|||||||
let ir = map_text_in_ir(ir, allocator, &mut |text, out| {
|
let ir = map_text_in_ir(ir, allocator, &mut |text, out| {
|
||||||
let escaped = super::escape_template_chars(text, allocator);
|
let escaped = super::escape_template_chars(text, allocator);
|
||||||
let text = escaped.unwrap_or(text);
|
let text = escaped.unwrap_or(text);
|
||||||
if text.contains(PLACEHOLDER_PREFIX) {
|
if super::find_ignore_ascii_case(text, prefix).is_some() {
|
||||||
let parts = super::split_on_placeholders(text, PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX);
|
let parts = super::split_on_placeholders(text, prefix, HTML_PLACEHOLDER_SUFFIX);
|
||||||
if parts.len() > 1 {
|
if parts.len() > 1 {
|
||||||
for (index, part) in parts.iter().enumerate() {
|
for (index, part) in parts.iter().enumerate() {
|
||||||
if index.is_multiple_of(2) {
|
if index.is_multiple_of(2) {
|
||||||
@@ -164,7 +158,10 @@ fn html_in_js_template(
|
|||||||
has_trailing_ws: bool,
|
has_trailing_ws: bool,
|
||||||
ws_ignore: bool,
|
ws_ignore: bool,
|
||||||
) -> HtmlInJsTemplate {
|
) -> HtmlInJsTemplate {
|
||||||
HtmlInJsTemplate { indent_multiple_roots: !(ws_ignore || has_leading_ws && has_trailing_ws) }
|
HtmlInJsTemplate {
|
||||||
|
indent_multiple_roots: !(ws_ignore || has_leading_ws && has_trailing_ws),
|
||||||
|
placeholder: (HTML_PLACEHOLDER_PREFIX, HTML_PLACEHOLDER_SUFFIX),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
@@ -224,15 +221,14 @@ fn write_html_template<'a>(
|
|||||||
/// `BestFitting` alternatives describe the same logical content,
|
/// `BestFitting` alternatives describe the same logical content,
|
||||||
/// so every variant must contain the same index sequence and contributes it only once to its parent sequence.
|
/// so every variant must contain the same index sequence and contributes it only once to its parent sequence.
|
||||||
/// On success `*next` is one past the last index seen; the caller checks it against the expression count.
|
/// On success `*next` is one past the last index seen; the caller checks it against the expression count.
|
||||||
fn placeholders_are_sequential(ir: &[FormatElement<'_>], next: &mut usize) -> bool {
|
fn placeholders_are_sequential(ir: &[FormatElement<'_>], prefix: &str, next: &mut usize) -> bool {
|
||||||
for element in ir {
|
for element in ir {
|
||||||
match element {
|
match element {
|
||||||
FormatElement::Text { text, .. } => {
|
FormatElement::Text { text, .. } => {
|
||||||
if !text.contains(PLACEHOLDER_PREFIX) {
|
if super::find_ignore_ascii_case(text, prefix).is_none() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let parts =
|
let parts = super::split_on_placeholders(text, prefix, HTML_PLACEHOLDER_SUFFIX);
|
||||||
super::split_on_placeholders(text, PLACEHOLDER_PREFIX, PLACEHOLDER_SUFFIX);
|
|
||||||
for part in parts.iter().skip(1).step_by(2) {
|
for part in parts.iter().skip(1).step_by(2) {
|
||||||
if !part.parse::<usize>().is_ok_and(|index| index == *next) {
|
if !part.parse::<usize>().is_ok_and(|index| index == *next) {
|
||||||
return false;
|
return false;
|
||||||
@@ -244,19 +240,21 @@ fn placeholders_are_sequential(ir: &[FormatElement<'_>], next: &mut usize) -> bo
|
|||||||
let start = *next;
|
let start = *next;
|
||||||
let mut variants = best_fitting.variants().iter();
|
let mut variants = best_fitting.variants().iter();
|
||||||
let Some(first) = variants.next() else { return false };
|
let Some(first) = variants.next() else { return false };
|
||||||
if !placeholders_are_sequential(first, next) {
|
if !placeholders_are_sequential(first, prefix, next) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for variant in variants {
|
for variant in variants {
|
||||||
let mut variant_next = start;
|
let mut variant_next = start;
|
||||||
if !placeholders_are_sequential(variant, &mut variant_next)
|
if !placeholders_are_sequential(variant, prefix, &mut variant_next)
|
||||||
|| variant_next != *next
|
|| variant_next != *next
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FormatElement::Interned(interned) if !placeholders_are_sequential(interned, next) => {
|
FormatElement::Interned(interned)
|
||||||
|
if !placeholders_are_sequential(interned, prefix, next) =>
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -293,12 +291,12 @@ mod tests {
|
|||||||
let allocator = Allocator::default();
|
let allocator = Allocator::default();
|
||||||
let element = best_fitting(
|
let element = best_fitting(
|
||||||
&allocator,
|
&allocator,
|
||||||
text("aPRETTIER_HTML_PLACEHOLDER_0_0_IN_JSbPRETTIER_HTML_PLACEHOLDER_1_0_IN_JSc"),
|
text("aPRETTIER_HTML_PLACEHOLDER_0_0_IN_JSbPRETTIER_HTML_PLACEHOLDER_0_1_IN_JSc"),
|
||||||
text("PRETTIER_HTML_PLACEHOLDER_0_0_IN_JS PRETTIER_HTML_PLACEHOLDER_1_0_IN_JS"),
|
text("PRETTIER_HTML_PLACEHOLDER_0_0_IN_JS PRETTIER_HTML_PLACEHOLDER_0_1_IN_JS"),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut next = 0;
|
let mut next = 0;
|
||||||
assert!(placeholders_are_sequential(&[element], &mut next));
|
assert!(placeholders_are_sequential(&[element], "PRETTIER_HTML_PLACEHOLDER_0_", &mut next));
|
||||||
assert_eq!(next, 2);
|
assert_eq!(next, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,10 +306,14 @@ mod tests {
|
|||||||
let element = best_fitting(
|
let element = best_fitting(
|
||||||
&allocator,
|
&allocator,
|
||||||
text("PRETTIER_HTML_PLACEHOLDER_0_0_IN_JS"),
|
text("PRETTIER_HTML_PLACEHOLDER_0_0_IN_JS"),
|
||||||
text("PRETTIER_HTML_PLACEHOLDER_1_0_IN_JS"),
|
text("PRETTIER_HTML_PLACEHOLDER_0_1_IN_JS"),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut next = 0;
|
let mut next = 0;
|
||||||
assert!(!placeholders_are_sequential(&[element], &mut next));
|
assert!(!placeholders_are_sequential(
|
||||||
|
&[element],
|
||||||
|
"PRETTIER_HTML_PLACEHOLDER_0_",
|
||||||
|
&mut next
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,16 +302,16 @@ fn get_angular_component_property<'a>(
|
|||||||
///
|
///
|
||||||
/// Handles both:
|
/// Handles both:
|
||||||
/// - CSS: `` `PLACEHOLDER-{N}` ``
|
/// - CSS: `` `PLACEHOLDER-{N}` ``
|
||||||
/// - HTML: `PRETTIER_HTML_PLACEHOLDER_{N}_{C}_IN_JS`
|
/// - HTML: `PRETTIER_HTML_PLACEHOLDER_{depth}_{N}_IN_JS`, the `{depth}_` passed as part of `prefix`
|
||||||
///
|
///
|
||||||
/// The optional `_{digits}` counter group between index and suffix is skipped when present,
|
/// The prefix and suffix match ASCII case-insensitively: a child may change their case
|
||||||
/// so the same function works for both formats.
|
/// (CSS lowercases a property name holding an HTML placeholder).
|
||||||
fn split_on_placeholders<'a>(text: &'a str, prefix: &str, suffix: &str) -> Vec<&'a str> {
|
fn split_on_placeholders<'a>(text: &'a str, prefix: &str, suffix: &str) -> Vec<&'a str> {
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
let mut remaining = text;
|
let mut remaining = text;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let Some(start) = remaining.find(prefix) else {
|
let Some(start) = find_ignore_ascii_case(remaining, prefix) else {
|
||||||
result.push(remaining);
|
result.push(remaining);
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
@@ -337,21 +337,14 @@ fn split_on_placeholders<'a>(text: &'a str, prefix: &str, suffix: &str) -> Vec<&
|
|||||||
}
|
}
|
||||||
|
|
||||||
let digits = &after_prefix[..digit_end];
|
let digits = &after_prefix[..digit_end];
|
||||||
let mut after_digits = &after_prefix[digit_end..];
|
let after_digits = &after_prefix[digit_end..];
|
||||||
|
|
||||||
// Skip optional `_{digits}` (e.g., HTML counter `_0`)
|
|
||||||
if let Some(after_underscore) = after_digits.strip_prefix('_') {
|
|
||||||
let counter_end = after_underscore
|
|
||||||
.bytes()
|
|
||||||
.position(|b| !b.is_ascii_digit())
|
|
||||||
.unwrap_or(after_underscore.len());
|
|
||||||
if counter_end > 0 {
|
|
||||||
after_digits = &after_underscore[counter_end..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for the suffix
|
// Check for the suffix
|
||||||
if let Some(after_suffix) = after_digits.strip_prefix(suffix) {
|
let after_suffix = after_digits
|
||||||
|
.get(..suffix.len())
|
||||||
|
.filter(|s| s.eq_ignore_ascii_case(suffix))
|
||||||
|
.map(|_| &after_digits[suffix.len()..]);
|
||||||
|
if let Some(after_suffix) = after_suffix {
|
||||||
// Valid placeholder — push the digit index
|
// Valid placeholder — push the digit index
|
||||||
result.push(digits);
|
result.push(digits);
|
||||||
remaining = after_suffix;
|
remaining = after_suffix;
|
||||||
@@ -368,6 +361,14 @@ fn split_on_placeholders<'a>(text: &'a str, prefix: &str, suffix: &str) -> Vec<&
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `haystack.find(needle)`, ASCII case-insensitively.
|
||||||
|
fn find_ignore_ascii_case(haystack: &str, needle: &str) -> Option<usize> {
|
||||||
|
haystack
|
||||||
|
.as_bytes()
|
||||||
|
.windows(needle.len())
|
||||||
|
.position(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
|
|
||||||
/// Re-escape template-literal characters (`` ` ``, `${`, `\`) in every `Text` element of an embedded IR.
|
/// Re-escape template-literal characters (`` ` ``, `${`, `\`) in every `Text` element of an embedded IR.
|
||||||
@@ -493,40 +494,3 @@ fn escape_backticks_raw<'a>(s: &'a str, allocator: &'a Allocator) -> Option<&'a
|
|||||||
result.push_str(&s[run_start..]);
|
result.push_str(&s[run_start..]);
|
||||||
Some(result.into_str())
|
Some(result.into_str())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Count placeholder occurrences matching `{prefix}{digits}(_{digits})?{suffix}`.
|
|
||||||
///
|
|
||||||
/// The optional `_{digits}` middle group lets this serve both:
|
|
||||||
/// - CSS sentinel `` `PLACEHOLDER-{N}` `` (no counter)
|
|
||||||
/// - HTML sentinel `PRETTIER_HTML_PLACEHOLDER_{N}_{C}_IN_JS` (with counter)
|
|
||||||
///
|
|
||||||
/// Mirrors the grammar that [`split_on_placeholders`] uses for substitution.
|
|
||||||
fn count_placeholders(text: &str, prefix: &str, suffix: &str) -> usize {
|
|
||||||
let mut count = 0;
|
|
||||||
let mut rest = text;
|
|
||||||
while let Some(start) = rest.find(prefix) {
|
|
||||||
let after_prefix = &rest[start + prefix.len()..];
|
|
||||||
let digit_end =
|
|
||||||
after_prefix.bytes().position(|b| !b.is_ascii_digit()).unwrap_or(after_prefix.len());
|
|
||||||
if digit_end > 0 {
|
|
||||||
let mut after_digits = &after_prefix[digit_end..];
|
|
||||||
// Skip optional `_{digits}` counter (HTML).
|
|
||||||
if let Some(after_underscore) = after_digits.strip_prefix('_') {
|
|
||||||
let counter_end = after_underscore
|
|
||||||
.bytes()
|
|
||||||
.position(|b| !b.is_ascii_digit())
|
|
||||||
.unwrap_or(after_underscore.len());
|
|
||||||
if counter_end > 0 {
|
|
||||||
after_digits = &after_underscore[counter_end..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(tail) = after_digits.strip_prefix(suffix) {
|
|
||||||
count += 1;
|
|
||||||
rest = tail;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rest = &rest[start + prefix.len()..];
|
|
||||||
}
|
|
||||||
count
|
|
||||||
}
|
|
||||||
@@ -46,8 +46,17 @@ impl<'a> FormatLiteralStringToken<'a> {
|
|||||||
let options = f.options();
|
let options = f.options();
|
||||||
let source_type = f.context().source_type();
|
let source_type = f.context().source_type();
|
||||||
|
|
||||||
let chosen_quote_style =
|
// In an HTML attribute, `'` always (Prettier's `__isInHtmlAttribute`), but a directive as usual
|
||||||
if self.jsx { options.jsx_quote_style } else { options.quote_style };
|
let in_html_attribute = !self.jsx
|
||||||
|
&& self.parent_kind != StringLiteralParentKind::Directive
|
||||||
|
&& f.context().in_html_attribute();
|
||||||
|
let chosen_quote_style = if in_html_attribute {
|
||||||
|
QuoteStyle::Single
|
||||||
|
} else if self.jsx {
|
||||||
|
options.jsx_quote_style
|
||||||
|
} else {
|
||||||
|
options.quote_style
|
||||||
|
};
|
||||||
|
|
||||||
let is_quote_needed = match options.quote_properties {
|
let is_quote_needed = match options.quote_properties {
|
||||||
QuoteProperties::AsNeeded => false,
|
QuoteProperties::AsNeeded => false,
|
||||||
@@ -55,8 +64,13 @@ impl<'a> FormatLiteralStringToken<'a> {
|
|||||||
QuoteProperties::Consistent => f.context().is_quote_needed(),
|
QuoteProperties::Consistent => f.context().is_quote_needed(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let string_cleaner =
|
let string_cleaner = LiteralStringNormalizer {
|
||||||
LiteralStringNormalizer::new(*self, chosen_quote_style, is_quote_needed, f.allocator());
|
token: *self,
|
||||||
|
chosen_quote_style,
|
||||||
|
force_chosen_quote: in_html_attribute,
|
||||||
|
is_quote_needed,
|
||||||
|
allocator: f.allocator(),
|
||||||
|
};
|
||||||
|
|
||||||
let content = string_cleaner.normalize_text(source_type);
|
let content = string_cleaner.normalize_text(source_type);
|
||||||
|
|
||||||
@@ -187,6 +201,8 @@ struct LiteralStringNormalizer<'a> {
|
|||||||
token: FormatLiteralStringToken<'a>,
|
token: FormatLiteralStringToken<'a>,
|
||||||
/// The quote that was set inside the configuration
|
/// The quote that was set inside the configuration
|
||||||
chosen_quote_style: QuoteStyle,
|
chosen_quote_style: QuoteStyle,
|
||||||
|
/// Use `chosen_quote_style` even where the other one would need fewer escapes.
|
||||||
|
force_chosen_quote: bool,
|
||||||
/// State whether we need to print the quotes or not.
|
/// State whether we need to print the quotes or not.
|
||||||
is_quote_needed: bool,
|
is_quote_needed: bool,
|
||||||
/// Arena for the strings with other quotes, so that `fmt` does not copy them again.
|
/// Arena for the strings with other quotes, so that `fmt` does not copy them again.
|
||||||
@@ -194,22 +210,16 @@ struct LiteralStringNormalizer<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> LiteralStringNormalizer<'a> {
|
impl<'a> LiteralStringNormalizer<'a> {
|
||||||
pub fn new(
|
|
||||||
token: FormatLiteralStringToken<'a>,
|
|
||||||
chosen_quote_style: QuoteStyle,
|
|
||||||
is_quote_needed: bool,
|
|
||||||
allocator: &'a Allocator,
|
|
||||||
) -> Self {
|
|
||||||
Self { token, chosen_quote_style, is_quote_needed, allocator }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn normalize_text(&self, source_type: SourceType) -> Cow<'a, str> {
|
fn normalize_text(&self, source_type: SourceType) -> Cow<'a, str> {
|
||||||
// Handle JSX attribute strings specially - they use HTML entity escaping
|
// Handle JSX attribute strings specially - they use HTML entity escaping
|
||||||
if self.token.jsx && self.token.parent_kind == StringLiteralParentKind::Expression {
|
if self.token.jsx && self.token.parent_kind == StringLiteralParentKind::Expression {
|
||||||
return self.normalize_jsx_attribute();
|
return self.normalize_jsx_attribute();
|
||||||
}
|
}
|
||||||
|
|
||||||
let str_info = self.token.compute_string_information(self.chosen_quote_style);
|
let mut str_info = self.token.compute_string_information(self.chosen_quote_style);
|
||||||
|
if self.force_chosen_quote {
|
||||||
|
str_info.preferred_quote = self.chosen_quote_style;
|
||||||
|
}
|
||||||
match self.token.parent_kind {
|
match self.token.parent_kind {
|
||||||
StringLiteralParentKind::Expression => self.normalize_string_literal(str_info),
|
StringLiteralParentKind::Expression => self.normalize_string_literal(str_info),
|
||||||
StringLiteralParentKind::Directive => self.normalize_directive(str_info),
|
StringLiteralParentKind::Directive => self.normalize_directive(str_info),
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ Output targets Prettier compatibility, but the domain is defined by what it is,
|
|||||||
|
|
||||||
Admission: core stores the service and hands it back (or applies it mechanically at finalize);
|
Admission: core stores the service and hands it back (or applies it mechanically at finalize);
|
||||||
every value crossing the closure boundary is an opaque string/vec, never a language enum or an option type, and core makes no decision from the result.
|
every value crossing the closure boundary is an opaque string/vec, never a language enum or an option type, and core makes no decision from the result.
|
||||||
`string_embedder` additionally carries an exit criterion: it is removed when (a) html gains an IR-capable formatter AND (b) the host's string-out consumers (JSDoc fences) can express their re-embedding in IR (see `apps/oxfmt`'s AGENTS.md for that half); until then it is the string-out channel's transport.
|
`string_embedder` additionally carries an exit criterion: it is removed when the host's string-out consumers (JSDoc fences) can express their re-embedding in IR (see `apps/oxfmt`'s AGENTS.md for that half; html, the other half, has an IR-capable formatter now); until then it is the string-out channel's transport.
|
||||||
|
|
||||||
- (5) `envelope/`: IR-composing behavior shared by document-envelope hosts (`write_front_matter`)
|
- (5) `envelope/`: IR-composing behavior shared by document-envelope hosts (`write_front_matter`)
|
||||||
|
|
||||||
|
|||||||
@@ -228,6 +228,12 @@ impl<'a> FormatSession<'a> {
|
|||||||
self.input_kind
|
self.input_kind
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How many dispatches deep this session is (`0` for a root):
|
||||||
|
/// nested embeds tell their own markers apart by it.
|
||||||
|
pub fn dispatch_depth(&self) -> u8 {
|
||||||
|
self.dispatch_depth
|
||||||
|
}
|
||||||
|
|
||||||
/// The string channel service ([`StringEmbedder`]), when this run installs one.
|
/// The string channel service ([`StringEmbedder`]), when this run installs one.
|
||||||
/// (A handle rather than an invoking method; see the alias for the `Err`-means-verbatim contract.)
|
/// (A handle rather than an invoking method; see the alias for the `Err`-means-verbatim contract.)
|
||||||
pub fn string_embedder(&self) -> Option<&StringEmbedder> {
|
pub fn string_embedder(&self) -> Option<&StringEmbedder> {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use std::path::Path;
|
|||||||
|
|
||||||
use oxc_allocator::Allocator;
|
use oxc_allocator::Allocator;
|
||||||
use oxc_formatter_core::{Document, FormatOptions, FormatSession, InputKind};
|
use oxc_formatter_core::{Document, FormatOptions, FormatSession, InputKind};
|
||||||
use oxc_formatter_css::{CssFormatOptions, CssVariant, format_to_ir};
|
use oxc_formatter_css::{CssEmbeddedIn, CssFormatOptions, CssVariant, format_to_ir};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut args = pico_args::Arguments::from_env();
|
let mut args = pico_args::Arguments::from_env();
|
||||||
@@ -32,7 +32,7 @@ fn main() {
|
|||||||
let allocator = Allocator::new();
|
let allocator = Allocator::new();
|
||||||
let session = FormatSession::new(&allocator, InputKind::Fragment);
|
let session = FormatSession::new(&allocator, InputKind::Fragment);
|
||||||
|
|
||||||
match format_to_ir(&session, &source_text, options, /* template_placeholders */ true) {
|
match format_to_ir(&session, &source_text, options, Some(CssEmbeddedIn::CssInJsTemplate)) {
|
||||||
Ok(embedded) => {
|
Ok(embedded) => {
|
||||||
let document = Document::new(embedded.ir, Vec::new());
|
let document = Document::new(embedded.ir, Vec::new());
|
||||||
// `elements` borrows the arena (not the document) and group modes are `Cell`s,
|
// `elements` borrows the arena (not the document) and group modes are `Cell`s,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ use oxc_formatter_core::{FormatContext, SourceText};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
comments::{Comments, CssComment},
|
comments::{Comments, CssComment},
|
||||||
|
format::CssEmbeddedIn,
|
||||||
options::CssFormatOptions,
|
options::CssFormatOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -16,9 +17,9 @@ pub struct CssFormatContext<'a> {
|
|||||||
/// Inside an ICSS rule (`:import(...)` / `:export`): property names keep
|
/// Inside an ICSS rule (`:import(...)` / `:export`): property names keep
|
||||||
/// their case (Prettier's `insideIcssRuleNode`).
|
/// their case (Prettier's `insideIcssRuleNode`).
|
||||||
in_icss_rule: std::cell::Cell<bool>,
|
in_icss_rule: std::cell::Cell<bool>,
|
||||||
/// The source may contain css-in-js `${}` placeholder markers
|
/// What the part is embedded in (embedded entry point only);
|
||||||
/// (embedded entry point only); gates the printer's placeholder handling.
|
/// css-in-js gates the printer's placeholder handling.
|
||||||
template_placeholders: bool,
|
embedded_in: Option<CssEmbeddedIn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CssFormatContext<'a> {
|
impl<'a> CssFormatContext<'a> {
|
||||||
@@ -26,7 +27,7 @@ impl<'a> CssFormatContext<'a> {
|
|||||||
options: CssFormatOptions,
|
options: CssFormatOptions,
|
||||||
source_code: &'a str,
|
source_code: &'a str,
|
||||||
comments: &'a [CssComment],
|
comments: &'a [CssComment],
|
||||||
template_placeholders: bool,
|
embedded_in: Option<CssEmbeddedIn>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
options,
|
options,
|
||||||
@@ -34,13 +35,18 @@ impl<'a> CssFormatContext<'a> {
|
|||||||
comments: Comments::new(comments),
|
comments: Comments::new(comments),
|
||||||
in_less_detached: std::cell::Cell::new(false),
|
in_less_detached: std::cell::Cell::new(false),
|
||||||
in_icss_rule: std::cell::Cell::new(false),
|
in_icss_rule: std::cell::Cell::new(false),
|
||||||
template_placeholders,
|
embedded_in,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the source may contain css-in-js `${}` placeholder markers.
|
/// Whether the source may contain css-in-js `${}` placeholder markers.
|
||||||
pub fn template_placeholders(&self) -> bool {
|
pub fn template_placeholders(&self) -> bool {
|
||||||
self.template_placeholders
|
self.embedded_in == Some(CssEmbeddedIn::CssInJsTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The source is an HTML `style` attribute's value.
|
||||||
|
pub fn is_style_attribute(&self) -> bool {
|
||||||
|
self.embedded_in == Some(CssEmbeddedIn::HtmlStyleAttribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn in_less_detached(&self) -> &std::cell::Cell<bool> {
|
pub fn in_less_detached(&self) -> &std::cell::Cell<bool> {
|
||||||
|
|||||||
@@ -64,8 +64,7 @@ pub fn format_with_session<'a>(
|
|||||||
let ParsedCss { stylesheet, comments, source, has_bom, front_matter } =
|
let ParsedCss { stylesheet, comments, source, has_bom, front_matter } =
|
||||||
parse_for_format(session.allocator(), source_text, options, CssRoot::Stylesheet)?;
|
parse_for_format(session.allocator(), source_text, options, CssRoot::Stylesheet)?;
|
||||||
|
|
||||||
let context =
|
let context = CssFormatContext::new(options, source, comments, None);
|
||||||
CssFormatContext::new(options, source, comments, /* template_placeholders */ false);
|
|
||||||
// A root `Document` owns a fresh Tailwind class scope
|
// A root `Document` owns a fresh Tailwind class scope
|
||||||
let session = session.with_new_tailwind_scope();
|
let session = session.with_new_tailwind_scope();
|
||||||
let mut state = FormatState::new_with_session(context, session.clone());
|
let mut state = FormatState::new_with_session(context, session.clone());
|
||||||
@@ -132,8 +131,8 @@ pub fn parse_for_format<'a>(
|
|||||||
/// - allocates from the session's shared arena and `GroupId` space
|
/// - allocates from the session's shared arena and `GroupId` space
|
||||||
/// - emits neither a BOM nor the trailing newline
|
/// - emits neither a BOM nor the trailing newline
|
||||||
/// - parses a fragment ([`CssRoot::Fragment`]), so root declarations are statements
|
/// - parses a fragment ([`CssRoot::Fragment`]), so root declarations are statements
|
||||||
/// - `template_placeholders` enables the css-in-js parse mode ([`CssRoot::CssInJsTemplate`]);
|
/// - `embedded_in` is what the part is embedded in, for the parse mode and print rules that depend on it ([`CssEmbeddedIn`]);
|
||||||
/// JSDoc / Markdown code blocks pass `false`
|
/// JSDoc / Markdown code blocks pass `None`
|
||||||
///
|
///
|
||||||
/// `@apply` Tailwind classes go into the session's class scope (shared with the parent),
|
/// `@apply` Tailwind classes go into the session's class scope (shared with the parent),
|
||||||
/// the parent document owns the batch sort.
|
/// the parent document owns the batch sort.
|
||||||
@@ -144,11 +143,15 @@ pub fn format_to_ir<'a>(
|
|||||||
session: &FormatSession<'a>,
|
session: &FormatSession<'a>,
|
||||||
source_text: &str,
|
source_text: &str,
|
||||||
options: CssFormatOptions,
|
options: CssFormatOptions,
|
||||||
template_placeholders: bool,
|
embedded_in: Option<CssEmbeddedIn>,
|
||||||
) -> Result<EmbeddedIr<'a>, OxcDiagnostic> {
|
) -> Result<EmbeddedIr<'a>, OxcDiagnostic> {
|
||||||
// `FormatSession::dispatch` never hands a BOM-headed input to an embedded part,
|
// `FormatSession::dispatch` never hands a BOM-headed input to an embedded part,
|
||||||
// so the BOM split inside `parse_for_format` is a no-op here.
|
// so the BOM split inside `parse_for_format` is a no-op here.
|
||||||
let root = if template_placeholders { CssRoot::CssInJsTemplate } else { CssRoot::Fragment };
|
let root = if embedded_in == Some(CssEmbeddedIn::CssInJsTemplate) {
|
||||||
|
CssRoot::CssInJsTemplate
|
||||||
|
} else {
|
||||||
|
CssRoot::Fragment
|
||||||
|
};
|
||||||
let ParsedCss { stylesheet, comments, source, front_matter, .. } =
|
let ParsedCss { stylesheet, comments, source, front_matter, .. } =
|
||||||
parse_for_format(session.allocator(), source_text, options, root)?;
|
parse_for_format(session.allocator(), source_text, options, root)?;
|
||||||
if front_matter.is_some() && !session.input_kind().owns_front_matter() {
|
if front_matter.is_some() && !session.input_kind().owns_front_matter() {
|
||||||
@@ -159,7 +162,7 @@ pub fn format_to_ir<'a>(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = CssFormatContext::new(options, source, comments, template_placeholders);
|
let context = CssFormatContext::new(options, source, comments, embedded_in);
|
||||||
let mut state = FormatState::new_with_session(context, session.clone());
|
let mut state = FormatState::new_with_session(context, session.clone());
|
||||||
let mut buffer = Formatter::new(&mut state);
|
let mut buffer = Formatter::new(&mut state);
|
||||||
|
|
||||||
@@ -168,6 +171,16 @@ pub fn format_to_ir<'a>(
|
|||||||
Ok(EmbeddedIr { ir: buffer.into_vec() })
|
Ok(EmbeddedIr { ir: buffer.into_vec() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What an embedded CSS part is embedded in (the host translates the parent's request into this).
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum CssEmbeddedIn {
|
||||||
|
/// A css-in-js template: SCSS grammar with `${}` placeholder markers ([`CssRoot::CssInJsTemplate`]).
|
||||||
|
CssInJsTemplate,
|
||||||
|
/// An HTML `style` attribute's value: its declarations print on one line when they fit,
|
||||||
|
/// the last `;` only when they do not (Prettier's `__isHTMLStyleAttribute`).
|
||||||
|
HtmlStyleAttribute,
|
||||||
|
}
|
||||||
|
|
||||||
/// What a CSS source is parsed as.
|
/// What a CSS source is parsed as.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub enum CssRoot {
|
pub enum CssRoot {
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ pub const TEMPLATE_PLACEHOLDER_SUFFIX: &str = "`";
|
|||||||
pub use crate::{
|
pub use crate::{
|
||||||
comments::CssComment,
|
comments::CssComment,
|
||||||
context::CssFormatContext,
|
context::CssFormatContext,
|
||||||
format::{CssRoot, ParsedCss, format, format_to_ir, format_with_session, parse_for_format},
|
format::{
|
||||||
|
CssEmbeddedIn, CssRoot, ParsedCss, format, format_to_ir, format_with_session,
|
||||||
|
parse_for_format,
|
||||||
|
},
|
||||||
options::{CssFormatOptions, CssVariant, SingleQuote, TrailingCommas},
|
options::{CssFormatOptions, CssVariant, SingleQuote, TrailingCommas},
|
||||||
};
|
};
|
||||||
@@ -8,7 +8,10 @@ use oxc_span::Span;
|
|||||||
|
|
||||||
use oxc_formatter_core::{
|
use oxc_formatter_core::{
|
||||||
arena_cow_str,
|
arena_cow_str,
|
||||||
builders::{block_indent, dedent, empty_line, hard_line_break, indent, space, text},
|
builders::{
|
||||||
|
block_indent, dedent, empty_line, hard_line_break, if_group_breaks, indent,
|
||||||
|
soft_line_break_or_space, space, text, token,
|
||||||
|
},
|
||||||
write,
|
write,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,6 +101,8 @@ pub(super) fn write_statement_sequence_bounded<'a>(
|
|||||||
// (`${m}\ncolor: red` -> no `;`, but `${m}\ncolor: red;\nx: 1` -> `x: 1;`).
|
// (`${m}\ncolor: red` -> no `;`, but `${m}\ncolor: red;\nx: 1` -> `x: 1;`).
|
||||||
// Track that run so swallowed declarations keep a source-driven `;`.
|
// Track that run so swallowed declarations keep a source-driven `;`.
|
||||||
let mut swallowed = false;
|
let mut swallowed = false;
|
||||||
|
// Only the attribute's own declarations, not a block's
|
||||||
|
let style_attribute = upper == u32::MAX && f.context().is_style_attribute();
|
||||||
for (i, stmt) in statements.iter().enumerate() {
|
for (i, stmt) in statements.iter().enumerate() {
|
||||||
let start = stmt_start(stmt);
|
let start = stmt_start(stmt);
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@@ -118,7 +123,12 @@ pub(super) fn write_statement_sequence_bounded<'a>(
|
|||||||
write!(f, space());
|
write!(f, space());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
write!(f, hard_line_break());
|
// A `style` attribute's declarations share a line when they fit
|
||||||
|
if style_attribute {
|
||||||
|
write!(f, soft_line_break_or_space());
|
||||||
|
} else {
|
||||||
|
write!(f, hard_line_break());
|
||||||
|
}
|
||||||
if gap == Gap::Blank {
|
if gap == Gap::Blank {
|
||||||
write!(f, empty_line());
|
write!(f, empty_line());
|
||||||
}
|
}
|
||||||
@@ -145,6 +155,13 @@ pub(super) fn write_statement_sequence_bounded<'a>(
|
|||||||
if has_source_semicolon {
|
if has_source_semicolon {
|
||||||
write!(f, ";");
|
write!(f, ";");
|
||||||
}
|
}
|
||||||
|
} else if style_attribute
|
||||||
|
&& i + 1 == statements.len()
|
||||||
|
&& let Statement::Declaration(decl) = stmt
|
||||||
|
{
|
||||||
|
// The last declaration's `;` only when the attribute breaks
|
||||||
|
write_declaration(decl, f);
|
||||||
|
write!(f, if_group_breaks(&token(";")));
|
||||||
} else {
|
} else {
|
||||||
write_statement(stmt, f);
|
write_statement(stmt, f);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -86,7 +86,10 @@ fn format_embedded(source: &str, options: CssFormatOptions) -> String {
|
|||||||
let session =
|
let session =
|
||||||
oxc_formatter_core::FormatSession::new(&allocator, oxc_formatter_core::InputKind::Fragment);
|
oxc_formatter_core::FormatSession::new(&allocator, oxc_formatter_core::InputKind::Fragment);
|
||||||
let embedded = oxc_formatter_css::format_to_ir(
|
let embedded = oxc_formatter_css::format_to_ir(
|
||||||
&session, source, options, /* template_placeholders */ true,
|
&session,
|
||||||
|
source,
|
||||||
|
options,
|
||||||
|
Some(oxc_formatter_css::CssEmbeddedIn::CssInJsTemplate),
|
||||||
)
|
)
|
||||||
.expect("format should succeed");
|
.expect("format should succeed");
|
||||||
// Simulate the host: replace each typed placeholder with the canonical
|
// Simulate the host: replace each typed placeholder with the canonical
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ which changes what `pre` renders and what `textarea` submits.
|
|||||||
|
|
||||||
- Why: uniform-rule (same construct, same output: the doctype of a `.html` file, which Prettier lowercases)
|
- Why: uniform-rule (same construct, same output: the doctype of a `.html` file, which Prettier lowercases)
|
||||||
- Pin: `tests/fixtures/vue/doctype-case.vue`
|
- Pin: `tests/fixtures/vue/doctype-case.vue`
|
||||||
|
- Oxfmt: `externals/mdn-learn/css_layout/multiple-column_layout/index.md`, `externals/mdn-learn/css_layout/positioning/index.md`
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
@@ -189,6 +190,7 @@ Prettier's srcset parser treats a zero density as absent and drops it.
|
|||||||
- Why: semantics (whitespace added on both sides of a `script` renders between its inline neighbors)
|
- Why: semantics (whitespace added on both sides of a `script` renders between its inline neighbors)
|
||||||
- Pin: `tests/fixtures/html/script-display-none.html`
|
- Pin: `tests/fixtures/html/script-display-none.html`
|
||||||
- Conformance: `html/tags/tags.html`
|
- Conformance: `html/tags/tags.html`
|
||||||
|
- Oxfmt: `externals/prettier/html/script/script.html`
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
@@ -277,6 +279,7 @@ With `htmlWhitespaceSensitivity: "ignore"` they stay blocks, as HTML inline elem
|
|||||||
- Why: semantics (whitespace added on both sides of a `dialog` renders between its inline neighbors)
|
- Why: semantics (whitespace added on both sides of a `dialog` renders between its inline neighbors)
|
||||||
- Pin: `tests/fixtures/html/dialog-out-of-flow.html`
|
- Pin: `tests/fixtures/html/dialog-out-of-flow.html`
|
||||||
- Conformance: `html/tags/tags2.html`
|
- Conformance: `html/tags/tags2.html`
|
||||||
|
- Oxfmt: `externals/prettier/html/tags/tags2.html`
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
@@ -435,6 +438,7 @@ An email template pads its preview text this way, NBSP alternating with zero-wid
|
|||||||
- Why: semantics (an end tag added where a fragment ends changes the page it is joined into; prettier/prettier#17761, prettier/prettier#5665)
|
- 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`
|
- Pin: `tests/fixtures/html/omitted-end-tag-at-eof.html`
|
||||||
- Conformance: `html/basics/broken-html.html`, `html/prettier_ignore/unclosed2.html`
|
- Conformance: `html/basics/broken-html.html`, `html/prettier_ignore/unclosed2.html`
|
||||||
|
- Oxfmt: `externals/prettier/html/basics/broken-html.html`, `externals/prettier/html/prettier_ignore/unclosed2.html`
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<!-- input (header.html, closed by footer.html) -->
|
<!-- input (header.html, closed by footer.html) -->
|
||||||
@@ -568,6 +572,7 @@ Vue and Angular keep every name as written, as Prettier does.
|
|||||||
- Why: uniform-rule (same construct, same output: `<html:p>` and every other prefixed name, which Prettier lays out as an unknown element)
|
- 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`
|
- Pin: `tests/fixtures/html/prefixed-script-style.html`
|
||||||
- Conformance: `html/basics/with-colon.html`
|
- Conformance: `html/basics/with-colon.html`
|
||||||
|
- Oxfmt: `externals/prettier/html/basics/with-colon.html`
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
@@ -597,6 +602,7 @@ Angular's compiler reads a prefix as a namespace (`svg:rect`), and so does the A
|
|||||||
|
|
||||||
- Why: semantics (Prettier ends an unquoted value at `/`: `src=a.png/` becomes `src="a.png"`, another URL)
|
- 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`
|
- Pin: `tests/fixtures/html/unquoted-value-with-slash.html`
|
||||||
|
- Oxfmt: `externals/mdn-learn/structuring_content/basic_html_syntax/index.md`, `externals/vite/html/valid.html`
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<!-- input -->
|
<!-- input -->
|
||||||
|
|||||||
@@ -1,17 +1,26 @@
|
|||||||
use oxc_formatter_core::{FormatContext, SourceText};
|
use oxc_formatter_core::{FormatContext, SourceText};
|
||||||
|
|
||||||
use crate::{options::HtmlishFormatOptions, tree::Tree};
|
use crate::{format::HtmlishEmbeddedIn, options::HtmlishFormatOptions, tree::Tree};
|
||||||
|
|
||||||
/// Formatting context for htmlish.
|
/// Formatting context for htmlish.
|
||||||
pub struct HtmlishFormatContext<'a> {
|
pub struct HtmlishFormatContext<'a> {
|
||||||
options: HtmlishFormatOptions,
|
options: HtmlishFormatOptions,
|
||||||
source_text: SourceText<'a>,
|
source_text: SourceText<'a>,
|
||||||
tree: &'a Tree<'a>,
|
tree: &'a Tree<'a>,
|
||||||
|
embedded_in: Option<HtmlishEmbeddedIn>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> HtmlishFormatContext<'a> {
|
impl<'a> HtmlishFormatContext<'a> {
|
||||||
pub fn new(options: HtmlishFormatOptions, tree: &'a Tree<'a>) -> Self {
|
pub fn new(
|
||||||
Self { options, source_text: SourceText::new(tree.source()), tree }
|
options: HtmlishFormatOptions,
|
||||||
|
tree: &'a Tree<'a>,
|
||||||
|
embedded_in: Option<HtmlishEmbeddedIn>,
|
||||||
|
) -> Self {
|
||||||
|
Self { options, source_text: SourceText::new(tree.source()), tree, embedded_in }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn embedded_in(&self) -> Option<HtmlishEmbeddedIn> {
|
||||||
|
self.embedded_in
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The preprocess table, with the arena lifetime so it outlives borrows of the formatter.
|
/// The preprocess table, with the arena lifetime so it outlives borrows of the formatter.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use oxc_allocator::Allocator;
|
|||||||
use oxc_diagnostics::OxcDiagnostic;
|
use oxc_diagnostics::OxcDiagnostic;
|
||||||
use oxc_formatter_core::{
|
use oxc_formatter_core::{
|
||||||
Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, Formatter, InputKind,
|
Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, Formatter, InputKind,
|
||||||
builders::{empty_line, hard_line_break, text},
|
builders::{empty_line, hard_line_break, indent, text},
|
||||||
spec::{FrontMatter, blank_front_matter, parse_front_matter, split_bom},
|
spec::{FrontMatter, blank_front_matter, parse_front_matter, split_bom},
|
||||||
write, write_front_matter,
|
write, write_front_matter,
|
||||||
};
|
};
|
||||||
@@ -14,7 +14,7 @@ use oxc_htmlish_parser::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
context::HtmlishFormatContext,
|
context::HtmlishFormatContext,
|
||||||
options::HtmlishFormatOptions,
|
options::HtmlishFormatOptions,
|
||||||
print::{self, HtmlishFormatter},
|
print::{self, HtmlishFormatter, format_with},
|
||||||
tree::Tree,
|
tree::Tree,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ pub fn format_with_session<'a>(
|
|||||||
);
|
);
|
||||||
let parsed = parse_for_format(session.allocator(), source_text, &options)?;
|
let parsed = parse_for_format(session.allocator(), source_text, &options)?;
|
||||||
|
|
||||||
let context = build_context(session.allocator(), &parsed, options, true);
|
let context = build_context(session.allocator(), &parsed, options, true, None);
|
||||||
// A root `Document` owns a fresh Tailwind class scope
|
// A root `Document` owns a fresh Tailwind class scope
|
||||||
let session = session.with_new_tailwind_scope();
|
let session = session.with_new_tailwind_scope();
|
||||||
let mut state = FormatState::new_with_session(context, session.clone());
|
let mut state = FormatState::new_with_session(context, session.clone());
|
||||||
@@ -114,6 +114,7 @@ pub fn format_to_ir<'a>(
|
|||||||
session: &FormatSession<'a>,
|
session: &FormatSession<'a>,
|
||||||
source_text: &str,
|
source_text: &str,
|
||||||
options: HtmlishFormatOptions,
|
options: HtmlishFormatOptions,
|
||||||
|
embedded_in: Option<HtmlishEmbeddedIn>,
|
||||||
) -> Result<EmbeddedIr<'a>, OxcDiagnostic> {
|
) -> Result<EmbeddedIr<'a>, OxcDiagnostic> {
|
||||||
let allocator = session.allocator();
|
let allocator = session.allocator();
|
||||||
// `FormatSession::dispatch` never hands a BOM-headed input to an embedded part
|
// `FormatSession::dispatch` never hands a BOM-headed input to an embedded part
|
||||||
@@ -125,7 +126,7 @@ pub fn format_to_ir<'a>(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = build_context(allocator, &parsed, options, false);
|
let context = build_context(allocator, &parsed, options, false, embedded_in);
|
||||||
let mut state = FormatState::new_with_session(context, session.clone());
|
let mut state = FormatState::new_with_session(context, session.clone());
|
||||||
let mut buffer = Formatter::new(&mut state);
|
let mut buffer = Formatter::new(&mut state);
|
||||||
|
|
||||||
@@ -134,6 +135,38 @@ pub fn format_to_ir<'a>(
|
|||||||
Ok(EmbeddedIr { ir: buffer.into_vec() })
|
Ok(EmbeddedIr { ir: buffer.into_vec() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What the HTML is embedded in, for the print rules that depend on it.
|
||||||
|
///
|
||||||
|
/// The host translates the parent's request into this;
|
||||||
|
/// `None` is a part formatted on its own (e.g. a JSDoc code block).
|
||||||
|
/// Embedded HTML leaves `class` and `allow` values as written (as Prettier does with a `parentParser`).
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct HtmlishEmbeddedIn {
|
||||||
|
/// Indent the content when more than one node prints at the root
|
||||||
|
/// (html-in-js, whose backticks hug the content).
|
||||||
|
pub indent_multiple_roots: bool,
|
||||||
|
/// The parent's placeholder for an expression (html-in-js's `${}`), as the prefix and suffix around its ASCII digits and `_`:
|
||||||
|
/// a value that is one stays unquoted (`attr=${value}` passes a value, not a string),
|
||||||
|
/// and a value holding one is no class list to sort.
|
||||||
|
pub placeholder: Option<(&'static str, &'static str)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlishEmbeddedIn {
|
||||||
|
/// `value` is the parent's placeholder as a whole.
|
||||||
|
pub(crate) fn is_placeholder(&self, value: &str) -> bool {
|
||||||
|
self.placeholder.is_some_and(|(prefix, suffix)| {
|
||||||
|
value.strip_prefix(prefix).and_then(|id| id.strip_suffix(suffix)).is_some_and(|id| {
|
||||||
|
!id.is_empty() && id.bytes().all(|b| b.is_ascii_digit() || b == b'_')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `value` holds the parent's placeholder.
|
||||||
|
pub(crate) fn has_placeholder(&self, value: &str) -> bool {
|
||||||
|
self.placeholder.is_some_and(|(prefix, _)| value.contains(prefix))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse the source into the AST, bailing out on any diagnostic.
|
/// 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`.
|
/// Copies the source into the arena so every slice taken from it carries `'a`.
|
||||||
@@ -168,6 +201,7 @@ fn build_context<'a>(
|
|||||||
parsed: &ParsedHtmlish<'a>,
|
parsed: &ParsedHtmlish<'a>,
|
||||||
options: HtmlishFormatOptions,
|
options: HtmlishFormatOptions,
|
||||||
standalone: bool,
|
standalone: bool,
|
||||||
|
embedded_in: Option<HtmlishEmbeddedIn>,
|
||||||
) -> HtmlishFormatContext<'a> {
|
) -> HtmlishFormatContext<'a> {
|
||||||
let tree = allocator.alloc(Tree::build(
|
let tree = allocator.alloc(Tree::build(
|
||||||
allocator,
|
allocator,
|
||||||
@@ -177,7 +211,7 @@ fn build_context<'a>(
|
|||||||
options.variant,
|
options.variant,
|
||||||
standalone,
|
standalone,
|
||||||
));
|
));
|
||||||
HtmlishFormatContext::new(options, tree)
|
HtmlishFormatContext::new(options, tree, embedded_in)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emits the front matter and the nodes with the final newline.
|
/// Emits the front matter and the nodes with the final newline.
|
||||||
@@ -213,16 +247,25 @@ impl<'a> Format<'a, HtmlishFormatContext<'a>> for FormatHtmlishEmbedded<'_, 'a>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The front matter (a blank line after it when a body follows) and the nodes.
|
/// The front matter (a blank line after it when a body follows) and the nodes,
|
||||||
|
/// indented when the parent asks for it and more than one prints at the root.
|
||||||
fn write_document<'a>(parsed: &ParsedHtmlish<'a>, f: &mut HtmlishFormatter<'_, 'a>) {
|
fn write_document<'a>(parsed: &ParsedHtmlish<'a>, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||||
let has_body = f.context().tree()[NodeId::DOCUMENT].first_child.is_some();
|
let document = &f.context().tree()[NodeId::DOCUMENT];
|
||||||
|
let has_body = document.first_child.is_some();
|
||||||
if let Some(fm) = &parsed.front_matter {
|
if let Some(fm) = &parsed.front_matter {
|
||||||
write_front_matter(fm, &["yaml", "toml"], f);
|
write_front_matter(fm, &["yaml", "toml"], f);
|
||||||
if has_body {
|
if has_body {
|
||||||
write!(f, empty_line());
|
write!(f, empty_line());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if has_body {
|
if !has_body {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let indent_body = f.context().embedded_in().is_some_and(|e| e.indent_multiple_roots)
|
||||||
|
&& document.only_child().is_none();
|
||||||
|
if indent_body {
|
||||||
|
write!(f, indent(&format_with(print::write_root)));
|
||||||
|
} else {
|
||||||
print::write_root(f);
|
print::write_root(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,10 @@ mod tree;
|
|||||||
|
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
context::HtmlishFormatContext,
|
context::HtmlishFormatContext,
|
||||||
format::{ParsedHtmlish, format, format_to_ir, format_with_session, parse_for_format},
|
format::{
|
||||||
|
HtmlishEmbeddedIn, ParsedHtmlish, format, format_to_ir, format_with_session,
|
||||||
|
parse_for_format,
|
||||||
|
},
|
||||||
options::{HtmlishFormatOptions, HtmlishVariant, WhitespaceSensitivity},
|
options::{HtmlishFormatOptions, HtmlishVariant, WhitespaceSensitivity},
|
||||||
|
print::{CssInHtmlStyleAttribute, JsInHtmlEventHandler},
|
||||||
};
|
};
|
||||||
@@ -29,7 +29,7 @@ impl HtmlishVariant {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Format options for htmlish (HTML and the template languages built on it).
|
/// Format options for htmlish (HTML and the template languages built on it).
|
||||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
|
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||||
pub struct HtmlishFormatOptions {
|
pub struct HtmlishFormatOptions {
|
||||||
pub indent_style: IndentStyle,
|
pub indent_style: IndentStyle,
|
||||||
pub indent_width: IndentWidth,
|
pub indent_width: IndentWidth,
|
||||||
@@ -42,6 +42,9 @@ pub struct HtmlishFormatOptions {
|
|||||||
pub bracket_same_line: bool,
|
pub bracket_same_line: bool,
|
||||||
/// Mirrors Prettier's `singleAttributePerLine`.
|
/// Mirrors Prettier's `singleAttributePerLine`.
|
||||||
pub single_attribute_per_line: bool,
|
pub single_attribute_per_line: bool,
|
||||||
|
/// Sort Tailwind CSS classes (by the session's sorter, as prettier-plugin-tailwindcss does):
|
||||||
|
/// those of `class`, and of the attributes listed here; `None` leaves them as they are.
|
||||||
|
pub sort_tailwindcss: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Which whitespace between elements is significant.
|
/// Which whitespace between elements is significant.
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
//! Attributes: the list in the start tag (line breaks, `prettier-ignore-attribute`),
|
//! 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.
|
//! 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 std::{any::Any, borrow::Cow};
|
||||||
|
|
||||||
use cow_utils::CowUtils;
|
use cow_utils::CowUtils;
|
||||||
|
|
||||||
use oxc_formatter_core::{
|
use oxc_formatter_core::{
|
||||||
Format, arena_cow_str,
|
Format, FormatElement, arena_cow_str,
|
||||||
builders::{
|
builders::{
|
||||||
group, hard_line_break, if_group_breaks, if_group_fits_on_line, indent, soft_block_indent,
|
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,
|
soft_line_break, soft_line_break_or_space, space, text,
|
||||||
@@ -14,9 +14,16 @@ use oxc_formatter_core::{
|
|||||||
spec::is_suppression_marker,
|
spec::is_suppression_marker,
|
||||||
write,
|
write,
|
||||||
};
|
};
|
||||||
use oxc_htmlish_parser::ast::{Attribute, NodeId, NodeKind};
|
use oxc_htmlish_parser::ast::{Attribute, NodeId, NodeKind, Quote};
|
||||||
|
|
||||||
use super::{HtmlishFormatter, format_with, tag::opening_end_borrowed};
|
use super::{
|
||||||
|
HtmlishFormatter,
|
||||||
|
embed::{
|
||||||
|
CssInHtmlStyleAttribute, JsInHtmlEventHandler, is_event_handler, write_attribute_value,
|
||||||
|
},
|
||||||
|
format_with,
|
||||||
|
tag::opening_end_borrowed,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
context::HtmlishFormatContext,
|
context::HtmlishFormatContext,
|
||||||
tree::{Tree, collapse_whitespace},
|
tree::{Tree, collapse_whitespace},
|
||||||
@@ -46,12 +53,13 @@ pub fn write_attributes<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatte
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let options = *f.options();
|
let (single_attribute_per_line, bracket_same_line) =
|
||||||
|
(f.options().single_attribute_per_line, f.options().bracket_same_line);
|
||||||
|
|
||||||
let force_not_to_break = t.tag(id) == "script"
|
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"))
|
&& matches!(el.attributes.as_slice(), [only] if t.attribute_is(only.name.slice(t.source()), "src"))
|
||||||
&& item.first_child.is_none();
|
&& item.first_child.is_none();
|
||||||
let per_line = options.single_attribute_per_line && el.attributes.len() > 1;
|
let per_line = single_attribute_per_line && el.attributes.len() > 1;
|
||||||
let ignored = ignored_attributes(t, id);
|
let ignored = ignored_attributes(t, id);
|
||||||
let is_ignored = |name: &str| {
|
let is_ignored = |name: &str| {
|
||||||
ignored.is_some_and(|names| {
|
ignored.is_some_and(|names| {
|
||||||
@@ -88,7 +96,7 @@ pub fn write_attributes<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatte
|
|||||||
let hug_end = opening_end_borrowed(t, id)
|
let hug_end = opening_end_borrowed(t, id)
|
||||||
|| (item.self_closing && t[t.parent(id)].borrows.last_child_closing_end)
|
|| (item.self_closing && t[t.parent(id)].borrows.last_child_closing_end)
|
||||||
|| force_not_to_break;
|
|| force_not_to_break;
|
||||||
if hug_end || options.bracket_same_line {
|
if hug_end || bracket_same_line {
|
||||||
if slash {
|
if slash {
|
||||||
write!(f, space());
|
write!(f, space());
|
||||||
}
|
}
|
||||||
@@ -110,13 +118,43 @@ fn write_attribute<'a>(
|
|||||||
let is = |known| t.attribute_is(name, known);
|
let is = |known| t.attribute_is(name, known);
|
||||||
write!(f, text(t.attribute_name(element, name)));
|
write!(f, text(t.attribute_name(element, name)));
|
||||||
let Some(value) = attribute.value else { return };
|
let Some(value) = attribute.value else { return };
|
||||||
|
let quoted = value.quote != Quote::None;
|
||||||
let value = value.span.slice(source);
|
let value = value.span.slice(source);
|
||||||
write!(f, "=");
|
write!(f, "=");
|
||||||
|
|
||||||
|
let embedded_in = f.context().embedded_in();
|
||||||
|
if !quoted && embedded_in.is_some_and(|e| e.is_placeholder(value)) {
|
||||||
|
write!(f, text(value));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A placeholder would hide in the sorted class list, out of the parent's reach
|
||||||
|
let tailwind = f.options().sort_tailwindcss.as_ref().is_some_and(|attributes| {
|
||||||
|
is("class") || attributes.iter().any(|attribute| attribute == name)
|
||||||
|
}) && !embedded_in.is_some_and(|e| e.has_placeholder(value));
|
||||||
|
if tailwind && write_tailwind_classes(value, is("class") && embedded_in.is_none(), f) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// `{{` may be a template's interpolation: those values print as written.
|
||||||
|
// Prettier formats the values below only in a document of its own.
|
||||||
|
let reformat = embedded_in.is_none() && !value.contains("{{");
|
||||||
|
let embedded = if !reformat {
|
||||||
|
None
|
||||||
|
} else if is("style") {
|
||||||
|
Some(("css", &CssInHtmlStyleAttribute as &dyn Any))
|
||||||
|
} else if is_event_handler(name) {
|
||||||
|
Some(("js", &JsInHtmlEventHandler as &dyn Any))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some((language, parent_context)) = embedded
|
||||||
|
&& write_attribute_value(language, parent_context, unescaped(value, f), f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
let tag = t.tag(element);
|
let tag = t.tag(element);
|
||||||
// A class list that collapses to nothing prints as written;
|
// A class list that collapses to nothing prints as written;
|
||||||
// the names are split at ASCII whitespace only (DIVERGENCES.md#class-ascii-whitespace).
|
// the names are split at ASCII whitespace only (DIVERGENCES.md#class-ascii-whitespace).
|
||||||
if is("class") && !value.contains("{{") && !value.trim_ascii().is_empty() {
|
if is("class") && reformat && !value.trim_ascii().is_empty() {
|
||||||
let names = collapse_whitespace(f.allocator(), unescaped(value, f));
|
let names = collapse_whitespace(f.allocator(), unescaped(value, f));
|
||||||
write!(f, ["\"", text(quoted_piece(names, f)), "\""]);
|
write!(f, ["\"", text(quoted_piece(names, f)), "\""]);
|
||||||
} else if is("srcset")
|
} else if is("srcset")
|
||||||
@@ -124,13 +162,34 @@ fn write_attribute<'a>(
|
|||||||
&& let Some(candidates) = parse_srcset(unescaped(value, f))
|
&& let Some(candidates) = parse_srcset(unescaped(value, f))
|
||||||
{
|
{
|
||||||
write_srcset(&candidates, f);
|
write_srcset(&candidates, f);
|
||||||
} else if is("allow") && tag == "iframe" && !value.contains("{{") {
|
} else if is("allow") && tag == "iframe" && reformat {
|
||||||
write_permissions_policy(unescaped(value, f), f);
|
write_permissions_policy(unescaped(value, f), f);
|
||||||
} else {
|
} else {
|
||||||
write!(f, AttributeValue(value));
|
write!(f, AttributeValue(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A class list for the session's Tailwind sorter, which collapses its whitespace unless told to preserve it;
|
||||||
|
/// `class` collapses it anyway when printed on its own (`collapse`).
|
||||||
|
/// Returns `false` for a value the sorter leaves alone (`{{`, whitespace only),
|
||||||
|
/// or holding a `"`, whose quote is chosen by content.
|
||||||
|
fn write_tailwind_classes<'a>(
|
||||||
|
value: &'a str,
|
||||||
|
collapse: bool,
|
||||||
|
f: &mut HtmlishFormatter<'_, 'a>,
|
||||||
|
) -> bool {
|
||||||
|
let value = unescaped(value, f);
|
||||||
|
if value.contains('"') || value.contains("{{") || value.trim_ascii().is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let classes = if collapse { collapse_whitespace(f.allocator(), value) } else { value };
|
||||||
|
let index = f.session().add_tailwind_class(classes.to_string());
|
||||||
|
write!(f, "\"");
|
||||||
|
f.write_element(FormatElement::TailwindClass(index));
|
||||||
|
write!(f, "\"");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// A quoted attribute value: `"` unless the value holds more `"` than `'`, the chosen quote escaped.
|
/// 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.
|
/// Quote entities are read back first, so the choice does not depend on how the source quoted.
|
||||||
pub struct AttributeValue<'a>(pub &'a str);
|
pub struct AttributeValue<'a>(pub &'a str);
|
||||||
@@ -159,12 +218,12 @@ fn unescaped<'a>(value: &'a str, f: &HtmlishFormatter<'_, 'a>) -> &'a str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `"` re-escaped for a value printed in `"`.
|
/// `"` re-escaped for a value printed in `"`.
|
||||||
fn quoted_piece<'a>(piece: &'a str, f: &HtmlishFormatter<'_, 'a>) -> &'a str {
|
pub(super) fn quoted_piece<'a>(piece: &'a str, f: &HtmlishFormatter<'_, 'a>) -> &'a str {
|
||||||
arena_cow_str(&piece.cow_replace('"', """), f)
|
arena_cow_str(&piece.cow_replace('"', """), f)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prettier's `printExpand`: `content` on its own indented lines when the group breaks.
|
/// Prettier's `printExpand`: `content` on its own indented lines when the group breaks.
|
||||||
fn write_expanded<'a>(
|
pub(super) fn write_expanded<'a>(
|
||||||
content: &impl Format<'a, HtmlishFormatContext<'a>>,
|
content: &impl Format<'a, HtmlishFormatContext<'a>>,
|
||||||
f: &mut HtmlishFormatter<'_, 'a>,
|
f: &mut HtmlishFormatter<'_, 'a>,
|
||||||
) {
|
) {
|
||||||
@@ -231,7 +290,7 @@ fn parse_srcset(input: &str) -> Option<Vec<Candidate<'_>>> {
|
|||||||
url = url.trim_end_matches(',');
|
url = url.trim_end_matches(',');
|
||||||
""
|
""
|
||||||
} else {
|
} else {
|
||||||
// Descriptors up to the next `,` outside parentheses.
|
// Descriptors up to the next `,` outside parentheses
|
||||||
rest = rest.trim_ascii_start();
|
rest = rest.trim_ascii_start();
|
||||||
let mut depth = 0;
|
let mut depth = 0;
|
||||||
let mut end = rest.len();
|
let mut end = rest.len();
|
||||||
@@ -258,7 +317,7 @@ fn parse_srcset(input: &str) -> Option<Vec<Candidate<'_>>> {
|
|||||||
'w' | 'h' if value.bytes().all(|b| b.is_ascii_digit()) => {
|
'w' | 'h' if value.bytes().all(|b| b.is_ascii_digit()) => {
|
||||||
value.parse::<f64>().ok().filter(|&v| v > 0.0)
|
value.parse::<f64>().ok().filter(|&v| v > 0.0)
|
||||||
}
|
}
|
||||||
// A zero density is kept (DIVERGENCES.md#srcset-zero-density).
|
// 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),
|
'x' if is_floating_point(value) => value.parse::<f64>().ok().filter(|&v| v >= 0.0),
|
||||||
_ => None,
|
_ => None,
|
||||||
}?;
|
}?;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Angular control flow blocks: transparent, their markers print no box (`@if (a) {` … `}`).
|
//! Angular control flow blocks: transparent, their markers print no box (`@if (a) {` … `}`).
|
||||||
|
|
||||||
|
use oxc_allocator::{Allocator, StringBuilder};
|
||||||
use oxc_formatter_core::{
|
use oxc_formatter_core::{
|
||||||
builders::{hard_line_break, indent, text},
|
builders::{hard_line_break, indent, text},
|
||||||
write,
|
write,
|
||||||
@@ -11,7 +12,6 @@ use super::{
|
|||||||
children::{Between, write_children},
|
children::{Between, write_children},
|
||||||
format_with,
|
format_with,
|
||||||
};
|
};
|
||||||
use oxc_allocator::{Allocator, StringBuilder};
|
|
||||||
|
|
||||||
use crate::tree::{Tree, collapse_whitespace};
|
use crate::tree::{Tree, collapse_whitespace};
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ use crate::tree::{Tree, collapse_whitespace};
|
|||||||
/// a space before a block that continues the previous one (`} @else {`), a line break otherwise.
|
/// 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 {
|
pub fn between_block(t: &Tree<'_>, prev: NodeId, next: NodeId) -> Between {
|
||||||
let (p, n) = (&t[prev], &t[next]);
|
let (p, n) = (&t[prev], &t[next]);
|
||||||
// The run continues into a block's content past its marker.
|
// The run continues into a block's content past its marker
|
||||||
let spaces = p.has_trailing_spaces
|
let spaces = p.has_trailing_spaces
|
||||||
|| n.has_leading_spaces
|
|| n.has_leading_spaces
|
||||||
|| (t.is_block(prev) && p.last_child.is_some_and(|last| t[last].has_trailing_spaces))
|
|| (t.is_block(prev) && p.last_child.is_some_and(|last| t[last].has_trailing_spaces))
|
||||||
@@ -67,13 +67,13 @@ pub fn write_block<'a>(
|
|||||||
let default_never = name == "default never";
|
let default_never = name == "default never";
|
||||||
write!(f, ["@", text(name)]);
|
write!(f, ["@", text(name)]);
|
||||||
if !block.parameters.is_empty() {
|
if !block.parameters.is_empty() {
|
||||||
// `@default never(state)`: Prettier glues the parameters to `never`.
|
// `@default never(state)`: Prettier glues the parameters to `never`
|
||||||
write!(f, if default_never { "(" } else { " (" });
|
write!(f, if default_never { "(" } else { " (" });
|
||||||
for (i, parameter) in block.parameters.iter().enumerate() {
|
for (i, parameter) in block.parameters.iter().enumerate() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
write!(f, "; ");
|
write!(f, "; ");
|
||||||
}
|
}
|
||||||
// As written until expressions are formatted, on one line.
|
// As written until expressions are formatted, on one line
|
||||||
let parameter =
|
let parameter =
|
||||||
collapse_whitespace_outside_quotes(f.allocator(), parameter.slice(t.source()));
|
collapse_whitespace_outside_quotes(f.allocator(), parameter.slice(t.source()));
|
||||||
write!(f, text(parameter));
|
write!(f, text(parameter));
|
||||||
@@ -81,7 +81,7 @@ pub fn write_block<'a>(
|
|||||||
write!(f, ")");
|
write!(f, ")");
|
||||||
}
|
}
|
||||||
if block.close.is_none() {
|
if block.close.is_none() {
|
||||||
// `@default never;`; a fall-through `@case (a)` has nothing more.
|
// `@default never;`; a fall-through `@case (a)` has nothing more
|
||||||
if default_never {
|
if default_never {
|
||||||
write!(f, ";");
|
write!(f, ";");
|
||||||
}
|
}
|
||||||
@@ -104,13 +104,14 @@ pub fn write_block<'a>(
|
|||||||
write!(f, hard_line_break());
|
write!(f, hard_line_break());
|
||||||
}
|
}
|
||||||
} else if item.next.is_some_and(|next| continues_block(t, id, next)) {
|
} 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.
|
// `@if (a) {⏎} @else {`, as Prettier prints a closing `}` before the next block
|
||||||
write!(f, hard_line_break());
|
write!(f, hard_line_break());
|
||||||
}
|
}
|
||||||
write!(f, "}");
|
write!(f, "}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`collapse_whitespace`] outside string literals (`'`, `"`, `` ` ``, a `\\` escaping the next character).
|
/// [`collapse_whitespace`] outside string literals
|
||||||
|
/// (`'`, `"`, `` ` ``, a `\\` escaping the next character).
|
||||||
fn collapse_whitespace_outside_quotes<'a>(allocator: &'a Allocator, text: &'a str) -> &'a str {
|
fn collapse_whitespace_outside_quotes<'a>(allocator: &'a Allocator, text: &'a str) -> &'a str {
|
||||||
if !text.contains(['\'', '"', '`']) {
|
if !text.contains(['\'', '"', '`']) {
|
||||||
return collapse_whitespace(allocator, text);
|
return collapse_whitespace(allocator, text);
|
||||||
|
|||||||
@@ -124,7 +124,8 @@ fn between(t: &Tree<'_>, prev: NodeId, next: NodeId) -> Between {
|
|||||||
if !n.has_leading_spaces {
|
if !n.has_leading_spaces {
|
||||||
return Between::Soft;
|
return Between::Soft;
|
||||||
}
|
}
|
||||||
// The compiler layer: between two elements, whitespace that renders keeps its kind (DIVERGENCES.md#vue-element-gap-kind).
|
// 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() {
|
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 };
|
return if has_trailing_line_break(t, prev) { Between::Hard } else { Between::Space };
|
||||||
}
|
}
|
||||||
@@ -148,7 +149,7 @@ pub fn write_children<'a>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each pair's separator and blank-line fact, computed once and carried to the next child.
|
// Each pair's separator and blank-line fact, computed once and carried to the next child
|
||||||
let mut carried = (Between::Nothing, false);
|
let mut carried = (Between::Nothing, false);
|
||||||
let mut prev_group_id: Option<GroupId> = None;
|
let mut prev_group_id: Option<GroupId> = None;
|
||||||
for child in t.children(id) {
|
for child in t.children(id) {
|
||||||
@@ -168,12 +169,12 @@ pub fn write_children<'a>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A line break on a side is printed outside the child's group; a softer separator inside it.
|
// A line break on a side is printed outside the child's group; a softer separator inside it
|
||||||
let breaks =
|
let breaks =
|
||||||
|between, blank| between != Between::Nothing && (blank || between == Between::Hard);
|
|between, blank| between != Between::Nothing && (blank || between == Between::Hard);
|
||||||
let prev_breaks = breaks(prev_between, prev_blank);
|
let prev_breaks = breaks(prev_between, prev_blank);
|
||||||
let leading = match node.prev {
|
let leading = match node.prev {
|
||||||
// After a `Space` (the previous group's trailing), no break before the next child.
|
// After a `Space` (the previous group's trailing), no break before the next child
|
||||||
Some(prev)
|
Some(prev)
|
||||||
if !matches!(prev_between, Between::Nothing | Between::Space) && !prev_breaks =>
|
if !matches!(prev_between, Between::Nothing | Between::Space) && !prev_breaks =>
|
||||||
{
|
{
|
||||||
@@ -286,7 +287,8 @@ pub fn has_trailing_line_break(t: &Tree<'_>, id: NodeId) -> bool {
|
|||||||
let parent = t.parent(id);
|
let parent = t.parent(id);
|
||||||
parent == NodeId::DOCUMENT
|
parent == NodeId::DOCUMENT
|
||||||
// A line break before a borrowed `>` cannot be printed (`b </p\n>`, `b</p\n>`),
|
// 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).
|
// 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)
|
|| (!closing_end_borrowed(t, parent)
|
||||||
&& t.end_tag_start(parent).is_some_and(|end| t.line_breaks(t.source_end(id), end) > 0))
|
&& t.end_tag_start(parent).is_some_and(|end| t.line_breaks(t.source_end(id), end) > 0))
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ pub fn write_element<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'
|
|||||||
let item = &t[id];
|
let item = &t[id];
|
||||||
|
|
||||||
if should_preserve_content(t, id) {
|
if should_preserve_content(t, id) {
|
||||||
// The opening tag prints the borrowed prefix, once (DIVERGENCES.md#pre-after-borrowed-closing-end).
|
// 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, group(&format_with(|f| write_opening_tag(t, id, f))));
|
||||||
write!(f, text(node_content(t, id)));
|
write!(f, text(node_content(t, id)));
|
||||||
write_closing_tag(t, id, f);
|
write_closing_tag(t, id, f);
|
||||||
@@ -88,13 +88,13 @@ pub fn write_element<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'
|
|||||||
let line_after = format_with(|f| {
|
let line_after = format_with(|f| {
|
||||||
if closing_end_borrowed(t, id) {
|
if closing_end_borrowed(t, id) {
|
||||||
if item.script_like {
|
if item.script_like {
|
||||||
// The body renders nothing, so its end tag takes its own line (DIVERGENCES.md#script-display-none).
|
// The body renders nothing, so its end tag takes its own line (DIVERGENCES.md#script-display-none)
|
||||||
write!(f, soft_line_break());
|
write!(f, soft_line_break());
|
||||||
} else if t[last].has_trailing_spaces && t[last].trailing_sensitive {
|
} else if t[last].has_trailing_spaces && t[last].trailing_sensitive {
|
||||||
write!(f, space());
|
write!(f, space());
|
||||||
}
|
}
|
||||||
} else if item.pre_like && t[last].borrows.parent_closing_start {
|
} else if item.pre_like && t[last].borrows.parent_closing_start {
|
||||||
// `<pre>` whose last text prints `</pre`: a line break would be content.
|
// `<pre>` whose last text prints `</pre`: a line break would be content
|
||||||
} else if hug {
|
} else if hug {
|
||||||
write!(f, if_group_breaks(&soft_line_break()).with_group_id(Some(attr_group_id)));
|
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 {
|
} else if t[last].has_trailing_spaces && t[last].trailing_sensitive {
|
||||||
@@ -102,7 +102,7 @@ pub fn write_element<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'
|
|||||||
} else if t[last].ends_in_pre {
|
} 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
|
// `<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).
|
// (DIVERGENCES.md#pre-content-without-end-tag).
|
||||||
} else if !ends_at_indentation(t, id, last, *f.options()) {
|
} else if !ends_at_indentation(t, id, last, f.options()) {
|
||||||
write!(f, soft_line_break());
|
write!(f, soft_line_break());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -129,12 +129,13 @@ pub fn write_element<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The last child is a comment or `pre` text ending in a line break plus exactly the element's indentation,
|
/// 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.
|
/// 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(
|
fn ends_at_indentation(
|
||||||
t: &Tree<'_>,
|
t: &Tree<'_>,
|
||||||
id: NodeId,
|
id: NodeId,
|
||||||
last: NodeId,
|
last: NodeId,
|
||||||
options: HtmlishFormatOptions,
|
options: &HtmlishFormatOptions,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let value = match t.kind(last) {
|
let value = match t.kind(last) {
|
||||||
NodeKind::Comment(comment) => comment.content.slice(t.source()),
|
NodeKind::Comment(comment) => comment.content.slice(t.source()),
|
||||||
@@ -150,7 +151,7 @@ fn ends_at_indentation(
|
|||||||
if !tail.bytes().all(|b| b == unit) {
|
if !tail.bytes().all(|b| b == unit) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// The element's own indentation: one level per ancestor below the document.
|
// The element's own indentation: one level per ancestor below the document
|
||||||
let mut depth = 0;
|
let mut depth = 0;
|
||||||
let mut ancestor = t.parent(id);
|
let mut ancestor = t.parent(id);
|
||||||
while ancestor != NodeId::DOCUMENT {
|
while ancestor != NodeId::DOCUMENT {
|
||||||
@@ -166,10 +167,10 @@ fn should_preserve_content(t: &Tree<'_>, id: NodeId) -> bool {
|
|||||||
match t.kind(id) {
|
match t.kind(id) {
|
||||||
NodeKind::ConditionalComment(comment) => {
|
NodeKind::ConditionalComment(comment) => {
|
||||||
!comment.parsed
|
!comment.parsed
|
||||||
// `<!--[if lt IE 9]><html><![endif]-->`: an unclosed element inside.
|
// `<!--[if lt IE 9]><html><![endif]-->`: an unclosed element inside
|
||||||
|| item.last_child.is_some_and(|last| t.is_unclosed(last))
|
|| item.last_child.is_some_and(|last| t.is_unclosed(last))
|
||||||
}
|
}
|
||||||
// `<pre>` with markup inside.
|
// `<pre>` with markup inside
|
||||||
_ => {
|
_ => {
|
||||||
item.pre_like
|
item.pre_like
|
||||||
&& t.children(id).any(|child| !(t.is_text(child) || t.is_interpolation(child)))
|
&& t.children(id).any(|child| !(t.is_text(child) || t.is_interpolation(child)))
|
||||||
@@ -181,7 +182,7 @@ fn should_preserve_content(t: &Tree<'_>, id: NodeId) -> bool {
|
|||||||
fn node_content<'a>(t: &'a Tree<'a>, id: NodeId) -> &'a str {
|
fn node_content<'a>(t: &'a Tree<'a>, id: NodeId) -> &'a str {
|
||||||
let item = &t[id];
|
let item = &t[id];
|
||||||
let mut start = t.content_start(id) as usize;
|
let mut start = t.content_start(id) as usize;
|
||||||
// An unclosed element's content runs to where the parser closed it.
|
// 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;
|
let mut end = t.end_tag_start(id).unwrap_or(item.span.end) as usize;
|
||||||
if opening_end_borrowed(t, id) {
|
if opening_end_borrowed(t, id) {
|
||||||
start -= opening_end_marker(t, id).len();
|
start -= opening_end_marker(t, id).len();
|
||||||
@@ -202,7 +203,8 @@ fn force_break_children(t: &Tree<'_>, id: NodeId) -> bool {
|
|||||||
|| item.display == Display::Table)
|
|| item.display == Display::Table)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Besides `force_break_children`: the content of `body`, `script`, `style`, nested markup, or a lone child on its own lines.
|
/// 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 {
|
fn force_break_content(t: &Tree<'_>, id: NodeId) -> bool {
|
||||||
let item = &t[id];
|
let item = &t[id];
|
||||||
if t.element(id).is_some()
|
if t.element(id).is_some()
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
//! Content written in another language, formatted by the session's dispatcher:
|
||||||
|
//! `<script>` and `<style>` bodies, `style` and event handler attribute values.
|
||||||
|
//! A part the dispatcher does not format prints as written.
|
||||||
|
|
||||||
|
use std::any::Any;
|
||||||
|
|
||||||
|
use oxc_allocator::{ArenaVec, StringBuilder};
|
||||||
|
use oxc_formatter_core::{
|
||||||
|
FormatElement, FormatOptions, builders::FormatOnce, dispatch_fragment_ir, map_text_in_ir, write,
|
||||||
|
};
|
||||||
|
use oxc_htmlish_parser::ast::NodeId;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
HtmlishFormatter,
|
||||||
|
attribute::{quoted_piece, write_expanded},
|
||||||
|
text::dedent_string,
|
||||||
|
};
|
||||||
|
use crate::tree::Tree;
|
||||||
|
|
||||||
|
/// Parent→child context for CSS dispatched from an HTML `style` attribute's value (css-in-html).
|
||||||
|
///
|
||||||
|
/// The host translates it into CSS's `CssEmbeddedIn`.
|
||||||
|
pub struct CssInHtmlStyleAttribute;
|
||||||
|
|
||||||
|
/// Parent→child context for JS dispatched from an HTML event handler attribute's value (js-in-html, `onclick="..."`).
|
||||||
|
///
|
||||||
|
/// The host translates it into JS's `JsEmbeddedIn`.
|
||||||
|
pub struct JsInHtmlEventHandler;
|
||||||
|
|
||||||
|
/// The event handler attributes (Prettier's `@prettier/html-event-attributes`).
|
||||||
|
const EVENT_HANDLERS: [&str; 90] = [
|
||||||
|
"onabort",
|
||||||
|
"onafterprint",
|
||||||
|
"onauxclick",
|
||||||
|
"onbeforeinput",
|
||||||
|
"onbeforematch",
|
||||||
|
"onbeforeprint",
|
||||||
|
"onbeforetoggle",
|
||||||
|
"onbeforeunload",
|
||||||
|
"onblur",
|
||||||
|
"oncancel",
|
||||||
|
"oncanplay",
|
||||||
|
"oncanplaythrough",
|
||||||
|
"onchange",
|
||||||
|
"onclick",
|
||||||
|
"onclose",
|
||||||
|
"oncommand",
|
||||||
|
"oncontextlost",
|
||||||
|
"oncontextmenu",
|
||||||
|
"oncontextrestored",
|
||||||
|
"oncopy",
|
||||||
|
"oncuechange",
|
||||||
|
"oncut",
|
||||||
|
"ondblclick",
|
||||||
|
"ondrag",
|
||||||
|
"ondragend",
|
||||||
|
"ondragenter",
|
||||||
|
"ondragleave",
|
||||||
|
"ondragover",
|
||||||
|
"ondragstart",
|
||||||
|
"ondrop",
|
||||||
|
"ondurationchange",
|
||||||
|
"onemptied",
|
||||||
|
"onended",
|
||||||
|
"onerror",
|
||||||
|
"onfocus",
|
||||||
|
"onformdata",
|
||||||
|
"onhashchange",
|
||||||
|
"oninput",
|
||||||
|
"oninvalid",
|
||||||
|
"onkeydown",
|
||||||
|
"onkeypress",
|
||||||
|
"onkeyup",
|
||||||
|
"onlanguagechange",
|
||||||
|
"onload",
|
||||||
|
"onloadeddata",
|
||||||
|
"onloadedmetadata",
|
||||||
|
"onloadstart",
|
||||||
|
"onmessage",
|
||||||
|
"onmessageerror",
|
||||||
|
"onmousedown",
|
||||||
|
"onmouseenter",
|
||||||
|
"onmouseleave",
|
||||||
|
"onmousemove",
|
||||||
|
"onmouseout",
|
||||||
|
"onmouseover",
|
||||||
|
"onmouseup",
|
||||||
|
"onoffline",
|
||||||
|
"ononline",
|
||||||
|
"onpagehide",
|
||||||
|
"onpagereveal",
|
||||||
|
"onpageshow",
|
||||||
|
"onpageswap",
|
||||||
|
"onpaste",
|
||||||
|
"onpause",
|
||||||
|
"onplay",
|
||||||
|
"onplaying",
|
||||||
|
"onpopstate",
|
||||||
|
"onprogress",
|
||||||
|
"onratechange",
|
||||||
|
"onrejectionhandled",
|
||||||
|
"onreset",
|
||||||
|
"onresize",
|
||||||
|
"onscroll",
|
||||||
|
"onscrollend",
|
||||||
|
"onsecuritypolicyviolation",
|
||||||
|
"onseeked",
|
||||||
|
"onseeking",
|
||||||
|
"onselect",
|
||||||
|
"onslotchange",
|
||||||
|
"onstalled",
|
||||||
|
"onstorage",
|
||||||
|
"onsubmit",
|
||||||
|
"onsuspend",
|
||||||
|
"ontimeupdate",
|
||||||
|
"ontoggle",
|
||||||
|
"onunhandledrejection",
|
||||||
|
"onunload",
|
||||||
|
"onvolumechange",
|
||||||
|
"onwaiting",
|
||||||
|
"onwheel",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// An event handler attribute (`onclick`), whose value is JS.
|
||||||
|
/// The name matches as written, as Prettier does (`onClick` stays as written).
|
||||||
|
pub fn is_event_handler(name: &str) -> bool {
|
||||||
|
name.starts_with("on") && EVENT_HANDLERS.contains(&name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes an attribute's (unescaped) `value` formatted in `language`, quoted, a `"` in it as `"`;
|
||||||
|
/// `false` when the dispatcher leaves it as written.
|
||||||
|
pub fn write_attribute_value<'a>(
|
||||||
|
language: &str,
|
||||||
|
parent_context: &dyn Any,
|
||||||
|
value: &'a str,
|
||||||
|
f: &mut HtmlishFormatter<'_, 'a>,
|
||||||
|
) -> bool {
|
||||||
|
let Some(ir) = dispatch_fragment_ir(f, language, value, Some(parent_context)) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if ir.is_empty() {
|
||||||
|
write!(f, "\"\"");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let indent_width = f.options().indent_width();
|
||||||
|
let ir = map_text_in_ir(ir, f.allocator(), &mut |text, out| {
|
||||||
|
if !text.contains('"') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out.push(FormatElement::text(quoted_piece(text, f), indent_width));
|
||||||
|
true
|
||||||
|
});
|
||||||
|
write_expanded(&FormatOnce::new(|f: &mut HtmlishFormatter<'_, 'a>| f.write_elements(ir)), f);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the body of `<script>` / `<style>` element `id` formatted in its language;
|
||||||
|
/// `false` when it has none or the dispatcher leaves it as written.
|
||||||
|
pub fn write_embedded_content<'a>(
|
||||||
|
t: &'a Tree<'a>,
|
||||||
|
id: NodeId,
|
||||||
|
value: &'a str,
|
||||||
|
f: &mut HtmlishFormatter<'_, 'a>,
|
||||||
|
) -> bool {
|
||||||
|
let Some(language) = content_language(t, id) else { return false };
|
||||||
|
// A Markdown body starts at the line after the tag, dedented (Prettier's `dedentString`)
|
||||||
|
let value = if language == "markdown" {
|
||||||
|
let value = value
|
||||||
|
.find('\n')
|
||||||
|
.filter(|&lf| value[..lf].trim_ascii().is_empty())
|
||||||
|
.map_or(value, |lf| &value[lf + 1..]);
|
||||||
|
dedent_string(value, f)
|
||||||
|
} else {
|
||||||
|
value
|
||||||
|
};
|
||||||
|
let Some(ir) = dispatch_fragment_ir(f, language, value, None) else { return false };
|
||||||
|
let ir = if t.tag(id) == "script" { escape_script_end(ir, f) } else { ir };
|
||||||
|
f.write_elements(ir);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The first `</script` ends a `<script>` body, so the source body holds none:
|
||||||
|
/// one in the formatted body is the formatting's own (a template literal's re-cooked part, e.g. html-in-js),
|
||||||
|
/// printed as `<\/script`, where `\/` means `/` (in a JS string or template, and in JSON).
|
||||||
|
///
|
||||||
|
/// A nested HTML part prints its end tags as one text (`tag.rs`), so none is split across texts.
|
||||||
|
fn escape_script_end<'a>(
|
||||||
|
ir: ArenaVec<'a, FormatElement<'a>>,
|
||||||
|
f: &HtmlishFormatter<'_, 'a>,
|
||||||
|
) -> ArenaVec<'a, FormatElement<'a>> {
|
||||||
|
let allocator = f.allocator();
|
||||||
|
let indent_width = f.options().indent_width();
|
||||||
|
map_text_in_ir(ir, allocator, &mut |text, out| {
|
||||||
|
let is_script_end = |i: usize| {
|
||||||
|
let bytes = &text.as_bytes()[i + 2..];
|
||||||
|
bytes.get(..6).is_some_and(|name| name.eq_ignore_ascii_case(b"script"))
|
||||||
|
&& !bytes.get(6).is_some_and(|&b| b.is_ascii_alphanumeric() || b == b'_')
|
||||||
|
};
|
||||||
|
let mut ends = text.match_indices("</").filter(|&(i, _)| is_script_end(i)).peekable();
|
||||||
|
if ends.peek().is_none() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut escaped = StringBuilder::new_in(allocator);
|
||||||
|
let mut run_start = 0;
|
||||||
|
for (i, _) in ends {
|
||||||
|
escaped.push_str(&text[run_start..i]);
|
||||||
|
escaped.push_str("<\\/");
|
||||||
|
run_start = i + 2;
|
||||||
|
}
|
||||||
|
escaped.push_str(&text[run_start..]);
|
||||||
|
out.push(FormatElement::text(escaped.into_str(), indent_width));
|
||||||
|
true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The language of the body of `<script>` / `<style>` element `id`, a name the dispatcher resolves
|
||||||
|
/// (Prettier's `inferScriptParser` / `inferStyleParser`); `None` for none (`<script src>`, a template type).
|
||||||
|
fn content_language<'a>(t: &'a Tree<'a>, id: NodeId) -> Option<&'a str> {
|
||||||
|
let el = t.element(id)?;
|
||||||
|
let attribute = |name: &str| {
|
||||||
|
el.attributes
|
||||||
|
.iter()
|
||||||
|
.find(|attribute| t.attribute_is(attribute.name.slice(t.source()), name))
|
||||||
|
};
|
||||||
|
let value =
|
||||||
|
|name: &str| attribute(name).map(|a| a.value.map_or("", |v| v.span.slice(t.source())));
|
||||||
|
match t.tag(id) {
|
||||||
|
"style" => Some(value("lang").unwrap_or("css")),
|
||||||
|
"script" if attribute("src").is_none() => match (value("lang"), value("type")) {
|
||||||
|
(None, None) => Some("js"),
|
||||||
|
(Some(lang), _) => Some(lang),
|
||||||
|
(None, Some(kind)) => script_type_language(kind),
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `<script type>` (Prettier's `inferParserByTypeAttribute`).
|
||||||
|
fn script_type_language(kind: &str) -> Option<&str> {
|
||||||
|
Some(match kind {
|
||||||
|
"module" | "text/javascript" | "application/javascript" => "js",
|
||||||
|
"text/babel" | "text/jsx" => "jsx",
|
||||||
|
"application/x-typescript" => "ts",
|
||||||
|
"text/markdown" => "markdown",
|
||||||
|
"text/html" => "html",
|
||||||
|
"text/x-handlebars-template" => "handlebars",
|
||||||
|
_ if kind.ends_with("json")
|
||||||
|
|| kind.ends_with("importmap")
|
||||||
|
|| kind == "speculationrules" =>
|
||||||
|
{
|
||||||
|
"json"
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -14,9 +14,12 @@ mod attribute;
|
|||||||
mod block;
|
mod block;
|
||||||
mod children;
|
mod children;
|
||||||
mod element;
|
mod element;
|
||||||
|
mod embed;
|
||||||
mod tag;
|
mod tag;
|
||||||
mod text;
|
mod text;
|
||||||
|
|
||||||
|
pub use embed::{CssInHtmlStyleAttribute, JsInHtmlEventHandler};
|
||||||
|
|
||||||
use block::write_block;
|
use block::write_block;
|
||||||
use children::write_children;
|
use children::write_children;
|
||||||
use element::write_element;
|
use element::write_element;
|
||||||
@@ -61,7 +64,8 @@ fn write_child<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>
|
|||||||
start += opening_start_marker(t, id).len();
|
start += opening_start_marker(t, id).len();
|
||||||
}
|
}
|
||||||
let mut end = ignored_end(t, id) as usize;
|
let mut end = ignored_end(t, id) as usize;
|
||||||
// Whoever borrows the closing `>` prints it, the parent included (DIVERGENCES.md#ignored-closing-end-once).
|
// Whoever borrows the closing `>` prints it,
|
||||||
|
// the parent included (DIVERGENCES.md#ignored-closing-end-once)
|
||||||
if closing_end_borrowed(t, id) {
|
if closing_end_borrowed(t, id) {
|
||||||
end -= closing_end_marker(t, id).len();
|
end -= closing_end_marker(t, id).len();
|
||||||
}
|
}
|
||||||
@@ -89,7 +93,7 @@ fn write_node<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>)
|
|||||||
write_closing_tag_end(t, id, f);
|
write_closing_tag_end(t, id, f);
|
||||||
}
|
}
|
||||||
NodeKind::Text(_) | NodeKind::Cdata => write_text(t, id, f),
|
NodeKind::Text(_) | NodeKind::Cdata => write_text(t, id, f),
|
||||||
// `{{ }}`; the expression keeps its lines until it is formatted.
|
// `{{ }}`; the expression keeps its lines until it is formatted
|
||||||
NodeKind::Interpolation(interpolation) => {
|
NodeKind::Interpolation(interpolation) => {
|
||||||
write_opening_tag_start(t, id, f);
|
write_opening_tag_start(t, id, f);
|
||||||
let expression = interpolation.expression.slice(t.source());
|
let expression = interpolation.expression.slice(t.source());
|
||||||
@@ -120,8 +124,8 @@ fn write_node<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>)
|
|||||||
);
|
);
|
||||||
write_closing_tag_end(t, id, f);
|
write_closing_tag_end(t, id, f);
|
||||||
}
|
}
|
||||||
// Comments are printed as written, a `</ div>` too (DIVERGENCES.md#end-tag-with-space-is-comment),
|
// Comments are printed as written,
|
||||||
// and an omitted element's end tag (`</body>`).
|
// a `</ div>` too (DIVERGENCES.md#end-tag-with-space-is-comment), and an omitted element's end tag (`</body>`).
|
||||||
NodeKind::Comment(_) | NodeKind::EndTag(_) => write!(
|
NodeKind::Comment(_) | NodeKind::EndTag(_) => write!(
|
||||||
f,
|
f,
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
//! which neighbor prints a marker is the output itself, and matching that output leaves no other structure.
|
//! 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 oxc_formatter_core::{Format, builders::text, write};
|
||||||
|
|
||||||
use super::{HtmlishFormatter, attribute::write_attributes};
|
|
||||||
use oxc_htmlish_parser::{
|
use oxc_htmlish_parser::{
|
||||||
Span,
|
Span,
|
||||||
ast::{NodeId, NodeKind},
|
ast::{NodeId, NodeKind},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::{HtmlishFormatter, attribute::write_attributes};
|
||||||
|
|
||||||
use crate::{context::HtmlishFormatContext, tree::Tree};
|
use crate::{context::HtmlishFormatContext, tree::Tree};
|
||||||
|
|
||||||
/// A tag marker, written as up to four pieces (`<` + name, `<!--[if ` + condition + `]><!--><` + name, ...).
|
/// A tag marker, written as up to four pieces (`<` + name, `<!--[if ` + condition + `]><!--><` + name, ...).
|
||||||
@@ -50,8 +50,7 @@ impl<'a> Format<'a, HtmlishFormatContext<'a>> for Marker<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
// --- Owner side
|
||||||
// Owner side
|
|
||||||
|
|
||||||
/// The node's closing `>` is printed by its next sibling or its parent.
|
/// The node's closing `>` is printed by its next sibling or its parent.
|
||||||
pub fn closing_end_borrowed(t: &Tree<'_>, id: NodeId) -> bool {
|
pub fn closing_end_borrowed(t: &Tree<'_>, id: NodeId) -> bool {
|
||||||
@@ -76,8 +75,7 @@ pub fn closing_start_borrowed(t: &Tree<'_>, id: NodeId) -> bool {
|
|||||||
t[id].last_child.is_some_and(|last| t[last].borrows.parent_closing_start)
|
t[id].last_child.is_some_and(|last| t[last].borrows.parent_closing_start)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
// --- Markers
|
||||||
// Markers
|
|
||||||
|
|
||||||
pub fn opening_start_marker<'a>(t: &'a Tree<'a>, id: NodeId) -> Marker<'a> {
|
pub fn opening_start_marker<'a>(t: &'a Tree<'a>, id: NodeId) -> Marker<'a> {
|
||||||
match t.kind(id) {
|
match t.kind(id) {
|
||||||
@@ -124,12 +122,17 @@ pub fn closing_start_marker<'a>(t: &'a Tree<'a>, id: NodeId) -> Marker<'a> {
|
|||||||
// The end tag as written, its name lowercase with the start tag's (DIVERGENCES.md#tag-name-case);
|
// 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).
|
// 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 `<//>`.
|
// What HTML ignores after the name (`</div foo>`) is kept too, and so is htm's `<//>`.
|
||||||
|
// As written it is one text: an enclosing `<script>` (html-in-js in it) sees a `</script` whole to escape it;
|
||||||
|
// lowercase names print only in a `.html` file, embedded in nothing.
|
||||||
NodeKind::Element(el) => el.end_tag.map_or(Marker::default(), |tag| {
|
NodeKind::Element(el) => el.end_tag.map_or(Marker::default(), |tag| {
|
||||||
let name = t.printed_name(id, el, tag.name.slice(t.source()));
|
let written = tag.name.slice(t.source());
|
||||||
let ignored = tag
|
let name = t.printed_name(id, el, written);
|
||||||
.ignored
|
let end = tag.ignored.map_or(tag.name.end, |ignored| ignored.end);
|
||||||
.map_or("", |ignored| Span::new(tag.name.end, ignored.end).slice(t.source()));
|
if std::ptr::eq(name, written) {
|
||||||
Marker(["</", name, ignored, ""])
|
Marker::one(Span::new(tag.span.start, end).slice(t.source()))
|
||||||
|
} else {
|
||||||
|
Marker(["</", name, Span::new(tag.name.end, end).slice(t.source()), ""])
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
_ => Marker::two("</", t.name(id)),
|
_ => Marker::two("</", t.name(id)),
|
||||||
}
|
}
|
||||||
@@ -154,8 +157,7 @@ pub fn after_html_word(value: &str) -> Option<&str> {
|
|||||||
(!rest.starts_with(|c: char| c.is_ascii_alphanumeric() || c == '_')).then_some(rest)
|
(!rest.starts_with(|c: char| c.is_ascii_alphanumeric() || c == '_')).then_some(rest)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
// --- Prefixes and suffixes: what a node prints for its neighbors
|
||||||
// Prefixes and suffixes: what a node prints for its neighbors
|
|
||||||
|
|
||||||
pub fn opening_tag_prefix<'a>(t: &'a Tree<'a>, id: NodeId) -> Marker<'a> {
|
pub fn opening_tag_prefix<'a>(t: &'a Tree<'a>, id: NodeId) -> Marker<'a> {
|
||||||
if t[id].borrows.parent_opening_end {
|
if t[id].borrows.parent_opening_end {
|
||||||
@@ -177,8 +179,7 @@ pub fn closing_tag_suffix<'a>(t: &'a Tree<'a>, id: NodeId) -> Marker<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---
|
// --- Tags
|
||||||
// Tags
|
|
||||||
|
|
||||||
pub fn write_opening_tag_start<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>) {
|
pub fn write_opening_tag_start<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||||
if !opening_start_borrowed(t, id) {
|
if !opening_start_borrowed(t, id) {
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
//! Text: filled words, or lines as written where whitespace is content.
|
//! Text: filled words, or lines as written where whitespace is content.
|
||||||
|
|
||||||
|
use oxc_allocator::StringBuilder;
|
||||||
use oxc_formatter_core::{
|
use oxc_formatter_core::{
|
||||||
builders::{exact_line_breaks, soft_line_break_or_space, text},
|
FormatOptions,
|
||||||
write,
|
builders::{align, dedent_to_root, exact_line_breaks, soft_line_break_or_space, text},
|
||||||
|
text_width, write,
|
||||||
};
|
};
|
||||||
use oxc_htmlish_parser::ast::NodeId;
|
use oxc_htmlish_parser::ast::NodeId;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
HtmlishFormatter, format_with,
|
HtmlishFormatter,
|
||||||
|
embed::write_embedded_content,
|
||||||
|
format_with,
|
||||||
tag::{closing_tag_suffix, opening_tag_prefix},
|
tag::{closing_tag_suffix, opening_tag_prefix},
|
||||||
};
|
};
|
||||||
use crate::tree::Tree;
|
use crate::tree::Tree;
|
||||||
@@ -19,12 +23,20 @@ pub fn write_text<'a>(t: &'a Tree<'a>, id: NodeId, f: &mut HtmlishFormatter<'_,
|
|||||||
let suffix = closing_tag_suffix(t, id);
|
let suffix = closing_tag_suffix(t, id);
|
||||||
|
|
||||||
if parent.pre_like {
|
if parent.pre_like {
|
||||||
write!(f, [prefix, text(value), suffix]);
|
// Opening the content, it starts on its own line from the root (`line_before` in `element.rs`)
|
||||||
|
let starts_line = parent.first_child == Some(id)
|
||||||
|
&& !(t[id].has_leading_spaces && t[id].leading_sensitive);
|
||||||
|
// A JS expression substituted into it indents from its line (`apps/oxfmt/DIVERGENCES.md#pre-like-template-expression`)
|
||||||
|
write!(f, prefix);
|
||||||
|
write_pre_like_text(value, starts_line, f);
|
||||||
|
write!(f, suffix);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if parent.script_like {
|
if parent.script_like {
|
||||||
write!(f, prefix);
|
write!(f, prefix);
|
||||||
write_dedented_lines(value, f);
|
if !write_embedded_content(t, t.parent(id), value, f) {
|
||||||
|
write_dedented_lines(value, f);
|
||||||
|
}
|
||||||
write!(f, suffix);
|
write!(f, suffix);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -104,14 +116,68 @@ fn trim_preserving_indentation(value: &str) -> &str {
|
|||||||
&value[start..]
|
&value[start..]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [`dedent`] as one text; borrowed when no line is indented.
|
||||||
|
pub fn dedent_string<'a>(value: &'a str, f: &HtmlishFormatter<'_, 'a>) -> &'a str {
|
||||||
|
let min = common_indentation(value);
|
||||||
|
if min == 0 {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
let mut out = StringBuilder::new_in(f.allocator());
|
||||||
|
for (i, line) in dedent_by(value, min).enumerate() {
|
||||||
|
if i > 0 {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
out.push_str(line);
|
||||||
|
}
|
||||||
|
out.into_str()
|
||||||
|
}
|
||||||
|
|
||||||
/// Prettier's `dedentString`: the lines with their common indentation removed.
|
/// Prettier's `dedentString`: the lines with their common indentation removed.
|
||||||
fn dedent(value: &str) -> impl Iterator<Item = &str> {
|
fn dedent(value: &str) -> impl Iterator<Item = &str> {
|
||||||
let indentation = |line: &str| line.len() - line.trim_ascii_start().len();
|
dedent_by(value, common_indentation(value))
|
||||||
let min = value
|
}
|
||||||
.split('\n')
|
|
||||||
.filter(|line| !line.trim_ascii_start().is_empty())
|
/// The lines of `value` with `min` bytes of indentation removed.
|
||||||
.map(indentation)
|
fn dedent_by(value: &str, min: usize) -> impl Iterator<Item = &str> {
|
||||||
.min()
|
|
||||||
.unwrap_or(0);
|
|
||||||
value.split('\n').map(move |line| line.get(min..).unwrap_or(""))
|
value.split('\n').map(move |line| line.get(min..).unwrap_or(""))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The indentation every non-blank line of `value` shares.
|
||||||
|
fn common_indentation(value: &str) -> usize {
|
||||||
|
value
|
||||||
|
.split('\n')
|
||||||
|
.filter(|line| !line.trim_ascii_start().is_empty())
|
||||||
|
.map(|line| line.len() - line.trim_ascii_start().len())
|
||||||
|
.min()
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `pre`-like text as written.
|
||||||
|
/// A parent's placeholder for a JS expression on a line that starts at the root (after a line break in the text,
|
||||||
|
/// or at its start when the text opens the content on its own line) aligns with the whitespace that line starts with,
|
||||||
|
/// so that the expression's own lines indent from its `${`, as anywhere else.
|
||||||
|
fn write_pre_like_text<'a>(value: &'a str, starts_line: bool, f: &mut HtmlishFormatter<'_, 'a>) {
|
||||||
|
let prefix = f.context().embedded_in().and_then(|e| e.placeholder).map(|(prefix, _)| prefix);
|
||||||
|
let indent_width = f.options().indent_width();
|
||||||
|
let mut written = 0;
|
||||||
|
while let Some(start) = prefix.and_then(|p| value[written..].find(p)).map(|i| written + i) {
|
||||||
|
let line_end = value[start..].find('\n').map_or(value.len(), |i| start + i);
|
||||||
|
let line_start = value[..start].rfind('\n').map(|i| i + 1).or(starts_line.then_some(0));
|
||||||
|
if let Some(line_start) = line_start {
|
||||||
|
let line = &value[line_start..start];
|
||||||
|
let indentation = &line[..line.len() - line.trim_start_matches([' ', '\t']).len()];
|
||||||
|
let width = u8::try_from(text_width(indentation, indent_width)).unwrap_or(u8::MAX);
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
[
|
||||||
|
text(&value[written..start]),
|
||||||
|
dedent_to_root(&align(width, &text(&value[start..line_end])))
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
write!(f, text(&value[written..line_end]));
|
||||||
|
}
|
||||||
|
written = line_end;
|
||||||
|
}
|
||||||
|
write!(f, text(&value[written..]));
|
||||||
|
}
|
||||||
+2
-2
@@ -32,7 +32,7 @@ impl FixtureFormatter for HtmlishHarness {
|
|||||||
|
|
||||||
fn format(source: &str, path: &Path, options: &Self::Options) -> String {
|
fn format(source: &str, path: &Path, options: &Self::Options) -> String {
|
||||||
let allocator = Allocator::default();
|
let allocator = Allocator::default();
|
||||||
let options = HtmlishFormatOptions { variant: variant_of(path), ..*options };
|
let options = HtmlishFormatOptions { variant: variant_of(path), ..options.clone() };
|
||||||
format(&allocator, source, options)
|
format(&allocator, source, options)
|
||||||
.expect("format should succeed")
|
.expect("format should succeed")
|
||||||
.print()
|
.print()
|
||||||
@@ -41,7 +41,7 @@ impl FixtureFormatter for HtmlishHarness {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn fingerprint(source: &str, path: &Path, options: &Self::Options) -> String {
|
fn fingerprint(source: &str, path: &Path, options: &Self::Options) -> String {
|
||||||
let options = HtmlishFormatOptions { variant: variant_of(path), ..*options };
|
let options = HtmlishFormatOptions { variant: variant_of(path), ..options.clone() };
|
||||||
let allocator = Allocator::default();
|
let allocator = Allocator::default();
|
||||||
let parsed = parse_for_format(&allocator, source, &options).expect("source should parse");
|
let parsed = parse_for_format(&allocator, source, &options).expect("source should parse");
|
||||||
fingerprint::fingerprint(
|
fingerprint::fingerprint(
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ versioned_files = [
|
|||||||
"crates/oxc_formatter/Cargo.toml",
|
"crates/oxc_formatter/Cargo.toml",
|
||||||
"crates/oxc_formatter_json/Cargo.toml",
|
"crates/oxc_formatter_json/Cargo.toml",
|
||||||
"crates/oxc_formatter_graphql/Cargo.toml",
|
"crates/oxc_formatter_graphql/Cargo.toml",
|
||||||
|
"crates/oxc_formatter_htmlish/Cargo.toml",
|
||||||
"crates/oxc_formatter_css/Cargo.toml",
|
"crates/oxc_formatter_css/Cargo.toml",
|
||||||
"crates/oxc_formatter_yaml/Cargo.toml",
|
"crates/oxc_formatter_yaml/Cargo.toml",
|
||||||
"crates/oxc_formatter_markdown/Cargo.toml",
|
"crates/oxc_formatter_markdown/Cargo.toml",
|
||||||
|
|||||||
Reference in new issue
Block a user