From ce9e3310923ef8bd73eed44a0048252e2f5a2b1c Mon Sep 17 00:00:00 2001 From: Ludovico Fischer Date: Sun, 16 Aug 2026 15:53:48 +0200 Subject: [PATCH] refactor: deduplicate math function list --- src/index.js | 28 +--------- src/lib/simplify/call.js | 91 +++++++++++++++++--------------- test/unit/plugin.test.mjs | 8 +++ test/unit/simplify/call.test.mjs | 37 +++++++++++++ types/lib/simplify/call.d.ts | 12 +++-- 5 files changed, 105 insertions(+), 71 deletions(-) create mode 100644 test/unit/simplify/call.test.mjs diff --git a/src/index.js b/src/index.js index 2f45952..93fbdad 100644 --- a/src/index.js +++ b/src/index.js @@ -5,35 +5,11 @@ import valueParser from 'postcss-value-parser'; import { tokenize } from './lib/tokenizer.js'; import { parse } from './lib/parser.js'; import { simplify } from './lib/simplify.js'; +import { isSupportedMathFunction } from './lib/simplify/call.js'; import { serialize } from './lib/serialize.js'; const MATCH_CALC = /^(?:-(?:moz|webkit)-)?calc$/i; -// Bare math-function calls (no calc() wrapper) — fed to the same pipeline. -// Mirrors the dispatch in lib/simplify/call.js. -const MATH_FUNCTIONS = new Set([ - 'min', - 'max', - 'clamp', - 'abs', - 'sign', - 'mod', - 'rem', - 'round', - 'sin', - 'cos', - 'tan', - 'asin', - 'acos', - 'atan', - 'atan2', - 'pow', - 'sqrt', - 'hypot', - 'log', - 'exp', -]); - /** * @typedef {object} PluginOptions * @property {number | false} [precision] @@ -60,7 +36,7 @@ function transformValue(value, options, result, item) { return; } const isCalc = MATCH_CALC.test(node.value); - const isMath = !isCalc && MATH_FUNCTIONS.has(node.value.toLowerCase()); + const isMath = !isCalc && isSupportedMathFunction(node.value); if (!isCalc && !isMath) { return; } diff --git a/src/lib/simplify/call.js b/src/lib/simplify/call.js index d4d931c..b701d18 100644 --- a/src/lib/simplify/call.js +++ b/src/lib/simplify/call.js @@ -19,6 +19,46 @@ import { simplifyHypot } from './hypot.js'; /** @typedef {import('../node.js').Node} Node */ /** @typedef {import('../simplify.js').SimplifyFn} SimplifyFn */ +/** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */ + +// Bare CSS math functions with implemented simplification semantics, keyed +// by lowercase name. calc() and its vendor-prefixed forms are handled +// separately as wrappers in simplifyCall. This map is the single source of +// truth for both dispatch and `isSupportedMathFunction`. +/** @type {Map} */ +const MATH_SIMPLIFIERS = new Map([ + ['min', simplifyMinMax], + ['max', simplifyMinMax], + ['clamp', (_name, args) => simplifyClamp(args)], + ['abs', (_name, args) => simplifyAbs(args)], + ['sign', (_name, args) => simplifySign(args)], + ['mod', (_name, args) => simplifyModRem('mod', args)], + ['rem', (_name, args) => simplifyModRem('rem', args)], + ['round', (_name, args) => simplifyRound(args)], + ['sin', (_name, args) => simplifyTrig('sin', args)], + ['cos', (_name, args) => simplifyTrig('cos', args)], + ['tan', (_name, args) => simplifyTrig('tan', args)], + ['asin', (_name, args) => simplifyInverseTrig('asin', args)], + ['acos', (_name, args) => simplifyInverseTrig('acos', args)], + ['atan', (_name, args) => simplifyInverseTrig('atan', args)], + ['atan2', (_name, args) => simplifyAtan2(args)], + ['pow', (_name, args) => simplifyPow(args)], + ['sqrt', (_name, args) => simplifySqrt(args)], + ['hypot', (_name, args) => simplifyHypot(args)], + ['log', (_name, args) => simplifyLog(args)], + ['exp', (_name, args) => simplifyExp(args)], +]); + +/** + * Whether a bare CSS math function has an implemented simplifier. + * + * @param {string} name + * @return {boolean} + */ +function isSupportedMathFunction(name) { + return MATH_SIMPLIFIERS.has(name.toLowerCase()); +} + /** * @param {Extract} node * @param {SimplifyFn} simplify @@ -36,50 +76,17 @@ function simplifyCall(node, simplify) { const args = node.args.map((a) => simplify(a)); - if (name === 'min' || name === 'max') { - return simplifyMinMax(node.name, args); - } - if (name === 'clamp') { - return simplifyClamp(args); - } - if (name === 'abs') { - return simplifyAbs(args); - } - if (name === 'sign') { - return simplifySign(args); - } - if (name === 'mod' || name === 'rem') { - return simplifyModRem(name, args); - } - if (name === 'round') { - return simplifyRound(args); - } - if (name === 'sin' || name === 'cos' || name === 'tan') { - return simplifyTrig(name, args); - } - if (name === 'asin' || name === 'acos' || name === 'atan') { - return simplifyInverseTrig(name, args); - } - if (name === 'atan2') { - return simplifyAtan2(args); - } - if (name === 'pow') { - return simplifyPow(args); - } - if (name === 'sqrt') { - return simplifySqrt(args); - } - if (name === 'hypot') { - return simplifyHypot(args); - } - if (name === 'log') { - return simplifyLog(args); - } - if (name === 'exp') { - return simplifyExp(args); + const simplifier = MATH_SIMPLIFIERS.get(name); + if (simplifier) { + // min/max preserve the call's original casing in their opaque-args + // fallback; the rest normalize to lowercase internally. + return simplifier( + name === 'min' || name === 'max' ? node.name : name, + args + ); } return { type: 'Call', name: node.name, args }; } -export { simplifyCall }; +export { isSupportedMathFunction, simplifyCall }; diff --git a/test/unit/plugin.test.mjs b/test/unit/plugin.test.mjs index 2a07828..514a5df 100644 --- a/test/unit/plugin.test.mjs +++ b/test/unit/plugin.test.mjs @@ -215,6 +215,14 @@ test('plugin: simplifies bare math functions case-insensitively', async () => { const { css } = await process('a{ width: MIN(1px, 2px) }'); assert.equal(css, 'a{ width: 1px }'); }); +test('plugin: simplifies a supported bare function from the dispatcher', async () => { + const { css } = await process('a{ width: pow(2, 3) }'); + assert.equal(css, 'a{ width: 8 }'); +}); +test('plugin: leaves unsupported bare functions untouched', async () => { + const { css } = await process('a{ width: unknown(1px + 2px) }'); + assert.equal(css, 'a{ width: unknown(1px + 2px) }'); +}); test('plugin: leaves opaque-arg bare min() preserved', async () => { const { css } = await process('a{ width: min(1px, var(--x)) }'); assert.equal(css, 'a{ width: min(1px, var(--x)) }'); diff --git a/test/unit/simplify/call.test.mjs b/test/unit/simplify/call.test.mjs new file mode 100644 index 0000000..2365e7e --- /dev/null +++ b/test/unit/simplify/call.test.mjs @@ -0,0 +1,37 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { isSupportedMathFunction } from '../../../src/lib/simplify/call.js'; + +test('isSupportedMathFunction: recognizes every implemented bare math function', () => { + const supported = [ + 'min', + 'max', + 'clamp', + 'abs', + 'sign', + 'mod', + 'rem', + 'round', + 'sin', + 'cos', + 'tan', + 'asin', + 'acos', + 'atan', + 'atan2', + 'pow', + 'sqrt', + 'hypot', + 'log', + 'exp', + ]; + + for (const name of supported) { + assert.equal(isSupportedMathFunction(name), true, name); + assert.equal(isSupportedMathFunction(name.toUpperCase()), true, name); + } + + for (const name of ['calc', '-webkit-calc', '-moz-calc', 'var', 'unknown']) { + assert.equal(isSupportedMathFunction(name), false, name); + } +}); diff --git a/types/lib/simplify/call.d.ts b/types/lib/simplify/call.d.ts index 3f1e66f..8e94a20 100644 --- a/types/lib/simplify/call.d.ts +++ b/types/lib/simplify/call.d.ts @@ -1,7 +1,13 @@ export type Node = import('../node.js').Node; export type SimplifyFn = import('../simplify.js').SimplifyFn; -/** @typedef {import('../node.js').Node} Node */ -/** @typedef {import('../simplify.js').SimplifyFn} SimplifyFn */ +export type MathSimplifier = (name: string, args: Node[]) => Node; +/** + * Whether a bare CSS math function has an implemented simplifier. + * + * @param {string} name + * @return {boolean} + */ +declare function isSupportedMathFunction(name: string): boolean; /** * @param {Extract} node * @param {SimplifyFn} simplify @@ -10,4 +16,4 @@ export type SimplifyFn = import('../simplify.js').SimplifyFn; declare function simplifyCall(node: Extract, simplify: SimplifyFn): Node; -export { simplifyCall }; +export { isSupportedMathFunction, simplifyCall };