@thejaredwilcurt/csslop 0.0.49 → 0.0.51

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.
Files changed (57) hide show
  1. package/README.md +0 -1
  2. package/package.json +2 -2
  3. package/src/declarations/border.js +1 -1
  4. package/src/declarations/process.js +155 -1
  5. package/src/index.js +7 -7
  6. package/src/rules/optimize/at-rules.js +113 -0
  7. package/src/rules/optimize/declaration-writes.js +70 -0
  8. package/src/rules/optimize/empty-rules.js +51 -0
  9. package/src/rules/optimize/expand-nesting.js +236 -0
  10. package/src/rules/optimize/factor-parents.js +343 -0
  11. package/src/rules/optimize/index.js +71 -0
  12. package/src/rules/optimize/merge-declarations.js +199 -0
  13. package/src/rules/optimize/merge-nested-rules.js +117 -0
  14. package/src/rules/optimize/merge-selectors.js +252 -0
  15. package/src/rules/optimize/nest-rules.js +202 -0
  16. package/src/rules/optimize/override-index.js +264 -0
  17. package/src/rules/optimize/prune-overridden.js +267 -0
  18. package/src/rules/optimize/selector-normalization.js +60 -0
  19. package/src/rules/optimize/specificity.js +280 -0
  20. package/src/{position-try.js → rules/position-try.js} +1 -1
  21. package/src/rules/stringify.js +13 -0
  22. package/src/utilities.js +14 -0
  23. package/src/value/border.js +85 -0
  24. package/src/value/{colors.js → color/conversion.js} +3 -2
  25. package/src/value/color/functions.js +463 -0
  26. package/src/value/{light-dark.js → color/light-dark.js} +1 -1
  27. package/src/value/{color-mix.js → color/mix.js} +3 -3
  28. package/src/value/{relative-colors.js → color/relative.js} +3 -3
  29. package/src/value/color/shorten.js +93 -0
  30. package/src/value/gradients.js +1 -1
  31. package/src/value/hyphenate-limit-chars.js +72 -0
  32. package/src/value/initial-keywords.js +128 -0
  33. package/src/value/lengths.js +55 -0
  34. package/src/value/math/evaluate.js +36 -0
  35. package/src/value/math/fold.js +315 -0
  36. package/src/value/math/format.js +230 -0
  37. package/src/value/math/functions.js +302 -0
  38. package/src/value/math/index.js +219 -0
  39. package/src/value/math/parser.js +301 -0
  40. package/src/value/math/property-ranges.js +174 -0
  41. package/src/value/math/rewrite.js +222 -0
  42. package/src/value/math/tokenizer.js +160 -0
  43. package/src/value/math/unit-totals.js +194 -0
  44. package/src/value/minify.js +127 -1669
  45. package/src/value/numbers.js +28 -0
  46. package/src/value/positions.js +152 -0
  47. package/src/value/property-optimizations.js +485 -0
  48. package/src/value/segments.js +74 -0
  49. package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
  50. package/src/value/{svg-markup.js → svg/markup.js} +1 -1
  51. package/src/value/time.js +75 -0
  52. package/src/value/urls.js +242 -0
  53. package/src/value/whitespace.js +88 -0
  54. package/src/rules/optimize.js +0 -2314
  55. package/src/value/math-expression.js +0 -1156
  56. package/src/value/math.js +0 -886
  57. /package/src/value/{named-colors.js → color/named.js} +0 -0
