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
package/README.md CHANGED
@@ -22,7 +22,7 @@ What it gives a course:
22
22
  per glyph per page, enforced corpus-wide. Named operators get the same
23
23
  treatment one tier softer: a lesson's `\operatorname{Var}` is a warning
24
24
  (`operator-explained`) until it carries an entry or the course lists it in
25
- `operators.allow`; a shipped baseline (`\exp`, `\int`, trig, …) is exempt.
25
+ `rules.N8.allow`; a shipped baseline (`\exp`, `\int`, trig, …) is exempt.
26
26
  - **Inline citations** — `[@source-id; locator]` → numbered marker + generated
27
27
  reference list, resolved against a schema-checked source collection.
28
28
  - **Equation identity** — `\label{eq:key}` gives a display equation a stable
@@ -187,8 +187,8 @@ or, without one, the numbers and short captions of the floats inside
187
187
 
188
188
  ### Lesson exports
189
189
 
190
- A course can opt into a self-contained HTML download and browser-native printing
191
- for each lesson:
190
+ A course can opt into self-contained HTML downloads of each chapter, each part
191
+ and the whole course, and browser-native printing of every page:
192
192
 
193
193
  ```ts
194
194
  export const courseConfig: CourseConfig = {
@@ -202,18 +202,36 @@ export const courseConfig: CourseConfig = {
202
202
  ```
203
203
 
204
204
  The existing `explicoContentRefresh(courseConfig, manifest)` integration emits
