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('')) { let oldValue = value return ({ opacityValue = 1 }) => oldValue.replace(//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] } }