postcss-calc 11.0.0-rc.2 → 11.0.0-rc.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-calc",
3
- "version": "11.0.0-rc.2",
3
+ "version": "11.0.0-rc.3",
4
4
  "type": "module",
5
5
  "description": "PostCSS plugin to reduce calc()",
6
6
  "keywords": [
@@ -31,14 +31,20 @@
31
31
  "engines": {
32
32
  "node": "^22.18 || ^24.11 || >=26.0"
33
33
  },
34
+ "devEngines": {
35
+ "packageManager": {
36
+ "name": "pnpm",
37
+ "version": "11.21.0"
38
+ }
39
+ },
34
40
  "devDependencies": {
35
41
  "@csstools/css-calc": "^3.3.0",
36
42
  "@rmenke/css-tokenizer-tests": "^1.2.0",
37
- "@types/node": "^26.1.2",
43
+ "@types/node": "^26.2.0",
38
44
  "fast-check": "^4.9.0",
39
- "oxfmt": "^0.61.0",
40
- "oxlint": "^1.76.0",
41
- "postcss": "^8.5.25",
45
+ "oxfmt": "^0.63.0",
46
+ "oxlint": "^1.78.0",
47
+ "postcss": "^8.5.26",
42
48
  "typescript": "~7.0.2"
43
49
  },
44
50
  "dependencies": {
package/src/lib/node.js CHANGED
@@ -5,7 +5,9 @@
5
5
  // is `Num(-5)`, never `Sum([{sign:-1, Num(5)}])`. One form per value.
6
6
  // - In a SumTerm with Num/Dim node, sign is always +1; the sign slot is
7
7
  // reserved for opaque nodes (Ident, Call, Product, multi-term Sum).
8
- // - No Sum directly contains another Sum (flattened on construction).
8
+ // - No ungrouped Sum directly contains another Sum (flattened on
9
+ // construction). A grouped Sum is retained so a later negative sign
10
+ // cannot be distributed across opaque terms.
9
11
  // - No Product directly contains another Product (flattened).
10
12
  // - A Sum/Product with one positive element collapses to that element.
11
13
  // - A Sum/Product with no elements collapses to Num(0) / Num(1).
@@ -18,7 +20,7 @@
18
20
  * @typedef {{type: 'Ident', name: string}} Ident
19
21
  * @typedef {{type: 'Call', name: string, args: Node[]}} Call
20
22
  * @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
21
- * @typedef {{type: 'Sum', terms: SumTerm[]}} Sum
23
+ * @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
22
24
  * @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
23
25
  * @typedef {{type: 'Product', factors: ProductFactor[]}} Product
24
26
  * @typedef {Num | Dim | Ident | Call | Sum | Product} Node
@@ -85,7 +87,7 @@ function mkSum(rawTerms) {
85
87
  function pushSumTerm(out, term) {
86
88
  let { sign, node } = term;
87
89
 
88
- if (node.type === 'Sum') {
90
+ if (node.type === 'Sum' && !node.grouped) {
89
91
  for (const inner of node.terms) {
90
92
  pushSumTerm(out, {
91
93
  sign: /** @type {1 | -1} */ (sign * inner.sign),
@@ -170,12 +172,15 @@ function negate(node) {
170
172
  return dim(-node.value, node.unit);
171
173
  }
172
174
  if (node.type === 'Sum') {
173
- return mkSum(
175
+ const result = mkSum(
174
176
  node.terms.map((t) => ({
175
177
  sign: /** @type {1 | -1} */ (-t.sign),
176
178
  node: t.node,
177
179
  }))
178
180
  );
181
+ return node.grouped && result.type === 'Sum'
182
+ ? { ...result, grouped: true }
183
+ : result;
179
184
  }
180
185
  // Opaque (Ident, Call, Product): wrap as a single negative-sign term —
181
186
  // the only case where sign=-1 remains on a SumTerm.
package/src/lib/parser.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Pratt parser. +/- emit Sum nodes; */÷ emit Product nodes. node.js
2
- // constructors flatten and normalize on construction, so the parser
3
- // never produces a Binary node.
2
+ // constructors flatten and normalize on construction, while parenthesized
3
+ // sums retain a grouping marker for the opaque-subtraction invariant.
4
4
  import { mkSum, mkProduct, negate } from './node.js';
5
5
 
6
6
  /**
@@ -266,7 +266,7 @@ const PREFIX = {
266
266
  '(': (p) => {
267
267
  const e = p.parseExpr(0);
268
268
  p.expect('punct', ')');
269
- return e;
269
+ return e.type === 'Sum' ? { ...e, grouped: true } : e;
270
270
  },
271
271
 
272
272
  '-': (p) => negate(p.parseExpr(UNARY_BP)),
@@ -77,6 +77,25 @@ function serialize(node, opts = {}) {
77
77
  return `${calcName}(${degenerateKeyword(node.value)} * 1${node.unit})`;
78
78
  }
79
79
 
80
+ // A grouped sum with a leading negative term is the canonical result of
81
+ // negating a parenthesized expression. Re-invert its terms for the body so
82
+ // the grouping survives as `-(...)` instead of becoming `-a - b`.
83
+ if (
84
+ node.type === 'Sum' &&
85
+ node.grouped &&
86
+ node.terms.length > 1 &&
87
+ displaySign(node.terms[0]).sign === -1
88
+ ) {
89
+ const body = /** @type {Sum} */ ({
90
+ type: 'Sum',
91
+ terms: node.terms.map((t) => ({
92
+ sign: /** @type {1 | -1} */ (-t.sign),
93
+ node: t.node,
94
+ })),
95
+ });
96
+ return `${calcName}(-(${serializeExpr(body, prec)}))`;
97
+ }
98
+
80
99
  if (
81
100
  node.type === 'Num' ||
82
101
  node.type === 'Dim' ||
@@ -166,13 +185,21 @@ function serializeSum(sum, prec) {
166
185
  for (const [i, t] of sum.terms.entries()) {
167
186
  const { sign, magnitude } = displaySign(t);
168
187
  if (i === 0) {
188
+ if (magnitude.type === 'Sum' && magnitude.grouped) {
189
+ const body = `(${serializeExpr(magnitude, prec)})`;
190
+ out = sign === 1 ? body : `-${body}`;
191
+ continue;
192
+ }
169
193
  out =
170
194
  sign === 1
171
195
  ? serializeExpr(magnitude, prec)
172
196
  : serializeLeadingNeg(magnitude, prec);
173
197
  } else {
174
198
  // `-` binds looser than `*`/`/` so the right side never needs parens.
175
- const body = serializeExpr(magnitude, prec);
199
+ let body = serializeExpr(magnitude, prec);
200
+ if (magnitude.type === 'Sum' && magnitude.grouped) {
201
+ body = `(${body})`;
202
+ }
176
203
  out += sign === 1 ? ` + ${body}` : ` - ${body}`;
177
204
  }
178
205
  }
@@ -48,6 +48,17 @@ function simplifySum(sum, simplify) {
48
48
  */
49
49
  function processTerm(sign, n) {
50
50
  if (n.type === 'Sum') {
51
+ // Parentheses around a sum are normally algebraically disposable. Keep
52
+ // them only for an opaque sum used negatively: distributing that sign
53
+ // changes the CSS value when a var() contains a sum of its own.
54
+ if (
55
+ n.grouped &&
56
+ sign === -1 &&
57
+ n.terms.some((t) => t.node.type !== 'Num' && t.node.type !== 'Dim')
58
+ ) {
59
+ opaque.push({ sign, node: n });
60
+ return;
61
+ }
51
62
  for (const inner of n.terms) {
52
63
  processTerm(/** @type {1 | -1} */ (sign * inner.sign), inner.node);
53
64
  }
@@ -95,7 +106,13 @@ function simplifySum(sum, simplify) {
95
106
  }
96
107
  terms.push(...opaque);
97
108
 
98
- return mkSum(terms);
109
+ const result = mkSum(terms);
110
+ // Keep the source grouping available to an enclosing subtraction. Numeric
111
+ // groups have already folded and therefore do not need the marker.
112
+ if (sum.grouped && result.type === 'Sum' && opaque.length > 0) {
113
+ return { ...result, grouped: true };
114
+ }
115
+ return result;
99
116
  }
100
117
 
101
118
  export { simplifySum };
@@ -23,6 +23,7 @@ export type SumTerm = {
23
23
  export type Sum = {
24
24
  type: 'Sum';
25
25
  terms: SumTerm[];
26
+ grouped?: boolean;
26
27
  };
27
28
  export type ProductFactor = {
28
29
  exponent: 1 | -1;
@@ -39,7 +40,7 @@ export type Node = Num | Dim | Ident | Call | Sum | Product;
39
40
  * @typedef {{type: 'Ident', name: string}} Ident
40
41
  * @typedef {{type: 'Call', name: string, args: Node[]}} Call
41
42
  * @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim.
42
- * @typedef {{type: 'Sum', terms: SumTerm[]}} Sum
43
+ * @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum
43
44
  * @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator.
44
45
  * @typedef {{type: 'Product', factors: ProductFactor[]}} Product
45
46
  * @typedef {Num | Dim | Ident | Call | Sum | Product} Node
@@ -1,4 +1,11 @@
1
- export type BaseType = "length" | "angle" | "time" | "frequency" | "resolution" | "flex" | "percentage";
1
+ export type BaseType =
2
+ | 'length'
3
+ | 'angle'
4
+ | 'time'
5
+ | 'frequency'
6
+ | 'resolution'
7
+ | 'flex'
8
+ | 'percentage';
2
9
  /**
3
10
  * @param {string} unit
4
11
  * @return {BaseType | null}