|
1 | | -import { parseCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate' |
| 1 | +import { |
| 2 | + parseCandidate, |
| 3 | + type Candidate, |
| 4 | + type NamedUtilityValue, |
| 5 | + type Variant, |
| 6 | +} from '../../../../tailwindcss/src/candidate' |
2 | 7 | import type { Config } from '../../../../tailwindcss/src/compat/plugin-api' |
3 | 8 | import type { DesignSystem } from '../../../../tailwindcss/src/design-system' |
4 | | -import { isPositiveInteger } from '../../../../tailwindcss/src/utils/infer-data-type' |
| 9 | +import { |
| 10 | + isPositiveInteger, |
| 11 | + isValidSpacingMultiplier, |
| 12 | +} from '../../../../tailwindcss/src/utils/infer-data-type' |
5 | 13 | import { segment } from '../../../../tailwindcss/src/utils/segment' |
6 | 14 | import { printCandidate } from './candidates' |
| 15 | +import { computeUtilitySignature } from './signatures' |
7 | 16 |
|
8 | 17 | export function migrateArbitraryValueToBareValue( |
9 | 18 | designSystem: DesignSystem, |
10 | 19 | _userConfig: Config | null, |
11 | 20 | rawCandidate: string, |
12 | 21 | ): string { |
| 22 | + let signatures = computeUtilitySignature.get(designSystem) |
| 23 | + |
13 | 24 | for (let candidate of parseCandidate(rawCandidate, designSystem)) { |
14 | 25 | let clone = structuredClone(candidate) |
15 | 26 | let changed = false |
16 | 27 |
|
17 | | - // Convert [subgrid] to subgrid |
18 | | - if ( |
19 | | - clone.kind === 'functional' && |
20 | | - clone.value?.kind === 'arbitrary' && |
21 | | - clone.value.value === 'subgrid' && |
22 | | - (clone.root === 'grid-cols' || clone.root == 'grid-rows') |
23 | | - ) { |
24 | | - changed = true |
25 | | - clone.value = { |
26 | | - kind: 'named', |
27 | | - value: 'subgrid', |
28 | | - fraction: null, |
29 | | - } |
30 | | - } |
31 | | - |
32 | | - // Convert utilities that accept bare values ending in % |
33 | | - if ( |
34 | | - clone.kind === 'functional' && |
35 | | - clone.value?.kind === 'arbitrary' && |
36 | | - clone.value.dataType === null && |
37 | | - (clone.root === 'from' || |
38 | | - clone.root === 'via' || |
39 | | - clone.root === 'to' || |
40 | | - clone.root === 'font-stretch') |
41 | | - ) { |
42 | | - if (clone.value.value.endsWith('%') && isPositiveInteger(clone.value.value.slice(0, -1))) { |
43 | | - let percentage = parseInt(clone.value.value) |
44 | | - if ( |
45 | | - clone.root === 'from' || |
46 | | - clone.root === 'via' || |
47 | | - clone.root === 'to' || |
48 | | - (clone.root === 'font-stretch' && percentage >= 50 && percentage <= 200) |
49 | | - ) { |
50 | | - changed = true |
51 | | - clone.value = { |
52 | | - kind: 'named', |
53 | | - value: clone.value.value, |
54 | | - fraction: null, |
| 28 | + // Migrate arbitrary values to bare values |
| 29 | + if (clone.kind === 'functional' && clone.value?.kind === 'arbitrary') { |
| 30 | + let expectedSignature = signatures.get(rawCandidate) |
| 31 | + if (expectedSignature !== null) { |
| 32 | + for (let value of tryValueReplacements(clone)) { |
| 33 | + let newSignature = signatures.get(printCandidate(designSystem, { ...clone, value })) |
| 34 | + if (newSignature === expectedSignature) { |
| 35 | + changed = true |
| 36 | + clone.value = value |
| 37 | + break |
55 | 38 | } |
56 | 39 | } |
57 | 40 | } |
58 | 41 | } |
59 | 42 |
|
60 | | - // Convert arbitrary values with positive integers to bare values |
61 | | - // Convert arbitrary values with fractions to bare values |
62 | | - else if ( |
63 | | - clone.kind === 'functional' && |
64 | | - clone.value?.kind === 'arbitrary' && |
65 | | - clone.value.dataType === null |
66 | | - ) { |
67 | | - if (clone.root === 'leading') { |
68 | | - // leading-[1] -> leading-none |
69 | | - if (clone.value.value === '1') { |
70 | | - changed = true |
71 | | - clone.value = { |
72 | | - kind: 'named', |
73 | | - value: 'none', |
74 | | - fraction: null, |
75 | | - } |
76 | | - } |
77 | | - |
78 | | - // Keep leading-[<number>] as leading-[<number>] |
79 | | - else { |
80 | | - continue |
81 | | - } |
82 | | - } |
83 | | - |
84 | | - let parts = segment(clone.value.value, '/') |
85 | | - if (parts.every((part) => isPositiveInteger(part))) { |
86 | | - changed = true |
87 | | - |
88 | | - let currentValue = clone.value |
89 | | - let currentModifier = clone.modifier |
90 | | - |
91 | | - // E.g.: `col-start-[12]` |
92 | | - // ^^ |
93 | | - if (parts.length === 1) { |
94 | | - clone.value = { |
95 | | - kind: 'named', |
96 | | - value: clone.value.value, |
97 | | - fraction: null, |
98 | | - } |
99 | | - } |
100 | | - |
101 | | - // E.g.: `aspect-[12/34]` |
102 | | - // ^^ ^^ |
103 | | - else { |
104 | | - clone.value = { |
105 | | - kind: 'named', |
106 | | - value: parts[0], |
107 | | - fraction: clone.value.value, |
108 | | - } |
109 | | - clone.modifier = { |
110 | | - kind: 'named', |
111 | | - value: parts[1], |
112 | | - } |
113 | | - } |
114 | | - |
115 | | - // Double check that the new value compiles correctly |
116 | | - if (designSystem.compileAstNodes(clone).length === 0) { |
117 | | - clone.value = currentValue |
118 | | - clone.modifier = currentModifier |
119 | | - changed = false |
120 | | - } |
121 | | - } |
122 | | - } |
123 | | - |
124 | 43 | for (let variant of variants(clone)) { |
125 | 44 | // Convert `data-[selected]` to `data-selected` |
126 | 45 | if ( |
@@ -201,3 +120,75 @@ function* variants(candidate: Candidate) { |
201 | 120 | yield* inner(variant) |
202 | 121 | } |
203 | 122 | } |
| 123 | + |
| 124 | +// Convert functional utilities with arbitrary values to bare values if we can. |
| 125 | +// We know that bare values can only be: |
| 126 | +// |
| 127 | +// 1. A number (with increments of .25) |
| 128 | +// 2. A percentage (with increments of .25 followed by a `%`) |
| 129 | +// 3. A ratio with whole numbers |
| 130 | +// |
| 131 | +// Not a bare value per se, but if we are dealing with a keyword, that could |
| 132 | +// potentially also look like a bare value (aka no `[` or `]`). E.g.: |
| 133 | +// ```diff |
| 134 | +// grid-cols-[subgrid] |
| 135 | +// grid-cols-subgrid |
| 136 | +// ``` |
| 137 | +function* tryValueReplacements( |
| 138 | + candidate: Extract<Candidate, { kind: 'functional' }>, |
| 139 | + value: string = candidate.value?.value ?? '', |
| 140 | + seen: Set<string> = new Set(), |
| 141 | +): Generator<NamedUtilityValue> { |
| 142 | + if (seen.has(value)) return |
| 143 | + seen.add(value) |
| 144 | + |
| 145 | + // 0. Just try to drop the square brackets and see if it works |
| 146 | + // 1. A number (with increments of .25) |
| 147 | + yield { |
| 148 | + kind: 'named', |
| 149 | + value, |
| 150 | + fraction: null, |
| 151 | + } |
| 152 | + |
| 153 | + // 2. A percentage (with increments of .25 followed by a `%`) |
| 154 | + // Try to drop the `%` and see if it works |
| 155 | + if (value.endsWith('%') && isValidSpacingMultiplier(value.slice(0, -1))) { |
| 156 | + yield { |
| 157 | + kind: 'named', |
| 158 | + value: value.slice(0, -1), |
| 159 | + fraction: null, |
| 160 | + } |
| 161 | + } |
| 162 | + |
| 163 | + // 3. A ratio with whole numbers |
| 164 | + if (value.includes('/')) { |
| 165 | + let [numerator, denominator] = value.split('/') |
| 166 | + if (isPositiveInteger(numerator) && isPositiveInteger(denominator)) { |
| 167 | + yield { |
| 168 | + kind: 'named', |
| 169 | + value: numerator, |
| 170 | + fraction: `${numerator}/${denominator}`, |
| 171 | + } |
| 172 | + } |
| 173 | + } |
| 174 | + |
| 175 | + // It could also be that we have `20px`, we can try just `20` and see if it |
| 176 | + // results in the same signature. |
| 177 | + let allNumbersAndFractions = new Set<string>() |
| 178 | + |
| 179 | + // Figure out all numbers and fractions in the value |
| 180 | + for (let match of value.matchAll(/(\d+\/\d+)|(\d+\.?\d+)/g)) { |
| 181 | + allNumbersAndFractions.add(match[0].trim()) |
| 182 | + } |
| 183 | + |
| 184 | + // Sort the numbers and fractions where the smallest length comes first. This |
| 185 | + // will result in the smallest replacement. |
| 186 | + let options = Array.from(allNumbersAndFractions).sort((a, z) => { |
| 187 | + return a.length - z.length |
| 188 | + }) |
| 189 | + |
| 190 | + // Try all the options |
| 191 | + for (let option of options) { |
| 192 | + yield* tryValueReplacements(candidate, option, seen) |
| 193 | + } |
| 194 | +} |
0 commit comments