Skip to content

Commit

Permalink
Remove duplicate classes and excess whitespace (#272)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
thecrypticace committed May 30, 2024
1 parent 3c9ce4e commit 1f83aae
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 16 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Nothing yet!
### Changed

- Remove duplicate classes ([#272](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/272))
- Remove extra whitespace around classes ([#272](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/pull/272))

## [0.5.14] - 2024-04-15

Expand Down
49 changes: 46 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,10 @@ function transformGlimmer(ast, { env }) {
env,
ignoreFirst: siblings?.prev && !/^\s/.test(node.chars),
ignoreLast: siblings?.next && !/\s$/.test(node.chars),
collapseWhitespace: {
start: !siblings?.prev,
end: !siblings?.next,
},
})
},

Expand All @@ -248,6 +252,10 @@ function transformGlimmer(ast, { env }) {
node.value = sortClasses(node.value, {
env,
ignoreLast: isConcat && !/[^\S\r\n]$/.test(node.value),
collapseWhitespace: {
start: false,
end: !isConcat,
},
})
},
})
Expand Down Expand Up @@ -299,6 +307,10 @@ function transformLiquid(ast, { env }) {
env,
ignoreFirst: i > 0 && !/^\s/.test(node.value),
ignoreLast: i < attr.value.length - 1 && !/\s$/.test(node.value),
collapseWhitespace: {
start: i === 0,
end: i >= attr.value.length - 1,
},
})

changes.push({
Expand Down Expand Up @@ -411,8 +423,17 @@ function sortTemplateLiteral(node, { env }) {

quasi.value.raw = sortClasses(quasi.value.raw, {
env,
// Is not the first "item" and does not start with a space
ignoreFirst: i > 0 && !/^\s/.test(quasi.value.raw),

// Is between two expressions
// And does not end with a space
ignoreLast: i < node.expressions.length && !/\s$/.test(quasi.value.raw),

collapseWhitespace: {
start: i === 0,
end: i >= node.expressions.length,
},
})

quasi.value.cooked = same
Expand All @@ -422,6 +443,10 @@ function sortTemplateLiteral(node, { env }) {
ignoreFirst: i > 0 && !/^\s/.test(quasi.value.cooked),
ignoreLast:
i < node.expressions.length && !/\s$/.test(quasi.value.cooked),
collapseWhitespace: {
start: i === 0,
end: i >= node.expressions.length,
},
})

if (
Expand Down Expand Up @@ -566,11 +591,17 @@ function transformJavaScript(ast, { env }) {
function transformCss(ast, { env }) {
ast.walk((node) => {
if (node.type === 'css-atrule' && node.name === 'apply') {
let isImportant = /\s+(?:!important|#{(['"]*)!important\1})\s*$/.test(
node.params,
)

node.params = sortClasses(node.params, {
env,
ignoreLast: /\s+(?:!important|#{(['"]*)!important\1})\s*$/.test(
node.params,
),
ignoreLast: isImportant,
collapseWhitespace: {
start: false,
end: !isImportant,
},
})
}
})
Expand Down Expand Up @@ -690,6 +721,10 @@ function transformMelody(ast, { env, changes }) {
isConcat && _key === 'right' && !/^[^\S\r\n]/.test(node.value),
ignoreLast:
isConcat && _key === 'left' && !/[^\S\r\n]$/.test(node.value),
collapseWhitespace: {
start: !(isConcat && _key === 'right'),
end: !(isConcat && _key === 'left'),
},
})
},
})
Expand Down Expand Up @@ -775,13 +810,21 @@ function transformSvelte(ast, { env, changes }) {
env,
ignoreFirst: i > 0 && !/^\s/.test(value.raw),
ignoreLast: i < attr.value.length - 1 && !/\s$/.test(value.raw),
collapseWhitespace: {
start: i === 0,
end: i >= attr.value.length - 1,
},
})
value.data = same
? value.raw
: sortClasses(value.data, {
env,
ignoreFirst: i > 0 && !/^\s/.test(value.data),
ignoreLast: i < attr.value.length - 1 && !/\s$/.test(value.data),
collapseWhitespace: {
start: i === 0,
end: i >= attr.value.length - 1,
},
})
} else if (value.type === 'MustacheTag') {
visit(value.expression, {
Expand Down
7 changes: 7 additions & 0 deletions src/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export const options = {
description:
'List of functions and tagged templates that contain sortable Tailwind classes',
},
tailwindPreserveWhitespace: {
since: '0.6.0',
type: 'boolean',
default: [{ value: false }],
category: 'Tailwind CSS',
description: 'Preserve whitespace around Tailwind classes when sorting',
},
}

/** @typedef {import('prettier').RequiredOptions} RequiredOptions */
Expand Down
56 changes: 52 additions & 4 deletions src/sorting.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,46 @@ function getClassOrderPolyfill(classes, { env }) {
return classNamesWithOrder
}

/**
* @param {string} classStr
* @param {object} opts
* @param {any} opts.env
* @param {boolean} [opts.ignoreFirst]
* @param {boolean} [opts.ignoreLast]
* @param {object} [opts.collapseWhitespace]
* @param {boolean} [opts.collapseWhitespace.start]
* @param {boolean} [opts.collapseWhitespace.end]
* @returns {string}
*/
export function sortClasses(
classStr,
{ env, ignoreFirst = false, ignoreLast = false },
{
env,
ignoreFirst = false,
ignoreLast = false,
collapseWhitespace = { start: true, end: true },
},
) {
if (typeof classStr !== 'string' || classStr === '') {
return classStr
}

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

if (env.options.tailwindPreserveWhitespace) {
collapseWhitespace = false
}

// This class list is purely whitespace
// Collapse it to a single space if the option is enabled
if (/^[\t\r\f\n ]+$/.test(classStr) && collapseWhitespace) {
return ' '
}

let result = ''
let parts = classStr.split(/([\t\r\f\n ]+)/)
let classes = parts.filter((_, i) => i % 2 === 0)
Expand All @@ -62,6 +88,10 @@ export function sortClasses(
classes.pop()
}

if (collapseWhitespace) {
whitespace = whitespace.map(() => ' ')
}

let prefix = ''
if (ignoreFirst) {
prefix = `${classes.shift() ?? ''}${whitespace.shift() ?? ''}`
Expand All @@ -72,12 +102,32 @@ export function sortClasses(
suffix = `${whitespace.pop() ?? ''}${classes.pop() ?? ''}`
}

// Remove duplicates
classes = classes.filter((cls, index, arr) => {
if (arr.indexOf(cls) === index) {
return true
}

whitespace.splice(index - 1, 1)

return false
})

classes = sortClassList(classes, { env })

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

if (collapseWhitespace) {
prefix = prefix.replace(/\s+$/g, ' ')
suffix = suffix.replace(/^\s+/g, ' ')

result = result
.replace(/^\s+/, collapseWhitespace.start ? '' : ' ')
.replace(/\s+$/, collapseWhitespace.end ? '' : ' ')
}

return prefix + result + suffix
}

Expand All @@ -89,8 +139,6 @@ export function sortClassList(classList, { env }) {
return classNamesWithOrder
.sort(([, a], [, z]) => {
if (a === z) return 0
// if (a === null) return options.unknownClassPosition === 'start' ? -1 : 1
// if (z === null) return options.unknownClassPosition === 'start' ? 1 : -1
if (a === null) return -1
if (z === null) return 1
return bigSign(a - z)
Expand Down
Loading

0 comments on commit 1f83aae

Please sign in to comment.