@thejaredwilcurt/csslop 0.0.47 → 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 +2 -2
- package/src/rules/custom-properties.js +19 -0
- package/src/rules/normalize.js +34 -1
- package/src/value/minify.js +97 -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",
|
|
@@ -57,7 +57,7 @@
|
|
|
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",
|
|
@@ -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/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,
|
|
@@ -918,26 +925,89 @@ function convertBackgroundPositionKeywords (value) {
|
|
|
918
925
|
}
|
|
919
926
|
|
|
920
927
|
/**
|
|
921
|
-
*
|
|
922
|
-
*
|
|
923
|
-
* 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.
|
|
924
930
|
*
|
|
925
|
-
* @
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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;
|
|
938
1008
|
}
|
|
939
|
-
return
|
|
940
|
-
});
|
|
1009
|
+
return shortenTimeToken(token.text);
|
|
1010
|
+
}).join('');
|
|
941
1011
|
}
|
|
942
1012
|
|
|
943
1013
|
/**
|
|
@@ -1304,7 +1374,7 @@ function applyPropertyOptimizations (val, property, urlTokens) {
|
|
|
1304
1374
|
|
|
1305
1375
|
// Convert ms to s for time-related properties when the seconds form is shorter
|
|
1306
1376
|
if (TIME_PROPERTIES.has(property)) {
|
|
1307
|
-
val =
|
|
1377
|
+
val = convertTimesToShortestUnit(val);
|
|
1308
1378
|
}
|
|
1309
1379
|
|
|
1310
1380
|
// Transition: remove " 0s" duration (transition: all 0s -> transition: all)
|
|
@@ -1616,12 +1686,17 @@ function computeMinifiedValue (declaration) {
|
|
|
1616
1686
|
// character budget a number is written within
|
|
1617
1687
|
if (/^[+-]?(?:\d+|\d*\.\d+)([a-z%]+)?$/i.test(val)) {
|
|
1618
1688
|
const [, rawNumber, rawUnit = ''] = val.match(/^([+-]?(?:\d+|\d*\.\d+))([a-z%]+)?$/i);
|
|
1619
|
-
const
|
|
1689
|
+
const isCustom = isCustomProperty(declaration.property);
|
|
1690
|
+
const scientificValue = isCustom ? val : shortenDimensionNumbers(val, declaration.property);
|
|
1620
1691
|
const roundedNumber = formatResolvedNumber(rawNumber);
|
|
1621
1692
|
const losesPrecision = Number(roundedNumber) !== Number(rawNumber);
|
|
1622
1693
|
// Preserve tiny dimensions whose significant digits would otherwise be
|
|
1623
1694
|
// rounded away before their shorter exponent form can be considered.
|
|
1624
|
-
|
|
1695
|
+
if (losesPrecision && scientificValue !== val) {
|
|
1696
|
+
val = scientificValue;
|
|
1697
|
+
} else if (!isCustom || keepsNumberType(rawNumber, roundedNumber)) {
|
|
1698
|
+
val = roundedNumber + rawUnit;
|
|
1699
|
+
}
|
|
1625
1700
|
}
|
|
1626
1701
|
|
|
1627
1702
|
val = lowercaseHexColors(val);
|
|
@@ -1670,7 +1745,7 @@ function computeMinifiedValue (declaration) {
|
|
|
1670
1745
|
!isCustomProperty(declaration.property)
|
|
1671
1746
|
);
|
|
1672
1747
|
if (elidesRedundantSeparators) {
|
|
1673
|
-
val = shortenDimensionNumbers(val);
|
|
1748
|
+
val = shortenDimensionNumbers(val, declaration.property);
|
|
1674
1749
|
// A property that punctuates its own components already had the whitespace
|
|
1675
1750
|
// after its closing parentheses taken out, and only the separators its
|
|
1676
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
|
|