Static math on by default: LaTeX → MathML at build time#639
Conversation
Closes #626. Uses the new RenderFeatures API from heist-extra PR #11: build-time LaTeX → MathML via texmath. Modern browsers render MathML natively, so the default config no longer needs any math JS bundle. - flake.nix: pin heist-extra to the static-math branch (until PR #11 merges and a tagged release cuts). - Pandoc/Renderer.hs: mkRenderCtxWithPandocRenderers takes RenderFeatures instead of a lone Bool flag. - View/Common.hs: build RenderFeatures from emanote.syntaxHighlighting and the new emanote.staticMath config key (both default true). - index.yaml: staticMath:true default, and drop the js.katex snippet (docs show how to paste KaTeX directly if a site still wants it). - docs/tips/js/math.md: rewritten; static is the primary story now.
Branch pins are moving targets; tag this one as deliberate development state so a rebuild months from now doesn't silently drift. Replace with a tagged release once heist-extra PR #11 merges.
Unanimous /simplify finding: if b then X else Y on plain Bool over nullary sum constructors is textbook 'bool' territory. Relude re-exports Data.Bool.bool; existing precedent at emanote/src/Emanote/Source/Patch.hs:124.
Adds a /math.html fixture and asserts the rendered page contains a <math> element. Guards the emanote.staticMath:true default — if the flag regresses, texmath won't fire and the locator will report 0 hits.
Hickey/Lowy Analysis
Hickey rationaleNo findings. The (The Lowy rationaleL1 (fixed) — the L2 (deferred) — the L3 (deferred) — shipping |
- texmath's TeX reader rejects the plain-TeX \over primitive; use
\frac{num}{den} instead so the Demo equation renders instead of
hitting the error span.
- The KaTeX loader lines in the opt-in code block were too long,
causing the <pre> to overflow its container horizontally. Break
each attribute onto its own line.
- CHANGELOG: collapse static-math entry to a single bullet.
- docs/tips/js/math.md: 'Demo' was H3 directly under H1 with no H2 between. Bump to H2 so the TOC hierarchy makes sense. - tests: split the math scenario into inline/display variants, assert the MathML namespace + display attribute, and add a negative scenario that no KaTeX asset is referenced by the default config. Fixture gains a $$…$$ display-math line. - .gitignore: add .claude/scheduled_tasks.lock (Claude Code local scheduler artifact, not source).
The page lives at docs/tips/syntax-highlighting.md, not under js/. This has been broken since the page moved; surfaced while verifying the adjacent Math link during static-math PR #639 review.
heist-extra PR #11 is merged, so flake.nix can go back to tracking the default branch. flake.lock bumps to heist-extra master head 13c70e9. With static math now default-on, the js.mathjax snippet in docs/guide/orgmode.yaml and docs/guide/html-template/external-links.md became redundant (texmath renders the equations at build time; MathJax would then scan a document with no remaining $…$ delimiters and no-op). Drop it. orgmode.yaml also gains a slug so the org page is reachable at /orgmode, matching other docs.
|
| Step | Status | Duration | Verification |
|---|---|---|---|
| sync | ✓ | 1s | git fetch ok; forge=github |
| research | ✓ | 30s | mapped integration points; one caller |
| branch | ✓ | 0s | static-math off origin/master |
| implement | ✓ | 1m 18s | RenderFeatures threaded through Renderer+Common; staticMath config added |
| check | ✓ | 1m 58s | cabal build all clean |
| docs | ✓ | 24s | rewrote math.md; CHANGELOG bullet |
| fmt | ✓ | 34s | fourmolu clean |
| commit | ✓ | 1m 1s | primary feature commit |
| hickey+lowy | ✓ | 2m 47s | 1 fixed (flake-pin TODO); 2 deferred (per-feature extraction / default-on coupling) |
| police | ✓ | 5m 44s | elegance: if/then/else → bool |
| test | ✓ | 1m 2s | e2e: inline + display MathML + no-KaTeX scenarios |
| create-pr | ✓ | 1m 7s | this PR |
| ci | ✓ | 14m 58s | vira ci signed off on HEAD |
| Total | 32m 40s |
Slowest step: ci (14m 58s) — vira runs nix build across both aarch64-darwin and x86_64-linux with caching disabled, so two serial emanote-binary builds carry almost all of that.
Optimization suggestions
- ci dominated at 46% of total. Running
vira ci --only-build(skips cache+signoff stages) against HEAD would cut the first build in half; pairing that with avira ciat the end (for the signoff) keeps green-ticks without paying the double cost between commits. - Two CI runs this PR (one after initial push, one after the heist-extra unpin). On a branch that's likely to accumulate follow-up commits,
--from ci-onlyavoids replaying hickey/lowy/police every time. - police's 5m 44s came almost entirely from the
/simplifylens catching theif/then/else → boolswap. Pre-running/simplifylocally before/dowould let the police step land in one pass. - Dev-server iteration caught two real bugs (
\overunsupported by texmath; long KaTeX lines overflowing the<pre>) and a third (brokentips/js/syntax-highlightinglink in orgmode.org). Those wouldn't have surfaced from CI alone — worth keeping the "spin up dev server, click through affected pages" loop after any static-rendering change.
Workflow completed at 2026-04-21T18:51:00Z.
…elector
HTML parsing places <math> in the MathML namespace automatically; the
xmlns=… attribute declaration is special and not visible to CSS
[xmlns=…] selectors, even though getAttribute('xmlns') returns the
value. Assert the namespace via element.namespaceURI inside
page.evaluate, matching the display attribute via the remaining CSS
bits. Also collapse CHANGELOG bullet.

$...$/$$...$$now render to MathML at build time, so a minimal Emanote site ships zero math JavaScript by default. Uses the newRenderFeaturesAPI from heist-extra#11, which swaps a grab-bag of per-featureBools onRenderCtxfor a single record with sum-typed backends (CodeBackend,MathBackend). Closes #626.Modern browsers (Firefox, Safari, Chrome ≥109) render MathML natively, so the default flips to
emanote.staticMath: true. Thejs.katexsnippet is removed from the default config — KaTeX is a minor enough alternative path that shipping a pinned CDN loader with SRI hashes that diverge from the KaTeX docs was doing readers a disservice. The MathJax snippet stays because it's the documented fallback. Existing sites referencing<snippet var="js.katex" />will need to paste the loader directly intopage.headHtml— see the rewrittendocs/tips/js/math.mdfor the exact lines.An e2e smoke scenario asserts every build produces a
<math display="inline">and a<math display="block">on the math fixture page, and that no KaTeX asset is referenced on a default-config page.Try it locally