Skip to content

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

Merged
cscheid merged 13 commits into
mainfrom
feature/bd-3evfzwal-mathml-stage
Sep 23, 2026
Merged

cscheid merged 13 commits into
mainfrom
feature/bd-3evfzwal-mathml-stage

Conversation

@cscheid

@cscheid cscheid commented Sep 23, 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


Re-opened against main (replaces #710). The original PR was part of a GitHub stack, whose auto-rebase rewrote #709's branch when #706 merged and would do the same to this branch's merge history when #709 lands; the stack also refuses a base change. Same branch, same content, plus the post-#704 fix: MathMlStage now ignores html-math-method on non-HTML formats (a mathml document rendered --to docx crashed the vendored crossref/equations.lua), math-ml joins PANDOC_STAGE_EXCLUDED, and an end-to-end docx test pins it. Conflict assessment and merge order for the whole stack: claude-notes/plans/2026-09-23-merge-math-stack.md.

cscheid and others added 12 commits September 21, 2026 18:19
Adds `Target::MathMl` and `crates/quarto-math/src/mathml.rs`: `MathAst`
to a complete `<math>` element for `format: html` (consumed by the
`MathMlStage` of bd-3evfzwal). Written against MathML Core only:

- coarse runs split into `mi`/`mn`/`mo` via `split::split_run`, one
  letter per `mi`; `-` becomes U+2212, `'` a prime;
- symbols classify by alphabet (Greek → `mi`, uppercase Greek upright,
  relations/arrows → `mo`); bare delimiters get `stretchy="false"`,
  `\left…\right` fences stretch;
- big operators place limits by mode and the integral family keeps
  side limits, `\limits` pins `movablelimits="false"`; functions get
  U+2061 function application;
- `\mathbf`/`\mathbb`/`\mathcal`/… map characters into the Mathematical
  Alphanumeric Symbols block (holes included) since Core keeps only
  `mathvariant="normal"`; `\text` keeps edge spaces as NBSP;
- environments become `mtable` (fenced for matrices, `{` for cases,
  right/left `columnalign` for aligned); a top-level `\\` becomes a
  table of lines; `\cancel` emits `menclose` (plan decision 3).

The source TeX rides in `<semantics>` as an `application/x-tex`
annotation, so `Normalized` gains a `text` field.

Tests: a corpus-wide validity check (quick-xml walk: Core element
allowlist, fixed child counts, `mathvariant` only `normal`), 263
per-fixture snapshots, 13 structural tests, and unit tests for the
variant table and accents.

Also fixes a normalizer bug the snapshots exposed: primes were dropped
(`f'` → `f`; the committed Typst snapshot for `basic/prime` read
`f ( x ) = f ( x )`). mitex parses `f'` as an attachment without a
`^`/`_` operator and `attach` returned only the base. Now a prime is a
superscript `Sym`, repeated primes merge into `″`/`‴`/`⁗`, and `x'^2`
shares the superscript slot (5 tests in `normalize.rs`). The same
fixture shows mitex taking `= f` as the base of `f''` after a relation;
filed as bd-0mzhnxft.

Snapshots: 263 added (`integration__mathml__mathml__*`), 6 modified
(`basic/prime` and `scripts/prime-with-sup` for normalize, OMML and
Typst — the prime fix; the Typst one now shows the bd-0mzhnxft quirk
as `(= f)^(″)`).

Verification: `cargo xtask verify --skip-hub-build` green; quarto-math
checks for wasm32-unknown-unknown.

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>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…vfzwal-mathml-stage

equation_number.rs keeps this branch's version: the text_source-preserving
append_to_tex that #708's merge lifted from here, plus the shared
for_each_inline_mut walk.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A document with html-math-method: mathml rendered --to docx crashed the
Pandoc leg: the stage converted every equation to a RawInline before
pandoc ran, and the vendored crossref/equations.lua died on it
('attempt to concatenate a nil value (field text)', exit 83, Q-20-3).
#704 landed after this branch was cut, so its exact Pandoc stage-list
test only reddened at the merge.

The key is an HTML option that commonly lives in shared metadata of a
project rendering docx or typst too, so it is ignored silently on the
Pandoc leg — like EquationNumberStage's Writer encoding and like Quarto
1, which forwards it to pandoc whose non-HTML writers ignore it.

- applies_to(format, method) gates the stage on is_html_based(); unit test
- math-ml joins PANDOC_STAGE_EXCLUDED; the exact surviving-name list
  gains equation-number (a deliberate no-op there)
- end-to-end: a mathml-method document renders to docx with one native
  OMML equation numbered (1) and no MathML in the body
- plan: claude-notes/plans/2026-09-23-merge-math-stack.md records the
  stack's conflict assessment and merge order

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds `Target::MathMl` and `crates/quarto-math/src/mathml.rs`: `MathAst`
to a complete `<math>` element for `format: html` (consumed by the
`MathMlStage` of bd-3evfzwal). Written against MathML Core only:

- coarse runs split into `mi`/`mn`/`mo` via `split::split_run`, one
  letter per `mi`; `-` becomes U+2212, `'` a prime;
- symbols classify by alphabet (Greek → `mi`, uppercase Greek upright,
  relations/arrows → `mo`); bare delimiters get `stretchy="false"`,
  `\left…\right` fences stretch;
- big operators place limits by mode and the integral family keeps
  side limits, `\limits` pins `movablelimits="false"`; functions get
  U+2061 function application;
- `\mathbf`/`\mathbb`/`\mathcal`/… map characters into the Mathematical
  Alphanumeric Symbols block (holes included) since Core keeps only
  `mathvariant="normal"`; `\text` keeps edge spaces as NBSP;
- environments become `mtable` (fenced for matrices, `{` for cases,
  right/left `columnalign` for aligned); a top-level `\\` becomes a
  table of lines; `\cancel` emits `menclose` (plan decision 3).

The source TeX rides in `<semantics>` as an `application/x-tex`
annotation, so `Normalized` gains a `text` field.

Tests: a corpus-wide validity check (quick-xml walk: Core element
allowlist, fixed child counts, `mathvariant` only `normal`), 263
per-fixture snapshots, 13 structural tests, and unit tests for the
variant table and accents.

Also fixes a normalizer bug the snapshots exposed: primes were dropped
(`f'` → `f`; the committed Typst snapshot for `basic/prime` read
`f ( x ) = f ( x )`). mitex parses `f'` as an attachment without a
`^`/`_` operator and `attach` returned only the base. Now a prime is a
superscript `Sym`, repeated primes merge into `″`/`‴`/`⁗`, and `x'^2`
shares the superscript slot (5 tests in `normalize.rs`). The same
fixture shows mitex taking `= f` as the base of `f''` after a relation;
filed as bd-0mzhnxft.

Snapshots: 263 added (`integration__mathml__mathml__*`), 6 modified
(`basic/prime` and `scripts/prime-with-sup` for normalize, OMML and
Typst — the prime fix; the Typst one now shows the bd-0mzhnxft quirk
as `(= f)^(″)`).

Verification: `cargo xtask verify --skip-hub-build` green; quarto-math
checks for wasm32-unknown-unknown.

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Plan doc: taken from the Phase 3 branch, which already carries the
Phase 1 and Phase 2 checkbox updates both sides made.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@posit-snyk-bot

posit-snyk-bot commented Sep 23, 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.

@cscheid
cscheid merged commit d50a428 into main Sep 23, 2026
10 checks passed
@cscheid
cscheid deleted the feature/bd-3evfzwal-mathml-stage branch September 23, 2026 19:27
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.

2 participants