Skip to content

html-math-method: mathml — native MathML for format: html (bd-3evfzwal) - #710

Draft
cscheid wants to merge 7 commits into
feature/bd-9z83tcv0-mathml-writerfrom
feature/bd-3evfzwal-mathml-stage
Draft

cscheid wants to merge 7 commits into
feature/bd-9z83tcv0-mathml-writerfrom
feature/bd-3evfzwal-mathml-stage

Conversation

@cscheid

@cscheid cscheid commented Sep 22, 2026

Copy link
Copy Markdown
Member

Phase 3 of claude-notes/plans/2026-09-21-equation-numbering-and-mathml.md (strand bd-3evfzwal). Stacked on the MathML writer PR (and so on #706 and #705). This branch also merges the Phase 1 equation-numbering branch (its own PR against main), which it needs for the sibling-label encoding; that part of the diff disappears once Phase 1 lands on main and the stack takes main.

What this adds

html-math-method: mathml renders natively. A new MathMlStage (after EquationNumberStage, before MathJsStage) converts every Inline::Math with quarto_math::convert(Target::MathMl) and replaces it by the usual span.math.inline|display wrapper around the <math> element. A document whose math all converts loads no JavaScript. Before this, the option was a silent no-op (raw TeX, no renderer).

Hybrid fallback (plan decision 2). An expression the converter rejects stays TeX; MathJsStage then loads MathJax only for those, and the Q-22-* diagnostic is reported as a located warning since the page renders. MathEngine::from_meta maps mathml to the MathJax default, inert unless math survives the MathML stage (pinned by the stage-order test and a unit test).

Also:

  • crate::ast_walk::for_each_inline_mut, the mutable inline walk both post-filter stages now share.
  • math-ml joins the q2-preview exclusion list (the preview keeps KaTeX client-side).
  • MathJax config fix: q2's skipHtmlTags overrode MathJax's defaults and dropped annotation/annotation-xml, so a loader brought in for a leftover expression re-typeset the \begin{…} text inside converted math's annotations (found in a headless browser as zero-size assistive MathML). Restored, with a unit test and an end-to-end assertion.
  • Docs: docs/guides/formats/html/math.qmd (methods table, the MathML mode, fallback, fonts, \cancel), linked from the Q-22-1 page.
  • Merge resolution: the Math.text_source extension for the appended tag moved from render_equation to EquationNumberStage::encode_number, with its test.

Tests

Written first: 5 end-to-end tests in math_mode_pipeline.rs (native MathML + sibling label + no loader; MathJax fallback with a Q-22-1 warning; math-free; math in headers/lists/tables/footnotes; two-page website), 5 stage unit tests, the fallback test in math-js, stage-list assertions (27 stages).

Verification

Full cargo xtask verify green (14248 Rust tests, ts-packages, hub-client build incl. the WASM leg with quarto-math linked in, hub-client tests). End-to-end through the real binary and headless Chromium (recorded in the plan): four native <math> elements with real heights, exactly one MathJax-typeset leftover, the (1) label right of the equation on the same row, one Q-22-1 warning pointing at \bogus in the .qmd.

🤖 Generated with Claude Code

cderv and others added 6 commits September 21, 2026 18:20
DIALBACK_PREAMBLE, DIALBACK_READ_UNTIL_EOF, and deno_dialback_child were
gated #[cfg(test)] only, but their only callers (proc_tests in this file,
and registry.rs's test_shutdown_all_kills_ts_engine) are unix-only. On
Windows those callers compile out, leaving the three items unused under
clippy's -D dead-code -- invisible on CI (Linux) and to Mac colleagues
since unix is true there. Tightened the gate to #[cfg(all(test, unix))]
to match. The dial-back mechanism itself is plain loopback TCP and works
fine on Windows; only proc_tests's kill -0 liveness probe and the crash
test's SIGKILL exit-code assertion are genuinely unix-only.

Also corrected proc_tests's stale header comment (claimed /bin/sleep and
sh -c dependencies that had already moved to portable cfg-pairs
elsewhere in the file) and added a cross-platform.md rule on matching a
test helper's cfg gate to its callers.
…-code

