mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
feat(oxfmt)!: format parser:markdown files by oxc_formatter_markdown (#27256)
Part of #24607 Now oxfmt formats Markdown files natively with `oxc_formatter_markdown`. Basically the same output with Prettier 3.9.9, but we already covers some not yet released fixes as well. Please see DIVERGENCES.md. Before, Markdown files were formatted by Prettier. So, JS in Markdown code fence was also formatted by Prettier (babel/typescript). But now it is handled by `oxc_formatter`, it means `sortImports` and `sortTailwindcss` works. For Markdown code fence langauge detection, we uses shiki languages (https://shiki.style/languages). We also supports `::: directive` syntax, both VitePress (`::: withspace`) and remark-directive (`:::withoutspace`). (BLOCK only, no LEAF and TEXT position)
This commit is contained in:
1 parent
cd77f1c5c3
commit
e2c68b17fa
75 files changed
+8331
-389
No files matched your search
Generated
+2
@@ -2151,6 +2151,7 @@ dependencies = [
|
||||
"oxc_span",
|
||||
"oxc_tasks_common",
|
||||
"pico-args",
|
||||
"rustc-hash",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
@@ -2888,6 +2889,7 @@ dependencies = [
|
||||
"oxc_formatter_css",
|
||||
"oxc_formatter_graphql",
|
||||
"oxc_formatter_json",
|
||||
"oxc_formatter_markdown",
|
||||
"oxc_formatter_yaml",
|
||||
"oxc_language_server",
|
||||
"oxc_napi",
|
||||
|
||||
@@ -165,6 +165,7 @@ oxc_formatter_core = { path = "crates/oxc_formatter_core" } # Language-agnostic
|
||||
oxc_formatter_css = { path = "crates/oxc_formatter_css" } # CSS formatter
|
||||
oxc_formatter_graphql = { path = "crates/oxc_formatter_graphql" } # GraphQL formatter
|
||||
oxc_formatter_json = { path = "crates/oxc_formatter_json" } # JSON 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_yaml = { path = "crates/oxc_formatter_yaml" } # YAML formatter
|
||||
oxc_language_server = { path = "crates/oxc_language_server", default-features = false } # Language server
|
||||
|
||||
+14
-5
@@ -69,7 +69,7 @@ Oxfmt utilizes different implementations depending on the file extension and fil
|
||||
|
||||
- Tier 1: Rust implementations using `oxc_formatter`, `oxc_formatter_json`, etc found in this repository
|
||||
- Tier 2: Rust implementations using external libraries like `oxc_toml`
|
||||
- Tier 3: Delegations to Prettier via NAPI-JS calls (e.g., for Vue or Markdown)
|
||||
- Tier 3: Delegations to Prettier via NAPI-JS calls (e.g., for Vue or MDX)
|
||||
- Tier 4: Delegations to Prettier that require additional Prettier plugins (e.g., for Svelte)
|
||||
|
||||
NOTE: Rust written formatters never fall back to Prettier, since they exist to reduce the dependency on Prettier.
|
||||
@@ -92,14 +92,23 @@ Conformance failures (`conformance/snapshots/`) no entry accounts for, to fix ra
|
||||
### Embedded language formatting
|
||||
|
||||
Embedded languages (e.g. css-in-js, CSS front matter YAML) go through the `FormatDispatcher` (defined in `oxc_formatter_core`) assembled by `src/core/embed/dispatcher.rs`.
|
||||
Routing is ONE table (`dispatcher::route`): `Native` languages (css/graphql/yaml/json/...) get a Rust branch, the `Prettier` set (html/angular/markdown) goes to the Prettier Doc→IR channel (`embed/prettier_doc.rs`, napi only), everything else is deliberately preserved.
|
||||
|
||||
Routing is ONE table (`dispatcher::route`):
|
||||
|
||||
- `Native` languages (js/ts/css/graphql/yaml/json/markdown/...) get a Rust branch
|
||||
- `Prettier` set (html/angular/vue/svelte/handlebars/mdx) goes to the Prettier Doc→IR channel
|
||||
- `embed/prettier_doc.rs`, napi only; svelte only with the `svelte` config
|
||||
- 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.
|
||||
|
||||
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, TOML/custom front matter) deliberately stay verbatim.
|
||||
|
||||
Three 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 and CSS file roots (`core/format.rs`, `PhysicalFile` sessions, both builds) and the Vue/Svelte `<script>` 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 and Markdown file roots (`core/format.rs`, `PhysicalFile` sessions, both builds) and the Vue/Svelte `<script>` 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); the shared `route()` table then decides who serves the language. Adding a dispatch call to a host crate is therefore a routing decision (check `route()` and the embedded conformance when doing so. A root needing a bespoke service set would assemble the `SessionServices` struct literally), none does today.
|
||||
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()` table then decides who serves the language.
|
||||
Adding a dispatch call to a host crate is therefore a routing decision (check `route()` and the embedded conformance when doing so. A root needing a bespoke service set would assemble the `SessionServices` struct literally), none does today.
|
||||
`embeddedLanguageFormatting: off` installs no dispatcher, every builder consults the same off-gate, `ResolvedDispatchConfig::is_embedded_formatting_enabled`.
|
||||
Tracing span namespaces: `oxfmt::embed::` = pure Rust work, `oxfmt::external::` = napi-crossing calls.
|
||||
|
||||
@@ -113,7 +122,7 @@ A separate string-out channel (the session's `string_embedder` service, NOT the
|
||||
- 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`)
|
||||
|
||||
NOTE: The string-out channel outlives the md/html/angular rewrites; 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 full 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) once the printer grows a per-line prefix mechanism for the `*` continuation, deferred for verification time, not by design.
|
||||
|
||||
### Tailwind CSS class sorting
|
||||
|
||||
@@ -35,6 +35,7 @@ oxc_formatter_core = { workspace = true }
|
||||
oxc_formatter_css = { workspace = true }
|
||||
oxc_formatter_graphql = { workspace = true }
|
||||
oxc_formatter_json = { workspace = true }
|
||||
oxc_formatter_markdown = { workspace = true }
|
||||
oxc_formatter_yaml = { workspace = true }
|
||||
oxc_language_server = { workspace = true }
|
||||
oxc_napi = { workspace = true }
|
||||
|
||||
@@ -119,3 +119,128 @@ The layout of an embedded template as a sole argument or an arrow body is decide
|
||||
Prettier falls back to the source shape when the content fails to format, while it hugs the same template with valid content.
|
||||
The verbatim content cannot be re-indented, so its source indentation stays and may look misaligned once hugged (as with any verbatim template Prettier hugs).
|
||||
Under `embeddedLanguageFormatting: off`, the source shape still decides, like Prettier.
|
||||
|
||||
## nested-fence-length
|
||||
|
||||
- Why: semantics (prettier/prettier#19908)
|
||||
- Pin: `conformance/fixtures/edge-cases/xxx-in-md/nested-fence-length.md`
|
||||
- Conformance: `externals/prettier/js/multiparser-markdown/codeblock.js`
|
||||
|
||||
`````markdown
|
||||
<!-- input -->
|
||||
```md
|
||||
~~~js
|
||||
const a = 1
|
||||
~~~
|
||||
```
|
||||
|
||||
<!-- ours -->
|
||||
````md
|
||||
```js
|
||||
const a = 1;
|
||||
```
|
||||
````
|
||||
|
||||
<!-- prettier -->
|
||||
```md
|
||||
```js
|
||||
const a = 1;
|
||||
```
|
||||
```
|
||||
`````
|
||||
|
||||
A fenced code block's fence outnumbers the backtick (or tilde) runs of the content it prints, the embedded formatter's output included.
|
||||
Markdown in a code block formats its own fences, so `~~~` becomes a backtick fence the outer one must outnumber;
|
||||
Prettier keeps the outer fence at three and the inner fence closes it on the next parse.
|
||||
The same holds for markdown-in-js, whose fences are `~`.
|
||||
Prettier `main` counts the printed content since #19908; the pin (3.9.9) still counts the source.
|
||||
|
||||
## line-ranged-code-block
|
||||
|
||||
- Why: uniform-rule (same construct, same output: `` ```js{1,3} ``)
|
||||
- Pin: `conformance/fixtures/edge-cases/xxx-in-md/line-ranged-code-block.md`
|
||||
|
||||
````markdown
|
||||
<!-- input -->
|
||||
```js {4}
|
||||
const a = [
|
||||
1, 2,
|
||||
];
|
||||
console.log(a);
|
||||
```
|
||||
|
||||
<!-- ours -->
|
||||
```js {4}
|
||||
const a = [
|
||||
1, 2,
|
||||
];
|
||||
console.log(a);
|
||||
```
|
||||
|
||||
<!-- prettier -->
|
||||
```js {4}
|
||||
const a = [1, 2];
|
||||
console.log(a);
|
||||
```
|
||||
````
|
||||
|
||||
A code block whose meta addresses lines by number (`{1,3-5}`, VitePress / Docusaurus line highlighting) stays as written:
|
||||
formatting moves the lines the numbers point at.
|
||||
Above, `{4}` highlights `console.log(a)`; Prettier's output has two lines, so it highlights nothing.
|
||||
Prettier formats it when a space separates the language, and keeps `js{1,3}` as written because `js{1,3}` is no language it knows.
|
||||
|
||||
## lwc-code-block
|
||||
|
||||
- Why: uniform-rule (code fence names follow Shiki's vocabulary)
|
||||
- Pin: `conformance/fixtures/edge-cases/xxx-in-md/lwc-code-block.md`
|
||||
- Conformance: `externals/prettier/markdown/code/lwc/lwc.md`
|
||||
|
||||
````markdown
|
||||
<!-- input -->
|
||||
```lwc
|
||||
<x-a attr={b}>
|
||||
c</x-a>
|
||||
```
|
||||
|
||||
<!-- ours -->
|
||||
```lwc
|
||||
<x-a attr={b}>
|
||||
c</x-a>
|
||||
```
|
||||
|
||||
<!-- prettier -->
|
||||
```lwc
|
||||
<x-a attr={b}> c</x-a>
|
||||
```
|
||||
````
|
||||
|
||||
A Markdown code block's language is looked up in Shiki's ids and aliases (`route()` in `src/core/embed/dispatcher.rs`);
|
||||
`lwc` is none of them, so the block stays as written.
|
||||
Prettier looks it up in linguist, where `lwc` is an alias of its Lightning Web Components language.
|
||||
|
||||
## whitespace-only-code-block
|
||||
|
||||
- Why: uniform-rule (embedded script formats like its standalone file)
|
||||
- Pin: `conformance/fixtures/edge-cases/xxx-in-md/whitespace-only-code-block.md`
|
||||
|
||||
`````markdown
|
||||
<!-- input -->
|
||||
```json
|
||||
|
||||
|
||||
```
|
||||
|
||||
<!-- ours -->
|
||||
```json
|
||||
|
||||
```
|
||||
|
||||
<!-- prettier -->
|
||||
```json
|
||||
|
||||
|
||||
```
|
||||
`````
|
||||
|
||||
A blank `.json` file formats to an empty one, and so does a code block of blank lines, an empty block like `jsonc` / `css` / `js` in Prettier too.
|
||||
Prettier keeps `json` / `json5` blocks as written: their parser rejects a blank document inside Markdown.
|
||||
@@ -74,6 +74,18 @@ const sources = [
|
||||
repo: "facebook/docusaurus/packages/docusaurus-theme-classic/src",
|
||||
version: "v3.9.2",
|
||||
},
|
||||
// markdown
|
||||
{
|
||||
name: "mdn-learn",
|
||||
repo: "mdn/content/files/en-us/learn_web_development/core",
|
||||
// No tags; pin to a commit (2026-10 main)
|
||||
version: "5137b45128dcf07ac636da68184f00aab30ec1cc",
|
||||
},
|
||||
{
|
||||
name: "mdn-css-guides",
|
||||
repo: "mdn/content/files/en-us/web/css/guides",
|
||||
version: "5137b45128dcf07ac636da68184f00aab30ec1cc",
|
||||
},
|
||||
// jsdoc
|
||||
{
|
||||
name: "svelte",
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
md`
|
||||
\`\`\`md
|
||||
~~~js
|
||||
const a = 1
|
||||
~~~
|
||||
\`\`\`
|
||||
`
|
||||
@@ -0,0 +1,27 @@
|
||||
- item
|
||||
|
||||
```js
|
||||
const s = `a
|
||||
b`
|
||||
/* x
|
||||
y */
|
||||
```
|
||||
|
||||
> - quoted
|
||||
>
|
||||
> ```ts
|
||||
> function f(){return `a
|
||||
> b`}
|
||||
> ```
|
||||
|
||||
```jsx
|
||||
<Button onClick={() => { a(); b() }} />
|
||||
```
|
||||
|
||||
```css
|
||||
a{color:red}
|
||||
```
|
||||
|
||||
```yaml
|
||||
a: 1
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
```javascript
|
||||
const a = 1
|
||||
```
|
||||
|
||||
```mjs
|
||||
export const a = 1
|
||||
```
|
||||
|
||||
```postcss
|
||||
a{color:red}
|
||||
```
|
||||
|
||||
```yml
|
||||
a: 1
|
||||
```
|
||||
|
||||
```gql
|
||||
{ a }
|
||||
```
|
||||
|
||||
```md
|
||||
# a
|
||||
```
|
||||
|
||||
```JS
|
||||
const a = 1
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
```hbs
|
||||
<div class="a">{{#if b}} c {{/if}}</div>
|
||||
```
|
||||
|
||||
```handlebars
|
||||
{{#each items as |item|}}<p>{{item}}</p>{{/each}}
|
||||
```
|
||||
|
||||
```mdx
|
||||
import {A} from './a'
|
||||
|
||||
# Title
|
||||
|
||||
<A b="c" />
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
```js {4}
|
||||
const a = [
|
||||
1, 2,
|
||||
];
|
||||
console.log(a);
|
||||
```
|
||||
@@ -0,0 +1,4 @@
|
||||
```lwc
|
||||
<x-a attr={b}>
|
||||
c</x-a>
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
```md
|
||||
~~~js
|
||||
const a = 1
|
||||
~~~
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
```json
|
||||
|
||||
|
||||
```
|
||||
|
||||
```json5
|
||||
|
||||
|
||||
```
|
||||
@@ -170,6 +170,31 @@ const categories: Category[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "markdown",
|
||||
sources: [
|
||||
{
|
||||
dir: join(EXTERNALS_DIR, "prettier", "markdown"),
|
||||
ext: ".md",
|
||||
// Cursor offsets and plugin loading are not formatter concerns
|
||||
excludes: ["markdown/cursor/", "markdown/broken-plugins/"],
|
||||
},
|
||||
{ dir: join(FIXTURES_DIR, "edge-cases", "xxx-in-md") },
|
||||
],
|
||||
optionSets: [{ printWidth: 80 }, { printWidth: 100, proseWrap: "always" }],
|
||||
},
|
||||
{
|
||||
// Real-world documents, dense with fenced code (css / html / js)
|
||||
name: "markdown-mdn",
|
||||
sources: [
|
||||
{ dir: join(EXTERNALS_DIR, "mdn-learn"), ext: ".md" },
|
||||
{ dir: join(EXTERNALS_DIR, "mdn-css-guides"), ext: ".md" },
|
||||
],
|
||||
// mdn's own `.prettierrc` first: the documents are already formatted by it.
|
||||
// `proseWrap: always` is left to the `markdown` category,
|
||||
// mdn's GitHub alerts and macros would fail on known divergences (`line-shapes`) in bulk.
|
||||
optionSets: [{ printWidth: 80, bracketSameLine: true }, { printWidth: 100 }],
|
||||
},
|
||||
{
|
||||
name: "graphql",
|
||||
sources: [{ dir: join(EXTERNALS_DIR, "gitlab"), ext: ".graphql" }],
|
||||
|
||||
@@ -172,18 +172,24 @@
|
||||
|
||||
## md-in-js
|
||||
|
||||
### Option 1: 9/9 (100.00%)
|
||||
### Option 1: 9/10 (90.00%)
|
||||
|
||||
```json
|
||||
{"printWidth":80}
|
||||
```
|
||||
|
||||
### Option 2: 9/9 (100.00%)
|
||||
- [externals/prettier/js/multiparser-markdown/codeblock.js](diffs/md-in-js/externals__prettier__js__multiparser-markdown__codeblock.js.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#nested-fence-length
|
||||
|
||||
### Option 2: 9/10 (90.00%)
|
||||
|
||||
```json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
```
|
||||
|
||||
- [externals/prettier/js/multiparser-markdown/codeblock.js](diffs/md-in-js/externals__prettier__js__multiparser-markdown__codeblock.js.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#nested-fence-length
|
||||
|
||||
## xxx-in-js-comment
|
||||
|
||||
### Option 1: 7/7 (100.00%)
|
||||
@@ -212,6 +218,88 @@
|
||||
{"printWidth":120,"singleQuote":true,"htmlWhitespaceSensitivity":"ignore","bracketSameLine":true,"svelteIndentScriptAndStyle":true,"svelteSortOrder":"options-scripts-styles-markup","svelte":{"indentScriptAndStyle":true,"sortOrder":"options-scripts-styles-markup"}}
|
||||
```
|
||||
|
||||
## markdown
|
||||
|
||||
### Option 1: 273/282 (96.81%)
|
||||
|
||||
```json
|
||||
{"printWidth":80}
|
||||
```
|
||||
|
||||
- [edge-cases/xxx-in-md/line-ranged-code-block.md](diffs/markdown/edge-cases__xxx-in-md__line-ranged-code-block.md.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#line-ranged-code-block
|
||||
- [edge-cases/xxx-in-md/lwc-code-block.md](diffs/markdown/edge-cases__xxx-in-md__lwc-code-block.md.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#lwc-code-block
|
||||
- [edge-cases/xxx-in-md/nested-fence-length.md](diffs/markdown/edge-cases__xxx-in-md__nested-fence-length.md.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#nested-fence-length
|
||||
- [edge-cases/xxx-in-md/whitespace-only-code-block.md](diffs/markdown/edge-cases__xxx-in-md__whitespace-only-code-block.md.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#whitespace-only-code-block
|
||||
- [externals/prettier/markdown/code/lwc/lwc.md](diffs/markdown/externals__prettier__markdown__code__lwc__lwc.md.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#lwc-code-block
|
||||
- [externals/prettier/markdown/link/encodedLink.md](diffs/markdown/externals__prettier__markdown__link__encodedLink.md.md)
|
||||
- crates/oxc_formatter_markdown/DIVERGENCES.md#url-escaping
|
||||
- [externals/prettier/markdown/list/parser-regression/issue-17778.md](diffs/markdown/externals__prettier__markdown__list__parser-regression__issue-17778.md.md)
|
||||
- crates/oxc_formatter_markdown/DIVERGENCES.md#wrapped-block-starts
|
||||
- [externals/prettier/markdown/markdown/real-world-case.md](diffs/markdown/externals__prettier__markdown__markdown__real-world-case.md.md)
|
||||
- crates/oxc_formatter/DIVERGENCES.md#suppressed-terminator-per-semi
|
||||
- [externals/prettier/markdown/thematicBreak/simple.md](diffs/markdown/externals__prettier__markdown__thematicBreak__simple.md.md)
|
||||
- crates/oxc_formatter_markdown/DIVERGENCES.md#leading-thematic-break
|
||||
|
||||
### Option 2: 270/282 (95.74%)
|
||||
|
||||
```json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
```
|
||||
|
||||
- [edge-cases/xxx-in-md/line-ranged-code-block.md](diffs/markdown/edge-cases__xxx-in-md__line-ranged-code-block.md.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#line-ranged-code-block
|
||||
- [edge-cases/xxx-in-md/lwc-code-block.md](diffs/markdown/edge-cases__xxx-in-md__lwc-code-block.md.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#lwc-code-block
|
||||
- [edge-cases/xxx-in-md/nested-fence-length.md](diffs/markdown/edge-cases__xxx-in-md__nested-fence-length.md.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#nested-fence-length
|
||||
- [edge-cases/xxx-in-md/whitespace-only-code-block.md](diffs/markdown/edge-cases__xxx-in-md__whitespace-only-code-block.md.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#whitespace-only-code-block
|
||||
- [externals/prettier/markdown/blockquote/ignore-code.md](diffs/markdown/externals__prettier__markdown__blockquote__ignore-code.md.md)
|
||||
- crates/oxc_formatter_markdown/DIVERGENCES.md#ignored-block-trailing-quote-line
|
||||
- [externals/prettier/markdown/blockquote/notext-end.md](diffs/markdown/externals__prettier__markdown__blockquote__notext-end.md.md)
|
||||
- crates/oxc_formatter_markdown/DIVERGENCES.md#line-shapes
|
||||
- [externals/prettier/markdown/code/lwc/lwc.md](diffs/markdown/externals__prettier__markdown__code__lwc__lwc.md.md)
|
||||
- apps/oxfmt/DIVERGENCES.md#lwc-code-block
|
||||
- [externals/prettier/markdown/heading/setext/issue-6013-2.md](diffs/markdown/externals__prettier__markdown__heading__setext__issue-6013-2.md.md)
|
||||
- crates/oxc_formatter_markdown/DIVERGENCES.md#setext-heading-wrap
|
||||
- [externals/prettier/markdown/link/encodedLink.md](diffs/markdown/externals__prettier__markdown__link__encodedLink.md.md)
|
||||
- crates/oxc_formatter_markdown/DIVERGENCES.md#url-escaping
|
||||
- [externals/prettier/markdown/markdown/real-world-case.md](diffs/markdown/externals__prettier__markdown__markdown__real-world-case.md.md)
|
||||
- crates/oxc_formatter/DIVERGENCES.md#suppressed-terminator-per-semi
|
||||
- [externals/prettier/markdown/paragraph/cjk.md](diffs/markdown/externals__prettier__markdown__paragraph__cjk.md.md)
|
||||
- crates/oxc_formatter_markdown/DIVERGENCES.md#container-directive
|
||||
- [externals/prettier/markdown/thematicBreak/simple.md](diffs/markdown/externals__prettier__markdown__thematicBreak__simple.md.md)
|
||||
- crates/oxc_formatter_markdown/DIVERGENCES.md#leading-thematic-break
|
||||
|
||||
## markdown-mdn
|
||||
|
||||
### Option 1: 360/362 (99.45%)
|
||||
|
||||
```json
|
||||
{"printWidth":80,"bracketSameLine":true}
|
||||
```
|
||||
|
||||
- [externals/mdn-css-guides/custom_functions_and_mixins/using_custom_functions/index.md](diffs/markdown-mdn/externals__mdn-css-guides__custom_functions_and_mixins__using_custom_functions__index.md.md)
|
||||
- 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)
|
||||
- crates/oxc_formatter_css/DIVERGENCES.md#prelude-string-requote
|
||||
|
||||
### Option 2: 360/362 (99.45%)
|
||||
|
||||
```json
|
||||
{"printWidth":100}
|
||||
```
|
||||
|
||||
- [externals/mdn-css-guides/custom_functions_and_mixins/using_custom_functions/index.md](diffs/markdown-mdn/externals__mdn-css-guides__custom_functions_and_mixins__using_custom_functions__index.md.md)
|
||||
- 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)
|
||||
- crates/oxc_formatter_css/DIVERGENCES.md#prelude-string-requote
|
||||
|
||||
## graphql
|
||||
|
||||
### Option 1: 712/712 (100.00%)
|
||||
|
||||
+1239
File diff suppressed because it is too large.
Load diff
+469
@@ -0,0 +1,469 @@
|
||||
# externals/mdn-css-guides/values_and_units/textual_data_types/index.md
|
||||
|
||||
## Option 1
|
||||
|
||||
`````json
|
||||
{"printWidth":80,"bracketSameLine":true}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -16,9 +16,9 @@
|
||||
```css
|
||||
@keyframes validIdent {
|
||||
/* keyframes go here */
|
||||
}
|
||||
-@keyframes 'validString' {
|
||||
+@keyframes "validString" {
|
||||
/* keyframes go here */
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
---
|
||||
title: Textual data types
|
||||
slug: Web/CSS/Guides/Values_and_units/Textual_data_types
|
||||
page-type: guide
|
||||
sidebar: cssref
|
||||
---
|
||||
|
||||
Every CSS declaration consists of a property/value pair. The value can include various data types depending on the property, such as a single keyword, integer, function, or a combination of different types; some values have units, while others do not. This guide provides an overview of the textual data types. Refer to the page for each value type for more detailed information.
|
||||
|
||||
Text data types are either {{cssxref("<string>")}}, a quoted series of characters, an {{cssxref("<ident>")}}, a "CSS Identifier" which is an unquoted string, or an optionally quoted {{cssxref("url_value", "<url>")}}. A `<string>` is quoted with either single or double quotes. CSS identifiers, listed in the specifications as `<ident>` or {{cssxref("<custom-ident>")}}, must be unquoted.
|
||||
|
||||
In CSS specifications, values that can be defined by the web developer — such as keyframe animation names, font-family names, or grid areas — are listed as a {{cssxref("<custom-ident>")}}, {{cssxref("<string>")}}, or both.
|
||||
|
||||
When both quoted and unquoted user-defined text values are permitted, the specification will list `<custom-ident> | <string>`, meaning quotes are optional, such as is the case with [keyframe animation names]:
|
||||
|
||||
```css
|
||||
@keyframes validIdent {
|
||||
/* keyframes go here */
|
||||
}
|
||||
@keyframes "validString" {
|
||||
/* keyframes go here */
|
||||
}
|
||||
```
|
||||
|
||||
Some text values are not valid if encompassed in quotes. For example, the value of {{cssxref("grid-area")}} can be a `<custom-ident>`, so if we had a grid area named `content` we would use it without quotes:
|
||||
|
||||
```css
|
||||
.item {
|
||||
grid-area: content;
|
||||
}
|
||||
```
|
||||
|
||||
In comparison, a data type that is a {{cssxref("<string>")}}, such as a string value of the {{cssxref("content")}} property, must be quoted:
|
||||
|
||||
```css
|
||||
.item::after {
|
||||
content: "This is my content.";
|
||||
}
|
||||
```
|
||||
|
||||
While you can generally create any name you want, including using emojis, the identifier can't be `none`, `unset`, `initial`, or `inherit`, start with a digit or two dashes, and generally you don't want it to be any other pre-defined CSS keyword. See the {{cssxref("<custom-ident>")}} and {{cssxref("<string>")}} reference pages for more details.
|
||||
|
||||
## Pre-defined keyword values
|
||||
|
||||
Pre-defined keywords are text values defined by the specification for that property. These keywords are also CSS identifiers, and are therefore used without quotes.
|
||||
|
||||
When viewing CSS property value syntax in a CSS specification or the MDN property page, allowable keywords will be listed in the following form. The following {{glossary("enumerated")}} values are the pre-defined keyword values allowed for {{cssxref("float")}}.
|
||||
|
||||
```plain
|
||||
left | right | none | inline-start | inline-end
|
||||
```
|
||||
|
||||
Such values are used without quotes:
|
||||
|
||||
```css
|
||||
.box {
|
||||
float: left;
|
||||
}
|
||||
```
|
||||
|
||||
## CSS-wide keywords
|
||||
|
||||
In addition to the pre-defined keywords that are part of the specification for a property, all CSS properties accept the CSS-wide, or "global", property values {{cssxref("initial")}}, {{cssxref("inherit")}}, {{cssxref("unset")}}, {{cssxref("revert")}}, and {{cssxref("revert-layer")}}, which explicitly specify defaulting behaviors.
|
||||
|
||||
- {{cssxref("initial")}}
|
||||
- : Represents the value specified as the property's initial value.
|
||||
- {{cssxref("inherit")}}
|
||||
- : Represents the computed value of the property on the element's parent, provided it is inherited.
|
||||
- {{cssxref("unset")}}
|
||||
- : Acts as either `inherit` or `initial`, depending on whether the property is inherited or not.
|
||||
- {{cssxref("revert")}}
|
||||
- : Resets the property to its inherited value if it inherits from its parent or to the default value established by the user agent's stylesheet (or by user styles if any exist).
|
||||
- {{cssxref("revert-layer")}}
|
||||
- : Rolls back the value of a property in a [cascade layer](/en-US/docs/Web/CSS/Reference/At-rules/@layer) to the value of the property in a CSS rule matching the element in a previous cascade layer. The value of the property with this keyword is recalculated as if no rules were specified on the target element in the current cascade layer.
|
||||
|
||||
## URLs
|
||||
|
||||
A {{cssxref("url_value", "<url>")}} type uses functional notation, which accepts a `<string>` that is a URL. This may be an absolute URL or a relative URL. For example, if you wanted to include a background image, you might use either of the following:
|
||||
|
||||
```css
|
||||
.box {
|
||||
background-image: url("images/my-background.png");
|
||||
}
|
||||
|
||||
.box {
|
||||
background-image: url("https://www.exammple.com/images/my-background.png");
|
||||
}
|
||||
```
|
||||
|
||||
The parameter for `url()` can be either quoted or unquoted. If unquoted, it is parsed as a `<url-token>`, which has extra requirements including escaping certain characters. See {{cssxref("url_value", "<url>")}} for more information.
|
||||
|
||||
## See also
|
||||
|
||||
- [Numeric data types](/en-US/docs/Web/CSS/Guides/Values_and_units/Numeric_data_types)
|
||||
- [CSS data types](/en-US/docs/Web/CSS/Reference/Values/Data_types)
|
||||
- [CSS values and units](/en-US/docs/Web/CSS/Guides/Values_and_units) module
|
||||
- [Learn: Values and units](/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units)
|
||||
- [CSS cascade and inheritance](/en-US/docs/Web/CSS/Guides/Cascade) module
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
---
|
||||
title: Textual data types
|
||||
slug: Web/CSS/Guides/Values_and_units/Textual_data_types
|
||||
page-type: guide
|
||||
sidebar: cssref
|
||||
---
|
||||
|
||||
Every CSS declaration consists of a property/value pair. The value can include various data types depending on the property, such as a single keyword, integer, function, or a combination of different types; some values have units, while others do not. This guide provides an overview of the textual data types. Refer to the page for each value type for more detailed information.
|
||||
|
||||
Text data types are either {{cssxref("<string>")}}, a quoted series of characters, an {{cssxref("<ident>")}}, a "CSS Identifier" which is an unquoted string, or an optionally quoted {{cssxref("url_value", "<url>")}}. A `<string>` is quoted with either single or double quotes. CSS identifiers, listed in the specifications as `<ident>` or {{cssxref("<custom-ident>")}}, must be unquoted.
|
||||
|
||||
In CSS specifications, values that can be defined by the web developer — such as keyframe animation names, font-family names, or grid areas — are listed as a {{cssxref("<custom-ident>")}}, {{cssxref("<string>")}}, or both.
|
||||
|
||||
When both quoted and unquoted user-defined text values are permitted, the specification will list `<custom-ident> | <string>`, meaning quotes are optional, such as is the case with [keyframe animation names]:
|
||||
|
||||
```css
|
||||
@keyframes validIdent {
|
||||
/* keyframes go here */
|
||||
}
|
||||
@keyframes 'validString' {
|
||||
/* keyframes go here */
|
||||
}
|
||||
```
|
||||
|
||||
Some text values are not valid if encompassed in quotes. For example, the value of {{cssxref("grid-area")}} can be a `<custom-ident>`, so if we had a grid area named `content` we would use it without quotes:
|
||||
|
||||
```css
|
||||
.item {
|
||||
grid-area: content;
|
||||
}
|
||||
```
|
||||
|
||||
In comparison, a data type that is a {{cssxref("<string>")}}, such as a string value of the {{cssxref("content")}} property, must be quoted:
|
||||
|
||||
```css
|
||||
.item::after {
|
||||
content: "This is my content.";
|
||||
}
|
||||
```
|
||||
|
||||
While you can generally create any name you want, including using emojis, the identifier can't be `none`, `unset`, `initial`, or `inherit`, start with a digit or two dashes, and generally you don't want it to be any other pre-defined CSS keyword. See the {{cssxref("<custom-ident>")}} and {{cssxref("<string>")}} reference pages for more details.
|
||||
|
||||
## Pre-defined keyword values
|
||||
|
||||
Pre-defined keywords are text values defined by the specification for that property. These keywords are also CSS identifiers, and are therefore used without quotes.
|
||||
|
||||
When viewing CSS property value syntax in a CSS specification or the MDN property page, allowable keywords will be listed in the following form. The following {{glossary("enumerated")}} values are the pre-defined keyword values allowed for {{cssxref("float")}}.
|
||||
|
||||
```plain
|
||||
left | right | none | inline-start | inline-end
|
||||
```
|
||||
|
||||
Such values are used without quotes:
|
||||
|
||||
```css
|
||||
.box {
|
||||
float: left;
|
||||
}
|
||||
```
|
||||
|
||||
## CSS-wide keywords
|
||||
|
||||
In addition to the pre-defined keywords that are part of the specification for a property, all CSS properties accept the CSS-wide, or "global", property values {{cssxref("initial")}}, {{cssxref("inherit")}}, {{cssxref("unset")}}, {{cssxref("revert")}}, and {{cssxref("revert-layer")}}, which explicitly specify defaulting behaviors.
|
||||
|
||||
- {{cssxref("initial")}}
|
||||
- : Represents the value specified as the property's initial value.
|
||||
- {{cssxref("inherit")}}
|
||||
- : Represents the computed value of the property on the element's parent, provided it is inherited.
|
||||
- {{cssxref("unset")}}
|
||||
- : Acts as either `inherit` or `initial`, depending on whether the property is inherited or not.
|
||||
- {{cssxref("revert")}}
|
||||
- : Resets the property to its inherited value if it inherits from its parent or to the default value established by the user agent's stylesheet (or by user styles if any exist).
|
||||
- {{cssxref("revert-layer")}}
|
||||
- : Rolls back the value of a property in a [cascade layer](/en-US/docs/Web/CSS/Reference/At-rules/@layer) to the value of the property in a CSS rule matching the element in a previous cascade layer. The value of the property with this keyword is recalculated as if no rules were specified on the target element in the current cascade layer.
|
||||
|
||||
## URLs
|
||||
|
||||
A {{cssxref("url_value", "<url>")}} type uses functional notation, which accepts a `<string>` that is a URL. This may be an absolute URL or a relative URL. For example, if you wanted to include a background image, you might use either of the following:
|
||||
|
||||
```css
|
||||
.box {
|
||||
background-image: url("images/my-background.png");
|
||||
}
|
||||
|
||||
.box {
|
||||
background-image: url("https://www.exammple.com/images/my-background.png");
|
||||
}
|
||||
```
|
||||
|
||||
The parameter for `url()` can be either quoted or unquoted. If unquoted, it is parsed as a `<url-token>`, which has extra requirements including escaping certain characters. See {{cssxref("url_value", "<url>")}} for more information.
|
||||
|
||||
## See also
|
||||
|
||||
- [Numeric data types](/en-US/docs/Web/CSS/Guides/Values_and_units/Numeric_data_types)
|
||||
- [CSS data types](/en-US/docs/Web/CSS/Reference/Values/Data_types)
|
||||
- [CSS values and units](/en-US/docs/Web/CSS/Guides/Values_and_units) module
|
||||
- [Learn: Values and units](/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units)
|
||||
- [CSS cascade and inheritance](/en-US/docs/Web/CSS/Guides/Cascade) module
|
||||
|
||||
`````
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -16,9 +16,9 @@
|
||||
```css
|
||||
@keyframes validIdent {
|
||||
/* keyframes go here */
|
||||
}
|
||||
-@keyframes 'validString' {
|
||||
+@keyframes "validString" {
|
||||
/* keyframes go here */
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
---
|
||||
title: Textual data types
|
||||
slug: Web/CSS/Guides/Values_and_units/Textual_data_types
|
||||
page-type: guide
|
||||
sidebar: cssref
|
||||
---
|
||||
|
||||
Every CSS declaration consists of a property/value pair. The value can include various data types depending on the property, such as a single keyword, integer, function, or a combination of different types; some values have units, while others do not. This guide provides an overview of the textual data types. Refer to the page for each value type for more detailed information.
|
||||
|
||||
Text data types are either {{cssxref("<string>")}}, a quoted series of characters, an {{cssxref("<ident>")}}, a "CSS Identifier" which is an unquoted string, or an optionally quoted {{cssxref("url_value", "<url>")}}. A `<string>` is quoted with either single or double quotes. CSS identifiers, listed in the specifications as `<ident>` or {{cssxref("<custom-ident>")}}, must be unquoted.
|
||||
|
||||
In CSS specifications, values that can be defined by the web developer — such as keyframe animation names, font-family names, or grid areas — are listed as a {{cssxref("<custom-ident>")}}, {{cssxref("<string>")}}, or both.
|
||||
|
||||
When both quoted and unquoted user-defined text values are permitted, the specification will list `<custom-ident> | <string>`, meaning quotes are optional, such as is the case with [keyframe animation names]:
|
||||
|
||||
```css
|
||||
@keyframes validIdent {
|
||||
/* keyframes go here */
|
||||
}
|
||||
@keyframes "validString" {
|
||||
/* keyframes go here */
|
||||
}
|
||||
```
|
||||
|
||||
Some text values are not valid if encompassed in quotes. For example, the value of {{cssxref("grid-area")}} can be a `<custom-ident>`, so if we had a grid area named `content` we would use it without quotes:
|
||||
|
||||
```css
|
||||
.item {
|
||||
grid-area: content;
|
||||
}
|
||||
```
|
||||
|
||||
In comparison, a data type that is a {{cssxref("<string>")}}, such as a string value of the {{cssxref("content")}} property, must be quoted:
|
||||
|
||||
```css
|
||||
.item::after {
|
||||
content: "This is my content.";
|
||||
}
|
||||
```
|
||||
|
||||
While you can generally create any name you want, including using emojis, the identifier can't be `none`, `unset`, `initial`, or `inherit`, start with a digit or two dashes, and generally you don't want it to be any other pre-defined CSS keyword. See the {{cssxref("<custom-ident>")}} and {{cssxref("<string>")}} reference pages for more details.
|
||||
|
||||
## Pre-defined keyword values
|
||||
|
||||
Pre-defined keywords are text values defined by the specification for that property. These keywords are also CSS identifiers, and are therefore used without quotes.
|
||||
|
||||
When viewing CSS property value syntax in a CSS specification or the MDN property page, allowable keywords will be listed in the following form. The following {{glossary("enumerated")}} values are the pre-defined keyword values allowed for {{cssxref("float")}}.
|
||||
|
||||
```plain
|
||||
left | right | none | inline-start | inline-end
|
||||
```
|
||||
|
||||
Such values are used without quotes:
|
||||
|
||||
```css
|
||||
.box {
|
||||
float: left;
|
||||
}
|
||||
```
|
||||
|
||||
## CSS-wide keywords
|
||||
|
||||
In addition to the pre-defined keywords that are part of the specification for a property, all CSS properties accept the CSS-wide, or "global", property values {{cssxref("initial")}}, {{cssxref("inherit")}}, {{cssxref("unset")}}, {{cssxref("revert")}}, and {{cssxref("revert-layer")}}, which explicitly specify defaulting behaviors.
|
||||
|
||||
- {{cssxref("initial")}}
|
||||
- : Represents the value specified as the property's initial value.
|
||||
- {{cssxref("inherit")}}
|
||||
- : Represents the computed value of the property on the element's parent, provided it is inherited.
|
||||
- {{cssxref("unset")}}
|
||||
- : Acts as either `inherit` or `initial`, depending on whether the property is inherited or not.
|
||||
- {{cssxref("revert")}}
|
||||
- : Resets the property to its inherited value if it inherits from its parent or to the default value established by the user agent's stylesheet (or by user styles if any exist).
|
||||
- {{cssxref("revert-layer")}}
|
||||
- : Rolls back the value of a property in a [cascade layer](/en-US/docs/Web/CSS/Reference/At-rules/@layer) to the value of the property in a CSS rule matching the element in a previous cascade layer. The value of the property with this keyword is recalculated as if no rules were specified on the target element in the current cascade layer.
|
||||
|
||||
## URLs
|
||||
|
||||
A {{cssxref("url_value", "<url>")}} type uses functional notation, which accepts a `<string>` that is a URL. This may be an absolute URL or a relative URL. For example, if you wanted to include a background image, you might use either of the following:
|
||||
|
||||
```css
|
||||
.box {
|
||||
background-image: url("images/my-background.png");
|
||||
}
|
||||
|
||||
.box {
|
||||
background-image: url("https://www.exammple.com/images/my-background.png");
|
||||
}
|
||||
```
|
||||
|
||||
The parameter for `url()` can be either quoted or unquoted. If unquoted, it is parsed as a `<url-token>`, which has extra requirements including escaping certain characters. See {{cssxref("url_value", "<url>")}} for more information.
|
||||
|
||||
## See also
|
||||
|
||||
- [Numeric data types](/en-US/docs/Web/CSS/Guides/Values_and_units/Numeric_data_types)
|
||||
- [CSS data types](/en-US/docs/Web/CSS/Reference/Values/Data_types)
|
||||
- [CSS values and units](/en-US/docs/Web/CSS/Guides/Values_and_units) module
|
||||
- [Learn: Values and units](/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units)
|
||||
- [CSS cascade and inheritance](/en-US/docs/Web/CSS/Guides/Cascade) module
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
---
|
||||
title: Textual data types
|
||||
slug: Web/CSS/Guides/Values_and_units/Textual_data_types
|
||||
page-type: guide
|
||||
sidebar: cssref
|
||||
---
|
||||
|
||||
Every CSS declaration consists of a property/value pair. The value can include various data types depending on the property, such as a single keyword, integer, function, or a combination of different types; some values have units, while others do not. This guide provides an overview of the textual data types. Refer to the page for each value type for more detailed information.
|
||||
|
||||
Text data types are either {{cssxref("<string>")}}, a quoted series of characters, an {{cssxref("<ident>")}}, a "CSS Identifier" which is an unquoted string, or an optionally quoted {{cssxref("url_value", "<url>")}}. A `<string>` is quoted with either single or double quotes. CSS identifiers, listed in the specifications as `<ident>` or {{cssxref("<custom-ident>")}}, must be unquoted.
|
||||
|
||||
In CSS specifications, values that can be defined by the web developer — such as keyframe animation names, font-family names, or grid areas — are listed as a {{cssxref("<custom-ident>")}}, {{cssxref("<string>")}}, or both.
|
||||
|
||||
When both quoted and unquoted user-defined text values are permitted, the specification will list `<custom-ident> | <string>`, meaning quotes are optional, such as is the case with [keyframe animation names]:
|
||||
|
||||
```css
|
||||
@keyframes validIdent {
|
||||
/* keyframes go here */
|
||||
}
|
||||
@keyframes 'validString' {
|
||||
/* keyframes go here */
|
||||
}
|
||||
```
|
||||
|
||||
Some text values are not valid if encompassed in quotes. For example, the value of {{cssxref("grid-area")}} can be a `<custom-ident>`, so if we had a grid area named `content` we would use it without quotes:
|
||||
|
||||
```css
|
||||
.item {
|
||||
grid-area: content;
|
||||
}
|
||||
```
|
||||
|
||||
In comparison, a data type that is a {{cssxref("<string>")}}, such as a string value of the {{cssxref("content")}} property, must be quoted:
|
||||
|
||||
```css
|
||||
.item::after {
|
||||
content: "This is my content.";
|
||||
}
|
||||
```
|
||||
|
||||
While you can generally create any name you want, including using emojis, the identifier can't be `none`, `unset`, `initial`, or `inherit`, start with a digit or two dashes, and generally you don't want it to be any other pre-defined CSS keyword. See the {{cssxref("<custom-ident>")}} and {{cssxref("<string>")}} reference pages for more details.
|
||||
|
||||
## Pre-defined keyword values
|
||||
|
||||
Pre-defined keywords are text values defined by the specification for that property. These keywords are also CSS identifiers, and are therefore used without quotes.
|
||||
|
||||
When viewing CSS property value syntax in a CSS specification or the MDN property page, allowable keywords will be listed in the following form. The following {{glossary("enumerated")}} values are the pre-defined keyword values allowed for {{cssxref("float")}}.
|
||||
|
||||
```plain
|
||||
left | right | none | inline-start | inline-end
|
||||
```
|
||||
|
||||
Such values are used without quotes:
|
||||
|
||||
```css
|
||||
.box {
|
||||
float: left;
|
||||
}
|
||||
```
|
||||
|
||||
## CSS-wide keywords
|
||||
|
||||
In addition to the pre-defined keywords that are part of the specification for a property, all CSS properties accept the CSS-wide, or "global", property values {{cssxref("initial")}}, {{cssxref("inherit")}}, {{cssxref("unset")}}, {{cssxref("revert")}}, and {{cssxref("revert-layer")}}, which explicitly specify defaulting behaviors.
|
||||
|
||||
- {{cssxref("initial")}}
|
||||
- : Represents the value specified as the property's initial value.
|
||||
- {{cssxref("inherit")}}
|
||||
- : Represents the computed value of the property on the element's parent, provided it is inherited.
|
||||
- {{cssxref("unset")}}
|
||||
- : Acts as either `inherit` or `initial`, depending on whether the property is inherited or not.
|
||||
- {{cssxref("revert")}}
|
||||
- : Resets the property to its inherited value if it inherits from its parent or to the default value established by the user agent's stylesheet (or by user styles if any exist).
|
||||
- {{cssxref("revert-layer")}}
|
||||
- : Rolls back the value of a property in a [cascade layer](/en-US/docs/Web/CSS/Reference/At-rules/@layer) to the value of the property in a CSS rule matching the element in a previous cascade layer. The value of the property with this keyword is recalculated as if no rules were specified on the target element in the current cascade layer.
|
||||
|
||||
## URLs
|
||||
|
||||
A {{cssxref("url_value", "<url>")}} type uses functional notation, which accepts a `<string>` that is a URL. This may be an absolute URL or a relative URL. For example, if you wanted to include a background image, you might use either of the following:
|
||||
|
||||
```css
|
||||
.box {
|
||||
background-image: url("images/my-background.png");
|
||||
}
|
||||
|
||||
.box {
|
||||
background-image: url("https://www.exammple.com/images/my-background.png");
|
||||
}
|
||||
```
|
||||
|
||||
The parameter for `url()` can be either quoted or unquoted. If unquoted, it is parsed as a `<url-token>`, which has extra requirements including escaping certain characters. See {{cssxref("url_value", "<url>")}} for more information.
|
||||
|
||||
## See also
|
||||
|
||||
- [Numeric data types](/en-US/docs/Web/CSS/Guides/Values_and_units/Numeric_data_types)
|
||||
- [CSS data types](/en-US/docs/Web/CSS/Reference/Values/Data_types)
|
||||
- [CSS values and units](/en-US/docs/Web/CSS/Guides/Values_and_units) module
|
||||
- [Learn: Values and units](/en-US/docs/Learn_web_development/Core/Styling_basics/Values_and_units)
|
||||
- [CSS cascade and inheritance](/en-US/docs/Web/CSS/Guides/Cascade) module
|
||||
|
||||
`````
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
# edge-cases/xxx-in-md/line-ranged-code-block.md
|
||||
|
||||
## Option 1
|
||||
|
||||
`````json
|
||||
{"printWidth":80}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,4 +1,6 @@
|
||||
```js {4}
|
||||
-const a = [1, 2];
|
||||
+const a = [
|
||||
+ 1, 2,
|
||||
+];
|
||||
console.log(a);
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
```js {4}
|
||||
const a = [
|
||||
1, 2,
|
||||
];
|
||||
console.log(a);
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
```js {4}
|
||||
const a = [1, 2];
|
||||
console.log(a);
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,4 +1,6 @@
|
||||
```js {4}
|
||||
-const a = [1, 2];
|
||||
+const a = [
|
||||
+ 1, 2,
|
||||
+];
|
||||
console.log(a);
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
```js {4}
|
||||
const a = [
|
||||
1, 2,
|
||||
];
|
||||
console.log(a);
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
```js {4}
|
||||
const a = [1, 2];
|
||||
console.log(a);
|
||||
```
|
||||
|
||||
`````
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
# edge-cases/xxx-in-md/lwc-code-block.md
|
||||
|
||||
## Option 1
|
||||
|
||||
`````json
|
||||
{"printWidth":80}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,3 +1,4 @@
|
||||
```lwc
|
||||
-<x-a attr={b}> c</x-a>
|
||||
+<x-a attr={b}>
|
||||
+ c</x-a>
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
```lwc
|
||||
<x-a attr={b}>
|
||||
c</x-a>
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
```lwc
|
||||
<x-a attr={b}> c</x-a>
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,3 +1,4 @@
|
||||
```lwc
|
||||
-<x-a attr={b}> c</x-a>
|
||||
+<x-a attr={b}>
|
||||
+ c</x-a>
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
```lwc
|
||||
<x-a attr={b}>
|
||||
c</x-a>
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
```lwc
|
||||
<x-a attr={b}> c</x-a>
|
||||
```
|
||||
|
||||
`````
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
# edge-cases/xxx-in-md/nested-fence-length.md
|
||||
|
||||
## Option 1
|
||||
|
||||
`````json
|
||||
{"printWidth":80}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,5 +1,5 @@
|
||||
-```md
|
||||
+````md
|
||||
```js
|
||||
const a = 1;
|
||||
```
|
||||
-```
|
||||
+````
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
````md
|
||||
```js
|
||||
const a = 1;
|
||||
```
|
||||
````
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
```md
|
||||
```js
|
||||
const a = 1;
|
||||
```
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,5 +1,5 @@
|
||||
-```md
|
||||
+````md
|
||||
```js
|
||||
const a = 1;
|
||||
```
|
||||
-```
|
||||
+````
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
````md
|
||||
```js
|
||||
const a = 1;
|
||||
```
|
||||
````
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
```md
|
||||
```js
|
||||
const a = 1;
|
||||
```
|
||||
```
|
||||
|
||||
`````
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
# edge-cases/xxx-in-md/whitespace-only-code-block.md
|
||||
|
||||
## Option 1
|
||||
|
||||
`````json
|
||||
{"printWidth":80}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,9 +1,7 @@
|
||||
```json
|
||||
|
||||
-
|
||||
```
|
||||
|
||||
```json5
|
||||
|
||||
-
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
```json
|
||||
|
||||
```
|
||||
|
||||
```json5
|
||||
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
```json
|
||||
|
||||
|
||||
```
|
||||
|
||||
```json5
|
||||
|
||||
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,9 +1,7 @@
|
||||
```json
|
||||
|
||||
-
|
||||
```
|
||||
|
||||
```json5
|
||||
|
||||
-
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
```json
|
||||
|
||||
```
|
||||
|
||||
```json5
|
||||
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
```json
|
||||
|
||||
|
||||
```
|
||||
|
||||
```json5
|
||||
|
||||
|
||||
```
|
||||
|
||||
`````
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
# externals/prettier/markdown/blockquote/ignore-code.md
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -41,9 +41,8 @@
|
||||
> <!-- prettier-ignore -->
|
||||
> - This is a long long
|
||||
> long long long long
|
||||
> long long paragraph.
|
||||
->
|
||||
|
||||
> ```js
|
||||
> // prettier-ignore
|
||||
> const x = 1,
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
> ````md
|
||||
> <!-- prettier-ignore -->
|
||||
> ```js
|
||||
> ugly ( code ) ;
|
||||
> ```
|
||||
> ````
|
||||
|
||||
> ```md
|
||||
> <!-- prettier-ignore -->
|
||||
> - This is a long long
|
||||
> long long long long
|
||||
> long long paragraph.
|
||||
> ```
|
||||
|
||||
> - test
|
||||
> ```md
|
||||
> <!-- prettier-ignore -->
|
||||
> - This is a long long
|
||||
> long long long long
|
||||
> long long paragraph.
|
||||
> ```
|
||||
|
||||
````md
|
||||
> ```md
|
||||
> <!-- prettier-ignore -->
|
||||
> - This is a long long
|
||||
> long long long long
|
||||
> long long paragraph.
|
||||
> ```
|
||||
````
|
||||
|
||||
> ````md
|
||||
> > ```md
|
||||
> > <!-- prettier-ignore -->
|
||||
> > - This is a long long
|
||||
> > long long long long
|
||||
> > long long paragraph.
|
||||
> > ```
|
||||
> ````
|
||||
|
||||
> <!-- prettier-ignore -->
|
||||
> - This is a long long
|
||||
> long long long long
|
||||
> long long paragraph.
|
||||
|
||||
> ```js
|
||||
> // prettier-ignore
|
||||
> const x = 1,
|
||||
> b = 2;
|
||||
> ```
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
> ````md
|
||||
> <!-- prettier-ignore -->
|
||||
> ```js
|
||||
> ugly ( code ) ;
|
||||
> ```
|
||||
> ````
|
||||
|
||||
> ```md
|
||||
> <!-- prettier-ignore -->
|
||||
> - This is a long long
|
||||
> long long long long
|
||||
> long long paragraph.
|
||||
> ```
|
||||
|
||||
> - test
|
||||
> ```md
|
||||
> <!-- prettier-ignore -->
|
||||
> - This is a long long
|
||||
> long long long long
|
||||
> long long paragraph.
|
||||
> ```
|
||||
|
||||
````md
|
||||
> ```md
|
||||
> <!-- prettier-ignore -->
|
||||
> - This is a long long
|
||||
> long long long long
|
||||
> long long paragraph.
|
||||
> ```
|
||||
````
|
||||
|
||||
> ````md
|
||||
> > ```md
|
||||
> > <!-- prettier-ignore -->
|
||||
> > - This is a long long
|
||||
> > long long long long
|
||||
> > long long paragraph.
|
||||
> > ```
|
||||
> ````
|
||||
|
||||
> <!-- prettier-ignore -->
|
||||
> - This is a long long
|
||||
> long long long long
|
||||
> long long paragraph.
|
||||
>
|
||||
|
||||
> ```js
|
||||
> // prettier-ignore
|
||||
> const x = 1,
|
||||
> b = 2;
|
||||
> ```
|
||||
|
||||
`````
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
# externals/prettier/markdown/blockquote/notext-end.md
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,5 +1,6 @@
|
||||
-> [!NOTE] `DOOM`
|
||||
+> [!NOTE]
|
||||
+> `DOOM`
|
||||
|
||||
> _b_
|
||||
>
|
||||
> > `A` `B`
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
> [!NOTE]
|
||||
> `DOOM`
|
||||
|
||||
> _b_
|
||||
>
|
||||
> > `A` `B`
|
||||
|
||||
> _a_
|
||||
>
|
||||
> > # foo
|
||||
> >
|
||||
> > `a` > `b`
|
||||
|
||||
> This is a quote with an italic _across multuple lines which should just work_. So make sure there
|
||||
> is no > if we set proseWrap to `never`
|
||||
|
||||
> This is a quote with a link [across multuple lines which should just work](<>). So make sure there
|
||||
> is no > if we set proseWrap to `never`
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
> [!NOTE] `DOOM`
|
||||
|
||||
> _b_
|
||||
>
|
||||
> > `A` `B`
|
||||
|
||||
> _a_
|
||||
>
|
||||
> > # foo
|
||||
> >
|
||||
> > `a` > `b`
|
||||
|
||||
> This is a quote with an italic _across multuple lines which should just work_. So make sure there
|
||||
> is no > if we set proseWrap to `never`
|
||||
|
||||
> This is a quote with a link [across multuple lines which should just work](<>). So make sure there
|
||||
> is no > if we set proseWrap to `never`
|
||||
|
||||
`````
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
# externals/prettier/markdown/code/lwc/lwc.md
|
||||
|
||||
## Option 1
|
||||
|
||||
`````json
|
||||
{"printWidth":80}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -2,6 +2,7 @@
|
||||
<test-this attr="{should-be-quoted}"> Awesome</test-this>
|
||||
```
|
||||
|
||||
```lwc
|
||||
-<test-this attr={should-NOT-quoted}> Awesome</test-this>
|
||||
+<test-this attr={should-NOT-quoted}>
|
||||
+ Awesome</test-this>
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
```html
|
||||
<test-this attr="{should-be-quoted}"> Awesome</test-this>
|
||||
```
|
||||
|
||||
```lwc
|
||||
<test-this attr={should-NOT-quoted}>
|
||||
Awesome</test-this>
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
```html
|
||||
<test-this attr="{should-be-quoted}"> Awesome</test-this>
|
||||
```
|
||||
|
||||
```lwc
|
||||
<test-this attr={should-NOT-quoted}> Awesome</test-this>
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -2,6 +2,7 @@
|
||||
<test-this attr="{should-be-quoted}"> Awesome</test-this>
|
||||
```
|
||||
|
||||
```lwc
|
||||
-<test-this attr={should-NOT-quoted}> Awesome</test-this>
|
||||
+<test-this attr={should-NOT-quoted}>
|
||||
+ Awesome</test-this>
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
```html
|
||||
<test-this attr="{should-be-quoted}"> Awesome</test-this>
|
||||
```
|
||||
|
||||
```lwc
|
||||
<test-this attr={should-NOT-quoted}>
|
||||
Awesome</test-this>
|
||||
```
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
```html
|
||||
<test-this attr="{should-be-quoted}"> Awesome</test-this>
|
||||
```
|
||||
|
||||
```lwc
|
||||
<test-this attr={should-NOT-quoted}> Awesome</test-this>
|
||||
```
|
||||
|
||||
`````
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
# externals/prettier/markdown/heading/setext/issue-6013-2.md
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,2 +1,3 @@
|
||||
-Heading [](https://shields.io)
|
||||
+Heading
|
||||
+[](https://shields.io)
|
||||
=======
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
Heading
|
||||
[](https://shields.io)
|
||||
=======
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
Heading [](https://shields.io)
|
||||
=======
|
||||
|
||||
`````
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
# externals/prettier/markdown/link/encodedLink.md
|
||||
|
||||
## Option 1
|
||||
|
||||
`````json
|
||||
{"printWidth":80}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,11 +1,11 @@
|
||||
-[link](<https://www.google.fr/()foo-%3Ebar>)
|
||||
+[link](<https://www.google.fr/()foo-\>bar>)
|
||||
[link](https://www.google.fr/foo->bar)
|
||||
[link](https://www.google.fr/foo-%3Ebar)
|
||||
[link](https://www.google.fr/foo-<bar)
|
||||
[link](https://www.google.fr/foo-%3Cbar)
|
||||
|
||||
-foo-%3Ebar>)
|
||||
+foo-\>bar>)
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
[link](<https://www.google.fr/()foo-\>bar>)
|
||||
[link](https://www.google.fr/foo->bar)
|
||||
[link](https://www.google.fr/foo-%3Ebar)
|
||||
[link](https://www.google.fr/foo-<bar)
|
||||
[link](https://www.google.fr/foo-%3Cbar)
|
||||
|
||||
foo-\>bar>)
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
[link]: https://www.google.fr/()foo->bar
|
||||
[link]: https://www.google.fr/foo->bar
|
||||
[link]: https://www.google.fr/foo-%3Ebar
|
||||
[link]: https://www.google.fr/foo-<bar
|
||||
[link]: https://www.google.fr/foo-%3Cbar
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
[link](<https://www.google.fr/()foo-%3Ebar>)
|
||||
[link](https://www.google.fr/foo->bar)
|
||||
[link](https://www.google.fr/foo-%3Ebar)
|
||||
[link](https://www.google.fr/foo-<bar)
|
||||
[link](https://www.google.fr/foo-%3Cbar)
|
||||
|
||||
foo-%3Ebar>)
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
[link]: https://www.google.fr/()foo->bar
|
||||
[link]: https://www.google.fr/foo->bar
|
||||
[link]: https://www.google.fr/foo-%3Ebar
|
||||
[link]: https://www.google.fr/foo-<bar
|
||||
[link]: https://www.google.fr/foo-%3Cbar
|
||||
|
||||
`````
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,9 +1,9 @@
|
||||
-[link](<https://www.google.fr/()foo-%3Ebar>) [link](https://www.google.fr/foo->bar)
|
||||
+[link](<https://www.google.fr/()foo-\>bar>) [link](https://www.google.fr/foo->bar)
|
||||
[link](https://www.google.fr/foo-%3Ebar) [link](https://www.google.fr/foo-<bar)
|
||||
[link](https://www.google.fr/foo-%3Cbar)
|
||||
|
||||
-foo-%3Ebar>) 
|
||||
+foo-\>bar>) 
|
||||
 
|
||||

|
||||
|
||||
[link]: https://www.google.fr/()foo->bar
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
[link](<https://www.google.fr/()foo-\>bar>) [link](https://www.google.fr/foo->bar)
|
||||
[link](https://www.google.fr/foo-%3Ebar) [link](https://www.google.fr/foo-<bar)
|
||||
[link](https://www.google.fr/foo-%3Cbar)
|
||||
|
||||
foo-\>bar>) 
|
||||
 
|
||||

|
||||
|
||||
[link]: https://www.google.fr/()foo->bar
|
||||
[link]: https://www.google.fr/foo->bar
|
||||
[link]: https://www.google.fr/foo-%3Ebar
|
||||
[link]: https://www.google.fr/foo-<bar
|
||||
[link]: https://www.google.fr/foo-%3Cbar
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
[link](<https://www.google.fr/()foo-%3Ebar>) [link](https://www.google.fr/foo->bar)
|
||||
[link](https://www.google.fr/foo-%3Ebar) [link](https://www.google.fr/foo-<bar)
|
||||
[link](https://www.google.fr/foo-%3Cbar)
|
||||
|
||||
foo-%3Ebar>) 
|
||||
 
|
||||

|
||||
|
||||
[link]: https://www.google.fr/()foo->bar
|
||||
[link]: https://www.google.fr/foo->bar
|
||||
[link]: https://www.google.fr/foo-%3Ebar
|
||||
[link]: https://www.google.fr/foo-<bar
|
||||
[link]: https://www.google.fr/foo-%3Cbar
|
||||
|
||||
`````
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# externals/prettier/markdown/list/parser-regression/issue-17778.md
|
||||
|
||||
## Option 1
|
||||
|
||||
`````json
|
||||
{"printWidth":80}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,4 +1,5 @@
|
||||
1. list item
|
||||
2. list item
|
||||
|
||||
-3. list item 1000000000. ordered list marker can't have more than 9 digits
|
||||
+3. list item
|
||||
+ 1000000000. ordered list marker can't have more than 9 digits
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
1. list item
|
||||
2. list item
|
||||
|
||||
3. list item
|
||||
1000000000. ordered list marker can't have more than 9 digits
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
1. list item
|
||||
2. list item
|
||||
|
||||
3. list item 1000000000. ordered list marker can't have more than 9 digits
|
||||
|
||||
`````
|
||||
+3927
File diff suppressed because it is too large.
Load diff
+81
@@ -0,0 +1,81 @@
|
||||
# externals/prettier/markdown/paragraph/cjk.md
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -9,9 +9,11 @@
|
||||
This ia an english paragraph with a CJK quote “中文“.
|
||||
|
||||
扩展运算符(spread)是三个点(`...`)。
|
||||
|
||||
-::: warning 注意该网站在国外无法访问,故以下演示无效 :::
|
||||
+::: warning 注意
|
||||
+该网站在国外无法访问,故以下演示无效
|
||||
+:::
|
||||
|
||||
IVS 麻󠄁羽󠄀‼️
|
||||
|
||||
⿰あ⿱あ⿲あ⿳あ⿴あ⿵あ⿶あ⿷あ⿸あ⿹あ⿺あ⿻あ
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
這是一段很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長的段落
|
||||
|
||||
這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!
|
||||
|
||||
全 形 空白全 形 空白全 形 空白全 形 空白全 形 空白全 形 空白全 形 空白全 形 空白
|
||||
|
||||
This ia an english paragraph with a CJK quote "中文".
|
||||
|
||||
This ia an english paragraph with a CJK quote “中文“.
|
||||
|
||||
扩展运算符(spread)是三个点(`...`)。
|
||||
|
||||
::: warning 注意
|
||||
该网站在国外无法访问,故以下演示无效
|
||||
:::
|
||||
|
||||
IVS 麻󠄁羽󠄀‼️
|
||||
|
||||
⿰あ⿱あ⿲あ⿳あ⿴あ⿵あ⿶あ⿷あ⿸あ⿹あ⿺あ⿻あ
|
||||
|
||||
1.a 2.b 3.中
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
這是一段很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長很長的段落
|
||||
|
||||
這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!這是一個English混合著中文的一段Paragraph!
|
||||
|
||||
全 形 空白全 形 空白全 形 空白全 形 空白全 形 空白全 形 空白全 形 空白全 形 空白
|
||||
|
||||
This ia an english paragraph with a CJK quote "中文".
|
||||
|
||||
This ia an english paragraph with a CJK quote “中文“.
|
||||
|
||||
扩展运算符(spread)是三个点(`...`)。
|
||||
|
||||
::: warning 注意该网站在国外无法访问,故以下演示无效 :::
|
||||
|
||||
IVS 麻󠄁羽󠄀‼️
|
||||
|
||||
⿰あ⿱あ⿲あ⿳あ⿴あ⿵あ⿶あ⿷あ⿸あ⿹あ⿺あ⿻あ
|
||||
|
||||
1.a 2.b 3.中
|
||||
|
||||
`````
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
# externals/prettier/markdown/thematicBreak/simple.md
|
||||
|
||||
## Option 1
|
||||
|
||||
`````json
|
||||
{"printWidth":80}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,1 +1,1 @@
|
||||
----
|
||||
+***
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
***
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
---
|
||||
|
||||
`````
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,1 +1,1 @@
|
||||
----
|
||||
+***
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````md
|
||||
***
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````md
|
||||
---
|
||||
|
||||
`````
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
# externals/prettier/js/multiparser-markdown/codeblock.js
|
||||
|
||||
## Option 1
|
||||
|
||||
`````json
|
||||
{"printWidth":80}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,9 +1,9 @@
|
||||
md`
|
||||
-~~~js
|
||||
+~~~~js
|
||||
markdown\`
|
||||
~~~js
|
||||
console.log("hi");
|
||||
~~~
|
||||
\`;
|
||||
-~~~
|
||||
+~~~~
|
||||
`;
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````js
|
||||
md`
|
||||
~~~~js
|
||||
markdown\`
|
||||
~~~js
|
||||
console.log("hi");
|
||||
~~~
|
||||
\`;
|
||||
~~~~
|
||||
`;
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````js
|
||||
md`
|
||||
~~~js
|
||||
markdown\`
|
||||
~~~js
|
||||
console.log("hi");
|
||||
~~~
|
||||
\`;
|
||||
~~~
|
||||
`;
|
||||
|
||||
`````
|
||||
|
||||
## Option 2
|
||||
|
||||
`````json
|
||||
{"printWidth":100,"proseWrap":"always"}
|
||||
`````
|
||||
|
||||
### Diff
|
||||
|
||||
`````diff
|
||||
===================================================================
|
||||
--- prettier
|
||||
+++ oxfmt
|
||||
@@ -1,9 +1,9 @@
|
||||
md`
|
||||
-~~~js
|
||||
+~~~~js
|
||||
markdown\`
|
||||
~~~js
|
||||
console.log("hi");
|
||||
~~~
|
||||
\`;
|
||||
-~~~
|
||||
+~~~~
|
||||
`;
|
||||
|
||||
`````
|
||||
|
||||
### Actual (oxfmt)
|
||||
|
||||
`````js
|
||||
md`
|
||||
~~~~js
|
||||
markdown\`
|
||||
~~~js
|
||||
console.log("hi");
|
||||
~~~
|
||||
\`;
|
||||
~~~~
|
||||
`;
|
||||
|
||||
`````
|
||||
|
||||
### Expected (prettier)
|
||||
|
||||
`````js
|
||||
md`
|
||||
~~~js
|
||||
markdown\`
|
||||
~~~js
|
||||
console.log("hi");
|
||||
~~~
|
||||
\`;
|
||||
~~~
|
||||
`;
|
||||
|
||||
`````
|
||||
@@ -36,7 +36,7 @@ async function loadPrettier(): Promise<typeof import("prettier")> {
|
||||
return loadCached("prettier", async () => {
|
||||
const prettier = await import("prettier");
|
||||
|
||||
// NOTE: This is needed for xxx-in-js formatting to work correctly.
|
||||
// NOTE: This is needed for xxx-in-js and xxx-in-md formatting to work correctly.
|
||||
//
|
||||
// Prettier internally extends `options` with hidden fields for embedded-formatters during printing.
|
||||
// However, `__debug.printToDoc()` runs `normalizeFormatOptions()` which strips unknown keys.
|
||||
@@ -49,7 +49,8 @@ async function loadPrettier(): Promise<typeof import("prettier")> {
|
||||
// there should be no side effects on other calls that don't set these fields.
|
||||
// @ts-expect-error: Use internal API
|
||||
const { formatOptionsHiddenDefaults } = prettier.__internal;
|
||||
// For html(angular)-in-js: Prevent attribute level formatting from running.
|
||||
// For every embedded call (html-in-js, xxx-in-md), as Prettier's `textToDoc()` always sets it.
|
||||
// For html-family children, this prevents attribute level formatting from running.
|
||||
// (e.g., CSS in `style="..."` attributes, JS in `onclick="..."` event handlers)
|
||||
// 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),
|
||||
@@ -62,8 +63,6 @@ async function loadPrettier(): Promise<typeof import("prettier")> {
|
||||
// - or flaky traversal of the `Doc` output
|
||||
// to extract the same information, since this hooks into the AST.
|
||||
formatOptionsHiddenDefaults.__onHtmlRoot = null;
|
||||
// For md-in-js: Use `~` instead of `` ` `` for code fences
|
||||
formatOptionsHiddenDefaults.__inJsTemplate = null;
|
||||
|
||||
return prettier;
|
||||
});
|
||||
@@ -85,7 +84,7 @@ export async function formatFile({ code, options }: FormatFileParam): Promise<st
|
||||
const prettier = CACHES.prettier ?? (await loadPrettier());
|
||||
|
||||
// NOTE: Plugins order matters here!
|
||||
// This plugin add `svelte` parser to support for `.svelte` files, and is also needed for `svelte-in-md` to work
|
||||
// This plugin add `svelte` parser to support for `.svelte` files, and is also needed for `svelte-in-mdx` to work
|
||||
if ("_useSveltePlugin" in options) await setupSveltePlugin(options);
|
||||
// Enable Tailwind CSS plugin, this plugin transforms `parsers` already installed by prior plugins
|
||||
if ("_useTailwindPlugin" in options) await setupTailwindPlugin(options);
|
||||
@@ -173,26 +172,25 @@ export async function formatEmbeddedDoc({
|
||||
}: FormatEmbeddedDocParam): Promise<string> {
|
||||
const prettier = CACHES.prettier ?? (await loadPrettier());
|
||||
|
||||
// NOTE: Plugins order matters here, same as `formatFile()`
|
||||
// Add `svelte` parser for ` ```svelte ` code blocks in Markdown
|
||||
if ("_useSveltePlugin" in options) await setupSveltePlugin(options);
|
||||
// Enable Tailwind CSS plugin for embedded code (e.g., html`...` in JS) if needed
|
||||
if ("_useTailwindPlugin" in options) await setupTailwindPlugin(options);
|
||||
|
||||
const metadata: Record<string, unknown> = {};
|
||||
|
||||
// html(angular)-in-js specific options: see the comment in `loadPrettier()` for rationale
|
||||
// Every caller is an embedding parent (JS, Markdown), as Prettier's `textToDoc()` sets it:
|
||||
// see the comment in `loadPrettier()` for rationale. Any truthy value works
|
||||
options.parentParser = "OXFMT";
|
||||
|
||||
// html(angular)-in-js specific options
|
||||
if (options.parser === "html" || options.parser === "angular") {
|
||||
// Any truthy value works
|
||||
options.parentParser = "OXFMT";
|
||||
// https://github.com/prettier/prettier/blob/90983f40dce5e20beea4e5618b5e0426a6a7f4f0/src/language-js/embed/html.js#L42-L44
|
||||
options.__onHtmlRoot = (root: { children?: unknown[] }) =>
|
||||
(metadata.htmlHasMultipleRootElements = (root.children?.length ?? 0) > 1);
|
||||
}
|
||||
|
||||
// md-in-js specific options: see the comment in `loadPrettier()` for rationale
|
||||
if (options.parser === "markdown") {
|
||||
// https://github.com/prettier/prettier/blob/90983f40dce5e20beea4e5618b5e0426a6a7f4f0/src/language-js/embed/markdown.js#L21
|
||||
options.__inJsTemplate = true;
|
||||
}
|
||||
|
||||
// NOTE: This will throw if:
|
||||
// - Specified parser is not available
|
||||
// - Or, code has syntax errors
|
||||
|
||||
@@ -13,8 +13,8 @@ export const textToDoc: Parser<Doc>["parse"] = async (embeddedSourceText, textTo
|
||||
// For (j|t)s-in-xxx, default `parser` is either `babel`(JS), `babel-ts` or `typescript`.
|
||||
// We need to infer the parse grammar (`source_ext`) for `oxc_formatter`.
|
||||
// - JS: always enable JSX for js-in-xxx, it's syntactically valid
|
||||
// - TS: `typescript` (ts-in-vue|markdown|mdx) or `babel-ts` (ts-in-vue(script generic="..."))
|
||||
// - In case of ts-in-md, `filepath` is overridden as `dummy.ts(x)` to distinguish TSX or TS
|
||||
// - TS: `typescript` (ts-in-vue|mdx) or `babel-ts` (ts-in-vue(script generic="..."))
|
||||
// - In case of ts-in-mdx, `filepath` is overridden as `dummy.ts(x)` to distinguish TSX or TS
|
||||
// - For tsx-in-vue (`<script lang="tsx">`), there is no signal from Prettier itself:
|
||||
// - both `lang="ts"` and `lang="tsx"` resolve to the `typescript` parser and `filepath` is the parent `.vue` file
|
||||
// - Nor from parsing: a JSX-free tsx block parses fine as plain ts
|
||||
|
||||
@@ -54,7 +54,7 @@ pub fn run(
|
||||
format_embedded_doc_cb: JsFormatEmbeddedDocCb,
|
||||
sort_tailwind_classes_cb: JsSortTailwindClassesCb,
|
||||
) -> Option<String> {
|
||||
// Embedded text belongs to the host file (`.vue`, `.md`, ...),
|
||||
// Embedded text belongs to the host file (`.vue`, `.mdx`, ...),
|
||||
// so the `SourceType` carries no file extension of its own.
|
||||
// `source_ext` selects the parse grammar only,
|
||||
// and extension-keyed formatter rules (e.g. the `.mts`/`.cts` trailing comma reservation) must not fire from it.
|
||||
@@ -132,13 +132,13 @@ fn run_full(
|
||||
);
|
||||
let _cleanup = external_services.cleanup_guard();
|
||||
|
||||
let EmbeddedCallbackResolved { format_options, config, core, parent_filepath } =
|
||||
let EmbeddedCallbackResolved { format_options, config, validated, parent_filepath } =
|
||||
resolve_for_embedded_js(config, parent_filepath)
|
||||
.expect("`_oxfmtPluginOptionsJson` should contain valid config");
|
||||
|
||||
// Per-language options (and the Prettier options JSON with the Tailwind payload)
|
||||
// are mapped lazily at dispatch time; `core` was validated during resolution.
|
||||
let dispatch_config = ResolvedDispatchConfig::for_root(&config, core, &parent_filepath);
|
||||
// are mapped lazily at dispatch time; `validated` comes from resolution.
|
||||
let dispatch_config = ResolvedDispatchConfig::for_root(&config, validated, &parent_filepath);
|
||||
|
||||
let services = embed::services::for_root(&external_services, &dispatch_config);
|
||||
|
||||
|
||||
@@ -25,8 +25,6 @@ use tracing::instrument;
|
||||
use oxc_config::{ConfigDiscovery, DiscoveredConfigFile, is_js_config_path, vp_version};
|
||||
#[cfg(feature = "napi")]
|
||||
use oxc_formatter::JsFormatOptions;
|
||||
#[cfg(feature = "napi")]
|
||||
use oxc_formatter_core::CoreFormatOptions;
|
||||
|
||||
use self::{
|
||||
editorconfig::{apply_editorconfig, resolve_editorconfig_overrides, root_properties},
|
||||
@@ -167,14 +165,13 @@ pub fn resolve_for_api(
|
||||
#[cfg(feature = "napi")]
|
||||
#[derive(Debug)]
|
||||
pub struct EmbeddedCallbackResolved {
|
||||
/// Other xxx-in-js options are may be or may not be used, so derived lazily with `config` and `core`.
|
||||
/// Other xxx-in-js options are may be or may not be used, so derived lazily with `config` and `validated`.
|
||||
/// `JsFormatOptions` is always needed, so hold it here.
|
||||
pub format_options: Box<JsFormatOptions>,
|
||||
/// Retained so nested embedded callbacks can derive Prettier options on demand.
|
||||
pub config: Arc<FormatConfig>,
|
||||
/// The validated core bundle, carried from resolution so dispatch-config
|
||||
/// construction never re-derives (or re-fails) it.
|
||||
pub core: CoreFormatOptions,
|
||||
/// For the root's dispatch config, see [`crate::core::embed::dispatcher::ResolvedDispatchConfig::for_root`].
|
||||
pub validated: ValidatedOptions,
|
||||
pub parent_filepath: PathBuf,
|
||||
}
|
||||
|
||||
@@ -194,9 +191,15 @@ pub fn resolve_for_embedded_js(
|
||||
config: FormatConfig,
|
||||
parent_filepath: PathBuf,
|
||||
) -> Result<EmbeddedCallbackResolved, String> {
|
||||
let ValidatedOptions { core, sort_imports } = validate(&config)?;
|
||||
let format_options = Box::new(to_oxc_formatter(&config, core, sort_imports));
|
||||
Ok(EmbeddedCallbackResolved { format_options, config: Arc::new(config), core, parent_filepath })
|
||||
let validated = validate(&config)?;
|
||||
let format_options =
|
||||
Box::new(to_oxc_formatter(&config, validated.core, validated.sort_imports.clone()));
|
||||
Ok(EmbeddedCallbackResolved {
|
||||
format_options,
|
||||
config: Arc::new(config),
|
||||
validated,
|
||||
parent_filepath,
|
||||
})
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
@@ -8,7 +8,9 @@ use std::sync::{Arc, OnceLock};
|
||||
|
||||
use tracing::{debug, debug_span};
|
||||
|
||||
use oxc_formatter::CssInJsTemplate;
|
||||
use oxc_formatter::{
|
||||
CssInJsTemplate, JsEmbeddedIn, JsFormatOptions, MarkdownInJsTemplate, SortImportsOptions,
|
||||
};
|
||||
use oxc_formatter_core::{
|
||||
CoreFormatOptions, DispatchRequest, DispatchResponse, EmbeddedIr, FormatDispatcher,
|
||||
FormatSession,
|
||||
@@ -17,12 +19,14 @@ use oxc_formatter_core::{FormatOptions, PrinterOptions};
|
||||
use oxc_formatter_css::{CssFormatOptions, CssVariant};
|
||||
use oxc_formatter_graphql::GraphqlFormatOptions;
|
||||
use oxc_formatter_json::{JsonFormatOptions, JsonVariant};
|
||||
use oxc_formatter_markdown::{MarkdownFormatOptions, XxxInMarkdownCodeBlock};
|
||||
use oxc_formatter_yaml::YamlFormatOptions;
|
||||
use oxc_span::SourceType;
|
||||
|
||||
use crate::core::{
|
||||
options::{
|
||||
to_oxc_formatter_css, to_oxc_formatter_graphql, to_oxc_formatter_json,
|
||||
to_oxc_formatter_yaml,
|
||||
ValidatedOptions, to_oxc_formatter, to_oxc_formatter_css, to_oxc_formatter_graphql,
|
||||
to_oxc_formatter_json, to_oxc_formatter_markdown, to_oxc_formatter_yaml,
|
||||
},
|
||||
oxfmtrc::FormatConfig,
|
||||
};
|
||||
@@ -30,24 +34,30 @@ use crate::core::{
|
||||
/// The native half of the routing table:
|
||||
/// a request/fence language routed to [`Route::Native`] parses to its Rust formatter branch here.
|
||||
pub enum NativeLanguage {
|
||||
Js(SourceType),
|
||||
Graphql,
|
||||
/// The fence-derived variant;
|
||||
/// the css-in-js typed context overrides it to Scss + placeholders at dispatch time (see the css branch).
|
||||
Css(CssVariant),
|
||||
Yaml,
|
||||
Json(JsonVariant),
|
||||
Markdown,
|
||||
}
|
||||
|
||||
/// Languages Prettier still formats for us (no Rust formatter yet).
|
||||
///
|
||||
/// [`PrettierDocFallback`] receives this instead of a raw string,
|
||||
/// so the fallback can never be handed a language the table did not route to it.
|
||||
/// The set shrinks as Rust ports land (markdown first), and the type disappears with the last port.
|
||||
/// The set shrinks as Rust ports land, and the type disappears with the last port.
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum PrettierLanguage {
|
||||
Html,
|
||||
Angular,
|
||||
Markdown,
|
||||
Vue,
|
||||
/// Formatted only when `prettier-plugin-svelte` is enabled (`svelte` config key).
|
||||
Svelte,
|
||||
Handlebars,
|
||||
Mdx,
|
||||
}
|
||||
|
||||
#[cfg(feature = "napi")]
|
||||
@@ -62,7 +72,10 @@ impl PrettierLanguage {
|
||||
match self {
|
||||
Self::Html => "html",
|
||||
Self::Angular => "angular",
|
||||
Self::Markdown => "markdown",
|
||||
Self::Vue => "vue",
|
||||
Self::Svelte => "svelte",
|
||||
Self::Handlebars => "glimmer",
|
||||
Self::Mdx => "mdx",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,11 +100,15 @@ pub enum Route {
|
||||
/// THE routing table: "which formatter serves this language?" answered in one place.
|
||||
/// [`build_dispatcher`] and the napi string channel's fence routing both consult it,
|
||||
/// so their notions of who formats what can never drift,
|
||||
/// and aliases (`"gql"` / `"yml"` / `"md"`) are resolved here and nowhere else.
|
||||
/// and aliases are resolved here and nowhere else.
|
||||
///
|
||||
/// A code fence's name arrives as written: the aliases are Shiki's ids and aliases,
|
||||
/// what Markdown tooling (VitePress, Astro, ...) highlights with.
|
||||
/// <https://shiki.style/languages>
|
||||
pub fn route(language: &str) -> Route {
|
||||
match language {
|
||||
"graphql" | "gql" => Route::Native(NativeLanguage::Graphql),
|
||||
"css" => Route::Native(NativeLanguage::Css(CssVariant::Css)),
|
||||
"css" | "postcss" => Route::Native(NativeLanguage::Css(CssVariant::Css)),
|
||||
"scss" => Route::Native(NativeLanguage::Css(CssVariant::Scss)),
|
||||
"less" => Route::Native(NativeLanguage::Css(CssVariant::Less)),
|
||||
"yaml" | "yml" => Route::Native(NativeLanguage::Yaml),
|
||||
@@ -99,9 +116,24 @@ pub fn route(language: &str) -> Route {
|
||||
"jsonc" => Route::Native(NativeLanguage::Json(JsonVariant::Jsonc)),
|
||||
"json5" => Route::Native(NativeLanguage::Json(JsonVariant::Json5)),
|
||||
"html" => Route::Prettier(PrettierLanguage::Html),
|
||||
"angular" => Route::Prettier(PrettierLanguage::Angular),
|
||||
"markdown" | "md" => Route::Prettier(PrettierLanguage::Markdown),
|
||||
_ => Route::Unsupported,
|
||||
"angular" | "angular-html" => Route::Prettier(PrettierLanguage::Angular),
|
||||
"vue" => Route::Prettier(PrettierLanguage::Vue),
|
||||
"svelte" => Route::Prettier(PrettierLanguage::Svelte),
|
||||
"handlebars" | "hbs" => Route::Prettier(PrettierLanguage::Handlebars),
|
||||
"mdx" => Route::Prettier(PrettierLanguage::Mdx),
|
||||
"markdown" | "md" => Route::Native(NativeLanguage::Markdown),
|
||||
// JS / TS by file extension, which carries the module kind and JSX.
|
||||
// A component's inline template is found from its decorator, not from the fence (`angular-ts`).
|
||||
_ => {
|
||||
let extension = match language {
|
||||
"javascript" => "js",
|
||||
"typescript" | "angular-ts" => "ts",
|
||||
extension => extension,
|
||||
};
|
||||
SourceType::from_extension(extension).map_or(Route::Unsupported, |source_type| {
|
||||
Route::Native(NativeLanguage::Js(source_type))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,12 +151,17 @@ pub struct ResolvedDispatchConfig {
|
||||
/// Core options validated once by the config-resolution gate (`options::validate`).
|
||||
/// Holding them pre-validated is what lets the per-language mappers be infallible.
|
||||
core: CoreFormatOptions,
|
||||
/// The gate's other artifact, for JS children: a Markdown code block is a whole program,
|
||||
/// sorted like the host's own imports (and a Vue `<script>`'s).
|
||||
sort_imports: Option<SortImportsOptions>,
|
||||
js: OnceLock<JsFormatOptions>,
|
||||
graphql: OnceLock<GraphqlFormatOptions>,
|
||||
/// One cell per [`CssVariant`]: JSDoc fences dispatch css/scss/less as-is, while css-in-js always uses Scss.
|
||||
css: [OnceLock<CssFormatOptions>; 3],
|
||||
yaml: OnceLock<YamlFormatOptions>,
|
||||
/// One cell per fence-reachable [`JsonVariant`] (json / jsonc / json5; `JsonStringify` is `package.json`-only).
|
||||
json: [OnceLock<JsonFormatOptions>; 3],
|
||||
markdown: OnceLock<MarkdownFormatOptions>,
|
||||
/// The options handed to Prettier; see [`PrettierOptions`].
|
||||
#[cfg(feature = "napi")]
|
||||
prettier: PrettierOptions,
|
||||
@@ -147,17 +184,19 @@ struct PrettierOptions {
|
||||
}
|
||||
|
||||
impl ResolvedDispatchConfig {
|
||||
/// `core` is the pre-validated bundle carried from the config-resolution gate (`options::validate`);
|
||||
/// it never gets re-derived here.
|
||||
/// Private so [`Self::for_root`] stays the only construction recipe.
|
||||
fn new(config: Arc<FormatConfig>, core: CoreFormatOptions) -> Self {
|
||||
fn new(config: Arc<FormatConfig>, validated: ValidatedOptions) -> Self {
|
||||
let ValidatedOptions { core, sort_imports } = validated;
|
||||
Self {
|
||||
config,
|
||||
core,
|
||||
sort_imports,
|
||||
js: OnceLock::new(),
|
||||
graphql: OnceLock::new(),
|
||||
css: [OnceLock::new(), OnceLock::new(), OnceLock::new()],
|
||||
yaml: OnceLock::new(),
|
||||
json: [OnceLock::new(), OnceLock::new(), OnceLock::new()],
|
||||
markdown: OnceLock::new(),
|
||||
#[cfg(feature = "napi")]
|
||||
prettier: PrettierOptions::default(),
|
||||
}
|
||||
@@ -166,12 +205,14 @@ impl ResolvedDispatchConfig {
|
||||
/// The one construction recipe for a root formatter run at `path`:
|
||||
/// [`Self::new`] plus the napi-only path recording
|
||||
/// (the pure build has no JS-side consumers, so `path` goes unused there).
|
||||
/// `validated` is the config-resolution gate's artifacts (`options::validate`),
|
||||
/// carried from resolution so they never get re-derived (or re-fail) here.
|
||||
pub fn for_root(
|
||||
config: &Arc<FormatConfig>,
|
||||
core: CoreFormatOptions,
|
||||
validated: ValidatedOptions,
|
||||
path: &std::path::Path,
|
||||
) -> Arc<Self> {
|
||||
let dispatch_config = Self::new(Arc::clone(config), core);
|
||||
let dispatch_config = Self::new(Arc::clone(config), validated);
|
||||
#[cfg(feature = "napi")]
|
||||
let dispatch_config = dispatch_config.with_path(path.to_path_buf());
|
||||
#[cfg(not(feature = "napi"))]
|
||||
@@ -196,6 +237,12 @@ impl ResolvedDispatchConfig {
|
||||
self.config.is_embedded_formatting_enabled()
|
||||
}
|
||||
|
||||
pub fn js_options(&self) -> JsFormatOptions {
|
||||
self.js
|
||||
.get_or_init(|| to_oxc_formatter(&self.config, self.core, self.sort_imports.clone()))
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn graphql_options(&self) -> GraphqlFormatOptions {
|
||||
*self.graphql.get_or_init(|| to_oxc_formatter_graphql(&self.config, self.core))
|
||||
}
|
||||
@@ -227,6 +274,10 @@ impl ResolvedDispatchConfig {
|
||||
*cell.get_or_init(|| to_oxc_formatter_json(&self.config, self.core, variant))
|
||||
}
|
||||
|
||||
pub fn markdown_options(&self) -> MarkdownFormatOptions {
|
||||
*self.markdown.get_or_init(|| to_oxc_formatter_markdown(&self.config, self.core))
|
||||
}
|
||||
|
||||
/// Printer options from the shared resolved core bundle;
|
||||
/// the fence adapter ([`super::jsdoc_fence`]) derives its per-fence options from these
|
||||
/// (width overridden to the fence's effective width).
|
||||
@@ -252,6 +303,23 @@ impl ResolvedDispatchConfig {
|
||||
self.config.is_tailwind_enabled()
|
||||
}
|
||||
|
||||
/// [`Self::prettier_options`] for one `language`: its parser, plus the payload of the plugin it needs.
|
||||
/// `None` when that plugin is not enabled (svelte without the `svelte` config key): the part stays as-is.
|
||||
pub fn prettier_options_for(&self, language: PrettierLanguage) -> Option<serde_json::Value> {
|
||||
let needs_svelte = matches!(language, PrettierLanguage::Svelte);
|
||||
|
||||
if needs_svelte && !self.config.is_svelte_enabled() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut options = self.prettier_options().clone();
|
||||
crate::core::options::inject_parser(&mut options, language.parser());
|
||||
if needs_svelte {
|
||||
crate::core::options::inject_svelte_plugin_payload(&mut options, &self.config);
|
||||
}
|
||||
Some(options)
|
||||
}
|
||||
|
||||
/// The options JSON handed to Prettier
|
||||
/// (see [`crate::core::options::build_prettier_options`]).
|
||||
pub fn prettier_options(&self) -> &serde_json::Value {
|
||||
@@ -288,6 +356,20 @@ pub fn build_dispatcher(
|
||||
Arc::new(move |session: &FormatSession<'_>, request: DispatchRequest<'_>| {
|
||||
let text = request.text;
|
||||
match route(request.language) {
|
||||
Route::Native(NativeLanguage::Js(source_type)) => {
|
||||
let embedded_in = request
|
||||
.parent_context_as::<XxxInMarkdownCodeBlock>()
|
||||
.map(|_| JsEmbeddedIn::MarkdownCodeBlock);
|
||||
Ok(format_native("js", || {
|
||||
oxc_formatter::format_to_ir(
|
||||
session,
|
||||
text,
|
||||
source_type,
|
||||
dispatch_config.js_options(),
|
||||
embedded_in,
|
||||
)
|
||||
}))
|
||||
}
|
||||
Route::Native(NativeLanguage::Graphql) => Ok(format_native("graphql", || {
|
||||
oxc_formatter_graphql::format_to_ir(
|
||||
session,
|
||||
@@ -298,14 +380,12 @@ pub fn build_dispatcher(
|
||||
Route::Native(NativeLanguage::Css(variant)) => {
|
||||
// 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.
|
||||
let (variant, template_placeholders) = if request
|
||||
.parent_context
|
||||
.is_some_and(|c| c.downcast_ref::<CssInJsTemplate>().is_some())
|
||||
{
|
||||
(CssVariant::Scss, true)
|
||||
} else {
|
||||
(variant, false)
|
||||
};
|
||||
let (variant, template_placeholders) =
|
||||
if request.parent_context_as::<CssInJsTemplate>().is_some() {
|
||||
(CssVariant::Scss, true)
|
||||
} else {
|
||||
(variant, false)
|
||||
};
|
||||
Ok(format_native("css", || {
|
||||
oxc_formatter_css::format_to_ir(
|
||||
session,
|
||||
@@ -325,6 +405,21 @@ pub fn build_dispatcher(
|
||||
dispatch_config.json_options(variant),
|
||||
)
|
||||
})),
|
||||
Route::Native(NativeLanguage::Markdown) => {
|
||||
// `~` fences in a JS template, and in Markdown nested in one (md-in-md-in-js)
|
||||
let in_js_template = request.parent_context_as::<MarkdownInJsTemplate>().is_some()
|
||||
|| request
|
||||
.parent_context_as::<XxxInMarkdownCodeBlock>()
|
||||
.is_some_and(|c| c.in_js_template);
|
||||
Ok(format_native("markdown", || {
|
||||
oxc_formatter_markdown::format_to_ir(
|
||||
session,
|
||||
text,
|
||||
dispatch_config.markdown_options(),
|
||||
in_js_template,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
// Prettier-served languages: Doc→IR fallback when available (napi),
|
||||
// deliberate skip otherwise (pure build).
|
||||
@@ -375,16 +470,16 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::{ResolvedDispatchConfig, build_dispatcher};
|
||||
use crate::core::oxfmtrc::FormatConfig;
|
||||
use crate::core::{options::ValidatedOptions, oxfmtrc::FormatConfig};
|
||||
|
||||
fn dispatch_config() -> Arc<ResolvedDispatchConfig> {
|
||||
Arc::new(ResolvedDispatchConfig::new(
|
||||
Arc::new(FormatConfig::default()),
|
||||
CoreFormatOptions::default(),
|
||||
ValidatedOptions { core: CoreFormatOptions::default(), sort_imports: None },
|
||||
))
|
||||
}
|
||||
|
||||
/// Every fence language the routing table claims as native must format
|
||||
/// Every language the routing table claims as native must format
|
||||
/// WITHOUT a fallback installed
|
||||
/// (an accidentally dropped [`super::route`] entry would fall through to `PreserveOriginal` and fail here).
|
||||
#[test]
|
||||
@@ -399,15 +494,39 @@ mod tests {
|
||||
},
|
||||
);
|
||||
|
||||
for language in
|
||||
["graphql", "gql", "css", "scss", "less", "yaml", "yml", "json", "jsonc", "json5"]
|
||||
{
|
||||
for language in [
|
||||
"js",
|
||||
"javascript",
|
||||
"jsx",
|
||||
"mjs",
|
||||
"cjs",
|
||||
"ts",
|
||||
"typescript",
|
||||
"angular-ts",
|
||||
"mts",
|
||||
"cts",
|
||||
"tsx",
|
||||
"graphql",
|
||||
"gql",
|
||||
"css",
|
||||
"postcss",
|
||||
"scss",
|
||||
"less",
|
||||
"yaml",
|
||||
"yml",
|
||||
"json",
|
||||
"jsonc",
|
||||
"json5",
|
||||
"markdown",
|
||||
"md",
|
||||
] {
|
||||
let text = match language {
|
||||
"graphql" | "gql" => "{ a }",
|
||||
"css" | "scss" | "less" => "a { color: red }",
|
||||
"css" | "postcss" | "scss" | "less" => "a { color: red }",
|
||||
"yaml" | "yml" => "a: 1",
|
||||
"json" | "jsonc" | "json5" => "{ \"a\": 1 }",
|
||||
other => panic!("no sample input for native language '{other}'"),
|
||||
"markdown" | "md" => "# a",
|
||||
_ => "a = 1",
|
||||
};
|
||||
let response = session.dispatch(DispatchRequest {
|
||||
language,
|
||||
|
||||
@@ -15,28 +15,27 @@ use oxc_formatter::HtmlEmbedMeta;
|
||||
use oxc_formatter_core::{DispatchPayload, DispatchResponse, FormatSession};
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
embed::{
|
||||
FormatEmbeddedDocWithConfigCallback,
|
||||
dispatcher::{PrettierDocFallback, PrettierLanguage, ResolvedDispatchConfig},
|
||||
},
|
||||
options::inject_parser,
|
||||
core::embed::{
|
||||
FormatEmbeddedDocWithConfigCallback,
|
||||
dispatcher::{PrettierDocFallback, PrettierLanguage, ResolvedDispatchConfig},
|
||||
},
|
||||
prettier_compat::from_prettier_doc,
|
||||
};
|
||||
|
||||
/// Build the Prettier Doc→IR fallback installed on the dispatcher's `Route::Prettier` arm.
|
||||
/// The routing table already narrowed the language, so there is nothing left to reject here.
|
||||
/// The routing table already narrowed the language; only a language whose plugin is off is rejected (as-is).
|
||||
pub fn build_prettier_fallback(
|
||||
dispatch_config: Arc<ResolvedDispatchConfig>,
|
||||
format_embedded_doc: FormatEmbeddedDocWithConfigCallback,
|
||||
) -> PrettierDocFallback {
|
||||
Arc::new(move |session: &FormatSession<'_>, language: PrettierLanguage, text: &str| {
|
||||
let parser_name = language.parser();
|
||||
let Some(options) = dispatch_config.prettier_options_for(language) else {
|
||||
debug!("The plugin for parser '{parser_name}' is not enabled, part stays as-is");
|
||||
return Ok(DispatchResponse::PreserveOriginal);
|
||||
};
|
||||
debug_span!("oxfmt::external::format_embedded_doc", parser = parser_name)
|
||||
.in_scope(|| {
|
||||
let mut options = dispatch_config.prettier_options().clone();
|
||||
inject_parser(&mut options, parser_name);
|
||||
let doc_json_str = (format_embedded_doc)(options, text).map_err(|err| {
|
||||
format!("Failed to get Doc for embedded code (parser '{parser_name}'): {err}")
|
||||
})?;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//!
|
||||
//! Fence routing follows ONE rule, the shared routing table ([`dispatcher::route`]):
|
||||
//! a `Route::Native` language formats through the dispatcher via [`super::jsdoc_fence::format_native_fence`] (the build-independent adapter);
|
||||
//! the `Route::Prettier` set (md/html/angular) stays on the Prettier string path
|
||||
//! 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).
|
||||
//!
|
||||
@@ -24,7 +24,7 @@ use crate::core::{
|
||||
dispatcher::{self, Route},
|
||||
jsdoc_fence,
|
||||
},
|
||||
options::{inject_parser, inject_print_width},
|
||||
options::inject_print_width,
|
||||
};
|
||||
|
||||
/// Build the napi build's string embedder installed on the session.
|
||||
@@ -43,7 +43,7 @@ pub fn build_string_embedder(
|
||||
// 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 parser_name = match dispatcher::route(language) {
|
||||
let prettier_language = match dispatcher::route(language) {
|
||||
// Native branch (JSDoc fenced code blocks): through the dispatcher, never Prettier.
|
||||
Route::Native(_) => {
|
||||
return jsdoc_fence::format_native_fence(
|
||||
@@ -55,16 +55,16 @@ pub fn build_string_embedder(
|
||||
sort_tailwind.as_ref(),
|
||||
);
|
||||
}
|
||||
Route::Prettier(prettier_language) => prettier_language.parser(),
|
||||
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(|| {
|
||||
// `clone()` is unavoidable here,
|
||||
// because there may be multiple embedded sections in one JS/TS file.
|
||||
let mut options = dispatch_config.prettier_options().clone();
|
||||
inject_parser(&mut options, parser_name);
|
||||
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 the native branch prints at
|
||||
inject_print_width(&mut options, print_width);
|
||||
(format_embedded)(options, code)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Root `SessionServices` assembly: [`for_root`] builds the build's default service set,
|
||||
//! installed by every session-taking root (JS / CSS / Vue-Svelte script; a future Markdown host too).
|
||||
//! installed by every session-taking root (JS / CSS / Markdown / Vue-Svelte script).
|
||||
//! One name, one definition per build; the napi one additionally takes the `ExternalServices` transport.
|
||||
//!
|
||||
//! "Which languages may dispatch at all from this host" is the host crate's own gate
|
||||
@@ -64,12 +64,12 @@ pub fn for_root(dispatch_config: &Arc<ResolvedDispatchConfig>) -> SessionService
|
||||
/// 1. Standalone JS/TS (`className` / functions / custom attributes):
|
||||
/// `oxc_formatter` collects classes into `FormatElement::TailwindClass`.
|
||||
/// When the entry document is finalized, the printer sorts them in one host batch via the session's `tailwind_sorter`.
|
||||
/// 2. Standalone CSS / SCSS / LESS (`@apply` at top level):
|
||||
/// the CSS root's session carries the same sorter (`CssFormatOptions::sort_tailwindcss` only switches collection);
|
||||
/// 2. Standalone CSS / SCSS / LESS (`@apply` at top level), and Markdown:
|
||||
/// the CSS / Markdown root's session carries the same sorter (`CssFormatOptions::sort_tailwindcss` only switches collection);
|
||||
/// classes sort once at finalize, no embedded boundary involved.
|
||||
/// 3. Embedded CSS (css-in-js + Angular `@Component({ styles })`):
|
||||
/// `oxc_formatter_css::format_to_ir()` adds pre-sort `@apply` classes into the session's class scope, shared with the JS parent,
|
||||
/// so they ride the SAME parent batch as path 1.
|
||||
/// 3. Embedded CSS (css-in-js + Angular `@Component({ styles })`, Markdown css code blocks):
|
||||
/// `oxc_formatter_css::format_to_ir()` adds pre-sort `@apply` classes into the session's class scope, shared with the JS / Markdown parent,
|
||||
/// so they ride the SAME parent batch as its root.
|
||||
/// The standalone CSS sort closure is NOT invoked here.
|
||||
/// 4. JSDoc fenced CSS (Markdown code fence in JSDoc descriptions):
|
||||
/// Goes through the string embedder's fence adapter, which returns a formatted string (not parent-integrated IR),
|
||||
|
||||
@@ -2,13 +2,14 @@ use std::{path::Path, sync::Arc};
|
||||
|
||||
use tracing::instrument;
|
||||
|
||||
use oxc_allocator::AllocatorPool;
|
||||
use oxc_allocator::{Allocator, AllocatorPool};
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_formatter::JsFormatOptions;
|
||||
use oxc_formatter_core::{CoreFormatOptions, FormatSession, InputKind, SessionServices};
|
||||
use oxc_formatter_core::{FormatSession, InputKind};
|
||||
use oxc_formatter_css::CssFormatOptions;
|
||||
use oxc_formatter_graphql::GraphqlFormatOptions;
|
||||
use oxc_formatter_json::{JsonFormatOptions, JsonVariant};
|
||||
use oxc_formatter_markdown::MarkdownFormatOptions;
|
||||
use oxc_formatter_yaml::YamlFormatOptions;
|
||||
use oxc_span::SourceType;
|
||||
use oxc_toml::Options as TomlFormatterOptions;
|
||||
@@ -22,7 +23,8 @@ use super::{
|
||||
embed::dispatcher::ResolvedDispatchConfig,
|
||||
options::{
|
||||
ValidatedOptions, to_oxc_formatter, to_oxc_formatter_css, to_oxc_formatter_graphql,
|
||||
to_oxc_formatter_json, to_oxc_formatter_yaml, to_oxc_toml, to_sort_package_json,
|
||||
to_oxc_formatter_json, to_oxc_formatter_markdown, to_oxc_formatter_yaml, to_oxc_toml,
|
||||
to_sort_package_json,
|
||||
},
|
||||
oxfmtrc::FormatConfig,
|
||||
support::FileKind,
|
||||
@@ -38,16 +40,14 @@ use super::{
|
||||
#[derive(Debug)]
|
||||
pub enum FormatStrategy {
|
||||
/// For JS/TS files formatted by oxc_formatter.
|
||||
/// `config` + `core` build the dispatch config for the root's session
|
||||
/// `config` + `validated` build the dispatch config for the root's session
|
||||
/// (native xxx-in-js in every build, plus the napi embedded callbacks / Tailwind).
|
||||
OxcFormatter {
|
||||
path: Arc<Path>,
|
||||
source_type: SourceType,
|
||||
format_options: Box<JsFormatOptions>,
|
||||
config: Arc<FormatConfig>,
|
||||
/// The validated core bundle, carried from resolution so dispatch-config
|
||||
/// construction never re-derives (or re-fails) it.
|
||||
core: CoreFormatOptions,
|
||||
validated: ValidatedOptions,
|
||||
insert_final_newline: bool,
|
||||
},
|
||||
/// For JSON (and JSON-like) files formatted by `oxc_formatter_json`.
|
||||
@@ -71,15 +71,13 @@ pub enum FormatStrategy {
|
||||
insert_final_newline: bool,
|
||||
},
|
||||
/// For CSS/SCSS/Less files formatted by `oxc_formatter_css`.
|
||||
/// `config` + `core` build the dispatch config for the root's session
|
||||
/// `config` + `validated` build the dispatch config for the root's session
|
||||
/// (front matter YAML, plus the napi Tailwind sorter options).
|
||||
OxcFormatterCss {
|
||||
path: Arc<Path>,
|
||||
format_options: Box<CssFormatOptions>,
|
||||
config: Arc<FormatConfig>,
|
||||
/// The validated core bundle, carried from resolution so dispatch-config
|
||||
/// construction never re-derives (or re-fails) it.
|
||||
core: CoreFormatOptions,
|
||||
validated: ValidatedOptions,
|
||||
insert_final_newline: bool,
|
||||
},
|
||||
/// For YAML files formatted by `oxc_formatter_yaml`.
|
||||
@@ -97,6 +95,16 @@ pub enum FormatStrategy {
|
||||
json_format_options: Box<JsonFormatOptions>,
|
||||
insert_final_newline: bool,
|
||||
},
|
||||
/// For Markdown files formatted by `oxc_formatter_markdown`.
|
||||
/// `config` + `validated` build the dispatch config for the root's session
|
||||
/// (front matter YAML and fenced code blocks, plus the napi Tailwind sorter options).
|
||||
OxcFormatterMarkdown {
|
||||
path: Arc<Path>,
|
||||
format_options: Box<MarkdownFormatOptions>,
|
||||
config: Arc<FormatConfig>,
|
||||
validated: ValidatedOptions,
|
||||
insert_final_newline: bool,
|
||||
},
|
||||
/// For TOML files.
|
||||
OxfmtToml { path: Arc<Path>, toml_options: TomlFormatterOptions, insert_final_newline: bool },
|
||||
/// For non-JS files formatted by delegating to Prettier (Tier 3/4).
|
||||
@@ -129,6 +137,7 @@ impl FormatStrategy {
|
||||
| Self::OxcFormatterCss { path, .. }
|
||||
| Self::OxcFormatterYaml { path, .. }
|
||||
| Self::OxcFormatterYamlRc { path, .. }
|
||||
| Self::OxcFormatterMarkdown { path, .. }
|
||||
| Self::OxfmtToml { path, .. } => path,
|
||||
#[cfg(feature = "napi")]
|
||||
Self::Prettier { path, .. } => path,
|
||||
@@ -151,7 +160,7 @@ impl FormatStrategy {
|
||||
) -> Self {
|
||||
let insert_final_newline = config.insert_final_newline.unwrap_or(true);
|
||||
// Borrowed so the resolver's fast path can hand out its cached artifacts per file;
|
||||
// `sort_imports` (the only non-`Copy` member) is cloned in the one arm that needs it.
|
||||
// cloned (for `sort_imports`, the only non-`Copy` member) only by the arms whose root dispatches.
|
||||
let core = validated.core;
|
||||
|
||||
match kind {
|
||||
@@ -164,7 +173,7 @@ impl FormatStrategy {
|
||||
validated.sort_imports.clone(),
|
||||
)),
|
||||
config,
|
||||
core,
|
||||
validated: validated.clone(),
|
||||
insert_final_newline,
|
||||
},
|
||||
FileKind::OxcFormatterJson { path, variant } => Self::OxcFormatterJson {
|
||||
@@ -191,7 +200,7 @@ impl FormatStrategy {
|
||||
path,
|
||||
format_options: Box::new(to_oxc_formatter_css(&config, core, variant)),
|
||||
config,
|
||||
core,
|
||||
validated: validated.clone(),
|
||||
insert_final_newline,
|
||||
},
|
||||
FileKind::OxcFormatterYaml { path } => Self::OxcFormatterYaml {
|
||||
@@ -209,6 +218,13 @@ impl FormatStrategy {
|
||||
)),
|
||||
insert_final_newline,
|
||||
},
|
||||
FileKind::OxcFormatterMarkdown { path } => Self::OxcFormatterMarkdown {
|
||||
path,
|
||||
format_options: Box::new(to_oxc_formatter_markdown(&config, core)),
|
||||
config,
|
||||
validated: validated.clone(),
|
||||
insert_final_newline,
|
||||
},
|
||||
FileKind::OxfmtToml { path } => Self::OxfmtToml {
|
||||
path,
|
||||
toml_options: to_oxc_toml(&config, core),
|
||||
@@ -256,18 +272,23 @@ impl SourceFormatter {
|
||||
}
|
||||
}
|
||||
|
||||
/// The build's default `SessionServices` for a root run (`embed::services::for_root`),
|
||||
/// with the napi transport threaded in; the cfg fork lives here once, not per root.
|
||||
/// A `PhysicalFile` root session at `path` carrying the build's default `SessionServices`
|
||||
/// (`embed::services::for_root`), with the napi transport threaded in;
|
||||
/// the cfg fork lives here once, not per root.
|
||||
#[cfg_attr(not(feature = "napi"), expect(clippy::unused_self))]
|
||||
fn root_services(&self, dispatch_config: &Arc<ResolvedDispatchConfig>) -> SessionServices {
|
||||
fn root_session<'a>(
|
||||
&self,
|
||||
allocator: &'a Allocator,
|
||||
config: &Arc<FormatConfig>,
|
||||
validated: ValidatedOptions,
|
||||
path: &Path,
|
||||
) -> FormatSession<'a> {
|
||||
let dispatch_config = ResolvedDispatchConfig::for_root(config, validated, path);
|
||||
#[cfg(feature = "napi")]
|
||||
{
|
||||
super::embed::services::for_root(self.external_services(), dispatch_config)
|
||||
}
|
||||
let services = super::embed::services::for_root(self.external_services(), &dispatch_config);
|
||||
#[cfg(not(feature = "napi"))]
|
||||
{
|
||||
super::embed::services::for_root(dispatch_config)
|
||||
}
|
||||
let services = super::embed::services::for_root(&dispatch_config);
|
||||
FormatSession::with_services(allocator, InputKind::PhysicalFile, services)
|
||||
}
|
||||
|
||||
/// Format a file based on its resolved strategy.
|
||||
@@ -290,7 +311,7 @@ impl SourceFormatter {
|
||||
source_type,
|
||||
format_options,
|
||||
config,
|
||||
core,
|
||||
validated,
|
||||
insert_final_newline,
|
||||
} => (
|
||||
self.format_by_oxc_formatter(
|
||||
@@ -299,7 +320,7 @@ impl SourceFormatter {
|
||||
source_type,
|
||||
*format_options,
|
||||
&config,
|
||||
core,
|
||||
validated,
|
||||
),
|
||||
insert_final_newline,
|
||||
),
|
||||
@@ -329,7 +350,7 @@ impl SourceFormatter {
|
||||
path,
|
||||
format_options,
|
||||
config,
|
||||
core,
|
||||
validated,
|
||||
insert_final_newline,
|
||||
} => (
|
||||
self.format_by_oxc_formatter_css(
|
||||
@@ -337,7 +358,7 @@ impl SourceFormatter {
|
||||
&path,
|
||||
*format_options,
|
||||
&config,
|
||||
core,
|
||||
validated,
|
||||
),
|
||||
insert_final_newline,
|
||||
),
|
||||
@@ -359,6 +380,22 @@ impl SourceFormatter {
|
||||
),
|
||||
insert_final_newline,
|
||||
),
|
||||
FormatStrategy::OxcFormatterMarkdown {
|
||||
path,
|
||||
format_options,
|
||||
config,
|
||||
validated,
|
||||
insert_final_newline,
|
||||
} => (
|
||||
self.format_by_oxc_formatter_markdown(
|
||||
source_text,
|
||||
&path,
|
||||
*format_options,
|
||||
&config,
|
||||
validated,
|
||||
),
|
||||
insert_final_newline,
|
||||
),
|
||||
FormatStrategy::OxfmtToml { toml_options, insert_final_newline, .. } => {
|
||||
(Ok(Self::format_by_toml(source_text, toml_options)), insert_final_newline)
|
||||
}
|
||||
@@ -414,14 +451,10 @@ impl SourceFormatter {
|
||||
source_type: SourceType,
|
||||
format_options: JsFormatOptions,
|
||||
config: &Arc<FormatConfig>,
|
||||
core: CoreFormatOptions,
|
||||
validated: ValidatedOptions,
|
||||
) -> Result<String, OxcDiagnostic> {
|
||||
let allocator = self.allocator_pool.get();
|
||||
let session = {
|
||||
let dispatch_config = ResolvedDispatchConfig::for_root(config, core, path);
|
||||
let services = self.root_services(&dispatch_config);
|
||||
FormatSession::with_services(&allocator, InputKind::PhysicalFile, services)
|
||||
};
|
||||
let session = self.root_session(&allocator, config, validated, path);
|
||||
|
||||
let code = {
|
||||
let formatted = oxc_formatter::format_with_session(
|
||||
@@ -533,14 +566,10 @@ impl SourceFormatter {
|
||||
path: &Path,
|
||||
format_options: CssFormatOptions,
|
||||
config: &Arc<FormatConfig>,
|
||||
core: CoreFormatOptions,
|
||||
validated: ValidatedOptions,
|
||||
) -> Result<String, OxcDiagnostic> {
|
||||
let allocator = self.allocator_pool.get();
|
||||
let session = {
|
||||
let dispatch_config = ResolvedDispatchConfig::for_root(config, core, path);
|
||||
let services = self.root_services(&dispatch_config);
|
||||
FormatSession::with_services(&allocator, InputKind::PhysicalFile, services)
|
||||
};
|
||||
let session = self.root_session(&allocator, config, validated, path);
|
||||
|
||||
let code = {
|
||||
let formatted =
|
||||
@@ -592,6 +621,37 @@ impl SourceFormatter {
|
||||
self.format_by_oxc_formatter_yaml(source_text, path, yaml_format_options)
|
||||
}
|
||||
|
||||
/// Format Markdown source using `oxc_formatter_markdown` on a `PhysicalFile` session
|
||||
/// carrying the build's default services:
|
||||
/// front matter (`yaml` / `toml`) and fenced code blocks dispatch through the registry
|
||||
/// (non-native languages reach Prettier in the napi build, stay verbatim otherwise).
|
||||
/// `embeddedLanguageFormatting: off` installs no dispatcher and both stay verbatim.
|
||||
#[instrument(level = "debug", name = "oxfmt::format::oxc_formatter_markdown", skip_all)]
|
||||
fn format_by_oxc_formatter_markdown(
|
||||
&self,
|
||||
source_text: &str,
|
||||
path: &Path,
|
||||
format_options: MarkdownFormatOptions,
|
||||
config: &Arc<FormatConfig>,
|
||||
validated: ValidatedOptions,
|
||||
) -> Result<String, OxcDiagnostic> {
|
||||
let allocator = self.allocator_pool.get();
|
||||
let session = self.root_session(&allocator, config, validated, path);
|
||||
|
||||
let code = {
|
||||
let formatted =
|
||||
oxc_formatter_markdown::format_with_session(&session, source_text, format_options)?;
|
||||
formatted.print().map_err(|err| {
|
||||
OxcDiagnostic::error(format!(
|
||||
"Failed to print formatted Markdown: {}\n{err}",
|
||||
path.display()
|
||||
))
|
||||
})?
|
||||
};
|
||||
|
||||
Ok(code.into_code())
|
||||
}
|
||||
|
||||
/// Format TOML file using `oxc_toml`.
|
||||
#[instrument(level = "debug", name = "oxfmt::format::oxc_toml", skip_all)]
|
||||
fn format_by_toml(source_text: &str, options: oxc_toml::Options) -> String {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//! - [`to_oxc_formatter_css()`]: `oxc_formatter_css::CssFormatOptions` for CSS/SCSS/Less 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_markdown()`]: `oxc_formatter_markdown::MarkdownFormatOptions` for Markdown formatting
|
||||
//! - [`to_oxc_toml()`]: `oxc_toml::Options` for TOML formatting
|
||||
//! - `to_prettier`(NAPI-only): Prettier-compatible JSON, plus `inject_*` helpers for
|
||||
//! layering in `parser` / `filepath` / plugin payloads at the format step
|
||||
@@ -19,6 +20,7 @@ mod to_oxc_formatter;
|
||||
mod to_oxc_formatter_css;
|
||||
mod to_oxc_formatter_graphql;
|
||||
mod to_oxc_formatter_json;
|
||||
mod to_oxc_formatter_markdown;
|
||||
mod to_oxc_formatter_yaml;
|
||||
mod to_oxc_toml;
|
||||
#[cfg(feature = "napi")]
|
||||
@@ -29,6 +31,7 @@ pub use to_oxc_formatter::to_oxc_formatter;
|
||||
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_json::{to_oxc_formatter_json, to_sort_package_json};
|
||||
pub use to_oxc_formatter_markdown::to_oxc_formatter_markdown;
|
||||
pub use to_oxc_formatter_yaml::to_oxc_formatter_yaml;
|
||||
pub use to_oxc_toml::to_oxc_toml;
|
||||
#[cfg(feature = "napi")]
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use oxc_formatter_core::{CoreFormatOptions, FormatOptions};
|
||||
use oxc_formatter_markdown::{MarkdownFormatOptions, ProseWrap, SingleQuote};
|
||||
|
||||
use super::super::oxfmtrc::{FormatConfig, ProseWrapConfig};
|
||||
|
||||
/// Convert `FormatConfig` into `MarkdownFormatOptions` for `oxc_formatter_markdown`.
|
||||
///
|
||||
/// Prettier's `markdown` language consumes the shared layout options plus
|
||||
/// `proseWrap` and `singleQuote`.
|
||||
///
|
||||
/// NOTE: Pure field translation:
|
||||
/// `core` comes pre-validated from the config-resolution gate (`validate()`), so this cannot fail.
|
||||
pub fn to_oxc_formatter_markdown(
|
||||
config: &FormatConfig,
|
||||
core_options: CoreFormatOptions,
|
||||
) -> MarkdownFormatOptions {
|
||||
let mut options = MarkdownFormatOptions::default();
|
||||
options.apply_core(core_options);
|
||||
|
||||
// [Prettier] proseWrap: "preserve" | "always" | "never"
|
||||
if let Some(prose_wrap) = config.prose_wrap {
|
||||
options.prose_wrap = match prose_wrap {
|
||||
ProseWrapConfig::Preserve => ProseWrap::Preserve,
|
||||
ProseWrapConfig::Always => ProseWrap::Always,
|
||||
ProseWrapConfig::Never => ProseWrap::Never,
|
||||
};
|
||||
}
|
||||
// [Prettier] singleQuote: boolean
|
||||
if let Some(single_quote) = config.single_quote {
|
||||
options.single_quote = SingleQuote::from(single_quote);
|
||||
}
|
||||
|
||||
options
|
||||
}
|
||||
@@ -230,8 +230,8 @@ pub fn build_prettier_options(config: &FormatConfig, path: &Path) -> Value {
|
||||
/// Inject Svelte plugin keys derived from `config.svelte`.
|
||||
///
|
||||
/// No-ops when `svelte` is disabled (unset or `false`) — `Bool(true)` falls back to defaults.
|
||||
/// The caller gates this on capability (`supports_svelte`):
|
||||
/// `.svelte` is the primary target, plus `markdown`/`mdx` for code blocks.
|
||||
/// The callers gate this on capability: a `.svelte` / `.mdx` file (`supports_svelte`),
|
||||
/// and a Markdown code block through the Doc→IR fallback (`ResolvedDispatchConfig::prettier_options_for`).
|
||||
///
|
||||
/// See: <https://github.com/sveltejs/prettier-plugin-svelte#options>
|
||||
pub fn inject_svelte_plugin_payload(opts: &mut Value, config: &FormatConfig) {
|
||||
|
||||
@@ -70,6 +70,9 @@ pub fn classify_file_kind(path: Arc<Path>) -> Option<FileKind> {
|
||||
if is_yaml_file(file_name, extension) {
|
||||
return Some(FileKind::OxcFormatterYaml { path });
|
||||
}
|
||||
if is_markdown_file(file_name, extension) {
|
||||
return Some(FileKind::OxcFormatterMarkdown { path });
|
||||
}
|
||||
|
||||
// Prettier-delegated files are only supported with the `napi` feature
|
||||
#[cfg(feature = "napi")]
|
||||
@@ -113,6 +116,8 @@ pub enum FileKind {
|
||||
/// Files like `.prettierrc`:
|
||||
/// mirroring Prettier's yaml embed, they are formatted as JSON first, then fall back to YAML if that fails.
|
||||
OxcFormatterYamlRc { path: Arc<Path> },
|
||||
/// Markdown files formatted by `oxc_formatter_markdown`.
|
||||
OxcFormatterMarkdown { path: Arc<Path> },
|
||||
/// TOML files formatted by taplo (Pure Rust).
|
||||
OxfmtToml { path: Arc<Path> },
|
||||
/// Files formatted by delegating to Prettier (Tier 3/4).
|
||||
@@ -141,6 +146,7 @@ impl FileKind {
|
||||
| Self::OxcFormatterCss { path, .. }
|
||||
| Self::OxcFormatterYaml { path }
|
||||
| Self::OxcFormatterYamlRc { path }
|
||||
| Self::OxcFormatterMarkdown { path }
|
||||
| Self::OxfmtToml { path } => path,
|
||||
#[cfg(feature = "napi")]
|
||||
Self::Prettier { path, .. } => path,
|
||||
@@ -187,16 +193,14 @@ static OXFMT_PARSERS: phf::Set<&'static str> = phf_set! {
|
||||
// "html",
|
||||
"vue",
|
||||
"svelte",
|
||||
// "markdown",
|
||||
// "mdx",
|
||||
};
|
||||
|
||||
/// Parsers(files) that benefit from `prettier-plugin-svelte`.
|
||||
/// `.svelte` is the primary target; `markdown`/`mdx` allow ` ```svelte ` code blocks.
|
||||
/// `.svelte` is the primary target; `mdx` allows ` ```svelte ` code blocks.
|
||||
#[cfg(feature = "napi")]
|
||||
static SVELTE_PARSERS: phf::Set<&'static str> = phf_set! {
|
||||
"svelte",
|
||||
"markdown",
|
||||
"mdx",
|
||||
};
|
||||
|
||||
@@ -414,19 +418,39 @@ static YAML_EXTENSIONS: phf::Set<&'static str> = phf_set! {
|
||||
|
||||
// ---
|
||||
|
||||
/// Returns `true` if this is a Markdown file (handled by `oxc_formatter_markdown`).
|
||||
fn is_markdown_file(file_name: &str, extension: Option<&str>) -> bool {
|
||||
if MARKDOWN_FILENAMES.contains(file_name) {
|
||||
return true;
|
||||
}
|
||||
extension.is_some_and(|ext| MARKDOWN_EXTENSIONS.contains(ext))
|
||||
}
|
||||
|
||||
static MARKDOWN_FILENAMES: phf::Set<&'static str> = phf_set! {
|
||||
"contents.lr",
|
||||
"README",
|
||||
};
|
||||
|
||||
static MARKDOWN_EXTENSIONS: phf::Set<&'static str> = phf_set! {
|
||||
"md",
|
||||
"livemd",
|
||||
"markdown",
|
||||
"mdown",
|
||||
"mdwn",
|
||||
"mkd",
|
||||
"mkdn",
|
||||
"mkdown",
|
||||
"ronn",
|
||||
"scd",
|
||||
"workbook",
|
||||
};
|
||||
|
||||
// ---
|
||||
|
||||
/// Returns the Prettier parser name for the file, if supported.
|
||||
/// See also `prettier --support-info | jq '.languages[]'`
|
||||
#[cfg(feature = "napi")]
|
||||
fn get_prettier_parser_name(file_name: &str, extension: Option<&str>) -> Option<&'static str> {
|
||||
// Markdown and variants
|
||||
if MARKDOWN_FILENAMES.contains(file_name) {
|
||||
return Some("markdown");
|
||||
}
|
||||
if let Some(ext) = extension
|
||||
&& MARKDOWN_EXTENSIONS.contains(ext)
|
||||
{
|
||||
return Some("markdown");
|
||||
}
|
||||
if extension == Some("mdx") {
|
||||
return Some("mdx");
|
||||
}
|
||||
@@ -480,27 +504,6 @@ static HANDLEBARS_EXTENSIONS: phf::Set<&'static str> = phf_set! {
|
||||
"hbs",
|
||||
};
|
||||
|
||||
#[cfg(feature = "napi")]
|
||||
static MARKDOWN_FILENAMES: phf::Set<&'static str> = phf_set! {
|
||||
"contents.lr",
|
||||
"README",
|
||||
};
|
||||
|
||||
#[cfg(feature = "napi")]
|
||||
static MARKDOWN_EXTENSIONS: phf::Set<&'static str> = phf_set! {
|
||||
"md",
|
||||
"livemd",
|
||||
"markdown",
|
||||
"mdown",
|
||||
"mdwn",
|
||||
"mkd",
|
||||
"mkdn",
|
||||
"mkdown",
|
||||
"ronn",
|
||||
"scd",
|
||||
"workbook",
|
||||
};
|
||||
|
||||
// ---
|
||||
|
||||
// Additional extensions from linguist-languages, which Prettier also supports
|
||||
@@ -661,12 +664,14 @@ mod tests {
|
||||
// Handlebars
|
||||
("template.handlebars", Some("glimmer")),
|
||||
("partial.hbs", Some("glimmer")),
|
||||
// Markdown
|
||||
("README", Some("markdown")),
|
||||
("contents.lr", Some("markdown")),
|
||||
("docs.md", Some("markdown")),
|
||||
("guide.markdown", Some("markdown")),
|
||||
("notes.mdown", Some("markdown")),
|
||||
// Markdown files are routed to `oxc_formatter_markdown` in `classify_file_kind`
|
||||
// and excluded from this map.
|
||||
("README", None),
|
||||
("contents.lr", None),
|
||||
("docs.md", None),
|
||||
("guide.markdown", None),
|
||||
("notes.mdown", None),
|
||||
// MDX
|
||||
("page.mdx", Some("mdx")),
|
||||
// YAML files are routed to `oxc_formatter_yaml` in `classify_file_kind`
|
||||
// and excluded from this map.
|
||||
@@ -788,6 +793,18 @@ mod tests {
|
||||
assert!(result.is_none(), "`pnpm-lock.yaml` should be excluded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_markdown_files_route_to_oxc_formatter_markdown() {
|
||||
// MARKDOWN_EXTENSIONS and MARKDOWN_FILENAMES
|
||||
for file_name in ["docs.md", "guide.markdown", "notes.mdown", "README", "contents.lr"] {
|
||||
let result = classify_file_kind(Arc::from(Path::new(file_name)));
|
||||
assert!(
|
||||
matches!(result, Some(FileKind::OxcFormatterMarkdown { .. })),
|
||||
"`{file_name}` should be routed to oxc_formatter_markdown"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toml_files() {
|
||||
// Files that should be detected as TOML
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Format js-in-markdown with prettier-plugin-oxfmt > should be disabled by \`embeddedLanguageFormatting: 'off'\` 1`] = `
|
||||
exports[`Format js-in-markdown > should be disabled by \`embeddedLanguageFormatting: 'off'\` 1`] = `
|
||||
"\`\`\`js
|
||||
const x=1;
|
||||
const y=2;
|
||||
@@ -9,7 +9,7 @@ console.log(x+y);
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Format js-in-markdown with prettier-plugin-oxfmt > should be disabled by prettier-ignore 1`] = `
|
||||
exports[`Format js-in-markdown > should be disabled by prettier-ignore 1`] = `
|
||||
"# Markdown
|
||||
|
||||
## Disabled
|
||||
@@ -34,7 +34,7 @@ const X = () => (
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Format js-in-markdown with prettier-plugin-oxfmt > should format .md w/o duplicating TS lines 1`] = `
|
||||
exports[`Format js-in-markdown > should format .md w/o duplicating TS lines 1`] = `
|
||||
"# Markdown
|
||||
|
||||
In Prettier, using \`typescript\` parser inside \`markdown\` parser duplicates lines after \`await { then }\`.
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { format } from "../../dist/index.js";
|
||||
|
||||
// oxlint-disable jest/no-disabled-tests
|
||||
describe.skip("Format js-in-markdown with prettier-plugin-oxfmt", () => {
|
||||
describe("Format js-in-markdown", () => {
|
||||
it("should format .md w/o duplicating TS lines", async () => {
|
||||
// https://prettier.io/playground/#N4Igxg9gdgLgprEAuEADdMDOAdKBDAdzwEsYACABQCcIBbYzOAOirkwgBsA3OACjLLYQXPBwCucIQICUAblyES5YLgEwAFgl7SyKqAIC+AGlwH5UAPQWyAFXUMykWrQTk8UACZkARnA4QCMgJiDg4fODIPMQAHDmIwPHgPBSJSMgBzOBhqOgY+aVx0VBAjEAhomGJoTGRQPCoaAgp6hBqUUSIATxrS7yo8MABrLIBlPBcAGWIoOGQAM1FGUohvACs4MBgAdX7o5BBo1kYqHhKQPoHhmBHogen05BgqCVLGekfnuFK4AA9ouCoxBcsFEAHl-v0YBAqBQIJhSFUoPsEB4zr8IUDXKIbAD8IC2PNFl8QPCoOkOHAAIpiCDwQkcJYgVaYH4je4U6m02ZIBYM4kARxp8By0TaIDwmAAtDM4B5ZWcniQ4mSAMJ0Wh4faiDhnUnkuAAQRgT2I3jEwoBUxm9MZ6hgtA4W3s8EwtzAcBGrQRXFInX2YEwPWEEgAkp5XCMwICKgbPCMYJ0KTbiYc4XAdng9ihDmwAadvp5QXMrdyQBw5mdpsdsv10hrk6VblRjvsNVRBh4AkjG4DYFtiB4NMgABwABlKrEFxFY1Dwdc1PKJpRgeG8-cH6mQACZSmJGDZV21eYy4LRfB45R4Ju50mI53AAGLQjXG+5a80QEAGAxAA
|
||||
const input = `
|
||||
@@ -47,6 +46,37 @@ But we are not!
|
||||
expect(result2.errors).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should sort imports with sortImports", async () => {
|
||||
const input = '```js\nimport z from "z";\nimport a from "a";\n```\n';
|
||||
const result = await format("a.md", input, { sortImports: {} });
|
||||
|
||||
expect(result.code).toMatchInlineSnapshot(`
|
||||
"\`\`\`js
|
||||
import a from "a";
|
||||
import z from "z";
|
||||
\`\`\`
|
||||
"
|
||||
`);
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should sort Tailwind classes of a JSX element", async () => {
|
||||
// The opening element is interned, so its classes sit below an `Interned` boundary
|
||||
const input =
|
||||
'```jsx\n<div className="p-4 flex"><span className="text-sm block">x</span></div>\n```\n';
|
||||
const result = await format("a.md", input, { sortTailwindcss: {} });
|
||||
|
||||
expect(result.code).toMatchInlineSnapshot(`
|
||||
"\`\`\`jsx
|
||||
<div className="flex p-4">
|
||||
<span className="block text-sm">x</span>
|
||||
</div>
|
||||
\`\`\`
|
||||
"
|
||||
`);
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("should be disabled by prettier-ignore", async () => {
|
||||
const input = `
|
||||
# Markdown
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { format } from "../../dist/index.js";
|
||||
|
||||
// Markdown is formatted by `oxc_formatter_markdown`;
|
||||
// its front matter and fenced code blocks dispatch through the session like any other host.
|
||||
// The outputs below were verified against bundled Prettier 3.9.
|
||||
// JS / TS code blocks are covered by `js-in-markdown.test.ts`.
|
||||
describe("Markdown", () => {
|
||||
it("formats every Markdown file name", async () => {
|
||||
for (const filename of ["a.md", "a.markdown", "README", "contents.lr"]) {
|
||||
// oxlint-disable-next-line no-await-in-loop
|
||||
const result = await format(filename, "# Title\n");
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toBe("# Title\n");
|
||||
}
|
||||
});
|
||||
|
||||
it("applies proseWrap and singleQuote", async () => {
|
||||
const source = 'a b c d e f\n\n[x](u "it\'s")\n';
|
||||
const result = await format("a.md", source, { printWidth: 5, proseWrap: "always" });
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toMatchInlineSnapshot(`
|
||||
"a b c
|
||||
d e f
|
||||
|
||||
[x](u "it's")
|
||||
"
|
||||
`);
|
||||
const single = await format("a.md", '[x](u "t")\n', { singleQuote: true });
|
||||
expect(single.code).toBe("[x](u 't')\n");
|
||||
});
|
||||
|
||||
it("formats YAML front matter", async () => {
|
||||
const result = await format("a.md", "---\ntitle: Home\n---\n# Hi\n");
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toMatchInlineSnapshot(`
|
||||
"---
|
||||
title: Home
|
||||
---
|
||||
|
||||
# Hi
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it("formats fenced code blocks, keeping their column", async () => {
|
||||
const source =
|
||||
"- a\n\n ```js\n const s = `x\n y`\n ```\n\n> ```css\n> a{color:red}\n> ```\n";
|
||||
const result = await format("a.md", source);
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toMatchInlineSnapshot(`
|
||||
"- a
|
||||
|
||||
\`\`\`js
|
||||
const s = \`x
|
||||
y\`;
|
||||
\`\`\`
|
||||
|
||||
> \`\`\`css
|
||||
> a {
|
||||
> color: red;
|
||||
> }
|
||||
> \`\`\`
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it("keeps container columns as spaces under useTabs", async () => {
|
||||
// A tab would shift the content column the next parse strips (the code and the quote change)
|
||||
const source =
|
||||
"- item\n\n ```js\n new Miniflare({\n httpsKey: 1,\n });\n ```\n\n > quoted\n > more\n";
|
||||
const result = await format("a.md", source, { useTabs: true });
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toBe(
|
||||
"- item\n\n ```js\n new Miniflare({\n \thttpsKey: 1,\n });\n ```\n\n > quoted\n > more\n",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps unknown or unparsable code blocks verbatim", async () => {
|
||||
const source = "```unknown\na b\n```\n\n```js\nconst =\n```\n";
|
||||
const result = await format("a.md", source);
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toBe(source);
|
||||
});
|
||||
|
||||
it("keeps front matter and code blocks verbatim under embeddedLanguageFormatting: off", async () => {
|
||||
const source = "---\ntitle: Home\n---\n\n```css\na{color:red}\n```\n";
|
||||
const result = await format("a.md", source, { embeddedLanguageFormatting: "off" });
|
||||
expect(result.errors).toStrictEqual([]);
|
||||
expect(result.code).toBe(source);
|
||||
});
|
||||
});
|
||||
@@ -26,6 +26,8 @@ The AST-wrapping IR primitives (`AstNode`, `Format`, `Buffer`, …) are `pub(cra
|
||||
- `format_program`: Special-purpose AST-in entry point
|
||||
- `format_with_session`: session-aware entry whose `FormatSession` carries the host-supplied `SessionServices`
|
||||
- the dispatcher (IR channel), the string embedder (JSDoc fences), and the Tailwind sorter (plain `format` / `format_program` wrap a service-less `PhysicalFile` session)
|
||||
- `format_to_ir`: Format a whole program as a dispatched child (e.g. a Markdown code block), returning IR for the parent's document
|
||||
- `JsEmbeddedIn` carries what it is embedded in, translated by the orchestrator from the parent's marker
|
||||
|
||||
### Generated code
|
||||
|
||||
|
||||
@@ -571,6 +571,7 @@ In the head the declaration has no terminator of its own; we keep it verbatim an
|
||||
|
||||
- Why: uniform-rule (same construct, same output: suppressed statement)
|
||||
- Pin: `tests/fixtures/js/semicolons/suppressed-statement.js`, `tests/fixtures/ts/semicolons/suppressed-class-member.ts`
|
||||
- Oxfmt: `externals/prettier/markdown/markdown/real-world-case.md`
|
||||
|
||||
```js
|
||||
// input (semi: true)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
//! JS↔child language-pair specific dispatch context types
|
||||
//! JS↔other language pair specific dispatch context types
|
||||
//! (host-delegated services travel as `SessionServices` on the `FormatSession` instead).
|
||||
//!
|
||||
//! With JS as the parent, JS picks the child language and passes the pair's own data.
|
||||
//! With JS as the child of a parent that cannot pick (a Markdown code block),
|
||||
//! the host translates the parent's fact about itself into the pair's data ([`JsEmbeddedIn`]).
|
||||
//!
|
||||
//! NOTE: These live here permanently, NOT in other formatter crate:
|
||||
//! the consumer is this crate's `embed/*.rs`, and `oxc_formatter` must never depend on language crates.
|
||||
//! What every pair shares (the Tailwind class scope) travels on the `FormatSession` in `oxc_formatter_core` instead.
|
||||
|
||||
/// Parent→child parse-mode context for CSS dispatched from a JS template literal (css-in-js).
|
||||
///
|
||||
@@ -7,21 +15,27 @@
|
||||
/// Travels as `DispatchRequest::parent_context`;
|
||||
/// its ABSENCE means the child parses as a plain standalone stylesheet
|
||||
/// (e.g. a JSDoc fence routed through the dispatcher).
|
||||
///
|
||||
/// Defined here for the same reason as [`HtmlEmbedMeta`]:
|
||||
/// it is JS↔CSS pair-specific, and `oxc_formatter` must never depend on language crates.
|
||||
pub struct CssInJsTemplate;
|
||||
|
||||
/// Child→parent pair context for HTML/Angular formatted as an embedded child.
|
||||
/// Parent→child context for Markdown dispatched from a JS template literal (markdown-in-js).
|
||||
///
|
||||
/// NOTE: This lives here permanently, NOT in a future HTML formatter crate:
|
||||
/// the consumer is this crate's `embed/html.rs` (the JS side of html-in-js),
|
||||
/// and `oxc_formatter` must never depend on language crates. Cross-language
|
||||
/// contract fields (placeholder counts, Tailwind classes) are first-class on
|
||||
/// `DispatchPayload` in `oxc_formatter_core` instead; only what is truly
|
||||
/// specific to the JS↔HTML pair travels as the `dyn Any` child context.
|
||||
/// The child prints its code fences with `~` instead of backticks,
|
||||
/// which would need escaping in the template.
|
||||
pub struct MarkdownInJsTemplate;
|
||||
|
||||
/// Child→parent pair context for HTML/Angular formatted as an embedded child (html-in-js).
|
||||
pub struct HtmlEmbedMeta {
|
||||
/// Whether the parsed HTML has more than one root element.
|
||||
/// Used to decide whether to `indent` the template content.
|
||||
pub has_multiple_root_elements: Option<bool>,
|
||||
}
|
||||
|
||||
/// What a dispatched JS/TS program is embedded in, for the print rules that depend on it.
|
||||
///
|
||||
/// The host translates the parent's fact about itself into this (see [`crate::format_to_ir`]).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum JsEmbeddedIn {
|
||||
/// A Markdown code block (js-in-markdown):
|
||||
/// a program of a single JSX expression statement prints without its semicolon.
|
||||
MarkdownCodeBlock,
|
||||
}
|
||||
@@ -79,15 +79,11 @@ impl TailwindContextEntry {
|
||||
/// Context object storing data relevant when formatting an object.
|
||||
pub struct JsFormatContext<'ast> {
|
||||
options: JsFormatOptions,
|
||||
|
||||
source_text: SourceText<'ast>,
|
||||
|
||||
source_type: SourceType,
|
||||
|
||||
comments: Comments<'ast>,
|
||||
|
||||
cached_elements: FxHashMap<Span, FormatElement<'ast>>,
|
||||
|
||||
/// One-shot handoff of the assignment layout to the arrow expression on the RHS of an assignment-like,
|
||||
/// keyed by the arrow's span so no other node can consume it.
|
||||
/// Set (and cleared) by `WithAssignmentLayout` around formatting the arrow, taken by the arrow's `write`.
|
||||
@@ -103,6 +99,10 @@ pub struct JsFormatContext<'ast> {
|
||||
/// Stack tracking whether we're inside a Tailwind class context.
|
||||
/// When non-empty, StringLiterals should be sorted as Tailwind classes.
|
||||
tailwind_context_stack: Vec<TailwindContextEntry>,
|
||||
|
||||
/// Set only for an embedded program (see [`crate::format_to_ir`]):
|
||||
/// its sole statement prints without its semicolon.
|
||||
embedding_omits_semicolon: bool,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for JsFormatContext<'_> {
|
||||
@@ -147,6 +147,7 @@ impl<'ast> JsFormatContext<'ast> {
|
||||
arrow_assignment_layout: None,
|
||||
quote_needed_stack: Vec::new(),
|
||||
tailwind_context_stack: Vec::new(),
|
||||
embedding_omits_semicolon: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,6 +233,14 @@ impl<'ast> JsFormatContext<'ast> {
|
||||
*self.quote_needed_stack.last().unwrap_or(&false)
|
||||
}
|
||||
|
||||
pub(crate) fn set_embedding_omits_semicolon(&mut self, omit: bool) {
|
||||
self.embedding_omits_semicolon = omit;
|
||||
}
|
||||
|
||||
pub(crate) fn embedding_omits_semicolon(&self) -> bool {
|
||||
self.embedding_omits_semicolon
|
||||
}
|
||||
|
||||
/// Push a Tailwind context entry onto the stack.
|
||||
/// Call this when entering a JSXAttribute or CallExpression with Tailwind class context.
|
||||
pub fn push_tailwind_context(&mut self, entry: TailwindContextEntry) {
|
||||
|
||||
@@ -33,8 +33,10 @@ pub use self::{
|
||||
context::{JsFormatContext, TailwindContextEntry},
|
||||
formatter_js::{JsFormatter, JsFormatterExt},
|
||||
};
|
||||
use oxc_allocator::ArenaVec;
|
||||
use oxc_formatter_core::{
|
||||
Arguments, Buffer as _, Document, FormatSession, FormatState, Formatted, VecBuffer,
|
||||
Arguments, Buffer as _, Document, FormatElement, FormatSession, FormatState, Formatted,
|
||||
VecBuffer,
|
||||
};
|
||||
|
||||
/// The `format` function takes an [`Arguments`] struct and returns the resulting formatting IR.
|
||||
@@ -45,23 +47,33 @@ pub fn format<'ast>(
|
||||
session: &FormatSession<'ast>,
|
||||
arguments: Arguments<'_, 'ast, JsFormatContext<'ast>>,
|
||||
) -> Formatted<'ast, JsFormatContext<'ast>> {
|
||||
// Pre-allocate buffer at 40% of source length (source_len * 2 / 5).
|
||||
// Analysis of 4,891 VSCode files shows FormatElement buffer length is typically 19% of source (median),
|
||||
// with 95th percentile at 30-38% across all file sizes. This 0.4x multiplier avoids reallocation for 95%+ of files.
|
||||
let capacity = (context.source_text().len() * 2) / 5;
|
||||
|
||||
// A root `Document` owns a fresh class scope;
|
||||
// a root nested on the same session (a JSDoc snippet) must not take its host's classes.
|
||||
let session = session.with_new_tailwind_scope();
|
||||
let mut state = FormatState::new_with_session(context, session.clone());
|
||||
let mut buffer = VecBuffer::with_capacity(capacity, &mut state);
|
||||
|
||||
buffer.write_fmt(arguments);
|
||||
|
||||
let elements = buffer.into_vec();
|
||||
let context = state.into_context();
|
||||
let (elements, context) = build_ir(context, &session, arguments);
|
||||
|
||||
let ir = Document::new(elements, session.take_sorted_tailwind_classes());
|
||||
|
||||
Formatted::new(ir, context)
|
||||
}
|
||||
|
||||
/// Builds the IR on `session` as is, returning it with the context.
|
||||
/// For embedding into another formatter's document ([`crate::format_to_ir`]),
|
||||
/// whose Tailwind classes go into the parent's scope (no fresh scope, no sort).
|
||||
pub fn build_ir<'ast>(
|
||||
context: JsFormatContext<'ast>,
|
||||
session: &FormatSession<'ast>,
|
||||
arguments: Arguments<'_, 'ast, JsFormatContext<'ast>>,
|
||||
) -> (ArenaVec<'ast, FormatElement<'ast>>, JsFormatContext<'ast>) {
|
||||
// Pre-allocate buffer at 40% of source length (source_len * 2 / 5).
|
||||
// Analysis of 4,891 VSCode files shows FormatElement buffer length is typically 19% of source (median),
|
||||
// with 95th percentile at 30-38% across all file sizes. This 0.4x multiplier avoids reallocation for 95%+ of files.
|
||||
let capacity = (context.source_text().len() * 2) / 5;
|
||||
|
||||
let mut state = FormatState::new_with_session(context, session.clone());
|
||||
let mut buffer = VecBuffer::with_capacity(capacity, &mut state);
|
||||
|
||||
buffer.write_fmt(arguments);
|
||||
|
||||
(buffer.into_vec(), state.into_context())
|
||||
}
|
||||
@@ -17,18 +17,19 @@ use oxc_allocator::Allocator;
|
||||
use oxc_ast::Comment;
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_formatter_core::{FormatSession, Formatted, InputKind};
|
||||
use oxc_formatter_core::{EmbeddedIr, FormatSession, Formatted, InputKind};
|
||||
use oxc_parser::{ParseOptions, Parser, ParserReturn};
|
||||
use oxc_span::SourceType;
|
||||
|
||||
// Internal only AST-wrapping IR primitives.
|
||||
// External call-sites use the text-in `format`, `format_fragment`,
|
||||
// External call-sites use the text-in `format`, `format_with_session`, `format_fragment`, `format_to_ir`,
|
||||
// or the special-purpose AST-in `format_program`.
|
||||
pub(crate) use crate::ast_nodes::{AstNode, AstNodes};
|
||||
pub use crate::embed_context::{CssInJsTemplate, HtmlEmbedMeta};
|
||||
// `JsFormatContext` is public solely as the type parameter of the `Formatted`
|
||||
// returned by `format` / `format_fragment`.
|
||||
// Its methods are not part of the public contract.
|
||||
pub use crate::embed_context::{
|
||||
CssInJsTemplate, HtmlEmbedMeta, JsEmbeddedIn, MarkdownInJsTemplate,
|
||||
};
|
||||
// `JsFormatContext` is public solely as the type parameter of the returned `Formatted`;
|
||||
// its methods are not part of the public contract.
|
||||
pub use crate::formatter::JsFormatContext;
|
||||
pub use crate::ir_transform::options::*;
|
||||
pub use crate::options::*;
|
||||
@@ -108,6 +109,51 @@ pub fn format_with_session<'a>(
|
||||
Ok(format_program_with_session(session, program, options))
|
||||
}
|
||||
|
||||
/// Parse `source_text` and build the IR for embedding into another formatter's document
|
||||
/// (dispatcher path, e.g. a `js` code fence in Markdown).
|
||||
///
|
||||
/// Unlike [`format_with_session`], this:
|
||||
/// - allocates from the session's shared arena and `GroupId` space, so the IR lives as long as the parent's document
|
||||
/// - leaves the Tailwind classes unsorted (the parent sorts them in its own batch)
|
||||
/// - emits neither a BOM nor the trailing newline
|
||||
///
|
||||
/// `embedded_in` is what the program is embedded in, for the print rules that depend on it ([`JsEmbeddedIn`]).
|
||||
///
|
||||
/// # Errors
|
||||
/// Same as [`format()`].
|
||||
pub fn format_to_ir<'a>(
|
||||
session: &FormatSession<'a>,
|
||||
source_text: &str,
|
||||
source_type: SourceType,
|
||||
options: JsFormatOptions,
|
||||
embedded_in: Option<JsEmbeddedIn>,
|
||||
) -> Result<EmbeddedIr<'a>, OxcDiagnostic> {
|
||||
let allocator = session.allocator();
|
||||
let program = parse(allocator, allocator.alloc_str(source_text), source_type)?;
|
||||
let node = AstNode::new(program, AstNodes::Dummy(), allocator);
|
||||
let mut context =
|
||||
JsFormatContext::new(program.source_text, program.source_type, &program.comments, options);
|
||||
|
||||
// Special cases for js-in-xxx:
|
||||
// - Single JSX in Markdown code block: omit the semicolon
|
||||
let embedding_omits_semicolon = embedded_in == Some(JsEmbeddedIn::MarkdownCodeBlock)
|
||||
&& program.directives.is_empty()
|
||||
&& matches!(
|
||||
program.body.as_slice(),
|
||||
[Statement::ExpressionStatement(statement)] if statement.expression.is_jsx()
|
||||
);
|
||||
context.set_embedding_omits_semicolon(embedding_omits_semicolon);
|
||||
|
||||
let (ir, _) = formatter::build_ir(
|
||||
context,
|
||||
session,
|
||||
oxc_formatter_core::Arguments::new(&[oxc_formatter_core::Argument::new(
|
||||
&print::FormatProgramBody(&node),
|
||||
)]),
|
||||
);
|
||||
Ok(EmbeddedIr { ir })
|
||||
}
|
||||
|
||||
/// Format a pre-wrapped JS/TS-in-xxx fragment from source text.
|
||||
///
|
||||
/// The caller passes source already wrapped per the [`FragmentContext`] input contract
|
||||
|
||||
@@ -23,6 +23,7 @@ mod object_like;
|
||||
mod object_pattern_like;
|
||||
mod parameters;
|
||||
mod program;
|
||||
pub use program::FormatProgramBody;
|
||||
mod return_or_throw_statement;
|
||||
pub mod semicolon;
|
||||
mod sequence_expression;
|
||||
@@ -529,6 +530,15 @@ impl<'a> FormatWrite<'a> for AstNode<'a, EmptyStatement> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the embedding decided this program's sole statement prints without its semicolon (see `crate::format_to_ir`):
|
||||
/// no trailing one, and no ASI guard under `semi: false` either.
|
||||
fn is_semicolon_omitted_by_embedding(
|
||||
stmt: &AstNode<'_, ExpressionStatement<'_>>,
|
||||
f: &JsFormatter<'_, '_>,
|
||||
) -> bool {
|
||||
f.context().embedding_omits_semicolon() && matches!(stmt.parent(), AstNodes::Program(_))
|
||||
}
|
||||
|
||||
/// Returns `true` if the expression needs a leading semicolon to prevent ASI issues.
|
||||
///
|
||||
/// `verbatim` is set for a suppressed statement, whose printed form is the source text:
|
||||
@@ -538,6 +548,10 @@ fn expression_statement_needs_semicolon<'a>(
|
||||
f: &JsFormatter<'_, 'a>,
|
||||
verbatim: bool,
|
||||
) -> bool {
|
||||
if is_semicolon_omitted_by_embedding(stmt, f) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if matches!(
|
||||
stmt.parent(),
|
||||
// `if (true) (() => {})`
|
||||
@@ -661,6 +675,11 @@ impl<'a> FormatWrite<'a> for AstNode<'a, ExpressionStatement<'a>> {
|
||||
write_leading_comments_with_asi_guard(self, false, f);
|
||||
|
||||
let expression = self.expression();
|
||||
if is_semicolon_omitted_by_embedding(self, f) {
|
||||
write!(f, expression);
|
||||
return;
|
||||
}
|
||||
|
||||
let content_end = semicolon_terminated_expression_content_end(
|
||||
f,
|
||||
expression.as_ref(),
|
||||
|
||||
@@ -21,6 +21,26 @@ use super::FormatWrite;
|
||||
|
||||
impl<'a> FormatWrite<'a> for AstNode<'a, Program<'a>> {
|
||||
fn write(&self, f: &mut JsFormatter<'_, 'a>) {
|
||||
// BOM: JS is the exception to the entries-own-the-strip rule.
|
||||
// `format_program` is AST-in (the formatter never owns pre-parse text)
|
||||
// and oxc_parser lexes U+FEFF as whitespace itself.
|
||||
// Detect at print time, re-emit once at byte 0.
|
||||
let has_bom = oxc_formatter_core::spec::split_bom(f.source_text().as_str()).0;
|
||||
|
||||
write!(
|
||||
f,
|
||||
[has_bom.then_some(text("\u{feff}")), FormatProgramBody(self), hard_line_break()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A program without its envelope (BOM, trailing newline): the embedded root of [`crate::format_to_ir`],
|
||||
/// whose host owns the layout around it.
|
||||
pub struct FormatProgramBody<'a, 'b>(pub &'b AstNode<'a, Program<'a>>);
|
||||
|
||||
impl<'a> Format<'a, JsFormatContext<'a>> for FormatProgramBody<'a, '_> {
|
||||
fn fmt(&self, f: &mut JsFormatter<'_, 'a>) {
|
||||
let program = self.0;
|
||||
let format_trailing_comments = format_with(|f| {
|
||||
write!(
|
||||
f,
|
||||
@@ -28,20 +48,13 @@ impl<'a> FormatWrite<'a> for AstNode<'a, Program<'a>> {
|
||||
);
|
||||
});
|
||||
|
||||
// BOM: JS is the exception to the entries-own-the-strip rule — `format_program`
|
||||
// is AST-in (the formatter never owns pre-parse text) and oxc_parser lexes
|
||||
// U+FEFF as whitespace itself. Detect at print time, re-emit once at byte 0.
|
||||
let has_bom = oxc_formatter_core::spec::split_bom(f.source_text().as_str()).0;
|
||||
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
has_bom.then_some(text("\u{feff}")),
|
||||
self.hashbang(),
|
||||
self.directives(),
|
||||
FormatStatementsWithImports(self.body()),
|
||||
format_trailing_comments,
|
||||
hard_line_break()
|
||||
program.hashbang(),
|
||||
program.directives(),
|
||||
FormatStatementsWithImports(program.body()),
|
||||
format_trailing_comments
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use oxc_allocator::{ArenaStringBuilder, ArenaVec};
|
||||
use oxc_allocator::ArenaStringBuilder;
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_formatter_core::{
|
||||
DispatchRequest, DispatchResponse, FormatElement, IndentWidth, InputKind,
|
||||
format_element::{LineMode, TextWidth},
|
||||
DispatchRequest, DispatchResponse, FormatElement, InputKind, format_element::TextWidth,
|
||||
map_text_in_ir, push_text_with_literal_lines,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -158,7 +158,7 @@ pub(super) fn format_html_doc<'a>(
|
||||
// and substituting placeholders inside every `BestFitting` variant.
|
||||
// Escaping cannot alter placeholders: the sentinel is ASCII word characters only.
|
||||
let indent_width = f.options().indent_width;
|
||||
let ir = super::map_text_in_ir(&ir, f, &mut |text, out| {
|
||||
let ir = map_text_in_ir(&ir, allocator, &mut |text, out| {
|
||||
let escaped = super::escape_template_chars(text, allocator);
|
||||
let text = escaped.unwrap_or(text);
|
||||
if text.contains(PLACEHOLDER_PREFIX) {
|
||||
@@ -166,7 +166,7 @@ pub(super) fn format_html_doc<'a>(
|
||||
if parts.len() > 1 {
|
||||
for (index, part) in parts.iter().enumerate() {
|
||||
if index.is_multiple_of(2) {
|
||||
push_text_with_line_breaks(out, part, indent_width);
|
||||
push_text_with_literal_lines(out, part, indent_width);
|
||||
} else {
|
||||
let expression = part
|
||||
.parse::<usize>()
|
||||
@@ -302,32 +302,6 @@ fn placeholders_are_sequential(ir: &[FormatElement<'_>], next: &mut usize) -> bo
|
||||
true
|
||||
}
|
||||
|
||||
/// Emit text with newlines converted to literal line breaks (`replaceEndOfLine()` equivalent).
|
||||
///
|
||||
/// Uses [`LineMode::Literal`] instead of a hard line break to avoid adding indentation:
|
||||
/// the returned HTML Doc already carries its indentation in the text content,
|
||||
/// so the surrounding `block_indent` must not add more.
|
||||
fn push_text_with_line_breaks<'a>(
|
||||
out: &mut ArenaVec<'a, FormatElement<'a>>,
|
||||
text: &'a str,
|
||||
indent_width: IndentWidth,
|
||||
) {
|
||||
let mut first = true;
|
||||
// Splitting on `\n` is safe because `Doc` only contains normalized linebreaks
|
||||
for line in text.split('\n') {
|
||||
if !first {
|
||||
out.push(FormatElement::Line(LineMode::Literal));
|
||||
}
|
||||
first = false;
|
||||
if !line.is_empty() {
|
||||
out.push(FormatElement::Text {
|
||||
text: line,
|
||||
width: TextWidth::from_text(line, indent_width),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use oxc_allocator::{Allocator, ArenaVec};
|
||||
|
||||
@@ -2,9 +2,9 @@ use oxc_allocator::{Allocator, ArenaStringBuilder};
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_formatter_core::dispatch_fragment_ir;
|
||||
|
||||
use crate::{ast_nodes::AstNode, format_args, formatter::prelude::*, write};
|
||||
use crate::{MarkdownInJsTemplate, ast_nodes::AstNode, format_args, formatter::prelude::*, write};
|
||||
|
||||
/// Format a Markdown-in-JS tagged template literal via the Doc→IR path.
|
||||
/// Format a Markdown-in-JS tagged template literal through the dispatcher.
|
||||
///
|
||||
/// Unescapes backticks in `.raw`, strips common indentation, formats as markdown,
|
||||
/// then re-escapes backticks and applies indented or dedent-to-root layout.
|
||||
@@ -31,7 +31,7 @@ pub(super) fn try_embed_markdown<'a>(
|
||||
let text = if has_indent { strip_indentation(text, indentation, allocator) } else { text };
|
||||
|
||||
// Phase 3: Get the IR from the dispatcher
|
||||
let Some(ir) = dispatch_fragment_ir(f, "markdown", text, None) else {
|
||||
let Some(ir) = dispatch_fragment_ir(f, "markdown", text, Some(&MarkdownInJsTemplate)) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
@@ -3,14 +3,9 @@ mod graphql;
|
||||
mod html;
|
||||
mod markdown;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use oxc_allocator::{Allocator, ArenaStringBuilder, ArenaVec};
|
||||
use oxc_ast::ast::*;
|
||||
use oxc_formatter_core::{
|
||||
FormatElement,
|
||||
format_element::{BestFittingElement, Interned, TextWidth},
|
||||
};
|
||||
use oxc_formatter_core::{FormatElement, format_element::TextWidth, map_text_in_ir};
|
||||
use oxc_span::GetSpan;
|
||||
|
||||
use crate::{
|
||||
@@ -378,7 +373,7 @@ fn split_on_placeholders<'a>(text: &'a str, prefix: &str, suffix: &str) -> Vec<&
|
||||
/// so these characters need escaping.
|
||||
fn escape_template_chars_in_ir<'a>(
|
||||
ir: &[FormatElement<'a>],
|
||||
f: &mut JsFormatter<'_, 'a>,
|
||||
f: &JsFormatter<'_, 'a>,
|
||||
) -> ArenaVec<'a, FormatElement<'a>> {
|
||||
escape_text_in_ir(ir, f, escape_template_chars)
|
||||
}
|
||||
@@ -387,92 +382,25 @@ fn escape_template_chars_in_ir<'a>(
|
||||
/// Used by markdown-in-JS, which uses `.raw` quasi values.
|
||||
fn escape_backticks_raw_in_ir<'a>(
|
||||
ir: &[FormatElement<'a>],
|
||||
f: &mut JsFormatter<'_, 'a>,
|
||||
f: &JsFormatter<'_, 'a>,
|
||||
) -> ArenaVec<'a, FormatElement<'a>> {
|
||||
escape_text_in_ir(ir, f, escape_backticks_raw)
|
||||
}
|
||||
|
||||
fn escape_text_in_ir<'a>(
|
||||
ir: &[FormatElement<'a>],
|
||||
f: &mut JsFormatter<'_, 'a>,
|
||||
f: &JsFormatter<'_, 'a>,
|
||||
escape: fn(&'a str, &'a Allocator) -> Option<&'a str>,
|
||||
) -> ArenaVec<'a, FormatElement<'a>> {
|
||||
let allocator = f.allocator();
|
||||
let indent_width = f.options().indent_width;
|
||||
map_text_in_ir(ir, f, &mut |text, out| {
|
||||
map_text_in_ir(ir, allocator, &mut |text, out| {
|
||||
let Some(text) = escape(text, allocator) else { return false };
|
||||
out.push(FormatElement::Text { text, width: TextWidth::from_text(text, indent_width) });
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
/// Rebuild an embedded IR, descending into BestFitting variants and interned content.
|
||||
///
|
||||
/// `map_text` receives each `Text` run;
|
||||
/// it either pushes replacement elements into the output and returns `true`, or returns `false` to keep the element unchanged.
|
||||
/// A shared `Interned` subtree is rebuilt once and the rebuilt element re-shared.
|
||||
#[expect(clippy::mutable_key_type)] // `Interned` hashes by pointer identity
|
||||
fn map_text_in_ir<'a, F>(
|
||||
ir: &[FormatElement<'a>],
|
||||
f: &mut JsFormatter<'_, 'a>,
|
||||
map_text: &mut F,
|
||||
) -> ArenaVec<'a, FormatElement<'a>>
|
||||
where
|
||||
F: FnMut(&'a str, &mut ArenaVec<'a, FormatElement<'a>>) -> bool,
|
||||
{
|
||||
let mut interned_cache = FxHashMap::default();
|
||||
map_text_in_ir_impl(ir, f, map_text, &mut interned_cache)
|
||||
}
|
||||
|
||||
#[expect(clippy::mutable_key_type)] // `Interned` hashes by pointer identity
|
||||
fn map_text_in_ir_impl<'a, F>(
|
||||
ir: &[FormatElement<'a>],
|
||||
f: &mut JsFormatter<'_, 'a>,
|
||||
map_text: &mut F,
|
||||
interned_cache: &mut FxHashMap<Interned<'a>, FormatElement<'a>>,
|
||||
) -> ArenaVec<'a, FormatElement<'a>>
|
||||
where
|
||||
F: FnMut(&'a str, &mut ArenaVec<'a, FormatElement<'a>>) -> bool,
|
||||
{
|
||||
let allocator = f.allocator();
|
||||
let mut out = ArenaVec::with_capacity_in(ir.len(), &allocator);
|
||||
for element in ir {
|
||||
match element {
|
||||
FormatElement::Text { text, .. } => {
|
||||
if !map_text(text, &mut out) {
|
||||
out.push(element.clone());
|
||||
}
|
||||
}
|
||||
FormatElement::BestFitting(best_fitting) => {
|
||||
let mut variants =
|
||||
ArenaVec::with_capacity_in(best_fitting.variants().len(), &allocator);
|
||||
for variant in best_fitting.variants() {
|
||||
let mapped = map_text_in_ir_impl(variant, f, map_text, interned_cache);
|
||||
variants.push(mapped.into_arena_slice());
|
||||
}
|
||||
// SAFETY: This rebuild preserves the original BestFitting's variant count.
|
||||
out.push(FormatElement::BestFitting(unsafe {
|
||||
BestFittingElement::from_vec_unchecked(variants)
|
||||
}));
|
||||
}
|
||||
FormatElement::Interned(interned) => {
|
||||
if let Some(mapped) = interned_cache.get(interned) {
|
||||
out.push(mapped.clone());
|
||||
continue;
|
||||
}
|
||||
let mapped = map_text_in_ir_impl(interned, f, map_text, interned_cache);
|
||||
let mapped = f
|
||||
.intern(&format_once(move |f| f.write_elements(mapped)))
|
||||
.expect("rebuilding a non-empty Interned element must remain non-empty");
|
||||
interned_cache.insert(interned.clone(), mapped.clone());
|
||||
out.push(mapped);
|
||||
}
|
||||
_ => out.push(element.clone()),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Escape characters that would break template literal syntax.
|
||||
///
|
||||
/// Equivalent to Prettier's `uncookTemplateElementValue`:
|
||||
|
||||
@@ -84,7 +84,10 @@ The core is parameterized over a consumer-supplied context so it stays language-
|
||||
- A child adds classes straight into the parent's index space, so its doc is written as-is (no remap)
|
||||
- A root formatter opens a fresh scope (`with_new_tailwind_scope`) and sorts it when finalizing its `Document` (`take_sorted_tailwind_classes`)
|
||||
- A language crate's `format_to_ir` entry returns `EmbeddedIr`, one shape for every child language, no per-crate tuples
|
||||
- Only truly language-pair specific data crosses as `dyn Any` (e.g. HTML's `has_multiple_root_elements`), core never learns concrete languages
|
||||
- Only language-pair specific data crosses as `dyn Any`, core never learns concrete languages:
|
||||
- When the parent picks the child language, it passes the pair's own data (css-in-js's `CssInJsTemplate`, html-in-js's `has_multiple_root_elements`)
|
||||
- When it cannot (a Markdown code block's name resolves in the host), it passes a fact about itself to every child,
|
||||
and the host translates it into the pair's data once the child language is known (`XxxInMarkdownCodeBlock` → `JsEmbeddedIn`)
|
||||
- `FormatSession` (`session/mod.rs`) is the execution unit:
|
||||
- One arena, one shared `GroupId` space (`Arc<UniqueGroupIdBuilder>`), one Tailwind class scope (`Rc<RefCell<Vec<String>>>`), the host's `SessionServices`, and the input's envelope semantics (`InputKind`), usable by standalone roots and dispatched children alike
|
||||
- `SessionServices` names the three per-run duties, one field each: `dispatcher` (IR channel), `string_embedder` (string-out channel, `(language, code, print_width)`; temporary, see domain (4)'s exit criterion), `tailwind_sorter` (print-time batch sort). Core only transports them
|
||||
@@ -119,7 +122,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);
|
||||
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) md/html/angular gain IR-capable formatters 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 (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.
|
||||
|
||||
- (5) `envelope/`: IR-composing behavior shared by document-envelope hosts (`write_front_matter`)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ pub use printer::{PrintResult, PrintWidth, Printed, PrinterOptions};
|
||||
pub(crate) use printer::Printer;
|
||||
pub use session::embedded::{
|
||||
DispatchPayload, DispatchRequest, DispatchResponse, EmbeddedIr, FormatDispatcher,
|
||||
dispatch_fragment_ir,
|
||||
dispatch_fragment_ir, dispatch_ir, map_text_in_ir, push_text_with_literal_lines,
|
||||
};
|
||||
pub use session::{FormatSession, InputKind, SessionServices, StringEmbedder, TailwindSorter};
|
||||
pub use source::{SourceText, SpanCursor};
|
||||
|
||||
@@ -13,13 +13,19 @@
|
||||
|
||||
use std::{any::Any, sync::Arc};
|
||||
|
||||
use oxc_allocator::ArenaVec;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::{FormatContext, FormatElement, FormatSession, Formatter, InputKind};
|
||||
use oxc_allocator::{Allocator, ArenaVec};
|
||||
|
||||
use crate::{
|
||||
FormatContext, FormatElement, FormatSession, Formatter, IndentWidth, InputKind, LineMode,
|
||||
format_element::{BestFittingElement, Interned, TextWidth},
|
||||
write::formatter::intern_exact,
|
||||
};
|
||||
|
||||
/// One embedded-language formatting request, as the host formatter states it.
|
||||
pub struct DispatchRequest<'r> {
|
||||
/// Generic language identifier (e.g. `"css"`, `"graphql"`);
|
||||
/// Generic language identifier (e.g. `"css"`, `"graphql"`), or a code fence's name as written;
|
||||
/// the dispatcher implementation maps it to its own parser/language names.
|
||||
pub language: &'r str,
|
||||
/// The code to format.
|
||||
@@ -33,6 +39,13 @@ pub struct DispatchRequest<'r> {
|
||||
pub parent_context: Option<&'r dyn Any>,
|
||||
}
|
||||
|
||||
impl DispatchRequest<'_> {
|
||||
/// [`Self::parent_context`] as a `T`, when the parent passed one.
|
||||
pub fn parent_context_as<T: Any>(&self) -> Option<&T> {
|
||||
self.parent_context.and_then(|c| c.downcast_ref::<T>())
|
||||
}
|
||||
}
|
||||
|
||||
/// The dispatcher's answer to a [`DispatchRequest`].
|
||||
///
|
||||
/// [`Self::PreserveOriginal`] is the DELIBERATE "do not format" answer
|
||||
@@ -94,24 +107,126 @@ pub struct DispatchPayload<'a> {
|
||||
pub child_context: Option<Box<dyn Any>>,
|
||||
}
|
||||
|
||||
/// Dispatches one embedded fragment (`InputKind::Fragment`) and hands out its doc.
|
||||
///
|
||||
/// `None` covers [`DispatchResponse::PreserveOriginal`] and operational errors alike;
|
||||
/// the caller keeps its original source.
|
||||
/// Embed sites that must inspect [`DispatchPayload::child_context`] first stay manual.
|
||||
/// Dispatches one embedded fragment and hands out its doc:
|
||||
/// [`dispatch_ir`] with `InputKind::Fragment`.
|
||||
pub fn dispatch_fragment_ir<'a, C: FormatContext>(
|
||||
f: &Formatter<'_, 'a, C>,
|
||||
language: &str,
|
||||
text: &str,
|
||||
parent_context: Option<&dyn Any>,
|
||||
) -> Option<ArenaVec<'a, FormatElement<'a>>> {
|
||||
let Ok(DispatchResponse::Formatted(result)) = f.session().dispatch(DispatchRequest {
|
||||
language,
|
||||
text,
|
||||
input_kind: InputKind::Fragment,
|
||||
parent_context,
|
||||
}) else {
|
||||
dispatch_ir(
|
||||
f,
|
||||
DispatchRequest { language, text, input_kind: InputKind::Fragment, parent_context },
|
||||
)
|
||||
}
|
||||
|
||||
/// Dispatches one embedded request and hands out its doc.
|
||||
///
|
||||
/// `None` covers [`DispatchResponse::PreserveOriginal`] and operational errors alike;
|
||||
/// the caller keeps its original source.
|
||||
/// Embed sites that must inspect [`DispatchPayload::child_context`] first stay manual.
|
||||
pub fn dispatch_ir<'a, C: FormatContext>(
|
||||
f: &Formatter<'_, 'a, C>,
|
||||
request: DispatchRequest<'_>,
|
||||
) -> Option<ArenaVec<'a, FormatElement<'a>>> {
|
||||
let Ok(DispatchResponse::Formatted(result)) = f.session().dispatch(request) else {
|
||||
return None;
|
||||
};
|
||||
Some(result.doc)
|
||||
}
|
||||
|
||||
/// Rebuild an embedded IR, descending into BestFitting variants and interned content.
|
||||
///
|
||||
/// `map_text` receives each `Text` and `Token` run (a child may print any character as a token, e.g. JS's `` ` ``);
|
||||
/// it either pushes replacement elements into the output and returns `true`, or returns `false` to keep the element unchanged.
|
||||
/// A shared `Interned` subtree is rebuilt once and the rebuilt element re-shared.
|
||||
#[expect(clippy::mutable_key_type)] // `Interned` hashes by pointer identity
|
||||
pub fn map_text_in_ir<'a, F>(
|
||||
ir: &[FormatElement<'a>],
|
||||
allocator: &'a Allocator,
|
||||
map_text: &mut F,
|
||||
) -> ArenaVec<'a, FormatElement<'a>>
|
||||
where
|
||||
F: FnMut(&'a str, &mut ArenaVec<'a, FormatElement<'a>>) -> bool,
|
||||
{
|
||||
let mut interned_cache = FxHashMap::default();
|
||||
map_text_in_ir_impl(ir, allocator, map_text, &mut interned_cache)
|
||||
}
|
||||
|
||||
#[expect(clippy::mutable_key_type)] // `Interned` hashes by pointer identity
|
||||
fn map_text_in_ir_impl<'a, F>(
|
||||
ir: &[FormatElement<'a>],
|
||||
allocator: &'a Allocator,
|
||||
map_text: &mut F,
|
||||
interned_cache: &mut FxHashMap<Interned<'a>, Option<FormatElement<'a>>>,
|
||||
) -> ArenaVec<'a, FormatElement<'a>>
|
||||
where
|
||||
F: FnMut(&'a str, &mut ArenaVec<'a, FormatElement<'a>>) -> bool,
|
||||
{
|
||||
let mut out = ArenaVec::with_capacity_in(ir.len(), &allocator);
|
||||
for element in ir {
|
||||
match element {
|
||||
FormatElement::Text { text, .. } | FormatElement::Token { text } => {
|
||||
if !map_text(text, &mut out) {
|
||||
out.push(element.clone());
|
||||
}
|
||||
}
|
||||
FormatElement::BestFitting(best_fitting) => {
|
||||
let mut variants =
|
||||
ArenaVec::with_capacity_in(best_fitting.variants().len(), &allocator);
|
||||
for variant in best_fitting.variants() {
|
||||
let mapped = map_text_in_ir_impl(variant, allocator, map_text, interned_cache);
|
||||
variants.push(mapped.into_arena_slice());
|
||||
}
|
||||
// SAFETY: This rebuild preserves the original BestFitting's variant count.
|
||||
out.push(FormatElement::BestFitting(unsafe {
|
||||
BestFittingElement::from_vec_unchecked(variants)
|
||||
}));
|
||||
}
|
||||
FormatElement::Interned(interned) => {
|
||||
let mapped = if let Some(mapped) = interned_cache.get(interned) {
|
||||
mapped.clone()
|
||||
} else {
|
||||
let mapped = map_text_in_ir_impl(interned, allocator, map_text, interned_cache);
|
||||
let mapped = intern_exact(allocator, mapped.into_iter());
|
||||
interned_cache.insert(interned.clone(), mapped.clone());
|
||||
mapped
|
||||
};
|
||||
out.extend(mapped);
|
||||
}
|
||||
_ => out.push(element.clone()),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Pushes `text` with each newline as a literal line (Prettier's `replaceEndOfLine()`).
|
||||
///
|
||||
/// For a [`map_text_in_ir`] callback:
|
||||
/// a literal line adds no indentation of its own, it resumes at the enclosing root (`mark_as_root`).
|
||||
///
|
||||
/// TODO: An embedding boundary rewrites the child IR with this, because a newline inside a `Text` has two readings:
|
||||
/// - the printer prints it as is (column 0), as Prettier prints a string
|
||||
/// - the IR to Prettier Doc conversion (oxfmt's `to_prettier_doc`) always makes it a `literalline`
|
||||
///
|
||||
/// A boundary scope tag (a `mark_as_root` variant whose `Text` newlines resume at the root) would replace the rewrite.
|
||||
/// Revisit once the Vue and HTML ports remove the Doc conversion path, which leaves the printer as the only reader.
|
||||
pub fn push_text_with_literal_lines<'a>(
|
||||
out: &mut ArenaVec<'a, FormatElement<'a>>,
|
||||
text: &'a str,
|
||||
indent_width: IndentWidth,
|
||||
) {
|
||||
// Splitting on `\n` is safe because the IR only contains normalized linebreaks
|
||||
for (i, line) in text.split('\n').enumerate() {
|
||||
if i > 0 {
|
||||
out.push(FormatElement::Line(LineMode::Literal));
|
||||
}
|
||||
if !line.is_empty() {
|
||||
out.push(FormatElement::Text {
|
||||
text: line,
|
||||
width: TextWidth::from_text(line, indent_width),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ use crate::{
|
||||
|
||||
/// Interns an exactly-sized element sequence, the single place encoding the interning policy:
|
||||
/// the 0/1-element cases return without ever allocating an `ArenaVec`.
|
||||
fn intern_exact<'ast>(
|
||||
pub fn intern_exact<'ast>(
|
||||
allocator: &'ast Allocator,
|
||||
mut elements: impl ExactSizeIterator<Item = FormatElement<'ast>>,
|
||||
) -> Option<FormatElement<'ast>> {
|
||||
|
||||
@@ -277,6 +277,7 @@ Prettier keeps SELECTOR interpolation verbatim.
|
||||
|
||||
- Why: uniform-rule (option governs: singleQuote)
|
||||
- Pin: `tests/fixtures/format/scss/unknown-at-rule-edges.scss`, `tests/fixtures/format/css/prelude-string-requote.css`
|
||||
- Oxfmt: `externals/mdn-css-guides/values_and_units/textual_data_types/index.md`
|
||||
|
||||
```scss
|
||||
/* input */
|
||||
@@ -1480,6 +1481,7 @@ A raw prelude (unknown at-rule params) still takes the same `//` test as Prettie
|
||||
|
||||
- Why: uniform-rule (same construct, same output: the SCSS `if()` branch separator; prettier/prettier#19384)
|
||||
- Pin: `tests/fixtures/format/css/value-semicolon-glue.css`
|
||||
- Oxfmt: `externals/mdn-css-guides/custom_functions_and_mixins/using_custom_functions/index.md`
|
||||
|
||||
```css
|
||||
/* input */
|
||||
|
||||
@@ -40,6 +40,12 @@ a leading BOM is preserved.
|
||||
Front matter (`---` / `+++`, `oxc_formatter_core::spec::parse_front_matter`) is blanked before parsing and printed by `envelope::write_front_matter` as CSS does:
|
||||
its yaml formats through the session's dispatcher when there is one, anything else stays verbatim; a blank line separates it from the body.
|
||||
|
||||
A fenced code block with a language dispatches its content as a `VirtualDocument` (`print/code.rs`), its name as written (decoded), the dispatcher resolving it:
|
||||
the child IR is wrapped in `mark_as_root` with every newline in its texts made a literal line, so continuation lines keep the fence's column,
|
||||
and the fence outnumbers the backtick runs of the printed content (printed once to count them when a text holds a backtick).
|
||||
No dispatcher, an unknown language or a failed parse keeps the block verbatim, as does a meta with line ranges (`apps/oxfmt/DIVERGENCES.md#line-ranged-code-block`).
|
||||
Embedding is verified end to end by oxfmt's conformance (`markdown` / `md-in-js` categories), not here.
|
||||
|
||||
## Dialects
|
||||
|
||||
Markdown grammars are open-ended (VitePress, Docusaurus, Pandoc, kramdown, Obsidian, ...), and the parser learns none of them:
|
||||
|
||||
@@ -23,6 +23,7 @@ oxc_allocator = { workspace = true }
|
||||
oxc_diagnostics = { workspace = true }
|
||||
oxc_formatter_core = { workspace = true }
|
||||
oxc_span = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true }
|
||||
|
||||
@@ -7,6 +7,7 @@ Admission reasons and rules: see `crates/oxc_formatter_core/FORMATTER_POLICY.md`
|
||||
- Why: semantics (prettier/prettier#19839)
|
||||
- Pin: `tests/fixtures/markdown/leading-thematic-break.md`
|
||||
- Conformance: `markdown/thematicBreak/simple.md`, `markdown/commonmark-test-suite/snippet: example-11.md`, `example-50.md` to `example-54.md`
|
||||
- Oxfmt: `externals/prettier/markdown/thematicBreak/simple.md`
|
||||
|
||||
```markdown
|
||||
<!-- input -->
|
||||
@@ -41,6 +42,7 @@ Prettier `main` prints `***` since #19839; the pin (3.9.9) still prints `---`.
|
||||
- Why: semantics (prettier/prettier#19482, prettier/prettier#19849, prettier/prettier#19891)
|
||||
- Pin: `tests/fixtures/markdown/url-escaping.md`
|
||||
- Conformance: `markdown/link/encodedLink.md`
|
||||
- Oxfmt: `externals/prettier/markdown/link/encodedLink.md`
|
||||
|
||||
```markdown
|
||||
<!-- input -->
|
||||
@@ -85,6 +87,7 @@ so an indent written as spaces plus a tab becomes a fenced block.
|
||||
|
||||
- Why: uniform-rule (same construct, same output: a blockquote's trailing blank `>` line under `proseWrap: preserve`)
|
||||
- Pin: `tests/fixtures/markdown/prose-wrap/ignored-block-trailing-quote-line.md`
|
||||
- Oxfmt: `externals/prettier/markdown/blockquote/ignore-code.md`
|
||||
|
||||
```markdown
|
||||
<!-- input -->
|
||||
@@ -275,6 +278,7 @@ Prettier alternates from the marker it would have printed (`-`), prints `*` next
|
||||
- Why: semantics
|
||||
- Pin: `tests/fixtures/markdown/prose-wrap/container-directive.md`
|
||||
- Conformance: `markdown/paragraph/cjk.md`
|
||||
- Oxfmt: `externals/prettier/markdown/paragraph/cjk.md`
|
||||
|
||||
```markdown
|
||||
<!-- input -->
|
||||
@@ -309,6 +313,7 @@ so the closing fence is printed explicitly: a reader without directives (CommonM
|
||||
- Why: semantics
|
||||
- Pin: `tests/fixtures/markdown/prose-wrap/line-shapes.md`
|
||||
- Conformance: `markdown/blockquote/notext-end.md`
|
||||
- Oxfmt: `externals/prettier/markdown/blockquote/notext-end.md`
|
||||
|
||||
```markdown
|
||||
<!-- input -->
|
||||
@@ -354,6 +359,7 @@ Prettier wraps it like any line: a break right after the destination makes it a
|
||||
|
||||
- Why: semantics (prettier/prettier#13634, prettier/prettier#19112, prettier/prettier#19847)
|
||||
- Pin: `tests/fixtures/markdown/prose-wrap/wrapped-block-starts.md`
|
||||
- Oxfmt: `externals/prettier/markdown/list/parser-regression/issue-17778.md`
|
||||
|
||||
```markdown
|
||||
<!-- input, proseWrap always -->
|
||||
@@ -374,14 +380,15 @@ The parser's `lexical::line_start` decides, asked about the line the wrap would
|
||||
Prettier tests a regex of list markers, `#`s and `>` only, so a wrapped `<div>`, `<!--`, `***`, `---`, fence,
|
||||
`|` row or `[^x]:` opens a block on the next parse.
|
||||
|
||||
A kept line break (`proseWrap: preserve`) is dropped by the same rule when its next line only stays text
|
||||
by its indentation, which a paragraph does not keep: `| x | y |` over an 8-space `|---|---|` prints as one line,
|
||||
A kept line break (`proseWrap: preserve`) is dropped by the same rule when its next line only stays text by its indentation,
|
||||
which a paragraph does not keep: `| x | y |` over an 8-space `|---|---|` prints as one line,
|
||||
Prettier's `|---|---|` at column 0 is a table on the next parse (prettier/prettier#19847).
|
||||
A delimiter row only counts under a header with as many cells (`lexical::table_delimiter_activates`, asked about the line above):
|
||||
under another one it opens nothing and the kept break before it stays.
|
||||
A break that cannot be dropped (a hard break, or a line shape on either side of it) keeps the line as text behind four spaces instead:
|
||||
indented code cannot interrupt a paragraph (the pin's math span after a hard break).
|
||||
Prettier drops the break before `- item`, `# heading` and `> quote` itself.
|
||||
Prettier drops the break before `- item`, `# heading` and `> quote` itself, and before `1000000000.` too,
|
||||
which its regex takes for a list marker (a marker has at most 9 digits).
|
||||
|
||||
## stray-delimiters
|
||||
|
||||
@@ -546,6 +553,7 @@ Ours keeps the line.
|
||||
|
||||
- Why: uniform-rule (same construct, same output: a paragraph)
|
||||
- Pin: `tests/fixtures/markdown/prose-wrap/setext-heading-wrap.md`
|
||||
- Oxfmt: `externals/prettier/markdown/heading/setext/issue-6013-2.md`
|
||||
|
||||
```markdown
|
||||
<!-- input, proseWrap always -->
|
||||
@@ -587,3 +595,29 @@ A NUL stays as written, in code spans and code blocks too.
|
||||
CommonMark replaces it with U+FFFD for security when rendering;
|
||||
Prettier prints the decoded value of code, so the replacement lands in the source, while plain text keeps its NUL (a raw slice).
|
||||
Ours prints every node as written, the next parse replaces it again.
|
||||
|
||||
## fence-info-backtick
|
||||
|
||||
- Why: semantics
|
||||
- Pin: `tests/fixtures/markdown/fence-info-backtick.md`
|
||||
|
||||
````markdown
|
||||
<!-- input -->
|
||||
~~~js `x`
|
||||
a
|
||||
~~~
|
||||
|
||||
<!-- ours -->
|
||||
~~~js `x`
|
||||
a
|
||||
~~~
|
||||
|
||||
<!-- prettier -->
|
||||
```js `x`
|
||||
a
|
||||
```
|
||||
````
|
||||
|
||||
A fenced code block whose info string has a backtick keeps its `~~~` fence; any other fence is printed with backticks.
|
||||
A backtick fence cannot have a backtick in its info string (CommonMark),
|
||||
so Prettier's opener is a paragraph on the next parse and its closer opens a code block that runs to the end of the document.
|
||||
@@ -38,6 +38,8 @@ pub struct MarkdownFormatContext<'a> {
|
||||
opaque_spans: RefCell<Vec<(Span, bool)>>,
|
||||
/// Emphasis markers are printed as written: normalizing them would change what pairs.
|
||||
literal_markers: Cell<bool>,
|
||||
/// Embedded in a JS template literal: code fences use `~`.
|
||||
in_js_template: bool,
|
||||
}
|
||||
|
||||
/// Whether text is printed as written, newlines included.
|
||||
@@ -61,7 +63,12 @@ pub struct ListFrame {
|
||||
}
|
||||
|
||||
impl<'a> MarkdownFormatContext<'a> {
|
||||
pub fn new(options: MarkdownFormatOptions, source_code: &'a str, blanks: &'a [Span]) -> Self {
|
||||
pub fn new(
|
||||
options: MarkdownFormatOptions,
|
||||
source_code: &'a str,
|
||||
blanks: &'a [Span],
|
||||
in_js_template: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
options,
|
||||
source_text: SourceText::new(source_code),
|
||||
@@ -74,6 +81,7 @@ impl<'a> MarkdownFormatContext<'a> {
|
||||
code_span_literal_runs: RefCell::new(Vec::new()),
|
||||
opaque_spans: RefCell::new(Vec::new()),
|
||||
literal_markers: Cell::new(false),
|
||||
in_js_template,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +139,10 @@ impl<'a> MarkdownFormatContext<'a> {
|
||||
pub fn literal_markers(&self) -> &Cell<bool> {
|
||||
&self.literal_markers
|
||||
}
|
||||
|
||||
pub fn in_js_template(&self) -> bool {
|
||||
self.in_js_template
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatContext for MarkdownFormatContext<'_> {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use oxc_allocator::Allocator;
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_formatter_core::{
|
||||
Buffer, Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, VecBuffer,
|
||||
Buffer, Document, EmbeddedIr, Format, FormatSession, FormatState, Formatted, InputKind,
|
||||
VecBuffer,
|
||||
builders::{empty_line, hard_line_break, text},
|
||||
spec::{FrontMatter, blank_front_matter, parse_front_matter},
|
||||
write, write_front_matter,
|
||||
@@ -25,10 +26,39 @@ pub fn format<'a>(
|
||||
source_text: &str,
|
||||
options: MarkdownFormatOptions,
|
||||
) -> Result<Formatted<'a, MarkdownFormatContext<'a>>, OxcDiagnostic> {
|
||||
let parsed = parse_for_format(allocator, source_text)?;
|
||||
// A service-less session: front matter and fenced code stay as-is.
|
||||
// Hosts that install services (oxfmt) use [`format_with_session`].
|
||||
format_with_session(
|
||||
&FormatSession::new(allocator, InputKind::PhysicalFile),
|
||||
source_text,
|
||||
options,
|
||||
)
|
||||
}
|
||||
|
||||
let context = MarkdownFormatContext::new(options, parsed.source, parsed.blanks);
|
||||
let mut state = FormatState::new(context, allocator);
|
||||
/// Like [`format()`], but on a caller-supplied [`FormatSession`]:
|
||||
/// the root whose session carries the host's `SessionServices`
|
||||
/// (front matter and fenced code dispatch, Tailwind sorting).
|
||||
///
|
||||
/// # Errors
|
||||
/// Same as [`format()`].
|
||||
pub fn format_with_session<'a>(
|
||||
session: &FormatSession<'a>,
|
||||
source_text: &str,
|
||||
options: MarkdownFormatOptions,
|
||||
) -> Result<Formatted<'a, MarkdownFormatContext<'a>>, OxcDiagnostic> {
|
||||
// The envelope matrix has one decision input, the session's `InputKind`:
|
||||
// this entry is the physical-root half (owns BOM + front matter);
|
||||
// every embedded kind goes through `format_to_ir`.
|
||||
debug_assert!(
|
||||
session.input_kind() == InputKind::PhysicalFile,
|
||||
"format_with_session is the physical-root entry; embedded inputs go through format_to_ir"
|
||||
);
|
||||
let parsed = parse_for_format(session.allocator(), source_text)?;
|
||||
|
||||
let context = MarkdownFormatContext::new(options, parsed.source, parsed.blanks, false);
|
||||
// A root `Document` owns a fresh Tailwind class scope (a fenced code block's classes join it)
|
||||
let session = session.with_new_tailwind_scope();
|
||||
let mut state = FormatState::new_with_session(context, session.clone());
|
||||
// TODO: Pre-allocate
|
||||
let mut buffer = VecBuffer::new(&mut state);
|
||||
|
||||
@@ -37,7 +67,7 @@ pub fn format<'a>(
|
||||
let elements = buffer.into_vec();
|
||||
let context = state.into_context();
|
||||
|
||||
let ir = Document::new(elements, Vec::new());
|
||||
let ir = Document::new(elements, session.take_sorted_tailwind_classes());
|
||||
|
||||
Ok(Formatted::new(ir, context))
|
||||
}
|
||||
@@ -78,12 +108,15 @@ pub fn parse_for_format<'a>(
|
||||
/// - allocates from the session's shared arena and `GroupId` space, so the IR lives as long as the parent's document
|
||||
/// - emits neither a BOM nor the trailing newline
|
||||
///
|
||||
/// `in_js_template` (markdown-in-js): code fences use `~` instead of backticks.
|
||||
///
|
||||
/// # Errors
|
||||
/// Same as [`format()`].
|
||||
pub fn format_to_ir<'a>(
|
||||
session: &FormatSession<'a>,
|
||||
source_text: &str,
|
||||
options: MarkdownFormatOptions,
|
||||
in_js_template: bool,
|
||||
) -> Result<EmbeddedIr<'a>, OxcDiagnostic> {
|
||||
let allocator = session.allocator();
|
||||
// `FormatSession::dispatch` never hands a BOM-headed input to an embedded part
|
||||
@@ -96,7 +129,7 @@ pub fn format_to_ir<'a>(
|
||||
));
|
||||
}
|
||||
|
||||
let context = MarkdownFormatContext::new(options, parsed.source, parsed.blanks);
|
||||
let context = MarkdownFormatContext::new(options, parsed.source, parsed.blanks, in_js_template);
|
||||
let mut state = FormatState::new_with_session(context, session.clone());
|
||||
let mut buffer = VecBuffer::new(&mut state);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ mod print;
|
||||
|
||||
pub use crate::{
|
||||
context::MarkdownFormatContext,
|
||||
format::{ParsedMarkdown, format, format_to_ir, parse_for_format},
|
||||
format::{ParsedMarkdown, format, format_to_ir, format_with_session, parse_for_format},
|
||||
options::{MarkdownFormatOptions, ProseWrap, SingleQuote},
|
||||
print::XxxInMarkdownCodeBlock,
|
||||
};
|
||||
@@ -1,12 +1,17 @@
|
||||
//! Code blocks: fences are recomputed (the shortest backtick run the content permits, at least 3),
|
||||
//! Code blocks: fences are recomputed (the shortest backtick or tilde run the content permits, at least 3),
|
||||
//! indented blocks stay indented.
|
||||
|
||||
use cow_utils::CowUtils;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use oxc_allocator::ArenaVec;
|
||||
use oxc_formatter_core::{
|
||||
Buffer, arena_cow_str,
|
||||
builders::{exact_line_breaks, hard_line_break, space_align, text, token},
|
||||
write,
|
||||
Buffer, BufferExtensions, DispatchRequest, Document, FormatElement, FormatOptions, InputKind,
|
||||
Interned, PrintWidth, arena_cow_str,
|
||||
builders::{
|
||||
FormatOnce, exact_line_breaks, hard_line_break, mark_as_root, space_align, text, token,
|
||||
},
|
||||
dispatch_ir, map_text_in_ir, push_text_with_literal_lines, write,
|
||||
};
|
||||
use oxc_markdown_parser::{
|
||||
ast::{CodeBlock, CodeBlockKind},
|
||||
@@ -14,7 +19,7 @@ use oxc_markdown_parser::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
MarkdownFormatter, backticks, format_with, join_pieces, max_run, write_indented_lines,
|
||||
MarkdownFormatter, fence_run, format_with, join_pieces, max_run, write_indented_lines,
|
||||
};
|
||||
|
||||
pub fn write_code_block<'a>(code: &'a CodeBlock<'a>, f: &mut MarkdownFormatter<'_, 'a>) {
|
||||
@@ -36,30 +41,160 @@ pub fn write_code_block<'a>(code: &'a CodeBlock<'a>, f: &mut MarkdownFormatter<'
|
||||
CodeBlockKind::Fenced { lang, meta, .. } => {
|
||||
let (lang, meta) =
|
||||
(lang.map(|s| f.context().slice(s)), meta.map(|s| f.context().slice(s)));
|
||||
// `~~~` fences normalize to backticks, unless the info string has one (only `~~~` allows it)
|
||||
// Backtick fences; tildes inside a JS template literal
|
||||
// or when the info string has a backtick (only a `~~~` fence allows one)
|
||||
let info_has_backtick = [lang, meta].iter().flatten().any(|part| part.contains('`'));
|
||||
let fence: &'a str = if info_has_backtick {
|
||||
"~~~"
|
||||
} else {
|
||||
arena_cow_str(&backticks((max_run(value, b'`') + 1).max(3)), f)
|
||||
let fence_char =
|
||||
if info_has_backtick || f.context().in_js_template() { b'~' } else { b'`' };
|
||||
|
||||
let embedded = lang.and_then(|lang| format_embedded(lang, meta, value, fence_char, f));
|
||||
// Whitespace-only content of a formattable language prints an empty block:
|
||||
// the child prints nothing, and `hard_line_break` would drop the block's one empty line
|
||||
let (embedded, value) = match embedded {
|
||||
Some(_) if value.trim().is_empty() => (None, ""),
|
||||
embedded => (embedded, value),
|
||||
};
|
||||
write!(f, text(fence));
|
||||
// `lang` + one space + `meta`, decoded, the whitespace between them normalized
|
||||
for (i, part) in [lang, meta].into_iter().flatten().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, token(" "));
|
||||
let content_run = embedded.as_ref().map_or_else(|| max_run(value, fence_char), |e| e.1);
|
||||
let fence: &'a str = arena_cow_str(&fence_run(fence_char, (content_run + 1).max(3)), f);
|
||||
// Single-use, so the child IR moves into the buffer instead of being cloned
|
||||
let fenced = FormatOnce::new(|f: &mut MarkdownFormatter<'_, 'a>| {
|
||||
write!(f, text(fence));
|
||||
// `lang` + one space + `meta`, decoded, the whitespace between them normalized
|
||||
for (i, part) in [lang, meta].into_iter().flatten().enumerate() {
|
||||
if i > 0 {
|
||||
write!(f, token(" "));
|
||||
}
|
||||
write!(f, text(cooked_info(part, f)));
|
||||
}
|
||||
write!(f, text(cooked_info(part, f)));
|
||||
}
|
||||
write!(f, hard_line_break());
|
||||
write_indented_lines(value, f);
|
||||
// A trailing blank line in the content (or an empty block's one empty line) must survive:
|
||||
// `hard_line_break` would see an empty line and print nothing.
|
||||
write!(f, [exact_line_breaks(1), text(fence)]);
|
||||
write!(f, hard_line_break());
|
||||
if let Some((ir, _)) = embedded {
|
||||
f.write_elements(ir);
|
||||
// Not `exact_line_breaks`: a child may end its IR with a line break (a JS directive, a CSS trailing comment)
|
||||
write!(f, [hard_line_break(), text(fence)]);
|
||||
} else {
|
||||
write_indented_lines(value, f);
|
||||
// A trailing blank line in the content (or an empty block's one empty line) must survive:
|
||||
// `hard_line_break` would see an empty line and print nothing.
|
||||
write!(f, [exact_line_breaks(1), text(fence)]);
|
||||
}
|
||||
});
|
||||
// The child's literal lines (a template literal, a block scalar) stay relative to the fence,
|
||||
// not column 0 (inside a blockquote, that would lose the `> `).
|
||||
write!(f, mark_as_root(&fenced));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parent→child context for a fenced code block dispatched from Markdown:
|
||||
/// a fact about the parent, not a pair's data, since the host resolves the block's language.
|
||||
///
|
||||
/// Sent to every child language as `DispatchRequest::parent_context`;
|
||||
/// the host translates it into the pair's data once the child language is known
|
||||
/// (e.g. JS's `JsEmbeddedIn`: a lone JSX element prints without its semicolon).
|
||||
pub struct XxxInMarkdownCodeBlock {
|
||||
/// This host is embedded in a JS template literal,
|
||||
/// so a Markdown child prints `~` fences too.
|
||||
pub in_js_template: bool,
|
||||
}
|
||||
|
||||
/// Formats the content through the session's dispatcher;
|
||||
/// returns the IR and the longest `fence_char` run it prints (the fence must outnumber it).
|
||||
/// `None` keeps the block verbatim: no dispatcher, an unknown language, a failed parse,
|
||||
/// or a block whose line numbers carry meaning (`apps/oxfmt/DIVERGENCES.md#line-ranged-code-block`).
|
||||
fn format_embedded<'a>(
|
||||
lang: &str,
|
||||
meta: Option<&str>,
|
||||
value: &str,
|
||||
fence_char: u8,
|
||||
f: &MarkdownFormatter<'_, 'a>,
|
||||
) -> Option<(ArenaVec<'a, FormatElement<'a>>, usize)> {
|
||||
if !f.session().has_dispatcher() || meta.is_some_and(has_line_ranges) {
|
||||
return None;
|
||||
}
|
||||
let language = decode::decode(lang, true);
|
||||
let ir = dispatch_ir(
|
||||
f,
|
||||
DispatchRequest {
|
||||
language: &language,
|
||||
text: value,
|
||||
input_kind: InputKind::VirtualDocument,
|
||||
parent_context: Some(&XxxInMarkdownCodeBlock {
|
||||
in_js_template: f.context().in_js_template(),
|
||||
}),
|
||||
},
|
||||
)?;
|
||||
|
||||
let (has_newline, has_fence_char) = scan_texts(&ir, fence_char);
|
||||
// Newline inside a text (a template literal, a block comment) becomes a literal line,
|
||||
// which `mark_as_root` keeps at the fence's column
|
||||
let ir = if has_newline {
|
||||
let indent_width = f.options().indent_width();
|
||||
map_text_in_ir(&ir, f.allocator(), &mut |text, out| {
|
||||
text.contains('\n') && {
|
||||
push_text_with_literal_lines(out, text, indent_width);
|
||||
true
|
||||
}
|
||||
})
|
||||
} else {
|
||||
ir
|
||||
};
|
||||
if !has_fence_char {
|
||||
return Some((ir, 0));
|
||||
}
|
||||
|
||||
// A run may span texts, so count it on the printed content.
|
||||
// The child's `TailwindClass` elements index the session's scope; unsorted is enough to count.
|
||||
let classes = f.session().unsorted_tailwind_classes();
|
||||
let probe = Document::new(ArenaVec::from_iter_in(ir.iter().cloned(), &f.allocator()), classes);
|
||||
// Unlimited width, as Prettier's: a run is counted on the content, not on how it breaks
|
||||
let options = f.options().as_print_options().with_print_width(PrintWidth::new(u32::MAX));
|
||||
let printed = probe.print(value.len(), options).ok()?;
|
||||
let run = max_run(printed.as_code(), fence_char);
|
||||
Some((ir, run))
|
||||
}
|
||||
|
||||
/// Whether any text of the IR holds a newline / the fence character;
|
||||
/// a shared `Interned` subtree is walked once.
|
||||
#[expect(clippy::mutable_key_type)] // `Interned` hashes by pointer identity
|
||||
fn scan_texts(ir: &[FormatElement<'_>], fence_char: u8) -> (bool, bool) {
|
||||
fn walk<'a>(
|
||||
elements: &[FormatElement<'a>],
|
||||
fence_char: u8,
|
||||
seen: &mut FxHashSet<Interned<'a>>,
|
||||
found: &mut (bool, bool),
|
||||
) {
|
||||
for element in elements {
|
||||
match element {
|
||||
FormatElement::Text { text, .. } | FormatElement::Token { text } => {
|
||||
found.0 |= text.contains('\n');
|
||||
found.1 |= text.as_bytes().contains(&fence_char);
|
||||
}
|
||||
FormatElement::BestFitting(best_fitting) => {
|
||||
for variant in best_fitting.variants() {
|
||||
walk(variant, fence_char, seen, found);
|
||||
}
|
||||
}
|
||||
FormatElement::Interned(interned) if seen.insert(interned.clone()) => {
|
||||
walk(interned, fence_char, seen, found);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut found = (false, false);
|
||||
walk(ir, fence_char, &mut FxHashSet::default(), &mut found);
|
||||
found
|
||||
}
|
||||
|
||||
/// Whether the meta addresses lines by number (`{1,3-5}`, VitePress / Docusaurus line highlighting),
|
||||
/// which formatting the content would shift.
|
||||
fn has_line_ranges(meta: &str) -> bool {
|
||||
meta.split('{').skip(1).filter_map(|rest| rest.split_once('}')).any(|(ranges, _)| {
|
||||
ranges.bytes().any(|b| b.is_ascii_digit())
|
||||
&& ranges.bytes().all(|b| b.is_ascii_digit() || matches!(b, b',' | b'-' | b' '))
|
||||
})
|
||||
}
|
||||
|
||||
/// An info string part decoded; a backslash left by decoding is escaped again,
|
||||
/// or the next parse would apply it to what it protected (`\\[` printed as `\[` decodes to `[`).
|
||||
fn cooked_info<'a>(part: &'a str, f: &MarkdownFormatter<'_, 'a>) -> &'a str {
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
HTML_WHITESPACE, MarkdownFormatter, backticks, escape, is_split_whitespace, join_pieces,
|
||||
HTML_WHITESPACE, MarkdownFormatter, escape, fence_run, is_split_whitespace, join_pieces,
|
||||
line_shape::{
|
||||
is_line_shape_start, line_above, line_from, line_opens_block, line_or_prefix_opens_block,
|
||||
line_shape, printed_line_at, printed_line_opens_block, source_line_at,
|
||||
@@ -781,7 +781,7 @@ pub fn print_code_span<'a>(code: &'a CodeSpan<'a>, f: &MarkdownFormatter<'_, 'a>
|
||||
while len <= 64 && literal_runs & (1u64 << (len - 1)) != 0 {
|
||||
len += 1;
|
||||
}
|
||||
let fence = backticks(len);
|
||||
let fence = fence_run(b'`', len);
|
||||
let is_space_or_newline = |c: char| c == ' ' || c == '\n';
|
||||
let padding = value.starts_with('`')
|
||||
|| value.ends_with('`')
|
||||
|
||||
@@ -20,6 +20,7 @@ mod line_shape;
|
||||
mod link;
|
||||
mod list;
|
||||
mod pairing;
|
||||
pub use code::XxxInMarkdownCodeBlock;
|
||||
pub use pairing::Mark;
|
||||
mod parts;
|
||||
mod table;
|
||||
@@ -170,8 +171,10 @@ pub fn is_split_whitespace(c: char) -> bool {
|
||||
/// HTML whitespace: `\t\n\f\r` and space.
|
||||
pub const HTML_WHITESPACE: [char; 5] = ['\t', '\n', '\u{c}', '\r', ' '];
|
||||
|
||||
/// `n` backticks; borrowed from a static run for every realistic length.
|
||||
pub fn backticks(n: usize) -> Cow<'static, str> {
|
||||
const RUN: &str = "````````````````````````````````";
|
||||
if n <= RUN.len() { Cow::Borrowed(&RUN[..n]) } else { Cow::Owned("`".repeat(n)) }
|
||||
/// `n` backticks or tildes (`ch`); borrowed from a static run for every realistic length.
|
||||
pub fn fence_run(ch: u8, n: usize) -> Cow<'static, str> {
|
||||
const BACKTICKS: &str = "````````````````````````````````";
|
||||
const TILDES: &str = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";
|
||||
let run = if ch == b'`' { BACKTICKS } else { TILDES };
|
||||
if n <= run.len() { Cow::Borrowed(&run[..n]) } else { Cow::Owned(run[..1].repeat(n)) }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
~~~js `x`
|
||||
a
|
||||
~~~
|
||||
|
||||
~~~ ``title`` lang
|
||||
b
|
||||
~~~
|
||||
|
||||
~~~js
|
||||
c
|
||||
~~~
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
source: crates/oxc_formatter_markdown/tests/fixtures/mod.rs
|
||||
---
|
||||
==================== Input ====================
|
||||
~~~js `x`
|
||||
a
|
||||
~~~
|
||||
|
||||
~~~ ``title`` lang
|
||||
b
|
||||
~~~
|
||||
|
||||
~~~js
|
||||
c
|
||||
~~~
|
||||
|
||||
==================== Output ====================
|
||||
------------------
|
||||
{ printWidth: 80 }
|
||||
------------------
|
||||
~~~js `x`
|
||||
a
|
||||
~~~
|
||||
|
||||
~~~``title`` lang
|
||||
b
|
||||
~~~
|
||||
|
||||
```js
|
||||
c
|
||||
```
|
||||
|
||||
-------------------
|
||||
{ printWidth: 100 }
|
||||
-------------------
|
||||
~~~js `x`
|
||||
a
|
||||
~~~
|
||||
|
||||
~~~``title`` lang
|
||||
b
|
||||
~~~
|
||||
|
||||
```js
|
||||
c
|
||||
```
|
||||
|
||||
===================== End =====================
|
||||
Reference in new issue
Block a user