@thejaredwilcurt/csslop 0.0.39 → 0.0.41
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 +10 -13
- package/src/rules/custom-properties.js +86 -9
- package/src/rules/optimize.js +88 -1
- package/src/value/math-expression.js +1103 -0
- package/src/value/math.js +342 -116
- package/src/value/minify.js +5 -3
- package/src/value/shared.js +52 -10
- package/vite.config.js +0 -4
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ These were the latest and greatest models at the time. Since then I've mostly us
|
|
|
35
35
|
|
|
36
36
|
**AI's used during maintenance:**
|
|
37
37
|
|
|
38
|
-
As new tests are created upstream I use whatever the latest models are
|
|
38
|
+
As new tests are created upstream I use whatever the latest models are and document them in the release notes.
|
|
39
39
|
|
|
40
40
|
These tools were prompted to pass the tests in the `/copiedTests` folder that came from `keithamus/css-minify-tests`.
|
|
41
41
|
|
package/package.json
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
"name": "@thejaredwilcurt/csslop",
|
|
3
3
|
"main": "index.js",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.41",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"prestart": "node ./scripts/
|
|
8
|
+
"prestart": "node ./scripts/pre/start.js",
|
|
9
9
|
"start": "vite",
|
|
10
10
|
"preview": "vite preview",
|
|
11
|
-
"prebuild": "node ./scripts/
|
|
11
|
+
"prebuild": "node ./scripts/pre/build.js",
|
|
12
12
|
"build": "vite build",
|
|
13
|
-
"postbuild": "node ./scripts/
|
|
13
|
+
"postbuild": "node ./scripts/post/build.js",
|
|
14
14
|
"copy": "node ./scripts/copyTests.js",
|
|
15
15
|
"test": "node ./tests/index.test.js",
|
|
16
16
|
"real": "node ./tests/realworld.test.js",
|
|
@@ -22,28 +22,25 @@
|
|
|
22
22
|
"outdated": "npm outdated",
|
|
23
23
|
"proto": "proto pin node latest --resolve --tool-native && proto pin npm latest --resolve --tool-native && git status"
|
|
24
24
|
},
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"@csstools/css-calc": "^3.4.0"
|
|
27
|
-
},
|
|
28
25
|
"devDependencies": {
|
|
29
26
|
"@codemirror/autocomplete": "^6.20.3",
|
|
30
27
|
"@codemirror/lang-css": "^6.3.1",
|
|
31
|
-
"@codemirror/view": "^6.43.
|
|
28
|
+
"@codemirror/view": "^6.43.13",
|
|
32
29
|
"@eslint/js": "^10.0.1",
|
|
33
30
|
"@node-projects/css-parser": "^5.2.0",
|
|
34
31
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
35
32
|
"codemirror": "^6.0.2",
|
|
36
|
-
"eslint": "^10.
|
|
33
|
+
"eslint": "^10.11.0",
|
|
37
34
|
"eslint-config-tjw-base": "^5.0.0",
|
|
38
35
|
"eslint-config-tjw-import-x": "^1.0.1",
|
|
39
36
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
40
37
|
"eslint-plugin-import-x": "^4.17.0",
|
|
41
|
-
"eslint-plugin-jsdoc": "^64.5.
|
|
38
|
+
"eslint-plugin-jsdoc": "^64.5.4",
|
|
42
39
|
"fflate": "^0.8.3",
|
|
43
40
|
"globals": "^17.12.0",
|
|
44
41
|
"pretty-ms": "^9.3.1",
|
|
45
42
|
"real-world-css-libraries": "^1.0.8",
|
|
46
|
-
"vite": "^8.3.
|
|
43
|
+
"vite": "^8.3.1"
|
|
47
44
|
},
|
|
48
45
|
"author": "The Jared Wilcurt",
|
|
49
46
|
"homepage": "https://github.com/TheJaredWilcurt/csslop#readme",
|
|
@@ -60,11 +57,11 @@
|
|
|
60
57
|
"devEngines": {
|
|
61
58
|
"runtime": {
|
|
62
59
|
"name": "node",
|
|
63
|
-
"version": "26.
|
|
60
|
+
"version": "26.10.0"
|
|
64
61
|
},
|
|
65
62
|
"packageManager": {
|
|
66
63
|
"name": "npm",
|
|
67
|
-
"version": "12.0
|
|
64
|
+
"version": "12.1.0"
|
|
68
65
|
}
|
|
69
66
|
}
|
|
70
67
|
}
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* @file Custom property value whitespace and comment processing for CSS minification.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { tokenizeCssValue } from '../parser/tokenizer.js';
|
|
6
|
+
import { formatCompactNumber } from '../value/shared.js';
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Removes spaces after commas only inside parenthesized groups (function
|
|
7
10
|
* calls like `var()`, `calc()`), leaving top-level comma spacing intact.
|
|
@@ -34,15 +37,89 @@ function removeSpacesAfterCommasInsideFunctions (value) {
|
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
40
|
+
* The token types whose text begins with a number, and so may hold digits that
|
|
41
|
+
* are written out longer than they need to be.
|
|
42
|
+
*
|
|
43
|
+
* @type {Set<string>}
|
|
44
|
+
*/
|
|
45
|
+
const NUMERIC_TOKEN_TYPES = new Set(['number', 'dimension', 'percentage']);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Splits a numeric token into the decimal number it states and the unit
|
|
49
|
+
* written on it. A number carrying a scientific exponent does not match, since
|
|
50
|
+
* its digits cannot be restated without reading the exponent along with them,
|
|
51
|
+
* and neither does a whole number, which has no zeros to shed.
|
|
52
|
+
*
|
|
53
|
+
* @type {RegExp}
|
|
54
|
+
*/
|
|
55
|
+
const DECIMAL_NUMBER_TOKEN = /^([+-]?(?:\d+\.\d+|\.\d+))([a-zA-Z%]*)$/;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Restates a numeric token as the shortest way of writing the same number,
|
|
59
|
+
* dropping the zeros that lead the decimal point and the ones that trail the
|
|
60
|
+
* digits behind it (`-0.020em` becomes `-.02em`). A token that is not a plain
|
|
61
|
+
* decimal, or whose number is only expressible in scientific notation, is left
|
|
62
|
+
* exactly as it was written.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} tokenText The text of a number, dimension, or percentage token.
|
|
65
|
+
* @return {string} The shortest way of writing that same token.
|
|
66
|
+
*/
|
|
67
|
+
function shortenNumericToken (tokenText) {
|
|
68
|
+
const decimalNumber = tokenText.match(DECIMAL_NUMBER_TOKEN);
|
|
69
|
+
if (!decimalNumber) {
|
|
70
|
+
return tokenText;
|
|
71
|
+
}
|
|
72
|
+
const [, number, unit] = decimalNumber;
|
|
73
|
+
const shortened = formatCompactNumber(number);
|
|
74
|
+
// A number too small or too large to write out plainly comes back in
|
|
75
|
+
// scientific notation, which reads as a different token once a unit follows
|
|
76
|
+
if (shortened.includes('e')) {
|
|
77
|
+
return tokenText;
|
|
78
|
+
}
|
|
79
|
+
return shortened + unit;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reports whether two token lists say the same thing, which is what tells a
|
|
84
|
+
* rewritten value apart from one whose tokens have run together. Shortening
|
|
85
|
+
* the numbers of `1.0.0` would leave `10`, a single number where two stood.
|
|
86
|
+
*
|
|
87
|
+
* @param {Array} tokens The tokens the value was read as.
|
|
88
|
+
* @param {Array} rewrittenTokens The tokens the rewritten value reads as.
|
|
89
|
+
* @return {boolean} Whether the rewrite left every token boundary in place.
|
|
90
|
+
*/
|
|
91
|
+
function hasSameTokenBoundaries (tokens, rewrittenTokens) {
|
|
92
|
+
if (tokens.length !== rewrittenTokens.length) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return tokens.every((token, index) => {
|
|
96
|
+
return token.type === rewrittenTokens[index].type;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Writes every number in a custom property value the shortest way it can be
|
|
102
|
+
* written. A custom property holds an arbitrary token sequence, so the value
|
|
103
|
+
* is walked token by token: only the tokens that state a number are rewritten,
|
|
104
|
+
* leaving strings, urls, and hash tokens exactly as the author wrote them.
|
|
39
105
|
*
|
|
40
106
|
* @param {string} value The custom property value string.
|
|
41
|
-
* @return {string} The value with
|
|
107
|
+
* @return {string} The value with its numbers shortened.
|
|
42
108
|
*/
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
109
|
+
function shortenNumbers (value) {
|
|
110
|
+
const tokens = tokenizeCssValue(value);
|
|
111
|
+
const rewritten = tokens
|
|
112
|
+
.map((token) => {
|
|
113
|
+
if (!NUMERIC_TOKEN_TYPES.has(token.type)) {
|
|
114
|
+
return token.text;
|
|
115
|
+
}
|
|
116
|
+
return shortenNumericToken(token.text);
|
|
117
|
+
})
|
|
118
|
+
.join('');
|
|
119
|
+
if (!hasSameTokenBoundaries(tokens, tokenizeCssValue(rewritten))) {
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
return rewritten;
|
|
46
123
|
}
|
|
47
124
|
|
|
48
125
|
/**
|
|
@@ -69,7 +146,7 @@ function processCustomPropertyComments (value) {
|
|
|
69
146
|
* Collapses whitespace in a custom property value while preserving
|
|
70
147
|
* token boundaries. Each whitespace sequence is reduced to a single
|
|
71
148
|
* space, spaces after commas inside function calls are removed, and
|
|
72
|
-
*
|
|
149
|
+
* every number is written the shortest way it can be.
|
|
73
150
|
*
|
|
74
151
|
* @param {string} value The raw custom property value string.
|
|
75
152
|
* @return {string} The minified custom property value.
|
|
@@ -79,8 +156,8 @@ function collapseCustomPropertyWhitespace (value) {
|
|
|
79
156
|
let collapsed = value.replace(/\s+/g, ' ');
|
|
80
157
|
// Remove spaces after commas only inside function calls (e.g. var(--bar, 1.5) → var(--bar,1.5))
|
|
81
158
|
collapsed = removeSpacesAfterCommasInsideFunctions(collapsed);
|
|
82
|
-
//
|
|
83
|
-
collapsed =
|
|
159
|
+
// Shorten decimals (e.g. 0.5 → .5, -0.020em → -.02em)
|
|
160
|
+
collapsed = shortenNumbers(collapsed);
|
|
84
161
|
return collapsed;
|
|
85
162
|
}
|
|
86
163
|
export {
|
package/src/rules/optimize.js
CHANGED
|
@@ -1351,6 +1351,74 @@ const MERGE_AT_LATER_RULE = 'later';
|
|
|
1351
1351
|
*/
|
|
1352
1352
|
const MERGE_AT_EARLIER_RULE = 'earlier';
|
|
1353
1353
|
|
|
1354
|
+
/**
|
|
1355
|
+
* Reports whether an entry of a rule's body is a declaration whose standing in
|
|
1356
|
+
* the cascade can be read off the property it writes. Anything else a body may
|
|
1357
|
+
* hold, such as a nested rule, is never shadowed by a declaration beside it.
|
|
1358
|
+
*
|
|
1359
|
+
* @param {object} entry An entry of a rule's declaration list.
|
|
1360
|
+
* @return {boolean} True when the entry is a property declaration.
|
|
1361
|
+
*/
|
|
1362
|
+
function isPropertyDeclaration (entry) {
|
|
1363
|
+
return entry.type === 'declaration' && Boolean(entry.property);
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
/**
|
|
1367
|
+
* Finds which declaration decides each leaf property once a list of
|
|
1368
|
+
* declarations is read as one rule's body. Within a rule, an important
|
|
1369
|
+
* declaration beats a non-important one, and between two of equal weight the
|
|
1370
|
+
* last one written wins.
|
|
1371
|
+
*
|
|
1372
|
+
* @param {Array} declarations The declarations of the combined rule, in the order they would be written.
|
|
1373
|
+
* @return {Map} Map of leaf property name to the declaration that decides it.
|
|
1374
|
+
*/
|
|
1375
|
+
function findDecidingDeclarations (declarations) {
|
|
1376
|
+
const decidingDeclarations = new Map();
|
|
1377
|
+
const importantDeclarations = new Set();
|
|
1378
|
+
for (const declaration of declarations) {
|
|
1379
|
+
if (!isPropertyDeclaration(declaration)) {
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
const isImportant = isImportantDeclaration(declaration);
|
|
1383
|
+
if (isImportant) {
|
|
1384
|
+
importantDeclarations.add(declaration);
|
|
1385
|
+
}
|
|
1386
|
+
for (const leafProperty of expandToOverridableProperties(declaration.property)) {
|
|
1387
|
+
const decidingDeclaration = decidingDeclarations.get(leafProperty);
|
|
1388
|
+
const outranksIt = (
|
|
1389
|
+
!decidingDeclaration ||
|
|
1390
|
+
isImportant ||
|
|
1391
|
+
!importantDeclarations.has(decidingDeclaration)
|
|
1392
|
+
);
|
|
1393
|
+
if (outranksIt) {
|
|
1394
|
+
decidingDeclarations.set(leafProperty, declaration);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return decidingDeclarations;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
/**
|
|
1402
|
+
* Reports whether anything a rule brings to a merge still decides a property
|
|
1403
|
+
* of the combined rule. A declaration that loses every property it writes to
|
|
1404
|
+
* another declaration of the same rule is dropped from the merged rule, so
|
|
1405
|
+
* where the merged rule sits cannot change how it reads.
|
|
1406
|
+
*
|
|
1407
|
+
* @param {object} rule The rule contributing the entries.
|
|
1408
|
+
* @param {Map} decidingDeclarations The deciding declaration of each leaf property of the merged rule.
|
|
1409
|
+
* @return {boolean} True when the rule still decides something after the merge.
|
|
1410
|
+
*/
|
|
1411
|
+
function decidesAnythingAfterMerge (rule, decidingDeclarations) {
|
|
1412
|
+
return (rule.declarations || []).some((entry) => {
|
|
1413
|
+
if (!isPropertyDeclaration(entry)) {
|
|
1414
|
+
return entry.type !== 'whitespace' && entry.type !== 'comment';
|
|
1415
|
+
}
|
|
1416
|
+
return [...expandToOverridableProperties(entry.property)].some((leafProperty) => {
|
|
1417
|
+
return decidingDeclarations.get(leafProperty) === entry;
|
|
1418
|
+
});
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1354
1422
|
/**
|
|
1355
1423
|
* Decides where two rules with the same selector can be combined. Merging them
|
|
1356
1424
|
* always makes one set of declarations cross whatever separates the two rules,
|
|
@@ -1358,6 +1426,12 @@ const MERGE_AT_EARLIER_RULE = 'earlier';
|
|
|
1358
1426
|
* the merged rule goes wherever the set that moved keeps its old standing.
|
|
1359
1427
|
* Nothing separates adjacent rules, so those always merge.
|
|
1360
1428
|
*
|
|
1429
|
+
* The declarations one rule loses to the other are the exception: they are
|
|
1430
|
+
* dropped from the merged rule, so the merged rule belongs wherever the
|
|
1431
|
+
* declarations that outlive the merge already were, and nothing has to cross
|
|
1432
|
+
* anything at all. That is what lets `.a{color:red!important}` swallow a later
|
|
1433
|
+
* `.a{color:tan}` across a rule that also writes `color`.
|
|
1434
|
+
*
|
|
1361
1435
|
* @param {object} overrideIndex The index of what each position writes to.
|
|
1362
1436
|
* @param {object} earlierRule The first of the two rules with this selector.
|
|
1363
1437
|
* @param {object} laterRule The second of the two rules with this selector.
|
|
@@ -1367,7 +1441,20 @@ const MERGE_AT_EARLIER_RULE = 'earlier';
|
|
|
1367
1441
|
*/
|
|
1368
1442
|
function chooseMergePlacement (overrideIndex, earlierRule, laterRule, earlierPosition, laterPosition) {
|
|
1369
1443
|
const rulesAreAdjacent = earlierPosition === laterPosition - 1;
|
|
1370
|
-
if (rulesAreAdjacent
|
|
1444
|
+
if (rulesAreAdjacent) {
|
|
1445
|
+
return MERGE_AT_LATER_RULE;
|
|
1446
|
+
}
|
|
1447
|
+
const decidingDeclarations = findDecidingDeclarations([
|
|
1448
|
+
...(earlierRule.declarations || []),
|
|
1449
|
+
...(laterRule.declarations || [])
|
|
1450
|
+
]);
|
|
1451
|
+
if (!decidesAnythingAfterMerge(laterRule, decidingDeclarations)) {
|
|
1452
|
+
return MERGE_AT_EARLIER_RULE;
|
|
1453
|
+
}
|
|
1454
|
+
if (!decidesAnythingAfterMerge(earlierRule, decidingDeclarations)) {
|
|
1455
|
+
return MERGE_AT_LATER_RULE;
|
|
1456
|
+
}
|
|
1457
|
+
if (!overrideIndex.blocksRelocation(earlierRule, earlierPosition)) {
|
|
1371
1458
|
return MERGE_AT_LATER_RULE;
|
|
1372
1459
|
}
|
|
1373
1460
|
if (!overrideIndex.blocksRelocation(laterRule, earlierPosition)) {
|