Skip to content

Static math on by default: LaTeX → MathML at build time#639

Merged
srid merged 10 commits intomasterfrom
static-math
Apr 21, 2026
Merged

Static math on by default: LaTeX → MathML at build time#639
srid merged 10 commits intomasterfrom
static-math

Conversation

@srid
Copy link
Copy Markdown
Owner

@srid srid commented Apr 21, 2026

$...$ / $$...$$ 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, which swaps a grab-bag of per-feature Bools 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

nix run github:srid/emanote/static-math -- run -L docs

srid added 4 commits April 21, 2026 18:25
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.
@srid
Copy link
Copy Markdown
Owner Author

srid commented Apr 21, 2026

Hickey/Lowy Analysis

# Lens Finding Disposition
1 Lowy flake.nix branch pin is an untracked time-bomb Fixed in this PR
2 Lowy Config reading in View/Common.hs groups by feature, not by volatility axis Deferred
3 Lowy Default-on + KaTeX snippet removal are two volatility axes crammed in one release Deferred

Hickey rationale

No findings. The renderFeatures builder's two if/then/else arms looked like copy-paste-with-variation, but the agent judged extraction premature at two features. The Bool → RenderFeatures signature swap in Renderer.hs is a strict improvement over the prior lone-Bool shape.

(The /simplify pass flagged the if/then/else → bool swap separately; fixed in de311c08.)

Lowy rationale

L1 (fixed) — the flake.nix pin to github:srid/heist-extra/static-math is a moving target. A TODO comment now anchors the intent so a rebuild months from now either flags drift or prompts the replacement with a tagged ref. Commit 551eabb8.

L2 (deferred) — the renderFeatures builder lives in View/Common.hs and hand-rolls YAML→sum-type mapping for both features in one block. At a third backend this pattern starts to smell, and per-page override (as opposed to site-wide) has no home here. The right seam is readRenderingConfig :: Aeson.Value → RenderingConfig + toRenderFeatures :: RenderingConfig → Splices.RenderFeatures, but with only two features the abstraction is speculative. Revisit when the third backend lands or when per-page override is a concrete need.

L3 (deferred) — shipping staticMath: true and removing the js.katex snippet in the same release bundles "new default-on feature" with "break a documented extension point." The volatility-correct split would be: flip the default in this PR with the KaTeX snippet still present (maybe deprecation-tagged), then remove the snippet in a follow-up. The user explicitly asked for both in one release — accepting the coupling for now and leaning on docs/CHANGELOG to carry the migration.

srid added 2 commits April 21, 2026 18:37
- 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.
@srid
Copy link
Copy Markdown
Owner Author

srid commented Apr 21, 2026

image

srid added 2 commits April 21, 2026 18:43
- 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.
@srid srid marked this pull request as ready for review April 21, 2026 22:45
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.
@srid
Copy link
Copy Markdown
Owner Author

srid commented Apr 21, 2026

/do results

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 a vira ci at 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-only avoids replaying hickey/lowy/police every time.
  • police's 5m 44s came almost entirely from the /simplify lens catching the if/then/else → bool swap. Pre-running /simplify locally before /do would let the police step land in one pass.
  • Dev-server iteration caught two real bugs (\over unsupported by texmath; long KaTeX lines overflowing the <pre>) and a third (broken tips/js/syntax-highlighting link 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.
@srid srid merged commit ee88943 into master Apr 21, 2026
4 checks passed
@srid srid deleted the static-math branch April 21, 2026 23:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Render math as native MathML via Pandoc (offline support)

1 participant