Document application modes (development/debug/production) Add global file drop handler, order column normalization, SPA hash fix Serve CDN assets via /_vendor/ URLs instead of merging into bundles Add production minification with license preservation Improve JSON formatting for debugging and production optimization Add CDN asset caching with CSS URL inlining for production builds Add three-mode system (development, debug, production) Update Manifest CLAUDE.md to reflect helper class architecture Refactor Manifest.php into helper classes for better organization Pre-manifest-refactor checkpoint: Add app_mode documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
397 lines
10 KiB
JavaScript
Executable File
397 lines
10 KiB
JavaScript
Executable File
'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 'SvhValue':
|
|
case 'LvhValue':
|
|
case 'DvhValue':
|
|
case 'VwValue':
|
|
case 'SvwValue':
|
|
case 'LvwValue':
|
|
case 'DvwValue':
|
|
case 'VminValue':
|
|
case 'SvminValue':
|
|
case 'LvminValue':
|
|
case 'DvminValue':
|
|
case 'VmaxValue':
|
|
case 'SvmaxValue':
|
|
case 'LvmaxValue':
|
|
case 'DvmaxValue':
|
|
case 'VbValue':
|
|
case 'SvbValue':
|
|
case 'LvbValue':
|
|
case 'DvbValue':
|
|
case 'ViValue':
|
|
case 'SviValue':
|
|
case 'LviValue':
|
|
case 'DviValue':
|
|
case 'CqwValue':
|
|
case 'CqhValue':
|
|
case 'CqiValue':
|
|
case 'CqbValue':
|
|
case 'CqminValue':
|
|
case 'CqmaxValue':
|
|
case 'PercentageValue':
|
|
case 'LhValue':
|
|
case 'RlhValue':
|
|
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' &&
|
|
(node.left.type === 'CalcKeyword' || node.right.type === 'CalcKeyword')
|
|
) {
|
|
return node;
|
|
}
|
|
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;
|