# Deviations from Prettier tidy prints what Prettier 3.9.9 prints, XML what `@prettier/plugin-xml` 3.4.2 prints, TOML, which Prettier doesn't format, what taplo 0.14.0 prints by default, Dockerfiles what dprint-plugin-dockerfile 0.7.0 prints on its first pass, and INI files what prettier-plugin-ini 1.3.0 prints, except where this file says otherwise. Markdown follows Prettier 3.9.10, and its deviations are in [`markdown-divergences.md`](markdown-divergences.md), which is generated from its cases. Each entry below says what tidy prints instead, why, where the rule is, and whether Luke has signed it off. `crates/tidy/tests/deviations.json` holds every input that was checked against both tools, and `cargo test --test deviations` fails if tidy stops printing what an entry says. ## Process 1. A change that makes tidy print something other than Prettier is a deviation. It is never shipped silently: add an entry here with status `proposed`, add its rows to `tests/deviations.json`, and raise it with Luke for sign-off. 2. Proposed rule, which itself needs sign-off: keep a deviation only where Prettier's output changes what the code means (it loses text, changes a value that a program reads, or no longer parses). Where the difference is only layout, print what Prettier prints. 3. Each entry is `proposed` (in the code, awaiting sign-off) or `approved` (with the date). A rejected entry is removed from the code and from this file, and its rows from the test. A deviation that is raised but not yet in the code is listed under [Raised, not in the code](#raised-not-in-the-code). Most entries come from bun's ports, which correct the places where Prettier's output changes the meaning of the code. When HTML was ported, the corrections that are only layout (an SVG ``, `\n" Prettier: "" tidy: "" ``` ### hbs-pre-descendant-whitespace - Status: approved 2026-11-01 - Why: Prettier keeps whitespace only in the direct text of `
`. Text in a nested element is reflowed, which changes what renders.
- Where: crates/tidy/src/handlebars/printer.rs:717-720 (`pre_depth`)
- Parser: handlebars

```
input:    "
a\n  b
\n" Prettier: "
a b
" tidy: "
a\n  b
" ``` ### hbs-whitespace-block-in-pre-textarea - Status: approved 2026-11-01 - Why: Prettier drops a whitespace-only block body even in `
` or `\n"
Prettier: ""
tidy:     ""
```

### hbs-void-with-children

- Status: approved 2026-11-01
- Why: Prettier treats `` as the void `` and drops its children. Glimmer treats a name with an upper-case letter as a component.
- Where: crates/tidy/src/handlebars/printer.rs:275-281
- Parser: handlebars

```
input:    "a\n"
Prettier: ""
tidy:     "a"
```

### hbs-path-round-trip

- Status: approved 2026-11-01
- Why: Prettier prints the path from the parsed parts, which drops `this/`, `@this`, empty brackets and dots, and adds or moves brackets. The result names a different value, or does not parse.
- Where: crates/tidy/src/handlebars/parser.rs:544-549 and parser.rs:610-611 (path kept as written), printer.rs:1375-1393 (`path_round_trips`)
- Parser: handlebars

```
input:    "{{this/a   }}\n"
Prettier: "{{a}}"
tidy:     "{{this/a}}"
```

### hbs-bracketed-segment

- Status: approved 2026-11-01
- Why: Prettier drops the brackets around a segment that needs them (`[true]`, `[-1]`, `[else]`, `[@b]`, `[b c]`), so it reads as a literal, a keyword, an argument or two names.
- Where: crates/tidy/src/handlebars/printer.rs:1375-1393, printer.rs:1497-1512
- Parser: handlebars

```
input:    "{{a   [true]}}\n"
Prettier: "{{a true}}"
tidy:     "{{a [true]}}"
```

### hbs-hash-key-in-brackets

- Status: approved 2026-11-01
- Why: Prettier prints `[b c]=d` as `b c=d`, which is a parameter and a different key.
- Where: crates/tidy/src/handlebars/parser.rs:378-384
- Parser: handlebars

```
input:    "{{a   [b c]=d}}\n"
Prettier: "{{a b c=d}}"
tidy:     "{{a [b c]=d}}"
```

### hbs-dropped-arguments

- Status: approved 2026-11-01
- Why: The parsers silently drop arguments after a literal, and a hash used as a callee, so Prettier deletes code.
- Where: crates/tidy/src/handlebars/parser.rs:842-845, parser.rs:416-419 (`has_dropped`, so the mustache is printed as written)
- Parser: handlebars

```
input:    "{{\"a\"   b}}\n"
Prettier: "{{\"a\"}}"
tidy:     "{{\"a\"   b}}"
```

### hbs-raw-block

- Status: approved 2026-11-01
- Why: Glimmer turns `{{{{a}}}}` into an ordinary block, so Prettier prints `{{#a}}` and escapes the body, which changes what renders.
- Where: crates/tidy/src/handlebars/parser.rs:796-799
- Parser: handlebars

```
input:    "{{{{a}}}}   {{b}} {{{{/a}}}}\n"
Prettier: "{{#a}} \\{{b}} {{/a}}"
tidy:     "{{{{a}}}}   {{b}} {{{{/a}}}}"
```

### hbs-number-exponent

- Status: approved 2026-11-01
- Why: Prettier prints `String(Number(x))`, which can be `1e+21` or `1e-7`. The Handlebars lexer does not accept exponents.
- Where: crates/tidy/src/handlebars/printer.rs:1356-1371
- Parser: handlebars

```
input:    "{{a   1000000000000000000000}}\n"
Prettier: "{{a 1e+21}}"
tidy:     "{{a 1000000000000000000000}}"
```

### hbs-triple-stash-strip

- Status: approved 2026-11-01
- Why: Prettier prints `{{{a~}}}`, which does not parse, and drops the third brace of a modifier.
- Where: crates/tidy/src/handlebars/printer.rs:1249-1261, tokenizer.rs:1006-1009
- Parser: handlebars

```
input:    "{{{a   }~}}\n"
Prettier: "{{{a~}}}"
tidy:     "{{{a}~}}"
```

### hbs-comment-dashes

- Status: approved 2026-11-01
- Why: Without the `--` form, `{{! a ~}}` reads as a strip mark and `{{!-- a }}` ends at a later `--}}`. Prettier drops the dashes in both cases.
- Where: crates/tidy/src/handlebars/printer.rs:380-383
- Parser: handlebars

```
input:    "{{!--   a ~--}}\n"
Prettier: "{{!   a ~}}"
tidy:     "{{!--   a ~--}}"
```

### hbs-else-chain

- Status: approved 2026-11-01
- Why: Prettier prints only the head of the name after `else` (`if.b` becomes `if`), and folds `{{else}}{{~#if}}` into `{{else if}}`, which loses the `~` and the inner `{{/if}}`.
- Where: crates/tidy/src/handlebars/printer.rs:860-871 (`is_else_if`), printer.rs:921-922 (whole path)
- Parser: handlebars

```
input:    "{{#a   }}{{else if.b c}}{{/a}}\n"
Prettier: "{{#a}}{{else if c}}{{/a}}"
tidy:     "{{#a}}{{else if.b c}}{{/a}}"
```

### hbs-line-separator-columns

- Status: approved 2026-11-01
- Why: Prettier slices source text by line and column, and counts U+2028 and U+2029 as line breaks there, so it cuts comments and ignored nodes at the wrong place.
- Where: No single rule: tidy slices by byte offset, for example crates/tidy/src/handlebars/printer.rs:290-294 (`ignored`)
- Parser: handlebars

```
input:    "a\n
{{~! b ~}} 

\n
{{! prettier-ignore }} {{ i }} {{!-- j --~}}\n{{k\n
 l}} {{~! m }}\n" Prettier: "a 
{{! b }}\n

\n\n

{{! prettier-ignore }\n {{ i }\n{{! j }}\n{{k l}}\n{{! m }}" tidy: "a 
{{~! b ~}}\n

\n\n
{{! prettier-ignore }}\n{{ i }}\n{{! j ~}}\n{{k l}}\n{{~! m }}" ``` ### hbs-ws-ignore-escaped-mustache - Status: approved 2026-11-01 - Why: With `--html-whitespace-sensitivity ignore`, Prettier joins a word and the next escaped mustache (`p\{{a}}`), which changes the text. - Where: crates/tidy/src/handlebars/printer.rs:760 (a text next to a text, from an escaped mustache) - Parser: handlebars ``` input: "p \\{{a}} {{q }}\n" Prettier: "p\\{{a}} {{q}}" tidy: "p \\{{a}} {{q}}" ``` ## CSS, SCSS and Less ### css-escaped-space-before-semicolon - Status: approved 2026-11-01 - Why: `\ ` at the end of at-rule params is an escaped space. Prettier trims the space, so the backslash escapes the `;` or `)`. - Where: crates/tidy/src/css/printer.rs:1091-1094 (at-rule), printer.rs:1180-1183 (media feature), printer.rs:766-768 (declaration) - Parser: scss ``` input: "a{@extend .a\\ ;}\n" Prettier: "a {\n @extend .a\\;\n}\n" tidy: "a {\n @extend .a\\ ;\n}\n" ``` ### css-escaped-space-in-value - Status: approved 2026-11-01 - Why: Prettier prints nothing after a word with a backslash, so `Font\ Awesome\ 5` becomes one word, and `\b c` loses the space that ends the hex escape. - Where: crates/tidy/src/css/value_groups.rs:491-505, value_groups.rs:1013-1015, value_groups.rs:1098, value_groups.rs:398 - Parser: css ``` input: "a{b:Font\\ Awesome\\ 5}\n" Prettier: "a {\n b: Font\\Awesome\\5;\n}\n" tidy: "a {\n b: Font\\ Awesome\\ 5;\n}\n" ``` ### css-selector-without-namespace - Status: approved 2026-11-01 - Why: `x |b` is a descendant `|b` (no namespace). Prettier prints `x|b` (namespace x), and turns `x,|b` into `x, ,|b`, which does not parse. - Where: crates/tidy/src/css/selector_parser.rs:375-379, selector_parser.rs:397 - Parser: css ``` input: "x |b{c:d}\n" Prettier: "x|b {\n c: d;\n}\n" tidy: "x |b {\n c: d;\n}\n" ``` ### css-non-ascii-lowercase - Status: approved 2026-11-01 - Why: Prettier lowercases a media query with `toLowerCase`, which turns U+212A KELVIN SIGN into `k`. CSS is case-insensitive only for ASCII. - Where: crates/tidy/src/css/misc.rs:240-247 - Parser: css ``` input: "@media (KA){a{b:c}}\n" Prettier: "@media (ka) {\n a {\n b: c;\n }\n}\n" tidy: "@media (Ka) {\n a {\n b: c;\n }\n}\n" ``` ### less-variable-colon-without-space - Status: approved 2026-11-01 - Why: postcss reads `@a:1px` as an at-rule name, and Prettier joins the value without the space after it: `1px solid` becomes `1pxsolid`. - Where: crates/tidy/src/css/parse.rs:448-452 - Parser: less ``` input: "@a:1px solid #ccc;\n" Prettier: "@a: 1pxsolid #ccc;\n" tidy: "@a: 1px solid #ccc;\n" ``` ### less-variable-comment - Status: approved 2026-11-01 - Why: Prettier parses the value after postcss has removed comments, then reads comment text at offsets that assume nothing was removed. It deletes comments or prints garbled text such as `b d c */ d f`. - Where: crates/tidy/src/css/parse.rs:397-400 - Parser: less ``` input: "@a: 1px /* why */ solid;\n" Prettier: "@a: 1px solid;\n" tidy: "@a: 1px /* why */ solid;\n" ``` ### css-comment-only-at-rule-params - Status: approved 2026-11-01 - Why: If the only thing after an at-rule name is a `/* */` comment, Prettier cuts it one character short (`/* why *`), so the comment does not close. - Where: crates/tidy/src/css/parse.rs:753-758 - Parser: scss ``` input: "@if $a {\n b: c;\n} @else /* why */ {\n d: e;\n}\n" Prettier: "@if $a {\n b: c;\n} @else /* why * {\n d: e;\n}\n" tidy: "@if $a {\n b: c;\n} @else /* why */ {\n d: e;\n}\n" ``` ### css-line-comment-before-brace-or-semicolon - Status: approved 2026-11-01 - Why: When `{` or `;` is on the line after a `//` comment, Prettier moves it onto the comment line, where it becomes part of the comment. Less also blanks quotes and `*` in such comments. - Where: crates/tidy/src/css/parse.rs:787-795 (at-rules), parse.rs:853-858 (`@media` query), parse.rs:424-430 (Less mixin call), parse.rs:666-672 (declaration) - Parser: scss ``` input: "@media (min-width: 1px) // Why\n{\n a {\n b: c;\n }\n}\n" Prettier: "@media (min-width: 1px) // Why {\n a {\n b: c;\n }\n}\n" tidy: "@media (min-width: 1px) // Why\n{\n a {\n b: c;\n }\n}\n" ``` ### css-nth-and-keyframe-sign - Status: approved 2026-11-01 - Why: Prettier puts a space after a leading `+` in `:nth-*()` and in a keyframe selector (`+ 50%`). Both are then invalid and the browser drops the rule. - Where: crates/tidy/src/css/printer.rs:1371-1387 - Parser: css ``` input: ":nth-child(+n+3){c:d}\n" Prettier: ":nth-child(+ n + 3) {\n c: d;\n}\n" tidy: ":nth-child(+n + 3) {\n c: d;\n}\n" ``` ### css-hex-escape-space-in-selector - Status: approved 2026-11-01 - Why: The first space after a hex escape (`\31 `) ends the escape. Prettier collapses `\31 .b` to `\31 .b`, which turns a descendant selector into a compound one. - Where: crates/tidy/src/css/printer.rs:1392-1402 - Parser: css ``` input: ".\\31 .b{c:d}\n" Prettier: ".\\31 .b {\n c: d;\n}\n" tidy: ".\\31 .b {\n c: d;\n}\n" ``` ### css-attribute-value-escape - Status: approved 2026-11-01 - Why: Prettier reads `\ ` or the space after a hex escape as the end of the value and the start of a flag: `[a=b\ c]` becomes `[a="b\" c]`, an unterminated string, and `[a=x\31 i]` gets a flag `i`. - Where: crates/tidy/src/css/misc.rs:212-216 - Parser: css ``` input: "[a=x\\31 i]{c:d}\n" Prettier: "[a=\"x\\31\" i] {\n c: d;\n}\n" tidy: "[a=\"x\\31 i\"] {\n c: d;\n}\n" ``` ### css-hex-escape-space-in-value - Status: approved 2026-11-01 - Why: `\31 x` is one word and `\31 x` is two. Prettier drops the second space, or breaks the line after `\31`, which splits one word into two. - Where: crates/tidy/src/css/value_groups.rs:472-484 - Parser: css ``` input: "a{animation-name:\\31 x}\n" Prettier: "a {\n animation-name: \\31 x;\n}\n" tidy: "a {\n animation-name: \\31 x;\n}\n" ``` ### css-author-defined-names - Status: approved 2026-11-01 - Why: Names inside `@plugin` (Tailwind options) and `@font-feature-values` blocks are case-sensitive. Prettier lowercases them (`className` becomes `classname`). - Where: crates/tidy/src/css/printer.rs:808-815 - Parser: css ``` input: "@plugin \"@tailwindcss/typography\" {className:wysiwyg}\n" Prettier: "@plugin \"@tailwindcss/typography\" {\n classname: wysiwyg;\n}\n" tidy: "@plugin \"@tailwindcss/typography\" {\n className: wysiwyg;\n}\n" ``` ### css-custom-property-values - Status: approved 2026-11-01 - Why: Scripts read a custom property's value as written (`getPropertyValue`), and `style()` queries compare it as written. Prettier lowercases colors and units, rewrites numbers, requotes strings and adds spaces (`1,000` becomes `1, 000`). - Where: crates/tidy/src/css/printer.rs:66-72 (`is_custom_property`), applied at printer.rs:1543, 1569, 1581, 1596 and value_groups.rs:243, 963 - Parser: css ``` input: "a{--x:#FFF}\n" Prettier: "a {\n --x: #fff;\n}\n" tidy: "a {\n --x: #FFF;\n}\n" ``` ### css-custom-property-line-breaks - Status: approved 2026-11-01 - Why: In a long custom property value, Prettier breaks lines and adds spaces after commas that had none (`1,000` becomes `1, 000`), which changes the value as written. - Where: crates/tidy/src/css/value_groups.rs:1077-1080 (no line after a comma without space), printer.rs:1581 - Parser: css ``` input: "a {\n --x: 1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000;\n}\n" Prettier: "a {\n --x:\n 1, 000, 1, 000, 1, 000, 1, 000, 1, 000, 1, 000, 1, 000, 1, 000, 1, 000, 1,\n 000, 1, 000, 1, 000, 1, 000, 1, 000, 1, 000, 1, 000;\n}\n" tidy: "a {\n --x:\n 1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000,1,000;\n}\n" ``` ### css-custom-property-json - Status: approved 2026-11-01 - Why: `--x: {"a":1}` is JSON for a script. Prettier parses it as a declaration block and adds a `;`, so the JSON no longer parses. - Where: crates/tidy/src/css/parse.rs:624-628 - Parser: css ``` input: "a{--x:{ \"a\": 1 }}\n" Prettier: "a {\n --x: {\n \"a\":1;\n };\n}\n" tidy: "a {\n --x: { \"a\": 1 };\n}\n" ``` ### css-custom-property-url - Status: approved 2026-11-01 - Why: Prettier treats every `//` as a comment start: `--x: https://a` becomes `--x: https: ; //a`. CSS has no `//` comments, and Less only after a space. - Where: crates/tidy/src/css/parse.rs:702-706 - Parser: css ``` input: "a{--x:https://Example.com/A;c:d}\n" Prettier: "a {\n --x: https: ; //Example.com/A\n c: d;\n}\n" tidy: "a {\n --x: https://Example.com/A;\n c: d;\n}\n" ``` ## HTML and Vue ### html-pre-without-end-tag - Status: approved 2026-11-01 - Why: With an element inside and no end tag, Prettier prints the contents of `
` or `` as empty plus a stray `>`. Everything after the start tag is lost.
- Where: crates/tidy/src/html/tag.rs:128-136 (`is_verbatim_without_end_tag`), tag.rs:304
- Parser: html

```
input:    "
a\n"
Prettier: "
>\n"
tidy:     "
a\n"
```

### html-unquoted-attribute-value

- Status: approved 2026-11-01
- Why: In HTML an unquoted value ends at whitespace or `>`. Prettier (Angular's lexer) ends it at the first `=`, `/` or no-break space, so `src=a.png?v=1` becomes `src="a.png?v" ="1"`. For some valid inputs Prettier fails with a syntax error; tidy formats them.
- Where: crates/tidy/src/html/lexer.rs:1098-1104 (`End::UnquotedValue` outside Angular)
- Parser: html

```
input:    "\n"
Prettier: "\n"
tidy:     "\n"
```

### html-whitespace-only-pre-textarea

- Status: approved 2026-11-01
- Why: Whitespace that is all of a `
` renders, and in a `\n"
Prettier: "\n"
tidy:     "\n"
```

### html-ignored-void-without-slash

- Status: approved 2026-11-01
- Why: A void element under `prettier-ignore` without `/>` lends `>` to its neighbor. Prettier assumes `/>` and cuts that many characters from the source, so `src=b>` becomes `src=/>` and `
` becomes ``. - Where: crates/tidy/src/html/tag.rs:120-126 (`is_verbatim_void_without_slash`), tag.rs:112 - Parser: html ``` input: "c\n" Prettier: "c\n" tidy: "c\n" ``` ### html-text-after-nul - Status: approved 2026-11-01 - Why: The parser's end-of-file is code 0, so Prettier silently drops everything after a NUL between tags. tidy refuses the file with "Unexpected NUL character" and leaves it unchanged. - Where: crates/tidy/src/html/parser.rs:671, error.rs:21-23, error.rs:113 - Parser: html ``` input: "

b

\n

c\u0000d

\n

e

\n" Prettier: "

b

\n

c

\n" tidy: "ERROR(2): tidy: Unexpected NUL character | stdout=\"\"" ``` ### html-borrowed-markers-once - Status: approved 2026-11-01 - Why: Where an element's contents print as written, or it follows ``, Prettier prints the markers that its tags borrow from the elements next to them twice: once on their own, and once in the tag. The extra `>` or `a
` Prettier also loses the contents of a `
` without an end tag.
- Where: crates/tidy/src/html/print.rs:266 (`print_child`), print.rs:442 (`print_element`)
- Parser: html

```
input:    "\n"
Prettier: ">\n"
tidy:     "\n"
```

With `--whitespace strict`:

```
input:    "\n\n"
Prettier: "\n    >\n"
tidy:     "\n    \n"

input:    "a
x\n"
Prettier: "a>
>\n"
tidy:     "a
x\n"
```

### vue-v-pre-interpolation

- Status: approved 2026-11-01
- Why: Vue does not compile anything under `v-pre`, so `{{name}}` renders as that text. Prettier formats it as an expression (`{{ name }}`), which changes the rendered text.
- Where: crates/tidy/src/html/preprocess.rs:205-208
- Parser: vue

```
input:    "\n"
Prettier: "\n"
tidy:     "\n"
```

Only visible when the embed hook formats JavaScript, so `tests/deviations.json` doesn't hold it. It was checked through the napi binding with a hook backed by Prettier, as `tests/oracle/conformance.ts` does.

## XML

### xml-embed-by-name

- Status: proposed
- Why: The plugin formats the content of any element that is named after one of Prettier's parsers as code in that language: `` as Markdown, ``, ``, ``, ``. These are ordinary elements whose text is data, such as DocBook's ``, and formatting it changes the text. tidy formats code only in `