explico 0.12.0 → 0.13.1

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 (279) hide show
  1. package/CHANGELOG.md +1137 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  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 +14 -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 +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  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 +2 -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 +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  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/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +707 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +321 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
@@ -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,9 +172,53 @@ 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
 
179
+ /**
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
+
125
222
  /**
126
223
  * Extend one compiled page bundle — a lesson's, or a non-chapter page's
127
224
  * (rule S10) — with only the definitions requested by components rendered on
@@ -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,23 @@ 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);
169
188
 
170
189
  // A page's own `notation.local` entries are in its scope by construction:
171
190
  // its math binds to one by shape — an operator entry `{}'` for `w'`, the
@@ -201,7 +220,6 @@ export function buildNotationRegistry(
201
220
 
202
221
  bundles.push({
203
222
  lessonId: lesson.lessonId,
204
- status: lesson.status,
205
223
  source: lesson.source,
206
224
  bindings: [...bindingByKey]
207
225
  .map(([key, definitionId]) => ({ key, definitionId }))
@@ -225,6 +243,28 @@ export function buildNotationRegistry(
225
243
  }
226
244
  }
227
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
+
228
268
  for (const candidate of definitions.values()) {
229
269
  if (!reachableDefinitions.has(candidate.id)) {
230
270
  diagnostics.push({
@@ -295,6 +335,7 @@ function buildLessonIndex(
295
335
  );
296
336
  continue;
297
337
  }
338
+ diagnostics.push(...malformedReferences(lesson));
298
339
  const previous = lessons.get(lesson.lessonId);
299
340
  if (previous !== undefined) {
300
341
  diagnostics.push(
@@ -351,7 +392,26 @@ function buildDefinitionIndex(
351
392
  diagnostics.push(
352
393
  diagnostic({
353
394
  code: 'invalid-key',
354
- 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(', ')}.`,
355
415
  key: candidate.input.key,
356
416
  lessonId:
357
417
  candidate.kind === 'local' ? candidate.lesson.lessonId : undefined,
@@ -474,7 +534,7 @@ function validateReferenceKey(
474
534
  diagnostics.push(
475
535
  diagnostic({
476
536
  code: 'invalid-key',
477
- 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)}`,
478
538
  key: reference.key,
479
539
  lessonId: owner.kind === 'local' ? owner.lesson.lessonId : undefined,
480
540
  definitionId: owner.id,
@@ -493,7 +553,7 @@ function validatePageReferenceKey(
493
553
  diagnostics.push(
494
554
  diagnostic({
495
555
  code: 'invalid-key',
496
- 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)}`,
497
557
  key: reference.key,
498
558
  lessonId: lesson.lessonId,
499
559
  source: reference.source,
@@ -554,7 +614,6 @@ function toDefinitionRecord(
554
614
  sources: input.sources,
555
615
  seeAlso: input.seeAlso,
556
616
  alignment: input.alignment,
557
- status: input.status,
558
617
  aiAssisted: input.aiAssisted,
559
618
  body: input.body,
560
619
  glosses: input.glosses,
@@ -562,6 +621,7 @@ function toDefinitionRecord(
562
621
  resolvedReferences: candidate.resolvedReferences,
563
622
  ...(input.formula === undefined ? {} : { formula: input.formula }),
564
623
  ...(input.units === undefined ? {} : { units: input.units }),
624
+ ...indexRecordFields(input),
565
625
  };
566
626
  return record;
567
627
  }
@@ -579,16 +639,33 @@ function toDefinitionRecord(
579
639
  sources: input.sources,
580
640
  seeAlso: input.seeAlso,
581
641
  alignment: input.alignment,
582
- status: candidate.lesson.status,
583
642
  glosses: input.glosses,
584
643
  source: input.source,
585
644
  resolvedReferences: candidate.resolvedReferences,
586
645
  ...(input.formula === undefined ? {} : { formula: input.formula }),
587
646
  ...(input.units === undefined ? {} : { units: input.units }),
647
+ ...indexRecordFields(input),
588
648
  };
589
649
  return record;
590
650
  }
591
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
+
592
669
  function definitionFingerprint(candidate: DefinitionCandidate): string {
593
670
  const references = sortedReferences(candidate.input.references).map(
594
671
  (reference) => ({
@@ -623,7 +700,6 @@ function definitionFingerprint(candidate: DefinitionCandidate): string {
623
700
  sources: [...input.sources].sort(),
624
701
  seeAlso: [...input.seeAlso].sort(),
625
702
  alignment,
626
- status: input.status,
627
703
  aiAssisted: input.aiAssisted,
628
704
  body: normalizeText(input.body),
629
705
  glosses: input.glosses,
@@ -774,6 +850,76 @@ function sortedSources(values: readonly SourceSpan[]): SourceSpan[] {
774
850
  return [...values].sort(compareSourceSpans);
775
851
  }
776
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
+
777
923
  function diagnostic(
778
924
  input: Omit<NotationDiagnostic, 'severity'>,
779
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
 
@@ -69,6 +69,11 @@ const HEADING_TAG = /^h([1-6])$/;
69
69
  const LINK_ICON_PATH =
70
70
  'm12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42';
71
71
 
72
+ /**
73
+ * A heading's text as a reader hears it. KaTeX writes each formula twice —
74
+ * MathML, whose `<annotation>` holds the LaTeX, and the `aria-hidden` glyphs —
75
+ * so a formula is read once, from its MathML, without the LaTeX.
76
+ */
72
77
  function headingText(node) {
73
78
  let text = '';
74
79
  const walk = (current) => {
@@ -77,10 +82,15 @@ function headingText(node) {
77
82
  text += String(current.value ?? '');
78
83
  return;
79
84
  }
85
+ if (current.type === 'element') {
86
+ const classes = current.properties?.className;
87
+ if (current.tagName === 'annotation') return;
88
+ if (Array.isArray(classes) && classes.includes('katex-html')) return;
89
+ }
80
90
  if (Array.isArray(current.children)) current.children.forEach(walk);
81
91
  };
82
92
  walk(node);
83
- return text.replace(/\s+/g, ' ').trim();
93
+ return text.replace(/[\u200b\u2061-\u2064]/g, '').replace(/\s+/g, ' ').trim();
84
94
  }
85
95
 
86
96
  // A heading's marker, e.g. `## Some title [collapsed]`, stripped from the
Binary file