UNPKG

postcss-calc

Version:
191 lines (175 loc) 5.08 kB
// Canonical AST. N-ary Sum and Product with signed numeric leaves. // Invariants enforced by the constructors below: // // - Num/Dim values may be any finite number (negatives allowed); a `-5` // is `Num(-5)`, never `Sum([{sign:-1, Num(5)}])`. One form per value. // - In a SumTerm with Num/Dim node, sign is always +1; the sign slot is // reserved for opaque nodes (Ident, Call, Product, multi-term Sum). // - No ungrouped Sum directly contains another Sum (flattened on // construction). A grouped Sum is retained so a later negative sign // cannot be distributed across opaque terms. // - No Product directly contains another Product (flattened). // - A Sum/Product with one positive element collapses to that element. // - A Sum/Product with no elements collapses to Num(0) / Num(1). // - Zero-valued Nums are dropped from sums (they contribute nothing). // Zero-valued Dims are kept — the unit carries type info. /** * @typedef {{type: 'Num', value: number}} Num * @typedef {{type: 'Dim', value: number, unit: string}} Dim * @typedef {{type: 'Ident', name: string}} Ident * @typedef {{type: 'Call', name: string, args: Node[]}} Call * @typedef {{sign: 1 | -1, node: Node}} SumTerm Sign is always +1 when node is Num or Dim. * @typedef {{type: 'Sum', terms: SumTerm[], grouped?: boolean}} Sum * @typedef {{exponent: 1 | -1, node: Node}} ProductFactor exponent +1 = numerator, -1 = denominator. * @typedef {{type: 'Product', factors: ProductFactor[]}} Product * @typedef {Num | Dim | Ident | Call | Sum | Product} Node */ /** * @param {number} value * @return {Num} */ function num(value) { return { type: 'Num', value }; } /** * @param {number} value * @param {string} unit * @return {Dim} */ function dim(value, unit) { return { type: 'Dim', value, unit }; } /** * @param {string} name * @return {Ident} */ function ident(name) { return { type: 'Ident', name }; } /** * @param {string} name * @param {Node[]} args * @return {Call} */ function call(name, args) { return { type: 'Call', name, args }; } /** * @param {SumTerm[]} rawTerms * @return {Node} */ function mkSum(rawTerms) { /** @type {SumTerm[]} */ const flat = []; for (const t of rawTerms) { pushSumTerm(flat, t); } if (flat.length === 0) { return { type: 'Num', value: 0 }; } if (flat.length === 1 && flat[0].sign === 1) { return flat[0].node; } return { type: 'Sum', terms: flat }; } /** * @param {SumTerm[]} out * @param {SumTerm} term * @return {void} */ function pushSumTerm(out, term) { let { sign, node } = term; if (node.type === 'Sum' && !node.grouped) { for (const inner of node.terms) { pushSumTerm(out, { sign: /** @type {1 | -1} */ (sign * inner.sign), node: inner.node, }); } return; } // sign=-1 around a Num/Dim leaf collapses into the value's sign — the // canonical-form rule downstream code relies on. if (sign === -1) { if (node.type === 'Num') { node = { type: 'Num', value: -node.value }; sign = 1; } else if (node.type === 'Dim') { node = { type: 'Dim', value: -node.value, unit: node.unit }; sign = 1; } } // Drop zero-valued Nums. Dims with value 0 stay — the unit carries type. if (node.type === 'Num' && node.value === 0) { return; } out.push({ sign, node }); } /** * @param {ProductFactor[]} rawFactors * @return {Node} */ function mkProduct(rawFactors) { /** @type {ProductFactor[]} */ const flat = []; for (const f of rawFactors) { pushProductFactor(flat, f); } if (flat.length === 0) { return { type: 'Num', value: 1 }; } if (flat.length === 1 && flat[0].exponent === 1) { return flat[0].node; } return { type: 'Product', factors: flat }; } /** * @param {ProductFactor[]} out * @param {ProductFactor} f * @return {void} */ function pushProductFactor(out, f) { const n = f.node; if (n.type === 'Product') { for (const inner of n.factors) { out.push({ exponent: /** @type {1 | -1} */ (f.exponent * inner.exponent), node: inner.node, }); } return; } // Factor of 1 contributes nothing regardless of exponent (1/1 = 1). if (n.type === 'Num' && n.value === 1) { return; } out.push(f); } /** * Negate any node, preserving canonical form. * @param {Node} node * @return {Node} */ function negate(node) { if (node.type === 'Num') { return num(-node.value); } if (node.type === 'Dim') { return dim(-node.value, node.unit); } if (node.type === 'Sum') { const result = mkSum( node.terms.map((t) => ({ sign: /** @type {1 | -1} */ (-t.sign), node: t.node, })) ); return node.grouped && result.type === 'Sum' ? { ...result, grouped: true } : result; } // Opaque (Ident, Call, Product): wrap as a single negative-sign term — // the only case where sign=-1 remains on a SumTerm. return { type: 'Sum', terms: [{ sign: -1, node }] }; } export { num, dim, ident, call, mkSum, mkProduct, negate };