import { parseAtRule } from './css-parser'
const AT_SIGN = 0x40
export type StyleRule = {
kind: 'rule'
selector: string
nodes: AstNode[]
}
export type AtRule = {
kind: 'at-rule'
name: string
params: 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 AtRoot = {
kind: 'at-root'
nodes: AstNode[]
}
export type Rule = StyleRule | AtRule
export type AstNode = StyleRule | AtRule | Declaration | Comment | Context | AtRoot
export function styleRule(selector: string, nodes: AstNode[] = []): StyleRule {
return {
kind: 'rule',
selector,
nodes,
}
}
export function atRule(name: string, params: string = '', nodes: AstNode[] = []): AtRule {
return {
kind: 'at-rule',
name,
params,
nodes,
}
}
export function rule(selector: string, nodes: AstNode[] = []): StyleRule | AtRule {
if (selector.charCodeAt(0) === AT_SIGN) {
return parseAtRule(selector, nodes)
}
return styleRule(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 function atRoot(nodes: AstNode[]): AtRoot {
return {
kind: 'at-root',
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>
path: AstNode[]
},
) => void | WalkAction,
parentPath: AstNode[] = [],
context: Record<string, string> = {},
) {
for (let i = 0; i < ast.length; i++) {
let node = ast[i]
let path = [...parentPath, node]
let parent = parentPath.at(-1) ?? null
if (node.kind === 'context') {
walk(node.nodes, visit, parentPath, { ...context, ...node.context })
continue
}
let status =
visit(node, {
parent,
context,
path,
replaceWith(newNode) {
ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode]))
i--
},
}) ?? WalkAction.Continue
if (status === WalkAction.Stop) return
if (status === WalkAction.Skip) continue
if (node.kind === 'rule' || node.kind === 'at-rule') {
walk(node.nodes, visit, path, context)
}
}
}
export function walkDepth(
ast: AstNode[],
visit: (
node: AstNode,
utils: {
parent: AstNode | null
path: AstNode[]
context: Record<string, string>
replaceWith(newNode: AstNode[]): void
},
) => void,
parentPath: AstNode[] = [],
context: Record<string, string> = {},
) {
for (let i = 0; i < ast.length; i++) {
let node = ast[i]
let path = [...parentPath, node]
let parent = parentPath.at(-1) ?? null
if (node.kind === 'rule' || node.kind === 'at-rule') {
walkDepth(node.nodes, visit, path, context)
} else if (node.kind === 'context') {
walkDepth(node.nodes, visit, parentPath, { ...context, ...node.context })
continue
}
visit(node, {
parent,
context,
path,
replaceWith(newNode) {
ast.splice(i, 1, ...newNode)
i += newNode.length - 1
},
})
}
}
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') {
css += `${indent}${node.selector} {\n`
for (let child of node.nodes) {
css += stringify(child, depth + 1)
}
css += `${indent}}\n`
}
else if (node.kind === 'at-rule') {
if (
node.name === '@tailwind' &&
(node.params === 'utilities' || node.params.startsWith('utilities'))
) {
for (let child of node.nodes) {
css += stringify(child, depth)
}
return css
}
else if (node.nodes.length === 0) {
return `${indent}${node.name} ${node.params};\n`
}
else if (node.name === '@property' && depth === 0) {
if (seenAtProperties.has(node.params)) {
return ''
}
let property = node.params
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.params)
}
css += `${indent}${node.name}${node.params ? ` ${node.params} ` : ' '}{\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.kind === 'at-root') {
for (let child of node.nodes) {
atRoots += stringify(child, 0)
}
return css
}
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(
atRule('@supports', '(-moz-orient: inline)', [atRule('@layer', 'base', fallbackAst)]),
)
}
return `${css}${fallback}${atRoots}`
}