explico 0.13.1 → 0.15.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 (256) hide show
  1. package/CHANGELOG.md +990 -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 +100 -0
  6. package/dist/compiler/cli-main.js +433 -0
  7. package/dist/compiler/cli-reference.d.ts +50 -0
  8. package/dist/compiler/cli-reference.js +213 -0
  9. package/dist/compiler/cli-verbs.d.ts +45 -0
  10. package/dist/compiler/cli-verbs.js +224 -0
  11. package/dist/compiler/cli-verify.d.ts +51 -0
  12. package/dist/compiler/cli-verify.js +151 -0
  13. package/dist/compiler/cli.d.ts +408 -24
  14. package/dist/compiler/cli.js +1328 -8
  15. package/dist/compiler/collections.d.ts +88 -1
  16. package/dist/compiler/collections.js +170 -25
  17. package/dist/compiler/content-release.d.ts +49 -0
  18. package/dist/compiler/content-release.js +63 -0
  19. package/dist/compiler/content-schemas.d.ts +2 -2
  20. package/dist/compiler/course-config.d.ts +1 -1
  21. package/dist/compiler/course-config.js +105 -1
  22. package/dist/compiler/decks.d.ts +161 -0
  23. package/dist/compiler/decks.js +235 -0
  24. package/dist/compiler/exports/combined-html.d.ts +5 -1
  25. package/dist/compiler/exports/deck-fit.d.ts +55 -0
  26. package/dist/compiler/exports/deck-fit.js +90 -0
  27. package/dist/compiler/exports/export-chapters.d.ts +37 -6
  28. package/dist/compiler/exports/export-chapters.js +64 -5
  29. package/dist/compiler/exports/export-pages.d.ts +30 -3
  30. package/dist/compiler/exports/export-pages.js +24 -1
  31. package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  32. package/dist/compiler/exports/latex/cache.d.ts +78 -0
  33. package/dist/compiler/exports/latex/cache.js +129 -0
  34. package/dist/compiler/exports/latex/capture.d.ts +10 -1
  35. package/dist/compiler/exports/latex/capture.js +3 -1
  36. package/dist/compiler/exports/latex/compile.d.ts +13 -2
  37. package/dist/compiler/exports/latex/compile.js +12 -1
  38. package/dist/compiler/exports/latex/deck.d.ts +92 -0
  39. package/dist/compiler/exports/latex/deck.js +349 -0
  40. package/dist/compiler/exports/latex/document.d.ts +51 -6
  41. package/dist/compiler/exports/latex/document.js +71 -12
  42. package/dist/compiler/exports/latex/explico-common.sty +198 -0
  43. package/dist/compiler/exports/latex/explico.cls +13 -183
  44. package/dist/compiler/exports/latex/serialize.d.ts +60 -4
  45. package/dist/compiler/exports/latex/serialize.js +4 -2
  46. package/dist/compiler/exports/latex-export.d.ts +24 -2
  47. package/dist/compiler/exports/latex-export.js +107 -26
  48. package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
  49. package/dist/compiler/exports/markdown/serialize.js +85 -8
  50. package/dist/compiler/exports/markdown-export.d.ts +6 -1
  51. package/dist/compiler/exports/markdown-export.js +50 -11
  52. package/dist/compiler/exports/standalone-html.d.ts +10 -5
  53. package/dist/compiler/exports/standalone-html.js +3 -3
  54. package/dist/compiler/exports/static-tree.d.ts +38 -1
  55. package/dist/compiler/exports/static-tree.js +120 -4
  56. package/dist/compiler/knowledge-version.d.ts +26 -2
  57. package/dist/compiler/knowledge-version.js +27 -17
  58. package/dist/compiler/lesson-derivation.d.ts +143 -6
  59. package/dist/compiler/lesson-derivation.js +145 -13
  60. package/dist/compiler/manifest.d.ts +133 -4
  61. package/dist/compiler/manifest.js +364 -58
  62. package/dist/compiler/page-versions.d.ts +184 -0
  63. package/dist/compiler/page-versions.js +217 -0
  64. package/dist/compiler/render-diagnostics.d.ts +5 -1
  65. package/dist/compiler/resolution-report.d.ts +24 -6
  66. package/dist/compiler/resolution-report.js +22 -4
  67. package/dist/compiler/validate-content.d.ts +7 -0
  68. package/dist/compiler/validate-content.js +160 -0
  69. package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
  70. package/dist/curriculum/competency-graph.d.ts +54 -0
  71. package/dist/curriculum/competency-graph.js +41 -0
  72. package/dist/reference/base-library.d.mts +8 -0
  73. package/dist/reference/base-library.mjs +19 -0
  74. package/dist/reference/consistency.d.ts +8 -4
  75. package/dist/reference/consistency.js +82 -26
  76. package/dist/reference/deck-remark.d.mts +123 -0
  77. package/dist/reference/deck-remark.mjs +600 -0
  78. package/dist/reference/deck-source.d.ts +107 -0
  79. package/dist/reference/deck-source.js +140 -0
  80. package/dist/reference/decks-validate.d.ts +133 -0
  81. package/dist/reference/decks-validate.js +359 -0
  82. package/dist/reference/derived-entries.d.ts +5 -1
  83. package/dist/reference/diagram-fit.d.ts +6 -2
  84. package/dist/reference/diagram-fit.js +5 -1
  85. package/dist/reference/figure-symbols.d.ts +23 -6
  86. package/dist/reference/figure-symbols.js +51 -15
  87. package/dist/reference/float-name.d.ts +43 -0
  88. package/dist/reference/float-name.js +58 -0
  89. package/dist/reference/folds-validate.d.ts +1 -1
  90. package/dist/reference/gate-abbreviations.d.ts +1 -1
  91. package/dist/reference/gate-indices.js +5 -4
  92. package/dist/reference/gate-math.d.ts +29 -2
  93. package/dist/reference/gate-math.js +99 -18
  94. package/dist/reference/gate-part-pages.d.ts +1 -1
  95. package/dist/reference/lab-katex-validate.d.ts +10 -6
  96. package/dist/reference/lab-katex-validate.js +17 -9
  97. package/dist/reference/lab-math-scope.js +29 -4
  98. package/dist/reference/math-glyphs.d.mts +25 -0
  99. package/dist/reference/math-glyphs.mjs +189 -25
  100. package/dist/reference/page-notation.d.ts +22 -3
  101. package/dist/reference/page-notation.js +32 -10
  102. package/dist/reference/references.d.ts +14 -1
  103. package/dist/reference/references.js +13 -0
  104. package/dist/reference/registry-scope.d.mts +33 -0
  105. package/dist/reference/registry-scope.mjs +34 -0
  106. package/dist/reference/registry.js +29 -10
  107. package/dist/reference/rehype-deck.d.mts +6 -0
  108. package/dist/reference/rehype-deck.mjs +286 -0
  109. package/dist/reference/rehype-heading-anchors.d.mts +20 -0
  110. package/dist/reference/rehype-heading-anchors.mjs +48 -6
  111. package/dist/reference/remark-notation.mjs +249 -40
  112. package/dist/reference/source-lines.d.mts +12 -0
  113. package/dist/reference/source-lines.mjs +65 -0
  114. package/dist/reference/types.d.ts +28 -0
  115. package/dist/rules/catalog.d.ts +21 -4
  116. package/dist/rules/catalog.js +237 -2
  117. package/dist/rules/course-rules.d.ts +5 -1
  118. package/dist/rules/course-rules.js +17 -3
  119. package/dist/rules/course-rules.schema.json +12 -0
  120. package/dist/rules/in-effect.d.ts +13 -3
  121. package/dist/rules/in-effect.js +34 -10
  122. package/dist/rules/rules.schema.json +280 -1
  123. package/dist/rules/settings.d.ts +1 -1
  124. package/dist/session/PreferenceStore.d.ts +6 -0
  125. package/dist/session/PreferenceStore.js +18 -1
  126. package/dist/session/deck-memory.d.ts +116 -0
  127. package/dist/session/deck-memory.js +143 -0
  128. package/dist/session/reading-memory.d.ts +96 -0
  129. package/dist/session/reading-memory.js +133 -0
  130. package/docs/VERSIONING.md +21 -18
  131. package/docs/authoring/README.md +31 -9
  132. package/docs/authoring/agents.md +72 -18
  133. package/docs/authoring/cli-reference.json +2172 -0
  134. package/docs/authoring/cli.md +887 -0
  135. package/docs/authoring/components.md +15 -0
  136. package/docs/authoring/deck.md +612 -0
  137. package/docs/authoring/migrating.md +1195 -16
  138. package/docs/authoring/notation.md +49 -6
  139. package/docs/authoring/structure.md +80 -1
  140. package/docs/authoring/versioning.md +56 -3
  141. package/docs/authoring/visualizations.md +7 -2
  142. package/docs/course-agent/AGENTS.md +40 -0
  143. package/docs/course-agent/agents/rules-reviewer.md +33 -0
  144. package/docs/course-agent/skills/draft-lesson/SKILL.md +38 -0
  145. package/docs/course-agent/skills/migrate-course/SKILL.md +32 -0
  146. package/docs/course-agent/skills/release-content/SKILL.md +26 -0
  147. package/docs/diagram-scaling.md +148 -0
  148. package/docs/privacy.md +59 -0
  149. package/package.json +8 -2
  150. package/src/compiler/astro.ts +60 -21
  151. package/src/compiler/cli-main.ts +557 -0
  152. package/src/compiler/cli-reference.ts +285 -0
  153. package/src/compiler/cli-verbs.ts +270 -0
  154. package/src/compiler/cli-verify.ts +241 -0
  155. package/src/compiler/cli.ts +2119 -170
  156. package/src/compiler/collections.ts +284 -25
  157. package/src/compiler/content-release.ts +91 -0
  158. package/src/compiler/course-config.ts +267 -0
  159. package/src/compiler/decks.ts +455 -0
  160. package/src/compiler/exports/deck-fit.ts +127 -0
  161. package/src/compiler/exports/export-chapters.ts +105 -6
  162. package/src/compiler/exports/export-pages.ts +46 -2
  163. package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  164. package/src/compiler/exports/latex/cache.ts +183 -0
  165. package/src/compiler/exports/latex/capture.ts +5 -2
  166. package/src/compiler/exports/latex/compile.ts +18 -0
  167. package/src/compiler/exports/latex/deck.ts +419 -0
  168. package/src/compiler/exports/latex/document.ts +117 -21
  169. package/src/compiler/exports/latex/explico-common.sty +198 -0
  170. package/src/compiler/exports/latex/explico.cls +13 -183
  171. package/src/compiler/exports/latex/serialize.ts +25 -21
  172. package/src/compiler/exports/latex/texlive-packages.txt +6 -2
  173. package/src/compiler/exports/latex-export.ts +187 -45
  174. package/src/compiler/exports/markdown/serialize.ts +118 -9
  175. package/src/compiler/exports/markdown-export.ts +56 -11
  176. package/src/compiler/exports/standalone-html.ts +8 -7
  177. package/src/compiler/exports/static-tree.ts +175 -3
  178. package/src/compiler/knowledge-version.ts +56 -21
  179. package/src/compiler/lesson-derivation.ts +282 -16
  180. package/src/compiler/manifest.ts +568 -63
  181. package/src/compiler/page-versions.ts +401 -0
  182. package/src/compiler/resolution-report.ts +38 -7
  183. package/src/compiler/validate-content.ts +215 -0
  184. package/src/components/CourseContents.astro +124 -6
  185. package/src/components/CurriculumMap.astro +49 -1
  186. package/src/components/FloatIndex.astro +1 -1
  187. package/src/components/PartOverview.astro +51 -5
  188. package/src/components/assessments/AssessmentSet.astro +41 -15
  189. package/src/components/block-heading.ts +15 -6
  190. package/src/components/card-carry.ts +7 -4
  191. package/src/components/card-manager.ts +14 -2
  192. package/src/components/card-memory.ts +13 -9
  193. package/src/components/deck/DeckEnhancer.astro +13 -0
  194. package/src/components/deck/deck-context.ts +104 -0
  195. package/src/components/deck/deck-controller.ts +1287 -0
  196. package/src/components/deck/deck-map.ts +91 -0
  197. package/src/components/deck-lane.ts +163 -0
  198. package/src/components/diagram/Diagram.astro +131 -86
  199. package/src/components/disclosure/persisted-disclosure.ts +9 -1
  200. package/src/components/figure/Figure.astro +7 -2
  201. package/src/components/figure/FloatCaption.astro +5 -3
  202. package/src/components/fold/Fold.astro +2 -2
  203. package/src/components/notation/NotationFormula.astro +81 -11
  204. package/src/components/notation/NotationGlossary.astro +132 -1
  205. package/src/components/notation/NotationLayer.astro +15 -7
  206. package/src/components/starlight/AboutPanel.astro +186 -3
  207. package/src/components/starlight/CollapsibleSections.astro +24 -3
  208. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  209. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  210. package/src/components/starlight/LayoutHeader.astro +17 -0
  211. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  212. package/src/components/starlight/LayoutSidebar.astro +807 -21
  213. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  214. package/src/components/starlight/LessonExportActions.astro +27 -9
  215. package/src/components/starlight/LessonFooter.astro +59 -0
  216. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  217. package/src/components/starlight/TextSizeControl.astro +24 -13
  218. package/src/components/starlight/page-marks.ts +139 -0
  219. package/src/components/starlight/section-summary.ts +1 -1
  220. package/src/components/starlight/sidebar-editions.ts +245 -0
  221. package/src/components/table/Table.astro +15 -7
  222. package/src/content-config.ts +87 -1
  223. package/src/curriculum/competency-graph.ts +95 -0
  224. package/src/reference/base-library.mjs +19 -0
  225. package/src/reference/consistency.ts +137 -39
  226. package/src/reference/deck-remark.mjs +600 -0
  227. package/src/reference/deck-source.ts +273 -0
  228. package/src/reference/decks-validate.ts +604 -0
  229. package/src/reference/diagram-fit.ts +18 -4
  230. package/src/reference/diagram-layout.ts +3 -1
  231. package/src/reference/figure-symbols.ts +108 -31
  232. package/src/reference/folds-validate.ts +1 -1
  233. package/src/reference/gate-indices.ts +5 -3
  234. package/src/reference/gate-math.ts +169 -25
  235. package/src/reference/lab-katex-validate.ts +30 -15
  236. package/src/reference/lab-math-scope.ts +34 -3
  237. package/src/reference/math-glyphs.mjs +189 -25
  238. package/src/reference/page-notation.ts +69 -15
  239. package/src/reference/references.ts +28 -0
  240. package/src/reference/registry-scope.mjs +34 -0
  241. package/src/reference/registry.ts +41 -11
  242. package/src/reference/rehype-deck.mjs +286 -0
  243. package/src/reference/rehype-heading-anchors.mjs +48 -6
  244. package/src/reference/remark-notation.mjs +249 -40
  245. package/src/reference/source-lines.mjs +65 -0
  246. package/src/reference/types.ts +28 -0
  247. package/src/rules/catalog.ts +274 -4
  248. package/src/rules/course-rules.ts +21 -1
  249. package/src/rules/in-effect.ts +58 -11
  250. package/src/session/PreferenceStore.ts +23 -0
  251. package/src/session/deck-memory.ts +263 -0
  252. package/src/session/reading-memory.ts +243 -0
  253. package/src/styles/deck.css +1150 -0
  254. package/src/styles/global.css +958 -27
  255. package/src/styles/zones.css +277 -0
  256. /package/src/{components → reference}/float-name.ts +0 -0
