postcss-calc
Version:
PostCSS plugin to reduce calc()
135 lines (128 loc) • 4.05 kB
JavaScript
// PostCSS adapter over the standalone component-value reducer.
import reduceCalc from './reduce.js';
import { hasPotentialMathFunction } from './lib/functions.js';
/**
* @typedef {object} PostCssCalcOptions
* @property {number | false} [precision]
* @property {boolean} [warnWhenCannotResolve]
* @property {boolean} [mediaQueries]
* @property {boolean} [selectors]
* @property {boolean} [unwrapSingleValue] Serialize fully resolved finite scalar results without calculation syntax. Defaults to `false`.
* @property {(error: Error, input: string) => void} [onParseError] Invoked when parse/simplify throws. Replaces the default `result.warn`.
*/
/** @typedef {Required<Omit<PostCssCalcOptions, 'onParseError'>> & Pick<PostCssCalcOptions, 'onParseError'>} ResolvedOptions */
/**
* 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 {ResolvedOptions} options
* @param {import('postcss').Result} result
* @param {boolean} selectorContext
* @return {void}
*/
function applyTransform(
node,
current,
setProp,
options,
result,
selectorContext
) {
if (!hasPotentialMathFunction(current)) {
return;
}
const transformed = reduceCalc(current, {
precision: options.precision,
warnWhenCannotResolve: options.warnWhenCannotResolve,
onParseError:
options.onParseError ??
((error) => {
result.warn(error.message, { node });
}),
onWarn: (message) => {
result.warn(message, { plugin: 'postcss-calc', node });
},
// Selectors cannot contain calc(), so they always unwrap resolved values.
unwrapSingleValue: selectorContext || options.unwrapSingleValue,
});
if (transformed !== current) {
setProp(node, transformed);
}
}
/**
* @param {PostCssCalcOptions} [opts]
* @return {import('postcss').Plugin}
*/
function pluginCreator(opts) {
/** @type {ResolvedOptions} */
const options = {
warnWhenCannotResolve: false,
mediaQueries: false,
selectors: false,
unwrapSingleValue: false,
...opts,
// An explicit `precision: undefined` keeps the default.
precision: opts?.precision ?? 5,
};
return {
postcssPlugin: 'postcss-calc',
/**
* @param {import('postcss').Root} css
* @param {import('postcss').Helpers} helpers
*/
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) {
// 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
);
}
});
},
};
}
/** @type {true} */
pluginCreator.postcss = true;
export default /** @type import('postcss').PluginCreator<PostCssCalcOptions>*/ (
pluginCreator
);
export { pluginCreator as 'module.exports' };