explico 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (279) hide show
  1. package/CHANGELOG.md +1137 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +14 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +2 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +707 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +321 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
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';
@@ -25,8 +26,8 @@ import '../rules/waivers.js';
25
26
  import './knowledge-version.js';
26
27
  import './lesson-derivation.js';
27
28
 
28
- /** Match only a configured lesson artifact below Astro's deployment base. */
29
- declare function matchStandaloneDownload(pathname: string, base: string, lessonSlugs: readonly string[]): string | undefined;
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;
30
31
  /**
31
32
  * Fail loudly instead of rendering math triggers whose local cards are
32
33
  * missing. The manifest bundles every `notation.local` entry with its page,
@@ -9,7 +9,18 @@ import {
9
9
  } from "./manifest.js";
10
10
  import { missingNotationPayloadKeys } from "../reference/page-notation.js";
11
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";
12
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";
13
24
  import remarkAbbreviations from "../reference/remark-abbreviations.mjs";
14
25
  import remarkCitation from "../reference/remark-citation.mjs";
15
26
  import remarkNotation from "../reference/remark-notation.mjs";
@@ -18,13 +29,13 @@ function normalizedBasePath(base) {
18
29
  const segment = base.replace(/^\/+|\/+$/gu, "");
19
30
  return segment ? `/${segment}/` : "/";
20
31
  }
21
- function matchStandaloneDownload(pathname, base, lessonSlugs) {
32
+ function matchStandaloneDownload(pathname, base, pageSlugs) {
22
33
  const prefix = `${normalizedBasePath(base)}downloads/`;
23
34
  if (!pathname.startsWith(prefix) || !pathname.endsWith(".html")) return void 0;
24
35
  const slug = pathname.slice(prefix.length, -".html".length);
25
- return lessonSlugs.includes(slug) ? slug : void 0;
36
+ return pageSlugs.includes(slug) ? slug : void 0;
26
37
  }
27
- async function buildDevelopmentDownloads(config, lessonSlugs) {
38
+ async function buildDevelopmentDownloads(config, pageSlugs) {
28
39
  const temporaryRoot = await mkdtemp(join(tmpdir(), "explico-standalone-"));
29
40
  const outputRoot = join(temporaryRoot, "dist");
30
41
  try {
@@ -71,7 +82,7 @@ ${diagnostic.trim()}` : "")
71
82
  });
72
83
  return new Map(
73
84
  await Promise.all(
74
- lessonSlugs.map(async (slug) => [
85
+ pageSlugs.map(async (slug) => [
75
86
  slug,
76
87
  await readFile(join(outputRoot, "downloads", `${slug}.html`))
77
88
  ])
@@ -132,8 +143,9 @@ function assertRemarkPluginsWired(manifest, markdown) {
132
143
  ${missing.map((line) => ` - ${line}`).join("\n")}`
133
144
  );
134
145
  }
135
- async function validateGeneratedNotationPayloads(directory) {
146
+ async function validateGeneratedPages(directory, manifest, warn) {
136
147
  const root = fileURLToPath(directory);
148
+ const rendered = [];
137
149
  const walk = async (path) => {
138
150
  for (const entry of await readdir(path, { withFileTypes: true })) {
139
151
  const child = join(path, entry.name);
@@ -141,12 +153,17 @@ async function validateGeneratedNotationPayloads(directory) {
141
153
  await walk(child);
142
154
  } else if (entry.isFile() && entry.name.endsWith(".html")) {
143
155
  const html = await readFile(child, "utf8");
144
- assertNotationPayloadHtml(relative(root, child), html);
145
- assertFigureSymbolsBound(relative(root, child), html);
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));
146
162
  }
147
163
  }
148
164
  };
149
165
  await walk(root);
166
+ return rendered;
150
167
  }
151
168
  function replaceInPlace(target, source) {
152
169
  if (Array.isArray(target) && Array.isArray(source)) {
@@ -243,16 +260,16 @@ function explicoContentRefresh(courseConfig, manifest) {
243
260
  const slug = matchStandaloneDownload(
244
261
  pathname,
245
262
  configuredDevelopment.base,
246
- manifest.lessons.map((lesson) => lesson.slug)
263
+ standalonePageSlugs(manifest)
247
264
  );
248
265
  if (!slug) return next();
249
266
  try {
250
267
  logger.info(
251
- 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"
252
269
  );
253
270
  developmentDownloads ??= buildDevelopmentDownloads(
254
271
  configuredDevelopment,
255
- manifest.lessons.map((lesson) => lesson.slug)
272
+ standalonePageSlugs(manifest)
256
273
  );
257
274
  const currentDownloads = developmentDownloads;
258
275
  currentDownloads.catch(() => {
@@ -262,7 +279,7 @@ function explicoContentRefresh(courseConfig, manifest) {
262
279
  });
263
280
  const artifact = (await currentDownloads).get(slug);
264
281
  if (!artifact) return next();
265
- const filename = `${slug.split("/").at(-1) ?? "lesson"}.html`;
282
+ const filename = standaloneFilename(slug);
266
283
  response.statusCode = 200;
267
284
  response.setHeader("Content-Type", "text/html; charset=utf-8");
268
285
  response.setHeader("Content-Length", artifact.byteLength);
@@ -274,19 +291,32 @@ function explicoContentRefresh(courseConfig, manifest) {
274
291
  response.end(request.method === "HEAD" ? void 0 : artifact);
275
292
  } catch (error) {
276
293
  const message = error instanceof Error ? error.message : String(error);
277
- logger.error(`Could not build standalone lesson downloads: ${message}`);
294
+ logger.error(`Could not build standalone page downloads: ${message}`);
278
295
  response.statusCode = 500;
279
296
  response.setHeader("Content-Type", "text/plain; charset=utf-8");
280
- response.end(`Could not build standalone lesson download.
297
+ response.end(`Could not build standalone page download.
281
298
  ${message}
282
299
  `);
283
300
  }
284
301
  });
285
302
  }
286
303
  },
287
- "astro:build:done": async ({ dir }) => {
288
- 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
+ }
289
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);
290
320
  }
291
321
  }
292
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';
@@ -52,6 +54,9 @@ import './lesson-derivation.js';
52
54
  * on any renderability error (unparseable MDX, KaTeX error, unresolved
53
55
  * notation, unknown `[[key]]`, unknown citation), on top of the manifest's
54
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.
55
60
  */