205
- `downloads/<lesson-slug>.html` after Astro's static build and adds a download
206
- control to the top-right header cluster on each lesson. During `astro dev`, the
207
- first download request runs an isolated static build and streams the resulting
208
- file; later requests use that result until a watched source file changes. The
209
- downloaded file contains its CSS,
205
+ `downloads/<docs-slug>.html` for each chapter after Astro's static build, and
206
+ adds a Download button to the top-right header cluster on each page. It opens a
207
+ menu of the scopes that contain the page — "This chapter" on a chapter, "This
208
+ part" on a chapter or its part's page, "Whole course" everywhere — with their
209
+ sizes, read from `downloads/index.json`, which the build writes last. A page
210
+ that is not a chapter (the landing page, a part's page, the glossary) has no
211
+ file of its own: it is in its part's file and the course's. The Print button
212
+ sits beside the menu, and Ctrl+P / Cmd+P run the same print preparation.
213
+ During `astro dev`, the first request for a chapter's file runs an isolated
214
+ static build and streams the resulting file; later requests use that result
215
+ until a watched source file changes. The downloaded file contains its CSS,
210
216
  KaTeX fonts, images, client scripts, Astro island components, and renderer; it
211
217
  opens directly through `file://` without a server or network access. Search,
212
218
  sidebars, pagination, and other site chrome are omitted. Same-page references
213
- stay local, while links that leave the lesson use `canonicalBaseUrl` and
219
+ stay local, while links that leave the page use `canonicalBaseUrl` and
214
220
  cross-lesson hover previews degrade to ordinary links. A retained theme control
215
221
  lets an offline reader choose Light, Dark, or Auto; Auto follows changes to the
216
- viewer’s system color scheme.
222
+ viewer’s system color scheme. Ctrl+P and Cmd+P load the file's labs before the
223
+ print dialog, as on the site.
224
+
225
+ Each part and the whole course also download as one offline file:
226
+ `downloads/part/<dir>.html` is the part's page and then its chapters, and
227
+ `downloads/course.html` is the landing page, the curriculum map, the figures and
228
+ tables, every part, and the glossary last, each opening with a list of
229
+ contents. They are assembled from the pages the build wrote, so each page keeps
230
+ its own cards, labs and checks. Every id gains the page as a suffix
231
+ (`eq-range--projectiles-projectile-range`), and a reference to another page in
232
+ the same file becomes a link into it, whose hover preview works offline. Each
233
+ page is a section with `content-visibility: auto`, which keeps a long course
234
+ responsive, and starts a new sheet when printed.
217
235
 
218
236
  The **Print / save as PDF** action calls the browser's native print dialog; it
219
237
  does not download or build a `.pdf` file itself. The print layout removes site
@@ -227,12 +245,72 @@ visual result, and keeps linked text clickable with canonical destinations.
227
245
  Paper size, margins, browser-added headers/footers, final filename, and the
228
246
  choice to save as PDF remain browser and reader settings.
229
247
 
230
- Set `canonicalBaseUrl` to the deployed course root whenever retained lesson
231
- content links elsewhere in the course. If such a link exists without the base,
232
- the build fails rather than shipping a dead local link. An unresolved or remote
248
+ Set `canonicalBaseUrl` to the deployed course root whenever retained page
249
+ content links elsewhere in the course. With `standaloneHtml` on that is
250
+ practically always: a chapter links to the glossary and to other chapters. If such a link exists without the base, the build fails rather than
251
+ shipping a dead local link. An unresolved or remote
233
252
  stylesheet, script, font, image, iframe, media source, worker, WASM file, or CSS
234
253
  URL likewise fails the standalone export audit.
235
254
 
255
+ #### Markdown
256
+
257
+ `markdown: true` writes each chapter, each part and the whole course as
258
+ Markdown after Astro's static build: `downloads/<slug>-markdown.zip`
259
+ (`lesson.md` and its `assets/`), `downloads/part/<dir>-markdown.zip` and
260
+ `downloads/course-markdown.zip` (an `index.md`, one file per chapter, shared
261
+ `assets/`, relative links). It is GitHub's flavour with `$…$` math, so GitHub,
262
+ Obsidian or remark-math with KaTeX shows it as written. Headings, equations and
263
+ floats keep their numbers; each chapter ends with its references, its notation
264
+ and the answers to its checks. A figure the browser draws is a PNG at 2× (it
265
+ needs Playwright with Chromium, as LaTeX does); a `<Diagram>` is a PNG with its
266
+ boxes and arrows listed as text.
267
+
268
+ #### LaTeX source and PDF
269
+
270
+ `latex: true` adds two downloads per chapter, both built after Astro's static
271
+ build:
272
+
273
+ - `downloads/<slug>-latex.zip` — the chapter's LaTeX folder: `main.tex`,
274
+ explico's book class `explico.cls`, the chapter as readable source, its
275
+ figures, a `latexmkrc` and a README. Unzip it and run `latexmk`.
276
+ - `downloads/<slug>.pdf` — that same folder compiled: a tagged PDF 2.0
277
+ (PDF/UA-2) with the chapter's answer key at its end.
278
+ - `downloads/part/<dir>-latex.zip` and `.pdf`, `downloads/course-latex.zip`
279
+ and `.pdf` — each part and the whole course as a book: title page,
280
+ contents, lists of figures, tables and diagrams, each part opening with its
281
+ page's text, its chapters, and notation and abbreviation appendices.
282
+
283
+ Every document compiles at once, up to one per core.
284
+
285
+ Numbers are LaTeX's own (`\label`, `\ref`), set up to count the way the website
286
+ does; the build compiles the source unchanged and fails if any number differs
287
+ from the website's. Figures stay vector: a lab is captured from the finished
288
+ site by Chromium, and a `<Diagram>` is drawn as TikZ from its build-time
289
+ layout. `latexPaper` is `letter` (default) or `a4`.
290
+
291
+ Prose in another script reaches the PDF too. A Greek letter on its own is a
292
+ symbol and is set as math; a Greek word, Cyrillic, Chinese, Japanese, Korean
293
+ and emoji come from fallback fonts under LuaLaTeX, the first the build machine
294
+ has of CMU Serif (TeX Live's `cm-unicode`), Noto Serif or DejaVu Serif for
295
+ Greek and Cyrillic, Noto CJK or Fandol, and Noto Color Emoji. A character no
296
+ font sets stays blank, and the build warns with its name; under pdfLaTeX, which
297
+ has none of these fonts, every such character does.
298
+
299
+ The build needs TeX Live 2025 or later with `latexmk` and LuaLaTeX
300
+ (`EXPLICO_LATEX_ENGINE=pdflatex` switches to pdfLaTeX), and Playwright with
301
+ Chromium in the course for figures a browser draws. In CI (`CI` set) a missing
302
+ engine or browser fails the build; on a machine without them the build warns
303
+ and still writes the source zips. On GitHub Actions:
304
+
305
+ ```yaml
306
+ - uses: zauguin/install-texlive@v4
307
+ with:
308
+ texlive_version: 2026
309
+ package_file: node_modules/explico/src/compiler/exports/latex/texlive-packages.txt
310
+ - run: pnpm exec playwright install --with-deps chromium
311
+ - run: pnpm build
312
+ ```
313
+
236
314
  ```jsonc
237
315
  // package.json — the read-side CLI
238
316
  "scripts": { "validate:content": "tsx scripts/compile-manifest.ts" }
@@ -1,5 +1,5 @@
1
1
  import { AstroConfig, AstroIntegration } from 'astro';
2
- import { C as CourseConfig } from '../course-config-Dsnri9Tk.js';
2
+ import { C as CourseConfig } from '../course-config-rRtME5UV.js';
3
3
  import { Manifest } from './manifest.js';
4
4
  import '../rules/catalog.js';
5
5
  import '../rules/course-rules.js';
@@ -7,6 +7,7 @@ import '../curriculum/validation.js';
7
7
  import '../reference/consistency.js';
8
8
  import './collections.js';
9
9
  import '../reference/types.js';
10
+ import '../reference/gate-retired-keys.js';
10
11
  import '../reference/lint-ignore.js';
11
12
  import '../reference/equations-validate.js';
12
13
  import '../reference/tables-validate.js';
@@ -17,6 +18,7 @@ import '../reference/examples-validate.js';
17
18
  import '../reference/folds-validate.js';
18
19
  import '../reference/knowledge-checks-validate.js';
19
20
  import '../reference/raw-floats-validate.js';
21
+ import '../reference/lab-katex-validate.js';
20
22
  import '../reference/gate-math.js';
21
23
  import '../reference/mixed-content.js';
22
24
  import '../reference/gloss.js';
@@ -24,9 +26,14 @@ import '../rules/waivers.js';
24
26
  import './knowledge-version.js';
25
27
  import './lesson-derivation.js';
26
28
 
27
- /** Match only a configured lesson artifact below Astro's deployment base. */
28
- declare function matchStandaloneDownload(pathname: string, base: string, lessonSlugs: readonly string[]): string | undefined;
29
- /** Fail loudly instead of rendering math triggers whose local cards are stale. */
29
+ /** Match only a configured page artifact below Astro's deployment base. */
30
+ declare function matchStandaloneDownload(pathname: string, base: string, pageSlugs: readonly string[]): string | undefined;
31
+ /**
32
+ * Fail loudly instead of rendering math triggers whose local cards are
33
+ * missing. The manifest bundles every `notation.local` entry with its page,
34
+ * so the two lists differ only when the manifest is older than the page, or
35
+ * when `validate:content` rejected an entry.
36
+ */
30
37
  declare function assertLocalNotationParity(lessonId: string, authoredKeys: readonly string[], renderedKeys: readonly string[]): void;
31
38
  /** Fail an Astro build if final rendered HTML contains an unusable trigger. */
32
39
  declare function assertNotationPayloadHtml(page: string, html: string): void;
@@ -8,7 +8,19 @@ import {
8
8
  writeManifest
9
9
  } from "./manifest.js";
10
10
  import { missingNotationPayloadKeys } from "../reference/page-notation.js";
11
+ import { assertFigureSymbolsBound } from "../reference/figure-symbols.js";
12
+ import { undrawnFigurePictures, undrawnFigureWarning } from "../reference/figure-pictures.js";
13
+ import {
14
+ pageRenderDiagnostics,
15
+ writeRenderDiagnostics
16
+ } from "./render-diagnostics.js";
11
17
  import { writeStandaloneLessonExports } from "./exports/standalone-html.js";
18
+ import { writeCombinedExports } from "./exports/combined-html.js";
19
+ import { writeLatexExports } from "./exports/latex-export.js";
20
+ import { writeMarkdownExports } from "./exports/markdown-export.js";
21
+ import { prepareChapters } from "./exports/export-chapters.js";
22
+ import { writeDownloadIndex } from "./exports/download-index.js";
23
+ import { standaloneFilename, standalonePageSlugs } from "./exports/export-pages.js";
12
24
  import remarkAbbreviations from "../reference/remark-abbreviations.mjs";
13
25
  import remarkCitation from "../reference/remark-citation.mjs";
14
26
  import remarkNotation from "../reference/remark-notation.mjs";
@@ -17,13 +29,13 @@ function normalizedBasePath(base) {
17
29
  const segment = base.replace(/^\/+|\/+$/gu, "");
18
30
  return segment ? `/${segment}/` : "/";
19
31
  }
20
- function matchStandaloneDownload(pathname, base, lessonSlugs) {
32
+ function matchStandaloneDownload(pathname, base, pageSlugs) {
21
33
  const prefix = `${normalizedBasePath(base)}downloads/`;
22
34
  if (!pathname.startsWith(prefix) || !pathname.endsWith(".html")) return void 0;
23
35
  const slug = pathname.slice(prefix.length, -".html".length);
24
- return lessonSlugs.includes(slug) ? slug : void 0;
36
+ return pageSlugs.includes(slug) ? slug : void 0;
25
37
  }
26
- async function buildDevelopmentDownloads(config, lessonSlugs) {
38
+ async function buildDevelopmentDownloads(config, pageSlugs) {
27
39
  const temporaryRoot = await mkdtemp(join(tmpdir(), "explico-standalone-"));
28
40
  const outputRoot = join(temporaryRoot, "dist");
29
41
  try {
@@ -70,7 +82,7 @@ ${diagnostic.trim()}` : "")
70
82
  });
