Number align/gather/alignat rows per-row (amsmath semantics) - #81
Merged
Conversation
…jats KaTeX 0.16.0 added the amsmath per-row numbering machinery (per-row \tag, \nonumber suppression, depth-aware row handling in align/gather/alignat) that per-row equation numbering builds on. 0.16.x keeps the 0.15-era CSS class vocabulary, so markup stays compatible with the katex@0.15.2 stylesheet themes currently load; the 0.18 line renames CSS classes and must be a coordinated bump (see #80). @types/katex is dropped in favor of the types bundled with katex since 0.16. Part of #80 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Under book-mode (and default) numbering, a multi-row align previously took
a single block enumerator, so a LaTeX source numbering N equations rendered
as one number and every later equation drifted off the source numbering.
Each \\ row of a non-starred align/gather/alignat now advances the
equation counter and takes its own number, rendered as an explicit KaTeX
\tag at the row with the shared alignment axis preserved:
- \nonumber / \notag rows render without a number (counter unchanged)
- \tag{...} rows display the tag without advancing the counter
- per-row \label{...} becomes that row's reference target: {eq} and
\eqref resolve to the row's number and anchor to the block
- starred environments are unnumbered (amsmath parity)
- single-row environments keep block-level numbering semantics unchanged
- rows are split with a depth-aware scan: \\ inside nested cases/matrix
environments or brace groups is not a row boundary; \\* and \\[len]
separators are handled
Implementation: mathLabelTransform extracts per-row structure onto
node.rows (myst-spec ext type); enumerateTargetsTransform assigns row
enumerators and registers row targets; renderRowNumberedMathTransform
re-renders affected nodes after enumeration with tags injected (wired into
postProcessMdast); myst-to-tex re-emits per-row labels so LaTeX/PDF export
numbers and anchors rows natively. The mhchem import moves to the katex
0.16 exports subpath.
Closes #73
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR implements amsmath-style per-row equation numbering for multi-row align / gather / alignat display-math blocks, ensuring equation counters, \nonumber/\notag, \tag, and per-row \label behave like LaTeX while preserving the shared alignment axis in KaTeX output.
Changes:
- Add a depth-aware row scanner (
scanMathRows) and serializer (buildRowTex) to model per-row semantics for aligned environments. - Update enumeration and reference-target registration to advance counters per numbered row and register per-row labels as targets (while avoiding block-level duplicate numbering).
- Re-render affected math after enumeration by injecting explicit KaTeX
\tag{...}per enumerated row; re-emit per-row labels during LaTeX export; bump KaTeX to^0.16.21.
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/myst-transforms/src/mathRows.ts | New row scanner/builder for per-row numbering environments. |
| packages/myst-transforms/src/mathRows.spec.ts | New unit + end-to-end tests for scanning/enumeration/rendering behavior. |
| packages/myst-transforms/src/math.ts | Extract row structure, render row-numbered math via injected \tags, add post-enumeration re-render transform. |
| packages/myst-transforms/src/index.ts | Export new math row helpers and post-enumeration render transform. |
| packages/myst-transforms/src/enumerate.ts | Enumerate aligned envs per-row and register per-row label targets while avoiding block enumerators. |
| packages/myst-transforms/package.json | Bump katex to ^0.16.21 and drop @types/katex. |
| packages/myst-to-tex/src/math.ts | Re-inject stripped per-row \labels for LaTeX/PDF parity. |
| packages/myst-to-jats/package.json | Bump katex to ^0.16.21 and drop @types/katex. |
| packages/myst-spec/src/index.ts | Export new MathRow / MathRows types. |
| packages/myst-spec/src/ext.ts | Define MathRow / MathRows and add rows?: MathRows to Math nodes. |
| packages/myst-spec-ext/src/index.ts | Re-export MathRow / MathRows (deprecated aliases) for compatibility. |
| packages/myst-cli/src/process/mdast.ts | Run row-numbered math re-render after enumeration in postProcessMdast. |
| bun.lock | Lockfile updates for KaTeX bump and removed @types/katex. |
| .changeset/per-row-align-numbering.md | Changeset documenting new per-row numbering behavior and KaTeX bump. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
A regex capture of [^}]* truncated tags containing nested groups (e.g.
\tag{\text{A}}) at the first closing brace, producing unbalanced TeX as
the row enumerator. Use the existing readBraceGroup scanner instead, and
cover nested-brace tags in the scanner and enumeration tests.
Addresses Copilot review on #81
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mmcky
added a commit
that referenced
this pull request
Aug 13, 2026
#82) * chore(quantecon): track per-row equation numbering (PR #81) in VERSION.yml and UPSTREAM-PRS.yml Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(quantecon): cut qe-v9 metadata — tag entries 12-14 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 13, 2026
mmcky
added a commit
to QuantEcon/claude-latex-to-myst
that referenced
this pull request
Aug 13, 2026
QuantEcon/mystmd#73 was closed 2026-08-13 by QuantEcon/mystmd#81, released as qe-v9: a multi-row non-starred align/gather/alignat now numbers each \\ row, honouring per-row \label/\tag/\nonumber, WITH the shared & alignment axis preserved. That option did not exist when #186 was triaged, so the numbering-parity-vs-alignment tradeoff the issue is written around no longer applies. Our CI still pins qe-v8, so the gap is real only on the pin — the docs now say which is which rather than flatly claiming an upstream gap. ROADMAP: promote #186 out of "Upstream trackers" into an actionable item (adopt qe-v9, re-validate the align path, re-examine whether the #70/#46 split path and parts of the #192 token handling are still needed once the renderer models the same semantics); add #194 as an actionable item and #191 to the feature backlog; repoint the mystmd#74 row at mystmd#51, which it was closed as a duplicate of; correct the deep-learning pin note (it no longer tracks main) and the stale ~600-test figure; renumber the sections. CLAUDE.md: generalize the fence-stack settled decision to "structure-aware transforms use a single left-to-right scan carrying explicit state" and extend it to math bodies, since lesson 056 is explicitly the same failure mode in a different guise; name math.py's two module-level layers in Code layout so the next session reaches for the scan rather than adding a fourth flat regex; mark the _apply_* inventory non-exhaustive (it was five preprocessors out of date); fix a settled decision still naming the pre-rename tikz_overrides.py. Lessons are annotated, never rewritten: 055 and 056 get dated update notes pointing at mystmd#81 and stating the qe-v8 pin caveat, and 032 gains forward pointers to both, since they amend the split path it created. Also: LESSONS.md by-axis lists, the CHANGELOG #192 pointer and its three-book release gate, README counts and a pointer at a file that never existed, and LESSON_COVERAGE.md which had stopped at lesson 044. No conversion behaviour changes; 928 tests pass. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
mmcky
added a commit
to QuantEcon/claude-latex-to-myst
that referenced
this pull request
Aug 13, 2026
…ring (#186) (#201) * ci: bump MYSTMD_REF to qe-v9 (de92a9e) qe-v9 lands QuantEcon/mystmd#81 (per-row align/gather/alignat numbering with the & axis preserved) and #80 (KaTeX ^0.15.2 -> ^0.16.21). The bump is behaviourally inert on our current output, which is why no baseline moves with it. Measured, not assumed: the deep-learning book's built AST is identical under both renderers — 412 math nodes, 401 enumerated, and ZERO nodes carrying per-row data. The reason is that the converter rewrites align -> \begin{aligned}, and `aligned` is not in qe-v9's row-numbering set (align/gather/alignat), so the new feature never fires on anything we emit. dp1 and dp2 contain no non-starred row-numbering environments at all. So this commit buys nothing on its own; it is the prerequisite for the converter-side work in #186, where the value is. All three render gates pass against the existing qe-v8-era baselines, all three count baselines match, 928 tests pass. The workflow comment already says to re-validate the three build baselines in the same commit as a bump — that was done, and the answer was "no change", which is the finding rather than an omission. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: pass non-starred align through for per-row numbering (#186) The deep-learning book's HTML equation numbers now match the printed PDF exactly: 272/272, all twelve chapters. Previously 255/272 — every equation after the first collapsed block in a chapter was shifted. The converter rewrote a multi-row align to $$\begin{aligned}…$$, which mystmd numbers ONCE while LaTeX numbers each row. qe-v9 (QuantEcon/mystmd#81) numbers rows natively WITH the & axis preserved, which dissolved the tradeoff this was built around: the old choice was numbering parity OR alignment, and both are now available. So a non-starred align is emitted verbatim and the renderer numbers it. Labels now stay IN the body, colon-normalised in place. mystmd reads a row's reference target out of the math source and nowhere else, so the long-standing "extract every \label{}" rule (#30) is exactly backwards here — extracting leaves the row with no target at all. Leaving it verbatim is not enough either: normalizeLabel does not map : to -, so \label{eq:foo} must be rewritten in place to \label{eq-foo}, or all 25 align-internal labels in the deep-learning book dangle. A leading \begin{align}\label{X} goes back into the body too rather than becoming a separate (X)= anchor, which would collide with the row target. Narrowed to the shapes that used to collapse: everything _align_needs_split already claimed keeps the split path, because each exclusion is a measured regression under passthrough. With 2+ \tag mystmd emits the tag text raw, so {eq} renders (\text{(capital Euler)}); with 2+ \label every row inherits the block's html_id, so references scroll to the block rather than the row; \intertext becomes a hard render failure whose rows still consume numbers; a labelled align* would take a real number, so the #113 wrapper stays. The narrowing costs nothing — 9 of the 10 collapsing blocks carry no label, so the split-path set contributes 0 of the 17 recovered numbers. _can_passthrough_rows is deliberately conservative, because every way mystmd's own row scan can fail is silent: it drops node.rows, falls back to one number, and destroys every label after the first with no diagnostic. So we only pass through bodies our own depth-aware scan (#193) agrees about, and bail on % comments and tikzcd. Verified against ground truth outside the pipeline — the compiled PDF, not our own counter. dp1 and dp2 output is byte-identical; all three count baselines unchanged (block count is the same, only per-row enumerators moved, which count_myst does not measure); all three render gates pass. Requires qe-v9 or later; MYSTMD_REF was bumped in the preceding commit. Lesson 057. Golden case align_passthrough_row_numbering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: pin MYSTMD_REF to the qe-v9 COMMIT, not the annotated tag object Copilot review on #201. `git rev-parse qe-v9` returns the tag OBJECT sha for an annotated tag (de92a9e), not the commit it points at (24f6ae8) — so the pin was a tag object. The previous qe-v8 pin was a bare commit, so this also broke the established form. Corrected the diagnosis while fixing it: the stated failure does not occur for this workflow. Tested against GitHub with the workflow's own commands — `git fetch --depth 1 origin <tag-object-sha>` succeeds and `checkout FETCH_HEAD` dereferences to the commit, so CI would not have broken. The real exposure is GitHub's commit APIs, which do not resolve a tag sha, so it would break the moment the ref were handed to actions/checkout or a commit query. Added a note at the pin so the next bump doesn't repeat it — `git rev-parse qe-vN` is the obvious command and it is the wrong one. Verified: new sha is a commit, equals qe-v9^{commit}, contains PR #81, and a real fetch+checkout of it succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #73. Closes #80.
What this does
Under book-mode (and default) numbering, a multi-row
alignpreviously took a single block enumerator, so a LaTeX source that numbers N equations rendered as one number and every later equation drifted off the source's numbering. Each\\row of a non-starredalign/gather/alignatnow advances the equation counter and takes its own number, rendered as an explicit KaTeX\tagat the row — with the shared&alignment axis preserved (the whole environment is still one KaTeX layout).amsmath semantics implemented exactly as scoped in #73:
\\row\nonumber/\notagrow\tag{...}/\tag*{...}row\label{...}{eq}and\eqrefresolve to the row's numberalign*and friendsequation/ single-row environmentsVerified end to end on a probe book: two aligns (one with
\nonumber+ two row labels), analign*, a labeled$$equation, and an align containing a nested\begin{cases}produce row numbers1.1 1.2 1.3 (skip) 1.4, unnumbered starred rows,1.5continuing the counter, and1.6 1.7with thecasesintact;{eq}references resolve to(1.3)/(1.4)and anchor to the block.How it works
^0.15.2→^0.16.21(first commit; closes Bump KaTeX ^0.15.2 → ^0.16 (per-row equation numbering machinery; prerequisite for #73) #80). KaTeX 0.16.0 added the per-row machinery this builds on: per-row\tagin aligned environments (0.15 hard-errorsMultiple \tag),\nonumbersuppression, and depth-aware row layout. The 0.16 line keeps the 0.15-era CSS class vocabulary, so markup stays compatible with thekatex@0.15.2stylesheet themes load today (the 0.18 class-rename line remains a coordinated later move — see Bump KaTeX ^0.15.2 → ^0.16 (per-row equation numbering machinery; prerequisite for #73) #80 / quantecon-theme.mystmd#118). Full test suite passes on 0.16.47 with zero snapshot churn.@types/katexis dropped in favor of the types bundled with katex since 0.16.myst-transforms/src/mathRows.ts, new): depth-aware row split of the environment body —\\inside nestedcases/matrix environments or brace groups is not a row boundary;\\*and\\[2em]separators are handled; per-row\labelis stripped (mirroring existing behavior) and recorded with\nonumber/\tagflags onnode.rows(typed inmyst-specext). This is deliberately not built ontransformSubEquations, per the findings on Number align/gather rows per-row (amsmath semantics) — honor per-row \label, \tag, \nonumber #73 — that path re-wraps rows in separatealign*s (losing the axis), is lettered-subequation semantics, and splits with a depth-blind.split('\\\\').enumerate.ts): math nodes withrowsadvance the equation counter once per numbered row (book prefixes andpreviousCountschaining apply automatically); row labels register as individual reference targets carrying the row's enumerator and the block'shtml_id; a block-level directive label resolves to the first numbered row. The block takes noenumerator, so the theme's block chrome doesn't draw a duplicate number.math.ts): row-numbered nodes always render as the starred environment with explicit\tags injected for enumerated rows — no dependence on KaTeX's automatic CSS-counter numbers (which are page-global and can't express1.15, and which the current 0.15.2 theme stylesheet couldn't draw anyway). Since enumeration runs after the initial render, a newrenderRowNumberedMathTransformre-renders affected nodes inpostProcessMdastonce enumerators exist.myst-to-tex): per-row labels are re-emitted into the environment (resolving an existing TODO there), so PDF export numbers and anchors each row natively and agrees with the HTML output.Notes / limitations
flalignandeqnarrayare excluded: KaTeX has no support for either (they fail to render today;eqnarraykeeps its existing rewrite-to-align*path).\eqrefs anchor to the block (rows of the same environment are adjacent on screen).align*becoming unnumbered is a behavior change for documents that relied on a starred environment taking a block number — it now matches amsmath, per the issue's requested behavior. Labeled starred environments keep MyST semantics (labeled = numbered as a block).alignthrough verbatim instead of rewriting toaligned, and the printed-PDF equation numbers match the site.Testing
mathRows.spec.ts: scanner (nesting, separators, labels/tags/nonumber, alignat args, malformed input) and end-to-end enumeration + target resolution + rendered HTML.--site(AST enumerators, xref text, KaTeX tag HTML) and--tex(per-row labels re-emitted, no blank lines inside environments, nestedcasesintact).🤖 Generated with Claude Code