Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/book-proof-scope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"myst-frontmatter": patch
"myst-transforms": patch
---

Book mode: `scope` option on `NumberingItem` opts kinds into a section-level (LaTeX `\newtheorem{...}[section]`) auto-prefix. With `numbering.proof.scope: section`, theorems/lemmas render as `5.1.1, 5.1.2, 5.2.1` (chapter.section.counter) and reset on each new heading_2. Accepted values: `chapter`/`heading_1` (default — today's behaviour), `section`/`heading_2`, `subsection`/`heading_3`, `heading_4`..`heading_6`. Per-kind `scope` (`numbering.proof:theorem.scope`) overrides the `proof` umbrella, which overrides `numbering.all.scope`. Applies to every auto-prefix kind (figure, equation, table, exercise, proof:*). Closes QuantEcon/mystmd#27.
37 changes: 37 additions & 0 deletions packages/myst-frontmatter/src/numbering/numbering.yml
Original file line number Diff line number Diff line change
Expand Up @@ -358,3 +358,40 @@ cases:
enabled: true
label: Chapter %s
format: arabic
- title: scope normalizes section/chapter/subsection aliases
raw:
numbering:
proof:
scope: section
figure:
scope: chapter
equation:
scope: subsection
normalized:
numbering:
proof:
enabled: true
scope: heading_2
figure:
enabled: true
scope: heading_1
equation:
enabled: true
scope: heading_3
- title: scope accepts heading_N directly
raw:
numbering:
proof:
scope: heading_2
normalized:
numbering:
proof:
enabled: true
scope: heading_2
- title: invalid scope warns
raw:
numbering:
proof:
scope: bogus
normalized: {}
warnings: 1
39 changes: 39 additions & 0 deletions packages/myst-frontmatter/src/numbering/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
export type CounterFormat = 'arabic' | 'alph' | 'Alph' | 'roman' | 'Roman';

/**
* Accepted values for `numbering.<kind>.scope` (#27). The validator
* normalises every spelling to `heading_N`; the alias forms are kept in
* the union so authors can write the LaTeX-familiar names directly.
* Source of truth for the alias map lives in
* `myst-frontmatter/src/numbering/validators.ts` (`SCOPE_ALIASES`).
*/
export type NumberingScope =
| 'chapter'
| 'section'
| 'subsection'
| 'subsubsection'
| 'heading_1'
| 'heading_2'
| 'heading_3'
| 'heading_4'
| 'heading_5'
| 'heading_6';

export type NumberingItem = {
enabled?: boolean;
start?: number;
Expand All @@ -10,6 +29,26 @@ export type NumberingItem = {
format?: CounterFormat; // counter rendering format (arabic/alph/Alph/roman/Roman)
label?: string; // cross-reference template, distinct from `template`
reset_on_part?: boolean; // chapters: restart counter at each part (only meaningful on `chapters`)
/**
* Book-mode auto-prefix depth (#27). For kinds that pick up the
* chapter/appendix prefix in `numbering.book: true` mode (figure,
* equation, table, exercise, proof:*), `scope` controls which heading
* depth contributes to the prefix and at which boundary the kind's
* counter resets. Accepted values:
*
* - `chapter` (default) / `heading_1` — current behaviour: prefix is the
* page's chapter enumerator, counter resets on chapter boundary.
* Renders e.g. `Theorem 5.1, 5.2`.
* - `section` / `heading_2` — LaTeX `\newtheorem{...}[section]` parity:
* prefix is `chapter.section`, counter resets on each new heading_2.
* Renders e.g. `Theorem 5.1.1, 5.1.2, 5.2.1`.
* - `subsection` / `heading_3` … `heading_6` — deeper variants.
*
* For `proof:*` kinds, a `scope` set on the umbrella `proof` key applies
* to every proof-family kind; per-kind `scope` (`numbering.proof:theorem.scope`)
* wins. `numbering.all.scope` is the project-wide default.
*/
scope?: NumberingScope;
};

/**
Expand Down
52 changes: 51 additions & 1 deletion packages/myst-frontmatter/src/numbering/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
validateString,
validationWarning,
} from 'simple-validators';
import type { CounterFormat, Numbering, NumberingItem } from './types.js';
import type { CounterFormat, Numbering, NumberingItem, NumberingScope } from './types.js';

export const NUMBERING_OPTIONS = ['enumerator', 'all', 'headings', 'title'];

Expand All @@ -36,12 +36,46 @@ const NUMBERING_ITEM_KEYS = [
'format',
'label',
'reset_on_part',
'scope',
];

const COUNTER_FORMATS: CounterFormat[] = ['arabic', 'alph', 'Alph', 'roman', 'Roman'];

const CONTINUE_STRINGS = ['continue', 'next'];

/**
* Single source of truth for `numbering.<kind>.scope` aliases (#27).
* Maps every accepted spelling to its canonical `heading_N` form.
* Both the validator (which normalises to `heading_N`) and consumers
* that need the depth integer (e.g. `myst-transforms/src/enumerate.ts`'s
* `effectiveScopeDepth`) import this and `scopeAliasToDepth` rather
* than maintaining parallel switch statements.
*/
export const SCOPE_ALIASES: Record<string, string> = {
chapter: 'heading_1',
section: 'heading_2',
subsection: 'heading_3',
subsubsection: 'heading_4',
heading_1: 'heading_1',
heading_2: 'heading_2',
heading_3: 'heading_3',
heading_4: 'heading_4',
heading_5: 'heading_5',
heading_6: 'heading_6',
};
export const SCOPE_VALUES = Object.keys(SCOPE_ALIASES);

/**
* Resolve a scope alias (`chapter`/`section`/`heading_N`/…) to its
* heading depth (1-based). Returns `undefined` for unrecognised values
* so the caller can fall through to the next candidate.
*/
export function scopeAliasToDepth(scope: string): number | undefined {
const canonical = SCOPE_ALIASES[scope];
if (!canonical) return undefined;
return Number(canonical.slice('heading_'.length));
}

export const NUMBERING_ALIAS = {
sections: 'headings',
h1: 'heading_1',
Expand Down Expand Up @@ -172,6 +206,22 @@ export function validateNumberingItem(
output.enabled = output.enabled ?? true;
}
}
if (defined(value.scope)) {
const scopeOpts = incrementOptions('scope', opts);
const scopeStr = validateString(value.scope, scopeOpts);
if (defined(scopeStr)) {
const normalized = SCOPE_ALIASES[scopeStr];
if (normalized) {
output.scope = normalized as NumberingScope;
output.enabled = output.enabled ?? true;
} else {
validationWarning(
`must be one of: ${SCOPE_VALUES.join(', ')} (got "${scopeStr}")`,
scopeOpts,
);
}
}
}
if (Object.keys(output).length === 0) return undefined;
return output;
}
Expand Down
Loading
Loading