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
@@ -51,11 +51,19 @@ import { indexEntryFormula } from '../reference/derived-notation.mjs';
51
51
  import { isIndexEntryLatex } from './content-schemas';
52
52
  import type { ContentRecord } from '../reference/gate-retired-keys';
53
53
  import {
54
+ deckLessonSlug,
55
+ deckPageId,
56
+ deckRouteSlug,
54
57
  deriveRequires,
55
58
  deriveSources,
59
+ isDeckSlug,
56
60
  isLessonSlug,
61
+ isPartDeckSlug,
57
62
  lessonIdFromSlug,
58
63
  parseCheckList,
64
+ partDeckDir,
65
+ partDeckPageId,
66
+ partDeckRouteSlug,
59
67
  } from './lesson-derivation';
60
68
 
61
69
  // Every invocation — the CLI, `astro build`/`check`, and vitest — runs with the
@@ -325,14 +333,166 @@ function docSlug(path: string): string {
325
333
  return relative(join(CONTENT_ROOT, 'docs'), path).replace(/\\/g, '/');
326
334
  }
327
335
 
328
- /** Assessment ids from a lesson's colocated `<name>.checks.yml`, or `[]`. */
336
+ /**
337
+ * Assessment ids from a lesson's colocated `<name>.checks.yml`, or `[]`. A
338
+ * deck file (`<name>.deck.mdx`) reads the same file as its chapter: the checks
339
+ * belong to the lesson, whichever edition shows them.
340
+ */
329
341
  function lessonCheckIds(lessonPath: string): string[] {
330
- const checksPath = lessonPath.replace(/\.mdx?$/, '.checks.yml');
342
+ const checksPath = lessonPath.replace(/(?:\.deck)?\.mdx?$/, '.checks.yml');
331
343
  return existsSync(checksPath)
332
344
  ? parseCheckList(readFileSync(checksPath, 'utf8'))
333
345
  : [];
334
346
  }
335
347
 
