Compare commits
4 commits
main
...
feat/v4-no
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8023be68ce | ||
|
|
a609809ce0 | ||
|
|
b86387e532 | ||
|
|
381d336eb1 |
4 changed files with 144 additions and 76 deletions
|
|
@ -79,16 +79,20 @@ export function walk(
|
|||
node: AstNode,
|
||||
utils: {
|
||||
parent: AstNode | null
|
||||
path: AstNode[]
|
||||
replaceWith(newNode: AstNode | AstNode[]): void
|
||||
},
|
||||
) => void | WalkAction,
|
||||
parent: AstNode | null = null,
|
||||
parentPath: AstNode[] = [],
|
||||
) {
|
||||
for (let i = 0; i < ast.length; i++) {
|
||||
let node = ast[i]
|
||||
let path = [node, ...parentPath]
|
||||
let status =
|
||||
visit(node, {
|
||||
parent,
|
||||
path,
|
||||
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 +109,7 @@ export function walk(
|
|||
if (status === WalkAction.Skip) continue
|
||||
|
||||
if (node.kind === 'rule') {
|
||||
walk(node.nodes, visit, node)
|
||||
walk(node.nodes, visit, node, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -705,23 +705,23 @@ test('group-[...]', () => {
|
|||
'group-[&:hover]:group-[&_p]:flex',
|
||||
]),
|
||||
).toMatchInlineSnapshot(`
|
||||
".group-\\[\\&_p\\]\\:flex:is(:where(.group) p *) {
|
||||
".group-\\[\\&_p\\]\\:flex:is(:where(.group):is(* p) *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.group-\\[\\&\\:hover\\]\\:group-\\[\\&_p\\]\\:flex:is(:where(.group):hover *):is(:where(.group) p *) {
|
||||
.group-\\[\\&\\:hover\\]\\:group-\\[\\&_p\\]\\:flex:is(:where(.group):hover *):is(:where(.group):is(* p) *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.group-\\[\\&_p\\]\\:hover\\:flex:is(:where(.group) p *):hover {
|
||||
.group-\\[\\&_p\\]\\:hover\\:flex:is(:where(.group):is(* p) *):hover {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hover\\:group-\\[\\&_p\\]\\:flex:hover:is(:where(.group) p *) {
|
||||
.hover\\:group-\\[\\&_p\\]\\:flex:hover:is(:where(.group):is(* p) *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hover\\:group-\\[\\&_p\\]\\:hover\\:flex:hover:is(:where(.group) p *):hover {
|
||||
.hover\\:group-\\[\\&_p\\]\\:hover\\:flex:hover:is(:where(.group):is(* p) *):hover {
|
||||
display: flex;
|
||||
}"
|
||||
`)
|
||||
|
|
@ -746,6 +746,13 @@ test('group-*', () => {
|
|||
@slot;
|
||||
}
|
||||
}
|
||||
@variant nested-selectors {
|
||||
&:hover {
|
||||
&:focus {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
[
|
||||
|
|
@ -755,6 +762,8 @@ test('group-*', () => {
|
|||
|
||||
'group-hover:group-focus:flex',
|
||||
'group-focus:group-hover:flex',
|
||||
|
||||
'group-nested-selectors:flex',
|
||||
],
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
|
|
@ -774,7 +783,11 @@ test('group-*', () => {
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) {
|
||||
.group-hocus\\:flex:is(:where(.group):is(:hover, :focus) *) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.group-nested-selectors\\:flex:is(:where(.group):focus:hover *) {
|
||||
display: flex;
|
||||
}"
|
||||
`)
|
||||
|
|
@ -783,16 +796,9 @@ test('group-*', () => {
|
|||
compileCss(
|
||||
css`
|
||||
@variant custom-at-rule (@media foo);
|
||||
@variant nested-selectors {
|
||||
&:hover {
|
||||
&:focus {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
['group-custom-at-rule:flex', 'group-nested-selectors:flex'],
|
||||
['group-custom-at-rule:flex'],
|
||||
),
|
||||
).toEqual('')
|
||||
})
|
||||
|
|
@ -1557,11 +1563,19 @@ test('not', () => {
|
|||
@slot;
|
||||
}
|
||||
}
|
||||
@variant nested-selectors {
|
||||
&:hover {
|
||||
&:focus {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
[
|
||||
'not-[:checked]:flex',
|
||||
'not-hocus:flex',
|
||||
'not-nested-selectors:flex',
|
||||
|
||||
'group-not-[:checked]:flex',
|
||||
'group-not-[:checked]/parent-name:flex',
|
||||
|
|
@ -1581,6 +1595,10 @@ test('not', () => {
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.not-nested-selectors\\:flex:is(:not(:focus), :not(:hover)) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.not-\\[\\:checked\\]\\:flex:not(:checked) {
|
||||
display: flex;
|
||||
}
|
||||
|
|
@ -1630,21 +1648,9 @@ test('not', () => {
|
|||
compileCss(
|
||||
css`
|
||||
@variant custom-at-rule (@media foo);
|
||||
@variant nested-selectors {
|
||||
&:hover {
|
||||
&:focus {
|
||||
@slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
@tailwind utilities;
|
||||
`,
|
||||
[
|
||||
'not-[:checked]/foo:flex',
|
||||
'not-[@media_print]:flex',
|
||||
'not-custom-at-rule:flex',
|
||||
'not-nested-selectors:flex',
|
||||
],
|
||||
['not-[:checked]/foo:flex', 'not-[@media_print]:flex', 'not-custom-at-rule:flex'],
|
||||
),
|
||||
).toEqual('')
|
||||
})
|
||||
|
|
@ -2518,3 +2524,48 @@ test('variant order', () => {
|
|||
}"
|
||||
`)
|
||||
})
|
||||
|
||||
test('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;
|
||||
}"
|
||||
`)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -206,39 +206,41 @@ 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, { path }) => {
|
||||
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 = []
|
||||
|
||||
return didApply ? WalkAction.Skip : WalkAction.Stop
|
||||
// 2. Collect the selectors of the parents
|
||||
for (let ancestor of path) {
|
||||
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
|
||||
}
|
||||
|
||||
ruleNode.selector = `&:is(${groups.join('')})`
|
||||
ruleNode.nodes = []
|
||||
})
|
||||
|
||||
variants.compound('group', (ruleNode, variant) => {
|
||||
|
|
@ -248,50 +250,52 @@ export function createVariants(theme: Theme): Variants {
|
|||
? `:where(.group\\/${variant.modifier.value})`
|
||||
: ':where(.group)'
|
||||
|
||||
let didApply = false
|
||||
let groups: string[] = []
|
||||
|
||||
walk([ruleNode], (node) => {
|
||||
walk([ruleNode], (node, { path }) => {
|
||||
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 = []
|
||||
|
||||
return didApply ? WalkAction.Skip : WalkAction.Stop
|
||||
// 2. Collect the selectors of the parents
|
||||
for (let ancestor of path) {
|
||||
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
|
||||
|
||||
// For most variants we rely entirely on CSS nesting to build-up the final
|
||||
// selector, but there is no way to use CSS nesting to make `&` refer to
|
||||
// just the `.group` class the way we'd need to for these variants, so we
|
||||
// need to replace it in the selector ourselves.
|
||||
let selector = ancestor.selector.replaceAll('&', '*')
|
||||
|
||||
// When the selector is a selector _list_ we need to wrap it in `:is`
|
||||
// to make sure the matching behavior is consistent with the original
|
||||
// variant / selector.
|
||||
selectors.push(`:is(${selector})`)
|
||||
}
|
||||
|
||||
// For most variants we rely entirely on CSS nesting to build-up the final
|
||||
// selector, but there is no way to use CSS nesting to make `&` refer to
|
||||
// just the `.group` class the way we'd need to for these variants, so we
|
||||
// need to replace it in the selector ourselves.
|
||||
node.selector = node.selector.replaceAll('&', groupSelector)
|
||||
if (selectors.length === 0) return
|
||||
|
||||
// When the selector is a selector _list_ we need to wrap it in `:is`
|
||||
// to make sure the matching behavior is consistent with the original
|
||||
// variant / selector.
|
||||
if (segment(node.selector, ',').length > 1) {
|
||||
node.selector = `:is(${node.selector})`
|
||||
}
|
||||
|
||||
node.selector = `&:is(${node.selector} *)`
|
||||
|
||||
// Track that the variant was actually applied
|
||||
didApply = true
|
||||
groups.push(selectors.join(''))
|
||||
})
|
||||
|
||||
// If the node wasn't modified, this variant is not compatible with
|
||||
// `group-*` so discard the candidate.
|
||||
if (!didApply) {
|
||||
if (groups.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
ruleNode.selector = `&:is(${groupSelector}:is(${groups.join(',')}) *)`
|
||||
ruleNode.nodes = []
|
||||
})
|
||||
|
||||
variants.suggest('group', () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue