postcss-calc
Version:
PostCSS plugin to reduce calc()
119 lines (109 loc) • 3.7 kB
JavaScript
import { mkSum, num, dim } from '../node.js';
import { baseOf } from '../convertUnits.js';
import { mergeConvertibleBuckets } from './bucket.js';
/**
* @typedef {import('../node.js').Node} Node
* @typedef {import('../node.js').Sum} Sum
* @typedef {import('../node.js').SumTerm} SumTerm
* @typedef {import('../simplify.js').SimplifyFn} SimplifyFn
* @typedef {import('./bucket.js').UnitBucket} UnitBucket
*/
// Subtracting near-equal terms leaves float dust: `0.07 * 1e7 - 700000` is
// 1.16e-10, not 0. Snap a total that's tiny next to its terms back to 0.
const NOISE_REL = Number.EPSILON * 8;
/**
* @param {number} total
* @param {number} scale largest |term| accumulated into `total`
* @return {number}
*/
function denoise(total, scale) {
return Math.abs(total) < scale * NOISE_REL ? 0 : total;
}
/**
* @param {Sum} sum
* @param {SimplifyFn} simplify
* @return {Node}
*/
function simplifySum(sum, simplify) {
// §10.10 two-phase dim handling: phase 1 buckets by exact unit (`1em + 1em`
// → `2em`); phase 2 merges convertible same-base buckets into the first-
// encountered unit. `100vh - 5rem - 10rem - 100px` → `-15rem` in phase 1,
// then vh/rem/px stay separate in phase 2 (none convert to each other).
let numTotal = 0;
let numScale = 0;
/** @type {Map<string, UnitBucket>} */
const byUnit = new Map();
/** @type {SumTerm[]} */
const opaque = [];
let bucketOrder = 0;
/**
* @param {1 | -1} sign
* @param {Node} n
* @return {void}
*/
function processTerm(sign, n) {
if (n.type === 'Sum') {
// Parentheses around a sum are normally algebraically disposable. Keep
// them only for an opaque sum used negatively: distributing that sign
// changes the CSS value when a var() contains a sum of its own.
if (
n.grouped &&
sign === -1 &&
n.terms.some((t) => t.node.type !== 'Num' && t.node.type !== 'Dim')
) {
opaque.push({ sign, node: n });
return;
}
for (const inner of n.terms) {
processTerm(/** @type {1 | -1} */ (sign * inner.sign), inner.node);
}
return;
}
if (n.type === 'Num') {
numTotal += sign * n.value;
numScale = Math.max(numScale, Math.abs(n.value));
return;
}
if (n.type === 'Dim') {
const key = n.unit.toLowerCase();
const existing = byUnit.get(key);
if (existing) {
existing.total += sign * n.value;
existing.scale = Math.max(existing.scale, Math.abs(n.value));
} else {
byUnit.set(key, {
unit: n.unit,
total: sign * n.value,
scale: Math.abs(n.value),
base: baseOf(n.unit),
order: bucketOrder++,
});
}
return;
}
opaque.push({ sign, node: n });
}
for (const t of sum.terms) {
processTerm(t.sign, simplify(t.node));
}
// mkSum drops zero-valued Nums, so pushing the numeric total
// unconditionally is harmless. Zero-valued unit buckets are kept for
// type info (WPT calc-serialization-002).
/** @type {SumTerm[]} */
const terms = [{ sign: 1, node: num(denoise(numTotal, numScale)) }];
for (const bucket of mergeConvertibleBuckets([...byUnit.values()])) {
terms.push({
sign: 1,
node: dim(denoise(bucket.total, bucket.scale), bucket.unit),
});
}
terms.push(...opaque);
const result = mkSum(terms);
// Keep the source grouping available to an enclosing subtraction. Numeric
// groups have already folded and therefore do not need the marker.
if (sum.grouped && result.type === 'Sum' && opaque.length > 0) {
return { ...result, grouped: true };
}
return result;
}
export { simplifySum };