Conversation
Mirrors the skylighting pattern from #10: a new Heist.Extra.Splices.Pandoc.Texmath module converts LaTeX math to MathML at build time using texmath's readTeX + writeMathML, producing XmlHtml nodes browsers render natively without a client-side JS dependency. Intercept B.Math in Render.hs, gated by a new enableStaticMath field on RenderCtx (alongside enableSyntaxHighlighting). mkRenderCtx now takes the extra Bool.
Hickey H1 + Lowy A1: replace positional Bool flags (enableSyntaxHighlighting, enableStaticMath) with a named RenderFeatures record carrying sum-typed backends (CodeBackend = NoHighlighting | Skylighting, MathBackend = NoStaticMath | StaticMathML). mkRenderCtx now takes one RenderFeatures arg instead of two trailing Bools, so adding a third backend is zero-cost at call sites and the 'which backend' axis is named rather than positional. Hickey H2: extract renderMathPassthrough so the B.Math case is purely backend selection, with error handling as a single local case in the static arm.
Hickey H3: fromContent previously returned [] for XL.CRef, which would have silently dropped character references. In practice texmath's MathML writer emits Unicode directly so this branch is unreachable, but preserve the entity literally so any future regression is visible rather than invisible.
Earlier append via heredoc lost the trailing newline and glued .direnv and .do-results.json into one broken token. Restore both entries on their own lines.
/simplify reuse pass: serialize texmath's Text.XML.Light.Element via showElement, then let xmlhtml's own parseXML rebuild the tree. Drops the fromElement/fromAttr/fromContent helpers (~20 lines) and the now-moot CRef-handling branch. Mirrors the existing pattern at Heist/Extra/TemplateState.hs:69.
Hickey/Lowy Analysis
Hickey rationaleH1 — H2 — the H3 — Lowy rationaleL4 (fixed) — L5 (deferred) — intercepting L6 (deferred) — the flags are site-wide; per-page/per-block override is a deferred problem, not a now-problem, since the |
|
| Step | Status | Duration | Verification |
|---|---|---|---|
| sync | ✓ | 0s | git fetch ok; forge=github |
| research | ✓ | 1m 28s | texmath + xml API; studied skylighting PR #10 as template |
| branch | ✓ | 0s | static-math off origin/master |
| implement | ✓ | 58s | New Texmath.hs, B.Math interception, cabal/changelog updates |
| check | ✓ | 2m 9s | cabal build all clean |
| docs | ✓ | 5s | CHANGELOG updated |
| fmt | ✓ | 14s | fourmolu clean |
| commit | ✓ | 20s | fbc81d9 — primary feature commit |
| hickey+lowy | ✓ | 6m 14s | 3 findings fixed (8e29493, 86a0c83); 2 deferred |
| police | ✓ | 7m 8s | fact-check caught .gitignore bug (06b5d47); elegance dropped hand-rolled walker (9e00d21) |
| test | ✓ | 5s | no test suite |
| create-pr | ✓ | 54s | this PR |
| ci | ✓ | 17s | nix flake check green against HEAD |
| Total | 20m 48s |
Slowest step: police (7m 8s) — the /simplify elegance pass discovered the hand-rolled XL.Element → XmlHtml.Node walker could be collapsed into showElement + parseXML, which required a rebuild and reformat.
Optimization suggestions
- Police dominated (34% of total). The elegance sub-pass was the expensive part because it surfaced a structural rewrite mid-workflow. Pre-reading the
xmlhtmlAPI surface (or checking adjacent modules likeTemplateState.hs) during research would have landed theparseXMLapproach on the first pass and avoided the re-build/re-commit cycle. - Hickey+lowy at 6m 14s is within normal range for a structural-critique pass that actually found findings to commit. On a trivial PR,
--skip-setup+ deselecting this step would trim wall-clock. - No CI retries, no test flakes, no police retries: the 3-hop commit cascade (primary → hickey fixes → police fixes) landed cleanly on the first attempt of each.
Workflow completed at 2026-04-21T22:22:13Z.
**`$...$` / `$$...$$` now render to MathML at build time**, so a minimal Emanote site ships zero math JavaScript by default. Uses the new `RenderFeatures` API from [heist-extra#11](srid/heist-extra#11), which swaps a grab-bag of per-feature `Bool`s on `RenderCtx` for 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`. The `js.katex` snippet 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 into `page.headHtml` — see the rewritten `docs/tips/js/math.md` for 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 ```sh nix run github:srid/emanote/static-math -- run -L docs ```
LaTeX math now renders to MathML at build time, so documents with
$...$/$$...$$no longer need a KaTeX or MathJax JS bundle to become readable. Mirrors the skylighting approach from #10:texmathtokenizes and emits the tree at build time; the browser renders the resulting<math>element natively.Along the way the per-feature
Boolscheme inRenderCtxwas replaced with a singleRenderFeaturesrecord carrying sum-typed backends (CodeBackend = NoHighlighting | Skylighting,MathBackend = NoStaticMath | StaticMathML). A post-implement/hickey+/lowypass flagged that bolting another trailingBoolontomkRenderCtxfor every new feature would scale linearly with features and break call sites each time. Named record + sum-typed axes make adding a third backend (Typst, server-side KaTeX, whatever) zero-cost at the call site.The actual XML↔XmlHtml conversion went through a
/simplifypass too — the first cut hand-rolled a recursiveText.XML.Light.Element → XmlHtml.Nodewalker, which turned out to be unnecessary once you noticexmlhtml'sparseXMLwill happily re-parse the serialized MathML. Dropped ~20 lines and the CRef-handling branch.Not listed as a dependency bump because
0.5.0.0is still unreleased — the changelog folds this into the existing unreleased entry.