Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9335394
chore: v3 scaffolding (configs, tsconfigs, lint, quality gates)
dkryaklin May 5, 2026
4784a49
feat(core): pratt tokenizer, parser, AST, type system
dkryaklin May 5, 2026
3a604ca
feat(core): simplifier (per-function fold modules) + serializer
dkryaklin May 5, 2026
ad0e981
feat(plugin): PostCSS adapter + public entry + v10 regression patches
dkryaklin May 5, 2026
c0a664f
test: helpers, unit, property, and conformance test suites
dkryaklin May 5, 2026
a6b7e50
test: real-world calc() corpus (8 frameworks + GitHub harvest, 21k+ e…
dkryaklin May 5, 2026
e9ed841
chore: tooling scripts (benchmarks, randomizer, harvester, issue-chec…
dkryaklin May 5, 2026
1e986b1
chore: address maintainer review (drop Node 20, consolidate tsconfigs…
dkryaklin May 5, 2026
68eb0f0
fix(core): spec-compliant -{ident-start} tokenization + selectors fix
dkryaklin May 23, 2026
333a7df
chore: address maintainer review feedback (round 2)
dkryaklin May 23, 2026
ce255f1
chore: drop internal version codenames from code/tests/docs
dkryaklin May 23, 2026
21fb5d2
chore: tighten package boundary + clean rebuild
dkryaklin May 23, 2026
69a11a0
chore: drop two dev scripts whose purpose ended with the rewrite
dkryaklin May 23, 2026
8fe6f28
refactor(core): delegate tokenization to @csstools/css-tokenizer
dkryaklin Jun 12, 2026
b9d9572
refactor: drop the three legacy-compat options per maintainer review
dkryaklin Jun 12, 2026
3142ae8
refactor: adopt cssnano source format — JS + JSDoc with strict checkJs
dkryaklin Jun 12, 2026
d8835ff
refactor: convert tests and scripts to plain JS, drop tsx
dkryaklin Jun 12, 2026
792c1ca
chore: address maintainer review feedback (round 3)
dkryaklin Jun 13, 2026
ddd5b3b
fix(round): strategy-dependent result for infinite step
dkryaklin Jun 13, 2026
4832f6b
chore: simplify package entry + test path construction
dkryaklin Jun 13, 2026
0df485c
chore: address maintainer review feedback (round 4)
dkryaklin Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
node_modules
.DS_Store
types/
test/fixtures/*.actual.css
src/parser.js
yarn-error.log
reports/
.stryker-tmp/
test/corpus/github/files/
test/corpus/github/.harvest-state.json
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,61 @@ div[data-size="calc(3*3)"] {
}
```

#### `onParseError`

Callback invoked when a `calc()` body fails to parse or simplify. Matches
[`@csstools/css-calc`][csstools-css-calc]'s shape:

```js
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.
- **Reciprocal conversion.** `calc(var(--x) / 2)` becomes
`calc(var(--x) * 0.5)`. The two are mathematically equivalent;
previously the division shape was kept.
- **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).
- **Calc unwrap (§10.6).** `calc(var(--foo))` becomes `var(--foo)` — a
`calc()` containing a single value is replaced by that value.
- **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
Expand Down Expand Up @@ -149,5 +204,5 @@ npm test
[PostCSS]: https://github.com/postcss
[PostCSS Calc]: https://github.com/postcss/postcss-calc
[PostCSS Custom Properties]: https://github.com/postcss/postcss-custom-properties
[tests]: src/__tests__/index.js
[tests]: test/index.js
[W3C calc() implementation]: https://www.w3.org/TR/css3-values/#calc-notation
47 changes: 46 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,63 @@
const js = require('@eslint/js');
const eslintConfigPrettier = require('eslint-config-prettier');
const sonarjs = require('eslint-plugin-sonarjs');

module.exports = [
{
ignores: ['src/parser.js'],
ignores: ['node_modules/**', '.stryker-tmp/**', 'reports/**', 'types/**'],
},
js.configs.recommended,
// SonarJS — code smells, cognitive complexity, dead stores, etc.
{
files: ['src/**/*.js', 'test/**/*.mjs', 'scripts/**/*.mjs'],
plugins: { sonarjs },
rules: {
...sonarjs.configs.recommended.rules,
// Math hot paths (simplify, tokenizer, foldConstArgs, naive oracles)
// have intrinsic complexity that's not extractable without diluting
// single-pass intent. Default 15 is too tight; 25 still flags real
// accidental complexity.
'sonarjs/cognitive-complexity': ['error', 25],
},
},
{
rules: {
// Underscore-prefix is the convention for "intentionally unused".
'no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
},
},
eslintConfigPrettier,
{
files: ['src/**/*.js', 'test/**/*.js', 'eslint.config.js'],
languageOptions: {
sourceType: 'commonjs',
globals: {
process: 'readonly',
require: 'readonly',
module: 'readonly',
__dirname: 'readonly',
Buffer: 'readonly',
},
},
rules: {
curly: 'error',
},
},
{
files: ['**/*.mjs'],
languageOptions: {
sourceType: 'module',
globals: {
console: 'readonly',
performance: 'readonly',
process: 'readonly',
fetch: 'readonly',
URL: 'readonly',
setTimeout: 'readonly',
},
},
},
];
16 changes: 10 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,35 @@
"LICENSE"
],
"scripts": {
"prepare": "pnpm run build && tsc",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the prepare script needed if we distribute precompiled JavaScript? Why would anyone to run build on install? https://docs.npmjs.com/cli/v11/using-npm/scripts#prepare-and-prepublish

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

"build": "jison ./parser.jison -o src/parser.js",
"lint": "eslint . && tsc",
"test": "node --test"
"test": "node --test",
"test:mutation": "stryker run"
},
"author": "Andy Jansson",
"license": "MIT",
"engines": {
"node": "^22.12 || ^24.11 || >=26.0"
},
"devDependencies": {
"@csstools/css-calc": "^3.2.0",
"@eslint/js": "^10.0.1",
"@rmenke/css-tokenizer-tests": "^1.2.0",
"@stryker-mutator/core": "^9.6.1",
"@stryker-mutator/typescript-checker": "^9.6.1",
"@types/node": "^26.1.1",
"eslint": "^10.6.0",
"eslint-config-prettier": "^10.1.8",
"jison-gho": "0.6.1-216",
"eslint-plugin-sonarjs": "^4.0.3",
"fast-check": "^4.7.0",
"postcss": "^8.5.16",
"prettier": "^3.9.5",
"typescript": "~6.0.3"
},
"dependencies": {
"postcss-selector-parser": "^7.1.4",
"@csstools/css-tokenizer": "^4.0.0",
"postcss-value-parser": "^4.2.0"
},
"peerDependencies": {
"postcss": "^8.5.16"
}
}
}
174 changes: 0 additions & 174 deletions parser.jison

This file was deleted.

Loading