diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b72f04f..97afb2c9 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines. +## Unreleased + +### Bug fixes + +- `reduceCalc` and the plugin no longer print an error for a + value nested more than 1024 levels deep which is not a math function, for + example when `calc(` only appears inside `url()`, a string or a comment. The + output is unchanged. + ## 11.2.3 (2026-10-10) ### Bug fixes diff --git a/src/index.js b/src/index.js index 9bcfe900..74f736aa 100644 --- a/src/index.js +++ b/src/index.js @@ -16,26 +16,19 @@ import { hasPotentialMathFunction } from './lib/functions.js'; /** * Runs `reduceCalc` over one text property of a decl/atrule/rule node * and updates it in place. - * `setProp` closes over the property name and the concrete node type at - * each call site, since `Declaration`/`AtRule`/`Rule` don't share a typed - * "text property" name to index generically. * - * @param {import('postcss').ChildNode} node - * @param {string} current - * @param {(target: import('postcss').ChildNode, value: string) => void} setProp + * @param {import('postcss').Declaration | import('postcss').AtRule | import('postcss').Rule} node + * @param {'value' | 'params' | 'selector'} prop * @param {ResolvedOptions} options * @param {import('postcss').Result} result - * @param {boolean} selectorContext * @return {void} */ -function applyTransform( - node, - current, - setProp, - options, - result, - selectorContext -) { +function applyTransform(node, prop, options, result) { + // `Declaration`/`AtRule`/`Rule` don't share a typed "text property" name. + const target = /** @type {Record} */ ( + /** @type {unknown} */ (node) + ); + const current = target[prop]; if (!hasPotentialMathFunction(current)) { return; } @@ -51,10 +44,10 @@ function applyTransform( result.warn(message, { plugin: 'postcss-calc', node }); }, // Selectors cannot contain calc(), so they always unwrap resolved values. - unwrapSingleValue: selectorContext || options.unwrapSingleValue, + unwrapSingleValue: prop === 'selector' || options.unwrapSingleValue, }); if (transformed !== current) { - setProp(node, transformed); + target[prop] = transformed; } } @@ -83,42 +76,13 @@ function pluginCreator(opts) { OnceExit(css, { result }) { css.walk((node) => { if (node.type === 'decl') { - applyTransform( - node, - node.value, - (n, v) => { - /** @type {import('postcss').Declaration} */ (n).value = v; - }, - options, - result, - false - ); - } - if (node.type === 'atrule' && options.mediaQueries) { - applyTransform( - node, - node.params, - (n, v) => { - /** @type {import('postcss').AtRule} */ (n).params = v; - }, - options, - result, - false - ); - } - if (node.type === 'rule' && options.selectors) { + applyTransform(node, 'value', options, result); + } else if (node.type === 'atrule' && options.mediaQueries) { + applyTransform(node, 'params', options, result); + } else if (node.type === 'rule' && options.selectors) { // Reduces `:nth-child(calc(...))` via the function walk. calc() in a // quoted attribute value is a literal match, so it's left untouched. - applyTransform( - node, - node.selector, - (n, v) => { - /** @type {import('postcss').Rule} */ (n).selector = v; - }, - options, - result, - true - ); + applyTransform(node, 'selector', options, result); } }); }, diff --git a/src/lib/analyze.js b/src/lib/analyze.js index 2f85a508..51b6f43a 100644 --- a/src/lib/analyze.js +++ b/src/lib/analyze.js @@ -13,21 +13,9 @@ import { assertDepth } from './limits.js'; /** @typedef {import('./node.js').Node} Node */ /** @typedef {import('./functions.js').CalculationType} CalculationType */ -/** @typedef {Extract} DimensionType */ /** @typedef {'number' | 'unknown' | {dimension: string | null}} AnalysisType */ /** @typedef {{type: AnalysisType, valid: boolean, unresolved: boolean}} Analysis */ -/** - * @typedef {Object} ProductFactors - * @property {boolean} valid - * @property {boolean} structurallyValid - * @property {boolean} hasUnresolved - * @property {DimensionType | null} numerator - * @property {DimensionType | null} denominator - * @property {boolean} hasOpaqueNumerator - * @property {boolean} hasOpaqueDenominator - */ - /** * Analyze the original complete tree and return its root summary. Analysis * validates and classifies the tree; it is not a rewrite plan. @@ -142,35 +130,41 @@ function analyzeProduct(node, depth) { // Consume one such pair before judging the remaining unknowns so a // surrounding sum does not mistake `% / %` for a length-compatible term. const cancelled = Math.min(percentageNumerator, percentageDenominator); - const hasOpaqueNumerator = opaqueNumerator - cancelled > 0; - const hasOpaqueDenominator = opaqueDenominator - cancelled > 0; - return finishProduct({ - valid, - structurallyValid, + return finishProduct( + valid && structurallyValid, hasUnresolved, numerator, denominator, - hasOpaqueNumerator, - hasOpaqueDenominator, - }); + opaqueNumerator - cancelled > 0, + opaqueDenominator - cancelled > 0 + ); } /** * Classify a product once its factors are analyzed. Opaque factors can supply * missing type information, but they cannot make an already-invalid * combination of known dimensions valid. - * @param {ProductFactors} factors + * @param {boolean} valid + * @param {boolean} hasUnresolved + * @param {Extract | null} numerator + * @param {Extract | null} denominator + * @param {boolean} hasOpaqueNumerator + * @param {boolean} hasOpaqueDenominator * @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */ -function finishProduct(factors) { - const { valid, structurallyValid, hasUnresolved } = factors; - if (!valid) return finish(failureType, false, hasUnresolved); - if (!structurallyValid) return finish(failureType, false, hasUnresolved); - const { numerator, denominator } = factors; +function finishProduct( + valid, + hasUnresolved, + numerator, + denominator, + hasOpaqueNumerator, + hasOpaqueDenominator +) { if ( - numerator !== null && - denominator !== null && - numerator.base !== denominator.base + !valid || + (numerator !== null && + denominator !== null && + numerator.base !== denominator.base) ) { return finish(failureType, false, hasUnresolved); } @@ -178,37 +172,24 @@ function finishProduct(factors) { // only leave that dimension in place (when it resolves to a number) or make // the product invalid. It can never make the product a bare number. Keep // that known constraint so a surrounding sum can reject `1px * 1% + 1`. - const constrained = constrainedNumerator(factors); - if (constrained !== null) { - return finish(constrained, true, hasUnresolved); + if ( + hasOpaqueNumerator && + !hasOpaqueDenominator && + numerator !== null && + denominator === null + ) { + return finish(numerator, true, hasUnresolved); } // Other opaque factors may supply type information that changes how the // known dimensions combine once the known factors are structurally valid. - if (factors.hasOpaqueNumerator || factors.hasOpaqueDenominator) { + if (hasOpaqueNumerator || hasOpaqueDenominator) { return finish(unknownType, true, hasUnresolved); } if (numerator !== null && denominator !== null) { - return finish( - numerator.base === denominator.base ? numberType : failureType, - valid && numerator.base === denominator.base, - hasUnresolved - ); + return finish(numberType, true, hasUnresolved); } if (denominator !== null) return finish(failureType, false, hasUnresolved); - return finish(numerator ?? numberType, valid, hasUnresolved); -} - -/** - * @param {ProductFactors} factors - * @return {DimensionType | null} - */ -function constrainedNumerator(factors) { - return factors.hasOpaqueNumerator && - !factors.hasOpaqueDenominator && - factors.numerator !== null && - factors.denominator === null - ? factors.numerator - : null; + return finish(numerator ?? numberType, true, hasUnresolved); } /** @param {Extract} node @param {number} depth @return {{type: CalculationType, valid: boolean, unresolved: boolean}} */ diff --git a/src/lib/block-index.js b/src/lib/block-index.js index 68d6024b..667bf123 100644 --- a/src/lib/block-index.js +++ b/src/lib/block-index.js @@ -10,36 +10,28 @@ const BLOCK_CLOSE = new Map([ ]); /** - * Read-only delimiter navigation for one native token stream range. + * Read-only delimiter navigation for one native token stream. */ class BlockIndex { /** @type {CSSToken[]} */ #tokens; - /** @type {number} */ - #rangeStart; - /** @type {number} */ - #rangeEnd; /** @type {Map} */ #closes = new Map(); /** @type {number} */ #maxDepth = 0; /** - * Build the delimiter index once for a token stream range. Matching remains + * Build the delimiter index once for a token stream. Matching remains * LIFO: a mismatched closer is ignored and does not disturb the open stack. * * @param {CSSToken[]} tokens - * @param {number} start - * @param {number} end */ - constructor(tokens, start, end) { + constructor(tokens) { this.#tokens = tokens; - this.#rangeStart = Math.max(0, start); - this.#rangeEnd = Math.min(tokens.length, Math.max(this.#rangeStart, end)); /** @type {{index: number, close: import('@csstools/css-tokenizer').TokenType}[]} */ const stack = []; - for (let i = this.#rangeStart; i < this.#rangeEnd; i++) { + for (let i = 0; i < tokens.length; i++) { const type = tokens[i][0]; const close = BLOCK_CLOSE.get(type); if (close !== undefined) { @@ -71,8 +63,8 @@ class BlockIndex { /** @param {number | undefined} end @return {number} */ #boundedEnd(end) { return Math.min( - this.#rangeEnd, - Math.max(this.#rangeStart, end ?? this.#rangeEnd) + this.#tokens.length, + Math.max(0, end ?? this.#tokens.length) ); } @@ -80,13 +72,13 @@ class BlockIndex { closeOf(openPosition, endPosition) { const close = this.#closes.get(openPosition) ?? -1; const bound = this.#boundedEnd(endPosition); - return close >= this.#rangeStart && close < bound ? close : -1; + return close < bound ? close : -1; } /** @param {number} position @param {number} endPosition @return {number} */ nextComponent(position, endPosition) { const bound = this.#boundedEnd(endPosition); - if (position < this.#rangeStart || position >= bound) return bound; + if (position < 0 || position >= bound) return bound; const close = this.closeOf(position, bound); return close === -1 ? position + 1 : close + 1; } @@ -94,7 +86,7 @@ class BlockIndex { /** @param {number} startPosition @param {number} endPosition @return {number} */ firstTopLevelComma(startPosition, endPosition) { const bound = this.#boundedEnd(endPosition); - for (let i = Math.max(this.#rangeStart, startPosition); i < bound;) { + for (let i = Math.max(0, startPosition); i < bound;) { if (this.#tokens[i][0] === CssType.Comma) return i; i = this.nextComponent(i, bound); } @@ -103,16 +95,14 @@ class BlockIndex { } /** - * Build the delimiter index once for a token stream range. Matching remains - * LIFO: a mismatched closer is ignored and does not disturb the open stack. + * Build the delimiter index once for a token stream. Matching remains LIFO: a + * mismatched closer is ignored and does not disturb the open stack. * * @param {CSSToken[]} tokens - * @param {number} [start] - * @param {number} [end] * @return {BlockIndex} */ -function indexBlocks(tokens, start = 0, end = tokens.length) { - return new BlockIndex(tokens, start, end); +function indexBlocks(tokens) { + return new BlockIndex(tokens); } export { indexBlocks }; diff --git a/src/lib/function-analyzers.js b/src/lib/function-analyzers.js index 338618de..153e4fdc 100644 --- a/src/lib/function-analyzers.js +++ b/src/lib/function-analyzers.js @@ -1,3 +1,4 @@ +import { isClampKeyword } from './keywords.js'; import { ROUND_STRATEGIES } from './simplify/round.js'; import { addTypes, @@ -45,10 +46,7 @@ function analyzeSign(args) { /** @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 hasStrategy = nodes.length > 0 && isRoundStrategy(nodes[0]); const start = hasStrategy ? 1 : 0; const valueCount = args.length - start; if (valueCount === 1) { @@ -72,25 +70,11 @@ function analyzeAtan2(args) { 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; @@ -107,13 +91,11 @@ function analyzeClamp(args, nodes) { export { analyzeAtan2, - analyzeCalc, analyzeClamp, analyzeIdentity, analyzeInverseTrig, analyzeRound, analyzeSign, analyzeTrig, - isClampKeyword, isRoundStrategy, }; diff --git a/src/lib/functions.js b/src/lib/functions.js index df39268e..a17ebfcd 100644 --- a/src/lib/functions.js +++ b/src/lib/functions.js @@ -1,29 +1,31 @@ +import { TokenType as CssType } from '@csstools/css-tokenizer'; 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 { 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'; +import { + simplifyAbs, + simplifyExp, + simplifyLog, + simplifyPow, + simplifySign, + simplifySqrt, +} from './simplify/numeric.js'; import { analyzeAtan2, - analyzeCalc, analyzeClamp, analyzeIdentity, analyzeInverseTrig, analyzeRound, analyzeSign, analyzeTrig, - isClampKeyword, isRoundStrategy, } from './function-analyzers.js'; +import { isClampKeyword } from './keywords.js'; import { addTypes, failureType, @@ -38,15 +40,15 @@ import { /** @typedef {import('./node.js').Node} Node */ /** @typedef {import('./types.js').CalculationType} CalculationType */ -/** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */ +/** @typedef {(args: Node[], name: string) => Node} MathSimplifier */ /** @typedef {(args: CalculationType[], nodes: Node[]) => CalculationType} TypeAnalyzer */ /** @typedef {{analyze: TypeAnalyzer, simplify?: MathSimplifier, isKeyword?: (node: Node, index: number) => boolean, calculation?: boolean}} MathFunction */ const mathFunctions = new Map( /** @type {[string, MathFunction][]} */ ([ - ['calc', { analyze: analyzeCalc, calculation: true }], - ['-webkit-calc', { analyze: analyzeCalc, calculation: true }], - ['-moz-calc', { analyze: analyzeCalc, calculation: true }], + ['calc', { analyze: analyzeIdentity, calculation: true }], + ['-webkit-calc', { analyze: analyzeIdentity, calculation: true }], + ['-moz-calc', { analyze: analyzeIdentity, calculation: true }], [ 'min', { @@ -65,7 +67,7 @@ const mathFunctions = new Map( 'clamp', { analyze: analyzeClamp, - simplify: (_name, args) => simplifyClamp(args), + simplify: simplifyClamp, isKeyword: isClampKeyword, }, ], @@ -73,35 +75,35 @@ const mathFunctions = new Map( 'abs', { analyze: analyzeIdentity, - simplify: (_name, args) => simplifyAbs(args), + simplify: simplifyAbs, }, ], [ 'sign', { analyze: analyzeSign, - simplify: (_name, args) => simplifySign(args), + simplify: simplifySign, }, ], [ 'mod', { analyze: (args) => matchingArguments(args, 2, 2), - simplify: (_name, args) => simplifyModRem('mod', args), + simplify: simplifyModRem, }, ], [ 'rem', { analyze: (args) => matchingArguments(args, 2, 2), - simplify: (_name, args) => simplifyModRem('rem', args), + simplify: simplifyModRem, }, ], [ 'round', { analyze: analyzeRound, - simplify: (_name, args) => simplifyRound(args), + simplify: simplifyRound, isKeyword: (node, index) => index === 0 && isRoundStrategy(node), }, ], @@ -109,84 +111,84 @@ const mathFunctions = new Map( 'sin', { analyze: analyzeTrig, - simplify: (_name, args) => simplifyTrig('sin', args), + simplify: simplifyTrig, }, ], [ 'cos', { analyze: analyzeTrig, - simplify: (_name, args) => simplifyTrig('cos', args), + simplify: simplifyTrig, }, ], [ 'tan', { analyze: analyzeTrig, - simplify: (_name, args) => simplifyTrig('tan', args), + simplify: simplifyTrig, }, ], [ 'asin', { analyze: analyzeInverseTrig, - simplify: (_name, args) => simplifyInverseTrig('asin', args), + simplify: simplifyInverseTrig, }, ], [ 'acos', { analyze: analyzeInverseTrig, - simplify: (_name, args) => simplifyInverseTrig('acos', args), + simplify: simplifyInverseTrig, }, ], [ 'atan', { analyze: analyzeInverseTrig, - simplify: (_name, args) => simplifyInverseTrig('atan', args), + simplify: simplifyInverseTrig, }, ], [ 'atan2', { analyze: analyzeAtan2, - simplify: (_name, args) => simplifyAtan2(args), + simplify: simplifyAtan2, }, ], [ 'pow', { analyze: (args) => numberArguments(args, 2, 2), - simplify: (_name, args) => simplifyPow(args), + simplify: simplifyPow, }, ], [ 'sqrt', { analyze: (args) => numberArguments(args, 1, 1), - simplify: (_name, args) => simplifySqrt(args), + simplify: simplifySqrt, }, ], [ 'hypot', { analyze: (args) => matchingArguments(args, 1, Infinity), - simplify: (_name, args) => simplifyHypot(args), + simplify: simplifyHypot, }, ], [ 'log', { analyze: (args) => numberArguments(args, 1, 2), - simplify: (_name, args) => simplifyLog(args), + simplify: simplifyLog, }, ], [ 'exp', { analyze: (args) => numberArguments(args, 1, 1), - simplify: (_name, args) => simplifyExp(args), + simplify: simplifyExp, }, ], ]) @@ -213,13 +215,6 @@ 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 ( @@ -227,14 +222,31 @@ function hasPotentialMathFunction(value) { ); } +/** + * Token-level counterpart of `hasPotentialMathFunction`: whether any function + * token names a supported math function, as the scanner would find it. Math + * functions inside `url()` and similar are part of another token. + * + * @param {import('@csstools/css-tokenizer').CSSToken[]} tokens + * @return {boolean} + */ +function hasMathFunctionToken(tokens) { + for (const token of tokens) { + if (token[0] === CssType.Function && lookupMathFunction(token[4].value)) { + return true; + } + } + return false; +} + export { addTypes, failureType, + hasMathFunctionToken, hasPotentialMathFunction, isCalculationFunction, isFailure, isPercentage, - isSupportedMathFunction, lookupMathFunction, mathFunctions, numberType, diff --git a/src/lib/keywords.js b/src/lib/keywords.js new file mode 100644 index 00000000..ae04ca3c --- /dev/null +++ b/src/lib/keywords.js @@ -0,0 +1,12 @@ +/** @typedef {import('./node.js').Node} Node */ + +/** @param {Node} node @param {number} index @return {boolean} */ +function isClampKeyword(node, index) { + return ( + (index === 0 || index === 2) && + node.type === 'Ident' && + node.name.toLowerCase() === 'none' + ); +} + +export { isClampKeyword }; diff --git a/src/lib/parser.js b/src/lib/parser.js index 92f369d1..05e65602 100644 --- a/src/lib/parser.js +++ b/src/lib/parser.js @@ -9,7 +9,7 @@ import { mkSum, num, } from './node.js'; -import { isCalculationFunction, isSupportedMathFunction } from './functions.js'; +import { lookupMathFunction } from './functions.js'; import { assertDepth } from './limits.js'; import { parseOpaqueCall, parseVar } from './parser/opaque.js'; import { @@ -160,7 +160,7 @@ function parseCall(input, cursor, t, depth) { const rawName = t.raw.slice(0, -1); if (name.toLowerCase() === 'var') return parseVar(input, cursor, t, name, rawName, parseRange); - if (!isCalculationFunction(name) && !isSupportedMathFunction(name)) + if (lookupMathFunction(name) === undefined) return parseOpaqueCall(input, cursor, t, name, rawName, parseRange); /** @type {Node[]} */ const args = []; if (!isPunct(input, cursor, ')')) { diff --git a/src/lib/parser/opaque.js b/src/lib/parser/opaque.js index 3a75f2c9..47ba1696 100644 --- a/src/lib/parser/opaque.js +++ b/src/lib/parser/opaque.js @@ -1,10 +1,7 @@ // Opaque function parsing: `var()`, unknown functions and their component trees. import { TokenType as CssType } from '@csstools/css-tokenizer'; import { ident, opaqueCall } from '../node.js'; -import { - isCalculationFunction, - isSupportedMathFunction, -} from '../functions.js'; +import { lookupMathFunction } from '../functions.js'; import { assertDepth } from '../limits.js'; import { eofPositionAt, sourceSpelling } from './tokens.js'; @@ -105,8 +102,7 @@ function componentTree(input, start, rangeEnd, parseRange, root) { const isMathFunction = token[0] === CssType.Function && - (isCalculationFunction(token[4].value) || - isSupportedMathFunction(token[4].value)); + lookupMathFunction(token[4].value) !== undefined; if (isMathFunction) { try { pushComponent(tree, parseRange(input, i, close + 1)); diff --git a/src/lib/serialize.js b/src/lib/serialize.js index 96665ea3..61fa332e 100644 --- a/src/lib/serialize.js +++ b/src/lib/serialize.js @@ -4,13 +4,7 @@ import { checkCalculationDepth } from './limits.js'; import { isCalculationFunction } from './functions.js'; -import { - isScalar, - serializeNumber, - roundedScalarValue, - isDegenerate, - degenerateKeyword, -} from './serialize/precision.js'; +import { isScalar } from './serialize/precision.js'; import { emitCall, emitLeadingNeg, @@ -38,29 +32,11 @@ function normalizeScalarPolicy(opts) { : 'standard'; } -/** - * Serialize a scalar result without allocating a render context or buffer. - * @param {import('./node.js').Num | import('./node.js').Dim} node - * @param {number | false} precision - * @param {'standard' | 'unwrap-all'} scalarPolicy - * @param {string} wrapper - * @return {string} - */ -function serializeScalarResult(node, precision, scalarPolicy, wrapper) { - const value = roundedScalarValue(node, precision); - const unit = node.type === 'Dim' ? (node.rawUnit ?? node.unit) : ''; - if (isDegenerate(value)) { - return `${wrapper}(${degenerateKeyword(value)}${unit ? ` * 1${unit}` : ''})`; - } - const scalar = serializeNumber(value) + unit; - return scalarPolicy === 'standard' ? `${wrapper}(${scalar})` : scalar; -} - /** * @param {SerializeOptions} opts - * @return {{buffer: string[], precision: number | false, scalarPolicy: 'standard' | 'unwrap-all'}} + * @return {import('./serialize/precision.js').SerializeSession} */ -function makeContext(opts) { +function makeSession(opts) { return { buffer: [], precision: opts.precision ?? 5, @@ -68,60 +44,7 @@ function makeContext(opts) { }; } -/** - * @param {Node} node - * @param {SerializeOptions} opts - * @return {{kind: 'math', node: Node, session: ReturnType, wrapper: string}} - */ -function planSerialize(node, opts) { - return { - kind: 'math', - node, - session: makeContext(opts), - wrapper: opts.calcName ?? 'calc', - }; -} - -/** - * @param {{tree: Node, status: 'resolved' | 'unresolved', rootName: string, rootSpelling: string, calculation?: boolean, original?: string}} result - * @param {SerializeOptions} opts - * @return {{kind: 'original', text: string} | {kind: 'root-call', node: Node, session: ReturnType, callNameOverride: string} | {kind: 'wrapped-expr', node: Node, session: ReturnType, wrapper: string} | {kind: 'math', node: Node, session: ReturnType, wrapper: string}} - */ -function planSerializeResult(result, opts) { - const isCalc = result.calculation ?? isCalculationFunction(result.rootName); - const normalizedRootName = - result.calculation === undefined - ? result.rootName.toLowerCase() - : result.rootName; - const wrapper = isCalc - ? result.rootSpelling || opts.calcName || 'calc' - : 'calc'; - const session = makeContext(opts); - - if (!isCalc && result.status === 'unresolved') { - if ( - (result.tree.type === 'Call' || result.tree.type === 'OpaqueCall') && - result.tree.name.toLowerCase() === normalizedRootName - ) { - return { - kind: 'root-call', - node: result.tree, - session, - callNameOverride: result.rootSpelling, - }; - } - return { kind: 'original', text: result.original ?? '' }; - } - - if (session.scalarPolicy === 'standard') { - if (isScalar(result.tree)) - return { kind: 'math', node: result.tree, session, wrapper }; - return { kind: 'wrapped-expr', node: result.tree, session, wrapper }; - } - return { kind: 'math', node: result.tree, session, wrapper }; -} - -/** @param {Node} node @param {ReturnType} session @return {void} */ +/** @param {Node} node @param {import('./serialize/precision.js').SerializeSession} session @return {void} */ function emitRootExpr(node, session) { if (node.type === 'Sum' && node.terms.length === 1) { emitLeadingNeg(node.terms[0].node, session); @@ -130,49 +53,16 @@ function emitRootExpr(node, session) { emitNode(node, session); } -/** - * @param {ReturnType | ReturnType} renderSpec - * @return {string} - */ -function emitOutput(renderSpec) { - if (renderSpec.kind === 'original') return renderSpec.text; - const { session } = renderSpec; - if (renderSpec.kind === 'root-call') { - if (renderSpec.node.type === 'Call') { - emitCall(renderSpec.node, session, renderSpec.callNameOverride); - } else { - emitOpaqueCall( - /** @type {import('./node.js').OpaqueCall} */ (renderSpec.node), - session, - renderSpec.callNameOverride - ); - } - } else if (renderSpec.kind === 'wrapped-expr') { - session.buffer.push(renderSpec.wrapper, '('); - emitRootExpr(renderSpec.node, session); - session.buffer.push(')'); - } else { - emitMathResult(renderSpec.node, session, renderSpec.wrapper); - } - return session.buffer.join(''); -} - /** * @param {Node} node * @param {SerializeOptions} [opts] * @return {string} */ function serialize(node, opts = {}) { - if (isScalar(node)) { - return serializeScalarResult( - node, - opts.precision ?? 5, - normalizeScalarPolicy(opts), - opts.calcName ?? 'calc' - ); - } checkCalculationDepth(node); - return emitOutput(planSerialize(node, opts)); + const session = makeSession(opts); + emitMathResult(node, session, opts.calcName ?? 'calc'); + return session.buffer.join(''); } /** @@ -182,21 +72,38 @@ function serialize(node, opts = {}) { */ function serializeResult(result, opts = {}) { const node = result.tree; - if (isScalar(node)) { - const isCalc = result.calculation ?? isCalculationFunction(result.rootName); - if (!isCalc && result.status === 'unresolved') return result.original ?? ''; - const wrapper = isCalc - ? result.rootSpelling || opts.calcName || 'calc' - : 'calc'; - return serializeScalarResult( - node, - opts.precision ?? 5, - normalizeScalarPolicy(opts), - wrapper - ); + checkCalculationDepth(node); + const isCalc = result.calculation ?? isCalculationFunction(result.rootName); + const session = makeSession(opts); + + if (!isCalc && result.status === 'unresolved') { + const rootName = + result.calculation === undefined + ? result.rootName.toLowerCase() + : result.rootName; + if ( + (node.type === 'Call' || node.type === 'OpaqueCall') && + node.name.toLowerCase() === rootName + ) { + if (node.type === 'Call') emitCall(node, session, result.rootSpelling); + else emitOpaqueCall(node, session, result.rootSpelling); + return session.buffer.join(''); + } + return result.original ?? ''; } - checkCalculationDepth(result.tree); - return emitOutput(planSerializeResult(result, opts)); + + const wrapper = isCalc + ? result.rootSpelling || opts.calcName || 'calc' + : 'calc'; + // Under the standard policy a bare Ident/Call result still needs calc(). + if (session.scalarPolicy === 'standard' && !isScalar(node)) { + session.buffer.push(wrapper, '('); + emitRootExpr(node, session); + session.buffer.push(')'); + } else { + emitMathResult(node, session, wrapper); + } + return session.buffer.join(''); } export { serialize, serializeResult }; diff --git a/src/lib/serialize/expression.js b/src/lib/serialize/expression.js index e4f567d3..8c342bad 100644 --- a/src/lib/serialize/expression.js +++ b/src/lib/serialize/expression.js @@ -15,7 +15,6 @@ import { isScalar, isSignedZero, round, - roundedScalarValue, } from './precision.js'; import { needsParentheses, @@ -271,7 +270,7 @@ function emitOpaqueCall(node, session, callNameOverride) { */ function emitMathResult(node, session, wrapper) { if (isScalar(node)) { - const scalarValue = roundedScalarValue(node, session.precision); + const scalarValue = round(node.value, session.precision); const buffer = session.buffer; if (isDegenerate(scalarValue)) { buffer.push(wrapper, '(', degenerateKeyword(scalarValue)); diff --git a/src/lib/serialize/precision.js b/src/lib/serialize/precision.js index 4924093c..292860df 100644 --- a/src/lib/serialize/precision.js +++ b/src/lib/serialize/precision.js @@ -129,16 +129,6 @@ function serializeNumber(v) { return text; } -/** - * @param {import('../node.js').Num | import('../node.js').Dim} node - * @param {number | false} precision - * @param {number} [value] - * @return {number} - */ -function roundedScalarValue(node, precision, value) { - return round(value ?? node.value, precision); -} - /** * @param {import('../node.js').Num | import('../node.js').Dim} node * @param {string[]} buffer @@ -152,18 +142,6 @@ function emitRoundedScalar(node, buffer, value) { } } -/** - * @param {import('../node.js').Num | import('../node.js').Dim} node - * @param {SerializeSession} session - * @param {number} [value] - * @return {number} - */ -function emitFiniteScalar(node, session, value) { - const rounded = roundedScalarValue(node, session.precision, value); - emitRoundedScalar(node, session.buffer, rounded); - return rounded; -} - /** * @param {import('../node.js').Num | import('../node.js').Dim} node * @param {SerializeSession} session @@ -189,7 +167,9 @@ function emitScalar(node, session, value, bare = false) { } else { buffer.push(degenerateKeyword(effective)); } - } else emitFiniteScalar(node, session, effective); + } else { + emitRoundedScalar(node, buffer, round(effective, session.precision)); + } } /** @@ -233,9 +213,7 @@ export { isDegenerate, degenerateKeyword, serializeNumber, - roundedScalarValue, emitRoundedScalar, - emitFiniteScalar, emitScalar, emitSignedZero, emitBareSignedZero, diff --git a/src/lib/simplify/abs.js b/src/lib/simplify/abs.js deleted file mode 100644 index 18486ba8..00000000 --- a/src/lib/simplify/abs.js +++ /dev/null @@ -1,23 +0,0 @@ -import { num, dim, call } from '../node.js'; - -/** @typedef {import('../node.js').Node} Node */ - -/** - * @param {Node[]} args - * @return {Node} - */ -function simplifyAbs(args) { - if (args.length !== 1) { - return call('abs', args); - } - const a = args[0]; - if (a.type === 'Num') { - return num(Math.abs(a.value)); - } - if (a.type === 'Dim' && a.unit !== '%') { - return dim(Math.abs(a.value), a.unit, a.rawUnit); - } - return call('abs', [a]); -} - -export { simplifyAbs }; diff --git a/src/lib/simplify/call.js b/src/lib/simplify/call.js index 14143974..f490fb21 100644 --- a/src/lib/simplify/call.js +++ b/src/lib/simplify/call.js @@ -6,7 +6,6 @@ import { mathFunctions, isCalculationFunction } from '../functions.js'; /** @typedef {import('../node.js').Node} Node */ /** @typedef {import('../simplify.js').SimplifyFn} SimplifyFn */ -/** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */ /** * @param {Extract} node @@ -27,7 +26,7 @@ function simplifyCall(node, simplify) { const simplifier = mathFunctions.get(name)?.simplify; if (simplifier) { - return simplifier(name, args); + return simplifier(args, name); } return call(node.name, args, node.rawName); diff --git a/src/lib/simplify/cancel.js b/src/lib/simplify/cancel.js index 2551a96b..ef154ec6 100644 --- a/src/lib/simplify/cancel.js +++ b/src/lib/simplify/cancel.js @@ -8,18 +8,19 @@ import { isExact } from './exact.js'; * denominator. Otherwise return null. Used by `simplifyProduct` for typed * division (§10.2). More complex cancellation (e.g. `px^2 / px`) is left * unreduced — consumers rarely rely on it and the spec doesn't require it. - * @template {{ exponent: 1 | -1, value: number, unit: string }} D - * @param {D[]} dims + * @param {{ exponent: 1 | -1, value: number, unit: string }[]} dims * @param {number | false} [precision] The denominator is converted into the * numerator's unit when that is exact at this precision (`1px / 1in` → * `1 / 96`); otherwise the numerator is converted (`1in / 1px` → `96 / 1`). - * @return {{ numerator: number, denominator: number, remaining: D[] } | null} + * @return {{ numerator: number, denominator: number } | null} */ function tryCancelPair(dims, precision = false) { if (dims.length !== 2) { return null; } - const [a, b] = /** @type {[D, D]} */ (dims); + const [a, b] = /** @type {[typeof dims[number], typeof dims[number]]} */ ( + dims + ); if (a.exponent === b.exponent) { return null; } @@ -40,7 +41,6 @@ function tryCancelPair(dims, precision = false) { return { numerator: numerator.value, denominator: converted, - remaining: [], }; } const numConverted = convert( @@ -52,7 +52,6 @@ function tryCancelPair(dims, precision = false) { return { numerator: numConverted, denominator: denominator.value, - remaining: [], }; } return null; diff --git a/src/lib/simplify/clamp.js b/src/lib/simplify/clamp.js index cb274304..d562ebfe 100644 --- a/src/lib/simplify/clamp.js +++ b/src/lib/simplify/clamp.js @@ -1,6 +1,7 @@ import { call } from '../node.js'; import { foldConstArgs, chosenArg } from './fold.js'; import { simplifyMinMax } from './min-max.js'; +import { isClampKeyword } from '../keywords.js'; /** @typedef {import('../node.js').Node} Node */ @@ -10,17 +11,17 @@ import { simplifyMinMax } from './min-max.js'; */ function simplifyClamp(args) { if (args.length === 3) { - const minNone = isNone(args[0]); - const maxNone = isNone(args[2]); + const minNone = isClampKeyword(args[0], 0); + const maxNone = isClampKeyword(args[2], 2); if (minNone && maxNone) { return args[1]; } if (minNone) { - return simplifyMinMax('min', [args[1], args[2]]); + return simplifyMinMax([args[1], args[2]], 'min'); } if (maxNone) { - return simplifyMinMax('max', [args[0], args[1]]); + return simplifyMinMax([args[0], args[1]], 'max'); } const fold = foldConstArgs(args); @@ -35,12 +36,4 @@ function simplifyClamp(args) { return call('clamp', args); } -/** - * @param {Node} node - * @return {boolean} - */ -function isNone(node) { - return node.type === 'Ident' && node.name.toLowerCase() === 'none'; -} - export { simplifyClamp }; diff --git a/src/lib/simplify/exp.js b/src/lib/simplify/exp.js deleted file mode 100644 index 0cacbc3d..00000000 --- a/src/lib/simplify/exp.js +++ /dev/null @@ -1,16 +0,0 @@ -import { num, call } from '../node.js'; - -/** @typedef {import('../node.js').Node} Node */ - -/** - * @param {Node[]} args - * @return {Node} - */ -function simplifyExp(args) { - if (args.length !== 1 || args[0].type !== 'Num') { - return call('exp', args); - } - return num(Math.exp(args[0].value)); -} - -export { simplifyExp }; diff --git a/src/lib/simplify/inverse-trig.js b/src/lib/simplify/inverse-trig.js index 7af2d738..03e2f4e0 100644 --- a/src/lib/simplify/inverse-trig.js +++ b/src/lib/simplify/inverse-trig.js @@ -11,11 +11,11 @@ const INVERSE_TRIG_OPS = /** @type {const} */ ({ }); /** - * @param {'asin' | 'acos' | 'atan'} name * @param {Node[]} args + * @param {string} name * @return {Node} */ -function simplifyInverseTrig(name, args) { +function simplifyInverseTrig(args, name) { if (args.length !== 1) { return call(name, args); } @@ -23,7 +23,9 @@ function simplifyInverseTrig(name, args) { if (a.type !== 'Num') { return call(name, args); } - const radians = INVERSE_TRIG_OPS[name](a.value); + const radians = INVERSE_TRIG_OPS[ + /** @type {keyof typeof INVERSE_TRIG_OPS} */ (name) + ](a.value); if (Number.isNaN(radians)) { return num(Number.NaN); } diff --git a/src/lib/simplify/log.js b/src/lib/simplify/log.js deleted file mode 100644 index ef362589..00000000 --- a/src/lib/simplify/log.js +++ /dev/null @@ -1,19 +0,0 @@ -import { num, call } from '../node.js'; - -/** @typedef {import('../node.js').Node} Node */ - -/** - * @param {Node[]} args - * @return {Node} - */ -function simplifyLog(args) { - if (args.length === 1 && args[0].type === 'Num') { - return num(Math.log(args[0].value)); - } - if (args.length === 2 && args[0].type === 'Num' && args[1].type === 'Num') { - return num(Math.log(args[0].value) / Math.log(args[1].value)); - } - return call('log', args); -} - -export { simplifyLog }; diff --git a/src/lib/simplify/min-max.js b/src/lib/simplify/min-max.js index 852b147d..41ea1ec4 100644 --- a/src/lib/simplify/min-max.js +++ b/src/lib/simplify/min-max.js @@ -4,11 +4,11 @@ import { foldConstArgs, chosenArg } from './fold.js'; /** @typedef {import('../node.js').Node} Node */ /** - * @param {string} name * @param {Node[]} args + * @param {string} name * @return {Node} */ -function simplifyMinMax(name, args) { +function simplifyMinMax(args, name) { const fold = foldConstArgs(args); if (fold !== null) { const fn = name.toLowerCase() === 'min' ? Math.min : Math.max; diff --git a/src/lib/simplify/mod-rem.js b/src/lib/simplify/mod-rem.js index 7745d968..b05ea95f 100644 --- a/src/lib/simplify/mod-rem.js +++ b/src/lib/simplify/mod-rem.js @@ -4,11 +4,11 @@ import { foldConstArgs, foldResult } from './fold.js'; /** @typedef {import('../node.js').Node} Node */ /** - * @param {'mod' | 'rem'} name * @param {Node[]} args + * @param {string} name * @return {Node} */ -function simplifyModRem(name, args) { +function simplifyModRem(args, name) { if (args.length !== 2) { return call(name, args); } @@ -27,7 +27,7 @@ function simplifyModRem(name, args) { } /** - * @param {'mod' | 'rem'} name + * @param {string} name * @param {number} a * @param {number} b * @return {number} diff --git a/src/lib/simplify/numeric.js b/src/lib/simplify/numeric.js new file mode 100644 index 00000000..dfcd667c --- /dev/null +++ b/src/lib/simplify/numeric.js @@ -0,0 +1,91 @@ +// §10.4–§10.7 — abs/sign and the -only functions pow/sqrt/exp/log. + +import { num, dim, call } from '../node.js'; + +/** @typedef {import('../node.js').Node} Node */ + +/** + * `%` is property-context-dependent (§10.6), so it stays opaque. + * @param {Node[]} args + * @param {string} name + * @return {Node} + */ +function simplifyAbs(args, name) { + if (args.length !== 1) return call(name, args); + const a = args[0]; + if (a.type === 'Num') return num(Math.abs(a.value)); + if (a.type === 'Dim' && a.unit !== '%') { + return dim(Math.abs(a.value), a.unit, a.rawUnit); + } + return call(name, args); +} + +/** + * @param {Node[]} args + * @param {string} name + * @return {Node} + */ +function simplifySign(args, name) { + if (args.length !== 1) return call(name, args); + const a = args[0]; + if (a.type === 'Num' || (a.type === 'Dim' && a.unit !== '%')) { + return num(Math.sign(a.value)); + } + return call(name, args); +} + +/** + * @param {Node[]} args + * @param {string} name + * @return {Node} + */ +function simplifySqrt(args, name) { + if (args.length !== 1 || args[0].type !== 'Num') return call(name, args); + return num(Math.sqrt(args[0].value)); +} + +/** + * @param {Node[]} args + * @param {string} name + * @return {Node} + */ +function simplifyExp(args, name) { + if (args.length !== 1 || args[0].type !== 'Num') return call(name, args); + return num(Math.exp(args[0].value)); +} + +/** + * @param {Node[]} args + * @param {string} name + * @return {Node} + */ +function simplifyPow(args, name) { + if (args.length !== 2 || args[0].type !== 'Num' || args[1].type !== 'Num') { + return call(name, args); + } + return num(Math.pow(args[0].value, args[1].value)); +} + +/** + * @param {Node[]} args + * @param {string} name + * @return {Node} + */ +function simplifyLog(args, name) { + if (args.length === 1 && args[0].type === 'Num') { + return num(Math.log(args[0].value)); + } + if (args.length === 2 && args[0].type === 'Num' && args[1].type === 'Num') { + return num(Math.log(args[0].value) / Math.log(args[1].value)); + } + return call(name, args); +} + +export { + simplifyAbs, + simplifyExp, + simplifyLog, + simplifyPow, + simplifySign, + simplifySqrt, +}; diff --git a/src/lib/simplify/pow.js b/src/lib/simplify/pow.js deleted file mode 100644 index 073da211..00000000 --- a/src/lib/simplify/pow.js +++ /dev/null @@ -1,18 +0,0 @@ -// §10.5 — pow is -only. - -import { num, call } from '../node.js'; - -/** @typedef {import('../node.js').Node} Node */ - -/** - * @param {Node[]} args - * @return {Node} - */ -function simplifyPow(args) { - if (args.length !== 2 || args[0].type !== 'Num' || args[1].type !== 'Num') { - return call('pow', args); - } - return num(Math.pow(args[0].value, args[1].value)); -} - -export { simplifyPow }; diff --git a/src/lib/simplify/product.js b/src/lib/simplify/product.js index dd84c7f3..79192b51 100644 --- a/src/lib/simplify/product.js +++ b/src/lib/simplify/product.js @@ -75,7 +75,7 @@ function foldFactors(items, start, end, simplify, precision) { denominator *= cancelled.denominator; } const divides = denominator !== 1 || cancelled !== null; - const remainingDims = cancelled ? cancelled.remaining : dims; + const remainingDims = cancelled ? [] : dims; const loneDim = remainingDims.length === 1 && remainingDims[0].exponent === 1 && diff --git a/src/lib/simplify/sign.js b/src/lib/simplify/sign.js deleted file mode 100644 index 37521117..00000000 --- a/src/lib/simplify/sign.js +++ /dev/null @@ -1,24 +0,0 @@ -import { num, call } from '../node.js'; - -/** @typedef {import('../node.js').Node} Node */ - -/** - * @param {Node[]} args - * @return {Node} - */ -function simplifySign(args) { - if (args.length !== 1) { - return call('sign', args); - } - const a = args[0]; - if (a.type === 'Num') { - return num(Math.sign(a.value)); - } - // %: sign is property-context-dependent (§10.6) — opaque. - if (a.type === 'Dim' && a.unit !== '%') { - return num(Math.sign(a.value)); - } - return call('sign', [a]); -} - -export { simplifySign }; diff --git a/src/lib/simplify/sqrt.js b/src/lib/simplify/sqrt.js deleted file mode 100644 index a4b28a5b..00000000 --- a/src/lib/simplify/sqrt.js +++ /dev/null @@ -1,15 +0,0 @@ -import { num, call } from '../node.js'; - -/** @typedef {import('../node.js').Node} Node */ -/** - * @param {Node[]} args - * @return {Node} - */ -function simplifySqrt(args) { - if (args.length !== 1 || args[0].type !== 'Num') { - return call('sqrt', args); - } - return num(Math.sqrt(args[0].value)); -} - -export { simplifySqrt }; diff --git a/src/lib/simplify/trig.js b/src/lib/simplify/trig.js index 0242112a..2c974b95 100644 --- a/src/lib/simplify/trig.js +++ b/src/lib/simplify/trig.js @@ -12,11 +12,11 @@ const TRIG_OPS = /** @type {const} */ ({ }); /** - * @param {'sin' | 'cos' | 'tan'} name * @param {Node[]} args + * @param {string} name * @return {Node} */ -function simplifyTrig(name, args) { +function simplifyTrig(args, name) { if (args.length !== 1) { return call(name, args); } @@ -38,7 +38,7 @@ function simplifyTrig(name, args) { if (radians === null) { return call(name, args); } - return num(TRIG_OPS[name](radians)); + return num(TRIG_OPS[/** @type {keyof typeof TRIG_OPS} */ (name)](radians)); } export { simplifyTrig }; diff --git a/src/reduce.js b/src/reduce.js index da7b8e4e..267be8ce 100644 --- a/src/reduce.js +++ b/src/reduce.js @@ -3,7 +3,10 @@ // at-rule parameters, or selector text. import { tokenize as cssTokenize } from '@csstools/css-tokenizer'; import { indexBlocks } from './lib/block-index.js'; -import { hasPotentialMathFunction } from './lib/functions.js'; +import { + hasMathFunctionToken, + hasPotentialMathFunction, +} from './lib/functions.js'; import { assertDepth } from './lib/limits.js'; import { findCalculations } from './lib/scan.js'; import { compileCandidates } from './lib/compile.js'; @@ -74,6 +77,10 @@ function reduceCalc(value, opts) { let index; try { tokens = cssTokenize({ css: value }); + // Nothing to reduce, so skip block indexing and its depth limit. + if (!hasMathFunctionToken(tokens)) { + return value; + } index = indexBlocks(tokens); assertDepth(index.maxDepth); } catch (error) { diff --git a/test/unit/output-policy.test.js b/test/unit/output-policy.test.js index f2747ab8..51b72061 100644 --- a/test/unit/output-policy.test.js +++ b/test/unit/output-policy.test.js @@ -217,6 +217,21 @@ describe('v12 output policy', () => { } }); + test('ignores deep nesting when math functions only appear inside url()', async () => { + const input = 'url(calc(1+2)) ' + '('.repeat(2000) + ')'.repeat(2000); + const errors = []; + assert.equal( + reduceCalc(input, { onParseError: (error) => errors.push(error) }), + input + ); + const declaration = 'a{x:' + input + '}'; + const result = await postcss( + plugin({ onParseError: (error) => errors.push(error) }) + ).process(declaration, { from: undefined }); + assert.equal(result.css, declaration); + assert.deepEqual(errors, []); + }); + test('applies the nesting limit through the plugin', async () => { const count = 1025; const input = diff --git a/test/unit/simplify/call.test.js b/test/unit/simplify/call.test.js index 21c21062..4886e83e 100644 --- a/test/unit/simplify/call.test.js +++ b/test/unit/simplify/call.test.js @@ -1,8 +1,8 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; -import { isSupportedMathFunction } from '../../../src/lib/functions.js'; +import { lookupMathFunction } from '../../../src/lib/functions.js'; -test('isSupportedMathFunction: recognizes every implemented bare math function', () => { +test('lookupMathFunction: recognizes every implemented bare math function', () => { const supported = [ 'min', 'max', @@ -27,11 +27,15 @@ test('isSupportedMathFunction: recognizes every implemented bare math function', ]; for (const name of supported) { - assert.equal(isSupportedMathFunction(name), true, name); - assert.equal(isSupportedMathFunction(name.toUpperCase()), true, name); + assert.notEqual(lookupMathFunction(name), undefined, name); + assert.notEqual(lookupMathFunction(name.toUpperCase()), undefined, name); } - for (const name of ['calc', '-webkit-calc', '-moz-calc', 'var', 'unknown']) { - assert.equal(isSupportedMathFunction(name), false, name); + for (const name of ['calc', '-webkit-calc', '-moz-calc']) { + assert.notEqual(lookupMathFunction(name), undefined, name); + } + + for (const name of ['var', 'unknown']) { + assert.equal(lookupMathFunction(name), undefined, name); } }); diff --git a/types/lib/analyze.d.ts b/types/lib/analyze.d.ts index e1c1a7f6..d1eadc6c 100644 --- a/types/lib/analyze.d.ts +++ b/types/lib/analyze.d.ts @@ -1,8 +1,5 @@ export type Node = import('./node.js').Node; export type CalculationType = import('./functions.js').CalculationType; -export type DimensionType = Extract; export type AnalysisType = 'number' | 'unknown' | { dimension: string | null; }; @@ -11,30 +8,10 @@ export type Analysis = { valid: boolean; unresolved: boolean; }; -export type ProductFactors = { - valid: boolean; - structurallyValid: boolean; - hasUnresolved: boolean; - numerator: DimensionType | null; - denominator: DimensionType | null; - hasOpaqueNumerator: boolean; - hasOpaqueDenominator: boolean; -}; /** @typedef {import('./node.js').Node} Node */ /** @typedef {import('./functions.js').CalculationType} CalculationType */ -/** @typedef {Extract} DimensionType */ /** @typedef {'number' | 'unknown' | {dimension: string | null}} AnalysisType */ /** @typedef {{type: AnalysisType, valid: boolean, unresolved: boolean}} Analysis */ -/** - * @typedef {Object} ProductFactors - * @property {boolean} valid - * @property {boolean} structurallyValid - * @property {boolean} hasUnresolved - * @property {DimensionType | null} numerator - * @property {DimensionType | null} denominator - * @property {boolean} hasOpaqueNumerator - * @property {boolean} hasOpaqueDenominator - */ /** * Analyze the original complete tree and return its root summary. Analysis * validates and classifies the tree; it is not a rewrite plan. diff --git a/types/lib/block-index.d.ts b/types/lib/block-index.d.ts index 32343a2e..5141745e 100644 --- a/types/lib/block-index.d.ts +++ b/types/lib/block-index.d.ts @@ -1,18 +1,16 @@ export type CSSToken = import('@csstools/css-tokenizer').CSSToken; /** - * Read-only delimiter navigation for one native token stream range. + * Read-only delimiter navigation for one native token stream. */ declare class BlockIndex { #private; /** - * Build the delimiter index once for a token stream range. Matching remains + * Build the delimiter index once for a token stream. Matching remains * LIFO: a mismatched closer is ignored and does not disturb the open stack. * * @param {CSSToken[]} tokens - * @param {number} start - * @param {number} end */ - constructor(tokens: CSSToken[], start: number, end: number); + constructor(tokens: CSSToken[]); /** @return {number} */ get maxDepth(): number; /** @param {number} openPosition @param {number} [endPosition] @return {number} */ @@ -23,13 +21,11 @@ declare class BlockIndex { firstTopLevelComma(startPosition: number, endPosition: number): number; } /** - * Build the delimiter index once for a token stream range. Matching remains - * LIFO: a mismatched closer is ignored and does not disturb the open stack. + * Build the delimiter index once for a token stream. Matching remains LIFO: a + * mismatched closer is ignored and does not disturb the open stack. * * @param {CSSToken[]} tokens - * @param {number} [start] - * @param {number} [end] * @return {BlockIndex} */ -declare function indexBlocks(tokens: CSSToken[], start?: number, end?: number): BlockIndex; +declare function indexBlocks(tokens: CSSToken[]): BlockIndex; export { indexBlocks }; diff --git a/types/lib/function-analyzers.d.ts b/types/lib/function-analyzers.d.ts index 11fbda51..dc2898c2 100644 --- a/types/lib/function-analyzers.d.ts +++ b/types/lib/function-analyzers.d.ts @@ -16,12 +16,8 @@ declare function analyzeSign(args: CalculationType[]): CalculationType; declare function analyzeRound(args: CalculationType[], nodes: Node[]): CalculationType; /** @param {CalculationType[]} args @return {CalculationType} */ declare function analyzeAtan2(args: CalculationType[]): CalculationType; -/** @param {CalculationType[]} args @return {CalculationType} */ -declare function analyzeCalc(args: CalculationType[]): CalculationType; /** @param {Node} node @return {boolean} */ declare function isRoundStrategy(node: Node): boolean; -/** @param {Node} node @param {number} index @return {boolean} */ -declare function isClampKeyword(node: Node, index: number): boolean; /** @param {CalculationType[]} args @param {Node[]} nodes @return {CalculationType} */ declare function analyzeClamp(args: CalculationType[], nodes: Node[]): CalculationType; -export { analyzeAtan2, analyzeCalc, analyzeClamp, analyzeIdentity, analyzeInverseTrig, analyzeRound, analyzeSign, analyzeTrig, isClampKeyword, isRoundStrategy, }; +export { analyzeAtan2, analyzeClamp, analyzeIdentity, analyzeInverseTrig, analyzeRound, analyzeSign, analyzeTrig, isRoundStrategy, }; diff --git a/types/lib/functions.d.ts b/types/lib/functions.d.ts index 3a562ea0..e9a4926e 100644 --- a/types/lib/functions.d.ts +++ b/types/lib/functions.d.ts @@ -1,7 +1,7 @@ import { addTypes, failureType, isFailure, isPercentage, numberType, percentageType, unknownType } from './types.js'; export type Node = import('./node.js').Node; export type CalculationType = import('./types.js').CalculationType; -export type MathSimplifier = (name: string, args: Node[]) => Node; +export type MathSimplifier = (args: Node[], name: string) => Node; export type TypeAnalyzer = (args: CalculationType[], nodes: Node[]) => CalculationType; export type MathFunction = { analyze: TypeAnalyzer; @@ -11,7 +11,7 @@ export type MathFunction = { }; /** @typedef {import('./node.js').Node} Node */ /** @typedef {import('./types.js').CalculationType} CalculationType */ -/** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */ +/** @typedef {(args: Node[], name: string) => Node} MathSimplifier */ /** @typedef {(args: CalculationType[], nodes: Node[]) => CalculationType} TypeAnalyzer */ /** @typedef {{analyze: TypeAnalyzer, simplify?: MathSimplifier, isKeyword?: (node: Node, index: number) => boolean, calculation?: boolean}} MathFunction */ declare const mathFunctions: Map; @@ -26,8 +26,15 @@ declare function lookupMathFunction(name: string): { declare const QUICK_MATH_TEST: RegExp; /** @param {string} name @return {boolean} */ declare function isCalculationFunction(name: string): boolean; -/** @param {string} name @return {boolean} */ -declare function isSupportedMathFunction(name: string): boolean; /** @param {string} value @return {boolean} */ declare function hasPotentialMathFunction(value: string): boolean; -export { addTypes, failureType, hasPotentialMathFunction, isCalculationFunction, isFailure, isPercentage, isSupportedMathFunction, lookupMathFunction, mathFunctions, numberType, percentageType, QUICK_MATH_TEST, unknownType, }; +/** + * Token-level counterpart of `hasPotentialMathFunction`: whether any function + * token names a supported math function, as the scanner would find it. Math + * functions inside `url()` and similar are part of another token. + * + * @param {import('@csstools/css-tokenizer').CSSToken[]} tokens + * @return {boolean} + */ +declare function hasMathFunctionToken(tokens: import('@csstools/css-tokenizer').CSSToken[]): boolean; +export { addTypes, failureType, hasMathFunctionToken, hasPotentialMathFunction, isCalculationFunction, isFailure, isPercentage, lookupMathFunction, mathFunctions, numberType, percentageType, QUICK_MATH_TEST, unknownType, }; diff --git a/types/lib/keywords.d.ts b/types/lib/keywords.d.ts new file mode 100644 index 00000000..5d3bd1fb --- /dev/null +++ b/types/lib/keywords.d.ts @@ -0,0 +1,5 @@ +/** @typedef {import('./node.js').Node} Node */ +export type Node = import('./node.js').Node; +/** @param {Node} node @param {number} index @return {boolean} */ +declare function isClampKeyword(node: Node, index: number): boolean; +export { isClampKeyword }; diff --git a/types/lib/serialize/precision.d.ts b/types/lib/serialize/precision.d.ts index a889b410..bf40670c 100644 --- a/types/lib/serialize/precision.d.ts +++ b/types/lib/serialize/precision.d.ts @@ -52,13 +52,6 @@ declare function isDegenerate(v: number): boolean; declare function degenerateKeyword(v: number): string; /** @param {number} v @return {string} */ declare function serializeNumber(v: number): string; -/** - * @param {import('../node.js').Num | import('../node.js').Dim} node - * @param {number | false} precision - * @param {number} [value] - * @return {number} - */ -declare function roundedScalarValue(node: import('../node.js').Num | import('../node.js').Dim, precision: number | false, value?: number): number; /** * @param {import('../node.js').Num | import('../node.js').Dim} node * @param {string[]} buffer @@ -66,13 +59,6 @@ declare function roundedScalarValue(node: import('../node.js').Num | import('../ * @return {void} */ declare function emitRoundedScalar(node: import('../node.js').Num | import('../node.js').Dim, buffer: string[], value: number): void; -/** - * @param {import('../node.js').Num | import('../node.js').Dim} node - * @param {SerializeSession} session - * @param {number} [value] - * @return {number} - */ -declare function emitFiniteScalar(node: import('../node.js').Num | import('../node.js').Dim, session: SerializeSession, value?: number): number; /** * @param {import('../node.js').Num | import('../node.js').Dim} node * @param {SerializeSession} session @@ -99,4 +85,4 @@ declare function emitBareSignedZero(buffer: string[], node: import('../node.js') declare function isScalar(node: Node): node is import('../node.js').Num | import('../node.js').Dim; /** @param {Node} node @return {node is import('../node.js').Num | import('../node.js').Dim} */ declare function isSignedZero(node: Node): node is import('../node.js').Num | import('../node.js').Dim; -export { NOISE_FLOOR, divideByPowerOfTen, roundDecimal, round, isDegenerate, degenerateKeyword, serializeNumber, roundedScalarValue, emitRoundedScalar, emitFiniteScalar, emitScalar, emitSignedZero, emitBareSignedZero, isScalar, isSignedZero, }; +export { NOISE_FLOOR, divideByPowerOfTen, roundDecimal, round, isDegenerate, degenerateKeyword, serializeNumber, emitRoundedScalar, emitScalar, emitSignedZero, emitBareSignedZero, isScalar, isSignedZero, }; diff --git a/types/lib/simplify/abs.d.ts b/types/lib/simplify/abs.d.ts deleted file mode 100644 index b68cadb2..00000000 --- a/types/lib/simplify/abs.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type Node = import('../node.js').Node; -/** @typedef {import('../node.js').Node} Node */ -/** - * @param {Node[]} args - * @return {Node} - */ -declare function simplifyAbs(args: Node[]): Node; -export { simplifyAbs }; diff --git a/types/lib/simplify/call.d.ts b/types/lib/simplify/call.d.ts index 0154686c..3f1e66f2 100644 --- a/types/lib/simplify/call.d.ts +++ b/types/lib/simplify/call.d.ts @@ -1,9 +1,7 @@ export type Node = import('../node.js').Node; export type SimplifyFn = import('../simplify.js').SimplifyFn; -export type MathSimplifier = (name: string, args: Node[]) => Node; /** @typedef {import('../node.js').Node} Node */ /** @typedef {import('../simplify.js').SimplifyFn} SimplifyFn */ -/** @typedef {(name: string, args: Node[]) => Node} MathSimplifier */ /** * @param {Extract} node * @param {SimplifyFn} simplify diff --git a/types/lib/simplify/cancel.d.ts b/types/lib/simplify/cancel.d.ts index e32d79fc..5b1749bd 100644 --- a/types/lib/simplify/cancel.d.ts +++ b/types/lib/simplify/cancel.d.ts @@ -5,20 +5,18 @@ * denominator. Otherwise return null. Used by `simplifyProduct` for typed * division (§10.2). More complex cancellation (e.g. `px^2 / px`) is left * unreduced — consumers rarely rely on it and the spec doesn't require it. - * @template {{ exponent: 1 | -1, value: number, unit: string }} D - * @param {D[]} dims + * @param {{ exponent: 1 | -1, value: number, unit: string }[]} dims * @param {number | false} [precision] The denominator is converted into the * numerator's unit when that is exact at this precision (`1px / 1in` → * `1 / 96`); otherwise the numerator is converted (`1in / 1px` → `96 / 1`). - * @return {{ numerator: number, denominator: number, remaining: D[] } | null} + * @return {{ numerator: number, denominator: number } | null} */ -declare function tryCancelPair(dims: D[], precision?: number | false): { +}[], precision?: number | false): { numerator: number; denominator: number; - remaining: D[]; } | null; export { tryCancelPair }; diff --git a/types/lib/simplify/exp.d.ts b/types/lib/simplify/exp.d.ts deleted file mode 100644 index f81f36ce..00000000 --- a/types/lib/simplify/exp.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type Node = import('../node.js').Node; -/** @typedef {import('../node.js').Node} Node */ -/** - * @param {Node[]} args - * @return {Node} - */ -declare function simplifyExp(args: Node[]): Node; -export { simplifyExp }; diff --git a/types/lib/simplify/inverse-trig.d.ts b/types/lib/simplify/inverse-trig.d.ts index 25404221..5ed06e2d 100644 --- a/types/lib/simplify/inverse-trig.d.ts +++ b/types/lib/simplify/inverse-trig.d.ts @@ -1,8 +1,8 @@ export type Node = import('../node.js').Node; /** - * @param {'asin' | 'acos' | 'atan'} name * @param {Node[]} args + * @param {string} name * @return {Node} */ -declare function simplifyInverseTrig(name: 'asin' | 'acos' | 'atan', args: Node[]): Node; +declare function simplifyInverseTrig(args: Node[], name: string): Node; export { simplifyInverseTrig }; diff --git a/types/lib/simplify/log.d.ts b/types/lib/simplify/log.d.ts deleted file mode 100644 index dc63a1f2..00000000 --- a/types/lib/simplify/log.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type Node = import('../node.js').Node; -/** @typedef {import('../node.js').Node} Node */ -/** - * @param {Node[]} args - * @return {Node} - */ -declare function simplifyLog(args: Node[]): Node; -export { simplifyLog }; diff --git a/types/lib/simplify/min-max.d.ts b/types/lib/simplify/min-max.d.ts index bd75d6b5..4506b3f5 100644 --- a/types/lib/simplify/min-max.d.ts +++ b/types/lib/simplify/min-max.d.ts @@ -1,9 +1,9 @@ export type Node = import('../node.js').Node; /** @typedef {import('../node.js').Node} Node */ /** - * @param {string} name * @param {Node[]} args + * @param {string} name * @return {Node} */ -declare function simplifyMinMax(name: string, args: Node[]): Node; +declare function simplifyMinMax(args: Node[], name: string): Node; export { simplifyMinMax }; diff --git a/types/lib/simplify/mod-rem.d.ts b/types/lib/simplify/mod-rem.d.ts index aebaa424..88a0968a 100644 --- a/types/lib/simplify/mod-rem.d.ts +++ b/types/lib/simplify/mod-rem.d.ts @@ -1,9 +1,9 @@ export type Node = import('../node.js').Node; /** @typedef {import('../node.js').Node} Node */ /** - * @param {'mod' | 'rem'} name * @param {Node[]} args + * @param {string} name * @return {Node} */ -declare function simplifyModRem(name: 'mod' | 'rem', args: Node[]): Node; +declare function simplifyModRem(args: Node[], name: string): Node; export { simplifyModRem }; diff --git a/types/lib/simplify/numeric.d.ts b/types/lib/simplify/numeric.d.ts new file mode 100644 index 00000000..1a29c2ff --- /dev/null +++ b/types/lib/simplify/numeric.d.ts @@ -0,0 +1,40 @@ +export type Node = import('../node.js').Node; +/** @typedef {import('../node.js').Node} Node */ +/** + * `%` is property-context-dependent (§10.6), so it stays opaque. + * @param {Node[]} args + * @param {string} name + * @return {Node} + */ +declare function simplifyAbs(args: Node[], name: string): Node; +/** + * @param {Node[]} args + * @param {string} name + * @return {Node} + */ +declare function simplifySign(args: Node[], name: string): Node; +/** + * @param {Node[]} args + * @param {string} name + * @return {Node} + */ +declare function simplifySqrt(args: Node[], name: string): Node; +/** + * @param {Node[]} args + * @param {string} name + * @return {Node} + */ +declare function simplifyExp(args: Node[], name: string): Node; +/** + * @param {Node[]} args + * @param {string} name + * @return {Node} + */ +declare function simplifyPow(args: Node[], name: string): Node; +/** + * @param {Node[]} args + * @param {string} name + * @return {Node} + */ +declare function simplifyLog(args: Node[], name: string): Node; +export { simplifyAbs, simplifyExp, simplifyLog, simplifyPow, simplifySign, simplifySqrt, }; diff --git a/types/lib/simplify/pow.d.ts b/types/lib/simplify/pow.d.ts deleted file mode 100644 index a53c7e07..00000000 --- a/types/lib/simplify/pow.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type Node = import('../node.js').Node; -/** @typedef {import('../node.js').Node} Node */ -/** - * @param {Node[]} args - * @return {Node} - */ -declare function simplifyPow(args: Node[]): Node; -export { simplifyPow }; diff --git a/types/lib/simplify/sign.d.ts b/types/lib/simplify/sign.d.ts deleted file mode 100644 index 18664e2b..00000000 --- a/types/lib/simplify/sign.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type Node = import('../node.js').Node; -/** @typedef {import('../node.js').Node} Node */ -/** - * @param {Node[]} args - * @return {Node} - */ -declare function simplifySign(args: Node[]): Node; -export { simplifySign }; diff --git a/types/lib/simplify/sqrt.d.ts b/types/lib/simplify/sqrt.d.ts deleted file mode 100644 index 17693dab..00000000 --- a/types/lib/simplify/sqrt.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type Node = import('../node.js').Node; -/** @typedef {import('../node.js').Node} Node */ -/** - * @param {Node[]} args - * @return {Node} - */ -declare function simplifySqrt(args: Node[]): Node; -export { simplifySqrt }; diff --git a/types/lib/simplify/trig.d.ts b/types/lib/simplify/trig.d.ts index 8536d43c..d1d71089 100644 --- a/types/lib/simplify/trig.d.ts +++ b/types/lib/simplify/trig.d.ts @@ -1,8 +1,8 @@ export type Node = import('../node.js').Node; /** - * @param {'sin' | 'cos' | 'tan'} name * @param {Node[]} args + * @param {string} name * @return {Node} */ -declare function simplifyTrig(name: 'sin' | 'cos' | 'tan', args: Node[]): Node; +declare function simplifyTrig(args: Node[], name: string): Node; export { simplifyTrig };