tailwindcss 3.2.4 → 3.2.6
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 +51 -1
- package/README.md +1 -1
- package/lib/cli/build/index.js +5 -1
- package/lib/cli/build/plugin.js +39 -34
- 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 +34 -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 +121 -80
- 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 +22 -22
- package/lib/util/pluginUtils.js +38 -40
- package/lib/util/prefixSelector.js +22 -8
- package/lib/util/resolveConfig.js +8 -10
- package/package.json +30 -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 +14 -9
- 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 +35 -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 +97 -55
- 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/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
|
@@ -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'
|
|
@@ -124,15 +125,32 @@ export default function expandTailwindAtRules(context) {
|
|
|
124
125
|
// ---
|
|
125
126
|
|
|
126
127
|
// Find potential rules in changed files
|
|
127
|
-
let candidates = new Set([sharedState.NOT_ON_DEMAND])
|
|
128
|
+
let candidates = new Set([...(context.candidates ?? []), sharedState.NOT_ON_DEMAND])
|
|
128
129
|
let seen = new Set()
|
|
129
130
|
|
|
130
131
|
env.DEBUG && console.time('Reading changed files')
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let
|
|
135
|
-
|
|
133
|
+
if (env.OXIDE) {
|
|
134
|
+
// TODO: Pass through or implement `extractor`
|
|
135
|
+
for (let candidate of require('@tailwindcss/oxide').parseCandidateStringsFromFiles(
|
|
136
|
+
context.changedContent
|
|
137
|
+
// Object.assign({}, builtInTransformers, context.tailwindConfig.content.transform)
|
|
138
|
+
)) {
|
|
139
|
+
candidates.add(candidate)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// for (let { file, content, extension } of context.changedContent) {
|
|
143
|
+
// let transformer = getTransformer(context.tailwindConfig, extension)
|
|
144
|
+
// let extractor = getExtractor(context, extension)
|
|
145
|
+
// getClassCandidatesOxide(file, transformer(content), extractor, candidates, seen)
|
|
146
|
+
// }
|
|
147
|
+
} else {
|
|
148
|
+
for (let { file, content, extension } of context.changedContent) {
|
|
149
|
+
let transformer = getTransformer(context.tailwindConfig, extension)
|
|
150
|
+
let extractor = getExtractor(context, extension)
|
|
151
|
+
content = file ? fs.readFileSync(file, 'utf8') : content
|
|
152
|
+
getClassCandidates(transformer(content), extractor, candidates, seen)
|
|
153
|
+
}
|
|
136
154
|
}
|
|
137
155
|
|
|
138
156
|
env.DEBUG && console.timeEnd('Reading changed files')
|
|
@@ -143,7 +161,18 @@ export default function expandTailwindAtRules(context) {
|
|
|
143
161
|
let classCacheCount = context.classCache.size
|
|
144
162
|
|
|
145
163
|
env.DEBUG && console.time('Generate rules')
|
|
146
|
-
|
|
164
|
+
env.DEBUG && console.time('Sorting candidates')
|
|
165
|
+
let sortedCandidates = env.OXIDE
|
|
166
|
+
? candidates
|
|
167
|
+
: new Set(
|
|
168
|
+
[...candidates].sort((a, z) => {
|
|
169
|
+
if (a === z) return 0
|
|
170
|
+
if (a < z) return -1
|
|
171
|
+
return 1
|
|
172
|
+
})
|
|
173
|
+
)
|
|
174
|
+
env.DEBUG && console.timeEnd('Sorting candidates')
|
|
175
|
+
generateRules(sortedCandidates, context)
|
|
147
176
|
env.DEBUG && console.timeEnd('Generate rules')
|
|
148
177
|
|
|
149
178
|
// 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
|
+
}
|