diff options
| author | Philipp Tanlak <philipp.tanlak@gmail.com> | 2025-11-24 20:54:57 +0100 |
|---|---|---|
| committer | Philipp Tanlak <philipp.tanlak@gmail.com> | 2025-11-24 20:57:48 +0100 |
| commit | b1e2c8fd5cb5dfa46bc440a12eafaf56cd844b1c (patch) | |
| tree | 49d360fd6cbc6a2754efe93524ac47ff0fbe0f7d /node_modules/tailwindcss/src/lib/generateRules.js | |
Docs
Diffstat (limited to 'node_modules/tailwindcss/src/lib/generateRules.js')
| -rw-r--r-- | node_modules/tailwindcss/src/lib/generateRules.js | 936 |
1 files changed, 936 insertions, 0 deletions
diff --git a/node_modules/tailwindcss/src/lib/generateRules.js b/node_modules/tailwindcss/src/lib/generateRules.js new file mode 100644 index 0000000..69b6827 --- /dev/null +++ b/node_modules/tailwindcss/src/lib/generateRules.js @@ -0,0 +1,936 @@ +import postcss from 'postcss' +import selectorParser from 'postcss-selector-parser' +import parseObjectStyles from '../util/parseObjectStyles' +import isPlainObject from '../util/isPlainObject' +import prefixSelector from '../util/prefixSelector' +import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils' +import log from '../util/log' +import * as sharedState from './sharedState' +import { + formatVariantSelector, + finalizeSelector, + eliminateIrrelevantSelectors, +} from '../util/formatVariantSelector' +import { asClass } from '../util/nameClass' +import { normalize } from '../util/dataTypes' +import { isValidVariantFormatString, parseVariant, INTERNAL_FEATURES } from './setupContextUtils' +import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue' +import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js' +import { flagEnabled } from '../featureFlags' +import { applyImportantSelector } from '../util/applyImportantSelector' + +let classNameParser = selectorParser((selectors) => { + return selectors.first.filter(({ type }) => type === 'class').pop().value +}) + +export function getClassNameFromSelector(selector) { + return classNameParser.transformSync(selector) +} + +// Generate match permutations for a class candidate, like: +// ['ring-offset-blue', '100'] +// ['ring-offset', 'blue-100'] +// ['ring', 'offset-blue-100'] +// Example with dynamic classes: +// ['grid-cols', '[[linename],1fr,auto]'] +// ['grid', 'cols-[[linename],1fr,auto]'] +function* candidatePermutations(candidate) { + let lastIndex = Infinity + + while (lastIndex >= 0) { + let dashIdx + let wasSlash = false + + if (lastIndex === Infinity && candidate.endsWith(']')) { + let bracketIdx = candidate.indexOf('[') + + // If character before `[` isn't a dash or a slash, this isn't a dynamic class + // eg. string[] + if (candidate[bracketIdx - 1] === '-') { + dashIdx = bracketIdx - 1 + } else if (candidate[bracketIdx - 1] === '/') { + dashIdx = bracketIdx - 1 + wasSlash = true + } else { + dashIdx = -1 + } + } else if (lastIndex === Infinity && candidate.includes('/')) { + dashIdx = candidate.lastIndexOf('/') + wasSlash = true + } else { + dashIdx = candidate.lastIndexOf('-', lastIndex) + } + + if (dashIdx < 0) { + break + } + + let prefix = candidate.slice(0, dashIdx) + let modifier = candidate.slice(wasSlash ? dashIdx : dashIdx + 1) + + lastIndex = dashIdx - 1 + + // TODO: This feels a bit hacky + if (prefix === '' || modifier === '/') { + continue + } + + yield [prefix, modifier] + } +} + +function applyPrefix(matches, context) { + if (matches.length === 0 || context.tailwindConfig.prefix === '') { + return matches + } + + for (let match of matches) { + let [meta] = match + if (meta.options.respectPrefix) { + let container = postcss.root({ nodes: [match[1].clone()] }) + let classCandidate = match[1].raws.tailwind.classCandidate + + container.walkRules((r) => { + // If this is a negative utility with a dash *before* the prefix we + // have to ensure that the generated selector matches the candidate + + // Not doing this will cause `-tw-top-1` to generate the class `.tw--top-1` + // The disconnect between candidate <-> class can cause @apply to hard crash. + let shouldPrependNegative = classCandidate.startsWith('-') + + r.selector = prefixSelector( + context.tailwindConfig.prefix, + r.selector, + shouldPrependNegative + ) + }) + + match[1] = container.nodes[0] + } + } + + return matches +} + +function applyImportant(matches, classCandidate) { + if (matches.length === 0) { + return matches + } + + let result = [] + + for (let [meta, rule] of matches) { + let container = postcss.root({ nodes: [rule.clone()] }) + + container.walkRules((r) => { + let ast = selectorParser().astSync(r.selector) + + // Remove extraneous selectors that do not include the base candidate + ast.each((sel) => eliminateIrrelevantSelectors(sel, classCandidate)) + + // Update all instances of the base candidate to include the important marker + updateAllClasses(ast, (className) => + className === classCandidate ? `!${className}` : className + ) + + r.selector = ast.toString() + + r.walkDecls((d) => (d.important = true)) + }) + + result.push([{ ...meta, important: true }, container.nodes[0]]) + } + + return result +} + +// Takes a list of rule tuples and applies a variant like `hover`, sm`, +// whatever to it. We used to do some extra caching here to avoid generating +// a variant of the same rule more than once, but this was never hit because +// we cache at the entire selector level further up the tree. +// +// Technically you can get a cache hit if you have `hover:focus:text-center` +// and `focus:hover:text-center` in the same project, but it doesn't feel +// worth the complexity for that case. + +function applyVariant(variant, matches, context) { + if (matches.length === 0) { + return matches + } + + /** @type {{modifier: string | null, value: string | null}} */ + let args = { modifier: null, value: sharedState.NONE } + + // Retrieve "modifier" + { + let [baseVariant, ...modifiers] = splitAtTopLevelOnly(variant, '/') + + // This is a hack to support variants with `/` in them, like `ar-1/10/20:text-red-500` + // In this case 1/10 is a value but /20 is a modifier + if (modifiers.length > 1) { + baseVariant = baseVariant + '/' + modifiers.slice(0, -1).join('/') + modifiers = modifiers.slice(-1) + } + + if (modifiers.length && !context.variantMap.has(variant)) { + variant = baseVariant + args.modifier = modifiers[0] + + if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) { + return [] + } + } + } + + // Retrieve "arbitrary value" + if (variant.endsWith(']') && !variant.startsWith('[')) { + // We either have: + // @[200px] + // group-[:hover] + // + // But we don't want: + // @-[200px] (`-` is incorrect) + // group[:hover] (`-` is missing) + let match = /(.)(-?)\[(.*)\]/g.exec(variant) + if (match) { + let [, char, separator, value] = match + // @-[200px] case + if (char === '@' && separator === '-') return [] + // group[:hover] case + if (char !== '@' && separator === '') return [] + + variant = variant.replace(`${separator}[${value}]`, '') + args.value = value + } + } + + // Register arbitrary variants + if (isArbitraryValue(variant) && !context.variantMap.has(variant)) { + let sort = context.offsets.recordVariant(variant) + + let selector = normalize(variant.slice(1, -1)) + let selectors = splitAtTopLevelOnly(selector, ',') + + // We do not support multiple selectors for arbitrary variants + if (selectors.length > 1) { + return [] + } + + if (!selectors.every(isValidVariantFormatString)) { + return [] + } + + let records = selectors.map((sel, idx) => [ + context.offsets.applyParallelOffset(sort, idx), + parseVariant(sel.trim()), + ]) + + context.variantMap.set(variant, records) + } + + if (context.variantMap.has(variant)) { + let isArbitraryVariant = isArbitraryValue(variant) + let internalFeatures = context.variantOptions.get(variant)?.[INTERNAL_FEATURES] ?? {} + let variantFunctionTuples = context.variantMap.get(variant).slice() + let result = [] + + let respectPrefix = (() => { + if (isArbitraryVariant) return false + if (internalFeatures.respectPrefix === false) return false + return true + })() + + for (let [meta, rule] of matches) { + // Don't generate variants for user css + if (meta.layer === 'user') { + continue + } + + let container = postcss.root({ nodes: [rule.clone()] }) + + for (let [variantSort, variantFunction, containerFromArray] of variantFunctionTuples) { + let clone = (containerFromArray ?? container).clone() + let collectedFormats = [] + + function prepareBackup() { + // Already prepared, chicken out + if (clone.raws.neededBackup) { + return + } + clone.raws.neededBackup = true + clone.walkRules((rule) => (rule.raws.originalSelector = rule.selector)) + } + + function modifySelectors(modifierFunction) { + prepareBackup() + clone.each((rule) => { + if (rule.type !== 'rule') { + return + } + + rule.selectors = rule.selectors.map((selector) => { + return modifierFunction({ + get className() { + return getClassNameFromSelector(selector) + }, + selector, + }) + }) + }) + + return clone + } + + let ruleWithVariant = variantFunction({ + // Public API + get container() { + prepareBackup() + return clone + }, + separator: context.tailwindConfig.separator, + modifySelectors, + + // Private API for now + wrap(wrapper) { + let nodes = clone.nodes + clone.removeAll() + wrapper.append(nodes) + clone.append(wrapper) + }, + format(selectorFormat) { + collectedFormats.push({ + format: selectorFormat, + respectPrefix, + }) + }, + args, + }) + + // It can happen that a list of format strings is returned from within the function. In that + // case, we have to process them as well. We can use the existing `variantSort`. + if (Array.isArray(ruleWithVariant)) { + for (let [idx, variantFunction] of ruleWithVariant.entries()) { + // This is a little bit scary since we are pushing to an array of items that we are + // currently looping over. However, you can also think of it like a processing queue + // where you keep handling jobs until everything is done and each job can queue more + // jobs if needed. + variantFunctionTuples.push([ + context.offsets.applyParallelOffset(variantSort, idx), + variantFunction, + + // If the clone has been modified we have to pass that back + // though so each rule can use the modified container + clone.clone(), + ]) + } + continue + } + + if (typeof ruleWithVariant === 'string') { + collectedFormats.push({ + format: ruleWithVariant, + respectPrefix, + }) + } + + if (ruleWithVariant === null) { + continue + } + + // We had to backup selectors, therefore we assume that somebody touched + // `container` or `modifySelectors`. Let's see if they did, so that we + // can restore the selectors, and collect the format strings. + if (clone.raws.neededBackup) { + delete clone.raws.neededBackup + clone.walkRules((rule) => { + let before = rule.raws.originalSelector + if (!before) return + delete rule.raws.originalSelector + if (before === rule.selector) return // No mutation happened + + let modified = rule.selector + + // Rebuild the base selector, this is what plugin authors would do + // as well. E.g.: `${variant}${separator}${className}`. + // However, plugin authors probably also prepend or append certain + // classes, pseudos, ids, ... + let rebuiltBase = selectorParser((selectors) => { + selectors.walkClasses((classNode) => { + classNode.value = `${variant}${context.tailwindConfig.separator}${classNode.value}` + }) + }).processSync(before) + + // Now that we know the original selector, the new selector, and + // the rebuild part in between, we can replace the part that plugin + // authors need to rebuild with `&`, and eventually store it in the + // collectedFormats. Similar to what `format('...')` would do. + // + // E.g.: + // variant: foo + // selector: .markdown > p + // modified (by plugin): .foo .foo\\:markdown > p + // rebuiltBase (internal): .foo\\:markdown > p + // format: .foo & + collectedFormats.push({ + format: modified.replace(rebuiltBase, '&'), + respectPrefix, + }) + rule.selector = before + }) + } + + // This tracks the originating layer for the variant + // For example: + // .sm:underline {} is a variant of something in the utilities layer + // .sm:container {} is a variant of the container component + clone.nodes[0].raws.tailwind = { ...clone.nodes[0].raws.tailwind, parentLayer: meta.layer } + + let withOffset = [ + { + ...meta, + sort: context.offsets.applyVariantOffset( + meta.sort, + variantSort, + Object.assign(args, context.variantOptions.get(variant)) + ), + collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats), + }, + clone.nodes[0], + ] + result.push(withOffset) + } + } + + return result + } + + return [] +} + +function parseRules(rule, cache, options = {}) { + // PostCSS node + if (!isPlainObject(rule) && !Array.isArray(rule)) { + return [[rule], options] + } + + // Tuple + if (Array.isArray(rule)) { + return parseRules(rule[0], cache, rule[1]) + } + + // Simple object + if (!cache.has(rule)) { + cache.set(rule, parseObjectStyles(rule)) + } + + return [cache.get(rule), options] +} + +const IS_VALID_PROPERTY_NAME = /^[a-z_-]/ + +function isValidPropName(name) { + return IS_VALID_PROPERTY_NAME.test(name) +} + +/** + * @param {string} declaration + * @returns {boolean} + */ +function looksLikeUri(declaration) { + // Quick bailout for obvious non-urls + // This doesn't support schemes that don't use a leading // but that's unlikely to be a problem + if (!declaration.includes('://')) { + return false + } + + try { + const url = new URL(declaration) + return url.scheme !== '' && url.host !== '' + } catch (err) { + // Definitely not a valid url + return false + } +} + +function isParsableNode(node) { + let isParsable = true + + node.walkDecls((decl) => { + if (!isParsableCssValue(decl.prop, decl.value)) { + isParsable = false + return false + } + }) + + return isParsable +} + +function isParsableCssValue(property, value) { + // We don't want to to treat [https://example.com] as a custom property + // Even though, according to the CSS grammar, it's a totally valid CSS declaration + // So we short-circuit here by checking if the custom property looks like a url + if (looksLikeUri(`${property}:${value}`)) { + return false + } + + try { + postcss.parse(`a{${property}:${value}}`).toResult() + return true + } catch (err) { + return false + } +} + +function extractArbitraryProperty(classCandidate, context) { + let [, property, value] = classCandidate.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/) ?? [] + + if (value === undefined) { + return null + } + + if (!isValidPropName(property)) { + return null + } + + if (!isValidArbitraryValue(value)) { + return null + } + + let normalized = normalize(value, { property }) + + if (!isParsableCssValue(property, normalized)) { + return null + } + + let sort = context.offsets.arbitraryProperty() + + return [ + [ + { sort, layer: 'utilities' }, + () => ({ + [asClass(classCandidate)]: { + [property]: normalized, + }, + }), + ], + ] +} + +function* resolveMatchedPlugins(classCandidate, context) { + if (context.candidateRuleMap.has(classCandidate)) { + yield [context.candidateRuleMap.get(classCandidate), 'DEFAULT'] + } + + yield* (function* (arbitraryPropertyRule) { + if (arbitraryPropertyRule !== null) { + yield [arbitraryPropertyRule, 'DEFAULT'] + } + })(extractArbitraryProperty(classCandidate, context)) + + let candidatePrefix = classCandidate + let negative = false + + const twConfigPrefix = context.tailwindConfig.prefix + + const twConfigPrefixLen = twConfigPrefix.length + + const hasMatchingPrefix = + candidatePrefix.startsWith(twConfigPrefix) || candidatePrefix.startsWith(`-${twConfigPrefix}`) + + if (candidatePrefix[twConfigPrefixLen] === '-' && hasMatchingPrefix) { + negative = true + candidatePrefix = twConfigPrefix + candidatePrefix.slice(twConfigPrefixLen + 1) + } + + if (negative && context.candidateRuleMap.has(candidatePrefix)) { + yield [context.candidateRuleMap.get(candidatePrefix), '-DEFAULT'] + } + + for (let [prefix, modifier] of candidatePermutations(candidatePrefix)) { + if (context.candidateRuleMap.has(prefix)) { + yield [context.candidateRuleMap.get(prefix), negative ? `-${modifier}` : modifier] + } + } +} + +function splitWithSeparator(input, separator) { + if (input === sharedState.NOT_ON_DEMAND) { + return [sharedState.NOT_ON_DEMAND] + } + + return splitAtTopLevelOnly(input, separator) +} + +function* recordCandidates(matches, classCandidate) { + for (const match of matches) { + match[1].raws.tailwind = { + ...match[1].raws.tailwind, + classCandidate, + preserveSource: match[0].options?.preserveSource ?? false, + } + + yield match + } +} + +function* resolveMatches(candidate, context) { + let separator = context.tailwindConfig.separator + let [classCandidate, ...variants] = splitWithSeparator(candidate, separator).reverse() + let important = false + + if (classCandidate.startsWith('!')) { + important = true + classCandidate = classCandidate.slice(1) + } + + // TODO: Reintroduce this in ways that doesn't break on false positives + // function sortAgainst(toSort, against) { + // return toSort.slice().sort((a, z) => { + // return bigSign(against.get(a)[0] - against.get(z)[0]) + // }) + // } + // let sorted = sortAgainst(variants, context.variantMap) + // if (sorted.toString() !== variants.toString()) { + // let corrected = sorted.reverse().concat(classCandidate).join(':') + // throw new Error(`Class ${candidate} should be written as ${corrected}`) + // } + + for (let matchedPlugins of resolveMatchedPlugins(classCandidate, context)) { + let matches = [] + let typesByMatches = new Map() + + let [plugins, modifier] = matchedPlugins + let isOnlyPlugin = plugins.length === 1 + + for (let [sort, plugin] of plugins) { + let matchesPerPlugin = [] + + if (typeof plugin === 'function') { + for (let ruleSet of [].concat(plugin(modifier, { isOnlyPlugin }))) { + let [rules, options] = parseRules(ruleSet, context.postCssNodeCache) + for (let rule of rules) { + matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule]) + } + } + } + // Only process static plugins on exact matches + else if (modifier === 'DEFAULT' || modifier === '-DEFAULT') { + let ruleSet = plugin + let [rules, options] = parseRules(ruleSet, context.postCssNodeCache) + for (let rule of rules) { + matchesPerPlugin.push([{ ...sort, options: { ...sort.options, ...options } }, rule]) + } + } + + if (matchesPerPlugin.length > 0) { + let matchingTypes = Array.from( + getMatchingTypes( + sort.options?.types ?? [], + modifier, + sort.options ?? {}, + context.tailwindConfig + ) + ).map(([_, type]) => type) + + if (matchingTypes.length > 0) { + typesByMatches.set(matchesPerPlugin, matchingTypes) + } + + matches.push(matchesPerPlugin) + } + } + + if (isArbitraryValue(modifier)) { + if (matches.length > 1) { + // Partition plugins in 2 categories so that we can start searching in the plugins that + // don't have `any` as a type first. + let [withAny, withoutAny] = matches.reduce( + (group, plugin) => { + let hasAnyType = plugin.some(([{ options }]) => + options.types.some(({ type }) => type === 'any') + ) + + if (hasAnyType) { + group[0].push(plugin) + } else { + group[1].push(plugin) + } + return group + }, + [[], []] + ) + + function findFallback(matches) { + // If only a single plugin matches, let's take that one + if (matches.length === 1) { + return matches[0] + } + + // Otherwise, find the plugin that creates a valid rule given the arbitrary value, and + // also has the correct type which preferOnConflicts the plugin in case of clashes. + return matches.find((rules) => { + let matchingTypes = typesByMatches.get(rules) + return rules.some(([{ options }, rule]) => { + if (!isParsableNode(rule)) { + return false + } + + return options.types.some( + ({ type, preferOnConflict }) => matchingTypes.includes(type) && preferOnConflict + ) + }) + }) + } + + // Try to find a fallback plugin, because we already know that multiple plugins matched for + // the given arbitrary value. + let fallback = findFallback(withoutAny) ?? findFallback(withAny) + if (fallback) { + matches = [fallback] + } + + // We couldn't find a fallback plugin which means that there are now multiple plugins that + // generated css for the current candidate. This means that the result is ambiguous and this + // should not happen. We won't generate anything right now, so let's report this to the user + // by logging some options about what they can do. + else { + let typesPerPlugin = matches.map( + (match) => new Set([...(typesByMatches.get(match) ?? [])]) + ) + + // Remove duplicates, so that we can detect proper unique types for each plugin. + for (let pluginTypes of typesPerPlugin) { + for (let type of pluginTypes) { + let removeFromOwnGroup = false + + for (let otherGroup of typesPerPlugin) { + if (pluginTypes === otherGroup) continue + + if (otherGroup.has(type)) { + otherGroup.delete(type) + removeFromOwnGroup = true + } + } + + if (removeFromOwnGroup) pluginTypes.delete(type) + } + } + + let messages = [] + + for (let [idx, group] of typesPerPlugin.entries()) { + for (let type of group) { + let rules = matches[idx] + .map(([, rule]) => rule) + .flat() + .map((rule) => + rule + .toString() + .split('\n') + .slice(1, -1) // Remove selector and closing '}' + .map((line) => line.trim()) + .map((x) => ` ${x}`) // Re-indent + .join('\n') + ) + .join('\n\n') + + messages.push( + ` Use \`${candidate.replace('[', `[${type}:`)}\` for \`${rules.trim()}\`` + ) + break + } + } + + log.warn([ + `The class \`${candidate}\` is ambiguous and matches multiple utilities.`, + ...messages, + `If this is content and not a class, replace it with \`${candidate + .replace('[', '[') + .replace(']', ']')}\` to silence this warning.`, + ]) + continue + } + } + + matches = matches.map((list) => list.filter((match) => isParsableNode(match[1]))) + } + + matches = matches.flat() + matches = Array.from(recordCandidates(matches, classCandidate)) + matches = applyPrefix(matches, context) + + if (important) { + matches = applyImportant(matches, classCandidate) + } + + for (let variant of variants) { + matches = applyVariant(variant, matches, context) + } + + for (let match of matches) { + match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate } + + // Apply final format selector + match = applyFinalFormat(match, { context, candidate }) + + // Skip rules with invalid selectors + // This will cause the candidate to be added to the "not class" + // cache skipping it entirely for future builds + if (match === null) { + continue + } + + yield match + } + } +} + +function applyFinalFormat(match, { context, candidate }) { + if (!match[0].collectedFormats) { + return match + } + + let isValid = true + let finalFormat + + try { + finalFormat = formatVariantSelector(match[0].collectedFormats, { + context, + candidate, + }) + } catch { + // The format selector we produced is invalid + // This could be because: + // - A bug exists + // - A plugin introduced an invalid variant selector (ex: `addVariant('foo', '&;foo')`) + // - The user used an invalid arbitrary variant (ex: `[&;foo]:underline`) + // Either way the build will fail because of this + // We would rather that the build pass "silently" given that this could + // happen because of picking up invalid things when scanning content + // So we'll throw out the candidate instead + + return null + } + + let container = postcss.root({ nodes: [match[1].clone()] }) + + container.walkRules((rule) => { + if (inKeyframes(rule)) { + return + } + + try { + let selector = finalizeSelector(rule.selector, finalFormat, { + candidate, + context, + }) + + // Finalize Selector determined that this candidate is irrelevant + // TODO: This elimination should happen earlier so this never happens + if (selector === null) { + rule.remove() + return + } + + rule.selector = selector + } catch { + // If this selector is invalid we also want to skip it + // But it's likely that being invalid here means there's a bug in a plugin rather than too loosely matching content + isValid = false + return false + } + }) + + if (!isValid) { + return null + } + + match[1] = container.nodes[0] + + return match +} + +function inKeyframes(rule) { + return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes' +} + +function getImportantStrategy(important) { + if (important === true) { + return (rule) => { + if (inKeyframes(rule)) { + return + } + + rule.walkDecls((d) => { + if (d.parent.type === 'rule' && !inKeyframes(d.parent)) { + d.important = true + } + }) + } + } + + if (typeof important === 'string') { + return (rule) => { + if (inKeyframes(rule)) { + return + } + + rule.selectors = rule.selectors.map((selector) => { + return applyImportantSelector(selector, important) + }) + } + } +} + +function generateRules(candidates, context, isSorting = false) { + let allRules = [] + let strategy = getImportantStrategy(context.tailwindConfig.important) + + for (let candidate of candidates) { + if (context.notClassCache.has(candidate)) { + continue + } + + if (context.candidateRuleCache.has(candidate)) { + allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate))) + continue + } + + let matches = Array.from(resolveMatches(candidate, context)) + + if (matches.length === 0) { + context.notClassCache.add(candidate) + continue + } + + context.classCache.set(candidate, matches) + + let rules = context.candidateRuleCache.get(candidate) ?? new Set() + context.candidateRuleCache.set(candidate, rules) + + for (const match of matches) { + let [{ sort, options }, rule] = match + + if (options.respectImportant && strategy) { + let container = postcss.root({ nodes: [rule.clone()] }) + container.walkRules(strategy) + rule = container.nodes[0] + } + + // Note: We have to clone rules during sorting + // so we eliminate some shared mutable state + let newEntry = [sort, isSorting ? rule.clone() : rule] + rules.add(newEntry) + context.ruleCache.add(newEntry) + allRules.push(newEntry) + } + } + + return allRules +} + +function isArbitraryValue(input) { + return input.startsWith('[') && input.endsWith(']') +} + +export { resolveMatches, generateRules } |