From 381d336eb1d14b36edd0c994fb17ca87bb6a260f Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Thu, 18 Jul 2024 12:22:26 -0400 Subject: [PATCH] wip --- packages/tailwindcss/src/ast.ts | 5 +- packages/tailwindcss/src/compile.ts | 9 ++++ packages/tailwindcss/src/variants.test.ts | 56 +++++++++++++++++++++++ packages/tailwindcss/src/variants.ts | 42 +++++++++-------- 4 files changed, 92 insertions(+), 20 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 66b37daf0..fb7d5b78c 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -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]) } } } diff --git a/packages/tailwindcss/src/compile.ts b/packages/tailwindcss/src/compile.ts index cac07451d..265630b8d 100644 --- a/packages/tailwindcss/src/compile.ts +++ b/packages/tailwindcss/src/compile.ts @@ -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 } diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 5fb6b4f22..64be22c25 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -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; + }" + `) +}) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 909cfef22..4af361a26 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -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) => {