Skip to content

Commit

Permalink
JIT: Add exhaustive pseudo-class and pseudo-element variant support (#…
Browse files Browse the repository at this point in the history
…4482)

* Add first-line, first-letter, and marker variants

* Add selection variant

Co-Authored-By: Eric Rodrigues Pires <eric@eric.dev.br>

* Add remaining pseudo-class variants

* Add target pseudo-class

Co-Authored-By: Peter Neupauer <peter@neupauer.sk>

* add test for parallel variants

* implement parallel variants

Co-authored-by: Eric Rodrigues Pires <eric@eric.dev.br>
Co-authored-by: Peter Neupauer <peter@neupauer.sk>
Co-authored-by: Robin Malfait <malfait.robin@gmail.com>
  • Loading branch information
4 people committed May 27, 2021
1 parent 8cd6017 commit d1e9632
Show file tree
Hide file tree
Showing 6 changed files with 437 additions and 53 deletions.
78 changes: 75 additions & 3 deletions src/jit/corePlugins.js
Expand Up @@ -11,7 +11,55 @@ import {
} from '../util/pluginUtils'

export default {
pseudoClassVariants: function ({ config, addVariant }) {
pseudoElementVariants: function ({ config, addVariant }) {
addVariant(
'first-letter',
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`first-letter${config('separator')}${className}`, '::first-letter')
})
})
)

addVariant(
'first-line',
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`first-line${config('separator')}${className}`, '::first-line')
})
})
)

addVariant('marker', [
transformAllSelectors((selector) => {
let variantSelector = updateAllClasses(selector, (className) => {
return `marker${config('separator')}${className}`
})

return `${variantSelector} *::marker`
}),
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`marker${config('separator')}${className}`, '::marker')
})
}),
])

addVariant('selection', [
transformAllSelectors((selector) => {
let variantSelector = updateAllClasses(selector, (className) => {
return `selection${config('separator')}${className}`
})

return `${variantSelector} *::selection`
}),
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`selection${config('separator')}${className}`, '::selection')
})
}),
])

