@thejaredwilcurt/csslop 0.0.26 → 0.0.28
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/package.json +3 -3
- package/src/context.js +4 -3
- package/src/declarations/border.js +280 -5
- package/src/declarations/config.js +4 -3
- package/src/declarations/css-wide-keywords.js +11 -5
- package/src/declarations/merge.js +1 -0
- package/src/declarations/order.js +25 -19
- package/src/declarations/process.js +14 -3
- package/src/declarations/reset-hazards.js +108 -0
- package/src/declarations/shorthand-values.js +17 -4
- package/src/index.js +5 -0
- package/src/value/minify.js +282 -46
- package/src/value/shared.js +28 -0
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.28",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"prestart": "node ./scripts/prestart.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@eslint/js": "^10.0.1",
|
|
32
32
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
33
33
|
"codemirror": "^6.0.2",
|
|
34
|
-
"eslint": "^10.9.
|
|
34
|
+
"eslint": "^10.9.1",
|
|
35
35
|
"eslint-config-tjw-base": "^5.0.0",
|
|
36
36
|
"eslint-config-tjw-import-x": "^1.0.1",
|
|
37
37
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"eslint-plugin-jsdoc": "^64.2.1",
|
|
40
40
|
"fflate": "^0.8.3",
|
|
41
41
|
"globals": "^17.11.0",
|
|
42
|
-
"pretty-ms": "^9.3.
|
|
42
|
+
"pretty-ms": "^9.3.1",
|
|
43
43
|
"real-world-css-libraries": "^1.0.8",
|
|
44
44
|
"vite": "^8.2.2"
|
|
45
45
|
},
|
package/src/context.js
CHANGED
|
@@ -5,14 +5,15 @@
|
|
|
5
5
|
import { isUnicodeCompatibleCharset } from './charset.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Creates a fresh minification context used to track `@property`-registered custom properties
|
|
8
|
+
* Creates a fresh minification context used to track `@property`-registered custom properties, their declared syntax types, and the properties that a newly assembled shorthand must not silently reset, across the entire stylesheet.
|
|
9
9
|
*
|
|
10
|
-
* @return {object} A context object with a registeredCustomProperties Set and a
|
|
10
|
+
* @return {object} A context object with a registeredCustomProperties Set, a registeredCustomPropertySyntax Map, and a stylesheetResetProperties Set.
|
|
11
11
|
*/
|
|
12
12
|
function createMinifyContext () {
|
|
13
13
|
return {
|
|
14
14
|
registeredCustomProperties: new Set(),
|
|
15
|
-
registeredCustomPropertySyntax: new Map()
|
|
15
|
+
registeredCustomPropertySyntax: new Map(),
|
|
16
|
+
stylesheetResetProperties: new Set()
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -3,12 +3,72 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { minifyValue } from '../value/minify.js';
|
|
6
|
+
import { namedColors } from '../value/named-colors.js';
|
|
7
|
+
import { collapseShorthandParts } from '../value/shared.js';
|
|
6
8
|
import { splitTopLevelComponents } from '../value/syntax.js';
|
|
7
9
|
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
BORDER_EDGE_PROPERTIES,
|
|
12
|
+
CSS_WIDE_KEYWORDS
|
|
13
|
+
} from './config.js';
|
|
14
|
+
import { collectDeclaredProperties } from './lookup.js';
|
|
15
|
+
import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
|
|
9
16
|
|
|
10
17
|
const BORDER_TRIO_PROPERTIES = ['border-width', 'border-style', 'border-color'];
|
|
11
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The keywords that state a border line style.
|
|
21
|
+
*
|
|
22
|
+
* @type {Set<string>}
|
|
23
|
+
*/
|
|
24
|
+
const BORDER_STYLE_KEYWORDS = new Set([
|
|
25
|
+
'none',
|
|
26
|
+
'hidden',
|
|
27
|
+
'dotted',
|
|
28
|
+
'dashed',
|
|
29
|
+
'solid',
|
|
30
|
+
'double',
|
|
31
|
+
'groove',
|
|
32
|
+
'ridge',
|
|
33
|
+
'inset',
|
|
34
|
+
'outset'
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The keywords that state a border line width without writing a length.
|
|
39
|
+
*
|
|
40
|
+
* @type {Set<string>}
|
|
41
|
+
*/
|
|
42
|
+
const BORDER_WIDTH_KEYWORDS = new Set(['thin', 'medium', 'thick']);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The functions that resolve to a length, so a border edge component calling one
|
|
46
|
+
* of them states the line width.
|
|
47
|
+
*
|
|
48
|
+
* @type {RegExp}
|
|
49
|
+
*/
|
|
50
|
+
const LENGTH_FUNCTION_PATTERN = /^(?:calc|min|max|clamp|round|rem|mod|abs|sign|anchor-size)\(/i;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The functions that resolve to a color, so a border edge component calling one
|
|
54
|
+
* of them states the line color.
|
|
55
|
+
*
|
|
56
|
+
* @type {RegExp}
|
|
57
|
+
*/
|
|
58
|
+
const COLOR_FUNCTION_PATTERN = /^(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color|color-mix|light-dark|contrast-color|device-cmyk)\(/i;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The value that each part of a border edge takes when the edge leaves it out,
|
|
62
|
+
* since an edge shorthand resets every part it does not state.
|
|
63
|
+
*
|
|
64
|
+
* @type {{[key: string]: string}}
|
|
65
|
+
*/
|
|
66
|
+
const INITIAL_BORDER_EDGE_PARTS = {
|
|
67
|
+
'border-width': 'medium',
|
|
68
|
+
'border-style': 'none',
|
|
69
|
+
'border-color': 'currentcolor'
|
|
70
|
+
};
|
|
71
|
+
|
|
12
72
|
/**
|
|
13
73
|
* Finds the index of the last declaration for a property, which is the one that
|
|
14
74
|
* wins the cascade within a rule.
|
|
@@ -32,10 +92,17 @@ function findLastDeclarationIndex (declarations, property) {
|
|
|
32
92
|
* by a `border-color` override that restores the per-edge colors. The rewrite is
|
|
33
93
|
* only applied when the resulting pair is shorter than the three longhands.
|
|
34
94
|
*
|
|
35
|
-
* @param {Array}
|
|
36
|
-
* @
|
|
95
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
96
|
+
* @param {object} context The minification context with the stylesheet's reset properties.
|
|
97
|
+
* @return {Array} The declarations, with the trio rewritten when it is shorter.
|
|
37
98
|
*/
|
|
38
|
-
function collapseBorderTrioWithPerEdgeColor (declarations) {
|
|
99
|
+
function collapseBorderTrioWithPerEdgeColor (declarations, context) {
|
|
100
|
+
// The `border` shorthand also resets `border-image`, so the trio stays as it
|
|
101
|
+
// is when another rule of the stylesheet relies on that value.
|
|
102
|
+
if (resetsPropertyDeclaredElsewhere('border', collectDeclaredProperties(declarations), context)) {
|
|
103
|
+
return declarations;
|
|
104
|
+
}
|
|
105
|
+
|
|
39
106
|
const trioIndexes = BORDER_TRIO_PROPERTIES.map((property) => {
|
|
40
107
|
return findLastDeclarationIndex(declarations, property);
|
|
41
108
|
});
|
|
@@ -117,4 +184,212 @@ function collapseBorderTrioWithPerEdgeColor (declarations) {
|
|
|
117
184
|
});
|
|
118
185
|
}
|
|
119
186
|
|
|
120
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Decides which part of a border edge a single component states. Every
|
|
189
|
+
* component has to be recognized, because a component sorted into the wrong
|
|
190
|
+
* part would change what the rule renders.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} component One component of a border edge value.
|
|
193
|
+
* @return {string|null} The trio property the component states, or null when it is unrecognized.
|
|
194
|
+
*/
|
|
195
|
+
function classifyBorderEdgeComponent (component) {
|
|
196
|
+
const keyword = component.toLowerCase();
|
|
197
|
+
if (BORDER_STYLE_KEYWORDS.has(keyword)) {
|
|
198
|
+
return 'border-style';
|
|
199
|
+
}
|
|
200
|
+
// Match a number with an optional unit, which is how a length is written
|
|
201
|
+
const isLength = /^[+-]?(?:\d+|\d*\.\d+)[a-z]*$/i.test(component);
|
|
202
|
+
if (BORDER_WIDTH_KEYWORDS.has(keyword) || isLength || LENGTH_FUNCTION_PATTERN.test(component)) {
|
|
203
|
+
return 'border-width';
|
|
204
|
+
}
|
|
205
|
+
const isColor = (
|
|
206
|
+
keyword === 'currentcolor' ||
|
|
207
|
+
component.startsWith('#') ||
|
|
208
|
+
Object.hasOwn(namedColors, keyword) ||
|
|
209
|
+
COLOR_FUNCTION_PATTERN.test(component)
|
|
210
|
+
);
|
|
211
|
+
if (isColor) {
|
|
212
|
+
return 'border-color';
|
|
213
|
+
}
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Splits one border edge value into the width, style, and color it sets. A part
|
|
219
|
+
* the edge leaves unstated is reset to its initial value by the edge shorthand,
|
|
220
|
+
* so the split states that initial value in its place.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} value The value of a `border-<side>` declaration, without its importance.
|
|
223
|
+
* @return {object|null} The value each trio property takes, or null when the edge cannot be split.
|
|
224
|
+
*/
|
|
225
|
+
function splitBorderEdgeValue (value) {
|
|
226
|
+
const statedProperties = new Set();
|
|
227
|
+
const parts = { ...INITIAL_BORDER_EDGE_PARTS };
|
|
228
|
+
|
|
229
|
+
for (const component of splitTopLevelComponents(value)) {
|
|
230
|
+
const property = classifyBorderEdgeComponent(component);
|
|
231
|
+
if (!property || statedProperties.has(property)) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
statedProperties.add(property);
|
|
235
|
+
parts[property] = component;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!statedProperties.size) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
return parts;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Collects the one declaration of each border edge, in top/right/bottom/left
|
|
246
|
+
* order. An edge that a rule states more than once is kept as it is, since the
|
|
247
|
+
* repeat is a deliberate fallback whose order the split would not preserve.
|
|
248
|
+
*
|
|
249
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
250
|
+
* @return {Array|null} The four edge declarations, or null when the rule does not state each edge exactly once.
|
|
251
|
+
*/
|
|
252
|
+
function collectBorderEdgeDeclarations (declarations) {
|
|
253
|
+
const edgeDeclarations = [];
|
|
254
|
+
for (const edgeProperty of BORDER_EDGE_PROPERTIES) {
|
|
255
|
+
const matches = declarations.filter((declaration) => {
|
|
256
|
+
return declaration.property === edgeProperty;
|
|
257
|
+
});
|
|
258
|
+
if (matches.length !== 1) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
edgeDeclarations.push(matches[0]);
|
|
262
|
+
}
|
|
263
|
+
return edgeDeclarations;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Builds the `border-width`, `border-style`, and `border-color` declarations
|
|
268
|
+
* that state per side what a set of border edges stated per edge. The edges are
|
|
269
|
+
* read as authored rather than minified, so that each component is still a
|
|
270
|
+
* component of its own and keeps the spelling it was written with.
|
|
271
|
+
*
|
|
272
|
+
* @param {Array} edgeDeclarations The four edge declarations, in top/right/bottom/left order.
|
|
273
|
+
* @return {Array|null} The trio declarations, or null when the edges cannot be split.
|
|
274
|
+
*/
|
|
275
|
+
function buildBorderTrioDeclarations (edgeDeclarations) {
|
|
276
|
+
const edgeParts = [];
|
|
277
|
+
for (const declaration of edgeDeclarations) {
|
|
278
|
+
if (typeof declaration.value !== 'string') {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const parts = splitBorderEdgeValue(declaration.value.trim());
|
|
282
|
+
if (!parts) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
edgeParts.push(parts);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// The three parts do not affect one another, so they are written in a fixed
|
|
289
|
+
// alphabetical order rather than in an order the values would decide.
|
|
290
|
+
return [...BORDER_TRIO_PROPERTIES].sort().map((property) => {
|
|
291
|
+
const sideValues = edgeParts.map((parts) => {
|
|
292
|
+
return parts[property];
|
|
293
|
+
});
|
|
294
|
+
return {
|
|
295
|
+
property,
|
|
296
|
+
value: collapseShorthandParts(sideValues).join(' '),
|
|
297
|
+
isAssembledShorthand: true
|
|
298
|
+
};
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Measures what a set of declarations costs in the output, which is the text of
|
|
304
|
+
* each of them joined by the semicolons that separate them.
|
|
305
|
+
*
|
|
306
|
+
* @param {Array} declarations The declarations to measure.
|
|
307
|
+
* @return {number} The number of characters the declarations take up.
|
|
308
|
+
*/
|
|
309
|
+
function measureDeclarations (declarations) {
|
|
310
|
+
return declarations.map((declaration) => {
|
|
311
|
+
return declaration.property + ':' + minifyValue(declaration);
|
|
312
|
+
}).join(';').length;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Rewrites four `border-<side>` declarations whose values differ into the
|
|
317
|
+
* `border-width`, `border-style`, and `border-color` trio. Unlike `margin`, the
|
|
318
|
+
* `border` shorthand is not a four-sided one: it takes a single width, style,
|
|
319
|
+
* and color, so four differing edges have no `border` value to collapse into.
|
|
320
|
+
* The trio, on the other hand, states each of the three parts once per side,
|
|
321
|
+
* which spells the same borders out in fewer characters.
|
|
322
|
+
*
|
|
323
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
324
|
+
* @return {Array} The declarations, with the edges rewritten when the trio is shorter.
|
|
325
|
+
*/
|
|
326
|
+
function splitBorderEdgesIntoTrio (declarations) {
|
|
327
|
+
const declaredProperties = collectDeclaredProperties(declarations);
|
|
328
|
+
// A rule that already states a part of the trio, or the whole border, would
|
|
329
|
+
// gain a second declaration of it rather than a shorter spelling of the edges.
|
|
330
|
+
const statesTrioAlready = ['border', ...BORDER_TRIO_PROPERTIES].some((property) => {
|
|
331
|
+
return declaredProperties.has(property);
|
|
332
|
+
});
|
|
333
|
+
if (statesTrioAlready) {
|
|
334
|
+
return declarations;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const edgeDeclarations = collectBorderEdgeDeclarations(declarations);
|
|
338
|
+
if (!edgeDeclarations) {
|
|
339
|
+
return declarations;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const edgeValues = edgeDeclarations.map((declaration) => {
|
|
343
|
+
return minifyValue(declaration);
|
|
344
|
+
});
|
|
345
|
+
const importantEdges = edgeValues.filter((value) => {
|
|
346
|
+
return value.includes('!important');
|
|
347
|
+
});
|
|
348
|
+
// An edge whose importance differs from its siblings cannot join a shared
|
|
349
|
+
// declaration, which carries one importance for all four sides.
|
|
350
|
+
const hasUniformImportance = importantEdges.length === 0 || importantEdges.length === edgeValues.length;
|
|
351
|
+
if (!hasUniformImportance) {
|
|
352
|
+
return declarations;
|
|
353
|
+
}
|
|
354
|
+
const importantSuffix = importantEdges.length ? '!important' : '';
|
|
355
|
+
|
|
356
|
+
const trioDeclarations = buildBorderTrioDeclarations(edgeDeclarations.map((declaration) => {
|
|
357
|
+
return {
|
|
358
|
+
...declaration,
|
|
359
|
+
// Match a trailing importance flag, which the trio carries once instead
|
|
360
|
+
value: String(declaration.value).replace(/\s*!\s*important\s*$/i, '')
|
|
361
|
+
};
|
|
362
|
+
}));
|
|
363
|
+
if (!trioDeclarations) {
|
|
364
|
+
return declarations;
|
|
365
|
+
}
|
|
366
|
+
const importantTrioDeclarations = trioDeclarations.map((declaration) => {
|
|
367
|
+
return {
|
|
368
|
+
...declaration,
|
|
369
|
+
value: declaration.value + importantSuffix
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const isShorter = measureDeclarations(importantTrioDeclarations) < measureDeclarations(edgeDeclarations);
|
|
374
|
+
if (!isShorter) {
|
|
375
|
+
return declarations;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const insertionIndex = declarations.findIndex((declaration) => {
|
|
379
|
+
return BORDER_EDGE_PROPERTIES.includes(declaration.property);
|
|
380
|
+
});
|
|
381
|
+
return declarations.flatMap((declaration, index) => {
|
|
382
|
+
if (index === insertionIndex) {
|
|
383
|
+
return importantTrioDeclarations;
|
|
384
|
+
}
|
|
385
|
+
if (BORDER_EDGE_PROPERTIES.includes(declaration.property)) {
|
|
386
|
+
return [];
|
|
387
|
+
}
|
|
388
|
+
return [declaration];
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export {
|
|
393
|
+
collapseBorderTrioWithPerEdgeColor,
|
|
394
|
+
splitBorderEdgesIntoTrio
|
|
395
|
+
};
|
|
@@ -66,7 +66,7 @@ const shorthandMap = {
|
|
|
66
66
|
'place-items': ['align-items', 'justify-items'],
|
|
67
67
|
'place-content': ['align-content', 'justify-content'],
|
|
68
68
|
'place-self': ['align-self', 'justify-self'],
|
|
69
|
-
columns: ['column-width', 'column-count'],
|
|
69
|
+
columns: ['column-width', 'column-count', 'column-height'],
|
|
70
70
|
'list-style': ['list-style-position', 'list-style-image', 'list-style-type'],
|
|
71
71
|
'margin-inline': ['margin-inline-start', 'margin-inline-end'],
|
|
72
72
|
'margin-block': ['margin-block-start', 'margin-block-end'],
|
|
@@ -89,9 +89,10 @@ const shorthandMap = {
|
|
|
89
89
|
|
|
90
90
|
const shorthandOverrideMap = {
|
|
91
91
|
animation: ['animation-timeline', 'animation-range', 'animation-range-start', 'animation-range-end'],
|
|
92
|
+
columns: ['column-wrap'],
|
|
92
93
|
border: ['border-image', 'border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'],
|
|
93
|
-
font: ['font-variant', 'font-variant-alternates', 'font-variant-caps', 'font-variant-east-asian', 'font-variant-ligatures', 'font-variant-numeric', 'font-variant-position'],
|
|
94
|
-
mask: ['mask-border', 'mask-border-source', 'mask-border-slice', 'mask-border-width', 'mask-border-outset', 'mask-border-repeat', 'mask-border-mode']
|
|
94
|
+
font: ['font-variant', 'font-variant-alternates', 'font-variant-caps', 'font-variant-east-asian', 'font-variant-ligatures', 'font-variant-numeric', 'font-variant-position', 'font-feature-settings', 'font-kerning', 'font-language-override', 'font-optical-sizing', 'font-size-adjust', 'font-variation-settings'],
|
|
95
|
+
mask: ['mask-border', 'mask-border-source', 'mask-border-slice', 'mask-border-width', 'mask-border-outset', 'mask-border-repeat', 'mask-border-mode', 'mask-position', 'mask-origin', 'mask-clip', 'mask-composite', 'mask-mode']
|
|
95
96
|
};
|
|
96
97
|
|
|
97
98
|
/**
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
collectDeclaredProperties,
|
|
14
14
|
describeDeclaration
|
|
15
15
|
} from './lookup.js';
|
|
16
|
+
import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Checks whether a group of longhands sets every leaf longhand that the
|
|
@@ -153,12 +154,16 @@ function resetsEarlierDeclaration (declarations, shorthandName, insertionIndex)
|
|
|
153
154
|
* @param {Array} declarations The declarations of a single rule.
|
|
154
155
|
* @param {string} shorthandName The target shorthand property name.
|
|
155
156
|
* @param {Set} declaredProperties The property names the rule currently declares.
|
|
157
|
+
* @param {object} context The minification context with the stylesheet's reset properties.
|
|
156
158
|
* @return {Array|null} The rewritten declarations, or null when the rewrite does not apply.
|
|
157
159
|
*/
|
|
158
|
-
function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredProperties) {
|
|
160
|
+
function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredProperties, context) {
|
|
159
161
|
if (declaredProperties.has(shorthandName)) {
|
|
160
162
|
return null;
|
|
161
163
|
}
|
|
164
|
+
if (resetsPropertyDeclaredElsewhere(shorthandName, declaredProperties, context)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
162
167
|
|
|
163
168
|
const entries = collectLonghandEntries(declarations, shorthandName);
|
|
164
169
|
if (entries.length < 2 || hasRepeatedProperty(entries)) {
|
|
@@ -230,10 +235,11 @@ function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredPr
|
|
|
230
235
|
* followed by `border-width:2px`, which inherits every border property and then
|
|
231
236
|
* overrides the one that differs.
|
|
232
237
|
*
|
|
233
|
-
* @param {Array}
|
|
234
|
-
* @
|
|
238
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
239
|
+
* @param {object} context The minification context with the stylesheet's reset properties.
|
|
240
|
+
* @return {Array} The declarations, with eligible groups rewritten.
|
|
235
241
|
*/
|
|
236
|
-
function hoistCssWideKeywordsIntoShorthands (declarations) {
|
|
242
|
+
function hoistCssWideKeywordsIntoShorthands (declarations, context) {
|
|
237
243
|
let result = declarations;
|
|
238
244
|
// Every shorthand needs to know which properties the rule declares, so that
|
|
239
245
|
// set is kept alongside the declarations and only rebuilt after a rewrite
|
|
@@ -242,7 +248,7 @@ function hoistCssWideKeywordsIntoShorthands (declarations) {
|
|
|
242
248
|
// Shorthands are visited in declaration order, so the widest shorthand of a
|
|
243
249
|
// family is rewritten before the narrower shorthands it contains.
|
|
244
250
|
for (const shorthandName of Object.keys(shorthandMap)) {
|
|
245
|
-
const rewritten = rewriteGroupAsKeywordShorthand(result, shorthandName, declaredProperties);
|
|
251
|
+
const rewritten = rewriteGroupAsKeywordShorthand(result, shorthandName, declaredProperties, context);
|
|
246
252
|
if (rewritten) {
|
|
247
253
|
result = rewritten;
|
|
248
254
|
declaredProperties = collectDeclaredProperties(result);
|
|
@@ -35,6 +35,7 @@ const PARTIAL_MERGE_REQUIREMENTS = {
|
|
|
35
35
|
background: [['background-color', 'background-image']],
|
|
36
36
|
'background-position': [['background-position-x'], ['background-position-y']],
|
|
37
37
|
'border-image': [['border-image-source']],
|
|
38
|
+
columns: [['column-width', 'column-count', 'column-height']],
|
|
38
39
|
font: [['font-size'], ['font-family']],
|
|
39
40
|
mask: [['mask-image']]
|
|
40
41
|
};
|
|
@@ -9,28 +9,34 @@ import {
|
|
|
9
9
|
import { collectDeclaredProperties } from './lookup.js';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
*
|
|
13
|
-
* the first resets the second
|
|
14
|
-
* longhands it was built from.
|
|
12
|
+
* Builds the pairs of properties where the first has to be emitted before the
|
|
13
|
+
* second, because the first resets the second while a merged shorthand is
|
|
14
|
+
* appended after the longhands it was built from. Every property a shorthand
|
|
15
|
+
* resets but cannot express has to be restated after that shorthand, and a
|
|
16
|
+
* `margin` built from a mixed `!important` group leaves its important longhands
|
|
17
|
+
* behind for the same reason.
|
|
18
|
+
*
|
|
19
|
+
* @return {Array} The property pairs, each as a shorthand followed by the property it resets.
|
|
20
|
+
*/
|
|
21
|
+
function buildRequiredPropertyOrder () {
|
|
22
|
+
const orderedPairs = [];
|
|
23
|
+
for (const [shorthandProperty, resetProperties] of Object.entries(shorthandOverrideMap)) {
|
|
24
|
+
for (const resetProperty of resetProperties) {
|
|
25
|
+
orderedPairs.push([shorthandProperty, resetProperty]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const longhandProperty of shorthandMap.margin) {
|
|
29
|
+
orderedPairs.push(['margin', longhandProperty]);
|
|
30
|
+
}
|
|
31
|
+
return orderedPairs;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The property pairs whose relative order the output has to correct.
|
|
15
36
|
*
|
|
16
37
|
* @type {Array}
|
|
17
38
|
*/
|
|
18
|
-
const REQUIRED_PROPERTY_ORDER =
|
|
19
|
-
['animation', 'animation-timeline'],
|
|
20
|
-
['animation', 'animation-range'],
|
|
21
|
-
['animation', 'animation-range-start'],
|
|
22
|
-
['animation', 'animation-range-end'],
|
|
23
|
-
['border', 'border-image'],
|
|
24
|
-
['font', 'font-feature-settings'],
|
|
25
|
-
['font', 'font-variant-ligatures'],
|
|
26
|
-
['font', 'font-kerning'],
|
|
27
|
-
['font', 'font-variation-settings'],
|
|
28
|
-
['mask', 'mask-border'],
|
|
29
|
-
['margin', 'margin-top'],
|
|
30
|
-
['margin', 'margin-right'],
|
|
31
|
-
['margin', 'margin-bottom'],
|
|
32
|
-
['margin', 'margin-left']
|
|
33
|
-
];
|
|
39
|
+
const REQUIRED_PROPERTY_ORDER = buildRequiredPropertyOrder();
|
|
34
40
|
|
|
35
41
|
/**
|
|
36
42
|
* Reorders declarations so that shorthands appear before any related longhands they would override, preventing cascade issues in the minified output.
|
|
@@ -6,7 +6,10 @@ import { minifyValue } from '../value/minify.js';
|
|
|
6
6
|
import { hasInvalidQuotesCount } from '../value/quotes.js';
|
|
7
7
|
|
|
8
8
|
import { absorbBackgroundLonghandsIntoShorthand } from './background.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
collapseBorderTrioWithPerEdgeColor,
|
|
11
|
+
splitBorderEdgesIntoTrio
|
|
12
|
+
} from './border.js';
|
|
10
13
|
import {
|
|
11
14
|
getLonghandsOf,
|
|
12
15
|
shorthandMap
|
|
@@ -25,6 +28,7 @@ import {
|
|
|
25
28
|
getOverriddenLonghands,
|
|
26
29
|
orderDeclarations
|
|
27
30
|
} from './order.js';
|
|
31
|
+
import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* Shorthands that keep their non-important longhands in the output, so a mixed
|
|
@@ -348,6 +352,12 @@ function mergeLonghandsIntoShorthands (declarations, context) {
|
|
|
348
352
|
if (declaredProperties.has(shorthand)) {
|
|
349
353
|
continue;
|
|
350
354
|
}
|
|
355
|
+
// Assembling a shorthand also resets the properties it cannot express, so
|
|
356
|
+
// a rule only collapses into one when nothing else in the stylesheet
|
|
357
|
+
// relies on a value that reset would discard.
|
|
358
|
+
if (resetsPropertyDeclaredElsewhere(shorthand, declaredProperties, context)) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
351
361
|
|
|
352
362
|
const mergeableProperties = getMergeProps(shorthand, longhands, declaredProperties);
|
|
353
363
|
if (!mergeableProperties) {
|
|
@@ -398,8 +408,9 @@ function processDeclarations (declarations, context) {
|
|
|
398
408
|
result = absorbBackgroundLonghandsIntoShorthand(result);
|
|
399
409
|
result = mergeLonghandsIntoShorthands(result, context);
|
|
400
410
|
result = foldLonghandOverridesIntoShorthands(result, context);
|
|
401
|
-
result = hoistCssWideKeywordsIntoShorthands(result);
|
|
402
|
-
result =
|
|
411
|
+
result = hoistCssWideKeywordsIntoShorthands(result, context);
|
|
412
|
+
result = splitBorderEdgesIntoTrio(result);
|
|
413
|
+
result = collapseBorderTrioWithPerEdgeColor(result, context);
|
|
403
414
|
|
|
404
415
|
return orderDeclarations(result);
|
|
405
416
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Tracks the properties a shorthand resets without being able to express them, so that assembling a shorthand out of longhands never cancels a value another rule of the stylesheet set.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getOverridesOf,
|
|
7
|
+
shorthandOverrideMap
|
|
8
|
+
} from './config.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Collects every property that some shorthand resets without being able to
|
|
12
|
+
* express it, such as `border-image` for `border`. Only these properties can
|
|
13
|
+
* lose their value to a newly assembled shorthand, so only these are worth
|
|
14
|
+
* tracking across the stylesheet.
|
|
15
|
+
*
|
|
16
|
+
* @return {Set} The property names a shorthand resets but cannot express.
|
|
17
|
+
*/
|
|
18
|
+
function collectResettableProperties () {
|
|
19
|
+
const resettableProperties = new Set();
|
|
20
|
+
for (const resetProperties of Object.values(shorthandOverrideMap)) {
|
|
21
|
+
for (const property of resetProperties) {
|
|
22
|
+
resettableProperties.add(property);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return resettableProperties;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The properties a shorthand resets but cannot express. The shorthand tables
|
|
30
|
+
* never change, so the set is built once.
|
|
31
|
+
*
|
|
32
|
+
* @type {Set<string>}
|
|
33
|
+
*/
|
|
34
|
+
const RESETTABLE_PROPERTIES = collectResettableProperties();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Collects the rules nested inside a rule. The parser stores the children of an
|
|
38
|
+
* at-rule as a list of rules, and the children of a style rule that uses CSS
|
|
39
|
+
* nesting as declarations that carry rules of their own.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} rule The AST rule node to look inside.
|
|
42
|
+
* @return {Array} The rule nodes nested within it.
|
|
43
|
+
*/
|
|
44
|
+
function collectNestedRules (rule) {
|
|
45
|
+
const nestedRules = [...(rule.rules || [])];
|
|
46
|
+
for (const declaration of rule.declarations || []) {
|
|
47
|
+
if (declaration.rules || declaration.declarations) {
|
|
48
|
+
nestedRules.push(declaration);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return nestedRules;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Records which resettable properties the stylesheet declares, in any rule at
|
|
56
|
+
* any nesting depth, so that each rule can later ask whether assembling a
|
|
57
|
+
* shorthand would cancel a value another rule set.
|
|
58
|
+
*
|
|
59
|
+
* @param {Array} rules The AST rule nodes of the whole stylesheet.
|
|
60
|
+
* @param {object} context The minification context to populate.
|
|
61
|
+
*/
|
|
62
|
+
function recordStylesheetResetProperties (rules, context) {
|
|
63
|
+
const pendingRules = [...rules];
|
|
64
|
+
while (pendingRules.length) {
|
|
65
|
+
const rule = pendingRules.pop();
|
|
66
|
+
for (const declaration of rule.declarations || []) {
|
|
67
|
+
if (RESETTABLE_PROPERTIES.has(declaration.property)) {
|
|
68
|
+
context.stylesheetResetProperties.add(declaration.property);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
pendingRules.push(...collectNestedRules(rule));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Checks whether assembling a shorthand out of longhands would reset a property
|
|
77
|
+
* that the stylesheet sets somewhere else. The `border` shorthand resets
|
|
78
|
+
* `border-image`, so turning the border longhands of one rule into `border`
|
|
79
|
+
* cancels the `border-image` that another rule sets on the same element. A rule
|
|
80
|
+
* that states the reset property itself is safe, because that declaration is
|
|
81
|
+
* emitted after the shorthand and restates the value the shorthand discarded.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} shorthandName The shorthand that would be assembled.
|
|
84
|
+
* @param {Set} declaredProperties The property names the rule declares.
|
|
85
|
+
* @param {object} context The minification context with the stylesheet's reset properties.
|
|
86
|
+
* @return {boolean} Whether assembling the shorthand would discard another rule's value.
|
|
87
|
+
*/
|
|
88
|
+
function resetsPropertyDeclaredElsewhere (shorthandName, declaredProperties, context) {
|
|
89
|
+
const stylesheetResetProperties = context?.stylesheetResetProperties;
|
|
90
|
+
if (!stylesheetResetProperties?.size) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
for (const resetProperty of getOverridesOf(shorthandName)) {
|
|
94
|
+
const isSetElsewhere = (
|
|
95
|
+
stylesheetResetProperties.has(resetProperty) &&
|
|
96
|
+
!declaredProperties.has(resetProperty)
|
|
97
|
+
);
|
|
98
|
+
if (isSetElsewhere) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
recordStylesheetResetProperties,
|
|
107
|
+
resetsPropertyDeclaredElsewhere
|
|
108
|
+
};
|
|
@@ -193,14 +193,27 @@ function buildTextDecorationValue ({ valueMap, importantSuffix }) {
|
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
* Builds a `columns` value
|
|
197
|
-
*
|
|
196
|
+
* Builds a `columns` value from its width, count, and height components. The
|
|
197
|
+
* grammar writes the width and the count next to each other and places the
|
|
198
|
+
* height behind a `/`. Every one of the three starts out as `auto`, so a
|
|
199
|
+
* component that holds `auto` states nothing and is left out, and a value whose
|
|
200
|
+
* components are all `auto` shrinks to a single `auto`.
|
|
198
201
|
*
|
|
199
202
|
* @param {ShorthandComponents} components The collected longhand values.
|
|
200
203
|
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
201
204
|
*/
|
|
202
|
-
function buildColumnsValue ({
|
|
203
|
-
|
|
205
|
+
function buildColumnsValue ({ valueMap, importantSuffix }) {
|
|
206
|
+
const height = valueMap.get('column-height');
|
|
207
|
+
const statedParts = ['column-width', 'column-count'].map((property) => {
|
|
208
|
+
return valueMap.get(property);
|
|
209
|
+
}).filter((value) => {
|
|
210
|
+
return value && value !== 'auto';
|
|
211
|
+
});
|
|
212
|
+
const inlineSize = statedParts.join(' ') || 'auto';
|
|
213
|
+
if (height && height !== 'auto') {
|
|
214
|
+
return inlineSize + '/' + height + importantSuffix;
|
|
215
|
+
}
|
|
216
|
+
return inlineSize + importantSuffix;
|
|
204
217
|
}
|
|
205
218
|
|
|
206
219
|
/**
|
package/src/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
createMinifyContext,
|
|
14
14
|
setActiveCharset
|
|
15
15
|
} from './context.js';
|
|
16
|
+
import { recordStylesheetResetProperties } from './declarations/reset-hazards.js';
|
|
16
17
|
import {
|
|
17
18
|
analyzePositionTryRules,
|
|
18
19
|
cleanPositionTryRules,
|
|
@@ -231,6 +232,10 @@ export const minifyCSS = function (input) {
|
|
|
231
232
|
const context = createMinifyContext();
|
|
232
233
|
|
|
233
234
|
if (ast?.stylesheet?.rules) {
|
|
235
|
+
// Which properties a shorthand may not silently reset is a question about
|
|
236
|
+
// the whole stylesheet, so it is answered before any rule is rewritten.
|
|
237
|
+
recordStylesheetResetProperties(ast.stylesheet.rules, context);
|
|
238
|
+
|
|
234
239
|
const {
|
|
235
240
|
positionTryRules,
|
|
236
241
|
positionTryUsage
|
package/src/value/minify.js
CHANGED
|
@@ -84,10 +84,11 @@ const COLOR_TOKEN_PATTERN = new RegExp(
|
|
|
84
84
|
* the shortest equivalent representation, comparing full hex, shortened hex,
|
|
85
85
|
* and any matching named color keyword.
|
|
86
86
|
*
|
|
87
|
-
* @param {string}
|
|
88
|
-
* @
|
|
87
|
+
* @param {string} segment A CSS value segment (outside strings and urls).
|
|
88
|
+
* @param {boolean} rewritesEqualLengthSpelling Whether a spelling of the same length is worth switching to.
|
|
89
|
+
* @return {string} The segment with all colors shortened to their minimal form.
|
|
89
90
|
*/
|
|
90
|
-
function shortenColorValues (segment) {
|
|
91
|
+
function shortenColorValues (segment, rewritesEqualLengthSpelling = true) {
|
|
91
92
|
// Match "color-mix(" as a whole word, case-insensitive
|
|
92
93
|
const hasColorMix = /\bcolor-mix\(/i.test(segment);
|
|
93
94
|
return segment.replace(COLOR_TOKEN_PATTERN, (match) => {
|
|
@@ -108,19 +109,30 @@ function shortenColorValues (segment) {
|
|
|
108
109
|
if (!channels) {
|
|
109
110
|
return match;
|
|
110
111
|
}
|
|
111
|
-
|
|
112
|
+
const shortest = shortestColor(channels[0], channels[1], channels[2], channels[3]);
|
|
113
|
+
if (!rewritesEqualLengthSpelling && shortest.length >= match.length) {
|
|
114
|
+
return match;
|
|
115
|
+
}
|
|
116
|
+
return shortest;
|
|
112
117
|
});
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
/**
|
|
116
|
-
*
|
|
121
|
+
* @typedef {object} ValueSegment
|
|
122
|
+
* @property {string} text The segment's slice of the value.
|
|
123
|
+
* @property {boolean} isLiteral Whether the segment is a quoted string or a url() token.
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Splits a CSS value into literal and syntax segments. A quoted string and a
|
|
128
|
+
* `url()` token are literals: they hold data rather than CSS syntax, so no pass
|
|
129
|
+
* may rewrite what is inside them. Everything between the literals is syntax.
|
|
117
130
|
*
|
|
118
|
-
* @param {string}
|
|
119
|
-
* @
|
|
120
|
-
* @return {string} The value with the replacer applied to all eligible segments.
|
|
131
|
+
* @param {string} value The full CSS value string.
|
|
132
|
+
* @return {Array} The value's segments, in order.
|
|
121
133
|
*/
|
|
122
|
-
function
|
|
123
|
-
|
|
134
|
+
function splitValueSegments (value) {
|
|
135
|
+
const segments = [];
|
|
124
136
|
let index = 0;
|
|
125
137
|
|
|
126
138
|
const consumeQuoted = (start) => {
|
|
@@ -144,31 +156,36 @@ function replaceOutsideStringsAndUrls (value, replacer) {
|
|
|
144
156
|
return value.slice(start, start + 4).toLowerCase() === 'url(';
|
|
145
157
|
};
|
|
146
158
|
|
|
159
|
+
const consumeUrl = (start) => {
|
|
160
|
+
let depth = 1;
|
|
161
|
+
let end = start + 4;
|
|
162
|
+
while (end < value.length && depth > 0) {
|
|
163
|
+
if (value[end] === '"' || value[end] === '\'') {
|
|
164
|
+
end = consumeQuoted(end);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (value[end] === '(') {
|
|
168
|
+
depth++;
|
|
169
|
+
}
|
|
170
|
+
if (value[end] === ')') {
|
|
171
|
+
depth--;
|
|
172
|
+
}
|
|
173
|
+
end++;
|
|
174
|
+
}
|
|
175
|
+
return end;
|
|
176
|
+
};
|
|
177
|
+
|
|
147
178
|
while (index < value.length) {
|
|
148
179
|
if (value[index] === '"' || value[index] === '\'') {
|
|
149
180
|
const end = consumeQuoted(index);
|
|
150
|
-
|
|
181
|
+
segments.push({ text: value.slice(index, end), isLiteral: true });
|
|
151
182
|
index = end;
|
|
152
183
|
continue;
|
|
153
184
|
}
|
|
154
185
|
|
|
155
186
|
if (startsUrl(index)) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
while (end < value.length && depth > 0) {
|
|
159
|
-
if (value[end] === '"' || value[end] === '\'') {
|
|
160
|
-
end = consumeQuoted(end);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (value[end] === '(') {
|
|
164
|
-
depth++;
|
|
165
|
-
}
|
|
166
|
-
if (value[end] === ')') {
|
|
167
|
-
depth--;
|
|
168
|
-
}
|
|
169
|
-
end++;
|
|
170
|
-
}
|
|
171
|
-
result += value.slice(index, end);
|
|
187
|
+
const end = consumeUrl(index);
|
|
188
|
+
segments.push({ text: value.slice(index, end), isLiteral: true });
|
|
172
189
|
index = end;
|
|
173
190
|
continue;
|
|
174
191
|
}
|
|
@@ -177,10 +194,40 @@ function replaceOutsideStringsAndUrls (value, replacer) {
|
|
|
177
194
|
while (index < value.length && value[index] !== '"' && value[index] !== '\'' && !startsUrl(index)) {
|
|
178
195
|
index++;
|
|
179
196
|
}
|
|
180
|
-
|
|
197
|
+
segments.push({ text: value.slice(start, index), isLiteral: false });
|
|
181
198
|
}
|
|
182
199
|
|
|
183
|
-
return
|
|
200
|
+
return segments;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Reports whether a segment is a quoted string. Every literal segment is either
|
|
205
|
+
* a quoted string or a url() token, so anything else is one of the latter.
|
|
206
|
+
*
|
|
207
|
+
* @param {ValueSegment} [segment] The segment to test, when the value has one there.
|
|
208
|
+
* @return {boolean} Whether the segment is a quoted string.
|
|
209
|
+
*/
|
|
210
|
+
function isQuotedStringSegment (segment) {
|
|
211
|
+
if (!segment?.isLiteral) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
return segment.text.startsWith('"') || segment.text.startsWith('\'');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Applies a replacer function only to segments of a CSS value that are outside quoted strings and url() functions, preserving those literal segments unchanged.
|
|
219
|
+
*
|
|
220
|
+
* @param {string} value The full CSS value string.
|
|
221
|
+
* @param {function(string): string} replacer A function called with each non-string, non-url segment, returning the replacement string.
|
|
222
|
+
* @return {string} The value with the replacer applied to all eligible segments.
|
|
223
|
+
*/
|
|
224
|
+
function replaceOutsideStringsAndUrls (value, replacer) {
|
|
225
|
+
return splitValueSegments(value).map((segment) => {
|
|
226
|
+
if (segment.isLiteral) {
|
|
227
|
+
return segment.text;
|
|
228
|
+
}
|
|
229
|
+
return replacer(segment.text);
|
|
230
|
+
}).join('');
|
|
184
231
|
}
|
|
185
232
|
|
|
186
233
|
/**
|
|
@@ -214,6 +261,108 @@ function elideSpaceBeforeHexColors (value) {
|
|
|
214
261
|
});
|
|
215
262
|
}
|
|
216
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Matches the separator whitespace at the head of a value segment. Whitespace
|
|
266
|
+
* that precedes a `+` or a `-` is left alone, because a math function requires
|
|
267
|
+
* those two operators to be surrounded by it.
|
|
268
|
+
*
|
|
269
|
+
* @type {RegExp}
|
|
270
|
+
*/
|
|
271
|
+
const LEADING_SEPARATOR_PATTERN = /^\s+(?![+-])/;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Removes the separator whitespace that follows a closing parenthesis. A
|
|
275
|
+
* parenthesis ends its own token, so the whitespace after one never keeps two
|
|
276
|
+
* tokens from merging: `url(a.png) 30 round` holds the same tokens as
|
|
277
|
+
* `url(a.png)30 round`.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} value The CSS value string.
|
|
280
|
+
* @return {string} The value without the redundant separators.
|
|
281
|
+
*/
|
|
282
|
+
function elideSpaceAfterParentheses (value) {
|
|
283
|
+
const segments = splitValueSegments(value);
|
|
284
|
+
return segments.map((segment, index) => {
|
|
285
|
+
if (segment.isLiteral) {
|
|
286
|
+
return segment.text;
|
|
287
|
+
}
|
|
288
|
+
let text = segment.text;
|
|
289
|
+
const previousSegment = segments[index - 1];
|
|
290
|
+
// A url() token ends with the parenthesis that closes it, so whitespace at
|
|
291
|
+
// the head of the segment after one is a separator of the same kind
|
|
292
|
+
if (previousSegment?.isLiteral && !isQuotedStringSegment(previousSegment)) {
|
|
293
|
+
text = text.replace(LEADING_SEPARATOR_PATTERN, '');
|
|
294
|
+
}
|
|
295
|
+
// Match whitespace that follows a closing parenthesis
|
|
296
|
+
return text.replace(/\)\s+(?![+-])/g, ')');
|
|
297
|
+
}).join('');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Removes the separator whitespace that follows a closing quote. A string ends
|
|
302
|
+
* its own token, so no separator is needed between it and the token that comes
|
|
303
|
+
* next: `"smcp" 1` holds the same tokens as `"smcp"1`.
|
|
304
|
+
*
|
|
305
|
+
* @param {string} value The CSS value string.
|
|
306
|
+
* @return {string} The value without the redundant separators.
|
|
307
|
+
*/
|
|
308
|
+
function elideSpaceAfterStrings (value) {
|
|
309
|
+
const segments = splitValueSegments(value);
|
|
310
|
+
return segments.map((segment, index) => {
|
|
311
|
+
if (segment.isLiteral || !isQuotedStringSegment(segments[index - 1])) {
|
|
312
|
+
return segment.text;
|
|
313
|
+
}
|
|
314
|
+
return segment.text.replace(LEADING_SEPARATOR_PATTERN, '');
|
|
315
|
+
}).join('');
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Reports whether a value separates top-level entries with commas, the way the
|
|
320
|
+
* repeatable values of properties such as `font-variation-settings` and
|
|
321
|
+
* `transition` do. Every literal is skipped, so a comma inside a string or a
|
|
322
|
+
* url does not count, and the parenthesis depth tells a function's argument
|
|
323
|
+
* separators apart from the value's own.
|
|
324
|
+
*
|
|
325
|
+
* @param {string} value The CSS value string.
|
|
326
|
+
* @return {boolean} Whether the value holds more than one comma-separated entry.
|
|
327
|
+
*/
|
|
328
|
+
function hasCommaSeparatedEntries (value) {
|
|
329
|
+
let depth = 0;
|
|
330
|
+
for (const segment of splitValueSegments(value)) {
|
|
331
|
+
if (segment.isLiteral) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
for (const character of segment.text) {
|
|
335
|
+
if (character === '(') {
|
|
336
|
+
depth++;
|
|
337
|
+
}
|
|
338
|
+
if (character === ')') {
|
|
339
|
+
depth = Math.max(0, depth - 1);
|
|
340
|
+
}
|
|
341
|
+
if (character === ',' && depth === 0) {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Restores the whitespace that a math function requires before a `+` or a `-`
|
|
351
|
+
* operator. Both operators have to be surrounded by whitespace to be read as
|
|
352
|
+
* operators rather than as the sign of the term that follows, and the
|
|
353
|
+
* whitespace before one that trails a closing parenthesis is removed together
|
|
354
|
+
* with the rest of the parenthesis padding.
|
|
355
|
+
*
|
|
356
|
+
* @param {string} value The CSS value string with parenthesis padding removed.
|
|
357
|
+
* @return {string} The value with the operator separator restored.
|
|
358
|
+
*/
|
|
359
|
+
function restoreSpaceBeforeMathOperators (value) {
|
|
360
|
+
return replaceOutsideStringsAndUrls(value, (segment) => {
|
|
361
|
+
// Match a closing parenthesis immediately followed by a `+` or `-` operator
|
|
362
|
+
return segment.replace(/\)(?=[+-])/g, ') ');
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
217
366
|
/**
|
|
218
367
|
* Chooses the shortest valid representation for the path inside a `url(...)`
|
|
219
368
|
* token, weighing an unquoted form, an escaped single space, and a quoted form.
|
|
@@ -800,16 +949,77 @@ const TIME_PROPERTIES = new Set([
|
|
|
800
949
|
'animation-delay'
|
|
801
950
|
]);
|
|
802
951
|
|
|
952
|
+
/**
|
|
953
|
+
* Properties whose grammar delimits its own components, either with punctuation
|
|
954
|
+
* (the `/` and `,` of the `background`, `mask`, and `src` layers) or by taking
|
|
955
|
+
* nothing but functions (`transform`). Their components stay readable without
|
|
956
|
+
* the whitespace that follows a closing parenthesis, so that whitespace is left
|
|
957
|
+
* elided and each of them restores only the separators its grammar still needs.
|
|
958
|
+
*
|
|
959
|
+
* @type {Set<string>}
|
|
960
|
+
*/
|
|
961
|
+
const PUNCTUATED_COMPONENT_PROPERTIES = new Set([
|
|
962
|
+
'background',
|
|
963
|
+
'mask',
|
|
964
|
+
'src',
|
|
965
|
+
'transform'
|
|
966
|
+
]);
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Matches one offset of a position: an edge keyword or a numeric distance.
|
|
970
|
+
*
|
|
971
|
+
* @type {string}
|
|
972
|
+
*/
|
|
973
|
+
const POSITION_OFFSET_SOURCE = '(?:left|center|right|top|bottom|[+-]?(?:\\d+|\\d*\\.\\d+)(?:%|[a-z]+)?)';
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Matches a whole position component, which is one or two of those offsets.
|
|
977
|
+
*
|
|
978
|
+
* @type {string}
|
|
979
|
+
*/
|
|
980
|
+
const POSITION_COMPONENT_SOURCE = '(' + POSITION_OFFSET_SOURCE + '(?:\\s+' + POSITION_OFFSET_SOURCE + ')?)';
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* Matches a close-paren directly followed by a position that no slash follows,
|
|
984
|
+
* which is the position that needs its separator put back.
|
|
985
|
+
*
|
|
986
|
+
* @type {RegExp}
|
|
987
|
+
*/
|
|
988
|
+
const UNSEPARATED_IMAGE_POSITION_PATTERN = new RegExp('\\)' + POSITION_COMPONENT_SOURCE + '(?!\\/)', 'gi');
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Matches a close-paren separated from a position that a slash follows, where
|
|
992
|
+
* the slash already delimits the position from the size behind it.
|
|
993
|
+
*
|
|
994
|
+
* @type {RegExp}
|
|
995
|
+
*/
|
|
996
|
+
const SEPARATED_IMAGE_SIZE_POSITION_PATTERN = new RegExp('\\)\\s+' + POSITION_COMPONENT_SOURCE + '(?=\\/)', 'gi');
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Restores the separator between an image function and the position that
|
|
1000
|
+
* follows it. Both `background` and `mask` take a `<position> [ / <size> ]`
|
|
1001
|
+
* component after their image, and a bare position only reads as its own
|
|
1002
|
+
* component while whitespace separates it from the image function. A position
|
|
1003
|
+
* that a `/` follows needs no separator, since the slash delimits the pair.
|
|
1004
|
+
*
|
|
1005
|
+
* @param {string} value The layered image value, with the parenthesis padding removed.
|
|
1006
|
+
* @return {string} The value with the position separator restored.
|
|
1007
|
+
*/
|
|
1008
|
+
function restoreImagePositionSeparator (value) {
|
|
1009
|
+
const separated = value.replace(UNSEPARATED_IMAGE_POSITION_PATTERN, ') $1');
|
|
1010
|
+
return separated.replace(SEPARATED_IMAGE_SIZE_POSITION_PATTERN, ')$1');
|
|
1011
|
+
}
|
|
1012
|
+
|
|
803
1013
|
/**
|
|
804
1014
|
* Applies property-specific optimizations to a CSS value (transition, flex, font,
|
|
805
1015
|
* background, display, scale, border-radius, shorthand collapsing, etc.).
|
|
806
1016
|
*
|
|
807
|
-
* @param {string} val
|
|
808
|
-
* @param {string} property
|
|
809
|
-
* @param {boolean}
|
|
810
|
-
* @return {string}
|
|
1017
|
+
* @param {string} val The CSS value string after generic minification.
|
|
1018
|
+
* @param {string} property The CSS property name.
|
|
1019
|
+
* @param {boolean} allowsSeparatorElision Whether redundant separator whitespace may be removed.
|
|
1020
|
+
* @return {string} The value with property-specific optimizations applied.
|
|
811
1021
|
*/
|
|
812
|
-
function applyPropertyOptimizations (val, property,
|
|
1022
|
+
function applyPropertyOptimizations (val, property, allowsSeparatorElision) {
|
|
813
1023
|
if (property === 'font-weight' && isUnicodeCharset()) {
|
|
814
1024
|
// Replace font-weight keyword "bold" with its numeric equivalent
|
|
815
1025
|
val = val.replace(/\bbold\b/gi, '700');
|
|
@@ -971,16 +1181,19 @@ function applyPropertyOptimizations (val, property, allowsHexSpaceElision) {
|
|
|
971
1181
|
});
|
|
972
1182
|
|
|
973
1183
|
// Shorten all color tokens (second pass after property-specific color evaluations)
|
|
974
|
-
val = replaceOutsideStringsAndUrls(val,
|
|
1184
|
+
val = replaceOutsideStringsAndUrls(val, (segment) => {
|
|
1185
|
+
return shortenColorValues(segment, allowsSeparatorElision);
|
|
1186
|
+
});
|
|
975
1187
|
|
|
976
1188
|
// Remove space before hex colors (second pass after color evaluations)
|
|
977
|
-
if (
|
|
1189
|
+
if (allowsSeparatorElision) {
|
|
978
1190
|
val = elideSpaceBeforeHexColors(val);
|
|
979
1191
|
}
|
|
980
|
-
if (property
|
|
1192
|
+
if (!PUNCTUATED_COMPONENT_PROPERTIES.has(property)) {
|
|
981
1193
|
// Restore space after close-paren when followed by an alphanumeric, hash, or hyphen
|
|
982
1194
|
val = val.replace(/\)(?=[0-9a-zA-Z#-])/g, ') ');
|
|
983
1195
|
}
|
|
1196
|
+
val = restoreSpaceBeforeMathOperators(val);
|
|
984
1197
|
|
|
985
1198
|
if (property === 'font') {
|
|
986
1199
|
// Split font shorthand on whitespace
|
|
@@ -1014,10 +1227,11 @@ function applyPropertyOptimizations (val, property, allowsHexSpaceElision) {
|
|
|
1014
1227
|
if (normalized) {
|
|
1015
1228
|
val = normalized;
|
|
1016
1229
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1230
|
+
val = restoreImagePositionSeparator(val);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (property === 'mask') {
|
|
1234
|
+
val = restoreImagePositionSeparator(val);
|
|
1021
1235
|
}
|
|
1022
1236
|
|
|
1023
1237
|
if (property === 'border') {
|
|
@@ -1094,7 +1308,7 @@ function computeMinifiedValue (declaration) {
|
|
|
1094
1308
|
// Values assembled from already-minified longhands keep the separator spaces
|
|
1095
1309
|
// between their components, because those spaces delimit the shorthand's
|
|
1096
1310
|
// parts rather than the authored whitespace of a single written value.
|
|
1097
|
-
const
|
|
1311
|
+
const allowsSeparatorElision = !declaration.isAssembledShorthand;
|
|
1098
1312
|
|
|
1099
1313
|
if (typeof val === 'string') {
|
|
1100
1314
|
val = val.trim();
|
|
@@ -1138,21 +1352,26 @@ function computeMinifiedValue (declaration) {
|
|
|
1138
1352
|
}
|
|
1139
1353
|
|
|
1140
1354
|
val = lowercaseHexColors(val);
|
|
1141
|
-
if (
|
|
1355
|
+
if (allowsSeparatorElision) {
|
|
1142
1356
|
val = elideSpaceBeforeHexColors(val);
|
|
1143
1357
|
}
|
|
1144
1358
|
|
|
1145
1359
|
// Convert color functions to hex equivalents
|
|
1146
1360
|
val = convertColorsToHex(val);
|
|
1147
1361
|
|
|
1148
|
-
// Shorten all color tokens (hex and named) to their shortest representation
|
|
1149
|
-
|
|
1362
|
+
// Shorten all color tokens (hex and named) to their shortest representation.
|
|
1363
|
+
// An assembled shorthand keeps the whitespace between its components, so a
|
|
1364
|
+
// spelling of the same length saves it nothing and its components keep the
|
|
1365
|
+
// spelling they were written with.
|
|
1366
|
+
val = replaceOutsideStringsAndUrls(val, (segment) => {
|
|
1367
|
+
return shortenColorValues(segment, allowsSeparatorElision);
|
|
1368
|
+
});
|
|
1150
1369
|
|
|
1151
1370
|
// Collapse light-dark() when both normalized branches are identical
|
|
1152
1371
|
val = simplifyEquivalentLightDarkFunctions(val);
|
|
1153
1372
|
|
|
1154
1373
|
// Property-specific optimizations
|
|
1155
|
-
val = applyPropertyOptimizations(val, declaration.property,
|
|
1374
|
+
val = applyPropertyOptimizations(val, declaration.property, allowsSeparatorElision);
|
|
1156
1375
|
|
|
1157
1376
|
// Minify relative color syntax (identity resolution and whitespace collapsing)
|
|
1158
1377
|
val = minifyRelativeColorSyntax(val);
|
|
@@ -1169,6 +1388,23 @@ function computeMinifiedValue (declaration) {
|
|
|
1169
1388
|
val = optimizeUnicodeRange(val);
|
|
1170
1389
|
}
|
|
1171
1390
|
|
|
1391
|
+
// Every earlier pass reads the value with its component separators in place,
|
|
1392
|
+
// so the ones that turned out to be redundant are only dropped at the end.
|
|
1393
|
+
// The properties that punctuate their own components never got them back.
|
|
1394
|
+
const elidesRedundantSeparators = (
|
|
1395
|
+
typeof val === 'string' &&
|
|
1396
|
+
allowsSeparatorElision &&
|
|
1397
|
+
!PUNCTUATED_COMPONENT_PROPERTIES.has(declaration.property)
|
|
1398
|
+
);
|
|
1399
|
+
if (elidesRedundantSeparators) {
|
|
1400
|
+
val = elideSpaceAfterParentheses(val);
|
|
1401
|
+
// A value written as one entry is elided down to its tokens, while a
|
|
1402
|
+
// comma-separated list keeps the whitespace that groups each of its entries.
|
|
1403
|
+
if (!hasCommaSeparatedEntries(val)) {
|
|
1404
|
+
val = elideSpaceAfterStrings(val);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1172
1408
|
return val;
|
|
1173
1409
|
}
|
|
1174
1410
|
|
package/src/value/shared.js
CHANGED
|
@@ -143,6 +143,31 @@ function parseAngleToDegrees (angleToken) {
|
|
|
143
143
|
return parseFloat(match[1]) * ANGLE_UNIT_TO_DEGREES[unit];
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The substitution functions whose result is only known at computed-value time.
|
|
148
|
+
* Each of them may stand in for any number of components, so a shorthand part
|
|
149
|
+
* that holds one cannot be counted as the single value it looks like.
|
|
150
|
+
*
|
|
151
|
+
* @type {RegExp}
|
|
152
|
+
*/
|
|
153
|
+
const SUBSTITUTION_FUNCTION_PATTERN = /\b(?:var|env|attr|if)\(/i;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Reports whether a shorthand part stands in for an unknown number of
|
|
157
|
+
* components. `margin: 1px 1px` reduces to `margin: 1px`, but the same
|
|
158
|
+
* reduction across two `var()` references is unsafe: a custom property that
|
|
159
|
+
* expands to two values makes the doubled form a valid four-component margin,
|
|
160
|
+
* while the reduced form is a two-component one.
|
|
161
|
+
*
|
|
162
|
+
* @param {Array} parts The shorthand value strings to test.
|
|
163
|
+
* @return {boolean} Whether any part substitutes a value of unknown length.
|
|
164
|
+
*/
|
|
165
|
+
function hasSubstitutedParts (parts) {
|
|
166
|
+
return parts.some((part) => {
|
|
167
|
+
return SUBSTITUTION_FUNCTION_PATTERN.test(part);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
146
171
|
/**
|
|
147
172
|
* Collapses redundant CSS shorthand parts using the standard box-model
|
|
148
173
|
* reduction rules: 4-value → 3-value → 2-value → 1-value.
|
|
@@ -153,6 +178,9 @@ function parseAngleToDegrees (angleToken) {
|
|
|
153
178
|
* @return {Array} The same array, mutated with redundant entries removed.
|
|
154
179
|
*/
|
|
155
180
|
function collapseShorthandParts (parts) {
|
|
181
|
+
if (hasSubstitutedParts(parts)) {
|
|
182
|
+
return parts;
|
|
183
|
+
}
|
|
156
184
|
if (parts.length === 4 && parts[1] === parts[3]) {
|
|
157
185
|
parts.splice(3, 1);
|
|
158
186
|
}
|