postcss-calc 11.0.3 → 11.1.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/src/reduce.js ADDED
@@ -0,0 +1,176 @@
1
+ // CSS component-value reducer. This module deliberately has no PostCSS
2
+ // dependency so it can also be used for individual declaration values,
3
+ // at-rule parameters, or selector text.
4
+ import {
5
+ tokenize as cssTokenize,
6
+ TokenType as CssType,
7
+ } from '@csstools/css-tokenizer';
8
+ import { tokenizeTokens } from './lib/tokenizer.js';
9
+ import { parse } from './lib/parser.js';
10
+ import { simplify } from './lib/simplify.js';
11
+ import {
12
+ isSupportedMathFunction,
13
+ hasPotentialMathFunction,
14
+ QUICK_MATH_TEST,
15
+ } from './lib/simplify/call.js';
16
+ import { serialize } from './lib/serialize.js';
17
+
18
+ const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i;
19
+
20
+ const BLOCK_CLOSE = new Map([
21
+ [CssType.OpenParen, CssType.CloseParen],
22
+ [CssType.OpenSquare, CssType.CloseSquare],
23
+ [CssType.OpenCurly, CssType.CloseCurly],
24
+ ]);
25
+
26
+ /**
27
+ * @typedef {object} ReduceCalcOptions
28
+ * @property {number | false} [precision]
29
+ * @property {boolean} [warnWhenCannotResolve]
30
+ * @property {boolean} [unwrapSingleNegativeNumber] Serialize finite negative results without a `calc()` wrapper. Defaults to `false`.
31
+ * @property {(error: Error, input: string) => void} [onParseError] Invoked when parse/simplify throws.
32
+ * @property {(message: string) => void} [onWarn] Invoked when `warnWhenCannotResolve` is set and an expression cannot be reduced to a single value.
33
+ */
34
+
35
+ /** @typedef {Required<Omit<ReduceCalcOptions, 'onParseError' | 'onWarn'>> & Pick<ReduceCalcOptions, 'onParseError' | 'onWarn'>} ResolvedReduceCalcOptions */
36
+
37
+ /**
38
+ * Fields threaded through the internal token-range walk.
39
+ *
40
+ * @typedef {object} TransformContext
41
+ * @property {ResolvedReduceCalcOptions} options
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
+ */
54
+
55
+ /**
56
+ * Walk one component-value level. Unsupported functions and simple blocks are
57
+ * traversed, while a supported function is treated as one opaque calculation
58
+ * even when parsing it fails. A missing closer consumes through EOF, matching
59
+ * CSS component-value parsing's error recovery.
60
+ *
61
+ * @param {number} start
62
+ * @param {import('@csstools/css-tokenizer').TokenType | undefined} expectedClose
63
+ * @param {TransformContext} ctx
64
+ * @param {boolean} transform
65
+ * @return {number} Index of the matching closer, or the EOF token.
66
+ */
67
+ function walkTokens(start, expectedClose, ctx, transform) {
68
+ for (let i = start; i < ctx.tokens.length; i++) {
69
+ const token = ctx.tokens[i];
70
+ if (token[0] === CssType.EOF || token[0] === expectedClose) return i;
71
+
72
+ const blockClose = BLOCK_CLOSE.get(token[0]);
73
+ if (blockClose) {
74
+ i = walkTokens(i + 1, blockClose, ctx, transform);
75
+ continue;
76
+ }
77
+
78
+ if (token[0] !== CssType.Function) continue;
79
+
80
+ const name = token[4].value;
81
+ const isCalc = MATCH_CALC.test(name);
82
+ const isMath = !isCalc && isSupportedMathFunction(name);
83
+ if (!transform || (!isCalc && !isMath)) {
84
+ i = walkTokens(i + 1, CssType.CloseParen, ctx, transform);
85
+ continue;
86
+ }
87
+
88
+ // Locate the complete outer function without transforming its children.
89
+ const close = walkTokens(i + 1, CssType.CloseParen, ctx, false);
90
+ const closed = ctx.tokens[close][0] === CssType.CloseParen;
91
+ const end = closed ? ctx.tokens[close][3] + 1 : ctx.value.length;
92
+ const sliceStart = isCalc ? i + 1 : i;
93
+ const sliceEnd = closed ? close + (isCalc ? 0 : 1) : close;
94
+ const inputStart = isCalc ? token[3] + 1 : token[2];
95
+ const inputEnd = closed && isCalc ? ctx.tokens[close][2] : end;
96
+ const contents = ctx.value.slice(inputStart, inputEnd);
97
+ try {
98
+ const node = simplify(
99
+ parse(tokenizeTokens(ctx.tokens, end, sliceStart, sliceEnd))
100
+ );
101
+ ctx.replacements.push({
102
+ start: token[2],
103
+ end,
104
+ node,
105
+ calcName: isCalc ? name : 'calc',
106
+ });
107
+ } catch (error) {
108
+ const err = error instanceof Error ? error : new Error('Error');
109
+ ctx.options.onParseError?.(err, contents);
110
+ }
111
+ i = close;
112
+ }
113
+
114
+ return ctx.tokens.length - 1;
115
+ }
116
+
117
+ /**
118
+ * @param {import('./lib/node.js').Node} node
119
+ * @return {boolean}
120
+ */
121
+ function isUnresolvedResult(node) {
122
+ if (node.type === 'Sum' || node.type === 'Product') {
123
+ return true;
124
+ }
125
+ return node.type === 'Call' && isSupportedMathFunction(node.name);
126
+ }
127
+
128
+ /**
129
+ * Simplify every supported CSS math function in a component-value string.
130
+ * Text outside those functions is preserved byte-for-byte.
131
+ *
132
+ * @param {string} value
133
+ * @param {ReduceCalcOptions} [opts]
134
+ * @return {string}
135
+ */
136
+ function reduceCalc(value, opts) {
137
+ if (!hasPotentialMathFunction(value)) {
138
+ return value;
139
+ }
140
+
141
+ /** @type {ResolvedReduceCalcOptions} */
142
+ const options = {
143
+ precision: 5,
144
+ warnWhenCannotResolve: false,
145
+ unwrapSingleNegativeNumber: false,
146
+ ...opts,
147
+ };
148
+ const tokens = cssTokenize({ css: value });
149
+ /** @type {Replacement[]} */
150
+ const replacements = [];
151
+ walkTokens(0, undefined, { options, value, tokens, replacements }, true);
152
+
153
+ if (replacements.length === 0) {
154
+ return value;
155
+ }
156
+
157
+ let output = '';
158
+ let lastIndex = 0;
159
+ for (const replacement of replacements) {
160
+ const text = serialize(replacement.node, {
161
+ precision: options.precision,
162
+ calcName: replacement.calcName,
163
+ unwrapSingleNegativeNumber: options.unwrapSingleNegativeNumber,
164
+ });
165
+ if (options.warnWhenCannotResolve && isUnresolvedResult(replacement.node)) {
166
+ options.onWarn?.('Could not reduce expression: ' + value);
167
+ }
168
+ output += value.slice(lastIndex, replacement.start) + text;
169
+ lastIndex = replacement.end;
170
+ }
171
+ output += value.slice(lastIndex);
172
+ return output;
173
+ }
174
+
175
+ export { QUICK_MATH_TEST, hasPotentialMathFunction };
176
+ export default reduceCalc;
package/types/index.d.ts CHANGED
@@ -9,21 +9,6 @@ export type PostCssCalcOptions = {
9
9
  onParseError?: (error: Error, input: string) => void;
10
10
  };
