Skip to content

Commit 1f83aae

Browse files
Remove duplicate classes and excess whitespace (#272)
* Add docblock * Update comments * Remove duplicate classes * Add option to collapse whitespace * Update tests * Don’t trim entirely empty class lists We leave one space in when a class list consists of just whitespace * Remove whitespace by default * Rename option to `tailwindPreserveWhitespace` * Update changelog
1 parent 3c9ce4e commit 1f83aae

File tree

7 files changed

+220
-16
lines changed

7 files changed

+220
-16
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Changed
11+
12+
- Remove duplicate classes ([#272](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/272))
13+
- Remove extra whitespace around classes ([#272](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/272))
1114

1215
## [0.5.14] - 2024-04-15
1316

src/index.js

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,10 @@ function transformGlimmer(ast, { env }) {
234234
env,
235235
ignoreFirst: siblings?.prev && !/^\s/.test(node.chars),
236236
ignoreLast: siblings?.next && !/\s$/.test(node.chars),
237+
collapseWhitespace: {
238+
start: !siblings?.prev,
239+
end: !siblings?.next,
240+
},
237241
})
238242
},
239243

@@ -248,6 +252,10 @@ function transformGlimmer(ast, { env }) {
248252
node.value = sortClasses(node.value, {
249253
env,
250254
ignoreLast: isConcat && !/[^\S\r\n]$/.test(node.value),
255+
collapseWhitespace: {
256+
start: false,
257+
end: !isConcat,
258+
},
251259
})
252260
},
253261
})
@@ -299,6 +307,10 @@ function transformLiquid(ast, { env }) {
299307
env,
300308
ignoreFirst: i > 0 && !/^\s/.test(node.value),
301309
ignoreLast: i < attr.value.length - 1 && !/\s$/.test(node.value),
310+
collapseWhitespace: {
311+
start: i === 0,
312+
end: i >= attr.value.length - 1,
313+
},
302314
})
303315

