source: frontend/node_modules/tailwindcss/src/lib/generateRules.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: 28.4 KB
RevLine 
[9af201e]1import postcss from 'postcss'
2import selectorParser from 'postcss-selector-parser'
3import parseObjectStyles from '../util/parseObjectStyles'
4import isPlainObject from '../util/isPlainObject'
5import prefixSelector from '../util/prefixSelector'
6import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils'
7import log from '../util/log'
8import * as sharedState from './sharedState'
9import {
10 formatVariantSelector,
11 finalizeSelector,
12 eliminateIrrelevantSelectors,
13} from '../util/formatVariantSelector'
14import { asClass } from '../util/nameClass'
15import { normalize } from '../util/dataTypes'
16import { isValidVariantFormatString, parseVariant, INTERNAL_FEATURES } from './setupContextUtils'
17import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue'
18import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
19import { flagEnabled } from '../featureFlags'
20import { applyImportantSelector } from '../util/applyImportantSelector'
21
22let classNameParser = selectorParser((selectors) => {
23 return selectors.first.filter(({ type }) => type === 'class').pop().value
24})
25
26export function getClassNameFromSelector(selector) {
27 return classNameParser.transformSync(selector)
28}
29
30// Generate match permutations for a class candidate, like:
31// ['ring-offset-blue', '100']
32// ['ring-offset', 'blue-100']
33// ['ring', 'offset-blue-100']
34// Example with dynamic classes:
35// ['grid-cols', '[[linename],1fr,auto]']
36// ['grid', 'cols-[[linename],1fr,auto]']
37function* candidatePermutations(candidate) {
38 let lastIndex = Infinity
39
40 while (lastIndex >= 0) {
41 let dashIdx
42 let wasSlash = false
43
44 if (lastIndex === Infinity && candidate.endsWith(']')) {
45 let bracketIdx = candidate.indexOf('[')
46
47 // If character before `[` isn't a dash or a slash, this isn't a dynamic class
48 // eg. string[]
49 if (candidate[bracketIdx - 1] === '-') {
50 dashIdx = bracketIdx - 1
51 } else if (candidate[bracketIdx - 1] === '/') {
52 dashIdx = bracketIdx - 1
53 wasSlash = true
54 } else {
55 dashIdx = -1
56 }
57 } else if (lastIndex === Infinity && candidate.includes('/')) {
58 dashIdx = candidate.lastIndexOf('/')
59 wasSlash = true
60 } else {
61 dashIdx = candidate.lastIndexOf('-', lastIndex)
62 }
63
64 if (dashIdx < 0) {
65 break
66 }
67
68 let prefix = candidate.slice(0, dashIdx)
69 let modifier = candidate.slice(wasSlash ? dashIdx : dashIdx + 1)
70
71 lastIndex = dashIdx - 1
72
73 // TODO: This feels a bit hacky
74 if (prefix === '' || modifier === '/') {
75 continue
76 }
77
78 yield [prefix, modifier]
79 }
80}
81
82function applyPrefix(matches, context) {
83 if (matches.length === 0 || context.tailwindConfig.prefix === '') {
84 return matches
85 }
86
87 for (let match of matches) {
88 let [meta] = match
89 if (meta.options.respectPrefix) {
90 let container = postcss.root({ nodes: [match[1].clone()] })
91 let classCandidate = match[1].raws.tailwind.classCandidate
92
93 container.walkRules((r) => {
94 // If this is a negative utility with a dash *before* the prefix we
95 // have to ensure that the generated selector matches the candidate
96
97 // Not doing this will cause `-tw-top-1` to generate the class `.tw--top-1`
98 // The disconnect between candidate <-> class can cause @apply to hard crash.
99 let shouldPrependNegative = classCandidate.startsWith('-')
100
101 r.selector = prefixSelector(
102 context.tailwindConfig.prefix,
103 r.selector,
104 shouldPrependNegative
105 )
106 })
107
108 match[1] = container.nodes[0]
109 }
110 }
111
112 return matches
113}
114
115function applyImportant(matches, classCandidate) {
116 if (matches.length === 0) {
117 return matches
118 }
119
120 let result = []
121
122 function isInKeyframes(rule) {
123 return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
124 }
125
126 for (let [meta, rule] of matches) {
127 let container = postcss.root({ nodes: [rule.clone()] })
128
129 container.walkRules((r) => {
130 // Declarations inside keyframes cannot be marked as important
131 // They will be ignored by the browser
132 if (isInKeyframes(r)) {
133 return
134 }
135
136 let ast = selectorParser().astSync(r.selector)
137
138 // Remove extraneous selectors that do not include the base candidate
139 ast.each((sel) => eliminateIrrelevantSelectors(sel, classCandidate))
140
141 // Update all instances of the base candidate to include the important marker
142 updateAllClasses(ast, (className) =>
143 className === classCandidate ? `!${className}` : className
144 )
145
146 let newSelector = ast.toString()
147
148 if (newSelector.trim() === '') {
149 r.remove()
150 return
151 }
152
153 r.selector = newSelector
154
155 r.walkDecls((d) => (d.important = true))
156 })
157
158 result.push([{ ...meta, important: true }, container.nodes[0]])
159 }
160
161 return result
162}
163
164// Takes a list of rule tuples and applies a variant like `hover`, sm`,
165// whatever to it. We used to do some extra caching here to avoid generating
166// a variant of the same rule more than once, but this was never hit because
167// we cache at the entire selector level further up the tree.
168//
169// Technically you can get a cache hit if you have `hover:focus:text-center`
170// and `focus:hover:text-center` in the same project, but it doesn't feel
171// worth the complexity for that case.
172
173function applyVariant(variant, matches, context) {
174 if (matches.length === 0) {
175 return matches
176 }
177
178 /** @type {{modifier: string | null, value: string | null}} */
179 let args = { modifier: null, value: sharedState.NONE }
180
181 // Retrieve "modifier"
182 {
183 let [baseVariant, ...modifiers] = splitAtTopLevelOnly(variant, '/')
184
185 // This is a hack to support variants with `/` in them, like `ar-1/10/20:text-red-500`
186 // In this case 1/10 is a value but /20 is a modifier
187 if (modifiers.length > 1) {
188 baseVariant = baseVariant + '/' + modifiers.slice(0, -1).join('/')
189 modifiers = modifiers.slice(-1)
190 }
191
192 if (modifiers.length && !context.variantMap.has(variant)) {
193 variant = baseVariant
194 args.modifier = modifiers[0]
195
196 if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
197 return []
198 }
199 }
200 }
201
202 // Retrieve "arbitrary value"
203 if (variant.endsWith(']') && !variant.startsWith('[')) {
204 // We either have:
205 // @[200px]
206 // group-[:hover]
207 //
208 // But we don't want:
209 // @-[200px] (`-` is incorrect)
210 // group[:hover] (`-` is missing)
211 let match = /(.)(-?)\[(.*)\]/g.exec(variant)
212 if (match) {
213 let [, char, separator, value] = match
214 // @-[200px] case
215 if (char === '@' && separator === '-') return []
216 // group[:hover] case
217 if (char !== '@' && separator === '') return []
218
219 variant = variant.replace(`${separator}[${value}]`, '')
220 args.value = value
221 }
222 }
223
224 // Register arbitrary variants
225 if (isArbitraryValue(variant) && !context.variantMap.has(variant)) {
226 let sort = context.offsets.recordVariant(variant)
227
228 let selector = normalize(variant.slice(1, -1))
229 let selectors = splitAtTopLevelOnly(selector, ',')
230
231 // We do not support multiple selectors for arbitrary variants
232 if (selectors.length > 1) {
233 return []
234 }
235
236 if (!selectors.every(isValidVariantFormatString)) {
237 return []
238 }
239
240 let records = selectors.map((sel, idx) => [
241 context.offsets.applyParallelOffset(sort, idx),
242 parseVariant(sel.trim()),
243 ])
244
245 context.variantMap.set(variant, records)
246 }
247
248 if (context.variantMap.has(variant)) {
249 let isArbitraryVariant = isArbitraryValue(variant)
250 let internalFeatures = context.variantOptions.get(variant)?.[INTERNAL_FEATURES] ?? {}
251 let variantFunctionTuples = context.variantMap.get(variant).slice()
252 let result = []
253
254 let respectPrefix = (() => {
255 if (isArbitraryVariant) return false
256 if (internalFeatures.respectPrefix === false) return false
257 return true
258 })()
259
260 for (let [meta, rule] of matches) {
261 // Don't generate variants for user css
262 if (meta.layer === 'user') {
263 continue
264 }
265
266 let container = postcss.root({ nodes: [rule.clone()] })
267
268 for (let [variantSort, variantFunction, containerFromArray] of variantFunctionTuples) {
269 let clone = (containerFromArray ?? container).clone()
270 let collectedFormats = []
271
272 function prepareBackup() {
273 // Already prepared, chicken out
274 if (clone.raws.neededBackup) {
275 return
276 }
277 clone.raws.neededBackup = true
278 clone.walkRules((rule) => (rule.raws.originalSelector = rule.selector))
279 }
280
281 function modifySelectors(modifierFunction) {
282 prepareBackup()
283 clone.each((rule) => {
284 if (rule.type !== 'rule') {
285 return
286 }
287
288 rule.selectors = rule.selectors.map((selector) => {
289 return modifierFunction({
290 get className() {
291 return getClassNameFromSelector(selector)
292 },
293 selector,
294 })
295 })
296 })
297
298 return clone
299 }
300
301 let ruleWithVariant = variantFunction({
302 // Public API
303 get container() {
304 prepareBackup()
305 return clone
306 },
307 separator: context.tailwindConfig.separator,
308 modifySelectors,
309
310 // Private API for now
311 wrap(wrapper) {
312 let nodes = clone.nodes
313 clone.removeAll()
314 wrapper.append(nodes)
315 clone.append(wrapper)
316 },
317 format(selectorFormat) {
318 collectedFormats.push({
319 format: selectorFormat,
320 respectPrefix,
321 })
322 },
323 args,
324 })
325
326 // It can happen that a list of format strings is returned from within the function. In that
327 // case, we have to process them as well. We can use the existing `variantSort`.
328 if (Array.isArray(ruleWithVariant)) {
329 for (let [idx, variantFunction] of ruleWithVariant.entries()) {
330 // This is a little bit scary since we are pushing to an array of items that we are
331 // currently looping over. However, you can also think of it like a processing queue
332 // where you keep handling jobs until everything is done and each job can queue more
333 // jobs if needed.
334 variantFunctionTuples.push([
335 context.offsets.applyParallelOffset(variantSort, idx),
336 variantFunction,
337
338 // If the clone has been modified we have to pass that back
339 // though so each rule can use the modified container
340 clone.clone(),
341 ])
342 }
343 continue
344 }
345
346 if (typeof ruleWithVariant === 'string') {
347 collectedFormats.push({
348 format: ruleWithVariant,
349 respectPrefix,
350 })
351 }
352
353 if (ruleWithVariant === null) {
354 continue
355 }
356
357 // We had to backup selectors, therefore we assume that somebody touched
358 // `container` or `modifySelectors`. Let's see if they did, so that we
359 // can restore the selectors, and collect the format strings.
360 if (clone.raws.neededBackup) {
361 delete clone.raws.neededBackup
362 clone.walkRules((rule) => {
363 let before = rule.raws.originalSelector
364 if (!before) return
365 delete rule.raws.originalSelector
366 if (before === rule.selector) return // No mutation happened
367
368 let modified = rule.selector
369
370 // Rebuild the base selector, this is what plugin authors would do
371 // as well. E.g.: `${variant}${separator}${className}`.
372 // However, plugin authors probably also prepend or append certain
373 // classes, pseudos, ids, ...
374 let rebuiltBase = selectorParser((selectors) => {
375 selectors.walkClasses((classNode) => {
376 classNode.value = `${variant}${context.tailwindConfig.separator}${classNode.value}`
377 })
378 }).processSync(before)
379
380 // Now that we know the original selector, the new selector, and
381 // the rebuild part in between, we can replace the part that plugin
382 // authors need to rebuild with `&`, and eventually store it in the
383 // collectedFormats. Similar to what `format('...')` would do.
384 //
385 // E.g.:
386 // variant: foo
387 // selector: .markdown > p
388 // modified (by plugin): .foo .foo\\:markdown > p
389 // rebuiltBase (internal): .foo\\:markdown > p
390 // format: .foo &
391 collectedFormats.push({
392 format: modified.replace(rebuiltBase, '&'),
393 respectPrefix,
394 })
395 rule.selector = before
396 })
397 }
398
399 // This tracks the originating layer for the variant
400 // For example:
401 // .sm:underline {} is a variant of something in the utilities layer
402 // .sm:container {} is a variant of the container component
403 clone.nodes[0].raws.tailwind = { ...clone.nodes[0].raws.tailwind, parentLayer: meta.layer }
404
405 let withOffset = [
406 {
407 ...meta,
408 sort: context.offsets.applyVariantOffset(
409 meta.sort,
410 variantSort,
411 Object.assign(args, context.variantOptions.get(variant))
412 ),
413 collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
414 },
415 clone.nodes[0],
416 ]
417 result.push(withOffset)
418 }
419 }
420
421 return result
422 }
423
424 return []
425}
426
427function parseRules(rule, cache, options = {}) {
428 // PostCSS node
429 if (!isPlainObject(rule) && !Array.isArray(rule)) {
430 return [[rule], options]
431 }
432
433 // Tuple
434 if (Array.isArray(rule)) {
435 return parseRules(rule[0], cache, rule[1])
436 }
437
438 // Simple object
439 if (!cache.has(rule)) {
440 cache.set(rule, parseObjectStyles(rule))
441 }
442
443 return [cache.get(rule), options]
444}
445
446const IS_VALID_PROPERTY_NAME = /^[a-z_-]/
447
448function isValidPropName(name) {
449 return IS_VALID_PROPERTY_NAME.test(name)
450}
451
452/**
453 * @param {string} declaration
454 * @returns {boolean}
455 */
456function looksLikeUri(declaration) {
457 // Quick bailout for obvious non-urls
458 // This doesn't support schemes that don't use a leading // but that's unlikely to be a problem
459 if (!declaration.includes('://')) {
460 return false
461 }
462
463 try {
464 const url = new URL(declaration)
465 return url.scheme !== '' && url.host !== ''
466 } catch (err) {
467 // Definitely not a valid url
468 return false
469 }
470}
471
472function isParsableNode(node) {
473 let isParsable = true
474
475 node.walkDecls((decl) => {
476 if (!isParsableCssValue(decl.prop, decl.value)) {
477 isParsable = false
478 return false
479 }
480 })
481
482 return isParsable
483}
484
485function isParsableCssValue(property, value) {
486 // We don't want to to treat [https://example.com] as a custom property
487 // Even though, according to the CSS grammar, it's a totally valid CSS declaration
488 // So we short-circuit here by checking if the custom property looks like a url
489 if (looksLikeUri(`${property}:${value}`)) {
490 return false
491 }
492
493 try {
494 postcss.parse(`a{${property}:${value}}`).toResult()
495 return true
496 } catch (err) {
497 return false
498 }
499}
500
501function extractArbitraryProperty(classCandidate, context) {
502 let [, property, value] = classCandidate.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/) ?? []
503
504 if (value === undefined) {
505 return null
506 }
507
508 if (!isValidPropName(property)) {
509 return null
510 }
511
512 if (!isValidArbitraryValue(value)) {
513 return null
514 }
515
516 let normalized = normalize(value, { property })
517
518 if (!isParsableCssValue(property, normalized)) {
519 return null
520 }
521
522 let sort = context.offsets.arbitraryProperty(classCandidate)
523
524 return [
525 [
526 { sort, layer: 'utilities', options: { respectImportant: true } },
527 () => ({
528 [asClass(classCandidate)]: {
529 [property]: normalized,
530 },
531 }),
532 ],
533 ]
534}
535
536function* resolveMatchedPlugins(classCandidate, context) {
537 if (context.candidateRuleMap.has(classCandidate)) {
538 yield [context.candidateRuleMap.get(classCandidate), 'DEFAULT']
539 }
540
541 yield* (function* (arbitraryPropertyRule) {
542 if (arbitraryPropertyRule !== null) {
543 yield [arbitraryPropertyRule, 'DEFAULT']
544 }
545 })(extractArbitraryProperty(classCandidate, context))
546
547 let candidatePrefix = classCandidate
548 let negative = false
549
550 const twConfigPrefix = context.tailwindConfig.prefix
551
552 const twConfigPrefixLen = twConfigPrefix.length
553
554 const hasMatchingPrefix =
555 candidatePrefix.startsWith(twConfigPrefix) || candidatePrefix.startsWith(`-${twConfigPrefix}`)
556
557 if (candidatePrefix[twConfigPrefixLen] === '-' && hasMatchingPrefix) {
558 negative = true
559 candidatePrefix = twConfigPrefix + candidatePrefix.slice(twConfigPrefixLen + 1)
560 }
561
562 if (negative && context.candidateRuleMap.has(candidatePrefix)) {
563 yield [context.candidateRuleMap.get(candidatePrefix), '-DEFAULT']
564 }
565
566 for (let [prefix, modifier] of candidatePermutations(candidatePrefix)) {
567 if (context.candidateRuleMap.has(prefix)) {
568 yield [context.candidateRuleMap.get(prefix), negative ? `-${modifier}` : modifier]
569 }
570 }
571}
572
573function splitWithSeparator(input, separator) {
574 if (input === sharedState.NOT_ON_DEMAND) {
575 return [sharedState.NOT_ON_DEMAND]
576 }
577
578 return splitAtTopLevelOnly(input, separator)
579}
580
581function* recordCandidates(matches, classCandidate) {
582 for (const match of matches) {
583 match[1].raws.tailwind = {
584 ...match[1].raws.tailwind,
585 classCandidate,
586 preserveSource: match[0].options?.preserveSource ?? false,
587 }
588
589 yield match
590 }
591}
592
593function* resolveMatches(candidate, context) {
594 let separator = context.tailwindConfig.separator
595 let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse()
596 let important = false
597
598 if (classCandidate.startsWith('!')) {
599 important = true
600 classCandidate = classCandidate.slice(1)
601 }
602
603 // TODO: Reintroduce this in ways that doesn't break on false positives
604 // function sortAgainst(toSort, against) {
605 // return toSort.slice().sort((a, z) => {
606 // return bigSign(against.get(a)[0] - against.get(z)[0])
607 // })
608 // }
609 // let sorted = sortAgainst(variants, context.variantMap)
610 // if (sorted.toString() !== variants.toString()) {
611 // let corrected = sorted.reverse().concat(classCandidate).join(':')
612 // throw new Error(`Class ${candidate} should be written as ${corrected}`)
613 // }
614
615 for (let matchedPlugins of resolveMatchedPlugins(classCandidate, context)) {
616 let matches = []
617 let typesByMatches = new Map()
618
619 let [plugins, modifier] = matchedPlugins
620 let isOnlyPlugin = plugins.length === 1
621
622 for (let [sort, plugin] of plugins) {
623 let matchesPerPlugin = []
624
625 if (typeof plugin === 'function') {
626 for (let ruleSet of [].concat(plugin(modifier, { isOnlyPlugin }))) {
627 let [rules, options] = parseRules(ruleSet, context.postCssNodeCache)
628 for (let rule of rules) {
629 matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
630 }
631 }
632 }
633 // Only process static plugins on exact matches
634 else if (modifier === 'DEFAULT' || modifier === '-DEFAULT') {
635 let ruleSet = plugin
636 let [rules, options] = parseRules(ruleSet, context.postCssNodeCache)
637 for (let rule of rules) {
638 matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule])
639 }
640 }
641
642 if (matchesPerPlugin.length > 0) {
643 let matchingTypes = Array.from(
644 getMatchingTypes(
645 sort.options?.types ?? [],
646 modifier,
647 sort.options ?? {},
648 context.tailwindConfig
649 )
650 ).map(([_, type]) => type)
651
652 if (matchingTypes.length > 0) {
653 typesByMatches.set(matchesPerPlugin, matchingTypes)
654 }
655
656 matches.push(matchesPerPlugin)
657 }
658 }
659
660 if (isArbitraryValue(modifier)) {
661 if (matches.length > 1) {
662 // Partition plugins in 2 categories so that we can start searching in the plugins that
663 // don't have `any` as a type first.
664 let [withAny, withoutAny] = matches.reduce(
665 (group, plugin) => {
666 let hasAnyType = plugin.some(([{ options }]) =>
667 options.types.some(({ type }) => type === 'any')
668 )
669
670 if (hasAnyType) {
671 group[0].push(plugin)
672 } else {
673 group[1].push(plugin)
674 }
675 return group
676 },
677 [[], []]
678 )
679
680 function findFallback(matches) {
681 // If only a single plugin matches, let's take that one
682 if (matches.length === 1) {
683 return matches[0]
684 }
685
686 // Otherwise, find the plugin that creates a valid rule given the arbitrary value, and
687 // also has the correct type which preferOnConflicts the plugin in case of clashes.
688 return matches.find((rules) => {
689 let matchingTypes = typesByMatches.get(rules)
690 return rules.some(([{ options }, rule]) => {
691 if (!isParsableNode(rule)) {
692 return false
693 }
694
695 return options.types.some(
696 ({ type, preferOnConflict }) => matchingTypes.includes(type) && preferOnConflict
697 )
698 })
699 })
700 }
701
702 // Try to find a fallback plugin, because we already know that multiple plugins matched for
703 // the given arbitrary value.
704 let fallback = findFallback(withoutAny) ?? findFallback(withAny)
705 if (fallback) {
706 matches = [fallback]
707 }
708
709 // We couldn't find a fallback plugin which means that there are now multiple plugins that
710 // generated css for the current candidate. This means that the result is ambiguous and this
711 // should not happen. We won't generate anything right now, so let's report this to the user
712 // by logging some options about what they can do.
713 else {
714 let typesPerPlugin = matches.map(
715 (match) => new Set([...(typesByMatches.get(match) ?? [])])
716 )
717
718 // Remove duplicates, so that we can detect proper unique types for each plugin.
719 for (let pluginTypes of typesPerPlugin) {
720 for (let type of pluginTypes) {
721 let removeFromOwnGroup = false
722
723 for (let otherGroup of typesPerPlugin) {
724 if (pluginTypes === otherGroup) continue
725
726 if (otherGroup.has(type)) {
727 otherGroup.delete(type)
728 removeFromOwnGroup = true
729 }
730 }
731
732 if (removeFromOwnGroup) pluginTypes.delete(type)
733 }
734 }
735
736 let messages = []
737
738 for (let [idx, group] of typesPerPlugin.entries()) {
739 for (let type of group) {
740 let rules = matches[idx]
741 .map(([, rule]) => rule)
742 .flat()
743 .map((rule) =>
744 rule
745 .toString()
746 .split('\n')
747 .slice(1, -1) // Remove selector and closing '}'
748 .map((line) => line.trim())
749 .map((x) => ` ${x}`) // Re-indent
750 .join('\n')
751 )
752 .join('\n\n')
753
754 messages.push(
755 ` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\``
756 )
757 break
758 }
759 }
760
761 log.warn([
762 `The class \`${candidate}\` is ambiguous and matches multiple utilities.`,
763 ...messages,
764 `If this is content and not a class, replace it with \`${candidate
765 .replace('[', '&lsqb;')
766 .replace(']', '&rsqb;')}\` to silence this warning.`,
767 ])
768 continue
769 }
770 }
771
772 matches = matches.map((list) => list.filter((match) => isParsableNode(match[1])))
773 }
774
775 matches = matches.flat()
776 matches = Array.from(recordCandidates(matches, classCandidate))
777 matches = applyPrefix(matches, context)
778
779 if (important) {
780 matches = applyImportant(matches, classCandidate)
781 }
782
783 for (let variant of variants) {
784 matches = applyVariant(variant, matches, context)
785 }
786
787 for (let match of matches) {
788 match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate }
789
790 // Apply final format selector
791 match = applyFinalFormat(match, { context, candidate })
792
793 // Skip rules with invalid selectors
794 // This will cause the candidate to be added to the "not class"
795 // cache skipping it entirely for future builds
796 if (match === null) {
797 continue
798 }
799
800 yield match
801 }
802 }
803}
804
805function applyFinalFormat(match, { context, candidate }) {
806 if (!match[0].collectedFormats) {
807 return match
808 }
809
810 let isValid = true
811 let finalFormat
812
813 try {
814 finalFormat = formatVariantSelector(match[0].collectedFormats, {
815 context,
816 candidate,
817 })
818 } catch {
819 // The format selector we produced is invalid
820 // This could be because:
821 // - A bug exists
822 // - A plugin introduced an invalid variant selector (ex: `addVariant('foo', '&;foo')`)
823 // - The user used an invalid arbitrary variant (ex: `[&;foo]:underline`)
824 // Either way the build will fail because of this
825 // We would rather that the build pass "silently" given that this could
826 // happen because of picking up invalid things when scanning content
827 // So we'll throw out the candidate instead
828
829 return null
830 }
831
832 let container = postcss.root({ nodes: [match[1].clone()] })
833
834 container.walkRules((rule) => {
835 if (inKeyframes(rule)) {
836 return
837 }
838
839 try {
840 let selector = finalizeSelector(rule.selector, finalFormat, {
841 candidate,
842 context,
843 })
844
845 // Finalize Selector determined that this candidate is irrelevant
846 // TODO: This elimination should happen earlier so this never happens
847 if (selector === null) {
848 rule.remove()
849 return
850 }
851
852 rule.selector = selector
853 } catch {
854 // If this selector is invalid we also want to skip it
855 // But it's likely that being invalid here means there's a bug in a plugin rather than too loosely matching content
856 isValid = false
857 return false
858 }
859 })
860
861 if (!isValid) {
862 return null
863 }
864
865 // If all rules have been eliminated we can skip this candidate entirely
866 if (container.nodes.length === 0) {
867 return null
868 }
869
870 match[1] = container.nodes[0]
871
872 return match
873}
874
875function inKeyframes(rule) {
876 return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
877}
878
879function getImportantStrategy(important) {
880 if (important === true) {
881 return (rule) => {
882 if (inKeyframes(rule)) {
883 return
884 }
885
886 rule.walkDecls((d) => {
887 if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
888 d.important = true
889 }
890 })
891 }
892 }
893
894 if (typeof important === 'string') {
895 return (rule) => {
896 if (inKeyframes(rule)) {
897 return
898 }
899
900 rule.selectors = rule.selectors.map((selector) => {
901 return applyImportantSelector(selector, important)
902 })
903 }
904 }
905}
906
907function generateRules(candidates, context, isSorting = false) {
908 let allRules = []
909 let strategy = getImportantStrategy(context.tailwindConfig.important)
910
911 for (let candidate of candidates) {
912 if (context.notClassCache.has(candidate)) {
913 continue
914 }
915
916 if (context.candidateRuleCache.has(candidate)) {
917 allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate)))
918 continue
919 }
920
921 let matches = Array.from(resolveMatches(candidate, context))
922
923 if (matches.length === 0) {
924 context.notClassCache.add(candidate)
925 continue
926 }
927
928 context.classCache.set(candidate, matches)
929
930 let rules = context.candidateRuleCache.get(candidate) ?? new Set()
931 context.candidateRuleCache.set(candidate, rules)
932
933 for (const match of matches) {
934 let [{ sort, options }, rule] = match
935
936 if (options.respectImportant && strategy) {
937 let container = postcss.root({ nodes: [rule.clone()] })
938 container.walkRules(strategy)
939 rule = container.nodes[0]
940 }
941
942 // Note: We have to clone rules during sorting
943 // so we eliminate some shared mutable state
944 let newEntry = [sort, isSorting ? rule.clone() : rule]
945 rules.add(newEntry)
946 context.ruleCache.add(newEntry)
947 allRules.push(newEntry)
948 }
949 }
950
951 return allRules
952}
953
954function isArbitraryValue(input) {
955 return input.startsWith('[') && input.endsWith(']')
956}
957
958export { resolveMatches, generateRules }
Note: See TracBrowser for help on using the repository browser.