import { substituteAtApply } from './apply'
import { atRule, cloneAstNode, styleRule, toCss, type AstNode } from './ast'
import * as AttributeSelectorParser from './attribute-selector-parser'
import {
cloneCandidate,
cloneVariant,
printArbitraryValue,
printModifier,
type Candidate,
type CandidateModifier,
type NamedUtilityValue,
type Variant,
} from './candidate'
import { keyPathToCssProperty } from './compat/apply-config-to-theme'
import { constantFoldDeclaration } from './constant-fold-declaration'
import type { DesignSystem as BaseDesignSystem } from './design-system'
import { CompileAstFlags } from './design-system'
import { expandDeclaration } from './expand-declaration'
import * as SelectorParser from './selector-parser'
import { ThemeOptions } from './theme'
import type { Writable } from './types'
import { DefaultMap } from './utils/default-map'
import { dimensions } from './utils/dimensions'
import { isPositiveInteger, isValidSpacingMultiplier } from './utils/infer-data-type'
import { replaceObject } from './utils/replace-object'
import { segment } from './utils/segment'
import { toKeyPath } from './utils/to-key-path'
import * as ValueParser from './value-parser'
import { walk, WalkAction } from './walk'
export interface CanonicalizeOptions {
rem?: number
collapse?: boolean
logicalToPhysical?: boolean
}
enum Features {
None = 0,
CollapseUtilities = 1 << 0,
}
interface InternalCanonicalizeOptions {
features: Features
designSystem: DesignSystem
signatureOptions: SignatureOptions
}
interface DesignSystem extends BaseDesignSystem {
storage: {
[SIGNATURE_OPTIONS_KEY]: DefaultMap<
number | null,
DefaultMap<SignatureFeatures, SignatureOptions>
>
[INTERNAL_OPTIONS_KEY]: DefaultMap<
SignatureOptions,
DefaultMap<Features, InternalCanonicalizeOptions>
>
[CANONICALIZE_CANDIDATE_KEY]: DefaultMap<
InternalCanonicalizeOptions,
DefaultMap<string, string>
>
[CANONICALIZE_VARIANT_KEY]: DefaultMap<
InternalCanonicalizeOptions,
DefaultMap<Variant, Variant[]>
>
[CANONICALIZE_UTILITY_KEY]: DefaultMap<InternalCanonicalizeOptions, DefaultMap<string, string>>
[CONVERTER_KEY]: (input: string, options?: Convert) => [string, CandidateModifier | null]
[SPACING_KEY]: DefaultMap<string, number | null> | null
[UTILITY_SIGNATURE_KEY]: DefaultMap<SignatureOptions, DefaultMap<string, string | Symbol>>
[STATIC_UTILITIES_KEY]: DefaultMap<
SignatureOptions,
DefaultMap<string, DefaultMap<string, Set<string>>>
>
[UTILITY_PROPERTIES_KEY]: DefaultMap<
SignatureOptions,
DefaultMap<string, DefaultMap<string, Set<string>>>
>
[PRE_COMPUTED_UTILITIES_KEY]: DefaultMap<SignatureOptions, DefaultMap<string, string[]>>
[VARIANT_SIGNATURE_KEY]: DefaultMap<string, string | Symbol>
[PRE_COMPUTED_VARIANTS_KEY]: DefaultMap<string, string[]>
}
}
export function prepareDesignSystemStorage(
baseDesignSystem: BaseDesignSystem,
options?: CanonicalizeOptions,
): DesignSystem {
let designSystem = baseDesignSystem as DesignSystem
designSystem.storage[SIGNATURE_OPTIONS_KEY] ??= createSignatureOptionsCache()
designSystem.storage[INTERNAL_OPTIONS_KEY] ??= createInternalOptionsCache(designSystem)
designSystem.storage[CANONICALIZE_CANDIDATE_KEY] ??= createCanonicalizeCandidateCache()
designSystem.storage[CANONICALIZE_VARIANT_KEY] ??= createCanonicalizeVariantCache()
designSystem.storage[CANONICALIZE_UTILITY_KEY] ??= createCanonicalizeUtilityCache()
designSystem.storage[CONVERTER_KEY] ??= createConverterCache(designSystem)
designSystem.storage[SPACING_KEY] ??= createSpacingCache(designSystem, options)
designSystem.storage[UTILITY_SIGNATURE_KEY] ??= createUtilitySignatureCache(designSystem)
designSystem.storage[STATIC_UTILITIES_KEY] ??= createStaticUtilitiesCache()
designSystem.storage[UTILITY_PROPERTIES_KEY] ??= createUtilityPropertiesCache(designSystem)
designSystem.storage[PRE_COMPUTED_UTILITIES_KEY] ??= createPreComputedUtilitiesCache(designSystem)
designSystem.storage[VARIANT_SIGNATURE_KEY] ??= createVariantSignatureCache(designSystem)
designSystem.storage[PRE_COMPUTED_VARIANTS_KEY] ??= createPreComputedVariantsCache(designSystem)
return designSystem
}
const SIGNATURE_OPTIONS_KEY = Symbol()
function createSignatureOptionsCache(): DesignSystem['storage'][typeof SIGNATURE_OPTIONS_KEY] {
return new DefaultMap((rem: number | null) => {
return new DefaultMap((features: SignatureFeatures) => {
return { rem, features } satisfies SignatureOptions
})
})
}
export function createSignatureOptions(
baseDesignSystem: BaseDesignSystem,
options?: CanonicalizeOptions,
): SignatureOptions {
let features = SignatureFeatures.None
if (options?.collapse) features |= SignatureFeatures.ExpandProperties
if (options?.logicalToPhysical) features |= SignatureFeatures.LogicalToPhysical
let designSystem = prepareDesignSystemStorage(baseDesignSystem, options)
return designSystem.storage[SIGNATURE_OPTIONS_KEY].get(options?.rem ?? null).get(features)
}
const INTERNAL_OPTIONS_KEY = Symbol()
function createInternalOptionsCache(
designSystem: DesignSystem,
): DesignSystem['storage'][typeof INTERNAL_OPTIONS_KEY] {
return new DefaultMap((signatureOptions: SignatureOptions) => {
return new DefaultMap((features: Features) => {
return { features, designSystem, signatureOptions } satisfies InternalCanonicalizeOptions
})
})
}
function createCanonicalizeOptions(
baseDesignSystem: BaseDesignSystem,
signatureOptions: SignatureOptions,
options?: CanonicalizeOptions,
) {
let features = Features.None
if (options?.collapse) features |= Features.CollapseUtilities
let designSystem = prepareDesignSystemStorage(baseDesignSystem)
return designSystem.storage[INTERNAL_OPTIONS_KEY].get(signatureOptions).get(features)
}
export function canonicalizeCandidates(
baseDesignSystem: BaseDesignSystem,
candidates: string[],
options?: CanonicalizeOptions,
): string[] {
let signatureOptions = createSignatureOptions(baseDesignSystem, options)
let canonicalizeOptions = createCanonicalizeOptions(baseDesignSystem, signatureOptions, options)
let designSystem = prepareDesignSystemStorage(baseDesignSystem)
let result = new Set<string>()
let cache = designSystem.storage[CANONICALIZE_CANDIDATE_KEY].get(canonicalizeOptions)
for (let candidate of candidates) {
result.add(cache.get(candidate))
}
return result.size <= 1 || !(canonicalizeOptions.features & Features.CollapseUtilities)
? Array.from(result)
: collapseCandidates(canonicalizeOptions, Array.from(result))
}
function collapseCandidates(options: InternalCanonicalizeOptions, candidates: string[]): string[] {
if (candidates.length <= 1) return candidates
let designSystem = options.designSystem
let groups = new DefaultMap((_before: string) => {
return new DefaultMap((_after: string) => {
return new Set<string>()
})
})
let prefix = options.designSystem.theme.prefix ? `${options.designSystem.theme.prefix}:` : ''
for (let candidate of candidates) {
let variants = segment(candidate, ':')
let utility = variants.pop()!
let important = utility.endsWith('!')
if (important) {
utility = utility.slice(0, -1)
}
let before = variants.length > 0 ? `${variants.join(':')}:` : ''
let after = important ? '!' : ''
groups.get(before).get(after).add(`${prefix}${utility}`)
}
let result = new Set<string>()
for (let [before, group] of groups.entries()) {
for (let [after, candidates] of group.entries()) {
for (let candidate of collapseGroup(Array.from(candidates))) {
if (prefix && candidate.startsWith(prefix)) {
candidate = candidate.slice(prefix.length)
}
result.add(`${before}${candidate}${after}`)
}
}
}
return Array.from(result)
function collapseGroup(candidates: string[]) {
let signatureOptions = options.signatureOptions
let computeUtilitiesPropertiesLookup =
designSystem.storage[UTILITY_PROPERTIES_KEY].get(signatureOptions)
let staticUtilities = designSystem.storage[STATIC_UTILITIES_KEY].get(signatureOptions)
let candidatePropertiesValues = candidates.map((candidate) =>
computeUtilitiesPropertiesLookup.get(candidate),
)
if (candidatePropertiesValues.some((x) => x.has('line-height'))) {
let fontSizeNames = designSystem.theme.keysInNamespaces(['--text'])
if (fontSizeNames.length > 0) {
let interestingLineHeights = new Set<string | number>()
let seenLineHeights = new Set<string>()
for (let pairs of candidatePropertiesValues) {
for (let lineHeight of pairs.get('line-height')) {
if (seenLineHeights.has(lineHeight)) continue
seenLineHeights.add(lineHeight)
let bareValue = designSystem.storage[SPACING_KEY]?.get(lineHeight) ?? null
if (bareValue !== null) {
if (isValidSpacingMultiplier(bareValue)) {
interestingLineHeights.add(bareValue)
for (let name of fontSizeNames) {
computeUtilitiesPropertiesLookup.get(`text-${name}/${bareValue}`)
}
} else {
interestingLineHeights.add(lineHeight)
for (let name of fontSizeNames) {
computeUtilitiesPropertiesLookup.get(`text-${name}/[${lineHeight}]`)
}
}
}
}
}
let seenFontSizes = new Set<string>()
for (let pairs of candidatePropertiesValues) {
for (let fontSize of pairs.get('font-size')) {
if (seenFontSizes.has(fontSize)) continue
seenFontSizes.add(fontSize)
for (let lineHeight of interestingLineHeights) {
if (isValidSpacingMultiplier(lineHeight)) {
computeUtilitiesPropertiesLookup.get(`text-[${fontSize}]/${lineHeight}`)
} else {
computeUtilitiesPropertiesLookup.get(`text-[${fontSize}]/[${lineHeight}]`)
}
}
}
}
}
}
let otherUtilities = candidatePropertiesValues.map((propertyValues) => {
let result: Set<string> | null = null
for (let property of propertyValues.keys()) {
let otherUtilities = new Set<string>()
for (let group of staticUtilities.get(property).values()) {
for (let candidate of group) {
otherUtilities.add(candidate)
}
}
if (result === null) result = otherUtilities
else result = intersection(result, otherUtilities)
if (result!.size === 0) return result!
}
return result!
})
let linked = new DefaultMap<number, Set<number>>((key) => new Set<number>([key]))
for (let i = 0; i < otherUtilities.length; i++) {
let current = otherUtilities[i]
for (let j = i + 1; j < otherUtilities.length; j++) {
let other = otherUtilities[j]
for (let property of current) {
if (other.has(property)) {
linked.get(i).add(j)
linked.get(j).add(i)
break
}
}
}
}
if (linked.size === 0) return candidates
let uniqueCombinations = new DefaultMap((key: string) => key.split(',').map(Number))
for (let group of linked.values()) {
let sorted = Array.from(group).sort((a, b) => a - b)
uniqueCombinations.get(sorted.join(','))
}
let result = new Set<string>(candidates)
let drop = new Set<string>()
for (let idxs of uniqueCombinations.values()) {
for (let combo of combinations(idxs)) {
if (combo.some((idx) => drop.has(candidates[idx]))) continue
let potentialReplacements = combo.flatMap((idx) => otherUtilities[idx]).reduce(intersection)
let collapsedSignature = designSystem.storage[UTILITY_SIGNATURE_KEY].get(
signatureOptions,
).get(
combo
.map((idx) => candidates[idx])
.sort((a, z) => a.localeCompare(z))
.join(' '),
)
for (let replacement of potentialReplacements) {
let signature =
designSystem.storage[UTILITY_SIGNATURE_KEY].get(signatureOptions).get(replacement)
if (signature !== collapsedSignature) continue
for (let item of combo) {
drop.add(candidates[item])
}
result.add(replacement)
break
}
}
}
for (let item of drop) {
result.delete(item)
}
return Array.from(result)
}
}
const CANONICALIZE_CANDIDATE_KEY = Symbol()
function createCanonicalizeCandidateCache(): DesignSystem['storage'][typeof CANONICALIZE_CANDIDATE_KEY] {
return new DefaultMap((options: InternalCanonicalizeOptions) => {
let ds = options.designSystem
let prefix = ds.theme.prefix ? `${ds.theme.prefix}:` : ''
let variantCache = ds.storage[CANONICALIZE_VARIANT_KEY].get(options)
let utilityCache = ds.storage[CANONICALIZE_UTILITY_KEY].get(options)
return new DefaultMap<string, string>((rawCandidate: string, self) => {
for (let candidate of ds.parseCandidate(rawCandidate)) {
let variants = candidate.variants
.slice()
.reverse()
.flatMap((variant) => variantCache.get(variant))
let important = candidate.important
if (important || variants.length > 0) {
let canonicalizedUtility = self.get(
ds.printCandidate({ ...candidate, variants: [], important: false }),
)
let result = canonicalizedUtility
if (ds.theme.prefix !== null && variants.length > 0) {
result = result.slice(prefix.length)
}
if (variants.length > 0) {
result = `${variants.map((v) => ds.printVariant(v)).join(':')}:${result}`
}
if (important) {
result += '!'
}
if (ds.theme.prefix !== null && variants.length > 0) {
result = `${prefix}${result}`
}
return result
}
let result = utilityCache.get(rawCandidate)
if (result !== rawCandidate) {
return result
}
}
return rawCandidate
})
})
}
type VariantCanonicalizationFunction = (
variant: Variant,
options: InternalCanonicalizeOptions,
) => Variant | Variant[]
const VARIANT_CANONICALIZATIONS: VariantCanonicalizationFunction[] = [
themeToVarVariant,
arbitraryValueToBareValueVariant,
modernizeArbitraryValuesVariant,
arbitraryVariants,
]
const CANONICALIZE_VARIANT_KEY = Symbol()
function createCanonicalizeVariantCache(): DesignSystem['storage'][typeof CANONICALIZE_VARIANT_KEY] {
return new DefaultMap((options: InternalCanonicalizeOptions) => {
return new DefaultMap((variant: Variant): Variant[] => {
let replacement = [variant]
for (let fn of VARIANT_CANONICALIZATIONS) {
for (let current of replacement.splice(0)) {
let result = fn(cloneVariant(current), options)
if (Array.isArray(result)) {
replacement.push(...result)
continue
} else {
replacement.push(result)
}
}
}
return replacement
})
})
}
type UtilityCanonicalizationFunction = (
candidate: Candidate,
options: InternalCanonicalizeOptions,
) => Candidate
const UTILITY_CANONICALIZATIONS: UtilityCanonicalizationFunction[] = [
bgGradientToLinear,
themeToVarUtility,
arbitraryUtilities,
bareValueUtilities,
deprecatedUtilities,
dropUnnecessaryDataTypes,
arbitraryValueToBareValueUtility,
optimizeModifier,
]
const CANONICALIZE_UTILITY_KEY = Symbol()
function createCanonicalizeUtilityCache(): DesignSystem['storage'][typeof CANONICALIZE_UTILITY_KEY] {
return new DefaultMap((options: InternalCanonicalizeOptions) => {
let designSystem = options.designSystem
return new DefaultMap((rawCandidate: string): string => {
for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) {
let replacement = cloneCandidate(readonlyCandidate) as Writable<typeof readonlyCandidate>
for (let fn of UTILITY_CANONICALIZATIONS) {
replacement = fn(replacement, options)
}
let canonicalizedCandidate = designSystem.printCandidate(replacement)
if (rawCandidate !== canonicalizedCandidate) {
return canonicalizedCandidate
}
}
return rawCandidate
})
})
}
const DIRECTIONS = ['t', 'tr', 'r', 'br', 'b', 'bl', 'l', 'tl']
function bgGradientToLinear(candidate: Candidate) {
if (candidate.kind === 'static' && candidate.root.startsWith('bg-gradient-to-')) {
let direction = candidate.root.slice(15)
if (!DIRECTIONS.includes(direction)) {
return candidate
}
candidate.root = `bg-linear-to-${direction}`
return candidate
}
return candidate
}
const enum Convert {
All = 0,
MigrateModifier = 1 << 0,
MigrateThemeOnly = 1 << 1,
}
function themeToVarUtility(candidate: Candidate, options: InternalCanonicalizeOptions): Candidate {
let convert = options.designSystem.storage[CONVERTER_KEY]
if (candidate.kind === 'arbitrary') {
let [newValue, modifier] = convert(
candidate.value,
candidate.modifier === null ? Convert.MigrateModifier : Convert.All,
)
if (newValue !== candidate.value) {
candidate.value = newValue
if (modifier !== null) {
candidate.modifier = modifier
}
}
} else if (candidate.kind === 'functional' && candidate.value?.kind === 'arbitrary') {
let [newValue, modifier] = convert(
candidate.value.value,
candidate.modifier === null ? Convert.MigrateModifier : Convert.All,
)
if (newValue !== candidate.value.value) {
candidate.value.value = newValue
if (modifier !== null) {
candidate.modifier = modifier
}
}
}
return candidate
}
function themeToVarVariant(
variant: Variant,
options: InternalCanonicalizeOptions,
): Variant | Variant[] {
let convert = options.designSystem.storage[CONVERTER_KEY]
let iterator = walkVariants(variant)
for (let [variant] of iterator) {
if (variant.kind === 'arbitrary') {
let [newValue] = convert(variant.selector, Convert.MigrateThemeOnly)
if (newValue !== variant.selector) {
variant.selector = newValue
}
} else if (variant.kind === 'functional' && variant.value?.kind === 'arbitrary') {
let [newValue] = convert(variant.value.value, Convert.MigrateThemeOnly)
if (newValue !== variant.value.value) {
variant.value.value = newValue
}
}
}
return variant
}
const CONVERTER_KEY = Symbol()
function createConverterCache(
designSystem: DesignSystem,
): DesignSystem['storage'][typeof CONVERTER_KEY] {
return createConverter(designSystem)
function createConverter(designSystem: DesignSystem) {
function convert(input: string, options = Convert.All): [string, CandidateModifier | null] {
let ast = ValueParser.parse(input)
if (options & Convert.MigrateThemeOnly) {
return [substituteFunctionsInValue(ast, toTheme), null]
}
let themeUsageCount = 0
let themeModifierCount = 0
walk(ast, (node) => {
if (node.kind !== 'function') return
if (node.value !== 'theme') return
themeUsageCount += 1
walk(node.nodes, (child) => {
if (child.kind === 'separator' && child.value.includes(',')) {
return WalkAction.Stop
}
else if (child.kind === 'word' && child.value === '/') {
themeModifierCount += 1
return WalkAction.Stop
}
return WalkAction.Skip
})
})
if (themeUsageCount === 0) {
return [input, null]
}
if (themeModifierCount === 0) {
return [substituteFunctionsInValue(ast, toVar), null]
}
if (themeModifierCount > 1) {
return [substituteFunctionsInValue(ast, toTheme), null]
}
let modifier: CandidateModifier | null = null
let result = substituteFunctionsInValue(ast, (path, fallback) => {
let parts = segment(path, '/').map((part) => part.trim())
if (parts.length > 2) return null
if (ast.length === 1 && parts.length === 2 && options & Convert.MigrateModifier) {
let [pathPart, modifierPart] = parts
if (/^\d+%$/.test(modifierPart)) {
modifier = { kind: 'named', value: modifierPart.slice(0, -1) }
}
else if (/^0?\.\d+$/.test(modifierPart)) {
let value = Number(modifierPart) * 100
modifier = {
kind: Number.isInteger(value) ? 'named' : 'arbitrary',
value: value.toString(),
}
}
else {
modifier = { kind: 'arbitrary', value: modifierPart }
}
path = pathPart
}
return toVar(path, fallback) || toTheme(path, fallback)
})
return [result, modifier]
}
function pathToVariableName(path: string, shouldPrefix = true) {
let variable = `--${keyPathToCssProperty(toKeyPath(path))}` as const
if (!designSystem.theme.get([variable])) return null
if (shouldPrefix && designSystem.theme.prefix) {
return `--${designSystem.theme.prefix}-${variable.slice(2)}`
}
return variable
}
function toVar(path: string, fallback?: string) {
let variable = pathToVariableName(path)
if (variable) return fallback ? `var(${variable}, ${fallback})` : `var(${variable})`
let keyPath = toKeyPath(path)
if (keyPath[0] === 'spacing' && designSystem.theme.get(['--spacing'])) {
let multiplier = keyPath[1]
if (!isValidSpacingMultiplier(multiplier)) return null
return `--spacing(${multiplier})`
}
return null
}
function toTheme(path: string, fallback?: string) {
let parts = segment(path, '/').map((part) => part.trim())
path = parts.shift()!
let variable = pathToVariableName(path, false)
if (!variable) return null
let modifier = parts.length > 0 ? `/${parts.join('/')}` : ''
return fallback
? `--theme(${variable}${modifier}, ${fallback})`
: `--theme(${variable}${modifier})`
}
return convert
}
}
function substituteFunctionsInValue(
ast: ValueParser.ValueAstNode[],
handle: (value: string, fallback?: string) => string | null,
) {
walk(ast, (node, ctx) => {
if (node.kind === 'function' && node.value === 'theme') {
if (node.nodes.length < 1) return
if (node.nodes[0].kind === 'separator' && node.nodes[0].value.trim() === '') {
node.nodes.shift()
}
let pathNode = node.nodes[0]
if (pathNode.kind !== 'word') return
let path = pathNode.value
let skipUntilIndex = 1
for (let i = skipUntilIndex; i < node.nodes.length; i++) {
if (node.nodes[i].value.includes(',')) {
break
}
path += ValueParser.toCss([node.nodes[i]])
skipUntilIndex = i + 1
}
path = eventuallyUnquote(path)
let fallbackValues = node.nodes.slice(skipUntilIndex + 1)
let replacement =
fallbackValues.length > 0 ? handle(path, ValueParser.toCss(fallbackValues)) : handle(path)
if (replacement === null) return
if (ctx.parent) {
let idx = ctx.parent.nodes.indexOf(node) - 1
while (idx !== -1) {
let previous = ctx.parent.nodes[idx]
if (previous.kind === 'separator' && previous.value.trim() === '') {
idx -= 1
continue
}
if (/^[-+*/]$/.test(previous.value.trim())) {
replacement = `(${replacement})`
}
break
}
}
return WalkAction.Replace(ValueParser.parse(replacement))
}
})
return ValueParser.toCss(ast)
}
function eventuallyUnquote(value: string) {
if (value[0] !== "'" && value[0] !== '"') return value
let unquoted = ''
let quoteChar = value[0]
for (let i = 1; i < value.length - 1; i++) {
let currentChar = value[i]
let nextChar = value[i + 1]
if (currentChar === '\\' && (nextChar === quoteChar || nextChar === '\\')) {
unquoted += nextChar
i++
} else {
unquoted += currentChar
}
}
return unquoted
}
function* walkVariants(variant: Variant) {
function* inner(
variant: Variant,
parent: Extract<Variant, { kind: 'compound' }> | null = null,
): Iterable<[Variant, Extract<Variant, { kind: 'compound' }> | null]> {
yield [variant, parent]
if (variant.kind === 'compound') {
yield* inner(variant.variant, variant)
}
}
yield* inner(variant, null)
}
function parseCandidate(designSystem: DesignSystem, input: string) {
return designSystem.parseCandidate(
designSystem.theme.prefix && !input.startsWith(`${designSystem.theme.prefix}:`)
? `${designSystem.theme.prefix}:${input}`
: input,
)
}
function printUnprefixedCandidate(designSystem: DesignSystem, candidate: Candidate) {
let candidateString = designSystem.printCandidate(candidate)
return designSystem.theme.prefix && candidateString.startsWith(`${designSystem.theme.prefix}:`)
? candidateString.slice(designSystem.theme.prefix.length + 1)
: candidateString
}
const SPACING_KEY = Symbol()
function createSpacingCache(
designSystem: DesignSystem,
options?: CanonicalizeOptions,
): DesignSystem['storage'][typeof SPACING_KEY] {
let spacingMultiplier = designSystem.resolveThemeValue('--spacing')
if (spacingMultiplier === undefined) return null
spacingMultiplier = constantFoldDeclaration(spacingMultiplier, options?.rem ?? null)
let parsed = dimensions.get(spacingMultiplier)
if (!parsed) return null
let [value, unit] = parsed
return new DefaultMap<string, number | null>((input) => {
if (value === 0) return null
let parsed = dimensions.get(constantFoldDeclaration(input, options?.rem ?? null))
if (!parsed) return null
let [myValue, myUnit] = parsed
if (myUnit !== unit) return null
return myValue / value
})
}
function arbitraryUtilities(candidate: Candidate, options: InternalCanonicalizeOptions): Candidate {
if (
candidate.kind !== 'arbitrary' &&
!(candidate.kind === 'functional' && candidate.value?.kind === 'arbitrary')
) {
return candidate
}
let designSystem = options.designSystem
let utilities = designSystem.storage[PRE_COMPUTED_UTILITIES_KEY].get(options.signatureOptions)
let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions)
let targetCandidateString = designSystem.printCandidate(candidate)
let targetSignature = signatures.get(targetCandidateString)
if (typeof targetSignature !== 'string') return candidate
for (let replacementCandidate of tryReplacements(targetSignature, candidate)) {
let replacementString = designSystem.printCandidate(replacementCandidate)
let replacementSignature = signatures.get(replacementString)
if (replacementSignature !== targetSignature) {
continue
}
if (!allVariablesAreUsed(designSystem, candidate, replacementCandidate)) {
continue
}
return replacementCandidate
}
return candidate
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
if (
options.signatureOptions.rem !== null &&
candidate.kind === 'functional' &&
candidate.value?.kind === 'arbitrary'
) {
let bareValue = designSystem.storage[SPACING_KEY]?.get(value) ?? null
if (bareValue !== null) {
if (isValidSpacingMultiplier(bareValue)) {
yield Object.assign({}, candidate, {
value: { kind: 'named', value: bareValue, fraction: null },
})
}
}
}
let spacingMultiplier = designSystem.storage[SPACING_KEY]?.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
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 WalkAction.Stop
}
}
})
return isSafeMigration
}
function bareValueUtilities(candidate: Candidate, options: InternalCanonicalizeOptions): Candidate {
if (candidate.kind !== 'functional' || candidate.value?.kind !== 'named') {
return candidate
}
let designSystem = options.designSystem
let utilities = designSystem.storage[PRE_COMPUTED_UTILITIES_KEY].get(options.signatureOptions)
let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions)
let targetCandidateString = designSystem.printCandidate(candidate)
let targetSignature = signatures.get(targetCandidateString)
if (typeof targetSignature !== 'string') return candidate
for (let replacementCandidate of tryReplacements(targetSignature, candidate)) {
let replacementString = designSystem.printCandidate(replacementCandidate)
let replacementSignature = signatures.get(replacementString)
if (replacementSignature !== targetSignature) {
continue
}
return replacementCandidate
}
return candidate
function* tryReplacements(
targetSignature: string,
candidate: Extract<Candidate, { kind: 'functional' }>,
): 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
}
}
}
}
const DEPRECATION_MAP = new Map([
['order-none', 'order-0'],
['break-words', 'wrap-break-word'],
])
function deprecatedUtilities(
candidate: Candidate,
options: InternalCanonicalizeOptions,
): Candidate {
let designSystem = options.designSystem
let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions)
let targetCandidateString = printUnprefixedCandidate(designSystem, candidate)
let replacementString = DEPRECATION_MAP.get(targetCandidateString) ?? null
if (replacementString === null) return candidate
let legacySignature = signatures.get(targetCandidateString)
if (typeof legacySignature !== 'string') return candidate
let replacementSignature = signatures.get(replacementString)
if (typeof replacementSignature !== 'string') return candidate
if (legacySignature !== replacementSignature) return candidate
let [replacement] = parseCandidate(designSystem, replacementString)
return replacement
}
function arbitraryVariants(
variant: Variant,
options: InternalCanonicalizeOptions,
): Variant | Variant[] {
let designSystem = options.designSystem
let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY]
let variants = designSystem.storage[PRE_COMPUTED_VARIANTS_KEY]
let iterator = walkVariants(variant)
for (let [variant] of iterator) {
if (variant.kind === 'compound') continue
let targetString = designSystem.printVariant(variant)
let targetSignature = signatures.get(targetString)
if (typeof targetSignature !== 'string') continue
let foundVariants = variants.get(targetSignature)
if (foundVariants.length !== 1) continue
let foundVariant = foundVariants[0]
let parsedVariant = designSystem.parseVariant(foundVariant)
if (parsedVariant === null) continue
replaceObject(variant, parsedVariant)
}
return variant
}
function dropUnnecessaryDataTypes(
candidate: Candidate,
options: InternalCanonicalizeOptions,
): Candidate {
let designSystem = options.designSystem
let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions)
if (
candidate.kind === 'functional' &&
candidate.value?.kind === 'arbitrary' &&
candidate.value.dataType !== null
) {
let replacement = designSystem.printCandidate({
...candidate,
value: { ...candidate.value, dataType: null },
})
if (signatures.get(designSystem.printCandidate(candidate)) === signatures.get(replacement)) {
candidate.value.dataType = null
}
}
return candidate
}
function arbitraryValueToBareValueUtility(
candidate: Candidate,
options: InternalCanonicalizeOptions,
): Candidate {
if (candidate.kind !== 'functional' || candidate.value?.kind !== 'arbitrary') {
return candidate
}
let designSystem = options.designSystem
let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions)
let expectedSignature = signatures.get(designSystem.printCandidate(candidate))
if (expectedSignature === null) return candidate
for (let value of tryValueReplacements(candidate)) {
let newSignature = signatures.get(designSystem.printCandidate({ ...candidate, value }))
if (newSignature === expectedSignature) {
candidate.value = value
return candidate
}
}
return candidate
}
function arbitraryValueToBareValueVariant(variant: Variant): Variant | Variant[] {
let iterator = walkVariants(variant)
for (let [variant] of iterator) {
if (
variant.kind === 'functional' &&
variant.root === 'data' &&
variant.value?.kind === 'arbitrary' &&
!variant.value.value.includes('=')
) {
variant.value = {
kind: 'named',
value: variant.value.value,
}
}
else if (
variant.kind === 'functional' &&
variant.root === 'aria' &&
variant.value?.kind === 'arbitrary' &&
(variant.value.value.endsWith('=true') ||
variant.value.value.endsWith('="true"') ||
variant.value.value.endsWith("='true'"))
) {
let [key, _value] = segment(variant.value.value, '=')
if (
key[key.length - 1] === '~' ||
key[key.length - 1] === '|' ||
key[key.length - 1] === '^' ||
key[key.length - 1] === '$' ||
key[key.length - 1] === '*'
) {
continue
}
variant.value = {
kind: 'named',
value: variant.value.value.slice(0, variant.value.value.indexOf('=')),
}
}
else if (
variant.kind === 'functional' &&
variant.root === 'supports' &&
variant.value?.kind === 'arbitrary' &&
/^[a-z-][a-z0-9-]*$/i.test(variant.value.value)
) {
variant.value = {
kind: 'named',
value: variant.value.value,
}
}
}
return variant
}
function* tryValueReplacements(
candidate: Extract<Candidate, { kind: 'functional' }>,
value: string = candidate.value?.value ?? '',
seen: Set<string> = new Set(),
): Generator<NamedUtilityValue> {
if (seen.has(value)) return
seen.add(value)
yield {
kind: 'named',
value,
fraction: null,
}
if (value.endsWith('%') && isValidSpacingMultiplier(value.slice(0, -1))) {
yield {
kind: 'named',
value: value.slice(0, -1),
fraction: null,
}
}
if (value.includes('/')) {
let [numerator, denominator] = value.split('/')
if (isPositiveInteger(numerator) && isPositiveInteger(denominator)) {
yield {
kind: 'named',
value: numerator,
fraction: `${numerator}/${denominator}`,
}
}
}
let allNumbersAndFractions = new Set<string>()
for (let match of value.matchAll(/(\d+\/\d+)|(\d+\.?\d+)/g)) {
allNumbersAndFractions.add(match[0].trim())
}
let options = Array.from(allNumbersAndFractions).sort((a, z) => {
return a.length - z.length
})
for (let option of options) {
yield* tryValueReplacements(candidate, option, seen)
}
}
function isSingleSelector(ast: SelectorParser.SelectorAstNode[]): boolean {
return !ast.some((node) => node.kind === 'separator' && node.value.trim() === ',')
}
function isAttributeSelector(node: SelectorParser.SelectorAstNode): boolean {
let value = node.value.trim()
return node.kind === 'selector' && value[0] === '[' && value[value.length - 1] === ']'
}
function modernizeArbitraryValuesVariant(
variant: Variant,
options: InternalCanonicalizeOptions,
): Variant | Variant[] {
let result = [variant]
let designSystem = options.designSystem
let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY]
let iterator = walkVariants(variant)
for (let [variant, parent] of iterator) {
if (
variant.kind === 'compound' &&
(variant.root === 'has' || variant.root === 'not' || variant.root === 'in')
) {
if (variant.modifier !== null) {
if ('modifier' in variant.variant) {
variant.variant.modifier = variant.modifier
variant.modifier = null
}
}
}
if (variant.kind === 'arbitrary') {
if (variant.relative) continue
let ast = SelectorParser.parse(variant.selector.trim())
if (!isSingleSelector(ast)) continue
if (
parent === null &&
ast.length === 3 &&
ast[0].kind === 'selector' &&
ast[0].value === '&' &&
ast[1].kind === 'combinator' &&
ast[1].value.trim() === '>' &&
ast[2].kind === 'selector' &&
ast[2].value === '*'
) {
replaceObject(variant, designSystem.parseVariant('*'))
continue
}
if (
parent === null &&
ast.length === 3 &&
ast[0].kind === 'selector' &&
ast[0].value === '&' &&
ast[1].kind === 'combinator' &&
ast[1].value.trim() === '' &&
ast[2].kind === 'selector' &&
ast[2].value === '*'
) {
replaceObject(variant, designSystem.parseVariant('**'))
continue
}
if (
parent === null &&
ast.length === 3 &&
ast[1].kind === 'combinator' &&
ast[1].value.trim() === '' &&
ast[2].kind === 'selector' &&
ast[2].value === '&'
) {
ast.pop()
ast.pop()
replaceObject(variant, designSystem.parseVariant(`in-[${SelectorParser.toCss(ast)}]`))
continue
}
if (
parent === null &&
ast[0].kind === 'selector' &&
(ast[0].value === '@media' || ast[0].value === '@supports')
) {
let targetSignature = signatures.get(designSystem.printVariant(variant))
let parsed = ValueParser.parse(SelectorParser.toCss(ast))
let containsNot = false
walk(parsed, (node) => {
if (node.kind === 'word' && node.value === 'not') {
containsNot = true
return WalkAction.Replace([])
}
})
parsed = ValueParser.parse(ValueParser.toCss(parsed))
walk(parsed, (node) => {
if (node.kind === 'separator' && node.value !== ' ' && node.value.trim() === '') {
node.value = ' '
}
})
if (containsNot) {
let hoistedNot = designSystem.parseVariant(`not-[${ValueParser.toCss(parsed)}]`)
if (hoistedNot === null) continue
let hoistedNotSignature = signatures.get(designSystem.printVariant(hoistedNot))
if (targetSignature === hoistedNotSignature) {
replaceObject(variant, hoistedNot)
continue
}
}
}
let prefixedVariant: Variant | null = null
if (
parent === null &&
ast.length === 3 &&
ast[0].kind === 'selector' &&
ast[0].value.trim() === '&' &&
ast[1].kind === 'combinator' &&
ast[1].value.trim() === '>' &&
ast[2].kind === 'selector' &&
(isAttributeSelector(ast[2]) || ast[2].value[0] === ':')
) {
ast = [ast[2]]
prefixedVariant = designSystem.parseVariant('*')
}
if (
parent === null &&
ast.length === 3 &&
ast[0].kind === 'selector' &&
ast[0].value.trim() === '&' &&
ast[1].kind === 'combinator' &&
ast[1].value.trim() === '' &&
ast[2].kind === 'selector' &&
(isAttributeSelector(ast[2]) || ast[2].value[0] === ':')
) {
ast = [ast[2]]
prefixedVariant = designSystem.parseVariant('**')
}
let selectorNodes = ast.filter(
(node) => !(node.kind === 'selector' && node.value.trim() === '&'),
)
if (selectorNodes.length !== 1) continue
let target = selectorNodes[0]
if (target.kind === 'function' && target.value === ':is') {
if (
!isSingleSelector(target.nodes) ||
target.nodes.length !== 1
) {
continue
}
if (!isAttributeSelector(target.nodes[0])) continue
target = target.nodes[0]
}
if (
(target.kind === 'function' && target.value[0] === ':') ||
(target.kind === 'selector' && target.value[0] === ':')
) {
let targetNode = target
let compoundNot = false
if (targetNode.kind === 'function' && targetNode.value === ':not') {
compoundNot = true
if (targetNode.nodes.length !== 1) continue
if (targetNode.nodes[0].kind !== 'selector' && targetNode.nodes[0].kind !== 'function') {
continue
}
if (targetNode.nodes[0].value[0] !== ':') continue
targetNode = targetNode.nodes[0]
}
let newVariant = ((value) => {
if (
value === ':nth-child' &&
targetNode.kind === 'function' &&
targetNode.nodes.length === 1 &&
targetNode.nodes[0].kind === 'value' &&
targetNode.nodes[0].value === 'odd'
) {
if (compoundNot) {
compoundNot = false
return 'even'
}
return 'odd'
}
if (
value === ':nth-child' &&
targetNode.kind === 'function' &&
targetNode.nodes.length === 1 &&
targetNode.nodes[0].kind === 'value' &&
targetNode.nodes[0].value === 'even'
) {
if (compoundNot) {
compoundNot = false
return 'odd'
}
return 'even'
}
for (let [selector, variantName] of [
[':nth-child', 'nth'],
[':nth-last-child', 'nth-last'],
[':nth-of-type', 'nth-of-type'],
[':nth-last-of-type', 'nth-of-last-type'],
]) {
if (
value === selector &&
targetNode.kind === 'function' &&
targetNode.nodes.length === 1
) {
if (
targetNode.nodes.length === 1 &&
targetNode.nodes[0].kind === 'value' &&
isPositiveInteger(targetNode.nodes[0].value)
) {
return `${variantName}-${targetNode.nodes[0].value}`
}
return `${variantName}-[${SelectorParser.toCss(targetNode.nodes)}]`
}
}
if (compoundNot) {
let targetSignature = signatures.get(designSystem.printVariant(variant))
let replacementSignature = signatures.get(`not-[${value}]`)
if (targetSignature === replacementSignature) {
return `[&${value}]`
}
}
return null
})(targetNode.value)
if (newVariant === null) {
if (prefixedVariant) {
replaceObject(variant, {
kind: 'arbitrary',
selector: target.value,
relative: false,
} satisfies Variant)
return [prefixedVariant, variant]
}
continue
}
if (compoundNot) newVariant = `not-${newVariant}`
let parsed = designSystem.parseVariant(newVariant)
if (parsed === null) continue
replaceObject(variant, parsed)
}
else if (isAttributeSelector(target)) {
let attributeSelector = AttributeSelectorParser.parse(target.value)
if (attributeSelector === null) continue
if (attributeSelector.attribute.startsWith('data-')) {
let name = attributeSelector.attribute.slice(5)
replaceObject(variant, {
kind: 'functional',
root: 'data',
modifier: null,
value:
attributeSelector.value === null
? { kind: 'named', value: name }
: {
kind: 'arbitrary',
value: `${name}${attributeSelector.operator}${attributeSelector.quote ?? ''}${attributeSelector.value}${attributeSelector.quote ?? ''}${attributeSelector.sensitivity ? ` ${attributeSelector.sensitivity}` : ''}`,
},
} satisfies Variant)
}
else if (attributeSelector.attribute.startsWith('aria-')) {
let name = attributeSelector.attribute.slice(5)
replaceObject(variant, {
kind: 'functional',
root: 'aria',
modifier: null,
value:
attributeSelector.value === null
? { kind: 'arbitrary', value: name }
: attributeSelector.operator === '=' &&
attributeSelector.value === 'true' &&
attributeSelector.sensitivity === null
? { kind: 'named', value: name }
: {
kind: 'arbitrary',
value: `${attributeSelector.attribute}${attributeSelector.operator}${attributeSelector.quote ?? ''}${attributeSelector.value}${attributeSelector.quote ?? ''}${attributeSelector.sensitivity ? ` ${attributeSelector.sensitivity}` : ''}`,
},
} satisfies Variant)
}
else {
replaceObject(variant, {
kind: 'arbitrary',
selector: target.value,
relative: false,
} satisfies Variant)
}
}
if (prefixedVariant) {
return [prefixedVariant, variant]
}
}
}
return result
}
function optimizeModifier(candidate: Candidate, options: InternalCanonicalizeOptions): Candidate {
if (
(candidate.kind !== 'functional' && candidate.kind !== 'arbitrary') ||
candidate.modifier === null
) {
return candidate
}
let designSystem = options.designSystem
let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options.signatureOptions)
let targetSignature = signatures.get(designSystem.printCandidate(candidate))
let modifier = candidate.modifier
if (
targetSignature ===
signatures.get(designSystem.printCandidate({ ...candidate, modifier: null }))
) {
candidate.modifier = null
return candidate
}
{
let newModifier: NamedUtilityValue = {
kind: 'named',
value: modifier.value.endsWith('%')
? modifier.value.includes('.')
? `${Number(modifier.value.slice(0, -1))}`
: modifier.value.slice(0, -1)
: modifier.value,
fraction: null,
}
if (
targetSignature ===
signatures.get(designSystem.printCandidate({ ...candidate, modifier: newModifier }))
) {
candidate.modifier = newModifier
return candidate
}
}
{
let newModifier: NamedUtilityValue = {
kind: 'named',
value: `${parseFloat(modifier.value) * 100}`,
fraction: null,
}
if (
targetSignature ===
signatures.get(designSystem.printCandidate({ ...candidate, modifier: newModifier }))
) {
candidate.modifier = newModifier
return candidate
}
}
return candidate
}
export enum SignatureFeatures {
None = 0,
ExpandProperties = 1 << 0,
LogicalToPhysical = 1 << 1,
}
interface SignatureOptions {
rem: number | null
features: SignatureFeatures
}
export const UTILITY_SIGNATURE_KEY = Symbol()
function createUtilitySignatureCache(
designSystem: DesignSystem,
): DesignSystem['storage'][typeof UTILITY_SIGNATURE_KEY] {
return new DefaultMap((options: SignatureOptions) => {
return new DefaultMap<string, string | Symbol>((utility) => {
try {
utility =
designSystem.theme.prefix && !utility.startsWith(designSystem.theme.prefix)
? `${designSystem.theme.prefix}:${utility}`
: utility
let ast: AstNode[] = [styleRule('.x', [atRule('@apply', utility)])]
temporarilyDisableThemeInline(designSystem, () => {
for (let candidate of designSystem.parseCandidate(utility)) {
designSystem.compileAstNodes(candidate, CompileAstFlags.RespectImportant)
}
substituteAtApply(ast, designSystem)
})
canonicalizeAst(designSystem, ast, options)
let signature = toCss(ast)
return signature
} catch {
return Symbol()
}
})
})
}
function canonicalizeAst(designSystem: DesignSystem, ast: AstNode[], options: SignatureOptions) {
let { rem } = options
walk(ast, {
enter(node, ctx) {
if (node.kind === 'declaration') {
if (node.value === undefined || node.property === '--tw-sort') {
return WalkAction.Replace([])
}
if (node.property.startsWith('--tw-')) {
if (
(ctx.parent?.nodes ?? []).some(
(sibling) =>
sibling.kind === 'declaration' &&
node.value === sibling.value &&
node.important === sibling.important &&
!sibling.property.startsWith('--tw-'),
)
) {
return WalkAction.Replace([])
}
}
if (options.features & SignatureFeatures.ExpandProperties) {
let replacement = expandDeclaration(node, options.features)
if (replacement) return WalkAction.Replace(replacement)
}
if (node.value.includes('var(')) {
node.value = resolveVariablesInValue(node.value, designSystem)
}
node.value = constantFoldDeclaration(node.value, rem)
node.value = printArbitraryValue(node.value)
}
else if (node.kind === 'context' || node.kind === 'at-root') {
return WalkAction.Replace(node.nodes)
}
else if (node.kind === 'comment') {
return WalkAction.Replace([])
}
else if (node.kind === 'at-rule' && node.name === '@property') {
return WalkAction.Replace([])
}
},
exit(node) {
if (node.kind === 'rule' || node.kind === 'at-rule') {
if (node.nodes.length > 1) {
let seen = new Set<string>()
for (let i = node.nodes.length - 1; i >= 0; i--) {
let child = node.nodes[i]
if (child.kind !== 'declaration') continue
if (child.value === undefined) continue
if (seen.has(child.property)) {
node.nodes.splice(i, 1)
}
seen.add(child.property)
}
}
node.nodes.sort((a, b) => {
if (a.kind !== 'declaration') return 0
if (b.kind !== 'declaration') return 0
return a.property.localeCompare(b.property)
})
}
},
})
return ast
}
function resolveVariablesInValue(value: string, designSystem: DesignSystem): string {
let changed = false
let valueAst = ValueParser.parse(value)
let seen = new Set<string>()
walk(valueAst, (valueNode) => {
if (valueNode.kind !== 'function') return
if (valueNode.value !== 'var') return
if (valueNode.nodes.length !== 1 && valueNode.nodes.length < 3) {
return
}
let variable = valueNode.nodes[0].value
if (designSystem.theme.prefix && variable.startsWith(`--${designSystem.theme.prefix}-`)) {
variable = variable.slice(`--${designSystem.theme.prefix}-`.length)
}
let variableValue = designSystem.resolveThemeValue(variable)
if (seen.has(variable)) return
seen.add(variable)
if (variableValue === undefined) return
{
if (valueNode.nodes.length === 1) {
changed = true
valueNode.nodes.push(...ValueParser.parse(`,${variableValue}`))
}
}
{
if (valueNode.nodes.length >= 3) {
let nodeAsString = ValueParser.toCss(valueNode.nodes)
let constructedValue = `${valueNode.nodes[0].value},${variableValue}`
if (nodeAsString === constructedValue) {
changed = true
return WalkAction.Replace(ValueParser.parse(variableValue))
}
}
}
})
if (changed) return ValueParser.toCss(valueAst)
return value
}
const STATIC_UTILITIES_KEY = Symbol()
function createStaticUtilitiesCache(): DesignSystem['storage'][typeof STATIC_UTILITIES_KEY] {
return new DefaultMap((_optiones: SignatureOptions) => {
return new DefaultMap((_property: string) => {
return new DefaultMap((_value: string) => {
return new Set<string>()
})
})
})
}
const UTILITY_PROPERTIES_KEY = Symbol()
function createUtilityPropertiesCache(
designSystem: DesignSystem,
): DesignSystem['storage'][typeof UTILITY_PROPERTIES_KEY] {
return new DefaultMap((options: SignatureOptions) => {
return new DefaultMap((className) => {
let localPropertyValueLookup = new DefaultMap((_property) => new Set<string>())
if (designSystem.theme.prefix && !className.startsWith(designSystem.theme.prefix)) {
className = `${designSystem.theme.prefix}:${className}`
}
let parsed = designSystem.parseCandidate(className)
if (parsed.length === 0) return localPropertyValueLookup
walk(
canonicalizeAst(
designSystem,
designSystem.compileAstNodes(parsed[0]).map((x) => cloneAstNode(x.node)),
options,
),
(node) => {
if (node.kind === 'declaration') {
localPropertyValueLookup.get(node.property).add(node.value!)
designSystem.storage[STATIC_UTILITIES_KEY].get(options)
.get(node.property)
.get(node.value!)
.add(className)
}
},
)
return localPropertyValueLookup
})
})
}
const PRE_COMPUTED_UTILITIES_KEY = Symbol()
function createPreComputedUtilitiesCache(
designSystem: DesignSystem,
): DesignSystem['storage'][typeof PRE_COMPUTED_UTILITIES_KEY] {
return new DefaultMap((options: SignatureOptions) => {
let signatures = designSystem.storage[UTILITY_SIGNATURE_KEY].get(options)
let lookup = new DefaultMap<string, string[]>(() => [])
for (let [className, meta] of designSystem.getClassList()) {
let signature = signatures.get(className)
if (typeof signature !== 'string') continue
if (className[0] === '-' && className.endsWith('-0')) {
let positiveSignature = signatures.get(className.slice(1))
if (typeof positiveSignature === 'string' && signature === positiveSignature) {
continue
}
}
lookup.get(signature).push(className)
designSystem.storage[UTILITY_PROPERTIES_KEY].get(options).get(className)
for (let modifier of meta.modifiers) {
if (isValidSpacingMultiplier(modifier)) {
continue
}
let classNameWithModifier = `${className}/${modifier}`
let signature = signatures.get(classNameWithModifier)
if (typeof signature !== 'string') continue
lookup.get(signature).push(classNameWithModifier)
designSystem.storage[UTILITY_PROPERTIES_KEY].get(options).get(classNameWithModifier)
}
}
return lookup
})
}
export const VARIANT_SIGNATURE_KEY = Symbol()
function createVariantSignatureCache(
designSystem: DesignSystem,
): DesignSystem['storage'][typeof VARIANT_SIGNATURE_KEY] {
return new DefaultMap<string, string | Symbol>((variant) => {
try {
variant =
designSystem.theme.prefix && !variant.startsWith(designSystem.theme.prefix)
? `${designSystem.theme.prefix}:${variant}`
: variant
let ast: AstNode[] = [styleRule('.x', [atRule('@apply', `${variant}:flex`)])]
substituteAtApply(ast, designSystem)
walk(ast, (node) => {
if (node.kind === 'at-rule' && node.params.includes(' ')) {
node.params = node.params.replaceAll(' ', '')
}
else if (node.kind === 'rule') {
let selectorAst = SelectorParser.parse(node.selector)
let changed = false
walk(selectorAst, (node) => {
if (node.kind === 'separator' && node.value !== ' ') {
node.value = node.value.trim()
changed = true
}
else if (node.kind === 'function' && node.value === ':is') {
if (node.nodes.length === 1) {
changed = true
return WalkAction.Replace(node.nodes)
}
else if (
node.nodes.length === 2 &&
node.nodes[0].kind === 'selector' &&
node.nodes[0].value === '*' &&
node.nodes[1].kind === 'selector' &&
node.nodes[1].value[0] === ':'
) {
changed = true
return WalkAction.Replace(node.nodes[1])
}
}
else if (
node.kind === 'function' &&
node.value[0] === ':' &&
node.nodes[0]?.kind === 'selector' &&
node.nodes[0]?.value[0] === ':'
) {
changed = true
node.nodes.unshift({ kind: 'selector', value: '*' })
}
})
if (changed) {
node.selector = SelectorParser.toCss(selectorAst)
}
}
})
let signature = toCss(ast)
return signature
} catch {
return Symbol()
}
})
}
export const PRE_COMPUTED_VARIANTS_KEY = Symbol()
function createPreComputedVariantsCache(
designSystem: DesignSystem,
): DesignSystem['storage'][typeof PRE_COMPUTED_VARIANTS_KEY] {
let signatures = designSystem.storage[VARIANT_SIGNATURE_KEY]
let lookup = new DefaultMap<string, string[]>(() => [])
for (let [root, variant] of designSystem.variants.entries()) {
if (variant.kind === 'static') {
let signature = signatures.get(root)
if (typeof signature !== 'string') continue
lookup.get(signature).push(root)
}
}
return lookup
}
function temporarilyDisableThemeInline<T>(designSystem: DesignSystem, cb: () => T): T {
let originalGet = designSystem.theme.values.get
let restorableInlineOptions = new Set<{ options: ThemeOptions }>()
designSystem.theme.values.get = (key: string) => {
let value = originalGet.call(designSystem.theme.values, key)
if (value === undefined) return value
if (value.options & ThemeOptions.INLINE) {
restorableInlineOptions.add(value)
value.options &= ~ThemeOptions.INLINE
}
return value
}
try {
return cb()
} finally {
designSystem.theme.values.get = originalGet
for (let value of restorableInlineOptions) {
value.options |= ThemeOptions.INLINE
}
}
}
function* combinations<T>(arr: T[]): Generator<T[]> {
let n = arr.length
let limit = 1n << BigInt(n)
for (let k = n; k >= 2; k--) {
let mask = (1n << BigInt(k)) - 1n
while (mask < limit) {
let out = []
for (let i = 0; i < n; i++) {
if ((mask >> BigInt(i)) & 1n) {
out.push(arr[i])
}
}
yield out
let carry = mask & -mask
let ripple = mask + carry
mask = (((ripple ^ mask) >> 2n) / carry) | ripple
}
}
}
function intersection<T>(a: Set<T>, b: Set<T>): Set<T> {
if (typeof a.intersection === 'function') return a.intersection(b)
if (a.size === 0 || b.size === 0) return new Set<T>()
let result = new Set<T>(a)
for (let item of b) {
if (!result.has(item)) {
result.delete(item)
}
}
return result
}