Skip to content

Commit 63d1f5c

Browse files
Don't repeatedly add backslashes to escape sequences when formatting (#381)
* fix: repeated backslash escaping * Cleanup * Bail early if the content didn’t change * Cleanup * Move test * Fix failing tests * Cleanup code and comments * Update changelog --------- Co-authored-by: Jordan Pittman <[email protected]>
1 parent a265c5b commit 63d1f5c

File tree

3 files changed

+28
-21
lines changed

3 files changed

+28
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
- Add support for OXC + Hermes Prettier plugins ([#376](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/376), [#380](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/380))
1111
- Sort template literals in Angular expressions ([#377](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/377))
12+
- Don't repeatedly add backslashes to escape sequences when formatting ([#381](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/381))
1213

1314
## [0.6.13] - 2025-06-19
1415

src/index.ts

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { spliceChangesIntoString, visit } from './utils.js'
2828

2929
let base = await loadPlugins()
3030

31+
const ESCAPE_SEQUENCE_PATTERN = /\\(['"\\nrtbfv0-7xuU])/g
32+
3133
function createParser(
3234
parserFormat: string,
3335
transform: (ast: any, context: TransformerContext) => void,
@@ -505,41 +507,41 @@ function sortStringLiteral(
505507
removeDuplicates,
506508
collapseWhitespace,
507509
})
510+
508511
let didChange = result !== node.value
509-
node.value = result
510512

511-
// A string literal was escaped if:
512-
// - There are backslashes in the raw value; AND
513-
// - The raw value is not the same as the value (excluding the surrounding quotes)
514-
let wasEscaped = false
513+
if (!didChange) return false
515514

516-
if (node.extra) {
517-
// JavaScript (StringLiteral)
518-
wasEscaped =
519-
node.extra?.rawValue.includes('\\') &&
520-
node.extra?.raw.slice(1, -1) !== node.value
521-
} else {
522-
// TypeScript (Literal)
523-
wasEscaped =
524-
node.value.includes('\\') && node.raw.slice(1, -1) !== node.value
525-
}
515+
node.value = result
526516

527-
let escaped = wasEscaped ? result.replace(/\\/g, '\\\\') : result
517+
// Preserve the original escaping level for the new content
518+
let raw = node.extra?.raw ?? node.raw
519+
let quote = raw[0]
520+
let originalRawContent = raw.slice(1, -1)
521+
let originalValue = node.extra?.rawValue ?? node.value
528522

529523
if (node.extra) {
524+
// The original list has ecapes so we ensure that the sorted list also
525+
// maintains those by replacing backslashes from escape sequences.
526+
//
527+
// It seems that TypeScript-based ASTs don't need this special handling
528+
// which is why this is guarded inside the `node.extra` check
529+
if (originalRawContent !== originalValue && originalValue.includes('\\')) {
530+
result = result.replace(ESCAPE_SEQUENCE_PATTERN, '\\\\$1')
531+
}
532+
530533
// JavaScript (StringLiteral)
531-
let raw = node.extra.raw
532534
node.extra = {
533535
...node.extra,
534536
rawValue: result,
535-
raw: raw[0] + escaped + raw.slice(-1),
537+
raw: quote + result + quote,
536538
}
537539
} else {
538540
// TypeScript (Literal)
539-
let raw = node.raw
540-
node.raw = raw[0] + escaped + raw.slice(-1)
541+
node.raw = quote + result + quote
541542
}
542-
return didChange
543+
544+
return true
543545
}
544546

545547
function isStringLiteral(node: any) {

tests/tests.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ export let javascript: TestEntry[] = [
111111
`;<div class={'object-cover' + (standalone ? ' aspect-square w-full' : ' min-h-0 grow basis-0')}></div>`,
112112
`;<div class={'object-cover' + (standalone ? ' aspect-square w-full' : ' min-h-0 grow basis-0')}></div>`,
113113
],
114+
[
115+
`;<div class="[&>.a\\_p]:after:content-['\\2'] [&>.a\\_p]:z-0"></div>`,
116+
`;<div class="[&>.a\\_p]:z-0 [&>.a\\_p]:after:content-['\\2']"></div>`,
117+
],
114118
]
115119
javascript = javascript.concat(
116120
javascript.map((test) => [

0 commit comments

Comments
 (0)