UNPKG

postcss-calc

Version:
395 lines (370 loc) • 12.1 kB
import { simplifyMinMax } from './simplify/min-max.js'; import { simplifyClamp } from './simplify/clamp.js'; import { simplifyAbs } from './simplify/abs.js'; import { simplifySign } from './simplify/sign.js'; import { simplifyModRem } from './simplify/mod-rem.js'; import { ROUND_STRATEGIES, simplifyRound } from './simplify/round.js'; import { simplifyTrig } from './simplify/trig.js'; import { simplifyInverseTrig } from './simplify/inverse-trig.js'; import { simplifyAtan2 } from './simplify/atan2.js'; import { simplifyPow } from './simplify/pow.js'; import { simplifySqrt } from './simplify/sqrt.js'; import { simplifyExp } from './simplify/exp.js'; import { simplifyLog } from './simplify/log.js'; import { simplifyHypot } from './simplify/hypot.js'; /** @typedef {import('./node.js').Node} Node */ /** * `percent` marks a value that resolves in the same percentage context as its * peers (a pure percentage). It is only produced at leaves, by abs(), and by * homogeneous sums and calls — never by a product, where an unpaired * percentage could no longer cancel against anything. * @typedef {{kind: 'number'} | {kind: 'dimension', base: string | null} | {kind: 'unknown', percent?: true} | {kind: 'failure'}} CalculationType */ /** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */ /** @typedef {(args: CalculationType[], nodes: Node[]) => CalculationType} TypeAnalyzer */ /** @typedef {{analyze: TypeAnalyzer, simplify?: MathSimplifier, isKeyword?: (node: Node, index: number) => boolean, calculation?: boolean}} MathFunction */ /** @type {CalculationType} */ const numberType = { kind: 'number' }; /** @type {CalculationType} */ const unknownType = { kind: 'unknown' }; /** @type {CalculationType} */ const percentageType = { kind: 'unknown', percent: true, }; /** @type {CalculationType} */ const failureType = { kind: 'failure' }; /** @param {CalculationType} type @return {boolean} */ function isFailure(type) { return type.kind === 'failure'; } /** @param {CalculationType} type @return {boolean} */ function isPercentage(type) { return type.kind === 'unknown' && type.percent === true; } /** @param {CalculationType} a @param {CalculationType} b @return {CalculationType} */ function addTypes(a, b) { if (isFailure(a) || isFailure(b)) return failureType; if (a.kind === 'unknown' || b.kind === 'unknown') { // A pure percentage keeps its contextual type so a surrounding product // can cancel `% / %`; mixing it with any other opaque operand loses the // guarantee that it resolves in the same context as its peers. A known // number + percentage sum is likewise kept unknown and valid: the spec // resolves the percentage against its surrounding context, which this // coarse type model does not track. return isPercentage(a) && isPercentage(b) ? percentageType : unknownType; } if (a.kind === 'number' && b.kind === 'number') return numberType; if (a.kind === 'dimension' && b.kind === 'dimension') { if (a.base === null || b.base === null) return unknownType; return a.base === b.base ? a : failureType; } return failureType; } /** * Check an all-number function without assuming anything about an unresolved * operand. A concrete dimension can never become a number, so it is still a * definite error when another argument is opaque. * @param {CalculationType[]} args * @param {number} min * @param {number} max * @return {CalculationType} */ function numberArguments(args, min, max) { if (args.length < min || args.length > max) return failureType; let hasUnknown = false; for (const arg of args) { if (arg.kind === 'dimension') return failureType; hasUnknown = hasUnknown || arg.kind === 'unknown'; } return hasUnknown ? unknownType : numberType; } /** * Check values that must share a calculation type. Unknown operands remain * unknown: they might resolve to the concrete type required by their peers. * @param {CalculationType[]} args * @param {number} min * @param {number} max * @return {CalculationType} */ function matchingArguments(args, min, max) { if (args.length < min || args.length > max) return failureType; let result = args[0]; for (let index = 1; index < args.length; index++) { result = addTypes(result, args[index]); if (isFailure(result)) return failureType; } return result; } /** @param {CalculationType[]} args @return {CalculationType} */ function analyzeTrig(args) { return args.length === 1 && (args[0].kind === 'unknown' || args[0].kind === 'number' || (args[0].kind === 'dimension' && args[0].base === 'angle')) ? numberType : failureType; } /** @param {CalculationType[]} args @return {CalculationType} */ function analyzeInverseTrig(args) { if (args.length !== 1 || args[0].kind === 'dimension') { return failureType; } return args[0].kind === 'unknown' ? unknownType : { kind: 'dimension', base: 'angle' }; } /** @param {CalculationType[]} args @return {CalculationType} */ function analyzeIdentity(args) { return args.length === 1 ? args[0] : failureType; } /** @param {CalculationType[]} args @return {CalculationType} */ function analyzeSign(args) { return args.length === 1 ? numberType : failureType; } /** @param {CalculationType[]} args @param {Node[]} nodes @return {CalculationType} */ function analyzeRound(args, nodes) { const strategy = nodes[0]; const hasStrategy = strategy?.type === 'Ident' && ROUND_STRATEGIES.has(strategy.name.toLowerCase()); const start = hasStrategy ? 1 : 0; const valueCount = args.length - start; if (valueCount === 1) { const value = args[start]; if (value.kind === 'dimension') return failureType; if (value.kind === 'unknown') return unknownType; return numberType; } if (valueCount !== 2) return failureType; const type = addTypes(args[start], args[start + 1]); return isFailure(type) ? failureType : type; } /** @param {CalculationType[]} args @return {CalculationType} */ function analyzeAtan2(args) { const type = matchingArguments(args, 2, 2); if (isFailure(type)) return type; // The type table gives atan2() «["angle" → 1]»; an unresolved result is // plain unknown, never the percentage of its arguments. if (type.kind === 'unknown') return unknownType; return { kind: 'dimension', base: 'angle' }; } /** @param {CalculationType[]} args @return {CalculationType} */ function analyzeCalc(args) { return args.length === 1 ? args[0] : failureType; } /** @param {Node} node @return {boolean} */ function isRoundStrategy(node) { return node.type === 'Ident' && ROUND_STRATEGIES.has(node.name.toLowerCase()); } /** @param {Node} node @param {number} index @return {boolean} */ function isClampKeyword(node, index) { return ( (index === 0 || index === 2) && node.type === 'Ident' && node.name.toLowerCase() === 'none' ); } /** @param {CalculationType[]} args @param {Node[]} nodes @return {CalculationType} */ function analyzeClamp(args, nodes) { if (args.length !== 3) return failureType; let valueCount = 0; /** @type {CalculationType} */ let result = failureType; for (let index = 0; index < args.length; index++) { if (isClampKeyword(nodes[index], index)) continue; result = valueCount === 0 ? args[index] : addTypes(result, args[index]); valueCount++; } if (valueCount < 1 || isFailure(result)) return failureType; return result; } const mathFunctions = new Map( /** @type {[string, MathFunction][]} */ ([ ['calc', { analyze: analyzeCalc, calculation: true }], ['-webkit-calc', { analyze: analyzeCalc, calculation: true }], ['-moz-calc', { analyze: analyzeCalc, calculation: true }], [ 'min', { analyze: (args) => matchingArguments(args, 1, Infinity), simplify: simplifyMinMax, }, ], [ 'max', { analyze: (args) => matchingArguments(args, 1, Infinity), simplify: simplifyMinMax, }, ], [ 'clamp', { analyze: analyzeClamp, simplify: (_name, args) => simplifyClamp(args), isKeyword: isClampKeyword, }, ], [ 'abs', { analyze: analyzeIdentity, simplify: (_name, args) => simplifyAbs(args), }, ], [ 'sign', { analyze: analyzeSign, simplify: (_name, args) => simplifySign(args), }, ], [ 'mod', { analyze: (args) => matchingArguments(args, 2, 2), simplify: (_name, args) => simplifyModRem('mod', args), }, ], [ 'rem', { analyze: (args) => matchingArguments(args, 2, 2), simplify: (_name, args) => simplifyModRem('rem', args), }, ], [ 'round', { analyze: analyzeRound, simplify: (_name, args) => simplifyRound(args), isKeyword: (node, index) => index === 0 && isRoundStrategy(node), }, ], [ 'sin', { analyze: analyzeTrig, simplify: (_name, args) => simplifyTrig('sin', args), }, ], [ 'cos', { analyze: analyzeTrig, simplify: (_name, args) => simplifyTrig('cos', args), }, ], [ 'tan', { analyze: analyzeTrig, simplify: (_name, args) => simplifyTrig('tan', args), }, ], [ 'asin', { analyze: analyzeInverseTrig, simplify: (_name, args) => simplifyInverseTrig('asin', args), }, ], [ 'acos', { analyze: analyzeInverseTrig, simplify: (_name, args) => simplifyInverseTrig('acos', args), }, ], [ 'atan', { analyze: analyzeInverseTrig, simplify: (_name, args) => simplifyInverseTrig('atan', args), }, ], [ 'atan2', { analyze: analyzeAtan2, simplify: (_name, args) => simplifyAtan2(args), }, ], [ 'pow', { analyze: (args) => numberArguments(args, 2, 2), simplify: (_name, args) => simplifyPow(args), }, ], [ 'sqrt', { analyze: (args) => numberArguments(args, 1, 1), simplify: (_name, args) => simplifySqrt(args), }, ], [ 'hypot', { analyze: (args) => matchingArguments(args, 1, Infinity), simplify: (_name, args) => simplifyHypot(args), }, ], [ 'log', { analyze: (args) => numberArguments(args, 1, 2), simplify: (_name, args) => simplifyLog(args), }, ], [ 'exp', { analyze: (args) => numberArguments(args, 1, 1), simplify: (_name, args) => simplifyExp(args), }, ], ]) ); /** * @param {string} name * @return {{normalizedName: string, definition: MathFunction} | undefined} */ function lookupMathFunction(name) { const normalizedName = name.toLowerCase(); const definition = mathFunctions.get(normalizedName); return definition === undefined ? undefined : { normalizedName, definition }; } const mathFunctionNames = [...mathFunctions.keys()]; const QUICK_MATH_TEST = new RegExp( `(?:${mathFunctionNames.join('|')})\\(`, 'i' ); /** @param {string} name @return {boolean} */ function isCalculationFunction(name) { return mathFunctions.get(name.toLowerCase())?.calculation === true; } /** @param {string} name @return {boolean} */ function isSupportedMathFunction(name) { const normalizedName = name.toLowerCase(); const definition = mathFunctions.get(normalizedName); return definition !== undefined && definition.calculation !== true; } /** @param {string} value @return {boolean} */ function hasPotentialMathFunction(value) { return ( value.includes('(') && (QUICK_MATH_TEST.test(value) || value.includes('\\')) ); } export { addTypes, failureType, hasPotentialMathFunction, isCalculationFunction, isFailure, isPercentage, isSupportedMathFunction, lookupMathFunction, mathFunctions, numberType, percentageType, QUICK_MATH_TEST, unknownType, };