@thejaredwilcurt/csslop 0.0.41 → 0.0.43
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 +1 -1
- package/package.json +9 -9
- package/src/rules/stringify.js +2 -8
- package/src/value/math.js +87 -18
- package/src/value/minify.js +73 -13
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.
|
|
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.
|
|
5
|
+
"version": "0.0.43",
|
|
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.
|
|
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.
|
|
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": "^
|
|
38
|
+
"eslint-plugin-jsdoc": "^65.3.8",
|
|
39
39
|
"fflate": "^0.8.3",
|
|
40
|
-
"globals": "^17.
|
|
41
|
-
"pretty-ms": "^
|
|
40
|
+
"globals": "^17.13.0",
|
|
41
|
+
"pretty-ms": "^10.0.0",
|
|
42
42
|
"real-world-css-libraries": "^1.0.8",
|
|
43
|
-
"vite": "^8.3.
|
|
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.
|
|
60
|
+
"version": "26.12.0"
|
|
61
61
|
},
|
|
62
62
|
"packageManager": {
|
|
63
63
|
"name": "npm",
|
|
64
|
-
"version": "12.
|
|
64
|
+
"version": "12.2.0"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
package/src/rules/stringify.js
CHANGED
|
@@ -404,14 +404,8 @@ function stringifyRule (rule, context) {
|
|
|
404
404
|
if (canUnwrapSupports(supports)) {
|
|
405
405
|
return children;
|
|
406
406
|
}
|
|
407
|
-
//
|
|
408
|
-
const
|
|
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
|
|
package/src/value/math.js
CHANGED
|
@@ -144,9 +144,58 @@ function rewriteMathFunctionContents (rewriteExpression, value) {
|
|
|
144
144
|
return result + value.slice(index);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Matches a number written with a decimal point that begins a token of its
|
|
149
|
+
* own, capturing its sign, the digits before its decimal point, and the digits
|
|
150
|
+
* after it. The lookbehind keeps the match from starting partway through an
|
|
151
|
+
* identifier (`--x0.5`) or another number, and the lookahead keeps it from
|
|
152
|
+
* matching a fragment of a dotted name such as `0.5.png`.
|
|
153
|
+
*
|
|
154
|
+
* @type {RegExp}
|
|
155
|
+
*/
|
|
156
|
+
const DECIMAL_NUMBER = /(?<![\w.-])([+-]?)(\d*)\.(\d+)(?![\d.])/g;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Writes a decimal number in the fewest characters that state the same value,
|
|
160
|
+
* dropping the zeros before its whole part (`0.25` → `.25`) and after its
|
|
161
|
+
* fraction (`1.50` → `1.5`), along with a decimal point left with no fraction
|
|
162
|
+
* behind it (`2.0` → `2`). The digits that remain are kept as written, so no
|
|
163
|
+
* precision is lost.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} sign The sign written before the number, empty when there is none.
|
|
166
|
+
* @param {string} wholeDigits The digits written before the decimal point.
|
|
167
|
+
* @param {string} fractionDigits The digits written after the decimal point.
|
|
168
|
+
* @return {string} The number written as compactly as it can be.
|
|
169
|
+
*/
|
|
170
|
+
function shortenDecimalNumber (sign, wholeDigits, fractionDigits) {
|
|
171
|
+
// Leading zeros of the whole part
|
|
172
|
+
const wholePart = wholeDigits.replace(/^0+/, '');
|
|
173
|
+
// Trailing zeros of the fraction
|
|
174
|
+
const fractionPart = fractionDigits.replace(/0+$/, '');
|
|
175
|
+
if (!fractionPart) {
|
|
176
|
+
return sign + (wholePart || '0');
|
|
177
|
+
}
|
|
178
|
+
return sign + wholePart + '.' + fractionPart;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Shortens every decimal number in the contents of a math function. Numbers
|
|
183
|
+
* that follow an operator written without whitespace (`x*0.25s`) are out of
|
|
184
|
+
* reach of the general number shortening, which only looks past a separator.
|
|
185
|
+
*
|
|
186
|
+
* @param {string} expression The contents of a math function.
|
|
187
|
+
* @return {string} The expression with every decimal number written as compactly as it can be.
|
|
188
|
+
*/
|
|
189
|
+
function shortenDecimalNumbers (expression) {
|
|
190
|
+
return expression.replace(DECIMAL_NUMBER, (match, sign, wholeDigits, fractionDigits) => {
|
|
191
|
+
return shortenDecimalNumber(sign, wholeDigits, fractionDigits);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
147
195
|
/**
|
|
148
196
|
* Matches a multiplication by a decimal below one, capturing the digits after
|
|
149
|
-
* its decimal point and the unit written on it
|
|
197
|
+
* its decimal point and the unit written on it (so a multiplier with a unit
|
|
198
|
+
* can be recognized and left alone). The trailing lookahead keeps
|
|
150
199
|
* the match away from numbers that only begin this way, such as the exponent
|
|
151
200
|
* of `.5e3` or a unit name that is still being read.
|
|
152
201
|
*
|
|
@@ -205,28 +254,47 @@ function reciprocalOfDecimalFraction (fractionDigits) {
|
|
|
205
254
|
}
|
|
206
255
|
|
|
207
256
|
/**
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
257
|
+
* Builds the division that scales a value exactly as multiplying it by a
|
|
258
|
+
* unitless decimal below one does, when that division is no longer to write
|
|
259
|
+
* than the multiplication. A multiplier carrying a unit is never rewritten:
|
|
260
|
+
* dividing by a dimension inverts its unit in the type of the result
|
|
261
|
+
* (`x*.25s` is a time, while `x/4s` is per unit of time), so the two would not
|
|
262
|
+
* be equivalent.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} fractionDigits The digits written after the decimal point of the multiplier.
|
|
265
|
+
* @param {string} unit The unit written on the multiplier, empty when it is unitless.
|
|
266
|
+
* @return {string|null} The equivalent division, or null when there is none worth using.
|
|
267
|
+
*/
|
|
268
|
+
function divisionEquivalentToFractionalMultiplier (fractionDigits, unit) {
|
|
269
|
+
if (unit) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const divisor = reciprocalOfDecimalFraction(fractionDigits);
|
|
273
|
+
if (divisor === null) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const division = '/' + divisor;
|
|
277
|
+
// Compared against the shortest the multiplication could have been written
|
|
278
|
+
const multiplication = '*.' + fractionDigits;
|
|
279
|
+
if (division.length > multiplication.length) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return division;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Rewrites a multiplication by a unitless decimal below one as a division by
|
|
287
|
+
* the whole number that decimal is the reciprocal of, which is never longer to
|
|
288
|
+
* write and usually shorter (`calc(var(--x)*.25)` → `calc(var(--x)/4)`).
|
|
289
|
+
* Multipliers with a unit are left as written, since dividing by a dimension
|
|
290
|
+
* would invert its unit.
|
|
213
291
|
*
|
|
214
292
|
* @param {string} expression The contents of a math function.
|
|
215
293
|
* @return {string} The expression with reciprocal multipliers stated as divisions.
|
|
216
294
|
*/
|
|
217
295
|
function convertReciprocalMultiplicationToDivision (expression) {
|
|
218
296
|
return expression.replace(FRACTIONAL_MULTIPLIER, (match, fractionDigits, unit) => {
|
|
219
|
-
|
|
220
|
-
if (divisor === null) {
|
|
221
|
-
return match;
|
|
222
|
-
}
|
|
223
|
-
const division = '/' + divisor + unit;
|
|
224
|
-
// Compared against the shortest the multiplication could have been written
|
|
225
|
-
const multiplication = '*.' + fractionDigits + unit;
|
|
226
|
-
if (division.length > multiplication.length) {
|
|
227
|
-
return match;
|
|
228
|
-
}
|
|
229
|
-
return division;
|
|
297
|
+
return divisionEquivalentToFractionalMultiplier(fractionDigits, unit) ?? match;
|
|
230
298
|
});
|
|
231
299
|
}
|
|
232
300
|
|
|
@@ -596,7 +664,8 @@ const RESOLVABLE_MATH_FUNCTION = new RegExp(
|
|
|
596
664
|
* @return {string} The value with its math functions simplified where possible.
|
|
597
665
|
*/
|
|
598
666
|
function simplifyMathFunctionsOnce (value, property) {
|
|
599
|
-
let result = rewriteMathFunctionContents(
|
|
667
|
+
let result = rewriteMathFunctionContents(shortenDecimalNumbers, value);
|
|
668
|
+
result = rewriteMathFunctionContents(convertReciprocalMultiplicationToDivision, result);
|
|
600
669
|
|
|
601
670
|
// Unwrap calc(1 / (1 / x)) → x (double-reciprocal identity)
|
|
602
671
|
result = result.replace(/calc\(\s*1\s*\/\s*\(\s*1\s*\/\s*([^()]+)\s*\)\s*\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
|
package/src/value/minify.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
896
|
-
* position that no slash follows
|
|
897
|
-
*
|
|
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
|
|
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
|
|
924
|
-
*
|
|
925
|
-
*
|
|
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
|
|
934
|
-
const joinedToUrl =
|
|
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
|
|