56
61
 
57
62
  type ContentCliCode = 'unknown-command' | 'usage' | 'unknown-lesson' | 'unknown-kind' | 'invalid-id' | 'exists' | 'not-a-lesson' | 'no-git';
@@ -88,7 +93,7 @@ interface CliDiagnostic {
88
93
  * when their message mentions the lesson; per-file checks (notation, math,
89
94
  * equations, consistency) are filtered by `file`.
90
95
  */
91
- declare function lessonDiagnostics(manifest: Manifest, lesson: ManifestLesson): CliDiagnostic[];
96
+ declare function lessonDiagnostics(manifest: Manifest, lesson: ManifestLesson, rendered?: readonly RenderDiagnostic[]): CliDiagnostic[];
92
97
  interface RenderResult {
93
98
  readonly rendered: boolean;
94
99
  readonly diagnostics: readonly CliDiagnostic[];
@@ -118,6 +123,15 @@ interface TokenCounts {
118
123
  /** Normalize `foundations/discount-factors` or the dotted id to the dotted id. */
119
124
  declare function normalizeLessonId(raw: string): string;
120
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
+ };
121
135
  interface DomainFileSignatures {
122
136
  readonly file: string;
123
137
  readonly signatures: readonly string[];
@@ -138,10 +152,6 @@ interface StatusReport {
138
152
  readonly keyedDiagrams: number;
139
153
  readonly keyedExamples: number;
140
154
  };
141
- readonly draftDebt: {
142
- readonly lessons: readonly string[];
143
- readonly notation: readonly string[];
144
- };
145
155
  readonly orphans: readonly CliDiagnostic[];
146
156
  readonly diagnostics: {
147
157
  readonly errors: number;
@@ -149,7 +159,7 @@ interface StatusReport {
149
159
  readonly infos: number;
150
160
  };
151
161
  }
152
- declare function contentStatus(manifest: Manifest): StatusReport;
162
+ declare function contentStatus(manifest: Manifest, rendered?: readonly RenderDiagnostic[]): StatusReport;
153
163
  type DiagnosticThreshold = DiagnosticSeverity;
154
164
  interface DiagnosticsReport {
155
165
  readonly minimumSeverity: DiagnosticThreshold;
@@ -160,10 +170,13 @@ interface WarningsReport {
160
170
  readonly count: number;
161
171
  readonly warnings: readonly CliDiagnostic[];
162
172
  }
163
- /** Manifest diagnostics at or above a severity, normalized for tooling. */
164
- 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;
165
178
  /** Every warning exactly; errors and informational advice are excluded. */
166
- declare function contentWarnings(manifest: Manifest): WarningsReport;
179
+ declare function contentWarnings(manifest: Manifest, rendered?: readonly RenderDiagnostic[]): WarningsReport;
167
180
  interface RulesReport {
168
181
  readonly filter: RulesFilter;
169
182
  readonly count: number;
@@ -188,7 +201,6 @@ interface ContextReport {
188
201
  readonly file: string;
189
202
  readonly title: string;
190
203
  readonly description: string;
191
- readonly status: string;
192
204
  readonly teaches: readonly string[];
193
205
  readonly requires: readonly string[];
194
206
  readonly assumptions: readonly string[];
@@ -272,6 +284,8 @@ interface CheckReport {
272
284
  declare function changedLessonIds(): string[];
273
285
  declare function contentCheck(manifest: Manifest, options: {
274
286
  lesson?: string;
287
+ /** Several lessons, each checked; `lesson` is the one-lesson form. */
288
+ lessons?: readonly string[];
275
289
  changed?: boolean;
276
290
  }): Promise<CheckReport>;
277
291
  interface Scaffold {
@@ -287,4 +301,4 @@ interface Scaffold {
287
301
  declare function scaffoldLesson(rawId: string): Scaffold;
288
302
  declare function scaffoldTerm(rawKey: string): Scaffold;
289
303
 
290
- 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 };