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
@@ -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
@@ -35,9 +35,12 @@ export const NOTATION_KEY_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
35
35
 
36
36
  import {
37
37
  GlyphResolutionError,
38
+ baseSymbolLatex,
39
+ derivedNotationLatex,
38
40
  resolveMathGlyphs,
39
41
  stripNotationMarkers,
40
42
  } from './math-glyphs.mjs';
43
+ import { composeDerivedEntry } from './derived-notation.mjs';
41
44
  import {
42
45
  EQ_CAPTION_PATTERN,
43
46
  EQ_LABEL_PATTERN,
@@ -46,6 +49,7 @@ import {
46
49
  equationAnchorId,
47
50
  parseEquationRef,
48
51
  stripEquationLabels,
52
+ TRANSPARENT_COMPONENTS,
49
53
  } from './equations.mjs';
50
54
  import {
51
55
  collectKeyedJsxFloats,
@@ -184,6 +188,25 @@ function lessonIdFrom(file) {
184
188
  return typeof lessonId === 'string' ? lessonId : undefined;
185
189
  }
186
190
 
191
+ /** True for any page under `content/docs/`; a lesson is one of them. */
192
+ function isDocsPageFile(file) {
193
+ return [file?.path, file?.history?.[0], file?.data?.file].some(
194
+ (value) => typeof value === 'string' && LESSON_DOC_PATH.test(value),
195
+ );
196
+ }
197
+
198
+ /**
199
+ * The blocks a chapter numbers (`makeFloatCtx` below, and the knowledge
200
+ * check). Rule S10 keeps each off a page that is not a chapter.
201
+ */
202
+ const NUMBERED_BLOCK_NAMES = new Set([
203
+ 'Table',
204
+ 'Figure',
205
+ 'Diagram',
206
+ 'CompactExamples',
207
+ 'AssessmentSet',
208
+ ]);
209
+
187
210
  const ANY_TERM_KEY = /\[\[\s*([^\][]+?)\s*\]\]/g;
188
211
  const ANY_EXPLAIN_KEY = /\\explain\s*\{([^{}]+)\}/g;
189
212
 
@@ -214,6 +237,26 @@ function collectPageSharedKeys(tree) {
214
237
  return keys;
215
238
  }
216
239
 
240
+ /**
241
+ * A derived card (rule N23) as the remark pass records it: the frontmatter of
242
+ * its base and index entries composed (`derived-notation.mjs`). Only an
243
+ * authored `label` is known here, so the title is composed when both entries
244
+ * author one, which is what a table header's label needs.
245
+ */
246
+ function derivedResolution(key, base, index) {
247
+ const entry = composeDerivedEntry({
248
+ key,
249
+ notation: derivedNotationLatex(base.notation, index.notation),
250
+ baseLatex: baseSymbolLatex(base.notation),
251
+ base: { ...base, label: base.label ?? '' },
252
+ index: { ...index, label: index.label ?? '', summary: index.meaning },
253
+ });
254
+ const labelled =
255
+ typeof base.label === 'string' && typeof index.label === 'string';
256
+ const { label, ...rest } = entry;
257
+ return { ...rest, ...(labelled ? { label } : {}), scope: 'derived' };
258
+ }
259
+
217
260
  function pageGlyphScope(localDefinitions, sharedDefinitions, pageSharedKeys) {
218
261
  return [
219
262
  ...localDefinitions.values(),
@@ -226,6 +269,8 @@ function pageGlyphScope(localDefinitions, sharedDefinitions, pageSharedKeys) {
226
269
  ].map((definition) => ({
227
270
  key: definition.key,
228
271
  notation: definition.notation,
272
+ // Rule N23: an entry whose index is a fixed label is no family.
273
+ ...(definition.fixedIndex === true ? { fixedIndex: true } : {}),
229
274
  }));
230
275
  }
231
276
 
@@ -385,8 +430,9 @@ function inlineEquationNode(mathNode) {
385
430
  * in a lesson is numbered; a keyed one anchors at `#eq-<key>`, an unkeyed one
386
431
  * at its positional `#eq-<section>-<index>`. The visible number is a focusable
387
432
  * `<a>` sibling — never authored, never inside KaTeX — so it survives print and
388
- * screen readers name it "Equation 2.4". A "Copy LaTeX" button sits beside it
389
- * (`.keyed-equation__controls`, styled in `global.css`; titled "Copy LaTeX —
433
+ * screen readers name it "Equation 2.4". A "Copy LaTeX" button sits on the
434
+ * equation's bottom border, under the number (`.keyed-equation__copy`, styled
435
+ * in `global.css` and placed by `float-toolbar.ts`; titled "Copy LaTeX —
390
436
  * ‹short›" when the equation has a `\caption{}`) — the equation's own
391
437
  * copy affordance, distinct from the notation popup card's (`NotationLayer.astro`),
392
438
  * which only exists for a symbol with a registry entry, not a bare display
@@ -431,19 +477,21 @@ function equationNode(mathNode, { key, number, short }) {
431
477
  },
432
478
  },
433
479
  },
434
- el(
435
- 'button',
436
- {
437
- type: 'button',
438
- className: ['keyed-equation__copy'],
439
- 'data-eq-copy': '',
440
- 'data-latex': latex,
441
- 'aria-label': `Copy LaTeX for Equation ${number}`,
442
- title: short ? `Copy LaTeX — ${short}` : 'Copy LaTeX',
443
- },
444
- [copyIconNode()],
445
- ),
446
480
  ]),
481
+ // On the block's border, not beside the number: the number's toolbar
482
+ // (`float-toolbar.ts`) takes the room around the number.
483
+ el(
484
+ 'button',
485
+ {
486
+ type: 'button',
487
+ className: ['keyed-equation__copy'],
488
+ 'data-eq-copy': '',
489
+ 'data-latex': latex,
490
+ 'aria-label': `Copy LaTeX for Equation ${number}`,
491
+ title: short ? `Copy LaTeX — ${short}` : 'Copy LaTeX',
492
+ },
493
+ [copyIconNode()],
494
+ ),
447
495
  ],
