source: frontend/node_modules/tailwindcss/src/lib/evaluateTailwindFunctions.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 12 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 7.2 KB
Line 
1import dlv from 'dlv'
2import didYouMean from 'didyoumean'
3import transformThemeValue from '../util/transformThemeValue'
4import parseValue from '../value-parser/index'
5import { normalizeScreens } from '../util/normalizeScreens'
6import buildMediaQuery from '../util/buildMediaQuery'
7import { toPath } from '../util/toPath'
8import { withAlphaValue } from '../util/withAlphaVariable'
9import { parseColorFormat } from '../util/pluginUtils'
10import log from '../util/log'
11
12function isObject(input) {
13 return typeof input === 'object' && input !== null
14}
15
16function findClosestExistingPath(theme, path) {
17 let parts = toPath(path)
18 do {
19 parts.pop()
20
21 if (dlv(theme, parts) !== undefined) break
22 } while (parts.length)
23
24 return parts.length ? parts : undefined
25}
26
27function pathToString(path) {
28 if (typeof path === 'string') return path
29 return path.reduce((acc, cur, i) => {
30 if (cur.includes('.')) return `${acc}[${cur}]`
31 return i === 0 ? cur : `${acc}.${cur}`
32 }, '')
33}
34
35function list(items) {
36 return items.map((key) => `'${key}'`).join(', ')
37}
38
39function listKeys(obj) {
40 return list(Object.keys(obj))
41}
42
43function validatePath(config, path, defaultValue, themeOpts = {}) {
44 const pathString = Array.isArray(path) ? pathToString(path) : path.replace(/^['"]+|['"]+$/g, '')
45 const pathSegments = Array.isArray(path) ? path : toPath(pathString)
46 const value = dlv(config.theme, pathSegments, defaultValue)
47
48 if (value === undefined) {
49 let error = `'${pathString}' does not exist in your theme config.`
50 const parentSegments = pathSegments.slice(0, -1)
51 const parentValue = dlv(config.theme, parentSegments)
52
53 if (isObject(parentValue)) {
54 const validKeys = Object.keys(parentValue).filter(
55 (key) => validatePath(config, [...parentSegments, key]).isValid
56 )
57 const suggestion = didYouMean(pathSegments[pathSegments.length - 1], validKeys)
58 if (suggestion) {
59 error += ` Did you mean '${pathToString([...parentSegments, suggestion])}'?`
60 } else if (validKeys.length > 0) {
61 error += ` '${pathToString(parentSegments)}' has the following valid keys: ${list(
62 validKeys
63 )}`
64 }
65 } else {
66 const closestPath = findClosestExistingPath(config.theme, pathString)
67 if (closestPath) {
68 const closestValue = dlv(config.theme, closestPath)
69 if (isObject(closestValue)) {
70 error += ` '${pathToString(closestPath)}' has the following keys: ${listKeys(
71 closestValue
72 )}`
73 } else {
74 error += ` '${pathToString(closestPath)}' is not an object.`
75 }
76 } else {
77 error += ` Your theme has the following top-level keys: ${listKeys(config.theme)}`
78 }
79 }
80
81 return {
82 isValid: false,
83 error,
84 }
85 }
86
87 if (
88 !(
89 typeof value === 'string' ||
90 typeof value === 'number' ||
91 typeof value === 'function' ||
92 value instanceof String ||
93 value instanceof Number ||
94 Array.isArray(value)
95 )
96 ) {
97 let error = `'${pathString}' was found but does not resolve to a string.`
98
99 if (isObject(value)) {
100 let validKeys = Object.keys(value).filter(
101 (key) => validatePath(config, [...pathSegments, key]).isValid
102 )
103 if (validKeys.length) {
104 error += ` Did you mean something like '${pathToString([...pathSegments, validKeys[0]])}'?`
105 }
106 }
107
108 return {
109 isValid: false,
110 error,
111 }
112 }
113
114 const [themeSection] = pathSegments
115
116 return {
117 isValid: true,
118 value: transformThemeValue(themeSection)(value, themeOpts),
119 }
120}
121
122function extractArgs(node, vNodes, functions) {
123 vNodes = vNodes.map((vNode) => resolveVNode(node, vNode, functions))
124
125 let args = ['']
126
127 for (let vNode of vNodes) {
128 if (vNode.type === 'div' && vNode.value === ',') {
129 args.push('')
130 } else {
131 args[args.length - 1] += parseValue.stringify(vNode)
132 }
133 }
134
135 return args
136}
137
138function resolveVNode(node, vNode, functions) {
139 if (vNode.type === 'function' && functions[vNode.value] !== undefined) {
140 let args = extractArgs(node, vNode.nodes, functions)
141 vNode.type = 'word'
142 vNode.value = functions[vNode.value](node, ...args)
143 }
144
145 return vNode
146}
147
148function resolveFunctions(node, input, functions) {
149 let hasAnyFn = Object.keys(functions).some((fn) => input.includes(`${fn}(`))
150 if (!hasAnyFn) return input
151
152 return parseValue(input)
153 .walk((vNode) => {
154 resolveVNode(node, vNode, functions)
155 })
156 .toString()
157}
158
159let nodeTypePropertyMap = {
160 atrule: 'params',
161 decl: 'value',
162}
163
164/**
165 * @param {string} path
166 * @returns {Iterable<[path: string, alpha: string|undefined]>}
167 */
168function* toPaths(path) {
169 // Strip quotes from beginning and end of string
170 // This allows the alpha value to be present inside of quotes
171 path = path.replace(/^['"]+|['"]+$/g, '')
172
173 let matches = path.match(/^([^\s]+)(?![^\[]*\])(?:\s*\/\s*([^\/\s]+))$/)
174 let alpha = undefined
175
176 yield [path, undefined]
177
178 if (matches) {
179 path = matches[1]
180 alpha = matches[2]
181
182 yield [path, alpha]
183 }
184}
185
186/**
187 *
188 * @param {any} config
189 * @param {string} path
190 * @param {any} defaultValue
191 */
192function resolvePath(config, path, defaultValue) {
193 const results = Array.from(toPaths(path)).map(([path, alpha]) => {
194 return Object.assign(validatePath(config, path, defaultValue, { opacityValue: alpha }), {
195 resolvedPath: path,
196 alpha,
197 })
198 })
199
200 return results.find((result) => result.isValid) ?? results[0]
201}
202
203export default function (context) {
204 let config = context.tailwindConfig
205
206 let functions = {
207 theme: (node, path, ...defaultValue) => {
208 let { isValid, value, error, alpha } = resolvePath(
209 config,
210 path,
211 defaultValue.length ? defaultValue : undefined
212 )
213
214 if (!isValid) {
215 let parentNode = node.parent
216 let candidate = parentNode?.raws.tailwind?.candidate
217
218 if (parentNode && candidate !== undefined) {
219 // Remove this utility from any caches
220 context.markInvalidUtilityNode(parentNode)
221
222 // Remove the CSS node from the markup
223 parentNode.remove()
224
225 // Show a warning
226 log.warn('invalid-theme-key-in-class', [
227 `The utility \`${candidate}\` contains an invalid theme value and was not generated.`,
228 ])
229
230 return
231 }
232
233 throw node.error(error)
234 }
235
236 let maybeColor = parseColorFormat(value)
237 let isColorFunction = maybeColor !== undefined && typeof maybeColor === 'function'
238
239 if (alpha !== undefined || isColorFunction) {
240 if (alpha === undefined) {
241 alpha = 1.0
242 }
243
244 value = withAlphaValue(maybeColor, alpha, maybeColor)
245 }
246
247 return value
248 },
249 screen: (node, screen) => {
250 screen = screen.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
251 let screens = normalizeScreens(config.theme.screens)
252 let screenDefinition = screens.find(({ name }) => name === screen)
253
254 if (!screenDefinition) {
255 throw node.error(`The '${screen}' screen does not exist in your theme.`)
256 }
257
258 return buildMediaQuery(screenDefinition)
259 },
260 }
261 return (root) => {
262 root.walk((node) => {
263 let property = nodeTypePropertyMap[node.type]
264
265 if (property === undefined) {
266 return
267 }
268
269 node[property] = resolveFunctions(node, node[property], functions)
270 })
271 }
272}
Note: See TracBrowser for help on using the repository browser.