postcss-calc
Version:
PostCSS plugin to reduce calc()
120 lines (109 loc) • 3.4 kB
JavaScript
import { num, dim, ident, call } from '../node.js';
import { foldConstArgs } from './fold.js';
/** @typedef {import('../node.js').Node} Node */
const ROUND_STRATEGIES = new Set(['nearest', 'up', 'down', 'to-zero']);
/** @typedef {'nearest' | 'up' | 'down' | 'to-zero'} RoundStrategy */
/**
* @param {Node[]} args
* @return {Node}
*/
function simplifyRound(args) {
/** @type {RoundStrategy} */ let strategy = 'nearest';
let rest = args;
const first = args[0];
if (first?.type === 'Ident') {
const n = first.name.toLowerCase();
if (!ROUND_STRATEGIES.has(n)) {
// Unrecognized strategy ident — opaque rather than guessing intent.
return call('round', args);
}
strategy = /** @type {RoundStrategy} */ (n);
rest = args.slice(1);
}
/** @type {() => Node} */
const passthrough = () =>
call('round', strategy === 'nearest' ? rest : [ident(strategy), ...rest]);
// B omitted: defaults to 1 when A is <number>; else opaque.
const argsForFold = argsForRoundFold(rest);
const fold = argsForFold && foldConstArgs(argsForFold);
if (!fold) {
return passthrough();
}
const [a, b] = /** @type {[number, number]} */ (fold.values);
// Spec §10.7.1 non-finite step: NaN propagates; both infinite cancels to
// NaN. Infinite step with finite A is strategy-dependent — the multiples
// of an infinite step are {-∞, 0, +∞}, so `up` (ceiling) lands on +∞ for
// positive A and `down` (floor) lands on -∞ for negative A; every other
// case folds to ±0 carrying A's sign. Infinite-A / finite-B falls through
// to applyRound, where floor*b===ceil*b===±∞ collapses back to A
// (§10.3.1 "result is the same infinity").
if (Number.isNaN(b)) {
return num(Number.NaN);
}
if (!Number.isFinite(b)) {
if (!Number.isFinite(a)) {
return num(Number.NaN);
}
let result;
if (strategy === 'up' && a > 0) {
result = Infinity;
} else if (strategy === 'down' && a < 0) {
result = -Infinity;
} else {
result = a < 0 || Object.is(a, -0) ? -0 : 0;
}
return fold.unit === '' ? num(result) : dim(result, fold.unit);
}
const result = applyRound(strategy, a, b);
if (Number.isNaN(result)) {
return num(Number.NaN);
}
return fold.unit === '' ? num(result) : dim(result, fold.unit);
}
/**
* @param {Node[]} args
* @return {Node[] | null}
*/
function argsForRoundFold(args) {
if (args.length === 2) {
return args;
}
if (args.length === 1 && args[0].type === 'Num') {
return [args[0], num(1)];
}
return null;
}
/**
* @param {RoundStrategy} strategy
* @param {number} a
* @param {number} b
* @return {number}
*/
function applyRound(strategy, a, b) {
if (b === 0) {
return Number.NaN;
}
const q = a / b;
const c1 = Math.floor(q) * b;
const c2 = Math.ceil(q) * b;
// With negative B, floor*B > ceil*B; spec defines lower as closer to -∞.
const lower = Math.min(c1, c2);
const upper = Math.max(c1, c2);
if (lower === upper) {
return a;
}
switch (strategy) {
case 'up':
return upper;
case 'down':
return lower;
case 'to-zero':
return Math.abs(lower) <= Math.abs(upper) ? lower : upper;
case 'nearest': {
const dl = a - lower;
const du = upper - a;
return du <= dl ? upper : lower; // tie → upper (§10.3 line 978)
}
}
}
export { simplifyRound };