explico 0.11.1 → 0.12.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 (62) hide show
  1. package/CHANGELOG.md +171 -1
  2. package/dist/compiler/astro.d.ts +7 -1
  3. package/dist/compiler/astro.js +5 -2
  4. package/dist/compiler/cli.d.ts +1 -0
  5. package/dist/compiler/collections.d.ts +25 -2
  6. package/dist/compiler/collections.js +37 -4
  7. package/dist/compiler/exports/standalone-html.d.ts +1 -0
  8. package/dist/compiler/manifest.d.ts +30 -4
  9. package/dist/compiler/manifest.js +49 -10
  10. package/dist/compiler/resolution-report.d.ts +1 -0
  11. package/dist/reference/base-operators.d.mts +6 -0
  12. package/dist/reference/base-operators.mjs +6 -8
  13. package/dist/reference/figure-symbols.d.ts +32 -0
  14. package/dist/reference/figure-symbols.js +55 -0
  15. package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
  16. package/dist/reference/gate-non-chapter-pages.js +156 -0
  17. package/dist/reference/gate-part-pages.d.ts +3 -1
  18. package/dist/reference/gate-part-pages.js +2 -1
  19. package/dist/reference/lab-katex-validate.d.ts +43 -0
  20. package/dist/reference/lab-katex-validate.js +96 -0
  21. package/dist/reference/lint-ignore.js +1 -1
  22. package/dist/reference/page-notation.d.ts +5 -3
  23. package/dist/reference/page-notation.js +8 -8
  24. package/dist/reference/raw-floats.mjs +37 -13
  25. package/dist/reference/registry.js +11 -1
  26. package/dist/reference/remark-notation.mjs +195 -7
  27. package/dist/reference/render-lab-math.d.ts +11 -2
  28. package/dist/reference/render-lab-math.js +2 -2
  29. package/dist/rules/catalog.js +33 -3
  30. package/dist/rules/rules.schema.json +22 -4
  31. package/docs/authoring/README.md +2 -1
  32. package/docs/authoring/components.md +4 -4
  33. package/docs/authoring/migrating.md +215 -0
  34. package/docs/authoring/notation.md +4 -1
  35. package/docs/authoring/structure-rules.md +75 -5
  36. package/docs/authoring/structure.md +14 -0
  37. package/docs/authoring/visualizations.md +26 -10
  38. package/package.json +1 -1
  39. package/src/compiler/astro.ts +17 -6
  40. package/src/compiler/collections.ts +67 -9
  41. package/src/compiler/manifest.ts +89 -13
  42. package/src/components/CourseContents.astro +6 -0
  43. package/src/components/PartOverview.astro +6 -0
  44. package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
  45. package/src/components/disclosure/print-as-shown.ts +42 -6
  46. package/src/components/figure/Figure.astro +6 -1
  47. package/src/components/figure/figure-state.ts +26 -17
  48. package/src/components/starlight/LessonFooter.astro +27 -7
  49. package/src/components/table/Table.astro +1 -1
  50. package/src/reference/base-operators.mjs +6 -8
  51. package/src/reference/figure-symbols.ts +117 -0
  52. package/src/reference/gate-non-chapter-pages.ts +241 -0
  53. package/src/reference/gate-part-pages.ts +1 -1
  54. package/src/reference/lab-katex-validate.ts +156 -0
  55. package/src/reference/lint-ignore.ts +4 -1
  56. package/src/reference/page-notation.ts +14 -11
  57. package/src/reference/raw-floats.mjs +37 -13
  58. package/src/reference/registry.ts +16 -1
  59. package/src/reference/remark-notation.mjs +195 -7
  60. package/src/reference/render-lab-math.ts +12 -1
  61. package/src/rules/catalog.ts +37 -3
  62. package/src/styles/global.css +32 -9
@@ -84,6 +84,10 @@ import {
84
84
  validateRawFloats,
85
85
  type RawFloatDiagnostic,
86
86
  } from '../reference/raw-floats-validate';
