- Add npm scripts for CSS/JS compilation (build:css, build:js, build) - Create PowerShell build automation script - Document development workflow in README - Add troubleshooting guide for build issues - Specify proper file structure and compilation process Supports Tailwind CSS v3.4.0 and esbuild bundling with source maps.
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]
|
|
}
|
|
}
|