Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
simplify candidate printing
  • Loading branch information
RobinMalfait committed Oct 18, 2024
commit 2de3ddcde25abe3d365e304f491a449bc4c86a69
69 changes: 46 additions & 23 deletions packages/@tailwindcss-upgrade/src/template/candidates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,40 +108,63 @@ const candidates = [
['bg-[#0088cc]/[0.5]', 'bg-[#0088cc]/[0.5]'],
['bg-[#0088cc]!', 'bg-[#0088cc]!'],
['!bg-[#0088cc]', 'bg-[#0088cc]!'],
['bg-[var(--spacing)-1px]', 'bg-[var(--spacing)-1px]'],
['bg-[var(--spacing)_-_1px]', 'bg-[var(--spacing)-1px]'],
['bg-[-1px_-1px]', 'bg-[-1px_-1px]'],
['w-1/2', 'w-1/2'],

// Keep spaces in strings
['content-["hello_world"]', 'content-["hello_world"]'],
['content-[____"hello_world"___]', 'content-["hello_world"]'],
]

const variants = [
'', // no variant
'*:',
'focus:',
'group-focus:',

'hover:focus:',
'hover:group-focus:',
'group-hover:focus:',
'group-hover:group-focus:',

'min-[10px]:',
// TODO: This currently expands `calc(1000px+12em)` to `calc(1000px_+_12em)`
'min-[calc(1000px_+_12em)]:',

'peer-[&_p]:',
'peer-[&_p]:hover:',
'hover:peer-[&_p]:',
'hover:peer-[&_p]:focus:',
'peer-[&:hover]:peer-[&_p]:',
['', ''], // no variant
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created a tuple with in and out (similar to the candidate above)

['*:', '*:'],
['focus:', 'focus:'],
['group-focus:', 'group-focus:'],

['hover:focus:', 'hover:focus:'],
['hover:group-focus:', 'hover:group-focus:'],
['group-hover:focus:', 'group-hover:focus:'],
['group-hover:group-focus:', 'group-hover:group-focus:'],

['min-[10px]:', 'min-[10px]:'],

// Normalize spaces
['min-[calc(1000px_+_12em)]:', 'min-[calc(1000px+12em)]:'],
['min-[calc(1000px_+12em)]:', 'min-[calc(1000px+12em)]:'],
['min-[calc(1000px+_12em)]:', 'min-[calc(1000px+12em)]:'],
['min-[calc(1000px___+___12em)]:', 'min-[calc(1000px+12em)]:'],

['peer-[&_p]:', 'peer-[&_p]:'],
['peer-[&_p]:hover:', 'peer-[&_p]:hover:'],
['hover:peer-[&_p]:', 'hover:peer-[&_p]:'],
['hover:peer-[&_p]:focus:', 'hover:peer-[&_p]:focus:'],
['peer-[&:hover]:peer-[&_p]:', 'peer-[&:hover]:peer-[&_p]:'],

['[p]:', '[p]:'],
['[_p_]:', '[p]:'],
['has-[p]:', 'has-[p]:'],
['has-[_p_]:', 'has-[p]:'],

// Simplify `&:is(p)` to `p`
['[&:is(p)]:', '[p]:'],
['[&:is(_p_)]:', '[p]:'],
['has-[&:is(p)]:', 'has-[p]:'],
['has-[&:is(_p_)]:', 'has-[p]:'],
]

