postcss-calc 6.0.1 → 10.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.
@@ -0,0 +1,396 @@
1
+ 'use strict';
2
+ const convertUnit = require('./convertUnit.js');
3
+
4
+ /**
5
+ * @param {import('../parser').CalcNode} node
6
+ * @return {node is import('../parser').ValueExpression}
7
+ */
8
+ function isValueType(node) {
9
+ switch (node.type) {
10
+ case 'LengthValue':
11
+ case 'AngleValue':
12
+ case 'TimeValue':
13
+ case 'FrequencyValue':
14
+ case 'ResolutionValue':
15
+ case 'EmValue':
16
+ case 'ExValue':
17
+ case 'ChValue':
18
+ case 'RemValue':
19
+ case 'VhValue':
20
+ case 'SvhValue':
21
+ case 'LvhValue':
22
+ case 'DvhValue':
23
+ case 'VwValue':
24
+ case 'SvwValue':
25
+ case 'LvwValue':
26
+ case 'DvwValue':
27
+ case 'VminValue':
28
+ case 'SvminValue':
29
+ case 'LvminValue':
30
+ case 'DvminValue':
31
+ case 'VmaxValue':
32
+ case 'SvmaxValue':
33
+ case 'LvmaxValue':
34
+ case 'DvmaxValue':
35
+ case 'VbValue':
36
+ case 'SvbValue':
37
+ case 'LvbValue':
38
+ case 'DvbValue':
39
+ case 'ViValue':
40
+ case 'SviValue':
41
+ case 'LviValue':
42
+ case 'DviValue':
43
+ case 'CqwValue':
44
+ case 'CqhValue':
45
+ case 'CqiValue':
46
+ case 'CqbValue':
47
+ case 'CqminValue':
48
+ case 'CqmaxValue':
49
+ case 'PercentageValue':
50
+ case 'LhValue':
51
+ case 'RlhValue':
52
+ case 'Number':
53
+ return true;
54
+ }
55
+ return false;
56
+ }
57
+
58
+ /** @param {'-'|'+'} operator */
59
+ function flip(operator) {
60
+ return operator === '+' ? '-' : '+';
61
+ }
62
+
63
+ /**
64
+ * @param {string} operator
65
+ * @returns {operator is '+'|'-'}
66
+ */
67
+ function isAddSubOperator(operator) {
68
+ return operator === '+' || operator === '-';
69
+ }
70
+
71
+ /**
72
+ * @typedef {{preOperator: '+'|'-', node: import('../parser').CalcNode}} Collectible
73
+ */
74
+
75
+ /**
76
+ * @param {'+'|'-'} preOperator
77
+ * @param {import('../parser').CalcNode} node
78
+ * @param {Collectible[]} collected
79
+ * @param {number} precision
80
+ */
81
+ function collectAddSubItems(preOperator, node, collected, precision) {
82
+ if (!isAddSubOperator(preOperator)) {
83
+ throw new Error(`invalid operator ${preOperator}`);
84
+ }
85
+ if (isValueType(node)) {
86
+ const itemIndex = collected.findIndex((x) => x.node.type === node.type);
87
+ if (itemIndex >= 0) {
88
+ if (node.value === 0) {
89
+ return;
90
+ }
91
+ // can cast because of the criterion used to find itemIndex
92
+ const otherValueNode = /** @type import('../parser').ValueExpression*/ (
93
+ collected[itemIndex].node
94
+ );
95
+ const { left: reducedNode, right: current } = convertNodesUnits(
96
+ otherValueNode,
97
+ node,
98
+ precision
99
+ );
100
+
101
+ if (collected[itemIndex].preOperator === '-') {
102
+ collected[itemIndex].preOperator = '+';
103
+ reducedNode.value *= -1;
104
+ }
105
+ if (preOperator === '+') {
106
+ reducedNode.value += current.value;
107
+ } else {
108
+ reducedNode.value -= current.value;
109
+ }
110
+ // make sure reducedNode.value >= 0
111
+ if (reducedNode.value >= 0) {
112
+ collected[itemIndex] = { node: reducedNode, preOperator: '+' };
113
+ } else {
114
+ reducedNode.value *= -1;
115
+ collected[itemIndex] = { node: reducedNode, preOperator: '-' };
116
+ }
117
+ } else {
118
+ // make sure node.value >= 0
119
+ if (node.value >= 0) {
120
+ collected.push({ node, preOperator });
121
+ } else {
122
+ node.value *= -1;
123
+ collected.push({ node, preOperator: flip(preOperator) });
124
+ }
125
+ }
126
+ } else if (node.type === 'MathExpression') {
127
+ if (isAddSubOperator(node.operator)) {
128
+ collectAddSubItems(preOperator, node.left, collected, precision);
129
+ const collectRightOperator =
130
+ preOperator === '-' ? flip(node.operator) : node.operator;
131
+ collectAddSubItems(
132
+ collectRightOperator,
133
+ node.right,
134
+ collected,
135
+ precision
136
+ );
137
+ } else {
138
+ // * or /
139
+ const reducedNode = reduce(node, precision);
140
+ // prevent infinite recursive call
141
+ if (
142
+ reducedNode.type !== 'MathExpression' ||
143
+ isAddSubOperator(reducedNode.operator)
144
+ ) {
145
+ collectAddSubItems(preOperator, reducedNode, collected, precision);
146
+ } else {
147
+ collected.push({ node: reducedNode, preOperator });
148
+ }
149
+ }
150
+ } else if (node.type === 'ParenthesizedExpression') {
151
+ collectAddSubItems(preOperator, node.content, collected, precision);
152
+ } else {
153
+ collected.push({ node, preOperator });
154
+ }
155
+ }
156
+
157
+ /**
158
+ * @param {import('../parser').CalcNode} node
159
+ * @param {number} precision
160
+ */
161
+ function reduceAddSubExpression(node, precision) {
162
+ /** @type Collectible[] */
163
+ const collected = [];
164
+ collectAddSubItems('+', node, collected, precision);
165
+
166
+ const withoutZeroItem = collected.filter(
167
+ (item) => !(isValueType(item.node) && item.node.value === 0)
168
+ );
169
+ const firstNonZeroItem = withoutZeroItem[0]; // could be undefined
170
+
171
+ // prevent producing "calc(-var(--a))" or "calc()"
172
+ // which is invalid css
173
+ if (
174
+ !firstNonZeroItem ||
175
+ (firstNonZeroItem.preOperator === '-' &&
176
+ !isValueType(firstNonZeroItem.node))
177
+ ) {
178
+ const firstZeroItem = collected.find(
179
+ (item) => isValueType(item.node) && item.node.value === 0
180
+ );
181
+ if (firstZeroItem) {
182
+ withoutZeroItem.unshift(firstZeroItem);
183
+ }
184
+ }
185
+
186
+ // make sure the preOperator of the first item is +
187
+ if (
188
+ withoutZeroItem[0].preOperator === '-' &&
189
+ isValueType(withoutZeroItem[0].node)
190
+ ) {
191
+ withoutZeroItem[0].node.value *= -1;
192
+ withoutZeroItem[0].preOperator = '+';
193
+ }
194
+
195
+ let root = withoutZeroItem[0].node;
196
+ for (let i = 1; i < withoutZeroItem.length; i++) {
197
+ root = {
198
+ type: 'MathExpression',
199
+ operator: withoutZeroItem[i].preOperator,
200
+ left: root,
201
+ right: withoutZeroItem[i].node,
202
+ };
203
+ }
204
+
205
+ return root;
206
+ }
207
+ /**
208
+ * @param {import('../parser').MathExpression} node
209
+ */
210
+ function reduceDivisionExpression(node) {
211
+ if (!isValueType(node.right)) {
212
+ return node;
213
+ }
214
+
215
+ if (node.right.type !== 'Number') {
216
+ throw new Error(`Cannot divide by "${node.right.unit}", number expected`);
217
+ }
218
+
219
+ return applyNumberDivision(node.left, node.right.value);
220
+ }
221
+
222
+ /**
223
+ * apply (expr) / number
224
+ *
225
+ * @param {import('../parser').CalcNode} node
226
+ * @param {number} divisor
227
+ * @return {import('../parser').CalcNode}
228
+ */
229
+ function applyNumberDivision(node, divisor) {
230
+ if (divisor === 0) {
231
+ throw new Error('Cannot divide by zero');
232
+ }
233
+ if (isValueType(node)) {
234
+ node.value /= divisor;
235
+ return node;
236
+ }
237
+ if (node.type === 'MathExpression' && isAddSubOperator(node.operator)) {
238
+ // turn (a + b) / num into a/num + b/num
239
+ // is good for further reduction
240
+ // checkout the test case
241
+ // "should reduce division before reducing additions"
242
+ return {
243
+ type: 'MathExpression',
244
+ operator: node.operator,
245
+ left: applyNumberDivision(node.left, divisor),
246
+ right: applyNumberDivision(node.right, divisor),
247
+ };
248
+ }
249
+ // it is impossible to reduce it into a single value
250
+ // .e.g the node contains css variable
251
+ // so we just preserve the division and let browser do it
252
+ return {
253
+ type: 'MathExpression',
254
+ operator: '/',
255
+ left: node,
256
+ right: {
257
+ type: 'Number',
258
+ value: divisor,
259
+ },
260
+ };
261
+ }
262
+ /**
263
+ * @param {import('../parser').MathExpression} node
264
+ */
265
+ function reduceMultiplicationExpression(node) {
266
+ // (expr) * number
267
+ if (node.right.type === 'Number') {
268
+ return applyNumberMultiplication(node.left, node.right.value);
269
+ }
270
+ // number * (expr)
271
+ if (node.left.type === 'Number') {
272
+ return applyNumberMultiplication(node.right, node.left.value);
273
+ }
274
+ return node;
275
+ }
276
+
277
+ /**
278
+ * apply (expr) * number
279
+ * @param {number} multiplier
280
+ * @param {import('../parser').CalcNode} node
281
+ * @return {import('../parser').CalcNode}
282
+ */
283
+ function applyNumberMultiplication(node, multiplier) {
284
+ if (isValueType(node)) {
285
+ node.value *= multiplier;
286
+ return node;
287
+ }
288
+ if (node.type === 'MathExpression' && isAddSubOperator(node.operator)) {
289
+ // turn (a + b) * num into a*num + b*num
290
+ // is good for further reduction
291
+ // checkout the test case
292
+ // "should reduce multiplication before reducing additions"
293
+ return {
294
+ type: 'MathExpression',
295
+ operator: node.operator,
296
+ left: applyNumberMultiplication(node.left, multiplier),
297
+ right: applyNumberMultiplication(node.right, multiplier),
298
+ };
299
+ }
300
+ // it is impossible to reduce it into a single value
301
+ // .e.g the node contains css variable
302
+ // so we just preserve the division and let browser do it
303
+ return {
304
+ type: 'MathExpression',
305
+ operator: '*',
306
+ left: node,
307
+ right: {
308
+ type: 'Number',
309
+ value: multiplier,
310
+ },
311
+ };
312
+ }
313
+
314
+ /**
315
+ * @param {import('../parser').ValueExpression} left
316
+ * @param {import('../parser').ValueExpression} right
317
+ * @param {number} precision
318
+ */
319
+ function convertNodesUnits(left, right, precision) {
320
+ switch (left.type) {
321
+ case 'LengthValue':
322
+ case 'AngleValue':
323
+ case 'TimeValue':
324
+ case 'FrequencyValue':
325
+ case 'ResolutionValue':
326
+ if (right.type === left.type && right.unit && left.unit) {
327
+ const converted = convertUnit(
328
+ right.value,
329
+ right.unit,
330
+ left.unit,
331
+ precision
332
+ );
333
+
334
+ right = {
335
+ type: left.type,
336
+ value: converted,
337
+ unit: left.unit,
338
+ };
339
+ }
340
+
341
+ return { left, right };
342
+ default:
343
+ return { left, right };
344
+ }
345
+ }
346
+
347
+ /**
348
+ * @param {import('../parser').ParenthesizedExpression} node
349
+ */
350
+ function includesNoCssProperties(node) {
351
+ return (
352
+ node.content.type !== 'Function' &&
353
+ (node.content.type !== 'MathExpression' ||
354
+ (node.content.right.type !== 'Function' &&
355
+ node.content.left.type !== 'Function'))
356
+ );
357
+ }
358
+ /**
359
+ * @param {import('../parser').CalcNode} node
360
+ * @param {number} precision
361
+ * @return {import('../parser').CalcNode}
362
+ */
363
+ function reduce(node, precision) {
364
+ if (
365
+ node.type === 'MathExpression' &&
366
+ (node.left.type === 'CalcKeyword' || node.right.type === 'CalcKeyword')
367
+ ) {
368
+ return node;
369
+ }
370
+ if (node.type === 'MathExpression') {
371
+ if (isAddSubOperator(node.operator)) {
372
+ // reduceAddSubExpression will call reduce recursively
373
+ return reduceAddSubExpression(node, precision);
374
+ }
375
+ node.left = reduce(node.left, precision);
376
+ node.right = reduce(node.right, precision);
377
+ switch (node.operator) {
378
+ case '/':
379
+ return reduceDivisionExpression(node);
380
+ case '*':
381
+ return reduceMultiplicationExpression(node);
382
+ }
383
+
384
+ return node;
385
+ }
386
+
387
+ if (node.type === 'ParenthesizedExpression') {
388
+ if (includesNoCssProperties(node)) {
389
+ return reduce(node.content, precision);
390
+ }
391
+ }
392
+
393
+ return node;
394
+ }
395
+
396
+ module.exports = reduce;
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+ const order = {
3
+ '*': 0,
4
+ '/': 0,
5
+ '+': 1,
6
+ '-': 1,
7
+ };
8
+
9
+ /**
10
+ * @param {number} value
11
+ * @param {number | false} prec
12
+ */
13
+ function round(value, prec) {
14
+ if (prec !== false) {
15
+ const precision = Math.pow(10, prec);
16
+ return Math.round(value * precision) / precision;
17
+ }
18
+ return value;
19
+ }
20
+
21
+ /**
22
+ * @param {number | false} prec
23
+ * @param {import('../parser').CalcNode} node
24
+ *
25
+ * @return {string}
26
+ */
27
+ function stringify(node, prec) {
28
+ switch (node.type) {
29
+ case 'MathExpression': {
30
+ const { left, right, operator: op } = node;
31
+ let str = '';
32
+ if (left.type === 'MathExpression' && order[op] < order[left.operator]) {
33
+ str += `(${stringify(left, prec)})`;
34
+ } else if (left.type === 'CalcKeyword') {
35
+ str += left.value;
36
+ } else {
37
+ str += stringify(left, prec);
38
+ }
39
+
40
+ str += order[op] ? ` ${node.operator} ` : node.operator;
41
+
42
+ if (
43
+ right.type === 'MathExpression' &&
44
+ order[op] < order[right.operator]
45
+ ) {
46
+ str += `(${stringify(right, prec)})`;
47
+ } else if (right.type === 'CalcKeyword') {
48
+ str += right.value;
49
+ } else {
50
+ str += stringify(right, prec);
51
+ }
52
+
53
+ return str;
54
+ }
55
+ case 'Number':
56
+ return round(node.value, prec).toString();
57
+ case 'Function':
58
+ return node.value.toString();
59
+ case 'ParenthesizedExpression':
60
+ return `(${stringify(node.content, prec)})`;
61
+ case 'CalcKeyword':
62
+ return node.value;
63
+ default:
64
+ return round(node.value, prec) + node.unit;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * @param {string} calc
70
+ * @param {import('../parser').CalcNode} node
71
+ * @param {string} originalValue
72
+ * @param {{precision: number | false, warnWhenCannotResolve: boolean}} options
73
+ * @param {import("postcss").Result} result
74
+ * @param {import("postcss").ChildNode} item
75
+ *
76
+ * @returns {string}
77
+ */
78
+ module.exports = function (calc, node, originalValue, options, result, item) {
79
+ let str = stringify(node, options.precision);
80
+
81
+ const shouldPrintCalc =
82
+ node.type === 'MathExpression' ||
83
+ node.type === 'Function' ||
84
+ node.type === 'ParenthesizedExpression' ||
85
+ node.type === 'CalcKeyword';
86
+
87
+ if (shouldPrintCalc) {
88
+ // if calc expression couldn't be resolved to a single value, re-wrap it as
89
+ // a calc()
90
+ if (node.type === 'ParenthesizedExpression') {
91
+ str = `${calc}${str}`;
92
+ } else {
93
+ str = `${calc}(${str})`;
94
+ }
95
+
96
+ // if the warnWhenCannotResolve option is on, inform the user that the calc
97
+ // expression could not be resolved to a single value
98
+ if (options.warnWhenCannotResolve) {
99
+ result.warn('Could not reduce expression: ' + originalValue, {
100
+ plugin: 'postcss-calc',
101
+ node: item,
102
+ });
103
+ }
104
+ }
105
+ return str;
106
+ };
@@ -0,0 +1,109 @@
1
+ 'use strict';
2
+ const selectorParser = require('postcss-selector-parser');
3
+ const valueParser = require('postcss-value-parser');
4
+
5
+ const { parser } = require('../parser.js');
6
+
7
+ const reducer = require('./reducer.js');
8
+ const stringifier = require('./stringifier.js');
9
+
10
+ const MATCH_CALC = /((?:-(moz|webkit)-)?calc(?!-))/i;
11
+
12
+ /**
13
+ * @param {string} value
14
+ * @param {{precision: number, warnWhenCannotResolve: boolean}} options
15
+ * @param {import("postcss").Result} result
16
+ * @param {import("postcss").ChildNode} item
17
+ */
18
+ function transformValue(value, options, result, item) {
19
+ return valueParser(value)
20
+ .walk((node) => {
21
+ // skip anything which isn't a calc() function
22
+ if (node.type !== 'function' || !MATCH_CALC.test(node.value)) {
23
+ return;
24
+ }
25
+
26
+ // stringify calc expression and produce an AST
27
+ const contents = valueParser.stringify(node.nodes);
28
+ const ast = parser.parse(contents);
29
+
30
+ // reduce AST to its simplest form, that is, either to a single value
31
+ // or a simplified calc expression
32
+ const reducedAst = reducer(ast, options.precision);
33
+
34
+ // stringify AST and write it back
35
+ /** @type {valueParser.Node} */ (node).type = 'word';
36
+ node.value = stringifier(
37
+ node.value,
38
+ reducedAst,
39
+ value,
40
+ options,
41
+ result,
42
+ item
43
+ );
44
+
45
+ return false;
46
+ })
47
+ .toString();
48
+ }
49
+ /**
50
+ * @param {import("postcss-selector-parser").Selectors} value
51
+ * @param {{precision: number, warnWhenCannotResolve: boolean}} options
52
+ * @param {import("postcss").Result} result
53
+ * @param {import("postcss").ChildNode} item
54
+ */
55
+ function transformSelector(value, options, result, item) {
56
+ return selectorParser((selectors) => {
57
+ selectors.walk((node) => {
58
+ // attribute value
59
+ // e.g. the "calc(3*3)" part of "div[data-size="calc(3*3)"]"
60
+ if (node.type === 'attribute' && node.value) {
61
+ node.setValue(transformValue(node.value, options, result, item));
62
+ }
63
+
64
+ // tag value
65
+ // e.g. the "calc(3*3)" part of "div:nth-child(2n + calc(3*3))"
66
+ if (node.type === 'tag') {
67
+ node.value = transformValue(node.value, options, result, item);
68
+ }
69
+
70
+ return;
71
+ });
72
+ }).processSync(value);
73
+ }
74
+
75
+ /**
76
+ * @param {any} node
77
+ * @param {{precision: number, preserve: boolean, warnWhenCannotResolve: boolean}} options
78
+ * @param {'value'|'params'|'selector'} property
79
+ * @param {import("postcss").Result} result
80
+ */
81
+ module.exports = (node, property, options, result) => {
82
+ let value = node[property];
83
+
84
+ try {
85
+ value =
86
+ property === 'selector'
87
+ ? transformSelector(node[property], options, result, node)
88
+ : transformValue(node[property], options, result, node);
89
+ } catch (error) {
90
+ if (error instanceof Error) {
91
+ result.warn(error.message, { node });
92
+ } else {
93
+ result.warn('Error', { node });
94
+ }
95
+ return;
96
+ }
97
+
98
+ // if the preserve option is enabled and the value has changed, write the
99
+ // transformed value into a cloned node which is inserted before the current
100
+ // node, preserving the original value. Otherwise, overwrite the original
101
+ // value.
102
+ if (options.preserve && node[property] !== value) {
103
+ const clone = node.clone();
104
+ clone[property] = value;
105
+ node.parent.insertBefore(node, clone);
106
+ } else {
107
+ node[property] = value;
108
+ }
109
+ };
@@ -0,0 +1,89 @@
1
+ export interface MathExpression {
2
+ type: 'MathExpression';
3
+ right: CalcNode;
4
+ left: CalcNode;
5
+ operator: '*' | '+' | '-' | '/';
6
+ }
7
+
8
+ export interface ParenthesizedExpression {
9
+ type: 'ParenthesizedExpression';
10
+ content: CalcNode;
11
+ }
12
+
13
+ export interface DimensionExpression {
14
+ type:
15
+ | 'LengthValue'
16
+ | 'AngleValue'
17
+ | 'TimeValue'
18
+ | 'FrequencyValue'
19
+ | 'PercentageValue'
20
+ | 'ResolutionValue'
21
+ | 'EmValue'
22
+ | 'ExValue'
23
+ | 'ChValue'
24
+ | 'RemValue'
25
+ | 'VhValue'
26
+ | 'SvhValue'
27
+ | 'LvhValue'
28
+ | 'DvhValue'
29
+ | 'VwValue'
30
+ | 'SvwValue'
31
+ | 'LvwValue'
32
+ | 'DvwValue'
33
+ | 'VminValue'
34
+ | 'SvminValue'
35
+ | 'LvminValue'
36
+ | 'DvminValue'
37
+ | 'VmaxValue'
38
+ | 'SvmaxValue'
39
+ | 'LvmaxValue'
40
+ | 'DvmaxValue'
41
+ | 'VbValue'
42
+ | 'SvbValue'
43
+ | 'LvbValue'
44
+ | 'DvbValue'
45
+ | 'ViValue'
46
+ | 'SviValue'
47
+ | 'LviValue'
48
+ | 'DviValue'
49
+ | 'CqwValue'
50
+ | 'CqhValue'
51
+ | 'CqbValue'
52
+ | 'CqiValue'
53
+ | 'CqminValue'
54
+ | 'CqmaxValue'
55
+ | 'LhValue'
56
+ | 'RlhValue';
57
+ value: number;
58
+ unit: string;
59
+ }
60
+
61
+ export interface NumberExpression {
62
+ type: 'Number';
63
+ value: number;
64
+ }
65
+
66
+ export interface FunctionExpression {
67
+ type: 'Function';
68
+ value: string;
69
+ }
70
+
71
+ export interface CalcKeywordExpression {
72
+ type: 'CalcKeyword';
73
+ value: string;
74
+ }
75
+
76
+ export type ValueExpression = DimensionExpression | NumberExpression;
77
+
78
+ export type CalcNode =
79
+ | MathExpression
80
+ | ValueExpression
81
+ | FunctionExpression
82
+ | ParenthesizedExpression
83
+ | CalcKeywordExpression;
84
+
85
+ export interface Parser {
86
+ parse: (arg: string) => CalcNode;
87
+ }
88
+
89
+ export const parser: Parser;