postcss-calc 11.1.2 → 11.2.1

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
@@ -43,10 +43,10 @@ you will get:
43
43
 
44
44
  ```css
45
45
  h1 {
46
- font-size: 32px;
46
+ font-size: calc(32px);
47
47
  height: calc(100px - 2em);
48
48
  width: calc(2 * var(--base-width));
49
- margin-bottom: 24px;
49
+ margin-bottom: calc(24px);
50
50
  }
51
51
  ```
52
52
 
@@ -62,14 +62,15 @@ leaving all other text untouched.
62
62
  import reduceCalc from 'postcss-calc/reduce';
63
63
 
64
64
  reduceCalc('calc(1in + 10px)');
65
- // => '1.10417in'
65
+ // => 'calc(1.10417in)'
66
66
 
67
67
  reduceCalc('min(50px, calc(2 * 40px))');
68
- // => '50px'
68
+ // => 'calc(50px)'
69
69
  ```
70
70
 
71
- It accepts `precision`, `unwrapSingleNegativeNumber`, `warnWhenCannotResolve`, `onParseError`,
72
- and `onWarn`:
71
+ It accepts `precision`, `unwrapSingleValue`, the deprecated
72
+ `unwrapSingleNegativeNumber` alias,
73
+ `warnWhenCannotResolve`, `onParseError`, and `onWarn`:
73
74
 
74
75
  ```js
