diff options
Diffstat (limited to 'node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js')
| -rw-r--r-- | node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js | 272 |
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) + }) + } +} |