Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 2 additions & 26 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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;
}
Expand Down
91 changes: 49 additions & 42 deletions src/lib/simplify/call.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, MathSimplifier>} */
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, { type: 'Call' }>} node
* @param {SimplifyFn} simplify
Expand All @@ -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 };
8 changes: 8 additions & 0 deletions test/unit/plugin.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)) }');
Expand Down
37 changes: 37 additions & 0 deletions test/unit/simplify/call.test.mjs
Original file line number Diff line number Diff line change
@@ -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);
}
});
12 changes: 9 additions & 3 deletions types/lib/simplify/call.d.ts
Original file line number Diff line number Diff line change
@@ -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, { type: 'Call' }>} node
* @param {SimplifyFn} simplify
Expand All @@ -10,4 +16,4 @@ export type SimplifyFn = import('../simplify.js').SimplifyFn;
declare function simplifyCall(node: Extract<Node, {
type: 'Call';
}>, simplify: SimplifyFn): Node;
export { simplifyCall };
export { isSupportedMathFunction, simplifyCall };