UNPKG

postcss-calc

Version:
354 lines (332 loc) • 10.2 kB
import { mkSum, mkProduct, mkGroup, num, dim } from '../node.js'; import { tryCancelPair } from './cancel.js'; import { isExact } from './exact.js'; /** * @typedef {import('../node.js').Node} Node * @typedef {import('../node.js').Product} Product * @typedef {import('../node.js').ProductFactor} ProductFactor * @typedef {import('../simplify.js').SimplifyFn} SimplifyFn */ /** * @param {number} a non-negative * @param {number} b non-negative * @return {number} */ function gcd(a, b) { while (b !== 0) { [a, b] = [b, a % b]; } return a || 1; } /** * @param {{exponent: 1 | -1, value: number}[]} chain * @return {number} */ function chainValue(chain) { let value = 1; for (const f of chain) { value = f.exponent === 1 ? value * f.value : value / f.value; } return value; } /** * Whether the opaque factors are a single Sum of only Num/Dim terms. * @param {ProductFactor[]} opaque * @return {boolean} */ function isScalarSum(opaque) { return ( opaque.length === 1 && opaque[0].exponent === 1 && opaque[0].node.type === 'Sum' && opaque[0].node.terms.every( (t) => t.node.type === 'Num' || t.node.type === 'Dim' ) ); } /** * Whether a folded quotient prints exactly. A quotient distributed over a * scalar Sum is judged term by term, so `(3px + 6em) / 3` still folds. * @param {number} value * @param {ProductFactor[]} opaque * @param {boolean} distributable * @param {number | false} precision * @return {boolean} */ function foldsExactly(value, opaque, distributable, precision) { if (!Number.isFinite(value) || isExact(value, precision)) { return true; } if (!distributable) { return false; } const sum = /** @type {import('../node.js').Sum} */ (opaque[0].node); return sum.terms.every((t) => isExact( value * /** @type {import('../node.js').Num | import('../node.js').Dim} */ ( t.node ).value, precision ) ); } /** * Emit an inexact quotient as `numerator * dims * opaque / denominator`, with * integer parts reduced by their gcd. A lone Dim absorbs the numerator. * @param {number} numerator * @param {number} denominator * @param {{value: number, unit: string, rawUnit?: string} | null} loneDim * @param {{exponent: 1 | -1, value: number, unit: string, rawUnit?: string}[]} dims * @param {ProductFactor[]} opaque * @param {number | false} precision * @return {Node | null} null when a part would be rounded on output */ function rationalProduct( numerator, denominator, loneDim, dims, opaque, precision ) { // Drop float noise (`1.9999999999999993 * 100`) so the parts print as // written and the gcd below can reduce them. let n = Number( (loneDim === null ? numerator : numerator * loneDim.value).toPrecision(15) ); let d = Number(denominator.toPrecision(15)); if (!isExact(n, precision) || !isExact(d, precision)) { return null; } if (Number.isSafeInteger(n) && Number.isSafeInteger(d)) { const g = gcd(Math.abs(n), Math.abs(d)); n /= g; d /= g; } if (d < 0) { n = -n; d = -d; } /** @type {ProductFactor[]} */ const factors = []; if (loneDim !== null) { factors.push({ exponent: 1, node: dim(n, loneDim.unit, loneDim.rawUnit), }); } else { if (n !== 1) { factors.push({ exponent: 1, node: num(n) }); } for (const dm of dims) { factors.push({ exponent: dm.exponent, node: dim(dm.value, dm.unit, dm.rawUnit), }); } } factors.push(...opaque); if (d !== 1) { factors.push({ exponent: -1, node: num(d) }); } return mkProduct(factors); } /** * Fold a run of factors that contains no substitution barrier. * @param {ProductFactor[]} items Simplified factors, flattened by the caller. * @param {number} start * @param {number} end * @param {SimplifyFn} simplify * @param {number | false} precision * @return {Node} */ function foldFactors(items, start, end, simplify, precision) { let coeff = 1; // Num factors by exponent, so an inexact quotient can be re-emitted as a // reduced `numerator / denominator`. let numerator = 1; let denominator = 1; /** @type {{exponent: 1 | -1, value: number, unit: string, rawUnit?: string}[]} */ const dims = []; /** @type {ProductFactor[]} */ const opaque = []; /** @type {{exponent: 1 | -1, value: number}[]} */ const scalarChain = []; /** * @param {1 | -1} exponent * @param {Node} n * @return {void} */ function processFactor(exponent, n) { if (n.type === 'Num') { if (exponent === 1) { coeff *= n.value; numerator *= n.value; } else { coeff /= n.value; // §10.9.1: 1/0 → ±Infinity, 0/0 → NaN per IEEE-754 denominator *= n.value; } scalarChain.push({ exponent, value: n.value }); return; } if (n.type === 'Dim') { dims.push({ exponent, value: n.value, unit: n.unit, rawUnit: n.rawUnit }); scalarChain.push({ exponent, value: n.value }); return; } opaque.push({ exponent, node: n }); } for (let i = start; i < end; i++) { processFactor(items[i].exponent, items[i].node); } // §10.2 typed division. Higher-power cancellation (`px^2 / px`) is left // unreduced — consumers don't rely on it and the spec doesn't require it. const cancelled = tryCancelPair(dims, precision); if (cancelled !== null) { coeff *= cancelled.numerator / cancelled.denominator; numerator *= cancelled.numerator; denominator *= cancelled.denominator; } const divides = denominator !== 1 || cancelled !== null; const remainingDims = cancelled ? cancelled.remaining : dims; const loneDim = remainingDims.length === 1 && remainingDims[0].exponent === 1 && opaque.length === 0 ? remainingDims[0] : null; const value = loneDim === null ? coeff : chainValue(scalarChain); const distributable = remainingDims.length === 0 && isScalarSum(opaque); // An inexact quotient stays symbolic when its numerator and denominator // print exactly; otherwise it is folded and rounded at serialization. if (divides && !foldsExactly(value, opaque, distributable, precision)) { const rational = rationalProduct( numerator, denominator, loneDim, remainingDims, opaque, precision ); if (rational !== null) { return rational; } } // §10.10 distributive multiplication: `0.5 * (100vw - 10px)` → `50vw - 5px`. // Only distribute when every Sum term is Num/Dim — partial distribution // over opaque terms matches neither the legacy implementation nor csstools. if (distributable) { const sum = /** @type {import('../node.js').Sum} */ (opaque[0].node); const distributed = sum.terms.map((t) => ({ sign: t.sign, node: simplify( mkProduct([ { exponent: 1, node: num(coeff) }, { exponent: 1, node: t.node }, ]) ), })); return mkSum(distributed); } if (loneDim !== null) { return dim(value, loneDim.unit, loneDim.rawUnit); } if (remainingDims.length === 0 && opaque.length === 0) { return num(coeff); } /** @type {ProductFactor[]} */ const factors = []; if (coeff !== 1) { factors.push({ exponent: 1, node: num(coeff) }); } for (const d of remainingDims) { factors.push({ exponent: d.exponent, node: dim(d.value, d.unit, d.rawUnit), }); } factors.push(...opaque); return mkProduct(factors); } /** * A substitution function (`var()`, `env()`, `attr()`, ...) is replaced by * tokens, not by a value, so factors cannot move or cancel across it. Any * function the parser does not recognise (`anchor()`, `foo()`, ...) is also an * OpaqueCall and is treated the same, conservatively. A grouped Product is a * parenthesized substitution and is equally opaque. * @param {Node} node * @return {boolean} */ function isBarrier(node) { return ( node.type === 'OpaqueCall' || (node.type === 'Product' && node.grouped === true) ); } /** * Flatten a simplified factor through nested ungrouped Products and the * canonical negation form, so barriers are visible at the top level. * @param {ProductFactor[]} out * @param {1 | -1} exponent * @param {Node} node * @return {boolean} Whether a barrier was pushed. */ function flattenFactor(out, exponent, node) { if (node.type === 'Product' && !node.grouped) { let barrier = false; for (const inner of node.factors) { if ( flattenFactor( out, /** @type {1 | -1} */ (exponent * inner.exponent), inner.node ) ) barrier = true; } return barrier; } if (node.type === 'Sum' && node.terms.length === 1) { out.push({ exponent, node: num(-1) }); return flattenFactor(out, exponent, node.terms[0].node); } out.push({ exponent, node }); return isBarrier(node); } /** * @param {Product} product * @param {SimplifyFn} simplify * @param {number | false} [precision] A quotient that is not exact at this * precision stays symbolic (`100% / 3`) instead of being rounded. * @return {Node} */ function simplifyProduct(product, simplify, precision = false) { /** @type {ProductFactor[]} */ const items = []; let hasBarrier = false; for (const f of product.factors) { if (flattenFactor(items, f.exponent, simplify(f.node))) hasBarrier = true; } if (!hasBarrier) return foldFactors(items, 0, items.length, simplify, precision); // Fold each run between barriers on its own; never move or cancel a factor // across a barrier, and keep each barrier's own operator. /** @type {ProductFactor[]} */ const factors = []; let start = 0; for (let i = 0; i <= items.length; i++) { if (i < items.length && !isBarrier(items[i].node)) continue; if (i - start === 1) factors.push(items[start]); else if (i - start > 1) factors.push({ exponent: 1, node: foldFactors(items, start, i, simplify, precision), }); if (i < items.length) factors.push(items[i]); start = i + 1; } const result = mkProduct(factors); return product.grouped === true ? mkGroup(result) : result; } export { simplifyProduct };