Skip to content

Commit 47cdab5

Browse files
committed
wip
1 parent 09f38d2 commit 47cdab5

File tree

2 files changed

+143
-2
lines changed

2 files changed

+143
-2
lines changed

src/util/formatVariantSelector.js

+89-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,58 @@ export function formatVariantSelector(current, ...others) {
2929
return current
3030
}
3131

32+
/**
33+
* Given any node in a selector this gets the "simple" selector it's a part of
34+
* A simple selector is just a list of nodes without any combinators
35+
* Technically :is(), :not(), :has(), etc… can have combinators but those are nested
36+
* inside the relevant node and won't be picked up so they're fine to ignore
37+
*
38+
* @param {import('postcss-selector-parser').Node} node
39+
* @returns {import('postcss-selector-parser').Node[]}
40+
**/
41+
function simpleSelectorForNode(node) {
42+
/** @type {import('postcss-selector-parser').Node[]} */
43+
let nodes = []
44+
45+
// Walk backwards until we hit a combinator node (or the start)
46+
while (node.prev() && node.prev().type !== 'combinator') {
47+
node = node.prev()
48+
}
49+
50+
// Now record all non-combinator nodes until we hit one (or the end)
51+
while (node && node.type !== 'combinator') {
52+
nodes.push(node)
53+
node = node.next()
54+
}
55+
56+
return nodes
57+
}
58+
59+
/**
60+
* Resorts the nodes in a selector to ensure they're in the correct order
61+
* Tags go before classes, and pseudo classes go after classes
62+
*
63+
* @param {import('postcss-selector-parser').Selector} sel
64+
* @returns {import('postcss-selector-parser').Selector}
65+
**/
66+
function resortSelector(sel) {
67+
sel.sort((a, b) => {
68+
if (a.type === 'tag' && b.type === 'class') {
69+
return -1
70+
} else if (a.type === 'class' && b.type === 'tag') {
71+
return 1
72+
} else if (a.type === 'class' && b.type === 'pseudo' && b.value !== ':merge') {
73+
return -1
74+
} else if (a.type === 'pseudo' && a.value !== ':merge' && b.type === 'class') {
75+
return 1
76+
}
77+
78+
return sel.index(a) - sel.index(b)
79+
})
80+
81+
return sel
82+
}
83+
3284
export function finalizeSelector(
3385
format,
3486
{
@@ -88,12 +140,47 @@ export function finalizeSelector(
88140
}
89141
})
90142

143+
let simpleStart = selectorParser.comment({ value: '/*__simple__*/' })
144+
let simpleEnd = selectorParser.comment({ value: '/*__simple__*/' })
145+
91146
// We can safely replace the escaped base now, since the `base` section is
92147
// now in a normalized escaped value.
93148
ast.walkClasses((node) => {
94-
if (node.value === base) {
95-
node.replaceWith(...formatAst.nodes)
149+
if (node.value !== base) {
150+
return
151+
}
152+
153+
let parent = node.parent
154+
let formatNodes = formatAst.nodes[0].nodes
155+
156+
// Perf optimization: if the parent is a single class we can just replace it and be done
157+
if (parent.nodes.length === 1) {
158+
node.replaceWith(...formatNodes)
159+
return
96160
}
161+
162+
let simpleSelector = simpleSelectorForNode(node)
163+
parent.insertBefore(simpleSelector[0], simpleStart)
164+
parent.insertAfter(simpleSelector[simpleSelector.length - 1], simpleEnd)
165+
166+
for (let child of formatNodes) {
167+
parent.insertBefore(simpleSelector[0], child)
168+
}
169+
170+
node.remove()
171+
172+
// Re-sort the simple selector to ensure it's in the correct order
173+
simpleSelector = simpleSelectorForNode(simpleStart)
174+
let firstNode = parent.index(simpleStart)
175+
176+
parent.nodes.splice(
177+
firstNode,
178+
simpleSelector.length,
179+
...resortSelector(selectorParser.selector({ nodes: simpleSelector })).nodes
180+
)
181+
182+
simpleStart.remove()
183+
simpleEnd.remove()
97184
})
98185

99186
// This will make sure to move pseudo's to the correct spot (the end for

tests/variants.test.js

+54
Original file line numberDiff line numberDiff line change
@@ -855,3 +855,57 @@ test('hoverOnlyWhenSupported adds hover and pointer media features by default',
855855
`)
856856
})
857857
})
858+
859+
test('multi-class utilities handle selector-mutating variants correctly', () => {
860+
let config = {
861+
content: [
862+
{
863+
raw: html`<div
864+
class="hover:foo hover:bar hover:baz group-hover:foo group-hover:bar group-hover:baz peer-checked:foo peer-checked:bar peer-checked:baz"
865+
></div>`,
866+
},
867+
],
868+
corePlugins: { preflight: false },
869+
}
870+
871+
let input = css`
872+
@tailwind utilities;
873+
@layer utilities {
874+
.foo.bar.baz {
875+
color: red;
876+
}
877+
}
878+
`
879+
880+
return run(input, config).then((result) => {
881+
expect(result.css).toMatchFormattedCss(css`
882+
.hover\:foo.bar.baz:hover {
883+
color: red;
884+
}
885+
.foo.hover\:bar.baz:hover {
886+
color: red;
887+
}
888+
.foo.bar.hover\:baz:hover {
889+
color: red;
890+
}
891+
.group:hover .group-hover\:foo.bar.baz {
892+
color: red;
893+
}
894+
.group:hover .foo.group-hover\:bar.baz {
895+
color: red;
896+
}
897+
.group:hover .foo.bar.group-hover\:baz {
898+
color: red;
899+
}
900+
.peer:checked ~ .peer-checked\:foo.bar.baz {
901+
color: red;
902+
}
903+
.peer:checked ~ .foo.peer-checked\:bar.baz {
904+
color: red;
905+
}
906+
.peer:checked ~ .foo.bar.peer-checked\:baz {
907+
color: red;
908+
}
909+
`)
910+
})
911+
})

0 commit comments

Comments
 (0)