export type Rule = {
kind: 'rule'
selector: string
nodes: AstNode[]
}
export type Declaration = {
kind: 'declaration'
property: string
value: string | undefined
important: boolean
}
export type Comment = {
kind: 'comment'
value: string
}
export type Context = {
kind: 'context'
context: Record<string, string>
nodes: AstNode[]
}
export type AstNode = Rule | Declaration | Comment | Context
export function rule(selector: string, nodes: AstNode[]): Rule {
return {
kind: 'rule',
selector,
nodes,
}
}
export function decl(property: string, value: string | undefined): Declaration {
return {
kind: 'declaration',
property,
value,
important: false,
}
}
export function comment(value: string): Comment {
return {
kind: 'comment',
value: value,
}
}
export function context(context: Record<string, string>, nodes: AstNode[]): Context {
return {
kind: 'context',
context,
nodes,
}
}
export enum WalkAction {
Continue,
Skip,
Stop,
}
export function walk(
ast: AstNode[],
visit: (
node: AstNode,
utils: {
parent: AstNode | null
replaceWith(newNode: AstNode | AstNode[]): void
context: Record<string, string>
},
) => void | WalkAction,
parent: AstNode | null = null,
context: Record<string, string> = {},
) {
for (let i = 0; i < ast.length; i++) {
let node = ast[i]
if (node.kind === 'context') {
walk(node.nodes, visit, parent, { ...context, ...node.context })
continue
}
let status =
visit(node, {
parent,
replaceWith(newNode) {
ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode]))
i--
},
context,
}) ?? WalkAction.Continue
if (status === WalkAction.Stop) return
if (status === WalkAction.Skip) continue
if (node.kind === 'rule') {
walk(node.nodes, visit, node, context)
}
}
}
export function toCss(ast: AstNode[]) {
let atRoots: string = ''
let seenAtProperties = new Set<string>()
let propertyFallbacksRoot: Declaration[] = []
let propertyFallbacksUniversal: Declaration[] = []
function stringify(node: AstNode, depth = 0): string {
let css = ''
let indent = ' '.repeat(depth)
if (node.kind === 'rule') {
if (node.selector === '@at-root') {
for (let child of node.nodes) {
atRoots += stringify(child, 0)
}
return css
}
if (node.selector === '@tailwind utilities') {
for (let child of node.nodes) {
css += stringify(child, depth)
}
return css
}
if (node.selector[0] === '@' && node.nodes.length === 0) {
return `${indent}${node.selector};\n`
}
if (node.selector[0] === '@' && node.selector.startsWith('@property ') && depth === 0) {
if (seenAtProperties.has(node.selector)) {
return ''
}
let property = node.selector.replace(/@property\s*/g, '')
let initialValue = null
let inherits = false
for (let prop of node.nodes) {
if (prop.kind !== 'declaration') continue
if (prop.property === 'initial-value') {
initialValue = prop.value
} else if (prop.property === 'inherits') {
inherits = prop.value === 'true'
}
}
if (inherits) {
propertyFallbacksRoot.push(decl(property, initialValue ?? 'initial'))
} else {
propertyFallbacksUniversal.push(decl(property, initialValue ?? 'initial'))
}
seenAtProperties.add(node.selector)
}
css += `${indent}${node.selector} {\n`
for (let child of node.nodes) {
css += stringify(child, depth + 1)
}
css += `${indent}}\n`
}
else if (node.kind === 'comment') {
css += `${indent}/*${node.value}*/\n`
}
else if (node.kind === 'context') {
for (let child of node.nodes) {
css += stringify(child, depth)
}
}
else if (node.property !== '--tw-sort' && node.value !== undefined && node.value !== null) {
css += `${indent}${node.property}: ${node.value}${node.important ? '!important' : ''};\n`
}
return css
}
let css = ''
for (let node of ast) {
let result = stringify(node)
if (result !== '') {
css += result
}
}
let fallbackAst = []
if (propertyFallbacksRoot.length) {
fallbackAst.push(rule(':root', propertyFallbacksRoot))
}
if (propertyFallbacksUniversal.length) {
fallbackAst.push(rule('*, ::before, ::after, ::backdrop', propertyFallbacksUniversal))
}
let fallback = ''
if (fallbackAst.length) {
fallback = stringify(
rule('@supports (-moz-orient: inline)', [rule('@layer base', fallbackAst)]),
)
}
return `${css}${fallback}${atRoots}`
}