Skip to content

Commit

Permalink
handle the prefix inside the group/peer variants
Browse files Browse the repository at this point in the history
Then add the `NoPrefix` feature to the variant itself, which will skip
prefixing any other class in the generated selector (because we already
took care of prefixing `.group` and `.peer`).

We are using an internal symbol such that:

- We can keep it as a private API
- We don't introduce a breaking change
  • Loading branch information
RobinMalfait committed Jun 28, 2023
1 parent 8e2a3df commit 4341237
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 103 deletions.
21 changes: 9 additions & 12 deletions src/corePlugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { formatBoxShadowValue, parseBoxShadowValue } from './util/parseBoxShadow
import { removeAlphaVariables } from './util/removeAlphaVariables'
import { flagEnabled } from './featureFlags'
import { normalize } from './util/dataTypes'
import { Features } from './lib/setupContextUtils'

export let variantPlugins = {
pseudoElementVariants: ({ addVariant }) => {
Expand Down Expand Up @@ -79,7 +80,7 @@ export let variantPlugins = {
})
},

pseudoClassVariants: ({ addVariant, matchVariant, config }) => {
pseudoClassVariants: ({ addVariant, matchVariant, config, prefix }) => {
let pseudoVariants = [
// Positional
['first', '&:first-child'],
Expand Down Expand Up @@ -150,12 +151,12 @@ export let variantPlugins = {
let variants = {
group: (_, { modifier }) =>
modifier
? [`:merge(.group\\/${escapeClassName(modifier)})`, ' &']
: [`:merge(.group)`, ' &'],
? [`:merge(${prefix('.group')}\\/${escapeClassName(modifier)})`, ' &']
: [`:merge(${prefix('.group')})`, ' &'],
peer: (_, { modifier }) =>
modifier
? [`:merge(.peer\\/${escapeClassName(modifier)})`, ' ~ &']
: [`:merge(.peer)`, ' ~ &'],
? [`:merge(${prefix('.peer')}\\/${escapeClassName(modifier)})`, ' ~ &']
: [`:merge(${prefix('.peer')})`, ' ~ &'],
}

for (let [name, fn] of Object.entries(variants)) {
Expand Down Expand Up @@ -187,15 +188,11 @@ export let variantPlugins = {
}

// Basically this but can handle quotes:
// result.replace(/&(\S+)?/g, (_, pseudo = '') => a + `:tw-no-prefix(${pseudo})` + b)
// result.replace(/&(\S+)?/g, (_, pseudo = '') => a + pseudo + b)

let pseudo = result.slice(start + 1, end)

pseudo = config('prefix') ? `:tw-no-prefix(${pseudo})` : pseudo

return result.slice(0, start) + a + pseudo + b + result.slice(end)
return result.slice(0, start) + a + result.slice(start + 1, end) + b + result.slice(end)
},
{ values: Object.fromEntries(pseudoVariants) }
{ values: Object.fromEntries(pseudoVariants), [Features]: Features.RespectPrefix }
)
}
},
Expand Down
15 changes: 11 additions & 4 deletions src/lib/generateRules.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '../util/formatVariantSelector'
import { asClass } from '../util/nameClass'
import { normalize } from '../util/dataTypes'
import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
import { isValidVariantFormatString, parseVariant, Features } from './setupContextUtils'
import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue'
import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
import { flagEnabled } from '../featureFlags'
Expand Down Expand Up @@ -226,9 +226,16 @@ function applyVariant(variant, matches, context) {

if (context.variantMap.has(variant)) {
let isArbitraryVariant = isArbitraryValue(variant)
let features = context.variantOptions.get(variant)?.[Features] ?? Features.None
let variantFunctionTuples = context.variantMap.get(variant).slice()
let result = []

let respectPrefix = (() => {
if (isArbitraryVariant) return false
if ((features & Features.RespectPrefix) === Features.RespectPrefix) return false
return true
})()

for (let [meta, rule] of matches) {
// Don't generate variants for user css
if (meta.layer === 'user') {
Expand Down Expand Up @@ -289,7 +296,7 @@ function applyVariant(variant, matches, context) {
format(selectorFormat) {
collectedFormats.push({
format: selectorFormat,
isArbitraryVariant,
respectPrefix,
})
},
args,
Expand Down Expand Up @@ -318,7 +325,7 @@ function applyVariant(variant, matches, context) {
if (typeof ruleWithVariant === 'string') {
collectedFormats.push({
format: ruleWithVariant,
isArbitraryVariant,
respectPrefix,
})
}

Expand Down Expand Up @@ -362,7 +369,7 @@ function applyVariant(variant, matches, context) {
// format: .foo &
collectedFormats.push({
format: modified.replace(rebuiltBase, '&'),
isArbitraryVariant,
respectPrefix,
})
rule.selector = before
})
Expand Down
19 changes: 17 additions & 2 deletions src/lib/setupContextUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ import { hasContentChanged } from './cacheInvalidation.js'
import { Offsets } from './offsets.js'
import { finalizeSelector, formatVariantSelector } from '../util/formatVariantSelector'

export const Features = Object.assign(Symbol(), {
// No features are enabled
None: 0,

// Whether or not we should respect the prefix
RespectPrefix: 1 << 0,
})

const VARIANT_TYPES = {
AddVariant: Symbol.for('ADD_VARIANT'),
MatchVariant: Symbol.for('MATCH_VARIANT'),
Expand Down Expand Up @@ -1110,17 +1118,24 @@ function registerPlugins(plugins, context) {
}

let isArbitraryVariant = !(value in (options.values ?? {}))
let features = options[Features] ?? Features.None

let respectPrefix = (() => {
if (isArbitraryVariant) return false
if ((features & Features.RespectPrefix) === Features.RespectPrefix) return false
return true
})()

formatStrings = formatStrings.map((format) =>
format.map((str) => ({
format: str,
isArbitraryVariant,
respectPrefix,
}))
)

manualFormatStrings = manualFormatStrings.map((format) => ({
format,
isArbitraryVariant,
respectPrefix,
}))

let opts = {
Expand Down
4 changes: 2 additions & 2 deletions src/util/formatVariantSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { movePseudos } from './pseudoElements'
/** @typedef {import('postcss-selector-parser').Pseudo} Pseudo */
/** @typedef {import('postcss-selector-parser').Node} Node */

/** @typedef {{format: string, isArbitraryVariant: boolean}[]} RawFormats */
/** @typedef {{format: string, respectPrefix: boolean}[]} RawFormats */
/** @typedef {import('postcss-selector-parser').Root} ParsedFormats */
/** @typedef {RawFormats | ParsedFormats} AcceptedFormats */

Expand All @@ -29,7 +29,7 @@ export function formatVariantSelector(formats, { context, candidate }) {

return {
...format,
ast: format.isArbitraryVariant ? ast : prefixSelector(prefix, ast),
ast: format.respectPrefix ? prefixSelector(prefix, ast) : ast,
}
})

Expand Down
35 changes: 8 additions & 27 deletions src/util/prefixSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,14 @@ export default function (prefix, selector, prependNegative = false) {
/** @type {import('postcss-selector-parser').Root} */
let ast = typeof selector === 'string' ? parser().astSync(selector) : selector

// ast.walk bails too early when returning so it's not usable here
function prefixClasses(node) {
// Here we look for `:tw-no-prefix` which is an *internal-use-only* marker
// used to stop traversal so we don't replace any classes inside it
if (node.type === 'pseudo' && node.value === ':tw-no-prefix') {
node.replaceWith(...node.nodes)
return
}

// Prefix any classes we find
if (node.type === 'class') {
let baseClass = node.value
let shouldPlaceNegativeBeforePrefix = prependNegative && baseClass.startsWith('-')

node.value = shouldPlaceNegativeBeforePrefix
? `-${prefix}${baseClass.slice(1)}`
: `${prefix}${baseClass}`
return
}

// Keep looking for classes
if (node.length) {
node.each(prefixClasses)
}
}

ast.each(prefixClasses)
ast.walkClasses((classSelector) => {
let baseClass = classSelector.value
let shouldPlaceNegativeBeforePrefix = prependNegative && baseClass.startsWith('-')

classSelector.value = shouldPlaceNegativeBeforePrefix
? `-${prefix}${baseClass.slice(1)}`
: `${prefix}${baseClass}`
})

return typeof selector === 'string' ? ast.toString() : ast
}
Loading

0 comments on commit 4341237

Please sign in to comment.