Compare commits
19 commits
main
...
feature/ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
880ceb49c9 | ||
|
|
b756b00ba1 | ||
|
|
f54152b19e | ||
|
|
52828f0100 | ||
|
|
b811c698fe | ||
|
|
16349e74c4 | ||
|
|
cd24c5904b | ||
|
|
985e35722b | ||
|
|
5bcd9d2f69 | ||
|
|
3cb47aaab6 | ||
|
|
92000a5820 | ||
|
|
c8be920468 | ||
|
|
3192f2a007 | ||
|
|
90209cfda8 | ||
|
|
f66873f35e | ||
|
|
5fe5a65029 | ||
|
|
0f98fec7f0 | ||
|
|
39c7a93f22 | ||
|
|
3ac72b7e7d |
14 changed files with 1727 additions and 417 deletions
1
.swcrc
1
.swcrc
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"exclude": ".d.ts$",
|
||||
"module": {
|
||||
"type": "commonjs"
|
||||
},
|
||||
|
|
|
|||
65
src/lib/candidate.d.ts
vendored
Normal file
65
src/lib/candidate.d.ts
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
type Arbitrary = {
|
||||
raw: string
|
||||
value: string
|
||||
dataType: AnyStringWithAutocomplete<DataType>
|
||||
}
|
||||
|
||||
type VariantCommon = { raw: string }
|
||||
type VariantType =
|
||||
| { type: 'constrained'; name: string }
|
||||
| { type: 'partial'; name: string; value: string; dataType: DataType }
|
||||
| { type: 'custom'; value: string; dataType: DataType }
|
||||
|
||||
type Variant = VariantCommon & VariantType
|
||||
|
||||
type ModifierCommon = { raw: string }
|
||||
type ModifierType = { value: string | Arbitrary }
|
||||
|
||||
type Modifier = ModifierCommon & ModifierType
|
||||
|
||||
type UtilityCommon = {
|
||||
raw: string
|
||||
className: string
|
||||
withoutVariants: string // TODO: remove this
|
||||
important: boolean
|
||||
prefix: string
|
||||
negative: boolean
|
||||
|
||||
variants: Variant[]
|
||||
}
|
||||
|
||||
export type Plugin = {
|
||||
plugin: string
|
||||
value: string | Arbitrary
|
||||
modifiers: Modifier[]
|
||||
}
|
||||
|
||||
type UtilityType =
|
||||
| {
|
||||
type: 'constrained'
|
||||
plugins(): Iterable<Plugin>
|
||||
}
|
||||
| { type: 'custom'; name: string; value: string; modifiers: Modifier[] }
|
||||
|
||||
export type Candidate<T = UtilityType['type']> = UtilityCommon & Extract<UtilityType, { type: T }>
|
||||
|
||||
export function parseCandidate(raw: string, context: any): Iterable<Candidate>
|
||||
|
||||
export type AnyStringWithAutocomplete<T> = T | (string & Record<never, never>)
|
||||
|
||||
export type DataType =
|
||||
| 'any'
|
||||
| 'color'
|
||||
| 'url'
|
||||
| 'image'
|
||||
| 'length'
|
||||
| 'percentage'
|
||||
| 'position'
|
||||
| 'lookup'
|
||||
| 'generic-name'
|
||||
| 'family-name'
|
||||
| 'number'
|
||||
| 'line-width'
|
||||
| 'absolute-size'
|
||||
| 'relative-size'
|
||||
| 'shadow'
|
||||
495
src/lib/candidate.js
Normal file
495
src/lib/candidate.js
Normal file
|
|
@ -0,0 +1,495 @@
|
|||
// @ts-check
|
||||
|
||||
/** @typedef {import('./candidate.d').Plugin} Plugin */
|
||||
/** @typedef {import('./candidate.d').Arbitrary} Arbitrary */
|
||||
/** @typedef {import('./candidate.d').Candidate} Candidate */
|
||||
/** @typedef {import('./candidate.d').DataType} DataType */
|
||||
/** @typedef {import('./candidate.d').Variant} Variant */
|
||||
/** @typedef {import('./candidate.d').Modifier} Modifier */
|
||||
|
||||
import { normalize } from '../util/dataTypes.js'
|
||||
import isValidArbitraryValue from '../util/isValidArbitraryValue.js'
|
||||
import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
|
||||
import { isParsableCssValue, isValidPropName, looksLikeUri } from '../util/css-validation.js'
|
||||
import { candidatePermutations } from '../util/candidatePermutations'
|
||||
import * as sharedState from './sharedState'
|
||||
import { asClass } from '../util/nameClass.js'
|
||||
|
||||
let caches = {
|
||||
/** @type {Map<string, Candidate[]>} */
|
||||
candidates: new Map(),
|
||||
|
||||
/** @type {Map<string, Variant>} */
|
||||
variants: new Map(),
|
||||
|
||||
/** @type {Map<string, Modifier>} */
|
||||
modifiers: new Map(),
|
||||
|
||||
/** @type {Map<string, Arbitrary | string>} */
|
||||
arbitrary: new Map(),
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} raw
|
||||
* @param {any} context
|
||||
* @returns {Iterable<Candidate>}
|
||||
*/
|
||||
export function parseCandidate(raw, context) {
|
||||
if (caches.candidates.has(raw)) {
|
||||
return caches.candidates.get(raw)
|
||||
}
|
||||
|
||||
let candidates = Array.from(parseStructure(raw, context))
|
||||
.map((candidate) => validateCandidate(candidate))
|
||||
.filter(Boolean)
|
||||
|
||||
return put(caches.candidates, raw, candidates)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Candidate} candidate
|
||||
* @returns {Candidate | null}
|
||||
*/
|
||||
function validateCandidate(candidate) {
|
||||
// Normalize arbitrary values in variants
|
||||
for (const item of candidate.variants) {
|
||||
if (item.type === 'partial' || item.type === 'custom') {
|
||||
item.value = normalize(item.value)
|
||||
}
|
||||
}
|
||||
|
||||
candidate = validateCustomCandidate(candidate)
|
||||
|
||||
if (!candidate) {
|
||||
return candidate
|
||||
}
|
||||
|
||||
if (candidate.type !== 'constrained') {
|
||||
return candidate
|
||||
}
|
||||
|
||||
// Normalize arbitrary values
|
||||
let plugins = candidate.plugins
|
||||
candidate.plugins = () => validatePlugins(plugins())
|
||||
|
||||
return candidate
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Candidate} candidate
|
||||
* @returns {Candidate | null}
|
||||
*/
|
||||
function validateCustomCandidate(candidate) {
|
||||
if (candidate.type !== 'custom') {
|
||||
return candidate
|
||||
}
|
||||
|
||||
// Ignore invalid custom property names
|
||||
if (!isValidPropName(candidate.name)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Ignore url-like custom properties
|
||||
if (looksLikeUri(`${candidate.name}:${candidate.value}`)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!isValidArbitraryValue(candidate.value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!isParsableCssValue(candidate.name, candidate.value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Normalize arbitrary values in modifiers
|
||||
candidate.modifiers = Array.from(normalizeModifiers(candidate.modifiers))
|
||||
candidate.value = normalize(candidate.value)
|
||||
|
||||
return candidate
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Iterable<Modifier>} modifiers
|
||||
* @returns {Iterable<Modifier>}
|
||||
*/
|
||||
function* normalizeModifiers(modifiers) {
|
||||
// Normalize arbitrary values in modifiers
|
||||
for (const modifier of modifiers) {
|
||||
if (typeof modifier.value === 'string') {
|
||||
modifier.value = normalize(modifier.value)
|
||||
} else if (typeof modifier.value === 'object') {
|
||||
modifier.value.value = normalize(modifier.value.value)
|
||||
}
|
||||
|
||||
yield modifier
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Iterable<Plugin>} plugins
|
||||
*/
|
||||
function* validatePlugins(plugins) {
|
||||
for (const plugin of plugins) {
|
||||
if (typeof plugin.value === 'object') {
|
||||
if (!isValidArbitraryValue(plugin.value.value)) {
|
||||
continue
|
||||
}
|
||||
|
||||
plugin.modifiers = Array.from(normalizeModifiers(plugin.modifiers))
|
||||
plugin.value.value = normalize(plugin.value.value)
|
||||
}
|
||||
|
||||
yield plugin
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} raw
|
||||
* @param {any} context
|
||||
* @returns {Iterable<Candidate>}
|
||||
*/
|
||||
function* parseStructure(raw, context) {
|
||||
// TODO: Can we remove this?
|
||||
if (raw === sharedState.NOT_ON_DEMAND) {
|
||||
yield {
|
||||
raw,
|
||||
withoutVariants: raw,
|
||||
className: `${sharedState.NOT_ON_DEMAND}`,
|
||||
|
||||
// This is always gonna be the same
|
||||
// but is included for completeness
|
||||
prefix: context.tailwindConfig.prefix ?? '',
|
||||
important: false,
|
||||
variants: [],
|
||||
negative: false,
|
||||
|
||||
/** @type {'constrained'} */
|
||||
type: 'constrained',
|
||||
|
||||
// @ts-ignore
|
||||
*plugins() {
|
||||
yield {
|
||||
// @ts-ignore: TODO Would love to remove the need for the new String('*) stuff if at all ever maybe possible
|
||||
plugin: sharedState.NOT_ON_DEMAND,
|
||||
value: 'DEFAULT',
|
||||
modifiers: [],
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Parse out the variants
|
||||
let [candidate, ...rawVariants] = Array.from(
|
||||
splitAtTopLevelOnly(raw, context.tailwindConfig.separator)
|
||||
).reverse()
|
||||
|
||||
let variants = rawVariants.map(parseVariant)
|
||||
|
||||
// Important?
|
||||
let important = candidate[0] === '!'
|
||||
if (important) {
|
||||
candidate = candidate.slice(1)
|
||||
}
|
||||
|
||||
let withoutVariants = candidate
|
||||
|
||||
// Negative before prefix
|
||||
let negative = false
|
||||
if (candidate[0] === '-') {
|
||||
negative = true
|
||||
candidate = candidate.slice(1)
|
||||
}
|
||||
|
||||
// Verify the prefix
|
||||
// If the candidate isn't prefixed with the configured prefix
|
||||
// Then we treat the entire thing as the candidate rather than just the prefixed portion
|
||||
let prefix = context.tailwindConfig.prefix ?? ''
|
||||
let prefixLength = prefix.length
|
||||
|
||||
if (prefix !== '') {
|
||||
if (candidate.slice(0, prefixLength) === prefix) {
|
||||
candidate = candidate.slice(prefixLength)
|
||||
} else {
|
||||
prefix = ''
|
||||
prefixLength = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Negative after prefix
|
||||
if (prefix !== '' && candidate[0] === '-') {
|
||||
// We had a negative before the prefix
|
||||
// This means we're looking at something like -tw--top-1
|
||||
// That's weird and invalid
|
||||
if (negative === true) {
|
||||
return
|
||||
}
|
||||
|
||||
negative = true
|
||||
candidate = candidate.slice(1)
|
||||
}
|
||||
|
||||
let common = {
|
||||
raw,
|
||||
className: asClass(raw),
|
||||
withoutVariants,
|
||||
prefix,
|
||||
important,
|
||||
variants,
|
||||
negative,
|
||||
}
|
||||
|
||||
let arbitraryProperty = parseArbitraryProperty(candidate)
|
||||
if (arbitraryProperty) {
|
||||
yield Object.assign({}, common, {
|
||||
/** @type {'custom'} */
|
||||
type: 'custom',
|
||||
name: arbitraryProperty[0],
|
||||
value: arbitraryProperty[1],
|
||||
modifiers: [],
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Scan for the name up to the modifier, opening arbitrary value bracket, or end of string
|
||||
yield Object.assign({}, common, {
|
||||
/** @type {'constrained'} */
|
||||
type: 'constrained',
|
||||
plugins: () => {
|
||||
// Generate each plugin
|
||||
let plugins = generatePlugins(candidate, negative)
|
||||
|
||||
// Insert modifier versions of each plugin (if necessary)
|
||||
plugins = insertModifierPlugins(plugins, parseModifiers(candidate))
|
||||
|
||||
return plugins
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} candidate
|
||||
* @param {boolean} negative
|
||||
* @returns {Iterable<Plugin>}
|
||||
*/
|
||||
function* generatePlugins(candidate, negative) {
|
||||
for (const plugin of candidatePermutations(candidate, negative)) {
|
||||
yield plugin
|
||||
|
||||
let arbitrary = extractArbitraryValue(`${plugin.value}`)
|
||||
if (arbitrary) {
|
||||
yield {
|
||||
...plugin,
|
||||
value: arbitrary,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Iterable<Plugin>} plugins
|
||||
* @param {Modifier[]} modifiers
|
||||
* @returns {Iterable<Plugin>}
|
||||
*/
|
||||
function* insertModifierPlugins(plugins, modifiers) {
|
||||
for (const plugin of plugins) {
|
||||
if (typeof plugin.value === 'object') {
|
||||
plugin.modifiers = modifiers
|
||||
}
|
||||
|
||||
yield plugin
|
||||
|
||||
if (modifiers.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof plugin.value === 'string' && plugin.value.includes(`/${modifiers[0].raw}`)) {
|
||||
yield {
|
||||
...plugin,
|
||||
value: plugin.value.slice(0, plugin.value.indexOf(`/${modifiers[0].raw}`)),
|
||||
modifiers,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {[string, string] | null}
|
||||
*/
|
||||
function parseArbitraryProperty(str) {
|
||||
let [, property, value] = str.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/) ?? []
|
||||
|
||||
if (value === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
return [property, value]
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} raw
|
||||
* @returns {Arbitrary | null}
|
||||
*/
|
||||
function extractArbitraryValue(raw) {
|
||||
let arbitraryStart = raw.indexOf('[')
|
||||
let arbitraryEnd = raw.lastIndexOf(']/')
|
||||
if (arbitraryEnd === -1) {
|
||||
arbitraryEnd = raw.lastIndexOf(']')
|
||||
}
|
||||
|
||||
if (arbitraryStart === -1 || arbitraryEnd === -1) {
|
||||
return null
|
||||
}
|
||||
|
||||
let arbitrary = parseArbitraryValue(raw.slice(arbitraryStart, arbitraryEnd + 1))
|
||||
|
||||
if (typeof arbitrary === 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
return arbitrary
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} raw
|
||||
* @returns {string | Arbitrary}
|
||||
*/
|
||||
function parseArbitraryValue(raw) {
|
||||
if (caches.arbitrary.has(raw)) {
|
||||
return caches.arbitrary.get(raw)
|
||||
}
|
||||
|
||||
if (raw[0] !== '[' || raw[raw.length - 1] !== ']') {
|
||||
return raw
|
||||
}
|
||||
|
||||
let dataTypeSeparator = raw.indexOf(':')
|
||||
let dataType = raw.slice(1, dataTypeSeparator)
|
||||
let isValidDataType = dataTypeSeparator !== -1 && /^[\w-_]+$/g.test(dataType)
|
||||
let value = raw.slice(dataTypeSeparator + 1, -1)
|
||||
|
||||
// It could also be that the user says something like bg-[color:]
|
||||
// This would be treated as an "empty" arbitrary value with type of `color` which is not what we want
|
||||
// This this is not really an arbitrary value but just a raw string
|
||||
if (value === '') {
|
||||
return raw
|
||||
}
|
||||
|
||||
// It could be that this resolves to `url(https` which is not a valid
|
||||
// identifier. We currently only support "simple" words with dashes or
|
||||
// underscores. E.g.: family-name
|
||||
if (!isValidDataType) {
|
||||
return put(caches.arbitrary, raw, {
|
||||
raw,
|
||||
value: raw.slice(1, -1),
|
||||
dataType: 'any',
|
||||
})
|
||||
}
|
||||
|
||||
return put(caches.arbitrary, raw, {
|
||||
raw,
|
||||
value,
|
||||
dataType,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} raw
|
||||
* @returns {Variant}
|
||||
*/
|
||||
function parseVariant(raw) {
|
||||
if (caches.variants.has(raw)) {
|
||||
return caches.variants.get(raw)
|
||||
}
|
||||
|
||||
if (raw[0] === '[' && raw[raw.length - 1] === ']') {
|
||||
return put(caches.variants, raw, {
|
||||
/** @type {'custom'} */
|
||||
type: 'custom',
|
||||
raw,
|
||||
value: raw.slice(1, -1),
|
||||
dataType: 'any',
|
||||
})
|
||||
}
|
||||
|
||||
if (raw[0] !== '[' && raw[raw.length - 1] === ']') {
|
||||
let value = raw.slice(raw.lastIndexOf('[') + 1, -1)
|
||||
let name = raw.slice(0, raw.indexOf(value) - 1 /* - */ - 1 /* [ */)
|
||||
|
||||
return put(caches.variants, raw, {
|
||||
type: 'partial',
|
||||
raw,
|
||||
name,
|
||||
value,
|
||||
dataType: 'any',
|
||||
})
|
||||
}
|
||||
|
||||
return put(caches.variants, raw, {
|
||||
type: 'constrained',
|
||||
raw,
|
||||
name: raw,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} raw
|
||||
* @returns {Modifier[]}
|
||||
*/
|
||||
function parseModifiers(raw) {
|
||||
let match = raw.match(/\/(\[[^\[\]]+\])|\/([^\[\]]+)$/) ?? []
|
||||
|
||||
if (match.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (match[1] !== undefined) {
|
||||
return [
|
||||
{
|
||||
raw: match[1],
|
||||
value: parseArbitraryValue(match[1]),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
if (match[2] !== undefined) {
|
||||
return [
|
||||
{
|
||||
raw: match[2],
|
||||
value: match[2],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Map<string, T>} cache
|
||||
* @param {string} key
|
||||
* @param {T} value
|
||||
* @returns {T}
|
||||
*/
|
||||
function put(cache, key, value) {
|
||||
cache.set(key, value)
|
||||
|
||||
// console.log('CACHE MISS', { key, value })
|
||||
|
||||
return value
|
||||
}
|
||||
|
|
@ -5,13 +5,11 @@ import isPlainObject from '../util/isPlainObject'
|
|||
import prefixSelector from '../util/prefixSelector'
|
||||
import { updateAllClasses } from '../util/pluginUtils'
|
||||
import log from '../util/log'
|
||||
import * as sharedState from './sharedState'
|
||||
import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
|
||||
import { asClass } from '../util/nameClass'
|
||||
import { normalize } from '../util/dataTypes'
|
||||
import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
|
||||
import isValidArbitraryValue from '../util/isValidArbitraryValue'
|
||||
import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
|
||||
import { isParsableNode } from '../util/css-validation'
|
||||
import { parseCandidate } from './candidate'
|
||||
|
||||
let classNameParser = selectorParser((selectors) => {
|
||||
return selectors.first.filter(({ type }) => type === 'class').pop().value
|
||||
|
|
@ -21,42 +19,6 @@ function getClassNameFromSelector(selector) {
|
|||
return classNameParser.transformSync(selector)
|
||||
}
|
||||
|
||||
// Generate match permutations for a class candidate, like:
|
||||
// ['ring-offset-blue', '100']
|
||||
// ['ring-offset', 'blue-100']
|
||||
// ['ring', 'offset-blue-100']
|
||||
// Example with dynamic classes:
|
||||
// ['grid-cols', '[[linename],1fr,auto]']
|
||||
// ['grid', 'cols-[[linename],1fr,auto]']
|
||||
function* candidatePermutations(candidate) {
|
||||
let lastIndex = Infinity
|
||||
|
||||
while (lastIndex >= 0) {
|
||||
let dashIdx
|
||||
|
||||
if (lastIndex === Infinity && candidate.endsWith(']')) {
|
||||
let bracketIdx = candidate.indexOf('[')
|
||||
|
||||
// If character before `[` isn't a dash or a slash, this isn't a dynamic class
|
||||
// eg. string[]
|
||||
dashIdx = ['-', '/'].includes(candidate[bracketIdx - 1]) ? bracketIdx - 1 : -1
|
||||
} else {
|
||||
dashIdx = candidate.lastIndexOf('-', lastIndex)
|
||||
}
|
||||
|
||||
if (dashIdx < 0) {
|
||||
break
|
||||
}
|
||||
|
||||
let prefix = candidate.slice(0, dashIdx)
|
||||
let modifier = candidate.slice(dashIdx + 1)
|
||||
|
||||
yield [prefix, modifier]
|
||||
|
||||
lastIndex = dashIdx - 1
|
||||
}
|
||||
}
|
||||
|
||||
function applyPrefix(matches, context) {
|
||||
if (matches.length === 0 || context.tailwindConfig.prefix === '') {
|
||||
return matches
|
||||
|
|
@ -90,10 +52,13 @@ function applyPrefix(matches, context) {
|
|||
return matches
|
||||
}
|
||||
|
||||
function applyImportant(matches, classCandidate) {
|
||||
function applyImportant(matches, parsed) {
|
||||
if (matches.length === 0) {
|
||||
return matches
|
||||
}
|
||||
|
||||
let classCandidate = parsed.withoutVariants
|
||||
|
||||
let result = []
|
||||
|
||||
for (let [meta, rule] of matches) {
|
||||
|
|
@ -129,159 +94,159 @@ function applyVariant(variant, matches, context) {
|
|||
|
||||
let args
|
||||
|
||||
// Find partial arbitrary variants
|
||||
if (variant.endsWith(']') && !variant.startsWith('[')) {
|
||||
args = variant.slice(variant.lastIndexOf('[') + 1, -1)
|
||||
variant = variant.slice(0, variant.indexOf(args) - 1 /* - */ - 1 /* [ */)
|
||||
}
|
||||
if (variant.type === 'constrained') {
|
||||
variant = variant.name
|
||||
} else if (variant.type === 'partial' && variant.value !== '') {
|
||||
// Find partial arbitrary variants
|
||||
args = variant.value
|
||||
variant = variant.name
|
||||
} else if (variant.type === 'custom' && !context.variantMap.has(variant.raw)) {
|
||||
// Register arbitrary variants
|
||||
let fn = parseVariant(variant.value)
|
||||
|
||||
// Register arbitrary variants
|
||||
if (isArbitraryValue(variant) && !context.variantMap.has(variant)) {
|
||||
let selector = normalize(variant.slice(1, -1))
|
||||
|
||||
if (!isValidVariantFormatString(selector)) {
|
||||
if (!isValidVariantFormatString(variant.value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
let fn = parseVariant(selector)
|
||||
|
||||
let sort = Array.from(context.variantOrder.values()).pop() << 1n
|
||||
context.variantMap.set(variant, [[sort, fn]])
|
||||
context.variantOrder.set(variant, sort)
|
||||
context.variantMap.set(variant.raw, [[sort, fn]])
|
||||
context.variantOrder.set(variant.raw, sort)
|
||||
|
||||
variant = variant.raw
|
||||
}
|
||||
|
||||
if (context.variantMap.has(variant)) {
|
||||
let variantFunctionTuples = context.variantMap.get(variant)
|
||||
let result = []
|
||||
if (!context.variantMap.has(variant)) {
|
||||
return []
|
||||
}
|
||||
|
||||
for (let [meta, rule] of matches) {
|
||||
// Don't generate variants for user css
|
||||
if (meta.layer === 'user') {
|
||||
let variantFunctionTuples = context.variantMap.get(variant)
|
||||
let result = []
|
||||
|
||||
for (let [meta, rule] of matches) {
|
||||
// Don't generate variants for user css
|
||||
if (meta.layer === 'user') {
|
||||
continue
|
||||
}
|
||||
|
||||
let container = postcss.root({ nodes: [rule.clone()] })
|
||||
|
||||
for (let [variantSort, variantFunction] of variantFunctionTuples) {
|
||||
let clone = container.clone()
|
||||
let collectedFormats = []
|
||||
|
||||
let originals = new Map()
|
||||
|
||||
function prepareBackup() {
|
||||
if (originals.size > 0) return // Already prepared, chicken out
|
||||
clone.walkRules((rule) => originals.set(rule, rule.selector))
|
||||
}
|
||||
|
||||
function modifySelectors(modifierFunction) {
|
||||
prepareBackup()
|
||||
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({
|
||||
// Public API
|
||||
get container() {
|
||||
prepareBackup()
|
||||
return clone
|
||||
},
|
||||
separator: context.tailwindConfig.separator,
|
||||
modifySelectors,
|
||||
|
||||
// Private API for now
|
||||
wrap(wrapper) {
|
||||
let nodes = clone.nodes
|
||||
clone.removeAll()
|
||||
wrapper.append(nodes)
|
||||
clone.append(wrapper)
|
||||
},
|
||||
format(selectorFormat) {
|
||||
collectedFormats.push(selectorFormat)
|
||||
},
|
||||
args,
|
||||
})
|
||||
|
||||
if (typeof ruleWithVariant === 'string') {
|
||||
collectedFormats.push(ruleWithVariant)
|
||||
}
|
||||
|
||||
if (ruleWithVariant === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
let container = postcss.root({ nodes: [rule.clone()] })
|
||||
// We filled the `originals`, therefore we assume that somebody touched
|
||||
// `container` or `modifySelectors`. Let's see if they did, so that we
|
||||
// can restore the selectors, and collect the format strings.
|
||||
if (originals.size > 0) {
|
||||
clone.walkRules((rule) => {
|
||||
if (!originals.has(rule)) return
|
||||
let before = originals.get(rule)
|
||||
if (before === rule.selector) return // No mutation happened
|
||||
|
||||
for (let [variantSort, variantFunction] of variantFunctionTuples) {
|
||||
let clone = container.clone()
|
||||
let collectedFormats = []
|
||||
let modified = rule.selector
|
||||
|
||||
let originals = new Map()
|
||||
|
||||
function prepareBackup() {
|
||||
if (originals.size > 0) return // Already prepared, chicken out
|
||||
clone.walkRules((rule) => originals.set(rule, rule.selector))
|
||||
}
|
||||
|
||||
function modifySelectors(modifierFunction) {
|
||||
prepareBackup()
|
||||
clone.each((rule) => {
|
||||
if (rule.type !== 'rule') {
|
||||
return
|
||||
}
|
||||
|
||||
rule.selectors = rule.selectors.map((selector) => {
|
||||
return modifierFunction({
|
||||
get className() {
|
||||
return getClassNameFromSelector(selector)
|
||||
},
|
||||
selector,
|
||||
})
|
||||
// Rebuild the base selector, this is what plugin authors would do
|
||||
// as well. E.g.: `${variant}${separator}${className}`.
|
||||
// However, plugin authors probably also prepend or append certain
|
||||
// classes, pseudos, ids, ...
|
||||
let rebuiltBase = selectorParser((selectors) => {
|
||||
selectors.walkClasses((classNode) => {
|
||||
classNode.value = `${variant}${context.tailwindConfig.separator}${classNode.value}`
|
||||
})
|
||||
})
|
||||
}).processSync(before)
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
let ruleWithVariant = variantFunction({
|
||||
// Public API
|
||||
get container() {
|
||||
prepareBackup()
|
||||
return clone
|
||||
},
|
||||
separator: context.tailwindConfig.separator,
|
||||
modifySelectors,
|
||||
|
||||
// Private API for now
|
||||
wrap(wrapper) {
|
||||
let nodes = clone.nodes
|
||||
clone.removeAll()
|
||||
wrapper.append(nodes)
|
||||
clone.append(wrapper)
|
||||
},
|
||||
format(selectorFormat) {
|
||||
collectedFormats.push(selectorFormat)
|
||||
},
|
||||
args,
|
||||
// Now that we know the original selector, the new selector, and
|
||||
// the rebuild part in between, we can replace the part that plugin
|
||||
// authors need to rebuild with `&`, and eventually store it in the
|
||||
// collectedFormats. Similar to what `format('...')` would do.
|
||||
//
|
||||
// E.g.:
|
||||
// variant: foo
|
||||
// selector: .markdown > p
|
||||
// modified (by plugin): .foo .foo\\:markdown > p
|
||||
// rebuiltBase (internal): .foo\\:markdown > p
|
||||
// format: .foo &
|
||||
collectedFormats.push(modified.replace(rebuiltBase, '&'))
|
||||
rule.selector = before
|
||||
})
|
||||
|
||||
if (typeof ruleWithVariant === 'string') {
|
||||
collectedFormats.push(ruleWithVariant)
|
||||
}
|
||||
|
||||
if (ruleWithVariant === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
// We filled the `originals`, therefore we assume that somebody touched
|
||||
// `container` or `modifySelectors`. Let's see if they did, so that we
|
||||
// can restore the selectors, and collect the format strings.
|
||||
if (originals.size > 0) {
|
||||
clone.walkRules((rule) => {
|
||||
if (!originals.has(rule)) return
|
||||
let before = originals.get(rule)
|
||||
if (before === rule.selector) return // No mutation happened
|
||||
|
||||
let modified = rule.selector
|
||||
|
||||
// Rebuild the base selector, this is what plugin authors would do
|
||||
// as well. E.g.: `${variant}${separator}${className}`.
|
||||
// However, plugin authors probably also prepend or append certain
|
||||
// classes, pseudos, ids, ...
|
||||
let rebuiltBase = selectorParser((selectors) => {
|
||||
selectors.walkClasses((classNode) => {
|
||||
classNode.value = `${variant}${context.tailwindConfig.separator}${classNode.value}`
|
||||
})
|
||||
}).processSync(before)
|
||||
|
||||
// Now that we know the original selector, the new selector, and
|
||||
// the rebuild part in between, we can replace the part that plugin
|
||||
// authors need to rebuild with `&`, and eventually store it in the
|
||||
// collectedFormats. Similar to what `format('...')` would do.
|
||||
//
|
||||
// E.g.:
|
||||
// variant: foo
|
||||
// selector: .markdown > p
|
||||
// modified (by plugin): .foo .foo\\:markdown > p
|
||||
// rebuiltBase (internal): .foo\\:markdown > p
|
||||
// format: .foo &
|
||||
collectedFormats.push(modified.replace(rebuiltBase, '&'))
|
||||
rule.selector = before
|
||||
})
|
||||
}
|
||||
|
||||
// This tracks the originating layer for the variant
|
||||
// For example:
|
||||
// .sm:underline {} is a variant of something in the utilities layer
|
||||
// .sm:container {} is a variant of the container component
|
||||
clone.nodes[0].raws.tailwind = { ...clone.nodes[0].raws.tailwind, parentLayer: meta.layer }
|
||||
|
||||
let withOffset = [
|
||||
{
|
||||
...meta,
|
||||
sort: variantSort | meta.sort,
|
||||
collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
|
||||
},
|
||||
clone.nodes[0],
|
||||
]
|
||||
result.push(withOffset)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
// This tracks the originating layer for the variant
|
||||
// For example:
|
||||
// .sm:underline {} is a variant of something in the utilities layer
|
||||
// .sm:container {} is a variant of the container component
|
||||
clone.nodes[0].raws.tailwind = { ...clone.nodes[0].raws.tailwind, parentLayer: meta.layer }
|
||||
|
||||
let withOffset = [
|
||||
{
|
||||
...meta,
|
||||
sort: variantSort | meta.sort,
|
||||
collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
|
||||
},
|
||||
clone.nodes[0],
|
||||
]
|
||||
result.push(withOffset)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
return result
|
||||
}
|
||||
|
||||
function parseRules(rule, cache, options = {}) {
|
||||
|
|
@ -303,156 +268,76 @@ function parseRules(rule, cache, options = {}) {
|
|||
return [cache.get(rule), options]
|
||||
}
|
||||
|
||||
const IS_VALID_PROPERTY_NAME = /^[a-z_-]/
|
||||
|
||||
function isValidPropName(name) {
|
||||
return IS_VALID_PROPERTY_NAME.test(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} declaration
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function looksLikeUri(declaration) {
|
||||
// Quick bailout for obvious non-urls
|
||||
// This doesn't support schemes that don't use a leading // but that's unlikely to be a problem
|
||||
if (!declaration.includes('://')) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(declaration)
|
||||
return url.scheme !== '' && url.host !== ''
|
||||
} catch (err) {
|
||||
// Definitely not a valid url
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isParsableNode(node) {
|
||||
let isParsable = true
|
||||
|
||||
node.walkDecls((decl) => {
|
||||
if (!isParsableCssValue(decl.name, decl.value)) {
|
||||
isParsable = false
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
return isParsable
|
||||
}
|
||||
|
||||
function isParsableCssValue(property, value) {
|
||||
// We don't want to to treat [https://example.com] as a custom property
|
||||
// Even though, according to the CSS grammar, it's a totally valid CSS declaration
|
||||
// So we short-circuit here by checking if the custom property looks like a url
|
||||
if (looksLikeUri(`${property}:${value}`)) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
postcss.parse(`a{${property}:${value}}`).toResult()
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function extractArbitraryProperty(classCandidate, context) {
|
||||
let [, property, value] = classCandidate.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/) ?? []
|
||||
|
||||
if (value === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!isValidPropName(property)) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!isValidArbitraryValue(value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
let normalized = normalize(value)
|
||||
|
||||
if (!isParsableCssValue(property, normalized)) {
|
||||
return null
|
||||
}
|
||||
|
||||
function extractArbitraryProperty(parsed, context) {
|
||||
return [
|
||||
[
|
||||
{ sort: context.arbitraryPropertiesSort, layer: 'utilities' },
|
||||
() => ({
|
||||
[asClass(classCandidate)]: {
|
||||
[property]: normalized,
|
||||
[asClass(parsed.withoutVariants)]: {
|
||||
[parsed.name]: parsed.value,
|
||||
},
|
||||
}),
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
function* resolveMatchedPlugins(classCandidate, context) {
|
||||
if (context.candidateRuleMap.has(classCandidate)) {
|
||||
yield [context.candidateRuleMap.get(classCandidate), 'DEFAULT']
|
||||
/**
|
||||
*
|
||||
* @param {import('./candidate').Candidate} parsed
|
||||
* @param {any} context
|
||||
*/
|
||||
function* resolveMatchedPlugins(parsed, context) {
|
||||
if (parsed.type === 'custom') {
|
||||
yield [extractArbitraryProperty(parsed, context), 'DEFAULT']
|
||||
}
|
||||
|
||||
yield* (function* (arbitraryPropertyRule) {
|
||||
if (arbitraryPropertyRule !== null) {
|
||||
yield [arbitraryPropertyRule, 'DEFAULT']
|
||||
}
|
||||
})(extractArbitraryProperty(classCandidate, context))
|
||||
|
||||
let candidatePrefix = classCandidate
|
||||
let negative = false
|
||||
|
||||
const twConfigPrefix = context.tailwindConfig.prefix
|
||||
|
||||
const twConfigPrefixLen = twConfigPrefix.length
|
||||
|
||||
const hasMatchingPrefix =
|
||||
candidatePrefix.startsWith(twConfigPrefix) || candidatePrefix.startsWith(`-${twConfigPrefix}`)
|
||||
|
||||
if (candidatePrefix[twConfigPrefixLen] === '-' && hasMatchingPrefix) {
|
||||
negative = true
|
||||
candidatePrefix = twConfigPrefix + candidatePrefix.slice(twConfigPrefixLen + 1)
|
||||
if (parsed.type !== 'constrained') {
|
||||
return
|
||||
}
|
||||
|
||||
if (negative && context.candidateRuleMap.has(candidatePrefix)) {
|
||||
// TODO: Can this be dropped?
|
||||
let candidatePrefix = parsed.negative
|
||||
? parsed.prefix + parsed.withoutVariants.slice(parsed.prefix.length + 1)
|
||||
: parsed.withoutVariants
|
||||
|
||||
if (parsed.negative && context.candidateRuleMap.has(candidatePrefix)) {
|
||||
yield [context.candidateRuleMap.get(candidatePrefix), '-DEFAULT']
|
||||
}
|
||||
|
||||
for (let [prefix, modifier] of candidatePermutations(candidatePrefix)) {
|
||||
if (context.candidateRuleMap.has(prefix)) {
|
||||
yield [context.candidateRuleMap.get(prefix), negative ? `-${modifier}` : modifier]
|
||||
for (let plugin of parsed.plugins()) {
|
||||
let ruleGenerator =
|
||||
context.candidateRuleMap.get(parsed.prefix + plugin.plugin) ||
|
||||
context.candidateRuleMap.get(plugin.plugin)
|
||||
|
||||
if (ruleGenerator) {
|
||||
yield [ruleGenerator, plugin.value, plugin]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function splitWithSeparator(input, separator) {
|
||||
if (input === sharedState.NOT_ON_DEMAND) {
|
||||
return [sharedState.NOT_ON_DEMAND]
|
||||
}
|
||||
|
||||
return Array.from(splitAtTopLevelOnly(input, separator))
|
||||
}
|
||||
|
||||
function* recordCandidates(matches, classCandidate) {
|
||||
function* recordCandidates(matches, candidate) {
|
||||
for (const match of matches) {
|
||||
match[1].raws.tailwind = { ...match[1].raws.tailwind, classCandidate }
|
||||
match[1].raws.tailwind = {
|
||||
...match[1].raws.tailwind,
|
||||
classCandidate: candidate.withoutVariants,
|
||||
|
||||
// todo: rename?
|
||||
parsedCandidate: candidate,
|
||||
}
|
||||
|
||||
yield match
|
||||
}
|
||||
}
|
||||
|
||||
function* resolveMatches(candidate, context) {
|
||||
let separator = context.tailwindConfig.separator
|
||||
let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
|
||||
let important = false
|
||||
|
||||
if (classCandidate.startsWith('!')) {
|
||||
important = true
|
||||
classCandidate = classCandidate.slice(1)
|
||||
function* resolveMatches(candidateRaw, context) {
|
||||
for (const candidate of parseCandidate(candidateRaw, context)) {
|
||||
yield* resolveMatchesForCandidate(candidate, context)
|
||||
}
|
||||
}
|
||||
|
||||
function* resolveMatchesForCandidate(candidate, context) {
|
||||
let important = candidate.important
|
||||
let variants = candidate.variants
|
||||
|
||||
// TODO: Reintroduce this in ways that doesn't break on false positives
|
||||
// function sortAgainst(toSort, against) {
|
||||
|
|
@ -466,18 +351,20 @@ function* resolveMatches(candidate, context) {
|
|||
// throw new Error(`Class ${candidate} should be written as ${corrected}`)
|
||||
// }
|
||||
|
||||
for (let matchedPlugins of resolveMatchedPlugins(classCandidate, context)) {
|
||||
for (let matchedPlugins of resolveMatchedPlugins(candidate, context)) {
|
||||
let matches = []
|
||||
let typesByMatches = new Map()
|
||||
|
||||
let [plugins, modifier] = matchedPlugins
|
||||
let [plugins, modifier, candidatePlugin] = matchedPlugins
|
||||
let isOnlyPlugin = plugins.length === 1
|
||||
|
||||
for (let [sort, plugin] of plugins) {
|
||||
let matchesPerPlugin = []
|
||||
|
||||
if (typeof plugin === 'function') {
|
||||
for (let ruleSet of [].concat(plugin(modifier, { isOnlyPlugin }))) {
|
||||
for (let ruleSet of [].concat(
|
||||
plugin(modifier, { isOnlyPlugin, candidate, candidatePlugin })
|
||||
)) {
|
||||
let [rules, options] = parseRules(ruleSet, context.postCssNodeCache)
|
||||
for (let rule of rules) {
|
||||
matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
|
||||
|
|
@ -499,7 +386,7 @@ function* resolveMatches(candidate, context) {
|
|||
}
|
||||
}
|
||||
|
||||
if (isArbitraryValue(modifier)) {
|
||||
if (typeof modifier === 'object') {
|
||||
// When generated arbitrary values are ambiguous, we can't know
|
||||
// which to pick so don't generate any utilities for them
|
||||
if (matches.length > 1) {
|
||||
|
|
@ -542,16 +429,16 @@ function* resolveMatches(candidate, context) {
|
|||
.join('\n\n')
|
||||
|
||||
messages.push(
|
||||
` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
|
||||
` Use \`${candidate.raw.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log.warn([
|
||||
`The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
|
||||
`The class \`${candidate.raw}\` is ambiguous and matches multiple utilities.`,
|
||||
...messages,
|
||||
`If this is content and not a class, replace it with \`${candidate
|
||||
`If this is content and not a class, replace it with \`${candidate.raw
|
||||
.replace('[', '[')
|
||||
.replace(']', ']')}\` to silence this warning.`,
|
||||
])
|
||||
|
|
@ -562,19 +449,22 @@ function* resolveMatches(candidate, context) {
|
|||
}
|
||||
|
||||
matches = matches.flat()
|
||||
matches = Array.from(recordCandidates(matches, classCandidate))
|
||||
matches = Array.from(recordCandidates(matches, candidate))
|
||||
matches = applyPrefix(matches, context)
|
||||
|
||||
if (important) {
|
||||
matches = applyImportant(matches, classCandidate)
|
||||
matches = applyImportant(matches, candidate)
|
||||
}
|
||||
|
||||
for (let variant of variants) {
|
||||
matches = applyVariant(variant, matches, context)
|
||||
}
|
||||
|
||||
let foundMatches = false
|
||||
for (let match of matches) {
|
||||
match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate }
|
||||
foundMatches = true
|
||||
|
||||
match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate: candidate.raw }
|
||||
|
||||
// Apply final format selector
|
||||
if (match[0].collectedFormats) {
|
||||
|
|
@ -594,6 +484,25 @@ function* resolveMatches(candidate, context) {
|
|||
|
||||
yield match
|
||||
}
|
||||
|
||||
// We always want to emit matches for `DEFAULT`-ish values
|
||||
// so we'll continue matching in that case
|
||||
if (
|
||||
candidatePlugin &&
|
||||
(candidatePlugin.value === 'DEFAULT' || candidatePlugin.value === '-DEFAULT')
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
// If we've emitted matches we're done searching
|
||||
// This is because the order of matched values is as follows:
|
||||
// - Real values from the config
|
||||
// - Real values from the config accounting for modifiers
|
||||
// - Arbitrary values
|
||||
// - Arbitrary values accounting for modifiers
|
||||
if (foundMatches) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -665,8 +574,4 @@ function generateRules(candidates, context) {
|
|||
})
|
||||
}
|
||||
|
||||
function isArbitraryValue(input) {
|
||||
return input.startsWith('[') && input.endsWith(']')
|
||||
}
|
||||
|
||||
export { resolveMatches, generateRules }
|
||||
|
|
|
|||
|
|
@ -371,10 +371,17 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
|||
|
||||
classList.add([prefixedIdentifier, options])
|
||||
|
||||
function wrapped(modifier, { isOnlyPlugin }) {
|
||||
function wrapped(modifier, { isOnlyPlugin, candidate, candidatePlugin }) {
|
||||
let { type = 'any' } = options
|
||||
type = [].concat(type)
|
||||
let [value, coercedType] = coerceValue(type, modifier, options, tailwindConfig)
|
||||
let [value, coercedType] = coerceValue(
|
||||
type,
|
||||
modifier,
|
||||
options,
|
||||
tailwindConfig,
|
||||
candidate,
|
||||
candidatePlugin
|
||||
)
|
||||
|
||||
if (value === undefined) {
|
||||
return []
|
||||
|
|
@ -384,15 +391,36 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
|||
return []
|
||||
}
|
||||
|
||||
// TODO: Can we remove this?
|
||||
// This check is done in the candidate section
|
||||
if (!isValidArbitraryValue(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
let tempIdentifier = identifier
|
||||
|
||||
if (typeof modifier === 'object') {
|
||||
modifier = modifier.raw
|
||||
if (candidate.negative && !tempIdentifier.startsWith('-')) {
|
||||
tempIdentifier = `-${tempIdentifier}`
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove and use .className
|
||||
let alphaModifierValue = candidatePlugin?.modifiers[0]?.value
|
||||
if (alphaModifierValue !== undefined) {
|
||||
if (typeof alphaModifierValue === 'object') {
|
||||
alphaModifierValue = alphaModifierValue.raw
|
||||
}
|
||||
|
||||
modifier += `/${alphaModifierValue}`
|
||||
}
|
||||
|
||||
let ruleSets = []
|
||||
.concat(rule(value))
|
||||
.filter(Boolean)
|
||||
.map((declaration) => ({
|
||||
[nameClass(identifier, modifier)]: declaration,
|
||||
[nameClass(tempIdentifier, modifier)]: declaration,
|
||||
}))
|
||||
|
||||
return ruleSets
|
||||
|
|
@ -423,10 +451,17 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
|||
|
||||
classList.add([prefixedIdentifier, options])
|
||||
|
||||
function wrapped(modifier, { isOnlyPlugin }) {
|
||||
function wrapped(modifier, { isOnlyPlugin, candidate, candidatePlugin }) {
|
||||
let { type = 'any' } = options
|
||||
type = [].concat(type)
|
||||
let [value, coercedType] = coerceValue(type, modifier, options, tailwindConfig)
|
||||
let [value, coercedType] = coerceValue(
|
||||
type,
|
||||
modifier,
|
||||
options,
|
||||
tailwindConfig,
|
||||
candidate,
|
||||
candidatePlugin
|
||||
)
|
||||
|
||||
if (value === undefined) {
|
||||
return []
|
||||
|
|
@ -446,15 +481,26 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Can we remove this?
|
||||
// This check is done in the candidate section
|
||||
if (!isValidArbitraryValue(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
let tempIdentifier = identifier
|
||||
|
||||
if (typeof modifier === 'object') {
|
||||
modifier = modifier.raw
|
||||
if (candidate.negative && !tempIdentifier.startsWith('-')) {
|
||||
tempIdentifier = `-${tempIdentifier}`
|
||||
}
|
||||
}
|
||||
|
||||
let ruleSets = []
|
||||
.concat(rule(value))
|
||||
.filter(Boolean)
|
||||
.map((declaration) => ({
|
||||
[nameClass(identifier, modifier)]: declaration,
|
||||
[nameClass(tempIdentifier, modifier)]: declaration,
|
||||
}))
|
||||
|
||||
return ruleSets
|
||||
|
|
|
|||
58
src/util/candidatePermutations.js
Normal file
58
src/util/candidatePermutations.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// @ts-check
|
||||
|
||||
/** @typedef {import('../lib/candidate.d').Plugin} Plugin */
|
||||
|
||||
/**
|
||||
* Generate plugin permutations for a class candidate, like:
|
||||
*
|
||||
* ['ring-offset-blue', '100']
|
||||
* ['ring-offset', 'blue-100']
|
||||
* ['ring', 'offset-blue-100']
|
||||
* Example with dynamic classes:
|
||||
* ['grid-cols', '[[linename],1fr,auto]']
|
||||
* ['grid', 'cols-[[linename],1fr,auto]']
|
||||
*
|
||||
* @param {string} candidate
|
||||
* @param {boolean} negative
|
||||
* @return {Iterable<Plugin>}
|
||||
*/
|
||||
export function* candidatePermutations(candidate, negative = false) {
|
||||
let lastIndex = Infinity
|
||||
|
||||
yield { plugin: candidate, value: negative ? '-DEFAULT' : 'DEFAULT', modifiers: [] }
|
||||
|
||||
let SPECIALS = /[\[\]]/g
|
||||
let indexes = Array.from(candidate.matchAll(SPECIALS)).map((r) => r.index)
|
||||
let firstOpening = indexes[0] ?? -1
|
||||
let lastOpening = indexes[1] ?? -1
|
||||
|
||||
while (lastIndex >= 0) {
|
||||
let dashIdx
|
||||
|
||||
if (lastIndex === Infinity && candidate.endsWith(']')) {
|
||||
let bracketIdx = candidate.indexOf('[')
|
||||
|
||||
// If character before `[` isn't a dash or a slash, this isn't a dynamic class
|
||||
// eg. string[]
|
||||
dashIdx = ['-', '/'].includes(candidate[bracketIdx - 1]) ? bracketIdx - 1 : -1
|
||||
} else {
|
||||
dashIdx = candidate.lastIndexOf('-', lastIndex)
|
||||
}
|
||||
|
||||
if (dashIdx < 0) {
|
||||
break
|
||||
}
|
||||
|
||||
if (firstOpening < dashIdx && dashIdx < lastOpening) {
|
||||
lastIndex = dashIdx - 1
|
||||
continue
|
||||
}
|
||||
|
||||
let plugin = candidate.slice(0, dashIdx)
|
||||
let value = candidate.slice(dashIdx + 1)
|
||||
|
||||
yield { plugin, value: negative ? `-${value}` : value, modifiers: [] }
|
||||
|
||||
lastIndex = dashIdx - 1
|
||||
}
|
||||
}
|
||||
56
src/util/css-validation.js
Normal file
56
src/util/css-validation.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import postcss from 'postcss'
|
||||
|
||||
const IS_VALID_PROPERTY_NAME = /^[a-z_-]/
|
||||
|
||||
export function isValidPropName(name) {
|
||||
return IS_VALID_PROPERTY_NAME.test(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} declaration
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function looksLikeUri(declaration) {
|
||||
// Quick bailout for obvious non-urls
|
||||
// This doesn't support schemes that don't use a leading // but that's unlikely to be a problem
|
||||
if (!declaration.includes('://')) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(declaration)
|
||||
return url.scheme !== '' && url.host !== ''
|
||||
} catch (err) {
|
||||
// Definitely not a valid url
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function isParsableNode(node) {
|
||||
let isParsable = true
|
||||
|
||||
node.walkDecls((decl) => {
|
||||
if (!isParsableCssValue(decl.name, decl.value)) {
|
||||
isParsable = false
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
return isParsable
|
||||
}
|
||||
|
||||
export function isParsableCssValue(property, value) {
|
||||
// We don't want to to treat [https://example.com] as a custom property
|
||||
// Even though, according to the CSS grammar, it's a totally valid CSS declaration
|
||||
// So we short-circuit here by checking if the custom property looks like a url
|
||||
if (looksLikeUri(`${property}:${value}`)) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
postcss.parse(`a{${property}:${value}}`).toResult()
|
||||
return true
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ let cssFunctions = ['min', 'max', 'clamp', 'calc']
|
|||
// Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types
|
||||
|
||||
let COMMA = /,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count.
|
||||
let UNDERSCORE = /_(?![^(]*\))/g // Underscore separator that is not located between brackets. E.g.: `rgba(255,_255,_255)_black` these don't count.
|
||||
let SPACE = /[ ](?![^(]*\))/g // space separator that is not located between brackets. E.g.: `rgba(255, 255, 255) black` these don't count.
|
||||
|
||||
// This is not a data type, but rather a function that can normalize the
|
||||
// correct values.
|
||||
|
|
@ -63,7 +63,7 @@ export function number(value) {
|
|||
}
|
||||
|
||||
export function percentage(value) {
|
||||
return value.split(UNDERSCORE).every((part) => {
|
||||
return value.split(SPACE).every((part) => {
|
||||
return /%$/g.test(part) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?%`).test(part))
|
||||
})
|
||||
}
|
||||
|
|
@ -88,7 +88,7 @@ let lengthUnits = [
|
|||
]
|
||||
let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
|
||||
export function length(value) {
|
||||
return value.split(UNDERSCORE).every((part) => {
|
||||
return value.split(SPACE).every((part) => {
|
||||
return (
|
||||
part === '0' ||
|
||||
new RegExp(`${lengthUnitsPattern}$`).test(part) ||
|
||||
|
|
@ -117,7 +117,7 @@ export function shadow(value) {
|
|||
export function color(value) {
|
||||
let colors = 0
|
||||
|
||||
let result = value.split(UNDERSCORE).every((part) => {
|
||||
let result = value.split(SPACE).every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
|
|
@ -173,7 +173,7 @@ export function gradient(value) {
|
|||
let validPositions = new Set(['center', 'top', 'right', 'bottom', 'left'])
|
||||
export function position(value) {
|
||||
let positions = 0
|
||||
let result = value.split(UNDERSCORE).every((part) => {
|
||||
let result = value.split(SPACE).every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
|
|
|
|||
|
|
@ -42,13 +42,13 @@ export function finalizeSelector(format, { selector, candidate, context }) {
|
|||
// ╰──┴─────┴─────────────── We will split here
|
||||
//
|
||||
let splitter = new RegExp(`\\${separator}(?![^[]*\\])`)
|
||||
let base = candidate.split(splitter).pop()
|
||||
let base = candidate.raw.split(splitter).pop()
|
||||
|
||||
if (context?.tailwindConfig?.prefix) {
|
||||
format = prefixSelector(context.tailwindConfig.prefix, format)
|
||||
}
|
||||
|
||||
format = format.replace(PARENT, `.${escapeClassName(candidate)}`)
|
||||
format = format.replace(PARENT, `.${escapeClassName(candidate.raw)}`)
|
||||
|
||||
let formatAst = selectorParser().astSync(format)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ export function asClass(name) {
|
|||
}
|
||||
|
||||
export default function nameClass(classPrefix, key) {
|
||||
if (typeof key === 'object') {
|
||||
key = key.raw
|
||||
}
|
||||
|
||||
return asClass(formatClass(classPrefix, key))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import selectorParser from 'postcss-selector-parser'
|
|||
import escapeCommas from './escapeCommas'
|
||||
import { withAlphaValue } from './withAlphaVariable'
|
||||
import {
|
||||
normalize,
|
||||
length,
|
||||
number,
|
||||
percentage,
|
||||
|
|
@ -36,26 +35,18 @@ export function updateAllClasses(selectors, updateClass) {
|
|||
}
|
||||
|
||||
function resolveArbitraryValue(modifier, validate) {
|
||||
if (!isArbitraryValue(modifier)) {
|
||||
return undefined
|
||||
if (isArbitraryValue(modifier)) {
|
||||
if (!validate(modifier.value)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return modifier.value
|
||||
}
|
||||
|
||||
let value = modifier.slice(1, -1)
|
||||
|
||||
if (!validate(value)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return normalize(value)
|
||||
return undefined
|
||||
}
|
||||
|
||||
function asNegativeValue(modifier, lookup = {}, validate) {
|
||||
let positiveValue = lookup[modifier]
|
||||
|
||||
if (positiveValue !== undefined) {
|
||||
return negateValue(positiveValue)
|
||||
}
|
||||
|
||||
export function asValue(modifier, options = {}, { candidate, validate = () => true } = {}) {
|
||||
if (isArbitraryValue(modifier)) {
|
||||
let resolved = resolveArbitraryValue(modifier, validate)
|
||||
|
||||
|
|
@ -63,11 +54,13 @@ function asNegativeValue(modifier, lookup = {}, validate) {
|
|||
return undefined
|
||||
}
|
||||
|
||||
return negateValue(resolved)
|
||||
}
|
||||
}
|
||||
if (options.supportsNegativeValues && candidate.negative) {
|
||||
return negateValue(resolved)
|
||||
}
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
export function asValue(modifier, options = {}, { validate = () => true } = {}) {
|
||||
let value = options.values?.[modifier]
|
||||
|
||||
if (value !== undefined) {
|
||||
|
|
@ -75,53 +68,68 @@ export function asValue(modifier, options = {}, { validate = () => true } = {})
|
|||
}
|
||||
|
||||
if (options.supportsNegativeValues && modifier.startsWith('-')) {
|
||||
return asNegativeValue(modifier.slice(1), options.values, validate)
|
||||
let positiveValue = options.values[modifier.slice(1)]
|
||||
|
||||
if (positiveValue !== undefined) {
|
||||
return negateValue(positiveValue)
|
||||
}
|
||||
}
|
||||
|
||||
return resolveArbitraryValue(modifier, validate)
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string|import('../lib/candidate').Arbitrary} input
|
||||
* @returns {input is import('../lib/candidate').Arbitrary}
|
||||
*/
|
||||
function isArbitraryValue(input) {
|
||||
return input.startsWith('[') && input.endsWith(']')
|
||||
return typeof input === 'object'
|
||||
}
|
||||
|
||||
function splitAlpha(modifier) {
|
||||
let slashIdx = modifier.lastIndexOf('/')
|
||||
|
||||
if (slashIdx === -1 || slashIdx === modifier.length - 1) {
|
||||
return [modifier]
|
||||
}
|
||||
|
||||
return [modifier.slice(0, slashIdx), modifier.slice(slashIdx + 1)]
|
||||
}
|
||||
|
||||
export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) {
|
||||
if (options.values?.[modifier] !== undefined) {
|
||||
/**
|
||||
*
|
||||
* @param {string} modifier
|
||||
* @param {any} options
|
||||
* @param {object} param2
|
||||
* @param {any} param2.tailwindConfig
|
||||
* @param {import('../lib/candidate').Candidate} param2.candidate
|
||||
* @param {import('../lib/candidate').Plugin} param2.candidatePlugin
|
||||
* @returns
|
||||
*/
|
||||
export function asColor(
|
||||
modifier,
|
||||
options = {},
|
||||
{ tailwindConfig = {}, candidate, candidatePlugin } = {}
|
||||
) {
|
||||
if (options.values?.[modifier] !== undefined && candidatePlugin.modifiers.length === 0) {
|
||||
return options.values?.[modifier]
|
||||
}
|
||||
|
||||
let [color, alpha] = splitAlpha(modifier)
|
||||
let alpha = candidatePlugin.modifiers[0]?.value ?? undefined
|
||||
|
||||
if (typeof alpha === 'string') {
|
||||
alpha = tailwindConfig.theme?.opacity?.[alpha] ?? undefined
|
||||
|
||||
if (alpha === undefined) {
|
||||
return undefined
|
||||
}
|
||||
} else if (typeof alpha === 'object') {
|
||||
alpha = alpha.value
|
||||
}
|
||||
|
||||
if (alpha !== undefined) {
|
||||
let normalizedColor =
|
||||
options.values?.[color] ?? (isArbitraryValue(color) ? color.slice(1, -1) : undefined)
|
||||
options.values?.[modifier] ?? (isArbitraryValue(modifier) ? modifier.value : undefined)
|
||||
|
||||
if (normalizedColor === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (isArbitraryValue(alpha)) {
|
||||
return withAlphaValue(normalizedColor, alpha.slice(1, -1))
|
||||
}
|
||||
|
||||
if (tailwindConfig.theme?.opacity?.[alpha] === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return withAlphaValue(normalizedColor, tailwindConfig.theme.opacity[alpha])
|
||||
return withAlphaValue(normalizedColor, alpha)
|
||||
}
|
||||
|
||||
return asValue(modifier, options, { validate: validateColor })
|
||||
return asValue(modifier, options, { candidate, validate: validateColor })
|
||||
}
|
||||
|
||||
export function asLookupValue(modifier, options = {}) {
|
||||
|
|
@ -129,8 +137,8 @@ export function asLookupValue(modifier, options = {}) {
|
|||
}
|
||||
|
||||
function guess(validate) {
|
||||
return (modifier, options) => {
|
||||
return asValue(modifier, options, { validate })
|
||||
return (modifier, options, { candidate }) => {
|
||||
return asValue(modifier, options, { candidate, validate })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -154,37 +162,22 @@ let typeMap = {
|
|||
|
||||
let supportedTypes = Object.keys(typeMap)
|
||||
|
||||
function splitAtFirst(input, delim) {
|
||||
let idx = input.indexOf(delim)
|
||||
if (idx === -1) return [undefined, input]
|
||||
return [input.slice(0, idx), input.slice(idx + 1)]
|
||||
}
|
||||
|
||||
export function coerceValue(types, modifier, options, tailwindConfig) {
|
||||
export function coerceValue(types, modifier, options, tailwindConfig, candidate, candidatePlugin) {
|
||||
if (isArbitraryValue(modifier)) {
|
||||
let arbitraryValue = modifier.slice(1, -1)
|
||||
let [explicitType, value] = splitAtFirst(arbitraryValue, ':')
|
||||
|
||||
// It could be that this resolves to `url(https` which is not a valid
|
||||
// identifier. We currently only support "simple" words with dashes or
|
||||
// underscores. E.g.: family-name
|
||||
if (!/^[\w-_]+$/g.test(explicitType)) {
|
||||
value = arbitraryValue
|
||||
}
|
||||
|
||||
//
|
||||
else if (explicitType !== undefined && !supportedTypes.includes(explicitType)) {
|
||||
if (!supportedTypes.includes(modifier.dataType)) {
|
||||
return []
|
||||
}
|
||||
|
||||
if (value.length > 0 && supportedTypes.includes(explicitType)) {
|
||||
return [asValue(`[${value}]`, options), explicitType]
|
||||
// 'any' is the default data type for arbitrary values
|
||||
// TODO: We should probably guess them when parsing candidates
|
||||
if (modifier.dataType !== 'any') {
|
||||
return [asValue(modifier, options), modifier.dataType]
|
||||
}
|
||||
}
|
||||
|
||||
// Find first matching type
|
||||
for (let type of [].concat(types)) {
|
||||
let result = typeMap[type](modifier, options, { tailwindConfig })
|
||||
let result = typeMap[type](modifier, options, { tailwindConfig, candidate, candidatePlugin })
|
||||
if (result !== undefined) return [result, type]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -416,3 +416,78 @@ it('should correctly validate each part when checking for `percentage` data type
|
|||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('arbitrary-value looking things in the config are preferred', () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="bg-[#c0ffee]"></div>` }],
|
||||
corePlugins: { preflight: false, backgroundOpacity: false },
|
||||
plugins: [],
|
||||
theme: {
|
||||
colors: {
|
||||
'[#c0ffee]': '#f0000d',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.bg-\[\#c0ffee\] {
|
||||
background-color: #f0000d;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('modifier-value looking things in the config are preferred', () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="bg-red-500/50"></div>` }],
|
||||
corePlugins: { preflight: false, backgroundOpacity: false },
|
||||
plugins: [],
|
||||
theme: {
|
||||
colors: {
|
||||
'red-500/50': '#f0000d',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.bg-red-500\/50 {
|
||||
background-color: #f0000d;
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('modifier-value looking things in the config are preferred', () => {
|
||||
let config = {
|
||||
content: [{ raw: html`<div class="bg-[#ff0000]/[0.5]"></div>` }],
|
||||
corePlugins: { preflight: false, backgroundOpacity: false },
|
||||
plugins: [],
|
||||
theme: {
|
||||
colors: {
|
||||
'[#ff0000]': '#f0000d',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
let input = css`
|
||||
@tailwind utilities;
|
||||
`
|
||||
|
||||
return run(input, config).then((result) => {
|
||||
expect(result.css).toMatchFormattedCss(css`
|
||||
.bg-\[\#ff0000\]\/\[0\.5\] {
|
||||
background-color: rgb(240 0 13 / 0.5);
|
||||
}
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
567
tests/candidate.test.js
Normal file
567
tests/candidate.test.js
Normal file
|
|
@ -0,0 +1,567 @@
|
|||
const { parseCandidate } = require('../src/lib/candidate.js')
|
||||
|
||||
it.each([
|
||||
[
|
||||
'border',
|
||||
[
|
||||
{
|
||||
raw: 'border',
|
||||
className: '.border',
|
||||
withoutVariants: 'border',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [],
|
||||
type: 'constrained',
|
||||
plugins: [{ plugin: 'border', value: 'DEFAULT', modifiers: [] }],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'w-4',
|
||||
[
|
||||
{
|
||||
raw: 'w-4',
|
||||
className: '.w-4',
|
||||
withoutVariants: 'w-4',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'w-4', value: 'DEFAULT', modifiers: [] },
|
||||
{ plugin: 'w', value: '4', modifiers: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'-w-4',
|
||||
[
|
||||
{
|
||||
raw: '-w-4',
|
||||
className: '.-w-4',
|
||||
withoutVariants: '-w-4',
|
||||
important: false,
|
||||
negative: true,
|
||||
prefix: '',
|
||||
variants: [],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'w-4', value: '-DEFAULT', modifiers: [] },
|
||||
{ plugin: 'w', value: '-4', modifiers: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'sm:w',
|
||||
[
|
||||
{
|
||||
raw: 'sm:w',
|
||||
className: '.sm\\:w',
|
||||
withoutVariants: 'w',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [{ raw: 'sm', type: 'constrained', name: 'sm' }],
|
||||
type: 'constrained',
|
||||
plugins: [{ plugin: 'w', value: 'DEFAULT', modifiers: [] }],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'w-[2px]',
|
||||
[
|
||||
{
|
||||
raw: 'w-[2px]',
|
||||
className: '.w-\\[2px\\]',
|
||||
withoutVariants: 'w-[2px]',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'w-[2px]', value: 'DEFAULT', modifiers: [] },
|
||||
{ plugin: 'w', value: '[2px]', modifiers: [] },
|
||||
{
|
||||
plugin: 'w',
|
||||
value: {
|
||||
raw: '[2px]',
|
||||
value: '2px',
|
||||
dataType: 'any',
|
||||
},
|
||||
modifiers: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'-mt-[10px]',
|
||||
[
|
||||
{
|
||||
raw: '-mt-[10px]',
|
||||
className: '.-mt-\\[10px\\]',
|
||||
withoutVariants: '-mt-[10px]',
|
||||
important: false,
|
||||
negative: true,
|
||||
prefix: '',
|
||||
variants: [],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'mt-[10px]', value: '-DEFAULT', modifiers: [] },
|
||||
{ plugin: 'mt', value: '-[10px]', modifiers: [] },
|
||||
{
|
||||
plugin: 'mt',
|
||||
value: {
|
||||
raw: '[10px]',
|
||||
value: '10px',
|
||||
dataType: 'any',
|
||||
},
|
||||
modifiers: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'sm:w-[2px]',
|
||||
[
|
||||
{
|
||||
raw: 'sm:w-[2px]',
|
||||
className: '.sm\\:w-\\[2px\\]',
|
||||
withoutVariants: 'w-[2px]',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [{ raw: 'sm', type: 'constrained', name: 'sm' }],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'w-[2px]', value: 'DEFAULT', modifiers: [] },
|
||||
{ plugin: 'w', value: '[2px]', modifiers: [] },
|
||||
{
|
||||
plugin: 'w',
|
||||
value: {
|
||||
raw: '[2px]',
|
||||
value: '2px',
|
||||
dataType: 'any',
|
||||
},
|
||||
modifiers: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'sm:w-[length:2px]',
|
||||
[
|
||||
{
|
||||
raw: 'sm:w-[length:2px]',
|
||||
className: '.sm\\:w-\\[length\\:2px\\]',
|
||||
withoutVariants: 'w-[length:2px]',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [{ raw: 'sm', type: 'constrained', name: 'sm' }],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'w-[length:2px]', value: 'DEFAULT', modifiers: [] },
|
||||
{ plugin: 'w', value: '[length:2px]', modifiers: [] },
|
||||
{
|
||||
plugin: 'w',
|
||||
value: {
|
||||
raw: '[length:2px]',
|
||||
value: '2px',
|
||||
dataType: 'length',
|
||||
},
|
||||
modifiers: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'sm:w-[2px]/20',
|
||||
[
|
||||
{
|
||||
raw: 'sm:w-[2px]/20',
|
||||
className: '.sm\\:w-\\[2px\\]\\/20',
|
||||
withoutVariants: 'w-[2px]/20',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [{ raw: 'sm', type: 'constrained', name: 'sm' }],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'w-[2px]/20', value: 'DEFAULT', modifiers: [] },
|
||||
{ plugin: 'w', value: '[2px]/20', modifiers: [] },
|
||||
{
|
||||
plugin: 'w',
|
||||
value: '[2px]',
|
||||
modifiers: [{ raw: '20', value: '20' }],
|
||||
},
|
||||
{
|
||||
plugin: 'w',
|
||||
value: {
|
||||
raw: `[2px]`,
|
||||
value: '2px',
|
||||
dataType: 'any',
|
||||
},
|
||||
modifiers: [{ raw: '20', value: '20' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'bg-red-500/50',
|
||||
[
|
||||
{
|
||||
raw: 'bg-red-500/50',
|
||||
className: '.bg-red-500\\/50',
|
||||
withoutVariants: 'bg-red-500/50',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'bg-red-500/50', value: 'DEFAULT', modifiers: [] },
|
||||
{ plugin: 'bg-red', value: '500/50', modifiers: [] },
|
||||
{ plugin: 'bg-red', value: '500', modifiers: [{ raw: '50', value: '50' }] },
|
||||
{ plugin: 'bg', value: 'red-500/50', modifiers: [] },
|
||||
{ plugin: 'bg', value: 'red-500', modifiers: [{ raw: '50', value: '50' }] },
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'bg-[url(https://example.com/image.png)]',
|
||||
[
|
||||
{
|
||||
raw: 'bg-[url(https://example.com/image.png)]',
|
||||
className: '.bg-\\[url\\(https\\:\\/\\/example\\.com\\/image\\.png\\)\\]',
|
||||
withoutVariants: 'bg-[url(https://example.com/image.png)]',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'bg-[url(https://example.com/image.png)]', value: 'DEFAULT', modifiers: [] },
|
||||
{ plugin: 'bg', value: '[url(https://example.com/image.png)]', modifiers: [] },
|
||||
{
|
||||
plugin: 'bg',
|
||||
value: {
|
||||
raw: `[url(https://example.com/image.png)]`,
|
||||
value: 'url(https://example.com/image.png)',
|
||||
dataType: 'any',
|
||||
},
|
||||
modifiers: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'bg-[color:]',
|
||||
[
|
||||
{
|
||||
raw: 'bg-[color:]',
|
||||
className: '.bg-\\[color\\:\\]',
|
||||
withoutVariants: 'bg-[color:]',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'bg-[color:]', value: 'DEFAULT', modifiers: [] },
|
||||
{ plugin: 'bg', value: '[color:]', modifiers: [] },
|
||||
// [color:] is not a valid arbitrary value
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'-mt-[10px]',
|
||||
[
|
||||
{
|
||||
raw: '-mt-[10px]',
|
||||
className: '.-mt-\\[10px\\]',
|
||||
withoutVariants: '-mt-[10px]',
|
||||
important: false,
|
||||
negative: true,
|
||||
prefix: '',
|
||||
variants: [],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'mt-[10px]', value: '-DEFAULT', modifiers: [] },
|
||||
{ plugin: 'mt', value: '-[10px]', modifiers: [] },
|
||||
{
|
||||
plugin: 'mt',
|
||||
value: {
|
||||
raw: `[10px]`,
|
||||
value: '10px',
|
||||
dataType: 'any',
|
||||
},
|
||||
modifiers: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'sm:w-[2px]/[0.55]',
|
||||
[
|
||||
{
|
||||
raw: 'sm:w-[2px]/[0.55]',
|
||||
className: '.sm\\:w-\\[2px\\]\\/\\[0\\.55\\]',
|
||||
withoutVariants: 'w-[2px]/[0.55]',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [{ raw: 'sm', type: 'constrained', name: 'sm' }],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'w-[2px]/[0.55]', value: 'DEFAULT', modifiers: [] },
|
||||
{ plugin: 'w', value: '[2px]/[0.55]', modifiers: [] },
|
||||
{
|
||||
plugin: 'w',
|
||||
value: '[2px]',
|
||||
modifiers: [
|
||||
{
|
||||
raw: '[0.55]',
|
||||
value: {
|
||||
raw: `[0.55]`,
|
||||
value: '0.55',
|
||||
dataType: 'any',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
plugin: 'w',
|
||||
value: {
|
||||
raw: `[2px]`,
|
||||
value: '2px',
|
||||
dataType: 'any',
|
||||
},
|
||||
modifiers: [
|
||||
{
|
||||
raw: '[0.55]',
|
||||
value: {
|
||||
raw: `[0.55]`,
|
||||
value: '0.55',
|
||||
dataType: 'any',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'sm:w-[calc(0.5rem_+_1px)]',
|
||||
[
|
||||
{
|
||||
raw: 'sm:w-[calc(0.5rem_+_1px)]',
|
||||
className: '.sm\\:w-\\[calc\\(0\\.5rem_\\+_1px\\)\\]',
|
||||
withoutVariants: 'w-[calc(0.5rem_+_1px)]',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [{ raw: 'sm', type: 'constrained', name: 'sm' }],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{ plugin: 'w-[calc(0.5rem_+_1px)]', value: 'DEFAULT', modifiers: [] },
|
||||
{ plugin: 'w', value: '[calc(0.5rem_+_1px)]', modifiers: [] },
|
||||
{
|
||||
plugin: 'w',
|
||||
value: {
|
||||
raw: `[calc(0.5rem_+_1px)]`,
|
||||
value: 'calc(0.5rem + 1px)',
|
||||
dataType: 'any',
|
||||
},
|
||||
modifiers: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'[width:2px]',
|
||||
[
|
||||
{
|
||||
raw: '[width:2px]',
|
||||
className: '.\\[width\\:2px\\]',
|
||||
withoutVariants: '[width:2px]',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [],
|
||||
modifiers: [],
|
||||
type: 'custom',
|
||||
name: 'width',
|
||||
value: '2px',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'sm:[width:2px]',
|
||||
[
|
||||
{
|
||||
raw: 'sm:[width:2px]',
|
||||
className: '.sm\\:\\[width\\:2px\\]',
|
||||
withoutVariants: '[width:2px]',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [{ raw: 'sm', type: 'constrained', name: 'sm' }],
|
||||
modifiers: [],
|
||||
type: 'custom',
|
||||
name: 'width',
|
||||
value: '2px',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'sm:focus:[width:2px]',
|
||||
[
|
||||
{
|
||||
raw: 'sm:focus:[width:2px]',
|
||||
className: '.sm\\:focus\\:\\[width\\:2px\\]',
|
||||
withoutVariants: '[width:2px]',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [
|
||||
{ raw: 'focus', type: 'constrained', name: 'focus' },
|
||||
{ raw: 'sm', type: 'constrained', name: 'sm' },
|
||||
],
|
||||
modifiers: [],
|
||||
type: 'custom',
|
||||
name: 'width',
|
||||
value: '2px',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'sm:[@media(min-width:200px)]:[width:2px]',
|
||||
[
|
||||
{
|
||||
raw: 'sm:[@media(min-width:200px)]:[width:2px]',
|
||||
className: '.sm\\:\\[\\@media\\(min-width\\:200px\\)\\]\\:\\[width\\:2px\\]',
|
||||
withoutVariants: '[width:2px]',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [
|
||||
{
|
||||
type: 'custom',
|
||||
raw: '[@media(min-width:200px)]',
|
||||
value: '@media(min-width:200px)',
|
||||
dataType: 'any',
|
||||
},
|
||||
{ raw: 'sm', type: 'constrained', name: 'sm' },
|
||||
],
|
||||
modifiers: [],
|
||||
type: 'custom',
|
||||
name: 'width',
|
||||
value: '2px',
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'sm:group-[foo_bar]:underline',
|
||||
[
|
||||
{
|
||||
raw: 'sm:group-[foo_bar]:underline',
|
||||
className: '.sm\\:group-\\[foo_bar\\]\\:underline',
|
||||
withoutVariants: 'underline',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [
|
||||
{
|
||||
type: 'partial',
|
||||
raw: 'group-[foo_bar]',
|
||||
name: 'group',
|
||||
value: 'foo bar',
|
||||
dataType: 'any',
|
||||
},
|
||||
{ raw: 'sm', type: 'constrained', name: 'sm' },
|
||||
],
|
||||
type: 'constrained',
|
||||
plugins: [{ plugin: 'underline', value: 'DEFAULT', modifiers: [] }],
|
||||
},
|
||||
],
|
||||
],
|
||||
[
|
||||
'bg-[hsl(var(--foo),var(--bar),var(--baz))]/50',
|
||||
[
|
||||
{
|
||||
raw: 'bg-[hsl(var(--foo),var(--bar),var(--baz))]/50',
|
||||
className: '.bg-\\[hsl\\(var\\(--foo\\)\\2c var\\(--bar\\)\\2c var\\(--baz\\)\\)\\]\\/50',
|
||||
withoutVariants: 'bg-[hsl(var(--foo),var(--bar),var(--baz))]/50',
|
||||
important: false,
|
||||
negative: false,
|
||||
prefix: '',
|
||||
variants: [],
|
||||
type: 'constrained',
|
||||
plugins: [
|
||||
{
|
||||
plugin: 'bg-[hsl(var(--foo),var(--bar),var(--baz))]/50',
|
||||
value: 'DEFAULT',
|
||||
modifiers: [],
|
||||
},
|
||||
{
|
||||
plugin: 'bg',
|
||||
value: '[hsl(var(--foo),var(--bar),var(--baz))]/50',
|
||||
modifiers: [],
|
||||
},
|
||||
{
|
||||
plugin: 'bg',
|
||||
value: '[hsl(var(--foo),var(--bar),var(--baz))]',
|
||||
modifiers: [{ raw: '50', value: '50' }],
|
||||
},
|
||||
{
|
||||
plugin: 'bg',
|
||||
value: {
|
||||
raw: '[hsl(var(--foo),var(--bar),var(--baz))]',
|
||||
value: 'hsl(var(--foo),var(--bar),var(--baz))',
|
||||
dataType: 'any',
|
||||
},
|
||||
modifiers: [{ raw: '50', value: '50' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
// TODO: test for `[color:#ff0000]/50`?
|
||||
['[http://example.com]', []],
|
||||
])('should be possible to parse: "%s"', (input, expected) => {
|
||||
let actual = parseCandidate(input, {
|
||||
tailwindConfig: {
|
||||
separator: ':',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
// So we don't mutate the cached versions
|
||||
actual = actual.map(candidate => ({
|
||||
...candidate,
|
||||
}))
|
||||
|
||||
for (const candidate of actual) {
|
||||
if ('plugins' in candidate) {
|
||||
candidate.plugins = Array.from(candidate.plugins())
|
||||
}
|
||||
}
|
||||
|
||||
expect(actual).toEqual(expected)
|
||||
})
|
||||
|
|
@ -1,8 +1,11 @@
|
|||
import { parseCandidate } from '../src/lib/candidate'
|
||||
import { formatVariantSelector, finalizeSelector } from '../src/util/formatVariantSelector'
|
||||
|
||||
it('should be possible to add a simple variant to a simple selector', () => {
|
||||
let selector = '.text-center'
|
||||
let candidate = 'hover:text-center'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('hover:text-center', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = ['&:hover']
|
||||
|
||||
|
|
@ -13,7 +16,9 @@ it('should be possible to add a simple variant to a simple selector', () => {
|
|||
|
||||
it('should be possible to add a multiple simple variants to a simple selector', () => {
|
||||
let selector = '.text-center'
|
||||
let candidate = 'focus:hover:text-center'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('focus:hover:text-center', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = ['&:hover', '&:focus']
|
||||
|
||||
|
|
@ -24,7 +29,9 @@ it('should be possible to add a multiple simple variants to a simple selector',
|
|||
|
||||
it('should be possible to add a simple variant to a selector containing escaped parts', () => {
|
||||
let selector = '.bg-\\[rgba\\(0\\,0\\,0\\)\\]'
|
||||
let candidate = 'hover:bg-[rgba(0,0,0)]'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('hover:bg-[rgba(0,0,0)]', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = ['&:hover']
|
||||
|
||||
|
|
@ -35,7 +42,9 @@ it('should be possible to add a simple variant to a selector containing escaped
|
|||
|
||||
it('should be possible to add a simple variant to a selector containing escaped parts (escape is slightly different)', () => {
|
||||
let selector = '.bg-\\[rgba\\(0\\2c 0\\2c 0\\)\\]'
|
||||
let candidate = 'hover:bg-[rgba(0,0,0)]'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('hover:bg-[rgba(0,0,0)]', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = ['&:hover']
|
||||
|
||||
|
|
@ -46,7 +55,9 @@ it('should be possible to add a simple variant to a selector containing escaped
|
|||
|
||||
it('should be possible to add a simple variant to a more complex selector', () => {
|
||||
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
|
||||
let candidate = 'hover:space-x-4'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('hover:space-x-4', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = ['&:hover']
|
||||
|
||||
|
|
@ -57,7 +68,9 @@ it('should be possible to add a simple variant to a more complex selector', () =
|
|||
|
||||
it('should be possible to add multiple simple variants to a more complex selector', () => {
|
||||
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
|
||||
let candidate = 'disabled:focus:hover:space-x-4'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('disabled:focus:hover:space-x-4', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = ['&:hover', '&:focus', '&:disabled']
|
||||
|
||||
|
|
@ -68,7 +81,9 @@ it('should be possible to add multiple simple variants to a more complex selecto
|
|||
|
||||
it('should be possible to add a single merge variant to a simple selector', () => {
|
||||
let selector = '.text-center'
|
||||
let candidate = 'group-hover:text-center'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('group-hover:text-center', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = [':merge(.group):hover &']
|
||||
|
||||
|
|
@ -79,7 +94,9 @@ it('should be possible to add a single merge variant to a simple selector', () =
|
|||
|
||||
it('should be possible to add multiple merge variants to a simple selector', () => {
|
||||
let selector = '.text-center'
|
||||
let candidate = 'group-focus:group-hover:text-center'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('group-focus:group-hover:text-center', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = [':merge(.group):hover &', ':merge(.group):focus &']
|
||||
|
||||
|
|
@ -90,7 +107,9 @@ it('should be possible to add multiple merge variants to a simple selector', ()
|
|||
|
||||
it('should be possible to add a single merge variant to a more complex selector', () => {
|
||||
let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])'
|
||||
let candidate = 'group-hover:space-x-4'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('group-hover:space-x-4', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = [':merge(.group):hover &']
|
||||
|
||||
|
|
@ -101,7 +120,9 @@ it('should be possible to add a single merge variant to a more complex selector'
|
|||
|
||||
it('should be possible to add multiple merge variants to a more complex selector', () => {
|
||||
let selector = '.space-x-4 ~ :not([hidden]) ~ :not([hidden])'
|
||||
let candidate = 'group-focus:group-hover:space-x-4'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('group-focus:group-hover:space-x-4', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = [':merge(.group):hover &', ':merge(.group):focus &']
|
||||
|
||||
|
|
@ -112,7 +133,9 @@ it('should be possible to add multiple merge variants to a more complex selector
|
|||
|
||||
it('should be possible to add multiple unique merge variants to a simple selector', () => {
|
||||
let selector = '.text-center'
|
||||
let candidate = 'peer-focus:group-hover:text-center'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('peer-focus:group-hover:text-center', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = [':merge(.group):hover &', ':merge(.peer):focus ~ &']
|
||||
|
||||
|
|
@ -123,7 +146,9 @@ it('should be possible to add multiple unique merge variants to a simple selecto
|
|||
|
||||
it('should be possible to add multiple unique merge variants to a simple selector', () => {
|
||||
let selector = '.text-center'
|
||||
let candidate = 'group-hover:peer-focus:text-center'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('group-hover:peer-focus:text-center', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = [':merge(.peer):focus ~ &', ':merge(.group):hover &']
|
||||
|
||||
|
|
@ -149,7 +174,9 @@ it('should be possible to use multiple :merge() calls with different "arguments"
|
|||
|
||||
it('group hover and prose headings combination', () => {
|
||||
let selector = '.text-center'
|
||||
let candidate = 'group-hover:prose-headings:text-center'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('group-hover:prose-headings:text-center', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
let variants = [
|
||||
':where(&) :is(h1, h2, h3, h4)', // Prose Headings
|
||||
':merge(.group):hover &', // Group Hover
|
||||
|
|
@ -162,7 +189,9 @@ it('group hover and prose headings combination', () => {
|
|||
|
||||
it('group hover and prose headings combination flipped', () => {
|
||||
let selector = '.text-center'
|
||||
let candidate = 'prose-headings:group-hover:text-center'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('prose-headings:group-hover:text-center', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
let variants = [
|
||||
':merge(.group):hover &', // Group Hover
|
||||
':where(&) :is(h1, h2, h3, h4)', // Prose Headings
|
||||
|
|
@ -175,7 +204,11 @@ it('group hover and prose headings combination flipped', () => {
|
|||
|
||||
it('should be possible to handle a complex utility', () => {
|
||||
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
|
||||
let candidate = 'peer-disabled:peer-first-child:group-hover:group-focus:focus:hover:space-x-4'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('peer-disabled:peer-first-child:group-hover:group-focus:focus:hover:space-x-4', {
|
||||
tailwindConfig: { separator: ':' },
|
||||
})
|
||||
)[0]
|
||||
let variants = [
|
||||
'&:hover', // Hover
|
||||
'&:focus', // Focus
|
||||
|
|
@ -193,7 +226,9 @@ it('should be possible to handle a complex utility', () => {
|
|||
describe('real examples', () => {
|
||||
it('example a', () => {
|
||||
let selector = '.placeholder-red-500::placeholder'
|
||||
let candidate = 'hover:placeholder-red-500'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('hover:placeholder-red-500', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = ['&:hover']
|
||||
|
||||
|
|
@ -204,7 +239,9 @@ describe('real examples', () => {
|
|||
|
||||
it('example b', () => {
|
||||
let selector = '.space-x-4 > :not([hidden]) ~ :not([hidden])'
|
||||
let candidate = 'group-hover:hover:space-x-4'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('group-hover:hover:space-x-4', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = ['&:hover', ':merge(.group):hover &']
|
||||
|
||||
|
|
@ -215,7 +252,9 @@ describe('real examples', () => {
|
|||
|
||||
it('should work for group-hover and class dark mode combinations', () => {
|
||||
let selector = '.text-center'
|
||||
let candidate = 'dark:group-hover:text-center'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('dark:group-hover:text-center', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = [':merge(.group):hover &', '.dark &']
|
||||
|
||||
|
|
@ -226,7 +265,9 @@ describe('real examples', () => {
|
|||
|
||||
it('should work for group-hover and class dark mode combinations (reversed)', () => {
|
||||
let selector = '.text-center'
|
||||
let candidate = 'group-hover:dark:text-center'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('group-hover:dark:text-center', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = ['.dark &', ':merge(.group):hover &']
|
||||
|
||||
|
|
@ -238,7 +279,9 @@ describe('real examples', () => {
|
|||
describe('prose-headings', () => {
|
||||
it('should be possible to use hover:prose-headings:text-center', () => {
|
||||
let selector = '.text-center'
|
||||
let candidate = 'hover:prose-headings:text-center'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('hover:prose-headings:text-center', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = [':where(&) :is(h1, h2, h3, h4)', '&:hover']
|
||||
|
||||
|
|
@ -249,7 +292,9 @@ describe('real examples', () => {
|
|||
|
||||
it('should be possible to use prose-headings:hover:text-center', () => {
|
||||
let selector = '.text-center'
|
||||
let candidate = 'prose-headings:hover:text-center'
|
||||
let candidate = Array.from(
|
||||
parseCandidate('prose-headings:hover:text-center', { tailwindConfig: { separator: ':' } })
|
||||
)[0]
|
||||
|
||||
let variants = ['&:hover', ':where(&) :is(h1, h2, h3, h4)']
|
||||
|
||||
|
|
@ -276,7 +321,7 @@ describe('pseudo elements', () => {
|
|||
`('should translate "$before" into "$after"', ({ before, after }) => {
|
||||
let result = finalizeSelector(formatVariantSelector('&', before), {
|
||||
selector: '.a',
|
||||
candidate: 'a',
|
||||
candidate: Array.from(parseCandidate('a', { tailwindConfig: { separator: ':' } }))[0],
|
||||
})
|
||||
|
||||
expect(result).toEqual(after.replace('&', '.a'))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue