summaryrefslogtreecommitdiff
path: root/node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js
diff options
context:
space:
mode:
authorPhilipp Tanlak <philipp.tanlak@gmail.com>2025-11-24 20:54:57 +0100
committerPhilipp Tanlak <philipp.tanlak@gmail.com>2025-11-24 20:57:48 +0100
commitb1e2c8fd5cb5dfa46bc440a12eafaf56cd844b1c (patch)
tree49d360fd6cbc6a2754efe93524ac47ff0fbe0f7d /node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js
Docs
Diffstat (limited to 'node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js')
-rw-r--r--node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js272
1 files changed, 272 insertions, 0 deletions
diff --git a/node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js b/node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js
new file mode 100644
index 0000000..ff73f46
--- /dev/null
+++ b/node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js
@@ -0,0 +1,272 @@
+import dlv from 'dlv'
+import didYouMean from 'didyoumean'
+import transformThemeValue from '../util/transformThemeValue'
+import parseValue from '../value-parser/index'
+import { normalizeScreens } from '../util/normalizeScreens'
+import buildMediaQuery from '../util/buildMediaQuery'
+import { toPath } from '../util/toPath'
+import { withAlphaValue } from '../util/withAlphaVariable'
+import { parseColorFormat } from '../util/pluginUtils'
+import log from '../util/log'
+
+function isObject(input) {
+ return typeof input === 'object' && input !== null
+}
+
+function findClosestExistingPath(theme, path) {
+ let parts = toPath(path)
+ do {
+ parts.pop()
+
+ if (dlv(theme, parts) !== undefined) break
+ } while (parts.length)
+
+ return parts.length ? parts : undefined
+}
+
+function pathToString(path) {
+ if (typeof path === 'string') return path
+ return path.reduce((acc, cur, i) => {
+ if (cur.includes('.')) return `${acc}[${cur}]`
+ return i === 0 ? cur : `${acc}.${cur}`
+ }, '')
+}
+
+function list(items) {
+ return items.map((key) => `'${key}'`).join(', ')
+}
+
+function listKeys(obj) {
+ return list(Object.keys(obj))
+}
+
+function validatePath(config, path, defaultValue, themeOpts = {}) {
+ const pathString = Array.isArray(path) ? pathToString(path) : path.replace(/^['"]+|['"]+$/g, '')
+ const pathSegments = Array.isArray(path) ? path : toPath(pathString)
+ const value = dlv(config.theme, pathSegments, defaultValue)
+
+ if (value === undefined) {
+ let error = `'${pathString}' does not exist in your theme config.`
+ const parentSegments = pathSegments.slice(0, -1)
+ const parentValue = dlv(config.theme, parentSegments)
+
+ if (isObject(parentValue)) {
+ const validKeys = Object.keys(parentValue).filter(
+ (key) => validatePath(config, [...parentSegments, key]).isValid
+ )
+ const suggestion = didYouMean(pathSegments[pathSegments.length - 1], validKeys)
+ if (suggestion) {
+ error += ` Did you mean '${pathToString([...parentSegments, suggestion])}'?`
+ } else if (validKeys.length > 0) {
+ error += ` '${pathToString(parentSegments)}' has the following valid keys: ${list(
+ validKeys
+ )}`
+ }
+ } else {
+ const closestPath = findClosestExistingPath(config.theme, pathString)
+ if (closestPath) {
+ const closestValue = dlv(config.theme, closestPath)
+ if (isObject(closestValue)) {
+ error += ` '${pathToString(closestPath)}' has the following keys: ${listKeys(
+ closestValue
+ )}`
+ } else {
+ error += ` '${pathToString(closestPath)}' is not an object.`
+ }
+ } else {
+ error += ` Your theme has the following top-level keys: ${listKeys(config.theme)}`
+ }
+ }
+
+ return {
+ isValid: false,
+ error,
+ }
+ }
+
+ if (
+ !(
+ typeof value === 'string' ||
+ typeof value === 'number' ||
+ typeof value === 'function' ||
+ value instanceof String ||
+ value instanceof Number ||
+ Array.isArray(value)
+ )
+ ) {
+ let error = `'${pathString}' was found but does not resolve to a string.`
+
+ if (isObject(value)) {
+ let validKeys = Object.keys(value).filter(
+ (key) => validatePath(config, [...pathSegments, key]).isValid
+ )
+ if (validKeys.length) {
+ error += ` Did you mean something like '${pathToString([...pathSegments, validKeys[0]])}'?`
+ }
+ }
+
+ return {
+ isValid: false,
+ error,
+ }
+ }
+
+ const [themeSection] = pathSegments
+
+ return {
+ isValid: true,
+ value: transformThemeValue(themeSection)(value, themeOpts),
+ }
+}
+
+function extractArgs(node, vNodes, functions) {
+ vNodes = vNodes.map((vNode) => resolveVNode(node, vNode, functions))
+
+ let args = ['']
+
+ for (let vNode of vNodes) {
+ if (vNode.type === 'div' && vNode.value === ',') {
+ args.push('')
+ } else {
+ args[args.length - 1] += parseValue.stringify(vNode)
+ }
+ }
+
+ return args
+}
+
+function resolveVNode(node, vNode, functions) {
+ if (vNode.type === 'function' && functions[vNode.value] !== undefined) {
+ let args = extractArgs(node, vNode.nodes, functions)
+ vNode.type = 'word'
+ vNode.value = functions[vNode.value](node, ...args)
+ }
+
+ return vNode
+}
+
+function resolveFunctions(node, input, functions) {
+ let hasAnyFn = Object.keys(functions).some((fn) => input.includes(`${fn}(`))
+ if (!hasAnyFn) return input
+
+ return parseValue(input)
+ .walk((vNode) => {
+ resolveVNode(node, vNode, functions)
+ })
+ .toString()
+}
+
+let nodeTypePropertyMap = {
+ atrule: 'params',
+ decl: 'value',
+}
+
+/**
+ * @param {string} path
+ * @returns {Iterable<[path: string, alpha: string|undefined]>}
+ */
+function* toPaths(path) {
+ // Strip quotes from beginning and end of string
+ // This allows the alpha value to be present inside of quotes
+ path = path.replace(/^['"]+|['"]+$/g, '')
+
+ let matches = path.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]+))$/)
+ let alpha = undefined
+
+ yield [path, undefined]
+
+ if (matches) {
+ path = matches[1]
+ alpha = matches[2]
+
+ yield [path, alpha]
+ }
+}
+
+/**
+ *
+ * @param {any} config
+ * @param {string} path
+ * @param {any} defaultValue
+ */
+function resolvePath(config, path, defaultValue) {
+ const results = Array.from(toPaths(path)).map(([path, alpha]) => {
+ return Object.assign(validatePath(config, path, defaultValue, { opacityValue: alpha }), {
+ resolvedPath: path,
+ alpha,
+ })
+ })
+
+ return results.find((result) => result.isValid) ?? results[0]
+}
+
+export default function (context) {
+ let config = context.tailwindConfig
+
+ let functions = {
+ theme: (node, path, ...defaultValue) => {
+ let { isValid, value, error, alpha } = resolvePath(
+ config,
+ path,
+ defaultValue.length ? defaultValue : undefined
+ )
+
+ if (!isValid) {
+ let parentNode = node.parent
+ let candidate = parentNode?.raws.tailwind?.candidate
+
+ if (parentNode && candidate !== undefined) {
+ // Remove this utility from any caches
+ context.markInvalidUtilityNode(parentNode)
+
+ // Remove the CSS node from the markup
+ parentNode.remove()
+
+ // Show a warning
+ log.warn('invalid-theme-key-in-class', [
+ `The utility \`${candidate}\` contains an invalid theme value and was not generated.`,
+ ])
+
+ return
+ }
+
+ throw node.error(error)
+ }
+
+ let maybeColor = parseColorFormat(value)
+ let isColorFunction = maybeColor !== undefined && typeof maybeColor === 'function'
+
+ if (alpha !== undefined || isColorFunction) {
+ if (alpha === undefined) {
+ alpha = 1.0
+ }
+
+ value = withAlphaValue(maybeColor, alpha, maybeColor)
+ }
+
+ return value
+ },
+ screen: (node, screen) => {
+ screen = screen.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
+ let screens = normalizeScreens(config.theme.screens)
+ let screenDefinition = screens.find(({ name }) => name === screen)
+
+ if (!screenDefinition) {
+ throw node.error(`The '${screen}' screen does not exist in your theme.`)
+ }
+
+ return buildMediaQuery(screenDefinition)
+ },
+ }
+ return (root) => {
+ root.walk((node) => {
+ let property = nodeTypePropertyMap[node.type]
+
+ if (property === undefined) {
+ return
+ }
+
+ node[property] = resolveFunctions(node, node[property], functions)
+ })
+ }
+}