@thejaredwilcurt/csslop 0.0.24 → 0.0.26

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.
@@ -350,6 +350,88 @@ function combineAdjacentIdenticalStops (args) {
350
350
  return normalizeBoundaryPositionTokens(mergedStops);
351
351
  }
352
352
 
353
+ /**
354
+ * The `to <side>` linear-gradient directions that are shorter to write as an
355
+ * angle, keyed by the normalized keyword form. Corner directions are listed
356
+ * under both keyword orders, since either spells the same corner.
357
+ *
358
+ * @type {Map<string, string>}
359
+ */
360
+ const LINEAR_DIRECTION_ANGLES = new Map([
361
+ ['to top', '0deg'],
362
+ ['to right', '90deg'],
363
+ ['to left', '270deg'],
364
+ ['to top right', '45deg'],
365
+ ['to right top', '45deg'],
366
+ ['to bottom right', '135deg'],
367
+ ['to right bottom', '135deg'],
368
+ ['to bottom left', '225deg'],
369
+ ['to left bottom', '225deg'],
370
+ ['to top left', '315deg'],
371
+ ['to left top', '315deg']
372
+ ]);
373
+
374
+ /**
375
+ * The linear-gradient directions that are already the default, so writing them
376
+ * out adds nothing.
377
+ *
378
+ * @type {Set<string>}
379
+ */
380
+ const DEFAULT_LINEAR_DIRECTIONS = new Set(['to bottom', '180deg']);
381
+
382
+ /**
383
+ * The radial-gradient shapes that are already the default.
384
+ *
385
+ * @type {Set<string>}
386
+ */
387
+ const DEFAULT_RADIAL_SHAPES = new Set(['ellipse at center', 'circle at center']);
388
+
389
+ /**
390
+ * Rewrites the leading direction argument of a linear gradient into its
391
+ * shortest form, dropping it when it is the default.
392
+ *
393
+ * @param {Array} args The gradient arguments, rewritten in place.
394
+ * @return {number} The number of leading arguments that are not color stops.
395
+ */
396
+ function normalizeLinearGradientDirection (args) {
397
+ const firstDirection = args[0].toLowerCase().replace(/\s+/g, ' ').trim();
398
+ if (DEFAULT_LINEAR_DIRECTIONS.has(firstDirection)) {
399
+ args.shift();
400
+ return 0;
401
+ }
402
+ const angle = LINEAR_DIRECTION_ANGLES.get(firstDirection);
403
+ if (angle) {
404
+ args[0] = angle;
405
+ return 1;
406
+ }
407
+ // Check if first arg looks like a direction (angle or "to ..." keyword)
408
+ const looksLikeDirection = /^\d+(\.\d+)?deg$/i.test(firstDirection) || firstDirection.startsWith('to ');
409
+ if (looksLikeDirection) {
410
+ return 1;
411
+ }
412
+ return 0;
413
+ }
414
+
415
+ /**
416
+ * Drops the leading shape argument of a radial gradient when it is the default.
417
+ *
418
+ * @param {Array} args The gradient arguments, rewritten in place.
419
+ * @return {number} The number of leading arguments that are not color stops.
420
+ */
421
+ function normalizeRadialGradientShape (args) {
422
+ const firstShape = args[0].toLowerCase().replace(/\s+/g, ' ').trim();
423
+ if (DEFAULT_RADIAL_SHAPES.has(firstShape)) {
424
+ args.shift();
425
+ return 0;
426
+ }
427
+ // Check if first arg is a radial shape/size descriptor
428
+ const looksLikeShape = /\b(circle|ellipse|closest|farthest|at)\b/i.test(firstShape);
429
+ if (looksLikeShape) {
430
+ return 1;
431
+ }
432
+ return 0;
433
+ }
434
+
353
435
  /**
354
436
  * Optimizes gradient arguments by removing default direction or shape keywords, combining adjacent identical color stops, and trimming redundant 0% or 100% stop positions from the first and last stops.
355
437
  *
@@ -363,52 +445,11 @@ function processGradientArgs (func, argsStr) {
363
445
 
364
446
  let directionArgCount = 0;
365
447
 
366
- if (functionLower.includes('linear')) {
367
- if (args.length > 1) {
368
- const firstDirection = args[0].toLowerCase().replace(/\s+/g, ' ').trim();
369
- if (firstDirection === 'to bottom' || firstDirection === '180deg') {
370
- args.shift();
371
- } else if (firstDirection === 'to top') {
372
- args[0] = '0deg';
373
- directionArgCount = 1;
374
- } else if (firstDirection === 'to right') {
375
- args[0] = '90deg';
376
- directionArgCount = 1;
377
- } else if (firstDirection === 'to left') {
378
- args[0] = '270deg';
379
- directionArgCount = 1;
380
- } else if (firstDirection === 'to top right' || firstDirection === 'to right top') {
381
- args[0] = '45deg';
382
- directionArgCount = 1;
383
- } else if (firstDirection === 'to bottom right' || firstDirection === 'to right bottom') {
384
- args[0] = '135deg';
385
- directionArgCount = 1;
386
- } else if (firstDirection === 'to bottom left' || firstDirection === 'to left bottom') {
387
- args[0] = '225deg';
388
- directionArgCount = 1;
389
- } else if (firstDirection === 'to top left' || firstDirection === 'to left top') {
390
- args[0] = '315deg';
391
- directionArgCount = 1;
392
- } else {
393
- // Check if first arg looks like a direction (angle or "to ..." keyword)
394
- const looksLikeDirection = /^\d+(\.\d+)?deg$/i.test(firstDirection) || firstDirection.startsWith('to ');
395
- if (looksLikeDirection) {
396
- directionArgCount = 1;
397
- }
398
- }
399
- }
400
- } else if (functionLower.includes('radial')) {
401
- if (args.length > 1) {
402
- const firstShape = args[0].toLowerCase().replace(/\s+/g, ' ').trim();
403
- if (firstShape === 'ellipse at center' || firstShape === 'circle at center') {
404
- args.shift();
405
- } else {
406
- // Check if first arg is a radial shape/size descriptor
407
- const looksLikeShape = /\b(circle|ellipse|closest|farthest|at)\b/i.test(firstShape);
408
- if (looksLikeShape) {
409
- directionArgCount = 1;
410
- }
411
- }
448
+ if (args.length > 1) {
449
+ if (functionLower.includes('linear')) {
450
+ directionArgCount = normalizeLinearGradientDirection(args);
451
+ } else if (functionLower.includes('radial')) {
452
+ directionArgCount = normalizeRadialGradientShape(args);
412
453
  }
413
454
  }
414
455
 
package/src/value/math.js CHANGED
@@ -10,6 +10,21 @@ import {
10
10
  roundCompactNumber
11
11
  } from './shared.js';
12
12
 
13
+ /**
14
+ * The units a folded calc() expression leads with, in the order they are
15
+ * written. Every other unit follows them alphabetically.
16
+ *
17
+ * @type {Array}
18
+ */
19
+ const PREFERRED_UNIT_ORDER = ['%', '', 'px'];
20
+
21
+ /**
22
+ * The same leading units, for excluding them from the alphabetical remainder.
23
+ *
24
+ * @type {Set<string>}
25
+ */
26
+ const PREFERRED_UNITS = new Set(PREFERRED_UNIT_ORDER);
27
+
13
28
  /**
14
29
  * Attempts to simplify a calc() expression by combining like-unit terms and evaluating pure arithmetic, returning the simplified string or null if folding is not possible.
15
30
  *
@@ -89,8 +104,8 @@ function tryFoldCalcExpression (expression) {
89
104
  totals.set(unit, (totals.get(unit) || 0) + number);
90
105
  }
91
106
 
92
- const orderedUnits = ['%', '', 'px', ...[...totals.keys()].filter((unit) => {
93
- return !['%', '', 'px'].includes(unit);
107
+ const orderedUnits = [...PREFERRED_UNIT_ORDER, ...[...totals.keys()].filter((unit) => {
108
+ return !PREFERRED_UNITS.has(unit);
94
109
  }).sort()];
95
110
  const outputTerms = [];
96
111
 
@@ -755,6 +755,51 @@ function convertAbsoluteLengthsToPx (value) {
755
755
  });
756
756
  }
757
757
 
758
+ /**
759
+ * Properties whose `initial` value is a number the browser resolves, so the
760
+ * keyword only ever shortens for the two that have a shorter written form.
761
+ *
762
+ * @type {Set<string>}
763
+ */
764
+ const NUMERIC_INITIAL_PROPERTIES = new Set([
765
+ 'opacity',
766
+ 'z-index',
767
+ 'flex-grow',
768
+ 'flex-shrink',
769
+ 'order',
770
+ 'line-height',
771
+ 'zoom'
772
+ ]);
773
+
774
+ /**
775
+ * Properties whose `initial` value is zero.
776
+ *
777
+ * @type {Set<string>}
778
+ */
779
+ const ZERO_INITIAL_PROPERTIES = new Set(['margin', 'padding']);
780
+
781
+ /**
782
+ * Properties whose `initial` value is `auto`.
783
+ *
784
+ * @type {Set<string>}
785
+ */
786
+ const AUTO_INITIAL_PROPERTIES = new Set(['min-width', 'min-height']);
787
+
788
+ /**
789
+ * Properties whose value is a time, where a millisecond amount may be worth
790
+ * rewriting in seconds.
791
+ *
792
+ * @type {Set<string>}
793
+ */
794
+ const TIME_PROPERTIES = new Set([
795
+ 'transition',
796
+ 'transition-duration',
797
+ 'transition-delay',
798
+ 'animation',
799
+ 'animation-duration',
800
+ 'animation-delay'
801
+ ]);
802
+
758
803
  /**
759
804
  * Applies property-specific optimizations to a CSS value (transition, flex, font,
760
805
  * background, display, scale, border-radius, shorthand collapsing, etc.).
@@ -773,15 +818,7 @@ function applyPropertyOptimizations (val, property, allowsHexSpaceElision) {
773
818
  }
774
819
 
775
820
  // Convert ms to s for time-related properties when the seconds form is shorter
776
- const isTimeProperty = (
777
- property === 'transition' ||
778
- property === 'transition-duration' ||
779
- property === 'transition-delay' ||
780
- property === 'animation' ||
781
- property === 'animation-duration' ||
782
- property === 'animation-delay'
783
- );
784
- if (isTimeProperty) {
821
+ if (TIME_PROPERTIES.has(property)) {
785
822
  val = convertMillisecondsToSeconds(val);
786
823
  }
787
824
 
@@ -823,7 +860,7 @@ function applyPropertyOptimizations (val, property, allowsHexSpaceElision) {
823
860
 
824
861
  // Initial values
825
862
  if (val === 'initial') {
826
- if (['opacity', 'z-index', 'flex-grow', 'flex-shrink', 'order', 'line-height', 'zoom'].includes(property)) {
863
+ if (NUMERIC_INITIAL_PROPERTIES.has(property)) {
827
864
  // Just leaving them or mapping some: opacity: initial -> opacity: 1
828
865
  if (property === 'opacity') {
829
866
  val = '1';
@@ -832,10 +869,10 @@ function applyPropertyOptimizations (val, property, allowsHexSpaceElision) {
832
869
  val = 'auto';
833
870
  }
834
871
  }
835
- if (['margin', 'padding'].includes(property)) {
872
+ if (ZERO_INITIAL_PROPERTIES.has(property)) {
836
873
  val = '0';
837
874
  }
838
- if (['min-width', 'min-height'].includes(property)) {
875
+ if (AUTO_INITIAL_PROPERTIES.has(property)) {
839
876
  val = 'auto';
840
877
  }
841
878
  // background-color: initial should become #0000 (transparent)
@@ -1043,7 +1080,7 @@ function applyPropertyOptimizations (val, property, allowsHexSpaceElision) {
1043
1080
  * @param {object} declaration The CSS declaration object with property and value fields.
1044
1081
  * @return {string} The minified value string.
1045
1082
  */
