tailwindcss/src/util/pluginUtils.js

306 lines
7.4 KiB
JavaScript
Raw Normal View History

import selectorParser from 'postcss-selector-parser'
import escapeCommas from './escapeCommas'
import { withAlphaValue } from './withAlphaVariable'
import isKeyframeRule from './isKeyframeRule'
import { parseColor } from './color'
2021-06-04 10:32:49 -04:00
export function applyPseudoToMarker(selector, marker, state, join) {
let states = [state]
let markerIdx = selector.indexOf(marker + ':')
if (markerIdx !== -1) {
let existingMarker = selector.slice(markerIdx, selector.indexOf(' ', markerIdx))
states = states.concat(
selector.slice(markerIdx + marker.length + 1, existingMarker.length).split(':')
)
selector = selector.replace(existingMarker, '')
}
return join(`${[marker, ...states].join(':')}`, selector)
}
export function updateAllClasses(selectors, updateClass) {
let parser = selectorParser((selectors) => {
selectors.walkClasses((sel) => {
let updatedClass = updateClass(sel.value, {
withPseudo(className, pseudo) {
sel.parent.insertAfter(sel, selectorParser.pseudo({ value: `${pseudo}` }))
return className
},
})
sel.value = updatedClass
if (sel.raws && sel.raws.value) {
sel.raws.value = escapeCommas(sel.raws.value)
}
})
})
let result = parser.processSync(selectors)
return result
}
export function updateLastClasses(selectors, updateClass) {
let parser = selectorParser((selectors) => {
selectors.each((sel) => {
let lastClass = sel.filter(({ type }) => type === 'class').pop()
if (lastClass === undefined) {
return
}
let updatedClass = updateClass(lastClass.value, {
withPseudo(className, pseudo) {
lastClass.parent.insertAfter(lastClass, selectorParser.pseudo({ value: `${pseudo}` }))
return className
},
})
lastClass.value = updatedClass
if (lastClass.raws && lastClass.raws.value) {
lastClass.raws.value = escapeCommas(lastClass.raws.value)
}
})
})
let result = parser.processSync(selectors)
return result
}
function splitByNotEscapedCommas(str) {
let chunks = []
let currentChunk = ''
for (let i = 0; i < str.length; i++) {
if (str[i] === ',' && str[i - 1] !== '\\') {
chunks.push(currentChunk)
currentChunk = ''
} else {
currentChunk += str[i]
}
}
chunks.push(currentChunk)
return chunks
}
export function transformAllSelectors(transformSelector, { wrap, withRule } = {}) {
return ({ container }) => {
container.walkRules((rule) => {
if (isKeyframeRule(rule)) {
return rule
}
let transformed = splitByNotEscapedCommas(rule.selector).map(transformSelector).join(',')
rule.selector = transformed
if (withRule) {
withRule(rule)
}
return rule
})
if (wrap) {
let wrapper = wrap()
let nodes = container.nodes
container.removeAll()
wrapper.append(nodes)
container.append(wrapper)
}
}
}
export function transformAllClasses(transformClass, { wrap, withRule } = {}) {
return ({ container }) => {
container.walkRules((rule) => {
let selector = rule.selector
let variantSelector = updateAllClasses(selector, transformClass)
rule.selector = variantSelector
if (withRule) {
withRule(rule)
}
return rule
})
if (wrap) {
let wrapper = wrap()
let nodes = container.nodes
container.removeAll()
wrapper.append(nodes)
container.append(wrapper)
}
}
}
export function transformLastClasses(transformClass, { wrap, withRule } = {}) {
return ({ container }) => {
container.walkRules((rule) => {
let selector = rule.selector
let variantSelector = updateLastClasses(selector, transformClass)
rule.selector = variantSelector
if (withRule) {
withRule(rule)
}
return rule
})
if (wrap) {
let wrapper = wrap()
let nodes = container.nodes
container.removeAll()
wrapper.append(nodes)
container.append(wrapper)
}
}
}
export function asValue(modifier, lookup = {}, { validate = () => true } = {}) {
let value = lookup[modifier]
if (value !== undefined) {
return value
}
if (modifier[0] !== '[' || modifier[modifier.length - 1] !== ']') {
return undefined
}
value = modifier.slice(1, -1)
if (!validate(value)) {
return undefined
}
// convert `_` to ` `, escept for escaped underscores `\_`
value = value
.replace(/([^\\])_/g, '$1 ')
.replace(/^_/g, ' ')
.replace(/\\_/g, '_')
// Keep raw strings if it starts with `url(`
if (value.startsWith('url(')) return value
// add spaces around operators inside calc() that do not follow an operator or (
return value.replace(
/(-?\d*\.?\d(?!\b-.+[,)](?![^+\-/*])\D)(?:%|[a-z]+)?|\))([+\-/*])/g,
'$1 $2 '
)
}
export function asUnit(modifier, units, lookup = {}) {
return asValue(modifier, lookup, {
validate: (value) => {
let unitsPattern = `(?:${units.join('|')})`
return (
new RegExp(`${unitsPattern}$`).test(value) ||
new RegExp(`^calc\\(.+?${unitsPattern}`).test(value)
)
},
})
}
function isArbitraryValue(input) {
return input.startsWith('[') && input.endsWith(']')
}
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, lookup = {}, tailwindConfig = {}) {
if (lookup[modifier] !== undefined) {
return lookup[modifier]
}
let [color, alpha] = splitAlpha(modifier)
if (lookup[color] !== undefined) {
if (isArbitraryValue(alpha)) {
return withAlphaValue(lookup[color], alpha.slice(1, -1))
}
if (tailwindConfig.theme?.opacity?.[alpha] === undefined) {
return undefined
}
return withAlphaValue(lookup[color], tailwindConfig.theme.opacity[alpha])
}
return asValue(modifier, lookup, {
validate: (value) => parseColor(value) !== null,
})
}
export function asAngle(modifier, lookup = {}) {
return asUnit(modifier, ['deg', 'grad', 'rad', 'turn'], lookup)
}
export function asURL(modifier, lookup = {}) {
return asValue(modifier, lookup, {
validate: (value) => value.startsWith('url('),
})
}
export function asLength(modifier, lookup = {}) {
return asUnit(
modifier,
[
'cm',
'mm',
'Q',
'in',
'pc',
'pt',
'px',
'em',
'ex',
'ch',
'rem',
'lh',
'vw',
'vh',
'vmin',
'vmax',
'%',
],
lookup
)
}
export function asLookupValue(modifier, lookup = {}) {
return lookup[modifier]
}
let typeMap = {
any: asValue,
color: asColor,
angle: asAngle,
length: asLength,
url: asURL,
lookup: asLookupValue,
}
function splitAtFirst(input, delim) {
return (([first, ...rest]) => [first, rest.join(delim)])(input.split(delim))
}
export function coerceValue(type, modifier, values, tailwindConfig) {
let [scaleType, arbitraryType = scaleType] = [].concat(type)
if (isArbitraryValue(modifier)) {
let [explicitType, value] = splitAtFirst(modifier.slice(1, -1), ':')
if (value.length > 0 && Object.keys(typeMap).includes(explicitType)) {
return [asValue(`[${value}]`, values, tailwindConfig), explicitType]
}
return [typeMap[arbitraryType](modifier, values, tailwindConfig), arbitraryType]
}
return [typeMap[scaleType](modifier, values, tailwindConfig), scaleType]
}