@thejaredwilcurt/csslop 0.0.27 → 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 +2 -2
- package/src/declarations/border.js +268 -2
- package/src/declarations/config.js +3 -2
- package/src/declarations/merge.js +1 -0
- package/src/declarations/process.js +5 -1
- package/src/declarations/shorthand-values.js +17 -4
- 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",
|
|
@@ -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
|
},
|
|
@@ -3,14 +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';
|
|
9
14
|
import { collectDeclaredProperties } from './lookup.js';
|
|
10
15
|
import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
|
|
11
16
|
|
|
12
17
|
const BORDER_TRIO_PROPERTIES = ['border-width', 'border-style', 'border-color'];
|
|
13
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
|
+
|
|
14
72
|
/**
|
|
15
73
|
* Finds the index of the last declaration for a property, which is the one that
|
|
16
74
|
* wins the cascade within a rule.
|
|
@@ -126,4 +184,212 @@ function collapseBorderTrioWithPerEdgeColor (declarations, context) {
|
|
|
126
184
|
});
|
|
127
185
|
}
|
|
128
186
|
|
|
129
|
-
|
|
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
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'],
|
|
94
|
-
mask: ['mask-border', 'mask-border-source', 'mask-border-slice', 'mask-border-width', 'mask-border-outset', 'mask-border-repeat', 'mask-border-mode']
|
|
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
|
/**
|
|
@@ -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
|
};
|
|
@@ -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
|
|
@@ -406,6 +409,7 @@ function processDeclarations (declarations, context) {
|
|
|
406
409
|
result = mergeLonghandsIntoShorthands(result, context);
|
|
407
410
|
result = foldLonghandOverridesIntoShorthands(result, context);
|
|
408
411
|
result = hoistCssWideKeywordsIntoShorthands(result, context);
|
|
412
|
+
result = splitBorderEdgesIntoTrio(result);
|
|
409
413
|
result = collapseBorderTrioWithPerEdgeColor(result, context);
|
|
410
414
|
|
|
411
415
|
return orderDeclarations(result);
|
|
@@ -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/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
|
}
|