import type { DesignSystem } from './design-system'
import { decodeArbitraryValue } from './utils/decode-arbitrary-value'
import { DefaultMap } from './utils/default-map'
import { isValidArbitrary } from './utils/is-valid-arbitrary'
import { segment } from './utils/segment'
import * as ValueParser from './value-parser'
const COLON = 0x3a
const DASH = 0x2d
const LOWER_A = 0x61
const LOWER_Z = 0x7a
export type ArbitraryUtilityValue = {
kind: 'arbitrary'
dataType: string | null
value: string
}
export type NamedUtilityValue = {
kind: 'named'
value: string
fraction: string | null
}
export type ArbitraryModifier = {
kind: 'arbitrary'
value: string
}
export type NamedModifier = {
kind: 'named'
value: string
}
export type CandidateModifier = ArbitraryModifier | NamedModifier
type ArbitraryVariantValue = {
kind: 'arbitrary'
value: string
}
type NamedVariantValue = {
kind: 'named'
value: string
}
export type Variant =
| {
kind: 'arbitrary'
selector: string
relative: boolean
}
| {
kind: 'static'
root: string
}
| {
kind: 'functional'
root: string
value: ArbitraryVariantValue | NamedVariantValue | null
modifier: ArbitraryModifier | NamedModifier | null
}
| {
kind: 'compound'
root: string
modifier: ArbitraryModifier | NamedModifier | null
variant: Variant
}
export type Candidate =
| {
kind: 'arbitrary'
property: string
value: string
modifier: ArbitraryModifier | NamedModifier | null
variants: Variant[]
important: boolean
raw: string
}
| {
kind: 'static'
root: string
variants: Variant[]
important: boolean
raw: string
}
| {
kind: 'functional'
root: string
value: ArbitraryUtilityValue | NamedUtilityValue | null
modifier: ArbitraryModifier | NamedModifier | null
variants: Variant[]
important: boolean
raw: string
}
export function* parseCandidate(input: string, designSystem: DesignSystem): Iterable<Candidate> {
let rawVariants = segment(input, ':')
if (designSystem.theme.prefix) {
if (rawVariants.length === 1) return null
if (rawVariants[0] !== designSystem.theme.prefix) return null
rawVariants.shift()
}
let base = rawVariants.pop()!
let parsedCandidateVariants: Variant[] = []
for (let i = rawVariants.length - 1; i >= 0; --i) {
let parsedVariant = designSystem.parseVariant(rawVariants[i])
if (parsedVariant === null) return
parsedCandidateVariants.push(parsedVariant)
}
let important = false
if (base[base.length - 1] === '!') {
important = true
base = base.slice(0, -1)
}
else if (base[0] === '!') {
important = true
base = base.slice(1)
}
if (designSystem.utilities.has(base, 'static') && !base.includes('[')) {
yield {
kind: 'static',
root: base,
variants: parsedCandidateVariants,
important,
raw: input,
}
}
let [baseWithoutModifier, modifierSegment = null, additionalModifier] = segment(base, '/')
if (additionalModifier) return
let parsedModifier = modifierSegment === null ? null : parseModifier(modifierSegment)
if (modifierSegment !== null && parsedModifier === null) return
if (baseWithoutModifier[0] === '[') {
if (baseWithoutModifier[baseWithoutModifier.length - 1] !== ']') return
let charCode = baseWithoutModifier.charCodeAt(1)
if (charCode !== DASH && !(charCode >= LOWER_A && charCode <= LOWER_Z)) {
return
}
baseWithoutModifier = baseWithoutModifier.slice(1, -1)
let idx = baseWithoutModifier.indexOf(':')
if (idx === -1 || idx === 0 || idx === baseWithoutModifier.length - 1) return
let property = baseWithoutModifier.slice(0, idx)
let value = decodeArbitraryValue(baseWithoutModifier.slice(idx + 1))
if (!isValidArbitrary(value)) return
yield {
kind: 'arbitrary',
property,
value,
modifier: parsedModifier,
variants: parsedCandidateVariants,
important,
raw: input,
}
return
}
let roots: Iterable<Root>
if (baseWithoutModifier[baseWithoutModifier.length - 1] === ']') {
let idx = baseWithoutModifier.indexOf('-[')
if (idx === -1) return
let root = baseWithoutModifier.slice(0, idx)
if (!designSystem.utilities.has(root, 'functional')) return
let value = baseWithoutModifier.slice(idx + 1)
roots = [[root, value]]
}
else if (baseWithoutModifier[baseWithoutModifier.length - 1] === ')') {
let idx = baseWithoutModifier.indexOf('-(')
if (idx === -1) return
let root = baseWithoutModifier.slice(0, idx)
if (!designSystem.utilities.has(root, 'functional')) return
let value = baseWithoutModifier.slice(idx + 2, -1)
let parts = segment(value, ':')
let dataType = null
if (parts.length === 2) {
dataType = parts[0]
value = parts[1]
}
if (value[0] !== '-' || value[1] !== '-') return
if (!isValidArbitrary(value)) return
roots = [[root, dataType === null ? `[var(${value})]` : `[${dataType}:var(${value})]`]]
}
else {
roots = findRoots(baseWithoutModifier, (root: string) => {
return designSystem.utilities.has(root, 'functional')
})
}
for (let [root, value] of roots) {
let candidate: Candidate = {
kind: 'functional',
root,
modifier: parsedModifier,
value: null,
variants: parsedCandidateVariants,
important,
raw: input,
}
if (value === null) {
yield candidate
continue
}
{
let startArbitraryIdx = value.indexOf('[')
let valueIsArbitrary = startArbitraryIdx !== -1
if (valueIsArbitrary) {
if (value[value.length - 1] !== ']') return
let arbitraryValue = decodeArbitraryValue(value.slice(startArbitraryIdx + 1, -1))
if (!isValidArbitrary(arbitraryValue)) continue
let typehint = ''
for (let i = 0; i < arbitraryValue.length; i++) {
let code = arbitraryValue.charCodeAt(i)
if (code === COLON) {
typehint = arbitraryValue.slice(0, i)
arbitraryValue = arbitraryValue.slice(i + 1)
break
}
if (code === DASH || (code >= LOWER_A && code <= LOWER_Z)) {
continue
}
break
}
if (arbitraryValue.length === 0 || arbitraryValue.trim().length === 0) {
continue
}
candidate.value = {
kind: 'arbitrary',
dataType: typehint || null,
value: arbitraryValue,
}
} else {
let fraction =
modifierSegment === null || candidate.modifier?.kind === 'arbitrary'
? null
: `${value}/${modifierSegment}`
candidate.value = {
kind: 'named',
value,
fraction,
}
}
}
yield candidate
}
}
function parseModifier(modifier: string): CandidateModifier | null {
if (modifier[0] === '[' && modifier[modifier.length - 1] === ']') {
let arbitraryValue = decodeArbitraryValue(modifier.slice(1, -1))
if (!isValidArbitrary(arbitraryValue)) return null
if (arbitraryValue.length === 0 || arbitraryValue.trim().length === 0) return null
return {
kind: 'arbitrary',
value: arbitraryValue,
}
}
if (modifier[0] === '(' && modifier[modifier.length - 1] === ')') {
modifier = modifier.slice(1, -1)
if (modifier[0] !== '-' || modifier[1] !== '-') return null
if (!isValidArbitrary(modifier)) return null
modifier = `var(${modifier})`
let arbitraryValue = decodeArbitraryValue(modifier)
return {
kind: 'arbitrary',
value: arbitraryValue,
}
}
return {
kind: 'named',
value: modifier,
}
}
export function parseVariant(variant: string, designSystem: DesignSystem): Variant | null {
if (variant[0] === '[' && variant[variant.length - 1] === ']') {
if (variant[1] === '@' && variant.includes('&')) return null
let selector = decodeArbitraryValue(variant.slice(1, -1))
if (!isValidArbitrary(selector)) return null
if (selector.length === 0 || selector.trim().length === 0) return null
let relative = selector[0] === '>' || selector[0] === '+' || selector[0] === '~'
if (!relative && selector[0] !== '@' && !selector.includes('&')) {
selector = `&:is(${selector})`
}
return {
kind: 'arbitrary',
selector,
relative,
}
}
{
let [variantWithoutModifier, modifier = null, additionalModifier] = segment(variant, '/')
if (additionalModifier) return null
let roots = findRoots(variantWithoutModifier, (root) => {
return designSystem.variants.has(root)
})
for (let [root, value] of roots) {
switch (designSystem.variants.kind(root)) {
case 'static': {
if (value !== null) return null
if (modifier !== null) return null
return {
kind: 'static',
root,
}
}
case 'functional': {
let parsedModifier = modifier === null ? null : parseModifier(modifier)
if (modifier !== null && parsedModifier === null) return null
if (value === null) {
return {
kind: 'functional',
root,
modifier: parsedModifier,
value: null,
}
}
if (value[value.length - 1] === ']') {
if (value[0] !== '[') continue
let arbitraryValue = decodeArbitraryValue(value.slice(1, -1))
if (!isValidArbitrary(arbitraryValue)) return null
if (arbitraryValue.length === 0 || arbitraryValue.trim().length === 0) return null
return {
kind: 'functional',
root,
modifier: parsedModifier,
value: {
kind: 'arbitrary',
value: arbitraryValue,
},
}
}
if (value[value.length - 1] === ')') {
if (value[0] !== '(') continue
let arbitraryValue = decodeArbitraryValue(value.slice(1, -1))
if (!isValidArbitrary(arbitraryValue)) return null
if (arbitraryValue.length === 0 || arbitraryValue.trim().length === 0) return null
if (arbitraryValue[0] !== '-' || arbitraryValue[1] !== '-') return null
return {
kind: 'functional',
root,
modifier: parsedModifier,
value: {
kind: 'arbitrary',
value: `var(${arbitraryValue})`,
},
}
}
return {
kind: 'functional',
root,
modifier: parsedModifier,
value: { kind: 'named', value },
}
}
case 'compound': {
if (value === null) return null
let subVariant = designSystem.parseVariant(value)
if (subVariant === null) return null
if (!designSystem.variants.compoundsWith(root, subVariant)) return null
let parsedModifier = modifier === null ? null : parseModifier(modifier)
if (modifier !== null && parsedModifier === null) return null
return {
kind: 'compound',
root,
modifier: parsedModifier,
variant: subVariant,
}
}
}
}
}
return null
}
type Root = [
root: string,
value: string | null,
]
function* findRoots(input: string, exists: (input: string) => boolean): Iterable<Root> {
if (exists(input)) {
yield [input, null]
}
let idx = input.lastIndexOf('-')
while (idx > 0) {
let maybeRoot = input.slice(0, idx)
if (exists(maybeRoot)) {
let root: Root = [maybeRoot, input.slice(idx + 1)]
if (root[1] === '') break
yield root
}
idx = input.lastIndexOf('-', idx - 1)
}
if (input[0] === '@' && exists('@')) {
yield ['@', input.slice(1)]
}
}
export function printCandidate(designSystem: DesignSystem, candidate: Candidate) {
let parts: string[] = []
for (let variant of candidate.variants) {
parts.unshift(printVariant(variant))
}
if (designSystem.theme.prefix) {
parts.unshift(designSystem.theme.prefix)
}
let base: string = ''
if (candidate.kind === 'static') {
base += candidate.root
}
if (candidate.kind === 'functional') {
base += candidate.root
if (candidate.value) {
if (candidate.value.kind === 'arbitrary') {
if (candidate.value !== null) {
let isVarValue = isVar(candidate.value.value)
let value = isVarValue ? candidate.value.value.slice(4, -1) : candidate.value.value
let [open, close] = isVarValue ? ['(', ')'] : ['[', ']']
if (candidate.value.dataType) {
base += `-${open}${candidate.value.dataType}:${printArbitraryValue(value)}${close}`
} else {
base += `-${open}${printArbitraryValue(value)}${close}`
}
}
} else if (candidate.value.kind === 'named') {
base += `-${candidate.value.value}`
}
}
}
if (candidate.kind === 'arbitrary') {
base += `[${candidate.property}:${printArbitraryValue(candidate.value)}]`
}
if (candidate.kind === 'arbitrary' || candidate.kind === 'functional') {
base += printModifier(candidate.modifier)
}
if (candidate.important) {
base += '!'
}
parts.push(base)
return parts.join(':')
}
export function printModifier(modifier: ArbitraryModifier | NamedModifier | null) {
if (modifier === null) return ''
let isVarValue = isVar(modifier.value)
let value = isVarValue ? modifier.value.slice(4, -1) : modifier.value
let [open, close] = isVarValue ? ['(', ')'] : ['[', ']']
if (modifier.kind === 'arbitrary') {
return `/${open}${printArbitraryValue(value)}${close}`
} else if (modifier.kind === 'named') {
return `/${modifier.value}`
} else {
modifier satisfies never
return ''
}
}
export function printVariant(variant: Variant) {
if (variant.kind === 'static') {
return variant.root
}
if (variant.kind === 'arbitrary') {
return `[${printArbitraryValue(simplifyArbitraryVariant(variant.selector))}]`
}
let base: string = ''
if (variant.kind === 'functional') {
base += variant.root
let hasDash = variant.root !== '@'
if (variant.value) {
if (variant.value.kind === 'arbitrary') {
let isVarValue = isVar(variant.value.value)
let value = isVarValue ? variant.value.value.slice(4, -1) : variant.value.value
let [open, close] = isVarValue ? ['(', ')'] : ['[', ']']
base += `${hasDash ? '-' : ''}${open}${printArbitraryValue(value)}${close}`
} else if (variant.value.kind === 'named') {
base += `${hasDash ? '-' : ''}${variant.value.value}`
}
}
}
if (variant.kind === 'compound') {
base += variant.root
base += '-'
base += printVariant(variant.variant)
}
if (variant.kind === 'functional' || variant.kind === 'compound') {
base += printModifier(variant.modifier)
}
return base
}
const printArbitraryValueCache = new DefaultMap<string, string>((input) => {
let ast = ValueParser.parse(input)
let drop = new Set<ValueParser.ValueAstNode>()
ValueParser.walk(ast, (node, { parent }) => {
let parentArray = parent === null ? ast : (parent.nodes ?? [])
if (
node.kind === 'word' &&
(node.value === '+' || node.value === '-' || node.value === '*' || node.value === '/')
) {
let idx = parentArray.indexOf(node) ?? -1
if (idx === -1) return
let previous = parentArray[idx - 1]
if (previous?.kind !== 'separator' || previous.value !== ' ') return
let next = parentArray[idx + 1]
if (next?.kind !== 'separator' || next.value !== ' ') return
drop.add(previous)
drop.add(next)
}
else if (node.kind === 'separator' && node.value.trim() === '/') {
node.value = '/'
}
else if (node.kind === 'separator' && node.value.length > 0 && node.value.trim() === '') {
if (parentArray[0] === node || parentArray[parentArray.length - 1] === node) {
drop.add(node)
}
}
else if (node.kind === 'separator' && node.value.trim() === ',') {
node.value = ','
}
})
if (drop.size > 0) {
ValueParser.walk(ast, (node, { replaceWith }) => {
if (drop.has(node)) {
drop.delete(node)
replaceWith([])
}
})
}
recursivelyEscapeUnderscores(ast)
return ValueParser.toCss(ast)
})
export function printArbitraryValue(input: string) {
return printArbitraryValueCache.get(input)
}
const simplifyArbitraryVariantCache = new DefaultMap<string, string>((input) => {
let ast = ValueParser.parse(input)
if (
ast.length === 3 &&
ast[0].kind === 'word' &&
ast[0].value === '&' &&
ast[1].kind === 'separator' &&
ast[1].value === ':' &&
ast[2].kind === 'function' &&
ast[2].value === 'is'
) {
return ValueParser.toCss(ast[2].nodes)
}
return input
})
function simplifyArbitraryVariant(input: string) {
return simplifyArbitraryVariantCache.get(input)
}
function recursivelyEscapeUnderscores(ast: ValueParser.ValueAstNode[]) {
for (let node of ast) {
switch (node.kind) {
case 'function': {
if (node.value === 'url' || node.value.endsWith('_url')) {
node.value = escapeUnderscore(node.value)
break
}
if (
node.value === 'var' ||
node.value.endsWith('_var') ||
node.value === 'theme' ||
node.value.endsWith('_theme')
) {
node.value = escapeUnderscore(node.value)
for (let i = 0; i < node.nodes.length; i++) {
recursivelyEscapeUnderscores([node.nodes[i]])
}
break
}
node.value = escapeUnderscore(node.value)
recursivelyEscapeUnderscores(node.nodes)
break
}
case 'separator':
node.value = escapeUnderscore(node.value)
break
case 'word': {
if (node.value[0] !== '-' || node.value[1] !== '-') {
node.value = escapeUnderscore(node.value)
}
break
}
default:
never(node)
}
}
}
const isVarCache = new DefaultMap<string, boolean>((value) => {
let ast = ValueParser.parse(value)
return ast.length === 1 && ast[0].kind === 'function' && ast[0].value === 'var'
})
function isVar(value: string) {
return isVarCache.get(value)
}
function never(value: never): never {
throw new Error(`Unexpected value: ${value}`)
}
function escapeUnderscore(value: string): string {
return value
.replaceAll('_', String.raw`\_`)
.replaceAll(' ', '_')
}