Skip to content

Commit

Permalink
use better algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinMalfait committed Nov 12, 2021
1 parent 03f6a71 commit fe41e98
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 20 deletions.
18 changes: 0 additions & 18 deletions src/lib/generateRules.js
Expand Up @@ -277,27 +277,9 @@ function splitWithSeparator(input, separator) {
return input.split(new RegExp(`\\${separator}(?![^[]*\\])`, 'g'))
}

// A list of variants that are forced to the end. This is useful for variants
// that have pseudo elements which can't really be combined with other variant
// if they are in the incorrect order.
//
// E.g.:
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
//
// `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
let forcedVariantOrder = ['before', 'after']

function* resolveMatches(candidate, context) {
let separator = context.tailwindConfig.separator
let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()

// Sort the variants if we used a forced variant.
// Note: this will not sort the others, it would only sort the forced variants.
if (variants.some((variant) => forcedVariantOrder.includes(variant))) {
variants.sort((a, z) => forcedVariantOrder.indexOf(a) - forcedVariantOrder.indexOf(z))
}

let important = false

if (classCandidate.startsWith('!')) {
Expand Down
81 changes: 81 additions & 0 deletions src/util/formatVariantSelector.js
Expand Up @@ -74,11 +74,92 @@ export function finalizeSelector(format, { selector, candidate, context }) {
return p
})

// This will make sure to move pseudo's to the correct spot (the end for
// pseudo elements) because otherwise the selector will never work
// anyway.
//
// E.g.:
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
//
// `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
function collectPseudoElements(selector) {
let nodes = []

for (let node of selector.nodes) {
if (isPseudoElement(node)) {
nodes.push(node)
selector.removeChild(node)
}

if (node?.nodes) {
nodes.push(...collectPseudoElements(node))
}
}

return nodes
}

let pseudoElements = collectPseudoElements(selector)
if (pseudoElements.length > 0) {
selector.nodes.push(pseudoElements.sort(sortSelector))
}

return selector
})
}).processSync(selector)
}

// Note: As a rule, double colons (::) should be used instead of a single colon
// (:). This distinguishes pseudo-classes from pseudo-elements. However, since
// this distinction was not present in older versions of the W3C spec, most
// browsers support both syntaxes for the original pseudo-elements.
let pseudoElementsBC = [':before', ':after', ':first-line', ':first-letter']

// These pseudo-elements _can_ be combined with other pseudo selectors AND the order does matter.
let pseudoElementExceptions = ['::file-selector-button']

// This will make sure to move pseudo's to the correct spot (the end for
// pseudo elements) because otherwise the selector will never work
// anyway.
//
// E.g.:
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
//
// `::before:hover` doesn't work, which means that we can make it work
// for you by flipping the order.
function sortSelector(a, z) {
// Both nodes are non-pseudo's so we can safely ignore them and keep
// them in the same order.
if (a.type !== 'pseudo' && z.type !== 'pseudo') {
return 0
}

// If one of them is a combinator, we need to keep it in the same order
// because that means it will start a new "section" in the selector.
if ((a.type === 'combinator') ^ (z.type === 'combinator')) {
return 0
}

// One of the items is a pseudo and the other one isn't. Let's move
// the pseudo to the right.
if ((a.type === 'pseudo') ^ (z.type === 'pseudo')) {
return (a.type === 'pseudo') - (z.type === 'pseudo')
}

// Both are pseudo's, move the pseudo elements (except for
// ::file-selector-button) to the right.
return isPseudoElement(a) - isPseudoElement(z)
}

function isPseudoElement(node) {
if (node.type !== 'pseudo') return false
if (pseudoElementExceptions.includes(node.value)) return false

return node.value.startsWith('::') || pseudoElementsBC.includes(node.value)
}

function resolveFunctionArgument(haystack, needle, arg) {
let startIdx = haystack.indexOf(arg ? `${needle}(${arg})` : needle)
if (startIdx === -1) return null
Expand Down
23 changes: 23 additions & 0 deletions tests/format-variant-selector.test.js
Expand Up @@ -259,3 +259,26 @@ describe('real examples', () => {
})
})
})

describe('pseudo elements', () => {
it.each`
before | after
${'&::before'} | ${'&::before'}
${'&::before:hover'} | ${'&:hover::before'}
${'&:before:hover'} | ${'&:hover:before'}
${'&::file-selector-button:hover'} | ${'&::file-selector-button:hover'}
${'&:hover::file-selector-button'} | ${'&:hover::file-selector-button'}
${'.parent:hover &'} | ${'.parent:hover &'}
${'.parent::before &'} | ${'.parent &::before'}
${'.parent::before &:hover'} | ${'.parent &:hover::before'}
${':where(&::before) :is(h1, h2, h3, h4)'} | ${':where(&) :is(h1, h2, h3, h4)::before'}
${':where(&::file-selector-button) :is(h1, h2, h3, h4)'} | ${':where(&::file-selector-button) :is(h1, h2, h3, h4)'}
`('should translate "$before" into "$after"', ({ before, after }) => {
let result = finalizeSelector(formatVariantSelector('&', before), {
selector: '.a',
candidate: 'a',
})

expect(result).toEqual(after.replace('&', '.a'))
})
})
4 changes: 2 additions & 2 deletions tests/parallel-variants.test.js
Expand Up @@ -27,7 +27,7 @@ test('basic parallel variants', async () => {
.test\:font-medium *::test {
font-weight: 500;
}
.hover\:test\:font-black *::test:hover {
.hover\:test\:font-black *:hover::test {
font-weight: 900;
}
.test\:font-bold::test {
Expand All @@ -36,7 +36,7 @@ test('basic parallel variants', async () => {
.test\:font-medium::test {
font-weight: 500;
}
.hover\:test\:font-black::test:hover {
.hover\:test\:font-black:hover::test {
font-weight: 900;
}
`)
Expand Down

0 comments on commit fe41e98

Please sign in to comment.