448
496
  };
449
497
  }
@@ -467,6 +515,137 @@ function injectFloatNumberAttributes(node, float, prefix) {
467
515
  });
468
516
  }
469
517
 
518
+ /**
519
+ * The URL path Starlight serves a docs page at: `motion/index.mdx` is
520
+ * `/motion/`, `motion/speed.mdx` is `/motion/speed/`, `index.mdx` is `/`.
521
+ */
522
+ function pageUrlFrom(file) {
523
+ for (const value of [file?.path, file?.history?.[0], file?.data?.file]) {
524
+ if (typeof value !== 'string') continue;
525
+ const match = value.match(LESSON_DOC_PATH);
526
+ if (!match) continue;
527
+ const route = match[1].replace(/\\/g, '/').replace(/(?:^|\/)index$/, '');
528
+ return route ? `/${route}/` : '/';
529
+ }
530
+ return undefined;
531
+ }
532
+
533
+ /**
534
+ * The chapter slug a link's `href` names — `motion/speed` for
535
+ * `/motion/speed/#sec-x`, for `../speed/` from a sibling page, and for
536
+ * `/base/motion/speed/` under the course's `base` — or `undefined` for a
537
+ * same-page anchor, another site, or a scheme such as `mailto:`.
538
+ */
539
+ function linkedSlug(href, pageUrl, base) {
540
+ if (typeof href !== 'string' || href === '' || href.startsWith('#')) return undefined;
541
+ if (href.startsWith('//') || /^[a-z][a-z0-9+.-]*:/i.test(href)) return undefined;
542
+ let url;
543
+ try {
544
+ url = new URL(href, `https://course.invalid${pageUrl ?? '/'}`);
545
+ } catch {
546
+ return undefined;
547
+ }
548
+ let path;
549
+ try {
550
+ path = decodeURIComponent(url.pathname);
551
+ } catch {
552
+ return undefined;
553
+ }
554
+ const prefix = base && base !== '/' ? base.replace(/\/+$/, '') : '';
555
+ if (prefix && (path === prefix || path.startsWith(`${prefix}/`))) {
556
+ path = path.slice(prefix.length);
557
+ }
558
+ return path.replace(/^\/+|\/+$/g, '');
559
+ }
560
+
561
+ /**
562
+ * A link the author wrote to another chapter shows that chapter's full title
563
+ * as its native tooltip (`title`): prose names a chapter any way it likes —
564
+ * "the previous chapter", "Chapter 1.2", "this derivation" — and a reader can
565
+ * see where the link goes before following it. A Markdown link and a JSX
566
+ * `<a href>` both get it; a link with a title of its own, or one to the page
567
+ * it is on, keeps what it has.
568
+ */
569
+ function titleChapterLinks(tree, file, titleBySlug, base) {
570
+ if (titleBySlug.size === 0) return;
571
+ const pageUrl = pageUrlFrom(file);
572
+ const here = pageUrl?.replace(/^\/+|\/+$/g, '');
573
+ const titleOf = (href) => {
574
+ const slug = linkedSlug(href, pageUrl, base);
575
+ return slug !== undefined && slug !== here ? titleBySlug.get(slug) : undefined;
576
+ };
577
+ const walk = (node) => {
578
+ if (!node || typeof node !== 'object') return;
579
+ if (node.type === 'link' && !node.title) {
580
+ const title = titleOf(node.url);
581
+ if (title) node.title = title;
582
+ } else if (
583
+ (node.type === 'mdxJsxTextElement' || node.type === 'mdxJsxFlowElement') &&
584
+ node.name === 'a'
585
+ ) {
586
+ const attributes = node.attributes ?? [];
587
+ const named = (name) =>
588
+ attributes.find((attribute) => attribute?.type === 'mdxJsxAttribute' && attribute.name === name);
589
+ const title = named('title') ? undefined : titleOf(named('href')?.value);
590
+ if (title) {
591
+ node.attributes = [...attributes, { type: 'mdxJsxAttribute', name: 'title', value: title }];
592
+ }
593
+ }
594
+ if (Array.isArray(node.children)) node.children.forEach(walk);
595
+ };
596
+ walk(tree);
597
+ }
598
+
599
+ /**
600
+ * The innermost component around `target` that numbering does not see
601
+ * through — the reason a float in it has no number — or `undefined`.
602
+ */
603
+ function blockingComponent(tree, target) {
604
+ const search = (node, blocking) => {
605
+ if (node === target) return blocking;
606
+ if (!node || typeof node !== 'object' || !Array.isArray(node.children)) return undefined;
607
+ const here =
608
+ node.type === 'mdxJsxFlowElement' &&
609
+ node.name &&
610
+ !TRANSPARENT_COMPONENTS.includes(node.name)
611
+ ? node.name
612
+ : node.type === 'containerDirective'
613
+ ? `:::${node.name ?? ''}`
614
+ : blocking;
615
+ for (const child of node.children) {
616
+ const found = search(child, here);
617
+ if (found !== undefined) return found;
618
+ }
619
+ return undefined;
620
+ };
621
+ return search(tree, undefined);
622
+ }
623
+
624
+ /**
625
+ * Why a `<Table>`/`<Figure>` on a lesson page got no number: it sits in a
626
+ * component other than the `TRANSPARENT_COMPONENTS`, or in a `:::note`
627
+ * aside, where a float is illustrative, as an equation there is
628
+ * (`keyed-jsx-floats.mjs`).
629
+ */
630
+ function unnumberedFloatMessage(tagName, component) {
631
+ const where = !component
632
+ ? 'another component'
633
+ : component.startsWith(':::')
634
+ ? `a ${component} aside`
635
+ : `<${component}>`;
636
+ const noun = tagName === 'Table' ? 'table' : 'figure';
637
+ const instead =
638
+ tagName === 'Table'
639
+ ? 'write it as a plain Markdown table (rule S3 does not flag one there)'
640
+ : 'show its content without <Figure>';
641
+ return (
642
+ `<${tagName}> inside ${where} gets no number: a lesson numbers a ${noun} ` +
643
+ `at its top level or in a <Memo>, <ShortNote> or <Fold>. A ${noun} in a ` +
644
+ `worked example or another component is illustrative, like an equation ` +
645
+ `there: ${instead}, or move the <${tagName}> out of ${where}.`
646
+ );
647
+ }
648
+
470
649
  /** Resolve a `[[eq-key]]` / `[[slug#eq-key]]` reference to `{ number, href }`. */
