11// @ts -check
22
33import bigSign from '../util/bigSign'
4+ import { remapBitfield } from './remap-bitfield.js'
45
56/**
67 * @typedef {'base' | 'defaults' | 'components' | 'utilities' | 'variants' | 'user' } Layer
@@ -190,7 +191,7 @@ export class Offsets {
190191
191192 return {
192193 ...this . create ( 'variants' ) ,
193- variants : 1n << this . reservedVariantBits ,
194+ variants : this . variantOffsets . get ( variant ) ,
194195 }
195196 }
196197
@@ -243,12 +244,68 @@ export class Offsets {
243244 return a . index - b . index
244245 }
245246
247+ /**
248+ * Arbitrary variants are recorded in the order they're encountered.
249+ * This means that the order is not stable between environments and sets of content files.
250+ *
251+ * In order to make the order stable, we need to remap the arbitrary variant offsets to
252+ * be in alphabetical order starting from the offset of the first arbitrary variant.
253+ */
254+ recalculateVariantOffsets ( ) {
255+ // Sort the variants by their name
256+ let variants = Array . from ( this . variantOffsets . entries ( ) )
257+ . filter ( ( [ v ] ) => v . startsWith ( '[' ) )
258+ . sort ( ( [ a ] , [ z ] ) => fastCompare ( a , z ) )
259+
260+ // Sort the list of offsets
261+ // This is not necessarily a discrete range of numbers which is why
262+ // we're using sort instead of creating a range from min/max
263+ let newOffsets = variants . map ( ( [ , offset ] ) => offset ) . sort ( ( a , z ) => bigSign ( a - z ) )
264+
265+ // Create a map from the old offsets to the new offsets in the new sort order
266+ /** @type {[bigint, bigint][] } */
267+ let mapping = variants . map ( ( [ , oldOffset ] , i ) => [ oldOffset , newOffsets [ i ] ] )
268+
269+ // Remove any variants that will not move letting us skip
270+ // remapping if everything happens to be in order
271+ return mapping . filter ( ( [ a , z ] ) => a !== z )
272+ }
273+
274+ /**
275+ * @template T
276+ * @param {[RuleOffset, T][] } list
277+ * @returns {[RuleOffset, T][] }
278+ */
279+ remapArbitraryVariantOffsets ( list ) {
280+ let mapping = this . recalculateVariantOffsets ( )
281+
282+ // No arbitrary variants? Nothing to do.
283+ // Everyhing already in order? Nothing to do.
284+ if ( mapping . length === 0 ) {
285+ return list
286+ }
287+
288+ // Remap every variant offset in the list
289+ return list . map ( ( item ) => {
290+ let [ offset , rule ] = item
291+
292+ offset = {
293+ ...offset ,
294+ variants : remapBitfield ( offset . variants , mapping ) ,
295+ }
296+
297+ return [ offset , rule ]
298+ } )
299+ }
300+
246301 /**
247302 * @template T
248303 * @param {[RuleOffset, T][] } list
249304 * @returns {[RuleOffset, T][] }
250305 */
251306 sort ( list ) {
307+ list = this . remapArbitraryVariantOffsets ( list )
308+
252309 return list . sort ( ( [ a ] , [ b ] ) => bigSign ( this . compare ( a , b ) ) )
253310 }
254311}
@@ -268,3 +325,27 @@ function max(nums) {
268325
269326 return max
270327}
328+
329+ /**
330+ * A fast ASCII order string comparison function.
331+ *
332+ * Using `.sort()` without a custom compare function is faster
333+ * But you can only use that if you're sorting an array of
334+ * only strings. If you're sorting strings inside objects
335+ * or arrays, you need must use a custom compare function.
336+ *
337+ * @param {string } a
338+ * @param {string } b
339+ */
340+ function fastCompare ( a , b ) {
341+ let aLen = a . length
342+ let bLen = b . length
343+ let minLen = aLen < bLen ? aLen : bLen
344+
345+ for ( let i = 0 ; i < minLen ; i ++ ) {
346+ let cmp = a . charCodeAt ( i ) - b . charCodeAt ( i )
347+ if ( cmp !== 0 ) return cmp
348+ }
349+
350+ return aLen - bLen
351+ }
0 commit comments