@thejaredwilcurt/csslop 0.0.47 → 0.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +4 -4
- package/src/declarations/css-wide-keywords.js +37 -1
- package/src/declarations/fold.js +13 -3
- package/src/declarations/process.js +60 -7
- package/src/index.js +8 -3
- package/src/parser/source-search.js +6 -0
- package/src/rules/custom-properties.js +19 -0
- package/src/rules/normalize.js +34 -1
- package/src/rules/optimize.js +422 -139
- package/src/rules/selectors.js +9 -0
- package/src/rules/stringify.js +138 -76
- package/src/utilities.js +0 -13
- package/src/value/minify.js +138 -37
- package/src/value/numbers.js +154 -0
- package/src/value/tokens.js +78 -20
package/src/rules/selectors.js
CHANGED
|
@@ -33,6 +33,11 @@ const COMPOUND_SELECTOR_BOUNDARIES = '>+~ \t\n\r\f';
|
|
|
33
33
|
* @return {number} The index of the next top-level occurrence, or -1 if none remains.
|
|
34
34
|
*/
|
|
35
35
|
function findNextFunctionCallOutsideStrings (text, functionCall, start) {
|
|
36
|
+
// A token that appears nowhere in the selector cannot appear at its top
|
|
37
|
+
// level either, and one search rules that out without walking every character
|
|
38
|
+
if (!text.toLowerCase().includes(functionCall)) {
|
|
39
|
+
return -1;
|
|
40
|
+
}
|
|
36
41
|
let index = start;
|
|
37
42
|
while (index < text.length) {
|
|
38
43
|
const character = text[index];
|
|
@@ -655,6 +660,10 @@ function readWholeSelectorIsParts (selector) {
|
|
|
655
660
|
* @return {Array} An array of one or more processed selector strings.
|
|
656
661
|
*/
|
|
657
662
|
function processIsSelector (selector, hasSiblingSelectors) {
|
|
663
|
+
// Every rewrite below starts from an `:is(`, so a selector without one is left as it is
|
|
664
|
+
if (!selector.toLowerCase().includes(':is(')) {
|
|
665
|
+
return [selector];
|
|
666
|
+
}
|
|
658
667
|
// Replace :is(:link,:visited) and :is(:visited,:link) with :any-link
|
|
659
668
|
selector = selector.replace(/:is\(:link,:visited\)/g, ':any-link');
|
|
660
669
|
selector = selector.replace(/:is\(:visited,:link\)/g, ':any-link');
|
package/src/rules/stringify.js
CHANGED
|
@@ -276,6 +276,143 @@ function stringifyAtRule (rule, context) {
|
|
|
276
276
|
const separator = minifiedPrelude ? ' ' : '';
|
|
277
277
|
return '@' + rule.name + separator + minifiedPrelude + '{' + body + '}';
|
|
278
278
|
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Rewrites `:nth-*()` pseudo-classes into their shortest equivalent forms.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} selector A selector with its whitespace already collapsed.
|
|
284
|
+
* @return {string} The selector with simplified `:nth-*()` pseudo-classes.
|
|
285
|
+
*/
|
|
286
|
+
function simplifyNthPseudoClasses (selector) {
|
|
287
|
+
let minified = selector;
|
|
288
|
+
// Simplify :nth-child(2n+1) to :nth-child(odd)
|
|
289
|
+
minified = minified.replace(/:nth-child\(2n\s*\+\s*1\)/g, ':nth-child(odd)');
|
|
290
|
+
// Simplify :nth-child(2n+0) to :nth-child(2n)
|
|
291
|
+
minified = minified.replace(/:nth-child\(2n\s*\+\s*0\)/g, ':nth-child(2n)');
|
|
292
|
+
// Simplify :nth-child(1n) to :nth-child(n)
|
|
293
|
+
minified = minified.replace(/:nth-child\(1n\)/g, ':nth-child(n)');
|
|
294
|
+
// Remove unnecessary leading + sign from :nth-child()
|
|
295
|
+
minified = minified.replace(/:nth-child\(\+\s*(\d+)\)/g, ':nth-child($1)');
|
|
296
|
+
// Simplify :nth-child(0n+N) to :nth-child(N)
|
|
297
|
+
minified = minified.replace(/:nth-child\(0n\s*\+\s*(\d+)\)/g, ':nth-child($1)');
|
|
298
|
+
// Replace :nth-child(1) with :first-child
|
|
299
|
+
minified = minified.replace(/:nth-child\(1\)/g, ':first-child');
|
|
300
|
+
// Replace :nth-last-child(1) with :last-child
|
|
301
|
+
minified = minified.replace(/:nth-last-child\(1\)/g, ':last-child');
|
|
302
|
+
// Replace :nth-of-type(1) with :first-of-type
|
|
303
|
+
minified = minified.replace(/:nth-of-type\(1\)/g, ':first-of-type');
|
|
304
|
+
// Replace :nth-last-of-type(1) with :last-of-type
|
|
305
|
+
minified = minified.replace(/:nth-last-of-type\(1\)/g, ':last-of-type');
|
|
306
|
+
return minified;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Writes each attribute selector's value quoted or escaped, whichever is
|
|
311
|
+
* shorter, and removes the whitespace inside its brackets.
|
|
312
|
+
*
|
|
313
|
+
* @param {string} selector A selector with its whitespace already collapsed.
|
|
314
|
+
* @return {string} The selector with minified attribute selectors.
|
|
315
|
+
*/
|
|
316
|
+
function minifyAttributeSelectors (selector) {
|
|
317
|
+
let minified = selector;
|
|
318
|
+
// Minify double-quoted attribute selectors: remove inner whitespace and escape when shorter
|
|
319
|
+
minified = minified.replace(/\[\s*([^=]+)\s*=\s*"(.*?)"\s*\]/g, (match, attribute, value) => {
|
|
320
|
+
// Escape special characters that require quoting (spaces, #, ., :, /, ;), and compare lengths
|
|
321
|
+
let escaped = value.replace(/([ #.:/;])/g, '\\$1');
|
|
322
|
+
if (escaped.length < value.length + 2) {
|
|
323
|
+
return '[' + attribute + '=' + escaped + ']';
|
|
324
|
+
}
|
|
325
|
+
return '[' + attribute + '="' + value + '"]';
|
|
326
|
+
});
|
|
327
|
+
// Minify single-quoted attribute selectors: remove inner whitespace and escape when shorter
|
|
328
|
+
minified = minified.replace(/\[\s*([^=]+)\s*=\s*'(.*?)'\s*\]/g, (match, attribute, value) => {
|
|
329
|
+
// Escape special characters that require quoting (spaces, #, ., :, /, ;), and compare lengths
|
|
330
|
+
let escaped = value.replace(/([ #.:/;])/g, '\\$1');
|
|
331
|
+
if (escaped.length < value.length + 2) {
|
|
332
|
+
return '[' + attribute + '=' + escaped + ']';
|
|
333
|
+
}
|
|
334
|
+
return '[' + attribute + '="' + value + '"]';
|
|
335
|
+
});
|
|
336
|
+
// Minify unquoted attribute selectors: quote when unescaping produces a shorter result
|
|
337
|
+
minified = minified.replace(/\[\s*([^=]+)\s*=\s*([^"'].*?)\s*\]/g, (match, attribute, value) => {
|
|
338
|
+
// Unescape special characters (spaces, #, ., :, /, ;) and compare with quoted form
|
|
339
|
+
let unescaped = value.replace(/\\([ #.:/;])/g, '$1');
|
|
340
|
+
if (unescaped.length + 2 < value.length) {
|
|
341
|
+
return '[' + attribute + '="' + unescaped + '"]';
|
|
342
|
+
}
|
|
343
|
+
return '[' + attribute + '=' + value + ']';
|
|
344
|
+
});
|
|
345
|
+
return minified;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Rewrites `:not()` pseudo-classes that negate a pseudo-class with a direct
|
|
350
|
+
* opposite, such as `:not(:enabled)`, into that opposite.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} selector A selector with its whitespace already collapsed.
|
|
353
|
+
* @return {string} The selector with simplified negations.
|
|
354
|
+
*/
|
|
355
|
+
function simplifyNegatedPseudoClasses (selector) {
|
|
356
|
+
let minified = selector;
|
|
357
|
+
// Minify logical combinations
|
|
358
|
+
// Replace :not(:invalid) with :valid, only directly after a form element type selector
|
|
359
|
+
minified = minified.replace(/(?<=\b(?:button|fieldset|form|input|select|textarea)):not\(:invalid\)/g, ':valid');
|
|
360
|
+
// Replace :not(:dir(ltr)) with :dir(rtl)
|
|
361
|
+
minified = minified.replace(/:not\(:dir\(ltr\)\)/g, ':dir(rtl)');
|
|
362
|
+
// Replace a double negation, :not(:not(X)), with the X it wraps
|
|
363
|
+
minified = minified.replace(/:not\(:not\((.*?)\)\)/g, '$1');
|
|
364
|
+
// Replace :not(:enabled) with :disabled
|
|
365
|
+
minified = minified.replace(/:not\(:enabled\)/g, ':disabled');
|
|
366
|
+
// Replace :not(:required) with :optional
|
|
367
|
+
minified = minified.replace(/:not\(:required\)/g, ':optional');
|
|
368
|
+
// Match a link element type selector at the start or after a combinator, with any attribute selectors and an optional :where( wrapper, followed by :not(:link)
|
|
369
|
+
minified = minified.replace(/(^|[\s,>+~])(a|area|link)(?:\[.*?\])*(?::where\()?:not\(:link\)\)?/g, (match) => {
|
|
370
|
+
return match.replace(':not(:link)', ':visited');
|
|
371
|
+
});
|
|
372
|
+
return minified;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Minifies a single selector's spacing, pseudo-classes, pseudo-elements, and
|
|
377
|
+
* attribute selectors. Most selectors hold none of the patterns a group of
|
|
378
|
+
* rewrites looks for, so each group only runs when its marker text is present.
|
|
379
|
+
*
|
|
380
|
+
* @param {string} selector A single selector from a rule's selector list.
|
|
381
|
+
* @return {string} The minified selector.
|
|
382
|
+
*/
|
|
383
|
+
function minifySelector (selector) {
|
|
384
|
+
let minified = unescapeSelector(selector);
|
|
385
|
+
// Collapse whitespace to single space
|
|
386
|
+
minified = minified.replace(/\s+/g, ' ');
|
|
387
|
+
// Strip whitespace around selector combinators and commas
|
|
388
|
+
minified = minified.replace(/\s*([,>+~])\s*/g, '$1');
|
|
389
|
+
// Strip whitespace inside parentheses for pseudo-class arguments
|
|
390
|
+
minified = minified.replace(/\(\s+/g, '(').replace(/\s+\)/g, ')');
|
|
391
|
+
if (minified.includes(':nth-')) {
|
|
392
|
+
minified = simplifyNthPseudoClasses(minified);
|
|
393
|
+
}
|
|
394
|
+
if (minified.includes('::')) {
|
|
395
|
+
// Convert double-colon pseudo-elements to single-colon legacy form
|
|
396
|
+
minified = minified.replace(/::before/g, ':before');
|
|
397
|
+
minified = minified.replace(/::after/g, ':after');
|
|
398
|
+
}
|
|
399
|
+
if (minified.includes('*')) {
|
|
400
|
+
// Strip redundant universal selector `*` when it precedes an ID, class, or attribute selector
|
|
401
|
+
minified = minified.replace(/\*([#.[])/g, '$1');
|
|
402
|
+
}
|
|
403
|
+
if (minified.includes('[') && minified.includes('=')) {
|
|
404
|
+
minified = minifyAttributeSelectors(minified);
|
|
405
|
+
}
|
|
406
|
+
if (minified.includes(':not(')) {
|
|
407
|
+
minified = simplifyNegatedPseudoClasses(minified);
|
|
408
|
+
}
|
|
409
|
+
// Remove redundant leading "& " nesting selector
|
|
410
|
+
minified = minified.replace(/^& /, '');
|
|
411
|
+
minified = mergeAdjacentWherePseudoClasses(minified);
|
|
412
|
+
minified = minifyLanguageSelector(minified);
|
|
413
|
+
return minified;
|
|
414
|
+
}
|
|
415
|
+
|
|
279
416
|
/**
|
|
280
417
|
* Converts a parsed CSS AST rule node into a minified CSS string, dispatching to specialized handlers for each rule type including selectors, `@media`, `@keyframes`, `@layer`, and other at-rules.
|
|
281
418
|
*
|
|
@@ -305,82 +442,7 @@ function stringifyRule (rule, context) {
|
|
|
305
442
|
if (rule.selectors?.length) {
|
|
306
443
|
let uniqueSelectors = [...new Set(rule.selectors)];
|
|
307
444
|
// Minify spacing within selectors (e.g. inside :is(), :where(), etc)
|
|
308
|
-
uniqueSelectors = uniqueSelectors.map(
|
|
309
|
-
let minified = unescapeSelector(selector);
|
|
310
|
-
// Collapse whitespace to single space
|
|
311
|
-
minified = minified.replace(/\s+/g, ' ');
|
|
312
|
-
// Strip whitespace around selector combinators and commas
|
|
313
|
-
minified = minified.replace(/\s*([,>+~])\s*/g, '$1');
|
|
314
|
-
// Strip whitespace inside parentheses for pseudo-class arguments
|
|
315
|
-
minified = minified.replace(/\(\s+/g, '(').replace(/\s+\)/g, ')');
|
|
316
|
-
// Simplify :nth-child(2n+1) to :nth-child(odd)
|
|
317
|
-
minified = minified.replace(/:nth-child\(2n\s*\+\s*1\)/g, ':nth-child(odd)');
|
|
318
|
-
// Simplify :nth-child(2n+0) to :nth-child(2n)
|
|
319
|
-
minified = minified.replace(/:nth-child\(2n\s*\+\s*0\)/g, ':nth-child(2n)');
|
|
320
|
-
// Simplify :nth-child(1n) to :nth-child(n)
|
|
321
|
-
minified = minified.replace(/:nth-child\(1n\)/g, ':nth-child(n)');
|
|
322
|
-
// Remove unnecessary leading + sign from :nth-child()
|
|
323
|
-
minified = minified.replace(/:nth-child\(\+\s*(\d+)\)/g, ':nth-child($1)');
|
|
324
|
-
// Simplify :nth-child(0n+N) to :nth-child(N)
|
|
325
|
-
minified = minified.replace(/:nth-child\(0n\s*\+\s*(\d+)\)/g, ':nth-child($1)');
|
|
326
|
-
// Replace :nth-child(1) with :first-child
|
|
327
|
-
minified = minified.replace(/:nth-child\(1\)/g, ':first-child');
|
|
328
|
-
// Replace :nth-last-child(1) with :last-child
|
|
329
|
-
minified = minified.replace(/:nth-last-child\(1\)/g, ':last-child');
|
|
330
|
-
// Replace :nth-of-type(1) with :first-of-type
|
|
331
|
-
minified = minified.replace(/:nth-of-type\(1\)/g, ':first-of-type');
|
|
332
|
-
// Replace :nth-last-of-type(1) with :last-of-type
|
|
333
|
-
minified = minified.replace(/:nth-last-of-type\(1\)/g, ':last-of-type');
|
|
334
|
-
// Convert double-colon pseudo-elements to single-colon legacy form
|
|
335
|
-
minified = minified.replace(/::before/g, ':before');
|
|
336
|
-
minified = minified.replace(/::after/g, ':after');
|
|
337
|
-
|
|
338
|
-
// Strip redundant universal selector `*` when it precedes an ID, class, or attribute selector
|
|
339
|
-
minified = minified.replace(/\*([#.[])/g, '$1');
|
|
340
|
-
|
|
341
|
-
// Minify double-quoted attribute selectors: remove inner whitespace and escape when shorter
|
|
342
|
-
minified = minified.replace(/\[\s*([^=]+)\s*=\s*"(.*?)"\s*\]/g, (match, attribute, value) => {
|
|
343
|
-
// Escape special characters that require quoting (spaces, #, ., :, /, ;), and compare lengths
|
|
344
|
-
let escaped = value.replace(/([ #.:/;])/g, '\\$1');
|
|
345
|
-
if (escaped.length < value.length + 2) {
|
|
346
|
-
return '[' + attribute + '=' + escaped + ']';
|
|
347
|
-
}
|
|
348
|
-
return '[' + attribute + '="' + value + '"]';
|
|
349
|
-
});
|
|
350
|
-
// Minify single-quoted attribute selectors: remove inner whitespace and escape when shorter
|
|
351
|
-
minified = minified.replace(/\[\s*([^=]+)\s*=\s*'(.*?)'\s*\]/g, (match, attribute, value) => {
|
|
352
|
-
// Escape special characters that require quoting (spaces, #, ., :, /, ;), and compare lengths
|
|
353
|
-
let escaped = value.replace(/([ #.:/;])/g, '\\$1');
|
|
354
|
-
if (escaped.length < value.length + 2) {
|
|
355
|
-
return '[' + attribute + '=' + escaped + ']';
|
|
356
|
-
}
|
|
357
|
-
return '[' + attribute + '="' + value + '"]';
|
|
358
|
-
});
|
|
359
|
-
// Minify unquoted attribute selectors: quote when unescaping produces a shorter result
|
|
360
|
-
minified = minified.replace(/\[\s*([^=]+)\s*=\s*([^"'].*?)\s*\]/g, (match, attribute, value) => {
|
|
361
|
-
// Unescape special characters (spaces, #, ., :, /, ;) and compare with quoted form
|
|
362
|
-
let unescaped = value.replace(/\\([ #.:/;])/g, '$1');
|
|
363
|
-
if (unescaped.length + 2 < value.length) {
|
|
364
|
-
return '[' + attribute + '="' + unescaped + '"]';
|
|
365
|
-
}
|
|
366
|
-
return '[' + attribute + '=' + value + ']';
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// Minify logical combinations
|
|
370
|
-
minified = minified.replace(/(?<=\b(?:button|fieldset|form|input|select|textarea)):not\(:invalid\)/g, ':valid');
|
|
371
|
-
minified = minified.replace(/:not\(:dir\(ltr\)\)/g, ':dir(rtl)');
|
|
372
|
-
minified = minified.replace(/:not\(:not\((.*?)\)\)/g, '$1');
|
|
373
|
-
minified = minified.replace(/:not\(:enabled\)/g, ':disabled');
|
|
374
|
-
minified = minified.replace(/:not\(:required\)/g, ':optional');
|
|
375
|
-
minified = minified.replace(/(^|[\s,>+~])(a|area|link)(?:\[.*?\])*(?::where\()?:not\(:link\)\)?/g, (match) => {
|
|
376
|
-
return match.replace(':not(:link)', ':visited');
|
|
377
|
-
});
|
|
378
|
-
// Remove redundant leading "& " nesting selector
|
|
379
|
-
minified = minified.replace(/^& /, '');
|
|
380
|
-
minified = mergeAdjacentWherePseudoClasses(minified);
|
|
381
|
-
minified = minifyLanguageSelector(minified);
|
|
382
|
-
return minified;
|
|
383
|
-
});
|
|
445
|
+
uniqueSelectors = uniqueSelectors.map(minifySelector);
|
|
384
446
|
uniqueSelectors = combineLegacyAnySelectors(uniqueSelectors);
|
|
385
447
|
// When this rule is a nesting parent, its whole selector list is treated
|
|
386
448
|
// as :is() for the children's specificity, so a top-level :is() can be
|
package/src/utilities.js
CHANGED
|
@@ -19,19 +19,6 @@ function resolveUnicodeEscape (hex) {
|
|
|
19
19
|
return String.fromCodePoint(codePoint);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Escapes special regex metacharacters in a string so it can be safely used
|
|
24
|
-
* as a literal pattern in a RegExp constructor.
|
|
25
|
-
*
|
|
26
|
-
* @param {string} input The string to escape.
|
|
27
|
-
* @return {string} The escaped string safe for use in a RegExp.
|
|
28
|
-
*/
|
|
29
|
-
function escapeRegexString (input) {
|
|
30
|
-
// Escape all regex metacharacters: . * + ? ^ $ { } ( ) | [ ] backslash
|
|
31
|
-
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
22
|
export {
|
|
35
|
-
escapeRegexString,
|
|
36
23
|
resolveUnicodeEscape
|
|
37
24
|
};
|
package/src/value/minify.js
CHANGED
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
findMatchingParenthesis,
|
|
8
8
|
skipParenthesizedGroup,
|
|
9
9
|
skipQuotedString,
|
|
10
|
+
splitTopLevelComponents,
|
|
10
11
|
startsUrlToken
|
|
11
12
|
} from '../parser/source-search.js';
|
|
13
|
+
import { tokenizeCssValue } from '../parser/tokenizer.js';
|
|
12
14
|
import { resolveUnicodeEscape } from '../utilities.js';
|
|
13
15
|
|
|
14
16
|
import { evaluateColorMix } from './color-mix.js';
|
|
@@ -30,6 +32,12 @@ import {
|
|
|
30
32
|
simplifyStandaloneCalc
|
|
31
33
|
} from './math.js';
|
|
32
34
|
import { namedColors } from './named-colors.js';
|
|
35
|
+
import {
|
|
36
|
+
formatShortestNumber,
|
|
37
|
+
parseNumericParts,
|
|
38
|
+
scaleByPowerOfTen,
|
|
39
|
+
shortenNumericText
|
|
40
|
+
} from './numbers.js';
|
|
33
41
|
import { isQuotesNoneEquivalent } from './quotes.js';
|
|
34
42
|
import {
|
|
35
43
|
evaluateRelativeColor,
|
|
@@ -918,26 +926,89 @@ function convertBackgroundPositionKeywords (value) {
|
|
|
918
926
|
}
|
|
919
927
|
|
|
920
928
|
/**
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
* and values at or below 99ms stay in milliseconds (shorter representation).
|
|
929
|
+
* The power of ten that separates a time in seconds from the same time in
|
|
930
|
+
* milliseconds, keyed by the lowercased time unit.
|
|
924
931
|
*
|
|
925
|
-
* @
|
|
926
|
-
* @return {string} The value with eligible ms times converted to seconds.
|
|
932
|
+
* @type {{[unit: string]: number}}
|
|
927
933
|
*/
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
934
|
+
const TIME_UNIT_POWERS = {
|
|
935
|
+
s: 0,
|
|
936
|
+
ms: -3
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Rewrites one time token in whichever of seconds or milliseconds spells it
|
|
941
|
+
* shortest, scientific notation included. Every conversion moves the exponent
|
|
942
|
+
* of the exact digits, so no precision is lost however large the time is.
|
|
943
|
+
* Seconds win a tie, and a zero time becomes `0s`.
|
|
944
|
+
*
|
|
945
|
+
* @param {string} text The text of a dimension token.
|
|
946
|
+
* @return {string} The shortest spelling of the same time.
|
|
947
|
+
*/
|
|
948
|
+
function shortenTimeToken (text) {
|
|
949
|
+
const parts = parseNumericParts(text);
|
|
950
|
+
const writtenUnit = parts?.unit.toLowerCase();
|
|
951
|
+
if (!parts || !(writtenUnit in TIME_UNIT_POWERS)) {
|
|
952
|
+
return text;
|
|
953
|
+
}
|
|
954
|
+
if (!parts.digits) {
|
|
955
|
+
return '0s';
|
|
956
|
+
}
|
|
957
|
+
const writtenSpelling = shortenNumericText(text, true);
|
|
958
|
+
const secondsSpelling = formatShortestNumber(
|
|
959
|
+
scaleByPowerOfTen(parts, TIME_UNIT_POWERS[writtenUnit], 's'),
|
|
960
|
+
true
|
|
961
|
+
);
|
|
962
|
+
const millisecondsSpelling = formatShortestNumber(
|
|
963
|
+
scaleByPowerOfTen(parts, TIME_UNIT_POWERS[writtenUnit] - TIME_UNIT_POWERS.ms, 'ms'),
|
|
964
|
+
true
|
|
965
|
+
);
|
|
966
|
+
// Seconds are the conventional unit, so they win a tie with milliseconds
|
|
967
|
+
const convertedSpelling = millisecondsSpelling.length < secondsSpelling.length ? millisecondsSpelling : secondsSpelling;
|
|
968
|
+
return convertedSpelling.length <= writtenSpelling.length ? convertedSpelling : writtenSpelling;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Reports whether rewriting a number keeps its numeric type. A number written
|
|
973
|
+
* with a decimal point or an exponent has the `<number>` type, while one
|
|
974
|
+
* written with digits alone is an `<integer>`. A custom property can be
|
|
975
|
+
* substituted into, or registered with, a grammar that tells the two apart, so
|
|
976
|
+
* `100.0` cannot become `100` there.
|
|
977
|
+
*
|
|
978
|
+
* @param {string} originalNumber The number as it was written.
|
|
979
|
+
* @param {string} rewrittenNumber The number as it would be rewritten.
|
|
980
|
+
* @return {boolean} True when both spellings have the same type.
|
|
981
|
+
*/
|
|
982
|
+
function keepsNumberType (originalNumber, rewrittenNumber) {
|
|
983
|
+
return isIntegerSpelling(originalNumber) === isIntegerSpelling(rewrittenNumber);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Reports whether a number is spelled as an `<integer>`: digits with an
|
|
988
|
+
* optional sign, and no decimal point or exponent.
|
|
989
|
+
*
|
|
990
|
+
* @param {string} number The number as written.
|
|
991
|
+
* @return {boolean} True when the spelling is an integer.
|
|
992
|
+
*/
|
|
993
|
+
function isIntegerSpelling (number) {
|
|
994
|
+
// An optional sign followed by nothing but digits
|
|
995
|
+
return /^[+-]?\d+$/.test(number);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Converts each time in a value between seconds and milliseconds, whichever
|
|
1000
|
+
* is the shorter string.
|
|
1001
|
+
*
|
|
1002
|
+
* @param {string} value The CSS value string potentially containing time values.
|
|
1003
|
+
* @return {string} The value with each time in its shortest unit.
|
|
1004
|
+
*/
|
|
1005
|
+
function convertTimesToShortestUnit (value) {
|
|
1006
|
+
return tokenizeCssValue(value).map((token) => {
|
|
1007
|
+
if (token.type !== 'dimension') {
|
|
1008
|
+
return token.text;
|
|
938
1009
|
}
|
|
939
|
-
return
|
|
940
|
-
});
|
|
1010
|
+
return shortenTimeToken(token.text);
|
|
1011
|
+
}).join('');
|
|
941
1012
|
}
|
|
942
1013
|
|
|
943
1014
|
/**
|
|
@@ -1304,7 +1375,7 @@ function applyPropertyOptimizations (val, property, urlTokens) {
|
|
|
1304
1375
|
|
|
1305
1376
|
// Convert ms to s for time-related properties when the seconds form is shorter
|
|
1306
1377
|
if (TIME_PROPERTIES.has(property)) {
|
|
1307
|
-
val =
|
|
1378
|
+
val = convertTimesToShortestUnit(val);
|
|
1308
1379
|
}
|
|
1309
1380
|
|
|
1310
1381
|
// Transition: remove " 0s" duration (transition: all 0s -> transition: all)
|
|
@@ -1536,7 +1607,12 @@ function applyPropertyOptimizations (val, property, urlTokens) {
|
|
|
1536
1607
|
// Shorthands: margin, padding, border-width, border-style, border-color, inset
|
|
1537
1608
|
// Check if property supports box-model shorthand collapsing (4 → 3 → 2 → 1 values)
|
|
1538
1609
|
if (/^(margin|padding|inset|border-width|border-style|border-color|gap|overflow)$/.test(property)) {
|
|
1539
|
-
|
|
1610
|
+
// Split components instead of splitting on spaces, so values that legally
|
|
1611
|
+
// touch, like the adjacent hex colors of `#ddd#ddd`, count separately
|
|
1612
|
+
val = collapseShorthandParts(splitTopLevelComponents(val))
|
|
1613
|
+
.join(' ')
|
|
1614
|
+
// A hash token may touch the component before it, so the separator space is not needed
|
|
1615
|
+
.replace(/ #/g, '#');
|
|
1540
1616
|
}
|
|
1541
1617
|
|
|
1542
1618
|
if (property === 'hyphenate-limit-chars') {
|
|
@@ -1616,12 +1692,17 @@ function computeMinifiedValue (declaration) {
|
|
|
1616
1692
|
// character budget a number is written within
|
|
1617
1693
|
if (/^[+-]?(?:\d+|\d*\.\d+)([a-z%]+)?$/i.test(val)) {
|
|
1618
1694
|
const [, rawNumber, rawUnit = ''] = val.match(/^([+-]?(?:\d+|\d*\.\d+))([a-z%]+)?$/i);
|
|
1619
|
-
const
|
|
1695
|
+
const isCustom = isCustomProperty(declaration.property);
|
|
1696
|
+
const scientificValue = isCustom ? val : shortenDimensionNumbers(val, declaration.property);
|
|
1620
1697
|
const roundedNumber = formatResolvedNumber(rawNumber);
|
|
1621
1698
|
const losesPrecision = Number(roundedNumber) !== Number(rawNumber);
|
|
1622
1699
|
// Preserve tiny dimensions whose significant digits would otherwise be
|
|
1623
1700
|
// rounded away before their shorter exponent form can be considered.
|
|
1624
|
-
|
|
1701
|
+
if (losesPrecision && scientificValue !== val) {
|
|
1702
|
+
val = scientificValue;
|
|
1703
|
+
} else if (!isCustom || keepsNumberType(rawNumber, roundedNumber)) {
|
|
1704
|
+
val = roundedNumber + rawUnit;
|
|
1705
|
+
}
|
|
1625
1706
|
}
|
|
1626
1707
|
|
|
1627
1708
|
val = lowercaseHexColors(val);
|
|
@@ -1670,7 +1751,7 @@ function computeMinifiedValue (declaration) {
|
|
|
1670
1751
|
!isCustomProperty(declaration.property)
|
|
1671
1752
|
);
|
|
1672
1753
|
if (elidesRedundantSeparators) {
|
|
1673
|
-
val = shortenDimensionNumbers(val);
|
|
1754
|
+
val = shortenDimensionNumbers(val, declaration.property);
|
|
1674
1755
|
// A property that punctuates its own components already had the whitespace
|
|
1675
1756
|
// after its closing parentheses taken out, and only the separators its
|
|
1676
1757
|
// grammar still needs put back, so those are the ones left alone here.
|
|
@@ -1685,12 +1766,25 @@ function computeMinifiedValue (declaration) {
|
|
|
1685
1766
|
* Memoizes minified values for the current stylesheet. Minification runs many
|
|
1686
1767
|
* passes over the same declarations (deduplication, shorthand assembly,
|
|
1687
1768
|
* CSS-wide keyword hoisting, stringification), and the result only depends on
|
|
1688
|
-
* the declaration
|
|
1769
|
+
* the declaration's property, its value, and whether it is an assembled
|
|
1770
|
+
* shorthand, so each distinct
|
|
1689
1771
|
* property/value pair is minified once per stylesheet.
|
|
1690
1772
|
*
|
|
1691
|
-
*
|
|
1773
|
+
* Values are filed under their property first, and assembled shorthands are
|
|
1774
|
+
* kept apart from authored declarations, so a lookup reuses the property and
|
|
1775
|
+
* value strings as they are instead of building a combined key on every call.
|
|
1776
|
+
*
|
|
1777
|
+
* @type {Map<string, Map<string, string>>}
|
|
1778
|
+
*/
|
|
1779
|
+
const minifiedValuesByProperty = new Map();
|
|
1780
|
+
|
|
1781
|
+
/**
|
|
1782
|
+
* The memoized values of shorthands the minifier assembled itself, filed the
|
|
1783
|
+
* same way as `minifiedValuesByProperty`.
|
|
1784
|
+
*
|
|
1785
|
+
* @type {Map<string, Map<string, string>>}
|
|
1692
1786
|
*/
|
|
1693
|
-
const
|
|
1787
|
+
const assembledMinifiedValuesByProperty = new Map();
|
|
1694
1788
|
|
|
1695
1789
|
/**
|
|
1696
1790
|
* Discards every memoized value. The minifier calls this at the start of each
|
|
@@ -1698,20 +1792,27 @@ const minifiedValueCache = new Map();
|
|
|
1698
1792
|
* the cache should not outlive the pass that filled it.
|
|
1699
1793
|
*/
|
|
1700
1794
|
function clearMinifiedValueCache () {
|
|
1701
|
-
|
|
1795
|
+
minifiedValuesByProperty.clear();
|
|
1796
|
+
assembledMinifiedValuesByProperty.clear();
|
|
1702
1797
|
}
|
|
1703
1798
|
|
|
1704
1799
|
/**
|
|
1705
|
-
*
|
|
1706
|
-
*
|
|
1707
|
-
* value, so it safely delimits the parts.
|
|
1800
|
+
* Finds the memoized values for a declaration's property, creating the empty
|
|
1801
|
+
* collection the first time the property is seen.
|
|
1708
1802
|
*
|
|
1709
|
-
* @param {object}
|
|
1710
|
-
* @return {string}
|
|
1803
|
+
* @param {object} declaration The CSS declaration object.
|
|
1804
|
+
* @return {Map<string, string>} Map of value to its minified form for that property.
|
|
1711
1805
|
*/
|
|
1712
|
-
function
|
|
1713
|
-
const
|
|
1714
|
-
|
|
1806
|
+
function getMinifiedValuesForProperty (declaration) {
|
|
1807
|
+
const valuesByProperty = declaration.isAssembledShorthand ?
|
|
1808
|
+
assembledMinifiedValuesByProperty :
|
|
1809
|
+
minifiedValuesByProperty;
|
|
1810
|
+
let minifiedValues = valuesByProperty.get(declaration.property);
|
|
1811
|
+
if (!minifiedValues) {
|
|
1812
|
+
minifiedValues = new Map();
|
|
1813
|
+
valuesByProperty.set(declaration.property, minifiedValues);
|
|
1814
|
+
}
|
|
1815
|
+
return minifiedValues;
|
|
1715
1816
|
}
|
|
1716
1817
|
|
|
1717
1818
|
/**
|
|
@@ -1727,13 +1828,13 @@ function minifyValue (declaration) {
|
|
|
1727
1828
|
if (typeof declaration.value !== 'string') {
|
|
1728
1829
|
return computeMinifiedValue(declaration);
|
|
1729
1830
|
}
|
|
1730
|
-
const
|
|
1731
|
-
const cachedValue =
|
|
1831
|
+
const minifiedValues = getMinifiedValuesForProperty(declaration);
|
|
1832
|
+
const cachedValue = minifiedValues.get(declaration.value);
|
|
1732
1833
|
if (cachedValue !== undefined) {
|
|
1733
1834
|
return cachedValue;
|
|
1734
1835
|
}
|
|
1735
1836
|
const minifiedValue = computeMinifiedValue(declaration);
|
|
1736
|
-
|
|
1837
|
+
minifiedValues.set(declaration.value, minifiedValue);
|
|
1737
1838
|
return minifiedValue;
|
|
1738
1839
|
}
|
|
1739
1840
|
|