Skip to content

Commit 2b2820c

Browse files
committed
wip
1 parent 09f38d2 commit 2b2820c

File tree

2 files changed

+178
-2
lines changed

2 files changed

+178
-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

+89
Original file line numberDiff line numberDiff line change
@@ -855,3 +855,92 @@ 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+
raw: html`<div
869+
class="hover:foo1 hover:bar1 hover:baz1 group-hover:foo1 group-hover:bar1 group-hover:baz1 peer-checked:foo1 peer-checked:bar1 peer-checked:baz1"
870+
></div>`,
871+
},
872+
],
873+
corePlugins: { preflight: false },
874+
}
875+
876+
let input = css`
877+
@tailwind utilities;
878+
@layer utilities {
879+
.foo.bar.baz {
880+
color: red;
881+
}
882+
.foo1 .bar1 .baz1 {
883+
color: red;
884+
}
885+
}
886+
`
887+
888+
return run(input, config).then((result) => {
889+
expect(result.css).toMatchFormattedCss(css`
890+
.hover\:foo.bar.baz:hover {
891+
color: red;
892+
}
893+
.hover\:bar.foo.baz:hover {
894+
color: red;
895+
}
896+
.hover\:baz.foo.bar:hover {
897+
color: red;
898+
}
899+
.hover\:foo1:hover .bar1 .baz1 {
900+
color: red;
901+
}
902+
.foo1 .hover\:bar1:hover .baz1 {
903+
color: red;
904+
}
905+
.foo1 .bar1 .hover\:baz1:hover {
906+
color: red;
907+
}
908+
.group:hover .group-hover\:foo.bar.baz {
909+
color: red;
910+
}
911+
.group:hover .group-hover\:bar.foo.baz {
912+
color: red;
913+
}
914+
.group:hover .group-hover\:baz.foo.bar {
915+
color: red;
916+
}
917+
.group:hover .group-hover\:foo1 .bar1 .baz1 {
918+
color: red;
919+
}
920+
.foo1 .group:hover .group-hover\:bar1 .baz1 {
921+
color: red;
922+
}
923+
.foo1 .bar1 .group:hover .group-hover\:baz1 {
924+
color: red;
925+
}
926+
.peer:checked ~ .peer-checked\:foo.bar.baz {
927+
color: red;
928+
}
929+
.peer:checked ~ .peer-checked\:bar.foo.baz {
930+
color: red;
931+
}
932+
.peer:checked ~ .peer-checked\:baz.foo.bar {
933+
color: red;
934+
}
935+
.peer:checked ~ .peer-checked\:foo1 .bar1 .baz1 {
936+
color: red;
937+
}
938+
.foo1 .peer:checked ~ .peer-checked\:bar1 .baz1 {
939+
color: red;
940+
}
941+
.foo1 .bar1 .peer:checked ~ .peer-checked\:baz1 {
942+
color: red;
943+
}
944+
`)
945+
})
946+
})

0 commit comments

Comments
 (0)