71
83
  return new Map(
72
84
  await Promise.all(
73
- lessonSlugs.map(async (slug) => [
85
+ pageSlugs.map(async (slug) => [
74
86
  slug,
75
87
  await readFile(join(outputRoot, "downloads", `${slug}.html`))
76
88
  ])
@@ -87,7 +99,7 @@ function assertLocalNotationParity(lessonId, authoredKeys, renderedKeys) {
87
99
  return;
88
100
  }
89
101
  throw new Error(
90
- `Stale Explico manifest for ${lessonId}: lesson notation.local has [${authored.join(", ")}], but the page notation bundle has [${rendered.join(", ")}]. Pass courseConfig to loadManifest() and add explicoContentRefresh() to the Astro integrations.`
102
+ `Explico notation mismatch for ${lessonId}: the page's frontmatter notation.local declares [${authored.join(", ")}], but the manifest's notation bundle for the page holds [${rendered.join(", ")}]. The manifest bundles every notation.local entry with its page, so either it is stale \u2014 pass courseConfig to loadManifest() and add explicoContentRefresh() to the Astro integrations \u2014 or \`pnpm validate:content\` rejected an entry: fix the errors it reports.`
91
103
  );
92
104
  }
93
105
  function assertNotationPayloadHtml(page, html) {
@@ -131,19 +143,27 @@ function assertRemarkPluginsWired(manifest, markdown) {
131
143
  ${missing.map((line) => ` - ${line}`).join("\n")}`
132
144
  );
133
145
  }
134
- async function validateGeneratedNotationPayloads(directory) {
146
+ async function validateGeneratedPages(directory, manifest, warn) {
135
147
  const root = fileURLToPath(directory);
148
+ const rendered = [];
136
149
  const walk = async (path) => {
137
150
  for (const entry of await readdir(path, { withFileTypes: true })) {
138
151
  const child = join(path, entry.name);
139
152
  if (entry.isDirectory()) {
140
153
  await walk(child);
141
154
  } else if (entry.isFile() && entry.name.endsWith(".html")) {
142
- assertNotationPayloadHtml(relative(root, child), await readFile(child, "utf8"));
155
+ const html = await readFile(child, "utf8");
156
+ const page = relative(root, child);
157
+ assertNotationPayloadHtml(page, html);
158
+ assertFigureSymbolsBound(page, html);
159
+ const undrawn = undrawnFigurePictures(html);
160
+ if (undrawn.length > 0) warn(undrawnFigureWarning(page, undrawn));
161
+ rendered.push(...pageRenderDiagnostics(manifest, page, html));
143
162
  }
144
163
  }
145
164
  };
146
165
  await walk(root);
166
+ return rendered;
147
167
  }
148
168
  function replaceInPlace(target, source) {
149
169
  if (Array.isArray(target) && Array.isArray(source)) {
@@ -240,16 +260,16 @@ function explicoContentRefresh(courseConfig, manifest) {
240
260
  const slug = matchStandaloneDownload(
241
261
  pathname,
242
262
  configuredDevelopment.base,
243
- manifest.lessons.map((lesson) => lesson.slug)
263
+ standalonePageSlugs(manifest)
244
264
  );
245
265
  if (!slug) return next();
246
266
  try {
247
267
  logger.info(
248
- developmentDownloads ? `Serving cached standalone lesson ${slug}.html` : "Building standalone lesson downloads for local development\u2026"
268
+ developmentDownloads ? `Serving cached standalone page ${slug}.html` : "Building standalone page downloads for local development\u2026"
249
269
  );
250
270
  developmentDownloads ??= buildDevelopmentDownloads(
251
271
  configuredDevelopment,
252
- manifest.lessons.map((lesson) => lesson.slug)
272
+ standalonePageSlugs(manifest)
253
273
  );
254
274
  const currentDownloads = developmentDownloads;
255
275
  currentDownloads.catch(() => {
@@ -259,7 +279,7 @@ function explicoContentRefresh(courseConfig, manifest) {
259
279
  });
260
280
  const artifact = (await currentDownloads).get(slug);
261
281
  if (!artifact) return next();
262
- const filename = `${slug.split("/").at(-1) ?? "lesson"}.html`;
282
+ const filename = standaloneFilename(slug);
263
283
  response.statusCode = 200;
264
284
  response.setHeader("Content-Type", "text/html; charset=utf-8");
265
285
  response.setHeader("Content-Length", artifact.byteLength);
@@ -271,19 +291,32 @@ function explicoContentRefresh(courseConfig, manifest) {
271
291
  response.end(request.method === "HEAD" ? void 0 : artifact);
272
292
  } catch (error) {
273
293
  const message = error instanceof Error ? error.message : String(error);
274
- logger.error(`Could not build standalone lesson downloads: ${message}`);
294
+ logger.error(`Could not build standalone page downloads: ${message}`);
275
295
  response.statusCode = 500;
276
296
  response.setHeader("Content-Type", "text/plain; charset=utf-8");
277
- response.end(`Could not build standalone lesson download.
297
+ response.end(`Could not build standalone page download.
278
298
  ${message}
279
299
  `);
280
300
  }
281
301
  });
282
302
  }
283
303
  },
