import { printModifier, type Candidate } from '../../../../tailwindcss/src/candidate'
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
import { dimensions } from '../../utils/dimension'
import type { Writable } from '../../utils/types'
import { baseCandidate, parseCandidate } from './candidates'
import { computeUtilitySignature, preComputedUtilities } from './signatures'
const baseReplacementsCache = new DefaultMap<DesignSystem, Map<string, Candidate>>(
() => new Map<string, Candidate>(),
)
const spacing = new DefaultMap<DesignSystem, DefaultMap<string, number | null> | null>((ds) => {
let spacingMultiplier = ds.resolveThemeValue('--spacing')
if (spacingMultiplier === undefined) return null
let parsed = dimensions.get(spacingMultiplier)
if (!parsed) return null
let [value, unit] = parsed
return new DefaultMap<string, number | null>((input) => {
let parsed = dimensions.get(input)
if (!parsed) return null
let [myValue, myUnit] = parsed
if (myUnit !== unit) return null
return myValue / value
})
})
export function migrateArbitraryUtilities(
designSystem: DesignSystem,
_userConfig: Config | null,
rawCandidate: string,
): string {
let utilities = preComputedUtilities.get(designSystem)
let signatures = computeUtilitySignature.get(designSystem)
for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) {
if (
readonlyCandidate.kind !== 'arbitrary' &&
!(readonlyCandidate.kind === 'functional' && readonlyCandidate.value?.kind === 'arbitrary')
) {
continue
}
let candidate = structuredClone(readonlyCandidate) as Writable<typeof readonlyCandidate>
let targetCandidate = baseCandidate(candidate)
let targetCandidateString = designSystem.printCandidate(targetCandidate)
if (baseReplacementsCache.get(designSystem).has(targetCandidateString)) {
let target = structuredClone(
baseReplacementsCache.get(designSystem).get(targetCandidateString)!,
)
target.variants = candidate.variants
target.important = candidate.important
return designSystem.printCandidate(target)
}
let targetSignature = signatures.get(targetCandidateString)
if (typeof targetSignature !== 'string') continue
for (let replacementCandidate of tryReplacements(targetSignature, targetCandidate)) {
let replacementString = designSystem.printCandidate(replacementCandidate)
let replacementSignature = signatures.get(replacementString)
if (replacementSignature !== targetSignature) {
continue
}
if (!allVariablesAreUsed(designSystem, candidate, replacementCandidate)) {
continue
}
replacementCandidate = structuredClone(replacementCandidate)
baseReplacementsCache.get(designSystem).set(targetCandidateString, replacementCandidate)
replacementCandidate.variants = candidate.variants
replacementCandidate.important = candidate.important
Object.assign(candidate, replacementCandidate)
return designSystem.printCandidate(candidate)
}
}
return rawCandidate
function* tryReplacements(
targetSignature: string,
candidate: Extract<Candidate, { kind: 'functional' | 'arbitrary' }>,
): Generator<Candidate> {
let replacements = utilities.get(targetSignature)
if (replacements.length > 1) return
if (replacements.length === 0 && candidate.modifier) {
let candidateWithoutModifier = { ...candidate, modifier: null }
let targetSignatureWithoutModifier = signatures.get(
designSystem.printCandidate(candidateWithoutModifier),
)
if (typeof targetSignatureWithoutModifier === 'string') {
for (let replacementCandidate of tryReplacements(
targetSignatureWithoutModifier,
candidateWithoutModifier,
)) {
yield Object.assign({}, replacementCandidate, { modifier: candidate.modifier })
}
}
}
if (replacements.length === 1) {
for (let replacementCandidate of parseCandidate(designSystem, replacements[0])) {
yield replacementCandidate
}
}
else if (replacements.length === 0) {
let value =
candidate.kind === 'arbitrary' ? candidate.value : (candidate.value?.value ?? null)
if (value === null) return
let spacingMultiplier = spacing.get(designSystem)?.get(value) ?? null
let rootPrefix = ''
if (spacingMultiplier !== null && spacingMultiplier < 0) {
rootPrefix = '-'
spacingMultiplier = Math.abs(spacingMultiplier)
}
for (let root of Array.from(designSystem.utilities.keys('functional')).sort(
(a, z) => Number(a[0] === '-') - Number(z[0] === '-'),
)) {
if (rootPrefix) root = `${rootPrefix}${root}`
for (let replacementCandidate of parseCandidate(designSystem, `${root}-${value}`)) {
yield replacementCandidate
}
if (candidate.modifier) {
for (let replacementCandidate of parseCandidate(
designSystem,
`${root}-${value}${candidate.modifier}`,
)) {
yield replacementCandidate
}
}
if (spacingMultiplier !== null) {
for (let replacementCandidate of parseCandidate(
designSystem,
`${root}-${spacingMultiplier}`,
)) {
yield replacementCandidate
}
if (candidate.modifier) {
for (let replacementCandidate of parseCandidate(
designSystem,
`${root}-${spacingMultiplier}${printModifier(candidate.modifier)}`,
)) {
yield replacementCandidate
}
}
}
for (let replacementCandidate of parseCandidate(designSystem, `${root}-[${value}]`)) {
yield replacementCandidate
}
if (candidate.modifier) {
for (let replacementCandidate of parseCandidate(
designSystem,
`${root}-[${value}]${printModifier(candidate.modifier)}`,
)) {
yield replacementCandidate
}
}
}
}
}
}
function allVariablesAreUsed(
designSystem: DesignSystem,
candidate: Candidate,
replacement: Candidate,
) {
let value: string | null = null
if (
candidate.kind === 'functional' &&
candidate.value?.kind === 'arbitrary' &&
candidate.value.value.includes('var(--')
) {
value = candidate.value.value
}
else if (candidate.kind === 'arbitrary' && candidate.value.includes('var(--')) {
value = candidate.value
}
if (value === null) {
return true
}
let replacementAsCss = designSystem
.candidatesToCss([designSystem.printCandidate(replacement)])
.join('\n')
let isSafeMigration = true
ValueParser.walk(ValueParser.parse(value), (node) => {
if (node.kind === 'function' && node.value === 'var') {
let variable = node.nodes[0].value
let r = new RegExp(`var\\(${variable}[,)]\\s*`, 'g')
if (
!r.test(replacementAsCss) ||
replacementAsCss.includes(`${variable}:`)
) {
isSafeMigration = false
return ValueParser.ValueWalkAction.Stop
}
}
})
return isSafeMigration
}