Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
use better algorithm
  • Loading branch information
RobinMalfait committed Nov 12, 2021
commit fe41e980cfd1bd44bbd985cda9201dd08d3f9781
18 changes: 0 additions & 18 deletions src/lib/generateRules.js
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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