@thejaredwilcurt/csslop 0.0.39 → 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 +10 -13
- package/src/rules/custom-properties.js +86 -9
- package/src/rules/optimize.js +88 -1
- package/src/value/math-expression.js +1103 -0
- package/src/value/math.js +342 -116
- package/src/value/minify.js +5 -3
- package/src/value/shared.js +52 -10
- package/vite.config.js +0 -4
package/src/value/math.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
* @file Simplifies CSS math functions (calc, min, max) by folding constant expressions, flattening nested calcs, and converting absolute lengths to pixels.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { calc } from '@csstools/css-calc';
|
|
6
|
-
|
|
7
5
|
import { findMatchingParenthesis } from '../parser/source-search.js';
|
|
8
6
|
|
|
7
|
+
import {
|
|
8
|
+
formatUnitTotals,
|
|
9
|
+
MATH_FUNCTION_NAMES,
|
|
10
|
+
simplifyCalc
|
|
11
|
+
} from './math-expression.js';
|
|
9
12
|
import {
|
|
10
13
|
convertAbsoluteLengthToPx,
|
|
11
|
-
|
|
14
|
+
formatResolvedDimension,
|
|
15
|
+
formatResolvedNumber,
|
|
12
16
|
roundCompactNumber
|
|
13
17
|
} from './shared.js';
|
|
14
18
|
|
|
@@ -85,25 +89,6 @@ function keepSeparatorAfterDissolvedFunction (replacer) {
|
|
|
85
89
|
};
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
/**
|
|
89
|
-
* The functions whose arguments are a math expression, where `*` and `/` are
|
|
90
|
-
* arithmetic operators rather than characters of some other syntax.
|
|
91
|
-
*
|
|
92
|
-
* @type {Set<string>}
|
|
93
|
-
*/
|
|
94
|
-
const MATH_FUNCTION_NAMES = new Set([
|
|
95
|
-
'abs',
|
|
96
|
-
'calc',
|
|
97
|
-
'clamp',
|
|
98
|
-
'hypot',
|
|
99
|
-
'max',
|
|
100
|
-
'min',
|
|
101
|
-
'mod',
|
|
102
|
-
'rem',
|
|
103
|
-
'round',
|
|
104
|
-
'sign'
|
|
105
|
-
]);
|
|
106
|
-
|
|
107
92
|
/**
|
|
108
93
|
* Matches a single character that may appear in a CSS function name.
|
|
109
94
|
*
|
|
@@ -246,19 +231,188 @@ function convertReciprocalMultiplicationToDivision (expression) {
|
|
|
246
231
|
}
|
|
247
232
|
|
|
248
233
|
/**
|
|
249
|
-
*
|
|
250
|
-
*
|
|
234
|
+
* Matches a whole expression that divides a percentage by a plain number,
|
|
235
|
+
* capturing the percentage and the divisor it is shared out between.
|
|
236
|
+
*
|
|
237
|
+
* @type {RegExp}
|
|
238
|
+
*/
|
|
239
|
+
const PERCENTAGE_DIVISION = /^([+-]?(?:\d+|\d*\.\d+))%\s*\/\s*([+-]?(?:\d+|\d*\.\d+))$/;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Picks between what a calculation resolved to and the calculation itself,
|
|
243
|
+
* keeping whichever takes fewer characters to write. A tie goes to the
|
|
244
|
+
* resolved value, which states the same thing without the arithmetic.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} resolved The value the calculation resolves to.
|
|
247
|
+
* @param {string} calculation The calculation, written as compactly as it can be.
|
|
248
|
+
* @return {string} The shorter of the two.
|
|
249
|
+
*/
|
|
250
|
+
function shorterOfResolutionAndCalculation (resolved, calculation) {
|
|
251
|
+
if (resolved.length <= calculation.length) {
|
|
252
|
+
return resolved;
|
|
253
|
+
}
|
|
254
|
+
return calculation;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Writes a calculation as compactly as its syntax allows, squeezing out every
|
|
259
|
+
* space but the ones a `+` or a `-` needs to be read as an operator.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} inner The contents of the `calc()`.
|
|
262
|
+
* @return {string} The whole `calc()`, written as compactly as it can be.
|
|
263
|
+
*/
|
|
264
|
+
function compactCalculation (inner) {
|
|
265
|
+
// Remove whitespace around multiplication and division operators
|
|
266
|
+
const compactInner = inner.replace(/\s+/g, ' ').trim().replace(/\s*([*/])\s*/g, '$1');
|
|
267
|
+
return 'calc(' + compactInner + ')';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Picks between a calculation and what resolving its arithmetic came to. A
|
|
272
|
+
* resolved value states the same thing without any arithmetic, so it wins a
|
|
273
|
+
* tie, but a result that is itself a calculation only states the same terms
|
|
274
|
+
* another way, and writing them out again for no saving gains nothing.
|
|
275
|
+
*
|
|
276
|
+
* @param {string} resolved The value the calculation resolved to.
|
|
277
|
+
* @param {string} calculation The calculation, written as compactly as it can be.
|
|
278
|
+
* @return {string} Whichever of the two the value is written as.
|
|
279
|
+
*/
|
|
280
|
+
function chooseResolvedCalculation (resolved, calculation) {
|
|
281
|
+
// A resolution that kept its function form left terms behind that only
|
|
282
|
+
// resolve once the element they are measured against is laid out
|
|
283
|
+
if (resolved.startsWith('calc(') && resolved.length >= calculation.length) {
|
|
284
|
+
return calculation;
|
|
285
|
+
}
|
|
286
|
+
return shorterOfResolutionAndCalculation(resolved, calculation);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Resolves a percentage divided by a number into the single percentage it
|
|
291
|
+
* equals, which is how `calc(100%/16)` comes to be written as `6.25%`. The
|
|
292
|
+
* division stays as it was written when the result is no shorter, and when
|
|
293
|
+
* the divisor is zero, since that resolves to nothing a stylesheet can state.
|
|
251
294
|
*
|
|
252
|
-
* @
|
|
295
|
+
* @param {string} expression The contents of a `calc()`, with its whitespace collapsed.
|
|
296
|
+
* @return {string|null} The shorter of the resolved percentage and the division, or null when the expression is not one.
|
|
253
297
|
*/
|
|
254
|
-
|
|
298
|
+
function resolvePercentageDivision (expression) {
|
|
299
|
+
const division = expression.match(PERCENTAGE_DIVISION);
|
|
300
|
+
if (!division) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const [, percentage, divisor] = division;
|
|
304
|
+
const calculation = 'calc(' + percentage + '%/' + divisor + ')';
|
|
305
|
+
if (Number(divisor) === 0) {
|
|
306
|
+
return calculation;
|
|
307
|
+
}
|
|
308
|
+
const resolved = formatResolvedNumber(Number(percentage) / Number(divisor)) + '%';
|
|
309
|
+
return shorterOfResolutionAndCalculation(resolved, calculation);
|
|
310
|
+
}
|
|
255
311
|
|
|
256
312
|
/**
|
|
257
|
-
* The
|
|
313
|
+
* The properties whose values every CSS specification defines as
|
|
314
|
+
* non-negative, so that a math function declared on one of them has its
|
|
315
|
+
* result clamped rather than read as written.
|
|
258
316
|
*
|
|
259
317
|
* @type {Set<string>}
|
|
260
318
|
*/
|
|
261
|
-
const
|
|
319
|
+
const NON_NEGATIVE_PROPERTIES = new Set([
|
|
320
|
+
'animation-duration',
|
|
321
|
+
'animation-iteration-count',
|
|
322
|
+
'background-size',
|
|
323
|
+
'block-size',
|
|
324
|
+
'border-block-end-width',
|
|
325
|
+
'border-block-start-width',
|
|
326
|
+
'border-block-width',
|
|
327
|
+
'border-bottom-left-radius',
|
|
328
|
+
'border-bottom-right-radius',
|
|
329
|
+
'border-bottom-width',
|
|
330
|
+
'border-end-end-radius',
|
|
331
|
+
'border-end-start-radius',
|
|
332
|
+
'border-image-outset',
|
|
333
|
+
'border-image-width',
|
|
334
|
+
'border-inline-end-width',
|
|
335
|
+
'border-inline-start-width',
|
|
336
|
+
'border-inline-width',
|
|
337
|
+
'border-left-width',
|
|
338
|
+
'border-radius',
|
|
339
|
+
'border-right-width',
|
|
340
|
+
'border-start-end-radius',
|
|
341
|
+
'border-start-start-radius',
|
|
342
|
+
'border-top-left-radius',
|
|
343
|
+
'border-top-right-radius',
|
|
344
|
+
'border-top-width',
|
|
345
|
+
'border-width',
|
|
346
|
+
'column-count',
|
|
347
|
+
'column-gap',
|
|
348
|
+
'column-rule-width',
|
|
349
|
+
'column-width',
|
|
350
|
+
'flex-basis',
|
|
351
|
+
'flex-grow',
|
|
352
|
+
'flex-shrink',
|
|
353
|
+
'font-size',
|
|
354
|
+
'gap',
|
|
355
|
+
'height',
|
|
356
|
+
'inline-size',
|
|
357
|
+
'line-height',
|
|
358
|
+
'mask-size',
|
|
359
|
+
'max-block-size',
|
|
360
|
+
'max-height',
|
|
361
|
+
'max-inline-size',
|
|
362
|
+
'max-width',
|
|
363
|
+
'min-block-size',
|
|
364
|
+
'min-height',
|
|
365
|
+
'min-inline-size',
|
|
366
|
+
'min-width',
|
|
367
|
+
'outline-width',
|
|
368
|
+
'padding',
|
|
369
|
+
'padding-block',
|
|
370
|
+
'padding-block-end',
|
|
371
|
+
'padding-block-start',
|
|
372
|
+
'padding-bottom',
|
|
373
|
+
'padding-inline',
|
|
374
|
+
'padding-inline-end',
|
|
375
|
+
'padding-inline-start',
|
|
376
|
+
'padding-left',
|
|
377
|
+
'padding-right',
|
|
378
|
+
'padding-top',
|
|
379
|
+
'perspective',
|
|
380
|
+
'row-gap',
|
|
381
|
+
'shape-margin',
|
|
382
|
+
'stroke-width',
|
|
383
|
+
'tab-size',
|
|
384
|
+
'transition-duration',
|
|
385
|
+
'width'
|
|
386
|
+
]);
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Matches a value written as a single negative number, with or without the
|
|
390
|
+
* unit or percent sign that follows its digits.
|
|
391
|
+
*
|
|
392
|
+
* @type {RegExp}
|
|
393
|
+
*/
|
|
394
|
+
const NEGATIVE_NUMBER_VALUE = /^-(?:\d+|\d*\.\d+)(?:[a-z]+|%)?$/i;
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Clamps what a math function resolved to into the range its property
|
|
398
|
+
* accepts. A negative length written out directly makes the declaration
|
|
399
|
+
* invalid and the CSS engine throws it away, but a math function is allowed
|
|
400
|
+
* to resolve to one: its result is clamped to the nearest value the property
|
|
401
|
+
* does take, which on a property that only accepts non-negative values is
|
|
402
|
+
* zero, as the range checking section of the CSS Values specification
|
|
403
|
+
* describes: https://drafts.csswg.org/css-values/#calc-range.
|
|
404
|
+
*
|
|
405
|
+
* @param {string} resolvedValue The value the math function resolved to.
|
|
406
|
+
* @param {string} property The property the math function is declared on.
|
|
407
|
+
* @return {string} The resolved value, held inside the property's range.
|
|
408
|
+
*/
|
|
409
|
+
function clampResolvedValueToPropertyRange (resolvedValue, property) {
|
|
410
|
+
const acceptsNegativeValues = !NON_NEGATIVE_PROPERTIES.has(String(property).toLowerCase());
|
|
411
|
+
if (acceptsNegativeValues || !NEGATIVE_NUMBER_VALUE.test(resolvedValue)) {
|
|
412
|
+
return resolvedValue;
|
|
413
|
+
}
|
|
414
|
+
return '0';
|
|
415
|
+
}
|
|
262
416
|
|
|
263
417
|
/**
|
|
264
418
|
* 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.
|
|
@@ -276,15 +430,15 @@ function tryFoldCalcExpression (expression) {
|
|
|
276
430
|
expr = expr.replace(/\(([^()]+)\)/g, '$1');
|
|
277
431
|
// Fold: <number> * <number><unit> → computed result in same unit
|
|
278
432
|
expr = expr.replace(/(-?(?:\d*\.\d+|\d+))\s*\*\s*(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q|%)/gi, (match, a, b, unit) => {
|
|
279
|
-
return
|
|
433
|
+
return formatResolvedDimension(parseFloat(a) * parseFloat(b), unit);
|
|
280
434
|
});
|
|
281
435
|
// Fold: <number><unit> * <number> → computed result in same unit
|
|
282
436
|
expr = expr.replace(/(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q|%)\s*\*\s*(-?(?:\d*\.\d+|\d+))/gi, (match, a, unit, b) => {
|
|
283
|
-
return
|
|
437
|
+
return formatResolvedDimension(parseFloat(a) * parseFloat(b), unit);
|
|
284
438
|
});
|
|
285
439
|
// Fold: <number><unit> / <number> → computed result in same unit
|
|
286
440
|
expr = expr.replace(/(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q)\s*\/\s*(-?(?:\d*\.\d+|\d+))/gi, (match, value, unit, divisor) => {
|
|
287
|
-
return
|
|
441
|
+
return formatResolvedDimension(parseFloat(value) / parseFloat(divisor), unit);
|
|
288
442
|
});
|
|
289
443
|
// Collapse zero-with-unit terms (e.g. 0px, 0%) to plain 0 or remove them
|
|
290
444
|
expr = expr.replace(/(^|[+-])\s*0(?:px|pt|pc|in|cm|mm|q|%)\b/g, (match, sign) => {
|
|
@@ -339,52 +493,109 @@ function tryFoldCalcExpression (expression) {
|
|
|
339
493
|
totals.set(unit, (totals.get(unit) || 0) + number);
|
|
340
494
|
}
|
|
341
495
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}).sort()];
|
|
345
|
-
const outputTerms = [];
|
|
496
|
+
return formatUnitTotals(totals);
|
|
497
|
+
}
|
|
346
498
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
499
|
+
/**
|
|
500
|
+
* Matches a whole expression that scales a percentage by a plain number,
|
|
501
|
+
* written either way around. Both operands are already as short as they can
|
|
502
|
+
* be, so the expression is only ever squeezed, never resolved.
|
|
503
|
+
*
|
|
504
|
+
* @type {RegExp}
|
|
505
|
+
*/
|
|
506
|
+
const PERCENTAGE_MULTIPLICATION = /^(?:-?(?:\d*\.\d+|\d+)%\s*\*\s*-?(?:\d*\.\d+|\d+)|-?(?:\d*\.\d+|\d+)\s*\*\s*-?(?:\d*\.\d+|\d+)%)$/;
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Matches a value that is a single percentage, which is all a calculation
|
|
510
|
+
* leaves behind once every term of it has resolved into one.
|
|
511
|
+
*
|
|
512
|
+
* @type {RegExp}
|
|
513
|
+
*/
|
|
514
|
+
const RESOLVED_PERCENTAGE = /^-?(?:\d+|\d*\.\d+)%$/;
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Chooses how a calculation that shares a percentage out resolves. The
|
|
518
|
+
* library states the result to the full precision a float carries, which is
|
|
519
|
+
* both longer than the calculation it replaces and finer than a stylesheet
|
|
520
|
+
* has any use for, so the result is rounded into the budget a resolved number
|
|
521
|
+
* is written within and kept only when it is the shorter of the two.
|
|
522
|
+
*
|
|
523
|
+
* @param {string} simplified The result the calculation library returned.
|
|
524
|
+
* @param {string} compactInner The contents of the `calc()`, with its whitespace collapsed.
|
|
525
|
+
* @param {string} match The whole `calc()` as it was written.
|
|
526
|
+
* @return {string} How the resolved calculation is written.
|
|
527
|
+
*/
|
|
528
|
+
function chooseResolvedPercentage (simplified, compactInner, match) {
|
|
529
|
+
// A division sign written after a percentage, which is what the resolved percentage was shared out by
|
|
530
|
+
const dividesAPercentage = RESOLVED_PERCENTAGE.test(simplified) && /%\s*\//.test(match);
|
|
531
|
+
if (!dividesAPercentage) {
|
|
532
|
+
return simplified;
|
|
356
533
|
}
|
|
534
|
+
// Remove whitespace around division operator
|
|
535
|
+
const calculation = 'calc(' + compactInner.replace(/\s*\/\s*/g, '/') + ')';
|
|
536
|
+
const resolved = formatResolvedNumber(parseFloat(simplified)) + '%';
|
|
537
|
+
return shorterOfResolutionAndCalculation(resolved, calculation);
|
|
538
|
+
}
|
|
357
539
|
|
|
358
|
-
|
|
359
|
-
|
|
540
|
+
/**
|
|
541
|
+
* Resolves the contents of a `calc()` into the shortest way of writing the
|
|
542
|
+
* same value, which is either the value the arithmetic works out to or the
|
|
543
|
+
* calculation itself with the whitespace squeezed out of it.
|
|
544
|
+
*
|
|
545
|
+
* @param {string} match The whole `calc()` as it was written.
|
|
546
|
+
* @param {string} compactInner The contents of the `calc()`, with its whitespace collapsed.
|
|
547
|
+
* @return {string} The shortest way of writing what the calculation states.
|
|
548
|
+
*/
|
|
549
|
+
function resolveCalcExpression (match, compactInner) {
|
|
550
|
+
if (PERCENTAGE_MULTIPLICATION.test(compactInner)) {
|
|
551
|
+
return compactCalculation(compactInner);
|
|
360
552
|
}
|
|
361
553
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
554
|
+
const dividedPercentage = resolvePercentageDivision(compactInner);
|
|
555
|
+
if (dividedPercentage) {
|
|
556
|
+
return dividedPercentage;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const folded = tryFoldCalcExpression(compactInner);
|
|
560
|
+
if (folded) {
|
|
561
|
+
return folded;
|
|
368
562
|
}
|
|
369
563
|
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const sign = term.value < 0 ? '-' : '+';
|
|
374
|
-
result += ' ' + sign + ' ' + roundCompactNumber(Math.abs(term.value)) + term.unit;
|
|
564
|
+
const simplified = simplifyCalc(match);
|
|
565
|
+
if (simplified === null) {
|
|
566
|
+
return match;
|
|
375
567
|
}
|
|
376
|
-
|
|
568
|
+
const resolved = chooseResolvedPercentage(simplified, compactInner, match);
|
|
569
|
+
return chooseResolvedCalculation(resolved, compactCalculation(compactInner));
|
|
377
570
|
}
|
|
378
571
|
|
|
379
572
|
/**
|
|
380
|
-
*
|
|
573
|
+
* Matches a math function other than `calc()` whose arguments hold no further
|
|
574
|
+
* parentheses, so that the whole of the function is matched. A `calc()` is
|
|
575
|
+
* left to its own pass, which weighs what the calculation resolves to against
|
|
576
|
+
* the calculation itself.
|
|
381
577
|
*
|
|
382
|
-
* @
|
|
383
|
-
* @param {string} property The CSS property name, used for context-aware simplification.
|
|
384
|
-
* @param {string} originalValue The original unmodified value to fall back to if simplification produces an invalid result.
|
|
385
|
-
* @return {string} The value with math functions simplified where possible.
|
|
578
|
+
* @type {RegExp}
|
|
386
579
|
*/
|
|
387
|
-
|
|
580
|
+
const RESOLVABLE_MATH_FUNCTION = new RegExp(
|
|
581
|
+
'\\b(?:' +
|
|
582
|
+
[...MATH_FUNCTION_NAMES].filter((name) => {
|
|
583
|
+
return name !== 'calc';
|
|
584
|
+
}).join('|') +
|
|
585
|
+
')\\([^()]+\\)',
|
|
586
|
+
'gi'
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Runs one pass of the math simplifications over a value: the rewrites that
|
|
591
|
+
* state a calculation more compactly, and the resolutions that replace a math
|
|
592
|
+
* function with the value it works out to.
|
|
593
|
+
*
|
|
594
|
+
* @param {string} value The CSS value string containing math functions to simplify.
|
|
595
|
+
* @param {string} property The CSS property name, whose range every resolved result is clamped into.
|
|
596
|
+
* @return {string} The value with its math functions simplified where possible.
|
|
597
|
+
*/
|
|
598
|
+
function simplifyMathFunctionsOnce (value, property) {
|
|
388
599
|
let result = rewriteMathFunctionContents(convertReciprocalMultiplicationToDivision, value);
|
|
389
600
|
|
|
390
601
|
// Unwrap calc(1 / (1 / x)) → x (double-reciprocal identity)
|
|
@@ -400,50 +611,47 @@ function normalizeMathFunctions (value, property, originalValue = '') {
|
|
|
400
611
|
return 'calc(' + inner + ')';
|
|
401
612
|
}));
|
|
402
613
|
|
|
403
|
-
//
|
|
404
|
-
result = result.replace(
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return typeof simplified === 'string' ? simplified : match;
|
|
408
|
-
} catch {
|
|
614
|
+
// Resolve every math function that compares or rounds its arguments
|
|
615
|
+
result = result.replace(RESOLVABLE_MATH_FUNCTION, keepSeparatorAfterDissolvedFunction((match) => {
|
|
616
|
+
const simplified = simplifyCalc(match);
|
|
617
|
+
if (simplified === null) {
|
|
409
618
|
return match;
|
|
410
619
|
}
|
|
620
|
+
return clampResolvedValueToPropertyRange(simplified, property);
|
|
411
621
|
}));
|
|
412
622
|
|
|
413
|
-
// Simplify calc() expressions using constant folding and
|
|
623
|
+
// Simplify calc() expressions using constant folding and expression resolution
|
|
414
624
|
result = result.replace(/calc\(([^()]+)\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
|
|
415
625
|
// Collapse whitespace inside calc expression
|
|
416
626
|
const compactInner = inner.replace(/\s+/g, ' ').trim();
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
return 'calc(' + compactInner.replace(/\s*([*/])\s*/g, '$1') + ')';
|
|
421
|
-
}
|
|
422
|
-
// Preserve percent/number division expressions (e.g. 100%/3) — just strip inner spaces
|
|
423
|
-
if (/^\d+(?:\.\d+)?%\s*\/\s*\d+(?:\.\d+)?$/i.test(compactInner)) {
|
|
424
|
-
// Remove whitespace around division operator
|
|
425
|
-
return 'calc(' + compactInner.replace(/\s*\/\s*/g, '/') + ')';
|
|
426
|
-
}
|
|
627
|
+
const resolved = resolveCalcExpression(match, compactInner);
|
|
628
|
+
return clampResolvedValueToPropertyRange(resolved, property);
|
|
629
|
+
}));
|
|
427
630
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
return folded;
|
|
431
|
-
}
|
|
631
|
+
return result;
|
|
632
|
+
}
|
|
432
633
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
634
|
+
/**
|
|
635
|
+
* Simplifies every math function within a CSS value string, resolving the ones
|
|
636
|
+
* whose operands are all known and stating the rest as compactly as they can
|
|
637
|
+
* be written.
|
|
638
|
+
*
|
|
639
|
+
* @param {string} value The CSS value string containing math functions to simplify.
|
|
640
|
+
* @param {string} property The CSS property name, whose range every resolved result is clamped into.
|
|
641
|
+
* @param {string} originalValue The value as it was authored, which tells whether a result was ever part of a calculation.
|
|
642
|
+
* @return {string} The value with math functions simplified where possible.
|
|
643
|
+
*/
|
|
644
|
+
function normalizeMathFunctions (value, property, originalValue = '') {
|
|
645
|
+
let result = value;
|
|
646
|
+
let previous;
|
|
647
|
+
|
|
648
|
+
do {
|
|
649
|
+
previous = result;
|
|
650
|
+
// Resolving a math function leaves a plain value where it stood, which can
|
|
651
|
+
// bring the function it was nested in within reach of the next pass, so
|
|
652
|
+
// the value is simplified until it settles
|
|
653
|
+
result = simplifyMathFunctionsOnce(result, property);
|
|
654
|
+
} while (result !== previous);
|
|
447
655
|
|
|
448
656
|
// When calc() folded to an absolute-length unit (pt, pc, in, cm, mm, q), convert to pixels
|
|
449
657
|
if (originalValue.includes('calc(') && /^-?(?:\d+|\d*\.\d+)(pt|pc|in|cm|mm|q)$/i.test(result)) {
|
|
@@ -456,29 +664,33 @@ function normalizeMathFunctions (value, property, originalValue = '') {
|
|
|
456
664
|
}
|
|
457
665
|
}
|
|
458
666
|
|
|
459
|
-
// Round
|
|
667
|
+
// Round a result whose decimal places run past the budget a resolved number is written within
|
|
460
668
|
result = result.replace(/(-?(?:\d+|\d*\.\d+)\.\d{4,})([a-z%]+)/gi, (match, number, unit) => {
|
|
461
|
-
return
|
|
669
|
+
return formatResolvedNumber(number) + unit;
|
|
462
670
|
});
|
|
463
671
|
return result;
|
|
464
672
|
}
|
|
465
673
|
|
|
466
674
|
/**
|
|
467
|
-
* Simplifies a standalone calc() value by flattening nested calc expressions, converting absolute length units to pixels, and folding constant terms.
|
|
675
|
+
* Simplifies a standalone calc() value by resolving its arithmetic, flattening nested calc expressions, converting absolute length units to pixels, and folding constant terms.
|
|
468
676
|
*
|
|
469
|
-
* @param {string} value The CSS value string
|
|
677
|
+
* @param {string} value The CSS value string, already known to be a single `calc()`.
|
|
470
678
|
* @return {string} The simplified value, or the original value if simplification is not applicable.
|
|
471
679
|
*/
|
|
472
|
-
function
|
|
473
|
-
// Check if value starts with calc( and ends with )
|
|
474
|
-
if (!/^calc\(/i.test(value) || !value.endsWith(')')) {
|
|
475
|
-
return value;
|
|
476
|
-
}
|
|
680
|
+
function foldStandaloneCalc (value) {
|
|
477
681
|
let inner = value.slice(5, -1).trim();
|
|
478
682
|
|
|
479
683
|
// Preserve percent-times-number expressions, only stripping whitespace around operators
|
|
480
|
-
if (
|
|
481
|
-
return
|
|
684
|
+
if (PERCENTAGE_MULTIPLICATION.test(inner.replace(/\s+/g, ' ').trim())) {
|
|
685
|
+
return compactCalculation(inner);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Resolve the calculation as it was written, which is the only reading of it
|
|
689
|
+
// that keeps the grouping its parentheses state. Everything below reads a
|
|
690
|
+
// flattened copy of the expression, where that grouping is already gone.
|
|
691
|
+
const resolved = simplifyCalc(value);
|
|
692
|
+
if (resolved !== null) {
|
|
693
|
+
return chooseResolvedCalculation(resolved, compactCalculation(inner));
|
|
482
694
|
}
|
|
483
695
|
|
|
484
696
|
// If no nested function calls and no multiplication/division involving parens, try flattening
|
|
@@ -515,14 +727,28 @@ function simplifyStandaloneCalc (value) {
|
|
|
515
727
|
}
|
|
516
728
|
|
|
517
729
|
// Collapse whitespace and check for percent-division expressions
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
return 'calc(' + compactInner.replace(/\s*\/\s*/g, '/') + ')';
|
|
730
|
+
const dividedPercentage = resolvePercentageDivision(inner.replace(/\s+/g, ' ').trim());
|
|
731
|
+
if (dividedPercentage) {
|
|
732
|
+
return dividedPercentage;
|
|
522
733
|
}
|
|
523
734
|
|
|
524
|
-
|
|
525
|
-
|
|
735
|
+
return compactCalculation(inner);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Simplifies a value that is a single `calc()`, holding whatever it resolves
|
|
740
|
+
* to inside the range the property it is declared on accepts.
|
|
741
|
+
*
|
|
742
|
+
* @param {string} value The CSS value string that may be a standalone calc() expression.
|
|
743
|
+
* @param {string} property The CSS property the value is declared on.
|
|
744
|
+
* @return {string} The simplified value, or the original value if simplification is not applicable.
|
|
745
|
+
*/
|
|
746
|
+
function simplifyStandaloneCalc (value, property = '') {
|
|
747
|
+
// Check if value starts with calc( and ends with )
|
|
748
|
+
if (!/^calc\(/i.test(value) || !value.endsWith(')')) {
|
|
749
|
+
return value;
|
|
750
|
+
}
|
|
751
|
+
return clampResolvedValueToPropertyRange(foldStandaloneCalc(value), property);
|
|
526
752
|
}
|
|
527
753
|
|
|
528
754
|
export {
|
package/src/value/minify.js
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
import {
|
|
38
38
|
collapseShorthandParts,
|
|
39
39
|
convertAbsoluteLengthToPx,
|
|
40
|
+
formatResolvedNumber,
|
|
40
41
|
hasSubstitutedParts,
|
|
41
42
|
normalizeScaleComponent,
|
|
42
43
|
parseAlphaString,
|
|
@@ -1376,7 +1377,7 @@ function computeMinifiedValue (declaration) {
|
|
|
1376
1377
|
// Remove whitespace around * and / operators (safe outside calc context)
|
|
1377
1378
|
val = val.replace(/\s*([*/])\s*/g, '$1');
|
|
1378
1379
|
val = normalizeMathFunctions(val, declaration.property, declaration.value || '');
|
|
1379
|
-
val = simplifyStandaloneCalc(val);
|
|
1380
|
+
val = simplifyStandaloneCalc(val, declaration.property);
|
|
1380
1381
|
// Simplify calc() expressions containing zero-percent additive terms
|
|
1381
1382
|
val = val.replace(/calc\(([^()]+)\)/gi, (match, inner) => {
|
|
1382
1383
|
// Collapse whitespace inside calc expression
|
|
@@ -1400,10 +1401,11 @@ function computeMinifiedValue (declaration) {
|
|
|
1400
1401
|
}
|
|
1401
1402
|
val = val.replace(/(^|\s|,|\()(-?)0+(\.\d+)/g, '$1$2$3'); // e.g. 0.5 -> .5, -0.5 -> -.5
|
|
1402
1403
|
|
|
1403
|
-
// If value is a standalone number with optional unit, round it
|
|
1404
|
+
// If value is a standalone number with optional unit, round it into the
|
|
1405
|
+
// character budget a number is written within
|
|
1404
1406
|
if (/^[+-]?(?:\d+|\d*\.\d+)([a-z%]+)?$/i.test(val)) {
|
|
1405
1407
|
const [, rawNumber, rawUnit = ''] = val.match(/^([+-]?(?:\d+|\d*\.\d+))([a-z%]+)?$/i);
|
|
1406
|
-
val =
|
|
1408
|
+
val = formatResolvedNumber(rawNumber) + rawUnit;
|
|
1407
1409
|
}
|
|
1408
1410
|
|
|
1409
1411
|
val = lowercaseHexColors(val);
|