348
+ /** Every `.md` / `.mdx` file under `content/docs/`, sorted. */
349
+ function docsFiles(): string[] {
350
+ const root = join(CONTENT_ROOT, 'docs');
351
+ return existsSync(root) ? filesBelow(root, ['.md', '.mdx']) : [];
352
+ }
353
+
354
+ /** What every deck file is, whichever it belongs to ({@link DeckFile}). */
355
+ interface DeckFileBase {
356
+ /** Absolute path of the deck file. */
357
+ readonly path: string;
358
+ /** Repository-relative path, the `file` of its diagnostics. */
359
+ readonly file: string;
360
+ /** Docs slug of the deck file, extension included: `motion/range.deck.mdx`. */
361
+ readonly slug: string;
362
+ /** Where the deck is served: `motion/range/slides`, `motion/range` alone, or a part's `motion/slides`. */
363
+ readonly route: string;
364
+ /** The id of the deck's page: `motion.range.slides`, the lesson's id alone, or a part's `motion.slides`. */
365
+ readonly pageId: string;
366
+ /** The part directory the deck sits in: `motion`; empty for a deck outside one. */
367
+ readonly partDir: string;
368
+ /** True when the deck sits in a part's directory, as a lesson does. */
369
+ readonly inPart: boolean;
370
+ /** The deck file's frontmatter. */
371
+ readonly data: Record<string, unknown>;
372
+ readonly body: string;
373
+ /** 1-based file line {@link body} starts on. */
374
+ readonly bodyLine: number;
375
+ }
376
+
377
+ /**
378
+ * A lesson's deck (`<lesson>.deck.mdx`): its slide edition, beside the
379
+ * lesson's chapter or, for a lesson that is only slides, alone. See
380
+ * `docs/authoring/deck.md`.
381
+ */
382
+ export interface LessonDeckFile extends DeckFileBase {
383
+ readonly scope: 'lesson';
384
+ /** The lesson's slug: `motion/range`. */
385
+ readonly lessonSlug: string;
386
+ readonly lessonId: string;
387
+ /** The lesson's chapter file, when it has one. */
388
+ readonly chapterPath?: string;
389
+ }
390
+
391
+ /**
392
+ * A part's deck (`<part>/index.deck.mdx`, beside the part's page): the
393
+ * slides of the part, whose sections may name the chapters they cover. It
394
+ * belongs to no lesson.
395
+ */
396
+ export interface PartDeckFile extends DeckFileBase {
397
+ readonly scope: 'part';
398
+ }
399
+
400
+ /** A deck file: a lesson's slide edition, or a part's deck. */
401
+ export type DeckFile = LessonDeckFile | PartDeckFile;
402
+
403
+ /** A lesson's chapter file, `.mdx` before `.md`, if it has one. */
404
+ function chapterPathOf(lessonSlug: string): string | undefined {
405
+ for (const extension of ['.mdx', '.md']) {
406
+ const path = join(CONTENT_ROOT, 'docs', `${lessonSlug}${extension}`);
407
+ if (existsSync(path)) return path;
408
+ }
409
+ return undefined;
410
+ }
411
+
412
+ /**
413
+ * Every deck file, by slug: each lesson's, and each part's. A part's deck
414
+ * (`<part>/index.deck.mdx`) is never read as the deck of a lesson
415
+ * `<part>/index` whose chapter is the part's page.
416
+ */
417
+ export function loadDeckFiles(): DeckFile[] {
418
+ const decks: DeckFile[] = [];
419
+ for (const path of docsFiles()) {
420
+ const slug = docSlug(path);
421
+ if (!isDeckSlug(slug)) continue;
422
+ const { file, data, body, bodyLine } = parseDocument(path);
423
+ if (isPartDeckSlug(slug)) {
424
+ const partDir = partDeckDir(slug);
425
+ decks.push({
426
+ scope: 'part',
427
+ path,
428
+ file,
429
+ slug,
430
+ route: partDeckRouteSlug(partDir),
431
+ pageId: partDeckPageId(partDir),
432
+ partDir,
433
+ inPart: true,
434
+ data,
435
+ body,
436
+ bodyLine,
437
+ });
438
+ continue;
439
+ }
440
+ const lessonSlug = deckLessonSlug(slug);
441
+ const lessonId = lessonIdFromSlug(lessonSlug);
442
+ const chapterPath = chapterPathOf(lessonSlug);
443
+ const hasChapter = chapterPath !== undefined;
444
+ decks.push({
445
+ scope: 'lesson',
446
+ path,
447
+ file,
448
+ slug,
449
+ lessonSlug,
450
+ lessonId,
451
+ ...(chapterPath === undefined ? {} : { chapterPath }),
452
+ route: deckRouteSlug(lessonSlug, hasChapter),
453
+ pageId: deckPageId(lessonId, hasChapter),
454
+ partDir: lessonSlug.includes('/') ? lessonSlug.slice(0, lessonSlug.indexOf('/')) : '',
455
+ inPart: lessonSlug.includes('/') && !lessonSlug.endsWith('/index'),
456
+ data,
457
+ body,
458
+ bodyLine,
459
+ });
460
+ }
461
+ return decks;
462
+ }
463
+
464
+ /**
465
+ * A file that defines a lesson: every chapter, and every deck whose lesson has
466
+ * no chapter (a slides-first lesson). Its frontmatter is the lesson's —
467
+ * `teaches`, `assumptions`, `studyMinutes`, `notation` — and its body is what
468
+ * the lesson's numbering and curriculum passes read.
469
+ */
470
+ export interface LessonFile {
471
+ readonly path: string;
472
+ /** Docs slug of the file, extension included. */
473
+ readonly slug: string;
474
+ readonly lessonSlug: string;
475
+ readonly lessonId: string;
476
+ readonly edition: 'chapter' | 'deck';
477
+ }
478
+
479
+ /** Every lesson's defining file, sorted by lesson slug. */
480
+ export function loadLessonFiles(): LessonFile[] {
481
+ const lessons: LessonFile[] = [];
482
+ for (const path of docsFiles()) {
483
+ const slug = docSlug(path);
484
+ if (isLessonSlug(slug)) {
485
+ const lessonSlug = slug.replace(/\.(md|mdx)$/, '');
486
+ lessons.push({ path, slug, lessonSlug, lessonId: lessonIdFromSlug(slug), edition: 'chapter' });
487
+ } else if (isDeckSlug(slug) && slug.includes('/') && !isPartDeckSlug(slug)) {
488
+ const lessonSlug = deckLessonSlug(slug);
489
+ if (chapterPathOf(lessonSlug) !== undefined) continue;
490
+ lessons.push({ path, slug, lessonSlug, lessonId: lessonIdFromSlug(lessonSlug), edition: 'deck' });
491
+ }
492
+ }
493
+ return lessons.sort((left, right) => left.lessonSlug.localeCompare(right.lessonSlug));
494
+ }
495
+
336
496
  /**
337
497
  * One part's **overview page** — `content/docs/<dir>/index.mdx`, the page the
338
498
  * part's own directory URL resolves to. `isLessonSlug` already excludes any
@@ -388,17 +548,26 @@ function lessonEntries(
388
548
  prerequisitesById: ReadonlyMap<string, readonly string[]>,
389
549
  ): LessonDefinition[] {
390
550
  const lessons: LessonDefinition[] = [];
391
- for (const path of filesBelow(join(CONTENT_ROOT, 'docs'), ['.md', '.mdx'])) {
392
- const slug = docSlug(path);
393
- if (!isLessonSlug(slug)) continue;
551
+ // A deck beside its chapter cites what its chapter does not need to: its
552
+ // sources count for the lesson too.
553
+ const deckBodies = new Map(
554
+ loadDeckFiles()
555
+ .filter((deck): deck is LessonDeckFile => deck.scope === 'lesson' && deck.chapterPath !== undefined)
556
+ .map((deck) => [deck.lessonId, parseDocument(deck.path).body]),
557
+ );
558
+ for (const { path, lessonId } of loadLessonFiles()) {
394
559
  const { data, body } = parseDocument(path);
395
560
  const teaches = (data.teaches as string[] | undefined) ?? [];
561
+ const deckBody = deckBodies.get(lessonId);
396
562
  lessons.push({
397
- id: lessonIdFromSlug(slug),
563
+ id: lessonId,
398
564
  requires: deriveRequires(teaches, prerequisitesById),
399
565
  teaches,
400
566
  assessments: lessonCheckIds(path),
401
- sources: deriveSources(body),
567
+ sources:
568
+ deckBody === undefined
569
+ ? deriveSources(body)
570
+ : [...new Set([...deriveSources(body), ...deriveSources(deckBody)])].sort(),
402
571
  assumptions: (data.assumptions as string[] | undefined) ?? [],
403
572
  body,
404
573
  });
@@ -538,7 +707,7 @@ function localDefinition(
538
707
  function sharedNotationEntries(): SharedNotationDefinitionInput[] {
539
708
  const directory = join(CONTENT_ROOT, 'notation');
540
709
  return filesBelow(directory, ['.md']).map((path) => {
541
- const { file, data, body } = parseDocument(path);
710
+ const { file, data, body, bodyLine } = parseDocument(path);
542
711
  const key = string(data.key, `${file} key`);
543
712
  const filenameKey = basename(path, '.md');
544
713
  if (key !== filenameKey) {
@@ -566,6 +735,7 @@ function sharedNotationEntries(): SharedNotationDefinitionInput[] {
566
735
  ? false
567
736
  : boolean(data.aiAssisted, `${file} aiAssisted`),
568
737
  body,
738
+ bodyLine,
569
739
  references: extractNotationReferences(body, file, undefined),
570
740
  glosses: glossEntries(data.glosses, key, `${file} glosses`),
571
741
  source: { file },
@@ -652,21 +822,31 @@ export function notationSuspensions(
652
822
  });
653
823
  }
654
824
 
825
+ /** A lesson file's `notation.local` entries and `notation.suspend` list, parsed. */
826
+ function lessonNotationOf(
827
+ file: string,
828
+ data: Record<string, unknown>,
829
+ ): { localDefinitions: LocalNotationDefinitionInput[]; suspended: NotationSuspension[] } {
830
+ const notation = record(data.notation ?? {}, `${file} notation`);
831
+ const local = Array.isArray(notation.local) ? notation.local : [];
832
+ const localDefinitions = local.map((entry, index) =>
833
+ localDefinition(entry, file, index),
834
+ );
835
+ return {
836
+ localDefinitions,
837
+ suspended: notationSuspensions(notation.suspend, localDefinitions, file),
838
+ };
839
+ }
840
+
655
841
  function notationLessonEntries(): NotationLessonInput[] {
656
842
  const lessons: NotationLessonInput[] = [];
657
- for (const path of filesBelow(join(CONTENT_ROOT, 'docs'), ['.md', '.mdx'])) {
658
- const slug = docSlug(path);
659
- if (!isLessonSlug(slug)) continue;
843
+ for (const { path, lessonId } of loadLessonFiles()) {
660
844
  const { file, data, body, bodyLine } = parseDocument(path);
661
- const notation = record(data.notation ?? {}, `${file} notation`);
662
- const local = Array.isArray(notation.local) ? notation.local : [];
663
- const localDefinitions = local.map((entry, index) =>
664
- localDefinition(entry, file, index),
665
- );
845
+ const { localDefinitions, suspended } = lessonNotationOf(file, data);
666
846
  lessons.push({
667
- lessonId: lessonIdFromSlug(slug),
847
+ lessonId,
668
848
  localDefinitions,
669
- suspended: notationSuspensions(notation.suspend, localDefinitions, file),
849
+ suspended,
670
850
  references: extractNotationReferences(body, file, undefined),
671
851
  body,
672
852
  bodyLine,
@@ -676,6 +856,87 @@ function notationLessonEntries(): NotationLessonInput[] {
676
856
  return lessons;
677
857
  }
678
858
 
859
+ /**
860
+ * A deck beside its chapter, as a page of the notation registry: its own id
861
+ * (`<lessonId>.slides`), no entries of its own, and its chapter's scope — the
862
+ * chapter's `notation.local` entries are the lesson's, so a deck names them
863
+ * as the chapter does, and it suspends what the chapter suspends. A part's
864
+ * deck (`<part>.slides`) has its part's scope: every chapter of the part, in
865
+ * the order the tracks read them, a key one chapter defines locally binding
866
+ * to the first that does; it suspends what any of them suspends. A deck that
867
+ * is a lesson's only edition is not here: it is that lesson's page
868
+ * (`loadNotationRegistryInput`).
869
+ */
870
+ export interface DeckNotationPage {
871
+ readonly deck: DeckFile;
872
+ readonly data: Record<string, unknown>;
873
+ readonly notation: NotationLessonInput;
874
+ }
875
+
876
+ export function loadDeckNotationPages(
877
+ decks: readonly DeckFile[] = loadDeckFiles(),
878
+ {
879
+ order = (lessonIds) => lessonIds,
880
+ }: {
881
+ /** The lessons of a part in reading order (the tracks'); by slug without it. */
882
+ readonly order?: (lessonIds: readonly string[]) => readonly string[];
883
+ } = {},
884
+ ): DeckNotationPage[] {
885
+ const pages: DeckNotationPage[] = [];
886
+ const chapterFiles = loadLessonFiles().filter((lesson) => lesson.edition === 'chapter');
887
+ for (const deck of decks) {
888
+ let chapters: { readonly lessonId: string; readonly path: string }[];
889
+ if (deck.scope === 'part') {
890
+ const inPart = chapterFiles.filter((lesson) => lesson.lessonSlug.startsWith(`${deck.partDir}/`));
891
+ const pathById = new Map(inPart.map((lesson) => [lesson.lessonId, lesson.path]));
892
+ chapters = order(inPart.map((lesson) => lesson.lessonId)).flatMap((lessonId) => {
893
+ const path = pathById.get(lessonId);
894
+ return path === undefined ? [] : [{ lessonId, path }];
895
+ });
896
+ } else if (deck.chapterPath !== undefined) {
897
+ chapters = [{ lessonId: deck.lessonId, path: deck.chapterPath }];
898
+ } else {
899
+ continue;
900
+ }
901
+ const inheritedLocalDefinitions: LocalNotationDefinitionInput[] = [];
902
+ const suspended: NotationSuspension[] = [];
903
+ const inheritedKeys = new Set<string>();
904
+ for (const { path } of chapters) {
905
+ const chapter = parseDocument(path);
906
+ const notation = lessonNotationOf(chapter.file, chapter.data);
907
+ for (const definition of notation.localDefinitions) {
908
+ if (!inheritedLocalDefinitions.some((known) => known.key === definition.key)) {
909
+ inheritedLocalDefinitions.push(definition);
910
+ }
911
+ }
912
+ for (const suspension of notation.suspended) {
913
+ if (!suspended.some((known) => known.operator === suspension.operator)) suspended.push(suspension);
914
+ }
915
+ for (const reference of extractNotationReferences(chapter.body, chapter.file, undefined)) {
916
+ inheritedKeys.add(reference.key);
917
+ }
918
+ }
919
+ const { file, data, body, bodyLine } = deck;
920
+ pages.push({
921
+ deck,
922
+ data,
923
+ notation: {
924
+ lessonId: deck.pageId,
925
+ localDefinitions: [],
926
+ suspended,
927
+ scopeLessonIds: chapters.map((chapter) => chapter.lessonId),
928
+ inheritedLocalDefinitions,
929
+ inheritedKeys: [...inheritedKeys].sort(),
930
+ references: extractNotationReferences(body, file, undefined),
931
+ body,
932
+ bodyLine,
933
+ source: { file },
934
+ },
935
+ });
936
+ }
937
+ return pages;
938
+ }
939
+
679
940
  /**
680
941
  * A docs page that is **not a chapter** (rule S10): the landing page, a part's
681
942
  * overview page, and any other page outside a part directory — every slug
@@ -701,9 +962,10 @@ export interface NonChapterPage {
701
962
 
702
963
  export function loadNonChapterPages(): NonChapterPage[] {
703
964
  const pages: NonChapterPage[] = [];
704
- for (const path of filesBelow(join(CONTENT_ROOT, 'docs'), ['.md', '.mdx'])) {
965
+ for (const path of docsFiles()) {
705
966
  const slug = docSlug(path);
706
- if (isLessonSlug(slug)) continue;
967
+ // A deck is an edition of a lesson (`loadDeckFiles`), not a page of its own kind.
968
+ if (isLessonSlug(slug) || isDeckSlug(slug)) continue;
707
969
  const { file, data, body, bodyLine } = parseDocument(path);
708
970
  const id = lessonIdFromSlug(slug);
709
971
  pages.push({
@@ -799,7 +1061,7 @@ export function loadSourceRecords(): Record<string, unknown>[] {
799
1061
  */
800
1062
  export function loadLessonChecks(lessonId: string): string[] {
801
1063
  const slug = lessonId.replace(/\./g, '/');
802
- for (const extension of ['.mdx', '.md']) {
1064
+ for (const extension of ['.mdx', '.md', '.deck.mdx', '.deck.md']) {
803
1065
  const lessonPath = join(CONTENT_ROOT, 'docs', `${slug}${extension}`);
804
1066
  if (existsSync(lessonPath)) return lessonCheckIds(lessonPath);
805
1067
  }
@@ -861,11 +1123,8 @@ export function loadNotationSpecs(): NotationSpec[] {
861
1123
  });
862
1124
  }
863
1125
 
864
- for (const path of filesBelow(join(CONTENT_ROOT, 'docs'), ['.md', '.mdx'])) {
865
- const slug = docSlug(path);
866
- if (!isLessonSlug(slug)) continue;
1126
+ for (const { path, lessonId } of loadLessonFiles()) {
867
1127
  const { file, data } = parseDocument(path);
868
- const lessonId = lessonIdFromSlug(slug);
869
1128
  const local = (data as { notation?: { local?: unknown } }).notation?.local;
870
1129
  if (!Array.isArray(local)) continue;
871
1130
  for (const raw of local) {
@@ -386,6 +386,258 @@ export function normalizeCards(config: CourseConfig): NormalizedCards {
386
386
  return { carry };
387
387
  }
388
388
 
389
+ /**
390
+ * The marks the sidebar puts beside a chapter or part page for one reader
391
+ * (`sidebarMarks` in the course settings, `page-marks.ts`): a quiet ring
392
+ * while they have not opened it, an accent dot when its content changed
393
+ * since they last did. Every field is optional; {@link normalizeSidebarMarks}
394
+ * fills in the defaults.
395
+ */
396
+ export interface CourseSidebarMarks {
397
+ /** Mark a page the reader has not opened yet. Default `true`. */
398
+ readonly unopened?: boolean;
399
+ /** Mark a page whose content changed since the reader last opened it. Default `true`. */
400
+ readonly changed?: boolean;
401
+ }
402
+
403
+ /** {@link CourseSidebarMarks} with every default applied. */
404
+ export interface NormalizedSidebarMarks {
405
+ readonly unopened: boolean;
406
+ readonly changed: boolean;
407
+ }
408
+
409
+ export const SIDEBAR_MARKS_DEFAULTS: NormalizedSidebarMarks = { unopened: true, changed: true };
410
+
411
+ /** Validate `CourseConfig.sidebarMarks` and apply its defaults. Throws on a bad value. */
412
+ export function normalizeSidebarMarks(config: CourseConfig): NormalizedSidebarMarks {
413
+ const marks = config.sidebarMarks ?? {};
414
+ const read = (field: keyof NormalizedSidebarMarks): boolean => {
415
+ const value: unknown = marks[field] ?? SIDEBAR_MARKS_DEFAULTS[field];
416
+ if (typeof value !== 'boolean') {
417
+ throw new Error(
418
+ `CourseConfig sidebarMarks.${field} must be true or false; received ${JSON.stringify(value)}.`,
419
+ );
420
+ }
421
+ return value;
422
+ };
423
+ return { unopened: read('unopened'), changed: read('changed') };
424
+ }
425
+
426
+ /**
427
+ * Which edition of a lesson its sidebar row opens, for a reader who has not
428
+ * chosen (`sidebarEditions` in the course settings, `sidebar-editions.ts`). A
429
+ * lesson with a chapter and a deck is one row: the row opens the **lead**
430
+ * edition, and the **other** is an icon at the row's end, or not listed. A
431
+ * reader switches both in the sidebar's foot. The plain `string` fields
432
+ * arrive as JSON; {@link normalizeSidebarEditions} rejects any value it does
433
+ * not list.
434
+ */
435
+ export interface CourseSidebarEditions {
436
+ /** The edition a lesson's row opens: `chapter` (default) or `slides`. */
437
+ readonly lead?: string;
438
+ /** Whether the row also lists the other edition: `listed` (default) or `hidden`. */
439
+ readonly other?: string;
440
+ }
441
+
442
+ export type SidebarEditionLead = 'chapter' | 'slides';
443
+ export type SidebarEditionOther = 'listed' | 'hidden';
444
+
445
+ /** {@link CourseSidebarEditions} with every default applied. */
446
+ export interface NormalizedSidebarEditions {
447
+ readonly lead: SidebarEditionLead;
448
+ readonly other: SidebarEditionOther;
449
+ }
450
+
451
+ /** The engine's defaults (decision D10): the chapter leads, the slides are listed. */
452
+ export const SIDEBAR_EDITIONS_DEFAULTS: NormalizedSidebarEditions = { lead: 'chapter', other: 'listed' };
453
+
454
+ /** Validate `CourseConfig.sidebarEditions` and apply its defaults. Throws on a bad value. */
455
+ export function normalizeSidebarEditions(config: CourseConfig): NormalizedSidebarEditions {
456
+ const editions = config.sidebarEditions ?? {};
457
+ const choice = <T extends string>(field: keyof NormalizedSidebarEditions, allowed: readonly T[]): T => {
458
+ const value: unknown = editions[field] ?? SIDEBAR_EDITIONS_DEFAULTS[field];
459
+ if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {
460
+ throw new Error(
461
+ `CourseConfig sidebarEditions.${field} must be one of ${allowed.map((item) => JSON.stringify(item)).join(', ')}; ` +
462
+ `received ${JSON.stringify(value)}.`,
463
+ );
464
+ }
465
+ return value as T;
466
+ };
467
+ return {
468
+ lead: choice<SidebarEditionLead>('lead', ['chapter', 'slides']),
469
+ other: choice<SidebarEditionOther>('other', ['listed', 'hidden']),
470
+ };
471
+ }
472
+
473
+ /**
474
+ * A course's decks: the slide edition of a lesson (`<lesson>.deck.mdx`,
475
+ * `docs/authoring/deck.md`). Every field is optional; {@link normalizeDeck}
476
+ * fills in the defaults. The plain `string` fields arrive as JSON, and
477
+ * {@link normalizeDeck} rejects any value it does not list.
478
+ */
479
+ export interface CourseDeck {
480
+ /**
481
+ * The canvas every deck of the course is laid out on: `16:9` (default),
482
+ * `16:10` or `4:3`. One setting for the course, because the frame-fit check,
483
+ * the captured figures and the PDF's page all read it.
484
+ */
485
+ readonly aspectRatio?: string;
486
+ /**
487
+ * The line on a frame that names the symbols it shows: `first-use`
488
+ * (default, the symbols the deck has not shown before), `every-frame`
489
+ * (every symbol on the frame), or `off`.
490
+ */
491
+ readonly legend?: string;
492
+ /**
493
+ * Who sees a frame's speaker notes: `reader` (default — under each frame in
494
+ * the scroll view, in the presenter window and in the handout), `presenter`
495
+ * (the presenter window and the handout only), or `off` (nowhere; the
496
+ * handout PDF is not built).
497
+ */
498
+ readonly notes?: string;
499
+ /** End each deck on a frame of what its lesson's competencies say a student can now do. Default `true`. */
500
+ readonly outcomesFrame?: boolean;
501
+ /** Offer the slides-with-notes PDF beside the slides PDF. Default `true`. */
502
+ readonly handoutPdf?: boolean;
503
+ }
504
+
505
+ export type DeckAspectRatio = '16:9' | '16:10' | '4:3';
506
+ export type DeckLegend = 'first-use' | 'every-frame' | 'off';
507
+ export type DeckNotes = 'reader' | 'presenter' | 'off';
508
+
509
+ /**
510
+ * The logical canvas a frame is laid out on, in CSS pixels: the width of the
511
+ * page's reading column, so a figure, a table or a lab lays out on a frame at
512
+ * the size it was made for, and the frame is scaled to the screen. Every
513
+ * ratio is 450 px tall, so the type scale and a frame's capacity in lines are
514
+ * the same in all three. Printed, the canvas is scaled by 1.6 onto a page
515
+ * 7.5 in tall (13.333 × 7.5 in for 16:9).
516
+ */
517
+ export const DECK_CANVAS: Readonly<Record<DeckAspectRatio, { readonly width: number; readonly height: number }>> = {
518
+ '16:9': { width: 800, height: 450 },
519
+ '16:10': { width: 720, height: 450 },
520
+ '4:3': { width: 600, height: 450 },
521
+ };
522
+
523
+ /** {@link CourseDeck} with every default applied, as the manifest publishes it. */
524
+ export interface NormalizedDeck {
525
+ readonly aspectRatio: DeckAspectRatio;
526
+ readonly legend: DeckLegend;
527
+ readonly notes: DeckNotes;
528
+ readonly outcomesFrame: boolean;
529
+ readonly handoutPdf: boolean;
530
+ readonly canvas: { readonly width: number; readonly height: number };
531
+ }
532
+
533
+ export const DECK_DEFAULTS: Omit<NormalizedDeck, 'canvas'> = {
534
+ aspectRatio: '16:9',
535
+ legend: 'first-use',
536
+ notes: 'reader',
537
+ outcomesFrame: true,
538
+ handoutPdf: true,
539
+ };
540
+
541
+ /** Validate `CourseConfig.deck` and apply its defaults. Throws on a bad value. */
542
+ export function normalizeDeck(config: CourseConfig): NormalizedDeck {
543
+ const deck = config.deck ?? {};
544
+ const choice = <T extends string>(field: keyof CourseDeck, allowed: readonly T[]): T => {
545
+ const value: unknown = deck[field] ?? DECK_DEFAULTS[field as keyof typeof DECK_DEFAULTS];
546
+ if (typeof value !== 'string' || !(allowed as readonly string[]).includes(value)) {
547
+ throw new Error(
548
+ `CourseConfig deck.${field} must be one of ${allowed.map((item) => JSON.stringify(item)).join(', ')}; ` +
549
+ `received ${JSON.stringify(value)}.`,
550
+ );
551
+ }
552
+ return value as T;
553
+ };
554
+ const flag = (field: 'outcomesFrame' | 'handoutPdf'): boolean => {
555
+ const value: unknown = deck[field] ?? DECK_DEFAULTS[field];
556
+ if (typeof value !== 'boolean') {
557
+ throw new Error(`CourseConfig deck.${field} must be true or false; received ${JSON.stringify(value)}.`);
558
+ }
559
+ return value;
560
+ };
561
+ const aspectRatio = choice<DeckAspectRatio>('aspectRatio', ['16:9', '16:10', '4:3']);
562
+ return {
563
+ aspectRatio,
564
+ legend: choice<DeckLegend>('legend', ['first-use', 'every-frame', 'off']),
565
+ notes: choice<DeckNotes>('notes', ['reader', 'presenter', 'off']),
566
+ outcomesFrame: flag('outcomesFrame'),
567
+ handoutPdf: flag('handoutPdf'),
568
+ canvas: DECK_CANVAS[aspectRatio],
569
+ };
570
+ }
571
+
572
+ /**
573
+ * Whether the course asks each reader if it may remember their reading in
574
+ * their browser (`readingMemory` in the course settings,
575
+ * `session/reading-memory.ts`): which chapters they opened (the sidebar's
576
+ * marks) and the cards they keep or carry, across visits. `ask` (default)
577
+ * shows the question until the reader answers; `off` never asks and never
578
+ * keeps reading memory, so the marks are off and cards last as long as the tab.
579
+ */
580
+ export type ReadingMemorySetting = 'ask' | 'off';
581
+
582
+ /** Validate `CourseConfig.readingMemory`. Throws on a bad value. */
583
+ export function normalizeReadingMemory(config: CourseConfig): ReadingMemorySetting {
584
+ const value: unknown = config.readingMemory ?? 'ask';
585
+ if (value !== 'ask' && value !== 'off') {
586
+ throw new Error(`CourseConfig readingMemory must be "ask" or "off"; received ${JSON.stringify(value)}.`);
587
+ }
588
+ return value;
589
+ }
590
+
591
+ /**
592
+ * How the page tells its zones apart, and how a deck's slides sit on it
593
+ * (`appearance` in the course settings; `zones.css` and `deck.css` hold the
594
+ * colours). `zones`: `tonal` (the default) gives both sidebars one step of the
595
+ * neutral grey and the header a step further; `accent-header` makes the
596
+ * header a deep band of the course accent; `paper` lays the reading column
597
+ * on the sidebars' colour as a sheet; `none` keeps Starlight's colours.
598
+ * `slides`: `lifted` (the default) gives each slide an edge and a drop
599
+ * shadow, and in the dark theme a sheet a step lighter than the page; `edge`
600
+ * the edge and shadow on the page's own colour; `sorter` lays the deck on a
601
+ * tinted well. Every field is optional; {@link normalizeAppearance} fills in
602
+ * the defaults.
603
+ */
604
+ export interface CourseAppearance {
605
+ /** `tonal` (default), `accent-header`, `paper` or `none`. */
606
+ readonly zones?: string;
607
+ /** `lifted` (default), `edge` or `sorter`. */
608
+ readonly slides?: string;
609
+ }
610
+
611
+ export type AppearanceZones = 'tonal' | 'accent-header' | 'paper' | 'none';
612
+ export type AppearanceSlides = 'lifted' | 'edge' | 'sorter';
613
+
614
+ /** {@link CourseAppearance} with every default applied. */
615
+ export interface NormalizedAppearance {
616
+ readonly zones: AppearanceZones;
617
+ readonly slides: AppearanceSlides;
618
+ }
619
+
620
+ export const APPEARANCE_DEFAULTS: NormalizedAppearance = { zones: 'tonal', slides: 'lifted' };
621
+
622
+ const APPEARANCE_ZONES: readonly AppearanceZones[] = ['tonal', 'accent-header', 'paper', 'none'];
623
+ const APPEARANCE_SLIDES: readonly AppearanceSlides[] = ['lifted', 'edge', 'sorter'];
624
+
625
+ /** Validate `CourseConfig.appearance` and apply its defaults. Throws on a bad value. */
626
+ export function normalizeAppearance(config: CourseConfig): NormalizedAppearance {
627
+ const appearance = config.appearance ?? {};
628
+ const choice = <T extends string>(field: keyof NormalizedAppearance, allowed: readonly T[]): T => {
629
+ const value: unknown = appearance[field] ?? APPEARANCE_DEFAULTS[field];
630
+ if (!allowed.includes(value as T)) {
631
+ throw new Error(
632
+ `CourseConfig appearance.${field} must be one of ${allowed.map((v) => `"${v}"`).join(', ')}; ` +
633
+ `received ${JSON.stringify(value)}.`,
634
+ );
635
+ }
636
+ return value as T;
637
+ };
638
+ return { zones: choice('zones', APPEARANCE_ZONES), slides: choice('slides', APPEARANCE_SLIDES) };
639
+ }
640
+
389
641
  /**
390
642
  * Rule S2's word ranges (`rules.S2.bridgeMinWords` … in the course settings),
391
643
  * in the shape `gate-part-pages.ts` takes. Built by `partPageLimits` in
@@ -488,6 +740,21 @@ export interface CourseConfig {
488
740
  readonly numbering?: CourseNumbering;
489
741
  /** How explanation cards start for a reader: "Carry cards" on or off. */
490
742
  readonly cards?: CourseCards;
743
+ /** The sidebar's marks for a page the reader has not opened, or that changed since. */
744
+ readonly sidebarMarks?: CourseSidebarMarks;
745
+ /** Which edition a lesson's sidebar row opens, and whether it lists the other. */
746
+ readonly sidebarEditions?: CourseSidebarEditions;
747
+ /** How the course's decks look and what they show (`docs/authoring/deck.md`). */
748
+ readonly deck?: CourseDeck;
749
+ /**
750
+ * Ask each reader whether the course may remember their reading in their
751
+ * browser (`ask`, the default), or never keep it (`off`). A plain `string`
752
+ * because the settings arrive as JSON; {@link normalizeReadingMemory}
753
+ * rejects any other value.
754
+ */
755
+ readonly readingMemory?: string;
756
+ /** How the page tells header, sidebars and article apart, and how slides sit on it. */
757
+ readonly appearance?: CourseAppearance;
491
758
  /**
492
759
  * How a check's and an example set's heading names its kind: `icon-word`
493
760
  * (default) or `icon` ({@link BlockLabels}). A plain `string` because the