75
76
  const result = reduceCalc('calc(100% + var(--gap))', {
@@ -87,21 +88,27 @@ by default; provide `onParseError` and/or `onWarn` if you want diagnostics.
87
88
 
88
89
  ### Standalone reducer options
89
90
 
90
- #### `unwrapSingleNegativeNumber` (default: `false`)
91
+ #### `unwrapSingleValue` (default: `false`)
91
92
 
92
- Controls whether a finite negative result is serialized as a bare value or
93
- wrapped in `calc()`. Keep the default when reducing declaration values; set it
94
- to `true` when the surrounding CSS context requires a bare negative value, such
95
- as a selector:
93
+ Serializes a fully resolved finite scalar result without calculation syntax.
94
+ Keep the default for standard CSS so the browser can perform range clamping
95
+ and integer rounding. Set it to `true` for a non-standard context that requires
96
+ a bare value, such as a selector:
96
97
 
97
98
  ```js
98
99
  reduceCalc('calc(5px - 10px)');
99
100
  // => 'calc(-5px)'
100
101
 
101
- reduceCalc('calc(5px - 10px)', { unwrapNegativeNumbers: true });
102
+ reduceCalc('calc(5px - 10px)', { unwrapSingleValue: true });
102
103
  // => '-5px'
104
+
105
+ reduceCalc('calc(1 / 2)', { unwrapSingleValue: true });
106
+ // => '.5'
103
107
  ```
104
108
 
109
+ The published `unwrapSingleNegativeNumber` option is retained as a deprecated
110
+ alias for `unwrapSingleValue`.
111
+
105
112
  ### PostCSS plugin options
106
113
 
107
114
  These options apply when using the PostCSS plugin:
@@ -113,7 +120,8 @@ postcss().use(calc({ precision: 10 }));
113
120
  #### `precision` (default: `5`)
114
121
 
115
122
  Allows you to define the precision for decimal numbers. Set it to `false` to
116
- disable rounding.
123
+ disable rounding and preserve full IEEE-754 floating-point precision (emitting
124
+ the shortest round-tripping decimal representation).
117
125
 
118
126
  ```js
119
127
  var out = postcss()
@@ -121,6 +129,12 @@ var out = postcss()
121
129
  .process(css).css;
122
130
  ```
123
131
 
132
+ #### `unwrapSingleValue` (default: `false`)
133
+
134
+ Serializes fully resolved finite scalar results without calculation syntax.
135
+ This can discard browser-applied range clamping or integer rounding. Selectors
136
+ enable it automatically because selectors cannot contain `calc()`.
137
+
124
138
  #### `warnWhenCannotResolve` (default: `false`)
125
139
 
126
140
  Adds warnings when calc() are not reduced to a single value.
@@ -165,9 +179,9 @@ With `mediaQueries: true`, this becomes:
165
179
 
166
180
  Reduces `calc()` functions found in selectors. Selectors do not accept
167
181
  `calc()` functions, so the plugin replaces them with their reduced values.
168
- Finite negative results are serialized as bare values because a selector cannot
169
- contain a `calc()` function; the plugin enables `unwrapSingleNegativeNumber` automatically
170
- for selectors.
182
+ Finite negative and fractional unitless results are serialized as bare values
183
+ because a selector cannot contain a `calc()` function; the plugin enables the
184
+ `unwrapSingleValue` automatically for selectors.
171
185
 
172
186
  ```js
173
187
  var out = postcss()
@@ -268,7 +282,15 @@ when changing parsing/simplification behavior:
268
282
  pnpm test:corpus:full
269
283
  ```
270
284
 
271
- Profile long arithmetic parser chains with `pnpm benchmark:arithmetic-chains`.
285
+ Profile parser chains with `pnpm benchmark:arithmetic-chains` or
286
+ `pnpm benchmark:nested-fallbacks`; both use 20 fresh paired blocks by default
287
+ and write ignored schema-v2 reports. Compare a saved report with
288
+ `node scripts/compare-parser-benchmarks.js <report>`. Run the correctness-aware
289
+ corpus benchmark with `pnpm benchmark:corpus`.
290
+
291
+ The PostCSS benchmark awaits `postcss().process(...)`, and that await already
292
+ triggers result stringification. It therefore does not add a redundant
293
+ `result.css` access.
272
294
 
273
295
  ## [Changelog](CHANGELOG.md)
274
296
 
@@ -281,5 +303,5 @@ Profile long arithmetic parser chains with `pnpm benchmark:arithmetic-chains`.
281
303
  [PostCSS]: https://github.com/postcss
282
304
  [PostCSS Calc]: https://github.com/postcss/postcss-calc
283
305
  [PostCSS Custom Properties]: https://github.com/postcss/postcss-custom-properties
284
- [tests]: test/index.js
306
+ [tests]: test/
285
307
  [W3C calc() implementation]: https://www.w3.org/TR/css3-values/#calc-notation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-calc",
3
- "version": "11.1.2",
3
+ "version": "11.2.1",
4
4
  "type": "module",
5
5
  "description": "PostCSS plugin to reduce calc()",
6
6
  "keywords": [
@@ -38,21 +38,20 @@
38
38
  "devEngines": {
39
39
  "packageManager": {
40
40
  "name": "pnpm",
41
- "version": "12.3.4"
41
+ "version": "12.4.2"
42
42
  }
43
43
  },
44
44
  "devDependencies": {
45
- "@csstools/css-calc": "^3.3.0",
46
- "@rmenke/css-tokenizer-tests": "^1.2.0",
47
- "@types/node": "^26.5.0",
48
- "fast-check": "^4.9.0",
49
- "oxfmt": "^0.67.0",
50
- "oxlint": "^1.82.0",
45
+ "@csstools/css-calc": "^3.4.0",
46
+ "@types/node": "^26.6.1",
47
+ "fast-check": "^4.10.1",
48
+ "oxfmt": "^0.68.0",
49
+ "oxlint": "^1.83.0",
51
50
  "postcss": "^8.5.28",
52
51
  "typescript": "~7.0.2"
53
52
  },
54
53
  "dependencies": {
55
- "@csstools/css-tokenizer": "^4.0.0"
54
+ "@csstools/css-tokenizer": "^4.0.1"
56
55
  },
57
56
  "peerDependencies": {
58
57
  "postcss": "^8.5.28"
@@ -60,10 +59,15 @@
60
59
  "scripts": {
61
60
  "lint": "oxlint . && tsc && oxfmt --check",
62
61
  "fmt": "oxfmt",
63
- "benchmark:arithmetic-chains": "node scripts/benchmark-arithmetic-chains.mjs",
64
- "benchmark:nested-fallbacks": "node scripts/benchmark-nested-fallbacks.mjs",
65
- "test": "node --test --test-reporter=dot 'test/**/*.test.mjs' test/index.cjs test/convertUnit.cjs",
66
- "test:mutation:corpus": "node test/mutation/corpus-selection.mjs",
67
- "test:corpus:full": "POSTCSS_CALC_FULL_CORPUS=1 node --test test/conformance/corpus.test.mjs"
62
+ "benchmark:arithmetic-chains": "node scripts/benchmark-arithmetic-chains.js",
63
+ "benchmark:nested-fallbacks": "node scripts/benchmark-nested-fallbacks.js",
64
+ "benchmark:corpus": "node scripts/benchmark.js",
65
+ "benchmark:serialization": "node scripts/benchmark-serialization.js",
66
+ "test:benchmark": "node --test 'test/unit/benchmark-*.test.js' test/unit/compare-parser-benchmarks.test.js test/unit/corpus-benchmark.test.js",
67
+ "test:benchmark:simulation": "node test/benchmark/statistical-simulation.js",
68
+ "benchmark:reanalyze": "node scripts/compare-parser-benchmarks.js",
69
+ "test": "node --test --test-reporter=dot 'test/**/*.test.js' 'test/**/*.test.cjs'",
70
+ "test:mutation:corpus": "node test/mutation/corpus-selection.js",
71
+ "test:corpus:full": "POSTCSS_CALC_FULL_CORPUS=1 node --test test/conformance/corpus.test.js"
68
72
  }
69
73
  }
package/src/index.js CHANGED
@@ -1,12 +1,13 @@
1
1
  // PostCSS adapter over the standalone component-value reducer.
2
- import reduceCalc, { hasPotentialMathFunction } from './reduce.js';
3
-
2
+ import reduceCalc from './reduce.js';
3
+ import { hasPotentialMathFunction } from './lib/functions.js';
4
4
  /**
5
5
  * @typedef {object} PostCssCalcOptions
6
6
  * @property {number | false} [precision]
7
7
  * @property {boolean} [warnWhenCannotResolve]
8
8
  * @property {boolean} [mediaQueries]
9
9
  * @property {boolean} [selectors]
10
+ * @property {boolean} [unwrapSingleValue] Serialize fully resolved finite scalar results without calculation syntax. Defaults to `false`.
10
11
  * @property {(error: Error, input: string) => void} [onParseError] Invoked when parse/simplify throws. Replaces the default `result.warn`.
11
12
  */
12
13
 
@@ -24,7 +25,7 @@ import reduceCalc, { hasPotentialMathFunction } from './reduce.js';
24
25
  * @param {(target: import('postcss').ChildNode, value: string) => void} setProp
25
26
  * @param {ResolvedOptions} options
26
27
  * @param {import('postcss').Result} result
27
- * @param {boolean} unwrapSingleNegativeNumber
28
+ * @param {boolean} selectorContext
28
29
  * @return {void}
29
30
  */
30
31
  function applyTransform(
@@ -33,7 +34,7 @@ function applyTransform(
33
34
  setProp,
34
35
  options,
35
36
  result,
36
- unwrapSingleNegativeNumber
37
+ selectorContext
37
38
  ) {
38
39
  if (!hasPotentialMathFunction(current)) {
39
40
  return;
@@ -49,7 +50,8 @@ function applyTransform(
49
50
  onWarn: (message) => {
50
51
  result.warn(message, { plugin: 'postcss-calc', node });
51
52
  },
52
- unwrapSingleNegativeNumber,
53
+ // Selectors cannot contain calc(), so they always unwrap resolved values.
54
+ unwrapSingleValue: selectorContext || options.unwrapSingleValue,
53
55
  });
54
56
  if (transformed !== current) {
55
57
  setProp(node, transformed);
@@ -67,6 +69,7 @@ function pluginCreator(opts) {
67
69
  warnWhenCannotResolve: false,
68
70
  mediaQueries: false,
69
71
  selectors: false,
72
+ unwrapSingleValue: false,
70
73
  ...opts,
71
74
  };
72
75
 
@@ -0,0 +1,272 @@
1
+ import { baseOf } from './convertUnits.js';
2
+ import {
3
+ addTypes,
4
+ failureType,
5
+ isFailure,
6
+ isPercentage,
7
+ mathFunctions,
8
+ numberType,
9
+ percentageType,
10
+ unknownType,
11
+ } from './functions.js';
12
+ import { assertDepth } from './limits.js';
13
+
14
+ /** @typedef {import('./node.js').Node} Node */
15
+ /** @typedef {import('./functions.js').CalculationType} CalculationType */
16
+ /** @typedef {Extract<CalculationType, {kind: 'dimension'}>} DimensionType */
17
+
18
+ /** @typedef {'number' | 'unknown' | {dimension: string | null}} AnalysisType */
19
+ /** @typedef {{type: AnalysisType, valid: boolean, unresolved: boolean}} Analysis */
20
+ /**
21
+ * @typedef {Object} ProductFactors
22
+ * @property {boolean} valid
23
+ * @property {boolean} structurallyValid
24
+ * @property {boolean} hasUnresolved
25
+ * @property {DimensionType | null} numerator
26
+ * @property {DimensionType | null} denominator
27
+ * @property {boolean} hasOpaqueNumerator
28
+ * @property {boolean} hasOpaqueDenominator
29
+ */
30
+
31
+ /**
32
+ * Analyze the original complete tree and return its root summary. Analysis
33
+ * validates and classifies the tree; it is not a rewrite plan.
34
+ * @param {Node} node
35
+ * @return {Analysis}
36
+ */
37
+ function analyze(node) {
38
+ const result = analyzeType(node);
39
+ return {
40
+ type: publicType(result.type),
41
+ valid: result.valid,
42
+ unresolved: result.unresolved,
43
+ };
44
+ }
45
+
46
+ /** @param {Node} node @param {number} [depth] @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
47
+ function analyzeType(node, depth = 0) {
48
+ assertDepth(depth);
49
+ switch (node.type) {
50
+ case 'Num':
51
+ return resolved(numberType);
52
+ case 'Dim':
53
+ // Percentages are contextual; their percent-ness is tracked so a
54
+ // `% / %` product cancels to a number. Unknown units are opaque, while
55
+ // known families can still reject px + seconds.
56
+ return node.unit === '%'
57
+ ? finish(percentageType, true, true)
58
+ : resolved({ kind: 'dimension', base: baseOf(node.unit) });
59
+ case 'Ident':
60
+ return markUnresolved(unknownType);
61
+ case 'Sum':
62
+ return analyzeSum(node, depth);
63
+ case 'Product':
64
+ return analyzeProduct(node, depth);
65
+ case 'Call':
66
+ return analyzeCall(node, depth);
67
+ case 'OpaqueCall':
68
+ return finish(unknownType, true, true);
69
+ }
70
+ }
71
+
72
+ /** @param {Extract<Node, {type: 'Sum'}>} node @param {number} depth @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
73
+ function analyzeSum(node, depth) {
74
+ let type = null;
75
+ let hasUnknown = false;
76
+ let hasPercentage = false;
77
+ let valid = true;
78
+ let hasUnresolved = false;
79
+ for (const term of node.terms) {
80
+ const child = analyzeType(term.node, depth + 1);
81
+ valid = valid && child.valid;
82
+ hasUnresolved = hasUnresolved || child.unresolved;
83
+ if (isFailure(child.type)) {
84
+ type = failureType;
85
+ } else if (child.type.kind === 'unknown') {
86
+ // A pure percentage sum stays percentage-typed so a surrounding
87
+ // product can cancel `% / %`; any other opaque term must widen the
88
+ // sum back to unknown.
89
+ if (isPercentage(child.type)) hasPercentage = true;
90
+ else hasUnknown = true;
91
+ } else if (type === null) {
92
+ type = child.type;
93
+ } else if (!isFailure(type)) {
94
+ type = addTypes(type, child.type);
95
+ }
96
+ }
97
+ if (type !== null && isFailure(type)) {
98
+ return finish(failureType, false, hasUnresolved);
99
+ }
100
+ let fallback = numberType;
101
+ if (hasUnknown) fallback = unknownType;
102
+ else if (hasPercentage) fallback = percentageType;
103
+ return finish(type ?? fallback, valid, hasUnresolved);
104
+ }
105
+
106
+ /** @param {Extract<Node, {type: 'Product'}>} node @param {number} depth @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
107
+ function analyzeProduct(node, depth) {
108
+ let numerator = null;
109
+ let denominator = null;
110
+ let valid = true;
111
+ let structurallyValid = true;
112
+ // Opaque factors (unknowns and pure percentages) are counted per side so
113
+ // the pass below can decide whether any unknown remains after cancelling
114
+ // `% / %` pairs.
115
+ let opaqueNumerator = 0;
116
+ let opaqueDenominator = 0;
117
+ let percentageNumerator = 0;
118
+ let percentageDenominator = 0;
119
+ let hasUnresolved = false;
120
+ for (const factor of node.factors) {
121
+ const child = analyzeType(factor.node, depth + 1);
122
+ valid = valid && child.valid;
123
+ hasUnresolved = hasUnresolved || child.unresolved;
124
+ if (isFailure(child.type)) {
125
+ continue;
126
+ }
127
+ if (child.type.kind === 'unknown') {
128
+ if (factor.exponent === 1) {
129
+ opaqueNumerator++;
130
+ if (isPercentage(child.type)) percentageNumerator++;
131
+ } else {
132
+ opaqueDenominator++;
133
+ if (isPercentage(child.type)) percentageDenominator++;
134
+ }
135
+ continue;
136
+ }
137
+ if (child.type.kind !== 'dimension') continue;
138
+ if (factor.exponent === 1) {
139
+ if (numerator !== null) structurallyValid = false;
140
+ else numerator = child.type;
141
+ } else {
142
+ if (denominator !== null) structurallyValid = false;
143
+ else denominator = child.type;
144
+ }
145
+ }
146
+ // A percentage divided by a percentage is always a plain number: both
147
+ // operands resolve in the same context, so their contextual type cancels.
148
+ // Consume one such pair before judging the remaining unknowns so a
149
+ // surrounding sum does not mistake `% / %` for a length-compatible term.
150
+ const cancelled = Math.min(percentageNumerator, percentageDenominator);
151
+ const hasOpaqueNumerator = opaqueNumerator - cancelled > 0;
152
+ const hasOpaqueDenominator = opaqueDenominator - cancelled > 0;
153
+ return finishProduct({
154
+ valid,
155
+ structurallyValid,
156
+ hasUnresolved,
157
+ numerator,
158
+ denominator,
159
+ hasOpaqueNumerator,
160
+ hasOpaqueDenominator,
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Classify a product once its factors are analyzed. Opaque factors can supply
166
+ * missing type information, but they cannot make an already-invalid
167
+ * combination of known dimensions valid.
168
+ * @param {ProductFactors} factors
169
+ * @return {{type: CalculationType, valid: boolean, unresolved: boolean}}
170
+ */
171
+ function finishProduct(factors) {
172
+ const { valid, structurallyValid, hasUnresolved } = factors;
173
+ if (!valid) return finish(failureType, false, hasUnresolved);
174
+ if (!structurallyValid) return finish(failureType, false, hasUnresolved);
175
+ const { numerator, denominator } = factors;
176
+ if (
177
+ numerator !== null &&
178
+ denominator !== null &&
179
+ numerator.base !== denominator.base
180
+ ) {
181
+ return finish(failureType, false, hasUnresolved);
182
+ }
183
+ // An unresolved numerator multiplied by one known numerator dimension can
184
+ // only leave that dimension in place (when it resolves to a number) or make
185
+ // the product invalid. It can never make the product a bare number. Keep
186
+ // that known constraint so a surrounding sum can reject `1px * 1% + 1`.
187
+ const constrained = constrainedNumerator(factors);
188
+ if (constrained !== null) {
189
+ return finish(constrained, true, hasUnresolved);
190
+ }
191
+ // Other opaque factors may supply type information that changes how the
192
+ // known dimensions combine once the known factors are structurally valid.
193
+ if (factors.hasOpaqueNumerator || factors.hasOpaqueDenominator) {
194
+ return finish(unknownType, true, hasUnresolved);
195
+ }
196
+ if (numerator !== null && denominator !== null) {
197
+ return finish(
198
+ numerator.base === denominator.base ? numberType : failureType,
199
+ valid && numerator.base === denominator.base,
200
+ hasUnresolved
201
+ );
202
+ }
203
+ if (denominator !== null) return finish(failureType, false, hasUnresolved);
204
+ return finish(numerator ?? numberType, valid, hasUnresolved);
205
+ }
206
+
207
+ /**
208
+ * @param {ProductFactors} factors
209
+ * @return {DimensionType | null}
210
+ */
211
+ function constrainedNumerator(factors) {
212
+ return factors.hasOpaqueNumerator &&
213
+ !factors.hasOpaqueDenominator &&
214
+ factors.numerator !== null &&
215
+ factors.denominator === null
216
+ ? factors.numerator
217
+ : null;
218
+ }
219
+
220
+ /** @param {Extract<Node, {type: 'Call'}>} node @param {number} depth @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
221
+ function analyzeCall(node, depth) {
222
+ const name = node.name.toLowerCase();
223
+ const definition = mathFunctions.get(name);
224
+ /** @type {CalculationType[]} */
225
+ const childTypes = [];
226
+ let valid = true;
227
+ let unresolvedArgs = false;
228
+ for (let index = 0; index < node.args.length; index++) {
229
+ const child = analyzeType(node.args[index], depth + 1);
230
+ childTypes.push(child.type);
231
+ valid = valid && child.valid;
232
+ if (!definition?.isKeyword?.(node.args[index], index) && child.unresolved) {
233
+ unresolvedArgs = true;
234
+ }
235
+ }
236
+ if (!definition) return finish(unknownType, valid, true);
237
+ const type = definition.analyze(childTypes, node.args);
238
+ const unresolvedType = type.kind === 'unknown';
239
+ return finish(
240
+ type,
241
+ valid && !isFailure(type),
242
+ unresolvedArgs || unresolvedType
243
+ );
244
+ }
245
+
246
+ /** @param {CalculationType} type @param {boolean} valid @param {boolean} unresolved @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
247
+ function finish(type, valid, unresolved) {
248
+ return {
249
+ type,
250
+ valid: valid && !isFailure(type),
251
+ unresolved,
252
+ };
253
+ }
254
+
255
+ /** @param {CalculationType} type @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
256
+ function resolved(type) {
257
+ return finish(type, true, false);
258
+ }
259
+
260
+ /** @param {CalculationType} type @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */
261
+ function markUnresolved(type) {
262
+ return finish(type, true, true);
263
+ }
264
+
265
+ /** @param {CalculationType} type @return {AnalysisType} */
266
+ function publicType(type) {
267
+ if (type.kind === 'number') return 'number';
268
+ if (type.kind === 'unknown' || type.kind === 'failure') return 'unknown';
269
+ return { dimension: type.base };
270
+ }
271
+
272
+ export { analyze };
@@ -0,0 +1,118 @@
1
+ import { TokenType as CssType } from '@csstools/css-tokenizer';
2
+
3
+ /** @typedef {import('@csstools/css-tokenizer').CSSToken} CSSToken */
4
+
5
+ const BLOCK_CLOSE = new Map([
6
+ [CssType.Function, CssType.CloseParen],
7
+ [CssType.OpenParen, CssType.CloseParen],
8
+ [CssType.OpenSquare, CssType.CloseSquare],
9
+ [CssType.OpenCurly, CssType.CloseCurly],
10
+ ]);
11
+
12
+ /**
13
+ * Read-only delimiter navigation for one native token stream range.
14
+ */
15
+ class BlockIndex {
16
+ /** @type {CSSToken[]} */
17
+ #tokens;
18
+ /** @type {number} */
19
+ #rangeStart;
20
+ /** @type {number} */
21
+ #rangeEnd;
22
+ /** @type {Map<number, number>} */
23
+ #closes = new Map();
24
+ /** @type {number} */
25
+ #maxDepth = 0;
26
+
27
+ /**
28
+ * Build the delimiter index once for a token stream range. Matching remains
29
+ * LIFO: a mismatched closer is ignored and does not disturb the open stack.
30
+ *
31
+ * @param {CSSToken[]} tokens
32
+ * @param {number} start
33
+ * @param {number} end
34
+ */
35
+ constructor(tokens, start, end) {
36
+ this.#tokens = tokens;
37
+ this.#rangeStart = Math.max(0, start);
38
+ this.#rangeEnd = Math.min(tokens.length, Math.max(this.#rangeStart, end));
39
+ /** @type {{index: number, close: import('@csstools/css-tokenizer').TokenType}[]} */
40
+ const stack = [];
41
+
42
+ for (let i = this.#rangeStart; i < this.#rangeEnd; i++) {
43
+ const type = tokens[i][0];
44
+ const close = BLOCK_CLOSE.get(type);
45
+ if (close !== undefined) {
46
+ stack.push({ index: i, close });
47
+ this.#maxDepth = Math.max(this.#maxDepth, stack.length);
48
+ continue;
49
+ }
50
+
51
+ if (
52
+ type === CssType.CloseParen ||
53
+ type === CssType.CloseSquare ||
54
+ type === CssType.CloseCurly
55
+ ) {
56
+ const open = stack.at(-1);
57
+ if (open === undefined || open.close !== type) {
58
+ continue;
59
+ }
60
+ stack.pop();
61
+ this.#closes.set(open.index, i);
62
+ }
63
+ }
64
+ }
65
+
66
+ /** @return {number} */
67
+ get maxDepth() {
68
+ return this.#maxDepth;
69
+ }
70
+
71
+ /** @param {number | undefined} end @return {number} */
72
+ #boundedEnd(end) {
73
+ return Math.min(
74
+ this.#rangeEnd,
75
+ Math.max(this.#rangeStart, end ?? this.#rangeEnd)
76
+ );
77
+ }
78
+
79
+ /** @param {number} openPosition @param {number} [endPosition] @return {number} */
80
+ closeOf(openPosition, endPosition) {
81
+ const close = this.#closes.get(openPosition) ?? -1;
82
+ const bound = this.#boundedEnd(endPosition);
83
+ return close >= this.#rangeStart && close < bound ? close : -1;
84
+ }
85
+
86
+ /** @param {number} position @param {number} endPosition @return {number} */
87
+ nextComponent(position, endPosition) {
88
+ const bound = this.#boundedEnd(endPosition);
89
+ if (position < this.#rangeStart || position >= bound) return bound;
90
+ const close = this.closeOf(position, bound);
91
+ return close === -1 ? position + 1 : close + 1;
92
+ }
93
+
94
+ /** @param {number} startPosition @param {number} endPosition @return {number} */
95
+ firstTopLevelComma(startPosition, endPosition) {
96
+ const bound = this.#boundedEnd(endPosition);
97
+ for (let i = Math.max(this.#rangeStart, startPosition); i < bound;) {
98
+ if (this.#tokens[i][0] === CssType.Comma) return i;
99
+ i = this.nextComponent(i, bound);
100
+ }
101
+ return -1;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Build the delimiter index once for a token stream range. Matching remains
107
+ * LIFO: a mismatched closer is ignored and does not disturb the open stack.
108
+ *
109
+ * @param {CSSToken[]} tokens
110
+ * @param {number} [start]
111
+ * @param {number} [end]
112
+ * @return {BlockIndex}
113
+ */
114
+ function indexBlocks(tokens, start = 0, end = tokens.length) {
115
+ return new BlockIndex(tokens, start, end);
116
+ }
117
+
118
+ export { indexBlocks };