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
@@ -3,9 +3,11 @@ import { basename, dirname, extname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import type { Manifest, ManifestLesson, ManifestPart } from '../manifest';
6
- import { courseFileStem } from './export-pages';
6
+ import { newestSince, versionPhrase } from '../page-versions';
7
+ import { courseFileStem, deckFileStem, deckOwnerSlug, deckSaveName } from './export-pages';
7
8
  import {
8
9
  consoleLogger,
10
+ deckBlocks,
9
11
  diagramsOf,
10
12
  figuresOf,
11
13
  imageExtension,
@@ -18,8 +20,9 @@ import {
18
20
  type PreparedChapters,
19
21
  } from './export-chapters';
20
22
  import type { CapturedFigure } from './latex/capture';
21
- import { checkItems } from './latex/document';
22
- import { serializeMarkdownNodes, serializeMarkdownPage } from './markdown/serialize';
23
+ import { deckSymbols } from './latex/deck';
24
+ import { checkItems, deckCheckItems } from './latex/document';
25
+ import { serializeMarkdownNodes, serializeMarkdownPage, serializeMarpDeck } from './markdown/serialize';
23
26
  import type { StaticPage } from './static-tree';
24
27
  import { zip, type ZipEntry } from './zip';
25
28
 
@@ -33,6 +36,8 @@ import { zip, type ZipEntry } from './zip';
33
36
  * contents), one file per chapter, and shared `assets/`.
34
37
  * - `downloads/course-markdown.zip`: `index.md` (the course and its
35
38
  * contents), each part's page, one file per chapter, and `assets/`.
39
+ * - `downloads/<lesson>-slides-markdown.zip`: a deck as Marp's Markdown,
40
+ * `<name>-slides-markdown/slides.md` and its `assets/`.
36
41
  *
37
42
  * A figure a browser draws is a PNG at two device pixels per CSS pixel; an
38
43
  * image is the site's own file; a `<Diagram>` is a PNG with its boxes and
@@ -112,16 +117,22 @@ export async function writeMarkdownExports(
112
117
  if (!manifest.lessonExports.markdown) return;
113
118
  const logger = options.logger ?? consoleLogger;
114
119
  const outputRoot = fileURLToPath(directory);
115
- const { base, chapters, captures } =
120
+ const { base, chapters, decks, captures } =
116
121
  options.prepared ?? (await prepareChapters(outputRoot, manifest, { logger, projectRoot: options.projectRoot }));
117
122
  const canonicalBaseUrl = manifest.lessonExports.canonicalBaseUrl;
118
123
  const course = manifest.about.material.name;
119
- const version = manifest.about.knowledge.version;
120
- const source = (route: string) => ({
124
+ const current = manifest.about.knowledge.version;
125
+ // The line under a title: the version the document's content is at (rule C5).
126
+ const source = (route: string, since: string | null) => ({
121
127
  course,
122
- version,
128
+ since: versionPhrase(since, current),
123
129
  ...(canonicalBaseUrl ? { url: `${canonicalBaseUrl}${route ? `${route}/` : ''}` } : {}),
124
130
  });
131
+ const partSince = (entry: { part: ManifestPart; chapters: readonly { lesson: ManifestLesson }[] }) =>
132
+ newestSince([
133
+ ...(entry.part.overview ? [entry.part.overview.since] : []),
134
+ ...entry.chapters.map(({ lesson }) => lesson.since),
135
+ ]);
125
136
 
126
137
  /** One chapter's Markdown and pictures, seen from `documentFiles`. */
127
138
  const chapterFiles = async (
@@ -139,7 +150,7 @@ export async function writeMarkdownExports(
139
150
  ...(canonicalBaseUrl ? { canonicalBaseUrl } : {}),
140
151
  pictures: new Map([...pictures].map(([key, picture]) => [key, picture.path])),
141
152
  checkItems: checkItems(manifest, lesson),
142
- source: source(page.route),
153
+ source: source(page.route, lesson.since),
143
154
  },
144
155
  { number: `${lesson.part}.${lesson.chapter}` },
145
156
  );
@@ -168,6 +179,40 @@ export async function writeMarkdownExports(
168
179
  );
169
180
  }
170
181
 
182
+ // Each deck as Marp's Markdown: a figure or diagram it recalls is its chapter's picture.
183
+ for (const { deck, page, figureRoutes } of decks) {
184
+ if (deck.part === undefined && !manifest.lessons.some((entry) => entry.id === deck.lessonId)) continue;
185
+ const owner = deckOwnerSlug(deck, manifest);
186
+ const captured = new Map<string, CapturedFigure>();
187
+ for (const [anchor, route] of figureRoutes) {
188
+ const figure = captures.get(route)?.get(anchor);
189
+ if (figure) captured.set(anchor, figure);
190
+ }
191
+ const blocks = [...deckBlocks(page), ...page.frames.flatMap((frame) => frame.notes)];
192
+ const pictures = await pagePictures(
193
+ outputRoot,
194
+ base,
195
+ { slug: page.slug, route: page.route, title: page.title, blocks, notation: [], notationRemarks: [] },
196
+ captured,
197
+ 'assets',
198
+ );
199
+ const items = deckCheckItems(manifest, deck);
200
+ const markdown = serializeMarpDeck({
201
+ deck: page,
202
+ documentFiles: new Map([[page.route, '']]),
203
+ ...(canonicalBaseUrl ? { canonicalBaseUrl } : {}),
204
+ pictures: new Map([...pictures].map(([key, picture]) => [key, picture.path])),
205
+ checkItems: items,
206
+ source: source(page.route, deck.since),
207
+ symbols: deckSymbols({ deck: page, documentRoutes: new Set([page.route]), pictures: new Map(), checkItems: items }),
208
+ size: manifest.deck.aspectRatio === '4:3' ? '4:3' : '16:9',
209
+ });
210
+ pack(`${deckFileStem(owner)}-markdown.zip`, `${deckSaveName(owner)}-markdown`, [
211
+ { path: 'slides.md', content: markdown },
212
+ ...[...pictures.values()].map((picture) => ({ path: picture.path, content: picture.content })),
213
+ ]);
214
+ }
215
+
171
216
  const byId = new Map(chapters.map((chapter) => [chapter.lesson.id, chapter]));
172
217
  const pageSlugs = new Set(manifest.pages.map((page) => page.slug));
173
218
  const parts = manifest.parts
@@ -196,7 +241,7 @@ export async function writeMarkdownExports(
196
241
  ...(canonicalBaseUrl ? { canonicalBaseUrl } : {}),
197
242
  pictures: new Map([...pictures].map(([key, picture]) => [key, picture.path])),
198
243
  checkItems: new Map(),
199
- source: source(page.route),
244
+ source: source(page.route, partSince(entry)),
200
245
  },
201
246
  { number: `Part ${entry.part.ordinal}:` },
202
247
  );
@@ -268,7 +313,7 @@ export async function writeMarkdownExports(
268
313
  {
269
314
  type: 'paragraph',
270
315
  children: [
271
- { type: 'text', value: `Content version ${version}.` },
316
+ { type: 'text', value: `Content ${versionPhrase(newestSince(parts.map(partSince)), current)}.` },
272
317
  ...(canonicalBaseUrl
273
318
  ? [
274
319
  { type: 'text' as const, value: ' Interactive version: ' },
@@ -322,7 +367,7 @@ export async function writeMarkdownExports(
322
367
  await Promise.all(pending.map((file) => rm(`${file.target}.tmp-${process.pid}`, { force: true })));
323
368
  }
324
369
  logger.info(
325
- `Markdown export: ${chapters.length} chapter(s), ${parts.length} part(s)` +
370
+ `Markdown export: ${chapters.length} chapter(s), ${decks.length} deck(s), ${parts.length} part(s)` +
326
371
  (parts.length > 0 ? ' and the course' : '') +
327
372
  '.',
328
373
  );
@@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url';
12
12
  import { parse, serialize } from 'parse5';
13
13
 
14
14
  import type { Manifest } from '../manifest';
15
- import { exportPageRoute, standalonePageSlugs } from './export-pages';
15
+ import { exportPageRoute, standaloneDownloads } from './export-pages';
16
16
  import {
17
17
  attribute,
18
18
  elements,
@@ -910,10 +910,11 @@ export async function pageSourceFile(outputRoot: string, slug: string): Promise<
910
910
  }
911
911
 
912
912
  /**
913
- * Emit the opt-in standalone HTML artifact of every chapter
914
- * (`standalonePageSlugs`) from Astro's completed static output, at
915
- * `downloads/<docs slug>.html`. A page that is not a chapter is in its
916
- * part's and the course's files (`combined-html.ts`).
913
+ * Emit the opt-in standalone HTML artifact of every chapter and deck
914
+ * (`standaloneDownloads`) from Astro's completed static output, at
915
+ * `downloads/<docs slug>.html` and `downloads/<lesson>-slides.html`. A page
916
+ * that is not a chapter is in its part's and the course's files
917
+ * (`combined-html.ts`).
917
918
  */
918
919
  export async function writeStandaloneLessonExports(
919
920
  directory: URL,
@@ -922,9 +923,9 @@ export async function writeStandaloneLessonExports(
922
923
  if (!manifest.lessonExports.standaloneHtml) return;
923
924
  const outputRoot = fileURLToPath(directory);
924
925
  const pending: { target: string; temporary: string; html: string }[] = [];
925
- for (const slug of standalonePageSlugs(manifest)) {
926
+ for (const { slug, download } of standaloneDownloads(manifest)) {
926
927
  const sourceFile = await pageSourceFile(outputRoot, slug);
927
- const target = join(outputRoot, 'downloads', `${slug}.html`);
928
+ const target = join(outputRoot, 'downloads', `${download}.html`);
928
929
  pending.push({
929
930
  target,
930
931
  temporary: `${target}.tmp-${process.pid}`,
@@ -130,6 +130,8 @@ export type Block =
130
130
  readonly ordered: boolean;
131
131
  readonly start?: number;
132
132
  readonly items: readonly (readonly Block[])[];
133
+ /** On a deck, the step each item appears at (1 for the first), when an item is a step of its own. */
134
+ readonly itemSteps?: readonly number[];
133
135
  }
134
136
  | {
135
137
  readonly kind: 'equation';
@@ -137,6 +139,8 @@ export type Block =
137
139
  readonly number: string;
138
140
  readonly key?: string;
139
141
  readonly tex: string;
142
+ /** On a deck, a recalled equation's place in its chapter. */
143
+ readonly recall?: string;
140
144
  }
141
145
  | { readonly kind: 'displayMath'; readonly tex: string }
142
146
  | (FloatBase & {
@@ -694,6 +698,7 @@ function readBlock(node: HtmlElement, context: ReadContext): Block[] | undefined
694
698
  number: attribute(node, 'data-eq-number') ?? '',
695
699
  ...(key ? { key } : {}),
696
700
  tex: (katex && texOf(katex)) ?? '',
701
+ ...(attribute(node, 'data-eq-recall') ? { recall: attribute(node, 'data-eq-recall')! } : {}),
697
702
  },
698
703
  ];
699
704
  }
@@ -788,6 +793,19 @@ function readBlock(node: HtmlElement, context: ReadContext): Block[] | undefined
788
793
  if (node.tagName === 'compact-examples' || hasClass(node, 'compact-examples')) {
789
794
  return [readExamples(node, context)];
790
795
  }
796
+ // A deck's question frame labels its check instead of heading it.
797
+ if (hasClass(node, 'deck-check__label') && attribute(node, 'data-block-kind') === 'check') {
798
+ const anchor = attribute(node, 'id') ?? '';
799
+ return [
800
+ {
801
+ kind: 'check',
802
+ anchor,
803
+ number: attribute(node, 'data-check-number') ?? '',
804
+ assessmentId: anchor.replace(/^check-/u, ''),
805
+ title: [{ kind: 'text', value: attribute(node, 'data-check-title') ?? '' }],
806
+ },
807
+ ];
808
+ }
791
809
  if (hasClass(node, 'sl-heading-wrapper')) {
792
810
  const heading = headingOf(node);
793
811
  if (!heading) return readBlocks(node, context);
@@ -918,6 +936,22 @@ export function readStaticPage(html: string, slug: string, route: string): Stati
918
936
  const title = elements(main, (node) => node.tagName === 'h1')[0];
919
937
  const content = elements(main, (node) => hasClass(node, 'sl-markdown-content'))[0];
920
938
  if (!content) throw new Error(`Built page ${slug} has no .sl-markdown-content element.`);
939
+ const { notation, notationRemarks } = readPageNotation(main, context);
940
+ return {
941
+ slug,
942
+ route,
943
+ title: title ? collapse(readableText(title)).trim() : slug,
944
+ blocks: readBlocks(content, context),
945
+ notation,
946
+ notationRemarks,
947
+ };
948
+ }
949
+
950
+ /** The page's notation list (its notation layer's entries) and its page-specific remarks. */
951
+ function readPageNotation(
952
+ main: HtmlElement,
953
+ context: ReadContext,
954
+ ): Pick<StaticPage, 'notation' | 'notationRemarks'> {
921
955
  const layer = elements(
922
956
  main,
923
957
  (node) => hasClass(node, 'notation-layer') && attribute(node, 'data-notation-layer-id') === 'page-notation',
@@ -933,12 +967,150 @@ export function readStaticPage(html: string, slug: string, route: string): Stati
933
967
  .map((item) => inlineContent(item, context)),
934
968
  )
935
969
  : [];
970
+ return { notation: entries.map((entry) => readNotationEntry(entry, context)), notationRemarks: remarks };
971
+ }
972
+
973
+ // --- decks ------------------------------------------------------------------
974
+
975
+ /** One frame of a deck page (docs/authoring/deck.md), as print exports read it. */
976
+ export interface DeckFrame {
977
+ readonly number: number;
978
+ readonly kind: 'title' | 'section' | 'frame' | 'outcomes' | 'references';
979
+ /** The frame heading's anchor. */
980
+ readonly anchor?: string;
981
+ readonly title: readonly Inline[];
982
+ /** The frame's content, a run of blocks per step, in reading order: step 1 is what shows first. */
983
+ readonly segments: readonly { readonly step: number; readonly blocks: readonly Block[] }[];
984
+ readonly steps: number;
985
+ readonly notes: readonly Block[];
986
+ /** The legend line: each symbol's TeX and its name. */
987
+ readonly legend: readonly { readonly tex: string; readonly label: readonly Inline[] }[];
988
+ }
989
+
990
+ export interface StaticDeck {
991
+ readonly slug: string;
992
+ readonly route: string;
993
+ readonly title: string;
994
+ readonly frames: readonly DeckFrame[];
995
+ /**
996
+ * The page's notation list: its lesson's symbols, its chapter's included.
997
+ * The PDF's appendix keeps the ones its frames use.
998
+ */
999
+ readonly notation: readonly NotationEntry[];
1000
+ }
1001
+
1002
+ /** Each item's step, for a list a step mark split, or `undefined` when no item is a step. */
1003
+ function listItemSteps(list: HtmlElement): number[] | undefined {
1004
+ const items = childrenOf(list).filter((child): child is HtmlElement => isElement(child) && child.tagName === 'li');
1005
+ let step = 1;
1006
+ let any = false;
1007
+ const steps = items.map((item) => {
1008
+ const at = attribute(item, 'data-step');
1009
+ if (at !== undefined) {
1010
+ step = Number(at) || step;
1011
+ any = true;
1012
+ }
1013
+ return step;
1014
+ });
1015
+ return any ? steps : undefined;
1016
+ }
1017
+
1018
+ /** A frame body's content, step by step. */
1019
+ function readSegments(body: HtmlElement, context: ReadContext): { step: number; blocks: Block[] }[] {
1020
+ const segments: { step: number; blocks: Block[] }[] = [];
1021
+ const add = (step: number, blocks: readonly Block[]) => {
1022
+ if (blocks.length === 0) return;
1023
+ const last = segments.at(-1);
1024
+ if (last && last.step === step) last.blocks.push(...blocks);
1025
+ else segments.push({ step, blocks: [...blocks] });
1026
+ };
1027
+ let pending: HtmlNode[] = [];
1028
+ const flush = () => {
1029
+ if (pending.length === 0) return;
1030
+ const holder = { nodeName: 'div', tagName: 'div', attrs: [], childNodes: pending } as unknown as HtmlElement;
1031
+ add(1, withItemSteps(holder, readBlocks(holder, context)));
1032
+ pending = [];
1033
+ };
1034
+ for (const child of childrenOf(body)) {
1035
+ if (isElement(child) && hasClass(child, 'deck-step') && attribute(child, 'data-step') !== undefined) {
1036
+ flush();
1037
+ add(Number(attribute(child, 'data-step')) || 1, withItemSteps(child, readBlocks(child, context)));
1038
+ continue;
1039
+ }
1040
+ pending.push(child);
1041
+ }
1042
+ flush();
1043
+ return segments;
1044
+ }
1045
+
1046
+ /** The list blocks of `node`'s own lists, given the step of each item. */
1047
+ function withItemSteps(node: HtmlElement, blocks: Block[]): Block[] {
1048
+ const lists = childrenOf(node).filter(
1049
+ (child): child is HtmlElement => isElement(child) && (child.tagName === 'ul' || child.tagName === 'ol'),
1050
+ );
1051
+ let index = 0;
1052
+ return blocks.map((block) => {
1053
+ if (block.kind !== 'list') return block;
1054
+ const list = lists[index++];
1055
+ const itemSteps = list ? listItemSteps(list) : undefined;
1056
+ return itemSteps ? { ...block, itemSteps } : block;
1057
+ });
1058
+ }
1059
+
1060
+ /** Read the frames of one built deck page. */
1061
+ export function readStaticDeck(html: string, slug: string, route: string): StaticDeck {
1062
+ const document = parse(html);
1063
+ const base = inferBasePrefix(document);
1064
+ const context: ReadContext = { route, base };
1065
+ const main = elements(document, (node) => node.tagName === 'main')[0];
1066
+ if (!main) throw new Error(`Built page ${slug} has no <main> element.`);
1067
+ const deck = elements(main, (node) => hasClass(node, 'deck') && attribute(node, 'data-deck') !== undefined)[0];
1068
+ if (!deck) throw new Error(`Built page ${slug} has no deck (.deck[data-deck]).`);
1069
+ const title = elements(main, (node) => node.tagName === 'h1')[0];
1070
+ const frames = childrenOf(deck)
1071
+ .filter((child): child is HtmlElement => isElement(child) && hasClass(child, 'deck-frame'))
1072
+ .map((frame): DeckFrame => {
1073
+ const canvas = firstByClass(frame, 'deck-canvas');
1074
+ const heading = canvas ? firstByClass(canvas, 'deck-frame__title') ?? firstByClass(canvas, 'deck-title__title') : undefined;
1075
+ const body = canvas ? firstByClass(canvas, 'deck-frame__body') : undefined;
1076
+ const notes = childrenOf(frame).find(
1077
+ (child): child is HtmlElement => isElement(child) && hasClass(child, 'deck-notes'),
1078
+ );
1079
+ const legend = canvas ? firstByClass(canvas, 'deck-legend') : undefined;
1080
+ const kind = (attribute(frame, 'data-kind') ?? 'frame') as DeckFrame['kind'];
1081
+ // A generated frame has no body of its own: its content is the canvas,
1082
+ // less its title and its foot.
1083
+ const generatedContent =
1084
+ !body && canvas
1085
+ ? readBlocks(
1086
+ canvas,
1087
+ context,
1088
+ (child) =>
1089
+ !(isElement(child) && (hasClass(child, 'deck-frame__title') || hasClass(child, 'deck-frame__foot') || hasClass(child, 'deck-title__title'))),
1090
+ )
1091
+ : [];
1092
+ return {
1093
+ number: Number(attribute(frame, 'data-frame')) || 0,
1094
+ kind,
1095
+ ...(heading && attribute(heading, 'id') ? { anchor: attribute(heading, 'id')! } : {}),
1096
+ title: heading ? inlineContent(heading, context) : [],
1097
+ segments: body ? readSegments(body, context) : generatedContent.length > 0 ? [{ step: 1, blocks: generatedContent }] : [],
1098
+ steps: Number(attribute(frame, 'data-steps')) || 1,
1099
+ notes: notes ? readBlocks(notes, context) : [],
1100
+ legend: legend
1101
+ ? elements(legend, (node) => hasClass(node, 'deck-legend__item')).map((item) => {
1102
+ const katex = elements(item, (node) => hasClass(node, 'katex'))[0];
1103
+ const label = firstByClass(item, 'deck-legend__label');
1104
+ return { tex: (katex && texOf(katex)) ?? '', label: label ? inlineContent(label, context) : [] };
1105
+ })
1106
+ : [],
1107
+ };
1108
+ });
936
1109
  return {
937
1110
  slug,
938
1111
  route,
939
1112
  title: title ? collapse(readableText(title)).trim() : slug,
940
- blocks: readBlocks(content, context),
941
- notation: entries.map((entry) => readNotationEntry(entry, context)),
942
- notationRemarks: remarks,
1113
+ frames,
1114
+ notation: readPageNotation(main, context).notation,
943
1115
  };
944
1116
  }
@@ -23,10 +23,18 @@ export interface KnowledgeChangelogEntry {
23
23
  readonly notes: readonly string[];
24
24
  }
25
25
 
26
+ /** The released entries and, apart from them, the bullets `## [Unreleased]` holds. */
27
+ export interface KnowledgeChangelog {
28
+ readonly entries: readonly KnowledgeChangelogEntry[];
29
+ /** Bullet lines under `## [Unreleased]`, any `###` group included, marker stripped. */
30
+ readonly unreleased: readonly string[];
31
+ }
32
+
26
33
  export type KnowledgeVersionCode =
27
34
  | 'knowledge-changelog-missing'
28
35
  | 'knowledge-version-invalid'
29
- | 'knowledge-version-mismatch';
36
+ | 'knowledge-version-mismatch'
37
+ | 'page-versions-invalid';
30
38
 
31
39
  export interface KnowledgeVersionDiagnostic {
32
40
  readonly code: KnowledgeVersionCode;
@@ -41,6 +49,15 @@ export interface KnowledgeVersion {
41
49
  readonly version: string;
42
50
  /** The newest released `## [x.y.z]` entry, when the changelog has one. */
43
51
  readonly latest?: KnowledgeChangelogEntry;
52
+ /** Every released entry, newest first; empty without a changelog. */
53
+ readonly entries: readonly KnowledgeChangelogEntry[];
54
+ /**
55
+ * The bullets `## [Unreleased]` holds: a content change not yet released
56
+ * (rule C1). A page changed while there are any is "not yet released"
57
+ * (rule C5); with none, a change is the release's own, or one rule C1 lets
58
+ * go unlisted.
59
+ */
60
+ readonly unreleased: readonly string[];
44
61
  readonly diagnostics: readonly KnowledgeVersionDiagnostic[];
45
62
  }
46
63
 
@@ -56,29 +73,32 @@ export function isKnowledgeSemver(value: string): boolean {
56
73
  }
57
74
 
58
75
  /**
59
- * Every released entry in a `content/CHANGELOG.md` body, in file order (newest
60
- * first, by Keep a Changelog convention). `## [Unreleased]` is skipped.
76
+ * A `content/CHANGELOG.md` body: every released entry in file order (newest
77
+ * first, by Keep a Changelog convention), and the bullets of
78
+ * `## [Unreleased]`. A released entry's notes end at a `###` group heading,
79
+ * as the About panel has always shown them; the unreleased bullets are
80
+ * counted through every group, since what they answer is whether anything
81
+ * is waiting for a release.
61
82
  */
62
- export function parseKnowledgeChangelog(
63
- text: string,
64
- ): KnowledgeChangelogEntry[] {
83
+ export function scanKnowledgeChangelog(text: string): KnowledgeChangelog {
65
84
  const lines = text.split(/\r?\n/);
66
85
  const entries: {
67
86
  version: string;
68
87
  date?: string;
69
88
  notes: string[];
70
89
  }[] = [];
90
+ const unreleased: string[] = [];
71
91
  let current: { version: string; date?: string; notes: string[] } | null =
72
92
  null;
93
+ let inUnreleased = false;
73
94
 
74
95
  for (const line of lines) {
75
96
  const heading = HEADING_RE.exec(line);
76
97
  if (heading) {
77
98
  const token = (heading[1] ?? '').trim();
78
- if (/^unreleased$/i.test(token)) {
79
- current = null;
80
- continue;
81
- }
99
+ current = null;
100
+ inUnreleased = /^unreleased$/i.test(token);
101
+ if (inUnreleased) continue;
82
102
  current = { version: token, notes: [] };
83
103
  const date = (heading[2] ?? '').trim();
84
104
  if (date) current.date = date;
@@ -88,18 +108,33 @@ export function parseKnowledgeChangelog(
88
108
  // A top-level `#` or a deeper `###` heading ends the current entry's notes.
89
109
  if (/^#{1,3}\s/.test(line) && !line.startsWith('## [')) {
90
110
  current = null;
111
+ if (/^#{1,2}\s/.test(line)) inUnreleased = false;
91
112
  continue;
92
113
  }
93
- if (!current) continue;
94
114
  const bullet = BULLET_RE.exec(line);
95
- if (bullet) current.notes.push(bullet[1] ?? '');
115
+ if (!bullet) continue;
116
+ if (current) current.notes.push(bullet[1] ?? '');
117
+ else if (inUnreleased) unreleased.push(bullet[1] ?? '');
96
118
  }
97
119
 
98
- return entries.map((entry) => ({
99
- version: entry.version,
100
- ...(entry.date === undefined ? {} : { date: entry.date }),
101
- notes: [...entry.notes],
102
- }));
120
+ return {
121
+ entries: entries.map((entry) => ({
122
+ version: entry.version,
123
+ ...(entry.date === undefined ? {} : { date: entry.date }),
124
+ notes: [...entry.notes],
125
+ })),
126
+ unreleased,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Every released entry in a `content/CHANGELOG.md` body, in file order (newest
132
+ * first, by Keep a Changelog convention). `## [Unreleased]` is skipped.
133
+ */
134
+ export function parseKnowledgeChangelog(
135
+ text: string,
136
+ ): KnowledgeChangelogEntry[] {
137
+ return [...scanKnowledgeChangelog(text).entries];
103
138
  }
104
139
 
105
140
  /**
@@ -133,17 +168,17 @@ export function resolveKnowledgeVersion(options: {
133
168
  'knowledge-changelog-missing',
134
169
  `No ${file}. Add a Keep a Changelog file whose newest \`## [x.y.z]\` heading is ${JSON.stringify(declaredVersion)} (rule C1).`,
135
170
  );
136
- return { version: declaredVersion, diagnostics };
171
+ return { version: declaredVersion, entries: [], unreleased: [], diagnostics };
137
172
  }
138
173
 
139
- const entries = parseKnowledgeChangelog(changelogText);
174
+ const { entries, unreleased } = scanKnowledgeChangelog(changelogText);
140
175
  const latest = entries[0];
141
176
  if (!latest) {
142
177
  warn(
143
178
  'knowledge-changelog-missing',
144
179
  `${file} has no released \`## [x.y.z]\` entry — only \`## [Unreleased]\`. Cut a release: move it to \`## [${declaredVersion}] - <date>\` (rule C3).`,
145
180
  );
146
- return { version: declaredVersion, diagnostics };
181
+ return { version: declaredVersion, entries, unreleased, diagnostics };
147
182
  }
148
183
 
149
184
  if (isKnowledgeSemver(declaredVersion) && latest.version !== declaredVersion) {
@@ -153,5 +188,5 @@ export function resolveKnowledgeVersion(options: {
153
188
  );
154
189
  }
155
190
 
156
- return { version: declaredVersion, latest, diagnostics };
191
+ return { version: declaredVersion, latest, entries, unreleased, diagnostics };
157
192
  }