tailwindcss 3.2.3 → 3.2.5
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/CHANGELOG.md +56 -1
- package/README.md +1 -1
- package/lib/cli/build/index.js +5 -1
- package/lib/cli/build/plugin.js +50 -34
- package/lib/cli/build/watching.js +6 -3
- package/lib/cli/index.js +231 -10
- package/lib/cli/init/index.js +2 -2
- package/lib/cli.js +4 -226
- package/lib/corePlugins.js +45 -27
- package/lib/featureFlags.js +8 -8
- package/lib/index.js +4 -46
- package/lib/lib/collapseAdjacentRules.js +2 -2
- package/lib/lib/collapseDuplicateDeclarations.js +2 -2
- package/lib/lib/content.js +16 -16
- package/lib/lib/defaultExtractor.js +10 -5
- package/lib/lib/detectNesting.js +7 -1
- package/lib/lib/evaluateTailwindFunctions.js +4 -4
- package/lib/lib/expandApplyAtRules.js +2 -2
- package/lib/lib/expandTailwindAtRules.js +35 -9
- package/lib/lib/findAtConfigPath.js +3 -3
- package/lib/lib/generateRules.js +105 -50
- package/lib/lib/offsets.js +88 -1
- package/lib/lib/remap-bitfield.js +87 -0
- package/lib/lib/resolveDefaultsAtRules.js +4 -4
- package/lib/lib/setupContextUtils.js +122 -79
- package/lib/lib/setupTrackingContext.js +25 -4
- package/lib/lib/sharedState.js +19 -1
- package/lib/oxide/cli/build/deps.js +81 -0
- package/lib/oxide/cli/build/index.js +47 -0
- package/lib/oxide/cli/build/plugin.js +364 -0
- package/lib/oxide/cli/build/utils.js +77 -0
- package/lib/oxide/cli/build/watching.js +177 -0
- package/lib/oxide/cli/help/index.js +70 -0
- package/lib/oxide/cli/index.js +220 -0
- package/lib/oxide/cli/init/index.js +35 -0
- package/lib/oxide/cli.js +5 -0
- package/lib/oxide/postcss-plugin.js +2 -0
- package/lib/plugin.js +98 -0
- package/lib/postcss-plugins/nesting/plugin.js +2 -2
- package/lib/util/cloneNodes.js +2 -2
- package/lib/util/color.js +20 -6
- package/lib/util/createUtilityPlugin.js +2 -2
- package/lib/util/dataTypes.js +26 -2
- package/lib/util/defaults.js +4 -4
- package/lib/util/escapeClassName.js +3 -3
- package/lib/util/formatVariantSelector.js +171 -105
- package/lib/util/getAllConfigs.js +2 -2
- package/lib/util/{isValidArbitraryValue.js → isSyntacticallyValidPropertyValue.js} +2 -2
- package/lib/util/negateValue.js +2 -2
- package/lib/util/normalizeConfig.js +36 -22
- package/lib/util/pluginUtils.js +38 -40
- package/lib/util/prefixSelector.js +22 -8
- package/lib/util/resolveConfig.js +8 -10
- package/oxide-node-api-shim/index.js +21 -0
- package/oxide-node-api-shim/package.json +5 -0
- package/package.json +32 -19
- package/peers/index.js +61 -42
- package/resolveConfig.d.ts +11 -2
- package/scripts/swap-engines.js +40 -0
- package/src/cli/build/index.js +6 -2
- package/src/cli/build/plugin.js +31 -9
- package/src/cli/build/watching.js +6 -3
- package/src/cli/index.js +234 -3
- package/src/cli.js +4 -220
- package/src/corePlugins.js +31 -3
- package/src/index.js +4 -46
- package/src/lib/content.js +12 -17
- package/src/lib/defaultExtractor.js +9 -3
- package/src/lib/detectNesting.js +9 -1
- package/src/lib/expandTailwindAtRules.js +37 -6
- package/src/lib/generateRules.js +90 -28
- package/src/lib/offsets.js +104 -1
- package/src/lib/remap-bitfield.js +82 -0
- package/src/lib/setupContextUtils.js +99 -56
- package/src/lib/setupTrackingContext.js +31 -6
- package/src/lib/sharedState.js +17 -0
- package/src/oxide/cli/build/deps.ts +91 -0
- package/src/oxide/cli/build/index.ts +47 -0
- package/src/oxide/cli/build/plugin.ts +436 -0
- package/src/oxide/cli/build/utils.ts +74 -0
- package/src/oxide/cli/build/watching.ts +225 -0
- package/src/oxide/cli/help/index.ts +69 -0
- package/src/oxide/cli/index.ts +212 -0
- package/src/oxide/cli/init/index.ts +32 -0
- package/src/oxide/cli.ts +1 -0
- package/src/oxide/postcss-plugin.ts +1 -0
- package/src/plugin.js +107 -0
- package/src/util/color.js +17 -2
- package/src/util/dataTypes.js +29 -4
- package/src/util/formatVariantSelector.js +215 -122
- package/src/util/{isValidArbitraryValue.js → isSyntacticallyValidPropertyValue.js} +1 -1
- package/src/util/negateValue.js +1 -1
- package/src/util/normalizeConfig.js +18 -0
- package/src/util/pluginUtils.js +22 -19
- package/src/util/prefixSelector.js +28 -10
- package/src/util/resolveConfig.js +0 -2
- package/stubs/defaultConfig.stub.js +149 -165
- package/types/config.d.ts +7 -2
- package/types/generated/default-theme.d.ts +77 -77
- package/lib/cli/shared.js +0 -12
- package/scripts/install-integrations.js +0 -27
- package/scripts/rebuildFixtures.js +0 -68
- package/src/cli/shared.js +0 -5
package/src/lib/content.js
CHANGED
|
@@ -164,50 +164,45 @@ function resolvePathSymlinks(contentPath) {
|
|
|
164
164
|
* @param {any} context
|
|
165
165
|
* @param {ContentPath[]} candidateFiles
|
|
166
166
|
* @param {Map<string, number>} fileModifiedMap
|
|
167
|
-
* @returns {{ content: string, extension: string }[]}
|
|
167
|
+
* @returns {[{ content: string, extension: string }[], Map<string, number>]}
|
|
168
168
|
*/
|
|
169
169
|
export function resolvedChangedContent(context, candidateFiles, fileModifiedMap) {
|
|
170
170
|
let changedContent = context.tailwindConfig.content.files
|
|
171
171
|
.filter((item) => typeof item.raw === 'string')
|
|
172
172
|
.map(({ raw, extension = 'html' }) => ({ content: raw, extension }))
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
let [changedFiles, mTimesToCommit] = resolveChangedFiles(candidateFiles, fileModifiedMap)
|
|
175
|
+
|
|
176
|
+
for (let changedFile of changedFiles) {
|
|
176
177
|
let extension = path.extname(changedFile).slice(1)
|
|
177
|
-
changedContent.push({
|
|
178
|
+
changedContent.push({ file: changedFile, extension })
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
return changedContent
|
|
181
|
+
return [changedContent, mTimesToCommit]
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
/**
|
|
184
185
|
*
|
|
185
186
|
* @param {ContentPath[]} candidateFiles
|
|
186
187
|
* @param {Map<string, number>} fileModifiedMap
|
|
187
|
-
* @returns {Set<string>}
|
|
188
|
+
* @returns {[Set<string>, Map<string, number>]}
|
|
188
189
|
*/
|
|
189
190
|
function resolveChangedFiles(candidateFiles, fileModifiedMap) {
|
|
190
191
|
let paths = candidateFiles.map((contentPath) => contentPath.pattern)
|
|
192
|
+
let mTimesToCommit = new Map()
|
|
191
193
|
|
|
192
194
|
let changedFiles = new Set()
|
|
193
195
|
env.DEBUG && console.time('Finding changed files')
|
|
194
196
|
let files = fastGlob.sync(paths, { absolute: true })
|
|
195
197
|
for (let file of files) {
|
|
196
|
-
let prevModified = fileModifiedMap.
|
|
198
|
+
let prevModified = fileModifiedMap.get(file) || -Infinity
|
|
197
199
|
let modified = fs.statSync(file).mtimeMs
|
|
198
200
|
|
|
199
|
-
|
|
200
|
-
// earier in the process and we want to make sure we don't miss any changes that happen
|
|
201
|
-
// when a context dependency is also a content dependency
|
|
202
|
-
// Ideally, we'd do all this tracking at one time but that is a larger refactor
|
|
203
|
-
// than we want to commit to right now, so this is a decent compromise.
|
|
204
|
-
// This should be sufficient because file modification times will be off by at least
|
|
205
|
-
// 1ms (the precision of fstat in Node) in most cases if they exist and were changed.
|
|
206
|
-
if (modified >= prevModified) {
|
|
201
|
+
if (modified > prevModified) {
|
|
207
202
|
changedFiles.add(file)
|
|
208
|
-
|
|
203
|
+
mTimesToCommit.set(file, modified)
|
|
209
204
|
}
|
|
210
205
|
}
|
|
211
206
|
env.DEBUG && console.timeEnd('Finding changed files')
|
|
212
|
-
return changedFiles
|
|
207
|
+
return [changedFiles, mTimesToCommit]
|
|
213
208
|
}
|
|
@@ -28,8 +28,14 @@ function* buildRegExps(context) {
|
|
|
28
28
|
: ''
|
|
29
29
|
|
|
30
30
|
let utility = regex.any([
|
|
31
|
-
// Arbitrary properties
|
|
32
|
-
/\[[^\s:'"`]+:[^\s]+\]/,
|
|
31
|
+
// Arbitrary properties (without square brackets)
|
|
32
|
+
/\[[^\s:'"`]+:[^\s\[\]]+\]/,
|
|
33
|
+
|
|
34
|
+
// Arbitrary properties with balanced square brackets
|
|
35
|
+
// This is a targeted fix to continue to allow theme()
|
|
36
|
+
// with square brackets to work in arbitrary properties
|
|
37
|
+
// while fixing a problem with the regex matching too much
|
|
38
|
+
/\[[^\s:'"`]+:[^\s]+?\[[^\s]+?\][^\s]+?\]/,
|
|
33
39
|
|
|
34
40
|
// Utilities
|
|
35
41
|
regex.pattern([
|
|
@@ -184,7 +190,7 @@ function clipAtBalancedParens(input) {
|
|
|
184
190
|
// This means that there was an extra closing `]`
|
|
185
191
|
// We'll clip to just before it
|
|
186
192
|
if (depth < 0) {
|
|
187
|
-
return input.substring(0, match.index)
|
|
193
|
+
return input.substring(0, match.index - 1)
|
|
188
194
|
}
|
|
189
195
|
|
|
190
196
|
// We've finished balancing the brackets but there still may be characters that can be included
|
package/src/lib/detectNesting.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
function isRoot(node) {
|
|
2
|
+
return node.type === 'root'
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function isAtLayer(node) {
|
|
6
|
+
return node.type === 'atrule' && node.name === 'layer'
|
|
7
|
+
}
|
|
8
|
+
|
|
1
9
|
export default function (_context) {
|
|
2
10
|
return (root, result) => {
|
|
3
11
|
let found = false
|
|
@@ -5,7 +13,7 @@ export default function (_context) {
|
|
|
5
13
|
root.walkAtRules('tailwind', (node) => {
|
|
6
14
|
if (found) return false
|
|
7
15
|
|
|
8
|
-
if (node.parent && node.parent
|
|
16
|
+
if (node.parent && !(isRoot(node.parent) || isAtLayer(node.parent))) {
|
|
9
17
|
found = true
|
|
10
18
|
node.warn(
|
|
11
19
|
result,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
1
2
|
import LRU from 'quick-lru'
|
|
2
3
|
import * as sharedState from './sharedState'
|
|
3
4
|
import { generateRules } from './generateRules'
|
|
@@ -5,6 +6,8 @@ import log from '../util/log'
|
|
|
5
6
|
import cloneNodes from '../util/cloneNodes'
|
|
6
7
|
import { defaultExtractor } from './defaultExtractor'
|
|
7
8
|
|
|
9
|
+
import oxide from '@tailwindcss/oxide'
|
|
10
|
+
|
|
8
11
|
let env = sharedState.env
|
|
9
12
|
|
|
10
13
|
const builtInExtractors = {
|
|
@@ -124,15 +127,32 @@ export default function expandTailwindAtRules(context) {
|
|
|
124
127
|
// ---
|
|
125
128
|
|
|
126
129
|
// Find potential rules in changed files
|
|
127
|
-
let candidates = new Set([sharedState.NOT_ON_DEMAND])
|
|
130
|
+
let candidates = new Set([...(context.candidates ?? []), sharedState.NOT_ON_DEMAND])
|
|
128
131
|
let seen = new Set()
|
|
129
132
|
|
|
130
133
|
env.DEBUG && console.time('Reading changed files')
|
|
131
134
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let
|
|
135
|
-
|
|
135
|
+
if (env.OXIDE) {
|
|
136
|
+
// TODO: Pass through or implement `extractor`
|
|
137
|
+
for (let candidate of oxide.parseCandidateStringsFromFiles(
|
|
138
|
+
context.changedContent
|
|
139
|
+
// Object.assign({}, builtInTransformers, context.tailwindConfig.content.transform)
|
|
140
|
+
)) {
|
|
141
|
+
candidates.add(candidate)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// for (let { file, content, extension } of context.changedContent) {
|
|
145
|
+
// let transformer = getTransformer(context.tailwindConfig, extension)
|
|
146
|
+
// let extractor = getExtractor(context, extension)
|
|
147
|
+
// getClassCandidatesOxide(file, transformer(content), extractor, candidates, seen)
|
|
148
|
+
// }
|
|
149
|
+
} else {
|
|
150
|
+
for (let { file, content, extension } of context.changedContent) {
|
|
151
|
+
let transformer = getTransformer(context.tailwindConfig, extension)
|
|
152
|
+
let extractor = getExtractor(context, extension)
|
|
153
|
+
content = file ? fs.readFileSync(file, 'utf8') : content
|
|
154
|
+
getClassCandidates(transformer(content), extractor, candidates, seen)
|
|
155
|
+
}
|
|
136
156
|
}
|
|
137
157
|
|
|
138
158
|
env.DEBUG && console.timeEnd('Reading changed files')
|
|
@@ -143,7 +163,18 @@ export default function expandTailwindAtRules(context) {
|
|
|
143
163
|
let classCacheCount = context.classCache.size
|
|
144
164
|
|
|
145
165
|
env.DEBUG && console.time('Generate rules')
|
|
146
|
-
|
|
166
|
+
env.DEBUG && console.time('Sorting candidates')
|
|
167
|
+
let sortedCandidates = env.OXIDE
|
|
168
|
+
? candidates
|
|
169
|
+
: new Set(
|
|
170
|
+
[...candidates].sort((a, z) => {
|
|
171
|
+
if (a === z) return 0
|
|
172
|
+
if (a < z) return -1
|
|
173
|
+
return 1
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
env.DEBUG && console.timeEnd('Sorting candidates')
|
|
177
|
+
generateRules(sortedCandidates, context)
|
|
147
178
|
env.DEBUG && console.timeEnd('Generate rules')
|
|
148
179
|
|
|
149
180
|
// We only ever add to the classCache, so if it didn't grow, there is nothing new.
|
package/src/lib/generateRules.js
CHANGED
|
@@ -10,7 +10,7 @@ import { formatVariantSelector, finalizeSelector } from '../util/formatVariantSe
|
|
|
10
10
|
import { asClass } from '../util/nameClass'
|
|
11
11
|
import { normalize } from '../util/dataTypes'
|
|
12
12
|
import { isValidVariantFormatString, parseVariant } from './setupContextUtils'
|
|
13
|
-
import isValidArbitraryValue from '../util/
|
|
13
|
+
import isValidArbitraryValue from '../util/isSyntacticallyValidPropertyValue'
|
|
14
14
|
import { splitAtTopLevelOnly } from '../util/splitAtTopLevelOnly.js'
|
|
15
15
|
import { flagEnabled } from '../featureFlags'
|
|
16
16
|
|
|
@@ -152,10 +152,18 @@ function applyVariant(variant, matches, context) {
|
|
|
152
152
|
|
|
153
153
|
// Retrieve "modifier"
|
|
154
154
|
{
|
|
155
|
-
let
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
let [baseVariant, ...modifiers] = splitAtTopLevelOnly(variant, '/')
|
|
156
|
+
|
|
157
|
+
// This is a hack to support variants with `/` in them, like `ar-1/10/20:text-red-500`
|
|
158
|
+
// In this case 1/10 is a value but /20 is a modifier
|
|
159
|
+
if (modifiers.length > 1) {
|
|
160
|
+
baseVariant = baseVariant + '/' + modifiers.slice(0, -1).join('/')
|
|
161
|
+
modifiers = modifiers.slice(-1)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (modifiers.length && !context.variantMap.has(variant)) {
|
|
165
|
+
variant = baseVariant
|
|
166
|
+
args.modifier = modifiers[0]
|
|
159
167
|
|
|
160
168
|
if (!flagEnabled(context.tailwindConfig, 'generalizedModifiers')) {
|
|
161
169
|
return []
|
|
@@ -201,6 +209,7 @@ function applyVariant(variant, matches, context) {
|
|
|
201
209
|
}
|
|
202
210
|
|
|
203
211
|
if (context.variantMap.has(variant)) {
|
|
212
|
+
let isArbitraryVariant = isArbitraryValue(variant)
|
|
204
213
|
let variantFunctionTuples = context.variantMap.get(variant).slice()
|
|
205
214
|
let result = []
|
|
206
215
|
|
|
@@ -262,7 +271,10 @@ function applyVariant(variant, matches, context) {
|
|
|
262
271
|
clone.append(wrapper)
|
|
263
272
|
},
|
|
264
273
|
format(selectorFormat) {
|
|
265
|
-
collectedFormats.push(
|
|
274
|
+
collectedFormats.push({
|
|
275
|
+
format: selectorFormat,
|
|
276
|
+
isArbitraryVariant,
|
|
277
|
+
})
|
|
266
278
|
},
|
|
267
279
|
args,
|
|
268
280
|
})
|
|
@@ -288,7 +300,10 @@ function applyVariant(variant, matches, context) {
|
|
|
288
300
|
}
|
|
289
301
|
|
|
290
302
|
if (typeof ruleWithVariant === 'string') {
|
|
291
|
-
collectedFormats.push(
|
|
303
|
+
collectedFormats.push({
|
|
304
|
+
format: ruleWithVariant,
|
|
305
|
+
isArbitraryVariant,
|
|
306
|
+
})
|
|
292
307
|
}
|
|
293
308
|
|
|
294
309
|
if (ruleWithVariant === null) {
|
|
@@ -329,7 +344,10 @@ function applyVariant(variant, matches, context) {
|
|
|
329
344
|
// modified (by plugin): .foo .foo\\:markdown > p
|
|
330
345
|
// rebuiltBase (internal): .foo\\:markdown > p
|
|
331
346
|
// format: .foo &
|
|
332
|
-
collectedFormats.push(
|
|
347
|
+
collectedFormats.push({
|
|
348
|
+
format: modified.replace(rebuiltBase, '&'),
|
|
349
|
+
isArbitraryVariant,
|
|
350
|
+
})
|
|
333
351
|
rule.selector = before
|
|
334
352
|
})
|
|
335
353
|
}
|
|
@@ -349,7 +367,6 @@ function applyVariant(variant, matches, context) {
|
|
|
349
367
|
Object.assign(args, context.variantOptions.get(variant))
|
|
350
368
|
),
|
|
351
369
|
collectedFormats: (meta.collectedFormats ?? []).concat(collectedFormats),
|
|
352
|
-
isArbitraryVariant: isArbitraryValue(variant),
|
|
353
370
|
},
|
|
354
371
|
clone.nodes[0],
|
|
355
372
|
]
|
|
@@ -412,7 +429,7 @@ function isParsableNode(node) {
|
|
|
412
429
|
let isParsable = true
|
|
413
430
|
|
|
414
431
|
node.walkDecls((decl) => {
|
|
415
|
-
if (!isParsableCssValue(decl.
|
|
432
|
+
if (!isParsableCssValue(decl.prop, decl.value)) {
|
|
416
433
|
isParsable = false
|
|
417
434
|
return false
|
|
418
435
|
}
|
|
@@ -736,24 +753,13 @@ function* resolveMatches(candidate, context, original = candidate) {
|
|
|
736
753
|
match[1].raws.tailwind = { ...match[1].raws.tailwind, candidate }
|
|
737
754
|
|
|
738
755
|
// Apply final format selector
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
selector: rule.selector,
|
|
747
|
-
candidate: original,
|
|
748
|
-
base: candidate
|
|
749
|
-
.split(new RegExp(`\\${context?.tailwindConfig?.separator ?? ':'}(?![^[]*\\])`))
|
|
750
|
-
.pop(),
|
|
751
|
-
isArbitraryVariant: match[0].isArbitraryVariant,
|
|
752
|
-
|
|
753
|
-
context,
|
|
754
|
-
})
|
|
755
|
-
})
|
|
756
|
-
match[1] = container.nodes[0]
|
|
756
|
+
match = applyFinalFormat(match, { context, candidate, original })
|
|
757
|
+
|
|
758
|
+
// Skip rules with invalid selectors
|
|
759
|
+
// This will cause the candidate to be added to the "not class"
|
|
760
|
+
// cache skipping it entirely for future builds
|
|
761
|
+
if (match === null) {
|
|
762
|
+
continue
|
|
757
763
|
}
|
|
758
764
|
|
|
759
765
|
yield match
|
|
@@ -761,6 +767,62 @@ function* resolveMatches(candidate, context, original = candidate) {
|
|
|
761
767
|
}
|
|
762
768
|
}
|
|
763
769
|
|
|
770
|
+
function applyFinalFormat(match, { context, candidate, original }) {
|
|
771
|
+
if (!match[0].collectedFormats) {
|
|
772
|
+
return match
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
let isValid = true
|
|
776
|
+
let finalFormat
|
|
777
|
+
|
|
778
|
+
try {
|
|
779
|
+
finalFormat = formatVariantSelector(match[0].collectedFormats, {
|
|
780
|
+
context,
|
|
781
|
+
candidate,
|
|
782
|
+
})
|
|
783
|
+
} catch {
|
|
784
|
+
// The format selector we produced is invalid
|
|
785
|
+
// This could be because:
|
|
786
|
+
// - A bug exists
|
|
787
|
+
// - A plugin introduced an invalid variant selector (ex: `addVariant('foo', '&;foo')`)
|
|
788
|
+
// - The user used an invalid arbitrary variant (ex: `[&;foo]:underline`)
|
|
789
|
+
// Either way the build will fail because of this
|
|
790
|
+
// We would rather that the build pass "silently" given that this could
|
|
791
|
+
// happen because of picking up invalid things when scanning content
|
|
792
|
+
// So we'll throw out the candidate instead
|
|
793
|
+
|
|
794
|
+
return null
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
let container = postcss.root({ nodes: [match[1].clone()] })
|
|
798
|
+
|
|
799
|
+
container.walkRules((rule) => {
|
|
800
|
+
if (inKeyframes(rule)) {
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
rule.selector = finalizeSelector(rule.selector, finalFormat, {
|
|
806
|
+
candidate: original,
|
|
807
|
+
context,
|
|
808
|
+
})
|
|
809
|
+
} catch {
|
|
810
|
+
// If this selector is invalid we also want to skip it
|
|
811
|
+
// But it's likely that being invalid here means there's a bug in a plugin rather than too loosely matching content
|
|
812
|
+
isValid = false
|
|
813
|
+
return false
|
|
814
|
+
}
|
|
815
|
+
})
|
|
816
|
+
|
|
817
|
+
if (!isValid) {
|
|
818
|
+
return null
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
match[1] = container.nodes[0]
|
|
822
|
+
|
|
823
|
+
return match
|
|
824
|
+
}
|
|
825
|
+
|
|
764
826
|
function inKeyframes(rule) {
|
|
765
827
|
return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
|
|
766
828
|
}
|
package/src/lib/offsets.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import bigSign from '../util/bigSign'
|
|
4
|
+
import { remapBitfield } from './remap-bitfield.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @typedef {'base' | 'defaults' | 'components' | 'utilities' | 'variants' | 'user'} Layer
|
|
@@ -12,6 +13,7 @@ import bigSign from '../util/bigSign'
|
|
|
12
13
|
* @property {function | undefined} sort The sort function
|
|
13
14
|
* @property {string|null} value The value we want to compare
|
|
14
15
|
* @property {string|null} modifier The modifier that was used (if any)
|
|
16
|
+
* @property {bigint} variant The variant bitmask
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -126,6 +128,8 @@ export class Offsets {
|
|
|
126
128
|
* @returns {RuleOffset}
|
|
127
129
|
*/
|
|
128
130
|
applyVariantOffset(rule, variant, options) {
|
|
131
|
+
options.variant = variant.variants
|
|
132
|
+
|
|
129
133
|
return {
|
|
130
134
|
...rule,
|
|
131
135
|
layer: 'variants',
|
|
@@ -190,7 +194,7 @@ export class Offsets {
|
|
|
190
194
|
|
|
191
195
|
return {
|
|
192
196
|
...this.create('variants'),
|
|
193
|
-
variants:
|
|
197
|
+
variants: this.variantOffsets.get(variant),
|
|
194
198
|
}
|
|
195
199
|
}
|
|
196
200
|
|
|
@@ -205,11 +209,30 @@ export class Offsets {
|
|
|
205
209
|
return this.layerPositions[a.layer] - this.layerPositions[b.layer]
|
|
206
210
|
}
|
|
207
211
|
|
|
212
|
+
// When sorting the `variants` layer, we need to sort based on the parent layer as well within
|
|
213
|
+
// this variants layer.
|
|
214
|
+
if (a.parentLayer !== b.parentLayer) {
|
|
215
|
+
return this.layerPositions[a.parentLayer] - this.layerPositions[b.parentLayer]
|
|
216
|
+
}
|
|
217
|
+
|
|
208
218
|
// Sort based on the sorting function
|
|
209
219
|
for (let aOptions of a.options) {
|
|
210
220
|
for (let bOptions of b.options) {
|
|
211
221
|
if (aOptions.id !== bOptions.id) continue
|
|
212
222
|
if (!aOptions.sort || !bOptions.sort) continue
|
|
223
|
+
|
|
224
|
+
let maxFnVariant = max([aOptions.variant, bOptions.variant]) ?? 0n
|
|
225
|
+
|
|
226
|
+
// Create a mask of 0s from bits 1..N where N represents the mask of the Nth bit
|
|
227
|
+
let mask = ~(maxFnVariant | (maxFnVariant - 1n))
|
|
228
|
+
let aVariantsAfterFn = a.variants & mask
|
|
229
|
+
let bVariantsAfterFn = b.variants & mask
|
|
230
|
+
|
|
231
|
+
// If the variants the same, we _can_ sort them
|
|
232
|
+
if (aVariantsAfterFn !== bVariantsAfterFn) {
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
|
|
213
236
|
let result = aOptions.sort(
|
|
214
237
|
{
|
|
215
238
|
value: aOptions.value,
|
|
@@ -243,12 +266,68 @@ export class Offsets {
|
|
|
243
266
|
return a.index - b.index
|
|
244
267
|
}
|
|
245
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Arbitrary variants are recorded in the order they're encountered.
|
|
271
|
+
* This means that the order is not stable between environments and sets of content files.
|
|
272
|
+
*
|
|
273
|
+
* In order to make the order stable, we need to remap the arbitrary variant offsets to
|
|
274
|
+
* be in alphabetical order starting from the offset of the first arbitrary variant.
|
|
275
|
+
*/
|
|
276
|
+
recalculateVariantOffsets() {
|
|
277
|
+
// Sort the variants by their name
|
|
278
|
+
let variants = Array.from(this.variantOffsets.entries())
|
|
279
|
+
.filter(([v]) => v.startsWith('['))
|
|
280
|
+
.sort(([a], [z]) => fastCompare(a, z))
|
|
281
|
+
|
|
282
|
+
// Sort the list of offsets
|
|
283
|
+
// This is not necessarily a discrete range of numbers which is why
|
|
284
|
+
// we're using sort instead of creating a range from min/max
|
|
285
|
+
let newOffsets = variants.map(([, offset]) => offset).sort((a, z) => bigSign(a - z))
|
|
286
|
+
|
|
287
|
+
// Create a map from the old offsets to the new offsets in the new sort order
|
|
288
|
+
/** @type {[bigint, bigint][]} */
|
|
289
|
+
let mapping = variants.map(([, oldOffset], i) => [oldOffset, newOffsets[i]])
|
|
290
|
+
|
|
291
|
+
// Remove any variants that will not move letting us skip
|
|
292
|
+
// remapping if everything happens to be in order
|
|
293
|
+
return mapping.filter(([a, z]) => a !== z)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @template T
|
|
298
|
+
* @param {[RuleOffset, T][]} list
|
|
299
|
+
* @returns {[RuleOffset, T][]}
|
|
300
|
+
*/
|
|
301
|
+
remapArbitraryVariantOffsets(list) {
|
|
302
|
+
let mapping = this.recalculateVariantOffsets()
|
|
303
|
+
|
|
304
|
+
// No arbitrary variants? Nothing to do.
|
|
305
|
+
// Everyhing already in order? Nothing to do.
|
|
306
|
+
if (mapping.length === 0) {
|
|
307
|
+
return list
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Remap every variant offset in the list
|
|
311
|
+
return list.map((item) => {
|
|
312
|
+
let [offset, rule] = item
|
|
313
|
+
|
|
314
|
+
offset = {
|
|
315
|
+
...offset,
|
|
316
|
+
variants: remapBitfield(offset.variants, mapping),
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return [offset, rule]
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
246
323
|
/**
|
|
247
324
|
* @template T
|
|
248
325
|
* @param {[RuleOffset, T][]} list
|
|
249
326
|
* @returns {[RuleOffset, T][]}
|
|
250
327
|
*/
|
|
251
328
|
sort(list) {
|
|
329
|
+
list = this.remapArbitraryVariantOffsets(list)
|
|
330
|
+
|
|
252
331
|
return list.sort(([a], [b]) => bigSign(this.compare(a, b)))
|
|
253
332
|
}
|
|
254
333
|
}
|
|
@@ -268,3 +347,27 @@ function max(nums) {
|
|
|
268
347
|
|
|
269
348
|
return max
|
|
270
349
|
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* A fast ASCII order string comparison function.
|
|
353
|
+
*
|
|
354
|
+
* Using `.sort()` without a custom compare function is faster
|
|
355
|
+
* But you can only use that if you're sorting an array of
|
|
356
|
+
* only strings. If you're sorting strings inside objects
|
|
357
|
+
* or arrays, you need must use a custom compare function.
|
|
358
|
+
*
|
|
359
|
+
* @param {string} a
|
|
360
|
+
* @param {string} b
|
|
361
|
+
*/
|
|
362
|
+
function fastCompare(a, b) {
|
|
363
|
+
let aLen = a.length
|
|
364
|
+
let bLen = b.length
|
|
365
|
+
let minLen = aLen < bLen ? aLen : bLen
|
|
366
|
+
|
|
367
|
+
for (let i = 0; i < minLen; i++) {
|
|
368
|
+
let cmp = a.charCodeAt(i) - b.charCodeAt(i)
|
|
369
|
+
if (cmp !== 0) return cmp
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return aLen - bLen
|
|
373
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* We must remap all the old bits to new bits for each set variant
|
|
5
|
+
* Only arbitrary variants are considered as those are the only
|
|
6
|
+
* ones that need to be re-sorted at this time
|
|
7
|
+
*
|
|
8
|
+
* An iterated process that removes and sets individual bits simultaneously
|
|
9
|
+
* will not work because we may have a new bit that is also a later old bit
|
|
10
|
+
* This means that we would be removing a previously set bit which we don't
|
|
11
|
+
* want to do
|
|
12
|
+
*
|
|
13
|
+
* For example (assume `bN` = `1<<N`)
|
|
14
|
+
* Given the "total" mapping `[[b1, b3], [b2, b4], [b3, b1], [b4, b2]]`
|
|
15
|
+
* The mapping is "total" because:
|
|
16
|
+
* 1. Every input and output is accounted for
|
|
17
|
+
* 2. All combinations are unique
|
|
18
|
+
* 3. No one input maps to multiple outputs and vice versa
|
|
19
|
+
* And, given an offset with all bits set:
|
|
20
|
+
* V = b1 | b2 | b3 | b4
|
|
21
|
+
*
|
|
22
|
+
* Let's explore the issue with removing and setting bits simultaneously:
|
|
23
|
+
* V & ~b1 | b3 = b2 | b3 | b4
|
|
24
|
+
* V & ~b2 | b4 = b3 | b4
|
|
25
|
+
* V & ~b3 | b1 = b1 | b4
|
|
26
|
+
* V & ~b4 | b2 = b1 | b2
|
|
27
|
+
*
|
|
28
|
+
* As you can see, we end up with the wrong result.
|
|
29
|
+
* This is because we're removing a bit that was previously set.
|
|
30
|
+
* And, thus the final result is missing b3 and b4.
|
|
31
|
+
*
|
|
32
|
+
* Now, let's explore the issue with removing the bits first:
|
|
33
|
+
* V & ~b1 = b2 | b3 | b4
|
|
34
|
+
* V & ~b2 = b3 | b4
|
|
35
|
+
* V & ~b3 = b4
|
|
36
|
+
* V & ~b4 = 0
|
|
37
|
+
*
|
|
38
|
+
* And then setting the bits:
|
|
39
|
+
* V | b3 = b3
|
|
40
|
+
* V | b4 = b3 | b4
|
|
41
|
+
* V | b1 = b1 | b3 | b4
|
|
42
|
+
* V | b2 = b1 | b2 | b3 | b4
|
|
43
|
+
*
|
|
44
|
+
* We get the correct result because we're not removing any bits that were
|
|
45
|
+
* previously set thus properly remapping the bits to the new order
|
|
46
|
+
*
|
|
47
|
+
* To collect this into a single operation that can be done simultaneously
|
|
48
|
+
* we must first create a mask for the old bits that are set and a mask for
|
|
49
|
+
* the new bits that are set. Then we can remove the old bits and set the new
|
|
50
|
+
* bits simultaneously in a "single" operation like so:
|
|
51
|
+
* OldMask = b1 | b2 | b3 | b4
|
|
52
|
+
* NewMask = b3 | b4 | b1 | b2
|
|
53
|
+
*
|
|
54
|
+
* So this:
|
|
55
|
+
* V & ~oldMask | newMask
|
|
56
|
+
*
|
|
57
|
+
* Expands to this:
|
|
58
|
+
* V & ~b1 & ~b2 & ~b3 & ~b4 | b3 | b4 | b1 | b2
|
|
59
|
+
*
|
|
60
|
+
* Which becomes this:
|
|
61
|
+
* b1 | b2 | b3 | b4
|
|
62
|
+
*
|
|
63
|
+
* Which is the correct result!
|
|
64
|
+
*
|
|
65
|
+
* @param {bigint} num
|
|
66
|
+
* @param {[bigint, bigint][]} mapping
|
|
67
|
+
*/
|
|
68
|
+
export function remapBitfield(num, mapping) {
|
|
69
|
+
// Create masks for the old and new bits that are set
|
|
70
|
+
let oldMask = 0n
|
|
71
|
+
let newMask = 0n
|
|
72
|
+
for (let [oldBit, newBit] of mapping) {
|
|
73
|
+
if (num & oldBit) {
|
|
74
|
+
oldMask = oldMask | oldBit
|
|
75
|
+
newMask = newMask | newBit
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Remove all old bits
|
|
80
|
+
// Set all new bits
|
|
81
|
+
return (num & ~oldMask) | newMask
|
|
82
|
+
}
|