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'
import { movePseudos } from './pseudoElements'
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
let MERGE = ':merge'
export function formatVariantSelector(formats, { context, candidate }) {
let prefix = context?.tailwindConfig.prefix ?? ''
let parsedFormats = formats.map((format) => {
let ast = selectorParser().astSync(format.format)
return {
...format,
ast: format.respectPrefix ? prefixSelector(prefix, ast) : ast,
}
})
let formatAst = selectorParser.root({
nodes: [
selectorParser.selector({
nodes: [selectorParser.className({ value: escapeClassName(candidate) })],
}),
],
})
for (let { ast } of parsedFormats) {
;[formatAst, ast] = handleMergePseudo(formatAst, ast)
ast.walkNesting((nesting) => nesting.replaceWith(...formatAst.nodes[0].nodes))
formatAst = ast
}
return formatAst
}
function simpleSelectorForNode(node) {
let nodes = []
while (node.prev() && node.prev().type !== 'combinator') {
node = node.prev()
}
while (node && node.type !== 'combinator') {
nodes.push(node)
node = node.next()
}
return nodes
}
function resortSelector(sel) {
sel.sort((a, b) => {
if (a.type === 'tag' && b.type === 'class') {
return -1
} else if (a.type === 'class' && b.type === 'tag') {
return 1
} else if (a.type === 'class' && b.type === 'pseudo' && b.value.startsWith('::')) {
return -1
} else if (a.type === 'pseudo' && a.value.startsWith('::') && b.type === 'class') {
return 1
}
return sel.index(a) - sel.index(b)
})
return sel
}
export function eliminateIrrelevantSelectors(sel, base) {
let hasClassesMatchingCandidate = false
sel.walk((child) => {
if (child.type === 'class' && child.value === base) {
hasClassesMatchingCandidate = true
return false }
})
if (!hasClassesMatchingCandidate) {
sel.remove()
}
}
export function finalizeSelector(current, formats, { context, candidate, base }) {
let separator = context?.tailwindConfig?.separator ?? ':'
base = base ?? splitAtTopLevelOnly(candidate, separator).pop()
let selector = selectorParser().astSync(current)
selector.walkClasses((node) => {
if (node.raws && node.value.includes(base)) {
node.raws.value = escapeClassName(unescape(node.raws.value))
}
})
selector.each((sel) => eliminateIrrelevantSelectors(sel, base))
if (selector.length === 0) {
return null
}
let formatAst = Array.isArray(formats)
? formatVariantSelector(formats, { context, candidate })
: formats
if (formatAst === null) {
return selector.toString()
}
let simpleStart = selectorParser.comment({ value: '/*__simple__*/' })
let simpleEnd = selectorParser.comment({ value: '/*__simple__*/' })
selector.walkClasses((node) => {
if (node.value !== base) {
return
}
let parent = node.parent
let formatNodes = formatAst.nodes[0].nodes
if (parent.nodes.length === 1) {
node.replaceWith(...formatNodes)
return
}
let simpleSelector = simpleSelectorForNode(node)
parent.insertBefore(simpleSelector[0], simpleStart)
parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd)
for (let child of formatNodes) {
parent.insertBefore(simpleSelector[0], child.clone())
}
node.remove()
simpleSelector = simpleSelectorForNode(simpleStart)
let firstNode = parent.index(simpleStart)
parent.nodes.splice(
firstNode,
simpleSelector.length,
...resortSelector(selectorParser.selector({ nodes: simpleSelector })).nodes
)
simpleStart.remove()
simpleEnd.remove()
})
selector.walkPseudos((p) => {
if (p.value === MERGE) {
p.replaceWith(p.nodes)
}
})
selector.each((sel) => movePseudos(sel))
return selector.toString()
}
export function handleMergePseudo(selector, format) {
let merges = []
selector.walkPseudos((pseudo) => {
if (pseudo.value === MERGE) {
merges.push({
pseudo,
value: pseudo.nodes[0].toString(),
})
}
})
format.walkPseudos((pseudo) => {
if (pseudo.value !== MERGE) {
return
}
let value = pseudo.nodes[0].toString()
let existing = merges.find((merge) => merge.value === value)
if (!existing) {
return
}
let attachments = []
let next = pseudo.next()
while (next && next.type !== 'combinator') {
attachments.push(next)
next = next.next()
}
let combinator = next
existing.pseudo.parent.insertAfter(
existing.pseudo,
selectorParser.selector({ nodes: attachments.map((node) => node.clone()) })
)
pseudo.remove()
attachments.forEach((node) => node.remove())
if (combinator && combinator.type === 'combinator') {
combinator.remove()
}
})
return [selector, format]
}