@thejaredwilcurt/csslop 0.0.46 → 0.0.48
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 +5 -5
- package/src/declarations/border.js +1 -1
- package/src/rules/custom-properties.js +19 -0
- package/src/rules/normalize.js +34 -1
- package/src/rules/stringify.js +57 -8
- package/src/value/math-expression.js +67 -14
- package/src/value/math.js +63 -3
- package/src/value/minify.js +135 -22
- package/src/value/numbers.js +154 -0
- package/src/value/tokens.js +78 -20
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.48",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"prestart": "node ./scripts/pre/start.js",
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"eslint-config-tjw-import-x": "^1.0.1",
|
|
36
36
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
37
37
|
"eslint-plugin-import-x": "^4.17.0",
|
|
38
|
-
"eslint-plugin-jsdoc": "^65.4.
|
|
38
|
+
"eslint-plugin-jsdoc": "^65.4.2",
|
|
39
39
|
"fflate": "^0.8.3",
|
|
40
40
|
"globals": "^17.13.0",
|
|
41
41
|
"pretty-ms": "^10.0.0",
|
|
42
42
|
"real-world-css-libraries": "^1.0.8",
|
|
43
|
-
"vite": "^8.3.
|
|
43
|
+
"vite": "^8.3.5"
|
|
44
44
|
},
|
|
45
45
|
"author": "The Jared Wilcurt",
|
|
46
46
|
"homepage": "https://github.com/TheJaredWilcurt/csslop#readme",
|
|
@@ -57,11 +57,11 @@
|
|
|
57
57
|
"devEngines": {
|
|
58
58
|
"runtime": {
|
|
59
59
|
"name": "node",
|
|
60
|
-
"version": "26.
|
|
60
|
+
"version": "26.13.0"
|
|
61
61
|
},
|
|
62
62
|
"packageManager": {
|
|
63
63
|
"name": "npm",
|
|
64
|
-
"version": "12.2.
|
|
64
|
+
"version": "12.2.1"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -182,7 +182,7 @@ function collapseBorderTrioWithPerEdgeOverrides (declarations, context) {
|
|
|
182
182
|
const borderValue = buildBorderShorthandValue(
|
|
183
183
|
widthComponents.length === 1 ? width : '',
|
|
184
184
|
styleComponents.length === 1 ? style : '',
|
|
185
|
-
colorComponents[0]
|
|
185
|
+
colorComponents.length === 1 ? colorComponents[0] : ''
|
|
186
186
|
) + importantSuffix;
|
|
187
187
|
const overrides = BORDER_TRIO_PROPERTIES.flatMap((property, index) => {
|
|
188
188
|
if (componentsByProperty[index].length === 1) {
|
|
@@ -76,6 +76,25 @@ function shortenNumericToken (tokenText) {
|
|
|
76
76
|
if (shortened.includes('e')) {
|
|
77
77
|
return tokenText;
|
|
78
78
|
}
|
|
79
|
+
return preserveNumberType(shortened, unit);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Keeps a unitless number that was written with a decimal point typed as a
|
|
84
|
+
* `<number>`. Shedding its zeros could leave only digits, which reads as an
|
|
85
|
+
* `<integer>`, and a custom property substituted into, or registered with, an
|
|
86
|
+
* `<integer>` grammar would then accept a value it used to reject.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} shortened The shortened number, without its unit.
|
|
89
|
+
* @param {string} unit The unit written after the number, if any.
|
|
90
|
+
* @return {string} The shortened token, still typed as a `<number>`.
|
|
91
|
+
*/
|
|
92
|
+
function preserveNumberType (shortened, unit) {
|
|
93
|
+
// An optional sign followed by nothing but digits reads as an integer
|
|
94
|
+
const isIntegerSpelling = /^[+-]?\d+$/.test(shortened);
|
|
95
|
+
if (!unit && isIntegerSpelling) {
|
|
96
|
+
return shortened + '.0';
|
|
97
|
+
}
|
|
79
98
|
return shortened + unit;
|
|
80
99
|
}
|
|
81
100
|
|
package/src/rules/normalize.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* @file Normalizes CSS selectors, `@media` queries, and `@supports` conditions by unescaping identifiers and collapsing whitespace.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { tokenizeCssValue } from '../parser/tokenizer.js';
|
|
5
6
|
import { resolveUnicodeEscape } from '../utilities.js';
|
|
7
|
+
import { shortenNumericText } from '../value/numbers.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Converts CSS Unicode escape sequences in an identifier to their literal characters, preserving control characters that must remain escaped.
|
|
@@ -161,7 +163,38 @@ function normalizeMedia (media) {
|
|
|
161
163
|
return fullMatch;
|
|
162
164
|
}
|
|
163
165
|
);
|
|
164
|
-
return compactLogicalOperators(media.trim());
|
|
166
|
+
return compactLogicalOperators(shortenRatioNumbers(media.trim()));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Shortens the numbers of every `<ratio>` in a media query, such as the
|
|
171
|
+
* `1000000/1` of `(min-aspect-ratio:1000000/1)`. A ratio is made of
|
|
172
|
+
* `<number>`s rather than `<integer>`s, so its terms may use scientific
|
|
173
|
+
* notation.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} media A normalized `@media` query string.
|
|
176
|
+
* @return {string} The query with shorter ratio numbers.
|
|
177
|
+
*/
|
|
178
|
+
function shortenRatioNumbers (media) {
|
|
179
|
+
const tokens = tokenizeCssValue(media);
|
|
180
|
+
const ratioTermIndexes = new Set();
|
|
181
|
+
tokens.forEach((token, index) => {
|
|
182
|
+
const isRatioSlash = (
|
|
183
|
+
token.text === '/' &&
|
|
184
|
+
tokens[index - 1]?.type === 'number' &&
|
|
185
|
+
tokens[index + 1]?.type === 'number'
|
|
186
|
+
);
|
|
187
|
+
if (isRatioSlash) {
|
|
188
|
+
ratioTermIndexes.add(index - 1);
|
|
189
|
+
ratioTermIndexes.add(index + 1);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return tokens.map((token, index) => {
|
|
193
|
+
if (ratioTermIndexes.has(index)) {
|
|
194
|
+
return shortenNumericText(token.text, true);
|
|
195
|
+
}
|
|
196
|
+
return token.text;
|
|
197
|
+
}).join('');
|
|
165
198
|
}
|
|
166
199
|
|
|
167
200
|
/**
|
package/src/rules/stringify.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { processDeclarations } from '../declarations/process.js';
|
|
6
6
|
import {
|
|
7
|
+
findMatchingParenthesis,
|
|
7
8
|
skipParenthesizedGroup,
|
|
8
9
|
skipQuotedString,
|
|
9
10
|
splitTopLevelCommaList,
|
|
@@ -74,7 +75,20 @@ const LAYER_STATEMENT_PATTERN = /^@layer [^{}();'"]*;$/;
|
|
|
74
75
|
*/
|
|
75
76
|
function removeRedundantLayerStatementSemicolon (ruleStrings) {
|
|
76
77
|
const lastIndex = ruleStrings.length - 1;
|
|
77
|
-
if (lastIndex < 0
|
|
78
|
+
if (lastIndex < 0) {
|
|
79
|
+
return ruleStrings;
|
|
80
|
+
}
|
|
81
|
+
const lastRule = ruleStrings[lastIndex];
|
|
82
|
+
const isTrailingLayerStatement = LAYER_STATEMENT_PATTERN.test(lastRule);
|
|
83
|
+
const isTrailingImportStatement = (
|
|
84
|
+
lastRule.endsWith(';') &&
|
|
85
|
+
(
|
|
86
|
+
lastRule.startsWith('@import ') ||
|
|
87
|
+
lastRule.startsWith('@import"') ||
|
|
88
|
+
lastRule.startsWith('@import\'')
|
|
89
|
+
)
|
|
90
|
+
);
|
|
91
|
+
if (!isTrailingLayerStatement && !isTrailingImportStatement) {
|
|
78
92
|
return ruleStrings;
|
|
79
93
|
}
|
|
80
94
|
const result = [...ruleStrings];
|
|
@@ -82,6 +96,47 @@ function removeRedundantLayerStatementSemicolon (ruleStrings) {
|
|
|
82
96
|
return result;
|
|
83
97
|
}
|
|
84
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Normalizes every `supports()` condition in an import prelude while leaving
|
|
101
|
+
* nested functions and quoted strings to the condition parser.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} importStatement The import prelude being minified.
|
|
104
|
+
* @return {string} The import prelude with normalized conditions.
|
|
105
|
+
*/
|
|
106
|
+
function normalizeImportSupports (importStatement) {
|
|
107
|
+
let result = '';
|
|
108
|
+
let index = 0;
|
|
109
|
+
const normalizedStatement = importStatement.toLowerCase();
|
|
110
|
+
while (index < importStatement.length) {
|
|
111
|
+
let supportsIndex = -1;
|
|
112
|
+
let scanIndex = index;
|
|
113
|
+
while (scanIndex < importStatement.length) {
|
|
114
|
+
if (importStatement[scanIndex] === '"' || importStatement[scanIndex] === '\'') {
|
|
115
|
+
scanIndex = skipQuotedString(importStatement, scanIndex);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (normalizedStatement.startsWith('supports(', scanIndex)) {
|
|
119
|
+
supportsIndex = scanIndex;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
scanIndex++;
|
|
123
|
+
}
|
|
124
|
+
if (supportsIndex === -1) {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
const openIndex = supportsIndex + 'supports'.length;
|
|
128
|
+
const closeIndex = findMatchingParenthesis(importStatement, openIndex);
|
|
129
|
+
if (closeIndex === -1) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
const condition = importStatement.slice(openIndex + 1, closeIndex);
|
|
133
|
+
result += importStatement.slice(index, openIndex + 1);
|
|
134
|
+
result += normalizeSupports(condition) + ')';
|
|
135
|
+
index = closeIndex + 1;
|
|
136
|
+
}
|
|
137
|
+
return result + importStatement.slice(index);
|
|
138
|
+
}
|
|
139
|
+
|
|
85
140
|
/**
|
|
86
141
|
* Recursively stringifies child rules into a concatenated minified CSS string.
|
|
87
142
|
*
|
|
@@ -550,13 +605,7 @@ function stringifyRule (rule, context) {
|
|
|
550
605
|
importStatement = importStatement.replace(/^(".*?"|'.*?') /, '$1');
|
|
551
606
|
// Remove space between closing paren and next at-rule condition keyword
|
|
552
607
|
importStatement = importStatement.replace(/\) ([a-zA-Z])/g, ')$1');
|
|
553
|
-
|
|
554
|
-
importStatement = importStatement.replace(
|
|
555
|
-
/supports\(([^()]*)\)/g,
|
|
556
|
-
(fullMatch, content) => {
|
|
557
|
-
return 'supports(' + content.replace(/\s*:\s*/g, ':') + ')';
|
|
558
|
-
}
|
|
559
|
-
);
|
|
608
|
+
importStatement = normalizeImportSupports(importStatement);
|
|
560
609
|
const startsWithQuote = importStatement.startsWith('"') || importStatement.startsWith('\'');
|
|
561
610
|
let importSeparator;
|
|
562
611
|
if (startsWithQuote) {
|
|
@@ -66,17 +66,29 @@ const MAXIMUM_NESTING_DEPTH = 32;
|
|
|
66
66
|
* length is held as the pixels it measures, since every length stating the
|
|
67
67
|
* same measurement then totals under the same unit.
|
|
68
68
|
*
|
|
69
|
-
* @param {number}
|
|
70
|
-
* @param {string}
|
|
71
|
-
* @
|
|
69
|
+
* @param {number} number The number the quantity is written from.
|
|
70
|
+
* @param {string} unit The unit the quantity is written with, or the empty string when it is a plain number.
|
|
71
|
+
* @param {boolean} preserveWrittenQuantity Whether to retain the original spelling for a standalone quantity.
|
|
72
|
+
* @return {Map} The unit totals the quantity states.
|
|
72
73
|
*/
|
|
73
|
-
function createUnitTotals (number, unit) {
|
|
74
|
+
function createUnitTotals (number, unit, preserveWrittenQuantity = false) {
|
|
74
75
|
const lowercasedUnit = unit.toLowerCase();
|
|
75
76
|
const pixels = convertAbsoluteLengthToPx(number, lowercasedUnit);
|
|
77
|
+
let totals;
|
|
76
78
|
if (pixels === null) {
|
|
77
|
-
|
|
79
|
+
totals = new Map([[lowercasedUnit, number]]);
|
|
80
|
+
} else {
|
|
81
|
+
totals = new Map([['px', pixels]]);
|
|
82
|
+
}
|
|
83
|
+
if (preserveWrittenQuantity) {
|
|
84
|
+
totals.writtenQuantity = {
|
|
85
|
+
number,
|
|
86
|
+
unit: lowercasedUnit,
|
|
87
|
+
canonicalUnit: pixels === null ? lowercasedUnit : 'px',
|
|
88
|
+
canonicalNumber: pixels === null ? number : pixels
|
|
89
|
+
};
|
|
78
90
|
}
|
|
79
|
-
return
|
|
91
|
+
return totals;
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
/**
|
|
@@ -162,6 +174,16 @@ function multiplyUnitTotals (left, right) {
|
|
|
162
174
|
if (isPlainNumber(left)) {
|
|
163
175
|
return scaleUnitTotals(right, left.get(UNITLESS));
|
|
164
176
|
}
|
|
177
|
+
const leftQuantity = readSingleQuantity(left);
|
|
178
|
+
const rightQuantity = readSingleQuantity(right);
|
|
179
|
+
if (leftQuantity && rightQuantity) {
|
|
180
|
+
if (leftQuantity.unit.startsWith('reciprocal:') && leftQuantity.unit.slice('reciprocal:'.length) === rightQuantity.unit) {
|
|
181
|
+
return createUnitTotals(leftQuantity.number * rightQuantity.number, UNITLESS);
|
|
182
|
+
}
|
|
183
|
+
if (rightQuantity.unit.startsWith('reciprocal:') && rightQuantity.unit.slice('reciprocal:'.length) === leftQuantity.unit) {
|
|
184
|
+
return createUnitTotals(leftQuantity.number * rightQuantity.number, UNITLESS);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
165
187
|
return null;
|
|
166
188
|
}
|
|
167
189
|
|
|
@@ -175,14 +197,28 @@ function multiplyUnitTotals (left, right) {
|
|
|
175
197
|
* @return {Map|null} The totals of the quotient, or null when the division cannot be worked out.
|
|
176
198
|
*/
|
|
177
199
|
function divideUnitTotals (left, right) {
|
|
178
|
-
if (
|
|
179
|
-
|
|
200
|
+
if (isPlainNumber(right)) {
|
|
201
|
+
const divisor = right.get(UNITLESS);
|
|
202
|
+
if (divisor === 0) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
return scaleUnitTotals(left, 1 / divisor);
|
|
180
206
|
}
|
|
181
|
-
const
|
|
182
|
-
|
|
207
|
+
const dividendQuantity = readSingleQuantity(left);
|
|
208
|
+
const divisorQuantity = readSingleQuantity(right);
|
|
209
|
+
if (!dividendQuantity || !divisorQuantity || divisorQuantity.number === 0) {
|
|
183
210
|
return null;
|
|
184
211
|
}
|
|
185
|
-
|
|
212
|
+
if (dividendQuantity.unit === divisorQuantity.unit) {
|
|
213
|
+
return createUnitTotals(dividendQuantity.number / divisorQuantity.number, UNITLESS);
|
|
214
|
+
}
|
|
215
|
+
if (dividendQuantity.unit === UNITLESS) {
|
|
216
|
+
return createUnitTotals(
|
|
217
|
+
dividendQuantity.number / divisorQuantity.number,
|
|
218
|
+
'reciprocal:' + divisorQuantity.unit
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
186
222
|
}
|
|
187
223
|
|
|
188
224
|
/**
|
|
@@ -478,7 +514,7 @@ function parseOperand (reader) {
|
|
|
478
514
|
}
|
|
479
515
|
if (token.type === 'number') {
|
|
480
516
|
reader.index++;
|
|
481
|
-
return createUnitTotals(token.number, token.unit);
|
|
517
|
+
return createUnitTotals(token.number, token.unit, true);
|
|
482
518
|
}
|
|
483
519
|
if (token.type === 'identifier') {
|
|
484
520
|
reader.index++;
|
|
@@ -661,7 +697,8 @@ function evaluateMinimum (argumentList) {
|
|
|
661
697
|
if (!comparable) {
|
|
662
698
|
return null;
|
|
663
699
|
}
|
|
664
|
-
|
|
700
|
+
const winningIndex = comparable.numbers.indexOf(Math.min(...comparable.numbers));
|
|
701
|
+
return argumentList[winningIndex].value;
|
|
665
702
|
}
|
|
666
703
|
|
|
667
704
|
/**
|
|
@@ -675,7 +712,8 @@ function evaluateMaximum (argumentList) {
|
|
|
675
712
|
if (!comparable) {
|
|
676
713
|
return null;
|
|
677
714
|
}
|
|
678
|
-
|
|
715
|
+
const winningIndex = comparable.numbers.indexOf(Math.max(...comparable.numbers));
|
|
716
|
+
return argumentList[winningIndex].value;
|
|
679
717
|
}
|
|
680
718
|
|
|
681
719
|
/**
|
|
@@ -1043,7 +1081,22 @@ function writeTermNumber (total) {
|
|
|
1043
1081
|
* @return {string|null} The value the calculation resolved to, or null when it cannot be written faithfully.
|
|
1044
1082
|
*/
|
|
1045
1083
|
function formatUnitTotals (totals) {
|
|
1084
|
+
const writtenQuantity = totals.writtenQuantity;
|
|
1085
|
+
if (
|
|
1086
|
+
writtenQuantity &&
|
|
1087
|
+
totals.size === 1 &&
|
|
1088
|
+
totals.has(writtenQuantity.canonicalUnit) &&
|
|
1089
|
+
totals.get(writtenQuantity.canonicalUnit) === writtenQuantity.canonicalNumber
|
|
1090
|
+
) {
|
|
1091
|
+
return formatResolvedNumber(writtenQuantity.number) + writtenQuantity.unit;
|
|
1092
|
+
}
|
|
1046
1093
|
const mergedTotals = mergeUnitsOfSameKind(totals);
|
|
1094
|
+
const hasUnrepresentableReciprocalUnit = [...mergedTotals.keys()].some((unit) => {
|
|
1095
|
+
return unit.startsWith('reciprocal:');
|
|
1096
|
+
});
|
|
1097
|
+
if (hasUnrepresentableReciprocalUnit) {
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1047
1100
|
const outputTerms = collectOutputTerms(mergedTotals);
|
|
1048
1101
|
if (dropsPercentageBasis(mergedTotals, outputTerms)) {
|
|
1049
1102
|
return null;
|
package/src/value/math.js
CHANGED
|
@@ -453,6 +453,30 @@ const NON_NEGATIVE_PROPERTIES = new Set([
|
|
|
453
453
|
'width'
|
|
454
454
|
]);
|
|
455
455
|
|
|
456
|
+
/**
|
|
457
|
+
* Properties whose computed math values must be integers.
|
|
458
|
+
*
|
|
459
|
+
* @type {Set<string>}
|
|
460
|
+
*/
|
|
461
|
+
const INTEGER_VALUE_PROPERTIES = new Set([
|
|
462
|
+
'column-count',
|
|
463
|
+
'order',
|
|
464
|
+
'orphans',
|
|
465
|
+
'widows',
|
|
466
|
+
'z-index'
|
|
467
|
+
]);
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* The inclusive bounds that CSS applies to numeric property values after
|
|
471
|
+
* resolving a math expression.
|
|
472
|
+
*
|
|
473
|
+
* @type {Map<string, {minimum?: number, maximum?: number}>}
|
|
474
|
+
*/
|
|
475
|
+
const NUMERIC_PROPERTY_RANGES = new Map([
|
|
476
|
+
['column-count', { minimum: 1 }],
|
|
477
|
+
['opacity', { minimum: 0, maximum: 1 }]
|
|
478
|
+
]);
|
|
479
|
+
|
|
456
480
|
/**
|
|
457
481
|
* Matches a value written as a single negative number, with or without the
|
|
458
482
|
* unit or percent sign that follows its digits.
|
|
@@ -461,6 +485,14 @@ const NON_NEGATIVE_PROPERTIES = new Set([
|
|
|
461
485
|
*/
|
|
462
486
|
const NEGATIVE_NUMBER_VALUE = /^-(?:\d+|\d*\.\d+)(?:[a-z]+|%)?$/i;
|
|
463
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Matches a resolved unitless number whose value can be rounded or clamped by
|
|
490
|
+
* a property's numeric range.
|
|
491
|
+
*
|
|
492
|
+
* @type {RegExp}
|
|
493
|
+
*/
|
|
494
|
+
const UNIT_LESS_NUMBER_VALUE = /^[+-]?(?:\d+|\d*\.\d+)$/;
|
|
495
|
+
|
|
464
496
|
/**
|
|
465
497
|
* Clamps what a math function resolved to into the range its property
|
|
466
498
|
* accepts. A negative length written out directly makes the declaration
|
|
@@ -475,11 +507,39 @@ const NEGATIVE_NUMBER_VALUE = /^-(?:\d+|\d*\.\d+)(?:[a-z]+|%)?$/i;
|
|
|
475
507
|
* @return {string} The resolved value, held inside the property's range.
|
|
476
508
|
*/
|
|
477
509
|
function clampResolvedValueToPropertyRange (resolvedValue, property) {
|
|
478
|
-
const
|
|
479
|
-
|
|
510
|
+
const normalizedProperty = String(property).toLowerCase();
|
|
511
|
+
const acceptsNegativeValues = !NON_NEGATIVE_PROPERTIES.has(normalizedProperty);
|
|
512
|
+
const hasPropertyRange = NUMERIC_PROPERTY_RANGES.has(normalizedProperty);
|
|
513
|
+
const range = NUMERIC_PROPERTY_RANGES.get(normalizedProperty) || {};
|
|
514
|
+
const isNegativeNonNegativeValue = !acceptsNegativeValues && NEGATIVE_NUMBER_VALUE.test(resolvedValue);
|
|
515
|
+
if (isNegativeNonNegativeValue) {
|
|
516
|
+
const isUnitlessNumber = UNIT_LESS_NUMBER_VALUE.test(resolvedValue);
|
|
517
|
+
if (!isUnitlessNumber) {
|
|
518
|
+
return formatResolvedNumber(range.minimum === undefined ? 0 : range.minimum);
|
|
519
|
+
}
|
|
520
|
+
if (!hasPropertyRange && !INTEGER_VALUE_PROPERTIES.has(normalizedProperty)) {
|
|
521
|
+
return '0';
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (!hasPropertyRange && !INTEGER_VALUE_PROPERTIES.has(normalizedProperty)) {
|
|
480
525
|
return resolvedValue;
|
|
481
526
|
}
|
|
482
|
-
|
|
527
|
+
if (!UNIT_LESS_NUMBER_VALUE.test(resolvedValue)) {
|
|
528
|
+
return resolvedValue;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const shouldRoundToInteger = INTEGER_VALUE_PROPERTIES.has(normalizedProperty);
|
|
532
|
+
let numericValue = Number(resolvedValue);
|
|
533
|
+
if (shouldRoundToInteger) {
|
|
534
|
+
numericValue = Math.round(numericValue);
|
|
535
|
+
}
|
|
536
|
+
if (range.minimum !== undefined) {
|
|
537
|
+
numericValue = Math.max(range.minimum, numericValue);
|
|
538
|
+
}
|
|
539
|
+
if (range.maximum !== undefined) {
|
|
540
|
+
numericValue = Math.min(range.maximum, numericValue);
|
|
541
|
+
}
|
|
542
|
+
return formatResolvedNumber(numericValue);
|
|
483
543
|
}
|
|
484
544
|
|
|
485
545
|
/**
|
package/src/value/minify.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
skipQuotedString,
|
|
10
10
|
startsUrlToken
|
|
11
11
|
} from '../parser/source-search.js';
|
|
12
|
+
import { tokenizeCssValue } from '../parser/tokenizer.js';
|
|
12
13
|
import { resolveUnicodeEscape } from '../utilities.js';
|
|
13
14
|
|
|
14
15
|
import { evaluateColorMix } from './color-mix.js';
|
|
@@ -30,6 +31,12 @@ import {
|
|
|
30
31
|
simplifyStandaloneCalc
|
|
31
32
|
} from './math.js';
|
|
32
33
|
import { namedColors } from './named-colors.js';
|
|
34
|
+
import {
|
|
35
|
+
formatShortestNumber,
|
|
36
|
+
parseNumericParts,
|
|
37
|
+
scaleByPowerOfTen,
|
|
38
|
+
shortenNumericText
|
|
39
|
+
} from './numbers.js';
|
|
33
40
|
import { isQuotesNoneEquivalent } from './quotes.js';
|
|
34
41
|
import {
|
|
35
42
|
evaluateRelativeColor,
|
|
@@ -38,6 +45,7 @@ import {
|
|
|
38
45
|
import {
|
|
39
46
|
collapseShorthandParts,
|
|
40
47
|
convertAbsoluteLengthToPx,
|
|
48
|
+
formatCompactNumber,
|
|
41
49
|
formatResolvedNumber,
|
|
42
50
|
hasSubstitutedParts,
|
|
43
51
|
normalizeScaleComponent,
|
|
@@ -623,6 +631,40 @@ function isCssDigit (character) {
|
|
|
623
631
|
return character >= '0' && character <= '9';
|
|
624
632
|
}
|
|
625
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Converts OKLCH lightness and chroma percentages when doing so is shorter,
|
|
636
|
+
* including the separator each spelling needs before the hue.
|
|
637
|
+
*
|
|
638
|
+
* @param {string} value The normalized arguments of an `oklch()` function.
|
|
639
|
+
* @return {string} The arguments with shorter percentage spellings.
|
|
640
|
+
*/
|
|
641
|
+
function minifyOklchPercentages (value) {
|
|
642
|
+
const slashIndex = value.indexOf('/');
|
|
643
|
+
const componentsText = slashIndex === -1 ? value : value.slice(0, slashIndex);
|
|
644
|
+
const alphaText = slashIndex === -1 ? '' : value.slice(slashIndex + 1);
|
|
645
|
+
const components = componentsText.trim().split(' ');
|
|
646
|
+
if (components.length !== 3) {
|
|
647
|
+
return value;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
let [lightness, chroma, hue] = components;
|
|
651
|
+
if (lightness.endsWith('%')) {
|
|
652
|
+
lightness = formatCompactNumber(Number.parseFloat(lightness) / 100);
|
|
653
|
+
}
|
|
654
|
+
if (chroma.endsWith('%')) {
|
|
655
|
+
const convertedChroma = formatCompactNumber(
|
|
656
|
+
Number.parseFloat(chroma) / 100 * OKLCH_CHROMA_PERCENT_REFERENCE
|
|
657
|
+
);
|
|
658
|
+
const convertedWithHueSeparator = convertedChroma.length + 1;
|
|
659
|
+
if (chroma.length < convertedWithHueSeparator) {
|
|
660
|
+
return [lightness, chroma + hue].join(' ') + (slashIndex === -1 ? '' : '/' + alphaText);
|
|
661
|
+
}
|
|
662
|
+
chroma = convertedChroma;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return [lightness, chroma, hue].join(' ') + (slashIndex === -1 ? '' : '/' + alphaText);
|
|
666
|
+
}
|
|
667
|
+
|
|
626
668
|
/**
|
|
627
669
|
* Converts `oklch()` colors that fall inside the sRGB gamut to their hex
|
|
628
670
|
* equivalent when that is shorter. Out-of-gamut colors have no sRGB
|
|
@@ -747,6 +789,9 @@ function convertColorsToHex (val) {
|
|
|
747
789
|
return integer;
|
|
748
790
|
});
|
|
749
791
|
minified = compactNegativeColorChannelSpacing(func, minified);
|
|
792
|
+
if (func.toLowerCase() === 'oklch') {
|
|
793
|
+
minified = minifyOklchPercentages(minified);
|
|
794
|
+
}
|
|
750
795
|
return func + '(' + minified.trim() + ')';
|
|
751
796
|
});
|
|
752
797
|
|
|
@@ -880,26 +925,89 @@ function convertBackgroundPositionKeywords (value) {
|
|
|
880
925
|
}
|
|
881
926
|
|
|
882
927
|
/**
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
* and values at or below 99ms stay in milliseconds (shorter representation).
|
|
928
|
+
* The power of ten that separates a time in seconds from the same time in
|
|
929
|
+
* milliseconds, keyed by the lowercased time unit.
|
|
886
930
|
*
|
|
887
|
-
* @
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
931
|
+
* @type {{[unit: string]: number}}
|
|
932
|
+
*/
|
|
933
|
+
const TIME_UNIT_POWERS = {
|
|
934
|
+
s: 0,
|
|
935
|
+
ms: -3
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
/**
|
|
939
|
+
* Rewrites one time token in whichever of seconds or milliseconds spells it
|
|
940
|
+
* shortest, scientific notation included. Every conversion moves the exponent
|
|
941
|
+
* of the exact digits, so no precision is lost however large the time is.
|
|
942
|
+
* Seconds win a tie, and a zero time becomes `0s`.
|
|
943
|
+
*
|
|
944
|
+
* @param {string} text The text of a dimension token.
|
|
945
|
+
* @return {string} The shortest spelling of the same time.
|
|
946
|
+
*/
|
|
947
|
+
function shortenTimeToken (text) {
|
|
948
|
+
const parts = parseNumericParts(text);
|
|
949
|
+
const writtenUnit = parts?.unit.toLowerCase();
|
|
950
|
+
if (!parts || !(writtenUnit in TIME_UNIT_POWERS)) {
|
|
951
|
+
return text;
|
|
952
|
+
}
|
|
953
|
+
if (!parts.digits) {
|
|
954
|
+
return '0s';
|
|
955
|
+
}
|
|
956
|
+
const writtenSpelling = shortenNumericText(text, true);
|
|
957
|
+
const secondsSpelling = formatShortestNumber(
|
|
958
|
+
scaleByPowerOfTen(parts, TIME_UNIT_POWERS[writtenUnit], 's'),
|
|
959
|
+
true
|
|
960
|
+
);
|
|
961
|
+
const millisecondsSpelling = formatShortestNumber(
|
|
962
|
+
scaleByPowerOfTen(parts, TIME_UNIT_POWERS[writtenUnit] - TIME_UNIT_POWERS.ms, 'ms'),
|
|
963
|
+
true
|
|
964
|
+
);
|
|
965
|
+
// Seconds are the conventional unit, so they win a tie with milliseconds
|
|
966
|
+
const convertedSpelling = millisecondsSpelling.length < secondsSpelling.length ? millisecondsSpelling : secondsSpelling;
|
|
967
|
+
return convertedSpelling.length <= writtenSpelling.length ? convertedSpelling : writtenSpelling;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Reports whether rewriting a number keeps its numeric type. A number written
|
|
972
|
+
* with a decimal point or an exponent has the `<number>` type, while one
|
|
973
|
+
* written with digits alone is an `<integer>`. A custom property can be
|
|
974
|
+
* substituted into, or registered with, a grammar that tells the two apart, so
|
|
975
|
+
* `100.0` cannot become `100` there.
|
|
976
|
+
*
|
|
977
|
+
* @param {string} originalNumber The number as it was written.
|
|
978
|
+
* @param {string} rewrittenNumber The number as it would be rewritten.
|
|
979
|
+
* @return {boolean} True when both spellings have the same type.
|
|
980
|
+
*/
|
|
981
|
+
function keepsNumberType (originalNumber, rewrittenNumber) {
|
|
982
|
+
return isIntegerSpelling(originalNumber) === isIntegerSpelling(rewrittenNumber);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Reports whether a number is spelled as an `<integer>`: digits with an
|
|
987
|
+
* optional sign, and no decimal point or exponent.
|
|
988
|
+
*
|
|
989
|
+
* @param {string} number The number as written.
|
|
990
|
+
* @return {boolean} True when the spelling is an integer.
|
|
991
|
+
*/
|
|
992
|
+
function isIntegerSpelling (number) {
|
|
993
|
+
// An optional sign followed by nothing but digits
|
|
994
|
+
return /^[+-]?\d+$/.test(number);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Converts each time in a value between seconds and milliseconds, whichever
|
|
999
|
+
* is the shorter string.
|
|
1000
|
+
*
|
|
1001
|
+
* @param {string} value The CSS value string potentially containing time values.
|
|
1002
|
+
* @return {string} The value with each time in its shortest unit.
|
|
1003
|
+
*/
|
|
1004
|
+
function convertTimesToShortestUnit (value) {
|
|
1005
|
+
return tokenizeCssValue(value).map((token) => {
|
|
1006
|
+
if (token.type !== 'dimension') {
|
|
1007
|
+
return token.text;
|
|
900
1008
|
}
|
|
901
|
-
return
|
|
902
|
-
});
|
|
1009
|
+
return shortenTimeToken(token.text);
|
|
1010
|
+
}).join('');
|
|
903
1011
|
}
|
|
904
1012
|
|
|
905
1013
|
/**
|
|
@@ -1266,7 +1374,7 @@ function applyPropertyOptimizations (val, property, urlTokens) {
|
|
|
1266
1374
|
|
|
1267
1375
|
// Convert ms to s for time-related properties when the seconds form is shorter
|
|
1268
1376
|
if (TIME_PROPERTIES.has(property)) {
|
|
1269
|
-
val =
|
|
1377
|
+
val = convertTimesToShortestUnit(val);
|
|
1270
1378
|
}
|
|
1271
1379
|
|
|
1272
1380
|
// Transition: remove " 0s" duration (transition: all 0s -> transition: all)
|
|
@@ -1578,12 +1686,17 @@ function computeMinifiedValue (declaration) {
|
|
|
1578
1686
|
// character budget a number is written within
|
|
1579
1687
|
if (/^[+-]?(?:\d+|\d*\.\d+)([a-z%]+)?$/i.test(val)) {
|
|
1580
1688
|
const [, rawNumber, rawUnit = ''] = val.match(/^([+-]?(?:\d+|\d*\.\d+))([a-z%]+)?$/i);
|
|
1581
|
-
const
|
|
1689
|
+
const isCustom = isCustomProperty(declaration.property);
|
|
1690
|
+
const scientificValue = isCustom ? val : shortenDimensionNumbers(val, declaration.property);
|
|
1582
1691
|
const roundedNumber = formatResolvedNumber(rawNumber);
|
|
1583
1692
|
const losesPrecision = Number(roundedNumber) !== Number(rawNumber);
|
|
1584
1693
|
// Preserve tiny dimensions whose significant digits would otherwise be
|
|
1585
1694
|
// rounded away before their shorter exponent form can be considered.
|
|
1586
|
-
|
|
1695
|
+
if (losesPrecision && scientificValue !== val) {
|
|
1696
|
+
val = scientificValue;
|
|
1697
|
+
} else if (!isCustom || keepsNumberType(rawNumber, roundedNumber)) {
|
|
1698
|
+
val = roundedNumber + rawUnit;
|
|
1699
|
+
}
|
|
1587
1700
|
}
|
|
1588
1701
|
|
|
1589
1702
|
val = lowercaseHexColors(val);
|
|
@@ -1632,7 +1745,7 @@ function computeMinifiedValue (declaration) {
|
|
|
1632
1745
|
!isCustomProperty(declaration.property)
|
|
1633
1746
|
);
|
|
1634
1747
|
if (elidesRedundantSeparators) {
|
|
1635
|
-
val = shortenDimensionNumbers(val);
|
|
1748
|
+
val = shortenDimensionNumbers(val, declaration.property);
|
|
1636
1749
|
// A property that punctuates its own components already had the whitespace
|
|
1637
1750
|
// after its closing parentheses taken out, and only the separators its
|
|
1638
1751
|
// grammar still needs put back, so those are the ones left alone here.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Reads the numbers of numeric CSS tokens as exact digits and an exponent, so they can be rewritten in their shortest decimal or scientific spelling without any floating point rounding.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A number taken apart into the pieces its spelling is built from. The digits
|
|
7
|
+
* never start or end with a zero, so the same value always has the same parts.
|
|
8
|
+
*
|
|
9
|
+
* @typedef {object} NumericParts
|
|
10
|
+
* @property {string} sign The sign as written, kept so a `+` that tells the number apart from a neighboring token survives, or an empty string for zero.
|
|
11
|
+
* @property {string} digits The significant digits, or an empty string for zero.
|
|
12
|
+
* @property {number} exponent The power of ten the digits, read as an integer, are multiplied by.
|
|
13
|
+
* @property {string} unit The unit or percent sign that follows the number.
|
|
14
|
+
* @property {boolean} hasExponent Whether the original spelling used scientific notation.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Matches the text of a numeric token, capturing its sign, its coefficient,
|
|
19
|
+
* the exponent of its scientific notation when it has one, and the unit or
|
|
20
|
+
* percent sign written after it. An exponent needs digits after the `e`, so the
|
|
21
|
+
* `e` that starts a unit such as `em` is left to the unit.
|
|
22
|
+
*
|
|
23
|
+
* @type {RegExp}
|
|
24
|
+
*/
|
|
25
|
+
const NUMERIC_TOKEN_PATTERN = /^([+-]?)(\d*\.\d+|\d+)(?:[eE]([+-]?\d+))?(%|[a-zA-Z_-][\w-]*)?$/;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Takes the text of a numeric token apart into its sign, significant digits,
|
|
29
|
+
* exponent, and unit.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} text The text of a number, percentage, or dimension token.
|
|
32
|
+
* @return {NumericParts|null} The parts of the number, or null when the text is not a plain number.
|
|
33
|
+
*/
|
|
34
|
+
function parseNumericParts (text) {
|
|
35
|
+
const match = text.match(NUMERIC_TOKEN_PATTERN);
|
|
36
|
+
if (!match) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const [, sign, coefficient, writtenExponent, unit = ''] = match;
|
|
40
|
+
const decimalIndex = coefficient.indexOf('.');
|
|
41
|
+
const fractionalDigitCount = decimalIndex === -1 ? 0 : coefficient.length - decimalIndex - 1;
|
|
42
|
+
// Leading zeros carry no magnitude, so only the digits after them count
|
|
43
|
+
const unpaddedDigits = coefficient.replace('.', '').replace(/^0+/, '');
|
|
44
|
+
// Trailing zeros only scale the digits before them, so they move into the exponent
|
|
45
|
+
const digits = unpaddedDigits.replace(/0+$/, '');
|
|
46
|
+
const trailingZeroCount = unpaddedDigits.length - digits.length;
|
|
47
|
+
return {
|
|
48
|
+
sign: digits ? sign : '',
|
|
49
|
+
digits,
|
|
50
|
+
exponent: Number(writtenExponent || 0) + trailingZeroCount - fractionalDigitCount,
|
|
51
|
+
unit,
|
|
52
|
+
hasExponent: writtenExponent !== undefined
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Spells a number out in plain decimal notation, without a leading zero before
|
|
58
|
+
* its decimal point.
|
|
59
|
+
*
|
|
60
|
+
* @param {NumericParts} parts The parts of a non-zero number.
|
|
61
|
+
* @return {string} The decimal spelling, without the unit.
|
|
62
|
+
*/
|
|
63
|
+
function formatDecimal (parts) {
|
|
64
|
+
const { sign, digits, exponent } = parts;
|
|
65
|
+
if (exponent >= 0) {
|
|
66
|
+
return sign + digits + '0'.repeat(exponent);
|
|
67
|
+
}
|
|
68
|
+
const decimalIndex = digits.length + exponent;
|
|
69
|
+
if (decimalIndex > 0) {
|
|
70
|
+
return sign + digits.slice(0, decimalIndex) + '.' + digits.slice(decimalIndex);
|
|
71
|
+
}
|
|
72
|
+
return sign + '.' + '0'.repeat(-decimalIndex) + digits;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Spells a number out in scientific notation, keeping every significant digit
|
|
77
|
+
* in an integer mantissa so no decimal point is ever needed.
|
|
78
|
+
*
|
|
79
|
+
* @param {NumericParts} parts The parts of a non-zero number.
|
|
80
|
+
* @return {string} The scientific spelling, without the unit.
|
|
81
|
+
*/
|
|
82
|
+
function formatScientific (parts) {
|
|
83
|
+
if (parts.exponent === 0) {
|
|
84
|
+
return parts.sign + parts.digits;
|
|
85
|
+
}
|
|
86
|
+
return parts.sign + parts.digits + 'e' + parts.exponent;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Spells a number in the shortest way its grammar allows. Plain decimal
|
|
91
|
+
* notation wins a tie, since it is the more familiar spelling.
|
|
92
|
+
*
|
|
93
|
+
* @param {NumericParts} parts The parts of a non-zero number.
|
|
94
|
+
* @param {boolean} allowsScientific Whether the number may be written in scientific notation.
|
|
95
|
+
* @return {string} The shortest spelling, unit included.
|
|
96
|
+
*/
|
|
97
|
+
function formatShortestNumber (parts, allowsScientific) {
|
|
98
|
+
const decimal = formatDecimal(parts);
|
|
99
|
+
if (!allowsScientific) {
|
|
100
|
+
return decimal + parts.unit;
|
|
101
|
+
}
|
|
102
|
+
const scientific = formatScientific(parts);
|
|
103
|
+
const shortest = scientific.length < decimal.length ? scientific : decimal;
|
|
104
|
+
return shortest + parts.unit;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Rewrites the text of a numeric token in its shortest spelling. A number
|
|
109
|
+
* written without an exponent only changes when the new spelling is shorter,
|
|
110
|
+
* while one written with an exponent is always normalized, which lowercases
|
|
111
|
+
* the `e`, drops a `+` sign, and folds trailing zeros into the exponent.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} text The text of a number, percentage, or dimension token.
|
|
114
|
+
* @param {boolean} allowsScientific Whether the number may be written in scientific notation.
|
|
115
|
+
* @return {string} The shortest equivalent text.
|
|
116
|
+
*/
|
|
117
|
+
function shortenNumericText (text, allowsScientific) {
|
|
118
|
+
const parts = parseNumericParts(text);
|
|
119
|
+
if (!parts || !parts.digits) {
|
|
120
|
+
return text;
|
|
121
|
+
}
|
|
122
|
+
if (parts.hasExponent && !allowsScientific) {
|
|
123
|
+
return text;
|
|
124
|
+
}
|
|
125
|
+
const shortest = formatShortestNumber(parts, allowsScientific);
|
|
126
|
+
if (parts.hasExponent || shortest.length < text.length) {
|
|
127
|
+
return shortest;
|
|
128
|
+
}
|
|
129
|
+
return text;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scales a number by a power of ten by moving its exponent, which is exact for
|
|
134
|
+
* any number of digits.
|
|
135
|
+
*
|
|
136
|
+
* @param {NumericParts} parts The parts of the number.
|
|
137
|
+
* @param {number} power The power of ten to multiply by.
|
|
138
|
+
* @param {string} unit The unit the scaled number is written with.
|
|
139
|
+
* @return {NumericParts} The parts of the scaled number.
|
|
140
|
+
*/
|
|
141
|
+
function scaleByPowerOfTen (parts, power, unit) {
|
|
142
|
+
return {
|
|
143
|
+
...parts,
|
|
144
|
+
exponent: parts.exponent + power,
|
|
145
|
+
unit
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export {
|
|
150
|
+
formatShortestNumber,
|
|
151
|
+
parseNumericParts,
|
|
152
|
+
scaleByPowerOfTen,
|
|
153
|
+
shortenNumericText
|
|
154
|
+
};
|
package/src/value/tokens.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { tokenizeCssValue } from '../parser/tokenizer.js';
|
|
6
6
|
|
|
7
|
+
import { shortenNumericText } from './numbers.js';
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* One token of a value, as the tokenizer reads it.
|
|
9
11
|
*
|
|
@@ -142,36 +144,92 @@ function elideRedundantSeparators (value, elidesAfterSelfEndingTokens = true) {
|
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
+
* Properties whose whole value is a unitless `<number>` rather than an
|
|
148
|
+
* `<integer>`, so a number written directly in them may use scientific
|
|
149
|
+
* notation.
|
|
150
|
+
*
|
|
151
|
+
* @type {Set<string>}
|
|
152
|
+
*/
|
|
153
|
+
const NUMBER_VALUED_PROPERTIES = new Set([
|
|
154
|
+
'animation-iteration-count',
|
|
155
|
+
'fill-opacity',
|
|
156
|
+
'flex-grow',
|
|
157
|
+
'flex-shrink',
|
|
158
|
+
'flood-opacity',
|
|
159
|
+
'opacity',
|
|
160
|
+
'stop-opacity',
|
|
161
|
+
'stroke-miterlimit',
|
|
162
|
+
'stroke-opacity',
|
|
163
|
+
'tab-size'
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Functions whose arguments are unitless `<number>`s rather than `<integer>`s,
|
|
168
|
+
* so the numbers written directly inside them may use scientific notation.
|
|
169
|
+
*
|
|
170
|
+
* @type {Set<string>}
|
|
171
|
+
*/
|
|
172
|
+
const NUMBER_ARGUMENT_FUNCTIONS = new Set([
|
|
173
|
+
'matrix',
|
|
174
|
+
'matrix3d',
|
|
175
|
+
'scale',
|
|
176
|
+
'scale3d',
|
|
177
|
+
'scalex',
|
|
178
|
+
'scaley',
|
|
179
|
+
'scalez'
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Reports whether a unitless number token sits where only a `<number>` is
|
|
184
|
+
* accepted, either directly in a number-valued property or directly inside a
|
|
185
|
+
* function that takes number arguments.
|
|
186
|
+
*
|
|
187
|
+
* @param {string|null} enclosingFunction The lowercased name of the innermost function holding the token, or null at the top level.
|
|
188
|
+
* @param {string} property The lowercased property the value belongs to.
|
|
189
|
+
* @return {boolean} Whether the number may use scientific notation.
|
|
190
|
+
*/
|
|
191
|
+
function isNumberTypedPosition (enclosingFunction, property) {
|
|
192
|
+
if (enclosingFunction === null) {
|
|
193
|
+
return NUMBER_VALUED_PROPERTIES.has(property);
|
|
194
|
+
}
|
|
195
|
+
return NUMBER_ARGUMENT_FUNCTIONS.has(enclosingFunction);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Shortens the numeric tokens of a value into their shortest spelling,
|
|
200
|
+
* including scientific notation. Dimensions and percentages are always
|
|
201
|
+
* `<number>` based, but a unitless number is only shortened where its grammar
|
|
202
|
+
* is known to take a `<number>`, because `<integer>` grammars reject
|
|
147
203
|
* scientific notation.
|
|
148
204
|
*
|
|
149
|
-
* @param {string} value
|
|
150
|
-
* @
|
|
205
|
+
* @param {string} value The CSS value.
|
|
206
|
+
* @param {string} [property] The property the value belongs to.
|
|
207
|
+
* @return {string} The value with shorter numeric spellings.
|
|
151
208
|
*/
|
|
152
|
-
function shortenDimensionNumbers (value) {
|
|
209
|
+
function shortenDimensionNumbers (value, property = '') {
|
|
210
|
+
const normalizedProperty = property.toLowerCase();
|
|
211
|
+
const functionStack = [];
|
|
153
212
|
return tokenizeCssValue(value).map((token) => {
|
|
154
|
-
if (token.type
|
|
213
|
+
if (token.type === 'function') {
|
|
214
|
+
functionStack.push(token.text.slice(0, -1).toLowerCase());
|
|
155
215
|
return token.text;
|
|
156
216
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (!match) {
|
|
217
|
+
if (token.text === '(') {
|
|
218
|
+
functionStack.push('');
|
|
160
219
|
return token.text;
|
|
161
220
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const fractionalDigits = decimalIndex === -1 ? 0 : coefficient.length - decimalIndex - 1;
|
|
165
|
-
const digits = coefficient.replace('.', '');
|
|
166
|
-
// Leading zeros carry no magnitude; trailing zeros move into the exponent.
|
|
167
|
-
const significantDigits = digits.replace(/^0+/, '').replace(/0+$/, '');
|
|
168
|
-
if (!significantDigits) {
|
|
221
|
+
if (token.text === ')') {
|
|
222
|
+
functionStack.pop();
|
|
169
223
|
return token.text;
|
|
170
224
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
225
|
+
if (token.type === 'dimension' || token.type === 'percentage') {
|
|
226
|
+
return shortenNumericText(token.text, true);
|
|
227
|
+
}
|
|
228
|
+
const enclosingFunction = functionStack.length ? functionStack[functionStack.length - 1] : null;
|
|
229
|
+
if (token.type === 'number' && isNumberTypedPosition(enclosingFunction, normalizedProperty)) {
|
|
230
|
+
return shortenNumericText(token.text, true);
|
|
231
|
+
}
|
|
232
|
+
return token.text;
|
|
175
233
|
}).join('');
|
|
176
234
|
}
|
|
177
235
|
|