1046
- function minifyValue (declaration) {
1083
+ function computeMinifiedValue (declaration) {
1047
1084
  if (declaration.property === 'position-area') {
1048
1085
  const shorthand = POSITION_AREA_SHORTHANDS[declaration.value];
1049
1086
  if (shorthand) {
@@ -1135,4 +1172,63 @@ function minifyValue (declaration) {
1135
1172
  return val;
1136
1173
  }
1137
1174
 
1138
- export { minifyValue };
1175
+ /**
1176
+ * Memoizes minified values for the current stylesheet. Minification runs many
1177
+ * passes over the same declarations (deduplication, shorthand assembly,
1178
+ * CSS-wide keyword hoisting, stringification), and the result only depends on
1179
+ * the declaration fields that make up the cache key, so each distinct
1180
+ * property/value pair is minified once per stylesheet.
1181
+ *
1182
+ * @type {Map<string, string>}
1183
+ */
1184
+ const minifiedValueCache = new Map();
1185
+
1186
+ /**
1187
+ * Discards every memoized value. The minifier calls this at the start of each
1188
+ * stylesheet, since the active `@charset` can change how a value minifies and
1189
+ * the cache should not outlive the pass that filled it.
1190
+ */
1191
+ function clearMinifiedValueCache () {
1192
+ minifiedValueCache.clear();
1193
+ }
1194
+
1195
+ /**
1196
+ * Builds the cache key for a declaration from every field the value minifier
1197
+ * reads. A null character cannot appear in a property name or in a parsed CSS
1198
+ * value, so it safely delimits the parts.
1199
+ *
1200
+ * @param {object} declaration The CSS declaration object.
1201
+ * @return {string} The cache key.
1202
+ */
1203
+ function createMinifiedValueCacheKey (declaration) {
1204
+ const assembledFlag = declaration.isAssembledShorthand ? '1' : '0';
1205
+ return declaration.property + '\u0000' + assembledFlag + '\u0000' + declaration.value;
1206
+ }
1207
+
1208
+ /**
1209
+ * Minifies a CSS declaration's value, reusing the memoized result when the same
1210
+ * property and value has already been minified during this pass.
1211
+ *
1212
+ * @param {object} declaration The CSS declaration object with property and value fields.
1213
+ * @return {string} The minified value string.
1214
+ */
1215
+ function minifyValue (declaration) {
1216
+ // Only string values have a stable, collision-free key; anything else is rare
1217
+ // enough that minifying it again costs less than encoding its type.
1218
+ if (typeof declaration.value !== 'string') {
1219
+ return computeMinifiedValue(declaration);
1220
+ }
1221
+ const cacheKey = createMinifiedValueCacheKey(declaration);
1222
+ const cachedValue = minifiedValueCache.get(cacheKey);
1223
+ if (cachedValue !== undefined) {
1224
+ return cachedValue;
1225
+ }
1226
+ const minifiedValue = computeMinifiedValue(declaration);
1227
+ minifiedValueCache.set(cacheKey, minifiedValue);
1228
+ return minifiedValue;
1229
+ }
1230
+
1231
+ export {
1232
+ clearMinifiedValueCache,
1233
+ minifyValue
1234
+ };