471
650
  function resolveEquationRef(eqRef, eqCtx, file, node) {
472
651
  if (eqRef.lesson) {
@@ -1161,6 +1340,16 @@ function assertManifestNumberParity({
1161
1340
 
1162
1341
  for (const [key, number] of pageLabels) {
1163
1342
  const expected = published[key];
1343
+ if (expected === undefined && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(key)) {
1344
+ // The manifest left it out on purpose: `validate:content` reports the
1345
+ // id (`<prefix>-invalid-key`), and recompiling changes nothing.
1346
+ fail(
1347
+ file,
1348
+ `${slug}: the id in ${syntax(key)} is not a valid key — lowercase ` +
1349
+ `letters, digits and hyphens only — so the manifest leaves it unnumbered. ` +
1350
+ `Fix the id.`,
1351
+ );
1352
+ }
1164
1353
  if (expected === undefined) {
1165
1354
  fail(
1166
1355
  file,
@@ -1287,6 +1476,9 @@ export default function remarkNotation(options = {}) {
1287
1476
  // The manifest supplies every numbering input at once; an explicit option
1288
1477
  // still overrides it, so existing per-field wiring keeps working.
1289
1478
  const numbering = { ...numberingFromManifest(options.manifest), ...options };
1479
+ const chapterTitleBySlug = new Map(
1480
+ (options.manifest?.lessons ?? []).map((lesson) => [lesson.slug, lesson.title]),
1481
+ );
1290
1482
  // Every lesson's headings by anchor, built once: `[[lesson#sec-…]]` reads
1291
1483
  // another page's, `[[sec-…]]` the page's own.
1292
1484
  const sectionSettings = { show: true, depth: 4, ...numbering.sectionNumbering };
@@ -1310,6 +1502,9 @@ export default function remarkNotation(options = {}) {
1310
1502
  const suspendedOperators = suspendedOperatorsFromFile(file);
1311
1503
  const lessonId = lessonIdFrom(file);
1312
1504
  const lessonSlug = lessonId ? lessonId.replace(/\./g, '/') : undefined;
1505
+ // The landing page, a part's overview page (rule S10): its math binds
1506
+ // like a chapter's, but it numbers nothing.
1507
+ const nonChapterPage = !lessonId && isDocsPageFile(file);
1313
1508
  // `{ prefix, offset }` per kind of float — see the `numbering` option.
1314
1509
  // Missing ⇒ the legacy `##`-section numbering.
1315
1510
  const equationNumbering = pageNumbering(numbering, lessonSlug, 'equations');
@@ -1329,6 +1524,13 @@ export default function remarkNotation(options = {}) {
1329
1524
  const equationLabels = collectEquationLabels(tree, equationNumbering);
1330
1525
  const pageLabels = new Map();
1331
1526
  for (const label of equationLabels) {
1527
+ if (nonChapterPage) {
1528
+ fail(
1529
+ file,
1530
+ `\\label{eq:${label.key}} on a page that is not a chapter (rule S10): only a chapter numbers an equation. Drop the label to show the equation unnumbered, or link the chapter's own with [[<chapter>#eq-<key>]].`,
1531
+ label.node,
1532
+ );
1533
+ }
1332
1534
  if (pageLabels.has(label.key)) {
1333
1535
  fail(
1334
1536
  file,
@@ -1568,20 +1770,28 @@ export default function remarkNotation(options = {}) {
1568
1770
 
1569
1771
  inspectExplainCalls(value, resolve, file, node, references);
1570
1772
 
1571
- const bindingScope = lessonId
1572
- ? mathScope
1573
- : registryFile
1574
- ? registryMathDefinitions
1575
- : undefined;
1773
+ const bindingScope =
1774
+ lessonId || nonChapterPage
1775
+ ? mathScope
1776
+ : registryFile
1777
+ ? registryMathDefinitions
1778
+ : undefined;
1576
1779
 
1577
1780
  if (bindingScope) {
1578
- const context = lessonId ? 'lesson math' : 'notation definition math';
1781
+ const context = lessonId
1782
+ ? 'lesson math'
1783
+ : nonChapterPage
1784
+ ? 'page math'
1785
+ : 'notation definition math';
1579
1786
 
1580
1787
  let result;
1581
1788
  try {
1582
1789
  // Suspension (rule N22) is lesson-body scope only.
1583
1790
  result = resolveMathGlyphs(value, bindingScope, {
1584
1791
  suspended: lessonId ? suspendedOperators : [],
1792
+ // Derived cards (rule N23) are a page's own, built by the
1793
+ // manifest for its body math — not the registry page's.
1794
+ derive: Boolean(lessonId || nonChapterPage),
1585
1795
  });
1586
1796
  } catch (error) {
1587
1797
  const message =
@@ -1596,28 +1806,46 @@ export default function remarkNotation(options = {}) {
1596
1806
  if (result.unresolved.length > 0) {
1597
1807
  const unresolved = result.unresolved
1598
1808
  .map(
1599
- ({ token, start, suspended, font }) =>
1809
+ ({ token, start, suspended, font, lenders }) =>
1600
1810
  `${tokenLocation(file, node, value, start)}: "${token}"${
1601
1811
  suspended
1602
1812
  ? ` (${suspended} is suspended on this page, so this form needs its own entry)`
1603
1813
  : font
1604
1814
  ? ' (a different symbol from the plain letter; it needs its own entry)'
1605
- : ''
1815
+ : lenders
1816
+ ? ` (${lenders.join(', ')} explains only its own instances with a subscript, never a bare "${token}", rule N4: give it its own entry, or \\explain{${lenders[0]}}{${token}} if it is that entry's shorthand)`
1817
+ : ''
1606
1818
  }`,
1607
1819
  )
1608
1820
  .join(', ');
1609
1821
  fail(
1610
1822
  file,
1611
- `Unresolved notation in ${context}: ${unresolved}. Introduce the symbol with [[key]], define it in notation.local, or wrap it in \\explain{key}{latex}.`,
1823
+ nonChapterPage
1824
+ ? `Unresolved notation in ${context}: ${unresolved}. Introduce the symbol with [[key]], or wrap it in \\explain{key}{latex}: a page that is not a chapter recalls shared notation and defines none (rule S10).`
1825
+ : `Unresolved notation in ${context}: ${unresolved}. Introduce the symbol with [[key]], define it in notation.local, or wrap it in \\explain{key}{latex}.`,
1612
1826
  node,
1613
1827
  );
1614
1828
  }
1615
1829
 
1616
1830
  for (const binding of result.bindings) {
1617
1831
  if (binding.level === 'explicit') continue;
1832
+ if (binding.match === 'derived') continue;
1618
1833
  resolve(binding.key, 'math', node);
1619
1834
  references.push({ key: binding.key, kind: 'math' });
1620
1835
  }
1836
+ // A derived card (rule N23) uses its base and its index entry; it
1837
+ // is recorded composed, so a table header reads its own label.
1838
+ for (const derived of result.derived) {
1839
+ const base = resolve(derived.base, 'math', node);
1840
+ const index = resolve(derived.index, 'math', node);
1841
+ references.push(
1842
+ { key: derived.base, kind: 'math' },
1843
+ { key: derived.index, kind: 'math' },
1844
+ );
1845
+ if (base && index) {
1846
+ resolved.set(derived.key, derivedResolution(derived.key, base, index));
1847
+ }
1848
+ }
1621
1849
  replaceMathSource(node, result.latex);
1622
1850
  }
1623
1851
  return;
@@ -1668,6 +1896,27 @@ export default function remarkNotation(options = {}) {
1668
1896
  injectFloatNumberAttributes(child, diagramByNode.get(child), 'dia');
1669
1897
  } else if (child?.type === 'mdxJsxFlowElement' && exampleByNode.has(child)) {
1670
1898
  injectFloatNumberAttributes(child, exampleByNode.get(child), 'ex');
1899
+ } else if (
1900
+ nonChapterPage &&
1901
+ child?.type === 'mdxJsxFlowElement' &&
1902
+ NUMBERED_BLOCK_NAMES.has(child.name)
1903
+ ) {
1904
+ // `validate:content` reports this first (gate-non-chapter-pages.ts);
1905
+ // this is the same message for a build that skipped it.
1906
+ fail(
1907
+ file,
1908
+ `<${child.name}> on a page that is not a chapter (rule S10): only a chapter numbers it. Move it into the chapter it belongs to, and link it from here: the link shows it in its card.`,
1909
+ child,
1910
+ );
1911
+ } else if (
1912
+ lessonId &&
1913
+ child?.type === 'mdxJsxFlowElement' &&
1914
+ (child.name === 'Table' || child.name === 'Figure')
1915
+ ) {
1916
+ // Numbered everywhere a lesson numbers one, so this one is nested
1917
+ // in a component that is not transparent. Say so here, at its
1918
+ // line, before the component fails on its missing number.
1919
+ fail(file, unnumberedFloatMessage(child.name, blockingComponent(tree, child)), child);
1671
1920
  }
1672
1921
  }
1673
1922
  };
@@ -1727,6 +1976,9 @@ export default function remarkNotation(options = {}) {
1727
1976
  }
1728
1977
  }
1729
1978
 
1979
+ // Before `visit`, which writes the `[[…]]` reference links: each of
1980
+ // those opens a card of its own.
1981
+ titleChapterLinks(tree, file, chapterTitleBySlug, options.base);
1730
1982
  visit(tree);
1731
1983
 
1732
1984
  file.data.notationReferences = references;
@@ -4,6 +4,17 @@ export { slotPlaceholder } from './lab-math-slot.js';
4
4
  interface LabMathScopeDefinition {
5
5
  readonly key: string;
6
6
  readonly notation: string;
7
+ /** Rule N23: the entry's index is a fixed label, so it is no family. */
8
+ readonly fixedIndex?: true;
9
+ }
10
+ interface RenderLabMathOptions {
11
+ /**
12
+ * `false` renders the template as inline math — a `.katex` with no
13
+ * `.katex-display` — for a formula that sits in a line: a legend entry,
14
+ * an axis title, a control label. It never scrolls (`global.css`
15
+ * `.lab-math`). Default `true`, a display equation.
16
+ */
17
+ readonly display?: boolean;
7
18
  }
8
19
  /**
9
20
  * Compile a developer-authored lab math template the same way lesson `$$`
@@ -11,6 +22,6 @@ interface LabMathScopeDefinition {
11
22
  * page's scope or this throws. Slots are set aside during binding and restored
12
23
  * as inert `data-lab-slot` spans the interactive island rewrites at runtime.
13
24
  */
14
- declare function renderLabMath(template: string, scope: readonly LabMathScopeDefinition[]): RenderedLabMath;
25
+ declare function renderLabMath(template: string, scope: readonly LabMathScopeDefinition[], { display }?: RenderLabMathOptions): RenderedLabMath;
15
26
 
16
- export { type LabMathScopeDefinition, RenderedLabMath, renderLabMath };
27
+ export { type LabMathScopeDefinition, type RenderLabMathOptions, RenderedLabMath, renderLabMath };
@@ -2,9 +2,11 @@ import katex from "./katex-delimiters.mjs";
2
2
  import { resolveMathGlyphs, stripNotationMarkers } from "./math-glyphs.mjs";
3
3
  import { createNotationKatexOptions } from "./katex-options.mjs";
4
4
  import { slotPlaceholder } from "./lab-math-slot.js";
5
+ import { pageOfScope, registerPageDerivedNotation } from "./page-notation.js";
6
+ import { indexFindings } from "./gate-indices.js";
5
7
  const SLOT_PATTERN = /\\slot\{([a-z][a-z0-9]*)\}\{((?:[^{}]|\{[^{}]*\})*)\}/g;
6
8
  const sentinelFor = (index) => `909909${index}.909909`;
7
- function renderLabMath(template, scope) {
9
+ function renderLabMath(template, scope, { display = true } = {}) {
8
10
  const slots = [];
9
11
  const forBinding = template.replace(
10
12
  SLOT_PATTERN,
@@ -14,9 +16,12 @@ function renderLabMath(template, scope) {
14
16
  return ` ${sentinel} `;
15
17
  }
16
18
  );
19
+ const page = pageOfScope(scope);
17
20
  let result;
18
21
  try {
19
- result = resolveMathGlyphs(forBinding, [...scope]);
22
+ result = resolveMathGlyphs(forBinding, [...scope], {
23
+ derive: page !== void 0
24
+ });
20
25
  } catch (error) {
21
26
  throw new Error(
22
27
  `Lab math failed to compile: ${error instanceof Error ? error.message : String(error)}
@@ -32,6 +37,16 @@ function renderLabMath(template, scope) {
32
37
  template: ${template.trim()}`
33
38
  );
34
39
  }
40
+ if (page?.checkIndices) {
41
+ const findings = indexFindings(result.indices);
42
+ if (findings.length > 0) {
43
+ throw new Error(
44
+ `Lab math has an index no card explains (rule N23, ${page.context}): ${findings.join("; ")}.
45
+ template: ${template.trim()}`
46
+ );
47
+ }
48
+ }
49
+ if (page) registerPageDerivedNotation(page.locals, result.derived, page.context);
35
50
  let bound = result.latex;
36
51
  let copyBound = result.latex;
37
52
  slots.forEach(({ name, fallback }, index) => {
@@ -48,10 +63,7 @@ function renderLabMath(template, scope) {
48
63
  );
49
64
  copyBound = copyBound.replace(sentinel, slotPlaceholder(name));
50
65
  });
51
- const html = katex.renderToString(
52
- bound,
53
- createNotationKatexOptions({ displayMode: true })
54
- );
66
+ const html = katex.renderToString(bound, createNotationKatexOptions({ displayMode: display })).replaceAll('<span class="katex-mathml">', '<span class="katex-mathml" data-pagefind-ignore="">');
55
67
  return {
56
68
  html,
57
69
  slots: slots.map(({ name }) => name),
@@ -21,7 +21,32 @@ interface SectionRefLessonInput {
21
21
  /** The anchors of the lesson's top-level headings (`manifest.lessons[].headings`). */
22
22
  readonly headingSlugs: readonly string[];
23
23
  }
24
- /** One `sec-ref-resolves` error per `[[sec-…]]` that names no heading. */
25
- declare function validateSectionRefs(lessons: readonly SectionRefLessonInput[]): MathGateDiagnostic[];
24
+ /**
25
+ * One `sec-ref-resolves` error per `[[sec-…]]` that names no heading.
26
+ * `pageSlugs` are the pages that are not chapters (`motion/index`), which a
27
+ * section reference cannot reach; the message says so instead of calling the
28
+ * page missing.
29
+ */
30
+ declare function validateSectionRefs(lessons: readonly SectionRefLessonInput[], pageSlugs?: readonly string[]): MathGateDiagnostic[];
31
+ interface HeadingAnchorLessonInput {
32
+ readonly file: string;
33
+ readonly body: string;
34
+ readonly bodyLine?: number;
35
+ /** The lesson's outline headings in order (`manifest.lessons[].headings`). */
36
+ readonly headings: readonly {
37
+ readonly slug: string;
38
+ readonly text: string;
39
+ }[];
40
+ /** Every anchor the engine writes on the page, with what it belongs to. */
41
+ readonly anchors: ReadonlyMap<string, string>;
42
+ }
43
+ /**
44
+ * One `heading-anchor-collision` error per heading whose anchor the engine
45
+ * already gives something else on the page — `## References` on a page that
46
+ * cites (the generated References section is `#references`), or `## Tab x`
47
+ * beside `<Table id="x">` (`#tab-x`). Two elements with one id send every link
48
+ * to the first, and the LaTeX export defines the label twice.
49
+ */
50
+ declare function validateHeadingAnchors(lessons: readonly HeadingAnchorLessonInput[]): MathGateDiagnostic[];
26
51
 
27
- export { type SectionRefLessonInput, validateSectionRefs };
52
+ export { type HeadingAnchorLessonInput, type SectionRefLessonInput, validateHeadingAnchors, validateSectionRefs };