postcss-calc 11.0.1 → 11.0.2

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 CHANGED
@@ -206,6 +206,8 @@ when changing parsing/simplification behavior:
206
206
  pnpm test:corpus:full
207
207
  ```
208
208
 
209
+ Profile long arithmetic parser chains with `pnpm benchmark:arithmetic-chains`.
210
+
209
211
  ## [Changelog](CHANGELOG.md)
210
212
 
211
213
  ## [License](LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-calc",
3
- "version": "11.0.1",
3
+ "version": "11.0.2",
4
4
  "type": "module",
5
5
  "description": "PostCSS plugin to reduce calc()",
6
6
  "keywords": [
@@ -13,7 +13,7 @@
13
13
  "homepage": "https://github.com/postcss/postcss-calc",
14
14
  "repository": {
15
15
  "type": "git",
16
- "url": "https://github.com/postcss/postcss-calc.git"
16
+ "url": "git+https://github.com/postcss/postcss-calc.git"
17
17
  },
18
18
  "exports": {
19
19
  ".": {
@@ -34,13 +34,13 @@
34
34
  "devEngines": {
35
35
  "packageManager": {
36
36
  "name": "pnpm",
37
- "version": "11.24.0"
37
+ "version": "11.25.0"
38
38
  }
39
39
  },
40
40
  "devDependencies": {
41
41
  "@csstools/css-calc": "^3.3.0",
42
42
  "@rmenke/css-tokenizer-tests": "^1.2.0",
43
- "@types/node": "^26.3.0",
43
+ "@types/node": "^26.4.0",
44
44
  "fast-check": "^4.9.0",
45
45
  "oxfmt": "^0.65.0",
46
46
  "oxlint": "^1.80.0",
@@ -57,6 +57,7 @@
57
57
  "scripts": {
58
58
  "lint": "oxlint . && tsc && oxfmt --check",
59
59
  "fmt": "oxfmt",
60
+ "benchmark:arithmetic-chains": "node scripts/benchmark-arithmetic-chains.mjs",
60
61
  "test": "node --test --test-reporter=dot 'test/**/*.test.mjs' test/index.cjs test/convertUnit.cjs",
61
62
  "test:mutation:corpus": "node test/mutation/corpus-selection.mjs",
62
63
  "test:corpus:full": "POSTCSS_CALC_FULL_CORPUS=1 node --test test/conformance/corpus.test.mjs"
package/src/lib/parser.js CHANGED
@@ -1,14 +1,13 @@
1
1
  // Pratt parser. +/- emit Sum nodes; */÷ emit Product nodes. node.js
2
2
  // constructors flatten and normalize on construction, while parenthesized
3
3
  // sums retain a grouping marker for the opaque-subtraction invariant.
4
- import { mkSum, mkProduct, negate, ident, call } from './node.js';
4
+ import { mkSum, mkProduct, negate, num, dim, ident, call } from './node.js';
5
5
 
6
6
  /**
7
7
  * @typedef {import('./tokenizer.js').Token} Token
8
8
  * @typedef {import('./tokenizer.js').TokenType} TokenType
9
9
  * @typedef {import('./node.js').Node} Node
10
10
  * @typedef {(p: Parser, token: Token) => Node} PrefixParselet
11
- * @typedef {{lbp: number, parse: (p: Parser, left: Node, token: Token) => Node}} InfixParselet
12
11
  */
13
12
 
14
13
  /**
@@ -30,17 +29,17 @@ function foldCalcKeyword(name) {
30
29
  // form arrives as a single ident because CSS Syntax tokenizes leading
31
30
  // `-` + ident-start as one ident-token.
32
31
  if (name === 'NaN' || name === '-NaN') {
33
- return { type: 'Num', value: Number.NaN };
32
+ return num(Number.NaN);
34
33
  }
35
34
  switch (name.toLowerCase()) {
36
35
  case 'pi':
37
- return { type: 'Num', value: Math.PI };
36
+ return num(Math.PI);
38
37
  case 'e':
39
- return { type: 'Num', value: Math.E };
38
+ return num(Math.E);
40
39
  case 'infinity':
41
- return { type: 'Num', value: Infinity };
40
+ return num(Infinity);
42
41
  case '-infinity':
43
- return { type: 'Num', value: -Infinity };
42
+ return num(-Infinity);
44
43
  }
45
44
  return null;
46
45
  }
@@ -102,40 +101,58 @@ class Parser {
102
101
  if (!rule || rule.lbp < minBp) {
103
102
  break;
104
103
  }
105
- this.next();
106
- left = rule.parse(this, left, nxt);
104
+ if (infixKey === '+' || infixKey === '-') {
105
+ /** @type {import('./node.js').SumTerm[]} */
106
+ const terms = [{ sign: /** @type {1} */ (1), node: left }];
107
+ do {
108
+ const token = this.next();
109
+ requireSurroundingWs(this, token);
110
+ terms.push({
111
+ sign: /** @type {1 | -1} */ (token.value === '+' ? 1 : -1),
112
+ node: this.parseExpr(ADD_BP + 1),
113
+ });
114
+ const next = this.peek();
115
+ if (
116
+ next.type !== 'punct' ||
117
+ (next.value !== '+' && next.value !== '-')
118
+ ) {
119
+ break;
120
+ }
121
+ } while (ADD_BP >= minBp);
122
+ left = mkSum(terms);
123
+ continue;
124
+ }
125
+
126
+ if (infixKey === '*' || infixKey === '/') {
127
+ /** @type {import('./node.js').ProductFactor[]} */
128
+ const factors = [{ exponent: /** @type {1} */ (1), node: left }];
129
+ do {
130
+ const token = this.next();
131
+ factors.push({
132
+ exponent: /** @type {1 | -1} */ (token.value === '*' ? 1 : -1),
133
+ node: this.parseExpr(MUL_BP + 1),
134
+ });
135
+ const next = this.peek();
136
+ if (
137
+ next.type !== 'punct' ||
138
+ (next.value !== '*' && next.value !== '/')
139
+ ) {
140
+ break;
141
+ }
142
+ } while (MUL_BP >= minBp);
143
+ left = mkProduct(factors);
144
+ continue;
145
+ }
146
+
147
+ // Every infix operator is handled above. Keep this defensive exit in
148
+ // case a future parselet is added without a chain implementation.
149
+ break;
107
150
  }
108
151
 
109
152
  return left;
110
153
  }
111
154
  }
112
155
 
113
- /**
114
- * @param {Node} left
115
- * @param {Node} right
116
- * @param {1 | -1} rightSign
117
- * @return {Node}
118
- */
119
- function addTerm(left, right, rightSign) {
120
- return mkSum([
121
- { sign: 1, node: left },
122
- { sign: rightSign, node: right },
123
- ]);
124
- }
125
-
126
- /**
127
- * @param {Node} left
128
- * @param {Node} right
129
- * @param {1 | -1} rightExp
130
- * @return {Node}
131
- */
132
- function mulFactor(left, right, rightExp) {
133
- return mkProduct([
134
- { exponent: 1, node: left },
135
- { exponent: rightExp, node: right },
136
- ]);
137
- }
138
-
139
156
  const ADD_BP = 1;
140
157
  const MUL_BP = 3;
141
158
  const UNARY_BP = 7;
@@ -228,14 +245,14 @@ function requireSurroundingWs(p, token) {
228
245
 
229
246
  /** @type {Record<string, PrefixParselet>} */
230
247
  const PREFIX = {
231
- number: (_p, t) => ({ type: 'Num', value: Number.parseFloat(t.value) }),
248
+ number: (_p, t) => num(Number.parseFloat(t.value)),
232
249
 
233
250
  // Unit case normalization per §10.12: `1PX` serializes as `1px`.
234
- dimension: (_p, t) => ({
235
- type: 'Dim',
236
- value: Number.parseFloat(t.value),
237
- unit: t.unit === '%' ? '%' : /** @type {string} */ (t.unit).toLowerCase(),
238
- }),
251
+ dimension: (_p, t) =>
252
+ dim(
253
+ Number.parseFloat(t.value),
254
+ t.unit === '%' ? '%' : /** @type {string} */ (t.unit).toLowerCase()
255
+ ),
239
256
 
240
257
  ident: (p, t) => {
241
258
  const nxt = p.peek();
@@ -273,30 +290,12 @@ const PREFIX = {
273
290
  '+': (p) => p.parseExpr(UNARY_BP),
274
291
  };
275
292
 
276
- /** @type {Record<string, InfixParselet>} */
293
+ /** @type {Record<string, {lbp: number}>} */
277
294
  const INFIX = {
278
- '+': {
279
- lbp: ADD_BP,
280
- parse: (p, left, token) => {
281
- requireSurroundingWs(p, token);
282
- return addTerm(left, p.parseExpr(ADD_BP + 1), 1);
283
- },
284
- },
285
- '-': {
286
- lbp: ADD_BP,
287
- parse: (p, left, token) => {
288
- requireSurroundingWs(p, token);
289
- return addTerm(left, p.parseExpr(ADD_BP + 1), -1);
290
- },
291
- },
292
- '*': {
293
- lbp: MUL_BP,
294
- parse: (p, left) => mulFactor(left, p.parseExpr(MUL_BP + 1), 1),
295
- },
296
- '/': {
297
- lbp: MUL_BP,
298
- parse: (p, left) => mulFactor(left, p.parseExpr(MUL_BP + 1), -1),
299
- },
295
+ '+': { lbp: ADD_BP },
296
+ '-': { lbp: ADD_BP },
297
+ '*': { lbp: MUL_BP },
298
+ '/': { lbp: MUL_BP },
300
299
  };
301
300
 
302
301
  /**
@@ -2,10 +2,6 @@ export type Token = import('./tokenizer.js').Token;
2
2
  export type TokenType = import('./tokenizer.js').TokenType;
3
3
  export type Node = import('./node.js').Node;
4
4
  export type PrefixParselet = (p: Parser, token: Token) => Node;
5
- export type InfixParselet = {
6
- lbp: number;
7
- parse: (p: Parser, left: Node, token: Token) => Node;
8
- };
9
5
  declare class Parser {
10
6
  /** @private */
11
7
  i;