|
| 1 | +import type { Candidate, Variant } from '../../../tailwindcss/src/candidate' |
| 2 | +import type { DesignSystem } from '../../../tailwindcss/src/design-system' |
| 3 | +import { Scanner } from '@tailwindcss/oxide' |
| 4 | +import * as ValueParser from './src/value-parser' |
| 5 | + |
| 6 | +export async function extractRawCandidates( |
| 7 | + content: string, |
| 8 | + extension: string = 'html', |
| 9 | +): Promise<{ rawCandidate: string, start: number, end: number }[]> { |
| 10 | + const scanner = new Scanner({}) |
| 11 | + const result = scanner.getCandidatesWithPositions({ content, extension }) |
| 12 | + |
| 13 | + const candidates: { rawCandidate: string, start: number, end: number }[] = [] |
| 14 | + for (const { candidate: rawCandidate, position: start } of result) { |
| 15 | + candidates.push({ rawCandidate, start, end: start + rawCandidate.length }) |
| 16 | + } |
| 17 | + return candidates |
| 18 | +} |
| 19 | + |
| 20 | +export function printCandidate(designSystem: DesignSystem, candidate: Candidate) { |
| 21 | + const parts: string[] = [] |
| 22 | + |
| 23 | + for (const variant of candidate.variants) { |
| 24 | + parts.unshift(printVariant(variant)) |
| 25 | + } |
| 26 | + |
| 27 | + // Handle prefix |
| 28 | + if (designSystem.theme.prefix) { |
| 29 | + parts.unshift(designSystem.theme.prefix) |
| 30 | + } |
| 31 | + |
| 32 | + let base: string = '' |
| 33 | + |
| 34 | + // Handle static |
| 35 | + if (candidate.kind === 'static') { |
| 36 | + base += candidate.root |
| 37 | + } |
| 38 | + |
| 39 | + // Handle functional |
| 40 | + if (candidate.kind === 'functional') { |
| 41 | + base += candidate.root |
| 42 | + |
| 43 | + if (candidate.value) { |
| 44 | + if (candidate.value.kind === 'arbitrary') { |
| 45 | + if (candidate.value !== null) { |
| 46 | + const isVarValue = isVar(candidate.value.value) |
| 47 | + const value = isVarValue ? candidate.value.value.slice(4, -1) : candidate.value.value |
| 48 | + const [open, close] = isVarValue ? ['(', ')'] : ['[', ']'] |
| 49 | + |
| 50 | + if (candidate.value.dataType) { |
| 51 | + base += `-${open}${candidate.value.dataType}:${printArbitraryValue(value)}${close}` |
| 52 | + } |
| 53 | + else { |
| 54 | + base += `-${open}${printArbitraryValue(value)}${close}` |
| 55 | + } |
| 56 | + } |
| 57 | + } |
| 58 | + else if (candidate.value.kind === 'named') { |
| 59 | + base += `-${candidate.value.value}` |
| 60 | + } |
| 61 | + } |
| 62 | + } |
| 63 | + |
| 64 | + // Handle arbitrary |
| 65 | + if (candidate.kind === 'arbitrary') { |
| 66 | + base += `[${candidate.property}:${printArbitraryValue(candidate.value)}]` |
| 67 | + } |
| 68 | + |
| 69 | + // Handle modifier |
| 70 | + if (candidate.kind === 'arbitrary' || candidate.kind === 'functional') { |
| 71 | + if (candidate.modifier) { |
| 72 | + const isVarValue = isVar(candidate.modifier.value) |
| 73 | + const value = isVarValue ? candidate.modifier.value.slice(4, -1) : candidate.modifier.value |
| 74 | + const [open, close] = isVarValue ? ['(', ')'] : ['[', ']'] |
| 75 | + |
| 76 | + if (candidate.modifier.kind === 'arbitrary') { |
| 77 | + base += `/${open}${printArbitraryValue(value)}${close}` |
| 78 | + } |
| 79 | + else if (candidate.modifier.kind === 'named') { |
| 80 | + base += `/${candidate.modifier.value}` |
| 81 | + } |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + // Handle important |
| 86 | + if (candidate.important) { |
| 87 | + base += '!' |
| 88 | + } |
| 89 | + |
| 90 | + parts.push(base) |
| 91 | + |
| 92 | + return parts.join(':') |
| 93 | +} |
| 94 | + |
| 95 | +function printVariant(variant: Variant) { |
| 96 | + // Handle static variants |
| 97 | + if (variant.kind === 'static') { |
| 98 | + return variant.root |
| 99 | + } |
| 100 | + |
| 101 | + // Handle arbitrary variants |
| 102 | + if (variant.kind === 'arbitrary') { |
| 103 | + return `[${printArbitraryValue(simplifyArbitraryVariant(variant.selector))}]` |
| 104 | + } |
| 105 | + |
| 106 | + let base: string = '' |
| 107 | + |
| 108 | + // Handle functional variants |
| 109 | + if (variant.kind === 'functional') { |
| 110 | + base += variant.root |
| 111 | + if (variant.value) { |
| 112 | + if (variant.value.kind === 'arbitrary') { |
| 113 | + const isVarValue = isVar(variant.value.value) |
| 114 | + const value = isVarValue ? variant.value.value.slice(4, -1) : variant.value.value |
| 115 | + const [open, close] = isVarValue ? ['(', ')'] : ['[', ']'] |
| 116 | + |
| 117 | + base += `-${open}${printArbitraryValue(value)}${close}` |
| 118 | + } |
| 119 | + else if (variant.value.kind === 'named') { |
| 120 | + base += `-${variant.value.value}` |
| 121 | + } |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + // Handle compound variants |
| 126 | + if (variant.kind === 'compound') { |
| 127 | + base += variant.root |
| 128 | + base += '-' |
| 129 | + base += printVariant(variant.variant) |
| 130 | + } |
| 131 | + |
| 132 | + // Handle modifiers |
| 133 | + if (variant.kind === 'functional' || variant.kind === 'compound') { |
| 134 | + if (variant.modifier) { |
| 135 | + if (variant.modifier.kind === 'arbitrary') { |
| 136 | + base += `/[${printArbitraryValue(variant.modifier.value)}]` |
| 137 | + } |
| 138 | + else if (variant.modifier.kind === 'named') { |
| 139 | + base += `/${variant.modifier.value}` |
| 140 | + } |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | + return base |
| 145 | +} |
| 146 | + |
| 147 | +function printArbitraryValue(input: string) { |
| 148 | + const ast = ValueParser.parse(input) |
| 149 | + |
| 150 | + const drop = new Set<ValueParser.ValueAstNode>() |
| 151 | + |
| 152 | + ValueParser.walk(ast, (node, { parent }) => { |
| 153 | + const parentArray = parent === null ? ast : (parent.nodes ?? []) |
| 154 | + |
| 155 | + // Handle operators (e.g.: inside of `calc(…)`) |
| 156 | + if ( |
| 157 | + node.kind === 'word' |
| 158 | + // Operators |
| 159 | + && (node.value === '+' || node.value === '-' || node.value === '*' || node.value === '/') |
| 160 | + ) { |
| 161 | + const idx = parentArray.indexOf(node) ?? -1 |
| 162 | + |
| 163 | + // This should not be possible |
| 164 | + if (idx === -1) { return } |
| 165 | + |
| 166 | + const previous = parentArray[idx - 1] |
| 167 | + if (previous?.kind !== 'separator' || previous.value !== ' ') { return } |
| 168 | + |
| 169 | + const next = parentArray[idx + 1] |
| 170 | + if (next?.kind !== 'separator' || next.value !== ' ') { return } |
| 171 | + |
| 172 | + drop.add(previous) |
| 173 | + drop.add(next) |
| 174 | + } |
| 175 | + |
| 176 | + // The value parser handles `/` as a separator in some scenarios. E.g.: |
| 177 | + // `theme(colors.red/50%)`. Because of this, we have to handle this case |
| 178 | + // separately. |
| 179 | + else if (node.kind === 'separator' && node.value.trim() === '/') { |
| 180 | + node.value = '/' |
| 181 | + } |
| 182 | + |
| 183 | + // Leading and trailing whitespace |
| 184 | + else if (node.kind === 'separator' && node.value.length > 0 && node.value.trim() === '') { |
| 185 | + if (parentArray[0] === node || parentArray[parentArray.length - 1] === node) { |
| 186 | + drop.add(node) |
| 187 | + } |
| 188 | + } |
| 189 | + |
| 190 | + // Whitespace around `,` separators can be removed. |
| 191 | + // E.g.: `min(1px , 2px)` -> `min(1px,2px)` |
| 192 | + else if (node.kind === 'separator' && node.value.trim() === ',') { |
| 193 | + node.value = ',' |
| 194 | + } |
| 195 | + }) |
| 196 | + |
| 197 | + if (drop.size > 0) { |
| 198 | + ValueParser.walk(ast, (node, { replaceWith }) => { |
| 199 | + if (drop.has(node)) { |
| 200 | + drop.delete(node) |
| 201 | + replaceWith([]) |
| 202 | + } |
| 203 | + }) |
| 204 | + } |
| 205 | + |
| 206 | + recursivelyEscapeUnderscores(ast) |
| 207 | + |
| 208 | + return ValueParser.toCss(ast) |
| 209 | +} |
| 210 | + |
| 211 | +function simplifyArbitraryVariant(input: string) { |
| 212 | + const ast = ValueParser.parse(input) |
| 213 | + |
| 214 | + // &:is(…) |
| 215 | + if ( |
| 216 | + ast.length === 3 |
| 217 | + // & |
| 218 | + && ast[0].kind === 'word' |
| 219 | + && ast[0].value === '&' |
| 220 | + // : |
| 221 | + && ast[1].kind === 'separator' |
| 222 | + && ast[1].value === ':' |
| 223 | + // is(…) |
| 224 | + && ast[2].kind === 'function' |
| 225 | + && ast[2].value === 'is' |
| 226 | + ) { |
| 227 | + return ValueParser.toCss(ast[2].nodes) |
| 228 | + } |
| 229 | + |
| 230 | + return input |
| 231 | +} |
| 232 | + |
| 233 | +function recursivelyEscapeUnderscores(ast: ValueParser.ValueAstNode[]) { |
| 234 | + for (const node of ast) { |
| 235 | + switch (node.kind) { |
| 236 | + case 'function': { |
| 237 | + if (node.value === 'url' || node.value.endsWith('_url')) { |
| 238 | + // Don't decode underscores in url() but do decode the function name |
| 239 | + node.value = escapeUnderscore(node.value) |
| 240 | + break |
| 241 | + } |
| 242 | + |
| 243 | + if ( |
| 244 | + node.value === 'var' |
| 245 | + || node.value.endsWith('_var') |
| 246 | + || node.value === 'theme' |
| 247 | + || node.value.endsWith('_theme') |
| 248 | + ) { |
| 249 | + // Don't decode underscores in the first argument of var() and theme() |
| 250 | + // but do decode the function name |
| 251 | + node.value = escapeUnderscore(node.value) |
| 252 | + for (let i = 0; i < node.nodes.length; i++) { |
| 253 | + if (i == 0 && node.nodes[i].kind === 'word') { |
| 254 | + continue |
| 255 | + } |
| 256 | + recursivelyEscapeUnderscores([node.nodes[i]]) |
| 257 | + } |
| 258 | + break |
| 259 | + } |
| 260 | + |
| 261 | + node.value = escapeUnderscore(node.value) |
| 262 | + recursivelyEscapeUnderscores(node.nodes) |
| 263 | + break |
| 264 | + } |
| 265 | + case 'separator': |
| 266 | + node.value = escapeUnderscore(node.value) |
| 267 | + break |
| 268 | + case 'word': { |
| 269 | + // Dashed idents and variables `var(--my-var)` and `--my-var` should not |
| 270 | + // have underscores escaped |
| 271 | + if (node.value[0] !== '-' && node.value[1] !== '-') { |
| 272 | + node.value = escapeUnderscore(node.value) |
| 273 | + } |
| 274 | + break |
| 275 | + } |
| 276 | + default: |
| 277 | + never(node) |
| 278 | + } |
| 279 | + } |
| 280 | +} |
| 281 | + |
| 282 | +function isVar(value: string) { |
| 283 | + const ast = ValueParser.parse(value) |
| 284 | + return ast.length === 1 && ast[0].kind === 'function' && ast[0].value === 'var' |
| 285 | +} |
| 286 | + |
| 287 | +function never(value: never): never { |
| 288 | + throw new Error(`Unexpected value: ${value}`) |
| 289 | +} |
| 290 | + |
| 291 | +function escapeUnderscore(value: string): string { |
| 292 | + return value |
| 293 | + .replaceAll('_', String.raw`\_`) // Escape underscores to keep them as-is |
| 294 | + .replaceAll(' ', '_') // Replace spaces with underscores |
| 295 | +} |
0 commit comments