@thejaredwilcurt/csslop 0.0.49 → 0.0.50

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 (55) hide show
  1. package/README.md +0 -1
  2. package/package.json +1 -1
  3. package/src/declarations/border.js +1 -1
  4. package/src/index.js +7 -7
  5. package/src/rules/optimize/at-rules.js +113 -0
  6. package/src/rules/optimize/declaration-writes.js +70 -0
  7. package/src/rules/optimize/empty-rules.js +51 -0
  8. package/src/rules/optimize/expand-nesting.js +236 -0
  9. package/src/rules/optimize/factor-parents.js +343 -0
  10. package/src/rules/optimize/index.js +71 -0
  11. package/src/rules/optimize/merge-declarations.js +199 -0
  12. package/src/rules/optimize/merge-nested-rules.js +117 -0
  13. package/src/rules/optimize/merge-selectors.js +252 -0
  14. package/src/rules/optimize/nest-rules.js +202 -0
  15. package/src/rules/optimize/override-index.js +264 -0
  16. package/src/rules/optimize/prune-overridden.js +267 -0
  17. package/src/rules/optimize/selector-normalization.js +60 -0
  18. package/src/rules/optimize/specificity.js +280 -0
  19. package/src/{position-try.js → rules/position-try.js} +1 -1
  20. package/src/utilities.js +14 -0
  21. package/src/value/border.js +85 -0
  22. package/src/value/{colors.js → color/conversion.js} +3 -2
  23. package/src/value/color/functions.js +463 -0
  24. package/src/value/{light-dark.js → color/light-dark.js} +1 -1
  25. package/src/value/{color-mix.js → color/mix.js} +3 -3
  26. package/src/value/{relative-colors.js → color/relative.js} +3 -3
  27. package/src/value/color/shorten.js +93 -0
  28. package/src/value/gradients.js +1 -1
  29. package/src/value/hyphenate-limit-chars.js +72 -0
  30. package/src/value/initial-keywords.js +128 -0
  31. package/src/value/lengths.js +55 -0
  32. package/src/value/math/evaluate.js +36 -0
  33. package/src/value/math/fold.js +315 -0
  34. package/src/value/math/format.js +230 -0
  35. package/src/value/math/functions.js +302 -0
  36. package/src/value/math/index.js +219 -0
  37. package/src/value/math/parser.js +301 -0
  38. package/src/value/math/property-ranges.js +174 -0
  39. package/src/value/math/rewrite.js +222 -0
  40. package/src/value/math/tokenizer.js +160 -0
  41. package/src/value/math/unit-totals.js +194 -0
  42. package/src/value/minify.js +127 -1669
  43. package/src/value/numbers.js +28 -0
  44. package/src/value/positions.js +152 -0
  45. package/src/value/property-optimizations.js +485 -0
  46. package/src/value/segments.js +74 -0
  47. package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
  48. package/src/value/{svg-markup.js → svg/markup.js} +1 -1
  49. package/src/value/time.js +75 -0
  50. package/src/value/urls.js +242 -0
  51. package/src/value/whitespace.js +88 -0
  52. package/src/rules/optimize.js +0 -2314
  53. package/src/value/math-expression.js +0 -1156
  54. package/src/value/math.js +0 -886
  55. /package/src/value/{named-colors.js → color/named.js} +0 -0