304316
changes.push({
@@ -411,8 +423,17 @@ function sortTemplateLiteral(node, { env }) {
411423

412424
quasi.value.raw = sortClasses(quasi.value.raw, {
413425
env,
426+
// Is not the first "item" and does not start with a space
414427
ignoreFirst: i > 0 && !/^\s/.test(quasi.value.raw),
428+
429+
// Is between two expressions
430+
// And does not end with a space
415431
ignoreLast: i < node.expressions.length && !/\s$/.test(quasi.value.raw),
432+
433+
collapseWhitespace: {
434+
start: i === 0,
435+
end: i >= node.expressions.length,
436+
},
416437
})
417438

418439
quasi.value.cooked = same
@@ -422,6 +443,10 @@ function sortTemplateLiteral(node, { env }) {
422443
ignoreFirst: i > 0 && !/^\s/.test(quasi.value.cooked),
423444
ignoreLast:
424445
i < node.expressions.length && !/\s$/.test(quasi.value.cooked),
446+
collapseWhitespace: {
447+
start: i === 0,
448+
end: i >= node.expressions.length,
449+
},
425450
})
426451

427452
if (
@@ -566,11 +591,17 @@ function transformJavaScript(ast, { env }) {
566591
function transformCss(ast, { env }) {
567592
ast.walk((node) => {
568593
if (node.type === 'css-atrule' && node.name === 'apply') {
594+
let isImportant = /\s+(?:!important|#{(['"]*)!important\1})\s*$/.test(
595+
node.params,
596+
)
597+
569598
node.params = sortClasses(node.params, {
570599
env,
571-
ignoreLast: /\s+(?:!important|#{(['"]*)!important\1})\s*$/.test(
572-
node.params,
573-
),
600+
ignoreLast: isImportant,
601+
collapseWhitespace: {
602+
start: false,
603+
end: !isImportant,
604+
},
574605
})
575606
}
576607
})
@@ -690,6 +721,10 @@ function transformMelody(ast, { env, changes }) {
690721
isConcat && _key === 'right' && !/^[^\S\r\n]/.test(node.value),
691722
ignoreLast:
692723
isConcat && _key === 'left' && !/[^\S\r\n]$/.test(node.value),
724+
collapseWhitespace: {
725+
start: !(isConcat && _key === 'right'),
726+
end: !(isConcat && _key === 'left'),
727+
},
693728
})
694729
},
695730
})
@@ -775,13 +810,21 @@ function transformSvelte(ast, { env, changes }) {
775810
env,
776811
ignoreFirst: i > 0 && !/^\s/.test(value.raw),
777812
ignoreLast: i < attr.value.length - 1 && !/\s$/.test(value.raw),
813+
collapseWhitespace: {
814+
start: i === 0,
815+
end: i >= attr.value.length - 1,
816+
},
778817
})
779818
value.data = same
780819
? value.raw
781820
: sortClasses(value.data, {
782821
env,
783822
ignoreFirst: i > 0 && !/^\s/.test(value.data),
784823
ignoreLast: i < attr.value.length - 1 && !/\s$/.test(value.data),
824+
collapseWhitespace: {
825+
start: i === 0,
826+
end: i >= attr.value.length - 1,
827+
},
785828
})
786829
} else if (value.type === 'MustacheTag') {
787830
visit(value.expression, {

src/options.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ export const options = {
3030
description:
3131
'List of functions and tagged templates that contain sortable Tailwind classes',
3232
},
33+
tailwindPreserveWhitespace: {
34+
since: '0.6.0',
35+
type: 'boolean',
36+
default: [{ value: false }],
37+
category: 'Tailwind CSS',
38+
description: 'Preserve whitespace around Tailwind classes when sorting',
39+
},
3340
}
3441

3542
/** @typedef {import('prettier').RequiredOptions} RequiredOptions */

src/sorting.js

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,46 @@ function getClassOrderPolyfill(classes, { env }) {
3939
return classNamesWithOrder
4040
}
4141

42+
/**
43+
* @param {string} classStr
44+
* @param {object} opts
45+
* @param {any} opts.env
46+
* @param {boolean} [opts.ignoreFirst]
47+
* @param {boolean} [opts.ignoreLast]
48+
* @param {object} [opts.collapseWhitespace]
49+
* @param {boolean} [opts.collapseWhitespace.start]
50+
* @param {boolean} [opts.collapseWhitespace.end]
51+
* @returns {string}
52+
*/
4253
export function sortClasses(
4354
classStr,
44-
{ env, ignoreFirst = false, ignoreLast = false },
55+
{
56+
env,
57+
ignoreFirst = false,
58+
ignoreLast = false,
59+
collapseWhitespace = { start: true, end: true },
60+
},
4561
) {
4662
if (typeof classStr !== 'string' || classStr === '') {
4763
return classStr
4864
}
4965

5066
// Ignore class attributes containing `{{`, to match Prettier behaviour:
51-
// https://github.com/prettier/prettier/blob/main/src/language-html/embed.js#L83-L88
67+
// https://github.com/prettier/prettier/blob/8a88cdce6d4605f206305ebb9204a0cabf96a070/src/language-html/embed/class-names.js#L9
5268
if (classStr.includes('{{')) {
5369
return classStr
5470
}
5571

72+
if (env.options.tailwindPreserveWhitespace) {
73+
collapseWhitespace = false
74+
}
75+
76+
// This class list is purely whitespace
77+
// Collapse it to a single space if the option is enabled
78+
if (/^[\t\r\f\n ]+$/.test(classStr) && collapseWhitespace) {
79+
return ' '
80+
}
81+
5682
let result = ''
5783
let parts = classStr.split(/([\t\r\f\n ]+)/)
5884
let classes = parts.filter((_, i) => i % 2 === 0)
@@ -62,6 +88,10 @@ export function sortClasses(
6288
classes.pop()
6389
}
6490

91+
if (collapseWhitespace) {
92+
whitespace = whitespace.map(() => ' ')
93+
}
94+
6595
let prefix = ''
6696
if (ignoreFirst) {
6797
prefix = `${classes.shift() ?? ''}${whitespace.shift() ?? ''}`
@@ -72,12 +102,32 @@ export function sortClasses(
72102
suffix = `${whitespace.pop() ?? ''}${classes.pop() ?? ''}`
73103
}
74104

105+
// Remove duplicates
106+
classes = classes.filter((cls, index, arr) => {
107+
if (arr.indexOf(cls) === index) {
108+
return true
109+
}
110+
111+
whitespace.splice(index - 1, 1)
112+
113+
return false
114+
})
115+
75116
classes = sortClassList(classes, { env })
76117

77118
for (let i = 0; i < classes.length; i++) {
78119
result += `${classes[i]}${whitespace[i] ?? ''}`
79120
}
80121

122+
if (collapseWhitespace) {
123+
prefix = prefix.replace(/\s+$/g, ' ')
124+
suffix = suffix.replace(/^\s+/g, ' ')
125+
126+
result = result
127+
.replace(/^\s+/, collapseWhitespace.start ? '' : ' ')
128+
.replace(/\s+$/, collapseWhitespace.end ? '' : ' ')
129+
}
130+
81131
return prefix + result + suffix
82132
}
83133

@@ -89,8 +139,6 @@ export function sortClassList(classList, { env }) {
89139
return classNamesWithOrder
90140
.sort(([, a], [, z]) => {
91141
if (a === z) return 0
92-
// if (a === null) return options.unknownClassPosition === 'start' ? -1 : 1
93-
// if (z === null) return options.unknownClassPosition === 'start' ? 1 : -1
94142
if (a === null) return -1
95143
if (z === null) return 1
96144
return bigSign(a - z)

0 commit comments

Comments
 (0)