87
+ import {
88
+ validateLabKatexImports,
89
+ type LabKatexDiagnostic,
90
+ } from '../reference/lab-katex-validate';
87
91
  import {
88
92
  gateContentMath,
89
93
  pageScope,
@@ -102,6 +106,7 @@ import { gateCourseRules } from '../reference/gate-course-rules';
102
106
  import { gateHeadingLength } from '../reference/gate-heading-length';
103
107
  import { gateHeadingStructure } from '../reference/gate-heading-structure';
104
108
  import { gatePartPages } from '../reference/gate-part-pages';
109
+ import { gateNonChapterPages } from '../reference/gate-non-chapter-pages';
105
110
  import {
106
111
  scanSectionHeadings,
107
112
  scanTitledBlocks,
@@ -161,6 +166,7 @@ import {
161
166
  loadCurriculumCatalog,
162
167
  loadNotationDefinitions,
163
168
  loadNotationRegistryInput,
169
+ loadNonChapterPages,
164
170
  loadNotationSpecs,
165
171
  loadPartOverviews,
166
172
  loadSourceRecords,
@@ -290,8 +296,14 @@ import {
290
296
  * state of a `<Fold>`; `disclosure.figures` now says whether a lab's tools
291
297
  * start open; `diagnostics.folds` holds `fold-unnamed`
292
298
  * (docs/plans/figures-and-short-captions.md, Parts 1–2).
299
+ *
300
+ * v25 (cards on non-chapter pages, rule S10): `pages` — every docs page that
301
+ * is not a chapter (the landing page, a part's overview page, …) with its
302
+ * `id`, `slug`, `file` and a `notation` bundle of the lesson shape, so the
303
+ * footer renders the cards its `[[key]]` terms and math open.
304
+ * `notation.bundles` and `notation.backlinks` now carry those pages' ids too.
293
305
  */
294
- export const MANIFEST_SCHEMA_VERSION = 24;
306
+ export const MANIFEST_SCHEMA_VERSION = 25;
295
307
 
296
308
  const REPOSITORY_ROOT = process.cwd();
297
309
  // Content is the top-level `content/` folder; the build output is `build/`
@@ -502,6 +514,20 @@ export interface ManifestLesson {
502
514
  };
503
515
  }
504
516
 
517
+ /**
518
+ * A docs page that is not a chapter (v25, rule S10) — the landing page, a
519
+ * part's overview page, any page outside a part directory. It numbers
520
+ * nothing, so it carries only what its cards need: the notation bundle its
521
+ * `[[key]]` terms and math reach, shared entries only.
522
+ */
523
+ export interface ManifestPage {
524
+ /** `index`, `motion.index`, `glossary` — `lessonIdFromSlug` of the slug; never a lesson id. */
525
+ readonly id: string;
526
+ readonly slug: string;
527
+ readonly file: string;
528
+ readonly notation: ManifestLesson['notation'];
529
+ }
530
+
505
531
  /** The reader-facing "about this material" bundle — the `[?]` panel's data. */
506
532
  export interface ManifestAbout {
507
533
  /** `explico`'s own identity, from its `package.json` (the version's golden source). */
@@ -597,6 +623,8 @@ export interface Manifest {
597
623
  /** The course's parts in config order, with their chapters and overview pages. */
598
624
  readonly parts: readonly ManifestPart[];
599
625
  readonly lessons: readonly ManifestLesson[];
626
+ /** Every docs page that is not a chapter, by id (v25). */
627
+ readonly pages: readonly ManifestPage[];
600
628
  /** Full competency prerequisite graph. */
601
629
  readonly prereqEdges: readonly PrereqEdge[];
602
630
  /** Equation identity (D7): every labelled display equation + the render-path lookup. */
@@ -660,8 +688,12 @@ export interface Manifest {
660
688
  readonly equations: readonly EquationDiagnostic[];
661
689
  /** `<Table>` identity checks (F-tables-figures) — same codes, `tab-` prefixed. */
662
690
  readonly tables: readonly TableDiagnostic[];
663
- /** `<Figure>` identity checks (F-tables-figures) — same codes, `fig-` prefixed. */
664
- readonly figures: readonly FigureDiagnostic[];
691
+ /**
692
+ * `<Figure>` identity checks (F-tables-figures) — same codes, `fig-`
693
+ * prefixed — and rule V3's `lab-katex-direct`: a file a lesson imports
694
+ * that calls KaTeX itself.
695
+ */
696
+ readonly figures: readonly (FigureDiagnostic | LabKatexDiagnostic)[];
665
697
  /** `<Diagram>` identity checks — same codes, `dia-` prefixed. */
666
698
  readonly diagrams: readonly DiagramDiagnostic[];
667
699
  /** Knowledge-check identity and `[[check-…]]` reference checks. */
@@ -925,7 +957,19 @@ export async function compileManifest(
925
957
  loadAbbreviationRegistryInput(),
926
958
  ]);
927
959
 
928
- const registry = buildNotationRegistry(notationInput);
960
+ // Rule S10: a page that is not a chapter explains its symbols like a
961
+ // chapter does, so its references resolve through the same registry and its
962
+ // text and math pass the same gates. It stays out of `notationInput.lessons`,
963
+ // which is the chapter list every numbering and curriculum pass reads.
964
+ const nonChapterPages = loadNonChapterPages();
965
+ const withPages: typeof notationInput = {
966
+ ...notationInput,
967
+ lessons: [
968
+ ...notationInput.lessons,
969
+ ...nonChapterPages.map((page) => page.notation),
970
+ ],
971
+ };
972
+ const registry = buildNotationRegistry(withPages);
929
973
  // Cards a `formula` may name with `\explain{key}` (D15). Shared entries
930
974
  // only: a page-local symbol is not addressable from another entry.
931
975
  const cardsByKey = new Map<string, GlyphScopeEntry>(
@@ -956,7 +1000,7 @@ export async function compileManifest(
956
1000
  };
957
1001
  const mathIssues = [
958
1002
  ...gateContentMath({
959
- notationInput,
1003
+ notationInput: withPages,
960
1004
  assessments: assessmentsInput,
961
1005
  lessonAssessments: catalog.lessons.map((lesson) => ({
962
1006
  lessonId: lesson.id,
@@ -965,7 +1009,7 @@ export async function compileManifest(
965
1009
  }),
966
1010
  ...(rules.isEnabled('N9')
967
1011
  ? [
968
- ...gateProseNotation(notationInput.lessons, {
1012
+ ...gateProseNotation(withPages.lessons, {
969
1013
  allowSpelledUnits: rules.strings('N9', 'allow'),
970
1014
  }),
971
1015
  ...gateProseNotationInAssessments(assessmentsInput, {
@@ -975,13 +1019,13 @@ export async function compileManifest(
975
1019
  : []),
976
1020
  ...(rules.isEnabled('N12')
977
1021
  ? [
978
- ...gateMathPunctuation(notationInput.lessons, mathEndAbbreviations),
1022
+ ...gateMathPunctuation(withPages.lessons, mathEndAbbreviations),
979
1023
  ...gateMathPunctuationInAssessments(assessmentsInput, mathEndAbbreviations),
980
1024
  ]
981
1025
  : []),
982
1026
  ...(rules.isEnabled('N14')
983
1027
  ? gateAbbreviations({
984
- notationInput,
1028
+ notationInput: withPages,
985
1029
  abbreviations: abbreviationsInput,
986
1030
  courseAbbreviations: rules.settings('N14'),
987
1031
  })
@@ -998,7 +1042,9 @@ export async function compileManifest(
998
1042
  // The course's own rules (`courseRules`): one pass for every enabled `X`
999
1043
  // rule that carries a `forbid` pattern. A disabled one, and a guide rule
1000
1044
  // with no pattern, do no work here.
1001
- ...gateCourseRules(notationInput.lessons, rules.course),
1045
+ ...gateCourseRules(withPages.lessons, rules.course),
1046
+ // S10 is locked: a float on a non-chapter page has no number to render.
1047
+ ...gateNonChapterPages(nonChapterPages),
1002
1048
  ];
1003
1049
 
1004
1050
  // Pre-render every assessment `prompt` / `explanation` into text/math
@@ -1077,10 +1123,12 @@ export async function compileManifest(
1077
1123
  }
1078
1124
  // Per-place waivers: drop the located findings each one covers, and
1079
1125
  // report the waivers themselves — a stale one is a warning, a bad one an
1080
- // error. Only lesson bodies are scanned; a part page has no waivable rule.
1126
+ // error. Chapters and non-chapter pages are scanned alike, since the text
1127
+ // and math gates run on both; S2 and S10 are per-file or locked, so no
1128
+ // waiver reaches them.
1081
1129
  const waiverDiagnostics: WaiverDiagnostic[] = [];
1082
1130
  const waiverRules = { courseRules: rules.course };
1083
- const waivers = notationInput.lessons.flatMap((lesson) => {
1131
+ const waivers = withPages.lessons.flatMap((lesson) => {
1084
1132
  const collected = collectWaivers(lesson, waiverRules);
1085
1133
  waiverDiagnostics.push(...collected.diagnostics);
1086
1134
  return collected.waivers;
@@ -1369,6 +1417,16 @@ export async function compileManifest(
1369
1417
  },
1370
1418
  );
1371
1419
  };
1420
+ // Rule V3: the course files a lesson's imports reach draw math through
1421
+ // `renderLabMath`, never KaTeX itself.
1422
+ const labKatexDiagnostics = validateLabKatexImports(
1423
+ notationInput.lessons.map((lesson) => ({
1424
+ lessonId: lesson.lessonId,
1425
+ file: lesson.source.file,
1426
+ body: lesson.body,
1427
+ })),
1428
+ REPOSITORY_ROOT,
1429
+ );
1372
1430
  const rawFloatDiagnostics = validateRawFloats(
1373
1431
  notationInput.lessons.map((lesson) => ({
1374
1432
  lessonId: lesson.lessonId,
@@ -1446,10 +1504,27 @@ export async function compileManifest(
1446
1504
  });
1447
1505
  }
1448
1506
  lessons.sort((a, b) => a.id.localeCompare(b.id));
1507
+ const pages: ManifestPage[] = nonChapterPages
1508
+ .map((page) => ({
1509
+ id: page.id,
1510
+ slug: page.slug,
1511
+ file: page.file,
1512
+ notation: lessonNotationBundle(
1513
+ bundlesByLesson.get(page.id),
1514
+ definitionsById,
1515
+ lessonNotationResolution(
1516
+ page.notation,
1517
+ definitionsById,
1518
+ availableDefinitionIds,
1519
+ ),
1520
+ cardsByKey,
1521
+ ),
1522
+ }))
1523
+ .sort((a, b) => a.id.localeCompare(b.id));
1449
1524
 
1450
1525
  const consistencyIssues = runConsistencyChecks({
1451
1526
  registry,
1452
- notationInput,
1527
+ notationInput: withPages,
1453
1528
  specs: loadNotationSpecs(),
1454
1529
  catalog,
1455
1530
  lintIgnore: loadLintIgnore(),
@@ -1543,6 +1618,7 @@ export async function compileManifest(
1543
1618
  sidebar,
1544
1619
  parts: manifestParts,
1545
1620
  lessons,
1621
+ pages,
1546
1622
  prereqEdges,
1547
1623
  equations: {
1548
1624
  labels: equationValidation.labels,
@@ -1578,7 +1654,7 @@ export async function compileManifest(
1578
1654
  consistency: dropDisabled(consistencyIssues, off),
1579
1655
  equations: dropDisabled(equationValidation.diagnostics, off),
1580
1656
  tables: dropDisabled(tableValidation.diagnostics, off),
1581
- figures: dropDisabled(figureValidation.diagnostics, off),
1657
+ figures: dropDisabled([...figureValidation.diagnostics, ...labKatexDiagnostics], off),
1582
1658
  diagrams: dropDisabled(diagramValidation.diagnostics, off),
1583
1659
  checks: dropDisabled(checkValidation.diagnostics, off),
1584
1660
  examples: dropDisabled(exampleValidation.diagnostics, off),
@@ -121,6 +121,12 @@ const hours = (range: { min: number; max: number }) => {
121
121
  margin-block-start: 1.5rem;
122
122
  }
123
123
 
124
+ /* `.not-content` keeps Starlight's Markdown link colour out, so the
125
+ contents set the course accent on their own links. */
126
+ .course-contents a {
127
+ color: var(--sl-color-text-accent);
128
+ }
129
+
124
130
  .course-meta,
125
131
  .part-line {
126
132
  display: flex;
@@ -262,6 +262,12 @@ const hasRail =
262
262
  margin-block-start: 1.5rem;
263
263
  }
264
264
 
265
+ /* `.not-content` keeps Starlight's Markdown link colour out, so the
266
+ overview sets the course accent on its own links. */
267
+ .part-overview a {
268
+ color: var(--sl-color-text-accent);
269
+ }
270
+
265
271
  .part-meta {
266
272
  display: flex;
267
273
  flex-wrap: wrap;
@@ -21,8 +21,8 @@ const entries = (await getCollection('abbreviations')).sort((left, right) =>
21
21
  ) : (
22
22
  <dl class="abbreviation-glossary">
23
23
  {entries.map((entry) => (
24
- <div class="abbreviation-entry" id={`abbr-${entry.data.key}`}>
25
- <dt>
24
+ <div class="abbreviation-entry">
25
+ <dt id={`abbr-${entry.data.key}`}>
26
26
  {entry.data.abbr}
27
27
  {entry.data.aliases.length > 0 && (
28
28
  <span class="abbreviation-aliases">
@@ -46,6 +46,9 @@ const entries = (await getCollection('abbreviations')).sort((left, right) =>
46
46
  row-gap: 0.75rem;
47
47
  }
48
48
 
49
+ /* The wrapper puts each `<dt>`/`<dd>` pair straight into the grid, so it
50
+ has no box of its own: the entry's anchor id sits on the `<dt>`, since
51
+ an element with no box can never be scrolled to. */
49
52
  .abbreviation-entry {
50
53
  display: contents;
51
54
  }
@@ -53,6 +56,7 @@ const entries = (await getCollection('abbreviations')).sort((left, right) =>
53
56
  .abbreviation-entry dt {
54
57
  font-weight: 600;
55
58
  white-space: nowrap;
59
+ scroll-margin-block-start: 5rem;
56
60
  }
57
61
 
58
62
  .abbreviation-entry dd {
@@ -16,6 +16,15 @@
16
16
  * never prints, open or folded, and is left exactly as it is: no note, not
17
17
  * opened for an export.
18
18
  *
19
+ * A note never goes inside an Astro island. An island still waiting to
20
+ * hydrate (`client:visible` — every knowledge check) that starts while the
21
+ * page is prepared for print would find a node its framework never rendered
22
+ * and throw away its server render (React's error #418); a hydrated one's
23
+ * markup is its framework's to own either way. A closed disclosure inside
24
+ * an island gets its note after the outermost island instead, once per
25
+ * target URL: the folded questions of one check all point at the check's
26
+ * own anchor, so one note after the check says it for all of them.
27
+ *
19
28
  * Two callers share this module:
20
29
  * - `LessonExportActions.astro`, the print controller a course gets with
21
30
  * `lessonExports.print`: `'as-shown'` for its print button and the
@@ -90,6 +99,15 @@ function anchorIdFor(element: Element): string | undefined {
90
99
  return undefined;
91
100
  }
92
101
 
102
+ /** The outermost `<astro-island>` around an element, if any. */
103
+ function outermostIsland(element: Element): Element | null {
104
+ let island: Element | null = null;
105
+ for (let node = element.parentElement; node; node = node.parentElement) {
106
+ if (node.tagName === 'ASTRO-ISLAND') island = node;
107
+ }
108
+ return island;
109
+ }
110
+
93
111
  function createNote(href: string): HTMLElement {
94
112
  const note = document.createElement('div');
95
113
  note.className = COLLAPSED_NOTE_CLASS;
@@ -152,20 +170,38 @@ export function prepareDisclosures(
152
170
  const insideClosed = (element: Element) =>
153
171
  !!element.parentElement?.closest('details:not([open])');
154
172
 
173
+ // The notes already placed after each island: the last one, which the
174
+ // next follows, and the URLs they name.
175
+ const afterIsland = new Map<Element, { last: Element; hrefs: Set<string> }>();
176
+ const place = (element: Element, where: 'before' | 'after') => {
177
+ const href = options.noteHref(anchorIdFor(element));
178
+ const island = outermostIsland(element);
179
+ if (!island) {
180
+ const note = createNote(href);
181
+ element[where](note);
182
+ notes.push(note);
183
+ return;
184
+ }
185
+ const placed = afterIsland.get(island) ?? { last: island, hrefs: new Set<string>() };
186
+ if (placed.hrefs.has(href)) return;
187
+ const note = createNote(href);
188
+ placed.last.after(note);
189
+ placed.last = note;
190
+ placed.hrefs.add(href);
191
+ afterIsland.set(island, placed);
192
+ notes.push(note);
193
+ };
194
+
155
195
  for (const details of main.querySelectorAll('details:not([open]):not([data-print-omit])')) {
156
196
  if (insideClosed(details)) continue;
157
- const note = createNote(options.noteHref(anchorIdFor(details)));
158
- details.after(note);
159
- notes.push(note);
197
+ place(details, 'after');
160
198
  }
161
199
  // Disclosures built on `hidden` rather than `<details>`.
162
200
  for (const element of main.querySelectorAll(
163
201
  ':is(.short-note, .assessment-panel)[hidden]',
164
202
  )) {
165
203
  if (insideClosed(element)) continue;
166
- const note = createNote(options.noteHref(anchorIdFor(element)));
167
- element.before(note);
168
- notes.push(note);
204
+ place(element, 'before');
169
205
  }
170
206
  }
171
207
 
@@ -84,7 +84,7 @@ if (short !== undefined && (typeof short !== 'string' || !short.trim())) {
84
84
  }
85
85
  if (!number || !anchorId) {
86
86
  throw new Error(
87
- 'Figure is missing its computed `number`/`anchorId` — this page must run remark-notation with a `figures` cross-page table configured (see astro.config.mjs).',
87
+ `Figure is missing its computed \`number\`/\`anchorId\`. remark-notation numbers a <Figure> only on a lesson page, at its top level or in a <Memo>, <ShortNote> or <Fold>: elsewhere (a worked example) show the content without <Figure>, and on a page that is not a chapter (rule S10) link the chapter's figure. At a lesson's top level, this page is not running remark-notation with the manifest's \`figures\` (see astro.config.mjs).`,
88
88
  );
89
89
  }
90
90
 
@@ -176,8 +176,13 @@ const name = `Figure ${number}`;
176
176
  Q8). A figure that should read as set apart goes in a `### … [card]`
177
177
  section. The caption line is styled once for every float, in
178
178
  `global.css` (`.float-caption`). */
179
+ /* A wide picture scrolls sideways. `overflow-x: auto` alone computes
180
+ `overflow-y` to `auto` too, and a body with no fixed height overflows
181
+ vertically only by rounding (a fractional height at a browser zoom),
182
+ which drew a one-pixel vertical scrollbar beside the figure. */
179
183
  .keyed-figure__body {
180
184
  overflow-x: auto;
185
+ overflow-y: hidden;
181
186
  }
182
187
 
183
188
  /* A lab's tools: a tray of their own after the caption line — a thin
@@ -7,35 +7,44 @@
7
7
  * caption (a preview card, a fold's bar, the list of figures) still reads the
8
8
  * caption alone, and it prints with the caption.
9
9
  *
10
- * A lab passes its clause as runs of text, never HTML: a string is plain
11
- * text, `{ text, italic: true }` an italic run (a symbol), `{ sub }` a
12
- * subscript. There is no `aria-live`: the controls already announce their
13
- * values.
10
+ * A lab passes its clause as runs, never its own HTML: a string is plain
11
+ * text, and `{ math }` is a symbol or a formula — lab math rendered at build
12
+ * time by `renderLabMath(template, scope, { display: false })`, so each of
13
+ * its symbols is bound to the page's notation and opens its card, as it
14
+ * does in the lesson (rule N4). A symbol typed as text — the italic and
15
+ * subscript runs this took before — opened no card and was not even the
16
+ * corpus glyph (an italic `x` for a bold `\mathbf{x}`), so they are gone.
17
+ * There is no `aria-live`: the controls already announce their values.
14
18
  */
15
19
 
16
- export type FigureStateRun =
17
- | string
18
- | { readonly text: string; readonly italic?: boolean }
19
- | { readonly sub: string };
20
+ import type { RenderedLabMath } from '../../reference/lab-math-slot';
21
+
22
+ export type FigureStateRun = string | { readonly math: RenderedLabMath };
20
23
 
21
24
  type NodeFactory = Pick<Document, 'createElement' | 'createTextNode'>;
22
25
 
23
- /** The nodes for `runs`, made by `doc`. Markup in a run stays text. */
26
+ /**
27
+ * The nodes for `runs`, made by `doc`. Markup in a text run stays text; a
28
+ * math run is `renderLabMath`'s own KaTeX, made at build time.
29
+ */
24
30
  export function figureStateNodes(
25
31
  runs: readonly FigureStateRun[],
26
32
  doc: NodeFactory,
27
33
  ): Node[] {
28
34
  return runs.map((run) => {
29
35
  if (typeof run === 'string') return doc.createTextNode(run);
30
- if ('sub' in run) {
31
- const sub = doc.createElement('sub');
32
- sub.textContent = run.sub;
33
- return sub;
36
+ if (typeof run === 'object' && run !== null && typeof run.math?.html === 'string') {
37
+ const math = doc.createElement('span');
38
+ math.className = 'float-caption__math';
39
+ math.innerHTML = run.math.html;
40
+ return math;
34
41
  }
35
- if (!run.italic) return doc.createTextNode(run.text);
36
- const italic = doc.createElement('i');
37
- italic.textContent = run.text;
38
- return italic;
42
+ throw new TypeError(
43
+ 'setFigureState takes text and { math } runs. A symbol in the clause is ' +
44
+ 'lab math, rendered with renderLabMath(template, scope, { display: false }) ' +
45
+ 'and passed to the island, so that it opens its notation card; an italic ' +
46
+ 'or a sub run no longer exists.',
47
+ );
39
48
  });
40
49
  }
41
50
 
@@ -5,6 +5,11 @@
5
5
  * footer. Both layers are optional progressive enhancements over static
6
6
  * markup that the remark adapters and the content collections already emit.
7
7
  *
8
+ * A page that is not a chapter (rule S10: the landing page, a part's overview
9
+ * page) gets the card layers too — notation, citation, abbreviation and float
10
+ * reference — from its own manifest bundle. The layers that serve a chapter's
11
+ * numbered floats and outline stay on chapters.
12
+ *
8
13
  * The resolved notation bundle comes from the one compiler manifest (Phase
9
14
  * D5); this component never rebuilds the registry.
10
15
  */
@@ -38,6 +43,15 @@ const routeSlug = Astro.locals.starlightRoute.entry.id;
38
43
  const lessonId = isLessonSlug(routeSlug)
39
44
  ? lessonIdFromSlug(routeSlug)
40
45
  : undefined;
46
+ const manifest = await loadManifest();
47
+ // A chapter, or a non-chapter page the manifest knows (not the 404 route).
48
+ // The glob loader shortens `<dir>/index.mdx` to the id `<dir>`, and may give
49
+ // the landing page an empty one, so match the page's docs slug either way.
50
+ const routeSlugs = new Set(
51
+ routeSlug === '' ? ['index'] : [routeSlug, `${routeSlug}/index`],
52
+ );
53
+ const pageId =
54
+ lessonId ?? manifest.pages.find((page) => routeSlugs.has(page.slug))?.id;
41
55
 
42
56
  const glossaryBase = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/glossary/`;
43
57
 
@@ -45,10 +59,11 @@ const definitions: NotationLayerDefinition[] = [];
45
59
  let notationDefaultOpen = false;
46
60
  let numberAliases = false;
47
61
  let suspended: NotationSuspension[] = [];
48
- if (lessonId) {
49
- const manifest = await loadManifest();
62
+ if (pageId) {
50
63
  notationDefaultOpen = manifest.disclosure.notation.defaultOpen;
51
64
  numberAliases = manifest.numbering.aliases;
65
+ }
66
+ if (lessonId) {
52
67
  const lesson = manifest.lessons.find((entry) => entry.id === lessonId);
53
68
  const routeData = Astro.locals.starlightRoute.entry.data as {
54
69
  notation?: {
@@ -74,10 +89,15 @@ if (lessonId) {
74
89
  .filter((definition) => definition.scope === 'local')
75
90
  .map((definition) => definition.key);
76
91
  assertLocalNotationParity(lessonId, authoredLocalKeys, renderedLocalKeys);
92
+ }
93
+ if (pageId) {
94
+ // A non-chapter page renders the bundle its body compiled to, and nothing a
95
+ // component registered: the glossary's formulas are its own layer's cards,
96
+ // and rule S10 keeps the floats that would register more off the page.
77
97
  const renderedDefinitions = notationDefinitionsForRenderedPage(
78
98
  manifest,
79
- lessonId,
80
- pageNotationDependencies(Astro.locals),
99
+ pageId,
100
+ lessonId ? pageNotationDependencies(Astro.locals) : [],
81
101
  );
82
102
  for (const definition of renderedDefinitions) {
83
103
  definitions.push({
@@ -107,10 +127,10 @@ if (lessonId) {
107
127
  suspended={suspended}
108
128
  />
109
129
  <AnchorScrollEnhancer />
110
- {lessonId && <CitationLayer />}
111
- {lessonId && <AbbreviationLayer />}
130
+ {pageId && <CitationLayer />}
131
+ {pageId && <AbbreviationLayer />}
112
132
  {lessonId && <KeyedFloatEnhancer aliases={numberAliases} />}
113
- {lessonId && <FloatRefLayer />}
133
+ {pageId && <FloatRefLayer />}
114
134
  {lessonId && <CollapsibleSections />}
115
135
  {lessonId && <TableOverflowExpander />}
116
136
  <DiagramEnhancer />
@@ -49,7 +49,7 @@ if (short !== undefined && (typeof short !== 'string' || !short.trim())) {
49
49
  }
50
50
  if (!number || !anchorId) {
51
51
  throw new Error(
52
- 'Table is missing its computed `number`/`anchorId` — this page must run remark-notation with a `tables` cross-page table configured (see astro.config.mjs).',
52
+ `Table is missing its computed \`number\`/\`anchorId\`. remark-notation numbers a <Table> only on a lesson page, at its top level or in a <Memo>, <ShortNote> or <Fold>: elsewhere (a worked example) write a plain Markdown table, and on a page that is not a chapter (rule S10) link the chapter's table. At a lesson's top level, this page is not running remark-notation with the manifest's \`tables\` (see astro.config.mjs).`,
53
53
  );
54
54
  }
55
55
  ---
@@ -13,6 +13,12 @@
13
13
  * baseline — trig, the exponential and logarithms, limits, extrema, and the
14
14
  * big operators — kept greppable and small like the base library.
15
15
  *
16
+ * Only the plain limit, `\lim`, is baseline. `\limsup` / `\liminf` and the
17
+ * category-theory `\injlim` / `\projlim` / `\varlimsup` / `\varliminf` /
18
+ * `\varinjlim` / `\varprojlim` read fine to someone who already knows them,
19
+ * but none is "everyone knows this" the way `\lim` or `\max` is — a course
20
+ * that uses one owes it a notation entry, like any other domain operator.
21
+ *
16
22
  * A domain operator (`Var`, `Cov`, `Corr`, `sgn`, `erf`, `Tr`, `argmax`, …) is
17
23
  * written `\operatorname{…}` and is NOT here: it earns a card, a
18
24
  * `notation.local` entry, or an explicit line in `course.config.ts`
@@ -47,14 +53,6 @@ export const BASE_OPERATORS = Object.freeze(
47
53
  'coth',
48
54
  // limits
49
55
  'lim',
50
- 'limsup',
51
- 'liminf',
52
- 'injlim',
53
- 'projlim',
54
- 'varlimsup',
55
- 'varliminf',
56
- 'varinjlim',
57
- 'varprojlim',
58
56
  // extrema
59
57
  'max',
60
58
  'min',