308 lines
8.0 KiB
JavaScript
308 lines
8.0 KiB
JavaScript
import escapeCommas from './escapeCommas'
|
|
import { withAlphaValue } from './withAlphaVariable'
|
|
import {
|
|
normalize,
|
|
length,
|
|
number,
|
|
percentage,
|
|
url,
|
|
color as validateColor,
|
|
genericName,
|
|
familyName,
|
|
image,
|
|
absoluteSize,
|
|
relativeSize,
|
|
position,
|
|
lineWidth,
|
|
shadow,
|
|
} from './dataTypes'
|
|
import negateValue from './negateValue'
|
|
import { backgroundSize } from './validateFormalSyntax'
|
|
import { flagEnabled } from '../featureFlags.js'
|
|
|
|
/**
|
|
* @param {import('postcss-selector-parser').Container} selectors
|
|
* @param {(className: string) => string} updateClass
|
|
* @returns {string}
|
|
*/
|
|
export function updateAllClasses(selectors, updateClass) {
|
|
selectors.walkClasses((sel) => {
|
|
sel.value = updateClass(sel.value)
|
|
|
|
if (sel.raws && sel.raws.value) {
|
|
sel.raws.value = escapeCommas(sel.raws.value)
|
|
}
|
|
})
|
|
}
|
|
|
|
function resolveArbitraryValue(modifier, validate) {
|
|
if (!isArbitraryValue(modifier)) {
|
|
return undefined
|
|
}
|
|
|
|
let value = modifier.slice(1, -1)
|
|
|
|
if (!validate(value)) {
|
|
return undefined
|
|
}
|
|
|
|
return normalize(value)
|
|
}
|
|
|
|
function asNegativeValue(modifier, lookup = {}, validate) {
|
|
let positiveValue = lookup[modifier]
|
|
|
|
if (positiveValue !== undefined) {
|
|
return negateValue(positiveValue)
|
|
}
|
|
|
|
if (isArbitraryValue(modifier)) {
|
|
let resolved = resolveArbitraryValue(modifier, validate)
|
|
|
|
if (resolved === undefined) {
|
|
return undefined
|
|
}
|
|
|
|
return negateValue(resolved)
|
|
}
|
|
}
|
|
|
|
export function asValue(modifier, options = {}, { validate = () => true } = {}) {
|
|
let value = options.values?.[modifier]
|
|
|
|
if (value !== undefined) {
|
|
return value
|
|
}
|
|
|
|
if (options.supportsNegativeValues && modifier.startsWith('-')) {
|
|
return asNegativeValue(modifier.slice(1), options.values, validate)
|
|
}
|
|
|
|
return resolveArbitraryValue(modifier, validate)
|
|
}
|
|
|
|
function isArbitraryValue(input) {
|
|
return input.startsWith('[') && input.endsWith(']')
|
|
}
|
|
|
|
function splitUtilityModifier(modifier) {
|
|
let slashIdx = modifier.lastIndexOf('/')
|
|
|
|
// If the `/` is inside an arbitrary, we want to find the previous one if any
|
|
// This logic probably isn't perfect but it should work for most cases
|
|
let arbitraryStartIdx = modifier.lastIndexOf('[', slashIdx)
|
|
let arbitraryEndIdx = modifier.indexOf(']', slashIdx)
|
|
|
|
let isNextToArbitrary = modifier[slashIdx - 1] === ']' || modifier[slashIdx + 1] === '['
|
|
|
|
// Backtrack to the previous `/` if the one we found was inside an arbitrary
|
|
if (!isNextToArbitrary) {
|
|
if (arbitraryStartIdx !== -1 && arbitraryEndIdx !== -1) {
|
|
if (arbitraryStartIdx < slashIdx && slashIdx < arbitraryEndIdx) {
|
|
slashIdx = modifier.lastIndexOf('/', arbitraryStartIdx)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (slashIdx === -1 || slashIdx === modifier.length - 1) {
|
|
return [modifier, undefined]
|
|
}
|
|
|
|
let arbitrary = isArbitraryValue(modifier)
|
|
|
|
// The modifier could be of the form `[foo]/[bar]`
|
|
// We want to handle this case properly
|
|
// without affecting `[foo/bar]`
|
|
if (arbitrary && !modifier.includes(']/[')) {
|
|
return [modifier, undefined]
|
|
}
|
|
|
|
return [modifier.slice(0, slashIdx), modifier.slice(slashIdx + 1)]
|
|
}
|
|
|
|
export function parseColorFormat(value) {
|
|
if (typeof value === 'string' && value.includes('<alpha-value>')) {
|
|
let oldValue = value
|
|
|
|
return ({ opacityValue = 1 }) => oldValue.replace(/<alpha-value>/g, opacityValue)
|
|
}
|
|
|
|
return value
|
|
}
|
|
|
|
function unwrapArbitraryModifier(modifier) {
|
|
return normalize(modifier.slice(1, -1))
|
|
}
|
|
|
|
export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) {
|
|
if (options.values?.[modifier] !== undefined) {
|
|
return parseColorFormat(options.values?.[modifier])
|
|
}
|
|
|
|
// TODO: Hoist this up to getMatchingTypes or something
|
|
// We do this here because we need the alpha value (if any)
|
|
let [color, alpha] = splitUtilityModifier(modifier)
|
|
|
|
if (alpha !== undefined) {
|
|
let normalizedColor =
|
|
options.values?.[color] ?? (isArbitraryValue(color) ? color.slice(1, -1) : undefined)
|
|
|
|
if (normalizedColor === undefined) {
|
|
return undefined
|
|
}
|
|
|
|
normalizedColor = parseColorFormat(normalizedColor)
|
|
|
|
if (isArbitraryValue(alpha)) {
|
|
return withAlphaValue(normalizedColor, unwrapArbitraryModifier(alpha))
|
|
}
|
|
|
|
if (tailwindConfig.theme?.opacity?.[alpha] === undefined) {
|
|
return undefined
|
|
}
|
|
|
|
return withAlphaValue(normalizedColor, tailwindConfig.theme.opacity[alpha])
|
|
}
|
|
|
|
return asValue(modifier, options, { validate: validateColor })
|
|
}
|
|
|
|
export function asLookupValue(modifier, options = {}) {
|
|
return options.values?.[modifier]
|
|
}
|
|
|
|
function guess(validate) {
|
|
return (modifier, options) => {
|
|
return asValue(modifier, options, { validate })
|
|
}
|
|
}
|
|
|
|
export let typeMap = {
|
|
any: asValue,
|
|
color: asColor,
|
|
url: guess(url),
|
|
image: guess(image),
|
|
length: guess(length),
|
|
percentage: guess(percentage),
|
|
position: guess(position),
|
|
lookup: asLookupValue,
|
|
'generic-name': guess(genericName),
|
|
'family-name': guess(familyName),
|
|
number: guess(number),
|
|
'line-width': guess(lineWidth),
|
|
'absolute-size': guess(absoluteSize),
|
|
'relative-size': guess(relativeSize),
|
|
shadow: guess(shadow),
|
|
size: guess(backgroundSize),
|
|
}
|
|
|
|
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) {
|
|
if (options.values && modifier in options.values) {
|
|
for (let { type } of types ?? []) {
|
|
let result = typeMap[type](modifier, options, {
|
|
tailwindConfig,
|
|
})
|
|
|
|
if (result === undefined) {
|
|
continue
|
|
}
|
|
|
|
return [result, type, null]
|
|
}
|
|
}
|
|
|
|
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)) {
|
|
return []
|
|
}
|
|
|
|
if (value.length > 0 && supportedTypes.includes(explicitType)) {
|
|
return [asValue(`[${value}]`, options), explicitType, null]
|
|
}
|
|
}
|
|
|
|
let matches = getMatchingTypes(types, modifier, options, tailwindConfig)
|
|
|
|
// Find first matching type
|
|
for (let match of matches) {
|
|
return match
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {{type: string}[]} types
|
|
* @param {string} rawModifier
|
|
* @param {any} options
|
|
* @param {any} tailwindConfig
|
|
* @returns {Iterator<[value: string, type: string, modifier: string | null]>}
|
|
*/
|
|
export function* getMatchingTypes(types, rawModifier, options, tailwindConfig) {
|
|
let modifiersEnabled = flagEnabled(tailwindConfig, 'generalizedModifiers')
|
|
|
|
let [modifier, utilityModifier] = splitUtilityModifier(rawModifier)
|
|
|
|
let canUseUtilityModifier =
|
|
modifiersEnabled &&
|
|
options.modifiers != null &&
|
|
(options.modifiers === 'any' ||
|
|
(typeof options.modifiers === 'object' &&
|
|
((utilityModifier && isArbitraryValue(utilityModifier)) ||
|
|
utilityModifier in options.modifiers)))
|
|
|
|
if (!canUseUtilityModifier) {
|
|
modifier = rawModifier
|
|
utilityModifier = undefined
|
|
}
|
|
|
|
if (utilityModifier !== undefined && modifier === '') {
|
|
modifier = 'DEFAULT'
|
|
}
|
|
|
|
// Check the full value first
|
|
// TODO: Move to asValue… somehow
|
|
if (utilityModifier !== undefined) {
|
|
if (typeof options.modifiers === 'object') {
|
|
let configValue = options.modifiers?.[utilityModifier] ?? null
|
|
if (configValue !== null) {
|
|
utilityModifier = configValue
|
|
} else if (isArbitraryValue(utilityModifier)) {
|
|
utilityModifier = unwrapArbitraryModifier(utilityModifier)
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let { type } of types ?? []) {
|
|
let result = typeMap[type](modifier, options, {
|
|
tailwindConfig,
|
|
})
|
|
|
|
if (result === undefined) {
|
|
continue
|
|
}
|
|
|
|
yield [result, type, utilityModifier ?? null]
|
|
}
|
|
}
|