Skip to content

Commit 1bfd575

Browse files
committed
use better algorithm
1 parent 03f6a71 commit 1bfd575

4 files changed

+105
-20
lines changed

src/lib/generateRules.js

-18
Original file line numberDiff line numberDiff line change
@@ -277,27 +277,9 @@ function splitWithSeparator(input, separator) {
277277
return input.split(new RegExp(`\\${separator}(?![^[]*\\])`, 'g'))
278278
}
279279

280-
// A list of variants that are forced to the end. This is useful for variants
281-
// that have pseudo elements which can't really be combined with other variant
282-
// if they are in the incorrect order.
283-
//
284-
// E.g.:
285-
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
286-
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
287-
//
288-
// `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
289-
let forcedVariantOrder = ['before', 'after']
290-
291280
function* resolveMatches(candidate, context) {
292281
let separator = context.tailwindConfig.separator
293282
let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
294-
295-
// Sort the variants if we used a forced variant.
296-
// Note: this will not sort the others, it would only sort the forced variants.
297-
if (variants.some((variant) => forcedVariantOrder.includes(variant))) {
298-
variants.sort((a, z) => forcedVariantOrder.indexOf(a) - forcedVariantOrder.indexOf(z))
299-
}
300-
301283
let important = false
302284

303285
if (classCandidate.startsWith('!')) {

src/util/formatVariantSelector.js

+81
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,92 @@ export function finalizeSelector(format, { selector, candidate, context }) {
7474
return p
7575
})
7676

77+
// This will make sure to move pseudo's to the correct spot (the end for
78+
// pseudo elements) because otherwise the selector will never work
79+
// anyway.
80+
//
81+
// E.g.:
82+
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
83+
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
84+
//
85+
// `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
86+
function collectPseudoElements(selector) {
87+
let nodes = []
88+
89+
for (let node of selector.nodes) {
90+
if (isPseudoElement(node)) {
91+
nodes.push(node)
92+
selector.removeChild(node)
93+
}
94+
95+
if (node?.nodes) {
96+
nodes.push(...collectPseudoElements(node))
97+
}
98+
}
99+
100+
return nodes
101+
}
102+
103+
let pseudoElements = collectPseudoElements(selector)
104+
if (pseudoElements.length > 0) {
105+
selector.nodes.push(pseudoElements.sort(sortSelector))
106+
}
107+
77108
return selector
78109
})
79110
}).processSync(selector)
80111
}
81112

113+
// Note: As a rule, double colons (::) should be used instead of a single colon
114+
// (:). This distinguishes pseudo-classes from pseudo-elements. However, since
115+
// this distinction was not present in older versions of the W3C spec, most
116+
// browsers support both syntaxes for the original pseudo-elements.
117+
let pseudoElementsBC = [':before', ':after', ':first-line', ':first-letter']
118+
119+
// These pseudo-elements _can_ be combined with other pseudo selectors AND the order does matter.
120+
let pseudoElementExceptions = ['::file-selector-button']
121+
122+
// This will make sure to move pseudo's to the correct spot (the end for
123+
// pseudo elements) because otherwise the selector will never work
124+
// anyway.
125+
//
126+
// E.g.:
127+
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
128+
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
129+
//
130+
// `::before:hover` doesn't work, which means that we can make it work
131+
// for you by flipping the order.
132+
function sortSelector(a, z) {
133+
// Both nodes are non-pseudo's so we can safely ignore them and keep
134+
// them in the same order.
135+
if (a.type !== 'pseudo' && z.type !== 'pseudo') {
136+
return 0
137+
}
138+
139+
// If one of them is a combinator, we need to keep it in the same order
140+
// because that means it will start a new "section" in the selector.
141+
if ((a.type === 'combinator') ^ (z.type === 'combinator')) {
142+
return 0
143+
}
144+
145+
// One of the items is a pseudo and the other one isn't. Let's move
146+
// the pseudo to the right.
147+
if ((a.type === 'pseudo') ^ (z.type === 'pseudo')) {
148+
return (a.type === 'pseudo') - (z.type === 'pseudo')
149+
}
150+
151+
// Both are pseudo's, move the pseudo elements (except for
152+
// ::file-selector-button) to the right.
153+
return isPseudoElement(a) - isPseudoElement(z)
154+
}
155+
156+
function isPseudoElement(node) {
157+
if (node.type !== 'pseudo') return false
158+
if (pseudoElementExceptions.includes(node.value)) return false
159+
160+
return node.value.startsWith('::') || pseudoElementsBC.includes(node.value)
161+
}
162+
82163
function resolveFunctionArgument(haystack, needle, arg) {
83164
let startIdx = haystack.indexOf(arg ? `${needle}(${arg})` : needle)
84165
if (startIdx === -1) return null

tests/format-variant-selector.test.js

+22
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,25 @@ describe('real examples', () => {
259259
})
260260
})
261261
})
262+
263+
describe('pseudo elements', () => {
264+
it.each`
265+
before | after
266+
${'&::before'} | ${'&::before'}
267+
${'&::before:hover'} | ${'&:hover::before'}
268+
${'&:before:hover'} | ${'&:hover:before'}
269+
${'&::file-selector-button:hover'} | ${'&::file-selector-button:hover'}
270+
${'&:hover::file-selector-button'} | ${'&:hover::file-selector-button'}
271+
${'.parent:hover &'} | ${'.parent:hover &'}
272+
${'.parent::before &'} | ${'.parent &::before'}
273+
${':where(&::before) :is(h1, h2, h3, h4)'} | ${':where(&) :is(h1, h2, h3, h4)::before'}
274+
${':where(&::file-selector-button) :is(h1, h2, h3, h4)'} | ${':where(&::file-selector-button) :is(h1, h2, h3, h4)'}
275+
`('should translate "$before" into "$after"', ({ before, after }) => {
276+
let result = finalizeSelector(formatVariantSelector('&', before), {
277+
selector: '.a',
278+
candidate: 'a',
279+
})
280+
281+
expect(result).toEqual(after.replace('&', '.a'))
282+
})
283+
})

tests/parallel-variants.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ test('basic parallel variants', async () => {
2727
.test\:font-medium *::test {
2828
font-weight: 500;
2929
}
30-
.hover\:test\:font-black *::test:hover {
30+
.hover\:test\:font-black *:hover::test {
3131
font-weight: 900;
3232
}
3333
.test\:font-bold::test {
@@ -36,7 +36,7 @@ test('basic parallel variants', async () => {
3636
.test\:font-medium::test {
3737
font-weight: 500;
3838
}
39-
.hover\:test\:font-black::test:hover {
39+
.hover\:test\:font-black:hover::test {
4040
font-weight: 900;
4141
}
4242
`)

0 commit comments

Comments
 (0)