Skip to content

Latest commit

 

History

History
201 lines (141 loc) · 10.3 KB

File metadata and controls

201 lines (141 loc) · 10.3 KB

postcss-calc v11 Roadmap

Spec-first rewrite of the plugin's internals, targeting CSS Values and Units Module Level 4 §10. The public plugin API (options, plugin shape) is preserved; every consumer on v10.x should upgrade without code changes.

Why v11

Current state (v10.1.1) has known gaps that the spec closes:

  • No min() / max() / clamp() support — treated as opaque blobs.
  • Typed division (calc(100vw / 1px)) throws "Cannot divide by 'px', number expected".
  • No folding of pi / e / infinity / NaN into arithmetic.
  • JISON-generated LR parser is hard to extend: every new math function requires editing the grammar and regenerating.
  • Ad-hoc reducer reinvents a weaker version of the spec's simplification algorithm, which means every edge case is discovered by users instead of derived from the standard.

v11 replaces the parser + reducer + stringifier with direct transcriptions of the spec. The existing 190 regression tests become the contract we ship against.

Non-goals (deliberate deferrals)

  • Relative color math (lch(from X l c calc(h + 80))) — channel-keyword scope is out-of-band; requires caller context the plugin doesn't have at transform time.
  • calc-size() — separate, evolving spec; continue to leave as-is.
  • Property-context percentage resolution — a PostCSS transform has no property-type info; spec explicitly allows plugins to leave percentages unresolved.
  • Transpiling modern math functions to legacy calc() — a separate concern; belongs in a companion plugin if wanted.

Architecture

Five modules, each one-to-one with a spec section. Every module file starts with the exact spec URL it implements, so a contributor (or reviewer) can check conformance section-by-section.

Module Spec section Purpose
tokenizer.ts §10.1 Numbers, dimensions (incl. 1px-2 single-token rule), idents, punct
parser.ts §10.1 Pratt parser + parselet registry → AST
type.ts §10.2 Calculation-type arithmetic (unit × power maps)
simplify.ts §10.10 Post-order simplification
serialize.ts §10.12 AST → CSS text with correct calc() wrapping

Current status

Done (on master, in playground/pratt/):

  • Pratt parser skeleton in TypeScript, parselet registries for prefix + infix.
  • Tokenizer for numbers / idents / punct.
  • Evaluator harness (number-only) proving the parser works for min/max/clamp/pow/trig/stepped-value/etc. as generic call nodes.
  • 48 tests covering precedence, associativity, unary stacking, function calls, and negative-syntax / pinned-behavior cases.
  • tsx runtime + strict tsconfig.json for the playground; pnpm test:pratt runs the TS suite directly.

The playground has validated the parser design on a clean slate. What remains is dimensions, the type system, simplification, and serialization — the three load-bearing spec pieces plus tokenizer extension.


Release plan

Each release is scoped to ship on its own. Earlier releases do not block on later ones.

v11.0 — Foundation

Goal: spec-correct parser + simplification core with min/max/clamp + typed division. Ships 80% of real-world value.

Scope:

  • Replace parser.jison with the Pratt parser from the playground, promoted into src/.
  • Drop jison-gho devDep; delete parser.jison; remove build script.
  • Dimension token rule including spec-required 1px-2 single-token behavior.
  • Calculation type system (§10.2).
  • simplify() core covering:
    • Nested calc() flattening (inner calc → bare parens).
    • Like-term combination across + / -.
    • Numeric folding for resolvable subtrees.
    • Unit conversion within a family (port src/lib/convertUnit.js).
    • Opaque-leaf preservation: var(), env(), attr(), and unknown functions are AST leaves that simplification flows around.
  • min(), max(), clamp() as first-class AST nodes with spec-correct reduction.
  • Typed division: <length> / <length> → <number>, <time> / <time> → <number>, etc.
  • Serializer matching current output shape for all v10.x regression cases.

Test coverage:

Suite File Count (estimate)
Regression (preserves v10 behavior) test/index.js 190 (existing)
min / max / clamp reduction test/v11-min-max-clamp.test.ts ~30
Typed division and unit arithmetic test/v11-typed-arithmetic.test.ts ~15
Opaque-leaf preservation (var(), env(), unknown fns) test/v11-opaque.test.ts ~15
Type-checking error cases (invalid mixes) test/v11-type-errors.test.ts ~10
WPT conformance subset test/wpt/calc-basic.test.ts ~25

Acceptance: all 190 existing + ~95 new ≈ ~285 tests green.


v11.1 — Stepped-value functions

Goal: spec coverage for the rounding / modulo family (§10.8).

Scope:

  • round([<rounding-strategy>,]? A, B?) with nearest | up | down | to-zero; default nearest; if B is omitted, rounds to 1 in A's type.
  • mod(A, B) — result sign follows divisor.
  • rem(A, B) — result sign follows dividend.
  • abs(A) — preserves type.
  • sign(A) — returns <number> regardless of input type.
  • Parser: one bespoke parselet for round to peek for a keyword first-argument.

