import selectorParser from 'postcss-selector-parser'
import unescape from 'postcss-selector-parser/dist/util/unesc'
import escapeClassName from '../util/escapeClassName'
import prefixSelector from '../util/prefixSelector'
let MERGE = ':merge'
let PARENT = '&'
export let selectorFunctions = new Set([MERGE])
export function formatVariantSelector(current, ...others) {
for (let other of others) {
let incomingValue = resolveFunctionArgument(other, MERGE)
if (incomingValue !== null) {
let existingValue = resolveFunctionArgument(current, MERGE, incomingValue)
if (existingValue !== null) {
let existingTarget = `${MERGE}(${incomingValue})`
let splitIdx = other.indexOf(existingTarget)
let addition = other.slice(splitIdx + existingTarget.length).split(' ')[0]
current = current.replace(existingTarget, existingTarget + addition)
continue
}
}
current = other.replace(PARENT, current)
}
return current
}
export function finalizeSelector(format, { selector, candidate, context }) {
let ast = selectorParser().astSync(selector)
let separator = context?.tailwindConfig?.separator ?? ':'
let splitter = new RegExp(`\\${separator}(?![^[]*\\])`)
let base = candidate.split(splitter).pop()
if (context?.tailwindConfig?.prefix) {
format = prefixSelector(context.tailwindConfig.prefix, format)
}
format = format.replace(PARENT, `.${escapeClassName(candidate)}`)
let formatAst = selectorParser().astSync(format)
ast.each((node) => {
let hasClassesMatchingCandidate = node.some((n) => n.type === 'class' && n.value === base)
if (!hasClassesMatchingCandidate) {
node.remove()
}
})
ast.walkClasses((node) => {
if (node.raws && node.value.includes(base)) {
node.raws.value = escapeClassName(unescape(node.raws.value))
}
})
ast.walkClasses((node) => {
if (node.value === base) {
node.replaceWith(...formatAst.nodes)
}
})
function collectPseudoElements(selector) {
let nodes = []
for (let node of selector.nodes) {
if (isPseudoElement(node)) {
nodes.push(node)
selector.removeChild(node)
}
if (node?.nodes) {
nodes.push(...collectPseudoElements(node))
}
}
return nodes
}
ast.each((selector) => {
selector.walkPseudos((p) => {
if (selectorFunctions.has(p.value)) {
p.replaceWith(p.nodes)
}
})
let pseudoElements = collectPseudoElements(selector)
if (pseudoElements.length > 0) {
selector.nodes.push(pseudoElements.sort(sortSelector))
}
})
return ast.toString()
}
let pseudoElementsBC = [':before', ':after', ':first-line', ':first-letter']
let pseudoElementExceptions = ['::file-selector-button']
function sortSelector(a, z) {
if (a.type !== 'pseudo' && z.type !== 'pseudo') {
return 0
}
if ((a.type === 'combinator') ^ (z.type === 'combinator')) {
return 0
}
if ((a.type === 'pseudo') ^ (z.type === 'pseudo')) {
return (a.type === 'pseudo') - (z.type === 'pseudo')
}
return isPseudoElement(a) - isPseudoElement(z)
}
function isPseudoElement(node) {
if (node.type !== 'pseudo') return false
if (pseudoElementExceptions.includes(node.value)) return false
return node.value.startsWith('::') || pseudoElementsBC.includes(node.value)
}
function resolveFunctionArgument(haystack, needle, arg) {
let startIdx = haystack.indexOf(arg ? `${needle}(${arg})` : needle)
if (startIdx === -1) return null
startIdx += needle.length + 1
let target = ''
let count = 0
for (let char of haystack.slice(startIdx)) {
if (char !== '(' && char !== ')') {
target += char
} else if (char === '(') {
target += char
count++
} else if (char === ')') {
if (--count < 0) break
target += char
}
}
return target
}