diff options
Diffstat (limited to 'node_modules/postcss-calc/src/lib/reducer.js')
| -rw-r--r-- | node_modules/postcss-calc/src/lib/reducer.js | 362 |
1 files changed, 362 insertions, 0 deletions
diff --git a/node_modules/postcss-calc/src/lib/reducer.js b/node_modules/postcss-calc/src/lib/reducer.js new file mode 100644 index 00000000..8a36bf13 --- /dev/null +++ b/node_modules/postcss-calc/src/lib/reducer.js @@ -0,0 +1,362 @@ +'use strict'; +const convertUnit = require('./convertUnit.js'); + +/** + * @param {import('../parser').CalcNode} node + * @return {node is import('../parser').ValueExpression} + */ +function isValueType(node) { + switch (node.type) { + case 'LengthValue': + case 'AngleValue': + case 'TimeValue': + case 'FrequencyValue': + case 'ResolutionValue': + case 'EmValue': + case 'ExValue': + case 'ChValue': + case 'RemValue': + case 'VhValue': + case 'VwValue': + case 'VminValue': + case 'VmaxValue': + case 'PercentageValue': + case 'Number': + return true; + } + return false; +} + +/** @param {'-'|'+'} operator */ +function flip(operator) { + return operator === '+' ? '-' : '+'; +} + +/** + * @param {string} operator + * @returns {operator is '+'|'-'} + */ +function isAddSubOperator(operator) { + return operator === '+' || operator === '-'; +} + +/** + * @typedef {{preOperator: '+'|'-', node: import('../parser').CalcNode}} Collectible + */ + +/** + * @param {'+'|'-'} preOperator + * @param {import('../parser').CalcNode} node + * @param {Collectible[]} collected + * @param {number} precision + */ +function collectAddSubItems(preOperator, node, collected, precision) { + if (!isAddSubOperator(preOperator)) { + throw new Error(`invalid operator ${preOperator}`); + } + if (isValueType(node)) { + const itemIndex = collected.findIndex((x) => x.node.type === node.type); + if (itemIndex >= 0) { + if (node.value === 0) { + return; + } + // can cast because of the criterion used to find itemIndex + const otherValueNode = /** @type import('../parser').ValueExpression*/ ( + collected[itemIndex].node + ); + const { left: reducedNode, right: current } = convertNodesUnits( + otherValueNode, + node, + precision + ); + + if (collected[itemIndex].preOperator === '-') { + collected[itemIndex].preOperator = '+'; + reducedNode.value *= -1; + } + if (preOperator === '+') { + reducedNode.value += current.value; + } else { + reducedNode.value -= current.value; + } + // make sure reducedNode.value >= 0 + if (reducedNode.value >= 0) { + collected[itemIndex] = { node: reducedNode, preOperator: '+' }; + } else { + reducedNode.value *= -1; + collected[itemIndex] = { node: reducedNode, preOperator: '-' }; + } + } else { + // make sure node.value >= 0 + if (node.value >= 0) { + collected.push({ node, preOperator }); + } else { + node.value *= -1; + collected.push({ node, preOperator: flip(preOperator) }); + } + } + } else if (node.type === 'MathExpression') { + if (isAddSubOperator(node.operator)) { + collectAddSubItems(preOperator, node.left, collected, precision); + const collectRightOperator = + preOperator === '-' ? flip(node.operator) : node.operator; + collectAddSubItems( + collectRightOperator, + node.right, + collected, + precision + ); + } else { + // * or / + const reducedNode = reduce(node, precision); + // prevent infinite recursive call + if ( + reducedNode.type !== 'MathExpression' || + isAddSubOperator(reducedNode.operator) + ) { + collectAddSubItems(preOperator, reducedNode, collected, precision); + } else { + collected.push({ node: reducedNode, preOperator }); + } + } + } else if (node.type === 'ParenthesizedExpression') { + collectAddSubItems(preOperator, node.content, collected, precision); + } else { + collected.push({ node, preOperator }); + } +} + +/** + * @param {import('../parser').CalcNode} node + * @param {number} precision + */ +function reduceAddSubExpression(node, precision) { + /** @type Collectible[] */ + const collected = []; + collectAddSubItems('+', node, collected, precision); + + const withoutZeroItem = collected.filter( + (item) => !(isValueType(item.node) && item.node.value === 0) + ); + const firstNonZeroItem = withoutZeroItem[0]; // could be undefined + + // prevent producing "calc(-var(--a))" or "calc()" + // which is invalid css + if ( + !firstNonZeroItem || + (firstNonZeroItem.preOperator === '-' && + !isValueType(firstNonZeroItem.node)) + ) { + const firstZeroItem = collected.find( + (item) => isValueType(item.node) && item.node.value === 0 + ); + if (firstZeroItem) { + withoutZeroItem.unshift(firstZeroItem); + } + } + + // make sure the preOperator of the first item is + + if ( + withoutZeroItem[0].preOperator === '-' && + isValueType(withoutZeroItem[0].node) + ) { + withoutZeroItem[0].node.value *= -1; + withoutZeroItem[0].preOperator = '+'; + } + + let root = withoutZeroItem[0].node; + for (let i = 1; i < withoutZeroItem.length; i++) { + root = { + type: 'MathExpression', + operator: withoutZeroItem[i].preOperator, + left: root, + right: withoutZeroItem[i].node, + }; + } + + return root; +} +/** + * @param {import('../parser').MathExpression} node + */ +function reduceDivisionExpression(node) { + if (!isValueType(node.right)) { + return node; + } + + if (node.right.type !== 'Number') { + throw new Error(`Cannot divide by "${node.right.unit}", number expected`); + } + + return applyNumberDivision(node.left, node.right.value); +} + +/** + * apply (expr) / number + * + * @param {import('../parser').CalcNode} node + * @param {number} divisor + * @return {import('../parser').CalcNode} + */ +function applyNumberDivision(node, divisor) { + if (divisor === 0) { + throw new Error('Cannot divide by zero'); + } + if (isValueType(node)) { + node.value /= divisor; + return node; + } + if (node.type === 'MathExpression' && isAddSubOperator(node.operator)) { + // turn (a + b) / num into a/num + b/num + // is good for further reduction + // checkout the test case + // "should reduce division before reducing additions" + return { + type: 'MathExpression', + operator: node.operator, + left: applyNumberDivision(node.left, divisor), + right: applyNumberDivision(node.right, divisor), + }; + } + // it is impossible to reduce it into a single value + // .e.g the node contains css variable + // so we just preserve the division and let browser do it + return { + type: 'MathExpression', + operator: '/', + left: node, + right: { + type: 'Number', + value: divisor, + }, + }; +} +/** + * @param {import('../parser').MathExpression} node + */ +function reduceMultiplicationExpression(node) { + // (expr) * number + if (node.right.type === 'Number') { + return applyNumberMultiplication(node.left, node.right.value); + } + // number * (expr) + if (node.left.type === 'Number') { + return applyNumberMultiplication(node.right, node.left.value); + } + return node; +} + +/** + * apply (expr) * number + * @param {number} multiplier + * @param {import('../parser').CalcNode} node + * @return {import('../parser').CalcNode} + */ +function applyNumberMultiplication(node, multiplier) { + if (isValueType(node)) { + node.value *= multiplier; + return node; + } + if (node.type === 'MathExpression' && isAddSubOperator(node.operator)) { + // turn (a + b) * num into a*num + b*num + // is good for further reduction + // checkout the test case + // "should reduce multiplication before reducing additions" + return { + type: 'MathExpression', + operator: node.operator, + left: applyNumberMultiplication(node.left, multiplier), + right: applyNumberMultiplication(node.right, multiplier), + }; + } + // it is impossible to reduce it into a single value + // .e.g the node contains css variable + // so we just preserve the division and let browser do it + return { + type: 'MathExpression', + operator: '*', + left: node, + right: { + type: 'Number', + value: multiplier, + }, + }; +} + +/** + * @param {import('../parser').ValueExpression} left + * @param {import('../parser').ValueExpression} right + * @param {number} precision + */ +function convertNodesUnits(left, right, precision) { + switch (left.type) { + case 'LengthValue': + case 'AngleValue': + case 'TimeValue': + case 'FrequencyValue': + case 'ResolutionValue': + if (right.type === left.type && right.unit && left.unit) { + const converted = convertUnit( + right.value, + right.unit, + left.unit, + precision + ); + + right = { + type: left.type, + value: converted, + unit: left.unit, + }; + } + + return { left, right }; + default: + return { left, right }; + } +} + +/** + * @param {import('../parser').ParenthesizedExpression} node + */ +function includesNoCssProperties(node) { + return ( + node.content.type !== 'Function' && + (node.content.type !== 'MathExpression' || + (node.content.right.type !== 'Function' && + node.content.left.type !== 'Function')) + ); +} +/** + * @param {import('../parser').CalcNode} node + * @param {number} precision + * @return {import('../parser').CalcNode} + */ +function reduce(node, precision) { + if (node.type === 'MathExpression') { + if (isAddSubOperator(node.operator)) { + // reduceAddSubExpression will call reduce recursively + return reduceAddSubExpression(node, precision); + } + node.left = reduce(node.left, precision); + node.right = reduce(node.right, precision); + switch (node.operator) { + case '/': + return reduceDivisionExpression(node); + case '*': + return reduceMultiplicationExpression(node); + } + + return node; + } + + if (node.type === 'ParenthesizedExpression') { + if (includesNoCssProperties(node)) { + return reduce(node.content, precision); + } + } + + return node; +} + +module.exports = reduce; |