@@ -0,0 +1,301 @@
1
+ /**
2
+ * @file Reads the tokens of a CSS math function as sums, products, and operands, resolving each into the unit totals it states.
3
+ */
4
+
5
+ import { MATH_FUNCTION_EVALUATORS } from './functions.js';
6
+ import {
7
+ addUnitTotals,
8
+ createUnitTotals,
9
+ divideUnitTotals,
10
+ multiplyUnitTotals,
11
+ scaleUnitTotals,
12
+ UNITLESS
13
+ } from './unit-totals.js';
14
+
15
+ /**
16
+ * The numeric constants a calculation may name instead of writing out, as the
17
+ * keywords section of the CSS Values specification defines them:
18
+ * https://drafts.csswg.org/css-values/#calc-keywords.
19
+ *
20
+ * @type {Map<string, number>}
21
+ */
22
+ const NUMERIC_CONSTANTS = new Map([
23
+ ['e', Math.E],
24
+ ['infinity', Infinity],
25
+ ['nan', NaN],
26
+ ['pi', Math.PI]
27
+ ]);
28
+
29
+ /**
30
+ * The deepest a calculation may nest one set of parentheses inside another
31
+ * before the evaluator gives up on it. Every level of nesting is a level of
32
+ * recursion, and a stylesheet states nothing at a depth anywhere near this.
33
+ *
34
+ * @type {number}
35
+ */
36
+ const MAXIMUM_NESTING_DEPTH = 32;
37
+
38
+ /**
39
+ * @typedef {object} ExpressionReader
40
+ * @property {Array} tokens The tokens of the math function, in the order they are written.
41
+ * @property {number} index The token the parser reads next.
42
+ * @property {number} depth How many sets of parentheses the parser is reading inside of.
43
+ */
44
+
45
+ /**
46
+ * Reads the token the parser stands on, or one of the tokens after it.
47
+ *
48
+ * @param {ExpressionReader} reader The token reader.
49
+ * @param {number} offset How many tokens past the current one to read.
50
+ * @return {object|undefined} The token read, or undefined past the end of the expression.
51
+ */
52
+ function peekToken (reader, offset = 0) {
53
+ return reader.tokens[reader.index + offset];
54
+ }
55
+
56
+ /**
57
+ * Reports whether the parser stands on an operator that adds or subtracts what
58
+ * follows it from what precedes it. A sign written directly against its number
59
+ * is part of that number rather than an operator, so a value written with one
60
+ * sits against the value before it with no operator between them, which is not
61
+ * a calculation any browser reads.
62
+ *
63
+ * @param {ExpressionReader} reader The token reader.
64
+ * @return {boolean} Whether an addition or a subtraction follows.
65
+ */
66
+ function readsAdditiveOperator (reader) {
67
+ const token = peekToken(reader);
68
+ const isAdditive = token?.type === 'operator' && (token.operator === '+' || token.operator === '-');
69
+ if (!isAdditive) {
70
+ return false;
71
+ }
72
+ const operandToken = peekToken(reader, 1);
73
+ const signsItsNumber = operandToken?.type === 'number' && !operandToken.precededBySpace;
74
+ return !signsItsNumber;
75
+ }
76
+
77
+ /**
78
+ * Reads a sum: the terms a calculation adds together, in the order they are
79
+ * written.
80
+ *
81
+ * @param {ExpressionReader} reader The token reader.
82
+ * @return {Map|null} The unit totals the sum states, or null when it cannot be resolved.
83
+ */
84
+ function parseSum (reader) {
85
+ let value = parseProduct(reader);
86
+ if (!value) {
87
+ return null;
88
+ }
89
+ while (readsAdditiveOperator(reader)) {
90
+ const subtracts = peekToken(reader).operator === '-';
91
+ reader.index++;
92
+ const term = parseProduct(reader);
93
+ if (!term) {
94
+ return null;
95
+ }
96
+ value = addUnitTotals(value, term, subtracts ? -1 : 1);
97
+ if (!value) {
98
+ return null;
99
+ }
100
+ }
101
+ return value;
102
+ }
103
+
104
+ /**
105
+ * Reads a product: the operands a calculation multiplies and divides, which
106
+ * bind more tightly than the terms of a sum do.
107
+ *
108
+ * @param {ExpressionReader} reader The token reader.
109
+ * @return {Map|null} The unit totals the product states, or null when it cannot be resolved.
110
+ */
111
+ function parseProduct (reader) {
112
+ let value = parseOperand(reader);
113
+ if (!value) {
114
+ return null;
115
+ }
116
+ let token = peekToken(reader);
117
+ while (token?.type === 'operator' && (token.operator === '*' || token.operator === '/')) {
118
+ reader.index++;
119
+ const operand = parseOperand(reader);
120
+ if (!operand) {
121
+ return null;
122
+ }
123
+ if (token.operator === '*') {
124
+ value = multiplyUnitTotals(value, operand);
125
+ } else {
126
+ value = divideUnitTotals(value, operand);
127
+ }
128
+ if (!value) {
129
+ return null;
130
+ }
131
+ token = peekToken(reader);
132
+ }
133
+ return value;
134
+ }
135
+
136
+ /**
137
+ * Reads the number a sign is written against, which is the only place a
138
+ * calculation states a sign: CSS has no operator that negates what follows it,
139
+ * so a sign standing apart from its number, or a second sign written in front
140
+ * of the first, is not a calculation any browser reads.
141
+ *
142
+ * @param {ExpressionReader} reader The token reader.
143
+ * @return {Map|null} The unit totals the signed number states, or null when the sign signs nothing.
144
+ */
145
+ function parseSignedNumber (reader) {
146
+ const signToken = peekToken(reader);
147
+ const signedToken = peekToken(reader, 1);
148
+ const signsItsValue = (
149
+ (signedToken?.type === 'number' || signedToken?.type === 'identifier') &&
150
+ !signedToken.precededBySpace
151
+ );
152
+ if (!signsItsValue) {
153
+ return null;
154
+ }
155
+ reader.index++;
156
+ const value = parseOperand(reader);
157
+ if (!value) {
158
+ return null;
159
+ }
160
+ if (signToken.operator === '-') {
161
+ return scaleUnitTotals(value, -1);
162
+ }
163
+ return value;
164
+ }
165
+
166
+ /**
167
+ * Reads a single operand: a signed number, a number, a numeric constant, a
168
+ * parenthesized sum, or another math function.
169
+ *
170
+ * @param {ExpressionReader} reader The token reader.
171
+ * @return {Map|null} The unit totals the operand states, or null when it cannot be resolved.
172
+ */
173
+ function parseOperand (reader) {
174
+ const token = peekToken(reader);
175
+ if (!token) {
176
+ return null;
177
+ }
178
+ if (token.type === 'operator' && (token.operator === '+' || token.operator === '-')) {
179
+ return parseSignedNumber(reader);
180
+ }
181
+ if (token.type === 'number') {
182
+ reader.index++;
183
+ return createUnitTotals(token.number, token.unit, true);
184
+ }
185
+ if (token.type === 'identifier') {
186
+ reader.index++;
187
+ const constant = NUMERIC_CONSTANTS.get(token.name);
188
+ if (constant === undefined) {
189
+ return null;
190
+ }
191
+ return createUnitTotals(constant, UNITLESS);
192
+ }
193
+ if (token.type === 'function') {
194
+ reader.index++;
195
+ return parseMathFunction(reader, token.name);
196
+ }
197
+ if (token.type === 'open') {
198
+ reader.index++;
199
+ return parseGroup(reader);
200
+ }
201
+ return null;
202
+ }
203
+
204
+ /**
205
+ * Reads a sum written inside parentheses, which is read as one operand of the
206
+ * calculation around it.
207
+ *
208
+ * @param {ExpressionReader} reader The token reader.
209
+ * @return {Map|null} The unit totals the group states, or null when it cannot be resolved.
210
+ */
211
+ function parseGroup (reader) {
212
+ if (reader.depth >= MAXIMUM_NESTING_DEPTH) {
213
+ return null;
214
+ }
215
+ reader.depth++;
216
+ const value = parseSum(reader);
217
+ reader.depth--;
218
+ if (!value || peekToken(reader)?.type !== 'close') {
219
+ return null;
220
+ }
221
+ reader.index++;
222
+ return value;
223
+ }
224
+
225
+ /**
226
+ * Reads one argument of a math function, which is either a sum or a lone
227
+ * keyword such as the rounding strategy of `round()` or the open end of a
228
+ * `clamp()` range.
229
+ *
230
+ * @param {ExpressionReader} reader The token reader.
231
+ * @return {object|null} The argument read, or null when it cannot be resolved.
232
+ */
233
+ function parseFunctionArgument (reader) {
234
+ const token = peekToken(reader);
235
+ const followingToken = peekToken(reader, 1);
236
+ const standsAlone = followingToken?.type === 'comma' || followingToken?.type === 'close';
237
+ const isKeyword = token?.type === 'identifier' && standsAlone && !NUMERIC_CONSTANTS.has(token.name);
238
+ if (isKeyword) {
239
+ reader.index++;
240
+ return { keyword: token.name, value: null };
241
+ }
242
+ const value = parseSum(reader);
243
+ if (!value) {
244
+ return null;
245
+ }
246
+ return { keyword: null, value };
247
+ }
248
+
249
+ /**
250
+ * Reads the comma-separated arguments a math function is called with, up to
251
+ * the parenthesis that closes the call.
252
+ *
253
+ * @param {ExpressionReader} reader The token reader.
254
+ * @return {Array|null} The arguments read, or null when the call cannot be resolved.
255
+ */
256
+ function parseFunctionArguments (reader) {
257
+ const argumentList = [];
258
+ while (reader.index < reader.tokens.length) {
259
+ const argument = parseFunctionArgument(reader);
260
+ if (!argument) {
261
+ return null;
262
+ }
263
+ argumentList.push(argument);
264
+ const token = peekToken(reader);
265
+ if (token?.type === 'close') {
266
+ reader.index++;
267
+ return argumentList;
268
+ }
269
+ if (token?.type !== 'comma') {
270
+ return null;
271
+ }
272
+ reader.index++;
273
+ }
274
+ return null;
275
+ }
276
+
277
+ /**
278
+ * Reads the arguments of a math function and works out the value the function
279
+ * resolves to.
280
+ *
281
+ * @param {ExpressionReader} reader The token reader.
282
+ * @param {string} name The lowercased name of the function being called.
283
+ * @return {Map|null} The unit totals the function resolves to, or null when it cannot be resolved.
284
+ */
285
+ function parseMathFunction (reader, name) {
286
+ const resolveFunction = MATH_FUNCTION_EVALUATORS.get(name);
287
+ if (!resolveFunction || reader.depth >= MAXIMUM_NESTING_DEPTH) {
288
+ return null;
289
+ }
290
+ reader.depth++;
291
+ const argumentList = parseFunctionArguments(reader);
292
+ reader.depth--;
293
+ if (!argumentList) {
294
+ return null;
295
+ }
296
+ return resolveFunction(argumentList);
297
+ }
298
+
299
+ export {
300
+ parseSum
301
+ };
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @file Holds what a math function resolved to inside the range of values the property it is declared on accepts.
3
+ */
4
+
5
+ import { formatResolvedNumber } from '../shared.js';
6
+
7
+ /**
8
+ * The properties whose values every CSS specification defines as
9
+ * non-negative, so that a math function declared on one of them has its
10
+ * result clamped rather than read as written.
11
+ *
12
+ * @type {Set<string>}
13
+ */
14
+ const NON_NEGATIVE_PROPERTIES = new Set([
15
+ 'animation-duration',
16
+ 'animation-iteration-count',
17
+ 'background-size',
18
+ 'block-size',
19
+ 'border-block-end-width',
20
+ 'border-block-start-width',
21
+ 'border-block-width',
22
+ 'border-bottom-left-radius',
23
+ 'border-bottom-right-radius',
24
+ 'border-bottom-width',
25
+ 'border-end-end-radius',
26
+ 'border-end-start-radius',
27
+ 'border-image-outset',
28
+ 'border-image-width',
29
+ 'border-inline-end-width',
30
+ 'border-inline-start-width',
31
+ 'border-inline-width',
32
+ 'border-left-width',
33
+ 'border-radius',
34
+ 'border-right-width',
35
+ 'border-start-end-radius',
36
+ 'border-start-start-radius',
37
+ 'border-top-left-radius',
38
+ 'border-top-right-radius',
39
+ 'border-top-width',
40
+ 'border-width',
41
+ 'column-count',
42
+ 'column-gap',
43
+ 'column-rule-width',
44
+ 'column-width',
45
+ 'flex-basis',
46
+ 'flex-grow',
47
+ 'flex-shrink',
48
+ 'font-size',
49
+ 'gap',
50
+ 'height',
51
+ 'inline-size',
52
+ 'line-height',
53
+ 'mask-size',
54
+ 'max-block-size',
55
+ 'max-height',
56
+ 'max-inline-size',
57
+ 'max-width',
58
+ 'min-block-size',
59
+ 'min-height',
60
+ 'min-inline-size',
61
+ 'min-width',
62
+ 'outline-width',
63
+ 'padding',
64
+ 'padding-block',
65
+ 'padding-block-end',
66
+ 'padding-block-start',
67
+ 'padding-bottom',
68
+ 'padding-inline',
69
+ 'padding-inline-end',
70
+ 'padding-inline-start',
71
+ 'padding-left',
72
+ 'padding-right',
73
+ 'padding-top',
74
+ 'perspective',
75
+ 'row-gap',
76
+ 'shape-margin',
77
+ 'stroke-width',
78
+ 'tab-size',
79
+ 'transition-duration',
80
+ 'width'
81
+ ]);
82
+
83
+ /**
84
+ * Properties whose computed math values must be integers.
85
+ *
86
+ * @type {Set<string>}
87
+ */
88
+ const INTEGER_VALUE_PROPERTIES = new Set([
89
+ 'column-count',
90
+ 'order',
91
+ 'orphans',
92
+ 'widows',
93
+ 'z-index'
94
+ ]);
95
+
96
+ /**
97
+ * The inclusive bounds that CSS applies to numeric property values after
98
+ * resolving a math expression.
99
+ *
100
+ * @type {Map<string, {minimum?: number, maximum?: number}>}
101
+ */
102
+ const NUMERIC_PROPERTY_RANGES = new Map([
103
+ ['column-count', { minimum: 1 }],
104
+ ['opacity', { minimum: 0, maximum: 1 }]
105
+ ]);
106
+
107
+ /**
108
+ * Matches a value written as a single negative number, with or without the
109
+ * unit or percent sign that follows its digits.
110
+ *
111
+ * @type {RegExp}
112
+ */
113
+ const NEGATIVE_NUMBER_VALUE = /^-(?:\d+|\d*\.\d+)(?:[a-z]+|%)?$/i;
114
+
115
+ /**
116
+ * Matches a resolved unitless number whose value can be rounded or clamped by
117
+ * a property's numeric range.
118
+ *
119
+ * @type {RegExp}
120
+ */
121
+ const UNIT_LESS_NUMBER_VALUE = /^[+-]?(?:\d+|\d*\.\d+)$/;
122
+
123
+ /**
124
+ * Clamps what a math function resolved to into the range its property
125
+ * accepts. A negative length written out directly makes the declaration
126
+ * invalid and the CSS engine throws it away, but a math function is allowed
127
+ * to resolve to one: its result is clamped to the nearest value the property
128
+ * does take, which on a property that only accepts non-negative values is
129
+ * zero, as the range checking section of the CSS Values specification
130
+ * describes: https://drafts.csswg.org/css-values/#calc-range.
131
+ *
132
+ * @param {string} resolvedValue The value the math function resolved to.
133
+ * @param {string} property The property the math function is declared on.
134
+ * @return {string} The resolved value, held inside the property's range.
135
+ */
136
+ function clampResolvedValueToPropertyRange (resolvedValue, property) {
137
+ const normalizedProperty = String(property).toLowerCase();
138
+ const acceptsNegativeValues = !NON_NEGATIVE_PROPERTIES.has(normalizedProperty);
139
+ const hasPropertyRange = NUMERIC_PROPERTY_RANGES.has(normalizedProperty);
140
+ const range = NUMERIC_PROPERTY_RANGES.get(normalizedProperty) || {};
141
+ const isNegativeNonNegativeValue = !acceptsNegativeValues && NEGATIVE_NUMBER_VALUE.test(resolvedValue);
142
+ if (isNegativeNonNegativeValue) {
143
+ const isUnitlessNumber = UNIT_LESS_NUMBER_VALUE.test(resolvedValue);
144
+ if (!isUnitlessNumber) {
145
+ return formatResolvedNumber(range.minimum === undefined ? 0 : range.minimum);
146
+ }
147
+ if (!hasPropertyRange && !INTEGER_VALUE_PROPERTIES.has(normalizedProperty)) {
148
+ return '0';
149
+ }
150
+ }
151
+ if (!hasPropertyRange && !INTEGER_VALUE_PROPERTIES.has(normalizedProperty)) {
152
+ return resolvedValue;
153
+ }
154
+ if (!UNIT_LESS_NUMBER_VALUE.test(resolvedValue)) {
155
+ return resolvedValue;
156
+ }
157
+
158
+ const shouldRoundToInteger = INTEGER_VALUE_PROPERTIES.has(normalizedProperty);
159
+ let numericValue = Number(resolvedValue);
160
+ if (shouldRoundToInteger) {
161
+ numericValue = Math.round(numericValue);
162
+ }
163
+ if (range.minimum !== undefined) {
164
+ numericValue = Math.max(range.minimum, numericValue);
165
+ }
166
+ if (range.maximum !== undefined) {
167
+ numericValue = Math.min(range.maximum, numericValue);
168
+ }
169
+ return formatResolvedNumber(numericValue);
170
+ }
171
+
172
+ export {
173
+ clampResolvedValueToPropertyRange
174
+ };
@@ -0,0 +1,222 @@
1
+ /**
2
+ * @file Rewrites the contents of CSS math functions into shorter equivalents, such as dropping redundant zeros from decimals and turning reciprocal multiplications into divisions.
3
+ */
4
+
5
+ import { findMatchingParenthesis } from '../../parser/source-search.js';
6
+
7
+ import { MATH_FUNCTION_NAMES } from './functions.js';
8
+
9
+ /**
10
+ * Matches a single character that may appear in a CSS function name.
11
+ *
12
+ * @type {RegExp}
13
+ */
14
+ const FUNCTION_NAME_CHARACTER = /[a-zA-Z-]/;
15
+
16
+ /**
17
+ * Finds where the function name that ends at an opening parenthesis begins.
18
+ *
19
+ * @param {string} value The CSS value string being scanned.
20
+ * @param {number} openIndex The index of the opening parenthesis.
21
+ * @return {number} The index of the function name's first character.
22
+ */
23
+ function findFunctionNameStart (value, openIndex) {
24
+ let index = openIndex;
25
+ while (index > 0 && FUNCTION_NAME_CHARACTER.test(value[index - 1])) {
26
+ index--;
27
+ }
28
+ return index;
29
+ }
30
+
31
+ /**
32
+ * Rewrites the contents of every math function in a value, leaving the rest of
33
+ * the value untouched. A `*` only means multiplication inside these functions,
34
+ * so an arithmetic rewrite may only reach the text between their parentheses.
35
+ * The whole of a math function is handed over at once, so a math function
36
+ * nested in another is rewritten as part of its parent.
37
+ *
38
+ * @param {function(string): string} rewriteExpression Receives a math function's contents and returns its replacement.
39
+ * @param {string} value The CSS value string to rewrite.
40
+ * @return {string} The value with every math function's contents rewritten.
41
+ */
42
+ function rewriteMathFunctionContents (rewriteExpression, value) {
43
+ let result = '';
44
+ let index = 0;
45
+ while (index < value.length) {
46
+ const openIndex = value.indexOf('(', index);
47
+ if (openIndex === -1) {
48
+ break;
49
+ }
50
+ const functionName = value.slice(findFunctionNameStart(value, openIndex), openIndex).toLowerCase();
51
+ const closeIndex = findMatchingParenthesis(value, openIndex);
52
+ if (!MATH_FUNCTION_NAMES.has(functionName) || closeIndex === -1) {
53
+ result += value.slice(index, openIndex + 1);
54
+ index = openIndex + 1;
55
+ continue;
56
+ }
57
+ result += value.slice(index, openIndex + 1);
58
+ result += rewriteExpression(value.slice(openIndex + 1, closeIndex));
59
+ index = closeIndex;
60
+ }
61
+ return result + value.slice(index);
62
+ }
63
+
64
+ /**
65
+ * Matches a number written with a decimal point that begins a token of its
66
+ * own, capturing its sign, the digits before its decimal point, and the digits
67
+ * after it. The lookbehind keeps the match from starting partway through an
68
+ * identifier (`--x0.5`) or another number, and the lookahead keeps it from
69
+ * matching a fragment of a dotted name such as `0.5.png`.
70
+ *
71
+ * @type {RegExp}
72
+ */
73
+ const DECIMAL_NUMBER = /(?<![\w.-])([+-]?)(\d*)\.(\d+)(?![\d.])/g;
74
+
75
+ /**
76
+ * Writes a decimal number in the fewest characters that state the same value,
77
+ * dropping the zeros before its whole part (`0.25` → `.25`) and after its
78
+ * fraction (`1.50` → `1.5`), along with a decimal point left with no fraction
79
+ * behind it (`2.0` → `2`). The digits that remain are kept as written, so no
80
+ * precision is lost.
81
+ *
82
+ * @param {string} sign The sign written before the number, empty when there is none.
83
+ * @param {string} wholeDigits The digits written before the decimal point.
84
+ * @param {string} fractionDigits The digits written after the decimal point.
85
+ * @return {string} The number written as compactly as it can be.
86
+ */
87
+ function shortenDecimalNumber (sign, wholeDigits, fractionDigits) {
88
+ // Leading zeros of the whole part
89
+ const wholePart = wholeDigits.replace(/^0+/, '');
90
+ // Trailing zeros of the fraction
91
+ const fractionPart = fractionDigits.replace(/0+$/, '');
92
+ if (!fractionPart) {
93
+ return sign + (wholePart || '0');
94
+ }
95
+ return sign + wholePart + '.' + fractionPart;
96
+ }
97
+
98
+ /**
99
+ * Shortens every decimal number in the contents of a math function. Numbers
100
+ * that follow an operator written without whitespace (`x*0.25s`) are out of
101
+ * reach of the general number shortening, which only looks past a separator.
102
+ *
103
+ * @param {string} expression The contents of a math function.
104
+ * @return {string} The expression with every decimal number written as compactly as it can be.
105
+ */
106
+ function shortenDecimalNumbers (expression) {
107
+ return expression.replace(DECIMAL_NUMBER, (match, sign, wholeDigits, fractionDigits) => {
108
+ return shortenDecimalNumber(sign, wholeDigits, fractionDigits);
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Matches a multiplication by a decimal below one, capturing the digits after
114
+ * its decimal point and the unit written on it (so a multiplier with a unit
115
+ * can be recognized and left alone). The trailing lookahead keeps
116
+ * the match away from numbers that only begin this way, such as the exponent
117
+ * of `.5e3` or a unit name that is still being read.
118
+ *
119
+ * @type {RegExp}
120
+ */
121
+ const FRACTIONAL_MULTIPLIER = /\s*\*\s*0*\.(\d+)([a-z%]*)(?![\w.%(])/gi;
122
+
123
+ /**
124
+ * The most decimal places a fraction may be built from while the power of ten
125
+ * it needs is still exactly representable as an integer.
126
+ *
127
+ * @type {number}
128
+ */
129
+ const MAXIMUM_FRACTION_DIGITS = 15;
130
+
131
+ /**
132
+ * Computes the greatest common divisor of two non-negative integers, which is
133
+ * what a fraction has to be divided by to be stated in lowest terms.
134
+ *
135
+ * @param {number} first The first integer.
136
+ * @param {number} second The second integer.
137
+ * @return {number} The greatest common divisor of both integers.
138
+ */
139
+ function greatestCommonDivisor (first, second) {
140
+ let dividend = first;
141
+ let divisor = second;
142
+ while (divisor) {
143
+ const remainder = dividend % divisor;
144
+ dividend = divisor;
145
+ divisor = remainder;
146
+ }
147
+ return dividend;
148
+ }
149
+
150
+ /**
151
+ * Converts the digits behind a decimal point into the whole number that
152
+ * dividing by scales a value exactly as multiplying by that decimal does.
153
+ * The digits `25` (of `.25`) state the fraction 25/100, which reduces to 1/4,
154
+ * so the divisor is `4`. A fraction that does not reduce to a numerator of one
155
+ * cannot be stated as a single division, and so has no divisor to give.
156
+ *
157
+ * @param {string} fractionDigits The digits written after the decimal point.
158
+ * @return {number|null} The equivalent whole number divisor, or null when there is none.
159
+ */
160
+ function reciprocalOfDecimalFraction (fractionDigits) {
161
+ if (fractionDigits.length > MAXIMUM_FRACTION_DIGITS) {
162
+ return null;
163
+ }
164
+ const numerator = Number(fractionDigits);
165
+ const denominator = 10 ** fractionDigits.length;
166
+ const commonFactor = greatestCommonDivisor(numerator, denominator);
167
+ if (numerator / commonFactor !== 1) {
168
+ return null;
169
+ }
170
+ return denominator / commonFactor;
171
+ }
172
+
173
+ /**
174
+ * Builds the division that scales a value exactly as multiplying it by a
175
+ * unitless decimal below one does, when that division is no longer to write
176
+ * than the multiplication. A multiplier carrying a unit is never rewritten:
177
+ * dividing by a dimension inverts its unit in the type of the result
178
+ * (`x*.25s` is a time, while `x/4s` is per unit of time), so the two would not
179
+ * be equivalent.
180
+ *
181
+ * @param {string} fractionDigits The digits written after the decimal point of the multiplier.
182
+ * @param {string} unit The unit written on the multiplier, empty when it is unitless.
183
+ * @return {string|null} The equivalent division, or null when there is none worth using.
184
+ */
185
+ function divisionEquivalentToFractionalMultiplier (fractionDigits, unit) {
186
+ if (unit) {
187
+ return null;
188
+ }
189
+ const divisor = reciprocalOfDecimalFraction(fractionDigits);
190
+ if (divisor === null) {
191
+ return null;
192
+ }
193
+ const division = '/' + divisor;
194
+ // Compared against the shortest the multiplication could have been written
195
+ const multiplication = '*.' + fractionDigits;
196
+ if (division.length > multiplication.length) {
197
+ return null;
198
+ }
199
+ return division;
200
+ }
201
+
202
+ /**
203
+ * Rewrites a multiplication by a unitless decimal below one as a division by
204
+ * the whole number that decimal is the reciprocal of, which is never longer to
205
+ * write and usually shorter (`calc(var(--x)*.25)` → `calc(var(--x)/4)`).
206
+ * Multipliers with a unit are left as written, since dividing by a dimension
207
+ * would invert its unit.
208
+ *
209
+ * @param {string} expression The contents of a math function.
210
+ * @return {string} The expression with reciprocal multipliers stated as divisions.
211
+ */
212
+ function convertReciprocalMultiplicationToDivision (expression) {
213
+ return expression.replace(FRACTIONAL_MULTIPLIER, (match, fractionDigits, unit) => {
214
+ return divisionEquivalentToFractionalMultiplier(fractionDigits, unit) ?? match;
215
+ });
216
+ }
217
+
218
+ export {
219
+ convertReciprocalMultiplicationToDivision,
220
+ rewriteMathFunctionContents,
221
+ shortenDecimalNumbers
222
+ };