export type ValueWordNode = {
kind: 'word'
value: string
}
export type ValueFunctionNode = {
kind: 'function'
value: string
nodes: ValueAstNode[]
}
export type ValueSeparatorNode = {
kind: 'separator'
value: string
}
export type ValueAstNode = ValueWordNode | ValueFunctionNode | ValueSeparatorNode
export function word(value: string): ValueWordNode {
return {
kind: 'word',
value,
}
}
function fun(value: string, nodes: ValueAstNode[]): ValueFunctionNode {
return {
kind: 'function',
value: value,
nodes,
}
}
function separator(value: string): ValueSeparatorNode {
return {
kind: 'separator',
value,
}
}
export function toCss(ast: ValueAstNode[]) {
let css = ''
for (const node of ast) {
switch (node.kind) {
case 'word':
case 'separator': {
css += node.value
break
}
case 'function': {
css += node.value + '(' + toCss(node.nodes) + ')'
}
}
}
return css
}
const BACKSLASH = 0x5c
const CLOSE_PAREN = 0x29
const COLON = 0x3a
const COMMA = 0x2c
const DOUBLE_QUOTE = 0x22
const EQUALS = 0x3d
const GREATER_THAN = 0x3e
const LESS_THAN = 0x3c
const NEWLINE = 0x0a
const OPEN_PAREN = 0x28
const SINGLE_QUOTE = 0x27
const SLASH = 0x2f
const SPACE = 0x20
const TAB = 0x09
export function parse(input: string) {
input = input.replaceAll('\r\n', '\n')
let ast: ValueAstNode[] = []
let stack: (ValueFunctionNode | null)[] = []
let parent = null as ValueFunctionNode | null
let buffer = ''
let peekChar
for (let i = 0; i < input.length; i++) {
let currentChar = input.charCodeAt(i)
switch (currentChar) {
case BACKSLASH: {
buffer += input[i] + input[i + 1]
i++
break
}
case SLASH: {
if (buffer.length > 0) {
let node = word(buffer)
if (parent) {
parent.nodes.push(node)
} else {
ast.push(node)
}
buffer = ''
}
let node = word(input[i])
if (parent) {
parent.nodes.push(node)
} else {
ast.push(node)
}
break
}
case COLON:
case COMMA:
case EQUALS:
case GREATER_THAN:
case LESS_THAN:
case NEWLINE:
case SPACE:
case TAB: {
if (buffer.length > 0) {
let node = word(buffer)
if (parent) {
parent.nodes.push(node)
} else {
ast.push(node)
}
buffer = ''
}
let start = i
let end = i + 1
for (; end < input.length; end++) {
peekChar = input.charCodeAt(end)
if (
peekChar !== COLON &&
peekChar !== COMMA &&
peekChar !== EQUALS &&
peekChar !== GREATER_THAN &&
peekChar !== LESS_THAN &&
peekChar !== NEWLINE &&
peekChar !== SPACE &&
peekChar !== TAB
) {
break
}
}
i = end - 1
let node = separator(input.slice(start, end))
if (parent) {
parent.nodes.push(node)
} else {
ast.push(node)
}
break
}
case SINGLE_QUOTE:
case DOUBLE_QUOTE: {
let start = i
for (let j = i + 1; j < input.length; j++) {
peekChar = input.charCodeAt(j)
if (peekChar === BACKSLASH) {
j += 1
}
else if (peekChar === currentChar) {
i = j
break
}
}
buffer += input.slice(start, i + 1)
break
}
case OPEN_PAREN: {
let node = fun(buffer, [])
buffer = ''
if (parent) {
parent.nodes.push(node)
} else {
ast.push(node)
}
stack.push(node)
parent = node
break
}
case CLOSE_PAREN: {
let tail = stack.pop()
if (buffer.length > 0) {
let node = word(buffer)
tail?.nodes.push(node)
buffer = ''
}
if (stack.length > 0) {
parent = stack[stack.length - 1]
} else {
parent = null
}
break
}
default: {
buffer += String.fromCharCode(currentChar)
}
}
}
if (buffer.length > 0) {
ast.push(word(buffer))
}
return ast
}