explico 0.11.1 → 0.13.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 (298) hide show
  1. package/CHANGELOG.md +1292 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +11 -4
  4. package/dist/compiler/astro.js +48 -15
  5. package/dist/compiler/cli.d.ts +27 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +33 -2
  8. package/dist/compiler/collections.js +91 -19
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +15 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +115 -8
  51. package/dist/compiler/manifest.js +221 -39
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +3 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +8 -2
  61. package/dist/reference/base-operators.mjs +8 -10
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figure-symbols.d.ts +32 -0
  88. package/dist/reference/figure-symbols.js +55 -0
  89. package/dist/reference/figures-validate.d.ts +1 -1
  90. package/dist/reference/gate-abbreviations.d.ts +1 -1
  91. package/dist/reference/gate-abbreviations.js +1 -1
  92. package/dist/reference/gate-course-rules.d.ts +4 -3
  93. package/dist/reference/gate-course-rules.js +9 -4
  94. package/dist/reference/gate-heading-length.d.ts +2 -0
  95. package/dist/reference/gate-heading-length.js +7 -5
  96. package/dist/reference/gate-heading-structure.js +26 -0
  97. package/dist/reference/gate-indices.d.ts +35 -0
  98. package/dist/reference/gate-indices.js +299 -0
  99. package/dist/reference/gate-math.d.ts +32 -2
  100. package/dist/reference/gate-math.js +68 -12
  101. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  102. package/dist/reference/gate-mdx-expression.js +29 -0
  103. package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
  104. package/dist/reference/gate-non-chapter-pages.js +156 -0
  105. package/dist/reference/gate-part-pages.d.ts +4 -2
  106. package/dist/reference/gate-part-pages.js +2 -1
  107. package/dist/reference/gate-prop-text.d.ts +28 -0
  108. package/dist/reference/gate-prop-text.js +106 -0
  109. package/dist/reference/gate-prose-notation.d.ts +3 -1
  110. package/dist/reference/gate-prose-notation.js +4 -2
  111. package/dist/reference/gate-retired-keys.d.ts +35 -0
  112. package/dist/reference/gate-retired-keys.js +58 -0
  113. package/dist/reference/gloss.d.ts +2 -0
  114. package/dist/reference/katex-measure.d.ts +17 -0
  115. package/dist/reference/katex-measure.js +223 -0
  116. package/dist/reference/keyed-float-validate.js +12 -0
  117. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  118. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  119. package/dist/reference/lab-katex-validate.d.ts +43 -0
  120. package/dist/reference/lab-katex-validate.js +96 -0
  121. package/dist/reference/lab-math-scope.d.ts +1 -0
  122. package/dist/reference/lab-math-scope.js +13 -2
  123. package/dist/reference/lint-ignore.js +1 -1
  124. package/dist/reference/math-glyphs.d.mts +94 -2
  125. package/dist/reference/math-glyphs.mjs +851 -46
  126. package/dist/reference/mixed-content.d.ts +8 -3
  127. package/dist/reference/mixed-content.js +4 -2
  128. package/dist/reference/page-notation.d.ts +39 -7
  129. package/dist/reference/page-notation.js +62 -10
  130. package/dist/reference/raw-floats.mjs +37 -13
  131. package/dist/reference/references.d.ts +11 -1
  132. package/dist/reference/references.js +6 -0
  133. package/dist/reference/registry.js +115 -11
  134. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  135. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  136. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  137. package/dist/reference/remark-citation.mjs +0 -0
  138. package/dist/reference/remark-notation.mjs +275 -23
  139. package/dist/reference/render-lab-math.d.ts +13 -2
  140. package/dist/reference/render-lab-math.js +18 -6
  141. package/dist/reference/sections-validate.d.ts +28 -3
  142. package/dist/reference/sections-validate.js +31 -4
  143. package/dist/reference/sections.d.mts +24 -6
  144. package/dist/reference/sections.mjs +92 -17
  145. package/dist/reference/tables-validate.d.ts +1 -1
  146. package/dist/reference/tables-validate.js +24 -1
  147. package/dist/reference/text-measure.d.ts +10 -0
  148. package/dist/reference/text-measure.js +41 -0
  149. package/dist/reference/text-widths.d.ts +10 -0
  150. package/dist/reference/text-widths.js +1238 -0
  151. package/dist/reference/types.d.ts +64 -8
  152. package/dist/rules/catalog.js +223 -32
  153. package/dist/rules/in-effect.d.ts +1 -1
  154. package/dist/rules/rules.schema.json +100 -27
  155. package/dist/rules/settings.d.ts +1 -1
  156. package/dist/rules/settings.js +7 -0
  157. package/dist/session/PreferenceStore.d.ts +18 -1
  158. package/dist/session/PreferenceStore.js +29 -1
  159. package/docs/authoring/README.md +33 -21
  160. package/docs/authoring/agents.md +38 -7
  161. package/docs/authoring/components.md +115 -10
  162. package/docs/authoring/migrating.md +878 -0
  163. package/docs/authoring/notation.md +285 -30
  164. package/docs/authoring/retired.md +1 -0
  165. package/docs/authoring/structure-rules.md +84 -45
  166. package/docs/authoring/structure.md +67 -2
  167. package/docs/authoring/visualizations.md +167 -25
  168. package/package.json +12 -7
  169. package/src/compiler/astro.ts +72 -21
  170. package/src/compiler/cli.ts +143 -35
  171. package/src/compiler/collections.ts +158 -27
  172. package/src/compiler/content-schema.ts +108 -0
  173. package/src/compiler/content-schemas.ts +391 -0
  174. package/src/compiler/course-config.ts +86 -15
  175. package/src/compiler/exports/combined-html.ts +669 -0
  176. package/src/compiler/exports/download-index.ts +36 -0
  177. package/src/compiler/exports/export-chapters.ts +233 -0
  178. package/src/compiler/exports/export-pages.ts +44 -0
  179. package/src/compiler/exports/html-tree.ts +62 -0
  180. package/src/compiler/exports/latex/capture.ts +651 -0
  181. package/src/compiler/exports/latex/compile.ts +168 -0
  182. package/src/compiler/exports/latex/document.ts +479 -0
  183. package/src/compiler/exports/latex/explico.cls +415 -0
  184. package/src/compiler/exports/latex/numbering.ts +74 -0
  185. package/src/compiler/exports/latex/serialize.ts +787 -0
  186. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  187. package/src/compiler/exports/latex-export.ts +255 -0
  188. package/src/compiler/exports/markdown/serialize.ts +632 -0
  189. package/src/compiler/exports/markdown-export.ts +329 -0
  190. package/src/compiler/exports/standalone-html.ts +39 -70
  191. package/src/compiler/exports/static-tree.ts +944 -0
  192. package/src/compiler/exports/zip.ts +88 -0
  193. package/src/compiler/lesson-derivation.ts +13 -8
  194. package/src/compiler/manifest.ts +387 -40
  195. package/src/compiler/render-diagnostics.ts +111 -0
  196. package/src/components/CourseContents.astro +6 -0
  197. package/src/components/HoverPanel.astro +204 -6
  198. package/src/components/PartOverview.astro +18 -9
  199. package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
  200. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  201. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  202. package/src/components/assessments/AssessmentSet.astro +14 -1
  203. package/src/components/block-heading.ts +38 -0
  204. package/src/components/card-arrow.ts +18 -4
  205. package/src/components/card-carry.ts +406 -0
  206. package/src/components/card-drag.ts +1 -30
  207. package/src/components/card-fetch.ts +87 -0
  208. package/src/components/card-manager.ts +1222 -38
  209. package/src/components/card-memory.ts +5 -1
  210. package/src/components/card-placement.ts +211 -13
  211. package/src/components/card-side.ts +281 -31
  212. package/src/components/citation/CitationLayer.astro +45 -40
  213. package/src/components/diagram/Diagram.astro +123 -44
  214. package/src/components/disclosure/print-as-shown.ts +42 -6
  215. package/src/components/figure/Figure.astro +17 -1
  216. package/src/components/figure/FloatCaption.astro +4 -17
  217. package/src/components/figure/figure-state.ts +26 -17
  218. package/src/components/float-toolbar.ts +274 -0
  219. package/src/components/memo/Memo.astro +21 -10
  220. package/src/components/memo/ShortNote.astro +4 -0
  221. package/src/components/notation/NotationGlossary.astro +22 -17
  222. package/src/components/notation/NotationLayer.astro +300 -79
  223. package/src/components/notation/NotationMath.astro +8 -1
  224. package/src/components/notation/render-notation-math.ts +51 -5
  225. package/src/components/readable-text.ts +25 -0
  226. package/src/components/reference/FloatRefLayer.astro +162 -89
  227. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  228. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  229. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  230. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  231. package/src/components/starlight/LessonExportActions.astro +384 -310
  232. package/src/components/starlight/LessonFooter.astro +66 -8
  233. package/src/components/starlight/PrintController.astro +28 -0
  234. package/src/components/starlight/TextSizeControl.astro +137 -0
  235. package/src/components/starlight/print-controller.ts +380 -0
  236. package/src/components/starlight/section-summary.ts +13 -5
  237. package/src/components/table/Table.astro +14 -22
  238. package/src/components/table/table-truncation.ts +15 -0
  239. package/src/content-config.ts +20 -312
  240. package/src/curriculum/validation.ts +72 -24
  241. package/src/reference/base-abbreviations.mjs +2 -2
  242. package/src/reference/base-operators.mjs +8 -10
  243. package/src/reference/base-roman-labels.mjs +4 -3
  244. package/src/reference/citation-format.mjs +74 -0
  245. package/src/reference/consistency.ts +47 -61
  246. package/src/reference/curriculum-alignment.ts +4 -41
  247. package/src/reference/derived-entries.ts +68 -0
  248. package/src/reference/derived-notation.mjs +121 -0
  249. package/src/reference/diagram-fit.ts +104 -0
  250. package/src/reference/diagram-layout.ts +182 -51
  251. package/src/reference/diagrams-validate.ts +2 -1
  252. package/src/reference/equations-validate.ts +41 -2
  253. package/src/reference/equations.mjs +120 -22
  254. package/src/reference/examples-validate.ts +1 -0
  255. package/src/reference/figure-fit.ts +139 -0
  256. package/src/reference/figure-pictures.ts +96 -0
  257. package/src/reference/figure-symbols.ts +117 -0
  258. package/src/reference/figures-validate.ts +2 -1
  259. package/src/reference/gate-abbreviations.ts +1 -1
  260. package/src/reference/gate-course-rules.ts +22 -7
  261. package/src/reference/gate-heading-length.ts +25 -7
  262. package/src/reference/gate-heading-structure.ts +40 -1
  263. package/src/reference/gate-indices.ts +470 -0
  264. package/src/reference/gate-math.ts +102 -13
  265. package/src/reference/gate-mdx-expression.ts +45 -0
  266. package/src/reference/gate-non-chapter-pages.ts +241 -0
  267. package/src/reference/gate-part-pages.ts +1 -1
  268. package/src/reference/gate-prop-text.ts +145 -0
  269. package/src/reference/gate-prose-notation.ts +19 -5
  270. package/src/reference/gate-retired-keys.ts +101 -0
  271. package/src/reference/gloss.ts +2 -0
  272. package/src/reference/katex-measure.ts +384 -0
  273. package/src/reference/keyed-float-validate.ts +22 -0
  274. package/src/reference/keyed-jsx-floats.mjs +11 -14
  275. package/src/reference/lab-katex-validate.ts +156 -0
  276. package/src/reference/lab-math-scope.ts +15 -1
  277. package/src/reference/lint-ignore.ts +4 -1
  278. package/src/reference/math-glyphs.mjs +851 -46
  279. package/src/reference/mixed-content.ts +11 -2
  280. package/src/reference/page-notation.ts +114 -14
  281. package/src/reference/raw-floats.mjs +37 -13
  282. package/src/reference/references.ts +14 -0
  283. package/src/reference/registry.ts +170 -9
  284. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  285. package/src/reference/rehype-heading-anchors.mjs +11 -1
  286. package/src/reference/remark-citation.mjs +0 -0
  287. package/src/reference/remark-notation.mjs +275 -23
  288. package/src/reference/render-lab-math.ts +40 -5
  289. package/src/reference/sections-validate.ts +67 -3
  290. package/src/reference/sections.mjs +92 -17
  291. package/src/reference/tables-validate.ts +40 -2
  292. package/src/reference/text-measure.ts +63 -0
  293. package/src/reference/text-widths.ts +133 -0
  294. package/src/reference/types.ts +75 -8
  295. package/src/rules/catalog.ts +241 -31
  296. package/src/rules/settings.ts +10 -0
  297. package/src/session/PreferenceStore.ts +53 -0
  298. package/src/styles/global.css +322 -83
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import { resolveMathGlyphs } from './math-glyphs.mjs';
22
22
  import type { ScopeEntry } from './gate-math';
