@thejaredwilcurt/csslop 0.0.38 → 0.0.40
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 +7 -2
- package/package.json +12 -11
- package/src/declarations/background.js +29 -37
- package/src/declarations/border.js +1 -1
- package/src/declarations/fold.js +1 -1
- package/src/declarations/process.js +17 -8
- package/src/declarations/shorthand-values.js +1 -1
- package/src/index.js +15 -44
- package/src/parser/at-rules.js +291 -0
- package/src/parser/index.js +826 -0
- package/src/parser/positions.js +71 -0
- package/src/parser/scanner.js +229 -0
- package/src/parser/source-search.js +287 -0
- package/src/parser/tokenizer.js +396 -0
- package/src/preprocess.js +20 -228
- package/src/rules/custom-properties.js +86 -9
- package/src/rules/lang.js +20 -56
- package/src/rules/normalize.js +80 -0
- package/src/rules/optimize.js +271 -111
- package/src/rules/selectors.js +56 -221
- package/src/rules/stringify.js +4 -5
- package/src/value/color-mix.js +22 -47
- package/src/value/gradients.js +44 -71
- package/src/value/light-dark.js +9 -94
- package/src/value/math.js +271 -76
- package/src/value/minify.js +89 -54
- package/src/value/relative-colors.js +15 -78
- package/src/value/shared.js +53 -10
- package/src/value/tokens.js +4 -387
- package/src/value/transforms.js +20 -53
- package/src/value/syntax.js +0 -144
package/src/value/math.js
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import { calc } from '@csstools/css-calc';
|
|
6
6
|
|
|
7
|
+
import { findMatchingParenthesis } from '../parser/source-search.js';
|
|
8
|
+
|
|
7
9
|
import {
|
|
8
10
|
convertAbsoluteLengthToPx,
|
|
9
|
-
|
|
11
|
+
formatResolvedDimension,
|
|
12
|
+
formatResolvedNumber,
|
|
10
13
|
roundCompactNumber
|
|
11
14
|
} from './shared.js';
|
|
12
15
|
|
|
@@ -124,28 +127,6 @@ function findFunctionNameStart (value, openIndex) {
|
|
|
124
127
|
return index;
|
|
125
128
|
}
|
|
126
129
|
|
|
127
|
-
/**
|
|
128
|
-
* Finds the parenthesis closing the one at the given index.
|
|
129
|
-
*
|
|
130
|
-
* @param {string} value The CSS value string being scanned.
|
|
131
|
-
* @param {number} openIndex The index of the opening parenthesis.
|
|
132
|
-
* @return {number} The index of the matching closing parenthesis, or -1 when it never closes.
|
|
133
|
-
*/
|
|
134
|
-
function findClosingParenthesis (value, openIndex) {
|
|
135
|
-
let depth = 0;
|
|
136
|
-
for (let index = openIndex; index < value.length; index++) {
|
|
137
|
-
if (value[index] === '(') {
|
|
138
|
-
depth++;
|
|
139
|
-
} else if (value[index] === ')') {
|
|
140
|
-
depth--;
|
|
141
|
-
if (depth === 0) {
|
|
142
|
-
return index;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return -1;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
130
|
/**
|
|
150
131
|
* Rewrites the contents of every math function in a value, leaving the rest of
|
|
151
132
|
* the value untouched. A `*` only means multiplication inside these functions,
|
|
@@ -166,7 +147,7 @@ function rewriteMathFunctionContents (rewriteExpression, value) {
|
|
|
166
147
|
break;
|
|
167
148
|
}
|
|
168
149
|
const functionName = value.slice(findFunctionNameStart(value, openIndex), openIndex).toLowerCase();
|
|
169
|
-
const closeIndex =
|
|
150
|
+
const closeIndex = findMatchingParenthesis(value, openIndex);
|
|
170
151
|
if (!MATH_FUNCTION_NAMES.has(functionName) || closeIndex === -1) {
|
|
171
152
|
result += value.slice(index, openIndex + 1);
|
|
172
153
|
index = openIndex + 1;
|
|
@@ -265,6 +246,158 @@ function convertReciprocalMultiplicationToDivision (expression) {
|
|
|
265
246
|
});
|
|
266
247
|
}
|
|
267
248
|
|
|
249
|
+
/**
|
|
250
|
+
* Matches a whole expression that divides a percentage by a plain number,
|
|
251
|
+
* capturing the percentage and the divisor it is shared out between.
|
|
252
|
+
*
|
|
253
|
+
* @type {RegExp}
|
|
254
|
+
*/
|
|
255
|
+
const PERCENTAGE_DIVISION = /^([+-]?(?:\d+|\d*\.\d+))%\s*\/\s*([+-]?(?:\d+|\d*\.\d+))$/;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Picks between what a calculation resolved to and the calculation itself,
|
|
259
|
+
* keeping whichever takes fewer characters to write. A tie goes to the
|
|
260
|
+
* resolved value, which states the same thing without the arithmetic.
|
|
261
|
+
*
|
|
262
|
+
* @param {string} resolved The value the calculation resolves to.
|
|
263
|
+
* @param {string} calculation The calculation, written as compactly as it can be.
|
|
264
|
+
* @return {string} The shorter of the two.
|
|
265
|
+
*/
|
|
266
|
+
function shorterOfResolutionAndCalculation (resolved, calculation) {
|
|
267
|
+
if (resolved.length <= calculation.length) {
|
|
268
|
+
return resolved;
|
|
269
|
+
}
|
|
270
|
+
return calculation;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Resolves a percentage divided by a number into the single percentage it
|
|
275
|
+
* equals, which is how `calc(100%/16)` comes to be written as `6.25%`. The
|
|
276
|
+
* division stays as it was written when the result is no shorter, and when
|
|
277
|
+
* the divisor is zero, since that resolves to nothing a stylesheet can state.
|
|
278
|
+
*
|
|
279
|
+
* @param {string} expression The contents of a `calc()`, with its whitespace collapsed.
|
|
280
|
+
* @return {string|null} The shorter of the resolved percentage and the division, or null when the expression is not one.
|
|
281
|
+
*/
|
|
282
|
+
function resolvePercentageDivision (expression) {
|
|
283
|
+
const division = expression.match(PERCENTAGE_DIVISION);
|
|
284
|
+
if (!division) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
const [, percentage, divisor] = division;
|
|
288
|
+
const calculation = 'calc(' + percentage + '%/' + divisor + ')';
|
|
289
|
+
if (Number(divisor) === 0) {
|
|
290
|
+
return calculation;
|
|
291
|
+
}
|
|
292
|
+
const resolved = formatResolvedNumber(Number(percentage) / Number(divisor)) + '%';
|
|
293
|
+
return shorterOfResolutionAndCalculation(resolved, calculation);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The properties whose values every CSS specification defines as
|
|
298
|
+
* non-negative, so that a math function declared on one of them has its
|
|
299
|
+
* result clamped rather than read as written.
|
|
300
|
+
*
|
|
301
|
+
* @type {Set<string>}
|
|
302
|
+
*/
|
|
303
|
+
const NON_NEGATIVE_PROPERTIES = new Set([
|
|
304
|
+
'animation-duration',
|
|
305
|
+
'animation-iteration-count',
|
|
306
|
+
'background-size',
|
|
307
|
+
'block-size',
|
|
308
|
+
'border-block-end-width',
|
|
309
|
+
'border-block-start-width',
|
|
310
|
+
'border-block-width',
|
|
311
|
+
'border-bottom-left-radius',
|
|
312
|
+
'border-bottom-right-radius',
|
|
313
|
+
'border-bottom-width',
|
|
314
|
+
'border-end-end-radius',
|
|
315
|
+
'border-end-start-radius',
|
|
316
|
+
'border-image-outset',
|
|
317
|
+
'border-image-width',
|
|
318
|
+
'border-inline-end-width',
|
|
319
|
+
'border-inline-start-width',
|
|
320
|
+
'border-inline-width',
|
|
321
|
+
'border-left-width',
|
|
322
|
+
'border-radius',
|
|
323
|
+
'border-right-width',
|
|
324
|
+
'border-start-end-radius',
|
|
325
|
+
'border-start-start-radius',
|
|
326
|
+
'border-top-left-radius',
|
|
327
|
+
'border-top-right-radius',
|
|
328
|
+
'border-top-width',
|
|
329
|
+
'border-width',
|
|
330
|
+
'column-count',
|
|
331
|
+
'column-gap',
|
|
332
|
+
'column-rule-width',
|
|
333
|
+
'column-width',
|
|
334
|
+
'flex-basis',
|
|
335
|
+
'flex-grow',
|
|
336
|
+
'flex-shrink',
|
|
337
|
+
'font-size',
|
|
338
|
+
'gap',
|
|
339
|
+
'height',
|
|
340
|
+
'inline-size',
|
|
341
|
+
'line-height',
|
|
342
|
+
'mask-size',
|
|
343
|
+
'max-block-size',
|
|
344
|
+
'max-height',
|
|
345
|
+
'max-inline-size',
|
|
346
|
+
'max-width',
|
|
347
|
+
'min-block-size',
|
|
348
|
+
'min-height',
|
|
349
|
+
'min-inline-size',
|
|
350
|
+
'min-width',
|
|
351
|
+
'outline-width',
|
|
352
|
+
'padding',
|
|
353
|
+
'padding-block',
|
|
354
|
+
'padding-block-end',
|
|
355
|
+
'padding-block-start',
|
|
356
|
+
'padding-bottom',
|
|
357
|
+
'padding-inline',
|
|
358
|
+
'padding-inline-end',
|
|
359
|
+
'padding-inline-start',
|
|
360
|
+
'padding-left',
|
|
361
|
+
'padding-right',
|
|
362
|
+
'padding-top',
|
|
363
|
+
'perspective',
|
|
364
|
+
'row-gap',
|
|
365
|
+
'shape-margin',
|
|
366
|
+
'stroke-width',
|
|
367
|
+
'tab-size',
|
|
368
|
+
'transition-duration',
|
|
369
|
+
'width'
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Matches a value written as a single negative number, with or without the
|
|
374
|
+
* unit or percent sign that follows its digits.
|
|
375
|
+
*
|
|
376
|
+
* @type {RegExp}
|
|
377
|
+
*/
|
|
378
|
+
const NEGATIVE_NUMBER_VALUE = /^-(?:\d+|\d*\.\d+)(?:[a-z]+|%)?$/i;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Clamps what a math function resolved to into the range its property
|
|
382
|
+
* accepts. A negative length written out directly makes the declaration
|
|
383
|
+
* invalid and the CSS engine throws it away, but a math function is allowed
|
|
384
|
+
* to resolve to one: its result is clamped to the nearest value the property
|
|
385
|
+
* does take, which on a property that only accepts non-negative values is
|
|
386
|
+
* zero, as the range checking section of the CSS Values specification
|
|
387
|
+
* describes: https://drafts.csswg.org/css-values/#calc-range.
|
|
388
|
+
*
|
|
389
|
+
* @param {string} resolvedValue The value the math function resolved to.
|
|
390
|
+
* @param {string} property The property the math function is declared on.
|
|
391
|
+
* @return {string} The resolved value, held inside the property's range.
|
|
392
|
+
*/
|
|
393
|
+
function clampResolvedValueToPropertyRange (resolvedValue, property) {
|
|
394
|
+
const acceptsNegativeValues = !NON_NEGATIVE_PROPERTIES.has(String(property).toLowerCase());
|
|
395
|
+
if (acceptsNegativeValues || !NEGATIVE_NUMBER_VALUE.test(resolvedValue)) {
|
|
396
|
+
return resolvedValue;
|
|
397
|
+
}
|
|
398
|
+
return '0';
|
|
399
|
+
}
|
|
400
|
+
|
|
268
401
|
/**
|
|
269
402
|
* The units a folded calc() expression leads with, in the order they are
|
|
270
403
|
* written. Every other unit follows them alphabetically.
|
|
@@ -296,15 +429,15 @@ function tryFoldCalcExpression (expression) {
|
|
|
296
429
|
expr = expr.replace(/\(([^()]+)\)/g, '$1');
|
|
297
430
|
// Fold: <number> * <number><unit> → computed result in same unit
|
|
298
431
|
expr = expr.replace(/(-?(?:\d*\.\d+|\d+))\s*\*\s*(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q|%)/gi, (match, a, b, unit) => {
|
|
299
|
-
return
|
|
432
|
+
return formatResolvedDimension(parseFloat(a) * parseFloat(b), unit);
|
|
300
433
|
});
|
|
301
434
|
// Fold: <number><unit> * <number> → computed result in same unit
|
|
302
435
|
expr = expr.replace(/(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q|%)\s*\*\s*(-?(?:\d*\.\d+|\d+))/gi, (match, a, unit, b) => {
|
|
303
|
-
return
|
|
436
|
+
return formatResolvedDimension(parseFloat(a) * parseFloat(b), unit);
|
|
304
437
|
});
|
|
305
438
|
// Fold: <number><unit> / <number> → computed result in same unit
|
|
306
439
|
expr = expr.replace(/(-?(?:\d*\.\d+|\d+))(px|pt|pc|in|cm|mm|q)\s*\/\s*(-?(?:\d*\.\d+|\d+))/gi, (match, value, unit, divisor) => {
|
|
307
|
-
return
|
|
440
|
+
return formatResolvedDimension(parseFloat(value) / parseFloat(divisor), unit);
|
|
308
441
|
});
|
|
309
442
|
// Collapse zero-with-unit terms (e.g. 0px, 0%) to plain 0 or remove them
|
|
310
443
|
expr = expr.replace(/(^|[+-])\s*0(?:px|pt|pc|in|cm|mm|q|%)\b/g, (match, sign) => {
|
|
@@ -381,26 +514,100 @@ function tryFoldCalcExpression (expression) {
|
|
|
381
514
|
|
|
382
515
|
if (outputTerms.length === 1) {
|
|
383
516
|
const { unit, value } = outputTerms[0];
|
|
384
|
-
|
|
385
|
-
return roundCompactNumber(value) + unit;
|
|
386
|
-
}
|
|
387
|
-
return roundCompactNumber(value);
|
|
517
|
+
return formatResolvedDimension(value, unit);
|
|
388
518
|
}
|
|
389
519
|
|
|
390
520
|
const [first, ...rest] = outputTerms;
|
|
391
|
-
let result =
|
|
521
|
+
let result = formatResolvedDimension(first.value, first.unit);
|
|
392
522
|
for (const term of rest) {
|
|
393
523
|
const sign = term.value < 0 ? '-' : '+';
|
|
394
|
-
result += ' ' + sign + ' ' +
|
|
524
|
+
result += ' ' + sign + ' ' + formatResolvedDimension(Math.abs(term.value), term.unit);
|
|
395
525
|
}
|
|
396
526
|
return 'calc(' + result + ')';
|
|
397
527
|
}
|
|
398
528
|
|
|
529
|
+
/**
|
|
530
|
+
* Matches a whole expression that scales a percentage by a plain number,
|
|
531
|
+
* written either way around. Both operands are already as short as they can
|
|
532
|
+
* be, so the expression is only ever squeezed, never resolved.
|
|
533
|
+
*
|
|
534
|
+
* @type {RegExp}
|
|
535
|
+
*/
|
|
536
|
+
const PERCENTAGE_MULTIPLICATION = /^(?:-?(?:\d*\.\d+|\d+)%\s*\*\s*-?(?:\d*\.\d+|\d+)|-?(?:\d*\.\d+|\d+)\s*\*\s*-?(?:\d*\.\d+|\d+)%)$/;
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Matches a value that is a single percentage, which is all a calculation
|
|
540
|
+
* leaves behind once every term of it has resolved into one.
|
|
541
|
+
*
|
|
542
|
+
* @type {RegExp}
|
|
543
|
+
*/
|
|
544
|
+
const RESOLVED_PERCENTAGE = /^-?(?:\d+|\d*\.\d+)%$/;
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Chooses how a calculation that shares a percentage out resolves. The
|
|
548
|
+
* library states the result to the full precision a float carries, which is
|
|
549
|
+
* both longer than the calculation it replaces and finer than a stylesheet
|
|
550
|
+
* has any use for, so the result is rounded into the budget a resolved number
|
|
551
|
+
* is written within and kept only when it is the shorter of the two.
|
|
552
|
+
*
|
|
553
|
+
* @param {string} simplified The result the calculation library returned.
|
|
554
|
+
* @param {string} compactInner The contents of the `calc()`, with its whitespace collapsed.
|
|
555
|
+
* @param {string} match The whole `calc()` as it was written.
|
|
556
|
+
* @return {string} How the resolved calculation is written.
|
|
557
|
+
*/
|
|
558
|
+
function chooseResolvedPercentage (simplified, compactInner, match) {
|
|
559
|
+
// A division sign written after a percentage, which is what the resolved percentage was shared out by
|
|
560
|
+
const dividesAPercentage = RESOLVED_PERCENTAGE.test(simplified) && /%\s*\//.test(match);
|
|
561
|
+
if (!dividesAPercentage) {
|
|
562
|
+
return simplified;
|
|
563
|
+
}
|
|
564
|
+
// Remove whitespace around division operator
|
|
565
|
+
const calculation = 'calc(' + compactInner.replace(/\s*\/\s*/g, '/') + ')';
|
|
566
|
+
const resolved = formatResolvedNumber(parseFloat(simplified)) + '%';
|
|
567
|
+
return shorterOfResolutionAndCalculation(resolved, calculation);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Resolves the contents of a `calc()` into the shortest way of writing the
|
|
572
|
+
* same value, which is either the value the arithmetic works out to or the
|
|
573
|
+
* calculation itself with the whitespace squeezed out of it.
|
|
574
|
+
*
|
|
575
|
+
* @param {string} match The whole `calc()` as it was written.
|
|
576
|
+
* @param {string} compactInner The contents of the `calc()`, with its whitespace collapsed.
|
|
577
|
+
* @return {string} The shortest way of writing what the calculation states.
|
|
578
|
+
*/
|
|
579
|
+
function resolveCalcExpression (match, compactInner) {
|
|
580
|
+
if (PERCENTAGE_MULTIPLICATION.test(compactInner)) {
|
|
581
|
+
// Remove whitespace around multiplication/division operators
|
|
582
|
+
return 'calc(' + compactInner.replace(/\s*([*/])\s*/g, '$1') + ')';
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const dividedPercentage = resolvePercentageDivision(compactInner);
|
|
586
|
+
if (dividedPercentage) {
|
|
587
|
+
return dividedPercentage;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const folded = tryFoldCalcExpression(compactInner);
|
|
591
|
+
if (folded) {
|
|
592
|
+
return folded;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
const simplified = calc(match);
|
|
597
|
+
if (typeof simplified !== 'string') {
|
|
598
|
+
return match;
|
|
599
|
+
}
|
|
600
|
+
return chooseResolvedPercentage(simplified, compactInner, match);
|
|
601
|
+
} catch {
|
|
602
|
+
return match;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
399
606
|
/**
|
|
400
607
|
* Simplifies calc(), min(), and max() expressions within a CSS value string using the `@csstools`/css-calc library, falling back to the original value on failure.
|
|
401
608
|
*
|
|
402
609
|
* @param {string} value The CSS value string containing math functions to simplify.
|
|
403
|
-
* @param {string} property The CSS property name,
|
|
610
|
+
* @param {string} property The CSS property name, whose range every resolved result is clamped into.
|
|
404
611
|
* @param {string} originalValue The original unmodified value to fall back to if simplification produces an invalid result.
|
|
405
612
|
* @return {string} The value with math functions simplified where possible.
|
|
406
613
|
*/
|
|
@@ -424,7 +631,10 @@ function normalizeMathFunctions (value, property, originalValue = '') {
|
|
|
424
631
|
result = result.replace(/\b(min|max)\(([^()]+)\)/gi, keepSeparatorAfterDissolvedFunction((match) => {
|
|
425
632
|
try {
|
|
426
633
|
const simplified = calc(match);
|
|
427
|
-
|
|
634
|
+
if (typeof simplified !== 'string') {
|
|
635
|
+
return match;
|
|
636
|
+
}
|
|
637
|
+
return clampResolvedValueToPropertyRange(simplified, property);
|
|
428
638
|
} catch {
|
|
429
639
|
return match;
|
|
430
640
|
}
|
|
@@ -434,35 +644,8 @@ function normalizeMathFunctions (value, property, originalValue = '') {
|
|
|
434
644
|
result = result.replace(/calc\(([^()]+)\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
|
|
435
645
|
// Collapse whitespace inside calc expression
|
|
436
646
|
const compactInner = inner.replace(/\s+/g, ' ').trim();
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
// Remove whitespace around multiplication/division operators
|
|
440
|
-
return 'calc(' + compactInner.replace(/\s*([*/])\s*/g, '$1') + ')';
|
|
441
|
-
}
|
|
442
|
-
// Preserve percent/number division expressions (e.g. 100%/3) — just strip inner spaces
|
|
443
|
-
if (/^\d+(?:\.\d+)?%\s*\/\s*\d+(?:\.\d+)?$/i.test(compactInner)) {
|
|
444
|
-
// Remove whitespace around division operator
|
|
445
|
-
return 'calc(' + compactInner.replace(/\s*\/\s*/g, '/') + ')';
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const folded = tryFoldCalcExpression(compactInner);
|
|
449
|
-
if (folded) {
|
|
450
|
-
return folded;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
try {
|
|
454
|
-
const simplified = calc(match);
|
|
455
|
-
if (typeof simplified !== 'string') {
|
|
456
|
-
return match;
|
|
457
|
-
}
|
|
458
|
-
// If simplified to a bare percentage but original had division, preserve the calc form
|
|
459
|
-
if (/^-?(?:\d+|\d*\.\d+)%$/.test(simplified) && /%\s*\//.test(match)) {
|
|
460
|
-
return 'calc(' + compactInner.replace(/\s*\/\s*/g, '/') + ')';
|
|
461
|
-
}
|
|
462
|
-
return simplified;
|
|
463
|
-
} catch {
|
|
464
|
-
return match;
|
|
465
|
-
}
|
|
647
|
+
const resolved = resolveCalcExpression(match, compactInner);
|
|
648
|
+
return clampResolvedValueToPropertyRange(resolved, property);
|
|
466
649
|
}));
|
|
467
650
|
|
|
468
651
|
// When calc() folded to an absolute-length unit (pt, pc, in, cm, mm, q), convert to pixels
|
|
@@ -476,9 +659,9 @@ function normalizeMathFunctions (value, property, originalValue = '') {
|
|
|
476
659
|
}
|
|
477
660
|
}
|
|
478
661
|
|
|
479
|
-
// Round
|
|
662
|
+
// Round a result whose decimal places run past the budget a resolved number is written within
|
|
480
663
|
result = result.replace(/(-?(?:\d+|\d*\.\d+)\.\d{4,})([a-z%]+)/gi, (match, number, unit) => {
|
|
481
|
-
return
|
|
664
|
+
return formatResolvedNumber(number) + unit;
|
|
482
665
|
});
|
|
483
666
|
return result;
|
|
484
667
|
}
|
|
@@ -486,18 +669,14 @@ function normalizeMathFunctions (value, property, originalValue = '') {
|
|
|
486
669
|
/**
|
|
487
670
|
* Simplifies a standalone calc() value by flattening nested calc expressions, converting absolute length units to pixels, and folding constant terms.
|
|
488
671
|
*
|
|
489
|
-
* @param {string} value The CSS value string
|
|
672
|
+
* @param {string} value The CSS value string, already known to be a single `calc()`.
|
|
490
673
|
* @return {string} The simplified value, or the original value if simplification is not applicable.
|
|
491
674
|
*/
|
|
492
|
-
function
|
|
493
|
-
// Check if value starts with calc( and ends with )
|
|
494
|
-
if (!/^calc\(/i.test(value) || !value.endsWith(')')) {
|
|
495
|
-
return value;
|
|
496
|
-
}
|
|
675
|
+
function foldStandaloneCalc (value) {
|
|
497
676
|
let inner = value.slice(5, -1).trim();
|
|
498
677
|
|
|
499
678
|
// Preserve percent-times-number expressions, only stripping whitespace around operators
|
|
500
|
-
if (
|
|
679
|
+
if (PERCENTAGE_MULTIPLICATION.test(inner.replace(/\s+/g, ' ').trim())) {
|
|
501
680
|
return 'calc(' + inner.replace(/\s*([*/])\s*/g, '$1') + ')';
|
|
502
681
|
}
|
|
503
682
|
|
|
@@ -536,15 +715,31 @@ function simplifyStandaloneCalc (value) {
|
|
|
536
715
|
|
|
537
716
|
// Collapse whitespace and check for percent-division expressions
|
|
538
717
|
const compactInner = inner.replace(/\s+/g, ' ').trim();
|
|
539
|
-
|
|
540
|
-
if (
|
|
541
|
-
return
|
|
718
|
+
const dividedPercentage = resolvePercentageDivision(compactInner);
|
|
719
|
+
if (dividedPercentage) {
|
|
720
|
+
return dividedPercentage;
|
|
542
721
|
}
|
|
543
722
|
|
|
544
723
|
// Default: strip whitespace around multiplication/division operators
|
|
545
724
|
return 'calc(' + compactInner.replace(/\s*([*/])\s*/g, '$1') + ')';
|
|
546
725
|
}
|
|
547
726
|
|
|
727
|
+
/**
|
|
728
|
+
* Simplifies a value that is a single `calc()`, holding whatever it resolves
|
|
729
|
+
* to inside the range the property it is declared on accepts.
|
|
730
|
+
*
|
|
731
|
+
* @param {string} value The CSS value string that may be a standalone calc() expression.
|
|
732
|
+
* @param {string} property The CSS property the value is declared on.
|
|
733
|
+
* @return {string} The simplified value, or the original value if simplification is not applicable.
|
|
734
|
+
*/
|
|
735
|
+
function simplifyStandaloneCalc (value, property = '') {
|
|
736
|
+
// Check if value starts with calc( and ends with )
|
|
737
|
+
if (!/^calc\(/i.test(value) || !value.endsWith(')')) {
|
|
738
|
+
return value;
|
|
739
|
+
}
|
|
740
|
+
return clampResolvedValueToPropertyRange(foldStandaloneCalc(value), property);
|
|
741
|
+
}
|
|
742
|
+
|
|
548
743
|
export {
|
|
549
744
|
normalizeMathFunctions,
|
|
550
745
|
simplifyStandaloneCalc
|
package/src/value/minify.js
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { isUnicodeCharset } from '../context.js';
|
|
6
|
+
import {
|
|
7
|
+
findMatchingParenthesis,
|
|
8
|
+
skipParenthesizedGroup,
|
|
9
|
+
skipQuotedString,
|
|
10
|
+
startsUrlToken
|
|
11
|
+
} from '../parser/source-search.js';
|
|
6
12
|
import { resolveUnicodeEscape } from '../utilities.js';
|
|
7
13
|
|
|
8
14
|
import { evaluateColorMix } from './color-mix.js';
|
|
@@ -31,12 +37,13 @@ import {
|
|
|
31
37
|
import {
|
|
32
38
|
collapseShorthandParts,
|
|
33
39
|
convertAbsoluteLengthToPx,
|
|
40
|
+
formatResolvedNumber,
|
|
41
|
+
hasSubstitutedParts,
|
|
34
42
|
normalizeScaleComponent,
|
|
35
43
|
parseAlphaString,
|
|
36
44
|
parseAngleToDegrees,
|
|
37
45
|
roundCompactNumber
|
|
38
46
|
} from './shared.js';
|
|
39
|
-
import { findMatchingParenthesis } from './syntax.js';
|
|
40
47
|
import { elideRedundantSeparators } from './tokens.js';
|
|
41
48
|
import { minifyTransformValue } from './transforms.js';
|
|
42
49
|
import { optimizeUnicodeRange } from './unicode-range.js';
|
|
@@ -136,68 +143,24 @@ function splitValueSegments (value) {
|
|
|
136
143
|
const segments = [];
|
|
137
144
|
let index = 0;
|
|
138
145
|
|
|
139
|
-
const consumeQuoted = (start) => {
|
|
140
|
-
const quote = value[start];
|
|
141
|
-
let end = start + 1;
|
|
142
|
-
while (end < value.length) {
|
|
143
|
-
if (value[end] === '\\') {
|
|
144
|
-
end += 2;
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (value[end] === quote) {
|
|
148
|
-
end++;
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
|
-
end++;
|
|
152
|
-
}
|
|
153
|
-
return end;
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const startsUrl = (start) => {
|
|
157
|
-
return value.slice(start, start + 4).toLowerCase() === 'url(';
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const consumeUrl = (start) => {
|
|
161
|
-
let depth = 1;
|
|
162
|
-
let end = start + 4;
|
|
163
|
-
while (end < value.length && depth > 0) {
|
|
164
|
-
if (value[end] === '"' || value[end] === '\'') {
|
|
165
|
-
end = consumeQuoted(end);
|
|
166
|
-
continue;
|
|
167
|
-
}
|
|
168
|
-
// An escaped character is url data, never the parenthesis that ends it
|
|
169
|
-
if (value[end] === '\\') {
|
|
170
|
-
end += 2;
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (value[end] === '(') {
|
|
174
|
-
depth++;
|
|
175
|
-
}
|
|
176
|
-
if (value[end] === ')') {
|
|
177
|
-
depth--;
|
|
178
|
-
}
|
|
179
|
-
end++;
|
|
180
|
-
}
|
|
181
|
-
return end;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
146
|
while (index < value.length) {
|
|
185
147
|
if (value[index] === '"' || value[index] === '\'') {
|
|
186
|
-
const end =
|
|
148
|
+
const end = skipQuotedString(value, index);
|
|
187
149
|
segments.push({ text: value.slice(index, end), isLiteral: true });
|
|
188
150
|
index = end;
|
|
189
151
|
continue;
|
|
190
152
|
}
|
|
191
153
|
|
|
192
|
-
if (
|
|
193
|
-
|
|
154
|
+
if (startsUrlToken(value, index)) {
|
|
155
|
+
// The `url(` token opens its parenthesized group at its fourth character
|
|
156
|
+
const end = skipParenthesizedGroup(value, index + 3);
|
|
194
157
|
segments.push({ text: value.slice(index, end), isLiteral: true });
|
|
195
158
|
index = end;
|
|
196
159
|
continue;
|
|
197
160
|
}
|
|
198
161
|
|
|
199
162
|
const start = index;
|
|
200
|
-
while (index < value.length && value[index] !== '"' && value[index] !== '\'' && !
|
|
163
|
+
while (index < value.length && value[index] !== '"' && value[index] !== '\'' && !startsUrlToken(value, index)) {
|
|
201
164
|
index++;
|
|
202
165
|
}
|
|
203
166
|
segments.push({ text: value.slice(start, index), isLiteral: false });
|
|
@@ -836,7 +799,11 @@ const ZERO_INITIAL_PROPERTIES = new Set(['margin', 'padding']);
|
|
|
836
799
|
*
|
|
837
800
|
* @type {Set<string>}
|
|
838
801
|
*/
|
|
839
|
-
const AUTO_INITIAL_PROPERTIES = new Set([
|
|
802
|
+
const AUTO_INITIAL_PROPERTIES = new Set([
|
|
803
|
+
'min-width',
|
|
804
|
+
'min-height',
|
|
805
|
+
'hyphenate-limit-chars'
|
|
806
|
+
]);
|
|
840
807
|
|
|
841
808
|
/**
|
|
842
809
|
* Properties whose initial value is a single keyword longer than the global
|
|
@@ -1008,6 +975,69 @@ function reorderBorderWidthBeforeStyle (value) {
|
|
|
1008
975
|
return value.replace(BORDER_STYLE_BEFORE_WIDTH_PATTERN, '$2 $1');
|
|
1009
976
|
}
|
|
1010
977
|
|
|
978
|
+
/**
|
|
979
|
+
* Matches a trailing `!important` flag, so the components in front of it can be
|
|
980
|
+
* rewritten without the flag being mistaken for part of the last one.
|
|
981
|
+
*
|
|
982
|
+
* @type {RegExp}
|
|
983
|
+
*/
|
|
984
|
+
const IMPORTANT_SUFFIX_PATTERN = /!important$/i;
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* Separates a value from the `!important` flag it may end with.
|
|
988
|
+
*
|
|
989
|
+
* @param {string} value The declaration value, possibly flagged important.
|
|
990
|
+
* @return {object} The components in front of the flag, and the flag itself or an empty string.
|
|
991
|
+
*/
|
|
992
|
+
function splitImportantSuffix (value) {
|
|
993
|
+
const match = value.match(IMPORTANT_SUFFIX_PATTERN);
|
|
994
|
+
if (!match) {
|
|
995
|
+
return { components: value, importantSuffix: '' };
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
components: value.slice(0, match.index).trim(),
|
|
999
|
+
importantSuffix: match[0]
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Collapses a `hyphenate-limit-chars` value to its shortest equivalent form.
|
|
1005
|
+
* The property states up to three components: the shortest word that may be
|
|
1006
|
+
* hyphenated, then the fewest characters allowed before a hyphen, then the
|
|
1007
|
+
* fewest allowed after it. An unstated "before" component is read as `auto`,
|
|
1008
|
+
* and an unstated "after" component repeats whatever "before" holds, so a
|
|
1009
|
+
* trailing component that only restates the default it stands in for is
|
|
1010
|
+
* redundant.
|
|
1011
|
+
*
|
|
1012
|
+
* @param {string} value The `hyphenate-limit-chars` value.
|
|
1013
|
+
* @return {string} The value with its redundant trailing components dropped.
|
|
1014
|
+
*/
|
|
1015
|
+
function collapseHyphenateLimitChars (value) {
|
|
1016
|
+
const {
|
|
1017
|
+
components,
|
|
1018
|
+
importantSuffix
|
|
1019
|
+
} = splitImportantSuffix(value);
|
|
1020
|
+
const parts = components.split(' ').filter(Boolean);
|
|
1021
|
+
if (hasSubstitutedParts(parts)) {
|
|
1022
|
+
return value;
|
|
1023
|
+
}
|
|
1024
|
+
const isAfterMatchingBefore = (
|
|
1025
|
+
parts.length === 3 &&
|
|
1026
|
+
parts[2].toLowerCase() === parts[1].toLowerCase()
|
|
1027
|
+
);
|
|
1028
|
+
if (isAfterMatchingBefore) {
|
|
1029
|
+
parts.pop();
|
|
1030
|
+
}
|
|
1031
|
+
const isBeforeLeftAtDefault = (
|
|
1032
|
+
parts.length === 2 &&
|
|
1033
|
+
parts[1].toLowerCase() === 'auto'
|
|
1034
|
+
);
|
|
1035
|
+
if (isBeforeLeftAtDefault) {
|
|
1036
|
+
parts.pop();
|
|
1037
|
+
}
|
|
1038
|
+
return parts.join(' ') + importantSuffix;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1011
1041
|
/**
|
|
1012
1042
|
* Minifies a declaration's value for output, rewriting its initial keyword to
|
|
1013
1043
|
* the shorter global `initial` keyword after shorthand assembly. Both are
|
|
@@ -1301,6 +1331,10 @@ function applyPropertyOptimizations (val, property) {
|
|
|
1301
1331
|
val = collapseShorthandParts(val.split(' ')).join(' ');
|
|
1302
1332
|
}
|
|
1303
1333
|
|
|
1334
|
+
if (property === 'hyphenate-limit-chars') {
|
|
1335
|
+
val = collapseHyphenateLimitChars(val);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1304
1338
|
if (property === 'border-radius') {
|
|
1305
1339
|
const segments = val.split('/').map((segment) => {
|
|
1306
1340
|
return segment.trim();
|
|
@@ -1343,7 +1377,7 @@ function computeMinifiedValue (declaration) {
|
|
|
1343
1377
|
// Remove whitespace around * and / operators (safe outside calc context)
|
|
1344
1378
|
val = val.replace(/\s*([*/])\s*/g, '$1');
|
|
1345
1379
|
val = normalizeMathFunctions(val, declaration.property, declaration.value || '');
|
|
1346
|
-
val = simplifyStandaloneCalc(val);
|
|
1380
|
+
val = simplifyStandaloneCalc(val, declaration.property);
|
|
1347
1381
|
// Simplify calc() expressions containing zero-percent additive terms
|
|
1348
1382
|
val = val.replace(/calc\(([^()]+)\)/gi, (match, inner) => {
|
|
1349
1383
|
// Collapse whitespace inside calc expression
|
|
@@ -1367,10 +1401,11 @@ function computeMinifiedValue (declaration) {
|
|
|
1367
1401
|
}
|
|
1368
1402
|
val = val.replace(/(^|\s|,|\()(-?)0+(\.\d+)/g, '$1$2$3'); // e.g. 0.5 -> .5, -0.5 -> -.5
|
|
1369
1403
|
|
|
1370
|
-
// 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
|
|
1371
1406
|
if (/^[+-]?(?:\d+|\d*\.\d+)([a-z%]+)?$/i.test(val)) {
|
|
1372
1407
|
const [, rawNumber, rawUnit = ''] = val.match(/^([+-]?(?:\d+|\d*\.\d+))([a-z%]+)?$/i);
|
|
1373
|
-
val =
|
|
1408
|
+
val = formatResolvedNumber(rawNumber) + rawUnit;
|
|
1374
1409
|
}
|
|
1375
1410
|
|
|
1376
1411
|
val = lowercaseHexColors(val);
|