@thejaredwilcurt/csslop 0.0.41 → 0.0.42

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/README.md CHANGED
@@ -31,7 +31,7 @@
31
31
  * Gemini 3.1 Pro (High Thinking)
32
32
  * GPT-5.4 High (Thinking)
33
33
 
34
- These were the latest and greatest models at the time. Since then I've mostly used the latest Claude version for maintenance.
34
+ These were the latest and greatest models at the time.
35
35
 
36
36
  **AI's used during maintenance:**
37
37
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@thejaredwilcurt/csslop",
3
3
  "main": "index.js",
4
4
  "type": "module",
5
- "version": "0.0.41",
5
+ "version": "0.0.42",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/pre/start.js",
@@ -25,22 +25,22 @@
25
25
  "devDependencies": {
26
26
  "@codemirror/autocomplete": "^6.20.3",
27
27
  "@codemirror/lang-css": "^6.3.1",
28
- "@codemirror/view": "^6.43.13",
28
+ "@codemirror/view": "^6.43.14",
29
29
  "@eslint/js": "^10.0.1",
30
30
  "@node-projects/css-parser": "^5.2.0",
31
31
  "@stylistic/eslint-plugin": "^5.10.0",
32
32
  "codemirror": "^6.0.2",
33
- "eslint": "^10.11.0",
33
+ "eslint": "^10.13.0",
34
34
  "eslint-config-tjw-base": "^5.0.0",
35
35
  "eslint-config-tjw-import-x": "^1.0.1",
36
36
  "eslint-config-tjw-jsdoc": "^2.0.1",
37
37
  "eslint-plugin-import-x": "^4.17.0",
38
- "eslint-plugin-jsdoc": "^64.5.4",
38
+ "eslint-plugin-jsdoc": "^65.3.8",
39
39
  "fflate": "^0.8.3",
40
- "globals": "^17.12.0",
41
- "pretty-ms": "^9.3.1",
40
+ "globals": "^17.13.0",
41
+ "pretty-ms": "^10.0.0",
42
42
  "real-world-css-libraries": "^1.0.8",
43
- "vite": "^8.3.1"
43
+ "vite": "^8.3.4"
44
44
  },
45
45
  "author": "The Jared Wilcurt",
46
46
  "homepage": "https://github.com/TheJaredWilcurt/csslop#readme",
@@ -57,11 +57,11 @@
57
57
  "devEngines": {
58
58
  "runtime": {
59
59
  "name": "node",
60
- "version": "26.10.0"
60
+ "version": "26.12.0"
61
61
  },
62
62
  "packageManager": {
63
63
  "name": "npm",
64
- "version": "12.1.0"
64
+ "version": "12.2.0"
65
65
  }
66
66
  }
67
67
  }
@@ -404,14 +404,8 @@ function stringifyRule (rule, context) {
404
404
  if (canUnwrapSupports(supports)) {
405
405
  return children;
406
406
  }
407
- // Check if @supports condition has adjacent logical operators that allow tight spacing
408
- const needsTightSpacing = supports.startsWith('(') && /\)(?:and|or)\s*\(/.test(supports);
409
- let supportsSeparator;
410
- if (needsTightSpacing) {
411
- supportsSeparator = '';
412
- } else {
413
- supportsSeparator = ' ';
414
- }
407
+ // A condition beginning with "(" can follow the at-rule name without whitespace.
408
+ const supportsSeparator = supports.startsWith('(') ? '' : ' ';
415
409
  return '@supports' + supportsSeparator + supports + '{' + children + '}';
416
410
  }
417
411
 
@@ -451,6 +451,67 @@ function parseOklchHue (token) {
451
451
  return parseAngleToDegrees(normalized);
452
452
  }
453
453
 
