explico 0.12.0 → 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 (279) hide show
  1. package/CHANGELOG.md +1122 -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 +663 -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 +300 -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
@@ -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`.
@@ -128,7 +130,10 @@ An interactive figure is a React island, hydrated `client:visible` (or
128
130
  `labMathScope` argument; that is the declaration that ships those definitions
129
131
  in this page's popup payload. See
130
132
  `playground/content/labs/ProjectileRangeExplorer.tsx` +
131
- `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.
132
137
  - **Labelled controls.** Every control is a labelled `<input>` — the visible
133
138
  label is how any reader knows what a slider sets; where precision
134
139
  matters, pair a `range` slider with a `number` field (see `RangeField` in
@@ -166,11 +171,20 @@ An interactive figure is a React island, hydrated `client:visible` (or
166
171
  the one thing to notice. The argument drawn from the figure is lesson
167
172
  text, after the figure. Instructions for the controls belong to the
168
173
  controls: the tools' first line, not the caption.
169
- - **A preview shows the plot.** A same-page `[[fig-…]]` reference to a lab
170
- previews a still copy of its plot as drawn; the tools are never copied. A
171
- plot not drawn yet — or one on another page, drawn only in the browser —
172
- leaves the card to the caption and number, so a lab no longer needs
173
- `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}`.
174
188
 
175
189
  The figure itself never hides; a lab that should fold away whole, with the
176
190
  prose around it, goes in a `<Fold>` or a collapsible section.
@@ -198,9 +212,14 @@ every figure it rendered and fails a symbol KaTeX drew outside a notation
198
212
  binding (`figure-symbols.ts`, rule [N4](notation.md#n4)); a course's browser
199
213
  test runs the same `unboundFigureSymbols` on a hydrated figure's
200
214
  `outerHTML`, which also holds what an island draws only in the browser, as
201
- the playground's `labs.spec.ts` does. `setFigureState` takes no symbol typed
202
- as text, so `astro check` fails an italic or a `sub` run. That a lab labels
203
- its controls and names nothing of its own is review-only.
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.
204
223
 
205
224
  _Exceptions._ None.
206
225
 
@@ -364,3 +383,110 @@ _Exceptions._
364
383
  are only two.
365
384
  - A sketch that draws axes is a chart. If the axes are worth drawing, their
366
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.12.0",
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",
@@ -13,7 +13,19 @@ import {
13
13
  } from './manifest';
14
14
  import { missingNotationPayloadKeys } from '../reference/page-notation';
15
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';
16
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';
17
29
  import remarkAbbreviations from '../reference/remark-abbreviations.mjs';
18
30
  import remarkCitation from '../reference/remark-citation.mjs';
19
31
  import remarkNotation from '../reference/remark-notation.mjs';
@@ -27,21 +39,21 @@ function normalizedBasePath(base: string): string {
27
39
  return segment ? `/${segment}/` : '/';
28
40
  }
29
41
 
30
- /** Match only a configured lesson artifact below Astro's deployment base. */
42
+ /** Match only a configured page artifact below Astro's deployment base. */
31
43
  export function matchStandaloneDownload(
32
44
  pathname: string,
33
45
  base: string,
34
- lessonSlugs: readonly string[],
46
+ pageSlugs: readonly string[],
35
47
  ): string | undefined {
36
48
  const prefix = `${normalizedBasePath(base)}downloads/`;
37
49
  if (!pathname.startsWith(prefix) || !pathname.endsWith('.html')) return undefined;
38
50
  const slug = pathname.slice(prefix.length, -'.html'.length);
39
- return lessonSlugs.includes(slug) ? slug : undefined;
51
+ return pageSlugs.includes(slug) ? slug : undefined;
40
52
  }
41
53
 
42
54
  async function buildDevelopmentDownloads(
43
55
  config: Pick<AstroConfig, 'root'>,
44
- lessonSlugs: readonly string[],
56
+ pageSlugs: readonly string[],
45
57
  ): Promise<ReadonlyMap<string, Buffer>> {
46
58
  const temporaryRoot = await mkdtemp(join(tmpdir(), 'explico-standalone-'));
47
59
  const outputRoot = join(temporaryRoot, 'dist');
@@ -89,7 +101,7 @@ async function buildDevelopmentDownloads(
89
101
  });
90
102
  return new Map(
91
103
  await Promise.all(
92
- lessonSlugs.map(async (slug) => [
104
+ pageSlugs.map(async (slug) => [
93
105
  slug,
94
106
  await readFile(join(outputRoot, 'downloads', `${slug}.html`)),
95
107
  ] as const),
@@ -229,8 +241,17 @@ export function assertRemarkPluginsWired(
229
241
  );
230
242
  }
231
243
 
232
- 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[]> {
233
253
  const root = fileURLToPath(directory);
254
+ const rendered: RenderDiagnostic[] = [];
234
255
  const walk = async (path: string): Promise<void> => {
235
256
  for (const entry of await readdir(path, { withFileTypes: true })) {
236
257
  const child = join(path, entry.name);
@@ -238,12 +259,17 @@ async function validateGeneratedNotationPayloads(directory: URL): Promise<void>
238
259
  await walk(child);
239
260
  } else if (entry.isFile() && entry.name.endsWith('.html')) {
240
261
  const html = await readFile(child, 'utf8');
241
- assertNotationPayloadHtml(relative(root, child), html);
242
- assertFigureSymbolsBound(relative(root, child), html);
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));
243
268
  }
244
269
  }
245
270
  };
246
271
  await walk(root);
272
+ return rendered;
247
273
  }
248
274
 
249
275
  function replaceInPlace(target: unknown, source: unknown): void {
@@ -381,19 +407,19 @@ export function explicoContentRefresh(
381
407
  const slug = matchStandaloneDownload(
382
408
  pathname,
383
409
  configuredDevelopment.base,
384
- manifest.lessons.map((lesson) => lesson.slug),
410
+ standalonePageSlugs(manifest),
385
411
  );
386
412
  if (!slug) return next();
387
413
 
388
414
  try {
389
415
  logger.info(
390
416
  developmentDownloads
391
- ? `Serving cached standalone lesson ${slug}.html`
392
- : 'Building standalone lesson downloads for local development…',
417
+ ? `Serving cached standalone page ${slug}.html`
418
+ : 'Building standalone page downloads for local development…',
393
419
  );
394
420
  developmentDownloads ??= buildDevelopmentDownloads(
395
421
  configuredDevelopment,
396
- manifest.lessons.map((lesson) => lesson.slug),
422
+ standalonePageSlugs(manifest),
397
423
  );
398
424
  const currentDownloads = developmentDownloads;
399
425
  currentDownloads.catch(() => {
@@ -403,7 +429,7 @@ export function explicoContentRefresh(
403
429
  });
404
430
  const artifact = (await currentDownloads).get(slug);
405
431
  if (!artifact) return next();
406
- const filename = `${slug.split('/').at(-1) ?? 'lesson'}.html`;
432
+ const filename = standaloneFilename(slug);
407
433
  response.statusCode = 200;
408
434
  response.setHeader('Content-Type', 'text/html; charset=utf-8');
409
435
  response.setHeader('Content-Length', artifact.byteLength);
@@ -415,17 +441,31 @@ export function explicoContentRefresh(
415
441
  response.end(request.method === 'HEAD' ? undefined : artifact);
416
442
  } catch (error) {
417
443
  const message = error instanceof Error ? error.message : String(error);
418
- logger.error(`Could not build standalone lesson downloads: ${message}`);
444
+ logger.error(`Could not build standalone page downloads: ${message}`);
419
445
  response.statusCode = 500;
420
446
  response.setHeader('Content-Type', 'text/plain; charset=utf-8');
421
- response.end(`Could not build standalone lesson download.\n${message}\n`);
447
+ response.end(`Could not build standalone page download.\n${message}\n`);
422
448
  }
423
449
  });
424
450
  }
425
451
  },
426
- 'astro:build:done': async ({ dir }) => {
427
- 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
+ }
428
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);
429
469
  },
430
470
  },
431
471
  };