Skip to content

Number align/gather/alignat rows per-row (amsmath semantics) - #81

Merged
mmcky merged 4 commits into
mainfrom
feat/per-row-align-numbering-73
Aug 13, 2026
Merged

Number align/gather/alignat rows per-row (amsmath semantics)#81
mmcky merged 4 commits into
mainfrom
feat/per-row-align-numbering-73

Conversation

@mmcky

@mmcky mmcky commented Aug 13, 2026

Copy link
Copy Markdown

Closes #73. Closes #80.

What this does

Under book-mode (and default) numbering, a multi-row align previously 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-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 (the whole environment is still one KaTeX layout).

amsmath semantics implemented exactly as scoped in #73:

Source Behavior
plain \\ row advances the counter, shows its number
\nonumber / \notag row renders without a number; counter does not advance
\tag{...} / \tag*{...} row displays the tag; counter does not advance
per-row \label{...} becomes that row's reference target — {eq} and \eqref resolve to the row's number
align* and friends unnumbered
equation / single-row environments unchanged block-level numbering (identical in amsmath)

Verified end to end on a probe book: two aligns (one with \nonumber + two row labels), an align*, a labeled $$ equation, and an align containing a nested \begin{cases} produce row numbers 1.1 1.2 1.3 (skip) 1.4, unnumbered starred rows, 1.5 continuing the counter, and 1.6 1.7 with the cases intact; {eq} references resolve to (1.3) / (1.4) and anchor to the block.

How it works

  1. KaTeX bump ^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 \tag in aligned environments (0.15 hard-errors Multiple \tag), \nonumber suppression, and depth-aware row layout. The 0.16 line keeps the 0.15-era CSS class vocabulary, so markup stays compatible with the katex@0.15.2 stylesheet 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/katex is dropped in favor of the types bundled with katex since 0.16.
  2. Scanner (myst-transforms/src/mathRows.ts, new): depth-aware row split of the environment body — \\ inside nested cases/matrix environments or brace groups is not a row boundary; \\* and \\[2em] separators are handled; per-row \label is stripped (mirroring existing behavior) and recorded with \nonumber/\tag flags on node.rows (typed in myst-spec ext). This is deliberately not built on transformSubEquations, 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 separate align*s (losing the axis), is lettered-subequation semantics, and splits with a depth-blind .split('\\\\').
  3. Enumeration (enumerate.ts): math nodes with rows advance the equation counter once per numbered row (book prefixes and previousCounts chaining apply automatically); row labels register as individual reference targets carrying the row's enumerator and the block's html_id; a block-level directive label resolves to the first numbered row. The block takes no enumerator, so the theme's block chrome doesn't draw a duplicate number.
  4. Rendering (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 express 1.15, and which the current 0.15.2 theme stylesheet couldn't draw anyway). Since enumeration runs after the initial render, a new renderRowNumberedMathTransform re-renders affected nodes in postProcessMdast once enumerators exist.
  5. LaTeX export (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

  • flalign and eqnarray are excluded: KaTeX has no support for either (they fail to render today; eqnarray keeps its existing rewrite-to-align* path).
  • Row numbers are rendered inside the KaTeX HTML, so unlike the block chrome they are not hash-links; row \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).
  • Downstream payoff (Multi-row align/gather rows collapse to one equation number — HTML numbering drifts from the PDF (needs per-row numbering upstream) claude-latex-to-myst#186): the converter can now pass align through verbatim instead of rewriting to aligned, and the printed-PDF equation numbers match the site.

Testing

  • 17 new tests in mathRows.spec.ts: scanner (nesting, separators, labels/tags/nonumber, alignat args, malformed input) and end-to-end enumeration + target resolution + rendered HTML.
  • Full monorepo suite green (72 turbo tasks; 405 tests in myst-transforms alone) — the KaTeX bump produced zero existing-test churn.
  • Probe book verified for both --site (AST enumerators, xref text, KaTeX tag HTML) and --tex (per-row labels re-emitted, no blank lines inside environments, nested cases intact).

🤖 Generated with Claude Code

mmcky and others added 2 commits August 13, 2026 14:09
…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>
Copilot AI lite review requested due to automatic review settings August 13, 2026 04:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/myst-transforms/src/mathRows.ts Outdated
Comment thread packages/myst-transforms/src/mathRows.spec.ts
mmcky and others added 2 commits August 13, 2026 15:02
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
mmcky merged commit 756a4d5 into main Aug 13, 2026
6 checks passed
@mmcky
mmcky deleted the feat/per-row-align-numbering-73 branch August 13, 2026 05:17
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants