@thejaredwilcurt/csslop 0.0.40 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +2 -5
- package/src/value/math-expression.js +1103 -0
- package/src/value/math.js +110 -99
- package/vite.config.js +0 -4
|
@@ -0,0 +1,1103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Reads the arithmetic a CSS math function states and resolves it into the value it works out to, whenever every operand of it is a number already known at minification time.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
convertAbsoluteLengthToPx,
|
|
7
|
+
formatResolvedNumber
|
|
8
|
+
} from './shared.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The unit a plain number is totalled under. A number carries no unit, and the
|
|
12
|
+
* empty string is the unit it does not carry.
|
|
13
|
+
*
|
|
14
|
+
* @type {string}
|
|
15
|
+
*/
|
|
16
|
+
const UNITLESS = '';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The unit a percentage is totalled under. A percentage is only resolved
|
|
20
|
+
* against its basis once the element is laid out, so it is totalled as a unit
|
|
21
|
+
* of its own rather than as the number it is written from.
|
|
22
|
+
*
|
|
23
|
+
* @type {string}
|
|
24
|
+
*/
|
|
25
|
+
const PERCENTAGE = '%';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The numeric constants a calculation may name instead of writing out, as the
|
|
29
|
+
* keywords section of the CSS Values specification defines them:
|
|
30
|
+
* https://drafts.csswg.org/css-values/#calc-keywords.
|
|
31
|
+
*
|
|
32
|
+
* @type {Map<string, number>}
|
|
33
|
+
*/
|
|
34
|
+
const NUMERIC_CONSTANTS = new Map([
|
|
35
|
+
['e', Math.E],
|
|
36
|
+
['infinity', Infinity],
|
|
37
|
+
['nan', NaN],
|
|
38
|
+
['pi', Math.PI]
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* How each rounding strategy of `round()` turns the number of intervals it is
|
|
43
|
+
* given into the whole number of them it keeps.
|
|
44
|
+
*
|
|
45
|
+
* @type {Map<string, function(number): number>}
|
|
46
|
+
*/
|
|
47
|
+
const ROUNDING_STRATEGIES = new Map([
|
|
48
|
+
['nearest', Math.round],
|
|
49
|
+
['up', Math.ceil],
|
|
50
|
+
['down', Math.floor],
|
|
51
|
+
['to-zero', Math.trunc]
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The deepest a calculation may nest one set of parentheses inside another
|
|
56
|
+
* before the evaluator gives up on it. Every level of nesting is a level of
|
|
57
|
+
* recursion, and a stylesheet states nothing at a depth anywhere near this.
|
|
58
|
+
*
|
|
59
|
+
* @type {number}
|
|
60
|
+
*/
|
|
61
|
+
const MAXIMUM_NESTING_DEPTH = 32;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates the totals of a value that states a single quantity, holding the
|
|
65
|
+
* number it is written from under the unit it is written with. An absolute
|
|
66
|
+
* length is held as the pixels it measures, since every length stating the
|
|
67
|
+
* same measurement then totals under the same unit.
|
|
68
|
+
*
|
|
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
|
+
* @return {Map} The unit totals the quantity states.
|
|
72
|
+
*/
|
|
73
|
+
function createUnitTotals (number, unit) {
|
|
74
|
+
const lowercasedUnit = unit.toLowerCase();
|
|
75
|
+
const pixels = convertAbsoluteLengthToPx(number, lowercasedUnit);
|
|
76
|
+
if (pixels === null) {
|
|
77
|
+
return new Map([[lowercasedUnit, number]]);
|
|
78
|
+
}
|
|
79
|
+
return new Map([['px', pixels]]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reports whether a value states a plain number, which is what an operand has
|
|
84
|
+
* to be to scale or divide the quantity it is written against.
|
|
85
|
+
*
|
|
86
|
+
* @param {Map} value The unit totals the value holds.
|
|
87
|
+
* @return {boolean} Whether the value states a plain number.
|
|
88
|
+
*/
|
|
89
|
+
function isPlainNumber (value) {
|
|
90
|
+
return value.size === 1 && value.has(UNITLESS);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Reads a value as the single quantity it states, which is what a function
|
|
95
|
+
* comparing its arguments needs each of them to be. A value totalling more
|
|
96
|
+
* than one unit is only resolved once its units are resolved against the
|
|
97
|
+
* element, so it states no single number to read.
|
|
98
|
+
*
|
|
99
|
+
* @param {Map} value The unit totals the value holds.
|
|
100
|
+
* @return {object|null} The number and the unit the value states, or null when it states more than one quantity.
|
|
101
|
+
*/
|
|
102
|
+
function readSingleQuantity (value) {
|
|
103
|
+
if (value.size !== 1) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const [[unit, number]] = [...value];
|
|
107
|
+
return { number, unit };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Scales every quantity a value states by a plain number, which is how a
|
|
112
|
+
* multiplication and a division are worked out.
|
|
113
|
+
*
|
|
114
|
+
* @param {Map} value The unit totals the value holds.
|
|
115
|
+
* @param {number} factor The number every total is scaled by.
|
|
116
|
+
* @return {Map} The scaled unit totals.
|
|
117
|
+
*/
|
|
118
|
+
function scaleUnitTotals (value, factor) {
|
|
119
|
+
const scaled = new Map();
|
|
120
|
+
for (const [unit, coefficient] of value) {
|
|
121
|
+
scaled.set(unit, coefficient * factor);
|
|
122
|
+
}
|
|
123
|
+
return scaled;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Adds one value to another, totalling the quantities they state under the
|
|
128
|
+
* units they are written with. A sum of a plain number and a quantity carrying
|
|
129
|
+
* a unit states nothing a stylesheet can hold, so such a sum has no total to
|
|
130
|
+
* give.
|
|
131
|
+
*
|
|
132
|
+
* @param {Map} left The unit totals the value on the left holds.
|
|
133
|
+
* @param {Map} right The unit totals the value on the right holds.
|
|
134
|
+
* @param {number} sign The direction the right value is added in: 1 to add it, -1 to subtract it.
|
|
135
|
+
* @return {Map|null} The totals of the sum, or null when the two cannot be added.
|
|
136
|
+
*/
|
|
137
|
+
function addUnitTotals (left, right, sign) {
|
|
138
|
+
const totals = new Map(left);
|
|
139
|
+
for (const [unit, coefficient] of right) {
|
|
140
|
+
totals.set(unit, (totals.get(unit) || 0) + (coefficient * sign));
|
|
141
|
+
}
|
|
142
|
+
const addsNumberToQuantity = totals.has(UNITLESS) && totals.size > 1;
|
|
143
|
+
if (addsNumberToQuantity) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
return totals;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Multiplies two values, which CSS only allows when one of them is a plain
|
|
151
|
+
* number: the product of two quantities carrying units measures an area or a
|
|
152
|
+
* rate, and no CSS property takes one.
|
|
153
|
+
*
|
|
154
|
+
* @param {Map} left The unit totals the value on the left holds.
|
|
155
|
+
* @param {Map} right The unit totals the value on the right holds.
|
|
156
|
+
* @return {Map|null} The totals of the product, or null when neither side is a plain number.
|
|
157
|
+
*/
|
|
158
|
+
function multiplyUnitTotals (left, right) {
|
|
159
|
+
if (isPlainNumber(right)) {
|
|
160
|
+
return scaleUnitTotals(left, right.get(UNITLESS));
|
|
161
|
+
}
|
|
162
|
+
if (isPlainNumber(left)) {
|
|
163
|
+
return scaleUnitTotals(right, left.get(UNITLESS));
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Divides a value by another, which CSS only allows when the divisor is a
|
|
170
|
+
* plain number. A division by zero resolves to an infinity that no property
|
|
171
|
+
* takes, so it is left for the stylesheet to state as written.
|
|
172
|
+
*
|
|
173
|
+
* @param {Map} left The unit totals of the value being divided.
|
|
174
|
+
* @param {Map} right The unit totals of the value dividing it.
|
|
175
|
+
* @return {Map|null} The totals of the quotient, or null when the division cannot be worked out.
|
|
176
|
+
*/
|
|
177
|
+
function divideUnitTotals (left, right) {
|
|
178
|
+
if (!isPlainNumber(right)) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const divisor = right.get(UNITLESS);
|
|
182
|
+
if (divisor === 0) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return scaleUnitTotals(left, 1 / divisor);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Matches a CSS number where the scanner stands: the digits it is written
|
|
190
|
+
* from, together with the fraction and the exponent it may be written with.
|
|
191
|
+
* A sign is left to the parser, which tells the sign of a number from an
|
|
192
|
+
* operator by whether whitespace stands between the two.
|
|
193
|
+
*
|
|
194
|
+
* @type {RegExp}
|
|
195
|
+
*/
|
|
196
|
+
const NUMBER_AT_INDEX = /(?:\d+\.\d+|\.\d+|\d+)(?:e[+-]?\d+)?/iy;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Matches a CSS identifier where the scanner stands: a name of letters,
|
|
200
|
+
* digits, underscores, and hyphens that does not begin with a digit. A numeric
|
|
201
|
+
* constant, a function name, and a keyword argument are all written as
|
|
202
|
+
* identifiers.
|
|
203
|
+
*
|
|
204
|
+
* @type {RegExp}
|
|
205
|
+
*/
|
|
206
|
+
const IDENTIFIER_AT_INDEX = /[a-z_][a-z0-9_-]*/iy;
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Matches the unit written against a number where the scanner stands. Every
|
|
210
|
+
* CSS unit is named in letters alone, so a name holding anything else is a
|
|
211
|
+
* token of its own that a number cannot carry.
|
|
212
|
+
*
|
|
213
|
+
* @type {RegExp}
|
|
214
|
+
*/
|
|
215
|
+
const UNIT_AT_INDEX = /[a-z]+/iy;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The characters CSS counts as whitespace, which separates the tokens of a
|
|
219
|
+
* calculation and tells an operator apart from the sign of a number.
|
|
220
|
+
*
|
|
221
|
+
* @type {Set<string>}
|
|
222
|
+
*/
|
|
223
|
+
const WHITESPACE_CHARACTERS = new Set([' ', '\t', '\n', '\r', '\f']);
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* The characters that punctuate a calculation, and the kind of token each of
|
|
227
|
+
* them stands for.
|
|
228
|
+
*
|
|
229
|
+
* @type {Map<string, string>}
|
|
230
|
+
*/
|
|
231
|
+
const PUNCTUATION_TOKEN_TYPES = new Map([
|
|
232
|
+
['(', 'open'],
|
|
233
|
+
[')', 'close'],
|
|
234
|
+
[',', 'comma']
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The arithmetic operators a calculation is written with.
|
|
239
|
+
*
|
|
240
|
+
* @type {Set<string>}
|
|
241
|
+
*/
|
|
242
|
+
const OPERATOR_CHARACTERS = new Set(['+', '-', '*', '/']);
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Reads the unit written directly against the number that ends at the given
|
|
246
|
+
* index, which is a percent sign or an identifier naming the unit. A number
|
|
247
|
+
* with nothing written against it carries no unit.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} text The math function being tokenized.
|
|
250
|
+
* @param {number} index The index just past the number's digits.
|
|
251
|
+
* @return {object} The unit read and the index just past it.
|
|
252
|
+
*/
|
|
253
|
+
function readUnitAtIndex (text, index) {
|
|
254
|
+
if (text[index] === PERCENTAGE) {
|
|
255
|
+
return { unit: PERCENTAGE, nextIndex: index + 1 };
|
|
256
|
+
}
|
|
257
|
+
UNIT_AT_INDEX.lastIndex = index;
|
|
258
|
+
const unitMatch = UNIT_AT_INDEX.exec(text);
|
|
259
|
+
if (!unitMatch) {
|
|
260
|
+
return { unit: UNITLESS, nextIndex: index };
|
|
261
|
+
}
|
|
262
|
+
return { unit: unitMatch[0], nextIndex: index + unitMatch[0].length };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Splits a math function into the tokens it is written from: its numbers with
|
|
267
|
+
* the units they carry, its identifiers, its function names, its operators,
|
|
268
|
+
* and its punctuation. Each token records whether whitespace precedes it,
|
|
269
|
+
* since that is what tells the sign of a number from an operator.
|
|
270
|
+
*
|
|
271
|
+
* @param {string} text The math function as it is written.
|
|
272
|
+
* @return {Array|null} The tokens of the math function, or null when it holds a character no calculation is written with.
|
|
273
|
+
*/
|
|
274
|
+
function tokenizeMathExpression (text) {
|
|
275
|
+
const tokens = [];
|
|
276
|
+
let index = 0;
|
|
277
|
+
let precededBySpace = false;
|
|
278
|
+
|
|
279
|
+
while (index < text.length) {
|
|
280
|
+
const character = text[index];
|
|
281
|
+
|
|
282
|
+
if (WHITESPACE_CHARACTERS.has(character)) {
|
|
283
|
+
precededBySpace = true;
|
|
284
|
+
index++;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
NUMBER_AT_INDEX.lastIndex = index;
|
|
289
|
+
const numberMatch = NUMBER_AT_INDEX.exec(text);
|
|
290
|
+
if (numberMatch) {
|
|
291
|
+
const { unit, nextIndex } = readUnitAtIndex(text, index + numberMatch[0].length);
|
|
292
|
+
tokens.push({
|
|
293
|
+
type: 'number',
|
|
294
|
+
number: Number(numberMatch[0]),
|
|
295
|
+
unit,
|
|
296
|
+
precededBySpace
|
|
297
|
+
});
|
|
298
|
+
index = nextIndex;
|
|
299
|
+
precededBySpace = false;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (OPERATOR_CHARACTERS.has(character) || PUNCTUATION_TOKEN_TYPES.has(character)) {
|
|
304
|
+
tokens.push({
|
|
305
|
+
type: PUNCTUATION_TOKEN_TYPES.get(character) || 'operator',
|
|
306
|
+
operator: character,
|
|
307
|
+
precededBySpace
|
|
308
|
+
});
|
|
309
|
+
index++;
|
|
310
|
+
precededBySpace = false;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
IDENTIFIER_AT_INDEX.lastIndex = index;
|
|
315
|
+
const identifierMatch = IDENTIFIER_AT_INDEX.exec(text);
|
|
316
|
+
if (!identifierMatch) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
index = index + identifierMatch[0].length;
|
|
320
|
+
// A name written directly against an opening parenthesis calls a function
|
|
321
|
+
const callsFunction = text[index] === '(';
|
|
322
|
+
if (callsFunction) {
|
|
323
|
+
index++;
|
|
324
|
+
}
|
|
325
|
+
tokens.push({
|
|
326
|
+
type: callsFunction ? 'function' : 'identifier',
|
|
327
|
+
name: identifierMatch[0].toLowerCase(),
|
|
328
|
+
precededBySpace
|
|
329
|
+
});
|
|
330
|
+
precededBySpace = false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return tokens;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @typedef {object} ExpressionReader
|
|
338
|
+
* @property {Array} tokens The tokens of the math function, in the order they are written.
|
|
339
|
+
* @property {number} index The token the parser reads next.
|
|
340
|
+
* @property {number} depth How many sets of parentheses the parser is reading inside of.
|
|
341
|
+
*/
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Reads the token the parser stands on, or one of the tokens after it.
|
|
345
|
+
*
|
|
346
|
+
* @param {ExpressionReader} reader The token reader.
|
|
347
|
+
* @param {number} offset How many tokens past the current one to read.
|
|
348
|
+
* @return {object|undefined} The token read, or undefined past the end of the expression.
|
|
349
|
+
*/
|
|
350
|
+
function peekToken (reader, offset = 0) {
|
|
351
|
+
return reader.tokens[reader.index + offset];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Reports whether the parser stands on an operator that adds or subtracts what
|
|
356
|
+
* follows it from what precedes it. A sign written directly against its number
|
|
357
|
+
* is part of that number rather than an operator, so a value written with one
|
|
358
|
+
* sits against the value before it with no operator between them, which is not
|
|
359
|
+
* a calculation any browser reads.
|
|
360
|
+
*
|
|
361
|
+
* @param {ExpressionReader} reader The token reader.
|
|
362
|
+
* @return {boolean} Whether an addition or a subtraction follows.
|
|
363
|
+
*/
|
|
364
|
+
function readsAdditiveOperator (reader) {
|
|
365
|
+
const token = peekToken(reader);
|
|
366
|
+
const isAdditive = token?.type === 'operator' && (token.operator === '+' || token.operator === '-');
|
|
367
|
+
if (!isAdditive) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
const operandToken = peekToken(reader, 1);
|
|
371
|
+
const signsItsNumber = operandToken?.type === 'number' && !operandToken.precededBySpace;
|
|
372
|
+
return !signsItsNumber;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Reads a sum: the terms a calculation adds together, in the order they are
|
|
377
|
+
* written.
|
|
378
|
+
*
|
|
379
|
+
* @param {ExpressionReader} reader The token reader.
|
|
380
|
+
* @return {Map|null} The unit totals the sum states, or null when it cannot be resolved.
|
|
381
|
+
*/
|
|
382
|
+
function parseSum (reader) {
|
|
383
|
+
let value = parseProduct(reader);
|
|
384
|
+
if (!value) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
while (readsAdditiveOperator(reader)) {
|
|
388
|
+
const subtracts = peekToken(reader).operator === '-';
|
|
389
|
+
reader.index++;
|
|
390
|
+
const term = parseProduct(reader);
|
|
391
|
+
if (!term) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
value = addUnitTotals(value, term, subtracts ? -1 : 1);
|
|
395
|
+
if (!value) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return value;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Reads a product: the operands a calculation multiplies and divides, which
|
|
404
|
+
* bind more tightly than the terms of a sum do.
|
|
405
|
+
*
|
|
406
|
+
* @param {ExpressionReader} reader The token reader.
|
|
407
|
+
* @return {Map|null} The unit totals the product states, or null when it cannot be resolved.
|
|
408
|
+
*/
|
|
409
|
+
function parseProduct (reader) {
|
|
410
|
+
let value = parseOperand(reader);
|
|
411
|
+
if (!value) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
let token = peekToken(reader);
|
|
415
|
+
while (token?.type === 'operator' && (token.operator === '*' || token.operator === '/')) {
|
|
416
|
+
reader.index++;
|
|
417
|
+
const operand = parseOperand(reader);
|
|
418
|
+
if (!operand) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
if (token.operator === '*') {
|
|
422
|
+
value = multiplyUnitTotals(value, operand);
|
|
423
|
+
} else {
|
|
424
|
+
value = divideUnitTotals(value, operand);
|
|
425
|
+
}
|
|
426
|
+
if (!value) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
token = peekToken(reader);
|
|
430
|
+
}
|
|
431
|
+
return value;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Reads the number a sign is written against, which is the only place a
|
|
436
|
+
* calculation states a sign: CSS has no operator that negates what follows it,
|
|
437
|
+
* so a sign standing apart from its number, or a second sign written in front
|
|
438
|
+
* of the first, is not a calculation any browser reads.
|
|
439
|
+
*
|
|
440
|
+
* @param {ExpressionReader} reader The token reader.
|
|
441
|
+
* @return {Map|null} The unit totals the signed number states, or null when the sign signs nothing.
|
|
442
|
+
*/
|
|
443
|
+
function parseSignedNumber (reader) {
|
|
444
|
+
const signToken = peekToken(reader);
|
|
445
|
+
const signedToken = peekToken(reader, 1);
|
|
446
|
+
const signsItsValue = (
|
|
447
|
+
(signedToken?.type === 'number' || signedToken?.type === 'identifier') &&
|
|
448
|
+
!signedToken.precededBySpace
|
|
449
|
+
);
|
|
450
|
+
if (!signsItsValue) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
reader.index++;
|
|
454
|
+
const value = parseOperand(reader);
|
|
455
|
+
if (!value) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
if (signToken.operator === '-') {
|
|
459
|
+
return scaleUnitTotals(value, -1);
|
|
460
|
+
}
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Reads a single operand: a signed number, a number, a numeric constant, a
|
|
466
|
+
* parenthesized sum, or another math function.
|
|
467
|
+
*
|
|
468
|
+
* @param {ExpressionReader} reader The token reader.
|
|
469
|
+
* @return {Map|null} The unit totals the operand states, or null when it cannot be resolved.
|
|
470
|
+
*/
|
|
471
|
+
function parseOperand (reader) {
|
|
472
|
+
const token = peekToken(reader);
|
|
473
|
+
if (!token) {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
if (token.type === 'operator' && (token.operator === '+' || token.operator === '-')) {
|
|
477
|
+
return parseSignedNumber(reader);
|
|
478
|
+
}
|
|
479
|
+
if (token.type === 'number') {
|
|
480
|
+
reader.index++;
|
|
481
|
+
return createUnitTotals(token.number, token.unit);
|
|
482
|
+
}
|
|
483
|
+
if (token.type === 'identifier') {
|
|
484
|
+
reader.index++;
|
|
485
|
+
const constant = NUMERIC_CONSTANTS.get(token.name);
|
|
486
|
+
if (constant === undefined) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
return createUnitTotals(constant, UNITLESS);
|
|
490
|
+
}
|
|
491
|
+
if (token.type === 'function') {
|
|
492
|
+
reader.index++;
|
|
493
|
+
return parseMathFunction(reader, token.name);
|
|
494
|
+
}
|
|
495
|
+
if (token.type === 'open') {
|
|
496
|
+
reader.index++;
|
|
497
|
+
return parseGroup(reader);
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Reads a sum written inside parentheses, which is read as one operand of the
|
|
504
|
+
* calculation around it.
|
|
505
|
+
*
|
|
506
|
+
* @param {ExpressionReader} reader The token reader.
|
|
507
|
+
* @return {Map|null} The unit totals the group states, or null when it cannot be resolved.
|
|
508
|
+
*/
|
|
509
|
+
function parseGroup (reader) {
|
|
510
|
+
if (reader.depth >= MAXIMUM_NESTING_DEPTH) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
reader.depth++;
|
|
514
|
+
const value = parseSum(reader);
|
|
515
|
+
reader.depth--;
|
|
516
|
+
if (!value || peekToken(reader)?.type !== 'close') {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
reader.index++;
|
|
520
|
+
return value;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Reads one argument of a math function, which is either a sum or a lone
|
|
525
|
+
* keyword such as the rounding strategy of `round()` or the open end of a
|
|
526
|
+
* `clamp()` range.
|
|
527
|
+
*
|
|
528
|
+
* @param {ExpressionReader} reader The token reader.
|
|
529
|
+
* @return {object|null} The argument read, or null when it cannot be resolved.
|
|
530
|
+
*/
|
|
531
|
+
function parseFunctionArgument (reader) {
|
|
532
|
+
const token = peekToken(reader);
|
|
533
|
+
const followingToken = peekToken(reader, 1);
|
|
534
|
+
const standsAlone = followingToken?.type === 'comma' || followingToken?.type === 'close';
|
|
535
|
+
const isKeyword = token?.type === 'identifier' && standsAlone && !NUMERIC_CONSTANTS.has(token.name);
|
|
536
|
+
if (isKeyword) {
|
|
537
|
+
reader.index++;
|
|
538
|
+
return { keyword: token.name, value: null };
|
|
539
|
+
}
|
|
540
|
+
const value = parseSum(reader);
|
|
541
|
+
if (!value) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
return { keyword: null, value };
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Reads the comma-separated arguments a math function is called with, up to
|
|
549
|
+
* the parenthesis that closes the call.
|
|
550
|
+
*
|
|
551
|
+
* @param {ExpressionReader} reader The token reader.
|
|
552
|
+
* @return {Array|null} The arguments read, or null when the call cannot be resolved.
|
|
553
|
+
*/
|
|
554
|
+
function parseFunctionArguments (reader) {
|
|
555
|
+
const argumentList = [];
|
|
556
|
+
while (reader.index < reader.tokens.length) {
|
|
557
|
+
const argument = parseFunctionArgument(reader);
|
|
558
|
+
if (!argument) {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
argumentList.push(argument);
|
|
562
|
+
const token = peekToken(reader);
|
|
563
|
+
if (token?.type === 'close') {
|
|
564
|
+
reader.index++;
|
|
565
|
+
return argumentList;
|
|
566
|
+
}
|
|
567
|
+
if (token?.type !== 'comma') {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
reader.index++;
|
|
571
|
+
}
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Reads the arguments of a math function and works out the value the function
|
|
577
|
+
* resolves to.
|
|
578
|
+
*
|
|
579
|
+
* @param {ExpressionReader} reader The token reader.
|
|
580
|
+
* @param {string} name The lowercased name of the function being called.
|
|
581
|
+
* @return {Map|null} The unit totals the function resolves to, or null when it cannot be resolved.
|
|
582
|
+
*/
|
|
583
|
+
function parseMathFunction (reader, name) {
|
|
584
|
+
const resolveFunction = MATH_FUNCTION_EVALUATORS.get(name);
|
|
585
|
+
if (!resolveFunction || reader.depth >= MAXIMUM_NESTING_DEPTH) {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
reader.depth++;
|
|
589
|
+
const argumentList = parseFunctionArguments(reader);
|
|
590
|
+
reader.depth--;
|
|
591
|
+
if (!argumentList) {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
return resolveFunction(argumentList);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Reads the arguments of a function as the numbers of one shared unit, which
|
|
599
|
+
* is what a function comparing or rounding its arguments needs them to be.
|
|
600
|
+
* Arguments carrying different units are only comparable once they are
|
|
601
|
+
* resolved against the element, which is long after minification.
|
|
602
|
+
*
|
|
603
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
604
|
+
* @return {object|null} The shared unit and the numbers the arguments state, or null when they do not share one.
|
|
605
|
+
*/
|
|
606
|
+
function readComparableArguments (argumentList) {
|
|
607
|
+
const numbers = [];
|
|
608
|
+
let sharedUnit = null;
|
|
609
|
+
for (const argument of argumentList) {
|
|
610
|
+
if (!argument.value) {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
const quantity = readSingleQuantity(argument.value);
|
|
614
|
+
if (!quantity) {
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
if (sharedUnit !== null && quantity.unit !== sharedUnit) {
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
sharedUnit = quantity.unit;
|
|
621
|
+
numbers.push(quantity.number);
|
|
622
|
+
}
|
|
623
|
+
if (!numbers.length) {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
return { numbers, unit: sharedUnit };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Reads the single value argument a function takes, which no keyword may
|
|
631
|
+
* stand in for.
|
|
632
|
+
*
|
|
633
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
634
|
+
* @return {Map|null} The unit totals of the lone argument, or null when the function was called with anything else.
|
|
635
|
+
*/
|
|
636
|
+
function readLoneArgument (argumentList) {
|
|
637
|
+
if (argumentList.length !== 1) {
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
return argumentList[0].value;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Resolves `calc()`, which states the value its one argument works out to.
|
|
645
|
+
*
|
|
646
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
647
|
+
* @return {Map|null} The unit totals the calculation states, or null when it cannot be resolved.
|
|
648
|
+
*/
|
|
649
|
+
function evaluateCalculation (argumentList) {
|
|
650
|
+
return readLoneArgument(argumentList);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Resolves `min()`, which states the smallest of the values it compares.
|
|
655
|
+
*
|
|
656
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
657
|
+
* @return {Map|null} The unit totals of the smallest argument, or null when the arguments are not comparable.
|
|
658
|
+
*/
|
|
659
|
+
function evaluateMinimum (argumentList) {
|
|
660
|
+
const comparable = readComparableArguments(argumentList);
|
|
661
|
+
if (!comparable) {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
return createUnitTotals(Math.min(...comparable.numbers), comparable.unit);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Resolves `max()`, which states the largest of the values it compares.
|
|
669
|
+
*
|
|
670
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
671
|
+
* @return {Map|null} The unit totals of the largest argument, or null when the arguments are not comparable.
|
|
672
|
+
*/
|
|
673
|
+
function evaluateMaximum (argumentList) {
|
|
674
|
+
const comparable = readComparableArguments(argumentList);
|
|
675
|
+
if (!comparable) {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
return createUnitTotals(Math.max(...comparable.numbers), comparable.unit);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Reports whether an end of a `clamp()` range is left open, which the `none`
|
|
683
|
+
* keyword does, leaving that end to bound nothing.
|
|
684
|
+
*
|
|
685
|
+
* @param {object} argument The argument written at that end of the range.
|
|
686
|
+
* @return {boolean} Whether that end of the range is open.
|
|
687
|
+
*/
|
|
688
|
+
function boundsNothing (argument) {
|
|
689
|
+
return argument.keyword === 'none';
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Resolves `clamp()`, which holds its preferred value inside the range its
|
|
694
|
+
* other two arguments bound. The smallest value wins a range written back to
|
|
695
|
+
* front, since the preferred value is held below the maximum before it is
|
|
696
|
+
* held above the minimum.
|
|
697
|
+
*
|
|
698
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
699
|
+
* @return {Map|null} The unit totals of the clamped value, or null when the arguments are not comparable.
|
|
700
|
+
*/
|
|
701
|
+
function evaluateClamp (argumentList) {
|
|
702
|
+
if (argumentList.length !== 3) {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
const [lowerBound, preferred, upperBound] = argumentList;
|
|
706
|
+
const boundedArguments = argumentList.filter((argument) => {
|
|
707
|
+
return !boundsNothing(argument);
|
|
708
|
+
});
|
|
709
|
+
const comparable = readComparableArguments(boundedArguments);
|
|
710
|
+
if (!comparable || !preferred.value) {
|
|
711
|
+
return null;
|
|
712
|
+
}
|
|
713
|
+
let clamped = readSingleQuantity(preferred.value).number;
|
|
714
|
+
if (!boundsNothing(upperBound)) {
|
|
715
|
+
clamped = Math.min(clamped, readSingleQuantity(upperBound.value).number);
|
|
716
|
+
}
|
|
717
|
+
if (!boundsNothing(lowerBound)) {
|
|
718
|
+
clamped = Math.max(clamped, readSingleQuantity(lowerBound.value).number);
|
|
719
|
+
}
|
|
720
|
+
return createUnitTotals(clamped, comparable.unit);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Resolves `abs()`, which states its argument's distance from zero.
|
|
725
|
+
*
|
|
726
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
727
|
+
* @return {Map|null} The unit totals of the absolute value, or null when the argument states more than one quantity.
|
|
728
|
+
*/
|
|
729
|
+
function evaluateAbsolute (argumentList) {
|
|
730
|
+
const comparable = readComparableArguments(argumentList);
|
|
731
|
+
if (!comparable || comparable.numbers.length !== 1) {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
return createUnitTotals(Math.abs(comparable.numbers[0]), comparable.unit);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Resolves `sign()`, which states which side of zero its argument lies on as
|
|
739
|
+
* a plain number.
|
|
740
|
+
*
|
|
741
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
742
|
+
* @return {Map|null} The unit totals of the sign, or null when the argument states more than one quantity.
|
|
743
|
+
*/
|
|
744
|
+
function evaluateSign (argumentList) {
|
|
745
|
+
const comparable = readComparableArguments(argumentList);
|
|
746
|
+
if (!comparable || comparable.numbers.length !== 1) {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
return createUnitTotals(Math.sign(comparable.numbers[0]), UNITLESS);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Resolves `hypot()`, which states the length of the vector its arguments
|
|
754
|
+
* measure the sides of.
|
|
755
|
+
*
|
|
756
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
757
|
+
* @return {Map|null} The unit totals of the hypotenuse, or null when the arguments are not comparable.
|
|
758
|
+
*/
|
|
759
|
+
function evaluateHypotenuse (argumentList) {
|
|
760
|
+
const comparable = readComparableArguments(argumentList);
|
|
761
|
+
if (!comparable) {
|
|
762
|
+
return null;
|
|
763
|
+
}
|
|
764
|
+
return createUnitTotals(Math.hypot(...comparable.numbers), comparable.unit);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Reads the two comparable numbers a function divides one of by the other,
|
|
769
|
+
* refusing a divisor of zero, which no division resolves against.
|
|
770
|
+
*
|
|
771
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
772
|
+
* @return {object|null} The shared unit with the dividend and the divisor, or null when the arguments cannot be divided.
|
|
773
|
+
*/
|
|
774
|
+
function readDivisionArguments (argumentList) {
|
|
775
|
+
const comparable = readComparableArguments(argumentList);
|
|
776
|
+
if (!comparable || comparable.numbers.length !== 2 || comparable.numbers[1] === 0) {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
dividend: comparable.numbers[0],
|
|
781
|
+
divisor: comparable.numbers[1],
|
|
782
|
+
unit: comparable.unit
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Resolves `mod()`, which states what is left of its first argument after
|
|
788
|
+
* taking out whole multiples of its second, carrying the sign of the divisor.
|
|
789
|
+
*
|
|
790
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
791
|
+
* @return {Map|null} The unit totals of the modulus, or null when the arguments cannot be divided.
|
|
792
|
+
*/
|
|
793
|
+
function evaluateModulus (argumentList) {
|
|
794
|
+
const division = readDivisionArguments(argumentList);
|
|
795
|
+
if (!division) {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
const { dividend, divisor, unit } = division;
|
|
799
|
+
return createUnitTotals(((dividend % divisor) + divisor) % divisor, unit);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Resolves `rem()`, which states what is left of its first argument after
|
|
804
|
+
* taking out whole multiples of its second, carrying the sign of the dividend.
|
|
805
|
+
*
|
|
806
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
807
|
+
* @return {Map|null} The unit totals of the remainder, or null when the arguments cannot be divided.
|
|
808
|
+
*/
|
|
809
|
+
function evaluateRemainder (argumentList) {
|
|
810
|
+
const division = readDivisionArguments(argumentList);
|
|
811
|
+
if (!division) {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
const { dividend, divisor, unit } = division;
|
|
815
|
+
return createUnitTotals(dividend % divisor, unit);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Resolves `round()`, which states the multiple of its rounding interval that
|
|
820
|
+
* the strategy it is given rounds its value to. A call written without a
|
|
821
|
+
* strategy rounds to the nearest multiple.
|
|
822
|
+
*
|
|
823
|
+
* @param {Array} argumentList The arguments the function was called with.
|
|
824
|
+
* @return {Map|null} The unit totals of the rounded value, or null when the arguments cannot be rounded.
|
|
825
|
+
*/
|
|
826
|
+
function evaluateRound (argumentList) {
|
|
827
|
+
let strategyName = 'nearest';
|
|
828
|
+
let valueArguments = argumentList;
|
|
829
|
+
if (argumentList.length === 3) {
|
|
830
|
+
strategyName = argumentList[0].keyword;
|
|
831
|
+
valueArguments = argumentList.slice(1);
|
|
832
|
+
}
|
|
833
|
+
const roundIntervals = ROUNDING_STRATEGIES.get(strategyName);
|
|
834
|
+
const division = readDivisionArguments(valueArguments);
|
|
835
|
+
if (!roundIntervals || !division) {
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
const { dividend, divisor, unit } = division;
|
|
839
|
+
return createUnitTotals(roundIntervals(dividend / divisor) * divisor, unit);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* How each math function resolves the arguments it is called with. The
|
|
844
|
+
* functions listed here are the ones whose arguments are arithmetic, so that
|
|
845
|
+
* `*` and `/` between them are operators rather than characters of some other
|
|
846
|
+
* syntax.
|
|
847
|
+
*
|
|
848
|
+
* @type {Map<string, function(Array): (Map|null)>}
|
|
849
|
+
*/
|
|
850
|
+
const MATH_FUNCTION_EVALUATORS = new Map([
|
|
851
|
+
['abs', evaluateAbsolute],
|
|
852
|
+
['calc', evaluateCalculation],
|
|
853
|
+
['clamp', evaluateClamp],
|
|
854
|
+
['hypot', evaluateHypotenuse],
|
|
855
|
+
['max', evaluateMaximum],
|
|
856
|
+
['min', evaluateMinimum],
|
|
857
|
+
['mod', evaluateModulus],
|
|
858
|
+
['rem', evaluateRemainder],
|
|
859
|
+
['round', evaluateRound],
|
|
860
|
+
['sign', evaluateSign]
|
|
861
|
+
]);
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* The names of the functions whose arguments are a math expression.
|
|
865
|
+
*
|
|
866
|
+
* @type {Set<string>}
|
|
867
|
+
*/
|
|
868
|
+
const MATH_FUNCTION_NAMES = new Set(MATH_FUNCTION_EVALUATORS.keys());
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* The smallest total a term is written with. A term totalling less than this
|
|
872
|
+
* is the rounding error binary floating point arithmetic leaves behind rather
|
|
873
|
+
* than a quantity the stylesheet states.
|
|
874
|
+
*
|
|
875
|
+
* @type {number}
|
|
876
|
+
*/
|
|
877
|
+
const NEGLIGIBLE_TOTAL = 1e-12;
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* The unit each kind of quantity is canonically stated in, and how much of
|
|
881
|
+
* that unit one of the unit written measures. Absolute lengths are absent
|
|
882
|
+
* because every length is already totalled in the pixels it measures.
|
|
883
|
+
*
|
|
884
|
+
* @type {Map<string, {unit: string, perUnit: number}>}
|
|
885
|
+
*/
|
|
886
|
+
const CANONICAL_UNITS = new Map([
|
|
887
|
+
['s', { unit: 's', perUnit: 1 }],
|
|
888
|
+
['ms', { unit: 's', perUnit: 1 / 1000 }],
|
|
889
|
+
['deg', { unit: 'deg', perUnit: 1 }],
|
|
890
|
+
['grad', { unit: 'deg', perUnit: 360 / 400 }],
|
|
891
|
+
['rad', { unit: 'deg', perUnit: 180 / Math.PI }],
|
|
892
|
+
['turn', { unit: 'deg', perUnit: 360 }],
|
|
893
|
+
['hz', { unit: 'hz', perUnit: 1 }],
|
|
894
|
+
['khz', { unit: 'hz', perUnit: 1000 }],
|
|
895
|
+
['dppx', { unit: 'dppx', perUnit: 1 }],
|
|
896
|
+
['x', { unit: 'dppx', perUnit: 1 }],
|
|
897
|
+
['dpi', { unit: 'dppx', perUnit: 1 / 96 }],
|
|
898
|
+
['dpcm', { unit: 'dppx', perUnit: 2.54 / 96 }]
|
|
899
|
+
]);
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Counts how many units of each kind of quantity a calculation totalled, so
|
|
903
|
+
* that the kinds it states in more than one unit can be told from the ones it
|
|
904
|
+
* states in a single unit.
|
|
905
|
+
*
|
|
906
|
+
* @param {Map} totals The unit totals the calculation resolved to.
|
|
907
|
+
* @return {Map} How many units each canonical unit was totalled from.
|
|
908
|
+
*/
|
|
909
|
+
function countUnitsByKind (totals) {
|
|
910
|
+
const counts = new Map();
|
|
911
|
+
for (const unit of totals.keys()) {
|
|
912
|
+
const conversion = CANONICAL_UNITS.get(unit);
|
|
913
|
+
if (!conversion) {
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
counts.set(conversion.unit, (counts.get(conversion.unit) || 0) + 1);
|
|
917
|
+
}
|
|
918
|
+
return counts;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Restates the totals of every kind of quantity a calculation measured in two
|
|
923
|
+
* units of, so that terms measuring the same thing are totalled together
|
|
924
|
+
* (`calc(1s - 200ms)` comes to `.8s`). A kind measured in a single unit keeps
|
|
925
|
+
* that unit: restating `2ms` as `.002s` measures the same time in more
|
|
926
|
+
* characters than it was written with.
|
|
927
|
+
*
|
|
928
|
+
* @param {Map} totals The unit totals the calculation resolved to.
|
|
929
|
+
* @return {Map} The totals, with each kind of quantity stated in one unit.
|
|
930
|
+
*/
|
|
931
|
+
function mergeUnitsOfSameKind (totals) {
|
|
932
|
+
const unitCounts = countUnitsByKind(totals);
|
|
933
|
+
const measuredInSeveralUnits = [...unitCounts.values()].some((count) => {
|
|
934
|
+
return count > 1;
|
|
935
|
+
});
|
|
936
|
+
if (!measuredInSeveralUnits) {
|
|
937
|
+
return totals;
|
|
938
|
+
}
|
|
939
|
+
const merged = new Map();
|
|
940
|
+
for (const [unit, total] of totals) {
|
|
941
|
+
const conversion = CANONICAL_UNITS.get(unit);
|
|
942
|
+
const restatesUnit = conversion && unitCounts.get(conversion.unit) > 1;
|
|
943
|
+
const mergedUnit = restatesUnit ? conversion.unit : unit;
|
|
944
|
+
const mergedTotal = restatesUnit ? total * conversion.perUnit : total;
|
|
945
|
+
merged.set(mergedUnit, (merged.get(mergedUnit) || 0) + mergedTotal);
|
|
946
|
+
}
|
|
947
|
+
return merged;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Collects the terms a resolved calculation is written from, leaving out the
|
|
952
|
+
* ones that totalled to nothing. A unit is totalled from where it is first
|
|
953
|
+
* written, so the terms keep the order the calculation states them in, which
|
|
954
|
+
* is both the order the author chose and one fewer difference between what
|
|
955
|
+
* was written and what is written back out.
|
|
956
|
+
*
|
|
957
|
+
* @param {Map} totals The unit totals the calculation resolved to.
|
|
958
|
+
* @return {Array} The terms to write, each holding its unit and its total.
|
|
959
|
+
*/
|
|
960
|
+
function collectOutputTerms (totals) {
|
|
961
|
+
const terms = [];
|
|
962
|
+
for (const [unit, total] of totals) {
|
|
963
|
+
if (Math.abs(total) < NEGLIGIBLE_TOTAL) {
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
terms.push({ total, unit });
|
|
967
|
+
}
|
|
968
|
+
return leadWithAddedTerm(terms);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Moves an added term to the front of a calculation that a subtracted one
|
|
973
|
+
* leads. Every term but the first is written after the operator adding or
|
|
974
|
+
* subtracting it, so a term written first states its own sign, and the minus
|
|
975
|
+
* sign of a subtracted one is a character the sum saves by being led by a
|
|
976
|
+
* term that is added instead.
|
|
977
|
+
*
|
|
978
|
+
* @param {Array} terms The terms to write, in the order the calculation states them.
|
|
979
|
+
* @return {Array} The same terms, led by one that is added whenever any of them is.
|
|
980
|
+
*/
|
|
981
|
+
function leadWithAddedTerm (terms) {
|
|
982
|
+
if (!terms.length || terms[0].total > 0) {
|
|
983
|
+
return terms;
|
|
984
|
+
}
|
|
985
|
+
const leadingIndex = terms.findIndex((term) => {
|
|
986
|
+
return term.total > 0;
|
|
987
|
+
});
|
|
988
|
+
if (leadingIndex === -1) {
|
|
989
|
+
return terms;
|
|
990
|
+
}
|
|
991
|
+
const reordered = [...terms];
|
|
992
|
+
const [addedTerm] = reordered.splice(leadingIndex, 1);
|
|
993
|
+
reordered.unshift(addedTerm);
|
|
994
|
+
return reordered;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Reports whether writing only the terms collected would drop a percentage the
|
|
999
|
+
* calculation was written with. A percentage that cancels out still ties the
|
|
1000
|
+
* value to the basis the percentage is resolved against, so a calculation
|
|
1001
|
+
* keeping other terms cannot be written without it.
|
|
1002
|
+
*
|
|
1003
|
+
* @param {Map} totals The unit totals the calculation resolved to.
|
|
1004
|
+
* @param {Array} outputTerms The terms that would be written.
|
|
1005
|
+
* @return {boolean} Whether a percentage the calculation states would be lost.
|
|
1006
|
+
*/
|
|
1007
|
+
function dropsPercentageBasis (totals, outputTerms) {
|
|
1008
|
+
if (!totals.has(PERCENTAGE) || !outputTerms.length) {
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
return !outputTerms.some((term) => {
|
|
1012
|
+
return term.unit === PERCENTAGE;
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* Writes the total of a term as the number a stylesheet states it with. A
|
|
1018
|
+
* total that no written number states faithfully — one that is infinite, one
|
|
1019
|
+
* beyond the integers a double holds exactly, and one that rounds away to
|
|
1020
|
+
* nothing — has no number to be written as.
|
|
1021
|
+
*
|
|
1022
|
+
* @param {number} total The total the term resolved to.
|
|
1023
|
+
* @return {string|null} The number as it is written into the stylesheet, or null when it cannot be written.
|
|
1024
|
+
*/
|
|
1025
|
+
function writeTermNumber (total) {
|
|
1026
|
+
if (!Number.isFinite(total) || Math.abs(total) > Number.MAX_SAFE_INTEGER) {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
const written = formatResolvedNumber(total);
|
|
1030
|
+
const roundsAwayToNothing = total !== 0 && parseFloat(written) === 0;
|
|
1031
|
+
if (roundsAwayToNothing) {
|
|
1032
|
+
return null;
|
|
1033
|
+
}
|
|
1034
|
+
return written;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Writes the unit totals a calculation resolved to as the shortest value
|
|
1039
|
+
* stating them: the lone quantity they came to, or, when units that only
|
|
1040
|
+
* resolve against the element are left over, a `calc()` summing what remains.
|
|
1041
|
+
*
|
|
1042
|
+
* @param {Map} totals The unit totals the calculation resolved to.
|
|
1043
|
+
* @return {string|null} The value the calculation resolved to, or null when it cannot be written faithfully.
|
|
1044
|
+
*/
|
|
1045
|
+
function formatUnitTotals (totals) {
|
|
1046
|
+
const mergedTotals = mergeUnitsOfSameKind(totals);
|
|
1047
|
+
const outputTerms = collectOutputTerms(mergedTotals);
|
|
1048
|
+
if (dropsPercentageBasis(mergedTotals, outputTerms)) {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
if (!outputTerms.length) {
|
|
1052
|
+
return '0';
|
|
1053
|
+
}
|
|
1054
|
+
const [leadingTerm, ...trailingTerms] = outputTerms;
|
|
1055
|
+
const leadingNumber = writeTermNumber(leadingTerm.total);
|
|
1056
|
+
if (leadingNumber === null) {
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
let written = leadingNumber + leadingTerm.unit;
|
|
1060
|
+
for (const term of trailingTerms) {
|
|
1061
|
+
const number = writeTermNumber(Math.abs(term.total));
|
|
1062
|
+
if (number === null) {
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
// Both operators have to be surrounded by whitespace to be read as one
|
|
1066
|
+
written = written + (term.total < 0 ? ' - ' : ' + ') + number + term.unit;
|
|
1067
|
+
}
|
|
1068
|
+
if (trailingTerms.length) {
|
|
1069
|
+
return 'calc(' + written + ')';
|
|
1070
|
+
}
|
|
1071
|
+
return written;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/**
|
|
1075
|
+
* Resolves a CSS math function into the value its arithmetic works out to.
|
|
1076
|
+
* A function whose operands are not all known at minification time — one
|
|
1077
|
+
* referencing a custom property, an environment variable, or an element the
|
|
1078
|
+
* value is only resolved against once the page is laid out — states a value
|
|
1079
|
+
* that cannot be worked out here, and so resolves to nothing.
|
|
1080
|
+
*
|
|
1081
|
+
* @param {string} mathFunction The math function as it is written, such as `calc(10 / 2)` or `min(10px, 20px)`.
|
|
1082
|
+
* @return {string|null} The value the math function states, or null when it cannot be resolved.
|
|
1083
|
+
*/
|
|
1084
|
+
function simplifyCalc (mathFunction) {
|
|
1085
|
+
const tokens = tokenizeMathExpression(mathFunction);
|
|
1086
|
+
if (!tokens) {
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
const reader = { tokens, index: 0, depth: 0 };
|
|
1090
|
+
const totals = parseSum(reader);
|
|
1091
|
+
// A token left unread means the expression states more than the arithmetic
|
|
1092
|
+
// that was resolved, so what was resolved is not the whole of its value
|
|
1093
|
+
if (!totals || reader.index !== tokens.length) {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
return formatUnitTotals(totals);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
export {
|
|
1100
|
+
formatUnitTotals,
|
|
1101
|
+
MATH_FUNCTION_NAMES,
|
|
1102
|
+
simplifyCalc
|
|
1103
|
+
};
|