284
- "astro:build:done": async ({ dir }) => {
285
- await validateGeneratedNotationPayloads(dir);
304
+ "astro:build:done": async ({ dir, logger }) => {
305
+ const rendered = await validateGeneratedPages(
306
+ dir,
307
+ manifest,
308
+ (message) => logger.warn(message)
309
+ );
310
+ for (const diagnostic of writeRenderDiagnostics(rendered)) {
311
+ logger.warn(`[${diagnostic.code}] ${diagnostic.file}: ${diagnostic.message}`);
312
+ }
286
313
  await writeStandaloneLessonExports(dir, manifest);
314
+ await writeCombinedExports(dir, manifest);
315
+ const { latex, markdown } = manifest.lessonExports;
316
+ const prepared = latex || markdown ? await prepareChapters(fileURLToPath(dir), manifest, { logger }) : void 0;
317
+ await writeLatexExports(dir, manifest, { logger, ...prepared ? { prepared } : {} });
318
+ await writeMarkdownExports(dir, manifest, { logger, ...prepared ? { prepared } : {} });
319
+ await writeDownloadIndex(dir);
287
320
  }
288
321
  }
289
322
  };
@@ -1,13 +1,15 @@
1
1
  import { DiagnosticSeverity } from '../reference/types.js';
2
2
  import { EquationLink } from '../reference/gate-math.js';
