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
@@ -0,0 +1,233 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { extname, join, resolve } from 'node:path';
3
+ import { parse } from 'parse5';
4
+
5
+ import type { Manifest, ManifestLesson } from '../manifest';
6
+ import { inferBasePrefix } from './html-tree';
7
+ import { readInlineFragment, readStaticPage, type Block, type StaticPage } from './static-tree';
8
+ import {
9
+ captureFigures,
10
+ courseChromium,
11
+ figureMeasure,
12
+ LABEL_PT,
13
+ type CaptureRequest,
14
+ type CaptureResults,
15
+ } from './latex/capture';
16
+
17
+ /**
18
+ * What the print exporters (LaTeX, Markdown) share: every chapter's static
19
+ * reading tree, read once from Astro's output, and one capture pass that
20
+ * draws what only a browser can — a lab, a course component's SVG, and for
21
+ * Markdown an explico `<Diagram>` — as vector PDF and, for Markdown, PNG.
22
+ */
23
+
24
+ export interface ExportLogger {
25
+ info(message: string): void;
26
+ warn(message: string): void;
27
+ }
28
+
29
+ export const consoleLogger: ExportLogger = {
30
+ info: (message) => console.log(message),
31
+ warn: (message) => console.warn(message),
32
+ };
33
+
34
+ /** In CI or with `EXPLICO_LATEX_STRICT=1`, a missing engine or browser fails the build. */
35
+ export function strictMode(): boolean {
36
+ const ci = process.env.CI;
37
+ return (!!ci && ci !== 'false' && ci !== '0') || process.env.EXPLICO_LATEX_STRICT === '1';
38
+ }
39
+
40
+ /** The content date as seconds since 1970: every export's timestamps. */
41
+ export function epochOf(manifest: Pick<Manifest, 'about'>): number {
42
+ const date = manifest.about.knowledge.date;
43
+ const parsed = date ? Date.parse(`${date}T00:00:00Z`) : Number.NaN;
44
+ return Number.isFinite(parsed) ? Math.floor(parsed / 1000) : 0;
45
+ }
46
+
47
+ type FigureBlock = Extract<Block, { kind: 'figure' }>;
48
+ type DiagramBlock = Extract<Block, { kind: 'diagram' }>;
49
+
50
+ /** Every block of a kind, at any depth, in reading order. */
51
+ function blocksOf<K extends Block['kind']>(blocks: readonly Block[], kind: K): Extract<Block, { kind: K }>[] {
52
+ const found: Extract<Block, { kind: K }>[] = [];
53
+ const visit = (list: readonly Block[]) => {
54
+ for (const block of list) {
55
+ if (block.kind === kind) found.push(block as Extract<Block, { kind: K }>);
56
+ if (block.kind === 'list') block.items.forEach(visit);
57
+ if (block.kind === 'examples') block.items.forEach((item) => visit(item.children));
58
+ if ('children' in block && block.kind !== 'paragraph' && block.kind !== 'heading' && Array.isArray(block.children)) {
59
+ visit(block.children as readonly Block[]);
60
+ }
61
+ }
62
+ };
63
+ visit(blocks);
64
+ return found;
65
+ }
66
+
67
+ export function figuresOf(blocks: readonly Block[]): FigureBlock[] {
68
+ return blocksOf(blocks, 'figure');
69
+ }
70
+
71
+ export function diagramsOf(blocks: readonly Block[]): DiagramBlock[] {
72
+ return blocksOf(blocks, 'diagram');
73
+ }
74
+
75
+ /** Every inline `image` block, in reading order. */
76
+ export function imagesOf(blocks: readonly Block[]): Extract<Block, { kind: 'image' }>[] {
77
+ return blocksOf(blocks, 'image');
78
+ }
79
+
80
+ /** Raster formats a document includes as they are; anything else is drawn by the browser. */
81
+ const INCLUDABLE_IMAGES = new Set(['.png', '.jpg', '.jpeg', '.pdf']);
82
+
83
+ /** The image file a URL names, by extension. */
84
+ export function imageExtension(src: string): string {
85
+ return extname(new URL(src, 'https://x.invalid/').pathname).toLowerCase();
86
+ }
87
+
88
+ /** Whether a figure is drawn by the browser rather than included as a file. */
89
+ export function needsCapture(figure: FigureBlock): boolean {
90
+ if (figure.picture.kind === 'capture') return true;
91
+ if (figure.picture.kind === 'image') return !INCLUDABLE_IMAGES.has(imageExtension(figure.picture.src));
92
+ return false;
93
+ }
94
+
95
+ /** The built file an image URL on a page names. */
96
+ export function imageFile(outputRoot: string, base: string, route: string, src: string): string {
97
+ const url = new URL(src, `https://x.invalid${base}${route ? `${route}/` : ''}`);
98
+ let path = decodeURIComponent(url.pathname);
99
+ if (path.startsWith(base)) path = path.slice(base.length);
100
+ return resolve(outputRoot, path);
101
+ }
102
+
103
+ /** The page with each captured lab's live caption clause filled in. */
104
+ export function withLiveClauses(
105
+ page: StaticPage,
106
+ captured: ReadonlyMap<string, { liveHtml: string }>,
107
+ base: string,
108
+ ): StaticPage {
109
+ const update = (blocks: readonly Block[]): Block[] =>
110
+ blocks.map((block) => {
111
+ if (block.kind === 'figure') {
112
+ const live = captured.get(block.anchor)?.liveHtml;
113
+ return live ? { ...block, live: readInlineFragment(live, page.route, base) } : block;
114
+ }
115
+ if (block.kind === 'memo' || block.kind === 'note' || block.kind === 'aside' || block.kind === 'fold' || block.kind === 'quote') {
116
+ return { ...block, children: update(block.children) };
117
+ }
118
+ if (block.kind === 'examples') {
119
+ return { ...block, items: block.items.map((item) => ({ ...item, children: update(item.children) })) };
120
+ }
121
+ if (block.kind === 'list') return { ...block, items: block.items.map(update) };
122
+ return block;
123
+ });
124
+ return { ...page, blocks: update(page.blocks) };
125
+ }
126
+
127
+ export interface PreparedChapter {
128
+ readonly lesson: ManifestLesson;
129
+ /** The chapter's reading tree, its labs' live caption clauses filled in. */
130
+ readonly page: StaticPage;
131
+ }
132
+
133
+ export interface PreparedChapters {
134
+ /** The site's base path, `/` or `/course/`. */
135
+ readonly base: string;
136
+ readonly chapters: readonly PreparedChapter[];
137
+ /** What the capture pass drew, by route and anchor. */
138
+ readonly captures: CaptureResults;
139
+ }
140
+
141
+ /** Read one built page into its reading tree. */
142
+ export async function readBuiltPage(outputRoot: string, slug: string): Promise<{ html: string; page: StaticPage }> {
143
+ const route = slug.replace(/(?:^|\/)index$/u, '');
144
+ const html = await readFile(join(outputRoot, route, 'index.html'), 'utf8');
145
+ return { html, page: readStaticPage(html, slug, route) };
146
+ }
147
+
148
+ /**
149
+ * Read every chapter and draw its figures once, for whichever print exports
150
+ * the course has on: a figure the browser draws for LaTeX (as vector PDF)
151
+ * and for Markdown (as PNG too), and for Markdown each `<Diagram>`, which
152
+ * LaTeX draws itself as TikZ. Without Playwright's Chromium the figures stay
153
+ * placeholders, with a warning — or, in CI, the build fails.
154
+ */
155
+ export async function prepareChapters(
156
+ outputRoot: string,
157
+ manifest: Manifest,
158
+ options: { readonly logger?: ExportLogger; readonly projectRoot?: string } = {},
159
+ ): Promise<PreparedChapters> {
160
+ const logger = options.logger ?? consoleLogger;
161
+ const strict = strictMode();
162
+ const { latex, markdown } = manifest.lessonExports;
163
+
164
+ const read: { lesson: ManifestLesson; page: StaticPage }[] = [];
165
+ let base = '/';
166
+ for (const lesson of manifest.lessons) {
167
+ const { html, page } = await readBuiltPage(outputRoot, lesson.slug);
168
+ if (read.length === 0) base = inferBasePrefix(parse(html));
169
+ read.push({ lesson, page });
170
+ }
171
+
172
+ const requests: CaptureRequest[] = (latex || markdown ? read : [])
173
+ .map(({ page }) => ({
174
+ route: page.route,
175
+ anchors: [
176
+ ...figuresOf(page.blocks).filter(needsCapture).map((figure) => figure.anchor),
177
+ ...(markdown ? diagramsOf(page.blocks).map((diagram) => diagram.anchor) : []),
178
+ ],
179
+ }))
180
+ .filter((request) => request.anchors.length > 0);
181
+ let captures: CaptureResults = new Map();
182
+ if (requests.length > 0) {
183
+ const chromium = await courseChromium(options.projectRoot ?? process.cwd());
184
+ if (chromium) {
185
+ try {
186
+ captures = await captureFigures(requests, {
187
+ outputRoot,
188
+ base,
189
+ chromium,
190
+ measure: figureMeasure(manifest.lessonExports.latexPaper === 'a4' ? 'a4' : 'letter'),
191
+ epoch: epochOf(manifest),
192
+ png: markdown,
193
+ });
194
+ } catch (error) {
195
+ const message = error instanceof Error ? error.message : String(error);
196
+ if (strict || !/Executable doesn't exist|browserType\.launch/u.test(message)) throw error;
197
+ logger.warn(`Exports: figures stay placeholders, the browser did not start. ${message.split('\n')[0]}`);
198
+ }
199
+ } else {
200
+ const message =
201
+ 'Exports: drawing figures needs Playwright with Chromium in the course ' +
202
+ '(`pnpm add -D @playwright/test && pnpm exec playwright install chromium`).';
203
+ if (strict) throw new Error(message);
204
+ logger.warn(`${message} Figures stay placeholders.`);
205
+ }
206
+ }
207
+
208
+ for (const [route, figures] of captures) {
209
+ for (const [anchor, figure] of figures) {
210
+ if (figure.machineFonts.length > 0) {
211
+ logger.warn(
212
+ `Exports: ${route}#${anchor} draws some text in this machine's ${figure.machineFonts.join(', ')}, ` +
213
+ "not the site's fonts, so another machine may draw it differently.",
214
+ );
215
+ }
216
+ if (figure.labelPt !== undefined && figure.labelPt < LABEL_PT - 0.5) {
217
+ logger.info(
218
+ `Exports: the labels of ${route}#${anchor} print at ${figure.labelPt} pt, not ${LABEL_PT} pt; ` +
219
+ 'any larger and two of them would touch.',
220
+ );
221
+ }
222
+ }
223
+ }
224
+
225
+ return {
226
+ base,
227
+ captures,
228
+ chapters: read.map(({ lesson, page }) => {
229
+ const captured = captures.get(page.route);
230
+ return { lesson, page: captured ? withLiveClauses(page, captured, base) : page };
231
+ }),
232
+ };
233
+ }
@@ -0,0 +1,44 @@
1
+ import type { Manifest } from '../manifest';
2
+
3
+ /**
4
+ * Every page a reader can print or download, by docs slug: each chapter
5
+ * (`motion/constant-acceleration`) and each non-chapter page the manifest
6
+ * knows — the landing page (`index`), a part's page (`motion/index`), the
7
+ * glossary. The 404 route is not one.
8
+ */
9
+ export function exportPageSlugs(
10
+ manifest: Pick<Manifest, 'lessons' | 'pages'>,
11
+ ): string[] {
12
+ return [...manifest.lessons, ...manifest.pages].map((page) => page.slug);
13
+ }
14
+
15
+ /** Where Astro serves a docs slug, below the base: `motion/index` → `motion`, `index` → ``. */
16
+ export function exportPageRoute(slug: string): string {
17
+ return slug.replace(/(?:^|\/)index$/u, '');
18
+ }
19
+
20
+ /** The name a download is saved under: `projectile-range.html`, `motion.html`, `index.html`. */
21
+ export function standaloneFilename(slug: string): string {
22
+ return `${exportPageRoute(slug).split('/').at(-1) || 'index'}.html`;
23
+ }
24
+
25
+ /**
26
+ * The pages with an HTML download of their own: every chapter (D8). A page
27
+ * that is not a chapter — the landing page, a part's page, the glossary —
28
+ * is in its part's file and the course's (`combined-html.ts`).
29
+ */
30
+ export function standalonePageSlugs(manifest: Pick<Manifest, 'lessons'>): string[] {
31
+ return manifest.lessons.map((lesson) => lesson.slug);
32
+ }
33
+
34
+ /** The name the course's files are saved under: `Explico Playground — Motion` → `explico-playground-motion`. */
35
+ export function courseFileStem(manifest: Pick<Manifest, 'about'>): string {
36
+ return (
37
+ manifest.about.material.name
38
+ .normalize('NFKD')
39
+ .replace(/[\u0300-\u036f]/gu, '')
40
+ .toLowerCase()
41
+ .replace(/[^a-z0-9]+/gu, '-')
42
+ .replace(/^-+|-+$/gu, '') || 'course'
43
+ );
44
+ }
@@ -0,0 +1,62 @@
1
+ import type { DefaultTreeAdapterMap } from 'parse5';
2
+
3
+ /**
4
+ * Read-only queries over a parse5 tree, shared by the exporters that work on
5
+ * Astro's built pages (`standalone-html.ts`, `static-tree.ts`).
6
+ */
7
+
8
+ export type HtmlNode = DefaultTreeAdapterMap['node'];
9
+ export type HtmlParent = DefaultTreeAdapterMap['parentNode'];
10
+ export type HtmlChild = DefaultTreeAdapterMap['childNode'];
11
+ export type HtmlElement = DefaultTreeAdapterMap['element'];
12
+ export type HtmlText = DefaultTreeAdapterMap['textNode'];
13
+
14
+ export function isElement(node: HtmlNode): node is HtmlElement {
15
+ return 'tagName' in node;
16
+ }
17
+
18
+ export function childrenOf(node: HtmlNode): readonly HtmlChild[] {
19
+ return 'childNodes' in node ? node.childNodes : [];
20
+ }
21
+
22
+ /** Depth-first, `<template>` content included. */
23
+ export function walk(node: HtmlNode, visit: (node: HtmlNode) => void): void {
24
+ visit(node);
25
+ for (const child of childrenOf(node)) walk(child, visit);
26
+ if (isElement(node) && node.tagName === 'template') {
27
+ const template = node as DefaultTreeAdapterMap['template'];
28
+ for (const child of template.content.childNodes) walk(child, visit);
29
+ }
30
+ }
31
+
32
+ export function elements(
33
+ root: HtmlNode,
34
+ predicate?: (node: HtmlElement) => boolean,
35
+ ): HtmlElement[] {
36
+ const found: HtmlElement[] = [];
37
+ walk(root, (node) => {
38
+ if (isElement(node) && (predicate?.(node) ?? true)) found.push(node);
39
+ });
40
+ return found;
41
+ }
42
+
43
+ export function attribute(node: HtmlElement, name: string): string | undefined {
44
+ return node.attrs.find((entry) => entry.name === name)?.value;
45
+ }
46
+
47
+ export function hasClass(node: HtmlElement, name: string): boolean {
48
+ return (attribute(node, 'class') ?? '').split(/\s+/u).includes(name);
49
+ }
50
+
51
+ /** The site's base path (`/`, `/course/`), read off Astro's asset URLs. */
52
+ export function inferBasePrefix(document: HtmlNode): string {
53
+ for (const node of elements(document)) {
54
+ for (const name of ['href', 'src', 'component-url', 'renderer-url']) {
55
+ const value = attribute(node, name);
56
+ if (!value?.startsWith('/')) continue;
57
+ const marker = value.indexOf('/astro-assets/');
58
+ if (marker !== -1) return `${value.slice(0, marker)}/`;
59
+ }
60
+ }
61
+ return '/';
62
+ }