11
11
  export type ResolvedOptions = Required<Omit<PostCssCalcOptions, 'onParseError'>> & Pick<PostCssCalcOptions, 'onParseError'>;
12
- export type TransformContext = {
13
- options: ResolvedOptions;
14
- result: import('postcss').Result;
15
- item: import('postcss').ChildNode;
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;
26
- };
27
12
  /**
28
13
  * @param {PostCssCalcOptions} [opts]
29
14
  * @return {import('postcss').Plugin}
@@ -21,6 +21,22 @@ declare class Parser {
21
21
  * @return {Token}
22
22
  */
23
23
  expect(type: TokenType, value?: string): Token;
24
+ /**
25
+ * @param {string} value
26
+ * @param {string} [value2]
27
+ * @return {boolean}
28
+ */
29
+ isPunct(value: string, value2?: string): boolean;
30
+ /**
31
+ * @param {string} value
32
+ * @return {boolean}
33
+ */
34
+ matchPunct(value: string): boolean;
35
+ /**
36
+ * @param {string} value
37
+ * @return {Token}
38
+ */
39
+ expectPunct(value: string): Token;
24
40
  /**
25
41
  * @param {number} [minBp]
26
42
  * @return {Node}
@@ -11,6 +11,10 @@ export type SerializeOptions = {
11
11
  * Wrapper name to use when `calc()` is needed. Default `'calc'`.
12
12
  */
13
13
  calcName?: string;
14
+ /**
15
+ * Serialize finite negative scalars without a wrapper. Internal selector-only mode.
16
+ */
17
+ unwrapSingleNegativeNumber?: boolean;
14
18
  };
15
19
  /**
16
20
  * @param {Node} node
@@ -1,6 +1,16 @@
1
1
  export type Node = import('../node.js').Node;
2
2
  export type SimplifyFn = import('../simplify.js').SimplifyFn;
3
3
  export type MathSimplifier = (name: string, args: Node[]) => Node;
4
+ declare const QUICK_MATH_TEST: RegExp;
5
+ /**
6
+ * Fast check to determine whether a CSS component value could contain
7
+ * a supported calculation or math function call (or an escape sequence
8
+ * that could decode to one).
9
+ *
10
+ * @param {string} value
11
+ * @return {boolean}
12
+ */
13
+ declare function hasPotentialMathFunction(value: string): boolean;
4
14
  /**
5
15
  * Whether a bare CSS math function has an implemented simplifier.
6
16
  *
@@ -16,4 +26,4 @@ declare function isSupportedMathFunction(name: string): boolean;
16
26
  declare function simplifyCall(node: Extract<Node, {
17
27
  type: 'Call';
18
28
  }>, simplify: SimplifyFn): Node;
19
- export { isSupportedMathFunction, simplifyCall };
29
+ export { isSupportedMathFunction, simplifyCall, hasPotentialMathFunction, QUICK_MATH_TEST, };
@@ -1,7 +1,20 @@
1
1
  export type Node = import('../node.js').Node;
2
+ export type Num = import('../node.js').Num;
3
+ export type Dim = import('../node.js').Dim;
2
4
  export type BaseType = import('../convertUnits.js').BaseType;
3
5
  /** @typedef {import('../node.js').Node} Node */
