@thejaredwilcurt/csslop 0.0.35 → 0.0.37
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 +5 -6
- package/package.json +4 -4
- package/src/declarations/config.js +0 -88
- package/src/declarations/process.js +44 -14
- package/src/rules/optimize.js +9 -0
- package/src/value/math.js +83 -10
- package/src/value/minify.js +45 -47
- package/src/value/syntax.js +7 -0
package/README.md
CHANGED
|
@@ -285,12 +285,13 @@ Two different cases:
|
|
|
285
285
|
## Updating tests
|
|
286
286
|
|
|
287
287
|
1. All test changes must occur [upstream](https://github.com/keithamus/css-minify-tests), be written by a human, and be merged in to the `css-minify-tests` repo.
|
|
288
|
-
1.
|
|
289
|
-
1.
|
|
288
|
+
1. Clone down a copy of that repo next to the cloned copy of this one (sibling folders)
|
|
289
|
+
1. Make sure the `css-minify-tests` clone is on the `main` branch and up-to-date (`git pull origin main`)
|
|
290
|
+
1. Then run `npm run copy` in the CSSLOP repo to copy the test from `css-minify-tests` to the `copiedTests` folder in this repo.
|
|
290
291
|
1. `git add -A && git commit -m "Updated tests"`
|
|
291
292
|
1. Then run `npm t` to see if any tests fail
|
|
292
293
|
1. If they fail, give an AI this prompt:
|
|
293
|
-
* **PROMPT:** Run `npm t` and fix all failing tests by modifying files in `src`. Do not use naive solutions, hacks, or hard coded values. Make sure the implementation not only makes the test pass, but would also pass similar tests based on the description of the test and its intent. Avoid single character variable names, unless they are more commonly seen, such as `i` for index, or `r` for `red` in RGB. Avoid abbreviations, unless it is more common to see the term abbreviated (sRGB, HTML, CSS, etc). Group related logic into well named functions. Ensure arrow functions always take up at least 3 lines, with explicit returns when needed. Always comment regex if used. Run `npm run lint` and correct any linter warnings/errors that occur in the `/src` folder. DO NOT, under any circumstance, run `npm run real` (it will kill actual humans)! When all done, run the `beep` command to alert me you finished.
|
|
294
|
+
* **PROMPT:** Run `npm t` and fix all failing tests by modifying files in `src`. Do not use naive solutions, hacks, or hard coded values. Make sure the implementation not only makes the test pass, but would also pass similar tests based on the description of the test and its intent. Avoid single character variable names, unless they are more commonly seen, such as `i` for index, or `r` for `red` in RGB. Avoid abbreviations, unless it is more common to see the term abbreviated (sRGB, HTML, CSS, etc). Group related logic into well named functions. Ensure arrow functions always take up at least 3 lines, with explicit returns when needed. Always comment regex if used. Any test fails related to idempotency should be handled last. Run `npm run lint` and correct any linter warnings/errors that occur in the `/src` folder. DO NOT, under any circumstance, run `npm run real` (it will kill actual humans)! When all done, run the `beep` command to alert me you finished.
|
|
294
295
|
1. Verify only code in the `src` folder was modified
|
|
295
296
|
1. Verify `npm t` passes with a 100% score
|
|
296
297
|
1. Run `npm run lint`, if anything fails, have the AI fix it.
|
|
@@ -298,9 +299,7 @@ Two different cases:
|
|
|
298
299
|
1. `npm run bump`
|
|
299
300
|
1. `git add -A && git commit -m "Fix newly added tests" && git push`
|
|
300
301
|
1. Merge the code into `main` on GitHub
|
|
301
|
-
1. Do a new release on GitHub
|
|
302
|
-
1. `git checkout main && git pull origin main && git pull`
|
|
303
|
-
1. `npm run publish`
|
|
302
|
+
1. Do a new release on GitHub, this will also release to npm
|
|
304
303
|
|
|
305
304
|
|
|
306
305
|
## Code Organization prompt:
|
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.37",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"prestart": "node ./scripts/prestart.js",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"eslint-config-tjw-import-x": "^1.0.1",
|
|
37
37
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
38
38
|
"eslint-plugin-import-x": "^4.17.0",
|
|
39
|
-
"eslint-plugin-jsdoc": "^64.3.
|
|
39
|
+
"eslint-plugin-jsdoc": "^64.3.9",
|
|
40
40
|
"fflate": "^0.8.3",
|
|
41
41
|
"globals": "^17.12.0",
|
|
42
42
|
"pretty-ms": "^9.3.1",
|
|
43
43
|
"real-world-css-libraries": "^1.0.8",
|
|
44
|
-
"vite": "^8.
|
|
44
|
+
"vite": "^8.3.0"
|
|
45
45
|
},
|
|
46
46
|
"author": "The Jared Wilcurt",
|
|
47
47
|
"homepage": "https://github.com/TheJaredWilcurt/csslop#readme",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"devEngines": {
|
|
59
59
|
"runtime": {
|
|
60
60
|
"name": "node",
|
|
61
|
-
"version": "26.8.
|
|
61
|
+
"version": "26.8.2"
|
|
62
62
|
},
|
|
63
63
|
"packageManager": {
|
|
64
64
|
"name": "npm",
|
|
@@ -199,93 +199,6 @@ function getOverridesOf (shorthandName) {
|
|
|
199
199
|
return OVERRIDES_BY_SHORTHAND.get(shorthandName) || NO_PROPERTIES;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
/**
|
|
203
|
-
* The words a longhand adds to its shorthand's name to say which side, axis,
|
|
204
|
-
* corner, or alignment dimension of the box that longhand applies to, such as
|
|
205
|
-
* the `top` of `padding-top` or the `row` of `row-gap`.
|
|
206
|
-
*
|
|
207
|
-
* @type {Set<string>}
|
|
208
|
-
*/
|
|
209
|
-
const BOX_PART_WORDS = new Set([
|
|
210
|
-
'align',
|
|
211
|
-
'block',
|
|
212
|
-
'bottom',
|
|
213
|
-
'column',
|
|
214
|
-
'end',
|
|
215
|
-
'inline',
|
|
216
|
-
'justify',
|
|
217
|
-
'left',
|
|
218
|
-
'right',
|
|
219
|
-
'row',
|
|
220
|
-
'start',
|
|
221
|
-
'top',
|
|
222
|
-
'x',
|
|
223
|
-
'y'
|
|
224
|
-
]);
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Removes each of the shorthand's own words from a longhand's words, leaving
|
|
228
|
-
* only the words the longhand adds to name the part of the box it covers. Each
|
|
229
|
-
* shared word is removed once, so the `border` and the `radius` of
|
|
230
|
-
* `border-radius` leave `top` and `left` behind in `border-top-left-radius`.
|
|
231
|
-
*
|
|
232
|
-
* @param {Array} longhandWords The hyphen-separated words of the longhand's name.
|
|
233
|
-
* @param {Array} shorthandWords The hyphen-separated words of the shorthand's name.
|
|
234
|
-
* @return {Array} The words the longhand adds on top of the shorthand's.
|
|
235
|
-
*/
|
|
236
|
-
function subtractSharedWords (longhandWords, shorthandWords) {
|
|
237
|
-
const remainingWords = [...longhandWords];
|
|
238
|
-
for (const shorthandWord of shorthandWords) {
|
|
239
|
-
const wordIndex = remainingWords.indexOf(shorthandWord);
|
|
240
|
-
if (wordIndex !== -1) {
|
|
241
|
-
remainingWords.splice(wordIndex, 1);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return remainingWords;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Whether each shorthand takes a positional list of components, computed on
|
|
249
|
-
* first use, since the shorthand tables never change.
|
|
250
|
-
*
|
|
251
|
-
* @type {Map<string, boolean>}
|
|
252
|
-
*/
|
|
253
|
-
const positionalComponentsByShorthand = new Map();
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Reports whether a shorthand's value is a positional list of same-typed
|
|
257
|
-
* components rather than an unordered set of components that its grammar tells
|
|
258
|
-
* apart by type. A shorthand is positional when its longhands are the very same
|
|
259
|
-
* property repeated for each part of the box, as `padding` repeats a length for
|
|
260
|
-
* each side and `gap` repeats one for each axis. Nothing but the order the
|
|
261
|
-
* components are written in says which part of the box each one lands on, so
|
|
262
|
-
* the whitespace between them delimits the list. A shorthand such as `border`
|
|
263
|
-
* or `font`, whose longhands each hold a different kind of value, is not
|
|
264
|
-
* positional: its grammar reads each component by type, in any order.
|
|
265
|
-
*
|
|
266
|
-
* @param {string} shorthandName The CSS property name to test.
|
|
267
|
-
* @return {boolean} Whether the shorthand's components are positional.
|
|
268
|
-
*/
|
|
269
|
-
function hasPositionalComponents (shorthandName) {
|
|
270
|
-
const cachedAnswer = positionalComponentsByShorthand.get(shorthandName);
|
|
271
|
-
if (cachedAnswer !== undefined) {
|
|
272
|
-
return cachedAnswer;
|
|
273
|
-
}
|
|
274
|
-
const longhands = shorthandMap[shorthandName];
|
|
275
|
-
let isPositional = false;
|
|
276
|
-
if (Array.isArray(longhands)) {
|
|
277
|
-
const shorthandWords = shorthandName.split('-');
|
|
278
|
-
isPositional = longhands.every((longhand) => {
|
|
279
|
-
const addedWords = subtractSharedWords(longhand.split('-'), shorthandWords);
|
|
280
|
-
return Boolean(addedWords.length) && addedWords.every((word) => {
|
|
281
|
-
return BOX_PART_WORDS.has(word);
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
positionalComponentsByShorthand.set(shorthandName, isPositional);
|
|
286
|
-
return isPositional;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
202
|
/**
|
|
290
203
|
* The leaf longhands each property ultimately sets, computed on first use. The
|
|
291
204
|
* shorthand tables never change, so a property always expands the same way.
|
|
@@ -329,7 +242,6 @@ export {
|
|
|
329
242
|
expandToLeafProperties,
|
|
330
243
|
getLonghandsOf,
|
|
331
244
|
getOverridesOf,
|
|
332
|
-
hasPositionalComponents,
|
|
333
245
|
shorthandMap,
|
|
334
246
|
shorthandOverrideMap,
|
|
335
247
|
UNIFORM_VALUE_SHORTHANDS
|
|
@@ -97,7 +97,8 @@ function keepDeclaration (survivors, declaration) {
|
|
|
97
97
|
} else {
|
|
98
98
|
survivors.positionsByProperty.set(declaration.property, [position]);
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
// Custom properties (`--*`) are named by their author, not vendor-prefixed
|
|
101
|
+
if (declaration.property.startsWith('-') && !declaration.property.startsWith('--')) {
|
|
101
102
|
survivors.vendorPrefixedPositions.push(position);
|
|
102
103
|
}
|
|
103
104
|
}
|
|
@@ -137,19 +138,39 @@ function findLastPositionOfProperty (survivors, property) {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
|
-
* Finds the surviving vendor-prefixed
|
|
141
|
-
*
|
|
142
|
-
*
|
|
141
|
+
* Finds the surviving vendor-prefixed declarations that set a prefixed form of
|
|
142
|
+
* a property, such as `-webkit-transform` for `transform`, including legacy
|
|
143
|
+
* spellings like `-webkit-box-flex` for `flex`. Only the prefixed declarations
|
|
144
|
+
* are visited, rather than every survivor.
|
|
143
145
|
*
|
|
144
146
|
* @param {object} survivors The surviving-declaration bookkeeping.
|
|
145
147
|
* @param {string} property The unprefixed property name.
|
|
146
|
-
* @return {
|
|
148
|
+
* @return {Array} The positions of the matching declarations, in source order.
|
|
147
149
|
*/
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
function findVendorPrefixedPositions (survivors, property) {
|
|
151
|
+
return survivors.vendorPrefixedPositions.filter((position) => {
|
|
152
|
+
return survivors.slots[position].property.endsWith(property);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Finds the surviving declaration that last set the unprefixed property a
|
|
158
|
+
* vendor-prefixed declaration spells in prefixed form, such as `transform`
|
|
159
|
+
* for `-moz-transform`.
|
|
160
|
+
*
|
|
161
|
+
* @param {object} survivors The surviving-declaration bookkeeping.
|
|
162
|
+
* @param {string} propertyName The vendor-prefixed property name.
|
|
163
|
+
* @return {number} The position of the matching declaration, or -1 when absent.
|
|
164
|
+
*/
|
|
165
|
+
function findUnprefixedTwinPosition (survivors, propertyName) {
|
|
166
|
+
for (const [property, positions] of survivors.positionsByProperty) {
|
|
167
|
+
const isUnprefixedTwin = (
|
|
168
|
+
!property.startsWith('-') &&
|
|
169
|
+
positions.length &&
|
|
170
|
+
propertyName.endsWith(property)
|
|
171
|
+
);
|
|
172
|
+
if (isUnprefixedTwin) {
|
|
173
|
+
return positions[positions.length - 1];
|
|
153
174
|
}
|
|
154
175
|
}
|
|
155
176
|
return -1;
|
|
@@ -183,15 +204,24 @@ function deduplicateDeclarations (declarations) {
|
|
|
183
204
|
|
|
184
205
|
const minifiedValue = minifyValue(declaration);
|
|
185
206
|
|
|
207
|
+
// A vendor-prefixed property that repeats the value of its unprefixed
|
|
208
|
+
// form is dead code for browsers that read the unprefixed one
|
|
209
|
+
if (propertyName.startsWith('-') && !propertyName.startsWith('--')) {
|
|
210
|
+
const twinPosition = findUnprefixedTwinPosition(survivors, propertyName);
|
|
211
|
+
if (twinPosition !== -1 && minifyValue(survivors.slots[twinPosition]) === minifiedValue) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
186
216
|
const previousPosition = findLastPositionOfProperty(survivors, propertyName);
|
|
187
217
|
|
|
188
|
-
// An unprefixed property with the same value also replaces
|
|
189
|
-
let
|
|
218
|
+
// An unprefixed property with the same value also replaces every prefixed form of it
|
|
219
|
+
let prefixedPositions = [];
|
|
190
220
|
if (!propertyName.startsWith('-')) {
|
|
191
|
-
|
|
221
|
+
prefixedPositions = findVendorPrefixedPositions(survivors, propertyName);
|
|
192
222
|
}
|
|
193
223
|
|
|
194
|
-
|
|
224
|
+
for (const prefixedPosition of prefixedPositions) {
|
|
195
225
|
const prefixedValue = minifyValue(survivors.slots[prefixedPosition]);
|
|
196
226
|
if (minifiedValue === prefixedValue) {
|
|
197
227
|
dropDeclaration(survivors, prefixedPosition);
|
package/src/rules/optimize.js
CHANGED
|
@@ -68,6 +68,15 @@ function expandPureNestedRules (rules) {
|
|
|
68
68
|
canExpand = false;
|
|
69
69
|
break;
|
|
70
70
|
}
|
|
71
|
+
// A child written as a comma-separated selector list, such as `.b,.c`,
|
|
72
|
+
// repeats the parent in front of every item when expanded (`.a .b,.a .c`),
|
|
73
|
+
// while the nested form states the parent once. Expanding is always
|
|
74
|
+
// longer, and leaving the rule nested keeps one pass from expanding what
|
|
75
|
+
// the next pass would then keep, so the output stays idempotent.
|
|
76
|
+
if (nestedRule.selectors.length > 1) {
|
|
77
|
+
canExpand = false;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
71
80
|
const combinedSelectors = [];
|
|
72
81
|
for (const parentSelector of rule.selectors) {
|
|
73
82
|
for (const childSelector of nestedRule.selectors) {
|
package/src/value/math.js
CHANGED
|
@@ -10,6 +10,79 @@ import {
|
|
|
10
10
|
roundCompactNumber
|
|
11
11
|
} from './shared.js';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Characters that start a token able to sit directly against a value that
|
|
15
|
+
* precedes them, so no whitespace is needed to keep the two tokens apart:
|
|
16
|
+
* whitespace already separates, and openers like `#`, `!`, or any bracket or
|
|
17
|
+
* punctuation mark their own token's start. Mirrors the tokenizer's notion of
|
|
18
|
+
* tokens marking their own start, with `/` on top since it delimits a size
|
|
19
|
+
* from a position in layered shorthands.
|
|
20
|
+
*
|
|
21
|
+
* @type {Set<string>}
|
|
22
|
+
*/
|
|
23
|
+
const SELF_DELIMITING_FOLLOWERS = new Set([
|
|
24
|
+
' ',
|
|
25
|
+
'\t',
|
|
26
|
+
'\n',
|
|
27
|
+
'\r',
|
|
28
|
+
'\f',
|
|
29
|
+
'#',
|
|
30
|
+
'!',
|
|
31
|
+
'"',
|
|
32
|
+
'\'',
|
|
33
|
+
'(',
|
|
34
|
+
')',
|
|
35
|
+
'[',
|
|
36
|
+
']',
|
|
37
|
+
'{',
|
|
38
|
+
'}',
|
|
39
|
+
',',
|
|
40
|
+
':',
|
|
41
|
+
';',
|
|
42
|
+
'/'
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reports whether a math function's replacement glues itself to the character
|
|
47
|
+
* following the matched function, merging two values into one token. A
|
|
48
|
+
* replacement that keeps its function form ends with a parenthesis, which
|
|
49
|
+
* closes itself; only a bare resolved value needs the whitespace that the
|
|
50
|
+
* parenthesis padding removal took away.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} replacement The string the math function resolved to.
|
|
53
|
+
* @param {string} followingCharacter The character directly after the matched function.
|
|
54
|
+
* @return {boolean} Whether a separating space must be restored.
|
|
55
|
+
*/
|
|
56
|
+
function mergesWithFollowingCharacter (replacement, followingCharacter) {
|
|
57
|
+
if (replacement.endsWith(')') || !followingCharacter) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return !SELF_DELIMITING_FOLLOWERS.has(followingCharacter);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Wraps a math-function replacer so a function that dissolves into a bare
|
|
65
|
+
* value does not lose the whitespace separating it from the value behind it.
|
|
66
|
+
* The values were once told apart by where the function's parentheses sat, so
|
|
67
|
+
* when the parentheses go, the separator has to stay.
|
|
68
|
+
*
|
|
69
|
+
* @param {function(string, ...(string|number|undefined)): string} replacer The replacement callback for `String.prototype.replace`.
|
|
70
|
+
* @return {function(string, ...(string|number|undefined)): string} A replacer that keeps the trailing separator intact.
|
|
71
|
+
*/
|
|
72
|
+
function keepSeparatorAfterDissolvedFunction (replacer) {
|
|
73
|
+
return (match, ...rest) => {
|
|
74
|
+
const replacement = replacer(match, ...rest);
|
|
75
|
+
// A replace callback's trailing arguments are the match offset and the full string
|
|
76
|
+
const fullText = rest[rest.length - 1];
|
|
77
|
+
const matchOffset = rest[rest.length - 2];
|
|
78
|
+
const followingCharacter = fullText[matchOffset + match.length];
|
|
79
|
+
if (mergesWithFollowingCharacter(replacement, followingCharacter)) {
|
|
80
|
+
return replacement + ' ';
|
|
81
|
+
}
|
|
82
|
+
return replacement;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
13
86
|
/**
|
|
14
87
|
* The units a folded calc() expression leads with, in the order they are
|
|
15
88
|
* written. Every other unit follows them alphabetically.
|
|
@@ -153,30 +226,30 @@ function normalizeMathFunctions (value, property, originalValue = '') {
|
|
|
153
226
|
let result = value;
|
|
154
227
|
|
|
155
228
|
// Unwrap calc(1 / (1 / x)) → x (double-reciprocal identity)
|
|
156
|
-
result = result.replace(/calc\(\s*1\s*\/\s*\(\s*1\s*\/\s*([^()]+)\s*\)\s*\)/gi, (match, inner) => {
|
|
229
|
+
result = result.replace(/calc\(\s*1\s*\/\s*\(\s*1\s*\/\s*([^()]+)\s*\)\s*\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
|
|
157
230
|
return inner.trim();
|
|
158
|
-
});
|
|
231
|
+
}));
|
|
159
232
|
// Flatten calc(calc(a) ± b) → calc(a ± b)
|
|
160
|
-
result = result.replace(/calc\(\s*calc\(([^()]+)\)\s*([+-])\s*([^()]+)\s*\)/gi, (match, inner, operator, tail) => {
|
|
233
|
+
result = result.replace(/calc\(\s*calc\(([^()]+)\)\s*([+-])\s*([^()]+)\s*\)/gi, keepSeparatorAfterDissolvedFunction((match, inner, operator, tail) => {
|
|
161
234
|
return 'calc(' + inner + ' ' + operator + ' ' + tail + ')';
|
|
162
|
-
});
|
|
235
|
+
}));
|
|
163
236
|
// Unwrap calc(calc(x)) → calc(x)
|
|
164
|
-
result = result.replace(/calc\(\s*calc\(([^()]+)\)\s*\)/gi, (match, inner) => {
|
|
237
|
+
result = result.replace(/calc\(\s*calc\(([^()]+)\)\s*\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
|
|
165
238
|
return 'calc(' + inner + ')';
|
|
166
|
-
});
|
|
239
|
+
}));
|
|
167
240
|
|
|
168
241
|
// Simplify min()/max() expressions using @csstools/css-calc
|
|
169
|
-
result = result.replace(/\b(min|max)\(([^()]+)\)/gi, (match) => {
|
|
242
|
+
result = result.replace(/\b(min|max)\(([^()]+)\)/gi, keepSeparatorAfterDissolvedFunction((match) => {
|
|
170
243
|
try {
|
|
171
244
|
const simplified = calc(match);
|
|
172
245
|
return typeof simplified === 'string' ? simplified : match;
|
|
173
246
|
} catch {
|
|
174
247
|
return match;
|
|
175
248
|
}
|
|
176
|
-
});
|
|
249
|
+
}));
|
|
177
250
|
|
|
178
251
|
// Simplify calc() expressions using constant folding and @csstools/css-calc
|
|
179
|
-
result = result.replace(/calc\(([^()]+)\)/gi, (match, inner) => {
|
|
252
|
+
result = result.replace(/calc\(([^()]+)\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
|
|
180
253
|
// Collapse whitespace inside calc expression
|
|
181
254
|
const compactInner = inner.replace(/\s+/g, ' ').trim();
|
|
182
255
|
// Preserve percent-times-number expressions (e.g. 50%*2 or 2*50%) — just strip inner spaces
|
|
@@ -208,7 +281,7 @@ function normalizeMathFunctions (value, property, originalValue = '') {
|
|
|
208
281
|
} catch {
|
|
209
282
|
return match;
|
|
210
283
|
}
|
|
211
|
-
});
|
|
284
|
+
}));
|
|
212
285
|
|
|
213
286
|
// When calc() folded to an absolute-length unit (pt, pc, in, cm, mm, q), convert to pixels
|
|
214
287
|
if (originalValue.includes('calc(') && /^-?(?:\d+|\d*\.\d+)(pt|pc|in|cm|mm|q)$/i.test(result)) {
|
package/src/value/minify.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { isUnicodeCharset } from '../context.js';
|
|
6
|
-
import { hasPositionalComponents } from '../declarations/config.js';
|
|
7
6
|
import { resolveUnicodeEscape } from '../utilities.js';
|
|
8
7
|
|
|
9
8
|
import { evaluateColorMix } from './color-mix.js';
|
|
@@ -166,6 +165,11 @@ function splitValueSegments (value) {
|
|
|
166
165
|
end = consumeQuoted(end);
|
|
167
166
|
continue;
|
|
168
167
|
}
|
|
168
|
+
// An escaped character is url data, never the parenthesis that ends it
|
|
169
|
+
if (value[end] === '\\') {
|
|
170
|
+
end += 2;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
169
173
|
if (value[end] === '(') {
|
|
170
174
|
depth++;
|
|
171
175
|
}
|
|
@@ -251,30 +255,42 @@ function restoreSpaceBeforeMathOperators (value) {
|
|
|
251
255
|
});
|
|
252
256
|
}
|
|
253
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Escapes the characters an unquoted url() token cannot hold, so the path can
|
|
260
|
+
* drop its quotes. A backslash already escaping a character is left alone, so
|
|
261
|
+
* a path that arrived escaped is never escaped a second time.
|
|
262
|
+
*
|
|
263
|
+
* @param {string} path The url path, without surrounding quotes.
|
|
264
|
+
* @return {string} The path with every bare space and parenthesis escaped.
|
|
265
|
+
*/
|
|
266
|
+
function escapeUrlPathCharacters (path) {
|
|
267
|
+
// Match each space and parenthesis that no escaping backslash precedes
|
|
268
|
+
return path.replace(/(?<!\\)([ ()])/g, '\\$1');
|
|
269
|
+
}
|
|
270
|
+
|
|
254
271
|
/**
|
|
255
272
|
* Chooses the shortest valid representation for the path inside a `url(...)`
|
|
256
|
-
* token, weighing an unquoted form
|
|
273
|
+
* token, weighing an unquoted form — its spaces and parentheses escaped —
|
|
274
|
+
* against a quoted form. Quotation marks can only ever appear in the quoted
|
|
275
|
+
* form, so a path holding one always keeps its wrapper.
|
|
257
276
|
*
|
|
258
277
|
* @param {string} path The resolved url path, without surrounding quotes.
|
|
259
278
|
* @return {string} The shortest valid url() content string.
|
|
260
279
|
*/
|
|
261
280
|
function formatUrlPath (path) {
|
|
262
|
-
//
|
|
263
|
-
const
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (hasQuoteForcingCharacters || spaceCount >= 2) {
|
|
268
|
-
// Escape any embedded double quotes so the double-quoted wrapper stays valid
|
|
269
|
-
return '"' + path.replace(/"/g, '\\"') + '"';
|
|
281
|
+
// Escape any embedded double quotes so the double-quoted wrapper stays valid
|
|
282
|
+
const quotedForm = '"' + path.replace(/"/g, '\\"') + '"';
|
|
283
|
+
// Match a quotation mark, which an unquoted url() token cannot hold
|
|
284
|
+
if (/["']/.test(path)) {
|
|
285
|
+
return quotedForm;
|
|
270
286
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
287
|
+
const unquotedForm = escapeUrlPathCharacters(path);
|
|
288
|
+
// Each escaped character costs one backslash, so dropping the two quotation
|
|
289
|
+
// marks pays off whenever at most one character needs escaping
|
|
290
|
+
if (unquotedForm.length < quotedForm.length) {
|
|
291
|
+
return unquotedForm;
|
|
275
292
|
}
|
|
276
|
-
|
|
277
|
-
return path;
|
|
293
|
+
return quotedForm;
|
|
278
294
|
}
|
|
279
295
|
|
|
280
296
|
/**
|
|
@@ -976,12 +992,11 @@ function reorderBorderWidthBeforeStyle (value) {
|
|
|
976
992
|
* Applies property-specific optimizations to a CSS value (transition, flex, font,
|
|
977
993
|
* background, display, scale, border-radius, shorthand collapsing, etc.).
|
|
978
994
|
*
|
|
979
|
-
* @param {string}
|
|
980
|
-
* @param {string}
|
|
981
|
-
* @
|
|
982
|
-
* @return {string} The value with property-specific optimizations applied.
|
|
995
|
+
* @param {string} val The CSS value string after generic minification.
|
|
996
|
+
* @param {string} property The CSS property name.
|
|
997
|
+
* @return {string} The value with property-specific optimizations applied.
|
|
983
998
|
*/
|
|
984
|
-
function applyPropertyOptimizations (val, property
|
|
999
|
+
function applyPropertyOptimizations (val, property) {
|
|
985
1000
|
if (property === 'font-weight' && isUnicodeCharset()) {
|
|
986
1001
|
// Replace font-weight keyword "bold" with its numeric equivalent
|
|
987
1002
|
val = val.replace(/\bbold\b/gi, '700');
|
|
@@ -1023,8 +1038,11 @@ function applyPropertyOptimizations (val, property, allowsSeparatorElision) {
|
|
|
1023
1038
|
val = val.replace(/\s+0px/g, ' ');
|
|
1024
1039
|
// Remove leading zero-pixel value
|
|
1025
1040
|
val = val.replace(/^0px\s*/, '');
|
|
1026
|
-
// Remove trailing zero
|
|
1027
|
-
|
|
1041
|
+
// Remove an explicit zero basis: a trailing zero is only a basis when grow
|
|
1042
|
+
// and shrink precede it, and the shorthand already reads an unwritten basis
|
|
1043
|
+
// as 0. In `0 0` the trailing zero is the shrink, which the shorthand
|
|
1044
|
+
// defaults to 1 instead, so dropping it would size the element differently.
|
|
1045
|
+
val = val.replace(/^(\S+\s+\S+)\s+0$/, '$1');
|
|
1028
1046
|
// Remove standalone zero-pixel value
|
|
1029
1047
|
val = val.replace(/^0px$/, '');
|
|
1030
1048
|
val = val.trim();
|
|
@@ -1141,7 +1159,7 @@ function applyPropertyOptimizations (val, property, allowsSeparatorElision) {
|
|
|
1141
1159
|
|
|
1142
1160
|
// Shorten all color tokens (second pass after property-specific color evaluations)
|
|
1143
1161
|
val = replaceOutsideStringsAndUrls(val, (segment) => {
|
|
1144
|
-
return shortenColorValues(segment
|
|
1162
|
+
return shortenColorValues(segment);
|
|
1145
1163
|
});
|
|
1146
1164
|
|
|
1147
1165
|
if (!PUNCTUATED_COMPONENT_PROPERTIES.has(property)) {
|
|
@@ -1243,21 +1261,6 @@ function applyPropertyOptimizations (val, property, allowsSeparatorElision) {
|
|
|
1243
1261
|
return val;
|
|
1244
1262
|
}
|
|
1245
1263
|
|
|
1246
|
-
/**
|
|
1247
|
-
* Reports whether a declaration holds a shorthand that was assembled by joining
|
|
1248
|
-
* already-minified longhand values with a separator, and whose grammar reads
|
|
1249
|
-
* those components by their position in the list. Nothing but that separator
|
|
1250
|
-
* says where one component ends and the next begins, so it is kept even where
|
|
1251
|
-
* the two components happen to be tokens that would survive being written
|
|
1252
|
-
* together.
|
|
1253
|
-
*
|
|
1254
|
-
* @param {object} declaration The CSS declaration object with property and value fields.
|
|
1255
|
-
* @return {boolean} Whether the assembled components keep their separators.
|
|
1256
|
-
*/
|
|
1257
|
-
function keepsAssembledComponentSeparators (declaration) {
|
|
1258
|
-
return Boolean(declaration.isAssembledShorthand) && hasPositionalComponents(declaration.property);
|
|
1259
|
-
}
|
|
1260
|
-
|
|
1261
1264
|
/**
|
|
1262
1265
|
* Minifies a CSS declaration's value by applying color conversion, math simplification, shorthand compression, gradient optimization, and other property-specific optimizations.
|
|
1263
1266
|
*
|
|
@@ -1275,7 +1278,6 @@ function computeMinifiedValue (declaration) {
|
|
|
1275
1278
|
return 'none';
|
|
1276
1279
|
}
|
|
1277
1280
|
let val = declaration.value;
|
|
1278
|
-
const allowsSeparatorElision = !keepsAssembledComponentSeparators(declaration);
|
|
1279
1281
|
|
|
1280
1282
|
if (typeof val === 'string') {
|
|
1281
1283
|
val = val.trim();
|
|
@@ -1323,19 +1325,16 @@ function computeMinifiedValue (declaration) {
|
|
|
1323
1325
|
// Convert color functions to hex equivalents
|
|
1324
1326
|
val = convertColorsToHex(val);
|
|
1325
1327
|
|
|
1326
|
-
// Shorten all color tokens (hex and named) to their shortest representation
|
|
1327
|
-
// A value that keeps the whitespace between its components saves nothing by
|
|
1328
|
-
// switching to a spelling of the same length, so its colors keep the
|
|
1329
|
-
// spelling they were written with.
|
|
1328
|
+
// Shorten all color tokens (hex and named) to their shortest representation
|
|
1330
1329
|
val = replaceOutsideStringsAndUrls(val, (segment) => {
|
|
1331
|
-
return shortenColorValues(segment
|
|
1330
|
+
return shortenColorValues(segment);
|
|
1332
1331
|
});
|
|
1333
1332
|
|
|
1334
1333
|
// Collapse light-dark() when both normalized branches are identical
|
|
1335
1334
|
val = simplifyEquivalentLightDarkFunctions(val);
|
|
1336
1335
|
|
|
1337
1336
|
// Property-specific optimizations
|
|
1338
|
-
val = applyPropertyOptimizations(val, declaration.property
|
|
1337
|
+
val = applyPropertyOptimizations(val, declaration.property);
|
|
1339
1338
|
|
|
1340
1339
|
// Minify relative color syntax (identity resolution and whitespace collapsing)
|
|
1341
1340
|
val = minifyRelativeColorSyntax(val);
|
|
@@ -1356,7 +1355,6 @@ function computeMinifiedValue (declaration) {
|
|
|
1356
1355
|
// so the ones that turned out to be redundant are only dropped at the end.
|
|
1357
1356
|
const elidesRedundantSeparators = (
|
|
1358
1357
|
typeof val === 'string' &&
|
|
1359
|
-
allowsSeparatorElision &&
|
|
1360
1358
|
!isCustomProperty(declaration.property)
|
|
1361
1359
|
);
|
|
1362
1360
|
if (elidesRedundantSeparators) {
|
package/src/value/syntax.js
CHANGED
|
@@ -29,6 +29,13 @@ function findMatchingParenthesis (value, openParenIndex) {
|
|
|
29
29
|
continue;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// An escaped character is data rather than a delimiter, even outside a
|
|
33
|
+
// quoted string, as in the `\)` of an unquoted url()
|
|
34
|
+
if (character === '\\') {
|
|
35
|
+
index += 2;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
if (character === '"' || character === '\'') {
|
|
33
40
|
activeQuote = character;
|
|
34
41
|
index++;
|