source: frontend/node_modules/postcss-calc/src/lib/reducer.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 13 days ago

Fix frontend appearance

  • Property mode set to 100644
File size: 9.4 KB
RevLine 
[9af201e]1'use strict';
2const convertUnit = require('./convertUnit.js');
3
4/**
5 * @param {import('../parser').CalcNode} node
6 * @return {node is import('../parser').ValueExpression}
7 */
8function 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 'VwValue':
21 case 'VminValue':
22 case 'VmaxValue':
23 case 'PercentageValue':
24 case 'Number':
25 return true;
26 }
27 return false;
28}
29
30/** @param {'-'|'+'} operator */
31function flip(operator) {
32 return operator === '+' ? '-' : '+';
33}
34
35/**
36 * @param {string} operator
37 * @returns {operator is '+'|'-'}
38 */
39function isAddSubOperator(operator) {
40 return operator === '+' || operator === '-';
41}
42
43/**
44 * @typedef {{preOperator: '+'|'-', node: import('../parser').CalcNode}} Collectible
45 */
46
47/**
48 * @param {'+'|'-'} preOperator
49 * @param {import('../parser').CalcNode} node
50 * @param {Collectible[]} collected
51 * @param {number} precision
52 */
53function collectAddSubItems(preOperator, node, collected, precision) {
54 if (!isAddSubOperator(preOperator)) {
55 throw new Error(`invalid operator ${preOperator}`);
56 }
57 if (isValueType(node)) {
58 const itemIndex = collected.findIndex((x) => x.node.type === node.type);
59 if (itemIndex >= 0) {
60 if (node.value === 0) {
61 return;
62 }
63 // can cast because of the criterion used to find itemIndex
64 const otherValueNode = /** @type import('../parser').ValueExpression*/ (
65 collected[itemIndex].node
66 );
67 const { left: reducedNode, right: current } = convertNodesUnits(
68 otherValueNode,
69 node,
70 precision
71 );
72
73 if (collected[itemIndex].preOperator === '-') {
74 collected[itemIndex].preOperator = '+';
75 reducedNode.value *= -1;
76 }
77 if (preOperator === '+') {
78 reducedNode.value += current.value;
79 } else {
80 reducedNode.value -= current.value;
81 }
82 // make sure reducedNode.value >= 0
83 if (reducedNode.value >= 0) {
84 collected[itemIndex] = { node: reducedNode, preOperator: '+' };
85 } else {
86 reducedNode.value *= -1;
87 collected[itemIndex] = { node: reducedNode, preOperator: '-' };
88 }
89 } else {
90 // make sure node.value >= 0
91 if (node.value >= 0) {
92 collected.push({ node, preOperator });
93 } else {
94 node.value *= -1;
95 collected.push({ node, preOperator: flip(preOperator) });
96 }
97 }
98 } else if (node.type === 'MathExpression') {
99 if (isAddSubOperator(node.operator)) {
100 collectAddSubItems(preOperator, node.left, collected, precision);
101 const collectRightOperator =
102 preOperator === '-' ? flip(node.operator) : node.operator;
103 collectAddSubItems(
104 collectRightOperator,
105 node.right,
106 collected,
107 precision
108 );
109 } else {
110 // * or /
111 const reducedNode = reduce(node, precision);
112 // prevent infinite recursive call
113 if (
114 reducedNode.type !== 'MathExpression' ||
115 isAddSubOperator(reducedNode.operator)
116 ) {
117 collectAddSubItems(preOperator, reducedNode, collected, precision);
118 } else {
119 collected.push({ node: reducedNode, preOperator });
120 }
121 }
122 } else if (node.type === 'ParenthesizedExpression') {
123 collectAddSubItems(preOperator, node.content, collected, precision);
124 } else {
125 collected.push({ node, preOperator });
126 }
127}
128
129/**
130 * @param {import('../parser').CalcNode} node
131 * @param {number} precision
132 */
133function reduceAddSubExpression(node, precision) {
134 /** @type Collectible[] */
135 const collected = [];
136 collectAddSubItems('+', node, collected, precision);
137
138 const withoutZeroItem = collected.filter(
139 (item) => !(isValueType(item.node) && item.node.value === 0)
140 );
141 const firstNonZeroItem = withoutZeroItem[0]; // could be undefined
142
143 // prevent producing "calc(-var(--a))" or "calc()"
144 // which is invalid css
145 if (
146 !firstNonZeroItem ||
147 (firstNonZeroItem.preOperator === '-' &&
148 !isValueType(firstNonZeroItem.node))
149 ) {
150 const firstZeroItem = collected.find(
151 (item) => isValueType(item.node) && item.node.value === 0
152 );
153 if (firstZeroItem) {
154 withoutZeroItem.unshift(firstZeroItem);
155 }
156 }
157
158 // make sure the preOperator of the first item is +
159 if (
160 withoutZeroItem[0].preOperator === '-' &&
161 isValueType(withoutZeroItem[0].node)
162 ) {
163 withoutZeroItem[0].node.value *= -1;
164 withoutZeroItem[0].preOperator = '+';
165 }
166
167 let root = withoutZeroItem[0].node;
168 for (let i = 1; i < withoutZeroItem.length; i++) {
169 root = {
170 type: 'MathExpression',
171 operator: withoutZeroItem[i].preOperator,
172 left: root,
173 right: withoutZeroItem[i].node,
174 };
175 }
176
177 return root;
178}
179/**
180 * @param {import('../parser').MathExpression} node
181 */
182function reduceDivisionExpression(node) {
183 if (!isValueType(node.right)) {
184 return node;
185 }
186
187 if (node.right.type !== 'Number') {
188 throw new Error(`Cannot divide by "${node.right.unit}", number expected`);
189 }
190
191 return applyNumberDivision(node.left, node.right.value);
192}
193
194/**
195 * apply (expr) / number
196 *
197 * @param {import('../parser').CalcNode} node
198 * @param {number} divisor
199 * @return {import('../parser').CalcNode}
200 */
201function applyNumberDivision(node, divisor) {
202 if (divisor === 0) {
203 throw new Error('Cannot divide by zero');
204 }
205 if (isValueType(node)) {
206 node.value /= divisor;
207 return node;
208 }
209 if (node.type === 'MathExpression' && isAddSubOperator(node.operator)) {
210 // turn (a + b) / num into a/num + b/num
211 // is good for further reduction
212 // checkout the test case
213 // "should reduce division before reducing additions"
214 return {
215 type: 'MathExpression',
216 operator: node.operator,
217 left: applyNumberDivision(node.left, divisor),
218 right: applyNumberDivision(node.right, divisor),
219 };
220 }
221 // it is impossible to reduce it into a single value
222 // .e.g the node contains css variable
223 // so we just preserve the division and let browser do it
224 return {
225 type: 'MathExpression',
226 operator: '/',
227 left: node,
228 right: {
229 type: 'Number',
230 value: divisor,
231 },
232 };
233}
234/**
235 * @param {import('../parser').MathExpression} node
236 */
237function reduceMultiplicationExpression(node) {
238 // (expr) * number
239 if (node.right.type === 'Number') {
240 return applyNumberMultiplication(node.left, node.right.value);
241 }
242 // number * (expr)
243 if (node.left.type === 'Number') {
244 return applyNumberMultiplication(node.right, node.left.value);
245 }
246 return node;
247}
248
249/**
250 * apply (expr) * number
251 * @param {number} multiplier
252 * @param {import('../parser').CalcNode} node
253 * @return {import('../parser').CalcNode}
254 */
255function applyNumberMultiplication(node, multiplier) {
256 if (isValueType(node)) {
257 node.value *= multiplier;
258 return node;
259 }
260 if (node.type === 'MathExpression' && isAddSubOperator(node.operator)) {
261 // turn (a + b) * num into a*num + b*num
262 // is good for further reduction
263 // checkout the test case
264 // "should reduce multiplication before reducing additions"
265 return {
266 type: 'MathExpression',
267 operator: node.operator,
268 left: applyNumberMultiplication(node.left, multiplier),
269 right: applyNumberMultiplication(node.right, multiplier),
270 };
271 }
272 // it is impossible to reduce it into a single value
273 // .e.g the node contains css variable
274 // so we just preserve the division and let browser do it
275 return {
276 type: 'MathExpression',
277 operator: '*',
278 left: node,
279 right: {
280 type: 'Number',
281 value: multiplier,
282 },
283 };
284}
285
286/**
287 * @param {import('../parser').ValueExpression} left
288 * @param {import('../parser').ValueExpression} right
289 * @param {number} precision
290 */
291function convertNodesUnits(left, right, precision) {
292 switch (left.type) {
293 case 'LengthValue':
294 case 'AngleValue':
295 case 'TimeValue':
296 case 'FrequencyValue':
297 case 'ResolutionValue':
298 if (right.type === left.type && right.unit && left.unit) {
299 const converted = convertUnit(
300 right.value,
301 right.unit,
302 left.unit,
303 precision
304 );
305
306 right = {
307 type: left.type,
308 value: converted,
309 unit: left.unit,
310 };
311 }
312
313 return { left, right };
314 default:
315 return { left, right };
316 }
317}
318
319/**
320 * @param {import('../parser').ParenthesizedExpression} node
321 */
322function includesNoCssProperties(node) {
323 return (
324 node.content.type !== 'Function' &&
325 (node.content.type !== 'MathExpression' ||
326 (node.content.right.type !== 'Function' &&
327 node.content.left.type !== 'Function'))
328 );
329}
330/**
331 * @param {import('../parser').CalcNode} node
332 * @param {number} precision
333 * @return {import('../parser').CalcNode}
334 */
335function reduce(node, precision) {
336 if (node.type === 'MathExpression') {
337 if (isAddSubOperator(node.operator)) {
338 // reduceAddSubExpression will call reduce recursively
339 return reduceAddSubExpression(node, precision);
340 }
341 node.left = reduce(node.left, precision);
342 node.right = reduce(node.right, precision);
343 switch (node.operator) {
344 case '/':
345 return reduceDivisionExpression(node);
346 case '*':
347 return reduceMultiplicationExpression(node);
348 }
349
350 return node;
351 }
352
353 if (node.type === 'ParenthesizedExpression') {
354 if (includesNoCssProperties(node)) {
355 return reduce(node.content, precision);
356 }
357 }
358
359 return node;
360}
361
362module.exports = reduce;
Note: See TracBrowser for help on using the repository browser.