postcss-calc 11.0.2 → 11.0.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.2",
3
+ "version": "11.0.3",
4
4
  "type": "module",
5
5
  "description": "PostCSS plugin to reduce calc()",
6
6
  "keywords": [
@@ -48,7 +48,6 @@
48
48
  "typescript": "~7.0.2"
49
49
  },
50
50
  "dependencies": {
51
- "@csstools/css-parser-algorithms": "^4.0.0",
52
51
  "@csstools/css-tokenizer": "^4.0.0"
53
52
  },
54
53
  "peerDependencies": {
package/src/index.js CHANGED
@@ -1,27 +1,24 @@
1
1
  // PostCSS adapter. Walks declaration values (and optionally @rule params
2
2
  // and selectors), feeds calc() bodies through tokenize → parse → simplify
3
3
  // → serialize, and writes the result back.
4
- import { tokenize as cssTokenize } from '@csstools/css-tokenizer';
5
4
  import {
6
- isFunctionNode,
7
- isSimpleBlockNode,
8
- parseListOfComponentValues,
9
- } from '@csstools/css-parser-algorithms';
10
- import { tokenize } from './lib/tokenizer.js';
5
+ tokenize as cssTokenize,
6
+ TokenType as CssType,
7
+ } from '@csstools/css-tokenizer';
8
+ import { tokenizeTokens } from './lib/tokenizer.js';
11
9
  import { parse } from './lib/parser.js';
12
10
  import { simplify } from './lib/simplify.js';
13
11
  import { isSupportedMathFunction } from './lib/simplify/call.js';
14
12
  import { serialize } from './lib/serialize.js';
15
13
 
16
- // The outer walk is deliberately forgiving: it only needs to locate calc()/
17
- // math-function boundaries in otherwise arbitrary (and possibly non-standard)
18
- // CSS values, so parse errors from the outer tokenizer/parser are swallowed.
19
- // Genuine syntax problems inside a matched call are
20
- // caught below via our own tokenize/parse/simplify pipeline.
21
- const NOOP_PARSE_ERROR = { onParseError: () => {} };
22
-
23
14
  const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
24
15
 
16
+ const BLOCK_CLOSE = new Map([
17
+ [CssType.OpenParen, CssType.CloseParen],
18
+ [CssType.OpenSquare, CssType.CloseSquare],
19
+ [CssType.OpenCurly, CssType.CloseCurly],
20
+ ]);
21
+
25
22
  /**
26
23
  * @typedef {object} PostCssCalcOptions
27
24
  * @property {number | false} [precision]
@@ -34,7 +31,7 @@ const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
34
31
  /** @typedef {Required<Omit<PostCssCalcOptions, 'onParseError'>> & Pick<PostCssCalcOptions, 'onParseError'>} ResolvedOptions */
35
32
 
36
33
  /**
37
- * Fields threaded unchanged through the recursive `transformList` walk.
34
+ * Fields threaded unchanged through the token-range walk.
38
35
  * `value` is the original full property text, used only for the
39
36
  * warnWhenCannotResolve message.
40
37
  *
@@ -43,59 +40,76 @@ const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
43
40
  * @property {import('postcss').Result} result
44
41
  * @property {import('postcss').ChildNode} item
45
42
  * @property {string} value
43
+ * @property {import('@csstools/css-tokenizer').CSSToken[]} tokens
44
+ * @property {Replacement[]} replacements
45
+ */
46
+
47
+ /**
48
+ * @typedef {object} Replacement
49
+ * @property {number} start
50
+ * @property {number} end
51
+ * @property {import('./lib/node.js').Node} node
52
+ * @property {string} calcName
53
+ * @property {string} matchedName
46
54
  */
47
55
 
48
56
  /**
49
- * Walks a list of component values in place, replacing matched calc()/math
50
- * function nodes with their simplified form. Unlike the library's generic
51
- * `walk` helper, this recurses manually so a matched node's own (stale,
52
- * pre-simplification) children are never independently re-visited.
57
+ * Walk one component-value level. Unsupported functions and simple blocks are
58
+ * traversed, while a supported function is treated as one opaque calculation
59
+ * even when parsing it fails. A missing closer consumes through EOF, matching
60
+ * CSS component-value parsing's error recovery.
53
61
  *
54
- * @param {import('@csstools/css-parser-algorithms').ComponentValue[]} list
62
+ * @param {number} start
63
+ * @param {import('@csstools/css-tokenizer').TokenType | undefined} expectedClose
55
64
  * @param {TransformContext} ctx
56
- * @return {void}
65
+ * @param {boolean} transform
66
+ * @return {number} Index of the matching closer, or the EOF token.
57
67
  */
