tailwindcss 3.0.0-alpha.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/colors.js +2 -1
  2. package/defaultConfig.js +2 -1
  3. package/defaultTheme.js +2 -1
  4. package/lib/cli.js +39 -35
  5. package/lib/constants.js +1 -1
  6. package/lib/corePluginList.js +10 -1
  7. package/lib/corePlugins.js +393 -259
  8. package/lib/css/preflight.css +14 -1
  9. package/lib/featureFlags.js +12 -7
  10. package/lib/lib/collapseDuplicateDeclarations.js +29 -0
  11. package/lib/lib/detectNesting.js +17 -2
  12. package/lib/lib/evaluateTailwindFunctions.js +9 -5
  13. package/lib/lib/expandApplyAtRules.js +26 -9
  14. package/lib/lib/expandTailwindAtRules.js +4 -1
  15. package/lib/lib/generateRules.js +151 -19
  16. package/lib/lib/resolveDefaultsAtRules.js +67 -56
  17. package/lib/lib/setupContextUtils.js +80 -80
  18. package/lib/lib/setupWatchingContext.js +5 -1
  19. package/lib/lib/sharedState.js +2 -2
  20. package/lib/lib/substituteScreenAtRules.js +7 -4
  21. package/lib/processTailwindFeatures.js +4 -0
  22. package/lib/util/buildMediaQuery.js +13 -24
  23. package/lib/util/createUtilityPlugin.js +5 -5
  24. package/lib/util/dataTypes.js +38 -7
  25. package/lib/util/formatVariantSelector.js +186 -0
  26. package/lib/util/isValidArbitraryValue.js +64 -0
  27. package/lib/util/nameClass.js +2 -1
  28. package/lib/util/negateValue.js +3 -1
  29. package/lib/util/normalizeConfig.js +22 -8
  30. package/lib/util/normalizeScreens.js +61 -0
  31. package/lib/util/parseBoxShadowValue.js +77 -0
  32. package/lib/util/pluginUtils.js +62 -158
  33. package/lib/util/prefixSelector.js +1 -3
  34. package/lib/util/resolveConfig.js +17 -13
  35. package/lib/util/transformThemeValue.js +23 -13
  36. package/package.json +15 -15
  37. package/peers/index.js +4456 -5450
  38. package/plugin.js +2 -1
  39. package/resolveConfig.js +2 -1
  40. package/src/.DS_Store +0 -0
  41. package/src/cli.js +9 -2
  42. package/src/corePluginList.js +1 -1
  43. package/src/corePlugins.js +392 -404
  44. package/src/css/preflight.css +14 -1
  45. package/src/featureFlags.js +14 -4
  46. package/src/lib/collapseDuplicateDeclarations.js +28 -0
  47. package/src/lib/detectNesting.js +22 -3
  48. package/src/lib/evaluateTailwindFunctions.js +5 -2
  49. package/src/lib/expandApplyAtRules.js +29 -2
  50. package/src/lib/expandTailwindAtRules.js +5 -2
  51. package/src/lib/generateRules.js +155 -11
  52. package/src/lib/resolveDefaultsAtRules.js +67 -50
  53. package/src/lib/setupContextUtils.js +77 -67
  54. package/src/lib/setupWatchingContext.js +7 -0
  55. package/src/lib/sharedState.js +1 -1
  56. package/src/lib/substituteScreenAtRules.js +6 -3
  57. package/src/processTailwindFeatures.js +5 -0
  58. package/src/util/buildMediaQuery.js +14 -18
  59. package/src/util/createUtilityPlugin.js +2 -2
  60. package/src/util/dataTypes.js +43 -11
  61. package/src/util/formatVariantSelector.js +196 -0
  62. package/src/util/isValidArbitraryValue.js +61 -0
  63. package/src/util/nameClass.js +2 -2
  64. package/src/util/negateValue.js +4 -2
  65. package/src/util/normalizeConfig.js +17 -1
  66. package/src/util/normalizeScreens.js +45 -0
  67. package/src/util/parseBoxShadowValue.js +71 -0
  68. package/src/util/pluginUtils.js +50 -146
  69. package/src/util/prefixSelector.js +1 -4
  70. package/src/util/resolveConfig.js +7 -1
  71. package/src/util/transformThemeValue.js +22 -7
  72. package/stubs/defaultConfig.stub.js +118 -58
  73. package/CHANGELOG.md +0 -1759
@@ -17,6 +17,37 @@ import * as sharedState from './sharedState'
17
17
  import { env } from './sharedState'
18
18
  import { toPath } from '../util/toPath'
19
19
  import log from '../util/log'
20
+ import negateValue from '../util/negateValue'
21
+ import isValidArbitraryValue from '../util/isValidArbitraryValue'
22
+
23
+ function parseVariantFormatString(input) {
24
+ if (input.includes('{')) {
25
+ if (!isBalanced(input)) throw new Error(`Your { and } are unbalanced.`)
26
+
27
+ return input
28
+ .split(/{(.*)}/gim)
29
+ .flatMap((line) => parseVariantFormatString(line))
30
+ .filter(Boolean)
31
+ }
32
+
33
+ return [input.trim()]
34
+ }
35
+
36
+ function isBalanced(input) {
37
+ let count = 0
38
+
39
+ for (let char of input) {
40
+ if (char === '{') {
41
+ count++
42
+ } else if (char === '}') {
43
+ if (--count < 0) {
44
+ return false // unbalanced
45
+ }
46
+ }
47
+ }
48
+
49
+ return count === 0
50
+ }
20
51
 
21
52
  function insertInto(list, value, { before = [] } = {}) {
22
53
  before = [].concat(before)
@@ -100,64 +131,6 @@ function withIdentifiers(styles) {
100
131
  })
101
132
  }
102
133
 
103
- let matchingBrackets = new Map([
104
- ['{', '}'],
105
- ['[', ']'],
106
- ['(', ')'],
107
- ])
108
- let inverseMatchingBrackets = new Map(
109
- Array.from(matchingBrackets.entries()).map(([k, v]) => [v, k])
110
- )
111
-
112
- let quotes = new Set(['"', "'", '`'])
113
-
114
- // Arbitrary values must contain balanced brackets (), [] and {}. Escaped
115
- // values don't count, and brackets inside quotes also don't count.
116
- //
117
- // E.g.: w-[this-is]w-[weird-and-invalid]
118
- // E.g.: w-[this-is\\]w-\\[weird-but-valid]
119
- // E.g.: content-['this-is-also-valid]-weirdly-enough']
120
- function isValidArbitraryValue(value) {
121
- let stack = []
122
- let inQuotes = false
123
-
124
- for (let i = 0; i < value.length; i++) {
125
- let char = value[i]
126
-
127
- // Non-escaped quotes allow us to "allow" anything in between
128
- if (quotes.has(char) && value[i - 1] !== '\\') {
129
- inQuotes = !inQuotes
130
- }
131
-
132
- if (inQuotes) continue
133
- if (value[i - 1] === '\\') continue // Escaped
134
-
135
- if (matchingBrackets.has(char)) {
136
- stack.push(char)
137
- } else if (inverseMatchingBrackets.has(char)) {
138
- let inverse = inverseMatchingBrackets.get(char)
139
-
140
- // Nothing to pop from, therefore it is unbalanced
141
- if (stack.length <= 0) {
142
- return false
143
- }
144
-
145
- // Popped value must match the inverse value, otherwise it is unbalanced
146
- if (stack.pop() !== inverse) {
147
- return false
148
- }
149
- }
150
- }
151
-
152
- // If there is still something on the stack, it is also unbalanced
153
- if (stack.length > 0) {
154
- return false
155
- }
156
-
157
- // All good, totally balanced!
158
- return true
159
- }
160
-
161
134
  function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offsets, classList }) {
162
135
  function getConfigValue(path, defaultValue) {
163
136
  return path ? dlv(tailwindConfig, path, defaultValue) : tailwindConfig
@@ -176,16 +149,41 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
176
149
  return identifier
177
150
  }
178
151
 
179
- if (typeof context.tailwindConfig.prefix === 'function') {
180
- return prefixSelector(context.tailwindConfig.prefix, `.${identifier}`).substr(1)
181
- }
182
-
183
152
  return context.tailwindConfig.prefix + identifier
184
153
  }
185
154
 