package/src/value/math.js DELETED
@@ -1,886 +0,0 @@
1
- /**
2
- * @file Simplifies CSS math functions (calc, min, max) by folding constant expressions, flattening nested calcs, and converting absolute lengths to pixels.
3
- */
4
-
5
- import { findMatchingParenthesis } from '../parser/source-search.js';
6
-
7
- import {
8
- formatUnitTotals,
9
- MATH_FUNCTION_NAMES,
10
- simplifyCalc
11
- } from './math-expression.js';
12
- import {
13
- convertAbsoluteLengthToPx,
14
- formatResolvedDimension,
15
- formatResolvedNumber,
16
- roundCompactNumber
17
- } from './shared.js';
18
-
19
- /**
20
- * Characters that start a token able to sit directly against a value that
21
- * precedes them, so no whitespace is needed to keep the two tokens apart:
22
- * whitespace already separates, and openers like `#`, `!`, or any bracket or
23
- * punctuation mark their own token's start. Mirrors the tokenizer's notion of
24
- * tokens marking their own start, with `/` on top since it delimits a size
25
- * from a position in layered shorthands.
26
- *
27
- * @type {Set<string>}
28
- */
29
- const SELF_DELIMITING_FOLLOWERS = new Set([
30
- ' ',
31
- '\t',
32
- '\n',
33
- '\r',
34
- '\f',
35
- '#',
36
- '!',
37
- '"',
38
- '\'',
39
- '(',
40
- ')',
41
- '[',
42
- ']',
43
- '{',
44
- '}',
45
- ',',
46
- ':',
47
- ';',
48
- '/'
49
- ]);
50
-
51
- /**
52
- * Reports whether a math function's replacement glues itself to the character
53
- * following the matched function, merging two values into one token. A
54
- * replacement that keeps its function form ends with a parenthesis, which
55
- * closes itself; only a bare resolved value needs the whitespace that the
56
- * parenthesis padding removal took away.
57
- *
58
- * @param {string} replacement The string the math function resolved to.
59
- * @param {string} followingCharacter The character directly after the matched function.
60
- * @return {boolean} Whether a separating space must be restored.
61
- */
62
- function mergesWithFollowingCharacter (replacement, followingCharacter) {
63
- if (replacement.endsWith(')') || !followingCharacter) {
64
- return false;
65
- }
66
- return !SELF_DELIMITING_FOLLOWERS.has(followingCharacter);
67
- }
68
-
69
- /**
70
- * Wraps a math-function replacer so a function that dissolves into a bare
71
- * value does not lose the whitespace separating it from the value behind it.
72
- * The values were once told apart by where the function's parentheses sat, so
73
- * when the parentheses go, the separator has to stay.
74
- *
75
- * @param {function(string, ...(string|number|undefined)): string} replacer The replacement callback for `String.prototype.replace`.
76
- * @return {function(string, ...(string|number|undefined)): string} A replacer that keeps the trailing separator intact.
77
- */
78
- function keepSeparatorAfterDissolvedFunction (replacer) {
79
- return (match, ...rest) => {
80
- const replacement = replacer(match, ...rest);
81
- // A replace callback's trailing arguments are the match offset and the full string
82
- const fullText = rest[rest.length - 1];
83
- const matchOffset = rest[rest.length - 2];
84
- const followingCharacter = fullText[matchOffset + match.length];
85
- if (mergesWithFollowingCharacter(replacement, followingCharacter)) {
86
- return replacement + ' ';
87
- }
88
- return replacement;
89
- };
90
- }
91
-
92
- /**
93
- * Matches a single character that may appear in a CSS function name.
94
- *
95
- * @type {RegExp}
96
- */
97
- const FUNCTION_NAME_CHARACTER = /[a-zA-Z-]/;
98
-
99
- /**
100
- * Finds where the function name that ends at an opening parenthesis begins.
101
- *
102
- * @param {string} value The CSS value string being scanned.
103
- * @param {number} openIndex The index of the opening parenthesis.
104
- * @return {number} The index of the function name's first character.
105
- */
106
- function findFunctionNameStart (value, openIndex) {
107
- let index = openIndex;
108
- while (index > 0 && FUNCTION_NAME_CHARACTER.test(value[index - 1])) {
109
- index--;
110
- }
111
- return index;
112
- }
113
-
114
- /**
115
- * Rewrites the contents of every math function in a value, leaving the rest of
116
- * the value untouched. A `*` only means multiplication inside these functions,
117
- * so an arithmetic rewrite may only reach the text between their parentheses.
118
- * The whole of a math function is handed over at once, so a math function
119
- * nested in another is rewritten as part of its parent.
120
- *
121
- * @param {function(string): string} rewriteExpression Receives a math function's contents and returns its replacement.
122
- * @param {string} value The CSS value string to rewrite.
123
- * @return {string} The value with every math function's contents rewritten.
124
- */
125
- function rewriteMathFunctionContents (rewriteExpression, value) {
126
- let result = '';
127
- let index = 0;
128
- while (index < value.length) {
129
- const openIndex = value.indexOf('(', index);
130
- if (openIndex === -1) {
131
- break;
132
- }
133
- const functionName = value.slice(findFunctionNameStart(value, openIndex), openIndex).toLowerCase();
134
- const closeIndex = findMatchingParenthesis(value, openIndex);
135
- if (!MATH_FUNCTION_NAMES.has(functionName) || closeIndex === -1) {
136
- result += value.slice(index, openIndex + 1);
137
- index = openIndex + 1;
138
- continue;
139
- }
140
- result += value.slice(index, openIndex + 1);
141
- result += rewriteExpression(value.slice(openIndex + 1, closeIndex));
142
- index = closeIndex;
143
- }
144
- return result + value.slice(index);
145
- }
146
-
147
- /**
148
- * Matches a number written with a decimal point that begins a token of its
149
- * own, capturing its sign, the digits before its decimal point, and the digits
150
- * after it. The lookbehind keeps the match from starting partway through an
151
- * identifier (`--x0.5`) or another number, and the lookahead keeps it from
152
- * matching a fragment of a dotted name such as `0.5.png`.
153
- *
154
- * @type {RegExp}
155
- */
156
- const DECIMAL_NUMBER = /(?<![\w.-])([+-]?)(\d*)\.(\d+)(?![\d.])/g;
157
-
158
- /**
159
- * Writes a decimal number in the fewest characters that state the same value,
160
- * dropping the zeros before its whole part (`0.25` → `.25`) and after its
161
- * fraction (`1.50` → `1.5`), along with a decimal point left with no fraction
162
- * behind it (`2.0` → `2`). The digits that remain are kept as written, so no
163
- * precision is lost.
164
- *
165
- * @param {string} sign The sign written before the number, empty when there is none.
166
- * @param {string} wholeDigits The digits written before the decimal point.
167
- * @param {string} fractionDigits The digits written after the decimal point.
168
- * @return {string} The number written as compactly as it can be.
169
- */
170
- function shortenDecimalNumber (sign, wholeDigits, fractionDigits) {
171
- // Leading zeros of the whole part
172
- const wholePart = wholeDigits.replace(/^0+/, '');
173
- // Trailing zeros of the fraction
174
- const fractionPart = fractionDigits.replace(/0+$/, '');
175
- if (!fractionPart) {
176
- return sign + (wholePart || '0');
177
- }
178
- return sign + wholePart + '.' + fractionPart;
179
- }
180
-
181
- /**
182
- * Shortens every decimal number in the contents of a math function. Numbers
183
- * that follow an operator written without whitespace (`x*0.25s`) are out of
184
- * reach of the general number shortening, which only looks past a separator.
185
- *
186
- * @param {string} expression The contents of a math function.
187
- * @return {string} The expression with every decimal number written as compactly as it can be.
188
- */
189
- function shortenDecimalNumbers (expression) {
190
- return expression.replace(DECIMAL_NUMBER, (match, sign, wholeDigits, fractionDigits) => {
191
- return shortenDecimalNumber(sign, wholeDigits, fractionDigits);
192
- });
193
- }
194
-
195
- /**
196
- * Matches a multiplication by a decimal below one, capturing the digits after
197
- * its decimal point and the unit written on it (so a multiplier with a unit
198
- * can be recognized and left alone). The trailing lookahead keeps
199
- * the match away from numbers that only begin this way, such as the exponent
200
- * of `.5e3` or a unit name that is still being read.
201
- *
202
- * @type {RegExp}
203
- */
204
- const FRACTIONAL_MULTIPLIER = /\s*\*\s*0*\.(\d+)([a-z%]*)(?![\w.%(])/gi;
205
-
206
- /**
207
- * The most decimal places a fraction may be built from while the power of ten
208
- * it needs is still exactly representable as an integer.
209
- *
210
- * @type {number}
211
- */
212
- const MAXIMUM_FRACTION_DIGITS = 15;
213
-
214
- /**
215
- * Computes the greatest common divisor of two non-negative integers, which is
216
- * what a fraction has to be divided by to be stated in lowest terms.
217
- *
218
- * @param {number} first The first integer.
219
- * @param {number} second The second integer.
220
- * @return {number} The greatest common divisor of both integers.
221
- */
222
- function greatestCommonDivisor (first, second) {
223
- let dividend = first;
224
- let divisor = second;
225
- while (divisor) {
226
- const remainder = dividend % divisor;
227
- dividend = divisor;
228
- divisor = remainder;
229
- }
230
- return dividend;
231
- }
232
-
233
- /**
234
- * Converts the digits behind a decimal point into the whole number that
235
- * dividing by scales a value exactly as multiplying by that decimal does.
236
- * The digits `25` (of `.25`) state the fraction 25/100, which reduces to 1/4,
237
- * so the divisor is `4`. A fraction that does not reduce to a numerator of one
238
- * cannot be stated as a single division, and so has no divisor to give.
239
- *
240
- * @param {string} fractionDigits The digits written after the decimal point.
241
- * @return {number|null} The equivalent whole number divisor, or null when there is none.
242
- */
243
- function reciprocalOfDecimalFraction (fractionDigits) {
244
- if (fractionDigits.length > MAXIMUM_FRACTION_DIGITS) {
245
- return null;
246
- }
247
- const numerator = Number(fractionDigits);
248
- const denominator = 10 ** fractionDigits.length;
249
- const commonFactor = greatestCommonDivisor(numerator, denominator);
250
- if (numerator / commonFactor !== 1) {
251
- return null;
252
- }
253
- return denominator / commonFactor;
254
- }
255
-
256
- /**
257
- * Builds the division that scales a value exactly as multiplying it by a
258
- * unitless decimal below one does, when that division is no longer to write
259
- * than the multiplication. A multiplier carrying a unit is never rewritten:
260
- * dividing by a dimension inverts its unit in the type of the result
261
- * (`x*.25s` is a time, while `x/4s` is per unit of time), so the two would not
262
- * be equivalent.
263
- *
264
- * @param {string} fractionDigits The digits written after the decimal point of the multiplier.
265
- * @param {string} unit The unit written on the multiplier, empty when it is unitless.
266
- * @return {string|null} The equivalent division, or null when there is none worth using.
267
- */
268
- function divisionEquivalentToFractionalMultiplier (fractionDigits, unit) {
269
- if (unit) {
270
- return null;
271
- }
272
- const divisor = reciprocalOfDecimalFraction(fractionDigits);
273
- if (divisor === null) {
274
- return null;
275
- }
276
- const division = '/' + divisor;
277
- // Compared against the shortest the multiplication could have been written
278
- const multiplication = '*.' + fractionDigits;
279
- if (division.length > multiplication.length) {
280
- return null;
281
- }
282
- return division;
283
- }
284
-
285
- /**
286
- * Rewrites a multiplication by a unitless decimal below one as a division by
287
- * the whole number that decimal is the reciprocal of, which is never longer to
288
- * write and usually shorter (`calc(var(--x)*.25)` → `calc(var(--x)/4)`).
289
- * Multipliers with a unit are left as written, since dividing by a dimension
290
- * would invert its unit.
291
- *
292
- * @param {string} expression The contents of a math function.
293
- * @return {string} The expression with reciprocal multipliers stated as divisions.
294
- */
295
- function convertReciprocalMultiplicationToDivision (expression) {
296
- return expression.replace(FRACTIONAL_MULTIPLIER, (match, fractionDigits, unit) => {
297
- return divisionEquivalentToFractionalMultiplier(fractionDigits, unit) ?? match;
298
- });
299
- }
300
-
301
- /**
302
- * Matches a whole expression that divides a percentage by a plain number,
303
- * capturing the percentage and the divisor it is shared out between.
304
- *
305
- * @type {RegExp}
306
- */
307
- const PERCENTAGE_DIVISION = /^([+-]?(?:\d+|\d*\.\d+))%\s*\/\s*([+-]?(?:\d+|\d*\.\d+))$/;
308
-
309
- /**
310
- * Picks between what a calculation resolved to and the calculation itself,
311
- * keeping whichever takes fewer characters to write. A tie goes to the
312
- * resolved value, which states the same thing without the arithmetic.
313
- *
314
- * @param {string} resolved The value the calculation resolves to.
315
- * @param {string} calculation The calculation, written as compactly as it can be.
316
- * @return {string} The shorter of the two.
317
- */
318
- function shorterOfResolutionAndCalculation (resolved, calculation) {
319
- if (resolved.length <= calculation.length) {
320
- return resolved;
321
- }
322
- return calculation;
323
- }
324
-
325
- /**
326
- * Writes a calculation as compactly as its syntax allows, squeezing out every
327
- * space but the ones a `+` or a `-` needs to be read as an operator.
328
- *
329
- * @param {string} inner The contents of the `calc()`.
330
- * @return {string} The whole `calc()`, written as compactly as it can be.
331
- */
332
- function compactCalculation (inner) {
333
- // Remove whitespace around multiplication and division operators
334
- const compactInner = inner.replace(/\s+/g, ' ').trim().replace(/\s*([*/])\s*/g, '$1');
335
- return 'calc(' + compactInner + ')';
336
- }
337
-
338
- /**
339
- * Picks between a calculation and what resolving its arithmetic came to. A
340
- * resolved value states the same thing without any arithmetic, so it wins a
341
- * tie, but a result that is itself a calculation only states the same terms
342
- * another way, and writing them out again for no saving gains nothing.
343
- *
344
- * @param {string} resolved The value the calculation resolved to.
345
- * @param {string} calculation The calculation, written as compactly as it can be.
346
- * @return {string} Whichever of the two the value is written as.
347
- */
348
- function chooseResolvedCalculation (resolved, calculation) {
349
- // A resolution that kept its function form left terms behind that only
350
- // resolve once the element they are measured against is laid out
351
- if (resolved.startsWith('calc(') && resolved.length >= calculation.length) {
352
- return calculation;
353
- }
354
- return shorterOfResolutionAndCalculation(resolved, calculation);
355
- }
356
-
357
- /**
358
- * Resolves a percentage divided by a number into the single percentage it
359
- * equals, which is how `calc(100%/16)` comes to be written as `6.25%`. The
360
- * division stays as it was written when the result is no shorter, and when
361
- * the divisor is zero, since that resolves to nothing a stylesheet can state.
362
- *
363
- * @param {string} expression The contents of a `calc()`, with its whitespace collapsed.
364
- * @return {string|null} The shorter of the resolved percentage and the division, or null when the expression is not one.
365
- */
366
- function resolvePercentageDivision (expression) {
367
- const division = expression.match(PERCENTAGE_DIVISION);
368
- if (!division) {
369
- return null;
370
- }
371
- const [, percentage, divisor] = division;
372
- const calculation = 'calc(' + percentage + '%/' + divisor + ')';
373
- if (Number(divisor) === 0) {
374
- return calculation;
375
- }
376
- const resolved = formatResolvedNumber(Number(percentage) / Number(divisor)) + '%';
377
- return shorterOfResolutionAndCalculation(resolved, calculation);
378
- }
379
-
380
- /**
381
- * The properties whose values every CSS specification defines as
382
- * non-negative, so that a math function declared on one of them has its
383
- * result clamped rather than read as written.
384
- *
385
- * @type {Set<string>}
386
- */
387
- const NON_NEGATIVE_PROPERTIES = new Set([
388
- 'animation-duration',
389
- 'animation-iteration-count',
390
- 'background-size',
391
- 'block-size',
392
- 'border-block-end-width',
393
- 'border-block-start-width',
394
- 'border-block-width',
395
- 'border-bottom-left-radius',
396
- 'border-bottom-right-radius',
397
- 'border-bottom-width',
398
- 'border-end-end-radius',
399
- 'border-end-start-radius',
400
- 'border-image-outset',
401
- 'border-image-width',
402
- 'border-inline-end-width',
403
- 'border-inline-start-width',
404
- 'border-inline-width',
405
- 'border-left-width',
406
- 'border-radius',
407
- 'border-right-width',
408
- 'border-start-end-radius',
409
- 'border-start-start-radius',
410
- 'border-top-left-radius',
411
- 'border-top-right-radius',
412
- 'border-top-width',
413
- 'border-width',
414
- 'column-count',
415
- 'column-gap',
416
- 'column-rule-width',
417
- 'column-width',
418
- 'flex-basis',
419
- 'flex-grow',
420
- 'flex-shrink',
421
- 'font-size',
422
- 'gap',
423
- 'height',
424
- 'inline-size',
425
- 'line-height',
426
- 'mask-size',
427
- 'max-block-size',
428
- 'max-height',
429
- 'max-inline-size',
430
- 'max-width',
431
- 'min-block-size',
432
- 'min-height',
433
- 'min-inline-size',
434
- 'min-width',
435
- 'outline-width',
436
- 'padding',
437
- 'padding-block',
438
- 'padding-block-end',
439
- 'padding-block-start',
440
- 'padding-bottom',
441
- 'padding-inline',
442
- 'padding-inline-end',
443
- 'padding-inline-start',
444
- 'padding-left',
445
- 'padding-right',
446
- 'padding-top',
447
- 'perspective',
448
- 'row-gap',
449
- 'shape-margin',
450
- 'stroke-width',
451
- 'tab-size',
452
- 'transition-duration',
453
- 'width'
454
- ]);
455
-
456
- /**
457
- * Properties whose computed math values must be integers.
458
- *
459
- * @type {Set<string>}
460
- */
461
- const INTEGER_VALUE_PROPERTIES = new Set([
462
- 'column-count',
463
- 'order',
464
- 'orphans',
465
- 'widows',
466
- 'z-index'
467
- ]);
468
-
469
- /**
470
- * The inclusive bounds that CSS applies to numeric property values after
471
- * resolving a math expression.
472
- *
473
- * @type {Map<string, {minimum?: number, maximum?: number}>}
474
- */
475
- const NUMERIC_PROPERTY_RANGES = new Map([
476
- ['column-count', { minimum: 1 }],
477
- ['opacity', { minimum: 0, maximum: 1 }]
478
- ]);
479
-
480
- /**
481
- * Matches a value written as a single negative number, with or without the
482
- * unit or percent sign that follows its digits.
483
- *
484
- * @type {RegExp}
485
- */
486
- const NEGATIVE_NUMBER_VALUE = /^-(?:\d+|\d*\.\d+)(?:[a-z]+|%)?$/i;
487
-
488
- /**
489
- * Matches a resolved unitless number whose value can be rounded or clamped by
490
- * a property's numeric range.
491
- *
492
- * @type {RegExp}
493
- */
494
- const UNIT_LESS_NUMBER_VALUE = /^[+-]?(?:\d+|\d*\.\d+)$/;
495
-
496
- /**
497
- * Clamps what a math function resolved to into the range its property
498
- * accepts. A negative length written out directly makes the declaration
499
- * invalid and the CSS engine throws it away, but a math function is allowed
500
- * to resolve to one: its result is clamped to the nearest value the property
501
- * does take, which on a property that only accepts non-negative values is
502
- * zero, as the range checking section of the CSS Values specification
503
- * describes: https://drafts.csswg.org/css-values/#calc-range.
504
- *
505
- * @param {string} resolvedValue The value the math function resolved to.
506
- * @param {string} property The property the math function is declared on.
507
- * @return {string} The resolved value, held inside the property's range.
508
- */
509
- function clampResolvedValueToPropertyRange (resolvedValue, property) {
510
- const normalizedProperty = String(property).toLowerCase();
511
- const acceptsNegativeValues = !NON_NEGATIVE_PROPERTIES.has(normalizedProperty);
512
- const hasPropertyRange = NUMERIC_PROPERTY_RANGES.has(normalizedProperty);
513
- const range = NUMERIC_PROPERTY_RANGES.get(normalizedProperty) || {};
514
- const isNegativeNonNegativeValue = !acceptsNegativeValues && NEGATIVE_NUMBER_VALUE.test(resolvedValue);
515
- if (isNegativeNonNegativeValue) {
516
- const isUnitlessNumber = UNIT_LESS_NUMBER_VALUE.test(resolvedValue);
517
- if (!isUnitlessNumber) {
518
- return formatResolvedNumber(range.minimum === undefined ? 0 : range.minimum);
519
- }
520
- if (!hasPropertyRange && !INTEGER_VALUE_PROPERTIES.has(normalizedProperty)) {
521
- return '0';
522
- }
523
- }
524
- if (!hasPropertyRange && !INTEGER_VALUE_PROPERTIES.has(normalizedProperty)) {
525
- return resolvedValue;
526
- }
527
- if (!UNIT_LESS_NUMBER_VALUE.test(resolvedValue)) {
528
- return resolvedValue;
529
- }
530
-
531
- const shouldRoundToInteger = INTEGER_VALUE_PROPERTIES.has(normalizedProperty);
532
- let numericValue = Number(resolvedValue);
533
- if (shouldRoundToInteger) {
534
- numericValue = Math.round(numericValue);
535
- }
536
- if (range.minimum !== undefined) {
537
- numericValue = Math.max(range.minimum, numericValue);
538
- }
539
- if (range.maximum !== undefined) {
540
- numericValue = Math.min(range.maximum, numericValue);
541
- }
542
- return formatResolvedNumber(numericValue);
543
- }
544
-
545
- /**
546
- * Attempts to simplify a calc() expression by combining like-unit terms and evaluating pure arithmetic, returning the simplified string or null if folding is not possible.
547
- *
548
- * @param {string} expression The expression inside calc() to attempt folding.
549
- * @return {string|null} The simplified expression, or null if it cannot be folded.
550
- */
551
- function tryFoldCalcExpression (expression) {
552
- let expr = expression.trim();
553
- let previous;
554
-
555
- do {
556
- previous = expr;
557
- // Remove innermost non-nested parentheses (flatten simple grouping)
558
- expr = expr.replace(/\(([^()]+)\)/g, '$1');
559
- // Fold: <number> * <number><unit> → computed result in same unit
560
- expr = expr.replace(/(-?(?:\d*\.\d+|\d+))\s*\*\s*(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q|%)/gi, (match, a, b, unit) => {
561
- return formatResolvedDimension(parseFloat(a) * parseFloat(b), unit);
562
- });
563
- // Fold: <number><unit> * <number> → computed result in same unit
564
- expr = expr.replace(/(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q|%)\s*\*\s*(-?(?:\d*\.\d+|\d+))/gi, (match, a, unit, b) => {
565
- return formatResolvedDimension(parseFloat(a) * parseFloat(b), unit);
566
- });
567
- // Fold: <number><unit> / <number> → computed result in same unit
568
- expr = expr.replace(/(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q)\s*\/\s*(-?(?:\d*\.\d+|\d+))/gi, (match, value, unit, divisor) => {
569
- return formatResolvedDimension(parseFloat(value) / parseFloat(divisor), unit);
570
- });
571
- // Collapse zero-with-unit terms (e.g. 0px, 0%) to plain 0 or remove them
572
- expr = expr.replace(/(^|[+-])\s*0(?:px|pt|pc|in|cm|mm|q|%)\b/g, (match, sign) => {
573
- if (sign && sign !== '+') {
574
- return sign + ' 0';
575
- }
576
- return '';
577
- });
578
- // Remove additive zero terms
579
- expr = expr.replace(/\+\s*0\b/g, '');
580
- // Remove subtractive zero terms
581
- expr = expr.replace(/-\s*0\b/g, '');
582
- // Collapse whitespace
583
- expr = expr.replace(/\s+/g, ' ').trim();
584
- } while (expr !== previous);
585
-
586
- // Simplify trivial identity division: 1 / 1 / <dimension> → <dimension>
587
- if (/^1\s*\/\s*1\s*\/\s*(-?(?:\d*\.\d+|\d+)(?:px|pt|pc|in|cm|mm|q))$/i.test(expr)) {
588
- return expr.replace(/^1\s*\/\s*1\s*\/\s*/i, '');
589
- }
590
-
591
- // Remove all whitespace for validation
592
- const normalized = expr.replace(/\s+/g, '');
593
- // Validate that the expression is a simple sequence of signed terms with optional units
594
- if (!/^[+-]?(?:\d*\.\d+|\d+)(?:[a-z%]+)?(?:[+-](?:\d*\.\d+|\d+)(?:[a-z%]+)?)*$/i.test(normalized)) {
595
- return null;
596
- }
597
-
598
- // Extract each signed term with its optional unit
599
- const terms = normalized.match(/[+-]?(?:\d*\.\d+|\d+)(?:[a-z%]+)?/gi) || [];
600
- const totals = new Map();
601
-
602
- for (const term of terms) {
603
- // Parse each term into sign, number, and unit parts
604
- const match = term.match(/^([+-]?)(\d*\.\d+|\d+)([a-z%]+)?$/i);
605
- if (!match) {
606
- return null;
607
- }
608
- const [, sign, rawNumber, rawUnit = ''] = match;
609
- let number = parseFloat(rawNumber) * (sign === '-' ? -1 : 1);
610
- let unit = rawUnit.toLowerCase();
611
-
612
- if (unit && unit !== '%' && unit !== 'px') {
613
- const pxValue = convertAbsoluteLengthToPx(number, unit);
614
- if (pxValue === null) {
615
- return null;
616
- }
617
- number = pxValue;
618
- unit = 'px';
619
- }
620
-
621
- totals.set(unit, (totals.get(unit) || 0) + number);
622
- }
623
-
624
- return formatUnitTotals(totals);
625
- }
626
-
627
- /**
628
- * Matches a whole expression that scales a percentage by a plain number,
629
- * written either way around. Both operands are already as short as they can
630
- * be, so the expression is only ever squeezed, never resolved.
631
- *
632
- * @type {RegExp}
633
- */
634
- const PERCENTAGE_MULTIPLICATION = /^(?:-?(?:\d*\.\d+|\d+)%\s*\*\s*-?(?:\d*\.\d+|\d+)|-?(?:\d*\.\d+|\d+)\s*\*\s*-?(?:\d*\.\d+|\d+)%)$/;
635
-
636
- /**
637
- * Matches a value that is a single percentage, which is all a calculation
638
- * leaves behind once every term of it has resolved into one.
639
- *
640
- * @type {RegExp}
641
- */
642
- const RESOLVED_PERCENTAGE = /^-?(?:\d+|\d*\.\d+)%$/;
643
-
644
- /**
645
- * Chooses how a calculation that shares a percentage out resolves. The
646
- * library states the result to the full precision a float carries, which is
647
- * both longer than the calculation it replaces and finer than a stylesheet
648
- * has any use for, so the result is rounded into the budget a resolved number
649
- * is written within and kept only when it is the shorter of the two.
650
- *
651
- * @param {string} simplified The result the calculation library returned.
652
- * @param {string} compactInner The contents of the `calc()`, with its whitespace collapsed.
653
- * @param {string} match The whole `calc()` as it was written.
654
- * @return {string} How the resolved calculation is written.
655
- */
656
- function chooseResolvedPercentage (simplified, compactInner, match) {
657
- // A division sign written after a percentage, which is what the resolved percentage was shared out by
658
- const dividesAPercentage = RESOLVED_PERCENTAGE.test(simplified) && /%\s*\//.test(match);
659
- if (!dividesAPercentage) {
660
- return simplified;
661
- }
662
- // Remove whitespace around division operator
663
- const calculation = 'calc(' + compactInner.replace(/\s*\/\s*/g, '/') + ')';
664
- const resolved = formatResolvedNumber(parseFloat(simplified)) + '%';
665
- return shorterOfResolutionAndCalculation(resolved, calculation);
666
- }
667
-
668
- /**
669
- * Resolves the contents of a `calc()` into the shortest way of writing the
670
- * same value, which is either the value the arithmetic works out to or the
671
- * calculation itself with the whitespace squeezed out of it.
672
- *
673
- * @param {string} match The whole `calc()` as it was written.
674
- * @param {string} compactInner The contents of the `calc()`, with its whitespace collapsed.
675
- * @return {string} The shortest way of writing what the calculation states.
676
- */
677
- function resolveCalcExpression (match, compactInner) {
678
- if (PERCENTAGE_MULTIPLICATION.test(compactInner)) {
679
- return compactCalculation(compactInner);
680
- }
681
-
682
- const dividedPercentage = resolvePercentageDivision(compactInner);
683
- if (dividedPercentage) {
684
- return dividedPercentage;
685
- }
686
-
687
- const folded = tryFoldCalcExpression(compactInner);
688
- if (folded) {
689
- return folded;
690
- }
691
-
692
- const simplified = simplifyCalc(match);
693
- if (simplified === null) {
694
- return match;
695
- }
696
- const resolved = chooseResolvedPercentage(simplified, compactInner, match);
697
- return chooseResolvedCalculation(resolved, compactCalculation(compactInner));
698
- }
699
-
700
- /**
701
- * Matches a math function other than `calc()` whose arguments hold no further
702
- * parentheses, so that the whole of the function is matched. A `calc()` is
703
- * left to its own pass, which weighs what the calculation resolves to against
704
- * the calculation itself.
705
- *
706
- * @type {RegExp}
707
- */
708
- const RESOLVABLE_MATH_FUNCTION = new RegExp(
709
- '\\b(?:' +
710
- [...MATH_FUNCTION_NAMES].filter((name) => {
711
- return name !== 'calc';
712
- }).join('|') +
713
- ')\\([^()]+\\)',
714
- 'gi'
715
- );
716
-
717
- /**
718
- * Runs one pass of the math simplifications over a value: the rewrites that
719
- * state a calculation more compactly, and the resolutions that replace a math
720
- * function with the value it works out to.
721
- *
722
- * @param {string} value The CSS value string containing math functions to simplify.
723
- * @param {string} property The CSS property name, whose range every resolved result is clamped into.
724
- * @return {string} The value with its math functions simplified where possible.
725
- */
726
- function simplifyMathFunctionsOnce (value, property) {
727
- let result = rewriteMathFunctionContents(shortenDecimalNumbers, value);
728
- result = rewriteMathFunctionContents(convertReciprocalMultiplicationToDivision, result);
729
-
730
- // Unwrap calc(1 / (1 / x)) → x (double-reciprocal identity)
731
- result = result.replace(/calc\(\s*1\s*\/\s*\(\s*1\s*\/\s*([^()]+)\s*\)\s*\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
732
- return inner.trim();
733
- }));
734
- // Flatten calc(calc(a) ± b) → calc(a ± b)
735
- result = result.replace(/calc\(\s*calc\(([^()]+)\)\s*([+-])\s*([^()]+)\s*\)/gi, keepSeparatorAfterDissolvedFunction((match, inner, operator, tail) => {
736
- return 'calc(' + inner + ' ' + operator + ' ' + tail + ')';
737
- }));
738
- // Unwrap calc(calc(x)) → calc(x)
739
- result = result.replace(/calc\(\s*calc\(([^()]+)\)\s*\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
740
- return 'calc(' + inner + ')';
741
- }));
742
-
743
- // Resolve every math function that compares or rounds its arguments
744
- result = result.replace(RESOLVABLE_MATH_FUNCTION, keepSeparatorAfterDissolvedFunction((match) => {
745
- const simplified = simplifyCalc(match);
746
- if (simplified === null) {
747
- return match;
748
- }
749
- return clampResolvedValueToPropertyRange(simplified, property);
750
- }));
751
-
752
- // Simplify calc() expressions using constant folding and expression resolution
753
- result = result.replace(/calc\(([^()]+)\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
754
- // Collapse whitespace inside calc expression
755
- const compactInner = inner.replace(/\s+/g, ' ').trim();
756
- const resolved = resolveCalcExpression(match, compactInner);
757
- return clampResolvedValueToPropertyRange(resolved, property);
758
- }));
759
-
760
- return result;
761
- }
762
-
763
- /**
764
- * Simplifies every math function within a CSS value string, resolving the ones
765
- * whose operands are all known and stating the rest as compactly as they can
766
- * be written.
767
- *
768
- * @param {string} value The CSS value string containing math functions to simplify.
769
- * @param {string} property The CSS property name, whose range every resolved result is clamped into.
770
- * @param {string} originalValue The value as it was authored, which tells whether a result was ever part of a calculation.
771
- * @return {string} The value with math functions simplified where possible.
772
- */
773
- function normalizeMathFunctions (value, property, originalValue = '') {
774
- let result = value;
775
- let previous;
776
-
777
- do {
778
- previous = result;
779
- // Resolving a math function leaves a plain value where it stood, which can
780
- // bring the function it was nested in within reach of the next pass, so
781
- // the value is simplified until it settles
782
- result = simplifyMathFunctionsOnce(result, property);
783
- } while (result !== previous);
784
-
785
- // When calc() folded to an absolute-length unit (pt, pc, in, cm, mm, q), convert to pixels
786
- if (originalValue.includes('calc(') && /^-?(?:\d+|\d*\.\d+)(pt|pc|in|cm|mm|q)$/i.test(result)) {
787
- // Extract the absolute-length unit from the folded result
788
- const [, unit] = result.match(/^-?(?:\d+|\d*\.\d+)(pt|pc|in|cm|mm|q)$/i);
789
- const numeric = parseFloat(result);
790
- const pxValue = convertAbsoluteLengthToPx(numeric, unit);
791
- if (pxValue !== null) {
792
- result = roundCompactNumber(pxValue) + 'px';
793
- }
794
- }
795
-
796
- // Round a result whose decimal places run past the budget a resolved number is written within
797
- result = result.replace(/(-?(?:\d+|\d*\.\d+)\.\d{4,})([a-z%]+)/gi, (match, number, unit) => {
798
- return formatResolvedNumber(number) + unit;
799
- });
800
- return result;
801
- }
802
-
803
- /**
804
- * Simplifies a standalone calc() value by resolving its arithmetic, flattening nested calc expressions, converting absolute length units to pixels, and folding constant terms.
805
- *
806
- * @param {string} value The CSS value string, already known to be a single `calc()`.
807
- * @return {string} The simplified value, or the original value if simplification is not applicable.
808
- */
809
- function foldStandaloneCalc (value) {
810
- let inner = value.slice(5, -1).trim();
811
-
812
- // Preserve percent-times-number expressions, only stripping whitespace around operators
813
- if (PERCENTAGE_MULTIPLICATION.test(inner.replace(/\s+/g, ' ').trim())) {
814
- return compactCalculation(inner);
815
- }
816
-
817
- // Resolve the calculation as it was written, which is the only reading of it
818
- // that keeps the grouping its parentheses state. Everything below reads a
819
- // flattened copy of the expression, where that grouping is already gone.
820
- const resolved = simplifyCalc(value);
821
- if (resolved !== null) {
822
- return chooseResolvedCalculation(resolved, compactCalculation(inner));
823
- }
824
-
825
- // If no nested function calls and no multiplication/division involving parens, try flattening
826
- if (!/[A-Za-z-]+\(/.test(inner) && !/[*/]\s*\(|\)\s*[*/]/.test(inner)) {
827
- // Remove all parentheses and collapse whitespace for folding
828
- const flattened = inner.replace(/[()]/g, '').replace(/\s+/g, ' ').trim();
829
- const foldedFlattened = tryFoldCalcExpression(flattened);
830
- if (foldedFlattened) {
831
- return foldedFlattened;
832
- }
833
- }
834
-
835
- let previous;
836
-
837
- do {
838
- previous = inner;
839
- // Fold innermost parenthesized sub-expressions that aren't function calls
840
- inner = inner.replace(/(^|[^A-Za-z-])\(([^()]+)\)/g, (match, prefix, content) => {
841
- const trimmed = content.trim();
842
- const folded = tryFoldCalcExpression(trimmed);
843
- if (folded) {
844
- if (folded.startsWith('calc(') && folded.endsWith(')')) {
845
- return prefix + trimmed;
846
- }
847
- return prefix + folded;
848
- }
849
- return prefix + trimmed;
850
- });
851
- } while (inner !== previous);
852
-
853
- const folded = tryFoldCalcExpression(inner);
854
- if (folded) {
855
- return folded;
856
- }
857
-
858
- // Collapse whitespace and check for percent-division expressions
859
- const dividedPercentage = resolvePercentageDivision(inner.replace(/\s+/g, ' ').trim());
860
- if (dividedPercentage) {
861
- return dividedPercentage;
862
- }
863
-
864
- return compactCalculation(inner);
865
- }
866
-
867
- /**
868
- * Simplifies a value that is a single `calc()`, holding whatever it resolves
869
- * to inside the range the property it is declared on accepts.
870
- *
871
- * @param {string} value The CSS value string that may be a standalone calc() expression.
872
- * @param {string} property The CSS property the value is declared on.
873
- * @return {string} The simplified value, or the original value if simplification is not applicable.
874
- */
875
- function simplifyStandaloneCalc (value, property = '') {
876
- // Check if value starts with calc( and ends with )
877
- if (!/^calc\(/i.test(value) || !value.endsWith(')')) {
878
- return value;
879
- }
880
- return clampResolvedValueToPropertyRange(foldStandaloneCalc(value), property);
881
- }
882
-
883
- export {
884
- normalizeMathFunctions,
885
- simplifyStandaloneCalc
886
- };