| 1 | import postcss from 'postcss'
|
|---|
| 2 | import selectorParser from 'postcss-selector-parser'
|
|---|
| 3 | import parseObjectStyles from '../util/parseObjectStyles'
|
|---|
| 4 | import isPlainObject from '../util/isPlainObject'
|
|---|
| 5 | import prefixSelector from '../util/prefixSelector'
|
|---|
| 6 | import { updateAllClasses, getMatchingTypes } from '../util/pluginUtils'
|
|---|
| 7 | import log from '../util/log'
|
|---|
| 8 | import * as sharedState from './sharedState'
|
|---|
| 9 | import {
|
|---|
| 10 | formatVariantSelector,
|
|---|
| 11 | finalizeSelector,
|
|---|
| 12 | eliminateIrrelevantSelectors,
|
|---|
| 13 | } from '../util/formatVariantSelector'
|
|---|
| 14 | import { asClass } from '../util/nameClass'
|
|---|
| 15 | import { normalize } from '../util/dataTypes'
|
|---|
| 16 | import { isValidVariantFormatString, parseVariant, INTERNAL_FEATURES } from './setupContextUtils'
|
|---|
| 17 | import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue'
|
|---|
| 18 | import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
|
|---|
| 19 | import { flagEnabled } from '../featureFlags'
|
|---|
| 20 | import { applyImportantSelector } from '../util/applyImportantSelector'
|
|---|
| 21 |
|
|---|
| 22 | let classNameParser = selectorParser((selectors) => {
|
|---|
| 23 | return selectors.first.filter(({ type }) => type === 'class').pop().value
|
|---|
| 24 | })
|
|---|
| 25 |
|
|---|
| 26 | export 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]']
|
|---|
| 37 | function* 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 |
|
|---|
| 82 | function 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 |
|
|---|
| 115 | function 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 |
|
|---|
| 173 | function 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 |
|
|---|
| 427 | function 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 |
|
|---|
| 446 | const IS_VALID_PROPERTY_NAME = /^[a-z_-]/
|
|---|
| 447 |
|
|---|
| 448 | function isValidPropName(name) {
|
|---|
| 449 | return IS_VALID_PROPERTY_NAME.test(name)
|
|---|
| 450 | }
|
|---|
| 451 |
|
|---|
| 452 | /**
|
|---|
| 453 | * @param {string} declaration
|
|---|
| 454 | * @returns {boolean}
|
|---|
| 455 | */
|
|---|
| 456 | function 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 |
|
|---|
| 472 | function 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 |
|
|---|
| 485 | function 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 |
|
|---|
| 501 | function 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 |
|
|---|
| 536 | function* 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 |
|
|---|
| 573 | function 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 |
|
|---|
| 581 | function* 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 |
|
|---|
| 593 | function* 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('[', '[')
|
|---|
| 766 | .replace(']', ']')}\` 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 |
|
|---|
| 805 | function 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 |
|
|---|
| 875 | function inKeyframes(rule) {
|
|---|
| 876 | return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
|
|---|
| 877 | }
|
|---|
| 878 |
|
|---|
| 879 | function 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 |
|
|---|
| 907 | function 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 |
|
|---|
| 954 | function isArbitraryValue(input) {
|
|---|
| 955 | return input.startsWith('[') && input.endsWith(']')
|
|---|
| 956 | }
|
|---|
| 957 |
|
|---|
| 958 | export { resolveMatches, generateRules }
|
|---|