aliaset
Version:
twind monorepo
221 lines (184 loc) • 5.84 kB
text/typescript
import { DEV } from 'distilt/env'
import { hash } from './utils'
import { define } from './internal/define'
import { format } from './internal/format'
import { Layer } from './internal/precedence'
export interface ParsedRule {
/**
* The utility name including `-` if set, but without `!` and variants
*/
readonly n: string
/**
* All variants without trailing colon: `hover`, `after:`, `[...]`
*/
readonly v: string[]
/**
* Something like `!underline` or `!bg-red-500` or `!red-500`
*/
readonly i?: boolean
}
export interface ParsedDevRule extends ParsedRule {
readonly a: string[]
readonly l: [start: number, end: number]
}
function createRule(
active: string[],
current: ParsedRule[][],
loc?: ParsedDevRule['l'] | false,
): void {
if (active[active.length - 1] != '(') {
const variants: string[] = []
let important = false
let negated = false
let name = ''
for (let value of active) {
if (value == '(' || /[~@]$/.test(value)) continue
if (value[0] == '!') {
value = value.slice(1)
important = !important
}
if (value.endsWith(':')) {
variants[value == 'dark:' ? 'unshift' : 'push'](value.slice(0, -1))
continue
}
if (value[0] == '-') {
value = value.slice(1)
negated = !negated
}
if (value.endsWith('-')) {
value = value.slice(0, -1)
}
if (value && value != '&') {
name += (name && '-') + value
}
}
if (name) {
if (negated) name = '-' + name
current[0].push(
DEV
? Object.defineProperties(
{ n: name, v: variants.filter(uniq), i: important },
{
a: { value: [...active] },
l: { value: loc },
},
)
: { n: name, v: variants.filter(uniq), i: important },
)
}
}
}
function uniq<T>(value: T, index: number, values: T[]): boolean {
return values.indexOf(value) == index
}
const cache = new Map<string, ParsedRule[]>()
/**
* @internal
* @param token
* @returns
*/
export function parse(token: string): ParsedRule[] {
let parsed = cache.get(token)
if (!parsed) {
// Stack of active groupings (`(`), variants, or nested (`~` or `@`)
const active: string[] = []
// Stack of current rule list to put new rules in
// the first `0` element is the current list
const current: ParsedRule[][] = [[]]
let startIndex = 0
let skip = 0
let comment: RegExp | null = null
let position = 0
// eslint-disable-next-line no-inner-declarations
const commit = (isRule?: boolean, endOffset = 0) => {
if (startIndex != position) {
active.push(token.slice(startIndex, position + endOffset))
if (isRule) {
createRule(active, current, DEV && [startIndex, position + endOffset])
}
}
startIndex = position + 1
}
for (; position < token.length; position++) {
const char = token[position]
if (skip) {
// within [...]
// skip over until not skipping
// ignore escaped chars
if (token[position - 1] != '\\') {
skip += +(char == '[') || -(char == ']')
}
} else if (char == '[') {
// start to skip
skip += 1
} else if (comment) {
if (token[position - 1] != '\\' && comment.test(token.slice(position))) {
comment = null
startIndex = position + RegExp.lastMatch.length
}
} else if (
char == '/' &&
token[position - 1] != '\\' &&
(token[position + 1] == '*' || token[position + 1] == '/')
) {
// multiline or single line comment
comment = token[position + 1] == '*' ? /^\*\// : /^[\r\n]/
} else if (char == '(') {
// hover:(...) or utilitity-(...)
commit()
active.push(char)
} else if (char == ':') {
// hover: or after::
if (token[position + 1] != ':') {
commit(false, 1)
}
} else if (/[\s,)]/.test(char)) {
// whitespace, comma or closing brace
commit(true)
let lastGroup = active.lastIndexOf('(')
if (char == ')') {
// Close nested block
const nested = active[lastGroup - 1]
if (/[~@]$/.test(nested)) {
const rules = current.shift() as ParsedRule[]
active.length = lastGroup
// remove variants that are already applied through active
createRule([...active, '#'], current, DEV && [startIndex, position])
const { v } = current[0].pop() as ParsedRule
for (const rule of rules) {
// if a rule has dark we need to splice after the first entry eg dark
rule.v.splice(+(rule.v[0] == 'dark') - +(v[0] == 'dark'), v.length)
}
createRule(
[
...active,
define(
// named nested
nested.length > 1
? nested.slice(0, -1) + hash(JSON.stringify([nested, rules]))
: nested + '(' + format(rules) + ')',
Layer.a,
rules,
/@$/.test(nested),
),
],
current,
DEV && [startIndex, position],
)
}
lastGroup = active.lastIndexOf('(', lastGroup - 1)
}
active.length = lastGroup + 1
} else if (/[~@]/.test(char) && token[position + 1] == '(') {
// start nested block
// ~(...) or button~(...)
// @(...) or button@(...)
current.unshift([])
}
}
// Consume remaining stack
commit(true)
cache.set(token, (parsed = current[0]))
}
return parsed
}