Initial commit
This commit is contained in:
+3
@@ -0,0 +1,3 @@
|
||||
export default function bigSign(bigIntValue) {
|
||||
return (bigIntValue > 0n) - (bigIntValue < 0n)
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
export default function buildMediaQuery(screens) {
|
||||
screens = Array.isArray(screens) ? screens : [screens]
|
||||
|
||||
return screens
|
||||
.map((screen) =>
|
||||
screen.values.map((screen) => {
|
||||
if (screen.raw !== undefined) {
|
||||
return screen.raw
|
||||
}
|
||||
|
||||
return [
|
||||
screen.min && `(min-width: ${screen.min})`,
|
||||
screen.max && `(max-width: ${screen.max})`,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' and ')
|
||||
})
|
||||
)
|
||||
.join(', ')
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
export function cloneDeep(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((child) => cloneDeep(child))
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cloneDeep(v)]))
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
export default function cloneNodes(nodes, source = undefined, raws = undefined) {
|
||||
return nodes.map((node) => {
|
||||
let cloned = node.clone()
|
||||
|
||||
// We always want override the source map
|
||||
// except when explicitly told not to
|
||||
let shouldOverwriteSource = node.raws.tailwind?.preserveSource !== true || !cloned.source
|
||||
|
||||
if (source !== undefined && shouldOverwriteSource) {
|
||||
cloned.source = source
|
||||
|
||||
if ('walk' in cloned) {
|
||||
cloned.walk((child) => {
|
||||
child.source = source
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (raws !== undefined) {
|
||||
cloned.raws.tailwind = {
|
||||
...cloned.raws.tailwind,
|
||||
...raws,
|
||||
}
|
||||
}
|
||||
|
||||
return cloned
|
||||
})
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import namedColors from 'color-name'
|
||||
|
||||
let HEX = /^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})?$/i
|
||||
let SHORT_HEX = /^#([a-f\d])([a-f\d])([a-f\d])([a-f\d])?$/i
|
||||
let VALUE = /(?:\d+|\d*\.\d+)%?/
|
||||
let SEP = /(?:\s*,\s*|\s+)/
|
||||
let ALPHA_SEP = /\s*[,/]\s*/
|
||||
let CUSTOM_PROPERTY = /var\(--(?:[^ )]*?)\)/
|
||||
|
||||
let RGB = new RegExp(
|
||||
`^(rgb)a?\\(\\s*(${VALUE.source}|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$`
|
||||
)
|
||||
let HSL = new RegExp(
|
||||
`^(hsl)a?\\(\\s*((?:${VALUE.source})(?:deg|rad|grad|turn)?|${CUSTOM_PROPERTY.source})(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?(?:${ALPHA_SEP.source}(${VALUE.source}|${CUSTOM_PROPERTY.source}))?\\s*\\)$`
|
||||
)
|
||||
|
||||
// In "loose" mode the color may contain fewer than 3 parts, as long as at least
|
||||
// one of the parts is variable.
|
||||
export function parseColor(value, { loose = false } = {}) {
|
||||
if (typeof value !== 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
value = value.trim()
|
||||
if (value === 'transparent') {
|
||||
return { mode: 'rgb', color: ['0', '0', '0'], alpha: '0' }
|
||||
}
|
||||
|
||||
if (value in namedColors) {
|
||||
return { mode: 'rgb', color: namedColors[value].map((v) => v.toString()) }
|
||||
}
|
||||
|
||||
let hex = value
|
||||
.replace(SHORT_HEX, (_, r, g, b, a) => ['#', r, r, g, g, b, b, a ? a + a : ''].join(''))
|
||||
.match(HEX)
|
||||
|
||||
if (hex !== null) {
|
||||
return {
|
||||
mode: 'rgb',
|
||||
color: [parseInt(hex[1], 16), parseInt(hex[2], 16), parseInt(hex[3], 16)].map((v) =>
|
||||
v.toString()
|
||||
),
|
||||
alpha: hex[4] ? (parseInt(hex[4], 16) / 255).toString() : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
let match = value.match(RGB) ?? value.match(HSL)
|
||||
|
||||
if (match === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
let color = [match[2], match[3], match[4]].filter(Boolean).map((v) => v.toString())
|
||||
|
||||
if (!loose && color.length !== 3) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (color.length < 3 && !color.some((part) => /^var\(.*?\)$/.test(part))) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
mode: match[1],
|
||||
color,
|
||||
alpha: match[5]?.toString?.(),
|
||||
}
|
||||
}
|
||||
|
||||
export function formatColor({ mode, color, alpha }) {
|
||||
let hasAlpha = alpha !== undefined
|
||||
return `${mode}(${color.join(' ')}${hasAlpha ? ` / ${alpha}` : ''})`
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
export default function (pluginConfig, plugins) {
|
||||
if (pluginConfig === undefined) {
|
||||
return plugins
|
||||
}
|
||||
|
||||
const pluginNames = Array.isArray(pluginConfig)
|
||||
? pluginConfig
|
||||
: [
|
||||
...new Set(
|
||||
plugins
|
||||
.filter((pluginName) => {
|
||||
return pluginConfig !== false && pluginConfig[pluginName] !== false
|
||||
})
|
||||
.concat(
|
||||
Object.keys(pluginConfig).filter((pluginName) => {
|
||||
return pluginConfig[pluginName] !== false
|
||||
})
|
||||
)
|
||||
),
|
||||
]
|
||||
|
||||
return pluginNames
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
function createPlugin(plugin, config) {
|
||||
return {
|
||||
handler: plugin,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
createPlugin.withOptions = function (pluginFunction, configFunction = () => ({})) {
|
||||
const optionsFunction = function (options) {
|
||||
return {
|
||||
__options: options,
|
||||
handler: pluginFunction(options),
|
||||
config: configFunction(options),
|
||||
}
|
||||
}
|
||||
|
||||
optionsFunction.__isOptionsFunction = true
|
||||
|
||||
// Expose plugin dependencies so that `object-hash` returns a different
|
||||
// value if anything here changes, to ensure a rebuild is triggered.
|
||||
optionsFunction.__pluginFunction = pluginFunction
|
||||
optionsFunction.__configFunction = configFunction
|
||||
|
||||
return optionsFunction
|
||||
}
|
||||
|
||||
export default createPlugin
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import transformThemeValue from './transformThemeValue'
|
||||
|
||||
export default function createUtilityPlugin(
|
||||
themeKey,
|
||||
utilityVariations = [[themeKey, [themeKey]]],
|
||||
{ filterDefault = false, ...options } = {}
|
||||
) {
|
||||
let transformValue = transformThemeValue(themeKey)
|
||||
return function ({ matchUtilities, theme }) {
|
||||
for (let utilityVariation of utilityVariations) {
|
||||
let group = Array.isArray(utilityVariation[0]) ? utilityVariation : [utilityVariation]
|
||||
|
||||
matchUtilities(
|
||||
group.reduce((obj, [classPrefix, properties]) => {
|
||||
return Object.assign(obj, {
|
||||
[classPrefix]: (value) => {
|
||||
return properties.reduce((obj, name) => {
|
||||
if (Array.isArray(name)) {
|
||||
return Object.assign(obj, { [name[0]]: name[1] })
|
||||
}
|
||||
return Object.assign(obj, { [name]: transformValue(value) })
|
||||
}, {})
|
||||
},
|
||||
})
|
||||
}, {}),
|
||||
{
|
||||
...options,
|
||||
values: filterDefault
|
||||
? Object.fromEntries(
|
||||
Object.entries(theme(themeKey) ?? {}).filter(([modifier]) => modifier !== 'DEFAULT')
|
||||
)
|
||||
: theme(themeKey),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
import { parseColor } from './color'
|
||||
import { parseBoxShadowValue } from './parseBoxShadowValue'
|
||||
|
||||
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.
|
||||
|
||||
// This is not a data type, but rather a function that can normalize the
|
||||
// correct values.
|
||||
export function normalize(value, isRoot = true) {
|
||||
// Keep raw strings if it starts with `url(`
|
||||
if (value.includes('url(')) {
|
||||
return value
|
||||
.split(/(url\(.*?\))/g)
|
||||
.filter(Boolean)
|
||||
.map((part) => {
|
||||
if (/^url\(.*?\)$/.test(part)) {
|
||||
return part
|
||||
}
|
||||
|
||||
return normalize(part, false)
|
||||
})
|
||||
.join('')
|
||||
}
|
||||
|
||||
// Convert `_` to ` `, except for escaped underscores `\_`
|
||||
value = value
|
||||
.replace(
|
||||
/([^\\])_+/g,
|
||||
(fullMatch, characterBefore) => characterBefore + ' '.repeat(fullMatch.length - 1)
|
||||
)
|
||||
.replace(/^_/g, ' ')
|
||||
.replace(/\\_/g, '_')
|
||||
|
||||
// Remove leftover whitespace
|
||||
if (isRoot) {
|
||||
value = value.trim()
|
||||
}
|
||||
|
||||
// Add spaces around operators inside math functions like calc() that do not follow an operator
|
||||
// or '('.
|
||||
value = value.replace(/(calc|min|max|clamp)\(.+\)/g, (match) => {
|
||||
return match.replace(
|
||||
/(-?\d*\.?\d(?!\b-.+[,)](?![^+\-/*])\D)(?:%|[a-z]+)?|\))([+\-/*])/g,
|
||||
'$1 $2 '
|
||||
)
|
||||
})
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function url(value) {
|
||||
return value.startsWith('url(')
|
||||
}
|
||||
|
||||
export function number(value) {
|
||||
return !isNaN(Number(value)) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?`).test(value))
|
||||
}
|
||||
|
||||
export function percentage(value) {
|
||||
return value.split(UNDERSCORE).every((part) => {
|
||||
return /%$/g.test(part) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?%`).test(part))
|
||||
})
|
||||
}
|
||||
|
||||
let lengthUnits = [
|
||||
'cm',
|
||||
'mm',
|
||||
'Q',
|
||||
'in',
|
||||
'pc',
|
||||
'pt',
|
||||
'px',
|
||||
'em',
|
||||
'ex',
|
||||
'ch',
|
||||
'rem',
|
||||
'lh',
|
||||
'vw',
|
||||
'vh',
|
||||
'vmin',
|
||||
'vmax',
|
||||
]
|
||||
let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
|
||||
export function length(value) {
|
||||
return value.split(UNDERSCORE).every((part) => {
|
||||
return (
|
||||
part === '0' ||
|
||||
new RegExp(`${lengthUnitsPattern}$`).test(part) ||
|
||||
cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?${lengthUnitsPattern}`).test(part))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
let lineWidths = new Set(['thin', 'medium', 'thick'])
|
||||
export function lineWidth(value) {
|
||||
return lineWidths.has(value)
|
||||
}
|
||||
|
||||
export function shadow(value) {
|
||||
let parsedShadows = parseBoxShadowValue(normalize(value))
|
||||
|
||||
for (let parsedShadow of parsedShadows) {
|
||||
if (!parsedShadow.valid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export function color(value) {
|
||||
let colors = 0
|
||||
|
||||
let result = value.split(UNDERSCORE).every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
if (parseColor(part, { loose: true }) !== null) return colors++, true
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
if (!result) return false
|
||||
return colors > 0
|
||||
}
|
||||
|
||||
export function image(value) {
|
||||
let images = 0
|
||||
let result = value.split(COMMA).every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
if (
|
||||
url(part) ||
|
||||
gradient(part) ||
|
||||
['element(', 'image(', 'cross-fade(', 'image-set('].some((fn) => part.startsWith(fn))
|
||||
) {
|
||||
images++
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
if (!result) return false
|
||||
return images > 0
|
||||
}
|
||||
|
||||
let gradientTypes = new Set([
|
||||
'linear-gradient',
|
||||
'radial-gradient',
|
||||
'repeating-linear-gradient',
|
||||
'repeating-radial-gradient',
|
||||
'conic-gradient',
|
||||
])
|
||||
export function gradient(value) {
|
||||
value = normalize(value)
|
||||
|
||||
for (let type of gradientTypes) {
|
||||
if (value.startsWith(`${type}(`)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
let validPositions = new Set(['center', 'top', 'right', 'bottom', 'left'])
|
||||
export function position(value) {
|
||||
let positions = 0
|
||||
let result = value.split(UNDERSCORE).every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
if (validPositions.has(part) || length(part) || percentage(part)) {
|
||||
positions++
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
if (!result) return false
|
||||
return positions > 0
|
||||
}
|
||||
|
||||
export function familyName(value) {
|
||||
let fonts = 0
|
||||
let result = value.split(COMMA).every((part) => {
|
||||
part = normalize(part)
|
||||
|
||||
if (part.startsWith('var(')) return true
|
||||
|
||||
// If it contains spaces, then it should be quoted
|
||||
if (part.includes(' ')) {
|
||||
if (!/(['"])([^"']+)\1/g.test(part)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// If it starts with a number, it's invalid
|
||||
if (/^\d/g.test(part)) {
|
||||
return false
|
||||
}
|
||||
|
||||
fonts++
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (!result) return false
|
||||
return fonts > 0
|
||||
}
|
||||
|
||||
let genericNames = new Set([
|
||||
'serif',
|
||||
'sans-serif',
|
||||
'monospace',
|
||||
'cursive',
|
||||
'fantasy',
|
||||
'system-ui',
|
||||
'ui-serif',
|
||||
'ui-sans-serif',
|
||||
'ui-monospace',
|
||||
'ui-rounded',
|
||||
'math',
|
||||
'emoji',
|
||||
'fangsong',
|
||||
])
|
||||
export function genericName(value) {
|
||||
return genericNames.has(value)
|
||||
}
|
||||
|
||||
let absoluteSizes = new Set([
|
||||
'xx-small',
|
||||
'x-small',
|
||||
'small',
|
||||
'medium',
|
||||
'large',
|
||||
'x-large',
|
||||
'x-large',
|
||||
'xxx-large',
|
||||
])
|
||||
export function absoluteSize(value) {
|
||||
return absoluteSizes.has(value)
|
||||
}
|
||||
|
||||
let relativeSizes = new Set(['larger', 'smaller'])
|
||||
export function relativeSize(value) {
|
||||
return relativeSizes.has(value)
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
export function defaults(target, ...sources) {
|
||||
for (let source of sources) {
|
||||
for (let k in source) {
|
||||
if (!target?.hasOwnProperty?.(k)) {
|
||||
target[k] = source[k]
|
||||
}
|
||||
}
|
||||
|
||||
for (let k of Object.getOwnPropertySymbols(source)) {
|
||||
if (!target?.hasOwnProperty?.(k)) {
|
||||
target[k] = source[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import parser from 'postcss-selector-parser'
|
||||
import escapeCommas from './escapeCommas'
|
||||
|
||||
export default function escapeClassName(className) {
|
||||
let node = parser.className()
|
||||
node.value = className
|
||||
return escapeCommas(node?.raws?.value ?? node.value)
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export default function escapeCommas(className) {
|
||||
return className.replace(/\\,/g, '\\2c ')
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
const flattenColorPalette = (colors) =>
|
||||
Object.assign(
|
||||
{},
|
||||
...Object.entries(colors ?? {}).flatMap(([color, values]) =>
|
||||
typeof values == 'object'
|
||||
? Object.entries(flattenColorPalette(values)).map(([number, hex]) => ({
|
||||
[color + (number === 'DEFAULT' ? '' : `-${number}`)]: hex,
|
||||
}))
|
||||
: [{ [`${color}`]: values }]
|
||||
)
|
||||
)
|
||||
|
||||
export default flattenColorPalette
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
import selectorParser from 'postcss-selector-parser'
|
||||
import unescape from 'postcss-selector-parser/dist/util/unesc'
|
||||
import escapeClassName from '../util/escapeClassName'
|
||||
import prefixSelector from '../util/prefixSelector'
|
||||
|
||||
let MERGE = ':merge'
|
||||
let PARENT = '&'
|
||||
|
||||
export let selectorFunctions = new Set([MERGE])
|
||||
|
||||
export function formatVariantSelector(current, ...others) {
|
||||
for (let other of others) {
|
||||
let incomingValue = resolveFunctionArgument(other, MERGE)
|
||||
if (incomingValue !== null) {
|
||||
let existingValue = resolveFunctionArgument(current, MERGE, incomingValue)
|
||||
if (existingValue !== null) {
|
||||
let existingTarget = `${MERGE}(${incomingValue})`
|
||||
let splitIdx = other.indexOf(existingTarget)
|
||||
let addition = other.slice(splitIdx + existingTarget.length).split(' ')[0]
|
||||
|
||||
current = current.replace(existingTarget, existingTarget + addition)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
current = other.replace(PARENT, current)
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
export function finalizeSelector(
|
||||
format,
|
||||
{
|
||||
selector,
|
||||
candidate,
|
||||
context,
|
||||
isArbitraryVariant,
|
||||
|
||||
// Split by the separator, but ignore the separator inside square brackets:
|
||||
//
|
||||
// E.g.: dark:lg:hover:[paint-order:markers]
|
||||
// ┬ ┬ ┬ ┬
|
||||
// │ │ │ ╰── We will not split here
|
||||
// ╰──┴─────┴─────────────── We will split here
|
||||
//
|
||||
base = candidate
|
||||
.split(new RegExp(`\\${context?.tailwindConfig?.separator ?? ':'}(?![^[]*\\])`))
|
||||
.pop(),
|
||||
}
|
||||
) {
|
||||
let ast = selectorParser().astSync(selector)
|
||||
|
||||
// We explicitly DO NOT prefix classes in arbitrary variants
|
||||
if (context?.tailwindConfig?.prefix && !isArbitraryVariant) {
|
||||
format = prefixSelector(context.tailwindConfig.prefix, format)
|
||||
}
|
||||
|
||||
format = format.replace(PARENT, `.${escapeClassName(candidate)}`)
|
||||
|
||||
let formatAst = selectorParser().astSync(format)
|
||||
|
||||
// Remove extraneous selectors that do not include the base class/candidate being matched against
|
||||
// For example if we have a utility defined `.a, .b { color: red}`
|
||||
// And the formatted variant is sm:b then we want the final selector to be `.sm\:b` and not `.a, .sm\:b`
|
||||
ast.each((node) => {
|
||||
let hasClassesMatchingCandidate = node.some((n) => n.type === 'class' && n.value === base)
|
||||
|
||||
if (!hasClassesMatchingCandidate) {
|
||||
node.remove()
|
||||
}
|
||||
})
|
||||
|
||||
// Normalize escaped classes, e.g.:
|
||||
//
|
||||
// The idea would be to replace the escaped `base` in the selector with the
|
||||
// `format`. However, in css you can escape the same selector in a few
|
||||
// different ways. This would result in different strings and therefore we
|
||||
// can't replace it properly.
|
||||
//
|
||||
// base: bg-[rgb(255,0,0)]
|
||||
// base in selector: bg-\\[rgb\\(255\\,0\\,0\\)\\]
|
||||
// escaped base: bg-\\[rgb\\(255\\2c 0\\2c 0\\)\\]
|
||||
//
|
||||
ast.walkClasses((node) => {
|
||||
if (node.raws && node.value.includes(base)) {
|
||||
node.raws.value = escapeClassName(unescape(node.raws.value))
|
||||
}
|
||||
})
|
||||
|
||||
// We can safely replace the escaped base now, since the `base` section is
|
||||
// now in a normalized escaped value.
|
||||
ast.walkClasses((node) => {
|
||||
if (node.value === base) {
|
||||
node.replaceWith(...formatAst.nodes)
|
||||
}
|
||||
})
|
||||
|
||||
// This will make sure to move pseudo's to the correct spot (the end for
|
||||
// pseudo elements) because otherwise the selector will never work
|
||||
// anyway.
|
||||
//
|
||||
// E.g.:
|
||||
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
|
||||
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
|
||||
//
|
||||
// `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
|
||||
function collectPseudoElements(selector) {
|
||||
let nodes = []
|
||||
|
||||
for (let node of selector.nodes) {
|
||||
if (isPseudoElement(node)) {
|
||||
nodes.push(node)
|
||||
selector.removeChild(node)
|
||||
}
|
||||
|
||||
if (node?.nodes) {
|
||||
nodes.push(...collectPseudoElements(node))
|
||||
}
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Remove unnecessary pseudo selectors that we used as placeholders
|
||||
ast.each((selector) => {
|
||||
selector.walkPseudos((p) => {
|
||||
if (selectorFunctions.has(p.value)) {
|
||||
p.replaceWith(p.nodes)
|
||||
}
|
||||
})
|
||||
|
||||
let pseudoElements = collectPseudoElements(selector)
|
||||
if (pseudoElements.length > 0) {
|
||||
selector.nodes.push(pseudoElements.sort(sortSelector))
|
||||
}
|
||||
})
|
||||
|
||||
return ast.toString()
|
||||
}
|
||||
|
||||
// Note: As a rule, double colons (::) should be used instead of a single colon
|
||||
// (:). This distinguishes pseudo-classes from pseudo-elements. However, since
|
||||
// this distinction was not present in older versions of the W3C spec, most
|
||||
// browsers support both syntaxes for the original pseudo-elements.
|
||||
let pseudoElementsBC = [':before', ':after', ':first-line', ':first-letter']
|
||||
|
||||
// These pseudo-elements _can_ be combined with other pseudo selectors AND the order does matter.
|
||||
let pseudoElementExceptions = ['::file-selector-button']
|
||||
|
||||
// This will make sure to move pseudo's to the correct spot (the end for
|
||||
// pseudo elements) because otherwise the selector will never work
|
||||
// anyway.
|
||||
//
|
||||
// E.g.:
|
||||
// - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
|
||||
// - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
|
||||
//
|
||||
// `::before:hover` doesn't work, which means that we can make it work
|
||||
// for you by flipping the order.
|
||||
function sortSelector(a, z) {
|
||||
// Both nodes are non-pseudo's so we can safely ignore them and keep
|
||||
// them in the same order.
|
||||
if (a.type !== 'pseudo' && z.type !== 'pseudo') {
|
||||
return 0
|
||||
}
|
||||
|
||||
// If one of them is a combinator, we need to keep it in the same order
|
||||
// because that means it will start a new "section" in the selector.
|
||||
if ((a.type === 'combinator') ^ (z.type === 'combinator')) {
|
||||
return 0
|
||||
}
|
||||
|
||||
// One of the items is a pseudo and the other one isn't. Let's move
|
||||
// the pseudo to the right.
|
||||
if ((a.type === 'pseudo') ^ (z.type === 'pseudo')) {
|
||||
return (a.type === 'pseudo') - (z.type === 'pseudo')
|
||||
}
|
||||
|
||||
// Both are pseudo's, move the pseudo elements (except for
|
||||
// ::file-selector-button) to the right.
|
||||
return isPseudoElement(a) - isPseudoElement(z)
|
||||
}
|
||||
|
||||
function isPseudoElement(node) {
|
||||
if (node.type !== 'pseudo') return false
|
||||
if (pseudoElementExceptions.includes(node.value)) return false
|
||||
|
||||
return node.value.startsWith('::') || pseudoElementsBC.includes(node.value)
|
||||
}
|
||||
|
||||
function resolveFunctionArgument(haystack, needle, arg) {
|
||||
let startIdx = haystack.indexOf(arg ? `${needle}(${arg})` : needle)
|
||||
if (startIdx === -1) return null
|
||||
|
||||
// Start inside the `(`
|
||||
startIdx += needle.length + 1
|
||||
|
||||
let target = ''
|
||||
let count = 0
|
||||
|
||||
for (let char of haystack.slice(startIdx)) {
|
||||
if (char !== '(' && char !== ')') {
|
||||
target += char
|
||||
} else if (char === '(') {
|
||||
target += char
|
||||
count++
|
||||
} else if (char === ')') {
|
||||
if (--count < 0) break // unbalanced
|
||||
target += char
|
||||
}
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import defaultConfig from '../../stubs/defaultConfig.stub.js'
|
||||
import { flagEnabled } from '../featureFlags'
|
||||
|
||||
export default function getAllConfigs(config) {
|
||||
const configs = (config?.presets ?? [defaultConfig])
|
||||
.slice()
|
||||
.reverse()
|
||||
.flatMap((preset) => getAllConfigs(preset instanceof Function ? preset() : preset))
|
||||
|
||||
const features = {
|
||||
// Add experimental configs here...
|
||||
respectDefaultRingColorOpacity: {
|
||||
theme: {
|
||||
ringColor: {
|
||||
DEFAULT: '#3b82f67f',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const experimentals = Object.keys(features)
|
||||
.filter((feature) => flagEnabled(config, feature))
|
||||
.map((feature) => features[feature])
|
||||
|
||||
return [config, ...experimentals, ...configs]
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import hash from 'object-hash'
|
||||
|
||||
export default function hashConfig(config) {
|
||||
return hash(config, { ignoreUnknown: true })
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export default function isKeyframeRule(rule) {
|
||||
return rule.parent && rule.parent.type === 'atrule' && /keyframes$/.test(rule.parent.name)
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
export default function isPlainObject(value) {
|
||||
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
||||
return false
|
||||
}
|
||||
|
||||
const prototype = Object.getPrototypeOf(value)
|
||||
return prototype === null || prototype === Object.prototype
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
let matchingBrackets = new Map([
|
||||
['{', '}'],
|
||||
['[', ']'],
|
||||
['(', ')'],
|
||||
])
|
||||
let inverseMatchingBrackets = new Map(
|
||||
Array.from(matchingBrackets.entries()).map(([k, v]) => [v, k])
|
||||
)
|
||||
|
||||
let quotes = new Set(['"', "'", '`'])
|
||||
|
||||
// Arbitrary values must contain balanced brackets (), [] and {}. Escaped
|
||||
// values don't count, and brackets inside quotes also don't count.
|
||||
//
|
||||
// E.g.: w-[this-is]w-[weird-and-invalid]
|
||||
// E.g.: w-[this-is\\]w-\\[weird-but-valid]
|
||||
// E.g.: content-['this-is-also-valid]-weirdly-enough']
|
||||
export default function isValidArbitraryValue(value) {
|
||||
let stack = []
|
||||
let inQuotes = false
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
let char = value[i]
|
||||
|
||||
if (char === ':' && !inQuotes && stack.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Non-escaped quotes allow us to "allow" anything in between
|
||||
if (quotes.has(char) && value[i - 1] !== '\\') {
|
||||
inQuotes = !inQuotes
|
||||
}
|
||||
|
||||
if (inQuotes) continue
|
||||
if (value[i - 1] === '\\') continue // Escaped
|
||||
|
||||
if (matchingBrackets.has(char)) {
|
||||
stack.push(char)
|
||||
} else if (inverseMatchingBrackets.has(char)) {
|
||||
let inverse = inverseMatchingBrackets.get(char)
|
||||
|
||||
// Nothing to pop from, therefore it is unbalanced
|
||||
if (stack.length <= 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Popped value must match the inverse value, otherwise it is unbalanced
|
||||
if (stack.pop() !== inverse) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is still something on the stack, it is also unbalanced
|
||||
if (stack.length > 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// All good, totally balanced!
|
||||
return true
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import colors from 'picocolors'
|
||||
|
||||
let alreadyShown = new Set()
|
||||
|
||||
function log(type, messages, key) {
|
||||
if (typeof process !== 'undefined' && process.env.JEST_WORKER_ID) return
|
||||
|
||||
if (key && alreadyShown.has(key)) return
|
||||
if (key) alreadyShown.add(key)
|
||||
|
||||
console.warn('')
|
||||
messages.forEach((message) => console.warn(type, '-', message))
|
||||
}
|
||||
|
||||
export function dim(input) {
|
||||
return colors.dim(input)
|
||||
}
|
||||
|
||||
export default {
|
||||
info(key, messages) {
|
||||
log(colors.bold(colors.cyan('info')), ...(Array.isArray(key) ? [key] : [messages, key]))
|
||||
},
|
||||
warn(key, messages) {
|
||||
log(colors.bold(colors.yellow('warn')), ...(Array.isArray(key) ? [key] : [messages, key]))
|
||||
},
|
||||
risk(key, messages) {
|
||||
log(colors.bold(colors.magenta('risk')), ...(Array.isArray(key) ? [key] : [messages, key]))
|
||||
},
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import escapeClassName from './escapeClassName'
|
||||
import escapeCommas from './escapeCommas'
|
||||
|
||||
export function asClass(name) {
|
||||
return escapeCommas(`.${escapeClassName(name)}`)
|
||||
}
|
||||
|
||||
export default function nameClass(classPrefix, key) {
|
||||
return asClass(formatClass(classPrefix, key))
|
||||
}
|
||||
|
||||
export function formatClass(classPrefix, key) {
|
||||
if (key === 'DEFAULT') {
|
||||
return classPrefix
|
||||
}
|
||||
|
||||
if (key === '-' || key === '-DEFAULT') {
|
||||
return `-${classPrefix}`
|
||||
}
|
||||
|
||||
if (key.startsWith('-')) {
|
||||
return `-${classPrefix}${key}`
|
||||
}
|
||||
|
||||
return `${classPrefix}-${key}`
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
export default function (value) {
|
||||
value = `${value}`
|
||||
|
||||
if (value === '0') {
|
||||
return '0'
|
||||
}
|
||||
|
||||
// Flip sign of numbers
|
||||
if (/^[+-]?(\d+|\d*\.\d+)(e[+-]?\d+)?(%|\w+)?$/.test(value)) {
|
||||
return value.replace(/^[+-]?/, (sign) => (sign === '-' ? '' : '-'))
|
||||
}
|
||||
|
||||
if (value.includes('var(') || value.includes('calc(')) {
|
||||
return `calc(${value} * -1)`
|
||||
}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
import log, { dim } from './log'
|
||||
|
||||
export function normalizeConfig(config) {
|
||||
// Quick structure validation
|
||||
/**
|
||||
* type FilePath = string
|
||||
* type RawFile = { raw: string, extension?: string }
|
||||
* type ExtractorFn = (content: string) => Array<string>
|
||||
* type TransformerFn = (content: string) => string
|
||||
*
|
||||
* type Content =
|
||||
* | Array<FilePath | RawFile>
|
||||
* | {
|
||||
* files: Array<FilePath | RawFile>,
|
||||
* extract?: ExtractorFn | { [extension: string]: ExtractorFn }
|
||||
* transform?: TransformerFn | { [extension: string]: TransformerFn }
|
||||
* }
|
||||
*/
|
||||
let valid = (() => {
|
||||
// `config.purge` should not exist anymore
|
||||
if (config.purge) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content` should exist
|
||||
if (!config.content) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content` should be an object or an array
|
||||
if (
|
||||
!Array.isArray(config.content) &&
|
||||
!(typeof config.content === 'object' && config.content !== null)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// When `config.content` is an array, it should consist of FilePaths or RawFiles
|
||||
if (Array.isArray(config.content)) {
|
||||
return config.content.every((path) => {
|
||||
// `path` can be a string
|
||||
if (typeof path === 'string') return true
|
||||
|
||||
// `path` can be an object { raw: string, extension?: string }
|
||||
// `raw` must be a string
|
||||
if (typeof path?.raw !== 'string') return false
|
||||
|
||||
// `extension` (if provided) should also be a string
|
||||
if (path?.extension && typeof path?.extension !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// When `config.content` is an object
|
||||
if (typeof config.content === 'object' && config.content !== null) {
|
||||
// Only `files`, `extract` and `transform` can exist in `config.content`
|
||||
if (
|
||||
Object.keys(config.content).some((key) => !['files', 'extract', 'transform'].includes(key))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content.files` should exist of FilePaths or RawFiles
|
||||
if (Array.isArray(config.content.files)) {
|
||||
if (
|
||||
!config.content.files.every((path) => {
|
||||
// `path` can be a string
|
||||
if (typeof path === 'string') return true
|
||||
|
||||
// `path` can be an object { raw: string, extension?: string }
|
||||
// `raw` must be a string
|
||||
if (typeof path?.raw !== 'string') return false
|
||||
|
||||
// `extension` (if provided) should also be a string
|
||||
if (path?.extension && typeof path?.extension !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content.extract` is optional, and can be a Function or a Record<String, Function>
|
||||
if (typeof config.content.extract === 'object') {
|
||||
for (let value of Object.values(config.content.extract)) {
|
||||
if (typeof value !== 'function') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
!(config.content.extract === undefined || typeof config.content.extract === 'function')
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// `config.content.transform` is optional, and can be a Function or a Record<String, Function>
|
||||
if (typeof config.content.transform === 'object') {
|
||||
for (let value of Object.values(config.content.transform)) {
|
||||
if (typeof value !== 'function') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
!(
|
||||
config.content.transform === undefined || typeof config.content.transform === 'function'
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
})()
|
||||
|
||||
if (!valid) {
|
||||
log.warn('purge-deprecation', [
|
||||
'The `purge`/`content` options have changed in Tailwind CSS v3.0.',
|
||||
'Update your configuration file to eliminate this warning.',
|
||||
'https://tailwindcss.com/docs/upgrade-guide#configure-content-sources',
|
||||
])
|
||||
}
|
||||
|
||||
// Normalize the `safelist`
|
||||
config.safelist = (() => {
|
||||
let { content, purge, safelist } = config
|
||||
|
||||
if (Array.isArray(safelist)) return safelist
|
||||
if (Array.isArray(content?.safelist)) return content.safelist
|
||||
if (Array.isArray(purge?.safelist)) return purge.safelist
|
||||
if (Array.isArray(purge?.options?.safelist)) return purge.options.safelist
|
||||
|
||||
return []
|
||||
})()
|
||||
|
||||
// Normalize prefix option
|
||||
if (typeof config.prefix === 'function') {
|
||||
log.warn('prefix-function', [
|
||||
'As of Tailwind CSS v3.0, `prefix` cannot be a function.',
|
||||
'Update `prefix` in your configuration to be a string to eliminate this warning.',
|
||||
'https://tailwindcss.com/docs/upgrade-guide#prefix-cannot-be-a-function',
|
||||
])
|
||||
config.prefix = ''
|
||||
} else {
|
||||
config.prefix = config.prefix ?? ''
|
||||
}
|
||||
|
||||
// Normalize the `content`
|
||||
config.content = {
|
||||
files: (() => {
|
||||
let { content, purge } = config
|
||||
|
||||
if (Array.isArray(purge)) return purge
|
||||
if (Array.isArray(purge?.content)) return purge.content
|
||||
if (Array.isArray(content)) return content
|
||||
if (Array.isArray(content?.content)) return content.content
|
||||
if (Array.isArray(content?.files)) return content.files
|
||||
|
||||
return []
|
||||
})(),
|
||||
|
||||
extract: (() => {
|
||||
let extract = (() => {
|
||||
if (config.purge?.extract) return config.purge.extract
|
||||
if (config.content?.extract) return config.content.extract
|
||||
|
||||
if (config.purge?.extract?.DEFAULT) return config.purge.extract.DEFAULT
|
||||
if (config.content?.extract?.DEFAULT) return config.content.extract.DEFAULT
|
||||
|
||||
if (config.purge?.options?.extractors) return config.purge.options.extractors
|
||||
if (config.content?.options?.extractors) return config.content.options.extractors
|
||||
|
||||
return {}
|
||||
})()
|
||||
|
||||
let extractors = {}
|
||||
|
||||
let defaultExtractor = (() => {
|
||||
if (config.purge?.options?.defaultExtractor) {
|
||||
return config.purge.options.defaultExtractor
|
||||
}
|
||||
|
||||
if (config.content?.options?.defaultExtractor) {
|
||||
return config.content.options.defaultExtractor
|
||||
}
|
||||
|
||||
return undefined
|
||||
})()
|
||||
|
||||
if (defaultExtractor !== undefined) {
|
||||
extractors.DEFAULT = defaultExtractor
|
||||
}
|
||||
|
||||
// Functions
|
||||
if (typeof extract === 'function') {
|
||||
extractors.DEFAULT = extract
|
||||
}
|
||||
|
||||
// Arrays
|
||||
else if (Array.isArray(extract)) {
|
||||
for (let { extensions, extractor } of extract ?? []) {
|
||||
for (let extension of extensions) {
|
||||
extractors[extension] = extractor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Objects
|
||||
else if (typeof extract === 'object' && extract !== null) {
|
||||
Object.assign(extractors, extract)
|
||||
}
|
||||
|
||||
return extractors
|
||||
})(),
|
||||
|
||||
transform: (() => {
|
||||
let transform = (() => {
|
||||
if (config.purge?.transform) return config.purge.transform
|
||||
if (config.content?.transform) return config.content.transform
|
||||
|
||||
if (config.purge?.transform?.DEFAULT) return config.purge.transform.DEFAULT
|
||||
if (config.content?.transform?.DEFAULT) return config.content.transform.DEFAULT
|
||||
|
||||
return {}
|
||||
})()
|
||||
|
||||
let transformers = {}
|
||||
|
||||
if (typeof transform === 'function') {
|
||||
transformers.DEFAULT = transform
|
||||
}
|
||||
|
||||
if (typeof transform === 'object' && transform !== null) {
|
||||
Object.assign(transformers, transform)
|
||||
}
|
||||
|
||||
return transformers
|
||||
})(),
|
||||
}
|
||||
|
||||
// Validate globs to prevent bogus globs.
|
||||
// E.g.: `./src/*.{html}` is invalid, the `{html}` should just be `html`
|
||||
for (let file of config.content.files) {
|
||||
if (typeof file === 'string' && /{([^,]*?)}/g.test(file)) {
|
||||
log.warn('invalid-glob-braces', [
|
||||
`The glob pattern ${dim(file)} in your Tailwind CSS configuration is invalid.`,
|
||||
`Update it to ${dim(file.replace(/{([^,]*?)}/g, '$1'))} to silence this warning.`,
|
||||
// TODO: Add https://tw.wtf/invalid-glob-braces
|
||||
])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* A function that normalizes the various forms that the screens object can be
|
||||
* provided in.
|
||||
*
|
||||
* Input(s):
|
||||
* - ['100px', '200px'] // Raw strings
|
||||
* - { sm: '100px', md: '200px' } // Object with string values
|
||||
* - { sm: { min: '100px' }, md: { max: '100px' } } // Object with object values
|
||||
* - { sm: [{ min: '100px' }, { max: '200px' }] } // Object with object array (multiple values)
|
||||
*
|
||||
* Output(s):
|
||||
* - [{ name: 'sm', values: [{ min: '100px', max: '200px' }] }] // List of objects, that contains multiple values
|
||||
*/
|
||||
export function normalizeScreens(screens, root = true) {
|
||||
if (Array.isArray(screens)) {
|
||||
return screens.map((screen) => {
|
||||
if (root && Array.isArray(screen)) {
|
||||
throw new Error('The tuple syntax is not supported for `screens`.')
|
||||
}
|
||||
|
||||
if (typeof screen === 'string') {
|
||||
return { name: screen.toString(), values: [{ min: screen, max: undefined }] }
|
||||
}
|
||||
|
||||
let [name, options] = screen
|
||||
name = name.toString()
|
||||
|
||||
if (typeof options === 'string') {
|
||||
return { name, values: [{ min: options, max: undefined }] }
|
||||
}
|
||||
|
||||
if (Array.isArray(options)) {
|
||||
return { name, values: options.map((option) => resolveValue(option)) }
|
||||
}
|
||||
|
||||
return { name, values: [resolveValue(options)] }
|
||||
})
|
||||
}
|
||||
|
||||
return normalizeScreens(Object.entries(screens ?? {}), false)
|
||||
}
|
||||
|
||||
function resolveValue({ 'min-width': _minWidth, min = _minWidth, max, raw } = {}) {
|
||||
return { min, max, raw }
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
const DIRECTIONS = new Set(['normal', 'reverse', 'alternate', 'alternate-reverse'])
|
||||
const PLAY_STATES = new Set(['running', 'paused'])
|
||||
const FILL_MODES = new Set(['none', 'forwards', 'backwards', 'both'])
|
||||
const ITERATION_COUNTS = new Set(['infinite'])
|
||||
const TIMINGS = new Set([
|
||||
'linear',
|
||||
'ease',
|
||||
'ease-in',
|
||||
'ease-out',
|
||||
'ease-in-out',
|
||||
'step-start',
|
||||
'step-end',
|
||||
])
|
||||
const TIMING_FNS = ['cubic-bezier', 'steps']
|
||||
|
||||
const COMMA = /\,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count.
|
||||
const SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead.
|
||||
const TIME = /^(-?[\d.]+m?s)$/
|
||||
const DIGIT = /^(\d+)$/
|
||||
|
||||
export default function parseAnimationValue(input) {
|
||||
let animations = input.split(COMMA)
|
||||
return animations.map((animation) => {
|
||||
let value = animation.trim()
|
||||
let result = { value }
|
||||
let parts = value.split(SPACE)
|
||||
let seen = new Set()
|
||||
|
||||
for (let part of parts) {
|
||||
if (!seen.has('DIRECTIONS') && DIRECTIONS.has(part)) {
|
||||
result.direction = part
|
||||
seen.add('DIRECTIONS')
|
||||
} else if (!seen.has('PLAY_STATES') && PLAY_STATES.has(part)) {
|
||||
result.playState = part
|
||||
seen.add('PLAY_STATES')
|
||||
} else if (!seen.has('FILL_MODES') && FILL_MODES.has(part)) {
|
||||
result.fillMode = part
|
||||
seen.add('FILL_MODES')
|
||||
} else if (
|
||||
!seen.has('ITERATION_COUNTS') &&
|
||||
(ITERATION_COUNTS.has(part) || DIGIT.test(part))
|
||||
) {
|
||||
result.iterationCount = part
|
||||
seen.add('ITERATION_COUNTS')
|
||||
} else if (!seen.has('TIMING_FUNCTION') && TIMINGS.has(part)) {
|
||||
result.timingFunction = part
|
||||
seen.add('TIMING_FUNCTION')
|
||||
} else if (!seen.has('TIMING_FUNCTION') && TIMING_FNS.some((f) => part.startsWith(`${f}(`))) {
|
||||
result.timingFunction = part
|
||||
seen.add('TIMING_FUNCTION')
|
||||
} else if (!seen.has('DURATION') && TIME.test(part)) {
|
||||
result.duration = part
|
||||
seen.add('DURATION')
|
||||
} else if (!seen.has('DELAY') && TIME.test(part)) {
|
||||
result.delay = part
|
||||
seen.add('DELAY')
|
||||
} else if (!seen.has('NAME')) {
|
||||
result.name = part
|
||||
seen.add('NAME')
|
||||
} else {
|
||||
if (!result.unknown) result.unknown = []
|
||||
result.unknown.push(part)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { splitAtTopLevelOnly } from './splitAtTopLevelOnly'
|
||||
|
||||
let KEYWORDS = new Set(['inset', 'inherit', 'initial', 'revert', 'unset'])
|
||||
let SPACE = /\ +(?![^(]*\))/g // Similar to the one above, but with spaces instead.
|
||||
let LENGTH = /^-?(\d+|\.\d+)(.*?)$/g
|
||||
|
||||
export function parseBoxShadowValue(input) {
|
||||
let shadows = Array.from(splitAtTopLevelOnly(input, ','))
|
||||
return shadows.map((shadow) => {
|
||||
let value = shadow.trim()
|
||||
let result = { raw: value }
|
||||
let parts = value.split(SPACE)
|
||||
let seen = new Set()
|
||||
|
||||
for (let part of parts) {
|
||||
// Reset index, since the regex is stateful.
|
||||
LENGTH.lastIndex = 0
|
||||
|
||||
// Keyword
|
||||
if (!seen.has('KEYWORD') && KEYWORDS.has(part)) {
|
||||
result.keyword = part
|
||||
seen.add('KEYWORD')
|
||||
}
|
||||
|
||||
// Length value
|
||||
else if (LENGTH.test(part)) {
|
||||
if (!seen.has('X')) {
|
||||
result.x = part
|
||||
seen.add('X')
|
||||
} else if (!seen.has('Y')) {
|
||||
result.y = part
|
||||
seen.add('Y')
|
||||
} else if (!seen.has('BLUR')) {
|
||||
result.blur = part
|
||||
seen.add('BLUR')
|
||||
} else if (!seen.has('SPREAD')) {
|
||||
result.spread = part
|
||||
seen.add('SPREAD')
|
||||
}
|
||||
}
|
||||
|
||||
// Color or unknown
|
||||
else {
|
||||
if (!result.color) {
|
||||
result.color = part
|
||||
} else {
|
||||
if (!result.unknown) result.unknown = []
|
||||
result.unknown.push(part)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if valid
|
||||
result.valid = result.x !== undefined && result.y !== undefined
|
||||
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
export function formatBoxShadowValue(shadows) {
|
||||
return shadows
|
||||
.map((shadow) => {
|
||||
if (!shadow.valid) {
|
||||
return shadow.raw
|
||||
}
|
||||
|
||||
return [shadow.keyword, shadow.x, shadow.y, shadow.blur, shadow.spread, shadow.color]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
})
|
||||
.join(', ')
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import isGlob from 'is-glob'
|
||||
import globParent from 'glob-parent'
|
||||
import path from 'path'
|
||||
|
||||
// Based on `glob-base`
|
||||
// https://github.com/micromatch/glob-base/blob/master/index.js
|
||||
function parseGlob(pattern) {
|
||||
let glob = pattern
|
||||
let base = globParent(pattern)
|
||||
|
||||
if (base !== '.') {
|
||||
glob = pattern.substr(base.length)
|
||||
if (glob.charAt(0) === '/') {
|
||||
glob = glob.substr(1)
|
||||
}
|
||||
}
|
||||
|
||||
if (glob.substr(0, 2) === './') {
|
||||
glob = glob.substr(2)
|
||||
}
|
||||
if (glob.charAt(0) === '/') {
|
||||
glob = glob.substr(1)
|
||||
}
|
||||
|
||||
return { base, glob }
|
||||
}
|
||||
|
||||
export default function parseDependency(normalizedFileOrGlob) {
|
||||
if (normalizedFileOrGlob.startsWith('!')) {
|
||||
return null
|
||||
}
|
||||
|
||||
let message
|
||||
|
||||
if (isGlob(normalizedFileOrGlob)) {
|
||||
let { base, glob } = parseGlob(normalizedFileOrGlob)
|
||||
message = { type: 'dir-dependency', dir: path.resolve(base), glob }
|
||||
} else {
|
||||
message = { type: 'dependency', file: path.resolve(normalizedFileOrGlob) }
|
||||
}
|
||||
|
||||
// rollup-plugin-postcss does not support dir-dependency messages
|
||||
// but directories can be watched in the same way as files
|
||||
if (message.type === 'dir-dependency' && process.env.ROLLUP_WATCH === 'true') {
|
||||
message = { type: 'dependency', file: message.dir }
|
||||
}
|
||||
|
||||
return message
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import postcss from 'postcss'
|
||||
import postcssNested from 'postcss-nested'
|
||||
import postcssJs from 'postcss-js'
|
||||
|
||||
export default function parseObjectStyles(styles) {
|
||||
if (!Array.isArray(styles)) {
|
||||
return parseObjectStyles([styles])
|
||||
}
|
||||
|
||||
return styles.flatMap((style) => {
|
||||
return postcss([
|
||||
postcssNested({
|
||||
bubble: ['screen'],
|
||||
}),
|
||||
]).process(style, {
|
||||
parser: postcssJs,
|
||||
}).root.nodes
|
||||
})
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
import selectorParser from 'postcss-selector-parser'
|
||||
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'
|
||||
|
||||
export function updateAllClasses(selectors, updateClass) {
|
||||
let parser = selectorParser((selectors) => {
|
||||
selectors.walkClasses((sel) => {
|
||||
let updatedClass = updateClass(sel.value)
|
||||
sel.value = updatedClass
|
||||
if (sel.raws && sel.raws.value) {
|
||||
sel.raws.value = escapeCommas(sel.raws.value)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
let result = parser.processSync(selectors)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
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 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 parseColorFormat(value) {
|
||||
if (typeof value === 'string' && value.includes('<alpha-value>')) {
|
||||
let oldValue = value
|
||||
|
||||
return ({ opacityValue = 1 }) => oldValue.replace('<alpha-value>', opacityValue)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export function asColor(modifier, options = {}, { tailwindConfig = {} } = {}) {
|
||||
if (options.values?.[modifier] !== undefined) {
|
||||
return parseColorFormat(options.values?.[modifier])
|
||||
}
|
||||
|
||||
let [color, alpha] = splitAlpha(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, alpha.slice(1, -1))
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
|
||||
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 (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]
|
||||
}
|
||||
}
|
||||
|
||||
// Find first matching type
|
||||
for (let type of [].concat(types)) {
|
||||
let result = typeMap[type](modifier, options, { tailwindConfig })
|
||||
if (result !== undefined) return [result, type]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import parser from 'postcss-selector-parser'
|
||||
|
||||
export default function (prefix, selector, prependNegative = false) {
|
||||
return parser((selectors) => {
|
||||
selectors.walkClasses((classSelector) => {
|
||||
let baseClass = classSelector.value
|
||||
let shouldPlaceNegativeBeforePrefix = prependNegative && baseClass.startsWith('-')
|
||||
|
||||
classSelector.value = shouldPlaceNegativeBeforePrefix
|
||||
? `-${prefix}${baseClass.slice(1)}`
|
||||
: `${prefix}${baseClass}`
|
||||
})
|
||||
}).processSync(selector)
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* This function removes any uses of CSS variables used as an alpha channel
|
||||
*
|
||||
* This is required for selectors like `:visited` which do not allow
|
||||
* changes in opacity or external control using CSS variables.
|
||||
*
|
||||
* @param {import('postcss').Container} container
|
||||
* @param {string[]} toRemove
|
||||
*/
|
||||
export function removeAlphaVariables(container, toRemove) {
|
||||
container.walkDecls((decl) => {
|
||||
if (toRemove.includes(decl.prop)) {
|
||||
decl.remove()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (let varName of toRemove) {
|
||||
if (decl.value.includes(`/ var(${varName})`)) {
|
||||
decl.value = decl.value.replace(`/ var(${varName})`, '')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
import negateValue from './negateValue'
|
||||
import corePluginList from '../corePluginList'
|
||||
import configurePlugins from './configurePlugins'
|
||||
import defaultConfig from '../../stubs/defaultConfig.stub'
|
||||
import colors from '../public/colors'
|
||||
import { defaults } from './defaults'
|
||||
import { toPath } from './toPath'
|
||||
import { normalizeConfig } from './normalizeConfig'
|
||||
import isPlainObject from './isPlainObject'
|
||||
import { cloneDeep } from './cloneDeep'
|
||||
import { parseColorFormat } from './pluginUtils'
|
||||
import { withAlphaValue } from './withAlphaVariable'
|
||||
import toColorValue from './toColorValue'
|
||||
|
||||
function isFunction(input) {
|
||||
return typeof input === 'function'
|
||||
}
|
||||
|
||||
function isObject(input) {
|
||||
return typeof input === 'object' && input !== null
|
||||
}
|
||||
|
||||
function mergeWith(target, ...sources) {
|
||||
let customizer = sources.pop()
|
||||
|
||||
for (let source of sources) {
|
||||
for (let k in source) {
|
||||
let merged = customizer(target[k], source[k])
|
||||
|
||||
if (merged === undefined) {
|
||||
if (isObject(target[k]) && isObject(source[k])) {
|
||||
target[k] = mergeWith(target[k], source[k], customizer)
|
||||
} else {
|
||||
target[k] = source[k]
|
||||
}
|
||||
} else {
|
||||
target[k] = merged
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target
|
||||
}
|
||||
|
||||
const configUtils = {
|
||||
colors,
|
||||
negative(scale) {
|
||||
// TODO: Log that this function isn't really needed anymore?
|
||||
return Object.keys(scale)
|
||||
.filter((key) => scale[key] !== '0')
|
||||
.reduce((negativeScale, key) => {
|
||||
let negativeValue = negateValue(scale[key])
|
||||
|
||||
if (negativeValue !== undefined) {
|
||||
negativeScale[`-${key}`] = negativeValue
|
||||
}
|
||||
|
||||
return negativeScale
|
||||
}, {})
|
||||
},
|
||||
breakpoints(screens) {
|
||||
return Object.keys(screens)
|
||||
.filter((key) => typeof screens[key] === 'string')
|
||||
.reduce(
|
||||
(breakpoints, key) => ({
|
||||
...breakpoints,
|
||||
[`screen-${key}`]: screens[key],
|
||||
}),
|
||||
{}
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
function value(valueToResolve, ...args) {
|
||||
return isFunction(valueToResolve) ? valueToResolve(...args) : valueToResolve
|
||||
}
|
||||
|
||||
function collectExtends(items) {
|
||||
return items.reduce((merged, { extend }) => {
|
||||
return mergeWith(merged, extend, (mergedValue, extendValue) => {
|
||||
if (mergedValue === undefined) {
|
||||
return [extendValue]
|
||||
}
|
||||
|
||||
if (Array.isArray(mergedValue)) {
|
||||
return [extendValue, ...mergedValue]
|
||||
}
|
||||
|
||||
return [extendValue, mergedValue]
|
||||
})
|
||||
}, {})
|
||||
}
|
||||
|
||||
function mergeThemes(themes) {
|
||||
return {
|
||||
...themes.reduce((merged, theme) => defaults(merged, theme), {}),
|
||||
|
||||
// In order to resolve n config objects, we combine all of their `extend` properties
|
||||
// into arrays instead of objects so they aren't overridden.
|
||||
extend: collectExtends(themes),
|
||||
}
|
||||
}
|
||||
|
||||
function mergeExtensionCustomizer(merged, value) {
|
||||
// When we have an array of objects, we do want to merge it
|
||||
if (Array.isArray(merged) && isObject(merged[0])) {
|
||||
return merged.concat(value)
|
||||
}
|
||||
|
||||
// When the incoming value is an array, and the existing config is an object, prepend the existing object
|
||||
if (Array.isArray(value) && isObject(value[0]) && isObject(merged)) {
|
||||
return [merged, ...value]
|
||||
}
|
||||
|
||||
// Override arrays (for example for font-families, box-shadows, ...)
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
// Execute default behaviour
|
||||
return undefined
|
||||
}
|
||||
|
||||
function mergeExtensions({ extend, ...theme }) {
|
||||
return mergeWith(theme, extend, (themeValue, extensions) => {
|
||||
// The `extend` property is an array, so we need to check if it contains any functions
|
||||
if (!isFunction(themeValue) && !extensions.some(isFunction)) {
|
||||
return mergeWith({}, themeValue, ...extensions, mergeExtensionCustomizer)
|
||||
}
|
||||
|
||||
return (resolveThemePath, utils) =>
|
||||
mergeWith(
|
||||
{},
|
||||
...[themeValue, ...extensions].map((e) => value(e, resolveThemePath, utils)),
|
||||
mergeExtensionCustomizer
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key
|
||||
* @return {Iterable<string[] & {alpha: string | undefined}>}
|
||||
*/
|
||||
function* toPaths(key) {
|
||||
let path = toPath(key)
|
||||
|
||||
if (path.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
yield path
|
||||
|
||||
if (Array.isArray(key)) {
|
||||
return
|
||||
}
|
||||
|
||||
let pattern = /^(.*?)\s*\/\s*([^/]+)$/
|
||||
let matches = key.match(pattern)
|
||||
|
||||
if (matches !== null) {
|
||||
let [, prefix, alpha] = matches
|
||||
|
||||
let newPath = toPath(prefix)
|
||||
newPath.alpha = alpha
|
||||
|
||||
yield newPath
|
||||
}
|
||||
}
|
||||
|
||||
function resolveFunctionKeys(object) {
|
||||
// theme('colors.red.500 / 0.5') -> ['colors', 'red', '500 / 0', '5]
|
||||
|
||||
const resolvePath = (key, defaultValue) => {
|
||||
for (const path of toPaths(key)) {
|
||||
let index = 0
|
||||
let val = object
|
||||
|
||||
while (val !== undefined && val !== null && index < path.length) {
|
||||
val = val[path[index++]]
|
||||
|
||||
let shouldResolveAsFn =
|
||||
isFunction(val) && (path.alpha === undefined || index <= path.length - 1)
|
||||
|
||||
val = shouldResolveAsFn ? val(resolvePath, configUtils) : val
|
||||
}
|
||||
|
||||
if (val !== undefined) {
|
||||
if (path.alpha !== undefined) {
|
||||
let normalized = parseColorFormat(val)
|
||||
|
||||
return withAlphaValue(normalized, path.alpha, toColorValue(normalized))
|
||||
}
|
||||
|
||||
if (isPlainObject(val)) {
|
||||
return cloneDeep(val)
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
Object.assign(resolvePath, {
|
||||
theme: resolvePath,
|
||||
...configUtils,
|
||||
})
|
||||
|
||||
return Object.keys(object).reduce((resolved, key) => {
|
||||
resolved[key] = isFunction(object[key]) ? object[key](resolvePath, configUtils) : object[key]
|
||||
|
||||
return resolved
|
||||
}, {})
|
||||
}
|
||||
|
||||
function extractPluginConfigs(configs) {
|
||||
let allConfigs = []
|
||||
|
||||
configs.forEach((config) => {
|
||||
allConfigs = [...allConfigs, config]
|
||||
|
||||
const plugins = config?.plugins ?? []
|
||||
|
||||
if (plugins.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
plugins.forEach((plugin) => {
|
||||
if (plugin.__isOptionsFunction) {
|
||||
plugin = plugin()
|
||||
}
|
||||
allConfigs = [...allConfigs, ...extractPluginConfigs([plugin?.config ?? {}])]
|
||||
})
|
||||
})
|
||||
|
||||
return allConfigs
|
||||
}
|
||||
|
||||
function resolveCorePlugins(corePluginConfigs) {
|
||||
const result = [...corePluginConfigs].reduceRight((resolved, corePluginConfig) => {
|
||||
if (isFunction(corePluginConfig)) {
|
||||
return corePluginConfig({ corePlugins: resolved })
|
||||
}
|
||||
return configurePlugins(corePluginConfig, resolved)
|
||||
}, corePluginList)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function resolvePluginLists(pluginLists) {
|
||||
const result = [...pluginLists].reduceRight((resolved, pluginList) => {
|
||||
return [...resolved, ...pluginList]
|
||||
}, [])
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export default function resolveConfig(configs) {
|
||||
let allConfigs = [
|
||||
...extractPluginConfigs(configs),
|
||||
{
|
||||
prefix: '',
|
||||
important: false,
|
||||
separator: ':',
|
||||
variantOrder: defaultConfig.variantOrder,
|
||||
},
|
||||
]
|
||||
|
||||
return normalizeConfig(
|
||||
defaults(
|
||||
{
|
||||
theme: resolveFunctionKeys(
|
||||
mergeExtensions(mergeThemes(allConfigs.map((t) => t?.theme ?? {})))
|
||||
),
|
||||
corePlugins: resolveCorePlugins(allConfigs.map((c) => c.corePlugins)),
|
||||
plugins: resolvePluginLists(configs.map((c) => c?.plugins ?? [])),
|
||||
},
|
||||
...allConfigs
|
||||
)
|
||||
)
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
function isObject(value) {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
function isEmpty(obj) {
|
||||
return Object.keys(obj).length === 0
|
||||
}
|
||||
|
||||
function isString(value) {
|
||||
return typeof value === 'string' || value instanceof String
|
||||
}
|
||||
|
||||
export default function resolveConfigPath(pathOrConfig) {
|
||||
// require('tailwindcss')({ theme: ..., variants: ... })
|
||||
if (isObject(pathOrConfig) && pathOrConfig.config === undefined && !isEmpty(pathOrConfig)) {
|
||||
return null
|
||||
}
|
||||
|
||||
// require('tailwindcss')({ config: 'custom-config.js' })
|
||||
if (
|
||||
isObject(pathOrConfig) &&
|
||||
pathOrConfig.config !== undefined &&
|
||||
isString(pathOrConfig.config)
|
||||
) {
|
||||
return path.resolve(pathOrConfig.config)
|
||||
}
|
||||
|
||||
// require('tailwindcss')({ config: { theme: ..., variants: ... } })
|
||||
if (
|
||||
isObject(pathOrConfig) &&
|
||||
pathOrConfig.config !== undefined &&
|
||||
isObject(pathOrConfig.config)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
// require('tailwindcss')('custom-config.js')
|
||||
if (isString(pathOrConfig)) {
|
||||
return path.resolve(pathOrConfig)
|
||||
}
|
||||
|
||||
// require('tailwindcss')
|
||||
for (const configFile of ['./tailwind.config.js', './tailwind.config.cjs']) {
|
||||
try {
|
||||
const configPath = path.resolve(configFile)
|
||||
fs.accessSync(configPath)
|
||||
return configPath
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import postcss from 'postcss'
|
||||
import cloneNodes from './cloneNodes'
|
||||
|
||||
export default function responsive(rules) {
|
||||
return postcss
|
||||
.atRule({
|
||||
name: 'responsive',
|
||||
})
|
||||
.append(cloneNodes(Array.isArray(rules) ? rules : [rules]))
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import * as regex from '../lib/regex'
|
||||
|
||||
/**
|
||||
* This splits a string on a top-level character.
|
||||
*
|
||||
* Regex doesn't support recursion (at least not the JS-flavored version).
|
||||
* So we have to use a tiny state machine to keep track of paren placement.
|
||||
*
|
||||
* Expected behavior using commas:
|
||||
* var(--a, 0 0 1px rgb(0, 0, 0)), 0 0 1px rgb(0, 0, 0)
|
||||
* ─┬─ ┬ ┬ ┬
|
||||
* x x x ╰──────── Split because top-level
|
||||
* ╰──────────────┴──┴───────────── Ignored b/c inside >= 1 levels of parens
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {string} separator
|
||||
*/
|
||||
export function* splitAtTopLevelOnly(input, separator) {
|
||||
let SPECIALS = new RegExp(`[(){}\\[\\]${regex.escape(separator)}]`, 'g')
|
||||
|
||||
let depth = 0
|
||||
let lastIndex = 0
|
||||
let found = false
|
||||
let separatorIndex = 0
|
||||
let separatorStart = 0
|
||||
let separatorLength = separator.length
|
||||
|
||||
// Find all paren-like things & character
|
||||
// And only split on commas if they're top-level
|
||||
for (let match of input.matchAll(SPECIALS)) {
|
||||
let matchesSeparator = match[0] === separator[separatorIndex]
|
||||
let atEndOfSeparator = separatorIndex === separatorLength - 1
|
||||
let matchesFullSeparator = matchesSeparator && atEndOfSeparator
|
||||
|
||||
if (match[0] === '(') depth++
|
||||
if (match[0] === ')') depth--
|
||||
if (match[0] === '[') depth++
|
||||
if (match[0] === ']') depth--
|
||||
if (match[0] === '{') depth++
|
||||
if (match[0] === '}') depth--
|
||||
|
||||
if (matchesSeparator && depth === 0) {
|
||||
if (separatorStart === 0) {
|
||||
separatorStart = match.index
|
||||
}
|
||||
|
||||
separatorIndex++
|
||||
}
|
||||
|
||||
if (matchesFullSeparator && depth === 0) {
|
||||
found = true
|
||||
|
||||
yield input.substring(lastIndex, separatorStart)
|
||||
lastIndex = separatorStart + separatorLength
|
||||
}
|
||||
|
||||
if (separatorIndex === separatorLength) {
|
||||
separatorIndex = 0
|
||||
separatorStart = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Provide the last segment of the string if available
|
||||
// Otherwise the whole string since no `char`s were found
|
||||
// This mirrors the behavior of string.split()
|
||||
if (found) {
|
||||
yield input.substring(lastIndex)
|
||||
} else {
|
||||
yield input
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export function tap(value, mutator) {
|
||||
mutator(value)
|
||||
return value
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export default function toColorValue(maybeFunction) {
|
||||
return typeof maybeFunction === 'function' ? maybeFunction({}) : maybeFunction
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Parse a path string into an array of path segments.
|
||||
*
|
||||
* Square bracket notation `a[b]` may be used to "escape" dots that would otherwise be interpreted as path separators.
|
||||
*
|
||||
* Example:
|
||||
* a -> ['a']
|
||||
* a.b.c -> ['a', 'b', 'c']
|
||||
* a[b].c -> ['a', 'b', 'c']
|
||||
* a[b.c].e.f -> ['a', 'b.c', 'e', 'f']
|
||||
* a[b][c][d] -> ['a', 'b', 'c', 'd']
|
||||
*
|
||||
* @param {string|string[]} path
|
||||
**/
|
||||
export function toPath(path) {
|
||||
if (Array.isArray(path)) return path
|
||||
|
||||
let openBrackets = path.split('[').length - 1
|
||||
let closedBrackets = path.split(']').length - 1
|
||||
|
||||
if (openBrackets !== closedBrackets) {
|
||||
throw new Error(`Path is invalid. Has unbalanced brackets: ${path}`)
|
||||
}
|
||||
|
||||
return path.split(/\.(?![^\[]*\])|[\[\]]/g).filter(Boolean)
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import postcss from 'postcss'
|
||||
|
||||
export default function transformThemeValue(themeSection) {
|
||||
if (['fontSize', 'outline'].includes(themeSection)) {
|
||||
return (value) => {
|
||||
if (typeof value === 'function') value = value({})
|
||||
if (Array.isArray(value)) value = value[0]
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
'fontFamily',
|
||||
'boxShadow',
|
||||
'transitionProperty',
|
||||
'transitionDuration',
|
||||
'transitionDelay',
|
||||
'transitionTimingFunction',
|
||||
'backgroundImage',
|
||||
'backgroundSize',
|
||||
'backgroundColor',
|
||||
'cursor',
|
||||
'animation',
|
||||
].includes(themeSection)
|
||||
) {
|
||||
return (value) => {
|
||||
if (typeof value === 'function') value = value({})
|
||||
if (Array.isArray(value)) value = value.join(', ')
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// For backwards compatibility reasons, before we switched to underscores
|
||||
// instead of commas for arbitrary values.
|
||||
if (['gridTemplateColumns', 'gridTemplateRows', 'objectPosition'].includes(themeSection)) {
|
||||
return (value) => {
|
||||
if (typeof value === 'function') value = value({})
|
||||
if (typeof value === 'string') value = postcss.list.comma(value).join(' ')
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return (value, opts = {}) => {
|
||||
if (typeof value === 'function') {
|
||||
value = value(opts)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import log from './log'
|
||||
|
||||
export function validateConfig(config) {
|
||||
if (config.content.files.length === 0) {
|
||||
log.warn('content-problems', [
|
||||
'The `content` option in your Tailwind CSS configuration is missing or empty.',
|
||||
'Configure your content sources or your generated CSS will be missing styles.',
|
||||
'https://tailwindcss.com/docs/content-configuration',
|
||||
])
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { parseColor, formatColor } from './color'
|
||||
|
||||
export function withAlphaValue(color, alphaValue, defaultValue) {
|
||||
if (typeof color === 'function') {
|
||||
return color({ opacityValue: alphaValue })
|
||||
}
|
||||
|
||||
let parsed = parseColor(color, { loose: true })
|
||||
|
||||
if (parsed === null) {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
return formatColor({ ...parsed, alpha: alphaValue })
|
||||
}
|
||||
|
||||
export default function withAlphaVariable({ color, property, variable }) {
|
||||
let properties = [].concat(property)
|
||||
if (typeof color === 'function') {
|
||||
return {
|
||||
[variable]: '1',
|
||||
...Object.fromEntries(
|
||||
properties.map((p) => {
|
||||
return [p, color({ opacityVariable: variable, opacityValue: `var(${variable})` })]
|
||||
})
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = parseColor(color)
|
||||
|
||||
if (parsed === null) {
|
||||
return Object.fromEntries(properties.map((p) => [p, color]))
|
||||
}
|
||||
|
||||
if (parsed.alpha !== undefined) {
|
||||
// Has an alpha value, return color as-is
|
||||
return Object.fromEntries(properties.map((p) => [p, color]))
|
||||
}
|
||||
|
||||
return {
|
||||
[variable]: '1',
|
||||
...Object.fromEntries(
|
||||
properties.map((p) => {
|
||||
return [p, formatColor({ ...parsed, alpha: `var(${variable})` })]
|
||||
})
|
||||
),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user