@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/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
- const orderedUnits = [...PREFERRED_UNIT_ORDER, ...[...totals.keys()].filter((unit) => {
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
- // Remove whitespace around multiplication/division operators
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
- try {
596
- const simplified = calc(match);
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
- * 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.
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
- * @param {string} value The CSS value string containing math functions to simplify.
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
- function normalizeMathFunctions (value, property, originalValue = '') {
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
- // Simplify min()/max() expressions using @csstools/css-calc
631
- result = result.replace(/\b(min|max)\(([^()]+)\)/gi, keepSeparatorAfterDissolvedFunction((match) => {
632
- try {
633
- const simplified = calc(match);
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 @csstools/css-calc
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 'calc(' + inner.replace(/\s*([*/])\s*/g, '$1') + ')';
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 compactInner = inner.replace(/\s+/g, ' ').trim();
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
- // Default: strip whitespace around multiplication/division operators
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'