6
+ /** @typedef {import('../node.js').Num} Num */
7
+ /** @typedef {import('../node.js').Dim} Dim */
4
8
  /** @typedef {import('../convertUnits.js').BaseType} BaseType */
9
+ /**
10
+ * Construct a Num or Dim node from a folded result.
11
+ * @param {{ unit: string }} fold
12
+ * @param {number} value
13
+ * @return {Num | Dim}
14
+ */
15
+ declare function foldResult(fold: {
16
+ unit: string;
17
+ }, value: number): Num | Dim;
5
18
  /**
6
19
  * @param {Node[]} args
7
20
  * @return {{ values: number[], unit: string } | null}
@@ -10,4 +23,4 @@ declare function foldConstArgs(args: Node[]): {
10
23
  values: number[];
11
24
  unit: string;
12
25
  } | null;
13
- export { foldConstArgs };
26
+ export { foldConstArgs, foldResult };
@@ -24,7 +24,9 @@ declare function tokenize(input: string): Token[];
24
24
  *
25
25
  * @param {import('@csstools/css-tokenizer').CSSToken[]} cssTokens
26
26
  * @param {number} eofPosition
27
+ * @param {number} [start]
28
+ * @param {number} [end]
27
29
  * @return {Token[]}
28
30
  */
29
- declare function tokenizeTokens(cssTokens: import('@csstools/css-tokenizer').CSSToken[], eofPosition: number): Token[];
31
+ declare function tokenizeTokens(cssTokens: import('@csstools/css-tokenizer').CSSToken[], eofPosition: number, start?: number, end?: number): Token[];
30
32
  export { tokenize, tokenizeTokens };
@@ -0,0 +1,41 @@
1
+ import { hasPotentialMathFunction, QUICK_MATH_TEST } from './lib/simplify/call.js';
2
+ export type ReduceCalcOptions = {
3
+ precision?: number | false;
4
+ warnWhenCannotResolve?: boolean;
5
+ /**
6
+ * Serialize finite negative results without a `calc()` wrapper. Defaults to `false`.
7
+ */
8
+ unwrapSingleNegativeNumber?: boolean;
9
+ /**
10
+ * Invoked when parse/simplify throws.
11
+ */
12
+ onParseError?: (error: Error, input: string) => void;
13
+ /**
14
+ * Invoked when `warnWhenCannotResolve` is set and an expression cannot be reduced to a single value.
15
+ */
16
+ onWarn?: (message: string) => void;
17
+ };
18
+ export type ResolvedReduceCalcOptions = Required<Omit<ReduceCalcOptions, 'onParseError' | 'onWarn'>> & Pick<ReduceCalcOptions, 'onParseError' | 'onWarn'>;
19
+ export type TransformContext = {
20
+ options: ResolvedReduceCalcOptions;
21
+ value: string;
22
+ tokens: import('@csstools/css-tokenizer').CSSToken[];
23
+ replacements: Replacement[];
24
+ };
25
+ export type Replacement = {
26
+ start: number;
27
+ end: number;
28
+ node: import('./lib/node.js').Node;
29
+ calcName: string;
30
+ };
31
+ /**
32
+ * Simplify every supported CSS math function in a component-value string.
33
+ * Text outside those functions is preserved byte-for-byte.
34
+ *
35
+ * @param {string} value
36
+ * @param {ReduceCalcOptions} [opts]
37
+ * @return {string}
38
+ */
39
+ declare function reduceCalc(value: string, opts?: ReduceCalcOptions): string;
40
+ export { QUICK_MATH_TEST, hasPotentialMathFunction };
41
+ export default reduceCalc;
@@ -1,23 +0,0 @@
1
- export type BaseType =
2
- | 'length'
3
- | 'angle'
4
- | 'time'
5
- | 'frequency'
6
- | 'resolution'
7
- | 'flex'
8
- | 'percentage';
9
- /**
10
- * @param {string} unit
11
- * @return {BaseType | null}
12
- */
13
- export function baseOf(unit: string): BaseType | null;
14
- /**
15
- * Convert a value within a single conversion family. Returns null when
16
- * either unit is missing from the table (em/rem/vw need runtime context)
17
- * or when the units belong to different base types.
18
- * @param {number} value
19
- * @param {string} from
20
- * @param {string} to
21
- * @return {number | null}
22
- */
23
- export function convert(value: number, from: string, to: string): number | null;