import { Features } from '.'
import { walk, type AstNode } from './ast'
import type { DesignSystem } from './design-system'
import { withAlpha } from './utilities'
import { segment } from './utils/segment'
import * as ValueParser from './value-parser'
const functions: Record<string, (designSystem: DesignSystem, ...args: string[]) => string> = {
'--alpha': alpha,
'--spacing': spacing,
'--theme': theme,
theme: legacyTheme,
}
function alpha(_designSystem: DesignSystem, value: string, ...rest: string[]) {
let [color, alpha] = segment(value, '/').map((v) => v.trim())
if (!color || !alpha) {
throw new Error(
`The --alpha(…) function requires a color and an alpha value, e.g.: \`--alpha(${color || 'var(--my-color)'} / ${alpha || '50%'})\``,
)
}
if (rest.length > 0) {
throw new Error(
`The --alpha(…) function only accepts one argument, e.g.: \`--alpha(${color || 'var(--my-color)'} / ${alpha || '50%'})\``,
)
}
return withAlpha(color, alpha)
}
function spacing(designSystem: DesignSystem, value: string, ...rest: string[]) {
if (!value) {
throw new Error(`The --spacing(…) function requires an argument, but received none.`)
}
if (rest.length > 0) {
throw new Error(
`The --spacing(…) function only accepts a single argument, but received ${rest.length + 1}.`,
)
}
let multiplier = designSystem.theme.resolve(null, ['--spacing'])
if (!multiplier) {
throw new Error(
'The --spacing(…) function requires that the `--spacing` theme variable exists, but it was not found.',
)
}
return `calc(${multiplier} * ${value})`
}
function theme(designSystem: DesignSystem, path: string, ...fallback: string[]) {
if (!path.startsWith('--')) {
throw new Error(`The --theme(…) function can only be used with CSS variables from your theme.`)
}
return legacyTheme(designSystem, path, ...fallback)
}
function legacyTheme(designSystem: DesignSystem, path: string, ...fallback: string[]) {
path = eventuallyUnquote(path)
let resolvedValue = designSystem.resolveThemeValue(path)
if (!resolvedValue && fallback.length > 0) {
return fallback.join(', ')
}
if (!resolvedValue) {
throw new Error(
`Could not resolve value for theme function: \`theme(${path})\`. Consider checking if the path is correct or provide a fallback value to silence this error.`,
)
}
return resolvedValue
}
export const THEME_FUNCTION_INVOCATION = new RegExp(
Object.keys(functions)
.map((x) => `${x}\\(`)
.join('|'),
)
export function substituteFunctions(ast: AstNode[], designSystem: DesignSystem) {
let features = Features.None
walk(ast, (node) => {
if (node.kind === 'declaration' && node.value && THEME_FUNCTION_INVOCATION.test(node.value)) {
features |= Features.ThemeFunction
node.value = substituteFunctionsInValue(node.value, designSystem)
return
}
if (node.kind === 'at-rule') {
if (
(node.name === '@media' ||
node.name === '@custom-media' ||
node.name === '@container' ||
node.name === '@supports') &&
THEME_FUNCTION_INVOCATION.test(node.params)
) {
features |= Features.ThemeFunction
node.params = substituteFunctionsInValue(node.params, designSystem)
}
}
})
return features
}
export function substituteFunctionsInValue(value: string, designSystem: DesignSystem): string {
let ast = ValueParser.parse(value)
ValueParser.walk(ast, (node, { replaceWith }) => {
if (node.kind === 'function' && node.value in functions) {
let args = segment(ValueParser.toCss(node.nodes).trim(), ',').map((x) => x.trim())
let result = functions[node.value as keyof typeof functions](designSystem, ...args)
return replaceWith(ValueParser.parse(result))
}
})
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
}