import { atRule, decl, rule, type AstNode, type Rule, type StyleRule } from './ast'
import { type Candidate, type Variant } from './candidate'
import { CompileAstFlags, type DesignSystem } from './design-system'
import GLOBAL_PROPERTY_ORDER from './property-order'
import { asColor, type Utility } from './utilities'
import { compare } from './utils/compare'
import { escape } from './utils/escape'
import type { Variants } from './variants'
import { walk, WalkAction } from './walk'
export function compileCandidates(
rawCandidates: Iterable<string>,
designSystem: DesignSystem,
{
onInvalidCandidate,
respectImportant,
}: { onInvalidCandidate?: (candidate: string) => void; respectImportant?: boolean } = {},
) {
let nodeSorting = new Map<
AstNode,
{ properties: { order: number[]; count: number }; variants: bigint; candidate: string }
>()
let astNodes: AstNode[] = []
let matches = new Map<string, Candidate[]>()
for (let rawCandidate of rawCandidates) {
if (designSystem.invalidCandidates.has(rawCandidate)) {
onInvalidCandidate?.(rawCandidate)
continue
}
let candidates = designSystem.parseCandidate(rawCandidate)
if (candidates.length === 0) {
onInvalidCandidate?.(rawCandidate)
continue
}
matches.set(rawCandidate, candidates)
}
let flags = CompileAstFlags.None
if (respectImportant ?? true) {
flags |= CompileAstFlags.RespectImportant
}
let variantOrderMap = designSystem.getVariantOrder()
for (let [rawCandidate, candidates] of matches) {
let found = false
for (let candidate of candidates) {
let rules = designSystem.compileAstNodes(candidate, flags)
if (rules.length === 0) continue
found = true
for (let { node, propertySort } of rules) {
let variantOrder = 0n
for (let variant of candidate.variants) {
variantOrder |= 1n << BigInt(variantOrderMap.get(variant)!)
}
nodeSorting.set(node, {
properties: propertySort,
variants: variantOrder,
candidate: rawCandidate,
})
astNodes.push(node)
}
}
if (!found) {
onInvalidCandidate?.(rawCandidate)
}
}
astNodes.sort((a, z) => {
let aSorting = nodeSorting.get(a)!
let zSorting = nodeSorting.get(z)!
if (aSorting.variants - zSorting.variants !== 0n) {
return Number(aSorting.variants - zSorting.variants)
}
let offset = 0
while (
offset < aSorting.properties.order.length &&
offset < zSorting.properties.order.length &&
aSorting.properties.order[offset] === zSorting.properties.order[offset]
) {
offset += 1
}
return (
(aSorting.properties.order[offset] ?? Infinity) -
(zSorting.properties.order[offset] ?? Infinity) ||
zSorting.properties.count - aSorting.properties.count ||
compare(aSorting.candidate, zSorting.candidate)
)
})
return {
astNodes,
nodeSorting,
}
}
export function compileAstNodes(
candidate: Candidate,
designSystem: DesignSystem,
flags: CompileAstFlags,
) {
let asts = compileBaseUtility(candidate, designSystem)
if (asts.length === 0) return []
let respectImportant = designSystem.important && Boolean(flags & CompileAstFlags.RespectImportant)
let rules: {
node: AstNode
propertySort: {
order: number[]
count: number
}
}[] = []
let selector = `.${escape(candidate.raw)}`
for (let nodes of asts) {
let propertySort = getPropertySort(nodes)
if (candidate.important || respectImportant) {
applyImportant(nodes)
}
let node: StyleRule = {
kind: 'rule',
selector,
nodes,
}
for (let variant of candidate.variants) {
let result = applyVariant(node, variant, designSystem.variants)
if (result === null) return []
}
rules.push({
node,
propertySort,
})
}
return rules
}
export function applyVariant(
node: Rule,
variant: Variant,
variants: Variants,
depth: number = 0,
): null | void {
if (variant.kind === 'arbitrary') {
if (variant.relative && depth === 0) return null
node.nodes = [rule(variant.selector, node.nodes)]
return
}
let { applyFn } = variants.get(variant.root)!
if (variant.kind === 'compound') {
let isolatedNode = atRule('@slot')
let result = applyVariant(isolatedNode, variant.variant, variants, depth + 1)
if (result === null) return null
if (variant.root === 'not' && isolatedNode.nodes.length > 1) {
return null
}
for (let child of isolatedNode.nodes) {
if (child.kind !== 'rule' && child.kind !== 'at-rule') return null
let result = applyFn(child, variant)
if (result === null) return null
}
{
walk(isolatedNode.nodes, (child) => {
if ((child.kind === 'rule' || child.kind === 'at-rule') && child.nodes.length <= 0) {
child.nodes = node.nodes
return WalkAction.Skip
}
})
node.nodes = isolatedNode.nodes
}
return
}
let result = applyFn(node, variant)
if (result === null) return null
}
function isFallbackUtility(utility: Utility) {
let types = utility.options?.types ?? []
return types.length > 1 && types.includes('any')
}
function compileBaseUtility(candidate: Candidate, designSystem: DesignSystem) {
if (candidate.kind === 'arbitrary') {
let value: string | null = candidate.value
if (candidate.modifier) {
value = asColor(value, candidate.modifier, designSystem.theme)
}
if (value === null) return []
return [[decl(candidate.property, value)]]
}
let utilities = designSystem.utilities.get(candidate.root) ?? []
let asts: AstNode[][] = []
let normalUtilities = utilities.filter((u) => !isFallbackUtility(u))
for (let utility of normalUtilities) {
if (utility.kind !== candidate.kind) continue
let compiledNodes = utility.compileFn(candidate)
if (compiledNodes === undefined) continue
if (compiledNodes === null) return asts
asts.push(compiledNodes)
}
if (asts.length > 0) return asts
let fallbackUtilities = utilities.filter((u) => isFallbackUtility(u))
for (let utility of fallbackUtilities) {
if (utility.kind !== candidate.kind) continue
let compiledNodes = utility.compileFn(candidate)
if (compiledNodes === undefined) continue
if (compiledNodes === null) return asts
asts.push(compiledNodes)
}
return asts
}
function applyImportant(ast: AstNode[]): void {
for (let node of ast) {
if (node.kind === 'at-root') {
continue
}
if (node.kind === 'declaration') {
node.important = true
} else if (node.kind === 'rule' || node.kind === 'at-rule') {
applyImportant(node.nodes)
}
}
}
function getPropertySort(nodes: AstNode[]) {
let order = new Set<number>()
let count = 0
let q: AstNode[] = nodes.slice()
let seenTwSort = false
while (q.length > 0) {
let node = q.shift()!
if (node.kind === 'declaration') {
if (node.value === undefined) continue
count++
if (seenTwSort) continue
if (node.property === '--tw-sort') {
let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.value ?? '')
if (idx !== -1) {
order.add(idx)
seenTwSort = true
continue
}
}
let idx = GLOBAL_PROPERTY_ORDER.indexOf(node.property)
if (idx !== -1) order.add(idx)
} else if (node.kind === 'rule' || node.kind === 'at-rule') {
for (let child of node.nodes) {
q.push(child)
}
}
}
return {
order: Array.from(order).sort((a, z) => a - z),
count,
}
}