type Combinator =
| ' '
| '>'
| '+'
| '~'
export type SelectorListNode = {
kind: 'list'
nodes: SelectorAstNode[]
}
export type SelectorCombinatorNode = {
kind: 'combinator'
value: Combinator
}
export type SelectorComplexNode = {
kind: 'complex'
nodes: SelectorAstNode[]
}
export type SelectorCompoundNode = {
kind: 'compound'
nodes: SelectorAstNode[]
}
export type SelectorNode = {
kind: 'selector'
value: string
}
export type SelectorFunctionNode = {
kind: 'function'
value: string
nodes: SelectorAstNode[]
}
export type SelectorValueNode = {
kind: 'value'
value: string
}
export type SelectorAstNode =
| SelectorFunctionNode
| SelectorCombinatorNode
| SelectorComplexNode
| SelectorCompoundNode
| SelectorListNode
| SelectorNode
| SelectorValueNode
function combinator(value: Combinator): SelectorCombinatorNode {
return {
kind: 'combinator',
value,
}
}
function complex(nodes: SelectorAstNode[]): SelectorComplexNode {
return {
kind: 'complex',
nodes,
}
}
function compound(nodes: SelectorAstNode[]): SelectorCompoundNode {
return {
kind: 'compound',
nodes,
}
}
function fun(value: string, nodes: SelectorAstNode[]): SelectorFunctionNode {
return {
kind: 'function',
value,
nodes,
}
}
function list(nodes: SelectorAstNode[]): SelectorListNode {
return {
kind: 'list',
nodes,
}
}
function selector(value: string): SelectorNode {
return {
kind: 'selector',
value,
}
}
function value(value: string): SelectorValueNode {
return {
kind: 'value',
value,
}
}
export function toCss(ast: SelectorAstNode[], minify = false) {
let css = ''
for (let node of ast) {
switch (node.kind) {
case 'selector':
case 'value': {
css += node.value
break
}
case 'combinator': {
if (minify || node.value === ' ') {
css += node.value
} else {
css += ` ${node.value} `
}
break
}
case 'function': {
css += `${node.value}(${toCss(node.nodes, minify)})`
break
}
case 'complex':
case 'compound': {
css += toCss(node.nodes, minify)
break
}
case 'list': {
css += node.nodes.map((node) => toCss([node], minify)).join(minify ? ',' : ', ')
break
}
}
}
return css
}
const BACKSLASH = 0x5c
const CLOSE_BRACKET = 0x5d
const CLOSE_PAREN = 0x29
const COLON = 0x3a
const COMMA = 0x2c
const DOUBLE_QUOTE = 0x22
const FULL_STOP = 0x2e
const GREATER_THAN = 0x3e
const NEWLINE = 0x0a
const NUMBER_SIGN = 0x23
const OPEN_BRACKET = 0x5b
const OPEN_PAREN = 0x28
const PLUS = 0x2b
const SINGLE_QUOTE = 0x27
const SPACE = 0x20
const TAB = 0x09
const TILDE = 0x7e
const AMPERSAND = 0x26
const ASTERISK = 0x2a
export function parse(input: string) {
input = input.replaceAll('\r\n', '\n')
let ast: SelectorAstNode[] = []
let target = ast
let containsCombinator = false
let contextStack: {
target: SelectorAstNode[]
currentList: SelectorListNode | null
containsCombinator: boolean
}[] = []
let currentList: SelectorListNode | null = null
let buffer = ''
let peekChar
function current(nodes = target): SelectorAstNode {
return nodes.length === 1 ? nodes[0] : containsCombinator ? complex(nodes) : compound(nodes)
}
function append(node: SelectorAstNode) {
let existing = target[target.length - 1]
if (existing?.kind === 'compound') {
existing.nodes.push(node)
} else if (existing && existing.kind !== 'list' && existing.kind !== 'combinator') {
target[target.length - 1] = compound([existing, node])
} else {
target.push(node)
}
}
for (let i = 0; i < input.length; i++) {
let currentChar = input.charCodeAt(i)
switch (currentChar) {
case COMMA: {
if (buffer.length > 0) {
append(selector(buffer))
buffer = ''
}
for (; i + 1 < input.length; i++) {
peekChar = input.charCodeAt(i + 1)
if (peekChar !== NEWLINE && peekChar !== SPACE && peekChar !== TAB) {
break
}
}
if (currentList) {
currentList.nodes.push(current())
target = []
containsCombinator = false
}
else {
let nodes = target.splice(0)
let item = current(nodes)
let node = list([item])
target.push(node)
currentList = node
target = []
containsCombinator = false
}
break
}
case GREATER_THAN:
case NEWLINE:
case SPACE:
case PLUS:
case TAB:
case TILDE: {
if (buffer.length > 0) {
append(selector(buffer))
buffer = ''
}
let start = i
let end = i + 1
for (; end < input.length; end++) {
peekChar = input.charCodeAt(end)
if (
peekChar !== GREATER_THAN &&
peekChar !== NEWLINE &&
peekChar !== SPACE &&
peekChar !== PLUS &&
peekChar !== TAB &&
peekChar !== TILDE
) {
break
}
}
i = end - 1
let value = input.slice(start, end).trim()
if (
value === '' &&
(target.length === 0 || end >= input.length || input.charCodeAt(end) === COMMA)
) {
break
}
target.push(combinator((value === '' ? ' ' : value) as Combinator))
containsCombinator = true
break
}
case OPEN_PAREN: {
let node = fun(buffer, [])
buffer = ''
if (
node.value !== ':not' &&
node.value !== ':where' &&
node.value !== ':has' &&
node.value !== ':is'
) {
let start = i + 1
let nesting = 0
for (let j = i + 1; j < input.length; j++) {
peekChar = input.charCodeAt(j)
if (peekChar === OPEN_PAREN) {
nesting++
continue
}
if (peekChar === CLOSE_PAREN) {
if (nesting === 0) {
i = j
break
}
nesting--
}
}
let end = i
node.nodes.push(value(input.slice(start, end)))
buffer = ''
i = end
append(node)
break
}
append(node)
contextStack.push({ target, currentList, containsCombinator })
target = node.nodes
containsCombinator = false
currentList = null
break
}
case CLOSE_PAREN: {
if (buffer.length > 0) {
append(selector(buffer))
buffer = ''
}
if (currentList) {
currentList.nodes.push(current())
} else if (containsCombinator) {
target.splice(0, target.length, complex(target.splice(0)))
}
let context = contextStack.pop()
target = context?.target ?? ast
currentList = context?.currentList ?? null
containsCombinator = context?.containsCombinator ?? false
break
}
case FULL_STOP:
case COLON:
case NUMBER_SIGN: {
if (currentChar === COLON && buffer === ':') {
buffer += input[i]
break
}
if (buffer.length > 0) {
append(selector(buffer))
}
buffer = input[i]
break
}
case OPEN_BRACKET: {
if (buffer.length > 0) {
append(selector(buffer))
}
buffer = ''
let start = i
let nesting = 0
for (let j = i + 1; j < input.length; j++) {
peekChar = input.charCodeAt(j)
if (peekChar === OPEN_BRACKET) {
nesting++
continue
}
if (peekChar === CLOSE_BRACKET) {
if (nesting === 0) {
i = j
break
}
nesting--
}
}
buffer += input.slice(start, i + 1)
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 AMPERSAND:
case ASTERISK: {
if (buffer.length > 0) {
append(selector(buffer))
buffer = ''
}
append(selector(input[i]))
break
}
case BACKSLASH: {
buffer += input[i] + input[i + 1]
i += 1
break
}
default: {
buffer += input[i]
}
}
}
if (buffer.length > 0) {
append(selector(buffer))
}
if (currentList) {
currentList.nodes.push(current())
} else if (containsCombinator) {
target.splice(0, target.length, complex(target.splice(0)))
}
return ast
}