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
@@ -106,6 +106,13 @@ Part 2 Projectile motion
106
106
 
107
107
  ## Part pages
108
108
 
109
+ A part is listed in `course.settings.json` → `parts` as `{ "dir", "label" }`
110
+ and lives in `content/docs/<dir>/`. A part listed before anything is written
111
+ in it — no chapter and no overview page yet — is left out of the sidebar and
112
+ the numbering, with a `part-empty` warning, so the parts after it keep their
113
+ numbers until it has content. A lesson in a directory no part lists fails
114
+ `validate:content` (`lesson-outside-parts`).
115
+
109
116
  A part is a directory, so `/<part>/` is a URL a reader will eventually type,
110
117
  follow, or be handed by a search engine. Without a page it is a 404, and the
111
118
  part exists only as a sidebar heading that cannot be linked to.
@@ -154,6 +161,14 @@ missing, holds more than one paragraph, or falls outside the course's
154
161
  takes no float numbers, and never appears in `manifest.lessons` or a track. A
155
162
  course adds one part at a time; a part without one behaves exactly as before.
156
163
 
164
+ **It explains its symbols, and links its floats**
165
+ ([S10](structure-rules.md#s10)). A `[[key]]` term, math, a citation, an
166
+ abbreviation and a `[[chapter#tab-key]]` link each open their card on an
167
+ overview page, as on the landing page and any other page outside a part; the
168
+ manifest lists each such page under `pages` with its notation bundle. A
169
+ `<Table>`, `<Figure>`, `<Diagram>`, lab or `\label{eq:…}` stays in its
170
+ chapter, which alone can number it, and the overview links it.
171
+
157
172
  `<PartOverview>` reads the manifest for everything it lists — chapters in
158
173
  reading order with their descriptions, the competencies the part teaches, the
159
174
  parts it builds on and leads to — so the page cannot fall out of date with the
@@ -301,7 +316,11 @@ punctuation dropped, so `## Two ways to name the same point` is
301
316
  `-1`, `-2`. A reference to a heading without a number (deeper than `depth`, or
302
317
  with `show: false`) prints the heading's title in quotes instead: a reader
303
318
  could not find the number. A reference that names no top-level heading fails
304
- `pnpm validate:content` (`sec-ref-resolves`) and the build.
319
+ `pnpm validate:content` (`sec-ref-resolves`) and the build. So does a heading
320
+ whose anchor the page already gives something else
321
+ (`heading-anchor-collision`): `## References` on a page that cites a source
322
+ (the References section is generated, at `#references`), or `## Tab totals`
323
+ beside `<Table id="totals">` (`#tab-totals`).
305
324
 
306
325
  ### Structure from the bottom up
307
326
 
@@ -339,6 +358,13 @@ worked-example set (`<CompactExamples>`) is numbered **`‹part›.‹chapter›
339
358
  - **Keyed or not, everything counts.** An equation with no `\label{eq:…}` still
340
359
  takes a number and a positional anchor (`#eq-2-1-4`); it just cannot be
341
360
  referenced by name.
361
+ - **The lesson's flow counts, a callout does not.** An equation or a float
362
+ in a list item or a `>` quote counts like one at the top level, and so
363
+ does one in a `<Memo>`, `<ShortNote>` or `<Fold>`. One in a `:::note`
364
+ aside (or `<Aside>`, or a worked example) is illustrative and takes no
365
+ number; a `<Table>` or `<Figure>` there fails the build. A display
366
+ equation is a `$$` block whose fences sit on lines of their own:
367
+ `$$x$$` inside a sentence, or on one line, is inline math.
342
368
  - **The number sits with the caption.** A figure or diagram prints it in the
343
369
  caption line below its picture, a table in the caption line above, and a
344
370
  display equation beside it. What names each
@@ -432,6 +458,12 @@ that wants them addressed should wrap the table in `<Table id="…">`/
432
458
  `<Figure id="…">`, or give the diagram an `id` and move it to the top level (or
433
459
  inside `<Memo>`/`<ShortNote>`), or wrap it in `<Figure>`.
434
460
 
461
+ A raw table counts only where a `<Table>` would number it: at the top level,
462
+ or in a `<Memo>`, `<ShortNote>` or `<Fold>`. One in a worked example
463
+ (`<CompactExample>`) or another component is illustrative, like an equation
464
+ there, and stays a plain table: a `<Table>` or a `<Figure>` there gets no
465
+ number, and the build fails on it, naming the component it sits in.
466
+
435
467
  ### Knowledge checks
436
468
 
437
469
  An assessment title is authored once, in `content/assessments/<id>.json`, and
@@ -557,7 +589,9 @@ A block folds in one of two ways, and every default lives in one place, the
557
589
  `sections`, `subsections` and `knowledgeChecks` take `{ collapsible,
558
590
  defaultOpen }`; the others take `{ defaultOpen }`; `tables` takes
559
591
  `{ truncateAfter }` — a table with more rows shows that many, half from each
560
- end, and a "… N more rows …" line. There is no `disclosure.diagrams`: a
592
+ end, and a "… N more rows …" line, unless that would hide a single row (the
593
+ line is as tall as the row, so the table shows whole). There is no
594
+ `disclosure.diagrams`: a
561
595
  diagram does not fold. Every collapsible remembers the reader's own choice,
562
596
  per page, over the default (`rememberState={false}` on a memo, a fold, a
563
597
  figure's tools or an example set opts that one out). A short note's in-place
@@ -632,3 +666,34 @@ While collapsed, the surrounding sentence remains one paragraph and only the
632
666
  point in the reading flow, and exposes a “Show less” control that restores the
633
667
  original sentence. Short notes are for a locally optional explanation, not for
634
668
  material that needs a number or citations elsewhere; use a memo for that.
669
+
670
+ ## Retired keys
671
+
672
+ A key the engine has stopped reading fails the build wherever a content file
673
+ still sets it, so a course cannot keep maintaining a field that does nothing.
674
+ Each `retired-key` error names the file and the key's line; deleting the line
675
+ clears it.
676
+
677
+ | Key | Where it was set | Why it went |
678
+ | --- | --- | --- |
679
+ | `editorialStatus` | pages, notation entries, abbreviations, competencies, assessments, sources, tracks | Lessons are reviewed continuously, so no page ever earns a one-time "reviewed" stamp. The draft / in-review / reviewed labels it put on part pages, notation cards, glossary entries and citations are gone. |
680
+
681
+ Two of Starlight's own keys do not work in a course, because the manifest,
682
+ not Starlight, decides which chapters a course lists and in what order. A
683
+ lesson's `draft: true` fails the build as `unsupported-key`: Starlight would
684
+ leave the page out of a production build while the sidebar still names it.
685
+ Keep an unfinished lesson outside `content/docs/` until it is ready. A
686
+ chapter's `sidebar.order` warns as `ignored-key`: chapters follow the order of
687
+ the tracks (`content/tracks/*.json`).
688
+
689
+ ## Content schemas
690
+
691
+ Every content file is checked against its collection's schema — the same
692
+ one `astro build` loads it with (`src/compiler/content-schemas.ts`) — by
693
+ `pnpm validate:content` and `pnpm content check`, so a value the build would
694
+ reject fails there first, at the key's line, as a `content-schema` error:
695
+ a notation `meaning` with `$` or `\`, a source `type` outside `primary`,
696
+ `official-guidance`, `paper`, `book` and `secondary`, or a `studyMinutes`
697
+ range whose low end exceeds its high end. A lesson is checked for the fields
698
+ explico adds (`teaches`, `assumptions`, `studyMinutes`, `notation`);
699
+ Starlight checks its own (`title`, `description`, …) when the site builds.
@@ -48,8 +48,8 @@ an independent diagram number, a `#dia-<id>` anchor, and references of the form
48
48
  equation, table, or figure sequences.
49
49
 
50
50
  A diagram is named like every float: a long `caption`, shown below the graph
51
- in the same caption line a figure has (`Diagram 2.1.1`, a chain icon that
52
- copies the diagram's link, the caption), and an optional `short` caption for
51
+ in the same caption line a figure has (`Diagram 2.1.1`, whose toolbar
52
+ copies the diagram's link, then the caption), and an optional `short` caption for
53
53
  the `[[dia-…]]` preview card
54
54
  ([components.md § The caption pair](components.md#the-caption-pair)).
55
55
 
@@ -63,14 +63,16 @@ or Escape, restores its original width. A diagram never folds by itself: it
63
63
  is its graph and then its caption line, in every state. To let a reader fold
64
64
  it away, put it in a collapsible section.
65
65
 
66
- By default a laid-out width above `800px` emits a compile-time warning. Treat
67
- that warning as a prompt to try
68
- `direction="TB"`, shorter labels, or explicit node sizes before accepting the
69
- wide layout. Set `warnAboveWidth={false}` only when horizontal overflow is
70
- intentional, or set a positive pixel value when the course needs a different
71
- review threshold.
66
+ Each node's box is sized from what it holds: its `text` measured in DejaVu
67
+ Sans, the widest common system font, and its `formula` measured from KaTeX's
68
+ own layout, so neither spills out of the box in any reader's font. A `text`
69
+ wider than about 190px wraps onto more lines and the box grows taller; a
70
+ `formula` never wraps, so a wide one widens its box and the text wraps to it.
71
+ Give a node a `width` to narrow it (its text wraps inside it and the height
72
+ follows) or a `height` to make it taller. A diagram that comes out wider than
73
+ the lesson column is [V8](#v8)'s concern.
72
74
 
73
- _Enforcement._ Review-only, with a compile-time width warning from `Diagram`.
75
+ _Enforcement._ Review-only.
74
76
 
75
77
  _Exceptions._ A one-off illustrative schematic that is not a graph (a labelled
76
78
  photo, a geometric figure) is a [V2](#v2) figure, not a `Diagram`.
@@ -115,16 +117,23 @@ equations.**
115
117
  An interactive figure is a React island, hydrated `client:visible` (or
116
118
  `client:idle`). It carries:
117
119
 
118
- - **Gate-checked math.** Any equation shown in the figure goes through the
120
+ - **Gate-checked math.** Every symbol the figure shows — an equation, a
121
+ legend entry, an axis title, a slider label — goes through the
119
122
  `labMathScope` / `renderLabMath` / `\slot{}` seam so its symbols are resolved
120
123
  against the page's real notation at build time and the island only patches
121
- numbers into pre-rendered KaTeX. A wrapper whose template names notation not
122
- otherwise present in lesson MDX passes
124
+ numbers into pre-rendered KaTeX. `renderLabMath(template, scope)` renders a
125
+ display equation; `renderLabMath(template, scope, { display: false })`
126
+ renders inline math for a formula that sits in a line, which never scrolls.
127
+ A lab never calls KaTeX itself: its symbols would open no card. A wrapper
128
+ whose template names notation not otherwise present in lesson MDX passes
123
129
  `{ locals: Astro.locals, context: '<ComponentName> <template>' }` as the third
124
130
  `labMathScope` argument; that is the declaration that ships those definitions
125
131
  in this page's popup payload. See
126
132
  `playground/content/labs/ProjectileRangeExplorer.tsx` +
127
- `ProjectileRangeMath.astro`.
133
+ `ProjectileRangeMath.astro`. Pass the table `labMathScope` returned to
134
+ `renderLabMath` unchanged: the page's index entries reach the lab through
135
+ it, so `\mathbf{v}_0` gets its derived card ([N23](notation.md#n23)), and a copy
136
+ (`[...scope]`) gets neither the cards nor the index check.
128
137
  - **Labelled controls.** Every control is a labelled `<input>` — the visible
129
138
  label is how any reader knows what a slider sets; where precision
130
139
  matters, pair a `range` slider with a `number` field (see `RangeField` in
@@ -136,8 +145,7 @@ An interactive figure is a React island, hydrated `client:visible` (or
136
145
  that describes the plot ("the dot marks the landing point") belongs in the
137
146
  figure's caption.
138
147
  - **Picture, then caption, then tools.** A lab is laid out as a LaTeX figure
139
- with its controls after it (docs/plans/figures-and-short-captions.md,
140
- decision 3). The plot goes in the `<Figure>`'s default slot — its
148
+ with its controls after it. The plot goes in the `<Figure>`'s default slot — its
141
149
  picture — and the controls in its `tools` slot: an instruction line, the
142
150
  sliders and fields, the live equation. The two are separate islands that
143
151
  share their values. The tools render after the caption line, in a tray of
@@ -151,20 +159,32 @@ An interactive figure is a React island, hydrated `client:visible` (or
151
159
  case shown — "Shown for v₀ = 20 m/s and θ = 30°: range 35.3 m, time of
152
160
  flight 2.04 s." The lab writes it with `setFigureState(element, runs)`
153
161
  (`explico/components/figure/figure-state.ts`), `element` being any element
154
- of its own inside the figure and `runs` plain text, `{ text, italic: true }`
155
- or `{ sub }` — never HTML. The clause prints with the caption; a reader
156
- of the long caption elsewhere (a preview card, a fold's bar) sees the
157
- caption alone.
162
+ of its own inside the figure and each run a string of plain text or
163
+ `{ math }`, a symbol rendered at build time with
164
+ `renderLabMath(template, scope, { display: false })` and handed to the
165
+ island — never HTML, and never a symbol typed as text, which would open no
166
+ card. `ProjectileRangePicture.astro` renders the playground's. The clause
167
+ prints with the caption; a reader of the long caption elsewhere (a
168
+ preview card, a fold's bar) sees the caption alone.
158
169
  - **Which text goes where.** The caption names the figure and says what it
159
170
  shows — its subject, its marks and symbols, the case shown — and may state
160
171
  the one thing to notice. The argument drawn from the figure is lesson
161
172
  text, after the figure. Instructions for the controls belong to the
162
173
  controls: the tools' first line, not the caption.
163
- - **A preview shows the plot.** A same-page `[[fig-…]]` reference to a lab
164
- previews a still copy of its plot as drawn; the tools are never copied. A
165
- plot not drawn yet — or one on another page, drawn only in the browser —
166
- leaves the card to the caption and number, so a lab no longer needs
167
- `previewable={false}`.
174
+ - **The build draws the default picture.** The island draws its plot in
175
+ the browser, after the page loads. So the lab's Astro wrapper draws the
176
+ same plot at the island's default values at build time and hands it to
177
+ the island as its children, which the island shows until it has drawn
178
+ its own. The built page then has the picture before hydration, with
179
+ JavaScript off, in print and in the standalone HTML export. With
180
+ Observable Plot that is one options function the island and the wrapper
181
+ share, and `Plot.plot({ ...options, document })` on a server-side
182
+ document in the wrapper; see `playground/content/labs/still-plot.ts`,
183
+ `LaunchAnglePicture.astro` and `LaunchAngleExplorer.tsx`.
184
+ - **A preview shows the plot.** A `[[fig-…]]` reference to a lab previews a
185
+ still copy of its plot, from this page or another: as drawn once the
186
+ island has run, else the build's default picture. The tools are never
187
+ copied, so a lab needs no `previewable={false}`.
168
188
 
169
189
  The figure itself never hides; a lab that should fold away whole, with the
170
190
  prose around it, goes in a `<Fold>` or a collapsible section.
@@ -184,7 +204,22 @@ _How to apply._ Keep the island's job to "patch numbers and redraw". Domain
184
204
  math lives in a plain module (`playground/content/domain/motion.ts`), not in the
185
205
  component.
186
206
 
187
- _Enforcement._ Review-only.
207
+ _Enforcement._ Partly build-enforced. `pnpm validate:content` follows each
208
+ lesson's relative imports to the course's components and fails one that
209
+ imports `katex` itself (`lab-katex-validate.ts`, `lab-katex-direct`, an
210
+ error), naming the chain of imports that reaches it. `astro build` then reads
211
+ every figure it rendered and fails a symbol KaTeX drew outside a notation
212
+ binding (`figure-symbols.ts`, rule [N4](notation.md#n4)); a course's browser
213
+ test runs the same `unboundFigureSymbols` on a hydrated figure's
214
+ `outerHTML`, which also holds what an island draws only in the browser, as
215
+ the playground's `labs.spec.ts` does. The same build warns about a figure
216
+ whose picture is an island with nothing drawn in the built page
217
+ (`figure-pictures.ts`, `undrawnFigurePictures`, `lab-picture-undrawn`, a
218
+ warning). `renderLabMath` and a `<Diagram>` formula fail `astro build` on
219
+ an index no card explains (rule [N23](notation.md#n23)), as they render.
220
+ `setFigureState` takes no symbol typed as text, so `astro check` fails an
221
+ italic or a `sub` run. That a lab labels its controls and names
222
+ nothing of its own is review-only.
188
223
 
189
224
  _Exceptions._ None.
190
225
 
@@ -348,3 +383,110 @@ _Exceptions._
348
383
  are only two.
349
384
  - A sketch that draws axes is a chart. If the axes are worth drawing, their
350
385
  scale is worth labelling.
386
+
387
+ ---
388
+
389
+ ## V7
390
+
391
+ **A figure’s picture takes at most `maxPercent` of the screen height.**
392
+
393
+ The picture is the default slot of a `<Figure>`: the plot, its key and any
394
+ data tables, without the caption line or the tools. On the screens the course
395
+ targets it stays within `maxPercent` (80% unless the course sets
396
+ `rules.V7.maxPercent`) of the viewport height. Then a reader who scrolls to the
397
+ figure sees the whole picture and its caption line at once, with a line of the
398
+ text that points to it.
399
+
400
+ - **A square or tall plot gets a width, not the column.** A chart drawn with
401
+ `aspectRatio: 1` (Observable Plot) or a height derived from the container
402
+ width grows with the column: a square in a 760px column is 760px tall, almost
403
+ all of a laptop screen. Give such a plot a fixed width (`width: 460`,
404
+ `aspectRatio: 1`) and centre it, or cap it with
405
+ `Math.min(containerWidth, 460)`.
406
+ - **Stacked panels share the budget.** Two panels of 300px each are one 600px
407
+ picture. Shorten both, put them side by side on a wide screen, or keep the
408
+ secondary one in the tools.
409
+ - **Measure, don't estimate.** Font size, the column width and a lab's
410
+ hydrated layout all change the height, so judge it from the rendered page.
411
+
412
+ _Rationale._ A figure is read with its caption and with the sentence that
413
+ sends the reader to it. A picture taller than the screen has to be scrolled
414
+ past to reach its caption, and a control in the tools that redraws it can no
415
+ longer be watched while it is moved.
416
+
417
+ _How to apply._ In the course’s Playwright suite, open every lesson that has
418
+ a figure, at each viewport the course supports, and expect no findings:
419
+
420
+ ```ts
421
+ import { readFileSync } from 'node:fs';
422
+ import { expect, test } from '@playwright/test';
423
+ import { checkFigureFit, lessonsWithFigures } from 'explico/reference/figure-fit';
424
+
425
+ const manifest = JSON.parse(readFileSync('build/manifest.json', 'utf8'));
426
+
427
+ for (const path of lessonsWithFigures(manifest)) {
428
+ test(`${path} figures fit on one screen (rule V7)`, async ({ page }) => {
429
+ await page.setViewportSize({ width: 1280, height: 800 });
430
+ await page.goto(path);
431
+ const findings = await checkFigureFit(page);
432
+ expect(findings.map((finding) => finding.message)).toEqual([]);
433
+ });
434
+ }
435
+ ```
436
+
437
+ Pass `{ maxPercent }` when the course sets `rules.V7.maxPercent`. The check
438
+ opens a figure inside a closed `<Fold>` before it measures, because a reader
439
+ can open it, and waits for each figure’s islands to hydrate and its height to
440
+ settle.
441
+
442
+ _Enforcement._ Partial. `checkFigureFit` in `src/reference/figure-fit.ts`
443
+ reports `figure-too-tall` for each picture over the budget, on the pages and
444
+ viewports a course’s browser tests open; the build cannot measure a laid-out
445
+ page.
446
+
447
+ _Exceptions._ A figure whose picture is itself the subject at full size — a
448
+ long table of values, a tall timeline the reader is meant to scroll — may be
449
+ exempted in the test with a stated reason,
450
+ `checkFigureFit(page, { allow: { 'fig-timeline': 'read by scrolling, one row per year' } })`,
451
+ or kept in a `<Fold>` with a
452
+ `<Table>` instead of a figure.
453
+
454
+ ---
455
+
456
+ ## V8
457
+
458
+ **A diagram is no wider than the lesson column.**
459
+
460
+ A `<Diagram>` lays out to at most `columnWidth` px (707 unless the course sets
461
+ `rules.V8.columnWidth`): the lesson column at a 1366px-wide window, the most
462
+ common laptop screen (the column is 632px at 1280 and 832px at 1920). Then a
463
+ reader sees the whole graph where the text sends them to it, without scrolling
464
+ it sideways or pressing its widen button ↔.
465
+
466
+ The engine does the first part itself. A diagram that comes out wider than
467
+ its limit is laid out again with its labels wrapped narrower, down to 120px
468
+ boxes, and only one that still does not fit is reported. Then:
469
+
470
+ - **Turn a long chain.** A left-to-right chain of five or more nodes rarely
471
+ fits; `direction="TB"` stacks it.
472
+ - **Shorten a label or a formula.** A node names a step; the explanation goes
473
+ in the prose or the caption. A formula never wraps, so a long one sets its
474
+ box's width.
475
+ - **Narrow a node.** A node's `width` makes its text wrap inside it, and its
476
+ height follows.
477
+
478
+ _Rationale._ A diagram is a picture of how steps connect. One that scrolls
479
+ shows only some of them at a time, and the reader has to remember the rest.
480
+
481
+ _How to apply._ Build the course. Each diagram wider than its limit is a
482
+ `dia-too-wide` warning naming the lesson file and the diagram `id`, printed by
483
+ `astro build` (and by `astro dev` as the page renders) and listed by
484
+ `pnpm content warnings` until the next build. A diagram meant to be read by
485
+ scrolling — a long timeline — says `warnAboveWidth={false}`; a number in
486
+ `warnAboveWidth` sets that one diagram's limit instead of the column.
487
+
488
+ _Enforcement._ Tier-3 warning `dia-too-wide`, for every rendered diagram:
489
+ `Diagram.astro` marks one wider than its limit, `oversizedDiagrams` in
490
+ `src/reference/diagram-fit.ts` finds the mark on each page `astro build`
491
+ writes, and `build/render-diagnostics.json` keeps the findings for
492
+ `content warnings`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "explico",
3
- "version": "0.11.1",
3
+ "version": "0.13.0",
4
4
  "description": "Build-time engine for prerequisite-aware, notation-rigorous course content: one validated content model, a semantic notation/citation/equation system with a completeness gate, and a deterministic JSON manifest.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -36,13 +36,17 @@
36
36
  "dependencies": {
37
37
  "@dagrejs/dagre": "3.1.1",
38
38
  "esbuild": "0.27.7",
39
- "parse5": "7.3.0"
39
+ "mdast-util-gfm-table": "2.0.0",
40
+ "mdast-util-math": "3.0.0",
41
+ "mdast-util-to-markdown": "2.1.2",
42
+ "parse5": "7.3.0",
43
+ "remark-directive": "^4.0.0"
40
44
  },
41
45
  "peerDependencies": {
42
- "@astrojs/markdown-remark": "7.2.4",
43
- "@astrojs/react": "6.0.4",
44
- "@astrojs/starlight": "0.41.9",
45
- "astro": "7.2.7",
46
+ "@astrojs/markdown-remark": "7.3.1",
47
+ "@astrojs/react": "7.0.0",
48
+ "@astrojs/starlight": "0.42.4",
49
+ "astro": "7.3.5",
46
50
  "katex": "0.16.47",
47
51
  "react": "19.2.8",
48
52
  "react-dom": "19.2.8",
@@ -51,7 +55,8 @@
51
55
  "remark-mdx": "3.1.1"
52
56
  },
53
57
  "devDependencies": {
54
- "@astrojs/markdown-remark": "7.2.4",
58
+ "@astrojs/markdown-remark": "7.3.1",
59
+ "@types/mdast": "4.0.4",
55
60
  "@types/node": "24.13.3",
56
61
  "@types/react": "19.2.18",
57
62
  "@types/react-dom": "19.2.5",
@@ -12,7 +12,20 @@ import {
12
12
  writeManifest,
13
13
  } from './manifest';
14
14
  import { missingNotationPayloadKeys } from '../reference/page-notation';
15
+ import { assertFigureSymbolsBound } from '../reference/figure-symbols';
16
+ import { undrawnFigurePictures, undrawnFigureWarning } from '../reference/figure-pictures';
17
+ import {
18
+ pageRenderDiagnostics,
19
+ type RenderDiagnostic,
20
+ writeRenderDiagnostics,
21
+ } from './render-diagnostics';
15
22
  import { writeStandaloneLessonExports } from './exports/standalone-html';
23
+ import { writeCombinedExports } from './exports/combined-html';
24
+ import { writeLatexExports } from './exports/latex-export';
25
+ import { writeMarkdownExports } from './exports/markdown-export';
26
+ import { prepareChapters } from './exports/export-chapters';
27
+ import { writeDownloadIndex } from './exports/download-index';
28
+ import { standaloneFilename, standalonePageSlugs } from './exports/export-pages';
16
29
  import remarkAbbreviations from '../reference/remark-abbreviations.mjs';
17
30
  import remarkCitation from '../reference/remark-citation.mjs';
18
31
  import remarkNotation from '../reference/remark-notation.mjs';
@@ -26,21 +39,21 @@ function normalizedBasePath(base: string): string {
26
39
  return segment ? `/${segment}/` : '/';
27
40
  }
28
41
 
29
- /** Match only a configured lesson artifact below Astro's deployment base. */
42
+ /** Match only a configured page artifact below Astro's deployment base. */
30
43
  export function matchStandaloneDownload(
31
44
  pathname: string,
32
45
  base: string,
33
- lessonSlugs: readonly string[],
46
+ pageSlugs: readonly string[],
34
47
  ): string | undefined {
35
48
  const prefix = `${normalizedBasePath(base)}downloads/`;
36
49
  if (!pathname.startsWith(prefix) || !pathname.endsWith('.html')) return undefined;
37
50
  const slug = pathname.slice(prefix.length, -'.html'.length);
38
- return lessonSlugs.includes(slug) ? slug : undefined;
51
+ return pageSlugs.includes(slug) ? slug : undefined;
39
52
  }
40
53
 
41
54
  async function buildDevelopmentDownloads(
42
55
  config: Pick<AstroConfig, 'root'>,
43
- lessonSlugs: readonly string[],
56
+ pageSlugs: readonly string[],
44
57
  ): Promise<ReadonlyMap<string, Buffer>> {
45
58
  const temporaryRoot = await mkdtemp(join(tmpdir(), 'explico-standalone-'));
46
59
  const outputRoot = join(temporaryRoot, 'dist');
@@ -88,7 +101,7 @@ async function buildDevelopmentDownloads(
88
101
  });
89
102
  return new Map(
90
103
  await Promise.all(
91
- lessonSlugs.map(async (slug) => [
104
+ pageSlugs.map(async (slug) => [
92
105
  slug,
93
106
  await readFile(join(outputRoot, 'downloads', `${slug}.html`)),
94
107
  ] as const),
@@ -99,7 +112,12 @@ async function buildDevelopmentDownloads(
99
112
  }
100
113
  }
101
114
 
102
- /** Fail loudly instead of rendering math triggers whose local cards are stale. */
115
+ /**
116
+ * Fail loudly instead of rendering math triggers whose local cards are
117
+ * missing. The manifest bundles every `notation.local` entry with its page,
118
+ * so the two lists differ only when the manifest is older than the page, or
119
+ * when `validate:content` rejected an entry.
120
+ */
103
121
  export function assertLocalNotationParity(
104
122
  lessonId: string,
105
123
  authoredKeys: readonly string[],
@@ -114,10 +132,13 @@ export function assertLocalNotationParity(
114
132
  return;
115
133
  }
116
134
  throw new Error(
117
- `Stale Explico manifest for ${lessonId}: lesson notation.local has ` +
118
- `[${authored.join(', ')}], but the page notation bundle has ` +
119
- `[${rendered.join(', ')}]. Pass courseConfig to loadManifest() and add ` +
120
- `explicoContentRefresh() to the Astro integrations.`,
135
+ `Explico notation mismatch for ${lessonId}: the page's frontmatter ` +
136
+ `notation.local declares [${authored.join(', ')}], but the manifest's ` +
137
+ `notation bundle for the page holds [${rendered.join(', ')}]. The ` +
138
+ `manifest bundles every notation.local entry with its page, so either ` +
139
+ `it is stale — pass courseConfig to loadManifest() and add ` +
140
+ `explicoContentRefresh() to the Astro integrations — or ` +
141
+ `\`pnpm validate:content\` rejected an entry: fix the errors it reports.`,
121
142
  );
122
143
  }
123
144
 
@@ -220,19 +241,35 @@ export function assertRemarkPluginsWired(
220
241
  );
221
242
  }
222
243
 
223
- async function validateGeneratedNotationPayloads(directory: URL): Promise<void> {
244
+ /**
245
+ * Check every built page, and return the findings only a rendered page gives
246
+ * (`render-diagnostics.ts`) for `content warnings`.
247
+ */
248
+ async function validateGeneratedPages(
249
+ directory: URL,
250
+ manifest: Manifest,
251
+ warn: (message: string) => void,
252
+ ): Promise<RenderDiagnostic[]> {
224
253
  const root = fileURLToPath(directory);
254
+ const rendered: RenderDiagnostic[] = [];
225
255
  const walk = async (path: string): Promise<void> => {
226
256
  for (const entry of await readdir(path, { withFileTypes: true })) {
227
257
  const child = join(path, entry.name);
228
258
  if (entry.isDirectory()) {
229
259
  await walk(child);
230
260
  } else if (entry.isFile() && entry.name.endsWith('.html')) {
231
- assertNotationPayloadHtml(relative(root, child), await readFile(child, 'utf8'));
261
+ const html = await readFile(child, 'utf8');
262
+ const page = relative(root, child);
263
+ assertNotationPayloadHtml(page, html);
264
+ assertFigureSymbolsBound(page, html);
265
+ const undrawn = undrawnFigurePictures(html);
266
+ if (undrawn.length > 0) warn(undrawnFigureWarning(page, undrawn));
267
+ rendered.push(...pageRenderDiagnostics(manifest, page, html));
232
268
  }
233
269
  }
234
270
  };
235
271
  await walk(root);
272
+ return rendered;
236
273
  }
237
274
 
238
275
  function replaceInPlace(target: unknown, source: unknown): void {
@@ -370,19 +407,19 @@ export function explicoContentRefresh(
370
407
  const slug = matchStandaloneDownload(
371
408
  pathname,
372
409
  configuredDevelopment.base,
373
- manifest.lessons.map((lesson) => lesson.slug),
410
+ standalonePageSlugs(manifest),
374
411
  );
375
412
  if (!slug) return next();
376
413
 
377
414
  try {
378
415
  logger.info(
379
416
  developmentDownloads
380
- ? `Serving cached standalone lesson ${slug}.html`
381
- : 'Building standalone lesson downloads for local development…',
417
+ ? `Serving cached standalone page ${slug}.html`
418
+ : 'Building standalone page downloads for local development…',
382
419
  );
383
420
  developmentDownloads ??= buildDevelopmentDownloads(
384
421
  configuredDevelopment,
385
- manifest.lessons.map((lesson) => lesson.slug),
422
+ standalonePageSlugs(manifest),
386
423
  );
387
424
  const currentDownloads = developmentDownloads;
388
425
  currentDownloads.catch(() => {
@@ -392,7 +429,7 @@ export function explicoContentRefresh(
392
429
  });
393
430
  const artifact = (await currentDownloads).get(slug);
394
431
  if (!artifact) return next();
395
- const filename = `${slug.split('/').at(-1) ?? 'lesson'}.html`;
432
+ const filename = standaloneFilename(slug);
396
433
  response.statusCode = 200;
397
434
  response.setHeader('Content-Type', 'text/html; charset=utf-8');
398
435
  response.setHeader('Content-Length', artifact.byteLength);
@@ -404,17 +441,31 @@ export function explicoContentRefresh(
404
441
  response.end(request.method === 'HEAD' ? undefined : artifact);
405
442
  } catch (error) {
406
443
  const message = error instanceof Error ? error.message : String(error);
407
- logger.error(`Could not build standalone lesson downloads: ${message}`);
444
+ logger.error(`Could not build standalone page downloads: ${message}`);
408
445
  response.statusCode = 500;
409
446
  response.setHeader('Content-Type', 'text/plain; charset=utf-8');
410
- response.end(`Could not build standalone lesson download.\n${message}\n`);
447
+ response.end(`Could not build standalone page download.\n${message}\n`);
411
448
  }
412
449
  });
413
450
  }
414
451
  },
415
- 'astro:build:done': async ({ dir }) => {
416
- await validateGeneratedNotationPayloads(dir);
452
+ 'astro:build:done': async ({ dir, logger }) => {
453
+ const rendered = await validateGeneratedPages(dir, manifest, (message) =>
454
+ logger.warn(message),
455
+ );
456
+ for (const diagnostic of writeRenderDiagnostics(rendered)) {
457
+ logger.warn(`[${diagnostic.code}] ${diagnostic.file}: ${diagnostic.message}`);
458
+ }
417
459
  await writeStandaloneLessonExports(dir, manifest);
460
+ await writeCombinedExports(dir, manifest);
461
+ // The print exports share one reading of the chapters and one
462
+ // capture of their figures.
463
+ const { latex, markdown } = manifest.lessonExports;
464
+ const prepared =
465
+ latex || markdown ? await prepareChapters(fileURLToPath(dir), manifest, { logger }) : undefined;
466
+ await writeLatexExports(dir, manifest, { logger, ...(prepared ? { prepared } : {}) });
467
+ await writeMarkdownExports(dir, manifest, { logger, ...(prepared ? { prepared } : {}) });
468
+ await writeDownloadIndex(dir);
418
469
  },
419
470
  },
420
471
  };