3
3
  import { Manifest, ManifestLesson } from './manifest.js';
4
+ import { RenderDiagnostic } from './render-diagnostics.js';
4
5
  import { RulesFilter, RuleInEffect } from '../rules/in-effect.js';
5
6
  import '../curriculum/validation.js';
6
7
  import '../reference/consistency.js';
7
- import '../course-config-Dsnri9Tk.js';
8
+ import '../course-config-rRtME5UV.js';
8
9
  import '../rules/catalog.js';
9
10
  import '../rules/course-rules.js';
10
11
  import './collections.js';
12
+ import '../reference/gate-retired-keys.js';
11
13
  import '../reference/lint-ignore.js';
12
14
  import '../reference/equations-validate.js';
13
15
  import '../reference/tables-validate.js';
@@ -18,6 +20,7 @@ import '../reference/examples-validate.js';
18
20
  import '../reference/folds-validate.js';
19
21
  import '../reference/knowledge-checks-validate.js';
20
22
  import '../reference/raw-floats-validate.js';
23
+ import '../reference/lab-katex-validate.js';
21
24
  import '../reference/mixed-content.js';
22
25
  import '../reference/gloss.js';
23
26
  import '../rules/waivers.js';
@@ -51,6 +54,9 @@ import './lesson-derivation.js';
51
54
  * on any renderability error (unparseable MDX, KaTeX error, unresolved
52
55
  * notation, unknown `[[key]]`, unknown citation), on top of the manifest's
53
56
  * schema / ref / math / equation diagnostics for that lesson.
57
+ * - Beside the manifest's diagnostics, every command lists what the last
58
+ * `astro build` found on the rendered pages (`render-diagnostics.ts`),
59
+ * under the `render:` source.
54
60
  */
55
61
 
56
62
  type ContentCliCode = 'unknown-command' | 'usage' | 'unknown-lesson' | 'unknown-kind' | 'invalid-id' | 'exists' | 'not-a-lesson' | 'no-git';
@@ -87,7 +93,7 @@ interface CliDiagnostic {
87
93
  * when their message mentions the lesson; per-file checks (notation, math,
88
94
  * equations, consistency) are filtered by `file`.
89
95
  */
90
- declare function lessonDiagnostics(manifest: Manifest, lesson: ManifestLesson): CliDiagnostic[];
96
+ declare function lessonDiagnostics(manifest: Manifest, lesson: ManifestLesson, rendered?: readonly RenderDiagnostic[]): CliDiagnostic[];
91
97
  interface RenderResult {
92
98
  readonly rendered: boolean;
93
99
  readonly diagnostics: readonly CliDiagnostic[];
@@ -117,6 +123,15 @@ interface TokenCounts {
117
123
  /** Normalize `foundations/discount-factors` or the dotted id to the dotted id. */
118
124
  declare function normalizeLessonId(raw: string): string;
119
125
  declare function findLesson(manifest: Manifest, rawId: string): ManifestLesson;
126
+ /**
127
+ * A lesson file's frontmatter and its body for rendering: the frontmatter is
128
+ * blanked with spaces, as Astro's MDX plugin does, so line N of the body is
129
+ * line N of the file and a render error points where the build's would.
130
+ */
131
+ declare function lessonSource(text: string): {
132
+ frontmatter: Record<string, unknown>;
133
+ body: string;
134
+ };
120
135
  interface DomainFileSignatures {
121
136
  readonly file: string;
122
137
  readonly signatures: readonly string[];
@@ -137,10 +152,6 @@ interface StatusReport {
137
152
  readonly keyedDiagrams: number;
138
153
  readonly keyedExamples: number;
139
154
  };
140
- readonly draftDebt: {
141
- readonly lessons: readonly string[];
142
- readonly notation: readonly string[];
143
- };
144
155
  readonly orphans: readonly CliDiagnostic[];
145
156
  readonly diagnostics: {
146
157
  readonly errors: number;
@@ -148,7 +159,7 @@ interface StatusReport {
148
159
  readonly infos: number;
149
160
  };
150
161
  }
151
- declare function contentStatus(manifest: Manifest): StatusReport;
162
+ declare function contentStatus(manifest: Manifest, rendered?: readonly RenderDiagnostic[]): StatusReport;
152
163
  type DiagnosticThreshold = DiagnosticSeverity;
153
164
  interface DiagnosticsReport {
154
165
  readonly minimumSeverity: DiagnosticThreshold;
@@ -159,10 +170,13 @@ interface WarningsReport {
159
170
  readonly count: number;
160
171
  readonly warnings: readonly CliDiagnostic[];
161
172
  }
162
- /** Manifest diagnostics at or above a severity, normalized for tooling. */
163
- declare function contentDiagnostics(manifest: Manifest, minimumSeverity?: DiagnosticThreshold): DiagnosticsReport;
173
+ /**
174
+ * Manifest diagnostics at or above a severity, and those the last
175
+ * `astro build` found on rendered pages, normalized for tooling.
176
+ */
177
+ declare function contentDiagnostics(manifest: Manifest, minimumSeverity?: DiagnosticThreshold, rendered?: readonly RenderDiagnostic[]): DiagnosticsReport;
164
178
  /** Every warning exactly; errors and informational advice are excluded. */
165
- declare function contentWarnings(manifest: Manifest): WarningsReport;
179
+ declare function contentWarnings(manifest: Manifest, rendered?: readonly RenderDiagnostic[]): WarningsReport;
166
180
  interface RulesReport {
167
181
  readonly filter: RulesFilter;
168
182
  readonly count: number;
@@ -187,7 +201,6 @@ interface ContextReport {
187
201
  readonly file: string;
188
202
  readonly title: string;
189
203
  readonly description: string;
190
- readonly status: string;
191
204
  readonly teaches: readonly string[];
192
205
  readonly requires: readonly string[];
193
206
  readonly assumptions: readonly string[];
@@ -271,6 +284,8 @@ interface CheckReport {
271
284
  declare function changedLessonIds(): string[];
272
285
  declare function contentCheck(manifest: Manifest, options: {
273
286
  lesson?: string;
287
+ /** Several lessons, each checked; `lesson` is the one-lesson form. */
288
+ lessons?: readonly string[];
274
289
  changed?: boolean;
275
290
  }): Promise<CheckReport>;
276
291
  interface Scaffold {
@@ -286,4 +301,4 @@ interface Scaffold {
286
301
  declare function scaffoldLesson(rawId: string): Scaffold;
287
302
  declare function scaffoldTerm(rawKey: string): Scaffold;
288
303
 
289
- export { type CheckLessonResult, type CheckReport, type CliDiagnostic, type ContentCliCode, ContentCliError, type ContextReport, type DiagnosticThreshold, type DiagnosticsReport, type DomainFileSignatures, type RenderResult, type RulesReport, type Scaffold, type StatusReport, type TokenCounts, type WarningsReport, changedLessonIds, contentCheck, contentContext, contentDiagnostics, contentRules, contentStatus, contentWarnings, estimateTokens, findLesson, lessonDiagnostics, normalizeLessonId, parseRulesFilter, renderLesson, scaffoldLesson, scaffoldTerm };
304
+ export { type CheckLessonResult, type CheckReport, type CliDiagnostic, type ContentCliCode, ContentCliError, type ContextReport, type DiagnosticThreshold, type DiagnosticsReport, type DomainFileSignatures, type RenderResult, type RulesReport, type Scaffold, type StatusReport, type TokenCounts, type WarningsReport, changedLessonIds, contentCheck, contentContext, contentDiagnostics, contentRules, contentStatus, contentWarnings, estimateTokens, findLesson, lessonDiagnostics, lessonSource, normalizeLessonId, parseRulesFilter, renderLesson, scaffoldLesson, scaffoldTerm };