@thejaredwilcurt/csslop 0.0.40 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +2 -5
- package/src/value/math-expression.js +1103 -0
- package/src/value/math.js +110 -99
- package/vite.config.js +0 -4
package/src/value/math.js
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
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,
|
|
@@ -86,25 +89,6 @@ function keepSeparatorAfterDissolvedFunction (replacer) {
|
|
|
86
89
|
};
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
/**
|
|
90
|
-
* The functions whose arguments are a math expression, where `*` and `/` are
|
|
91
|
-
* arithmetic operators rather than characters of some other syntax.
|
|
92
|
-
*
|
|
93
|
-
* @type {Set<string>}
|
|
94
|
-
*/
|
|
95
|
-
const MATH_FUNCTION_NAMES = new Set([
|
|
96
|
-
'abs',
|
|
97
|
-
'calc',
|
|
98
|
-
'clamp',
|
|
99
|
-
'hypot',
|
|
100
|
-
'max',
|
|
101
|
-
'min',
|
|
102
|
-
'mod',
|
|
103
|
-
'rem',
|
|
104
|
-
'round',
|
|
105
|
-
'sign'
|
|
106
|
-
]);
|
|
107
|
-
|
|
108
92
|
/**
|
|
109
93
|
* Matches a single character that may appear in a CSS function name.
|
|
110
94
|
*
|
|
@@ -270,6 +254,38 @@ function shorterOfResolutionAndCalculation (resolved, calculation) {
|
|
|
270
254
|
return calculation;
|
|
271
255
|
}
|
|
272
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
|
+
|
|
273
289
|
/**
|
|
274
290
|
* Resolves a percentage divided by a number into the single percentage it
|
|
275
291
|
* equals, which is how `calc(100%/16)` comes to be written as `6.25%`. The
|
|
@@ -398,21 +414,6 @@ function clampResolvedValueToPropertyRange (resolvedValue, property) {
|
|
|
398
414
|
return '0';
|
|
399
415
|
}
|
|
400
416
|
|
|
401
|
-
/**
|
|
402
|
-
* The units a folded calc() expression leads with, in the order they are
|
|
403
|
-
* written. Every other unit follows them alphabetically.
|
|
404
|
-
*
|
|
405
|
-
* @type {Array}
|
|
406
|
-
*/
|
|
407
|
-
const PREFERRED_UNIT_ORDER = ['%', '', 'px'];
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* The same leading units, for excluding them from the alphabetical remainder.
|
|
411
|
-
*
|
|
412
|
-
* @type {Set<string>}
|
|
413
|
-
*/
|
|
414
|
-
const PREFERRED_UNITS = new Set(PREFERRED_UNIT_ORDER);
|
|
415
|
-
|
|
416
417
|
/**
|
|
417
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.
|
|
418
419
|
*
|
|
@@ -492,38 +493,7 @@ function tryFoldCalcExpression (expression) {
|
|
|
492
493
|
totals.set(unit, (totals.get(unit) || 0) + number);
|
|
493
494
|
}
|
|
494
495
|
|
|
495
|
-
|
|
496
|
-
return !PREFERRED_UNITS.has(unit);
|
|
497
|
-
}).sort()];
|
|
498
|
-
const outputTerms = [];
|
|
499
|
-
|
|
500
|
-
for (const unit of orderedUnits) {
|
|
501
|
-
if (!totals.has(unit)) {
|
|
502
|
-
continue;
|
|
503
|
-
}
|
|
504
|
-
const value = totals.get(unit);
|
|
505
|
-
if (Math.abs(value) < 1e-12) {
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
outputTerms.push({ unit, value });
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
if (!outputTerms.length) {
|
|
512
|
-
return '0';
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (outputTerms.length === 1) {
|
|
516
|
-
const { unit, value } = outputTerms[0];
|
|
517
|
-
return formatResolvedDimension(value, unit);
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const [first, ...rest] = outputTerms;
|
|
521
|
-
let result = formatResolvedDimension(first.value, first.unit);
|
|
522
|
-
for (const term of rest) {
|
|
523
|
-
const sign = term.value < 0 ? '-' : '+';
|
|
524
|
-
result += ' ' + sign + ' ' + formatResolvedDimension(Math.abs(term.value), term.unit);
|
|
525
|
-
}
|
|
526
|
-
return 'calc(' + result + ')';
|
|
496
|
+
return formatUnitTotals(totals);
|
|
527
497
|
}
|
|
528
498
|
|
|
529
499
|
/**
|
|
@@ -578,8 +548,7 @@ function chooseResolvedPercentage (simplified, compactInner, match) {
|
|
|
578
548
|
*/
|
|
579
549
|
function resolveCalcExpression (match, compactInner) {
|
|
580
550
|
if (PERCENTAGE_MULTIPLICATION.test(compactInner)) {
|
|
581
|
-
|
|
582
|
-
return 'calc(' + compactInner.replace(/\s*([*/])\s*/g, '$1') + ')';
|
|
551
|
+
return compactCalculation(compactInner);
|
|
583
552
|
}
|
|
584
553
|
|
|
585
554
|
const dividedPercentage = resolvePercentageDivision(compactInner);
|
|
@@ -592,26 +561,41 @@ function resolveCalcExpression (match, compactInner) {
|
|
|
592
561
|
return folded;
|
|
593
562
|
}
|
|
594
563
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
if (typeof simplified !== 'string') {
|
|
598
|
-
return match;
|
|
599
|
-
}
|
|
600
|
-
return chooseResolvedPercentage(simplified, compactInner, match);
|
|
601
|
-
} catch {
|
|
564
|
+
const simplified = simplifyCalc(match);
|
|
565
|
+
if (simplified === null) {
|
|
602
566
|
return match;
|
|
603
567
|
}
|
|
568
|
+
const resolved = chooseResolvedPercentage(simplified, compactInner, match);
|
|
569
|
+
return chooseResolvedCalculation(resolved, compactCalculation(compactInner));
|
|
604
570
|
}
|
|
605
571
|
|
|
606
572
|
/**
|
|
607
|
-
*
|
|
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.
|
|
608
577
|
*
|
|
609
|
-
* @
|
|
610
|
-
* @param {string} property The CSS property name, whose range every resolved result is clamped into.
|
|
611
|
-
* @param {string} originalValue The original unmodified value to fall back to if simplification produces an invalid result.
|
|
612
|
-
* @return {string} The value with math functions simplified where possible.
|
|
578
|
+
* @type {RegExp}
|
|
613
579
|
*/
|
|
614
|
-
|
|
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) {
|
|
615
599
|
let result = rewriteMathFunctionContents(convertReciprocalMultiplicationToDivision, value);
|
|
616
600
|
|
|
617
601
|
// Unwrap calc(1 / (1 / x)) → x (double-reciprocal identity)
|
|
@@ -627,20 +611,16 @@ function normalizeMathFunctions (value, property, originalValue = '') {
|
|
|
627
611
|
return 'calc(' + inner + ')';
|
|
628
612
|
}));
|
|
629
613
|
|
|
630
|
-
//
|
|
631
|
-
result = result.replace(
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (typeof simplified !== 'string') {
|
|
635
|
-
return match;
|
|
636
|
-
}
|
|
637
|
-
return clampResolvedValueToPropertyRange(simplified, property);
|
|
638
|
-
} 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) {
|
|
639
618
|
return match;
|
|
640
619
|
}
|
|
620
|
+
return clampResolvedValueToPropertyRange(simplified, property);
|
|
641
621
|
}));
|
|
642
622
|
|
|
643
|
-
// Simplify calc() expressions using constant folding and
|
|
623
|
+
// Simplify calc() expressions using constant folding and expression resolution
|
|
644
624
|
result = result.replace(/calc\(([^()]+)\)/gi, keepSeparatorAfterDissolvedFunction((match, inner) => {
|
|
645
625
|
// Collapse whitespace inside calc expression
|
|
646
626
|
const compactInner = inner.replace(/\s+/g, ' ').trim();
|
|
@@ -648,6 +628,31 @@ function normalizeMathFunctions (value, property, originalValue = '') {
|
|
|
648
628
|
return clampResolvedValueToPropertyRange(resolved, property);
|
|
649
629
|
}));
|
|
650
630
|
|
|
631
|
+
return result;
|
|
632
|
+
}
|
|
633
|
+
|
|
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);
|
|
655
|
+
|
|
651
656
|
// When calc() folded to an absolute-length unit (pt, pc, in, cm, mm, q), convert to pixels
|
|
652
657
|
if (originalValue.includes('calc(') && /^-?(?:\d+|\d*\.\d+)(pt|pc|in|cm|mm|q)$/i.test(result)) {
|
|
653
658
|
// Extract the absolute-length unit from the folded result
|
|
@@ -667,7 +672,7 @@ function normalizeMathFunctions (value, property, originalValue = '') {
|
|
|
667
672
|
}
|
|
668
673
|
|
|
669
674
|
/**
|
|
670
|
-
* 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.
|
|
671
676
|
*
|
|
672
677
|
* @param {string} value The CSS value string, already known to be a single `calc()`.
|
|
673
678
|
* @return {string} The simplified value, or the original value if simplification is not applicable.
|
|
@@ -677,7 +682,15 @@ function foldStandaloneCalc (value) {
|
|
|
677
682
|
|
|
678
683
|
// Preserve percent-times-number expressions, only stripping whitespace around operators
|
|
679
684
|
if (PERCENTAGE_MULTIPLICATION.test(inner.replace(/\s+/g, ' ').trim())) {
|
|
680
|
-
return
|
|
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));
|
|
681
694
|
}
|
|
682
695
|
|
|
683
696
|
// If no nested function calls and no multiplication/division involving parens, try flattening
|
|
@@ -714,14 +727,12 @@ function foldStandaloneCalc (value) {
|
|
|
714
727
|
}
|
|
715
728
|
|
|
716
729
|
// Collapse whitespace and check for percent-division expressions
|
|
717
|
-
const
|
|
718
|
-
const dividedPercentage = resolvePercentageDivision(compactInner);
|
|
730
|
+
const dividedPercentage = resolvePercentageDivision(inner.replace(/\s+/g, ' ').trim());
|
|
719
731
|
if (dividedPercentage) {
|
|
720
732
|
return dividedPercentage;
|
|
721
733
|
}
|
|
722
734
|
|
|
723
|
-
|
|
724
|
-
return 'calc(' + compactInner.replace(/\s*([*/])\s*/g, '$1') + ')';
|
|
735
|
+
return compactCalculation(inner);
|
|
725
736
|
}
|
|
726
737
|
|
|
727
738
|
/**
|
package/vite.config.js
CHANGED
|
@@ -27,10 +27,6 @@ const config = defineConfig({
|
|
|
27
27
|
test: /node_modules\/@codemirror\/lang-css/,
|
|
28
28
|
name: 'codemirror-lang-css'
|
|
29
29
|
},
|
|
30
|
-
{
|
|
31
|
-
test: /node_modules\/@csstools\/css-calc/,
|
|
32
|
-
name: 'css-calc'
|
|
33
|
-
},
|
|
34
30
|
{
|
|
35
31
|
test: /node_modules\/@node-projects\/css-parser/,
|
|
36
32
|
name: 'css-parser'
|