let combinations: [string, string][] = []
for (let variant of variants) {
for (let [input, output] of candidates) {
combinations.push([`${variant}${input}`, `${variant}${output}`])

for (let [inputVariant, outputVariant] of variants) {
for (let [inputCandidate, outputCandidate] of candidates) {
combinations.push([`${inputVariant}${inputCandidate}`, `${outputVariant}${outputCandidate}`])
}
}

describe('printCandidate()', () => {
test.each(combinations)('%s', async (candidate: string, result: string) => {
test.each(combinations)('%s -> %s', async (candidate: string, result: string) => {
let designSystem = await __unstable__loadDesignSystem('@import "tailwindcss";', {
base: __dirname,
})
Expand Down
85 changes: 78 additions & 7 deletions packages/@tailwindcss-upgrade/src/template/candidates.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Scanner } from '@tailwindcss/oxide'
import type { Candidate, Variant } from '../../../tailwindcss/src/candidate'
import type { DesignSystem } from '../../../tailwindcss/src/design-system'
import * as ValueParser from '../../../tailwindcss/src/value-parser'

export async function extractRawCandidates(
content: string,
Expand Down Expand Up @@ -51,9 +52,9 @@ export function printCandidate(designSystem: DesignSystem, candidate: Candidate)
if (candidate.value === null) {
base += ''
} else if (candidate.value.dataType) {
base += `-[${candidate.value.dataType}:${escapeArbitrary(candidate.value.value)}]`
base += `-[${candidate.value.dataType}:${escapeArbitrary(trimWhitespace(candidate.value.value))}]`
} else {
base += `-[${escapeArbitrary(candidate.value.value)}]`
base += `-[${escapeArbitrary(trimWhitespace(candidate.value.value))}]`
}
} else if (candidate.value.kind === 'named') {
base += `-${candidate.value.value}`
Expand All @@ -63,14 +64,14 @@ export function printCandidate(designSystem: DesignSystem, candidate: Candidate)

// Handle arbitrary
if (candidate.kind === 'arbitrary') {
base += `[${candidate.property}:${escapeArbitrary(candidate.value)}]`
base += `[${candidate.property}:${escapeArbitrary(trimWhitespace(candidate.value))}]`
}

// Handle modifier
if (candidate.kind === 'arbitrary' || candidate.kind === 'functional') {
if (candidate.modifier) {
if (candidate.modifier.kind === 'arbitrary') {
base += `/[${escapeArbitrary(candidate.modifier.value)}]`
base += `/[${escapeArbitrary(trimWhitespace(candidate.modifier.value))}]`
} else if (candidate.modifier.kind === 'named') {
base += `/${candidate.modifier.value}`
}
Expand All @@ -95,7 +96,7 @@ function printVariant(variant: Variant) {

// Handle arbitrary variants
if (variant.kind === 'arbitrary') {
return `[${escapeArbitrary(variant.selector)}]`
return `[${escapeArbitrary(trimWhitespace(simplifyArbitraryVariant(variant.selector)))}]`
}

let base: string = ''
Expand All @@ -105,7 +106,7 @@ function printVariant(variant: Variant) {
base += variant.root
if (variant.value) {
if (variant.value.kind === 'arbitrary') {
base += `-[${escapeArbitrary(variant.value.value)}]`
base += `-[${escapeArbitrary(trimWhitespace(variant.value.value))}]`
} else if (variant.value.kind === 'named') {
base += `-${variant.value.value}`
}
Expand All @@ -123,7 +124,7 @@ function printVariant(variant: Variant) {
if (variant.kind === 'functional' || variant.kind === 'compound') {
if (variant.modifier) {
if (variant.modifier.kind === 'arbitrary') {
base += `/[${escapeArbitrary(variant.modifier.value)}]`
base += `/[${escapeArbitrary(trimWhitespace(variant.modifier.value))}]`
} else if (variant.modifier.kind === 'named') {
base += `/${variant.modifier.value}`
}
Expand All @@ -138,3 +139,73 @@ function escapeArbitrary(input: string) {
.replaceAll('_', String.raw`\_`) // Escape underscores to keep them as-is
.replaceAll(' ', '_') // Replace spaces with underscores
}

function trimWhitespace(input: string) {
let ast = ValueParser.parse(input)
let drop = new Set<ValueParser.ValueAstNode>()

ValueParser.walk(ast, (node, { parent }) => {
let parentArray = parent === null ? ast : (parent.nodes ?? [])

// Handle operators (e.g.: inside of `calc(…)`)
if (
node.kind === 'word' &&
// Operators
(node.value === '+' || node.value === '-' || node.value === '*' || node.value === '/')
) {
let idx = parentArray.indexOf(node) ?? -1

// This should not be possible
if (idx === -1) return

let previous = parentArray[idx - 1]
if (previous?.kind !== 'separator' || previous.value !== ' ') return

let next = parentArray[idx + 1]
if (next?.kind !== 'separator' || next.value !== ' ') return

drop.add(previous)
drop.add(next)
}

// Leading and trailing whitespace
if (node.kind === 'separator' && node.value.length > 0 && node.value.trim() === '') {
if (parentArray[0] === node || parentArray[parentArray.length - 1] === node) {
drop.add(node)
}
}
})

if (drop.size > 0) {
ValueParser.walk(ast, (node, { replaceWith }) => {
if (drop.has(node)) {
drop.delete(node)
replaceWith([])
}
})
}

return ValueParser.toCss(ast)
}

function simplifyArbitraryVariant(input: string) {
let ast = ValueParser.parse(input)

// &:is(…)
if (
ast.length === 3 &&
// &
ast[0].kind === 'word' &&
ast[0].value === '&' &&
// :
ast[1].kind === 'separator' &&
ast[1].value === ':' &&
// is(…)
ast[2].kind === 'function' &&
ast[2].value === 'is'
) {
return ValueParser.toCss(ast[2].nodes)
}

return input
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { important } from './important'

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

// Does not change non-important candidates
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ test.each([
],

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

// Multiple `theme(… / …)` calls should result in modern syntax of `theme(…)`
// - Can't convert to `var(…)` because that would lose the modifier.
Expand Down
5 changes: 3 additions & 2 deletions packages/tailwindcss/src/value-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type ValueSeparatorNode = {
}

export type ValueAstNode = ValueWordNode | ValueFunctionNode | ValueSeparatorNode
type ValueParentNode = ValueFunctionNode | null

function word(value: string): ValueWordNode {
return {
Expand Down Expand Up @@ -54,11 +55,11 @@ export function walk(
visit: (
node: ValueAstNode,
utils: {
parent: ValueAstNode | null
parent: ValueParentNode
replaceWith(newNode: ValueAstNode | ValueAstNode[]): void
},
) => void | ValueWalkAction,
parent: ValueAstNode | null = null,
parent: ValueParentNode = null,
) {
for (let i = 0; i < ast.length; i++) {
let node = ast[i]
Expand Down