explico 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CHANGELOG.md +72 -1
  2. package/README.md +21 -2
  3. package/dist/compiler/astro.d.ts +30 -0
  4. package/dist/compiler/astro.js +79 -0
  5. package/dist/compiler/cli.js +5 -9
  6. package/dist/compiler/course-config.d.ts +29 -4
  7. package/dist/compiler/course-config.js +12 -0
  8. package/dist/compiler/lesson-derivation.d.ts +1 -1
  9. package/dist/compiler/lesson-derivation.js +2 -2
  10. package/dist/compiler/manifest.d.ts +25 -4
  11. package/dist/compiler/manifest.js +59 -27
  12. package/dist/reference/equations-validate.d.ts +8 -3
  13. package/dist/reference/equations-validate.js +1 -1
  14. package/dist/reference/equations.d.mts +58 -22
  15. package/dist/reference/equations.mjs +75 -26
  16. package/dist/reference/keyed-float-validate.d.ts +9 -3
  17. package/dist/reference/keyed-float-validate.js +1 -1
  18. package/dist/reference/keyed-jsx-floats.d.mts +18 -10
  19. package/dist/reference/keyed-jsx-floats.mjs +42 -18
  20. package/dist/reference/remark-notation.d.mts +54 -5
  21. package/dist/reference/remark-notation.mjs +161 -17
  22. package/docs/authoring/README.md +4 -0
  23. package/docs/authoring/migrating.md +62 -0
  24. package/docs/authoring/notation.md +8 -7
  25. package/docs/authoring/structure.md +139 -0
  26. package/package.json +1 -1
  27. package/src/compiler/astro.ts +127 -0
  28. package/src/compiler/cli.ts +5 -9
  29. package/src/compiler/course-config.ts +39 -3
  30. package/src/compiler/lesson-derivation.ts +2 -2
  31. package/src/compiler/manifest.ts +108 -36
  32. package/src/components/notation/NotationLayer.astro +11 -0
  33. package/src/components/starlight/LayoutSidebar.astro +62 -1
  34. package/src/components/starlight/LessonFooter.astro +11 -0
  35. package/src/components/table/Table.astro +3 -2
  36. package/src/reference/equations-validate.ts +6 -4
  37. package/src/reference/equations.mjs +75 -26
  38. package/src/reference/keyed-float-validate.ts +7 -4
  39. package/src/reference/keyed-jsx-floats.mjs +42 -18
  40. package/src/reference/remark-notation.mjs +161 -17
  41. package/src/styles/global.css +129 -0
