explico 0.1.1 → 0.3.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 (94) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +46 -5
  3. package/dist/compiler/cli.d.ts +17 -0
  4. package/dist/compiler/cli.js +41 -3
  5. package/dist/compiler/collections.js +31 -1
  6. package/dist/compiler/course-config.d.ts +52 -1
  7. package/dist/compiler/knowledge-version.d.ts +58 -0
  8. package/dist/compiler/knowledge-version.js +80 -0
  9. package/dist/compiler/manifest.d.ts +87 -3
  10. package/dist/compiler/manifest.js +133 -20
  11. package/dist/compiler/resolution-report.d.ts +5 -0
  12. package/dist/reference/base-operators.d.mts +38 -0
  13. package/dist/reference/base-operators.mjs +103 -0
  14. package/dist/reference/consistency.d.ts +7 -2
  15. package/dist/reference/consistency.js +142 -2
  16. package/dist/reference/diagram-layout.d.ts +63 -0
  17. package/dist/reference/diagram-layout.js +122 -0
  18. package/dist/reference/equations-validate.d.ts +5 -0
  19. package/dist/reference/equations-validate.js +1 -1
  20. package/dist/reference/equations.d.mts +76 -18
  21. package/dist/reference/equations.mjs +118 -41
  22. package/dist/reference/figures-validate.d.ts +21 -0
  23. package/dist/reference/figures-validate.js +13 -0
  24. package/dist/reference/gate-math.d.ts +5 -4
  25. package/dist/reference/gate-math.js +73 -2
  26. package/dist/reference/gloss.d.ts +87 -0
  27. package/dist/reference/gloss.js +59 -0
  28. package/dist/reference/keyed-float-validate.d.ts +67 -0
  29. package/dist/reference/keyed-float-validate.js +110 -0
  30. package/dist/reference/keyed-jsx-floats.d.mts +56 -0
  31. package/dist/reference/keyed-jsx-floats.mjs +167 -0
  32. package/dist/reference/math-glyphs.d.mts +18 -0
  33. package/dist/reference/math-glyphs.mjs +92 -0
  34. package/dist/reference/registry.js +25 -0
  35. package/dist/reference/rehype-heading-anchors.d.mts +1 -0
  36. package/dist/reference/rehype-heading-anchors.mjs +144 -0
  37. package/dist/reference/remark-notation.d.mts +12 -0
  38. package/dist/reference/remark-notation.mjs +228 -32
  39. package/dist/reference/tables-validate.d.ts +21 -0
  40. package/dist/reference/tables-validate.js +13 -0
  41. package/dist/reference/types.d.ts +25 -2
  42. package/dist/reference/units-vocab.js +19 -1
  43. package/docs/VERSIONING.md +66 -0
  44. package/docs/authoring/README.md +96 -0
  45. package/docs/authoring/agents.md +63 -0
  46. package/docs/authoring/migrating.md +107 -0
  47. package/docs/authoring/notation.md +296 -0
  48. package/docs/authoring/versioning.md +116 -0
  49. package/docs/authoring/visualizations.md +195 -0
  50. package/package.json +20 -5
  51. package/src/compiler/cli.ts +49 -0
  52. package/src/compiler/collections.ts +52 -3
  53. package/src/compiler/course-config.ts +54 -0
  54. package/src/compiler/knowledge-version.ts +157 -0
  55. package/src/compiler/manifest.ts +270 -17
  56. package/src/components/CurriculumMap.astro +27 -0
  57. package/src/components/HoverPanel.astro +42 -0
  58. package/src/components/citation/CitationLayer.astro +1 -0
  59. package/src/components/diagram/Diagram.astro +317 -0
  60. package/src/components/examples/CompactExamples.astro +1 -1
  61. package/src/components/figure/Figure.astro +104 -0
  62. package/src/components/hover-panel.ts +71 -5
  63. package/src/components/notation/NotationFormula.astro +154 -0
  64. package/src/components/notation/NotationGlossary.astro +60 -0
  65. package/src/components/notation/NotationLayer.astro +248 -103
  66. package/src/components/notation/render-notation-math.ts +66 -0
  67. package/src/components/popup-stack.ts +345 -0
  68. package/src/components/reference/FloatRefLayer.astro +671 -0
  69. package/src/components/starlight/AboutPanel.astro +255 -0
  70. package/src/components/starlight/HeadingAnchorEnhancer.astro +131 -0
  71. package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
  72. package/src/components/starlight/LayoutSocialIcons.astro +12 -0
  73. package/src/components/starlight/LessonFooter.astro +8 -2
  74. package/src/components/table/Table.astro +104 -0
  75. package/src/content-config.ts +38 -0
  76. package/src/reference/base-operators.mjs +103 -0
  77. package/src/reference/consistency.ts +236 -2
  78. package/src/reference/diagram-layout.ts +246 -0
  79. package/src/reference/equations-validate.ts +6 -1
  80. package/src/reference/equations.mjs +118 -41
  81. package/src/reference/figures-validate.ts +40 -0
  82. package/src/reference/gate-math.ts +104 -7
  83. package/src/reference/gloss.ts +153 -0
  84. package/src/reference/keyed-float-validate.ts +207 -0
  85. package/src/reference/keyed-jsx-floats.mjs +167 -0
  86. package/src/reference/math-glyphs.mjs +92 -0
  87. package/src/reference/registry.ts +43 -0
  88. package/src/reference/rehype-heading-anchors.mjs +144 -0
  89. package/src/reference/remark-notation.mjs +228 -32
  90. package/src/reference/tables-validate.ts +40 -0
  91. package/src/reference/types.ts +26 -1
  92. package/src/reference/units-vocab.ts +18 -0
  93. package/src/styles/global.css +193 -3
  94. package/src/components/starlight/EquationEnhancer.astro +0 -72
