UNPKG

postcss-calc

Version:
336 lines (258 loc) • 9.77 kB
# PostCSS Calc [<img src="https://postcss.github.io/postcss/logo.svg" alt="PostCSS" width="90" height="90" align="right">][PostCSS] [![NPM Version][npm-img]][npm-url] [![Support Chat][git-img]][git-url] [PostCSS Calc] lets you reduce `calc()` references whenever it's possible. When an expression mixes units that cannot be combined exactly (such as `px` and `em`), or contains values only known at runtime (such as `var()`), the unresolved part is left for the browser's [W3C calc() implementation]. ## Installation ```bash npm install postcss-calc ``` ## PostCSS usage ```js // dependencies var fs = require('fs'); var postcss = require('postcss'); var calc = require('postcss-calc'); // css to be processed var css = fs.readFileSync('input.css', 'utf8'); // process css var output = postcss().use(calc()).process(css).css; ``` Using this `input.css`: ```css h1 { font-size: calc(16px * 2); height: calc(100px - 2em); width: calc(2 * var(--base-width)); margin-bottom: calc(16px * 1.5); } ``` you will get: ```css h1 { font-size: calc(32px); height: calc(100px - 2em); width: calc(2 * var(--base-width)); margin-bottom: calc(24px); } ``` Checkout [tests] for more examples. ## Use the reducer without PostCSS For a single CSS component-value string, import the dedicated reducer entry point. It reduces `calc()` and the supported CSS math functions it finds while leaving all other text untouched. ```js import reduceCalc from 'postcss-calc/reduce'; reduceCalc('calc(1in + 10px)'); // => 'calc(106px)' reduceCalc('min(50px, calc(2 * 40px))'); // => 'calc(50px)' ``` It accepts `precision`, `unwrapSingleValue`, the deprecated `unwrapSingleNegativeNumber` alias, `warnWhenCannotResolve`, `onParseError`, and `onWarn`: ```js const result = reduceCalc('calc(100% + var(--gap))', { precision: false, warnWhenCannotResolve: true, onWarn: console.warn, onParseError(error, input) { console.error(`Invalid calculation: ${input}`, error); }, }); ``` Unlike the PostCSS plugin, the standalone reducer does not show warnings by default; provide `onParseError` and/or `onWarn` if you want diagnostics. ### Standalone reducer options #### `unwrapSingleValue` (default: `false`) Serializes a fully resolved finite scalar result without calculation syntax. Keep the default for standard CSS so the browser can perform range clamping and integer rounding. Set it to `true` for a non-standard context that requires a bare value, such as a selector: ```js reduceCalc('calc(5px - 10px)'); // => 'calc(-5px)' reduceCalc('calc(5px - 10px)', { unwrapSingleValue: true }); // => '-5px' reduceCalc('calc(1 / 2)', { unwrapSingleValue: true }); // => '.5' ``` The published `unwrapSingleNegativeNumber` option is retained as a deprecated alias for `unwrapSingleValue`. ### PostCSS plugin options These options apply when using the PostCSS plugin: ```js postcss().use(calc({ precision: 10 })); ``` #### `precision` (default: `5`) Allows you to define the precision for decimal numbers. Set it to `false` to disable rounding and preserve full IEEE-754 floating-point precision (emitting the shortest round-tripping decimal representation). Values below 1 keep `precision` significant digits (`.0123456px` becomes `.012346px`), and larger values keep `precision` decimals. Divisions and unit conversions are folded only when the result is exact at the precision; otherwise they stay symbolic and everything around them is still simplified: ```css .a { width: calc(100% / 4); } /* calc(25%) */ .b { width: calc(100% / 3); } /* calc(100% / 3), not 33.33333% */ .c { width: calc(1cm + 1px); } /* calc(1cm + 1px) */ .d { width: calc(1px + 1pt); } /* calc(1.75pt) */ ``` With `precision: false` nothing is rounded, so every division is folded. ```js var out = postcss() .use(calc({ precision: 10 })) .process(css).css; ``` #### `unwrapSingleValue` (default: `false`) Serializes fully resolved finite scalar results without calculation syntax. This can discard browser-applied range clamping or integer rounding. Selectors enable it automatically because selectors cannot contain `calc()`. #### `warnWhenCannotResolve` (default: `false`) Adds warnings when calc() are not reduced to a single value. ```js var out = postcss() .use(calc({ warnWhenCannotResolve: true })) .process(css).css; ``` #### `mediaQueries` (default: `false`) Allows calc() usage in media query parameters. ```js var out = postcss() .use(calc({ mediaQueries: true })) .process(css).css; ``` Example: ```css @media (min-width: calc(100px + 100px)) { div { width: 100px; } } ``` With `mediaQueries: true`, this becomes: ```css @media (min-width: 200px) { div { width: 100px; } } ``` #### `selectors` (default: `false`) Reduces `calc()` functions found in selectors. Selectors do not accept `calc()` functions, so the plugin replaces them with their reduced values. Finite negative and fractional unitless results are serialized as bare values because a selector cannot contain a `calc()` function; the plugin enables the `unwrapSingleValue` automatically for selectors. ```js var out = postcss() .use(calc({ selectors: true })) .process(css).css; ``` Example: ```css div:nth-child(calc(1 + 2)) { width: 100px; } ``` With `selectors: true`, this becomes `div:nth-child(3)`. #### `onParseError` Callback invoked when a `calc()` body fails to parse or simplify. Matches [`@csstools/css-calc`][csstools-css-calc]'s shape: ```js postcss().use( calc({ onParseError: (err, input) => { throw err; // or log, route to a different channel, etc. }, }) ); ``` When omitted, errors are reported via PostCSS `result.warn()` so the plugin never throws at the postcss level. ### Behavior differences from the legacy parser The legacy [jison][jison]-generated parser was replaced by a hand-written Pratt parser whose simplifier follows [CSS Values 4][css-values-4]. Most inputs reduce to identical output; the differences are spec-aligned or canonical-form decisions: - **Strict whitespace (§10.1).** `calc(2px+3px)` is invalid CSS (binary `+` / `-` require surrounding whitespace) and is preserved with a warning instead of reduced. - **Canonical operand order.** Commutative operands serialize numeric-first, matching [`@csstools/css-calc`][csstools-css-calc]: `calc(var(--foo) + 10px)` → `calc(10px + var(--foo))`. - **Zero buckets are kept.** `calc(100px - (100px - 100%))` → `calc(0px + 100%)`, not `100%` — [WPT calc-serialization-002][wpt-calc-serialization] requires the zero term because it carries the length-percentage type. - **Constant folding.** `calc(43 + pi)` now folds to `46.14159` (§10.7.1). Previously `pi` / `e` stayed symbolic. - **Distributive multiplication.** `calc(0.5 * (100vw - 10px))` becomes `calc(50vw - 5px)`. - **Unit case normalization.** `2PX` becomes `2px` (CSS units are case- insensitive; lowercase is conventional). - **Spec-style spaced operators.** `2px*var(--x)` is serialized as `2px * var(--x)`. The tokenizer is unaffected; only output spacing differs. - **Division by zero / by a unit.** `calc(500px/0)` reduces to `calc(infinity * 1px)` (§10.13) instead of throwing. Use `onParseError` if you want validation behavior. [css-values-4]: https://www.w3.org/TR/css-values-4/ [csstools-css-calc]: https://www.npmjs.com/package/@csstools/css-calc [wpt-calc-serialization]: https://github.com/web-platform-tests/wpt/blob/master/css/css-values/calc-serialization-002.html [jison]: https://github.com/zaach/jison --- ## Related PostCSS plugins To replace the value of CSS custom properties at build time, try [PostCSS Custom Properties]. ## Contributing Work on a branch, install dev-dependencies, respect coding style & run tests before submitting a bug fix or a feature. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guidelines. The project uses [pnpm](https://pnpm.io/). ```bash git clone git@github.com:postcss/postcss-calc.git git checkout -b patch-1 pnpm install pnpm test pnpm lint ``` The normal test run uses a deterministic structural sample of the harvested real-world corpus. Run the complete differential corpus before releases or when changing parsing/simplification behavior: ```bash pnpm test:corpus:full ``` Performance changes to the parser, analyzer, simplifier, or serializer should be checked with the benchmarks. `pnpm benchmark:arithmetic-chains` and `pnpm benchmark:nested-fallbacks` compare the working tree against `HEAD` using 20 fresh-process paired blocks by default and write git-ignored schema-v2 reports under `reports/benchmarks/`. Re-check a saved report with `pnpm benchmark:reanalyze <report>`. `pnpm benchmark:corpus` compares the whole pipeline with `@csstools/css-calc` on real-world expressions and is report-only. These benchmarks time only the parser (or, for the corpus, the whole reducer) on one machine, and a `pass` means "no regression detected at the declared margin", not "no change". Read [BENCHMARKS.md](BENCHMARKS.md) before interpreting results. The PostCSS benchmark awaits `postcss().process(...)`, and that await already triggers result stringification. It therefore does not add a redundant `result.css` access. ## [Changelog](CHANGELOG.md) ## [License](LICENSE) [git-img]: https://img.shields.io/badge/support-chat-blue.svg [git-url]: https://gitter.im/postcss/postcss [npm-img]: https://img.shields.io/npm/v/postcss-calc.svg [npm-url]: https://www.npmjs.com/package/postcss-calc [PostCSS]: https://github.com/postcss [PostCSS Calc]: https://github.com/postcss/postcss-calc [PostCSS Custom Properties]: https://github.com/postcss/postcss-custom-properties [tests]: test/ [W3C calc() implementation]: https://www.w3.org/TR/css3-values/#calc-notation