Book-style numbering: format, label, section-tagged TOC, auto-prefix#22
Merged
Conversation
Extends `NumberingItem` with `format`, `label`, and `reset_on_part`, and adds `parts`, `chapters`, `appendices` as well-known kinds on `Numbering`. Introduces the `book` opt-in flag (typed as a `NumberingItem` for index-signature compatibility; consumers check `numbering.book?.enabled === true`). The new fields are purely additive at this commit — no consumer wires them up yet. Validators normalize and golden-test all the new shapes.
Pure helper that renders an integer counter under any of the five formats from §3.2(b). Wired into: - formatHeadingEnumerator: new optional per-depth `formats` array, so the chapter/appendix prefix of a sub-heading (e.g. "A.1", "III.2.1") renders correctly. Omitted formats default to arabic so today's behaviour is preserved. - ReferenceState.incrementCount: per-kind `format` on figure / equation / table / etc. is applied when stringifying the main counter (and the subcontainer parent enumerator). Render-only — `format` does not touch counter state (§3.4(9)).
…(5)) Implements §3.2(h)'s heading cross-ref policy: when [](#target) resolves to a heading-type target and link text is omitted, prefer `numbering.heading_N.label` over `template`, with the heading text as the unnumbered fallback. - §3.2(h) label precedence: `label` wins over `template` for heading cross-refs, so `[](#ch1)` renders "Chapter 1" rather than "Section 1" once a project sets `numbering.heading_1.label: "Chapter %s"`. `template` continues to drive cross-refs when no label is set, so existing projects see no change. - #12 fix: a heading whose numbering is nominally enabled but which never received an enumerator (e.g. on a page with page-level `numbering: false`, or anything that would land in front/back matter once book mode is wired) now falls through to the heading text instead of substituting `%s` against UNKNOWN_REFERENCE_ENUMERATOR and rendering "Chapter ??". Scope-limited to heading-type targets. Figures, equations, tables, and other kinds keep today's labelling — extending the policy to non-heading kinds is PR #2 (§6).
The cross-ref to a page's H1 (e.g. \`[](#ch1)\` where \`ch1\` is the file slug) resolves through the file-target path, not the heading-target path. Without this change, that path always rendered the page title — so a book with \`numbering.heading_1.label: 'Chapter %s'\` still showed "Introduction" instead of "Chapter 1" for \`[](#ch1)\`. Authors had to invent extra anchors to get label rendering. Extend \`ReferenceState.resolveReferenceContent\` so the file-target branch applies the same \`label > template > title\` policy as inline headings, keyed off the page's \`heading_1\` numbering item and the page-level \`enumerator\` that the constructor already computes. Title remains the fallback when no label/template is set or the page is unnumbered (#12 case).
… §3.5(3))
When a ParentEntry in the TOC carries `section: chapters | appendices
| frontmatter | backmatter` (new field on myst-toc CommonEntry), each
descendant page inherits that section. With project
`numbering.book: true`, the section drives sensible heading_1
defaults: arabic + "Chapter %s" for chapters, Alph + "Appendix %s"
for appendices, and `enabled: false` (skip-semantic) for front/back
matter. The first page of each section gets `start: 1` so the first
appendix renders "A" rather than continuing the chapter sequence.
This is a deviation from PLAN.md §3.2(a)'s literal YAML, which puts
named sections as top-level keys under `toc: { format: jb-book }`.
That form is the legacy Sphinx `_toc.yml` surface (used only by
`myst upgrade`); MyST's `myst.yml toc:` is `MySTEntry[]`. Adding the
named keys at the myst.yml level needs a format-discriminated union
on the toc validator, which is invasive enough that PR #1 ships the
section-tagged primitive instead. A later PR can layer the named-key
form on top.
Authors now write:
numbering:
book: true
toc:
- file: index
- title: Appendices
section: appendices
children:
- file: app-a
The section subtree is logical, not structural — no folder is
emitted, no level bump — so app-a's H1 stays at heading_1 instead of
becoming heading_2.
#1, §3.5(4)) When `numbering.book.enabled` is true and the page itself is numbered (heading_1 ticks, so `state.enumerator` is set), every auto-prefixed kind picks up that enumerator as a leading prefix: ch1.md (heading_1 → "1"): Figure 1.1, 1.2; (1.1), (1.2); Table 1.1 app-a.md (heading_1 → "A"): Figure A.1, A.2; Theorem A.1; Exercise A.2 Each kind keeps its own per-page counter (today's reset-per-page behaviour), so figures/equations restart at the chapter/appendix boundary naturally. Pages without an enumerator (front-/backmatter, explicit `numbering: false`) get the flat global counter — no prefix. Authors opt out per-kind via the existing `continue: true` field (§3.4(6)): `numbering.figure.continue: true` keeps the figure counter flat across the whole book and drops the prefix. Auto-prefix kinds: figure, subfigure, equation, subequation, table, exercise, plus all `proof:*` / `prf:*` (theorem, lemma, proposition, …). The matcher uses a prefix test on `proof:` so new proof-family kinds added upstream are picked up automatically.
Closes the remaining test gaps in PR #1: - **§3.5(6) integration**: confirm a `proof` node with `kind: theorem` (which renders as kind "proof:theorem") and an `exercise` both pick up the chapter prefix in book mode. Asserts each proof-family kind keeps its own counter and only the chapter enumerator is shared — what the matcher claims, now exercised. - **§3.4(9) regression**: a chapter page sets `heading_1.format: Roman` in its frontmatter. The page renders "II" but the underlying counter stays 2, so the next chapter (without the override) continues at "3" rather than restarting or re-formatting. Uses the `previousCounts` chain across three ReferenceState instances to model the multi-page flow.
There was a problem hiding this comment.
Pull request overview
Adds opt-in book-style numbering across frontmatter schema, TOC parsing, project processing, and reference enumeration so book projects can render chapter/appendix labels and prefixed counters.
Changes:
- Extends numbering schema/types with book mode, counter formats, labels, and section-related keys.
- Adds formatted counters, heading label rendering, file-target label rendering, and book-mode auto-prefixing in reference enumeration.
- Adds TOC
sectionpropagation and CLI-side injection of per-section numbering defaults.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
packages/myst-transforms/src/index.ts |
Exports new counter formatting helpers. |
packages/myst-transforms/src/enumerate.ts |
Implements formatted counters, heading labels, file-target labels, and book-mode auto-prefixing. |
packages/myst-transforms/src/enumerate.spec.ts |
Adds coverage for counter formats, labels, and auto-prefix behavior. |
packages/myst-toc/src/types.ts |
Adds BookSection and section metadata to TOC entries. |
packages/myst-toc/src/toc.ts |
Validates the new TOC section field. |
packages/myst-toc/tests/examples.spec.ts |
Adds TOC validation tests for book sections. |
packages/myst-frontmatter/src/numbering/types.ts |
Extends numbering item and numbering map types. |
packages/myst-frontmatter/src/numbering/validators.ts |
Validates new numbering keys and item options. |
packages/myst-frontmatter/src/numbering/numbering.yml |
Adds golden cases for new numbering options. |
packages/myst-cli/src/project/types.ts |
Carries inherited book section metadata on local project entries. |
packages/myst-cli/src/project/fromTOC.ts |
Propagates TOC section metadata into project pages. |
packages/myst-cli/src/process/site.ts |
Computes section metadata and passes it into mdast transforms. |
packages/myst-cli/src/process/mdast.ts |
Injects book-section numbering defaults before document transforms. |
packages/myst-cli/src/process/bookSection.spec.ts |
Adds tests for injected book-section defaults. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
CI `lint:format` job flagged these two files. Pure prettier --write output, no semantic change. Tests stay green (345/345).
Three errors surfaced by the `lint` workflow's `eslint` job (the `lint:format` job, which is prettier-based, went green after db7ddea): - mdast.ts: inline `import('myst-toc').BookSection` annotations violate `@typescript-eslint/consistent-type-imports`. Hoist to a top-level `import type { BookSection } from 'myst-toc'` and use the bare name in the two annotations. - site.ts: `sawAnyInSection` is only `.add()`-ed, never reassigned, so `prefer-const` flags it. Change `let` → `const`. 273/273 myst-cli tests still pass. Local `npm run lint` now reports 0 errors (12 pre-existing warnings unrelated to this PR).
Three substantive findings on the PR, plus one type-import lint fixup discovered along the way. **#1 — File-target labels for nested pages (enumerate.ts:633)** The file-target branch of `resolveReferenceContent` unconditionally read `numbering.heading_1` when applying the chapter/appendix label. A nested TOC page (offset > 0) has its title enumerator generated at `heading_${offset + 1}`, so reading heading_1 produced "Chapter 1.1" where the page is actually a heading_2 subsection. Use the page's offset to pick the right depth. Adds a regression test using `MultiPageReferenceResolver` with an offset=1 file target. **#2 — `numbering.chapters` / `numbering.appendices` were inert** PR #1 accepted these schema keys but `injectBookSectionDefaults` never read them — so setting `numbering.chapters.label: "Module %s"` or `numbering.appendices.format: roman` had no effect. The keys were live config that did nothing. Now the injection merges the section's block into `heading_1` between page frontmatter and the hardcoded defaults. Precedence: page heading_1 > numbering.<section> > hardcoded fallback All via `??=` so explicit author values always win. Adds three spec cases pinning the precedence. **#3 — section-tagged `FileEntry` should still bump children's level** The same-level recursion fired for any entry with `section:`, including a `FileEntry` like `{ file: ch1.md, section: chapters, children: [...] }`. Only section-only `ParentEntry` groups are meant to be logical wrappers; a section-tagged file is still a structural parent and its sub-pages should land at the next level. Narrowed the condition with an `isFile` check. **Drive-by — `Math` type import shadowed the JS global** CI surfaced `consistent-type-imports: Type import "Math" is used by decorator metadata`. The type-only import of `Math` from myst-spec-ext shadowed `Math.floor` (used in `formatCounter`). Renamed to `MathNode` and updated the one type reference. All four touched packages green: myst-frontmatter 499/499, myst-transforms 346/346 (+1 nested-file-target), myst-toc 34/34, myst-cli 276/276 (+3 chapters/appendices precedence cases).
4 tasks
4 tasks
mmcky
added a commit
that referenced
this pull request
May 14, 2026
…ker (#24) Records which QuantEcon-specific features are merged into this fork's main, identified by a `qe-vN` tag that also exists as a git tag on the corresponding squash-merge commit. Doubles as a tracker for upstream PR status — each feature has an `upstream` block that fills in as features get upstreamed to jupyter-book/mystmd. Populated with the two features already on main: - qe-v1: feature/myst-to-ipynb (#16) - qe-v2: feature/book-numbering (#22) Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This was referenced May 14, 2026
mmcky
added a commit
that referenced
this pull request
May 15, 2026
…ERSION.yml Two tracker files now record orthogonal facts, cross-referenced by squash-commit SHA: - VERSION.yml answers "what's in our main?" — diagnostic identifier for the integration build. Drops its `upstream:` block (was an always-`pending` placeholder). - UPSTREAM-PRS.yml answers "how do we plan to ship those squash commits upstream?" — bundles related squashes into logical upstream PR candidates (so a feature spanning multiple commits, or two features that form one upstream story, can be tracked as a single unit), records dependency order for cherry-pick, tracks status. Seeds UPSTREAM-PRS.yml with three candidates: myst-to-ipynb (#16), book-mode-with-section-scope (#22 + #28, bundled — they form one coherent upstream story since #28 builds on #22's auto-prefix hooks), and book-parts (#26, planned). Adds the missing book-proof-scope entry to VERSION.yml's merged_features. README updated to partition the two files' roles and reference UPSTREAM-PRS.yml from the cherry-pick workflow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
mmcky
added a commit
that referenced
this pull request
May 15, 2026
* docs(quantecon): cherry-pick upstream model, batched tags
Replace the "preserve feature branch, push as upstream PR" workflow
with "delete feature branch after merge; cherry-pick main squash
commits onto a fresh branch off upstream/main when upstreaming."
Why:
- Feature branches that depend on prior QuantEcon-only features were
awkward to branch from upstream/main (had to carry the dependency
inline or wait for it to upstream first).
- Branching from main lets dependencies just work; the squash commit
on main is already the upstream-ready artifact.
- Cherry-picking at upstream-PR time lets us bundle related squashes
into a coherent story ("book mode + section scope") when that's the
right unit for upstream review, or split them.
Tagging also moves from per-PR to batched: a `qe-vN` tag is cut at a
checkpoint covering multiple features, not on each merge.
Updates the diagram, branching-model table, "Develop a new feature",
"Opening an upstream PR" (now cherry-pick-driven), "When upstream
merges", "Resolving merge conflicts between features", and the
VERSION.yml comment block. Adds an `upstream/<topic>` branch type to
the branching model.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(quantecon): add UPSTREAM-PRS.yml; split upstream tracking from VERSION.yml
Two tracker files now record orthogonal facts, cross-referenced by
squash-commit SHA:
- VERSION.yml answers "what's in our main?" — diagnostic identifier
for the integration build. Drops its `upstream:` block (was an
always-`pending` placeholder).
- UPSTREAM-PRS.yml answers "how do we plan to ship those squash
commits upstream?" — bundles related squashes into logical upstream
PR candidates (so a feature spanning multiple commits, or two
features that form one upstream story, can be tracked as a single
unit), records dependency order for cherry-pick, tracks status.
Seeds UPSTREAM-PRS.yml with three candidates: myst-to-ipynb (#16),
book-mode-with-section-scope (#22 + #28, bundled — they form one
coherent upstream story since #28 builds on #22's auto-prefix hooks),
and book-parts (#26, planned). Adds the missing book-proof-scope
entry to VERSION.yml's merged_features.
README updated to partition the two files' roles and reference
UPSTREAM-PRS.yml from the cherry-pick workflow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(quantecon): address Copilot feedback on PR #30
- VERSION.yml header: remove double-accounting in the tag-cut steps.
Per-PR landing already appends each feature to merged_features with
tag: null; the tag-cut workflow just *sets* the tag field on those
pre-existing entries and bumps qe_version. The old wording said
"Append each newly-included feature" which contradicted the
per-PR-landing description below it.
- README "About this folder": the allow-list now contains README.md,
VERSION.yml, UPSTREAM-PRS.yml, and .gitignore itself — the original
text only mentioned the first two.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
mmcky
added a commit
that referenced
this pull request
May 18, 2026
* feat(book): full title→heading chain in injectBookSectionDefaults (#25) Wires `title.enabled`, `heading_2.enabled`, and `heading_3.enabled` per `section:` tag in `injectBookSectionDefaults` so the chapter-prefix machinery in enumerate.ts composes without authors needing project-level workarounds. Background: `enumerate.ts`'s book-mode auto-prefix only fires when the page-title heading is itself numbered (it provides `this.enumerator` for figures/theorems/sub-headings to prepend). The page-title heading in turn requires `numbering.title.enabled: true`. PR #22 wired `heading_1.enabled` per section but missed `title`, so chapter pages rendered `Figure 1` / `Section 1` instead of `Figure 1.1` / `Section 1.1` on a default `numbering.book: true` config. Authors hit this on pandoc- generated chapter files (LaTeX `\chapter{}` → MD `# Title`) where they cannot add per-page frontmatter. This patch: - chapters / appendices: seeds `title.enabled ??= true`, `heading_2.enabled ??= true`, `heading_3.enabled ??= true` alongside the existing `heading_1` defaults. - frontmatter / backmatter: seeds those same keys to `??= false` so a project-level `heading_2.enabled: true` (often used so chapters number correctly) doesn't leak through and number the preface's `##` headings. - Changes the legacy hard `h1.enabled = false` on frontmatter/backmatter to `??= false` for symmetry — a page that explicitly wants its title numbered (e.g. "Chapter 0: Preface") can now override the section default. The hard assignment had made this impossible. All assignments use `??=` so the layered precedence (page > project > section default > hardcoded) is preserved. Adds 10 new tests covering the new wiring and the per-page override paths. Closes #25. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * docs(book): align section-defaults claims with §3.5(g) precedence Comment-and-changeset cleanup of injectBookSectionDefaults after a deep review confirmed the documented precedence chain (page > project > section > built-in) matches the code. The previous claim that frontmatter/backmatter section defaults block project-level numbering leakage contradicted that chain — `injectBookSectionDefaults` runs after `processPageFrontmatter` merges project numbering into the page, so `??=` no-ops for any key the project explicitly set. Reframe the comments and changeset around the real story: the new chapter-side wiring (title/heading_2/heading_3 seeded true on chapter pages) means authors no longer need a project-level `heading_2.enabled: true` workaround, so the leak scenario stops happening in the common case. A project that explicitly enables those depths still wins — by design, per §3.5(g). Also replace the `start: 0` page-override test (rejected by the `min: 1` validator at validators.ts:148) with a valid override using `title.enabled: true`. No code logic changes, no signature changes. All 19 bookSection tests pass; full myst-cli suite 293/293 green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * feat(book): extend section-defaults chain to heading_6 (#36) Extend `injectBookSectionDefaults` to wire heading_4/5/6 alongside the existing title/heading_1/heading_2/heading_3 chain. With the depth-1–3 fix in place, authors who removed the project-level workaround discovered that `####`+ headings on chapter pages rendered unnumbered (verified against book-dp1 — ch_intro had H4s rendering with enum=None instead of `1.1.1.1`). mystmd's numbering schema caps at heading_6 (matches HTML h1–h6, see myst-frontmatter numbering/types.ts), so wiring 1–6 is exhaustive rather than arbitrary — there is no heading_7 to forget. All assignments stay `??=` per §3.5(g) precedence (page > project > section > built-in), so a project or page that explicitly disables a deeper level still wins. Tests updated to assert the full 1–6 chain on chapters/appendices and the matching full disable on frontmatter/backmatter. All 19 bookSection tests pass; full myst-cli suite 293/293 green. Closes #36. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(book): no-op injectBookSectionDefaults for unhandled sections Move all numbering mutations inside the `switch` cases so any `BookSection` value not explicitly handled (currently only `parts`, which is divider-only) is a clean no-op past the gate checks. Previously the function pre-initialized `heading_1` and (after the heading_6 extension in 33ae25c) `title`/`heading_2…heading_6` to empty `{}` objects before the switch, which leaked into the resolved frontmatter for any page tagged `section: parts`. The `firstInSection` branch could also have set `h1.start = 1` on such pages. Both are now contained to the chapters/appendices and frontmatter/backmatter cases that actually need them. Caught by Copilot review on PR #33. Add a regression test pinning the no-op behavior for `section: parts` with `firstInSection: true` — the path that previously leaked the most. 20 bookSection tests pass; full myst-cli suite 294/294 green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.
Summary
Adds opt-in book-style numbering so MyST can render long-form books — chapters, appendices, prefixed figures/equations, and the "Chapter N" / "Appendix A" cross-ref conventions readers expect. Strict superset of today's behaviour; gated by
numbering.book: true. Originating discussion: #12.Authors enable book mode with:
Then
[](#ch1)renders "Chapter 1", figures inch2.mdrender2.1, 2.2, an appendix H1 renders "A", and[](#preface)on asection: frontmatterpage falls back to the heading text — fixing the literal-template bug from jupyter-book#12.What's in this PR (7 commits)
fbed3d44Add schema for book-style numberingd39288c5AddformatCounterfor arabic/alph/Alph/roman/Roman1c3251a6Render heading cross-refs via label / fall back to title88ffc86aApply label rendering to file-target cross-refs61f74f4bInject per-section numbering defaults for tagged TOC subtreesccff61a1Auto-prefix figures/equations/tables/proofs with chapter enumerator8409c8cbTest `proof:*` / `exercise` auto-prefix and render-only format overrideNumberingItemextensions —format(arabic/alph/Alph/roman/Roman),label(cross-ref template distinct fromtemplate),reset_on_part, new well-known kindsparts/chapters/appendices, and thebookopt-in flag.formatCounterhelper inmyst-transforms— pure function plus wiring intoformatHeadingEnumerator(per-depth formats) andReferenceState.incrementCount(per-kind format).label>template> heading-text fallback for heading-type targets, including the file-target path ([](#ch1)wherech1is a page slug).section:field onParentEntryinmyst-toc—frontmatter | chapters | appendices | backmatterpropagates to descendants; section-tagged subtrees are logical (no folder entry, no level bump). Per-section defaults injected intransformMdast.exercise, and the wholeproof:*family get the page's chapter/appendix enumerator prepended in book mode. Per-kind opt-out viacontinue: true.Backwards compatibility
numbering.book: true. Projects without the flag see zero behaviour change.section:field on TOC entries is optional and inert outside book mode.numbering.ymlgolden suite passes unchanged; existingenumerate.spec.tscases pass unchanged.Test plan
myst-frontmattertests pass (499/499, +7 new golden cases forformat/label/reset_on_part/book)myst-transformstests pass (345/345, +28formatCountercases, +4 heading-formats, +5 heading cross-ref, +9 book-mode auto-prefix, +2 proof/render-only-format)myst-toctests pass (34/34, +6sectionfield cases)myst-clitests pass (273/273, +9injectBookSectionDefaultscases)quantecon/PLAN.mdandquantecon/demo-book-numbering/onmain)Open scope (deferred)
Honest scope notes are in
quantecon/PLAN.md§3.1 onmain. The biggest miss vs the plan: parts are not implemented in PR #1. Schema acceptsreset_on_partand apartskind, but no consumer reads them. Books without parts (just chapters + appendices) work fully today.Also deferred: polymorphic
{cref}/{Cref}/{eqref}(a future PR), genericreset_on:/shares_counter:primitives, i18n onlabel.Status
We're keeping this PR open and unmerged in
QuantEcon/mystmdso the work is available viafeature/book-numbering+ the combinedquanteconbuild for dogfooding (book-dp1, book-dp2). A future submission upstream tojupyter-book/mystmdwill reuse this branch.🤖 Generated with Claude Code