tailwindcss 3.0.0-alpha.2 → 3.0.3

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 (53) hide show
  1. package/CHANGELOG.md +59 -2
  2. package/colors.js +2 -1
  3. package/defaultConfig.js +2 -1
  4. package/defaultTheme.js +2 -1
  5. package/lib/cli.js +58 -58
  6. package/lib/corePluginList.js +3 -0
  7. package/lib/corePlugins.js +227 -172
  8. package/lib/css/preflight.css +5 -3
  9. package/lib/featureFlags.js +3 -1
  10. package/lib/lib/detectNesting.js +17 -2
  11. package/lib/lib/evaluateTailwindFunctions.js +6 -2
  12. package/lib/lib/expandApplyAtRules.js +23 -6
  13. package/lib/lib/expandTailwindAtRules.js +19 -1
  14. package/lib/lib/generateRules.js +54 -0
  15. package/lib/lib/resolveDefaultsAtRules.js +23 -9
  16. package/lib/lib/setupContextUtils.js +48 -71
  17. package/lib/lib/substituteScreenAtRules.js +7 -4
  18. package/lib/util/buildMediaQuery.js +13 -24
  19. package/lib/util/dataTypes.js +14 -3
  20. package/lib/util/defaults.js +6 -0
  21. package/lib/util/formatVariantSelector.js +88 -4
  22. package/lib/util/isValidArbitraryValue.js +64 -0
  23. package/lib/util/log.js +4 -0
  24. package/lib/util/nameClass.js +1 -0
  25. package/lib/util/normalizeConfig.js +34 -5
  26. package/lib/util/normalizeScreens.js +61 -0
  27. package/lib/util/resolveConfig.js +8 -8
  28. package/package.json +14 -13
  29. package/peers/index.js +3739 -3027
  30. package/plugin.js +2 -1
  31. package/resolveConfig.js +2 -1
  32. package/src/corePluginList.js +1 -1
  33. package/src/corePlugins.js +205 -165
  34. package/src/css/preflight.css +5 -3
  35. package/src/featureFlags.js +5 -1
  36. package/src/lib/detectNesting.js +22 -3
  37. package/src/lib/evaluateTailwindFunctions.js +5 -2
  38. package/src/lib/expandApplyAtRules.js +29 -2
  39. package/src/lib/expandTailwindAtRules.js +18 -0
  40. package/src/lib/generateRules.js +57 -0
  41. package/src/lib/resolveDefaultsAtRules.js +28 -7
  42. package/src/lib/setupContextUtils.js +45 -64
  43. package/src/lib/substituteScreenAtRules.js +6 -3
  44. package/src/util/buildMediaQuery.js +14 -18
  45. package/src/util/dataTypes.js +11 -6
  46. package/src/util/defaults.js +6 -0
  47. package/src/util/formatVariantSelector.js +92 -1
  48. package/src/util/isValidArbitraryValue.js +61 -0
  49. package/src/util/log.js +4 -0
  50. package/src/util/nameClass.js +1 -1
  51. package/src/util/normalizeConfig.js +14 -1
  52. package/src/util/normalizeScreens.js +45 -0
  53. package/stubs/defaultConfig.stub.js +17 -0
@@ -2,6 +2,7 @@ import dlv from 'dlv'
2
2
  import didYouMean from 'didyoumean'
3
3
  import transformThemeValue from '../util/transformThemeValue'
4
4
  import parseValue from 'postcss-value-parser'
5
+ import { normalizeScreens } from '../util/normalizeScreens'
5
6
  import buildMediaQuery from '../util/buildMediaQuery'
6
7
  import { toPath } from '../util/toPath'
7
8
 
@@ -173,12 +174,14 @@ export default function ({ tailwindConfig: config }) {
173
174
  },