@@ -1,3 +1,5 @@
1
+ import { effectiveOperatorAllowlist } from "./base-operators.mjs";
2
+ import { collectOperators } from "./math-glyphs.mjs";
1
3
  import { offVocabularyTokens } from "./units-vocab.js";
2
4
  const CONVENTION_RE = /\bsign convention\b|\bcash-flow perspective\b|\bholder(?:'s)?(?:\s+\w+){0,2}\s+perspective\b|\bfrom (?:a|one|the|its)(?:\s+\w+){0,3}\s+perspective\b|\breverses? every sign\b|\bpositive (?:means|amounts?)\b/i;
3
5
  function stripCode(text) {
@@ -200,13 +202,147 @@ function checkNotationSourceLocator(input, out) {
200
202
  }
201
203
  }
202
204
  }
205
+ const GLOSS_PROMOTION_LIMIT = 3;
206
+ function checkGlossWantsPromoting(input, out) {
207
+ const owners = /* @__PURE__ */ new Map();
208
+ const record = (ownerKey, gloss) => {
209
+ const id = `${gloss.notation}\0${gloss.label.toLowerCase()}`;
210
+ const current = owners.get(id) ?? { label: gloss.label, keys: /* @__PURE__ */ new Set() };
211
+ current.keys.add(ownerKey);
212
+ owners.set(id, current);
213
+ };
214
+ for (const definition of input.notationInput.sharedDefinitions) {
215
+ for (const gloss of definition.glosses) record(definition.key, gloss);
216
+ }
217
+ for (const lesson of input.notationInput.lessons) {
218
+ for (const definition of lesson.localDefinitions) {
219
+ for (const gloss of definition.glosses) record(definition.key, gloss);
220
+ }
221
+ }
222
+ for (const [id, { label, keys }] of [...owners].sort(
223
+ ([a], [b]) => a.localeCompare(b)
224
+ )) {
225
+ if (keys.size < GLOSS_PROMOTION_LIMIT) continue;
226
+ const glyph = id.split("\0")[0] ?? "";
227
+ if (input.lintIgnore.matches("gloss-wants-promoting", glyph)) continue;
228
+ out.push({
229
+ code: "gloss-wants-promoting",
230
+ severity: "warning",
231
+ glyph,
232
+ message: `gloss ${JSON.stringify(glyph)} (${label}) is declared on ${keys.size} entries (${[...keys].sort().join(", ")}); a meaning reused this widely wants promoting to a card`
233
+ });
234
+ }
235
+ }
236
+ const CARD_BODY_MINIMUM_WORDS = 25;
237
+ function checkCardWantsDemoting(input, out) {
238
+ const used = new Set(
239
+ input.registry.backlinks.map((backlink) => backlink.key)
240
+ );
241
+ for (const definition of input.notationInput.sharedDefinitions) {
242
+ if (definition.formula !== void 0) continue;
243
+ if (definition.sources.length > 0) continue;
244
+ if (used.has(definition.key)) continue;
245
+ const words = stripCode(definition.body).split(/\s+/).filter((word) => word.length > 0).length;
246
+ if (words >= CARD_BODY_MINIMUM_WORDS) continue;
247
+ if (input.lintIgnore.matches("card-wants-demoting", definition.key)) {
248
+ continue;
249
+ }
250
+ out.push({
251
+ code: "card-wants-demoting",
252
+ severity: "warning",
253
+ key: definition.key,
254
+ file: definition.source.file,
255
+ count: words,
256
+ message: `notation card ${definition.key} has no formula, no sources, a ${words}-word body, and no lesson uses it; it carries no more than a name \u2014 a gloss on the entry that needs it may be the right home`
257
+ });
258
+ }
259
+ }
260
+ const GLOSS_NAME_WORD_LIMIT = 6;
261
+ const GLOSS_NAME_PROSE = /[.;]|\b(?:which|whose|that is|such that)\b/i;
262
+ function checkGlossNameShape(input, out) {
263
+ const check = (gloss, file, lessonId) => {
264
+ const words = gloss.label.trim().split(/\s+/).filter(Boolean);
265
+ const reason = words.length > GLOSS_NAME_WORD_LIMIT ? `${words.length} words` : GLOSS_NAME_PROSE.test(gloss.label) ? "sentence punctuation or a relative clause" : void 0;
266
+ if (reason === void 0) return;
267
+ if (input.lintIgnore.matches("gloss-name-shape", gloss.key)) return;
268
+ out.push({
269
+ code: "gloss-name-shape",
270
+ severity: "warning",
271
+ key: gloss.key,
272
+ file,
273
+ ...lessonId ? { lessonId } : {},
274
+ message: `gloss ${gloss.key} is named ${JSON.stringify(gloss.label)} (${reason}); a gloss name is a short noun phrase naming the symbol, not a description of it`
275
+ });
276
+ };
277
+ for (const definition of input.notationInput.sharedDefinitions) {
278
+ for (const gloss of definition.glosses) {
279
+ check(gloss, definition.source.file);
280
+ }
281
+ }
282
+ for (const lesson of input.notationInput.lessons) {
283
+ for (const definition of lesson.localDefinitions) {
284
+ for (const gloss of definition.glosses) {
285
+ check(gloss, definition.source.file, lesson.lessonId);
286
+ }
287
+ }
288
+ }
289
+ }
290
+ function checkOperatorExplained(input, out) {
291
+ const allowlist = effectiveOperatorAllowlist(input.operators);
292
+ const byId = new Map(
293
+ input.registry.definitions.map((definition) => [definition.id, definition])
294
+ );
295
+ const bindingsByLesson = new Map(
296
+ input.registry.bundles.map((bundle) => [bundle.lessonId, bundle.bindings])
297
+ );
298
+ for (const lesson of input.notationInput.lessons) {
299
+ const explained = /* @__PURE__ */ new Set();
300
+ const noteEntryOperators = (latex) => {
301
+ for (const operator of collectOperators(latex)) {
302
+ explained.add(operator.name);
303
+ }
304
+ };
305
+ for (const definition of lesson.localDefinitions) {
306
+ noteEntryOperators(definition.notation);
307
+ }
308
+ for (const binding of bindingsByLesson.get(lesson.lessonId) ?? []) {
309
+ const definition = byId.get(binding.definitionId);
310
+ if (definition) noteEntryOperators(definition.notation);
311
+ }
312
+ const reported = /* @__PURE__ */ new Set();
313
+ for (const span of mathSpans(lesson.body)) {
314
+ for (const operator of collectOperators(span.latex)) {
315
+ if (allowlist.has(operator.name)) continue;
316
+ if (explained.has(operator.name)) continue;
317
+ if (reported.has(operator.name)) continue;
318
+ reported.add(operator.name);
319
+ if (input.lintIgnore.matches("operator-explained", operator.name)) {
320
+ continue;
321
+ }
322
+ const display = operator.source === "operatorname" ? `\\operatorname{${operator.name}}` : `\\${operator.name}`;
323
+ out.push({
324
+ code: "operator-explained",
325
+ severity: "warning",
326
+ lessonId: lesson.lessonId,
327
+ file: lesson.source.file,
328
+ glyph: operator.name,
329
+ message: `${lesson.lessonId}: operator ${JSON.stringify(display)} is neither on the course operator allowlist nor explained by a notation entry; give it a card or notation.local entry (latex ${JSON.stringify(display)}) introduced with [[key]], or add ${JSON.stringify(operator.name)} to operators.allow in course.config.ts`
330
+ });
331
+ }
332
+ }
333
+ }
334
+ }
203
335
  const CHECKS = [
204
336
  checkGlyphUniqueness,
205
337
  checkNotationUnits,
206
338
  checkConventionSingleDefinition,
207
339
  checkNumeralsTagged,
208
340
  checkWeakLocal,
209
- checkNotationSourceLocator
341
+ checkNotationSourceLocator,
342
+ checkGlossWantsPromoting,
343
+ checkCardWantsDemoting,
344
+ checkGlossNameShape,
345
+ checkOperatorExplained
210
346
  ];