Test coverage:

Suite File Count (estimate)
round with all four strategies × (A) / (A,B) × resolvable/opaque test/v11-round.test.ts ~20
mod / rem sign behavior and opaque passthrough test/v11-mod-rem.test.ts ~12
abs / sign incl. type preservation and sign-return-type test/v11-abs-sign.test.ts ~10
WPT conformance subset (round, mod, rem) test/wpt/round-mod-rem.test.ts ~25

Acceptance: adds ~67 tests, cumulative ~352.


v11.2 — Trigonometric functions

Goal: spec coverage for trig (§10.8) including angle-unit coercion.

Scope:

  • sin(A), cos(A), tan(A) — accept <angle> (deg/rad/grad/turn) or bare <number> (treated as radians per spec).
  • asin(A), acos(A), atan(A) — return <angle> in degrees.
  • atan2(Y, X) — returns <angle> in degrees; accepts any matching types.
  • Angle-unit normalization inside trig argument reduction.
  • Calc keyword pi folds into trig arguments (sin(pi / 2)1).

Test coverage:

Suite File Count (estimate)
sin / cos / tan × (deg, rad, grad, turn, bare number) test/v11-trig.test.ts ~25
asin / acos / atan — return type is degrees test/v11-trig-inverse.test.ts ~12
atan2 two-argument type matching test/v11-atan2.test.ts ~8
Trig with pi folding test/v11-trig-pi.test.ts ~10
WPT conformance subset (trig) test/wpt/trig.test.ts ~25

Acceptance: adds ~80 tests, cumulative ~432.


v11.3 — Exponential family + degenerate-numeric finalization

Goal: complete the math-function set (§10.8) and pin spec-correct serialization of degenerate numbers (§10.9, §10.12).

Scope:

  • pow(A, B), sqrt(A), hypot(A, B, …).
  • log(A) (natural log), log(A, B) (log base B), exp(A).
  • Unit handling: pow/sqrt/hypot resolve to bare numbers where required by spec; preserve through opaque subtrees.
  • Full <calc-keyword> arithmetic: e, pi, infinity, -infinity, NaN fold per IEEE-754.
  • Degenerate-numeric serialization: calc(1px / 0)calc(infinity * 1px); calc(0 / 0 * 1px)calc(NaN * 1px).
  • Case-sensitivity: NaN is case-sensitive (spec); all other keywords are case-insensitive.

Test coverage:

Suite File Count (estimate)
pow / sqrt / hypot with type handling test/v11-exponential.test.ts ~18
log / exp incl. two-arg log(a, b) test/v11-log-exp.test.ts ~10
Calc-keyword folding in arithmetic test/v11-calc-keywords.test.ts ~15
Degenerate numeric serialization (infinity, NaN) test/v11-degenerate.test.ts ~12
WPT conformance subset (exp family, keywords) test/wpt/exp-keywords.test.ts ~25

Acceptance: adds ~80 tests, cumulative ~512.


Testing strategy

Three tiers, each preserving a distinct guarantee:

  1. Regression — test/index.js (190 tests). The v10.x contract. Only ever extended, never deleted. A new v11 implementation is considered correct only if every one of these passes unchanged.
  2. Spec — test/v11-*.test.ts. One file per feature cluster. Every test has a // §10.x comment citing the spec paragraph it exercises. This makes correctness reviewable section-by-section.
  3. Conformance — test/wpt/*.test.ts. Cribbed from web-platform-tests css/css-values/ for each version's scope. Test names mirror upstream so drift is detectable. Keeps us honest about cross-implementation interop.

All tiers run under pnpm test via node --test (and tsx for the TS suites).

Running test-count totals

Version New tests (est.) Cumulative
baseline 190 190
v11.0 ~95 ~285
v11.1 ~67 ~352
v11.2 ~80 ~432
v11.3 ~80 ~512

Counts are forecasts, not commitments — they guide scope, not effort estimates.

Open questions to resolve before v11.0

  • Strict whitespace around +/-. Spec requires it; current plugin is permissive. Leaning strict, with a strict: false escape hatch for legacy inputs.
  • Output-format opt-out. Do we add spec: 'css-values-4' (default true) so projects that pattern-match on current serialization can stay on the old format for one major? Probably yes at low cost.
  • Deprecation of mediaQueries / selectors options. Carry forward unchanged — no reason to remove.

What will NOT change in v11

  • Plugin name, export, and pluginCreator.postcss = true.
  • Options surface (precision, preserve, warnWhenCannotResolve, mediaQueries, selectors).
  • peerDependencies on postcss ^8.
  • Semver: v11 means internal-breaking + output-difference in edge cases (typed division, degenerate numerics). No API-breaking changes for normal consumers.