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.
- package/colors.js +2 -1
- package/defaultConfig.js +2 -1
- package/defaultTheme.js +2 -1
- package/lib/cli.js +39 -35
- package/lib/constants.js +1 -1
- package/lib/corePluginList.js +10 -1
- package/lib/corePlugins.js +393 -259
- package/lib/css/preflight.css +14 -1
- package/lib/featureFlags.js +12 -7
- package/lib/lib/collapseDuplicateDeclarations.js +29 -0
- package/lib/lib/detectNesting.js +17 -2
- package/lib/lib/evaluateTailwindFunctions.js +9 -5
- package/lib/lib/expandApplyAtRules.js +26 -9
- package/lib/lib/expandTailwindAtRules.js +4 -1
- package/lib/lib/generateRules.js +151 -19
- package/lib/lib/resolveDefaultsAtRules.js +67 -56
- package/lib/lib/setupContextUtils.js +80 -80
- package/lib/lib/setupWatchingContext.js +5 -1
- package/lib/lib/sharedState.js +2 -2
- package/lib/lib/substituteScreenAtRules.js +7 -4
- package/lib/processTailwindFeatures.js +4 -0
- package/lib/util/buildMediaQuery.js +13 -24
- package/lib/util/createUtilityPlugin.js +5 -5
- package/lib/util/dataTypes.js +38 -7
- package/lib/util/formatVariantSelector.js +186 -0
- package/lib/util/isValidArbitraryValue.js +64 -0
- package/lib/util/nameClass.js +2 -1
- package/lib/util/negateValue.js +3 -1
- package/lib/util/normalizeConfig.js +22 -8
- package/lib/util/normalizeScreens.js +61 -0
- package/lib/util/parseBoxShadowValue.js +77 -0
- package/lib/util/pluginUtils.js +62 -158
- package/lib/util/prefixSelector.js +1 -3
- package/lib/util/resolveConfig.js +17 -13
- package/lib/util/transformThemeValue.js +23 -13
- package/package.json +15 -15
- package/peers/index.js +4456 -5450
- package/plugin.js +2 -1
- package/resolveConfig.js +2 -1
- package/src/.DS_Store +0 -0
- package/src/cli.js +9 -2
- package/src/corePluginList.js +1 -1
- package/src/corePlugins.js +392 -404
- package/src/css/preflight.css +14 -1
- package/src/featureFlags.js +14 -4
- package/src/lib/collapseDuplicateDeclarations.js +28 -0
- package/src/lib/detectNesting.js +22 -3
- package/src/lib/evaluateTailwindFunctions.js +5 -2
- package/src/lib/expandApplyAtRules.js +29 -2
- package/src/lib/expandTailwindAtRules.js +5 -2
- package/src/lib/generateRules.js +155 -11
- package/src/lib/resolveDefaultsAtRules.js +67 -50
- package/src/lib/setupContextUtils.js +77 -67
- package/src/lib/setupWatchingContext.js +7 -0
- package/src/lib/sharedState.js +1 -1
- package/src/lib/substituteScreenAtRules.js +6 -3
- package/src/processTailwindFeatures.js +5 -0
- package/src/util/buildMediaQuery.js +14 -18
- package/src/util/createUtilityPlugin.js +2 -2
- package/src/util/dataTypes.js +43 -11
- package/src/util/formatVariantSelector.js +196 -0
- package/src/util/isValidArbitraryValue.js +61 -0
- package/src/util/nameClass.js +2 -2
- package/src/util/negateValue.js +4 -2
- package/src/util/normalizeConfig.js +17 -1
- package/src/util/normalizeScreens.js +45 -0
- package/src/util/parseBoxShadowValue.js +71 -0
- package/src/util/pluginUtils.js +50 -146
- package/src/util/prefixSelector.js +1 -4
- package/src/util/resolveConfig.js +7 -1
- package/src/util/transformThemeValue.js +22 -7
- package/stubs/defaultConfig.stub.js +118 -58
- 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
|
|
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
|
|
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.
|
|
675
|
-
output.push(formatClass(utilName,
|
|
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)
|
package/src/lib/sharedState.js
CHANGED
|
@@ -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
|
-
|
|
7
|
+
let screen = atRule.params
|
|
8
|
+
let screens = normalizeScreens(theme.screens)
|
|
9
|
+
let screenDefinition = screens.find(({ name }) => name === screen)
|
|
7
10
|
|
|
8
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
.map((screen) =>
|
|
6
|
+
screen.values.map((screen) => {
|
|
7
|
+
if (screen.raw !== undefined) {
|
|
8
|
+
return screen.raw
|
|
9
|
+
}
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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,
|
|
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
|
}
|
package/src/util/dataTypes.js
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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) ||
|
|
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
|
-
|
|
68
|
-
|
|
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
|
+
}
|