186
155
  return {
187
156
  addVariant(variantName, variantFunctions, options = {}) {
188
- variantFunctions = [].concat(variantFunctions)
157
+ variantFunctions = [].concat(variantFunctions).map((variantFunction) => {
158
+ if (typeof variantFunction !== 'string') {
159
+ // Safelist public API functions
160
+ return ({ modifySelectors, container, separator }) => {
161
+ return variantFunction({ modifySelectors, container, separator })
162
+ }
163
+ }
164
+
165
+ variantFunction = variantFunction
166
+ .replace(/\n+/g, '')
167
+ .replace(/\s{1,}/g, ' ')
168
+ .trim()
169
+
170
+ let fns = parseVariantFormatString(variantFunction)
171
+ .map((str) => {
172
+ if (!str.startsWith('@')) {
173
+ return ({ format }) => format(str)
174
+ }
175
+
176
+ let [, name, params] = /@(.*?) (.*)/g.exec(str)
177
+ return ({ wrap }) => wrap(postcss.atRule({ name, params }))
178
+ })
179
+ .reverse()
180
+
181
+ return (api) => {
182
+ for (let fn of fns) {
183
+ fn(api)
184
+ }
185
+ }
186
+ })
189
187
 
190
188
  insertInto(variantList, variantName, options)
191
189
  variantMap.set(variantName, variantFunctions)
@@ -300,7 +298,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
300
298
  function wrapped(modifier, { isOnlyPlugin }) {
301
299
  let { type = 'any' } = options
302
300
  type = [].concat(type)
303
- let [value, coercedType] = coerceValue(type, modifier, options.values, tailwindConfig)
301
+ let [value, coercedType] = coerceValue(type, modifier, options, tailwindConfig)
304
302
 
305
303
  if (value === undefined) {
306
304
  return []
@@ -352,7 +350,7 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
352
350
  function wrapped(modifier, { isOnlyPlugin }) {
353
351
  let { type = 'any' } = options
354
352
  type = [].concat(type)
355
- let [value, coercedType] = coerceValue(type, modifier, options.values, tailwindConfig)
353
+ let [value, coercedType] = coerceValue(type, modifier, options, tailwindConfig)
356
354
 
357
355
  if (value === undefined) {
358
356
  return []
@@ -518,7 +516,9 @@ function resolvePlugins(context, root) {
518
516
  variantPlugins['directionVariants'],
519
517
  variantPlugins['reducedMotionVariants'],
520
518
  variantPlugins['darkVariants'],
519
+ variantPlugins['printVariant'],
521
520
  variantPlugins['screenVariants'],
521
+ variantPlugins['orientationVariants'],
522
522
  ]
523
523
 
524
524
  return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins]
@@ -561,6 +561,10 @@ function registerPlugins(plugins, context) {
561
561
  ])
562
562
  let reservedBits = BigInt(highestOffset.toString(2).length)
563
563
 
564
+ // A number one less than the top range of the highest offset area
565
+ // so arbitrary properties are always sorted at the end.
566
+ context.arbitraryPropertiesSort = ((1n << reservedBits) << 0n) - 1n
567
+
564
568
  context.layerOrder = {
565
569
  base: (1n << reservedBits) << 0n,
566
570
  components: (1n << reservedBits) << 1n,
@@ -670,10 +674,16 @@ function registerPlugins(plugins, context) {
670
674
  for (let util of classList) {
671
675
  if (Array.isArray(util)) {
672
676
  let [utilName, options] = util
677
+ let negativeClasses = []
673
678
 
674
- for (let value of Object.keys(options?.values ?? {})) {
675
- output.push(formatClass(utilName, value))
679
+ for (let [key, value] of Object.entries(options?.values ?? {})) {
680
+ output.push(formatClass(utilName, key))
681
+ if (options?.supportsNegativeValues && negateValue(value)) {
682
+ negativeClasses.push(formatClass(utilName, `-${key}`))
683
+ }
676
684
  }
685
+
686
+ output.push(...negativeClasses)
677
687
  } else {
678
688
  output.push(util)
679
689
  }
@@ -84,6 +84,13 @@ function rebootWatcher(context, configPath, configDependencies, candidateFiles)
84
84
 
85
85
  watcher = chokidar.watch([...candidateFiles, ...configDependencies], {
86
86
  ignoreInitial: true,
87
+ awaitWriteFinish:
88
+ process.platform === 'win32'
89
+ ? {
90
+ stabilityThreshold: 50,
91
+ pollInterval: 10,
92
+ }
93
+ : false,
87
94
  })
88
95
 
89
96
  setWatcher(context, watcher)
@@ -1,7 +1,7 @@
1
1
  export const env = {
2
2
  TAILWIND_MODE: process.env.TAILWIND_MODE,
3
3
  NODE_ENV: process.env.NODE_ENV,
4
- DEBUG: process.env.DEBUG !== undefined,
4
+ DEBUG: process.env.DEBUG !== undefined && process.env.DEBUG !== '0',
5
5
  TAILWIND_DISABLE_TOUCH: process.env.TAILWIND_DISABLE_TOUCH !== undefined,
6
6
  TAILWIND_TOUCH_DIR: process.env.TAILWIND_TOUCH_DIR,
7
7
  }
@@ -1,16 +1,19 @@
1
+ import { normalizeScreens } from '../util/normalizeScreens'
1
2
  import buildMediaQuery from '../util/buildMediaQuery'
2
3
 
3
4
  export default function ({ tailwindConfig: { theme } }) {
4
5
  return function (css) {
5
6
  css.walkAtRules('screen', (atRule) => {
6
- const screen = atRule.params
7
+ let screen = atRule.params
8
+ let screens = normalizeScreens(theme.screens)
9
+ let screenDefinition = screens.find(({ name }) => name === screen)
7
10
 
8
- if (!theme.screens?.hasOwnProperty?.(screen)) {
11
+ if (!screenDefinition) {
9
12
  throw atRule.error(`No \`${screen}\` screen found.`)
10
13
  }
11
14
 
12
15
  atRule.name = 'media'
13
- atRule.params = buildMediaQuery(theme.screens[screen])
16
+ atRule.params = buildMediaQuery(screenDefinition)
14
17
  })
15
18
  }
16
19
  }
@@ -5,8 +5,10 @@ import evaluateTailwindFunctions from './lib/evaluateTailwindFunctions'
5
5
  import substituteScreenAtRules from './lib/substituteScreenAtRules'
6
6
  import resolveDefaultsAtRules from './lib/resolveDefaultsAtRules'
7
7
  import collapseAdjacentRules from './lib/collapseAdjacentRules'
8
+ import collapseDuplicateDeclarations from './lib/collapseDuplicateDeclarations'
8
9
  import detectNesting from './lib/detectNesting'
9
10
  import { createContext } from './lib/setupContextUtils'
11
+ import { issueFlagNotices } from './featureFlags'
10
12
 
11
13
  export default function processTailwindFeatures(setupContext) {
12
14
  return function (root, result) {
@@ -32,6 +34,8 @@ export default function processTailwindFeatures(setupContext) {
32
34
  )
33
35
  }
34
36
 
37
+ issueFlagNotices(context.tailwindConfig)
38
+
35
39
  detectNesting(context)(root, result)
36
40
  expandTailwindAtRules(context)(root, result)
37
41
  expandApplyAtRules(context)(root, result)
@@ -39,5 +43,6 @@ export default function processTailwindFeatures(setupContext) {
39
43
  substituteScreenAtRules(context)(root, result)
40
44
  resolveDefaultsAtRules(context)(root, result)
41
45
  collapseAdjacentRules(context)(root, result)
46
+ collapseDuplicateDeclarations(context)(root, result)
42
47
  }
43
48
  }
@@ -1,24 +1,20 @@
1
1
  export default function buildMediaQuery(screens) {
2
- if (typeof screens === 'string') {
3
- screens = { min: screens }
4
- }
5
-
6
- if (!Array.isArray(screens)) {
7
- screens = [screens]
8
- }
2
+ screens = Array.isArray(screens) ? screens : [screens]
9
3
 
10
4
  return screens
11
- .map((screen) => {
12
- if (screen?.hasOwnProperty?.('raw')) {
13
- return screen.raw
14
- }
5
+ .map((screen) =>
6
+ screen.values.map((screen) => {
7
+ if (screen.raw !== undefined) {
8
+ return screen.raw
9
+ }
15
10
 
16
- return Object.entries(screen)
17
- .map(([feature, value]) => {
18
- feature = { min: 'min-width', max: 'max-width' }[feature] ?? feature
19
- return `(${feature}: ${value})`
20
- })
21
- .join(' and ')
22
- })
11
+ return [
12
+ screen.min && `(min-width: ${screen.min})`,
13
+ screen.max && `(max-width: ${screen.max})`,
14
+ ]
15
+ .filter(Boolean)
16
+ .join(' and ')
17
+ })
18
+ )
23
19
  .join(', ')
24
20
  }
@@ -3,7 +3,7 @@ import transformThemeValue from './transformThemeValue'
3
3
  export default function createUtilityPlugin(
4
4
  themeKey,
5
5
  utilityVariations = [[themeKey, [themeKey]]],
6
- { filterDefault = false, type = 'any' } = {}
6
+ { filterDefault = false, ...options } = {}
7
7
  ) {
8
8
  let transformValue = transformThemeValue(themeKey)
9
9
  return function ({ matchUtilities, theme }) {
@@ -24,12 +24,12 @@ export default function createUtilityPlugin(
24
24
  })
25
25
  }, {}),
26
26
  {
27
+ ...options,
27
28
  values: filterDefault
28
29
  ? Object.fromEntries(
29
30
  Object.entries(theme(themeKey) ?? {}).filter(([modifier]) => modifier !== 'DEFAULT')
30
31
  )
31
32
  : theme(themeKey),
32
- type,
33
33
  }
34
34
  )
35
35
  }
@@ -1,4 +1,7 @@
1
1
  import { parseColor } from './color'
2
+ import { parseBoxShadowValue } from './parseBoxShadowValue'
3
+
4
+ let cssFunctions = ['min', 'max', 'clamp', 'calc']
2
5
 
3
6
  // Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types
4
7
 
@@ -7,7 +10,22 @@ let UNDERSCORE = /_(?![^(]*\))/g // Underscore separator that is not located bet
7
10
 
8
11
  // This is not a data type, but rather a function that can normalize the
9
12
  // correct values.
10
- export function normalize(value) {
13
+ export function normalize(value, isRoot = true) {
14
+ // Keep raw strings if it starts with `url(`
15
+ if (value.includes('url(')) {
16
+ return value
17
+ .split(/(url\(.*?\))/g)
18
+ .filter(Boolean)
19
+ .map((part) => {
20
+ if (/^url\(.*?\)$/.test(part)) {
21
+ return part
22
+ }
23
+
24
+ return normalize(part, false)
25
+ })
26
+ .join('')
27
+ }
28
+
11
29
  // Convert `_` to ` `, except for escaped underscores `\_`
12
30
  value = value
13
31
  .replace(
@@ -18,10 +36,9 @@ export function normalize(value) {
18
36
  .replace(/\\_/g, '_')
19
37
 
20
38
  // Remove leftover whitespace
21
- value = value.trim()
22
-
23
- // Keep raw strings if it starts with `url(`
24
- if (value.startsWith('url(')) return value
39
+ if (isRoot) {
40
+ value = value.trim()
41
+ }
25
42
 
26
43
  // Add spaces around operators inside calc() that do not follow an operator
27
44
  // or '('.
@@ -36,11 +53,11 @@ export function url(value) {
36
53
  }
37
54
 
38
55
  export function number(value) {
39
- return !isNaN(Number(value))
56
+ return !isNaN(Number(value)) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?`).test(value))
40
57
  }
41
58
 
42
59
  export function percentage(value) {
43
- return /%$/g.test(value) || /^calc\(.+?%\)/g.test(value)
60
+ return /%$/g.test(value) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?%`).test(value))
44
61
  }
45
62
 
46
63
  let lengthUnits = [
@@ -63,10 +80,13 @@ let lengthUnits = [
63
80
  ]
64
81
  let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
65
82
  export function length(value) {
66
- return (
67
- new RegExp(`${lengthUnitsPattern}$`).test(value) ||
68
- new RegExp(`^calc\\(.+?${lengthUnitsPattern}`).test(value)
69
- )
83
+ return value.split(UNDERSCORE).every((part) => {
84
+ return (
85
+ part === '0' ||
86
+ new RegExp(`${lengthUnitsPattern}$`).test(part) ||
87
+ cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?${lengthUnitsPattern}`).test(part))
88
+ )
89
+ })
70
90
  }
71
91
 
72
92
  let lineWidths = new Set(['thin', 'medium', 'thick'])
@@ -74,6 +94,18 @@ export function lineWidth(value) {
74
94
  return lineWidths.has(value)
75
95
  }
76
96
 
97
+ export function shadow(value) {
98
+ let parsedShadows = parseBoxShadowValue(normalize(value))
99
+
100
+ for (let parsedShadow of parsedShadows) {
101
+ if (!parsedShadow.valid) {
102
+ return false
103
+ }
104
+ }
105
+
106
+ return true
107
+ }
108
+
77
109
  export function color(value) {
78
110
  let colors = 0
79
111
 
@@ -0,0 +1,196 @@
1
+ import selectorParser from 'postcss-selector-parser'
2
+ import unescape from 'postcss-selector-parser/dist/util/unesc'
3
+ import escapeClassName from '../util/escapeClassName'
4
+ import prefixSelector from '../util/prefixSelector'
5
+
6
+ let MERGE = ':merge'
7
+ let PARENT = '&'
8
+
9
+ export let selectorFunctions = new Set([MERGE])
10
+
11
+ export function formatVariantSelector(current, ...others) {
12
+ for (let other of others) {
13
+ let incomingValue = resolveFunctionArgument(other, MERGE)
14
+ if (incomingValue !== null) {
15
+ let existingValue = resolveFunctionArgument(current, MERGE, incomingValue)
16
+ if (existingValue !== null) {
17
+ let existingTarget = `${MERGE}(${incomingValue})`
18
+ let splitIdx = other.indexOf(existingTarget)
19
+ let addition = other.slice(splitIdx + existingTarget.length).split(' ')[0]
20
+
21
+ current = current.replace(existingTarget, existingTarget + addition)
22
+ continue
23
+ }
24
+ }
25
+
26
+ current = other.replace(PARENT, current)
27
+ }
28
+
29
+ return current
30
+ }
31
+
32
+ export function finalizeSelector(format, { selector, candidate, context }) {
33
+ let separator = context?.tailwindConfig?.separator ?? ':'
34
+
35
+ // Split by the separator, but ignore the separator inside square brackets:
36
+ //
37
+ // E.g.: dark:lg:hover:[paint-order:markers]
38
+ // ┬ ┬ ┬ ┬
39
+ // │ │ │ ╰── We will not split here
40
+ // ╰──┴─────┴─────────────── We will split here
41
+ //
42
+ let splitter = new RegExp(`\\${separator}(?![^[]*\\])`)
43
+ let base = candidate.split(splitter).pop()
44
+
45
+ if (context?.tailwindConfig?.prefix) {
46
+ format = prefixSelector(context.tailwindConfig.prefix, format)
47
+ }
48
+
49
+ format = format.replace(PARENT, `.${escapeClassName(candidate)}`)
50
+
51
+ // Normalize escaped classes, e.g.:
52
+ //
53
+ // The idea would be to replace the escaped `base` in the selector with the
54
+ // `format`. However, in css you can escape the same selector in a few
55
+ // different ways. This would result in different strings and therefore we
56
+ // can't replace it properly.
57
+ //
58
+ // base: bg-[rgb(255,0,0)]
59
+ // base in selector: bg-\\[rgb\\(255\\,0\\,0\\)\\]
60
+ // escaped base: bg-\\[rgb\\(255\\2c 0\\2c 0\\)\\]
61
+ //
62
+ selector = selectorParser((selectors) => {
63
+ return selectors.walkClasses((node) => {
64
+ if (node.raws && node.value.includes(base)) {
65
+ node.raws.value = escapeClassName(unescape(node.raws.value))
66
+ }
67
+
68
+ return node
69
+ })
70
+ }).processSync(selector)
71
+
72
+ // We can safely replace the escaped base now, since the `base` section is
73
+ // now in a normalized escaped value.
74
+ selector = selector.replace(`.${escapeClassName(base)}`, format)
75
+
76
+ // Remove unnecessary pseudo selectors that we used as placeholders
77
+ return selectorParser((selectors) => {
78
+ return selectors.map((selector) => {
79
+ selector.walkPseudos((p) => {
80
+ if (selectorFunctions.has(p.value)) {
81
+ p.replaceWith(p.nodes)
82
+ }
83
+
84
+ return p
85
+ })
86
+
87
+ // This will make sure to move pseudo's to the correct spot (the end for
88
+ // pseudo elements) because otherwise the selector will never work
89
+ // anyway.
90
+ //
91
+ // E.g.:
92
+ // - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
93
+ // - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
94
+ //
95
+ // `::before:hover` doesn't work, which means that we can make it work for you by flipping the order.
96
+ function collectPseudoElements(selector) {
97
+ let nodes = []
98
+
99
+ for (let node of selector.nodes) {
100
+ if (isPseudoElement(node)) {
101
+ nodes.push(node)
102
+ selector.removeChild(node)
103
+ }
104
+
105
+ if (node?.nodes) {
106
+ nodes.push(...collectPseudoElements(node))
107
+ }
108
+ }
109
+
110
+ return nodes
111
+ }
112
+
113
+ let pseudoElements = collectPseudoElements(selector)
114
+ if (pseudoElements.length > 0) {
115
+ selector.nodes.push(pseudoElements.sort(sortSelector))
116
+ }
117
+
118
+ return selector
119
+ })
120
+ }).processSync(selector)
121
+ }
122
+
123
+ // Note: As a rule, double colons (::) should be used instead of a single colon
124
+ // (:). This distinguishes pseudo-classes from pseudo-elements. However, since
125
+ // this distinction was not present in older versions of the W3C spec, most
126
+ // browsers support both syntaxes for the original pseudo-elements.
127
+ let pseudoElementsBC = [':before', ':after', ':first-line', ':first-letter']
128
+
129
+ // These pseudo-elements _can_ be combined with other pseudo selectors AND the order does matter.
130
+ let pseudoElementExceptions = ['::file-selector-button']
131
+
132
+ // This will make sure to move pseudo's to the correct spot (the end for
133
+ // pseudo elements) because otherwise the selector will never work
134
+ // anyway.
135
+ //
136
+ // E.g.:
137
+ // - `before:hover:text-center` would result in `.before\:hover\:text-center:hover::before`
138
+ // - `hover:before:text-center` would result in `.hover\:before\:text-center:hover::before`
139
+ //
140
+ // `::before:hover` doesn't work, which means that we can make it work
141
+ // for you by flipping the order.
142
+ function sortSelector(a, z) {
143
+ // Both nodes are non-pseudo's so we can safely ignore them and keep
144
+ // them in the same order.
145
+ if (a.type !== 'pseudo' && z.type !== 'pseudo') {
146
+ return 0
147
+ }
148
+
149
+ // If one of them is a combinator, we need to keep it in the same order
150
+ // because that means it will start a new "section" in the selector.
151
+ if ((a.type === 'combinator') ^ (z.type === 'combinator')) {
152
+ return 0
153
+ }
154
+
155
+ // One of the items is a pseudo and the other one isn't. Let's move
156
+ // the pseudo to the right.
157
+ if ((a.type === 'pseudo') ^ (z.type === 'pseudo')) {
158
+ return (a.type === 'pseudo') - (z.type === 'pseudo')
159
+ }
160
+
161
+ // Both are pseudo's, move the pseudo elements (except for
162
+ // ::file-selector-button) to the right.
163
+ return isPseudoElement(a) - isPseudoElement(z)
164
+ }
165
+
166
+ function isPseudoElement(node) {
167
+ if (node.type !== 'pseudo') return false
168
+ if (pseudoElementExceptions.includes(node.value)) return false
169
+
170
+ return node.value.startsWith('::') || pseudoElementsBC.includes(node.value)
171
+ }
172
+
173
+ function resolveFunctionArgument(haystack, needle, arg) {
174
+ let startIdx = haystack.indexOf(arg ? `${needle}(${arg})` : needle)
175
+ if (startIdx === -1) return null
176
+
177
+ // Start inside the `(`
178
+ startIdx += needle.length + 1
179
+
180
+ let target = ''
181
+ let count = 0
182
+
183
+ for (let char of haystack.slice(startIdx)) {
184
+ if (char !== '(' && char !== ')') {
185
+ target += char
186
+ } else if (char === '(') {
187
+ target += char
188
+ count++
189
+ } else if (char === ')') {
190
+ if (--count < 0) break // unbalanced
191
+ target += char
192
+ }
193
+ }
194
+
195
+ return target
196
+ }