58
- function transformList(list, ctx) {
59
- for (let i = 0; i < list.length; i++) {
60
- const node = list[i];
61
- if (!isFunctionNode(node)) {
62
- if (isSimpleBlockNode(node)) {
63
- transformList(node.value, ctx);
64
- }
68
+ function walkTokens(start, expectedClose, ctx, transform) {
69
+ for (let i = start; i < ctx.tokens.length; i++) {
70
+ const token = ctx.tokens[i];
71
+ if (token[0] === CssType.EOF || token[0] === expectedClose) {
72
+ return i;
73
+ }
74
+
75
+ const blockClose = BLOCK_CLOSE.get(token[0]);
76
+ if (blockClose) {
77
+ i = walkTokens(i + 1, blockClose, ctx, transform);
65
78
  continue;
66
79
  }
67
80
 
68
- const name = node.getName();
81
+ if (token[0] !== CssType.Function) {
82
+ continue;
83
+ }
84
+
85
+ const name = token[4].value;
69
86
  const isCalc = MATCH_CALC.test(name);
70
87
  const isMath = !isCalc && isSupportedMathFunction(name);
71
- if (!isCalc && !isMath) {
72
- transformList(node.value, ctx);
88
+ if (!transform || (!isCalc && !isMath)) {
89
+ i = walkTokens(i + 1, CssType.CloseParen, ctx, transform);
73
90
  continue;
74
91
  }
75
92
 
76
- // calc(): feed the body. Bare math: feed the whole call.
77
- const inner = node.value.map((child) => child.toString()).join('');
78
- const contents = isCalc ? inner : `${name}(${inner})`;
93
+ // Locate the complete outer function without transforming its children.
94
+ const close = walkTokens(i + 1, CssType.CloseParen, ctx, false);
95
+ const closed = ctx.tokens[close][0] === CssType.CloseParen;
96
+ const end = closed ? ctx.tokens[close][3] + 1 : ctx.value.length;
97
+ const sliceStart = isCalc ? i + 1 : i;
98
+ const sliceEnd = closed ? close + (isCalc ? 0 : 1) : close;
99
+ const inputStart = isCalc ? token[3] + 1 : token[2];
100
+ const inputEnd = closed && isCalc ? ctx.tokens[close][2] : end;
101
+ const contents = ctx.value.slice(inputStart, inputEnd);
79
102
  try {
80
- const simplified = simplify(parse(tokenize(contents)));
81
- const str = serialize(simplified, {
82
- precision: ctx.options.precision,
83
- calcName: isCalc ? name : 'calc', // preserve vendor prefix on calc()
84
- });
85
-
86
- if (ctx.options.warnWhenCannotResolve && str.startsWith(`${name}(`)) {
87
- ctx.result.warn('Could not reduce expression: ' + ctx.value, {
88
- plugin: 'postcss-calc',
89
- node: ctx.item,
90
- });
91
- }
92
-
93
- const replacement = parseListOfComponentValues(
94
- cssTokenize({ css: str }),
95
- NOOP_PARSE_ERROR
103
+ const node = simplify(
104
+ parse(tokenizeTokens(ctx.tokens.slice(sliceStart, sliceEnd), end))
96
105
  );
97
- list.splice(i, 1, ...replacement);
98
- i += replacement.length - 1;
106
+ ctx.replacements.push({
107
+ start: token[2],
108
+ end,
109
+ node,
110
+ calcName: isCalc ? name : 'calc',
111
+ matchedName: name,
112
+ });
99
113
  } catch (error) {
100
114
  const err = error instanceof Error ? error : new Error('Error');
101
115
  if (ctx.options.onParseError) {
@@ -104,7 +118,10 @@ function transformList(list, ctx) {
104
118
  ctx.result.warn(err.message, { node: ctx.item });
105
119
  }
106
120
  }
121
+ i = close;
107
122
  }
123
+
124
+ return ctx.tokens.length - 1;
108
125
  }
109
126
 
110
127
  /**
@@ -115,14 +132,39 @@ function transformList(list, ctx) {
115
132
  * @return {string}
116
133
  */
117
134
  function transformValue(value, options, result, item) {
118
- const componentValues = parseListOfComponentValues(
119
- cssTokenize({ css: value }),
120
- NOOP_PARSE_ERROR
121
- );
135
+ const tokens = cssTokenize({ css: value });
136
+ /** @type {Replacement[]} */
137
+ const replacements = [];
138
+ const ctx = { options, result, item, value, tokens, replacements };
139
+ walkTokens(0, undefined, ctx, true);
122
140
 
123
- transformList(componentValues, { options, result, item, value });
141
+ /** @type {(Replacement & {text: string})[]} */
142
+ const serialized = replacements.map((replacement) => {
143
+ const text = serialize(replacement.node, {
144
+ precision: options.precision,
145
+ calcName: replacement.calcName,
146
+ });
147
+ if (
148
+ options.warnWhenCannotResolve &&
149
+ text.startsWith(`${replacement.matchedName}(`)
150
+ ) {
151
+ result.warn('Could not reduce expression: ' + value, {
152
+ plugin: 'postcss-calc',
153
+ node: item,
154
+ });
155
+ }
156
+ return { ...replacement, text };
157
+ });
124
158
 
125
- return componentValues.map((node) => node.toString()).join('');
159
+ let output = value;
160
+ for (let i = serialized.length - 1; i >= 0; i--) {
161
+ const replacement = serialized[i];
162
+ output =
163
+ output.slice(0, replacement.start) +
164
+ replacement.text +
165
+ output.slice(replacement.end);
166
+ }
167
+ return output;
126
168
  }
127
169
 
128
170
  /**
@@ -23,6 +23,19 @@ const NUMERIC_RAW = /^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:[eE][+-]?\d+)?/;
23
23
  * @return {Token[]}
24
24
  */
25
25
  function tokenize(input) {
26
+ return tokenizeTokens(tokenizeCss({ css: input }), input.length);
27
+ }
28
+
29
+ /**
30
+ * Convert a slice of an existing CSS token stream into the token subset used
31
+ * by the calculation parser. Token positions remain relative to the original
32
+ * source text, which keeps parse errors useful to adapter callers.
33
+ *
34
+ * @param {import('@csstools/css-tokenizer').CSSToken[]} cssTokens
35
+ * @param {number} eofPosition
36
+ * @return {Token[]}
37
+ */
38
+ function tokenizeTokens(cssTokens, eofPosition) {
26
39
  /** @type {Token[]} */
27
40
  const tokens = [];
28
41
  let ws = true;
@@ -52,7 +65,7 @@ function tokenize(input) {
52
65
  ws = false;
53
66
  }
54
67
 
55
- for (const t of tokenizeCss({ css: input })) {
68
+ for (const t of cssTokens) {
56
69
  switch (t[0]) {
57
70
  case CssType.Whitespace:
58
71
  case CssType.Comment:
@@ -97,8 +110,7 @@ function tokenize(input) {
97
110
  tokens.push({ type: 'punct', value: t[4].value, pos: t[2], ws });
98
111
  break;
99
112
  case CssType.EOF:
100
- tokens.push({ type: 'eof', value: '', pos: input.length, ws });
101
- break;
113
+ continue;
102
114
  default:
103
115
  throw new Error(
104
116
  `Unexpected character "${t[1][0] ?? ''}" at position ${t[2]}`
@@ -107,7 +119,9 @@ function tokenize(input) {
107
119
  ws = false;
108
120
  }
109
121
 
122
+ tokens.push({ type: 'eof', value: '', pos: eofPosition, ws });
123
+
110
124
  return tokens;
111
125
  }
112
126
 
113
- export { tokenize };
127
+ export { tokenize, tokenizeTokens };
package/types/index.d.ts CHANGED
@@ -14,6 +14,15 @@ export type TransformContext = {
14
14
  result: import('postcss').Result;
15
15
  item: import('postcss').ChildNode;
16
16
  value: string;
17
+ tokens: import('@csstools/css-tokenizer').CSSToken[];
18
+ replacements: Replacement[];
19
+ };
20
+ export type Replacement = {
21
+ start: number;
22
+ end: number;
23
+ node: import('./lib/node.js').Node;
24
+ calcName: string;
25
+ matchedName: string;
17
26
  };
18
27
  /**
19
28
  * @param {PostCssCalcOptions} [opts]
@@ -17,4 +17,14 @@ export type Token = {
17
17
  * @return {Token[]}
18
18
  */
19
19
  declare function tokenize(input: string): Token[];
20
- export { tokenize };
20
+ /**
21
+ * Convert a slice of an existing CSS token stream into the token subset used
22
+ * by the calculation parser. Token positions remain relative to the original
23
+ * source text, which keeps parse errors useful to adapter callers.
24
+ *
25
+ * @param {import('@csstools/css-tokenizer').CSSToken[]} cssTokens
26
+ * @param {number} eofPosition
27
+ * @return {Token[]}
28
+ */
29
+ declare function tokenizeTokens(cssTokens: import('@csstools/css-tokenizer').CSSToken[], eofPosition: number): Token[];
30
+ export { tokenize, tokenizeTokens };