@@ -1,10 +1,13 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { readFileSync } from 'node:fs';
3
3
 
4
- import type { Manifest, ManifestLesson, ManifestPart } from '../../manifest';
4
+ import type { Manifest, ManifestDeck, ManifestLesson, ManifestPart } from '../../manifest';
5
+ import { newestSince, versionPhrase } from '../../page-versions';
5
6
  import type { Block, StaticPage } from '../static-tree';
6
7
  import {
7
8
  escapeText,
9
+ escapeUrl,
10
+ labelOf,
8
11
  serializeAbbreviationAppendix,
9
12
  serializeChapter,
10
13
  serializeNotationAppendix,
@@ -30,6 +33,11 @@ export function explicoClass(): string {
30
33
  return readFileSync(new URL('./explico.cls', import.meta.url), 'utf8');
31
34
  }
32
35
 
36
+ /** `explico-common.sty`: what the book class and the deck theme share. */
37
+ export function explicoCommon(): string {
38
+ return readFileSync(new URL('./explico-common.sty', import.meta.url), 'utf8');
39
+ }
40
+
33
41
  function twoDigits(value: number): string {
34
42
  return String(value).padStart(2, '0');
35
43
  }
@@ -59,6 +67,26 @@ export function checkItems(manifest: Pick<Manifest, 'assessmentContent'>, lesson
59
67
  return items;
60
68
  }
61
69
 
70
+ /**
71
+ * The items a deck's question frames ask: its lesson's checks, or on a
72
+ * part's deck those of each chapter it covers, as its frames show them
73
+ * (an assessment two chapters share, as the first one does).
74
+ */
75
+ export function deckCheckItems(
76
+ manifest: Pick<Manifest, 'assessmentContent'>,
77
+ deck: Pick<ManifestDeck, 'lessonId' | 'covers'>,
78
+ ): Map<string, CheckItem[]> {
79
+ const lessonIds =
80
+ deck.lessonId !== undefined ? [deck.lessonId] : deck.covers.map((cover) => cover.chapter.replace(/\//g, '.'));
81
+ const items = new Map<string, CheckItem[]>();
82
+ for (const id of lessonIds) {
83
+ for (const [assessment, list] of checkItems(manifest, { id })) {
84
+ if (!items.has(assessment)) items.set(assessment, list);
85
+ }
86
+ }
87
+ return items;
88
+ }
89
+
62
90
  /**
63
91
  * The seed comes first: tagpdf names its "user" structure namespace with
64
92
  * `\int_rand:n` digits as it loads, and TeX seeds its random numbers from
@@ -85,16 +113,31 @@ const DOCUMENT_METADATA = [
85
113
 
86
114
  /**
87
115
  * The PDF's file identifier (its trailer `/ID`), which the engines derive
88
- * from the output file's path unless told: fixed from the course, its
89
- * content version and the document, so every compile of a source agrees.
116
+ * from the output file's path unless told: fixed from the course, the
117
+ * version the document's content is at (rule C5) and the document, so every
118
+ * compile of a source agrees, and a release that left the document alone
119
+ * leaves its identifier.
90
120
  */
91
- export function trailerId(manifest: Pick<Manifest, 'about'>, document: string): string {
121
+ export function trailerId(manifest: Pick<Manifest, 'about'>, document: string, since: string | null): string {
92
122
  return createHash('md5')
93
- .update(`${manifest.about.material.name}\n${manifest.about.knowledge.version}\n${document}`)
123
+ .update(`${manifest.about.material.name}\n${versionPhrase(since, manifest.about.knowledge.version)}\n${document}`)
94
124
  .digest('hex')
95
125
  .toUpperCase();
96
126
  }
97
127
 
128
+ /**
129
+ * The version a book's content is at: the newest among its chapters and
130
+ * part pages, `null` when any has a change not yet released.
131
+ */
132
+ export function bookSince(parts: readonly BookPart[]): string | null {
133
+ return newestSince(
134
+ parts.flatMap(({ part, chapters }) => [
135
+ ...(part.overview ? [part.overview.since] : []),
136
+ ...chapters.map(({ lesson }) => lesson.since),
137
+ ]),
138
+ );
139
+ }
140
+
98
141
  function trailerIdLine(id: string): string {
99
142
  return `\\ifdefined\\directlua\\pdfvariable trailerid {[<${id}><${id}>]}\\else\\pdftrailerid{${id}}\\fi`;
100
143
  }
@@ -116,7 +159,7 @@ function mainTex(
116
159
  ...DOCUMENT_METADATA,
117
160
  `\\documentclass[${paper}]{explico}`,
118
161
  '% The same identifier for every compile of this source.',
119
- trailerIdLine(trailerId(manifest, lesson.slug)),
162
+ trailerIdLine(trailerId(manifest, lesson.slug, lesson.since)),
120
163
  ...(numbering.floats === 'part' ? ['\\ExplicoFloatsByPart'] : []),
121
164
  `\\ExplicoSectionDepth{${numbering.sections.show ? numbering.sections.depth : 1}}`,
122
165
  `\\hypersetup{pdftitle={${escapeText(lesson.title)}},pdfauthor={${escapeText(
@@ -146,7 +189,7 @@ function readme(manifest: Manifest, lesson: ManifestLesson, url: string | undefi
146
189
  `# ${lesson.title}`,
147
190
  '',
148
191
  `The LaTeX source of a chapter of *${course}*` +
149
- ` (content version ${manifest.about.knowledge.version}).` +
192
+ ` (${versionPhrase(lesson.since, manifest.about.knowledge.version)}).` +
150
193
  (url ? ` Interactive version: ${url}` : ''),
151
194
  '',
152
195
  '## Build',
@@ -166,6 +209,8 @@ function readme(manifest: Manifest, lesson: ManifestLesson, url: string | undefi
166
209
  '- `main.tex` — the document: class options, then the chapter.',
167
210
  '- `explico.cls` — explico\'s book class: layout, numbering, and the',
168
211
  ' environments the chapter uses (memos, notes, worked examples, checks).',
212
+ '- `explico-common.sty` — what the class shares with explico\'s slides: fonts,',
213
+ ' colours, figures, diagrams and the notation markers.',
169
214
  `- \`${chapterPath(manifest, lesson)}\` — the chapter.`,
170
215
  '- `figures/` — the chapter\'s figures, as vector PDF where the website draws them.',
171
216
  '',
@@ -199,7 +244,7 @@ export function chapterSource(input: ChapterSourceInput): SourceFile[] {
199
244
  checkItems: checkItems(manifest, lesson),
200
245
  source: {
201
246
  course: manifest.about.material.name,
202
- version: manifest.about.knowledge.version,
247
+ since: versionPhrase(lesson.since, manifest.about.knowledge.version),
203
248
  ...(url ? { url } : {}),
204
249
  },
205
250
  });
@@ -208,6 +253,7 @@ export function chapterSource(input: ChapterSourceInput): SourceFile[] {
208
253
  { path: 'latexmkrc', content: LATEXMKRC },
209
254
  { path: 'main.tex', content: mainTex(manifest, lesson, file) },
210
255
  { path: 'explico.cls', content: explicoClass() },
256
+ { path: 'explico-common.sty', content: explicoCommon() },
211
257
  { path: file, content: chapter },
212
258
  ...[...input.pictures.values()].map((picture) => ({ path: picture.path, content: picture.content })),
213
259
  ]);
@@ -229,15 +275,19 @@ const MATH_GREEK = /[αβγδεθλμπρστφωΓΔΘΛΠΣΦΩ]/gu;
229
275
  * fallback fonts under LuaLaTeX, which costs every compile that does not
230
276
  * need them about two seconds.
231
277
  */
278
+ /**
279
+ * Whether a document's LaTeX holds a character of another script, so its
280
+ * class (or a deck's theme) should load the fallback fonts. A Greek letter on
281
+ * its own is a symbol set as math; a Greek word (`\ExplicoGreekWord`), an
282
+ * accented letter or another script needs a text font.
283
+ */
284
+ export function needsScripts(tex: string): boolean {
285
+ return tex.includes('\\ExplicoGreekWord{') || OTHER_SCRIPTS.test(tex.replace(MATH_GREEK, ''));
286
+ }
287
+
232
288
  function withScripts(files: SourceFile[]): SourceFile[] {
233
- // A Greek letter on its own is a symbol the class sets as math; a Greek
234
- // word (`\ExplicoGreekWord`), an accented letter or another script needs a
235
- // text font.
236
289
  const needed = files.some(
237
- (file) =>
238
- file.path.endsWith('.tex') &&
239
- typeof file.content === 'string' &&
240
- (file.content.includes('\\ExplicoGreekWord{') || OTHER_SCRIPTS.test(file.content.replace(MATH_GREEK, ''))),
290
+ (file) => file.path.endsWith('.tex') && typeof file.content === 'string' && needsScripts(file.content),
241
291
  );
242
292
  if (!needed) return files;
243
293
  return files.map((file) =>
@@ -254,11 +304,42 @@ export interface BookChapter {
254
304
  readonly pictures: ReadonlyMap<string, { readonly path: string; readonly content: Uint8Array }>;
255
305
  }
256
306
 
307
+ /**
308
+ * A lesson of a book's part that has no chapter, only slides (a slides-first
309
+ * lesson, docs/authoring/deck.md): the book keeps its place and its number
310
+ * with a short chapter that points at the slides.
311
+ */
312
+ export interface BookSlidesLesson {
313
+ readonly lesson: ManifestLesson;
314
+ /** The deck's PDF and interactive version, when the course publishes them. */
315
+ readonly slidesUrl?: string;
316
+ readonly outcomes: readonly string[];
317
+ }
318
+
257
319
  /** A part of a book: its own page, when it has one, and its chapters in order. */
258
320
  export interface BookPart {
259
321
  readonly part: ManifestPart;
260
322
  readonly overview?: StaticPage;
261
323
  readonly chapters: readonly BookChapter[];
324
+ /** Its slides-first lessons, placed among the chapters by their chapter number. */
325
+ readonly slides?: readonly BookSlidesLesson[];
326
+ }
327
+
328
+ /** A slides-first lesson's place in a book: its title, outcomes and where its slides are. */
329
+ function slidesChapter(entry: BookSlidesLesson): string {
330
+ // As many checks as the website numbers for it (manifest part offsets).
331
+ const checks = entry.lesson.assessments.length;
332
+ return [
333
+ `\\chapter{${escapeText(entry.lesson.title)}}\\label{${labelOf(entry.lesson.slug, '_top')}}`,
334
+ `This lesson is taught as slides${entry.slidesUrl ? `: \\url{${escapeUrl(entry.slidesUrl)}}` : ''}.` +
335
+ (checks > 0 ? ` Its knowledge check${checks === 1 ? ' is' : 's are'} on its slides.` : ''),
336
+ ...(entry.outcomes.length > 0
337
+ ? ['', 'After it, you can:', '\\begin{itemize}', ...entry.outcomes.map((outcome) => `\\item ${escapeText(outcome)}`), '\\end{itemize}']
338
+ : []),
339
+ // Its checks keep their numbers, so the chapters after it keep theirs.
340
+ ...(checks > 0 ? [`\\addtocounter{explicocheck}{${checks}}`] : []),
341
+ '',
342
+ ].join('\n');
262
343
  }
263
344
 
264
345
  export interface BookSourceInput {
@@ -335,7 +416,7 @@ function bookMainTex(input: BookSourceInput, inputs: readonly string[], appendic
335
416
  ...DOCUMENT_METADATA,
336
417
  `\\documentclass[${paper}]{explico}`,
337
418
  '% The same identifier for every compile of this source.',
338
- trailerIdLine(trailerId(manifest, input.document)),
419
+ trailerIdLine(trailerId(manifest, input.document, bookSince(input.parts))),
339
420
  ...(numbering.floats === 'part' ? ['\\ExplicoFloatsByPart'] : []),
340
421
  `\\ExplicoSectionDepth{${numbering.sections.show ? numbering.sections.depth : 1}}`,
341
422
  '% The contents list parts, chapters and sections.',
@@ -346,7 +427,7 @@ function bookMainTex(input: BookSourceInput, inputs: readonly string[], appendic
346
427
  '\\begin{document}',
347
428
  '\\frontmatter',
348
429
  `\\ExplicoTitlePage{${escapeText(input.title)}}{${escapeText(input.subtitle ?? '')}}{${escapeText(
349
- manifest.about.knowledge.version,
430
+ versionPhrase(bookSince(input.parts), manifest.about.knowledge.version),
350
431
  )}}{${escapeText(authors.join(', '))}}{${url ? url.replace(/[\\{}]/gu, '').replace(/[%#]/gu, (char) => `\\${char}`) : ''}}`,
351
432
  '% The contents and lists in black, as a book sets them; links in the text keep their colour.',
352
433
  '\\begingroup\\hypersetup{linkcolor=black}',
@@ -368,7 +449,7 @@ function bookReadme(input: BookSourceInput): string {
368
449
  `# ${input.title}`,
369
450
  '',
370
451
  `The LaTeX source of ${input.subtitle ? `${input.subtitle.replace(/^Part/u, 'part')}` : `*${manifest.about.material.name}*`}` +
371
- ` (content version ${manifest.about.knowledge.version}).` +
452
+ ` (${versionPhrase(bookSince(input.parts), manifest.about.knowledge.version)}).` +
372
453
  (url ? ` Interactive version: ${url}` : ''),
373
454
  '',
374
455
  '## Build',
@@ -389,6 +470,8 @@ function bookReadme(input: BookSourceInput): string {
389
470
  ' diagrams, then each part and its chapters, then the appendices.',
390
471
  "- `explico.cls` — explico's book class: layout, numbering, and the",
391
472
  ' environments the chapters use (memos, notes, worked examples, checks).',
473
+ "- `explico-common.sty` — what the class shares with explico's slides: fonts,",
474
+ ' colours, figures, diagrams and the notation markers.',
392
475
  "- `parts/NN-<part>.tex` — a part's opening page and text;",
393
476
  ' `parts/NN-<part>/MM-<chapter>.tex` — its chapters.',
394
477
  '- `backmatter/` — the notation and abbreviation appendices.',
@@ -410,7 +493,6 @@ export function bookSource(input: BookSourceInput): SourceFile[] {
410
493
  const { manifest, parts } = input;
411
494
  const canonicalBaseUrl = manifest.lessonExports.canonicalBaseUrl;
412
495
  const course = manifest.about.material.name;
413
- const version = manifest.about.knowledge.version;
414
496
  const documentRoutes = new Set(
415
497
  parts.flatMap(({ overview, chapters }) => [
416
498
  ...(overview ? [overview.route] : []),
@@ -422,7 +504,7 @@ export function bookSource(input: BookSourceInput): SourceFile[] {
422
504
  const common = { documentRoutes, canonicalBaseUrl, inBook: true, notationTargets } as const;
423
505
  const files: SourceFile[] = [];
424
506
  const inputs: string[] = [];
425
- for (const { part, overview, chapters } of parts) {
507
+ for (const { part, overview, chapters, slides = [] } of parts) {
426
508
  const partFile = `parts/${twoDigits(part.ordinal)}-${part.dir}.tex`;
427
509
  files.push({
428
510
  path: partFile,
@@ -431,7 +513,18 @@ export function bookSource(input: BookSourceInput): SourceFile[] {
431
513
  : `\\ExplicoPart{${part.ordinal}}{${escapeText(part.label)}}\n`,
432
514
  });
433
515
  inputs.push(partFile);
516
+ // A slides-first lesson keeps its chapter number with a short chapter of its own.
517
+ const pending = [...slides].sort((a, b) => a.lesson.chapter - b.lesson.chapter);
518
+ const writeSlides = (before: number) => {
519
+ while (pending.length > 0 && pending[0]!.lesson.chapter < before) {
520
+ const entry = pending.shift()!;
521
+ const file = chapterPath(manifest, entry.lesson);
522
+ files.push({ path: file, content: slidesChapter(entry) });
523
+ inputs.push(file);
524
+ }
525
+ };
434
526
  for (const { lesson, page, pictures } of chapters) {
527
+ writeSlides(lesson.chapter);
435
528
  const file = chapterPath(manifest, lesson);
436
529
  const url = canonicalBaseUrl ? `${canonicalBaseUrl}${lesson.slug}/` : undefined;
437
530
  files.push({
@@ -441,12 +534,14 @@ export function bookSource(input: BookSourceInput): SourceFile[] {
441
534
  page,
442
535
  pictures: new Map([...pictures].map(([anchor, picture]) => [anchor, picture.path])),
443
536
  checkItems: checkItems(manifest, lesson),
444
- source: { course, version, ...(url ? { url } : {}) },
537
+ // In a book the line is the URL alone; the title page carries the version.
538
+ source: { course, ...(url ? { url } : {}) },
445
539
  }),
446
540
  });
447
541
  inputs.push(file);
448
542
  files.push(...[...pictures.values()].map((picture) => ({ path: picture.path, content: picture.content })));
449
543
  }
544
+ writeSlides(Number.POSITIVE_INFINITY);
450
545
  }
451
546
  const appendices: string[] = [];
452
547
  const notation = serializeNotationAppendix(appendixNotation(manifest, parts), {
@@ -474,6 +569,7 @@ export function bookSource(input: BookSourceInput): SourceFile[] {
474
569
  { path: 'latexmkrc', content: LATEXMKRC },
475
570
  { path: 'main.tex', content: bookMainTex(input, inputs, appendices) },
476
571
  { path: 'explico.cls', content: explicoClass() },
572
+ { path: 'explico-common.sty', content: explicoCommon() },
477
573
  ...files,
478
574
  ]);
479
575
  }
@@ -0,0 +1,198 @@
1
+ %% explico-common.sty — what every LaTeX document explico writes shares: its
2
+ %% book class (`explico.cls`) and its deck theme (`beamerthemeexplico.sty`).
3
+ %%
4
+ %% The engine, the fonts and the Unicode characters, the colours, the
5
+ %% figures and tables, the TikZ styles of a diagram, the box style, the
6
+ %% notation markers (\explain, \slot) and the cross-references. Edit it
7
+ %% freely in a download.
8
+ \NeedsTeXFormat{LaTeX2e}
9
+ \ProvidesPackage{explico-common}[2026/10/10 explico shared definitions]
10
+
11
+ %% `scripts`: the document holds Greek, Cyrillic, CJK or emoji (the exporter
12
+ %% sets it when it does), so LuaLaTeX loads the fallback fonts below.
13
+ \newif\ifexplico@scripts
14
+ \DeclareOption{scripts}{\explico@scriptstrue}
15
+ \ProcessOptions\relax
16
+
17
+ %% --- Engine and fonts ------------------------------------------------------
18
+ \RequirePackage{iftex}
19
+ \RequirePackage{amsmath,amssymb,mathtools}
20
+ %% Rule N17's engine half, as on the website (src/reference/katex-delimiters.mjs):
21
+ %% a negative \delimitershortfall makes every \left...\right pair strictly taller
22
+ %% than its content, so a nest grows outward at each level. TeX's default (5pt)
23
+ %% lets nested pairs round to the same glyph.
24
+ \delimitershortfall=-1pt
25
+ %% Whether a text font sets Greek; if none does, a Greek letter in prose is
26
+ %% set as its math symbol (below, with the other Unicode characters).
27
+ \newif\ifexplico@greek
28
+ \ifPDFTeX
29
+ \RequirePackage[T1]{fontenc}
30
+ \RequirePackage{lmodern}
31
+ \RequirePackage{textcomp}
32
+ \RequirePackage{bm}
33
+ %% A character no pdfTeX font sets (accented Greek, Chinese, an emoji) stays
34
+ %% blank instead of stopping the compile, and the log says so in LuaLaTeX's
35
+ %% words, which the export turns into its warning about blank characters.
36
+ \def\UTFviii@undefined@err#1{%
37
+ \typeout{Missing character: There is no
38
+ \expandafter\UTFviii@splitcsname\string#1\relax\space in font pdftex!}}
39
+ \else
40
+ %% Latin Modern for text and math. unicode-math is also what lets a tagged
41
+ %% PDF carry MathML for every formula (luamml needs it); bm does not work
42
+ %% with it, so \bm becomes \boldsymbol.
43
+ \RequirePackage{unicode-math}
44
+ \providecommand{\bm}{\boldsymbol}
45
+ %% Latin Modern has no Greek, Cyrillic, Chinese, Japanese, Korean or emoji.
46
+ %% With `scripts`, prose in those scripts comes from fallback fonts: for each
47
+ %% script, the first of the fonts below this machine has (CMU Serif, from
48
+ %% TeX Live's cm-unicode, matches Latin Modern). A script none of them
49
+ %% covers stays blank, and the export warns about it.
50
+ \ifexplico@scripts
51
+ \def\explico@fallbacks{}
52
+ \def\explico@fallback#1#2{%
53
+ \let\explico@found\@empty
54
+ \@for\explico@candidate:=#1\do{%
55
+ \ifx\explico@found\@empty
56
+ \IfFontExistsTF{\explico@candidate}{%
57
+ \edef\explico@fallbacks{\explico@fallbacks"\explico@candidate:mode=harf;",}%
58
+ \let\explico@found\explico@candidate}{}%
59
+ \fi}%
60
+ \ifx\explico@found\@empty\else#2\fi}
61
+ \explico@fallback{CMU Serif,Noto Serif,DejaVu Serif,FreeSerif}{\explico@greektrue}
62
+ \explico@fallback{Noto Serif CJK SC,Noto Sans CJK SC,FandolSong,Source Han Serif SC}{}
63
+ \explico@fallback{Noto Color Emoji,Twemoji Mozilla,Symbola}{}
64
+ \ifx\explico@fallbacks\@empty\else
65
+ \directlua{luaotfload.add_fallback("explico", {\explico@fallbacks})}%
66
+ \setmainfont{Latin Modern Roman}[RawFeature={fallback=explico}]%
67
+ \fi
68
+ \fi
69
+ \fi
70
+ \RequirePackage{microtype}
71
+
72
+ %% --- Page and colour --------------------------------------------------------
73
+ \RequirePackage{xcolor}
74
+ \definecolor{explicoaccent}{HTML}{12775C}
75
+ \definecolor{explicoframe}{HTML}{9DB8AD}
76
+ \definecolor{explicoback}{HTML}{F3F8F6}
77
+ \definecolor{explicoasidecaution}{HTML}{B5651D}
78
+ \definecolor{explicoasidecautionback}{HTML}{FBF3EA}
79
+ \definecolor{explicorule}{HTML}{8A9A93}
80
+
81
+ %% --- Floats, tables and captions -------------------------------------------
82
+ \RequirePackage{graphicx}
83
+ \RequirePackage{array,booktabs,tabularx}
84
+ \RequirePackage[export]{adjustbox}
85
+ %% explico's short caption, run in bold before the long one.
86
+ \newcommand*{\ExplicoCaptionTitle}[1]{\textbf{#1}}
87
+
88
+ %% --- Diagrams ---------------------------------------------------------------
89
+ \RequirePackage{tikz}
90
+ \usetikzlibrary{arrows.meta}
91
+ \tikzset{
92
+ explico diagram node/.style={
93
+ draw=explicorule, fill=explicoback, rounded corners=3pt, line width=0.5pt,
94
+ align=center, inner sep=3pt, font=\small,
95
+ },
96
+ explico diagram edge/.style={
97
+ draw=explicorule, line width=0.6pt, -{Stealth[length=5pt,width=4pt]},
98
+ },
99
+ }
100
+
101
+ %% --- Boxes: memos, notes, asides, worked examples, checks ------------------
102
+ \RequirePackage[most]{tcolorbox}
103
+ \tcbset{
104
+ explico/.style={
105
+ enhanced, breakable, colback=explicoback, colframe=explicoframe,
106
+ boxrule=0.5pt, arc=2pt, left=6pt, right=6pt, top=4pt, bottom=4pt,
107
+ fonttitle=\bfseries\small, coltitle=black, colbacktitle=explicoback!60!white,
108
+ titlerule=0.4pt, before skip=10pt plus 2pt, after skip=10pt plus 2pt,
109
+ },
110
+ }
111
+
112
+ %% --- Unicode in text that the default fonts lack ---------------------------
113
+ \RequirePackage{newunicodechar}
114
+ \newunicodechar{°}{\ifmmode^\circ\else\textdegree\fi}
115
+ \newunicodechar{−}{\ensuremath{-}}
116
+ \newunicodechar{×}{\ensuremath{\times}}
117
+ \newunicodechar{±}{\ensuremath{\pm}}
118
+ \newunicodechar{≤}{\ensuremath{\le}}
119
+ \newunicodechar{≥}{\ensuremath{\ge}}
120
+ \newunicodechar{≈}{\ensuremath{\approx}}
121
+ \newunicodechar{≠}{\ensuremath{\neq}}
122
+ \newunicodechar{→}{\ensuremath{\rightarrow}}
123
+ \newunicodechar{←}{\ensuremath{\leftarrow}}
124
+ \newunicodechar{↑}{\ensuremath{\uparrow}}
125
+ \newunicodechar{↓}{\ensuremath{\downarrow}}
126
+ \newunicodechar{⟨}{\ensuremath{\langle}}
127
+ \newunicodechar{⟩}{\ensuremath{\rangle}}
128
+ \newunicodechar{⋮}{\ensuremath{\vdots}}
129
+ \newunicodechar{√}{\ensuremath{\surd}}
130
+ \newunicodechar{∞}{\ensuremath{\infty}}
131
+ \newunicodechar{′}{\ensuremath{'}}
132
+ \newunicodechar{𝒩}{\ensuremath{\mathcal{N}}}
133
+ \newunicodechar{①}{\textcircled{\scriptsize 1}}
134
+ \newunicodechar{②}{\textcircled{\scriptsize 2}}
135
+ \newunicodechar{③}{\textcircled{\scriptsize 3}}
136
+ \newunicodechar{④}{\textcircled{\scriptsize 4}}
137
+ \newunicodechar{⑤}{\textcircled{\scriptsize 5}}
138
+ \newunicodechar{₀}{\textsubscript{0}}
139
+ \newunicodechar{₁}{\textsubscript{1}}
140
+ \newunicodechar{₂}{\textsubscript{2}}
141
+ \newunicodechar{₃}{\textsubscript{3}}
142
+ \newunicodechar{¹}{\textsuperscript{1}}
143
+ \newunicodechar{²}{\textsuperscript{2}}
144
+ \newunicodechar{³}{\textsuperscript{3}}
145
+ %% A Greek letter on its own is a symbol (the polar coordinates ρ and φ in a
146
+ %% caption, which holds no math): it is set as its math symbol. Inside a Greek
147
+ %% word, which the exporter marks \ExplicoGreekWord{…}, it is a letter, set
148
+ %% from the text font's Greek where one is loaded (`scripts`).
149
+ \newif\ifexplico@word
150
+ \DeclareRobustCommand{\ExplicoGreekWord}[1]{{\explico@wordtrue#1}}
151
+ %% Protected, as \ensuremath is: the text of an equation is expanded for its
152
+ %% tagging (alt text, MathML), and an expandable body breaks it there.
153
+ \protected\def\explico@greekletter#1#2{%
154
+ \ifmmode#2\else
155
+ \ifexplico@word
156
+ \ifexplico@greek\char#1\relax\else\ensuremath{#2}\fi
157
+ \else\ensuremath{#2}\fi
158
+ \fi}
159
+ \newunicodechar{α}{\explico@greekletter{"03B1}{\alpha}}
160
+ \newunicodechar{β}{\explico@greekletter{"03B2}{\beta}}
161
+ \newunicodechar{γ}{\explico@greekletter{"03B3}{\gamma}}
162
+ \newunicodechar{δ}{\explico@greekletter{"03B4}{\delta}}
163
+ \newunicodechar{ε}{\explico@greekletter{"03B5}{\varepsilon}}
164
+ \newunicodechar{θ}{\explico@greekletter{"03B8}{\theta}}
165
+ \newunicodechar{λ}{\explico@greekletter{"03BB}{\lambda}}
166
+ \newunicodechar{μ}{\explico@greekletter{"03BC}{\mu}}
167
+ \newunicodechar{π}{\explico@greekletter{"03C0}{\pi}}
168
+ \newunicodechar{ρ}{\explico@greekletter{"03C1}{\rho}}
169
+ \newunicodechar{σ}{\explico@greekletter{"03C3}{\sigma}}
170
+ \newunicodechar{τ}{\explico@greekletter{"03C4}{\tau}}
171
+ \newunicodechar{φ}{\explico@greekletter{"03C6}{\varphi}}
172
+ \newunicodechar{ω}{\explico@greekletter{"03C9}{\omega}}
173
+ \newunicodechar{Γ}{\explico@greekletter{"0393}{\Gamma}}
174
+ \newunicodechar{Δ}{\explico@greekletter{"0394}{\Delta}}
175
+ \newunicodechar{Θ}{\explico@greekletter{"0398}{\Theta}}
176
+ \newunicodechar{Λ}{\explico@greekletter{"039B}{\Lambda}}
177
+ \newunicodechar{Π}{\explico@greekletter{"03A0}{\Pi}}
178
+ \newunicodechar{Σ}{\explico@greekletter{"03A3}{\Sigma}}
179
+ \newunicodechar{Φ}{\explico@greekletter{"03A6}{\Phi}}
180
+ \newunicodechar{Ω}{\explico@greekletter{"03A9}{\Omega}}
181
+
182
+ %% --- Notation ---------------------------------------------------------------
183
+ %% \explain{key}{tex} and \slot{key}{tex} are explico's markers in math: the
184
+ %% website opens a card for the key; on paper they print their content.
185
+ \providecommand{\explain}[2]{#2}
186
+ \providecommand{\slot}[2]{#2}
187
+ %% \ExplicoNotation{target}{tex}: a formula's symbol that links to its notation entry
188
+ %% (the serializer writes it only where the book has that entry).
189
+ \providecommand{\ExplicoNotation}[2]{\hyperlink{#1}{#2}}
190
+ %% A term in prose with a notation card on the website.
191
+ \newcommand{\ExplicoTerm}[2]{#2}
192
+
193
+ %% --- Cross-references ---------------------------------------------------------
194
+ %% #1 label, #2 text before the number, #3 text after it.
195
+ \newcommand{\ExplicoRef}[3]{\hyperref[#1]{#2\ref*{#1}#3}}
196
+ \newcommand{\ExplicoEqRef}[1]{\hyperref[#1]{(\ref*{#1})}}
197
+
198
+ \endinput