Skip to content

Commit 2abf228

Browse files
authored
Minify arbitrary values when printing candidates (#14720)
This PR will optimize and simplify the candidates when printing the candidate again after running codemods. When we parse a candidate, we will add spaces around operators, for example `p-[calc(1px+1px)]]` will internally be handled as `calc(1px + 1px)`. Before this change, we would re-print this as: `p-[calc(1px_+_1px)]`. This PR changes that by simplifying the candidate again so that the output is `p-[calc(1px+1px)]`. In addition, if _you_ wrote `p-[calc(1px_+_1px)]` then we will also simplify it to the concise form `p-[calc(1px_+_1px)]`. Some examples: Input: ```html <div class="[p]:flex"></div> <div class="[&:is(p)]:flex"></div> <div class="has-[p]:flex"></div> <div class="px-[theme(spacing.4)-1px]"></div> ``` Output before: ```html <div class="[&:is(p)]:flex"></div> <div class="[&:is(p)]:flex"></div> <div class="has-[&:is(p)]:flex"></div> <div class="px-[var(--spacing-4)_-_1px]"></div> ``` Output after: ```html <div class="[p]:flex"></div> <div class="[p]:flex"></div> <div class="has-[p]:flex"></div> <div class="px-[var(--spacing-4)-1px]"></div> ``` --- This is alternative implementation to #14717 and #14718 Closes: #14717 Closes: #14718
1 parent c4b97f6 commit 2abf228

File tree

6 files changed

+140
-37
lines changed

6 files changed

+140
-37
lines changed

CHANGELOG.md

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

1616
- Allow spaces spaces around operators in attribute selector variants ([#14703](https://github.com/tailwindlabs/tailwindcss/pull/14703))
1717
- _Upgrade (experimental)_: Migrate `flex-grow` to `grow` and `flex-shrink` to `shrink` ([#14721](https://github.com/tailwindlabs/tailwindcss/pull/14721))
18+
- _Upgrade (experimental)_: Minify arbitrary values when printing candidates ([#14720](https://github.com/tailwindlabs/tailwindcss/pull/14720))
1819

1920
### Changed
2021

packages/@tailwindcss-upgrade/src/template/candidates.test.ts

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -108,40 +108,65 @@ const candidates = [
108108
['bg-[#0088cc]/[0.5]', 'bg-[#0088cc]/[0.5]'],
109109
['bg-[#0088cc]!', 'bg-[#0088cc]!'],
110110
['!bg-[#0088cc]', 'bg-[#0088cc]!'],
111+
['bg-[var(--spacing)-1px]', 'bg-[var(--spacing)-1px]'],
112+
['bg-[var(--spacing)_-_1px]', 'bg-[var(--spacing)-1px]'],
113+
['bg-[-1px_-1px]', 'bg-[-1px_-1px]'],
114+
['p-[round(to-zero,1px)]', 'p-[round(to-zero,1px)]'],
111115
['w-1/2', 'w-1/2'],
116+
['p-[calc((100vw-theme(maxWidth.2xl))_/_2)]', 'p-[calc((100vw-theme(maxWidth.2xl))/2)]'],
117+
118+
// Keep spaces in strings
119+
['content-["hello_world"]', 'content-["hello_world"]'],
120+
['content-[____"hello_world"___]', 'content-["hello_world"]'],
112121
]
113122

114123
const variants = [
115-
'', // no variant
116-
'*:',
117-
'focus:',
118-
'group-focus:',
119-
120-
'hover:focus:',
121-
'hover:group-focus:',
122-
'group-hover:focus:',
123-
'group-hover:group-focus:',
124-
125-
'min-[10px]:',
126-
// TODO: This currently expands `calc(1000px+12em)` to `calc(1000px_+_12em)`
127-
'min-[calc(1000px_+_12em)]:',
128-
129-
'peer-[&_p]:',
130-
'peer-[&_p]:hover:',
131-
'hover:peer-[&_p]:',
132-
'hover:peer-[&_p]:focus:',
133-
'peer-[&:hover]:peer-[&_p]:',
124+
['', ''], // no variant
125+
['*:', '*:'],
126+
['focus:', 'focus:'],
127+
['group-focus:', 'group-focus:'],
128+
129+
['hover:focus:', 'hover:focus:'],
130+
['hover:group-focus:', 'hover:group-focus:'],
131+
['group-hover:focus:', 'group-hover:focus:'],
132+
['group-hover:group-focus:', 'group-hover:group-focus:'],
133+
134+
['min-[10px]:', 'min-[10px]:'],
135+
136+
// Normalize spaces
137+
['min-[calc(1000px_+_12em)]:', 'min-[calc(1000px+12em)]:'],
138+
['min-[calc(1000px_+12em)]:', 'min-[calc(1000px+12em)]:'],
139+
['min-[calc(1000px+_12em)]:', 'min-[calc(1000px+12em)]:'],
140+
['min-[calc(1000px___+___12em)]:', 'min-[calc(1000px+12em)]:'],
141+
142+
['peer-[&_p]:', 'peer-[&_p]:'],
143+
['peer-[&_p]:hover:', 'peer-[&_p]:hover:'],
144+
['hover:peer-[&_p]:', 'hover:peer-[&_p]:'],
145+
['hover:peer-[&_p]:focus:', 'hover:peer-[&_p]:focus:'],
146+
['peer-[&:hover]:peer-[&_p]:', 'peer-[&:hover]:peer-[&_p]:'],
147+
148+
['[p]:', '[p]:'],
149+
['[_p_]:', '[p]:'],
150+
['has-[p]:', 'has-[p]:'],
151+
['has-[_p_]:', 'has-[p]:'],
152+
153+
// Simplify `&:is(p)` to `p`
154+
['[&:is(p)]:', '[p]:'],
155+
['[&:is(_p_)]:', '[p]:'],
156+
['has-[&:is(p)]:', 'has-[p]:'],
157+
['has-[&:is(_p_)]:', 'has-[p]:'],
134158
]
135159

136160
let combinations: [string, string][] = []
137-
for (let variant of variants) {
138-
for (let [input, output] of candidates) {
139-
combinations.push([`${variant}${input}`, `${variant}${output}`])
161+
162+
for (let [inputVariant, outputVariant] of variants) {
163+
for (let [inputCandidate, outputCandidate] of candidates) {
164+
combinations.push([`${inputVariant}${inputCandidate}`, `${outputVariant}${outputCandidate}`])
140165
}
141166
}
142167

143168
describe('printCandidate()', () => {
144-
test.each(combinations)('%s', async (candidate: string, result: string) => {
169+
test.each(combinations)('%s -> %s', async (candidate: string, result: string) => {
145170
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
146171
base: __dirname,
147172
})

packages/@tailwindcss-upgrade/src/template/candidates.ts

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Scanner } from '@tailwindcss/oxide'
22
import type { Candidate, Variant } from '../../../tailwindcss/src/candidate'
33
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
4+
import * as ValueParser from '../../../tailwindcss/src/value-parser'
45

56
export async function extractRawCandidates(
67
content: string,
@@ -51,9 +52,9 @@ export function printCandidate(designSystem: DesignSystem, candidate: Candidate)
5152
if (candidate.value === null) {
5253
base += ''
5354
} else if (candidate.value.dataType) {
54-
base += `-[${candidate.value.dataType}:${escapeArbitrary(candidate.value.value)}]`
55+
base += `-[${candidate.value.dataType}:${printArbitraryValue(candidate.value.value)}]`
5556
} else {
56-
base += `-[${escapeArbitrary(candidate.value.value)}]`
57+
base += `-[${printArbitraryValue(candidate.value.value)}]`
5758
}
5859
} else if (candidate.value.kind === 'named') {
5960
base += `-${candidate.value.value}`
@@ -63,14 +64,14 @@ export function printCandidate(designSystem: DesignSystem, candidate: Candidate)
6364

6465
// Handle arbitrary
6566
if (candidate.kind === 'arbitrary') {
66-
base += `[${candidate.property}:${escapeArbitrary(candidate.value)}]`
67+
base += `[${candidate.property}:${printArbitraryValue(candidate.value)}]`
6768
}
6869

6970
// Handle modifier
7071
if (candidate.kind === 'arbitrary' || candidate.kind === 'functional') {
7172
if (candidate.modifier) {
7273
if (candidate.modifier.kind === 'arbitrary') {
73-
base += `/[${escapeArbitrary(candidate.modifier.value)}]`
74+
base += `/[${printArbitraryValue(candidate.modifier.value)}]`
7475
} else if (candidate.modifier.kind === 'named') {
7576
base += `/${candidate.modifier.value}`
7677
}
@@ -95,7 +96,7 @@ function printVariant(variant: Variant) {
9596

9697
// Handle arbitrary variants
9798
if (variant.kind === 'arbitrary') {
98-
return `[${escapeArbitrary(variant.selector)}]`
99+
return `[${printArbitraryValue(simplifyArbitraryVariant(variant.selector))}]`
99100
}
100101

101102
let base: string = ''
@@ -105,7 +106,7 @@ function printVariant(variant: Variant) {
105106
base += variant.root
106107
if (variant.value) {
107108
if (variant.value.kind === 'arbitrary') {
108-
base += `-[${escapeArbitrary(variant.value.value)}]`
109+
base += `-[${printArbitraryValue(variant.value.value)}]`
109110
} else if (variant.value.kind === 'named') {
110111
base += `-${variant.value.value}`
111112
}
@@ -123,7 +124,7 @@ function printVariant(variant: Variant) {
123124
if (variant.kind === 'functional' || variant.kind === 'compound') {
124125
if (variant.modifier) {
125126
if (variant.modifier.kind === 'arbitrary') {
126-
base += `/[${escapeArbitrary(variant.modifier.value)}]`
127+
base += `/[${printArbitraryValue(variant.modifier.value)}]`
127128
} else if (variant.modifier.kind === 'named') {
128129
base += `/${variant.modifier.value}`
129130
}
@@ -133,8 +134,82 @@ function printVariant(variant: Variant) {
133134
return base
134135
}
135136

136-
function escapeArbitrary(input: string) {
137-
return input
137+
function printArbitraryValue(input: string) {
138+
let ast = ValueParser.parse(input)
139+
140+
let drop = new Set<ValueParser.ValueAstNode>()
141+
142+
ValueParser.walk(ast, (node, { parent }) => {
143+
let parentArray = parent === null ? ast : (parent.nodes ?? [])
144+
145+
// Handle operators (e.g.: inside of `calc(…)`)
146+
if (
147+
node.kind === 'word' &&
148+
// Operators
149+
(node.value === '+' || node.value === '-' || node.value === '*' || node.value === '/')
150+
) {
151+
let idx = parentArray.indexOf(node) ?? -1
152+
153+
// This should not be possible
154+
if (idx === -1) return
155+
156+
let previous = parentArray[idx - 1]
157+
if (previous?.kind !== 'separator' || previous.value !== ' ') return
158+
159+
let next = parentArray[idx + 1]
160+
if (next?.kind !== 'separator' || next.value !== ' ') return
161+
162+
drop.add(previous)
163+
drop.add(next)
164+
}
165+
166+
// The value parser handles `/` as a separator in some scenarios. E.g.:
167+
// `theme(colors.red/50%)`. Because of this, we have to handle this case
168+
// separately.
169+
else if (node.kind === 'separator' && node.value.trim() === '/') {
170+
node.value = '/'
171+
}
172+
173+
// Leading and trailing whitespace
174+
else if (node.kind === 'separator' && node.value.length > 0 && node.value.trim() === '') {
175+
if (parentArray[0] === node || parentArray[parentArray.length - 1] === node) {
176+
drop.add(node)
177+
}
178+
}
179+
})
180+
181+
if (drop.size > 0) {
182+
ValueParser.walk(ast, (node, { replaceWith }) => {
183+
if (drop.has(node)) {
184+
drop.delete(node)
185+
replaceWith([])
186+
}
187+
})
188+
}
189+
190+
return ValueParser.toCss(ast)
138191
.replaceAll('_', String.raw`\_`) // Escape underscores to keep them as-is
139192
.replaceAll(' ', '_') // Replace spaces with underscores
140193
}
194+
195+
function simplifyArbitraryVariant(input: string) {
196+
let ast = ValueParser.parse(input)
197+
198+
// &:is(…)
199+
if (
200+
ast.length === 3 &&
201+
// &
202+
ast[0].kind === 'word' &&
203+
ast[0].value === '&' &&
204+
// :
205+
ast[1].kind === 'separator' &&
206+
ast[1].value === ':' &&
207+
// is(…)
208+
ast[2].kind === 'function' &&
209+
ast[2].value === 'is'
210+
) {
211+
return ValueParser.toCss(ast[2].nodes)
212+
}
213+
214+
return input
215+
}

packages/@tailwindcss-upgrade/src/template/codemods/important.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { important } from './important'
44

55
test.each([
66
['!flex', 'flex!'],
7-
['min-[calc(1000px+12em)]:!flex', 'min-[calc(1000px_+_12em)]:flex!'],
7+
['min-[calc(1000px+12em)]:!flex', 'min-[calc(1000px+12em)]:flex!'],
88
['md:!block', 'md:block!'],
99

1010
// Does not change non-important candidates

packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ test.each([
2626
],
2727

2828
// Use `theme(…)` (deeply nested) inside of a `calc(…)` function
29-
['text-[calc(theme(fontSize.xs)*2)]', 'text-[calc(var(--font-size-xs)_*_2)]'],
29+
['text-[calc(theme(fontSize.xs)*2)]', 'text-[calc(var(--font-size-xs)*2)]'],
3030

3131
// Multiple `theme(… / …)` calls should result in modern syntax of `theme(…)`
3232
// - Can't convert to `var(…)` because that would lose the modifier.

packages/tailwindcss/src/value-parser.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type ValueSeparatorNode = {
1515
}
1616

1717
export type ValueAstNode = ValueWordNode | ValueFunctionNode | ValueSeparatorNode
18+
type ValueParentNode = ValueFunctionNode | null
1819

1920
function word(value: string): ValueWordNode {
2021
return {
@@ -54,11 +55,11 @@ export function walk(
5455
visit: (
5556
node: ValueAstNode,
5657
utils: {
57-
parent: ValueAstNode | null
58+
parent: ValueParentNode
5859
replaceWith(newNode: ValueAstNode | ValueAstNode[]): void
5960
},
6061
) => void | ValueWalkAction,
61-
parent: ValueAstNode | null = null,
62+
parent: ValueParentNode = null,
6263
) {
6364
for (let i = 0; i < ast.length; i++) {
6465
let node = ast[i]
@@ -149,7 +150,7 @@ export function parse(input: string) {
149150
case GREATER_THAN:
150151
case EQUALS: {
151152
// 1. Handle everything before the separator as a word
152-
// Handle everything before the closing paren a word
153+
// Handle everything before the closing paren as a word
153154
if (buffer.length > 0) {
154155
let node = word(buffer)
155156
if (parent) {
@@ -169,6 +170,7 @@ export function parse(input: string) {
169170
peekChar !== COLON &&
170171
peekChar !== COMMA &&
171172
peekChar !== SPACE &&
173+
peekChar !== SLASH &&
172174
peekChar !== LESS_THAN &&
173175
peekChar !== GREATER_THAN &&
174176
peekChar !== EQUALS

0 commit comments

Comments
 (0)