23
+ import type { DerivedUse } from './types';
23
24
 
24
25
  export type ContentSegment =
25
26
  | { readonly type: 'text'; readonly value: string }
@@ -35,11 +36,17 @@ const MATH_SPAN =
35
36
 
36
37
  /**
37
38
  * Split `text` into ordered segments, resolving each math span's glyphs
38
- * against `scope`. Never throws.
39
+ * against `scope`. Never throws. With `derive` (an assessment, which is page
40
+ * math), an indexed symbol takes its derived card (rule N23), and each one
41
+ * used is added to `derived`.
39
42
  */
40
43
  export function splitMixedContent(
41
44
  text: string,
42
45
  scope: readonly ScopeEntry[],
46
+ options: {
47
+ readonly derive?: boolean;
48
+ readonly derived?: Map<string, DerivedUse>;
49
+ } = {},
43
50
  ): ContentSegment[] {
44
51
  const segments: ContentSegment[] = [];
45
52
  let cursor = 0;
@@ -53,7 +60,9 @@ export function splitMixedContent(
53
60
  const raw = match[1] ?? match[2] ?? '';
54
61
  let latex = raw;
55
62
  try {
56
- latex = resolveMathGlyphs(raw, [...scope]).latex;
63
+ const result = resolveMathGlyphs(raw, [...scope], { derive: options.derive === true });
64
+ latex = result.latex;
65
+ for (const use of result.derived) options.derived?.set(use.key, use);
57
66
  } catch {
58
67
  latex = raw;
59
68
  }
@@ -1,11 +1,16 @@
1
- import type { Manifest, ManifestLessonNotation } from '../compiler/manifest';
1
+ import type {
2
+ Manifest,
3
+ ManifestDerivedNotation,
4
+ ManifestLessonNotation,
5
+ } from '../compiler/manifest';
2
6
  import {
3
7
  formulaExplainKeys,
4
8
  formulaGlyphScope,
5
9
  type GlyphScopeEntry,
6
10
  } from './gloss';
7
11
  import { resolveReferenceId } from './references';
8
- import type { NotationDefinitionRecord } from './types';
12
+ import { derivedNotationEntries } from './derived-entries';
13
+ import type { DerivedUse, NotationDefinitionRecord } from './types';
9
14
 
10
15
  export interface PageNotationDependency {
11
16
  readonly key: string;
@@ -47,6 +52,55 @@ export function registerPageNotationDependencies(
47
52
  }
48
53
  }
49
54
 
55
+ const derivedByLocals = new WeakMap<object, Map<string, DerivedUse>>();
56
+
57
+ /**
58
+ * Record the derived cards (rule N23) a nested server component's math uses —
59
+ * a lab's or a diagram's — so the page ships them with its own, and their
60
+ * base and index entries, which a derived card's formula opens. Keyed by
61
+ * `Astro.locals`, like {@link registerPageNotationDependencies}.
62
+ */
63
+ export function registerPageDerivedNotation(
64
+ locals: object,
65
+ uses: Iterable<DerivedUse>,
66
+ context: string,
67
+ ): void {
68
+ let byKey = derivedByLocals.get(locals);
69
+ if (!byKey) {
70
+ byKey = new Map();
71
+ derivedByLocals.set(locals, byKey);
72
+ }
73
+ const entries: string[] = [];
74
+ for (const use of uses) {
75
+ byKey.set(use.key, use);
76
+ entries.push(use.base, use.index);
77
+ }
78
+ registerPageNotationDependencies(locals, entries, context);
79
+ }
80
+
81
+ /** The page a glyph table was built for, and whether rule N23 is on there. */
82
+ export interface ScopePage {
83
+ readonly locals: object;
84
+ readonly context: string;
85
+ readonly checkIndices: boolean;
86
+ }
87
+
88
+ const pageByScope = new WeakMap<object, ScopePage>();
89
+
90
+ /**
91
+ * Tie a lab's glyph table to the page it renders on (`labMathScope` does), so
92
+ * `renderLabMath` can give its indexed symbols derived cards and register
93
+ * them, and knows whether to hold them to rule N23. A table a course copies
94
+ * loses the tie: its math then composes no derived card.
95
+ */
96
+ export function bindScopeToPage(scope: object, page: ScopePage): void {
97
+ pageByScope.set(scope, page);
98
+ }
99
+
100
+ export function pageOfScope(scope: object): ScopePage | undefined {
101
+ return pageByScope.get(scope);
102
+ }
103
+
50
104
  export function pageNotationDependencies(
51
105
  locals: object,
52
106
  ): PageNotationDependency[] {
@@ -103,7 +157,6 @@ function manifestDefinition(
103
157
  notation: definition.notation,
104
158
  label: definition.label,
105
159
  summary: definition.summary,
106
- editorialStatus: definition.status,
107
160
  seeAlso: [...definition.seeAlso],
108
161
  glosses: [...definition.glosses],
109
162
  formulaScope: formulaGlyphScope({
@@ -119,26 +172,73 @@ function manifestDefinition(
119
172
  ...(definition.formula === undefined
120
173
  ? {}
121
174
  : { formula: definition.formula }),
175
+ ...(definition.range === undefined ? {} : { range: definition.range }),
122
176
  };
123
177
  }
124
178
 
125
179
  /**
126
- * Extend one compiled lesson bundle with only the definitions requested by
127
- * components rendered on that page (plus those definitions' normal closure).
180
+ * The derived cards (rule N23) a page uses: the ones its body and assessment
181
+ * math use, as the manifest composed them, and the ones a lab or a diagram on
182
+ * it registered while rendering (`locals`), composed here the same way. They
183
+ * travel with the page as hidden sources; their base and index entries are
184
+ * in its payload already.
185
+ */
186
+ export function derivedNotationForPage(
187
+ manifest: Manifest,
188
+ pageId: string,
189
+ locals?: object,
190
+ ): readonly ManifestDerivedNotation[] {
191
+ const page =
192
+ manifest.lessons.find((entry) => entry.id === pageId) ??
193
+ manifest.pages.find((entry) => entry.id === pageId);
194
+ const compiled = page?.notation.derived ?? [];
195
+ const registered = [...((locals && derivedByLocals.get(locals)?.values()) ?? [])].filter(
196
+ (use) => !compiled.some((entry) => entry.key === use.key),
197
+ );
198
+ if (registered.length === 0) return compiled;
199
+
200
+ const definitionsById = new Map(
201
+ manifest.notation.definitions.map((definition) => [definition.id, definition]),
202
+ );
203
+ const availableIds = new Set(definitionsById.keys());
204
+ const records = new Map<string, NotationDefinitionRecord>();
205
+ for (const use of registered) {
206
+ for (const key of [use.base, use.index]) {
207
+ const id = resolveReferenceId(key, { kind: 'page', lessonId: pageId }, availableIds);
208
+ const record = id === undefined ? undefined : definitionsById.get(id);
209
+ if (record) records.set(key, record);
210
+ }
211
+ }
212
+ const cardsByKey = new Map<string, GlyphScopeEntry>(
213
+ manifest.notation.definitions
214
+ .filter((definition) => definition.kind === 'shared')
215
+ .map((definition) => [definition.key, { key: definition.key, notation: definition.notation }]),
216
+ );
217
+ return [...compiled, ...derivedNotationEntries(registered, records, cardsByKey)].sort(
218
+ (left, right) => left.key.localeCompare(right.key),
219
+ );
220
+ }
221
+
222
+ /**
223
+ * Extend one compiled page bundle — a lesson's, or a non-chapter page's
224
+ * (rule S10) — with only the definitions requested by components rendered on
225
+ * that page (plus those definitions' normal closure).
128
226
  */
129
227
  export function notationDefinitionsForRenderedPage(
130
228
  manifest: Manifest,
131
- lessonId: string,
229
+ pageId: string,
132
230
  dependencies: readonly PageNotationDependency[],
133
231
  ): ManifestLessonNotation[] {
134
- const lesson = manifest.lessons.find((entry) => entry.id === lessonId);
135
- if (!lesson) return [];
232
+ const page =
233
+ manifest.lessons.find((entry) => entry.id === pageId) ??
234
+ manifest.pages.find((entry) => entry.id === pageId);
235
+ if (!page) return [];
136
236
 
137
237
  const definitionsById = new Map(
138
238
  manifest.notation.definitions.map((definition) => [definition.id, definition]),
139
239
  );
140
240
  const availableIds = new Set(definitionsById.keys());
141
- const ids = new Set(lesson.notation.definitionIds);
241
+ const ids = new Set(page.notation.definitionIds);
142
242
  const contextsByKey = new Map<string, Set<string>>();
143
243
 
144
244
  for (const dependency of dependencies) {
@@ -151,14 +251,14 @@ export function notationDefinitionsForRenderedPage(
151
251
 
152
252
  const definitionId = resolveReferenceId(
153
253
  dependency.key,
154
- { kind: 'page', lessonId },
254
+ { kind: 'page', lessonId: pageId },
155
255
  availableIds,
156
256
  );
157
257
  if (!definitionId) {
158
258
  throw new Error(
159
- `Notation payload error in lesson "${lessonId}": ${dependency.context} ` +
259
+ `Notation payload error on page "${pageId}": ${dependency.context} ` +
160
260
  `emitted data-notation-key="${dependency.key}", but that key is not ` +
161
- `defined in this lesson or the shared notation registry.`,
261
+ `defined on this page or in the shared notation registry.`,
162
262
  );
163
263
  }
164
264
  ids.add(definitionId);
@@ -202,7 +302,7 @@ export function notationDefinitionsForRenderedPage(
202
302
  if (previous && previous !== id) {
203
303
  const context = [...(contextsByKey.get(definition.key) ?? [])].join(', ');
204
304
  throw new Error(
205
- `Notation payload error in lesson "${lessonId}": key "${definition.key}" ` +
305
+ `Notation payload error on page "${pageId}": key "${definition.key}" ` +
206
306
  `would have two meanings (${previous} and ${id})` +
207
307
  (context ? ` after ${context} emitted it.` : '.'),
208
308
  );
@@ -220,7 +320,7 @@ export function notationDefinitionsForRenderedPage(
220
320
  }
221
321
 
222
322
  const existingById = new Map(
223
- lesson.notation.definitions.map((definition) => [definition.id, definition]),
323
+ page.notation.definitions.map((definition) => [definition.id, definition]),
224
324
  );
225
325
  return [...ids]
226
326
  .sort()
@@ -12,11 +12,13 @@
12
12
  * `keyed-jsx-floats.mjs`'s top-level-only scan entirely, so it never even
13
13
  * reaches diagram numbering.
14
14
  *
15
- * A raw table nested in `<Table>`/`<Figure>` is the intended, wrapped case
16
- * and is not flagged; nesting inside anything else (`<CompactExample>`,
17
- * `<Aside>`, `<Memo>`, …) still is, because a raw table has no numbering
18
- * scheme of its own to opt into the way a `<Diagram>`'s top-level/`<Memo>`-
19
- * transparent scan does.
15
+ * A raw table counts only where wrapping it would number it: at the top level
16
+ * of the lesson body or in a `TRANSPARENT_COMPONENTS` body (`<Memo>`,
17
+ * `<ShortNote>`, `<Fold>`), exactly where `keyed-jsx-floats.mjs` numbers a
18
+ * `<Table>`. A raw table in `<Table>`/`<Figure>` is the intended, wrapped
19
+ * case; one in any other component (a worked example's `<CompactExample>`,
20
+ * an `<Aside>`) is illustrative, like an equation there, and a `<Table>`
21
+ * there would get no number — so neither is flagged.
20
22
  *
21
23
  * `scanRawFloats` (raw Markdown, for the manifest builder) and
22
24
  * `collectRawFloats` (mdast, for `remark-notation.mjs`) MUST agree on counts
@@ -24,7 +26,11 @@
24
26
  * `keyed-jsx-floats.mjs` holds `scanKeyedJsxFloats`/`collectKeyedJsxFloats` to.
25
27
  */
26
28
 
27
- import { stripFencedCode } from './equations.mjs';
29
+ import {
30
+ stripFencedCode,
31
+ stripJsxComponents,
32
+ TRANSPARENT_COMPONENTS,
33
+ } from './equations.mjs';
28
34
  import { collectKeyedJsxFloats, scanKeyedJsxFloats } from './keyed-jsx-floats.mjs';
29
35
 
30
36
  /** Blank `<Table>…</Table>` and `<Figure>…</Figure>` bodies — legitimately wrapped, not raw. */
@@ -67,9 +73,16 @@ function findDiagramTags(text) {
67
73
  export function scanRawFloats(markdown) {
68
74
  const codeStripped = stripFencedCode(String(markdown));
69
75
  const withoutWrappedFloats = blankFloatWrappers(codeStripped);
76
+ // Every component body but a transparent one is blanked — `<Table>` and
77
+ // `<Figure>` among them — so only the tables a `<Table>` would number remain.
78
+ const numberableText = stripJsxComponents(
79
+ codeStripped,
80
+ undefined,
81
+ TRANSPARENT_COMPONENTS,
82
+ );
70
83
 
71
- const markdownTableCount = findMarkdownTableStarts(withoutWrappedFloats).length;
72
- const htmlTableCount = findHtmlTableTags(withoutWrappedFloats).length;
84
+ const markdownTableCount = findMarkdownTableStarts(numberableText).length;
85
+ const htmlTableCount = findHtmlTableTags(numberableText).length;
73
86
 
74
87
  const diagramsOutsideWrappers = findDiagramTags(withoutWrappedFloats).length;
75
88
  const visibleDiagrams = scanKeyedJsxFloats(markdown, { tagName: 'Diagram' });
@@ -95,7 +108,10 @@ export function collectRawFloats(tree) {
95
108
  const htmlTableNodes = [];
96
109
  const allDiagramNodes = [];
97
110
 
98
- const walk = (node) => {
111
+ // `numberable`: a `<Table>` here would be numbered — the top level, or a
112
+ // transparent component's body. Any other MDX component (a capitalized
113
+ // name, the ones `stripJsxComponents` blanks) ends it for its subtree.
114
+ const walk = (node, numberable) => {
99
115
  if (!node || typeof node !== 'object') return;
100
116
  if (
101
117
  node.type === 'mdxJsxFlowElement' &&
@@ -103,18 +119,26 @@ export function collectRawFloats(tree) {
103
119
  ) {
104
120
  return; // legitimately wrapped — don't descend
105
121
  }
106
- if (node.type === 'table') markdownTableNodes.push(node);
107
- if (node.type === 'html' && /<table\b/i.test(String(node.value ?? ''))) {
122
+ if (numberable && node.type === 'table') markdownTableNodes.push(node);
123
+ if (
124
+ numberable &&
125
+ node.type === 'html' &&
126
+ /<table\b/i.test(String(node.value ?? ''))
127
+ ) {
108
128
  htmlTableNodes.push(node);
109
129
  }
110
130
  if (node.type === 'mdxJsxFlowElement' && node.name === 'Diagram') {
111
131
  allDiagramNodes.push(node);
112
132
  }
133
+ const component =
134
+ (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') &&
135
+ /^[A-Z]/.test(String(node.name ?? '')) &&
136
+ !TRANSPARENT_COMPONENTS.includes(node.name);
113
137
  if (Array.isArray(node.children)) {
114
- for (const child of node.children) walk(child);
138
+ for (const child of node.children) walk(child, numberable && !component);
115
139
  }
116
140
  };
117
- walk(tree);
141
+ walk(tree, true);
118
142
 
119
143
  const visibleDiagrams = collectKeyedJsxFloats(tree, { tagName: 'Diagram' });
120
144
  const visibleNodes = new Set(visibleDiagrams.map((diagram) => diagram.node));
@@ -10,6 +10,20 @@ export function isNotationKey(value: string): boolean {
10
10
  return NOTATION_KEY_PATTERN.test(value);
11
11
  }
12
12
 
13
+ /**
14
+ * The `[[…]]` prefixes that name something other than a notation entry: an
15
+ * equation, a table, a figure, a diagram, a worked-example set, a knowledge
16
+ * check, a section, a memo. `remark-notation.mjs` and `remark-memo.mjs` route
17
+ * a reference by its prefix before notation keys are looked at, so a key
18
+ * that starts with one could never be reached.
19
+ */
20
+ export const RESERVED_KEY_PREFIXES = ['eq-', 'tab-', 'fig-', 'dia-', 'ex-', 'check-', 'sec-', 'memo-'] as const;
21
+
22
+ /** The reserved prefix `key` starts with, if any. */
23
+ export function reservedKeyPrefix(key: string): string | undefined {
24
+ return RESERVED_KEY_PREFIXES.find((prefix) => key.startsWith(prefix));
25
+ }
26
+
13
27
  export function sharedDefinitionId(key: string): string {
14
28
  return `shared:${key}`;
15
29
  }
@@ -3,9 +3,12 @@ import {
3
3
  compareSourceSpans,
4
4
  isNotationKey,
5
5
  localDefinitionId,
6
+ RESERVED_KEY_PREFIXES,
7
+ reservedKeyPrefix,
6
8
  resolveReferenceId,
7
9
  sharedDefinitionId,
8
10
  } from './references';
11
+ import { lineOf, proseOnly } from './gate-prose-notation';
9
12
  import type {
10
13
  LocalNotationDefinitionInput,
11
14
  LocalNotationDefinitionRecord,
@@ -165,7 +168,35 @@ export function buildNotationRegistry(
165
168
  ),
166
169
  );
167
170
  }
168
- detectCycles(definitions, edgeIndex, diagnostics);
171
+ // A formula's references bring their cards onto the page (above) but are
172
+ // no dependency: formulas may name each other, as `confidence-level`
173
+ // (γ = 1 − α) and `significance-level` (α = 1 − γ) do. A cycle through
174
+ // meanings and bodies is still an error.
175
+ const dependencyEdges = new Map<string, readonly string[]>();
176
+ for (const candidate of definitions.values()) {
177
+ dependencyEdges.set(
178
+ candidate.id,
179
+ uniqueSorted(
180
+ candidate.resolvedReferences
181
+ .filter((reference) => reference.kind !== 'formula')
182
+ .map((reference) => reference.definitionId)
183
+ .filter((definitionId) => definitionId !== candidate.id),
184
+ ),
185
+ );
186
+ }
187
+ detectCycles(definitions, dependencyEdges, diagnostics);
188
+
189
+ // A page's own `notation.local` entries are in its scope by construction:
190
+ // its math binds to one by shape — an operator entry `{}'` for `w'`, the
191
+ // fix rule N22 itself suggests — even where no `[[key]]` or `\explain`
192
+ // names it. Each seeds its page's bundle, so the page renders its card.
193
+ const localIdsByLesson = new Map<string, string[]>();
194
+ for (const candidate of definitions.values()) {
195
+ if (candidate.kind !== 'local') continue;
196
+ const ids = localIdsByLesson.get(candidate.lesson.lessonId) ?? [];
197
+ ids.push(candidate.id);
198
+ localIdsByLesson.set(candidate.lesson.lessonId, ids);
199
+ }
169
200
 
170
201
  const bundles: NotationPageBundle[] = [];
171
202
  const backlinks: NotationBacklink[] = [];
@@ -176,7 +207,10 @@ export function buildNotationRegistry(
176
207
  const directIds = new Set(
177
208
  references.map((reference) => reference.definitionId),
178
209
  );
179
- const definitionIds = transitiveClosure(directIds, edgeIndex);
210
+ const definitionIds = transitiveClosure(
211
+ new Set([...directIds, ...(localIdsByLesson.get(lesson.lessonId) ?? [])]),
212
+ edgeIndex,
213
+ );
180
214
  for (const id of definitionIds) reachableDefinitions.add(id);
181
215
 
182
216
  const bindingByKey = new Map<string, string>();
@@ -186,7 +220,6 @@ export function buildNotationRegistry(
186
220
 
187
221
  bundles.push({
188
222
  lessonId: lesson.lessonId,
189
- status: lesson.status,
190
223
  source: lesson.source,
191
224
  bindings: [...bindingByKey]
192
225
  .map(([key, definitionId]) => ({ key, definitionId }))
@@ -210,6 +243,28 @@ export function buildNotationRegistry(
210
243
  }
211
244
  }
212
245
 
246
+ // A page-local entry under a shared entry's key wins on its page, so the
247
+ // one key means two things in the course: the glossary's, and the page's.
248
+ for (const candidate of definitions.values()) {
249
+ if (candidate.kind !== 'local') continue;
250
+ const shared = definitions.get(sharedDefinitionId(candidate.input.key));
251
+ if (shared === undefined) continue;
252
+ diagnostics.push({
253
+ code: 'local-shadows-shared',
254
+ severity: 'warning',
255
+ message:
256
+ `${candidate.lesson.lessonId} defines notation.local ${JSON.stringify(candidate.input.key)}, ` +
257
+ `which is also a shared entry (${shared.input.source.file}): on this page ` +
258
+ `[[${candidate.input.key}]] means "${candidate.input.label}", everywhere else ` +
259
+ `"${shared.input.label}". Give the local entry a key of its own, or use the shared one.`,
260
+ key: candidate.input.key,
261
+ lessonId: candidate.lesson.lessonId,
262
+ definitionId: candidate.id,
263
+ source: candidate.input.source,
264
+ relatedSources: [shared.input.source],
265
+ });
266
+ }
267
+
213
268
  for (const candidate of definitions.values()) {
214
269
  if (!reachableDefinitions.has(candidate.id)) {
215
270
  diagnostics.push({
@@ -280,6 +335,7 @@ function buildLessonIndex(
280
335
  );
281
336
  continue;
282
337
  }
338
+ diagnostics.push(...malformedReferences(lesson));
283
339
  const previous = lessons.get(lesson.lessonId);
284
340
  if (previous !== undefined) {
285
341
  diagnostics.push(
@@ -336,7 +392,26 @@ function buildDefinitionIndex(
336
392
  diagnostics.push(
337
393
  diagnostic({
338
394
  code: 'invalid-key',
339
- message: `${candidate.id} has invalid notation key ${JSON.stringify(candidate.input.key)}`,
395
+ message: `${candidate.id} has invalid notation key ${JSON.stringify(candidate.input.key)}${keyHint(candidate.input.key)}`,
396
+ key: candidate.input.key,
397
+ lessonId:
398
+ candidate.kind === 'local' ? candidate.lesson.lessonId : undefined,
399
+ definitionId: candidate.id,
400
+ source: candidate.input.source,
401
+ }),
402
+ );
403
+ continue;
404
+ }
405
+ const reserved = reservedKeyPrefix(candidate.input.key);
406
+ if (reserved !== undefined) {
407
+ diagnostics.push(
408
+ diagnostic({
409
+ code: 'invalid-key',
410
+ message:
411
+ `${candidate.id} has notation key ${JSON.stringify(candidate.input.key)}, which starts with ` +
412
+ `the reserved \`${reserved}\`: [[${candidate.input.key}]] is read as a reference to that kind ` +
413
+ `of float and never reaches this entry. Rename the key; none may start with ` +
414
+ `${RESERVED_KEY_PREFIXES.map((prefix) => `\`${prefix}\``).join(', ')}.`,
340
415
  key: candidate.input.key,
341
416
  lessonId:
342
417
  candidate.kind === 'local' ? candidate.lesson.lessonId : undefined,
@@ -459,7 +534,7 @@ function validateReferenceKey(
459
534
  diagnostics.push(
460
535
  diagnostic({
461
536
  code: 'invalid-key',
462
- message: `${owner.id} references invalid notation key ${JSON.stringify(reference.key)}`,
537
+ message: `${owner.id} references invalid notation key ${JSON.stringify(reference.key)}${keyHint(reference.key)}`,
463
538
  key: reference.key,
464
539
  lessonId: owner.kind === 'local' ? owner.lesson.lessonId : undefined,
465
540
  definitionId: owner.id,
@@ -478,7 +553,7 @@ function validatePageReferenceKey(
478
553
  diagnostics.push(
479
554
  diagnostic({
480
555
  code: 'invalid-key',
481
- message: `${lesson.lessonId} references invalid notation key ${JSON.stringify(reference.key)}`,
556
+ message: `${lesson.lessonId} references invalid notation key ${JSON.stringify(reference.key)}${keyHint(reference.key)}`,
482
557
  key: reference.key,
483
558
  lessonId: lesson.lessonId,
484
559
  source: reference.source,
@@ -539,7 +614,6 @@ function toDefinitionRecord(
539
614
  sources: input.sources,
540
615
  seeAlso: input.seeAlso,
541
616
  alignment: input.alignment,
542
- status: input.status,
543
617
  aiAssisted: input.aiAssisted,
544
618
  body: input.body,
545
619
  glosses: input.glosses,
@@ -547,6 +621,7 @@ function toDefinitionRecord(
547
621
  resolvedReferences: candidate.resolvedReferences,
548
622
  ...(input.formula === undefined ? {} : { formula: input.formula }),
549
623
  ...(input.units === undefined ? {} : { units: input.units }),
624
+ ...indexRecordFields(input),
550
625
  };
551
626
  return record;
552
627
  }
@@ -564,16 +639,33 @@ function toDefinitionRecord(
564
639
  sources: input.sources,
565
640
  seeAlso: input.seeAlso,
566
641
  alignment: input.alignment,
567
- status: candidate.lesson.status,
568
642
  glosses: input.glosses,
569
643
  source: input.source,
570
644
  resolvedReferences: candidate.resolvedReferences,
571
645
  ...(input.formula === undefined ? {} : { formula: input.formula }),
572
646
  ...(input.units === undefined ? {} : { units: input.units }),
647
+ ...indexRecordFields(input),
573
648
  };
574
649
  return record;
575
650
  }
576
651
 
652
+ /** Rule N23's optional fields, carried from the input to the record as is. */
653
+ function indexRecordFields(input: {
654
+ readonly reads?: string;
655
+ readonly range?: string;
656
+ readonly fixedIndex?: true;
657
+ readonly formulaTemplate?: string;
658
+ }): { reads?: string; range?: string; fixedIndex?: true; formulaTemplate?: string } {
659
+ return {
660
+ ...(input.reads === undefined ? {} : { reads: input.reads }),
661
+ ...(input.range === undefined ? {} : { range: input.range }),
662
+ ...(input.fixedIndex === undefined ? {} : { fixedIndex: input.fixedIndex }),
663
+ ...(input.formulaTemplate === undefined
664
+ ? {}
665
+ : { formulaTemplate: input.formulaTemplate }),
666
+ };
667
+ }
668
+
577
669
  function definitionFingerprint(candidate: DefinitionCandidate): string {
578
670
  const references = sortedReferences(candidate.input.references).map(
579
671
  (reference) => ({
@@ -608,7 +700,6 @@ function definitionFingerprint(candidate: DefinitionCandidate): string {
608
700
  sources: [...input.sources].sort(),
609
701
  seeAlso: [...input.seeAlso].sort(),
610
702
  alignment,
611
- status: input.status,
612
703
  aiAssisted: input.aiAssisted,
613
704
  body: normalizeText(input.body),
614
705
  glosses: input.glosses,
@@ -759,6 +850,76 @@ function sortedSources(values: readonly SourceSpan[]): SourceSpan[] {
759
850
  return [...values].sort(compareSourceSpans);
760
851
  }
761
852
 
853
+ /**
854
+ * A `[[` that never closes. remark-notation fails the page on one ("Malformed
855
+ * notation reference near …"), but nothing before the render said so. Each
856
+ * `[[` in the prose (code, math and JSX blanked) must open a reference that
857
+ * closes on its line, with no `[` or `]` inside.
858
+ */
859
+ function malformedReferences(lesson: NotationLessonInput): NotationDiagnostic[] {
860
+ if (!lesson.body?.includes('[[')) return [];
861
+ const prose = proseOnly(lesson.body, { keepReferences: true });
862
+ const found: NotationDiagnostic[] = [];
863
+ for (let at = prose.indexOf('[['); at !== -1; at = prose.indexOf('[[', at + 2)) {
864
+ if (lesson.body[at - 1] === '\\') continue;
865
+ const closed = /^\[\[[^[\]\n]*\]\]/.exec(prose.slice(at));
866
+ if (closed) {
867
+ at += closed[0].length - 2;
868
+ continue;
869
+ }
870
+ const near = lesson.body.slice(at, at + 40).split('\n')[0];
871
+ const line = lineOf(lesson.body, at) + (lesson.bodyLine ?? 1) - 1;
872
+ found.push(
873
+ diagnostic({
874
+ code: 'malformed-reference',
875
+ message:
876
+ `${lesson.lessonId} opens a reference that never closes, near ${JSON.stringify(near)}: ` +
877
+ 'close it with ]], as [[semantic.key]], on the same line.',
878
+ lessonId: lesson.lessonId,
879
+ source: { ...lesson.source, line },
880
+ }),
881
+ );
882
+ }
883
+ return found;
884
+ }
885
+
886
+ /** What each reserved `[[…]]` prefix names. */
887
+ const FLOAT_KINDS: Readonly<Record<string, string>> = {
888
+ 'eq-': 'an equation',
889
+ 'tab-': 'a table',
890
+ 'fig-': 'a figure',
891
+ 'dia-': 'a diagram',
892
+ 'ex-': 'a worked-example set',
893
+ 'check-': 'a knowledge check',
894
+ 'sec-': 'a section',
895
+ 'memo-': 'a memo',
896
+ };
897
+
898
+ /**
899
+ * What a malformed key most likely meant, after its message: a wiki-style
900
+ * `[[key|text]]` link text, or anything else outside the key grammar.
901
+ */
902
+ function keyHint(key: string): string {
903
+ // `[[dia-My_Diagram]]`: a float reference whose id breaks the grammar falls
904
+ // through to the notation keys, which it never was.
905
+ const reserved = reservedKeyPrefix(key);
906
+ if (reserved !== undefined) {
907
+ return (
908
+ `: [[${reserved}…]] names ${FLOAT_KINDS[reserved]}, whose id is lower-case words and digits ` +
909
+ `joined by "-"; ${JSON.stringify(key.slice(reserved.length))} is not one.`
910
+ );
911
+ }
912
+ const bar = key.indexOf('|');
913
+ if (bar > 0) {
914
+ const target = key.slice(0, bar).trim();
915
+ return (
916
+ `: a [[…]] reference takes no link text of its own — it reads as the entry's label. ` +
917
+ `Write [[${target}]], with the words around it.`
918
+ );
919
+ }
920
+ return ': a key is lower-case words and digits joined by "-" or "." (motion.launch-speed).';
921
+ }
922
+
762
923
  function diagnostic(
763
924
  input: Omit<NotationDiagnostic, 'severity'>,
764
925
  ): NotationDiagnostic {
@@ -31,13 +31,32 @@ function katexErrors(node, output = []) {
31
31
  return output;
32
32
  }
33
33
 
34
+ /**
35
+ * KaTeX writes each formula twice: as MathML, whose `<annotation>` holds the
36
+ * LaTeX source, for assistive technology, and as the visible glyphs. Mark the
37
+ * MathML copy `data-pagefind-ignore`, so the site search indexes a formula
38
+ * once, as it reads, and never its LaTeX (`\explain{…}{…}`, `\dfrac`).
39
+ */
40
+ export function markMathmlForSearch(node) {
41
+ if (!node || typeof node !== 'object') return;
42
+ const classes = node.properties?.className;
43
+ if (node.type === 'element' && Array.isArray(classes) && classes.includes('katex-mathml')) {
44
+ node.properties.dataPagefindIgnore = '';
45
+ return;
46
+ }
47
+ if (Array.isArray(node.children)) node.children.forEach(markMathmlForSearch);
48
+ }
49
+
34
50
  /**
35
51
  * rehype-katex reports parse failures as non-fatal VFile messages and renders
36
52
  * a `.katex-error` fallback. Turn those recovered failures into compiler
37
53
  * errors so Astro cannot successfully build a lesson containing broken math.
54
+ * The pass every course runs right after KaTeX, it also keeps the LaTeX
55
+ * annotations out of the site search (`markMathmlForSearch`).
38
56
  */
39
57
  export default function rehypeFailKatexErrors() {
40
58
  return function failOnKatexErrors(tree, file) {
59
+ markMathmlForSearch(tree);
41
60
  const errors = katexErrors(tree);
42
61
  if (errors.length === 0) return;
43
62