export type SelectorCombinatorNode = {
kind: 'combinator'
value: string
}
export type SelectorFunctionNode = {
kind: 'function'
value: string
nodes: SelectorAstNode[]
}
export type SelectorNode = {
kind: 'selector'
value: string
}
export type SelectorValueNode = {
kind: 'value'
value: string
}
export type SelectorSeparatorNode = {
kind: 'separator'
value: string
}
export type SelectorAstNode =
| SelectorCombinatorNode
| SelectorFunctionNode
| SelectorNode
| SelectorSeparatorNode
| SelectorValueNode
type SelectorParentNode = SelectorFunctionNode | null
function combinator(value: string): SelectorCombinatorNode {
return {
kind: 'combinator',
value,
}
}
function fun(value: string, nodes: SelectorAstNode[]): SelectorFunctionNode {
return {
kind: 'function',
value: value,
nodes,
}
}
function selector(value: string): SelectorNode {
return {
kind: 'selector',
value,
}
}
function separator(value: string): SelectorSeparatorNode {
return {
kind: 'separator',
value,
}
}
function value(value: string): SelectorValueNode {
return {
kind: 'value',
value,
}
}
export const enum SelectorWalkAction {
Continue,
Skip,
Stop,
}
export function walk(
ast: SelectorAstNode[],
visit: (
node: SelectorAstNode,
utils: {
parent: SelectorParentNode
replaceWith(newNode: SelectorAstNode | SelectorAstNode[]): void
},
) => void | SelectorWalkAction,
parent: SelectorParentNode = null,
) {
for (let i = 0; i < ast.length; i++) {
let node = ast[i]
let replacedNode = false
let replacedNodeOffset = 0
let status =
visit(node, {
parent,
replaceWith(newNode) {
if (replacedNode) return
replacedNode = true
if (Array.isArray(newNode)) {
if (newNode.length === 0) {
ast.splice(i, 1)
replacedNodeOffset = 0
} else if (newNode.length === 1) {
ast[i] = newNode[0]
replacedNodeOffset = 1
} else {
ast.splice(i, 1, ...newNode)
replacedNodeOffset = newNode.length
}
} else {
ast[i] = newNode
replacedNodeOffset = 1
}
},
}) ?? SelectorWalkAction.Continue
if (replacedNode) {
if (status === SelectorWalkAction.Continue) {
i--
} else {
i += replacedNodeOffset - 1
}
continue
}
if (status === SelectorWalkAction.Stop) return SelectorWalkAction.Stop
if (status === SelectorWalkAction.Skip) continue
if (node.kind === 'function') {
if (walk(node.nodes, visit, node) === SelectorWalkAction.Stop) {
return SelectorWalkAction.Stop
}
}
}
}
export function toCss(ast: SelectorAstNode[]) {
let css = ''
for (const node of ast) {
switch (node.kind) {
case 'combinator':
case 'selector':
case 'separator':
case 'value': {
css += node.value
break
}
case 'function': {
css += node.value + '(' + toCss(node.nodes) + ')'
}
}
}
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
export function parse(input: string) {
input = input.replaceAll('\r\n', '\n')
let ast: SelectorAstNode[] = []
let stack: (SelectorFunctionNode | null)[] = []
let parent = null as SelectorFunctionNode | null
let buffer = ''
let peekChar
for (let i = 0; i < input.length; i++) {
let currentChar = input.charCodeAt(i)
switch (currentChar) {
case COMMA:
case GREATER_THAN:
case NEWLINE:
case SPACE:
case PLUS:
case TAB:
case TILDE: {
if (buffer.length > 0) {
let node = selector(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 !== COMMA &&
peekChar !== GREATER_THAN &&
peekChar !== NEWLINE &&
peekChar !== SPACE &&
peekChar !== PLUS &&
peekChar !== TAB &&
peekChar !== TILDE
) {
break
}
}
i = end - 1
let contents = input.slice(start, end)
let node = contents.trim() === ',' ? separator(contents) : combinator(contents)
if (parent) {
parent.nodes.push(node)
} else {
ast.push(node)
}
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
if (parent) {
parent.nodes.push(node)
} else {
ast.push(node)
}
break
}
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 = selector(buffer)
tail!.nodes.push(node)
buffer = ''
}
if (stack.length > 0) {
parent = stack[stack.length - 1]
} else {
parent = null
}
break
}
case FULL_STOP:
case COLON:
case NUMBER_SIGN: {
if (buffer.length > 0) {
let node = selector(buffer)
if (parent) {
parent.nodes.push(node)
} else {
ast.push(node)
}
}
buffer = String.fromCharCode(currentChar)
break
}
case OPEN_BRACKET: {
if (buffer.length > 0) {
let node = selector(buffer)
if (parent) {
parent.nodes.push(node)
} else {
ast.push(node)
}
}
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 BACKSLASH: {
let nextChar = input.charCodeAt(i + 1)
buffer += String.fromCharCode(currentChar) + String.fromCharCode(nextChar)
i += 1
break
}
default: {
buffer += String.fromCharCode(currentChar)
}
}
}
if (buffer.length > 0) {
ast.push(selector(buffer))
}
return ast
}