addVariant(
'before',
transformAllSelectors(
Expand Down Expand Up @@ -55,16 +103,40 @@ export default {
}
)
)

},
pseudoClassVariants: function ({ config, addVariant }) {
let pseudoVariants = [
// Positional
['first', 'first-child'],
['last', 'last-child'],
['only', 'only-child'],
['odd', 'nth-child(odd)'],
['even', 'nth-child(even)'],
'first-of-type',
'last-of-type',
'only-of-type',

// State
'visited',
'target',

// Forms
'default',
'checked',
'empty',
'indeterminate',
'placeholder-shown',
'autofill',
'required',
'valid',
'invalid',
'in-range',
'out-of-range',
'read-only',

// Content
'empty',

// Interactive
'focus-within',
'hover',
'focus',
Expand Down
53 changes: 28 additions & 25 deletions src/jit/lib/generateRules.js
Expand Up @@ -100,7 +100,7 @@ function applyVariant(variant, matches, context) {
}

if (context.variantMap.has(variant)) {
let [variantSort, applyThisVariant] = context.variantMap.get(variant)
let variantFunctionTuples = context.variantMap.get(variant)
let result = []

for (let [{ sort, layer, options }, rule] of matches) {
Expand All @@ -112,36 +112,39 @@ function applyVariant(variant, matches, context) {
let container = postcss.root()
container.append(rule.clone())

function modifySelectors(modifierFunction) {
container.each((rule) => {
if (rule.type !== 'rule') {
return
}

rule.selectors = rule.selectors.map((selector) => {
return modifierFunction({
get className() {
return getClassNameFromSelector(selector)
},
selector,
for (let [variantSort, variantFunction] of variantFunctionTuples) {
let clone = container.clone()
function modifySelectors(modifierFunction) {
clone.each((rule) => {
if (rule.type !== 'rule') {
return
}

rule.selectors = rule.selectors.map((selector) => {
return modifierFunction({
get className() {
return getClassNameFromSelector(selector)
},
selector,
})
})
})
return clone
}

let ruleWithVariant = variantFunction({
container: clone,
separator: context.tailwindConfig.separator,
modifySelectors,
})
return container
}

let ruleWithVariant = applyThisVariant({
container,
separator: context.tailwindConfig.separator,
modifySelectors,
})
if (ruleWithVariant === null) {
continue
}

if (ruleWithVariant === null) {
continue
let withOffset = [{ sort: variantSort | sort, layer, options }, clone.nodes[0]]
result.push(withOffset)
}

let withOffset = [{ sort: variantSort | sort, layer, options }, container.nodes[0]]
result.push(withOffset)
}

return result
Expand Down
30 changes: 22 additions & 8 deletions src/jit/lib/setupContextUtils.js
Expand Up @@ -13,6 +13,7 @@ import isPlainObject from '../../util/isPlainObject'
import escapeClassName from '../../util/escapeClassName'
import nameClass from '../../util/nameClass'
import { coerceValue } from '../../util/pluginUtils'
import bigSign from '../../util/bigSign'
import corePlugins from '../corePlugins'
import * as sharedState from './sharedState'
import { env } from './sharedState'
Expand Down Expand Up @@ -152,9 +153,11 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
}

return {
addVariant(variantName, applyThisVariant, options = {}) {
addVariant(variantName, variantFunctions, options = {}) {
variantFunctions = [].concat(variantFunctions)

insertInto(variantList, variantName, options)
variantMap.set(variantName, applyThisVariant)
variantMap.set(variantName, variantFunctions)
},
postcss,
prefix: applyConfiguredPrefix,
Expand Down Expand Up @@ -395,7 +398,7 @@ function resolvePlugins(context, tailwindDirectives, root) {

// TODO: This is a workaround for backwards compatibility, since custom variants
// were historically sorted before screen/stackable variants.
let beforeVariants = [corePlugins['pseudoClassVariants']]
let beforeVariants = [corePlugins['pseudoElementVariants'], corePlugins['pseudoClassVariants']]
let afterVariants = [
corePlugins['directionVariants'],
corePlugins['reducedMotionVariants'],
Expand Down Expand Up @@ -445,17 +448,28 @@ function registerPlugins(plugins, context) {
}

reservedBits += 3n
context.variantOrder = variantList.reduce(
(map, variant, i) => map.set(variant, (1n << BigInt(i)) << reservedBits),
new Map()

let offset = 0
context.variantOrder = new Map(
variantList
.map((variant, i) => {
let variantFunctions = variantMap.get(variant).length
let bits = (1n << BigInt(i + offset)) << reservedBits
offset += variantFunctions - 1
return [variant, bits]
})
.sort(([, a], [, z]) => bigSign(a - z))
)

context.minimumScreen = [...context.variantOrder.values()].shift()

// Build variantMap
for (let [variantName, variantFunction] of variantMap.entries()) {
for (let [variantName, variantFunctions] of variantMap.entries()) {
let sort = context.variantOrder.get(variantName)
context.variantMap.set(variantName, [sort, variantFunction])
context.variantMap.set(
variantName,
variantFunctions.map((variantFunction, idx) => [sort << BigInt(idx), variantFunction])
)
}
}

Expand Down
70 changes: 70 additions & 0 deletions tests/jit/parallel-variants.test.js
@@ -0,0 +1,70 @@
import postcss from 'postcss'
import path from 'path'
import tailwind from '../../src/jit/index.js'
import { transformAllSelectors, updateAllClasses } from '../../src/util/pluginUtils.js'

function run(input, config = {}) {
const { currentTestName } = expect.getState()

return postcss(tailwind(config)).process(input, {
from: `${path.resolve(__filename)}?test=${currentTestName}`,
})
}

test('basic parallel variants', async () => {
let config = {
mode: 'jit',
purge: [
{
raw: '<div class="font-normal hover:test:font-black test:font-bold test:font-medium"></div>',
},
],
theme: {},
plugins: [
function test({ addVariant, config }) {
addVariant('test', [
transformAllSelectors((selector) => {
let variantSelector = updateAllClasses(selector, (className) => {
return `test${config('separator')}${className}`
})

return `${variantSelector} *::test`
}),
transformAllSelectors((selector) => {
return updateAllClasses(selector, (className, { withPseudo }) => {
return withPseudo(`test${config('separator')}${className}`, '::test')
})
}),
])
},
],
}

let css = `@tailwind utilities`

return run(css, config).then((result) => {
expect(result.css).toMatchFormattedCss(`
.font-normal {
font-weight: 400;
}
.test\\:font-bold *::test {
font-weight: 700;
}
.test\\:font-medium *::test {
font-weight: 500;
}
.hover\\:test\\:font-black:hover *::test {
font-weight: 900;
}
.test\\:font-bold::test {
font-weight: 700;
}
.test\\:font-medium::test {
font-weight: 500;
}
.hover\\:test\\:font-black:hover::test {
font-weight: 900;
}
`)
})
})

0 comments on commit d1e9632

Please sign in to comment.