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