@@ -9,12 +9,28 @@ export function parseEquationRef(raw: any): {
9
9
  /** Remove every `\label{eq:…}` (and the whitespace it leaves) from a math source. */
10
10
  export function stripEquationLabels(latex: any): string;
11
11
  /**
12
- * The visible number for one display equation. With `chapter` (the lesson's
13
- * course ordinal) the scheme is `‹chapter›.‹indexInLesson›` and `##` headings
12
+ * Normalise the numbering input to `{ prefix, offset }`.
13
+ *
14
+ * `prefix` is the number before the dot — the **part**'s ordinal (its position
15
+ * among the sidebar groups). `offset` is how many display equations already
16
+ * appeared in earlier lessons of that same part, so `n` runs continuously
17
+ * across a part instead of restarting on every page. A bare number is the
18
+ * legacy per-lesson form (`prefix = n`, `offset = 0`) and is still accepted so
19
+ * hand-built trees and unit tests keep working. `undefined`/`null`/`0` ⇒ the
20
+ * `##`-section fallback for non-lesson pages.
21
+ */
22
+ export function resolveNumbering(input: any): {
23
+ prefix: any;
24
+ offset: number;
25
+ };
26
+ /**
27
+ * The visible number for one display equation. With a `prefix` (the part's
28
+ * ordinal) the scheme is `‹part›.‹offset + indexInLesson›` and `##` headings
14
29
  * are ignored; without it, the legacy `##`-section scheme.
15
30
  */
16
- export function displayEquationNumber({ chapter, section, indexInSection, indexInLesson, }: {
17
- chapter: any;
31
+ export function displayEquationNumber({ prefix, offset, section, indexInSection, indexInLesson, }: {
32
+ prefix: any;
33
+ offset?: number | undefined;
18
34
  section: any;
19
35
  indexInSection: any;
20
36
  indexInLesson: any;
@@ -38,28 +54,43 @@ export function stripFencedCode(markdown: any): string;
38
54
  */
39
55
  export function stripJsxComponents(markdown: any, keep: any): string;
40
56
  /**
41
- * Every keyed display equation in a Markdown body, in appearance order, with
42
- * its assigned number. `$$…$$` blocks only. With `chapter` (the lesson's
43
- * course ordinal) the number is `‹chapter›.‹n›`, `n` sequential over every
44
- * display block in the lesson; without it, `##` headings advance a section
45
- * counter and the number is `‹section›.‹indexInSection›`. Either way every
46
- * display block advances the index, so a keyed equation's number reflects its
47
- * position among *all* display equations, not only the keyed ones.
57
+ * Every display equation in a Markdown body, keyed or not, in appearance order
58
+ * with its assigned number. `$$…$$` blocks only. With `numbering`
59
+ * (`{ prefix, offset }`, the part's ordinal and the count of display equations
60
+ * in earlier lessons of that part) the number is `‹part›.‹offset + n›`, `n`
61
+ * sequential over every display block in the lesson; without it, `##` headings
62
+ * advance a section counter and the number is `‹section›.‹indexInSection›`.
63
+ * Either way every display block advances the index, so a keyed equation's
64
+ * number reflects its position among *all* display equations, not only the
65
+ * keyed ones.
48
66
  */
49
- export function scanEquationLabels(markdown: any, chapter: any): {
50
- key: string;
67
+ export function scanDisplayEquations(markdown: any, numbering: any): {
68
+ key: string | null | undefined;
69
+ number: string;
70
+ section: number;
71
+ indexInSection: number;
72
+ }[];
73
+ /** The keyed subset of `scanDisplayEquations` — the manifest builder's view. */
74
+ export function scanEquationLabels(markdown: any, numbering: any): {
75
+ key: string | null | undefined;
51
76
  number: string;
52
77
  section: number;
53
78
  indexInSection: number;
54
79
  }[];
80
+ /**
81
+ * How many display equations a lesson body holds. This is what makes `n` run
82
+ * continuously across a part: a lesson's `offset` is the sum of this over every
83
+ * earlier lesson in the same part.
84
+ */
85
+ export function countDisplayEquations(markdown: any): number;
55
86
  /**
56
87
  * The mdast equivalent of `scanEquationLabels` for *every* display equation:
57
88
  * walk the tree in document order, advancing the section counter on every
58
89
  * depth-2 heading and emitting a record for every `math` node. `key` is the
59
- * `\label{eq:…}` payload when present, else `null`. `chapter` is threaded the
90
+ * `\label{eq:…}` payload when present, else `null`. `numbering` is threaded the
60
91
  * same way as in `scanEquationLabels`.
61
92
  */
62
- export function collectDisplayEquations(tree: any, chapter: any): {
93
+ export function collectDisplayEquations(tree: any, numbering: any): {
63
94
  key: string | null;
64
95
  number: string;
65
96
  section: number;
@@ -70,7 +101,7 @@ export function collectDisplayEquations(tree: any, chapter: any): {
70
101
  * The keyed subset of `collectDisplayEquations` — the tree-walk counterpart of
71
102
  * `scanEquationLabels`. Numbers reflect position among all display equations.
72
103
  */
73
- export function collectEquationLabels(tree: any, chapter: any): {
104
+ export function collectEquationLabels(tree: any, numbering: any): {
74
105
  key: string | null;
75
106
  number: string;
76
107
  section: number;
@@ -81,12 +112,17 @@ export function collectEquationLabels(tree: any, chapter: any): {
81
112
  * Equation identity (Step D7, extended F3) — shared, framework-free helpers.
82
113
  *
83
114
  * Every display equation (`$$…$$`) in a lesson is numbered and given an `id`
84
- * from appearance order. When the caller supplies a `chapter` (the lesson's
85
- * ordinal position in the course), the number is `‹chapter›.‹n›` — `n` counts
86
- * every display equation in the lesson and `##` headings do not affect it
87
- * (`(2.4)` = fourth display equation of the second lesson). With no `chapter`
88
- * the number falls back to the `##`-section scheme `‹section›.‹indexInSection›`
89
- * (used by hand-built test trees and non-lesson pages). An author additionally
115
+ * from appearance order. When the caller supplies `numbering`
116
+ * (`{ prefix, offset }` — see `resolveNumbering`), the number is
117
+ * `‹part›.‹offset + n›`: the prefix is the **part**'s ordinal among the sidebar
118
+ * groups, `n` counts every display equation in the lesson, and `offset` carries
119
+ * the count from earlier lessons of the same part so the sequence runs
120
+ * continuously across a part rather than restarting on every page. `##` and
121
+ * `###` headings never affect it — `(2.4)` is the fourth display equation of
122
+ * part 2, wherever in that part it sits. See `docs/authoring/structure.md` for
123
+ * the level names. With no `numbering` the number falls back to the
124
+ * `##`-section scheme `‹section›.‹indexInSection›` (used by hand-built test
125
+ * trees and non-lesson pages). An author additionally
90
126
  * marks an equation with a trailing `\label{eq:<key>}` inside its `$$…$$`
91
127
  * block (inside math, so the `:` is safe and the form matches amsmath) when
92
128
  * they want a *stable* anchor that survives edits and cross-page references:
@@ -2,12 +2,17 @@
2
2
  * Equation identity (Step D7, extended F3) — shared, framework-free helpers.
3
3
  *
4
4
  * Every display equation (`$$…$$`) in a lesson is numbered and given an `id`
5
- * from appearance order. When the caller supplies a `chapter` (the lesson's
6
- * ordinal position in the course), the number is `‹chapter›.‹n›` — `n` counts
7
- * every display equation in the lesson and `##` headings do not affect it
8
- * (`(2.4)` = fourth display equation of the second lesson). With no `chapter`
9
- * the number falls back to the `##`-section scheme `‹section›.‹indexInSection›`
10
- * (used by hand-built test trees and non-lesson pages). An author additionally
5
+ * from appearance order. When the caller supplies `numbering`
6
+ * (`{ prefix, offset }` — see `resolveNumbering`), the number is
7
+ * `‹part›.‹offset + n›`: the prefix is the **part**'s ordinal among the sidebar
8
+ * groups, `n` counts every display equation in the lesson, and `offset` carries
9
+ * the count from earlier lessons of the same part so the sequence runs
10
+ * continuously across a part rather than restarting on every page. `##` and
11
+ * `###` headings never affect it — `(2.4)` is the fourth display equation of
12
+ * part 2, wherever in that part it sits. See `docs/authoring/structure.md` for
13
+ * the level names. With no `numbering` the number falls back to the
14
+ * `##`-section scheme `‹section›.‹indexInSection›` (used by hand-built test
15
+ * trees and non-lesson pages). An author additionally
11
16
  * marks an equation with a trailing `\label{eq:<key>}` inside its `$$…$$`
12
17
  * block (inside math, so the `:` is safe and the form matches amsmath) when
13
18
  * they want a *stable* anchor that survives edits and cross-page references:
@@ -63,19 +68,42 @@ function equationNumber(section, index) {
63
68
  }
64
69
 
65
70
  /**
66
- * The visible number for one display equation. With `chapter` (the lesson's
67
- * course ordinal) the scheme is `‹chapter›.‹indexInLesson›` and `##` headings
71
+ * Normalise the numbering input to `{ prefix, offset }`.
72
+ *
73
+ * `prefix` is the number before the dot — the **part**'s ordinal (its position
74
+ * among the sidebar groups). `offset` is how many display equations already
75
+ * appeared in earlier lessons of that same part, so `n` runs continuously
76
+ * across a part instead of restarting on every page. A bare number is the
77
+ * legacy per-lesson form (`prefix = n`, `offset = 0`) and is still accepted so
78
+ * hand-built trees and unit tests keep working. `undefined`/`null`/`0` ⇒ the
79
+ * `##`-section fallback for non-lesson pages.
80
+ */
81
+ export function resolveNumbering(input) {
82
+ if (input == null) return { prefix: undefined, offset: 0 };
83
+ if (typeof input === 'number') {
84
+ return { prefix: input || undefined, offset: 0 };
85
+ }
86
+ return {
87
+ prefix: input.prefix || undefined,
88
+ offset: Number(input.offset) || 0,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * The visible number for one display equation. With a `prefix` (the part's
94
+ * ordinal) the scheme is `‹part›.‹offset + indexInLesson›` and `##` headings
68
95
  * are ignored; without it, the legacy `##`-section scheme.
69
96
  */
70
97
  export function displayEquationNumber({
71
- chapter,
98
+ prefix,
99
+ offset = 0,
72
100
  section,
73
101
  indexInSection,
74
102
  indexInLesson,
75
103
  }) {
76
- return chapter == null
104
+ return prefix == null
77
105
  ? equationNumber(section, indexInSection)
78
- : `${chapter}.${indexInLesson}`;
106
+ : `${prefix}.${offset + indexInLesson}`;
79
107
  }
80
108
 
81
109
  /** The positional anchor id for an unkeyed equation, e.g. `2.4` → `eq-2-4`. */
@@ -109,15 +137,18 @@ export function stripJsxComponents(markdown, keep) {
109
137
  }
110
138
 
111
139
  /**
112
- * Every keyed display equation in a Markdown body, in appearance order, with
113
- * its assigned number. `$$…$$` blocks only. With `chapter` (the lesson's
114
- * course ordinal) the number is `‹chapter›.‹n›`, `n` sequential over every
115
- * display block in the lesson; without it, `##` headings advance a section
116
- * counter and the number is `‹section›.‹indexInSection›`. Either way every
117
- * display block advances the index, so a keyed equation's number reflects its
118
- * position among *all* display equations, not only the keyed ones.
140
+ * Every display equation in a Markdown body, keyed or not, in appearance order
141
+ * with its assigned number. `$$…$$` blocks only. With `numbering`
142
+ * (`{ prefix, offset }`, the part's ordinal and the count of display equations
143
+ * in earlier lessons of that part) the number is `‹part›.‹offset + n›`, `n`
144
+ * sequential over every display block in the lesson; without it, `##` headings
145
+ * advance a section counter and the number is `‹section›.‹indexInSection›`.
146
+ * Either way every display block advances the index, so a keyed equation's
147
+ * number reflects its position among *all* display equations, not only the
148
+ * keyed ones.
119
149
  */
120
- export function scanEquationLabels(markdown, chapter) {
150
+ export function scanDisplayEquations(markdown, numbering) {
151
+ const { prefix, offset } = resolveNumbering(numbering);
121
152
  const text = stripJsxComponents(stripFencedCode(markdown));
122
153
  const tokens = [];
123
154
  const heading = /^[ \t]{0,3}##[ \t]+\S/gm;
@@ -148,11 +179,11 @@ export function scanEquationLabels(markdown, chapter) {
148
179
  }
149
180
  indexInSection += 1;
150
181
  indexInLesson += 1;
151
- if (token.key == null) continue;
152
182
  labels.push({
153
183
  key: token.key,
154
184
  number: displayEquationNumber({
155
- chapter,
185
+ prefix,
186
+ offset,
156
187
  section,
157
188
  indexInSection,
158
189
  indexInLesson,
@@ -164,14 +195,31 @@ export function scanEquationLabels(markdown, chapter) {
164
195
  return labels;
165
196
  }
166
197
 
198
+ /** The keyed subset of `scanDisplayEquations` — the manifest builder's view. */
199
+ export function scanEquationLabels(markdown, numbering) {
200
+ return scanDisplayEquations(markdown, numbering).filter(
201
+ (equation) => equation.key != null,
202
+ );
203
+ }
204
+
205
+ /**
206
+ * How many display equations a lesson body holds. This is what makes `n` run
207
+ * continuously across a part: a lesson's `offset` is the sum of this over every
208
+ * earlier lesson in the same part.
209
+ */
210
+ export function countDisplayEquations(markdown) {
211
+ return scanDisplayEquations(markdown).length;
212
+ }
213
+
167
214
  /**
168
215
  * The mdast equivalent of `scanEquationLabels` for *every* display equation:
169
216
  * walk the tree in document order, advancing the section counter on every
170
217
  * depth-2 heading and emitting a record for every `math` node. `key` is the
171
- * `\label{eq:…}` payload when present, else `null`. `chapter` is threaded the
218
+ * `\label{eq:…}` payload when present, else `null`. `numbering` is threaded the
172
219
  * same way as in `scanEquationLabels`.
173
220
  */
174
- export function collectDisplayEquations(tree, chapter) {
221
+ export function collectDisplayEquations(tree, numbering) {
222
+ const { prefix, offset } = resolveNumbering(numbering);
175
223
  const equations = [];
176
224
  let section = 0;
177
225
  let indexInSection = 0;
@@ -196,7 +244,8 @@ export function collectDisplayEquations(tree, chapter) {
196
244
  equations.push({
197
245
  key: label ? label[1] : null,
198
246
  number: displayEquationNumber({
199
- chapter,
247
+ prefix,
248
+ offset,
200
249
  section,
201
250
  indexInSection,
202
251
  indexInLesson,
@@ -214,8 +263,8 @@ export function collectDisplayEquations(tree, chapter) {
214
263
  * The keyed subset of `collectDisplayEquations` — the tree-walk counterpart of
215
264
  * `scanEquationLabels`. Numbers reflect position among all display equations.
216
265
  */
217
- export function collectEquationLabels(tree, chapter) {
218
- return collectDisplayEquations(tree, chapter).filter(
266
+ export function collectEquationLabels(tree, numbering) {
267
+ return collectDisplayEquations(tree, numbering).filter(
219
268
  (equation) => equation.key != null,
220
269
  );
221
270
  }
@@ -57,10 +57,16 @@ interface KeyedFloatLessonInput {
57
57
  readonly file: string;
58
58
  readonly body: string;
59
59
  /**
60
- * The lesson's 1-based ordinal in the course. When set, floats are numbered
61
- * `‹chapter›.‹n›`; when omitted, the legacy `##`-section scheme.
60
+ * `{ prefix, offset }` — the part's 1-based ordinal among the sidebar groups,
61
+ * and how many floats of this kind earlier lessons of that part already used.
62
+ * When set, floats are numbered `‹part›.‹offset + n›`; when omitted, the
63
+ * legacy `##`-section scheme. Each kind counts on its own, so the caller
64
+ * passes a different `offset` for `<Table>` than for `<Figure>`.
62
65
  */
63
- readonly chapter?: number;
66
+ readonly numbering?: {
67
+ readonly prefix: number;
68
+ readonly offset: number;
69
+ };
64
70
  }
65
71
  declare function validateKeyedFloats(lessons: readonly KeyedFloatLessonInput[], kind: KeyedFloatKind): KeyedFloatValidation;
66
72
 
@@ -32,7 +32,7 @@ function validateKeyedFloats(lessons, kind) {
32
32
  const table = {};
33
33
  for (const raw of scanKeyedJsxFloats(lesson.body, {
34
34
  tagName,
35
- chapter: lesson.chapter
35
+ numbering: lesson.numbering
36
36
  })) {
37
37
  if (raw.key == null) continue;
38
38
  if (!KEYED_FLOAT_KEY_PATTERN.test(raw.key)) {
@@ -20,31 +20,39 @@ export function parseKeyedFloatRef(raw: any, { prefix }: {
20
20
  /**
21
21
  * The raw-text equivalent of `collectKeyedJsxFloats`, mirroring
22
22
  * `scanEquationLabels`: every top-level `<TagName …>` in appearance order.
23
- * With `chapter` (the lesson's course ordinal) the number is `‹chapter›.‹n›`,
24
- * `n` sequential over the whole lesson; without it, `##` headings advance a
25
- * section counter. Other JSX components' bodies are blanked first so a
26
- * `<Table>` nested inside e.g. `<CompactExample>` (and any `##`-like text
27
- * inside an unrelated component) is never counted.
23
+ * With `numbering` (`{ prefix, offset }` — the part's ordinal and the count of
24
+ * this kind of float in earlier lessons of that part) the number is
25
+ * `‹part›.‹offset + n›`, `n` sequential over the whole lesson; without it, `##`
26
+ * headings advance a section counter. Other JSX components' bodies are blanked
27
+ * first so a `<Table>` nested inside e.g. `<CompactExample>` (and any `##`-like
28
+ * text inside an unrelated component) is never counted.
28
29
  */
29
- export function scanKeyedJsxFloats(markdown: any, { tagName, chapter }: {
30
+ export function scanKeyedJsxFloats(markdown: any, { tagName, numbering }: {
30
31
  tagName: any;
31
- chapter: any;
32
+ numbering: any;
32
33
  }): {
33
34
  key: string | null | undefined;
34
35
  number: string;
35
36
  section: number;
36
37
  indexInSection: number;
37
38
  }[];
39
+ /**
40
+ * How many floats of this kind a lesson body holds — the input to the next
41
+ * lesson's `offset` within the same part.
42
+ */
43
+ export function countKeyedJsxFloats(markdown: any, { tagName }: {
44
+ tagName: any;
45
+ }): number;
38
46
  /**
39
47
  * The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's *top-level*
40
48
  * children in document order, advancing the section counter on every
41
49
  * depth-2 heading and emitting a record for every `mdxJsxFlowElement` named
42
- * `tagName` — keyed or not, exactly like `collectDisplayEquations`. `chapter`
50
+ * `tagName` — keyed or not, exactly like `collectDisplayEquations`. `numbering`
43
51
  * is threaded the same way as in `scanKeyedJsxFloats`.
44
52
  */
45
- export function collectKeyedJsxFloats(tree: any, { tagName, chapter }: {
53
+ export function collectKeyedJsxFloats(tree: any, { tagName, numbering }: {
46
54
  tagName: any;
47
- chapter: any;
55
+ numbering: any;
48
56
  }): {
49
57
  key: any;
50
58
  number: string;
@@ -22,7 +22,11 @@
22
22
  * `equations.test.ts` holds equations to.
23
23
  */
24
24
 
25
- import { stripFencedCode, stripJsxComponents } from './equations.mjs';
25
+ import {
26
+ resolveNumbering,
27
+ stripFencedCode,
28
+ stripJsxComponents,
29
+ } from './equations.mjs';
26
30
 
27
31
  /** Same shape as `EQ_KEY_PATTERN` — the notation-key grammar without dots. */
28
32
  export const KEYED_FLOAT_KEY_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
@@ -33,15 +37,22 @@ function floatNumber(section, index) {
33
37
  }
34
38
 
35
39
  /**
36
- * The visible number for one float. With `chapter` (the lesson's course
37
- * ordinal) the scheme is `‹chapter›.‹indexInLesson›` and `##` headings are
38
- * ignored; without it, the legacy `##`-section scheme. Mirrors
39
- * `displayEquationNumber` in `equations.mjs`.
40
+ * The visible number for one float. With a `prefix` (the part's ordinal) the
41
+ * scheme is `‹part›.‹offset + indexInLesson›` and `##` headings are ignored;
42
+ * without it, the legacy `##`-section scheme. Mirrors `displayEquationNumber`
43
+ * in `equations.mjs`. Each kind of float counts on its own — a `<Table>` never
44
+ * advances the `<Figure>` sequence — so `offset` is per kind too.
40
45
  */
41
- function keyedFloatNumber({ chapter, section, indexInSection, indexInLesson }) {
42
- return chapter == null
46
+ function keyedFloatNumber({
47
+ prefix,
48
+ offset = 0,
49
+ section,
50
+ indexInSection,
51
+ indexInLesson,
52
+ }) {
53
+ return prefix == null
43
54
  ? floatNumber(section, indexInSection)
44
- : `${chapter}.${indexInLesson}`;
55
+ : `${prefix}.${offset + indexInLesson}`;
45
56
  }
46
57
 
47
58
  /** The anchor id for a float, e.g. `{prefix:'tab', key:'foo'}` → `tab-foo`. */
@@ -71,13 +82,15 @@ function readIdAttribute(attributesSource) {
71
82
  /**
72
83
  * The raw-text equivalent of `collectKeyedJsxFloats`, mirroring
73
84
  * `scanEquationLabels`: every top-level `<TagName …>` in appearance order.
74
- * With `chapter` (the lesson's course ordinal) the number is `‹chapter›.‹n›`,
75
- * `n` sequential over the whole lesson; without it, `##` headings advance a
76
- * section counter. Other JSX components' bodies are blanked first so a
77
- * `<Table>` nested inside e.g. `<CompactExample>` (and any `##`-like text
78
- * inside an unrelated component) is never counted.
85
+ * With `numbering` (`{ prefix, offset }` — the part's ordinal and the count of
86
+ * this kind of float in earlier lessons of that part) the number is
87
+ * `‹part›.‹offset + n›`, `n` sequential over the whole lesson; without it, `##`
88
+ * headings advance a section counter. Other JSX components' bodies are blanked
89
+ * first so a `<Table>` nested inside e.g. `<CompactExample>` (and any `##`-like
90
+ * text inside an unrelated component) is never counted.
79
91
  */
80
- export function scanKeyedJsxFloats(markdown, { tagName, chapter }) {
92
+ export function scanKeyedJsxFloats(markdown, { tagName, numbering }) {
93
+ const { prefix, offset } = resolveNumbering(numbering);
81
94
  const text = stripJsxComponents(stripFencedCode(markdown), tagName);
82
95
  const tokens = [];
83
96
  const heading = /^[ \t]{0,3}##[ \t]+\S/gm;
@@ -106,7 +119,8 @@ export function scanKeyedJsxFloats(markdown, { tagName, chapter }) {
106
119
  labels.push({
107
120
  key: token.key,
108
121
  number: keyedFloatNumber({
109
- chapter,
122
+ prefix,
123
+ offset,
110
124
  section,
111
125
  indexInSection,
112
126
  indexInLesson,
@@ -118,14 +132,23 @@ export function scanKeyedJsxFloats(markdown, { tagName, chapter }) {
118
132
  return labels;
119
133
  }
120
134
 
135
+ /**
136
+ * How many floats of this kind a lesson body holds — the input to the next
137
+ * lesson's `offset` within the same part.
138
+ */
139
+ export function countKeyedJsxFloats(markdown, { tagName }) {
140
+ return scanKeyedJsxFloats(markdown, { tagName }).length;
141
+ }
142
+
121
143
  /**
122
144
  * The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's *top-level*
123
145
  * children in document order, advancing the section counter on every
124
146
  * depth-2 heading and emitting a record for every `mdxJsxFlowElement` named
125
- * `tagName` — keyed or not, exactly like `collectDisplayEquations`. `chapter`
147
+ * `tagName` — keyed or not, exactly like `collectDisplayEquations`. `numbering`
126
148
  * is threaded the same way as in `scanKeyedJsxFloats`.
127
149
  */
128
- export function collectKeyedJsxFloats(tree, { tagName, chapter }) {
150
+ export function collectKeyedJsxFloats(tree, { tagName, numbering }) {
151
+ const { prefix, offset } = resolveNumbering(numbering);
129
152
  const floats = [];
130
153
  let section = 0;
131
154
  let indexInSection = 0;
@@ -151,7 +174,8 @@ export function collectKeyedJsxFloats(tree, { tagName, chapter }) {
151
174
  floats.push({
152
175
  key: key ?? null,
153
176
  number: keyedFloatNumber({
154
- chapter,
177
+ prefix,
178
+ offset,
155
179
  section,
156
180
  indexInSection,
157
181
  indexInLesson,
@@ -1,29 +1,78 @@
1
+ /**
2
+ * The numbering inputs a lesson page needs, read off one manifest.
3
+ *
4
+ * Every number on a page comes from two places that MUST agree: the page's own
5
+ * counters (`numbering`) and the manifest tables a cross-page `[[slug#eq-key]]`
6
+ * is resolved against (`equations` / `tables` / `figures`). Deriving all four
7
+ * from the same manifest in one step is what stops a course config from
8
+ * supplying one without the other — the omission that produced page-local
9
+ * `(2.2)` and cross-page `(4.5)` for the same equation.
10
+ *
11
+ * `numbering` maps a lesson slug to `{ part, equations, tables, figures }`:
12
+ * the part's ordinal (the number before the dot) and, per kind of float, how
13
+ * many of them the earlier lessons of that part already used.
14
+ */
15
+ export function numberingFromManifest(manifest: any): {
16
+ equations?: undefined;
17
+ tables?: undefined;
18
+ figures?: undefined;
19
+ numbering?: undefined;
20
+ } | {
21
+ equations: any;
22
+ tables: any;
23
+ figures: any;
24
+ numbering: {
25
+ [k: string]: any;
26
+ };
27
+ };
1
28
  /**
2
29
  * @param {{
3
30
  * definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>),
4
31
  * resolve?: (key: string, context: object) => object | undefined,
5
32
  * unknown?: 'error' | 'warn' | 'ignore',
6
33
  * base?: string,
34
+ * manifest?: object,
7
35
  * equations?: Record<string, Record<string, string>>,
8
36
  * tables?: Record<string, Record<string, string>>,
9
37
  * figures?: Record<string, Record<string, string>>,
38
+ * numbering?: Record<string, { part: number, equations: number, tables: number, figures: number }>,
10
39
  * chapters?: Record<string, number>,
11
40
  * }} [options]
12
41
  *
13
- * `chapters` maps a lesson slug to its 1-based course ordinal. When the
14
- * current page has one, every display equation / `<Table>` / `<Figure>` is
15
- * numbered `‹chapter›.‹n›` (`##` headings ignored); with no entry the legacy
16
- * `##`-section scheme applies. It must agree with `manifest.lessons[].chapter`,
17
- * which is what feeds `equations`/`tables`/`figures` above.
42
+ * `manifest` is the preferred wiring: pass the compiled manifest and the four
43
+ * numbering inputs below are read off it together, so they cannot drift apart.
44
+ * They stay available individually for hand-built trees, and an explicitly
45
+ * supplied one still wins over the manifest.
46
+ *
47
+ * `numbering` maps a lesson slug to its **part** (the number before the dot)
48
+ * and, per kind of float, how many the earlier lessons of that part already
49
+ * used. Every display equation / `<Table>` / `<Figure>` on the page is then
50
+ * numbered `‹part›.‹offset + n›`, with `##` and `###` headings ignored; with no
51
+ * entry the legacy `##`-section scheme applies. It must agree with
52
+ * `manifest.lessons[]`, which is what feeds `equations`/`tables`/`figures`
53
+ * above — supplying the tables without `numbering` numbers the same equation
54
+ * twice, and `assertManifestNumberParity` turns that into a build error rather
55
+ * than a page whose references disagree with their targets.
56
+ *
57
+ * `chapters` is the superseded per-lesson form (slug → the lesson's course
58
+ * ordinal, no running offset). Still read when `numbering` has no entry, so a
59
+ * config written against the old option keeps building.
18
60
  */
19
61
  export default function remarkNotation(options?: {
20
62
  definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>);
21
63
  resolve?: (key: string, context: object) => object | undefined;
22
64
  unknown?: "error" | "warn" | "ignore";
23
65
  base?: string;
66
+ manifest?: object;
24
67
  equations?: Record<string, Record<string, string>>;
25
68
  tables?: Record<string, Record<string, string>>;
26
69
  figures?: Record<string, Record<string, string>>;
70
+ numbering?: Record<string, {
71
+ part: number;
72
+ equations: number;
73
+ tables: number;
74
+ figures: number;
75
+ }>;
27
76
  chapters?: Record<string, number>;
28
77
  }): (tree: any, file: any) => void;
29
78
  /**