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
@@ -0,0 +1,76 @@
1
+ # TeX Live 2026 packages for explico's LaTeX export, for the package_file of
2
+ # zauguin/install-texlive; tlmgr adds each one's dependencies. This is what a
3
+ # chapter reads under LuaLaTeX and pdfLaTeX (latexmk -recorder), less what
4
+ # latex-bin brings. Package names change between TeX Live years: check the
5
+ # list again when moving texlive_version.
6
+
7
+ # Engines, formats, build tool
8
+ latex-bin
9
+ latexmk
10
+
11
+ # Latin Modern text and math
12
+ lm
13
+ lm-math
14
+ cm-unicode # optional: Greek and Cyrillic prose in Latin Modern's design (CMU Serif)
15
+
16
+ # Tagged PDF 2.0 with MathML
17
+ latex-lab
18
+ luamml
19
+ pdfmanagement
20
+ tagpdf
21
+
22
+ # explico.cls
23
+ adjustbox
24
+ alphalph
25
+ amsfonts
26
+ booktabs
27
+ caption
28
+ ec
29
+ enumitem
30
+ float # optional in the class, but without it an in-box float loses its tagged caption
31
+ geometry
32
+ hyperref
33
+ iftex
34
+ lualatex-math
35
+ mathtools
36
+ microtype
37
+ newfloat
38
+ newunicodechar
39
+ pgf
40
+ tcolorbox
41
+ unicode-math
42
+ xcolor
43
+
44
+ # What those load
45
+ amscls
46
+ bigintcalc
47
+ bitset
48
+ bookmark
49
+ collectbox
50
+ environ
51
+ etoolbox
52
+ fontspec
53
+ footmisc
54
+ gettitlestring
55
+ hycolor
56
+ ifoddpage
57
+ infwarerr
58
+ intcalc
59
+ kvdefinekeys
60
+ kvoptions
61
+ kvsetkeys
62
+ l3packages
63
+ listings
64
+ listingsutf8
65
+ ltxcmds
66
+ mptopdf
67
+ pdfcol
68
+ pdfescape
69
+ pdftexcmds
70
+ refcount
71
+ stringenc
72
+ tikzfill
73
+ trimspaces
74
+ url
75
+ varwidth
76
+ xkeyval
@@ -0,0 +1,255 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { availableParallelism } from 'node:os';
3
+ import { dirname, extname, join } from 'node:path';
4
+ import { performance } from 'node:perf_hooks';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ import type { Manifest } from '../manifest';
8
+ import type { StaticPage } from './static-tree';
9
+ import { zip } from './zip';
10
+ import {
11
+ consoleLogger,
12
+ epochOf,
13
+ figuresOf,
14
+ imageFile,
15
+ needsCapture,
16
+ prepareChapters,
17
+ readBuiltPage,
18
+ strictMode,
19
+ type ExportLogger,
20
+ type PreparedChapters,
21
+ } from './export-chapters';
22
+ import { compileLatex, missingCharacters, findLatexEngine } from './latex/compile';
23
+ import { bookSource, chapterFolderName, chapterSource, type BookPart, type SourceFile } from './latex/document';
24
+ import { courseFileStem } from './export-pages';
25
+ import { auxLabels, numberingMismatches, websiteNumbers } from './latex/numbering';
26
+
27
+ export type { ExportLogger } from './export-chapters';
28
+
29
+ /**
30
+ * The LaTeX export (`lessonExports.latex`): after Astro's static build, each
31
+ * chapter's source folder — `main.tex`, `explico.cls`, the chapter and its
32
+ * figures — is zipped to `downloads/<slug>-latex.zip`, and the same folder is
33
+ * compiled to `downloads/<slug>.pdf`. Each part and the course are books
34
+ * (`bookSource`): `downloads/part/<dir>-latex.zip` and `.pdf`,
35
+ * `downloads/course-latex.zip` and `.pdf`. Every document compiles at once,
36
+ * up to one per core.
37
+ *
38
+ * The build fails when a compiled number differs from the website's, when
39
+ * LaTeX reports an error, and — in CI (`CI` set) or with
40
+ * `EXPLICO_LATEX_STRICT=1` — when the TeX engine or the browser for figures
41
+ * is missing. On a developer's machine without them it warns and still
42
+ * writes the source zips.
43
+ */
44
+
45
+ async function pagePictures(
46
+ outputRoot: string,
47
+ base: string,
48
+ page: StaticPage,
49
+ captured: ReadonlyMap<string, { pdf: Uint8Array }> | undefined,
50
+ ): Promise<Map<string, { path: string; content: Uint8Array }>> {
51
+ const pictures = new Map<string, { path: string; content: Uint8Array }>();
52
+ for (const figure of figuresOf(page.blocks)) {
53
+ const capturedFigure = captured?.get(figure.anchor);
54
+ if (capturedFigure) {
55
+ pictures.set(figure.anchor, {
56
+ path: `figures/${page.route}/${figure.anchor}.pdf`,
57
+ content: capturedFigure.pdf,
58
+ });
59
+ continue;
60
+ }
61
+ if (figure.picture.kind !== 'image' || needsCapture(figure)) continue;
62
+ const file = imageFile(outputRoot, base, page.route, figure.picture.src);
63
+ pictures.set(figure.anchor, {
64
+ path: `figures/${page.route}/${figure.anchor}${extname(file).toLowerCase()}`,
65
+ content: await readFile(file),
66
+ });
67
+ }
68
+ return pictures;
69
+ }
70
+
71
+ interface PendingFile {
72
+ readonly target: string;
73
+ readonly content: Uint8Array;
74
+ }
75
+
76
+ /** `work` over every item, at most `limit` at a time; results in the items' order. */
77
+ async function mapWithLimit<T, R>(items: readonly T[], limit: number, work: (item: T) => Promise<R>): Promise<R[]> {
78
+ const results: R[] = new Array(items.length);
79
+ let next = 0;
80
+ const worker = async () => {
81
+ while (next < items.length) {
82
+ const index = next++;
83
+ results[index] = await work(items[index]!);
84
+ }
85
+ };
86
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
87
+ return results;
88
+ }
89
+
90
+ /**
91
+ * Write each chapter's, each part's and the course's LaTeX source zip and PDF
92
+ * under `downloads/`, from the chapters `prepareChapters` read and drew (read
93
+ * here when not given).
94
+ */
95
+ export async function writeLatexExports(
96
+ directory: URL,
97
+ manifest: Manifest,
98
+ options: {
99
+ readonly logger?: ExportLogger;
100
+ readonly projectRoot?: string;
101
+ readonly prepared?: PreparedChapters;
102
+ } = {},
103
+ ): Promise<void> {
104
+ if (!manifest.lessonExports.latex) return;
105
+ const logger = options.logger ?? consoleLogger;
106
+ const strict = strictMode();
107
+ const outputRoot = fileURLToPath(directory);
108
+ const { base, chapters, captures } =
109
+ options.prepared ?? (await prepareChapters(outputRoot, manifest, { logger, projectRoot: options.projectRoot }));
110
+
111
+ const engine = await findLatexEngine();
112
+ if ('missing' in engine) {
113
+ if (strict) throw new Error(`LaTeX export cannot build PDFs: ${engine.missing}`);
114
+ logger.warn(`LaTeX export: writing the source zips only. ${engine.missing}`);
115
+ }
116
+
117
+ /** One LaTeX document: its folder, where its zip and PDF go, and the numbers it must print. */
118
+ interface Job {
119
+ readonly kind: 'chapter' | 'part' | 'course';
120
+ readonly name: string;
121
+ readonly files: SourceFile[];
122
+ readonly target: string;
123
+ readonly folder: string;
124
+ readonly website: Map<string, string>;
125
+ }
126
+ const jobs: Job[] = [];
127
+ const pictures = new Map<string, Awaited<ReturnType<typeof pagePictures>>>();
128
+ for (const { lesson, page } of chapters) {
129
+ const chapterPictures = await pagePictures(outputRoot, base, page, captures.get(page.route));
130
+ pictures.set(lesson.id, chapterPictures);
131
+ jobs.push({
132
+ kind: 'chapter',
133
+ name: lesson.slug,
134
+ files: chapterSource({ manifest, lesson, page, documentRoutes: new Set([page.route]), pictures: chapterPictures }),
135
+ target: lesson.slug,
136
+ folder: chapterFolderName(lesson),
137
+ website: websiteNumbers(page, `${lesson.part}.${lesson.chapter}`),
138
+ });
139
+ }
140
+
141
+ // Each part, and the course, as a book (plan step 6).
142
+ const byId = new Map(chapters.map((chapter) => [chapter.lesson.id, chapter]));
143
+ const pageSlugs = new Set(manifest.pages.map((page) => page.slug));
144
+ const parts: BookPart[] = [];
145
+ for (const part of manifest.parts) {
146
+ const partChapters = part.lessons.flatMap((id) => {
147
+ const chapter = byId.get(id);
148
+ return chapter ? [{ ...chapter, pictures: pictures.get(id)! }] : [];
149
+ });
150
+ if (partChapters.length === 0) continue;
151
+ const overview = pageSlugs.has(`${part.dir}/index`)
152
+ ? (await readBuiltPage(outputRoot, `${part.dir}/index`)).page
153
+ : undefined;
154
+ parts.push({ part, ...(overview ? { overview } : {}), chapters: partChapters });
155
+ }
156
+ const bookNumbers = (bookParts: readonly BookPart[]) =>
157
+ new Map(
158
+ bookParts.flatMap(({ chapters: partChapters }) =>
159
+ partChapters.flatMap(({ lesson, page }) => [...websiteNumbers(page, `${lesson.part}.${lesson.chapter}`)]),
160
+ ),
161
+ );
162
+ const course = manifest.about.material.name;
163
+ for (const entry of parts) {
164
+ jobs.push({
165
+ kind: 'part',
166
+ name: `part/${entry.part.dir}`,
167
+ files: bookSource({
168
+ manifest,
169
+ parts: [entry],
170
+ document: `part/${entry.part.dir}`,
171
+ title: entry.part.label,
172
+ subtitle: `Part ${entry.part.ordinal} of ${course}`,
173
+ }),
174
+ target: `part/${entry.part.dir}`,
175
+ folder: `${entry.part.dir}-latex`,
176
+ website: bookNumbers([entry]),
177
+ });
178
+ }
179
+ if (parts.length > 0) {
180
+ jobs.push({
181
+ kind: 'course',
182
+ name: 'course',
183
+ files: bookSource({ manifest, parts, document: 'course', title: course }),
184
+ target: 'course',
185
+ folder: `${courseFileStem(manifest)}-latex`,
186
+ website: bookNumbers(parts),
187
+ });
188
+ }
189
+
190
+ const pending: PendingFile[] = jobs.map((job) => ({
191
+ target: join(outputRoot, 'downloads', `${job.target}-latex.zip`),
192
+ content: zip(job.files.map((file) => ({ path: `${job.folder}/${file.path}`, content: file.content }))),
193
+ }));
194
+
195
+ // Compile every document at once, up to one per core; a book numbers
196
+ // every chapter, so each is checked against the website whole.
197
+ const durations: { kind: Job['kind']; seconds: number }[] = [];
198
+ const started = performance.now();
199
+ if (!('missing' in engine)) {
200
+ const compiled = await mapWithLimit(jobs, availableParallelism(), async (job) => {
201
+ const jobStarted = performance.now();
202
+ const result = await compileLatex(job.files, engine, { epoch: epochOf(manifest), name: job.name });
203
+ durations.push({ kind: job.kind, seconds: (performance.now() - jobStarted) / 1000 });
204
+ const problems = numberingMismatches(job.website, auxLabels(result.aux));
205
+ if (problems.length > 0) {
206
+ throw new Error(`LaTeX numbers ${job.name} differently from the website:\n ${problems.join('\n ')}`);
207
+ }
208
+ // A part's and the course's PDFs repeat their chapters' text, so each
209
+ // character is reported once, by the chapter that has it.
210
+ const missing = job.kind === 'chapter' ? missingCharacters(result.log) : [];
211
+ if (missing.length > 0) {
212
+ logger.warn(
213
+ `LaTeX export: the PDF of ${job.name} leaves ${missing.length} character(s) blank, ` +
214
+ `which its fonts cannot draw: ${missing.slice(0, 12).join(', ')}` +
215
+ `${missing.length > 12 ? ', …' : ''}. The web page shows them, the PDF does not. Under ` +
216
+ 'LuaLaTeX, install a font that covers them on the build machine (explico.cls looks for CMU ' +
217
+ 'Serif, Noto Serif or DejaVu Serif for Greek, Noto CJK or Fandol for Chinese, Japanese and ' +
218
+ 'Korean, and Noto Color Emoji for emoji); pdfLaTeX sets none of them. Or write the word in ' +
219
+ 'Latin script, or a symbol as math ($\\alpha$).',
220
+ );
221
+ }
222
+ return result.pdf;
223
+ });
224
+ jobs.forEach((job, index) => {
225
+ pending.push({ target: join(outputRoot, 'downloads', `${job.target}.pdf`), content: compiled[index]! });
226
+ });
227
+ }
228
+ const wall = (performance.now() - started) / 1000;
229
+
230
+ // Nothing is published until every document has compiled and matched.
231
+ const temporary = pending.map((file) => ({ ...file, temporary: `${file.target}.tmp-${process.pid}` }));
232
+ try {
233
+ for (const file of temporary) {
234
+ await mkdir(dirname(file.target), { recursive: true });
235
+ await writeFile(file.temporary, file.content);
236
+ }
237
+ for (const file of temporary) await rename(file.temporary, file.target);
238
+ } finally {
239
+ await Promise.all(temporary.map((file) => rm(file.temporary, { force: true })));
240
+ }
241
+ const pdfs = pending.filter((file) => file.target.endsWith('.pdf')).length;
242
+ const seconds = (kind: Job['kind']) =>
243
+ durations.filter((entry) => entry.kind === kind).reduce((sum, entry) => sum + entry.seconds, 0);
244
+ logger.info(
245
+ `LaTeX export: ${chapters.length} chapter(s), ${parts.length} part(s)${parts.length > 0 ? ' and the course' : ''}: ` +
246
+ `${jobs.length} source zip(s)` +
247
+ (pdfs > 0
248
+ ? `, ${pdfs} PDF(s) with ${'name' in engine ? engine.name : ''} in ${wall.toFixed(1)} s on up to ` +
249
+ `${availableParallelism()} core(s) (compile time: chapters ${seconds('chapter').toFixed(1)} s, ` +
250
+ `parts ${seconds('part').toFixed(1)} s, course ${seconds('course').toFixed(1)} s)`
251
+ : '') +
252
+ (captures.size > 0 ? `, figures from ${captures.size} page(s) drawn as vector PDF` : '') +
253
+ '.',
254
+ );
255
+ }