174
175
  screen: (node, screen) => {
175
176
  screen = screen.replace(/^['"]+/g, '').replace(/['"]+$/g, '')
177
+ let screens = normalizeScreens(config.theme.screens)
178
+ let screenDefinition = screens.find(({ name }) => name === screen)
176
179
 
177
- if (config.theme.screens[screen] === undefined) {
180
+ if (!screenDefinition) {
178
181
  throw node.error(`The '${screen}' screen does not exist in your theme.`)
179
182
  }
180
183
 
181
- return buildMediaQuery(config.theme.screens[screen])
184
+ return buildMediaQuery(screenDefinition)
182
185
  },
183
186
  }
184
187
  return (root) => {
@@ -1,8 +1,24 @@
1
1
  import postcss from 'postcss'
2
+ import parser from 'postcss-selector-parser'
2
3
  import { resolveMatches } from './generateRules'
3
4
  import bigSign from '../util/bigSign'
4
5
  import escapeClassName from '../util/escapeClassName'
5
6
 
7
+ function containsBase(selector, classCandidateBase, separator) {
8
+ return parser((selectors) => {
9
+ let contains = false
10
+
11
+ selectors.walkClasses((classSelector) => {
12
+ if (classSelector.value.split(separator).pop() === classCandidateBase) {
13
+ contains = true
14
+ return false
15
+ }
16
+ })
17
+
18
+ return contains
19
+ }).transformSync(selector)
20
+ }
21
+
6
22
  function prefix(context, selector) {
7
23
  let prefix = context.tailwindConfig.prefix
8
24
  return typeof prefix === 'function' ? prefix(selector) : prefix + selector
@@ -129,10 +145,10 @@ function processApply(root, context) {
129
145
  // TODO: Should we use postcss-selector-parser for this instead?
130
146
  function replaceSelector(selector, utilitySelectors, candidate) {
131
147
  let needle = `.${escapeClassName(candidate)}`
132
- let utilitySelectorsList = utilitySelectors.split(/\s*,\s*/g)
148
+ let utilitySelectorsList = utilitySelectors.split(/\s*\,(?![^(]*\))\s*/g)
133
149
 
134
150
  return selector
135
- .split(/\s*,\s*/g)
151
+ .split(/\s*\,(?![^(]*\))\s*/g)
136
152
  .map((s) => {
137
153
  let replaced = []
138
154
 
@@ -196,7 +212,18 @@ function processApply(root, context) {
196
212
  let siblings = []
197
213
 
198
214
  for (let [applyCandidate, important, rules] of candidates) {
215
+ let base = applyCandidate.split(context.tailwindConfig.separator).pop()
216
+
199
217
  for (let [meta, node] of rules) {
218
+ if (
219
+ containsBase(parent.selector, base, context.tailwindConfig.separator) &&
220
+ containsBase(node.selector, base, context.tailwindConfig.separator)
221
+ ) {
222
+ throw node.error(
223
+ `Circular dependency detected when using: \`@apply ${applyCandidate}\``
224
+ )
225
+ }
226
+
200
227
  let root = postcss.root({ nodes: [node.clone()] })
201
228
  let canRewriteSelector =
202
229
  node.type !== 'atrule' || (node.type === 'atrule' && node.name !== 'keyframes')
@@ -15,6 +15,8 @@ const PATTERNS = [
15
15
  /([^<>"'`\s]*\[\w*\("[^'`\s]*"\)\])/.source, // bg-[url("..."),url("...")]
16
16
  /([^<>"'`\s]*\['[^"'`\s]*'\])/.source, // `content-['hello']` but not `content-['hello']']`
17
17
  /([^<>"'`\s]*\["[^"'`\s]*"\])/.source, // `content-["hello"]` but not `content-["hello"]"]`
18
+ /([^<>"'`\s]*\[[^<>"'`\s]*:'[^"'`\s]*'\])/.source, // `[content:'hello']` but not `[content:"hello"]`
19
+ /([^<>"'`\s]*\[[^<>"'`\s]*:"[^"'`\s]*"\])/.source, // `[content:"hello"]` but not `[content:'hello']`
18
20
  /([^<>"'`\s]*\[[^"'`\s]+\][^<>"'`\s]*)/.source, // `fill-[#bada55]`, `fill-[#bada55]/50`
19
21
  /([^<>"'`\s]*[^"'`\s:])/.source, // `px-1.5`, `uppercase` but not `uppercase:`
20
22
  ].join('|')
@@ -97,6 +99,7 @@ function buildStylesheet(rules, context) {
97
99
 
98
100
  let returnValue = {
99
101
  base: new Set(),
102
+ defaults: new Set(),
100
103
  components: new Set(),
101
104
  utilities: new Set(),
102
105
  variants: new Set(),
@@ -123,6 +126,11 @@ function buildStylesheet(rules, context) {
123
126
  continue
124
127
  }
125
128
 
129
+ if (sort & context.layerOrder.defaults) {
130
+ returnValue.defaults.add(rule)
131
+ continue
132
+ }
133
+
126
134
  if (sort & context.layerOrder.components) {
127
135
  returnValue.components.add(rule)
128
136
  continue
@@ -142,6 +150,8 @@ function buildStylesheet(rules, context) {
142
150
  return returnValue
143
151
  }
144
152
 
153
+ export const DEFAULTS_LAYER = Symbol('defaults-layer')
154
+
145
155
  export default function expandTailwindAtRules(context) {
146
156
  return (root) => {
147
157
  let layerNodes = {
@@ -200,6 +210,7 @@ export default function expandTailwindAtRules(context) {
200
210
  env.DEBUG && console.timeEnd('Build stylesheet')
201
211
 
202
212
  let {
213
+ defaults: defaultNodes,
203
214
  base: baseNodes,
204
215
  components: componentNodes,
205
216
  utilities: utilityNodes,
@@ -210,6 +221,13 @@ export default function expandTailwindAtRules(context) {
210
221
 
211
222
  // Replace any Tailwind directives with generated CSS
212
223
 
224
+ // @defaults rules are unconditionally added first to ensure that
225
+ // using any utility that relies on defaults will work even when
226
+ // compiled in an isolated environment like CSS modules
227
+ if (context.tailwindConfig[DEFAULTS_LAYER] !== false) {
228
+ root.prepend(cloneNodes([...defaultNodes], root.source))
229
+ }
230
+
213
231
  if (layerNodes.base) {
214
232
  layerNodes.base.before(cloneNodes([...baseNodes], layerNodes.base.source))
215
233
  layerNodes.base.remove()
@@ -6,6 +6,9 @@ import prefixSelector from '../util/prefixSelector'
6
6
  import { updateAllClasses } from '../util/pluginUtils'
7
7
  import log from '../util/log'
8
8
  import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSelector'
9
+ import { asClass } from '../util/nameClass'
10
+ import { normalize } from '../util/dataTypes'
11
+ import isValidArbitraryValue from '../util/isValidArbitraryValue'
9
12
 
10
13
  let classNameParser = selectorParser((selectors) => {
11
14
  return selectors.first.filter(({ type }) => type === 'class').pop().value
@@ -245,11 +248,65 @@ function parseRules(rule, cache, options = {}) {
245
248
  return [cache.get(rule), options]
246
249
  }
247
250
 
251
+ const IS_VALID_PROPERTY_NAME = /^[a-z_-]/
252
+
253
+ function isValidPropName(name) {
254
+ return IS_VALID_PROPERTY_NAME.test(name)
255
+ }
256
+
257
+ function isParsableCssValue(property, value) {
258
+ try {
259
+ postcss.parse(`a{${property}:${value}}`).toResult()
260
+ return true
261
+ } catch (err) {
262
+ return false
263
+ }
264
+ }
265
+
266
+ function extractArbitraryProperty(classCandidate, context) {
267
+ let [, property, value] = classCandidate.match(/^\[([a-zA-Z0-9-_]+):(\S+)\]$/) ?? []
268
+
269
+ if (value === undefined) {
270
+ return null
271
+ }
272
+
273
+ if (!isValidPropName(property)) {
274
+ return null
275
+ }
276
+
277
+ if (!isValidArbitraryValue(value)) {
278
+ return null
279
+ }
280
+
281
+ let normalized = normalize(value)
282
+
283
+ if (!isParsableCssValue(property, normalized)) {
284
+ return null
285
+ }
286
+
287
+ return [
288
+ [
289
+ { sort: context.arbitraryPropertiesSort, layer: 'utilities' },
290
+ () => ({
291
+ [asClass(classCandidate)]: {
292
+ [property]: normalized,
293
+ },
294
+ }),
295
+ ],
296
+ ]
297
+ }
298
+
248
299
  function* resolveMatchedPlugins(classCandidate, context) {
249
300
  if (context.candidateRuleMap.has(classCandidate)) {
250
301
  yield [context.candidateRuleMap.get(classCandidate), 'DEFAULT']
251
302
  }
252
303
 
304
+ yield* (function* (arbitraryPropertyRule) {
305
+ if (arbitraryPropertyRule !== null) {
306
+ yield [arbitraryPropertyRule, 'DEFAULT']
307
+ }
308
+ })(extractArbitraryProperty(classCandidate, context))
309
+
253
310
  let candidatePrefix = classCandidate
254
311
  let negative = false
255
312
 
@@ -71,6 +71,8 @@ function extractElementSelector(selector) {
71
71
  export default function resolveDefaultsAtRules({ tailwindConfig }) {
72
72
  return (root) => {
73
73
  let variableNodeMap = new Map()
74
+
75
+ /** @type {Set<import('postcss').AtRule>} */
74
76
  let universals = new Set()
75
77
 
76
78
  root.walkAtRules('defaults', (rule) => {
@@ -90,31 +92,50 @@ export default function resolveDefaultsAtRules({ tailwindConfig }) {
90
92
  })
91
93
 
92
94
  for (let universal of universals) {
93
- let selectors = new Set()
95
+ /** @type {Map<string, Set<string>>} */
96
+ let selectorGroups = new Map()
94
97
 
95
98
  let rules = variableNodeMap.get(universal.params) ?? []
96
99
 
97
100
  for (let rule of rules) {
98
101
  for (let selector of extractElementSelector(rule.selector)) {
102
+ // If selector contains a vendor prefix after a pseudo element or class,
103
+ // we consider them separately because merging the declarations into
104
+ // a single rule will cause browsers that do not understand the
105
+ // vendor prefix to throw out the whole rule
106
+ let selectorGroupName =
107
+ selector.includes(':-') || selector.includes('::-') ? selector : '__DEFAULT__'
108
+
109
+ let selectors = selectorGroups.get(selectorGroupName) ?? new Set()
110
+ selectorGroups.set(selectorGroupName, selectors)
111
+
99
112
  selectors.add(selector)
100
113
  }
101
114
  }
102
115
 
103
- if (selectors.size === 0) {
116
+ if (selectorGroups.size === 0) {
104
117
  universal.remove()
105
118
  continue
106
119
  }
107
120
 
108
- let universalRule = postcss.rule()
109
-
110
121
  if (flagEnabled(tailwindConfig, 'optimizeUniversalDefaults')) {
111
- universalRule.selectors = [...selectors]
122
+ for (let [, selectors] of selectorGroups) {
123
+ let universalRule = postcss.rule()
124
+
125
+ universalRule.selectors = [...selectors]
126
+
127
+ universalRule.append(universal.nodes.map((node) => node.clone()))
128
+ universal.before(universalRule)
129
+ }
112
130
  } else {
131
+ let universalRule = postcss.rule()
132
+
113
133
  universalRule.selectors = ['*', '::before', '::after']
134
+
135
+ universalRule.append(universal.nodes)
136
+ universal.before(universalRule)
114
137
  }
115
138
 
116
- universalRule.append(universal.nodes)
117
- universal.before(universalRule)
118
139
  universal.remove()
119
140
  }
120
141
  }
@@ -18,6 +18,7 @@ import { env } from './sharedState'
18
18
  import { toPath } from '../util/toPath'
19
19
  import log from '../util/log'
20
20
  import negateValue from '../util/negateValue'
21
+ import isValidArbitraryValue from '../util/isValidArbitraryValue'
21
22
 
22
23
  function parseVariantFormatString(input) {
23
24
  if (input.includes('{')) {
@@ -130,64 +131,6 @@ function withIdentifiers(styles) {
130
131
  })
131
132
  }
132
133
 
133
- let matchingBrackets = new Map([
134
- ['{', '}'],
135
- ['[', ']'],
136
- ['(', ')'],
137
- ])
138
- let inverseMatchingBrackets = new Map(
139
- Array.from(matchingBrackets.entries()).map(([k, v]) => [v, k])
140
- )
141
-
142
- let quotes = new Set(['"', "'", '`'])
143
-
144
- // Arbitrary values must contain balanced brackets (), [] and {}. Escaped
145
- // values don't count, and brackets inside quotes also don't count.
146
- //
147
- // E.g.: w-[this-is]w-[weird-and-invalid]
148
- // E.g.: w-[this-is\\]w-\\[weird-but-valid]
149
- // E.g.: content-['this-is-also-valid]-weirdly-enough']
150
- function isValidArbitraryValue(value) {
151
- let stack = []
152
- let inQuotes = false
153
-
154
- for (let i = 0; i < value.length; i++) {
155
- let char = value[i]
156
-
157
- // Non-escaped quotes allow us to "allow" anything in between
158
- if (quotes.has(char) && value[i - 1] !== '\\') {
159
- inQuotes = !inQuotes
160
- }
161
-
162
- if (inQuotes) continue
163
- if (value[i - 1] === '\\') continue // Escaped
164
-
165
- if (matchingBrackets.has(char)) {
166
- stack.push(char)
167
- } else if (inverseMatchingBrackets.has(char)) {
168
- let inverse = inverseMatchingBrackets.get(char)
169
-
170
- // Nothing to pop from, therefore it is unbalanced
171
- if (stack.length <= 0) {
172
- return false
173
- }
174
-
175
- // Popped value must match the inverse value, otherwise it is unbalanced
176
- if (stack.pop() !== inverse) {
177
- return false
178
- }
179
- }
180
- }
181
-
182
- // If there is still something on the stack, it is also unbalanced
183
- if (stack.length > 0) {
184
- return false
185
- }
186
-
187
- // All good, totally balanced!
188
- return true
189
- }
190
-
191
134
  function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offsets, classList }) {
192
135
  function getConfigValue(path, defaultValue) {
193
136
  return path ? dlv(tailwindConfig, path, defaultValue) : tailwindConfig
@@ -290,6 +233,28 @@ function buildPluginApi(tailwindConfig, context, { variantList, variantMap, offs
290
233
  .push([{ sort: offset, layer: 'base' }, rule])
291
234
  }
292
235
  },
236
+ /**
237
+ * @param {string} group
238
+ * @param {Record<string, string | string[]>} declarations
239
+ */
240
+ addDefaults(group, declarations) {
241
+ const groups = {
242
+ [`@defaults ${group}`]: declarations,
243
+ }
244
+
245
+ for (let [identifier, rule] of withIdentifiers(groups)) {
246
+ let prefixedIdentifier = prefixIdentifier(identifier, {})
247
+ let offset = offsets.base++
248
+
249
+ if (!context.candidateRuleMap.has(prefixedIdentifier)) {
250
+ context.candidateRuleMap.set(prefixedIdentifier, [])
251
+ }
252
+
253
+ context.candidateRuleMap
254
+ .get(prefixedIdentifier)
255
+ .push([{ sort: offset, layer: 'defaults' }, rule])
256
+ }
257
+ },
293
258
  addComponents(components, options) {
294
259
  let defaultOptions = {
295
260
  respectPrefix: true,
@@ -575,6 +540,7 @@ function resolvePlugins(context, root) {
575
540
  variantPlugins['darkVariants'],
576
541
  variantPlugins['printVariant'],
577
542
  variantPlugins['screenVariants'],
543
+ variantPlugins['orientationVariants'],
578
544
  ]
579
545
 
580
546
  return [...corePluginList, ...beforeVariants, ...userPlugins, ...afterVariants, ...layerPlugins]
@@ -584,6 +550,7 @@ function registerPlugins(plugins, context) {
584
550
  let variantList = []
585
551
  let variantMap = new Map()
586
552
  let offsets = {
553
+ defaults: 0n,
587
554
  base: 0n,
588
555
  components: 0n,
589
556
  utilities: 0n,
@@ -611,20 +578,26 @@ function registerPlugins(plugins, context) {
611
578
 
612
579
  let highestOffset = ((args) => args.reduce((m, e) => (e > m ? e : m)))([
613
580
  offsets.base,
581
+ offsets.defaults,
614
582
  offsets.components,
615
583
  offsets.utilities,
616
584
  offsets.user,
617
585
  ])
618
586
  let reservedBits = BigInt(highestOffset.toString(2).length)
619
587
 
588
+ // A number one less than the top range of the highest offset area
589
+ // so arbitrary properties are always sorted at the end.
590
+ context.arbitraryPropertiesSort = ((1n << reservedBits) << 0n) - 1n
591
+
620
592
  context.layerOrder = {
621
- base: (1n << reservedBits) << 0n,
622
- components: (1n << reservedBits) << 1n,
623
- utilities: (1n << reservedBits) << 2n,
624
- user: (1n << reservedBits) << 3n,
593
+ defaults: (1n << reservedBits) << 0n,
594
+ base: (1n << reservedBits) << 1n,
595
+ components: (1n << reservedBits) << 2n,
596
+ utilities: (1n << reservedBits) << 3n,
597
+ user: (1n << reservedBits) << 4n,
625
598
  }
626
599
 
627
- reservedBits += 4n
600
+ reservedBits += 5n
628
601
 
629
602
  let offset = 0
630
603
  context.variantOrder = new Map(
@@ -678,7 +651,15 @@ function registerPlugins(plugins, context) {
678
651
  let utils = Array.isArray(util)
679
652
  ? (() => {
680
653
  let [utilName, options] = util
681
- return Object.keys(options?.values ?? {}).map((value) => formatClass(utilName, value))
654
+ let classes = Object.keys(options?.values ?? {}).map((value) =>
655
+ formatClass(utilName, value)
656
+ )
657
+
658
+ if (options?.supportsNegativeValues) {
659
+ classes = [...classes, ...classes.map((cls) => '-' + cls)]
660
+ }
661
+
662
+ return classes
682
663
  })()
683
664
  : [util]
684
665
 
@@ -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
  }
@@ -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
  }
@@ -1,6 +1,8 @@
1
1
  import { parseColor } from './color'
2
2
  import { parseBoxShadowValue } from './parseBoxShadowValue'
3
3
 
4
+ let cssFunctions = ['min', 'max', 'clamp', 'calc']
5
+
4
6
  // Ref: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Types
5
7
 
6
8
  let COMMA = /,(?![^(]*\))/g // Comma separator that is not located between brackets. E.g.: `cubiz-bezier(a, b, c)` these don't count.
@@ -51,11 +53,11 @@ export function url(value) {
51
53
  }
52
54
 
53
55
  export function number(value) {
54
- return !isNaN(Number(value))
56
+ return !isNaN(Number(value)) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?`).test(value))
55
57
  }
56
58
 
57
59
  export function percentage(value) {
58
- return /%$/g.test(value) || /^calc\(.+?%\)/g.test(value)
60
+ return /%$/g.test(value) || cssFunctions.some((fn) => new RegExp(`^${fn}\\(.+?%`).test(value))
59
61
  }
60
62
 
61
63
  let lengthUnits = [
@@ -78,10 +80,13 @@ let lengthUnits = [
78
80
  ]
79
81
  let lengthUnitsPattern = `(?:${lengthUnits.join('|')})`
80
82
  export function length(value) {
81
- return (
82
- new RegExp(`${lengthUnitsPattern}$`).test(value) ||
83
- new RegExp(`^calc\\(.+?${lengthUnitsPattern}`).test(value)
84
- )
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
+ })
85
90
  }
86
91
 
87
92
  let lineWidths = new Set(['thin', 'medium', 'thick'])
@@ -5,6 +5,12 @@ export function defaults(target, ...sources) {
5
5
  target[k] = source[k]
6
6
  }
7
7
  }
8
+
9
+ for (let k of Object.getOwnPropertySymbols(source)) {
10
+ if (!target?.hasOwnProperty?.(k)) {
11
+ target[k] = source[k]
12
+ }
13
+ }
8
14
  }
9
15
 
10
16
  return target
@@ -30,7 +30,17 @@ export function formatVariantSelector(current, ...others) {
30
30
  }
31
31
 
32
32
  export function finalizeSelector(format, { selector, candidate, context }) {
33
- let base = candidate.split(context?.tailwindConfig?.separator ?? ':').pop()
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()
34
44
 
35
45
  if (context?.tailwindConfig?.prefix) {
36
46
  format = prefixSelector(context.tailwindConfig.prefix, format)
@@ -74,11 +84,92 @@ export function finalizeSelector(format, { selector, candidate, context }) {
74
84
  return p
75
85
  })
76
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
+
77
118
  return selector
78
119
  })
79
120
  }).processSync(selector)
80
121
  }
81
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
+
82
173
  function resolveFunctionArgument(haystack, needle, arg) {
83
174
  let startIdx = haystack.indexOf(arg ? `${needle}(${arg})` : needle)
84
175
  if (startIdx === -1) return null