211
347
  function runConsistencyChecks(input) {
212
348
  const diagnostics = [];
@@ -223,7 +359,11 @@ function consistencyCounts(diagnostics) {
223
359
  "convention-single-definition": 0,
224
360
  "numerals-tagged": 0,
225
361
  "weak-local": 0,
226
- "notation-source-locator": 0
362
+ "notation-source-locator": 0,
363
+ "gloss-wants-promoting": 0,
364
+ "card-wants-demoting": 0,
365
+ "gloss-name-shape": 0,
366
+ "operator-explained": 0
227
367
  };
228
368
  for (const diagnostic of diagnostics) counts[diagnostic.code] += 1;
229
369
  return counts;
@@ -0,0 +1,63 @@
1
+ interface DiagramNodeInput {
2
+ readonly id: string;
3
+ readonly width: number;
4
+ readonly height: number;
5
+ }
6
+ interface DiagramEdgeInput {
7
+ readonly from: string;
8
+ readonly to: string;
9
+ }
10
+ interface DiagramLayoutOptions {
11
+ /** Top-to-bottom (default) or left-to-right. */
12
+ readonly direction?: 'TB' | 'LR';
13
+ readonly nodeSeparation?: number;
14
+ readonly rankSeparation?: number;
15
+ /** Uniform padding added around the computed bounding box. */
16
+ readonly padding?: number;
17
+ }
18
+ interface Point {
19
+ readonly x: number;
20
+ readonly y: number;
21
+ }
22
+ interface LaidOutNode {
23
+ readonly id: string;
24
+ /** Top-left corner, not the center dagre itself works in. */
25
+ readonly x: number;
26
+ readonly y: number;
27
+ readonly width: number;
28
+ readonly height: number;
29
+ }
30
+ interface LaidOutEdge {
31
+ readonly from: string;
32
+ readonly to: string;
33
+ readonly points: readonly Point[];
34
+ }
35
+ interface DiagramLayout {
36
+ readonly nodes: readonly LaidOutNode[];
37
+ readonly edges: readonly LaidOutEdge[];
38
+ /** The SVG canvas size a caller should use as the `viewBox`. */
39
+ readonly width: number;
40
+ readonly height: number;
41
+ }
42
+ /**
43
+ * Lay out a small directed graph. Node ids must be unique; an edge naming an
44
+ * undeclared node fails loudly rather than silently dropping it (dagre itself
45
+ * would otherwise create a phantom zero-size node).
46
+ */
47
+ declare function layoutDiagram(nodes: readonly DiagramNodeInput[], edges: readonly DiagramEdgeInput[], options?: DiagramLayoutOptions): DiagramLayout;
48
+ interface BoxSizeEstimate {
49
+ readonly width: number;
50
+ readonly height: number;
51
+ }
52
+ /**
53
+ * Estimate a diagram node's box from its label text (or raw, pre-resolution
54
+ * LaTeX for a formula node). Deliberately generous: it is meant to avoid
55
+ * clipping, not to match a real render pixel-for-pixel. Pass an explicit
56
+ * `width`/`height` on the node to override for a label this estimate gets
57
+ * badly wrong.
58
+ */
59
+ declare function estimateBoxSize(content: string, kind: 'text' | 'formula', options?: {
60
+ readonly maxWidth?: number;
61
+ }): BoxSizeEstimate;
62
+
63
+ export { type BoxSizeEstimate, type DiagramEdgeInput, type DiagramLayout, type DiagramLayoutOptions, type DiagramNodeInput, type LaidOutEdge, type LaidOutNode, type Point, estimateBoxSize, layoutDiagram };
@@ -0,0 +1,122 @@
1
+ import dagre from "@dagrejs/dagre";
2
+ function layoutDiagram(nodes, edges, options = {}) {
3
+ const ids = /* @__PURE__ */ new Set();
4
+ for (const node of nodes) {
5
+ if (ids.has(node.id)) {
6
+ throw new Error(`Diagram declares node "${node.id}" more than once.`);
7
+ }
8
+ if (!(node.width > 0) || !(node.height > 0)) {
9
+ throw new Error(
10
+ `Diagram node "${node.id}" must have a positive width and height.`
11
+ );
12
+ }
13
+ ids.add(node.id);
14
+ }
15
+ for (const edge of edges) {
16
+ if (!ids.has(edge.from)) {
17
+ throw new Error(`Diagram edge references unknown node "${edge.from}".`);
18
+ }
19
+ if (!ids.has(edge.to)) {
20
+ throw new Error(`Diagram edge references unknown node "${edge.to}".`);
21
+ }
22
+ }
23
+ const graph = new dagre.graphlib.Graph();
24
+ graph.setGraph({
25
+ rankdir: options.direction ?? "TB",
26
+ nodesep: options.nodeSeparation ?? 24,
27
+ ranksep: options.rankSeparation ?? 48,
28
+ marginx: 0,
29
+ marginy: 0
30
+ });
31
+ graph.setDefaultEdgeLabel(() => ({}));
32
+ for (const node of nodes) {
33
+ graph.setNode(node.id, { width: node.width, height: node.height });
34
+ }
35
+ for (const edge of edges) {
36
+ graph.setEdge(edge.from, edge.to);
37
+ }
38
+ dagre.layout(graph);
39
+ const centeredNodes = nodes.map((node) => {
40
+ const label = graph.node(node.id);
41
+ const cx = label.x ?? 0;
42
+ const cy = label.y ?? 0;
43
+ return {
44
+ id: node.id,
45
+ x: cx - node.width / 2,
46
+ y: cy - node.height / 2,
47
+ width: node.width,
48
+ height: node.height
49
+ };
50
+ });
51
+ const rawEdges = edges.map((edge) => {
52
+ const label = graph.edge(edge.from, edge.to) ?? {};
53
+ return {
54
+ from: edge.from,
55
+ to: edge.to,
56
+ points: (label.points ?? []).map((point) => ({
57
+ x: point.x,
58
+ y: point.y
59
+ }))
60
+ };
61
+ });
62
+ const padding = options.padding ?? 12;
63
+ const xs = [
64
+ ...centeredNodes.flatMap((node) => [node.x, node.x + node.width]),
65
+ ...rawEdges.flatMap((edge) => edge.points.map((point) => point.x))
66
+ ];
67
+ const ys = [
68
+ ...centeredNodes.flatMap((node) => [node.y, node.y + node.height]),
69
+ ...rawEdges.flatMap((edge) => edge.points.map((point) => point.y))
70
+ ];
71
+ const minX = xs.length > 0 ? Math.min(...xs) : 0;
72
+ const minY = ys.length > 0 ? Math.min(...ys) : 0;
73
+ const maxX = xs.length > 0 ? Math.max(...xs) : 0;
74
+ const maxY = ys.length > 0 ? Math.max(...ys) : 0;
75
+ const shiftX = padding - minX;
76
+ const shiftY = padding - minY;
77
+ return {
78
+ nodes: centeredNodes.map((node) => ({
79
+ ...node,
80
+ x: node.x + shiftX,
81
+ y: node.y + shiftY
82
+ })),
83
+ edges: rawEdges.map((edge) => ({
84
+ ...edge,
85
+ points: edge.points.map((point) => ({
86
+ x: point.x + shiftX,
87
+ y: point.y + shiftY
88
+ }))
89
+ })),
90
+ width: maxX - minX + padding * 2,
91
+ height: maxY - minY + padding * 2
92
+ };
93
+ }
94
+ const DEFAULT_MAX_WIDTH = 220;
95
+ const MIN_WIDTH = 90;
96
+ const PADDING_X = 20;
97
+ const PADDING_Y = 16;
98
+ const LINE_HEIGHT = 22;
99
+ const CHAR_WIDTH = {
100
+ text: 7.2,
101
+ formula: 11
102
+ };
103
+ function formulaVisibleLength(latex) {
104
+ const collapsed = latex.replace(/\\[a-zA-Z]+/g, "x").replace(/[{}$]/g, "").trim();
105
+ return Math.max(1, collapsed.length);
106
+ }
107
+ function estimateBoxSize(content, kind, options = {}) {
108
+ const maxWidth = options.maxWidth ?? DEFAULT_MAX_WIDTH;
109
+ const visibleLength = kind === "formula" ? formulaVisibleLength(content) : content.trim().length || 1;
110
+ const charWidth = CHAR_WIDTH[kind];
111
+ const naturalWidth = visibleLength * charWidth + PADDING_X * 2;
112
+ const width = Math.min(maxWidth, Math.max(MIN_WIDTH, naturalWidth));
113
+ const usableWidth = width - PADDING_X * 2;
114
+ const charsPerLine = Math.max(1, Math.floor(usableWidth / charWidth));
115
+ const lines = Math.max(1, Math.ceil(visibleLength / charsPerLine));
116
+ const height = lines * LINE_HEIGHT + PADDING_Y * 2;
117
+ return { width: Math.round(width), height: Math.round(height) };
118
+ }
119
+ export {
120
+ estimateBoxSize,
121
+ layoutDiagram
122
+ };
@@ -44,6 +44,11 @@ interface EquationLessonInput {
44
44
  readonly slug: string;
45
45
  readonly file: string;
46
46
  readonly body: string;
47
+ /**
48
+ * The lesson's 1-based ordinal in the course. When set, equations are
49
+ * numbered `‹chapter›.‹n›`; when omitted, the legacy `##`-section scheme.
50
+ */
51
+ readonly chapter?: number;
47
52
  }
48
53
  declare function validateEquations(lessons: readonly EquationLessonInput[]): EquationValidation;
49
54
 
@@ -25,7 +25,7 @@ function validateEquations(lessons) {
25
25
  )) {
26
26
  const seen = /* @__PURE__ */ new Set();
27
27
  const table = {};
28
- for (const raw of scanEquationLabels(lesson.body)) {
28
+ for (const raw of scanEquationLabels(lesson.body, lesson.chapter)) {
29
29
  if (seen.has(raw.key)) {
30
30
  diagnostics.push({
31
31
  code: "eq-duplicate-key",
@@ -8,42 +8,100 @@ export function parseEquationRef(raw: any): {
8
8
  } | undefined;
9
9
  /** Remove every `\label{eq:…}` (and the whitespace it leaves) from a math source. */
10
10
  export function stripEquationLabels(latex: any): string;
11
+ /**
12
+ * The visible number for one display equation. With `chapter` (the lesson's
13
+ * course ordinal) the scheme is `‹chapter›.‹indexInLesson›` and `##` headings
14
+ * are ignored; without it, the legacy `##`-section scheme.
15
+ */
16
+ export function displayEquationNumber({ chapter, section, indexInSection, indexInLesson, }: {
17
+ chapter: any;
18
+ section: any;
19
+ indexInSection: any;
20
+ indexInLesson: any;
21
+ }): string;
22
+ /** The positional anchor id for an unkeyed equation, e.g. `2.4` → `eq-2-4`. */
23
+ export function equationAnchorId({ key, number }: {
24
+ key: any;
25
+ number: any;
26
+ }): string;
27
+ export function stripFencedCode(markdown: any): string;
28
+ /**
29
+ * Blank the body of every block-level MDX component (`<Capitalised …> … </…>`)
30
+ * so a `$$…$$` used to illustrate a worked example inside `<CompactExample>`
31
+ * (or an `<Aside>`, etc.) is not counted as a lesson-body equation. The
32
+ * outermost match wins, so nested components are blanked in one pass;
33
+ * newlines are kept so line/section structure is unchanged.
34
+ *
35
+ * `keep`, when given, leaves a component of that name untouched instead of
36
+ * blanking it — `keyed-jsx-floats.mjs` uses this to blank every *other*
37
+ * component while still finding `<Table>`/`<Figure>` tags at the top level.
38
+ */
39
+ export function stripJsxComponents(markdown: any, keep: any): string;
11
40
  /**
12
41
  * Every keyed display equation in a Markdown body, in appearance order, with
13
- * its assigned number. `$$…$$` blocks only; `##` headings between them advance
14
- * the section counter.
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.
15
48
  */
16
- export function scanEquationLabels(markdown: any): {
17
- key: string | undefined;
49
+ export function scanEquationLabels(markdown: any, chapter: any): {
50
+ key: string;
18
51
  number: string;
19
52
  section: number;
20
53
  indexInSection: number;
21
54
  }[];
22
55
  /**
23
- * The mdast equivalent of `scanEquationLabels`: walk the tree in document
24
- * order, advancing the section counter on every depth-2 heading and emitting a
25
- * label for every `math` node whose source carries `\label{eq:…}`.
56
+ * The mdast equivalent of `scanEquationLabels` for *every* display equation:
57
+ * walk the tree in document order, advancing the section counter on every
58
+ * 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
60
+ * same way as in `scanEquationLabels`.
26
61
  */
27
- export function collectEquationLabels(tree: any): any[];
62
+ export function collectDisplayEquations(tree: any, chapter: any): {
63
+ key: string | null;
64
+ number: string;
65
+ section: number;
66
+ indexInSection: number;
67
+ node: any;
68
+ }[];
69
+ /**
70
+ * The keyed subset of `collectDisplayEquations` — the tree-walk counterpart of
71
+ * `scanEquationLabels`. Numbers reflect position among all display equations.
72
+ */
73
+ export function collectEquationLabels(tree: any, chapter: any): {
74
+ key: string | null;
75
+ number: string;
76
+ section: number;
77
+ indexInSection: number;
78
+ node: any;
79
+ }[];
28
80
  /**
29
- * Equation identity (Step D7) — shared, framework-free helpers.
81
+ * Equation identity (Step D7, extended F3) — shared, framework-free helpers.
30
82
  *
31
- * An author marks a display equation by putting a trailing `\label{eq:<key>}`
32
- * inside its `$$…$$` block (inside math, so the `:` is safe and the form
33
- * matches amsmath). The compiler assigns the *visible* number from appearance
34
- * order, scoped to the enclosing `##` section (`(2.4)` = fourth keyed equation
35
- * under the second section). The number is never authored; unkeyed display
36
- * equations render clean and stay un-referenceable. Prose refers to an
37
- * equation with the reserved `eq-` prefix (a bare `:` in `[[…]]` prose would
38
- * be eaten by `remark-directive`):
83
+ * 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
90
+ * marks an equation with a trailing `\label{eq:<key>}` inside its `$$…$$`
91
+ * block (inside math, so the `:` is safe and the form matches amsmath) when
92
+ * they want a *stable* anchor that survives edits and cross-page references:
39
93
  *
40
94
  * [[eq-discount-factor-def]] same page → "(2.4)" linked to #eq-…
41
95
  * [[foundations/present-value#eq-pv]] cross page → number from the manifest
42
96
  *
97
+ * An unkeyed equation still gets a positional anchor (`#eq-2-4`) so it can be
98
+ * deep-linked, but only a keyed equation is referenceable by name.
99
+ *
43
100
  * `scanEquationLabels` (raw Markdown) and `collectEquationLabels` (mdast tree)
44
101
  * MUST agree — `tests/notation/equations.test.ts` checks that on the real
45
102
  * corpus. Both are here so `manifest.ts` and `remark-notation.mjs` share one
46
- * numbering algorithm.
103
+ * numbering algorithm. `collectDisplayEquations` is the superset the render
104
+ * path uses to number and anchor every display equation, keyed or not.
47
105
  */
48
106
  /** `eq:` key grammar — the notation-key shape without the dotted namespaces. */
49
107
  export const EQ_KEY_PATTERN: RegExp;