454
+ /**
455
+ * Removes whitespace before a negative numeric channel in Lab-family colors.
456
+ * A preceding numeric token already separates the sign from the prior channel.
457
+ *
458
+ * @param {string} functionName The name of the color function.
459
+ * @param {string} value The minified function arguments.
460
+ * @return {string} The arguments with redundant channel spacing removed.
461
+ */
462
+ function compactNegativeColorChannelSpacing (functionName, value) {
463
+ const normalizedFunctionName = functionName.toLowerCase();
464
+ if (!['lab', 'lch', 'oklab', 'oklch'].includes(normalizedFunctionName)) {
465
+ return value;
466
+ }
467
+
468
+ let result = '';
469
+ let nestingDepth = 0;
470
+ for (let index = 0; index < value.length; index++) {
471
+ const character = value[index];
472
+ if (character === ' ' && nestingDepth === 0) {
473
+ const previousCharacter = result[result.length - 1];
474
+ const nextCharacter = value[index + 1];
475
+ const characterAfterSign = value[index + 2];
476
+ const characterAfterDecimal = value[index + 3];
477
+ const previousEndsNumber = (
478
+ isCssDigit(previousCharacter) ||
479
+ previousCharacter === '%'
480
+ );
481
+ const nextStartsNegativeNumber = (
482
+ nextCharacter === '-' &&
483
+ (
484
+ isCssDigit(characterAfterSign) ||
485
+ (
486
+ characterAfterSign === '.' &&
487
+ isCssDigit(characterAfterDecimal)
488
+ )
489
+ )
490
+ );
491
+ if (previousEndsNumber && nextStartsNegativeNumber) {
492
+ continue;
493
+ }
494
+ }
495
+ result += character;
496
+ if (character === '(') {
497
+ nestingDepth++;
498
+ } else if (character === ')') {
499
+ nestingDepth--;
500
+ }
501
+ }
502
+ return result;
503
+ }
504
+
505
+ /**
506
+ * Checks whether a character is an ASCII decimal digit.
507
+ *
508
+ * @param {string} character The character to inspect.
509
+ * @return {boolean} Whether the character is a digit.
510
+ */
511
+ function isCssDigit (character) {
512
+ return character >= '0' && character <= '9';
513
+ }
514
+
454
515
  /**
455
516
  * Converts `oklch()` colors that fall inside the sRGB gamut to their hex
456
517
  * equivalent when that is shorter. Out-of-gamut colors have no sRGB
@@ -574,11 +635,13 @@ function convertColorsToHex (val) {
574
635
  }
575
636
  return integer;
576
637
  });
638
+ minified = compactNegativeColorChannelSpacing(func, minified);
577
639
  return func + '(' + minified.trim() + ')';
578
640
  });
579
641
 
580
642
  // Convert in-gamut oklab() to hex when it produces a shorter representation
581
- val = val.replace(/\boklab\(\s*(-?(?:\d+|\d*\.\d+))\s+(-?(?:\d+|\d*\.\d+))\s+(-?(?:\d+|\d*\.\d+))(?:\s*\/\s*(-?(?:\d+|\d*\.\d+)%?))?\s*\)/gi, (match, lStr, aStr, bStr, alphaStr) => {
643
+ // A negative channel may touch the preceding number because its sign separates the CSS number tokens.
644
+ val = val.replace(/\boklab\(\s*(-?(?:\d+|\d*\.\d+))(?:\s+|(?=-))(-?(?:\d+|\d*\.\d+))(?:\s+|(?=-))(-?(?:\d+|\d*\.\d+))(?:\s*\/\s*(-?(?:\d+|\d*\.\d+)%?))?\s*\)/gi, (match, lStr, aStr, bStr, alphaStr) => {
582
645
  const alpha = parseAlphaString(alphaStr);
583
646
  const hex = convertOklabToHex(parseFloat(lStr), parseFloat(aStr), parseFloat(bStr), alpha);
584
647
  if (!hex) {
@@ -892,14 +955,13 @@ const POSITION_COMPONENT_SOURCE = '(' + POSITION_OFFSET_SOURCE + '(?:\\s+' + POS
892
955
  const URL_TOKEN_BODY_SOURCE = 'url\\([^()]*';
893
956
 
894
957
  /**
895
- * Matches a close-paren that ends an image function, directly followed by a
896
- * position that no slash follows, which is the position that needs its
897
- * separator put back. The parenthesis that ends a `url()` is skipped, since
898
- * that one closes a token rather than a function.
958
+ * Matches a close-paren that ends an image function, followed by a separated
959
+ * position that no slash follows. The parenthesis that ends a `url()` is
960
+ * skipped, since that one closes a token rather than a function.
899
961
  *
900
962
  * @type {RegExp}
901
963
  */
902
- const UNSEPARATED_FUNCTION_POSITION_PATTERN = new RegExp('(?<!' + URL_TOKEN_BODY_SOURCE + ')\\)' + POSITION_COMPONENT_SOURCE + '(?!\\/)', 'gi');
964
+ const SEPARATED_FUNCTION_POSITION_PATTERN = new RegExp('(?<!' + URL_TOKEN_BODY_SOURCE + ')\\)\\s+' + POSITION_COMPONENT_SOURCE + '(?!\\/)', 'gi');
903
965
 
904
966
  /**
905
967
  * Matches a `url()` separated from the position that follows it, which is the
@@ -920,18 +982,16 @@ const SEPARATED_IMAGE_SIZE_POSITION_PATTERN = new RegExp('\\)\\s+' + POSITION_CO
920
982
  /**
921
983
  * Normalizes the separator between the image of a layer and the position that
922
984
  * follows it. Both `background` and `mask` take a `<position> [ / <size> ]`
923
- * component after their image, and a bare position only reads as its own
924
- * component while whitespace separates it from an image function. Neither a
925
- * position that a `/` follows nor one that follows a `url()` needs the
926
- * separator, since the slash delimits the pair and a url is consumed whole as a
927
- * single token that ends at its own closing parenthesis.
985
+ * component after their image. The closing parenthesis already delimits a
986
+ * function token from the position, and neither a position that a `/` follows
987
+ * nor one that follows a `url()` needs a separator either.
928
988
  *
929
989
  * @param {string} value The layered image value, with the parenthesis padding removed.
930
990
  * @return {string} The value with the position separator normalized.
931
991
  */
932
992
  function normalizeImagePositionSeparator (value) {
933
- const separatedFromFunction = value.replace(UNSEPARATED_FUNCTION_POSITION_PATTERN, ') $1');
934
- const joinedToUrl = separatedFromFunction.replace(SEPARATED_URL_POSITION_PATTERN, '$1$2');
993
+ const joinedToFunction = value.replace(SEPARATED_FUNCTION_POSITION_PATTERN, ')$1');
994
+ const joinedToUrl = joinedToFunction.replace(SEPARATED_URL_POSITION_PATTERN, '$1$2');
935
995
  return joinedToUrl.replace(SEPARATED_IMAGE_SIZE_POSITION_PATTERN, ')$1');
936
996
  }
937
997