explico 0.13.1 → 0.14.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 (239) hide show
  1. package/CHANGELOG.md +879 -1
  2. package/README.md +200 -30
  3. package/dist/compiler/astro.d.ts +15 -2
  4. package/dist/compiler/astro.js +40 -11
  5. package/dist/compiler/cli-main.d.ts +43 -0
  6. package/dist/compiler/cli-main.js +392 -0
  7. package/dist/compiler/cli.d.ts +424 -9
  8. package/dist/compiler/cli.js +1289 -8
  9. package/dist/compiler/collections.d.ts +88 -1
  10. package/dist/compiler/collections.js +170 -25
  11. package/dist/compiler/course-config.d.ts +1 -1
  12. package/dist/compiler/course-config.js +105 -1
  13. package/dist/compiler/decks.d.ts +161 -0
  14. package/dist/compiler/decks.js +235 -0
  15. package/dist/compiler/exports/combined-html.d.ts +5 -1
  16. package/dist/compiler/exports/deck-fit.d.ts +55 -0
  17. package/dist/compiler/exports/deck-fit.js +90 -0
  18. package/dist/compiler/exports/export-chapters.d.ts +37 -6
  19. package/dist/compiler/exports/export-chapters.js +64 -5
  20. package/dist/compiler/exports/export-pages.d.ts +30 -3
  21. package/dist/compiler/exports/export-pages.js +24 -1
  22. package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  23. package/dist/compiler/exports/latex/cache.d.ts +78 -0
  24. package/dist/compiler/exports/latex/cache.js +129 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +10 -1
  26. package/dist/compiler/exports/latex/capture.js +3 -1
  27. package/dist/compiler/exports/latex/compile.d.ts +13 -2
  28. package/dist/compiler/exports/latex/compile.js +12 -1
  29. package/dist/compiler/exports/latex/deck.d.ts +92 -0
  30. package/dist/compiler/exports/latex/deck.js +349 -0
  31. package/dist/compiler/exports/latex/document.d.ts +51 -6
  32. package/dist/compiler/exports/latex/document.js +71 -12
  33. package/dist/compiler/exports/latex/explico-common.sty +198 -0
  34. package/dist/compiler/exports/latex/explico.cls +13 -183
  35. package/dist/compiler/exports/latex/serialize.d.ts +60 -4
  36. package/dist/compiler/exports/latex/serialize.js +4 -2
  37. package/dist/compiler/exports/latex-export.d.ts +24 -2
  38. package/dist/compiler/exports/latex-export.js +107 -26
  39. package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
  40. package/dist/compiler/exports/markdown/serialize.js +85 -8
  41. package/dist/compiler/exports/markdown-export.d.ts +6 -1
  42. package/dist/compiler/exports/markdown-export.js +50 -11
  43. package/dist/compiler/exports/standalone-html.d.ts +10 -5
  44. package/dist/compiler/exports/standalone-html.js +3 -3
  45. package/dist/compiler/exports/static-tree.d.ts +38 -1
  46. package/dist/compiler/exports/static-tree.js +120 -4
  47. package/dist/compiler/knowledge-version.d.ts +26 -2
  48. package/dist/compiler/knowledge-version.js +27 -17
  49. package/dist/compiler/lesson-derivation.d.ts +143 -6
  50. package/dist/compiler/lesson-derivation.js +145 -13
  51. package/dist/compiler/manifest.d.ts +133 -4
  52. package/dist/compiler/manifest.js +359 -58
  53. package/dist/compiler/page-versions.d.ts +160 -0
  54. package/dist/compiler/page-versions.js +198 -0
  55. package/dist/compiler/render-diagnostics.d.ts +5 -1
  56. package/dist/compiler/resolution-report.d.ts +24 -6
  57. package/dist/compiler/resolution-report.js +22 -4
  58. package/dist/compiler/validate-content.d.ts +7 -0
  59. package/dist/compiler/validate-content.js +160 -0
  60. package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
  61. package/dist/curriculum/competency-graph.d.ts +54 -0
  62. package/dist/curriculum/competency-graph.js +41 -0
  63. package/dist/reference/base-library.d.mts +8 -0
  64. package/dist/reference/base-library.mjs +19 -0
  65. package/dist/reference/consistency.d.ts +8 -4
  66. package/dist/reference/consistency.js +76 -26
  67. package/dist/reference/deck-remark.d.mts +123 -0
  68. package/dist/reference/deck-remark.mjs +600 -0
  69. package/dist/reference/deck-source.d.ts +107 -0
  70. package/dist/reference/deck-source.js +140 -0
  71. package/dist/reference/decks-validate.d.ts +133 -0
  72. package/dist/reference/decks-validate.js +359 -0
  73. package/dist/reference/derived-entries.d.ts +5 -1
  74. package/dist/reference/diagram-fit.d.ts +1 -1
  75. package/dist/reference/figure-symbols.d.ts +23 -6
  76. package/dist/reference/figure-symbols.js +51 -15
  77. package/dist/reference/float-name.d.ts +43 -0
  78. package/dist/reference/float-name.js +58 -0
  79. package/dist/reference/folds-validate.d.ts +1 -1
  80. package/dist/reference/gate-abbreviations.d.ts +1 -1
  81. package/dist/reference/gate-indices.js +5 -4
  82. package/dist/reference/gate-math.d.ts +29 -2
  83. package/dist/reference/gate-math.js +99 -18
  84. package/dist/reference/gate-part-pages.d.ts +1 -1
  85. package/dist/reference/lab-katex-validate.d.ts +10 -6
  86. package/dist/reference/lab-katex-validate.js +17 -9
  87. package/dist/reference/lab-math-scope.js +29 -4
  88. package/dist/reference/math-glyphs.d.mts +25 -0
  89. package/dist/reference/math-glyphs.mjs +189 -25
  90. package/dist/reference/page-notation.d.ts +22 -3
  91. package/dist/reference/page-notation.js +32 -10
  92. package/dist/reference/references.d.ts +14 -1
  93. package/dist/reference/references.js +13 -0
  94. package/dist/reference/registry-scope.d.mts +33 -0
  95. package/dist/reference/registry-scope.mjs +34 -0
  96. package/dist/reference/registry.js +29 -10
  97. package/dist/reference/rehype-deck.d.mts +6 -0
  98. package/dist/reference/rehype-deck.mjs +286 -0
  99. package/dist/reference/rehype-heading-anchors.d.mts +20 -0
  100. package/dist/reference/rehype-heading-anchors.mjs +48 -6
  101. package/dist/reference/remark-notation.mjs +249 -40
  102. package/dist/reference/source-lines.d.mts +12 -0
  103. package/dist/reference/source-lines.mjs +65 -0
  104. package/dist/reference/types.d.ts +28 -0
  105. package/dist/rules/catalog.d.ts +21 -4
  106. package/dist/rules/catalog.js +221 -2
  107. package/dist/rules/course-rules.d.ts +5 -1
  108. package/dist/rules/course-rules.js +17 -3
  109. package/dist/rules/course-rules.schema.json +12 -0
  110. package/dist/rules/in-effect.d.ts +13 -3
  111. package/dist/rules/in-effect.js +30 -7
  112. package/dist/rules/rules.schema.json +259 -1
  113. package/dist/rules/settings.d.ts +1 -1
  114. package/dist/session/PreferenceStore.d.ts +6 -0
  115. package/dist/session/PreferenceStore.js +18 -1
  116. package/dist/session/deck-memory.d.ts +116 -0
  117. package/dist/session/deck-memory.js +143 -0
  118. package/dist/session/reading-memory.d.ts +96 -0
  119. package/dist/session/reading-memory.js +133 -0
  120. package/docs/VERSIONING.md +21 -18
  121. package/docs/authoring/README.md +29 -9
  122. package/docs/authoring/agents.md +63 -16
  123. package/docs/authoring/components.md +15 -0
  124. package/docs/authoring/deck.md +588 -0
  125. package/docs/authoring/migrating.md +1045 -16
  126. package/docs/authoring/notation.md +49 -6
  127. package/docs/authoring/structure.md +80 -1
  128. package/docs/authoring/versioning.md +54 -2
  129. package/docs/authoring/visualizations.md +7 -2
  130. package/docs/course-agent/AGENTS.md +37 -0
  131. package/docs/course-agent/agents/rules-reviewer.md +33 -0
  132. package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
  133. package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
  134. package/docs/course-agent/skills/release-content/SKILL.md +24 -0
  135. package/docs/diagram-scaling.md +148 -0
  136. package/docs/privacy.md +59 -0
  137. package/package.json +8 -2
  138. package/src/compiler/astro.ts +60 -21
  139. package/src/compiler/cli-main.ts +469 -0
  140. package/src/compiler/cli.ts +1905 -16
  141. package/src/compiler/collections.ts +284 -25
  142. package/src/compiler/course-config.ts +267 -0
  143. package/src/compiler/decks.ts +455 -0
  144. package/src/compiler/exports/deck-fit.ts +127 -0
  145. package/src/compiler/exports/export-chapters.ts +105 -6
  146. package/src/compiler/exports/export-pages.ts +46 -2
  147. package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  148. package/src/compiler/exports/latex/cache.ts +183 -0
  149. package/src/compiler/exports/latex/capture.ts +5 -2
  150. package/src/compiler/exports/latex/compile.ts +18 -0
  151. package/src/compiler/exports/latex/deck.ts +419 -0
  152. package/src/compiler/exports/latex/document.ts +117 -21
  153. package/src/compiler/exports/latex/explico-common.sty +198 -0
  154. package/src/compiler/exports/latex/explico.cls +13 -183
  155. package/src/compiler/exports/latex/serialize.ts +25 -21
  156. package/src/compiler/exports/latex/texlive-packages.txt +6 -2
  157. package/src/compiler/exports/latex-export.ts +187 -45
  158. package/src/compiler/exports/markdown/serialize.ts +118 -9
  159. package/src/compiler/exports/markdown-export.ts +56 -11
  160. package/src/compiler/exports/standalone-html.ts +8 -7
  161. package/src/compiler/exports/static-tree.ts +175 -3
  162. package/src/compiler/knowledge-version.ts +56 -21
  163. package/src/compiler/lesson-derivation.ts +282 -16
  164. package/src/compiler/manifest.ts +558 -63
  165. package/src/compiler/page-versions.ts +359 -0
  166. package/src/compiler/resolution-report.ts +38 -7
  167. package/src/compiler/validate-content.ts +212 -0
  168. package/src/components/CourseContents.astro +124 -6
  169. package/src/components/CurriculumMap.astro +49 -1
  170. package/src/components/FloatIndex.astro +1 -1
  171. package/src/components/PartOverview.astro +51 -5
  172. package/src/components/assessments/AssessmentSet.astro +41 -15
  173. package/src/components/block-heading.ts +15 -6
  174. package/src/components/card-carry.ts +7 -4
  175. package/src/components/card-manager.ts +14 -2
  176. package/src/components/card-memory.ts +13 -9
  177. package/src/components/deck/DeckEnhancer.astro +13 -0
  178. package/src/components/deck/deck-context.ts +104 -0
  179. package/src/components/deck/deck-controller.ts +1287 -0
  180. package/src/components/deck/deck-map.ts +91 -0
  181. package/src/components/deck-lane.ts +163 -0
  182. package/src/components/diagram/Diagram.astro +131 -86
  183. package/src/components/disclosure/persisted-disclosure.ts +9 -1
  184. package/src/components/figure/Figure.astro +7 -2
  185. package/src/components/figure/FloatCaption.astro +5 -3
  186. package/src/components/fold/Fold.astro +2 -2
  187. package/src/components/notation/NotationFormula.astro +81 -11
  188. package/src/components/notation/NotationGlossary.astro +132 -1
  189. package/src/components/notation/NotationLayer.astro +15 -7
  190. package/src/components/starlight/AboutPanel.astro +186 -3
  191. package/src/components/starlight/CollapsibleSections.astro +24 -3
  192. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  193. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  194. package/src/components/starlight/LayoutHeader.astro +17 -0
  195. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  196. package/src/components/starlight/LayoutSidebar.astro +807 -21
  197. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  198. package/src/components/starlight/LessonExportActions.astro +27 -9
  199. package/src/components/starlight/LessonFooter.astro +59 -0
  200. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  201. package/src/components/starlight/TextSizeControl.astro +24 -13
  202. package/src/components/starlight/page-marks.ts +139 -0
  203. package/src/components/starlight/section-summary.ts +1 -1
  204. package/src/components/starlight/sidebar-editions.ts +245 -0
  205. package/src/components/table/Table.astro +15 -7
  206. package/src/content-config.ts +87 -1
  207. package/src/curriculum/competency-graph.ts +95 -0
  208. package/src/reference/base-library.mjs +19 -0
  209. package/src/reference/consistency.ts +126 -39
  210. package/src/reference/deck-remark.mjs +600 -0
  211. package/src/reference/deck-source.ts +273 -0
  212. package/src/reference/decks-validate.ts +604 -0
  213. package/src/reference/diagram-layout.ts +3 -1
  214. package/src/reference/figure-symbols.ts +108 -31
  215. package/src/reference/folds-validate.ts +1 -1
  216. package/src/reference/gate-indices.ts +5 -3
  217. package/src/reference/gate-math.ts +169 -25
  218. package/src/reference/lab-katex-validate.ts +30 -15
  219. package/src/reference/lab-math-scope.ts +34 -3
  220. package/src/reference/math-glyphs.mjs +189 -25
  221. package/src/reference/page-notation.ts +69 -15
  222. package/src/reference/references.ts +28 -0
  223. package/src/reference/registry-scope.mjs +34 -0
  224. package/src/reference/registry.ts +41 -11
  225. package/src/reference/rehype-deck.mjs +286 -0
  226. package/src/reference/rehype-heading-anchors.mjs +48 -6
  227. package/src/reference/remark-notation.mjs +249 -40
  228. package/src/reference/source-lines.mjs +65 -0
  229. package/src/reference/types.ts +28 -0
  230. package/src/rules/catalog.ts +257 -4
  231. package/src/rules/course-rules.ts +21 -1
  232. package/src/rules/in-effect.ts +54 -8
  233. package/src/session/PreferenceStore.ts +23 -0
  234. package/src/session/deck-memory.ts +263 -0
  235. package/src/session/reading-memory.ts +243 -0
  236. package/src/styles/deck.css +1150 -0
  237. package/src/styles/global.css +958 -27
  238. package/src/styles/zones.css +277 -0
  239. /package/src/{components → reference}/float-name.ts +0 -0
