UNPKG

postcss-calc

Version:
377 lines (356 loc) • 9.99 kB
// Spec: https://www.w3.org/TR/css-values-4/#serialize-a-calculation-tree // Canonical AST expression emission: operator precedence, parenthesization, // sum terms, product factors, and function calls. import { serializeComponents } from '../opaque.js'; import { num } from '../node.js'; import { round, isDegenerate, degenerateKeyword, roundedScalarValue, emitRoundedScalar, emitScalar, emitSignedZero, isScalar, isSignedZero, isEffectivelyNegative, } from './precision.js'; /** * @typedef {import('../node.js').Node} Node * @typedef {import('../node.js').Sum} Sum * @typedef {import('../node.js').Product} Product * @typedef {import('../node.js').ProductFactor} ProductFactor * @typedef {import('./precision.js').SerializeSession} SerializeSession */ // The AST is canonical: sums and products are flat, except grouped nodes that // keep their parentheses, so these precedence levels cover every binary expression const SUM_PRECEDENCE = 1; const PRODUCT_PRECEDENCE = 2; const ATOMIC_PRECEDENCE = 3; // Negation (-1 * ...) binds more tightly than a sum but has the same atomic boundary // for deciding whether the operand needs parentheses. const UNARY_PRECEDENCE = ATOMIC_PRECEDENCE; /** @param {Node} node @return {number} */ function precedence(node) { if (node.type === 'Sum') return SUM_PRECEDENCE; if (node.type === 'Product') return PRODUCT_PRECEDENCE; return ATOMIC_PRECEDENCE; } /** * @param {Node} node * @param {number} parentPrecedence * @param {boolean} groupedRequired * @return {boolean} */ function needsParentheses(node, parentPrecedence, groupedRequired) { return ( precedence(node) < parentPrecedence || (node.type === 'Sum' && node.grouped === true && groupedRequired === true) || (node.type === 'Product' && node.grouped === true && parentPrecedence > 0) ); } /** * @param {Node} node * @param {SerializeSession} session * @param {number} [parentPrecedence] * @param {boolean} [groupedRequired] * @return {void} */ function emitNode( node, session, parentPrecedence = 0, groupedRequired = false ) { const parenthesized = needsParentheses( node, parentPrecedence, groupedRequired ); if (parenthesized) session.buffer.push('('); emitNodeBody(node, session); if (parenthesized) session.buffer.push(')'); } /** * @param {Node} node * @param {SerializeSession} session * @return {void} */ function emitNodeBody(node, session) { const buffer = session.buffer; switch (node.type) { case 'Num': case 'Dim': emitScalar(node, session); return; case 'Ident': buffer.push(node.rawName ?? node.name); return; case 'Call': emitCall(node, session); return; case 'OpaqueCall': emitOpaqueCall(node, session); return; case 'Sum': emitSum(node, session); return; case 'Product': emitProduct(node, session); return; } } /** * @param {import('../node.js').Call} node * @param {SerializeSession} session * @param {string} [callNameOverride] * @return {void} */ function emitCall(node, session, callNameOverride) { const buffer = session.buffer; buffer.push(callNameOverride ?? node.rawName ?? node.name, '('); for (let i = 0; i < node.args.length; i++) { if (i > 0) buffer.push(', '); emitNode(node.args[i], session); } buffer.push(')'); } /** * @param {import('../node.js').OpaqueCall} node * @param {SerializeSession} session * @param {string} [callNameOverride] * @return {void} */ function emitOpaqueCall(node, session, callNameOverride) { const buffer = session.buffer; buffer.push(callNameOverride ?? node.rawName ?? node.name, '('); serializeComponents(node.components, buffer, (child, childBuffer) => { emitNestedMathResult(child, session, childBuffer); }); buffer.push(')'); } /** * @param {import('../node.js').SumTerm} term * @param {1 | -1} multiplier * @param {number | false} precision * @return {1 | -1} */ function termSign(term, multiplier, precision) { let sign = /** @type {1 | -1} */ (term.sign * multiplier); if (isEffectivelyNegative(term.node, precision)) { sign = /** @type {1 | -1} */ (-sign); } return sign; } /** * @param {import('../node.js').SumTerm} term * @param {SerializeSession} session * @param {1 | -1} sign * @return {void} */ function emitSumTerm(term, session, sign) { if (sign === 1) { emitNode(term.node, session, SUM_PRECEDENCE, true); } else { emitLeadingNeg(term.node, session); } } /** * @param {import('../node.js').Num | import('../node.js').Dim} termNode * @param {1 | -1} sign * @param {number} i * @param {SerializeSession} session * @return {void} */ function emitScalarSumTerm(termNode, sign, i, session) { const buffer = session.buffer; const effectiveVal = sign * termNode.value; if (Object.is(effectiveVal, -0)) { if (i > 0) buffer.push(' + '); emitSignedZero(buffer, termNode); return; } if (isDegenerate(effectiveVal)) { if (i === 0 && sign === -1) buffer.push('-'); else if (i > 0) buffer.push(sign === 1 ? ' + ' : ' - '); emitScalar(termNode, session); return; } const rounded = round(effectiveVal, session.precision); if (rounded < 0) { buffer.push(i === 0 ? '-' : ' - '); emitRoundedScalar(termNode, buffer, -rounded); } else { if (i > 0) buffer.push(' + '); emitRoundedScalar(termNode, buffer, rounded); } } /** * @param {import('../node.js').SumTerm[]} terms * @param {SerializeSession} session * @param {1 | -1} [multiplier] * @return {void} */ function emitSumTerms(terms, session, multiplier = 1) { const buffer = session.buffer; for (let i = 0; i < terms.length; i++) { const term = terms[i]; const termNode = term.node; const sign = /** @type {1 | -1} */ (term.sign * multiplier); if (isScalar(termNode)) { emitScalarSumTerm(termNode, sign, i, session); continue; } if (i === 0) { emitSumTerm(term, session, sign); } else { buffer.push(sign === 1 ? ' + ' : ' - '); emitNode(termNode, session, SUM_PRECEDENCE, true); } } } /** @param {Sum} sum @param {SerializeSession} session @return {void} */ function emitSum(sum, session) { emitSumTerms(sum.terms, session); } /** * @param {Node} node * @param {SerializeSession} session * @return {void} */ function emitLeadingNeg(node, session) { if (node.type === 'Product' && !node.grouped) { if ( node.factors.length > 0 && node.factors[0].exponent === 1 && node.factors[0].node.type === 'Num' ) { const head = node.factors[0].node; emitProductFactors(node.factors, session, 1, -head.value, head); return; } emitProductFactors(node.factors, session, 0, -1, num(-1)); return; } session.buffer.push('-1 * '); emitNode(node, session, UNARY_PRECEDENCE, false); } /** * @param {ProductFactor[]} factors * @param {SerializeSession} session * @param {number} [start] * @param {number} [coefficientValue] * @param {import('../node.js').Num} [coefficientNode] * @return {void} */ function emitProductFactors( factors, session, start = 0, coefficientValue, coefficientNode ) { const buffer = session.buffer; let first = true; if (coefficientValue !== undefined && coefficientValue !== 1) { emitScalar( /** @type {import('../node.js').Num} */ (coefficientNode), session, coefficientValue ); first = false; } for (let i = start; i < factors.length; i++) { const factor = factors[i]; const factorNode = factor.node; if (first) { if (factor.exponent === -1) buffer.push('1 / '); if (isScalar(factorNode)) emitScalar(factorNode, session); else emitNode(factorNode, session, PRODUCT_PRECEDENCE); first = false; } else { buffer.push(factor.exponent === 1 ? ' * ' : ' / '); if (isScalar(factorNode)) emitScalar(factorNode, session); else emitNode(factorNode, session, PRODUCT_PRECEDENCE); } } } /** @param {Product} product @param {SerializeSession} session @return {void} */ function emitProduct(product, session) { emitProductFactors(product.factors, session); } /** * @param {Node} node * @param {SerializeSession} session * @param {string} wrapper * @return {void} */ function emitMathResult(node, session, wrapper) { if (isScalar(node)) { const scalarValue = roundedScalarValue(node, session.precision); const buffer = session.buffer; if (isDegenerate(scalarValue)) { buffer.push(wrapper, '(', degenerateKeyword(scalarValue)); if (node.type === 'Dim') buffer.push(' * 1', node.rawUnit ?? node.unit); buffer.push(')'); } else if (session.scalarPolicy === 'standard') { buffer.push(wrapper, '('); emitRoundedScalar(node, buffer, scalarValue); buffer.push(')'); } else { emitRoundedScalar(node, buffer, scalarValue); } return; } if ( node.type === 'Ident' || node.type === 'Call' || node.type === 'OpaqueCall' ) { emitNode(node, session); return; } if (node.type === 'Sum' && node.terms.length === 1) { session.buffer.push(wrapper, '('); emitLeadingNeg(node.terms[0].node, session); session.buffer.push(')'); return; } session.buffer.push(wrapper, '('); emitNode(node, session); session.buffer.push(')'); } /** * @param {Node} node * @param {SerializeSession} session * @param {string[]} [buffer] * @return {void} */ function emitNestedMathResult(node, session, buffer = session.buffer) { if (isSignedZero(node)) { emitSignedZero(buffer, node); return; } emitMathResult(node, session, 'calc'); } export { SUM_PRECEDENCE, PRODUCT_PRECEDENCE, ATOMIC_PRECEDENCE, UNARY_PRECEDENCE, precedence, needsParentheses, emitNode, emitCall, emitOpaqueCall, termSign, emitSumTerms, emitSum, emitLeadingNeg, emitProductFactors, emitProduct, emitMathResult, emitNestedMathResult, };