Fix Windows dead-code build failure in dial-back test helpers
`CrossrefRenderTransform` appended `\tag{N}` to every numbered
equation's TeX. `\tag` is an amsmath command only MathJax and KaTeX
read: Quarto 1 emits it solely for those two engines and uses
`\qquad(N)` otherwise, and a math-only converter (Pandoc's, or
quarto-math's for the coming `html-math-method: mathml`) rejects the
whole expression. The encoding is a presentation decision, so it moves
out of the format-agnostic transform.

- crossref-render now leaves the `Math` text byte-identical and records
  the number on the reserved span attribute `quarto-eq-number`
  (`EQ_NUMBER_ATTR` in `crossref/mod.rs`).
- New `EquationNumberStage` (after `UserFiltersStage::post`, before
  `CodeHighlightStage`) picks the encoding from the format and
  `html-math-method`: `\tag{N}` for MathJax/KaTeX, ` \qquad(N)` for
  `plain`/unknown, a sibling `span.quarto-eq-number` outside the math
  (plus a `quarto-eq-sibling-number` modifier class) for `mathml`,
  nothing for non-HTML formats. It removes the attribute in every case.
  Running after post filters is the point: a Lua filter can read,
  rewrite or delete `el.attributes["quarto-eq-number"]`.
- New `math_method.rs` parses `html-math-method` once (string and
  object forms); `MathEngine::from_meta` in math-js maps over it so the
  two stages cannot drift.
- New shared SCSS layer `equation-number.scss` (loaded at every HTML
  compile site and in `assemble_reveal_scss`, like `copy-code.scss`)
  lays the sibling label out at the right edge of the equation row.
- Design doc gains a "post-filter presentation slot" section; the Lua
  filters guide documents the attribute; `Equation.tsx`'s comment now
  points at the stage.

Tests (written first): crossref-render unit tests assert the attribute
instead of the tag; 13 stage unit tests (encodings, selection, walker);
13 end-to-end tests in `equation_numbering_pipeline.rs` covering every
encoding, revealjs, KaTeX, the object form, unlabelled math, sequential
numbering, and Lua post filters rewriting/deleting the number plus the
pin that pre filters never see it; quarto-sass asserts the layer in the
HTML and revealjs CSS.

Collateral test updates: HTML stage-list assertions (25 → 26 stages);
the `styles.css` byte-identity baseline in
`tests/fixtures/phase5-single-doc-baseline/expected_hashes.txt` is
re-captured with a dated note (additive layer; doc.html unchanged).

End-to-end (real binary, output inspected, recorded in the plan):
default → `\tag{1}` inside the math with MathJax loaded; `plain` →
` \qquad(1)`; `mathml` → `<span class="quarto-eq-number">(1)</span>`
after the math, no engine loaded; the compiled stylesheet carries the
three `.quarto-eq-sibling-number` rules.

Plan: claude-notes/plans/2026-09-21-equation-numbering-and-mathml.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Phase 3 (bd-3evfzwal) needs both the equation-numbering restructure
(Phase 1, off main) and the MathML writer (Phase 2, off the quarto-math
branch). Conflicts resolved:

- `crossref_render.rs`: `render_equation` keeps Phase 1's shape (the
  number rides on `quarto-eq-number`, the TeX is untouched). The
  `Math.text_source` extension that the quarto-math branch did for the
  appended `\tag{N}` moves to `EquationNumberStage::encode_number`
  (`append_to_tex`), which is the only place that appends to the text
  now; its provenance test moves with it, and crossref-render gains a
  test that the text and its mapping pass through untouched.
- the plan file: the Phase 2 copy (a superset).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
New `MathMlStage` (after `EquationNumberStage`, before `MathJsStage`):
under `html-math-method: mathml` every `Inline::Math` is converted with
`quarto_math::convert(Target::MathMl)` and replaced by the usual
`span.math.inline|display` wrapper around the `<math>` element, so a
document whose math all converts loads no JavaScript. Expressions the
converter rejects stay TeX; `MathJsStage` then loads MathJax only for
those (the hybrid fallback, plan decision 2), and the `Q-22-*`
diagnostic is reported as a located warning since the page renders.
Before this, `html-math-method: mathml` was a silent no-op (raw TeX,
no renderer).

- quarto-core depends on quarto-math; `MathEngine::from_meta` maps
  `mathml` to the MathJax default, which is inert unless math survives
  the MathML stage (pinned by the stage-order test and a unit test).
- New `crate::ast_walk::for_each_inline_mut`, the mutable inline walk
  both post-filter stages now share (the numbering stage's private
  walker is gone).
- `math-ml` joins the q2-preview exclusion list (the preview keeps
  KaTeX client-side).
- MathJax config fix: q2's `skipHtmlTags` overrode MathJax's defaults
  and dropped `annotation`/`annotation-xml`, so a loader brought in for
  a leftover expression re-typeset the `\begin{…}` text inside the
  converted math's annotations (found in a headless browser as
  zero-size assistive MathML). Restored with a unit test and an
  end-to-end assertion.
- Docs: `docs/guides/formats/html/math.qmd` (methods table, the
  MathML mode, fallback, fonts, `\cancel`), linked from `Q-22-1`.

Tests (written first): 5 end-to-end tests in `math_mode_pipeline.rs`
(native MathML + sibling label + no loader; MathJax fallback with a
`Q-22-1` warning; math-free; math in headers/lists/tables/footnotes;
two-page website), 5 stage unit tests, the fallback test in math-js,
and the stage-list assertions (27 stages). Three Phase 1 tests that
asserted the TeX stayed under `mathml` now assert the annotation.

End-to-end (real binary + headless Chromium, recorded in the plan):
four native `<math>` elements with real heights, exactly one
MathJax-typeset leftover, the `(1)` label right of the equation on the
same row, one `Q-22-1` warning pointing at `\bogus` in the `.qmd`.

Plan: claude-notes/plans/2026-09-21-equation-numbering-and-mathml.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@cscheid
cscheid added this pull request to stack #707 September 22, 2026 00:10
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@posit-snyk-bot

posit-snyk-bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

This branch has not been deployed

No deployments
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.

3 participants