@@ -0,0 +1,419 @@
1
+ /**
2
+ * A lesson's deck as beamer (docs/authoring/deck.md): the frames its page
3
+ * shows, read off the built page (`readStaticDeck`), in explico's theme
4
+ * (`beamerthemeexplico.sty`, over `explico-common.sty`). The folder holds
5
+ * `main.tex` (the slides) and `handout.tex` (each slide followed by its
6
+ * notes), both reading `deck.tex`. Each frame carries `\label{frame:N}`, and
7
+ * the build checks that LaTeX numbers every frame as the page does.
8
+ *
9
+ * On paper a step is beamer's overlay (`uncoverenv`, `\item<k->`); a step
10
+ * that drives a figure or lights a symbol shows nothing new, so it takes no
11
+ * slide. After the last frame, an appendix lists the symbols the frames use,
12
+ * and each symbol on a frame links to its entry (rule D5's paper half).
13
+ * Beamer cannot write a tagged PDF (it refuses `\DocumentMetadata`):
14
+ * the deck's PDFs are untagged, and the chapter's PDF is the accessible
15
+ * paper edition.
16
+ */
17
+ import { readFileSync } from 'node:fs';
18
+
19
+ import type { Manifest, ManifestDeck } from '../../manifest';
20
+ import type { Block, DeckFrame, Inline, NotationEntry, StaticDeck, StaticPage } from '../static-tree';
21
+ import { escapeText, ChapterWriter, labelOf, linkExplains, type ChapterContext } from './serialize';
22
+ import { versionPhrase } from '../../page-versions';
23
+ import { explicoCommon, needsScripts, trailerId, type SourceFile } from './document';
24
+
25
+ /** `beamerthemeexplico.sty`, read next to this module. */
26
+ export function deckTheme(): string {
27
+ return readFileSync(new URL('./beamerthemeexplico.sty', import.meta.url), 'utf8');
28
+ }
29
+
30
+ const ASPECT: Readonly<Record<string, string>> = { '16:9': '169', '16:10': '1610', '4:3': '43' };
31
+
32
+ /** The frames of a deck as the static tree reads them, flattened for the citation keys. */
33
+ function deckPage(deck: StaticDeck): StaticPage {
34
+ return {
35
+ slug: deck.slug,
36
+ route: deck.route,
37
+ title: deck.title,
38
+ blocks: deck.frames.flatMap((frame) => [...frame.segments.flatMap((segment) => segment.blocks), ...frame.notes]),
39
+ notation: [],
40
+ notationRemarks: [],
41
+ };
42
+ }
43
+
44
+ class DeckWriter extends ChapterWriter {
45
+ inline(inline: Inline): string {
46
+ // A deck has no bibliography of its own: a citation is its number, and the
47
+ // references frame lists the entries.
48
+ if (inline.kind === 'cite') return `[${escapeText(inline.number)}]`;
49
+ return super.inline(inline);
50
+ }
51
+
52
+ /** A float's caption on a frame: its number (the chapter's, when recalled), its short and long caption. */
53
+ private deckCaption(kind: string, block: { number: string; short?: string; caption: readonly Inline[] }, extra: readonly Inline[] = []): string {
54
+ const number = block.number ? `${kind}~${escapeText(block.number)}` : '';
55
+ const short = block.short?.trim() ? escapeText(block.short.trim().replace(/\.$/u, '')) : '';
56
+ const long = [this.plain(block.caption), this.plain(extra)].filter(Boolean).join(' ');
57
+ return `\\ExplicoDeckCaption{${number}}{${short}}{${long}}`;
58
+ }
59
+
60
+ block(block: Block, inBox = false): string {
61
+ switch (block.kind) {
62
+ case 'heading':
63
+ return `\\par\\textbf{${this.plain(block.children)}}\\par`;
64
+ case 'equation':
65
+ return `\\begin{equation*}\n${this.math(block.tex)}\n\\tag{${escapeText(block.number)}}\n\\end{equation*}`;
66
+ case 'list': {
67
+ const environment = block.ordered ? 'enumerate' : 'itemize';
68
+ const items = block.items.map((item, index) => {
69
+ const step = block.itemSteps?.[index] ?? 1;
70
+ return `\\item${step > 1 ? `<${step}->` : ''} ${this.blocks(item, inBox)}`;
71
+ });
72
+ return `\\begin{${environment}}\n${items.join('\n')}\n\\end{${environment}}`;
73
+ }
74
+ case 'figure': {
75
+ const alt = block.alt ?? [block.short, ...block.caption.map((inline) => ('value' in inline ? inline.value : ''))].filter(Boolean).join(' ');
76
+ const picture = this.picture(block.anchor, block.picture, alt).replace('max height=0.6\\textheight', 'max height=0.62\\textheight');
77
+ return `\\begin{center}\n${picture}\n${this.deckCaption('Figure', block, block.live)}\n\\end{center}`;
78
+ }
79
+ case 'diagram':
80
+ return [
81
+ '\\begin{center}',
82
+ '\\begin{adjustbox}{max width=\\linewidth,max height=0.62\\textheight}',
83
+ this.unlinkedWhile(() => this.picture(block.anchor, { kind: 'diagram', geometry: block.geometry }, '')),
84
+ '\\end{adjustbox}',
85
+ this.deckCaption('Diagram', block),
86
+ '\\end{center}',
87
+ ].join('\n');
88
+ case 'table': {
89
+ const tabular = this.table({ ...block, number: '' }, true);
90
+ return `\\begin{center}\n${this.deckCaption('Table', block)}\n\\smallskip\n${tabular}\n\\end{center}`;
91
+ }
92
+ case 'memo':
93
+ return [`\\begin{block}{${this.plain(block.title)}}`, this.blocks(block.children, true), '\\end{block}'].join('\n');
94
+ case 'note':
95
+ return [`\\begin{block}{${escapeText(block.title)}}`, this.blocks(block.children, true), '\\end{block}'].join('\n');
96
+ case 'aside': {
97
+ const environment = block.variant === 'caution' || block.variant === 'danger' ? 'alertblock' : 'block';
98
+ return [`\\begin{${environment}}{${this.plain(block.title)}}`, this.blocks(block.children, true), `\\end{${environment}}`].join('\n');
99
+ }
100
+ case 'examples':
101
+ return block.items
102
+ .map((item) => [`\\begin{block}{${this.plain(item.title)}}`, this.blocks(item.children, true), '\\end{block}'].join('\n'))
103
+ .join('\n');
104
+ case 'check': {
105
+ const items = this.context.checkItems.get(block.assessmentId) ?? [];
106
+ const questions = items.map((item) => {
107
+ const options =
108
+ item.type === 'single-choice' && item.options && item.options.length > 0
109
+ ? `\n\\begin{enumerate}\n${item.options.map((option) => `\\item[(${String.fromCharCode(97 + item.options!.indexOf(option))})] ${this.optionLabel(option)}`).join('\n')}\n\\end{enumerate}`
110
+ : '';
111
+ return `\\item ${this.segments(item.promptSegments)}${options}`;
112
+ });
113
+ return [
114
+ `\\par{\\usebeamercolor[fg]{structure}\\textbf{Knowledge check ${escapeText(block.number)}}}\\enspace ${this.plain(block.title)}\\par`,
115
+ ...(questions.length > 0 ? ['\\begin{enumerate}', ...questions, '\\end{enumerate}'] : []),
116
+ ].join('\n');
117
+ }
118
+ default:
119
+ return super.block(block, inBox);
120
+ }
121
+ }
122
+
123
+ /** A check's answers, for the frame's notes. */
124
+ checkAnswers(block: Extract<Block, { kind: 'check' }>): string {
125
+ const items = this.context.checkItems.get(block.assessmentId) ?? [];
126
+ return items
127
+ .map((item) => {
128
+ if (item.type === 'single-choice' && item.options) {
129
+ const index = item.options.findIndex((option) => option.id === item.correctOptionId);
130
+ const answer = index === -1 ? '' : `(${String.fromCharCode(97 + index)}) ${this.optionLabel(item.options[index]!)}. `;
131
+ return `Answer: ${answer}${this.segments(item.explanationSegments)}`;
132
+ }
133
+ if (item.type === 'numeric' && item.answer) {
134
+ return `Answer: $${item.answer.value}$ (within $\\pm${item.answer.tolerance}$). ${this.segments(item.explanationSegments)}`;
135
+ }
136
+ return this.segments(item.explanationSegments);
137
+ })
138
+ .join('\\par ');
139
+ }
140
+
141
+ frameTitle(frame: DeckFrame): string {
142
+ return this.plain(frame.title);
143
+ }
144
+
145
+ /** A legend line's symbol, linked to its appendix entry. */
146
+ legendSymbol(tex: string): string {
147
+ return this.math(tex);
148
+ }
149
+
150
+ /**
151
+ * The notation appendix: each symbol the deck uses, by first use, with its
152
+ * name, what it is, its units and its defining formula, as the chapter's
153
+ * notation lists it. It breaks over as many frames as it needs.
154
+ */
155
+ notationFrames(entries: readonly NotationEntry[]): string {
156
+ if (entries.length === 0) return '';
157
+ const targets = new Set(entries.map((entry) => entry.key));
158
+ const items = entries.map((entry) => {
159
+ const formula = entry.formulaTex
160
+ ? linkExplains(entry.formulaTex, (named) => (named !== entry.key && targets.has(named) ? labelOf('notation', named) : undefined))
161
+ : undefined;
162
+ const parts = [
163
+ `\\textbf{${this.inlines(entry.label)}}.`,
164
+ this.inlines(entry.summary),
165
+ ...(entry.units ? [`\\emph{Units:} ${this.inlines(entry.units)}.`] : []),
166
+ ...(formula ? [`$${formula}$`] : []),
167
+ ];
168
+ return `\\item[{${entry.symbolTex ? `$${entry.symbolTex}$` : ''}}]\\hypertarget{${labelOf('notation', entry.key)}}{}${parts.join(' ')}`;
169
+ });
170
+ return [
171
+ '\\appendix',
172
+ '\\section{Notation}',
173
+ '\\begin{frame}[allowframebreaks,t]{Notation}',
174
+ '\\small',
175
+ '\\begin{description}',
176
+ ...items,
177
+ '\\end{description}',
178
+ '\\end{frame}',
179
+ ].join('\n');
180
+ }
181
+ }
182
+
183
+ /**
184
+ * The notation keys a deck's LaTeX names, in order of first use: in a
185
+ * formula (`\explain{key}{…}`) or in prose (`\ExplicoTerm{key}{…}`).
186
+ */
187
+ export function deckNotationKeys(tex: string): string[] {
188
+ const keys = new Set<string>();
189
+ for (const match of tex.matchAll(/\\(?:explain|ExplicoTerm)\{([^{}]+)\}\{/gu)) keys.add(match[1]!);
190
+ return [...keys];
191
+ }
192
+
193
+ /**
194
+ * The symbols a deck's frames use, in order of first use, from its page's
195
+ * notation list: its appendix in every download.
196
+ */
197
+ export function deckSymbols(
198
+ context: Omit<ChapterContext, 'page'> & { readonly deck: StaticDeck },
199
+ ): NotationEntry[] {
200
+ const byKey = new Map(context.deck.notation.map((entry) => [entry.key, entry]));
201
+ return deckNotationKeys(serializeDeck(context).tex).flatMap((key) => {
202
+ const entry = byKey.get(key);
203
+ return entry ? [entry] : [];
204
+ });
205
+ }
206
+
207
+ /** Every check a frame shows, at any depth. */
208
+ function checksOf(blocks: readonly Block[]): Extract<Block, { kind: 'check' }>[] {
209
+ const out: Extract<Block, { kind: 'check' }>[] = [];
210
+ for (const block of blocks) {
211
+ if (block.kind === 'check') out.push(block);
212
+ if ('children' in block && Array.isArray(block.children) && block.kind !== 'paragraph' && block.kind !== 'heading') {
213
+ out.push(...checksOf(block.children as readonly Block[]));
214
+ }
215
+ }
216
+ return out;
217
+ }
218
+
219
+ function hasCode(blocks: readonly Block[]): boolean {
220
+ return blocks.some(
221
+ (block) =>
222
+ block.kind === 'code' ||
223
+ ('children' in block && Array.isArray(block.children) && block.kind !== 'paragraph' && block.kind !== 'heading' && hasCode(block.children as readonly Block[])),
224
+ );
225
+ }
226
+
227
+ export interface DeckSerialization {
228
+ /** `deck.tex`: every frame, in order. */
229
+ readonly tex: string;
230
+ /** The line of `deck.tex` each frame ends on, by frame number: a paper overflow names its frame by it. */
231
+ readonly frameEnds: ReadonlyMap<number, number>;
232
+ }
233
+
234
+ /**
235
+ * The frames of a deck in beamer, then the notation appendix of the symbols
236
+ * they use (`appendix`, each linked from its uses through `notationTargets`).
237
+ */
238
+ export function serializeDeck(
239
+ context: Omit<ChapterContext, 'page'> & { readonly deck: StaticDeck; readonly appendix?: readonly NotationEntry[] },
240
+ ): DeckSerialization {
241
+ const writer = new DeckWriter({ ...context, page: deckPage(context.deck) });
242
+ const lines: string[] = [];
243
+ const frameEnds = new Map<number, number>();
244
+ const push = (text: string) => lines.push(...text.split('\n'));
245
+ // The progress bar runs over the deck's own frames, not the appendix's.
246
+ push(`\\ExplicoDeckFrames{${context.deck.frames.length}}`);
247
+ lines.push('');
248
+ for (const frame of context.deck.frames) {
249
+ const label = `\\label{frame:${frame.number}}`;
250
+ if (frame.kind === 'title') {
251
+ push(`\\begin{frame}[plain]${label}\n\\titlepage\n\\end{frame}`);
252
+ } else if (frame.kind === 'section') {
253
+ push(`\\section{${writer.frameTitle(frame)}}\n\\begin{frame}${label}\n\\ExplicoSectionPage\n\\end{frame}`);
254
+ } else {
255
+ const blocks = frame.segments.flatMap((segment) => segment.blocks);
256
+ const options = hasCode(blocks) ? '[fragile]' : '';
257
+ const body = frame.segments.map((segment) => {
258
+ const content = writer.blocks(segment.blocks);
259
+ return segment.step > 1 ? `\\begin{uncoverenv}<${segment.step}->\n${content}\n\\end{uncoverenv}` : content;
260
+ });
261
+ const legend = frame.legend.filter((item) => item.tex);
262
+ const answers = checksOf(blocks).map((check) => writer.checkAnswers(check)).filter(Boolean);
263
+ const notes = [writer.blocks(frame.notes), ...answers].filter((text) => text.trim().length > 0);
264
+ push(
265
+ [
266
+ `\\begin{frame}${options}{${writer.frameTitle(frame)}}${label}`,
267
+ ...body,
268
+ ...(legend.length > 0
269
+ ? [`\\ExplicoLegend{${legend.map((item) => `$${writer.legendSymbol(item.tex)}$~${escapeText(item.label.map((inline) => ('value' in inline ? inline.value : '')).join(''))}`).join('\\quad ')}}`]
270
+ : []),
271
+ ...(notes.length > 0 ? [`\\note{${notes.join('\n\n')}}`] : []),
272
+ '\\end{frame}',
273
+ ].join('\n'),
274
+ );
275
+ }
276
+ frameEnds.set(frame.number, lines.length);
277
+ lines.push('');
278
+ }
279
+ const appendix = writer.notationFrames(context.appendix ?? []);
280
+ if (appendix) push(`${appendix}\n`);
281
+ return { tex: `${lines.join('\n')}\n`, frameEnds };
282
+ }
283
+
284
+ const LATEXMKRC = [
285
+ '# LuaLaTeX by default; `latexmk -pdf` uses pdfLaTeX instead.',
286
+ '$pdf_mode = 4;',
287
+ '$postscript_mode = $dvi_mode = 0;',
288
+ "# luaotfload rewrites its font caches as fonts are looked up; they are not",
289
+ "# this document's sources, so a rewrite is no reason to run again.",
290
+ "$hash_calc_ignore_pattern{'luc'} = '^';",
291
+ '',
292
+ ].join('\n');
293
+
294
+ /** `main.tex` or `handout.tex` of a deck. */
295
+ function deckMain(manifest: Manifest, deck: ManifestDeck, handout: boolean, scripts: boolean): string {
296
+ const ratio = ASPECT[manifest.deck?.aspectRatio ?? '16:9'] ?? '169';
297
+ const course = manifest.about.material.name;
298
+ const authors = (manifest.about.authors ?? []).map((author) => author.name).filter(Boolean).join(', ');
299
+ // The version the deck's content is at (rule C5): an unchanged deck's PDF
300
+ // stays the same bytes from release to release.
301
+ const version = versionPhrase(deck.since, manifest.about.knowledge.version);
302
+ const document = `${deck.slug}${handout ? '#handout' : ''}`;
303
+ const trailer = trailerId(manifest, document, deck.since);
304
+ return [
305
+ `% The deck's ${handout ? 'handout: each slide followed by its notes' : 'slides'}. Compile with \`latexmk${handout ? ' handout.tex' : ''}\` (LuaLaTeX, see latexmkrc).`,
306
+ '% A fixed seed for TeX\'s random numbers: the same source gives the same PDF.',
307
+ '\\ExplSyntaxOn \\sys_gset_rand_seed:n { 1 } \\ExplSyntaxOff',
308
+ // usepdftitle=false: the title below is the PDF's, not beamer's "title - subtitle".
309
+ `\\documentclass[aspectratio=${ratio},11pt,usepdftitle=false${handout ? ',handout' : ''}]{beamer}`,
310
+ `\\usetheme${scripts ? '[scripts]' : ''}{explico}`,
311
+ ...(handout ? ['\\setbeameroption{show notes}'] : []),
312
+ `\\ifdefined\\directlua\\pdfvariable trailerid {[<${trailer}><${trailer}>]}\\else\\pdftrailerid{${trailer}}\\fi`,
313
+ `\\title{${escapeText(deck.title)}}`,
314
+ `\\subtitle{${escapeText(course)}}`,
315
+ `\\author{${escapeText(authors || course)}}`,
316
+ `\\date{Content ${escapeText(version)}}`,
317
+ `\\hypersetup{pdftitle={${escapeText(deck.title)}},pdfauthor={${escapeText(authors || course)}},pdfsubject={${escapeText(course)}}}`,
318
+ '\\begin{document}',
319
+ '\\input{deck}',
320
+ '\\end{document}',
321
+ '',
322
+ ].join('\n');
323
+ }
324
+
325
+ function deckReadme(manifest: Manifest, deck: ManifestDeck, url: string | undefined): string {
326
+ const course = manifest.about.material.name;
327
+ return [
328
+ `# ${deck.title} — slides`,
329
+ '',
330
+ `The LaTeX source of a deck of *${course}*, ${versionPhrase(deck.since, manifest.about.knowledge.version)}.` +
331
+ (url ? ` Its interactive version: ${url}` : ''),
332
+ '',
333
+ '- `main.tex` — the slides: `latexmk` builds `main.pdf`.',
334
+ '- `handout.tex` — each slide followed by its speaker notes: `latexmk handout.tex`.',
335
+ '- `deck.tex` — the frames, which both read.',
336
+ '- `beamerthemeexplico.sty` — explico\'s beamer theme: the legend line, the frame number, the progress bar.',
337
+ '- `explico-common.sty` — what the theme shares with explico\'s book class: fonts, colours, figures,',
338
+ ' diagrams and the notation markers.',
339
+ '- `figures/` — the deck\'s figures, as vector PDF where the website draws them.',
340
+ '- `latexmkrc` — LuaLaTeX by default; `latexmk -pdf` uses pdfLaTeX.',
341
+ '',
342
+ 'Beamer cannot write a tagged PDF: these PDFs are not accessible to a screen reader. The deck\'s',
343
+ 'web page and its chapter\'s PDF are.',
344
+ '',
345
+ ].join('\n');
346
+ }
347
+
348
+ export interface DeckSourceInput {
349
+ readonly manifest: Manifest;
350
+ readonly deck: ManifestDeck;
351
+ readonly page: StaticDeck;
352
+ readonly checkItems: ChapterContext['checkItems'];
353
+ readonly pictures: ReadonlyMap<string, { readonly path: string; readonly content: Uint8Array }>;
354
+ readonly scripts?: boolean;
355
+ }
356
+
357
+ /** A deck's LaTeX folder, and where each of its frames ends in `deck.tex`. */
358
+ export function deckSource(input: DeckSourceInput): { files: SourceFile[]; frameEnds: ReadonlyMap<number, number> } {
359
+ const { manifest, deck, page } = input;
360
+ const base = manifest.lessonExports.canonicalBaseUrl;
361
+ const url = base ? `${base}${deck.slug}/` : undefined;
362
+ const context = {
363
+ deck: page,
364
+ documentRoutes: new Set([page.route]),
365
+ ...(base ? { canonicalBaseUrl: base } : {}),
366
+ pictures: new Map([...input.pictures].map(([anchor, picture]) => [anchor, picture.path])),
367
+ checkItems: input.checkItems,
368
+ };
369
+ // The appendix lists the symbols the frames name, in order of first use;
370
+ // a second pass links each use to its entry.
371
+ const appendix = deckSymbols(context);
372
+ const serialized = serializeDeck({
373
+ ...context,
374
+ appendix,
375
+ notationTargets: new Set(appendix.map((entry) => entry.key)),
376
+ });
377
+ const scripts = input.scripts ?? needsScripts(serialized.tex);
378
+ const files: SourceFile[] = [
379
+ { path: 'main.tex', content: deckMain(manifest, deck, false, scripts) },
380
+ { path: 'handout.tex', content: deckMain(manifest, deck, true, scripts) },
381
+ { path: 'deck.tex', content: serialized.tex },
382
+ { path: 'beamerthemeexplico.sty', content: deckTheme() },
383
+ { path: 'explico-common.sty', content: explicoCommon() },
384
+ { path: 'latexmkrc', content: LATEXMKRC },
385
+ { path: 'README.md', content: deckReadme(manifest, deck, url) },
386
+ ...[...input.pictures.values()].map((picture) => ({ path: picture.path, content: picture.content })),
387
+ ];
388
+ return { files, frameEnds: serialized.frameEnds };
389
+ }
390
+
391
+ /**
392
+ * The frames LaTeX numbered differently from the deck page: each frame's
393
+ * `\label{frame:N}` must carry frame number N.
394
+ */
395
+ export function deckNumberingProblems(aux: string, frames: number): string[] {
396
+ const numbers = new Map<number, number>();
397
+ for (const match of aux.matchAll(/\\newlabel\{frame:(\d+)\}\{\{(\d+)\}/gu)) {
398
+ numbers.set(Number(match[1]), Number(match[2]));
399
+ }
400
+ const problems: string[] = [];
401
+ for (let frame = 1; frame <= frames; frame += 1) {
402
+ const printed = numbers.get(frame);
403
+ if (printed === undefined) problems.push(`slide ${frame} has no frame in the PDF`);
404
+ else if (printed !== frame) problems.push(`slide ${frame} is frame ${printed} in the PDF`);
405
+ }
406
+ return problems;
407
+ }
408
+
409
+ /** The frames a beamer log says run over their slide, by `deck.tex` line: `{ frame, points }`. */
410
+ export function deckOverflows(log: string, frameEnds: ReadonlyMap<number, number>): { frame: number; points: number }[] {
411
+ const ends = [...frameEnds].sort((a, b) => a[1] - b[1]);
412
+ const found = new Map<number, number>();
413
+ for (const match of log.matchAll(/Overfull \\vbox \(([\d.]+)pt too high\) detected at line (\d+)/gu)) {
414
+ const line = Number(match[2]);
415
+ const frame = ends.find(([, end]) => line <= end + 1)?.[0];
416
+ if (frame !== undefined) found.set(frame, Math.max(found.get(frame) ?? 0, Number(match[1])));
417
+ }
418
+ return [...found].map(([frame, points]) => ({ frame, points: Math.round(points) })).sort((a, b) => a.frame - b.frame);
419
+ }