This commit is contained in:
Jordan Pittman 2024-07-18 12:22:26 -04:00
parent 6ab289343d
commit 381d336eb1
4 changed files with 92 additions and 20 deletions

View file

@ -79,16 +79,19 @@ export function walk(
node: AstNode,
utils: {
parent: AstNode | null
ancestors: AstNode[]
replaceWith(newNode: AstNode | AstNode[]): void
},
) => void | WalkAction,
parent: AstNode | null = null,
ancestors: AstNode[] = [],
) {
for (let i = 0; i < ast.length; i++) {
let node = ast[i]
let status =
visit(node, {
parent,
ancestors,
replaceWith(newNode) {
ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode]))
// We want to visit the newly replaced node(s), which start at the
@ -105,7 +108,7 @@ export function walk(
if (status === WalkAction.Skip) continue
if (node.kind === 'rule') {
walk(node.nodes, visit, node)
walk(node.nodes, visit, node, [node, ...ancestors])
}
}
}

View file

@ -188,6 +188,14 @@ export function applyVariant(node: Rule, variant: Variant, variants: Variants):
let result = applyVariant(isolatedNode, variant.variant, variants)
if (result === null) return null
// Wrap in a single `&` rule so :not(…) can have the entire list of
// selectors / nodes from the "root"
//
// TODO: Rework this because it's not a great solution
if (isolatedNode.nodes.length > 1) {
isolatedNode.nodes = [rule('&', isolatedNode.nodes)]
}
for (let child of isolatedNode.nodes) {
// Only some variants wrap children in rules. For example, the `force`
// variant is a noop on the AST. And the `has` variant modifies the
@ -217,6 +225,7 @@ export function applyVariant(node: Rule, variant: Variant, variants: Variants):
// All other variants
let result = applyFn(node, variant)
if (result === null) return null
}

View file

@ -1557,6 +1557,13 @@ test('not', () => {
@slot;
}
}
@variant nested-selectors {
&:hover {
&:focus {
@slot;
}
}
}
@tailwind utilities;
`,
[
@ -1581,6 +1588,10 @@ test('not', () => {
display: flex;
}
.not-nested-selectors\:flex:not(:hover), .not-nested-selectors\:flex:not(:focus) {
display: flex;
}
.not-\\[\\:checked\\]\\:flex:not(:checked) {
display: flex;
}
@ -2518,3 +2529,48 @@ test('variant order', () => {
}"
`)
})
test.only('not selector inversion creation thing', () => {
let input = css`
@variant omg {
&:hover {
&:focus {
&:active {
@slot;
}
&[data-whatever] {
@slot;
}
}
&[data-foo] {
@slot;
}
}
&:visited {
@slot;
}
}
@tailwind utilities;
`
expect(compileCss(input, ['omg:flex', 'not-omg:flex'])).toMatchInlineSnapshot(`
".not-omg\\:flex:is(:not(:active), :not(:focus), :not(:hover)):is(:not([data-whatever]), :not(:focus), :not(:hover)):is(:not([data-foo]), :not(:hover)):not(:visited) {
display: flex;
}
.omg\\:flex:hover:focus:active, .omg\\:flex:hover:focus[data-whatever] {
display: flex;
}
.omg\\:flex:hover[data-foo] {
display: flex;
}
.omg\\:flex:visited {
display: flex;
}"
`)
})

View file

@ -206,39 +206,43 @@ export function createVariants(theme: Theme): Variants {
variants.compound('not', (ruleNode, variant) => {
if (variant.modifier) return null
let didApply = false
let groups: string[] = []
walk([ruleNode], (node) => {
walk([ruleNode], (node, { ancestors }) => {
if (node.kind !== 'rule') return WalkAction.Continue
// Skip past at-rules, and continue traversing the children of the at-rule
if (node.selector[0] === '@') return WalkAction.Continue
// Throw out any candidates with variants using nested selectors
if (didApply) {
walk([node], (childNode) => {
if (childNode.kind !== 'rule' || childNode.selector[0] === '@') return WalkAction.Continue
// 1. Walk the tree until we find `@slot`
if (node.nodes.length !== 0) return WalkAction.Continue
didApply = false
return WalkAction.Stop
})
let selectors = [`:not(${node.selector.replaceAll('&', '*')})`]
return didApply ? WalkAction.Skip : WalkAction.Stop
// 2. Collect the selectors of the parents
for (let ancestor of ancestors) {
if (ancestor.kind !== 'rule') continue
// Skip past at-rules, and continue traversing the children of the at-rule
if (ancestor.selector[0] === '@') continue
// Skip over the "root" node
if (ancestor.selector === '&') continue
selectors.push(`:not(${ancestor.selector.replaceAll('&', '*')})`)
}
// Replace `&` in target variant with `*`, so variants like `&:hover`
// become `&:not(*:hover)`. The `*` will often be optimized away.
node.selector = `&:not(${node.selector.replaceAll('&', '*')})`
// Track that the variant was actually applied
didApply = true
groups.push(`:is(${selectors.join(', ')})`)
})
// If the node wasn't modified, this variant is not compatible with
// `not-*` so discard the candidate.
if (!didApply) {
if (groups.length === 0) {
return null
}
console.log({ groups })
ruleNode.selector = '&' + groups.join('')
ruleNode.nodes = []
})
variants.compound('group', (ruleNode, variant) => {