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
@@ -26,19 +26,92 @@
26
26
 
27
27
  import { citedIds, withoutLinkCitations } from '../reference/citation-format.mjs';
28
28
 
29
- /** `foundations/discount-factors` → `foundations.discount-factors`. */
29
+ /**
30
+ * `foundations/discount-factors` → `foundations.discount-factors`. A deck
31
+ * file's slug (`foundations/discount-factors.deck.mdx`) gives its lesson's id;
32
+ * a deck's route (`foundations/discount-factors/slides`) gives the deck's page
33
+ * id, `foundations.discount-factors.slides` ({@link deckPageId}).
34
+ */
30
35
  export function lessonIdFromSlug(slug: string): string {
31
- return slug.replace(/\.(md|mdx)$/, '').replace(/\//g, '.');
36
+ return slug.replace(/\.(md|mdx)$/, '').replace(/\.deck$/, '').replace(/\//g, '.');
32
37
  }
33
38
 
34
39
  /**
35
- * A docs entry is a lesson when it lives in a section directory. The three
40
+ * A docs entry is a chapter when it lives in a section directory. The three
36
41
  * top-level pages (`index`, `curriculum-map`, `glossary`) are not lessons; a
37
- * future `<section>/index` page would not be one either.
42
+ * future `<section>/index` page would not be one either, and neither is a
43
+ * lesson's deck file (`<lesson>.deck.mdx`, {@link isDeckSlug}), which is an
44
+ * edition of a lesson rather than a lesson.
38
45
  */
39
46
  export function isLessonSlug(slug: string): boolean {
40
47
  const clean = slug.replace(/\.(md|mdx)$/, '');
41
- return clean.includes('/') && !clean.endsWith('/index');
48
+ return clean.includes('/') && !clean.endsWith('/index') && !isDeckSlug(clean);
49
+ }
50
+
51
+ /**
52
+ * The last segment of a deck's route when the deck stands beside its chapter:
53
+ * `projectiles/projectile-range` is the chapter, `projectiles/projectile-range/slides`
54
+ * the deck.
55
+ */
56
+ export const DECK_ROUTE_SEGMENT = 'slides';
57
+
58
+ /**
59
+ * A deck file — a lesson's slide edition, `content/docs/<part>/<lesson>.deck.mdx`
60
+ * beside the chapter `<lesson>.mdx` or alone for a lesson that is only slides,
61
+ * or a part's deck ({@link isPartDeckSlug}). True for the docs slug with or
62
+ * without its extension; a caller that needs the lesson asks
63
+ * {@link isPartDeckSlug} first.
64
+ */
65
+ export function isDeckSlug(slug: string): boolean {
66
+ return /\.deck(?:\.mdx?)?$/.test(slug);
67
+ }
68
+
69
+ /** The lesson a lesson's deck file belongs to: `motion/range.deck.mdx` → `motion/range`. */
70
+ export function deckLessonSlug(slug: string): string {
71
+ return slug.replace(/\.(md|mdx)$/, '').replace(/\.deck$/, '');
72
+ }
73
+
74
+ /**
75
+ * A part's deck, `content/docs/<part>/index.deck.mdx` beside the part's page
76
+ * `index.mdx`: the slides of the part, served at `/<part>/slides/`
77
+ * ({@link partDeckRouteSlug}), whose `##` sections may each name the chapter
78
+ * they cover (`docs/authoring/deck.md`). It is no lesson's edition.
79
+ */
80
+ export function isPartDeckSlug(slug: string): boolean {
81
+ return /^[^/]+\/index\.deck(?:\.mdx?)?$/.test(slug);
82
+ }
83
+
84
+ /** The part a part's deck belongs to: `motion/index.deck.mdx` → `motion`. */
85
+ export function partDeckDir(slug: string): string {
86
+ return slug.replace(/\/index\.deck(?:\.mdx?)?$/, '');
87
+ }
88
+
89
+ /** Where a part's deck is served: `<part>/slides`, as a chapter's deck is `<lesson>/slides`. */
90
+ export function partDeckRouteSlug(partDir: string): string {
91
+ return `${partDir}/${DECK_ROUTE_SEGMENT}`;
92
+ }
93
+
94
+ /** The page id of a part's deck, the id of its route: `<part>.slides`. */
95
+ export function partDeckPageId(partDir: string): string {
96
+ return `${partDir}.${DECK_ROUTE_SEGMENT}`;
97
+ }
98
+
99
+ /**
100
+ * Where a lesson's deck is served: `<lesson>/slides` beside its chapter, or
101
+ * the lesson's own route when the lesson has no chapter (a slides-first
102
+ * lesson, whose deck is its only edition).
103
+ */
104
+ export function deckRouteSlug(lessonSlug: string, hasChapter: boolean): string {
105
+ return hasChapter ? `${lessonSlug}/${DECK_ROUTE_SEGMENT}` : lessonSlug;
106
+ }
107
+
108
+ /**
109
+ * The page id a deck's notation bundle, gates and diagnostics use — the id of
110
+ * its route ({@link lessonIdFromSlug}): `<lessonId>.slides` beside a chapter,
111
+ * the lesson's own id for a slides-first lesson.
112
+ */
113
+ export function deckPageId(lessonId: string, hasChapter: boolean): string {
114
+ return hasChapter ? `${lessonId}.${DECK_ROUTE_SEGMENT}` : lessonId;
42
115
  }
43
116
 
44
117
  /**
@@ -153,7 +226,8 @@ export interface SidebarSection {
153
226
  /**
154
227
  * One sidebar entry: a lesson slug, or an explicit link. `attrs` is Starlight's
155
228
  * own passthrough onto the rendered `<a>` — the engine uses it to mark a part's
156
- * overview row so CSS can give it its own icon.
229
+ * overview row so CSS can give it its own icon, and to name each edition of a
230
+ * lesson for the sidebar's script ({@link buildSidebar}).
157
231
  */
158
232
  export type SidebarItem =
159
233
  | string
@@ -161,6 +235,15 @@ export type SidebarItem =
161
235
  readonly label: string;
162
236
  readonly link: string;
163
237
  readonly attrs?: Readonly<Record<string, string>>;
238
+ }
239
+ | {
240
+ /**
241
+ * A docs entry by its id (Starlight's `slug` item), with its own label,
242
+ * or the entry's title without one.
243
+ */
244
+ readonly slug: string;
245
+ readonly label?: string;
246
+ readonly attrs?: Readonly<Record<string, string>>;
164
247
  };
165
248
 
166
249
  export interface SidebarGroup {
@@ -171,49 +254,190 @@ export interface SidebarGroup {
171
254
  /** Marks the overview row so `global.css` can give it its own icon. */
172
255
  export const PART_OVERVIEW_CLASS = 'explico-part-overview';
173
256
 
257
+ /**
258
+ * Marks a deck's row, listed under its chapter, so `deck.css` can set it
259
+ * apart. That is the sidebar without its script; with it, the deck is its
260
+ * chapter row's trailing icon, or leads the row (`sidebar-editions.ts`).
261
+ */
262
+ export const DECK_ROW_CLASS = 'explico-deck-row';
263
+
264
+ /**
265
+ * Marks a slides-first lesson's row, whose deck is the lesson, so `deck.css`
266
+ * gives it the deck's glyph where a chapter has none (a row) or a sheet (a
267
+ * compact tile). It is a lesson of the reading order, unlike {@link DECK_ROW_CLASS}.
268
+ */
269
+ export const DECK_LESSON_CLASS = 'explico-deck-lesson';
270
+
271
+ /**
272
+ * Marks a part's deck, listed right after the part's Overview (with
273
+ * {@link DECK_ROW_CLASS}, so Next and Previous step past it as past any
274
+ * deck): the part's own, as Overview is, with the part's deck glyph. The
275
+ * sidebar's script makes the two one row, the part's, as a lesson's chapter
276
+ * and deck are one (`sidebar-editions.ts`).
277
+ */
278
+ export const PART_DECK_CLASS = 'explico-part-deck';
279
+
280
+ /**
281
+ * Numbers as a reader reads a run of them: `1–2`, `1, 3`, `2–4, 6`. A run of
282
+ * two or more is its ends with an en dash.
283
+ */
284
+ export function numberList(numbers: readonly number[]): string {
285
+ const sorted = [...new Set(numbers)].sort((left, right) => left - right);
286
+ const runs: string[] = [];
287
+ for (let start = 0; start < sorted.length; ) {
288
+ let end = start;
289
+ while (end + 1 < sorted.length && sorted[end + 1] === sorted[end]! + 1) end += 1;
290
+ runs.push(end === start ? String(sorted[start]) : `${sorted[start]}–${sorted[end]}`);
291
+ start = end + 1;
292
+ }
293
+ return runs.join(', ');
294
+ }
295
+
296
+ /**
297
+ * The lesson a sidebar item lists, by its slug: an item served at its
298
+ * lesson's own URL (its `data-lesson` is its `slug`), a chapter or a
299
+ * slides-first lesson's deck. A part's overview, a chapter's slides row
300
+ * (served under the chapter) and a link list no lesson. A bare slug is a
301
+ * lesson too.
302
+ */
303
+ export function sidebarLessonSlug(item: SidebarItem): string | undefined {
304
+ if (typeof item === 'string') return item;
305
+ if (!('slug' in item)) return undefined;
306
+ return item.attrs?.['data-lesson'] === item.slug ? item.slug : undefined;
307
+ }
308
+
174
309
  /**
175
310
  * The Starlight `sidebar` config: one group per section, lessons ordered by the
176
311
  * merged track order (ties broken by slug), then the static Reference group.
177
312
  * Replaces seven `autogenerate` blocks that read an authored `sidebar.order`.
178
313
  * `floatIndex`: the course has a top-level "Figures and tables" page
179
314
  * (`figures-and-tables.mdx`, `FloatIndex.astro`), listed in Reference.
315
+ *
316
+ * Each edition of a lesson is an item of its own, its chapter first, and
317
+ * names its edition, lesson and title (`data-edition`, `data-lesson`,
318
+ * `data-title` on the link): the HTML lists both, and the sidebar's script
319
+ * (`LayoutSidebar.astro`, `sidebar-editions.ts`) makes the two one row.
320
+ *
321
+ * A part's deck is an item of the part right after its Overview. Both name
322
+ * their part and which of its two editions they are (`data-part`,
323
+ * `data-part-edition`), and the deck its part's label and the ordinals of
324
+ * the chapters it covers (`data-title`, `data-chapters`: `1–2`), so the
325
+ * script makes them the part's one row. A chapter it covers that has no deck
326
+ * of its own carries the address of its section in the part's deck
327
+ * (`data-deck-href`, `/<part>/slides/#slide-N`, without the site's base),
328
+ * which the script makes the chapter's other edition: a lesson's own deck
329
+ * wins over its part's. The frames the deck and each section hold are
330
+ * `buildDecks`'s, added once it has run ({@link withPartDeckFrames}).
180
331
  */
181
332
  export function buildSidebar(
182
333
  tracks: readonly TrackLike[],
183
334
  lessonSlugs: readonly string[],
184
335
  parts: readonly SidebarSection[],
185
- { floatIndex = false }: { readonly floatIndex?: boolean } = {},
336
+ {
337
+ floatIndex = false,
338
+ decks = new Map(),
339
+ slidesFirst = new Set(),
340
+ partDecks = new Map(),
341
+ titleOf = () => undefined,
342
+ }: {
343
+ readonly floatIndex?: boolean;
344
+ /** A deck beside its chapter: chapter slug → the deck's route, listed under the chapter. */
345
+ readonly decks?: ReadonlyMap<string, string>;
346
+ /** The lessons whose only edition is a deck, by slug: listed with the deck's glyph. */
347
+ readonly slidesFirst?: ReadonlySet<string>;
348
+ /**
349
+ * A part's deck, by the part's directory: its route, and the frame each
350
+ * chapter it covers opens on (its section's divider), by chapter slug.
351
+ */
352
+ readonly partDecks?: ReadonlyMap<
353
+ string,
354
+ { readonly route: string; readonly covers: ReadonlyMap<string, number> }
355
+ >;
356
+ /** A lesson's title, by its slug: `data-title` on each of its items, left out when unknown. */
357
+ readonly titleOf?: (slug: string) => string | undefined;
358
+ } = {},
186
359
  ): SidebarGroup[] {
187
360
  const rank = new Map(
188
361
  mergeTrackOrder(tracks).map((lessonId, index) => [lessonId, index]),
189
362
  );
190
363
  const positionOf = (slug: string) =>
191
364
  rank.get(lessonIdFromSlug(slug)) ?? Number.POSITIVE_INFINITY;
365
+ const edition = (lesson: string, name: 'chapter' | 'slides'): Record<string, string> => {
366
+ const title = titleOf(lesson);
367
+ return {
368
+ 'data-edition': name,
369
+ 'data-lesson': lesson,
370
+ ...(title === undefined ? {} : { 'data-title': title }),
371
+ };
372
+ };
192
373
 
193
374
  const groups: SidebarGroup[] = parts.map(
194
375
  ({ dir, label, overviewLabel, overviewLink }) => {
195
- const chapters = lessonSlugs
376
+ const partDeck = partDecks.get(dir);
377
+ const ordered = lessonSlugs
196
378
  .filter((slug) => slug.startsWith(`${dir}/`))
197
379
  .sort(
198
380
  (left, right) =>
199
381
  positionOf(left) - positionOf(right) || left.localeCompare(right),
200
382
  );
383
+ const chapters: SidebarItem[] = ordered.flatMap((slug): SidebarItem[] => {
384
+ if (slidesFirst.has(slug)) {
385
+ return [{ slug, attrs: { class: DECK_LESSON_CLASS, ...edition(slug, 'slides') } }];
386
+ }
387
+ const deck = decks.get(slug);
388
+ // A chapter with no deck of its own: its section of the part's deck.
389
+ const frame = deck === undefined ? partDeck?.covers.get(slug) : undefined;
390
+ const chapter = {
391
+ slug,
392
+ attrs: {
393
+ ...edition(slug, 'chapter'),
394
+ ...(frame === undefined || partDeck === undefined
395
+ ? {}
396
+ : { 'data-deck-href': `/${partDeck.route}/#slide-${frame}` }),
397
+ },
398
+ };
399
+ return deck === undefined
400
+ ? [chapter]
401
+ : [chapter, { slug: deck, label: 'Slides', attrs: { class: DECK_ROW_CLASS, ...edition(slug, 'slides') } }];
402
+ });
201
403
  // The overview leads the group: it is the part's own page, so it reads
202
404
  // as the chapter list's heading rather than an appendix to it, and in
203
- // compact mode it is the first icon instead of the last.
204
- return {
205
- label,
206
- items: overviewLink
405
+ // compact mode it is the first icon instead of the last. The part's
406
+ // deck follows it, as the part's own too.
407
+ const own: SidebarItem[] = [
408
+ ...(overviewLink
207
409
  ? [
208
410
  {
209
411
  label: overviewLabel ?? 'Overview',
210
412
  link: overviewLink,
211
- attrs: { class: PART_OVERVIEW_CLASS },
413
+ attrs: { class: PART_OVERVIEW_CLASS, 'data-part': dir, 'data-part-edition': 'overview' },
212
414
  },
213
- ...chapters,
214
415
  ]
215
- : chapters,
216
- };
416
+ : []),
417
+ ...(partDeck === undefined
418
+ ? []
419
+ : [
420
+ {
421
+ slug: partDeck.route,
422
+ label: 'Slides',
423
+ attrs: {
424
+ class: `${DECK_ROW_CLASS} ${PART_DECK_CLASS}`,
425
+ 'data-edition': 'slides',
426
+ 'data-part': dir,
427
+ 'data-part-edition': 'slides',
428
+ 'data-title': label,
429
+ // A chapter's ordinal is its place in the part, as `lessons[].chapter` numbers it.
430
+ 'data-chapters': numberList(
431
+ [...partDeck.covers.keys()].flatMap((slug) => {
432
+ const at = ordered.indexOf(slug);
433
+ return at === -1 ? [] : [at + 1];
434
+ }),
435
+ ),
436
+ },
437
+ },
438
+ ]),
439
+ ];
440
+ return { label, items: [...own, ...chapters] };
217
441
  },
218
442
  );
219
443
 
@@ -228,3 +452,45 @@ export function buildSidebar(
228
452
 
229
453
  return groups;
230
454
  }
455
+
456
+ /** A part's deck as {@link withPartDeckFrames} reads it: its part and its frames, each with the chapter it covers. */
457
+ export interface PartDeckFrames {
458
+ readonly part?: string;
459
+ readonly frames: readonly { readonly number: number; readonly chapter?: string }[];
460
+ }
461
+
462
+ /**
463
+ * The sidebar with each part's deck's frames, as `buildDecks` numbers them:
464
+ * the deck's count on its item (`data-frames`), and on each chapter that
465
+ * links into its section the frames that section holds (`data-deck-range`,
466
+ * `2–16`, or `5` for one), its first to its last when it runs over several
467
+ * sections. `buildDecks` runs after the sidebar is built, since the chapters
468
+ * it numbers are ordered by the sidebar.
469
+ */
470
+ export function withPartDeckFrames(
471
+ groups: readonly SidebarGroup[],
472
+ decks: readonly PartDeckFrames[],
473
+ ): SidebarGroup[] {
474
+ const byPart = new Map(decks.flatMap((deck) => (deck.part === undefined ? [] : [[deck.part, deck] as const])));
475
+ const rangeOf = (chapter: string): string | undefined => {
476
+ const numbers = decks.flatMap((deck) =>
477
+ deck.part === undefined ? [] : deck.frames.filter((frame) => frame.chapter === chapter).map((frame) => frame.number),
478
+ );
479
+ if (numbers.length === 0) return undefined;
480
+ const first = Math.min(...numbers);
481
+ const last = Math.max(...numbers);
482
+ return first === last ? String(first) : `${first}–${last}`;
483
+ };
484
+ return groups.map((group) => ({
485
+ ...group,
486
+ items: group.items.map((item): SidebarItem => {
487
+ if (typeof item === 'string' || item.attrs === undefined) return item;
488
+ const attrs = item.attrs;
489
+ const deck = attrs['data-part-edition'] === 'slides' ? byPart.get(attrs['data-part'] ?? '') : undefined;
490
+ if (deck) return { ...item, attrs: { ...attrs, 'data-frames': String(deck.frames.length) } };
491
+ const lesson = attrs['data-lesson'];
492
+ const range = attrs['data-deck-href'] === undefined || lesson === undefined ? undefined : rangeOf(lesson);
493
+ return range === undefined ? item : { ...item, attrs: { ...attrs, 'data-deck-range': range } };
494
+ }),
495
+ }));
496
+ }