explico 0.13.0 → 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 +894 -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 +1087 -14
  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 +29 -0
  190. package/src/components/starlight/AboutPanel.astro +186 -3
  191. package/src/components/starlight/CollapsibleSections.astro +24 -3
  192. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  193. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  194. package/src/components/starlight/LayoutHeader.astro +17 -0
  195. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  196. package/src/components/starlight/LayoutSidebar.astro +807 -21
  197. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  198. package/src/components/starlight/LessonExportActions.astro +27 -9
  199. package/src/components/starlight/LessonFooter.astro +59 -0
  200. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  201. package/src/components/starlight/TextSizeControl.astro +24 -13
  202. package/src/components/starlight/page-marks.ts +139 -0
  203. package/src/components/starlight/section-summary.ts +1 -1
  204. package/src/components/starlight/sidebar-editions.ts +245 -0
  205. package/src/components/table/Table.astro +15 -7
  206. package/src/content-config.ts +87 -1
  207. package/src/curriculum/competency-graph.ts +95 -0
  208. package/src/reference/base-library.mjs +19 -0
  209. package/src/reference/consistency.ts +126 -39
  210. package/src/reference/deck-remark.mjs +600 -0
  211. package/src/reference/deck-source.ts +273 -0
  212. package/src/reference/decks-validate.ts +604 -0
  213. package/src/reference/diagram-layout.ts +3 -1
  214. package/src/reference/figure-symbols.ts +108 -31
  215. package/src/reference/folds-validate.ts +1 -1
  216. package/src/reference/gate-indices.ts +5 -3
  217. package/src/reference/gate-math.ts +169 -25
  218. package/src/reference/lab-katex-validate.ts +30 -15
  219. package/src/reference/lab-math-scope.ts +34 -3
  220. package/src/reference/math-glyphs.mjs +189 -25
  221. package/src/reference/page-notation.ts +69 -15
  222. package/src/reference/references.ts +28 -0
  223. package/src/reference/registry-scope.mjs +34 -0
  224. package/src/reference/registry.ts +41 -11
  225. package/src/reference/rehype-deck.mjs +286 -0
  226. package/src/reference/rehype-heading-anchors.mjs +48 -6
  227. package/src/reference/remark-notation.mjs +249 -40
  228. package/src/reference/source-lines.mjs +65 -0
  229. package/src/reference/types.ts +28 -0
  230. package/src/rules/catalog.ts +257 -4
  231. package/src/rules/course-rules.ts +21 -1
  232. package/src/rules/in-effect.ts +54 -8
  233. package/src/session/PreferenceStore.ts +23 -0
  234. package/src/session/deck-memory.ts +263 -0
  235. package/src/session/reading-memory.ts +243 -0
  236. package/src/styles/deck.css +1150 -0
  237. package/src/styles/global.css +958 -27
  238. package/src/styles/zones.css +277 -0
  239. /package/src/{components → reference}/float-name.ts +0 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The glyph table a notation entry's Markdown body resolves against, and the
3
+ * advice for a symbol it does not bind. The remark pass renders an entry's
4
+ * body with this table (`remark-notation.mjs`) and `validate:content` checks
5
+ * the same body with it (`gate-math.ts`), so the fast gate and the build read
6
+ * one table and cannot disagree on what binds.
7
+ *
8
+ * The table is every shared entry. An entry's body has no `notation.local`,
9
+ * names its neighbours without `[[key]]`, suspends no decoration (rule N22)
10
+ * and derives no cards (rule N23, a page's own): resolve it with
11
+ * `{ suspended: [], derive: false }`.
12
+ */
13
+
14
+ /**
15
+ * One row per shared definition. `fixedIndex` is kept (rule N23): an entry
16
+ * whose index is a fixed label is no family, in an entry's body as on a page.
17
+ *
18
+ * @param {Iterable<{ readonly key: string; readonly notation: string; readonly fixedIndex?: boolean }>} definitions
19
+ * @returns {{ key: string; notation: string; fixedIndex?: true }[]}
20
+ */
21
+ export function registryGlyphScope(definitions) {
22
+ return [...definitions].map((definition) => ({
23
+ key: definition.key,
24
+ notation: definition.notation,
25
+ ...(definition.fixedIndex === true ? { fixedIndex: true } : {}),
26
+ }));
27
+ }
28
+
29
+ /**
30
+ * Every shared entry is already in scope, so neither `[[key]]` nor
31
+ * `notation.local` can bind a symbol the table missed.
32
+ */
33
+ export const REGISTRY_BODY_FIX =
34
+ 'A notation entry’s body binds every shared entry and nothing else: give the symbol an entry of its own in content/notation/, or wrap it in \\explain{key}{latex} with the entry it stands for.';
@@ -3,8 +3,10 @@ import {
3
3
  compareSourceSpans,
4
4
  isNotationKey,
5
5
  localDefinitionId,
6
+ pageScopeLessonIds,
6
7
  RESERVED_KEY_PREFIXES,
7
8
  reservedKeyPrefix,
9
+ resolvePageReferenceId,
8
10
  resolveReferenceId,
9
11
  sharedDefinitionId
10
12
  } from "./references.js";
@@ -72,15 +74,12 @@ function buildNotationRegistry(rawInput) {
72
74
  const directReferences = /* @__PURE__ */ new Map();
73
75
  for (const lesson of lessons.values()) {
74
76
  const resolved = [];
75
- const scope = {
76
- kind: "page",
77
- lessonId: lesson.lessonId
78
- };
77
+ const scopes = pageScopeLessonIds(lesson);
79
78
  for (const reference of sortedReferences(lesson.references)) {
80
79
  if (!validatePageReferenceKey(reference, lesson, diagnostics)) continue;
81
- const definitionId = resolveReferenceId(
80
+ const definitionId = resolvePageReferenceId(
82
81
  reference.key,
83
- scope,
82
+ scopes,
84
83
  availableDefinitionIds
85
84
  );
86
85
  if (definitionId === void 0) {
@@ -129,18 +128,37 @@ function buildNotationRegistry(rawInput) {
129
128
  const backlinks = [];
130
129
  const reachableDefinitions = /* @__PURE__ */ new Set();
131
130
  for (const lesson of lessons.values()) {
132
- const references = directReferences.get(lesson.lessonId) ?? [];
131
+ const scopes = pageScopeLessonIds(lesson);
132
+ const references = [
133
+ ...directReferences.get(lesson.lessonId) ?? [],
134
+ // A chapter's reference, bound as this page binds its key: on a part's
135
+ // deck, a key two of its chapters define is the first one's.
136
+ ...scopes.filter((lessonId) => lessonId !== lesson.lessonId).flatMap((lessonId) => directReferences.get(lessonId) ?? []).flatMap((reference) => {
137
+ const definitionId = resolvePageReferenceId(reference.key, scopes, availableDefinitionIds);
138
+ return definitionId === void 0 ? [] : [{ ...reference, definitionId }];
139
+ })
140
+ ];
133
141
  const directIds = new Set(
134
142
  references.map((reference) => reference.definitionId)
135
143
  );
144
+ const scopedLocalIds = [];
145
+ const scopedLocalKeys = /* @__PURE__ */ new Set();
146
+ for (const lessonId of scopes) {
147
+ for (const id of localIdsByLesson.get(lessonId) ?? []) {
148
+ const key = definitions.get(id)?.input.key;
149
+ if (key === void 0 || scopedLocalKeys.has(key)) continue;
150
+ scopedLocalKeys.add(key);
151
+ scopedLocalIds.push(id);
152
+ }
153
+ }
136
154
  const definitionIds = transitiveClosure(
137
- /* @__PURE__ */ new Set([...directIds, ...localIdsByLesson.get(lesson.lessonId) ?? []]),
155
+ /* @__PURE__ */ new Set([...directIds, ...scopedLocalIds]),
138
156
  edgeIndex
139
157
  );
140
158
  for (const id of definitionIds) reachableDefinitions.add(id);
141
159
  const bindingByKey = /* @__PURE__ */ new Map();
142
160
  for (const reference of references) {
143
- bindingByKey.set(reference.key, reference.definitionId);
161
+ if (!bindingByKey.has(reference.key)) bindingByKey.set(reference.key, reference.definitionId);
144
162
  }
145
163
  bundles.push({
146
164
  lessonId: lesson.lessonId,
@@ -150,7 +168,8 @@ function buildNotationRegistry(rawInput) {
150
168
  ),
151
169
  definitionIds
152
170
  });
153
- for (const definitionId of definitionIds) {
171
+ const isDeck = lesson.scopeLessonIds !== void 0;
172
+ for (const definitionId of isDeck ? [] : definitionIds) {
154
173
  const definition = definitions.get(definitionId);
155
174
  if (definition === void 0) continue;
156
175
  backlinks.push({
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Turn a deck page's flow into its frames, in place. `deck` is
3
+ * `file.data.explicoDeck`: the ratio, the canvas, who sees the notes, each
4
+ * frame's id and, on a part's deck, each frame's chapter.
5
+ */
6
+ export function frameDeck(tree: any, deck: any): void;
@@ -0,0 +1,286 @@
1
+ /**
2
+ * The rehype half of a deck page (`<lesson>.deck.mdx`, docs/authoring/deck.md):
3
+ * the page's flow becomes its frames. `rehype-heading-anchors.mjs` hands a
4
+ * deck page here (`remark-notation.mjs` marks one with `file.data.explicoDeck`),
5
+ * so a course wires nothing new.
6
+ *
7
+ * Each `##` (a section's divider) and `###` (a frame) heading, stamped with
8
+ * its frame number by the remark half (`deck-remark.mjs`), opens a
9
+ *
10
+ * <section class="deck-frame" id="slide-N" data-frame="N" data-kind="frame"
11
+ * data-frame-id="3f9a0c1e77b2" what it shows (`ManifestDeckFrame.id`)
12
+ * role="group" aria-roledescription="slide" aria-label="N of T">
13
+ * <div class="deck-stage"> the frame's box on the page
14
+ * <div class="deck-canvas"> the fixed canvas the course's ratio sets, scaled to it
15
+ * <h3 class="deck-frame__title">…</h3>
16
+ * <div class="deck-frame__body">…</div> its steps: `.deck-step[data-step]`
17
+ * <div class="deck-legend">…</div> the symbols it introduces
18
+ * <div class="deck-frame__foot">…</div> section and frame number
19
+ * </div>
20
+ * <aside class="deck-notes">…</aside> the speaker's notes, outside the canvas
21
+ * </section>
22
+ *
23
+ * `<Pause />` splits a frame's body into steps; its props (`control`, `from`,
24
+ * `to`, `seconds`; `highlight`) ride on the step for the page's controller
25
+ * (`components/deck/deck-controller.ts`). Two lists a step boundary splits
26
+ * become one list whose later items are the step.
27
+ */
28
+ import { stripMarkers } from './rehype-heading-anchors.mjs';
29
+
30
+ const HEADING = /^h[1-6]$/;
31
+
32
+ /** A hast property by its attribute name, in either the attribute or the property spelling. */
33
+ function prop(node, name) {
34
+ const properties = node?.properties ?? {};
35
+ if (properties[name] !== undefined) return properties[name];
36
+ const camel = name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
37
+ return properties[camel];
38
+ }
39
+
40
+ function classes(node) {
41
+ const value = prop(node, 'className') ?? prop(node, 'class');
42
+ if (Array.isArray(value)) return value.map(String);
43
+ return typeof value === 'string' ? value.split(/\s+/) : [];
44
+ }
45
+
46
+ function element(tagName, properties, children = []) {
47
+ return { type: 'element', tagName, properties, children };
48
+ }
49
+
50
+ function isWhitespace(node) {
51
+ return node?.type === 'text' && !String(node.value ?? '').trim();
52
+ }
53
+
54
+ function isJsx(node, name) {
55
+ return (node?.type === 'mdxJsxFlowElement' || node?.type === 'mdxJsxTextElement') && node.name === name;
56
+ }
57
+
58
+ function jsxAttributes(node) {
59
+ const values = {};
60
+ for (const attribute of node?.attributes ?? []) {
61
+ if (attribute?.type === 'mdxJsxAttribute' && typeof attribute.value === 'string') {
62
+ values[attribute.name] = attribute.value;
63
+ }
64
+ }
65
+ return values;
66
+ }
67
+
68
+ function textOf(node) {
69
+ if (!node) return '';
70
+ if (node.type === 'text') return String(node.value ?? '');
71
+ if (classes(node).includes('katex-mathml') || node.tagName === 'annotation') return '';
72
+ return (node.children ?? []).map(textOf).join('');
73
+ }
74
+
75
+ /** A heading the remark half stamped, and what kind of frame it opens. */
76
+ function frameKindOf(node) {
77
+ if (node?.type !== 'element' || !HEADING.test(node.tagName)) return undefined;
78
+ const kind = prop(node, 'data-deck-kind');
79
+ if (kind === 'section' || kind === 'frame') return kind;
80
+ if (node.tagName === 'h2' && prop(node, 'id') === 'references') return 'references';
81
+ return undefined;
82
+ }
83
+
84
+ function isGeneratedFrame(node) {
85
+ return node?.type === 'element' && node.tagName === 'section' && classes(node).includes('deck-frame');
86
+ }
87
+
88
+ /** A step's data attributes, from its `<Pause>` props. */
89
+ function stepProperties(step, pause) {
90
+ const properties = { 'data-step': String(step) };
91
+ for (const name of ['control', 'from', 'to', 'seconds', 'highlight']) {
92
+ if (pause[name] !== undefined) properties[`data-step-${name}`] = pause[name];
93
+ }
94
+ return properties;
95
+ }
96
+
97
+ /** A frame's content in steps: what follows the k-th `<Pause />` is step k + 1. */
98
+ function stepped(content) {
99
+ const groups = [{ step: 1, pause: {}, nodes: [] }];
100
+ for (const node of content) {
101
+ if (isJsx(node, 'Pause')) {
102
+ groups.push({ step: groups.length + 1, pause: jsxAttributes(node), nodes: [] });
103
+ continue;
104
+ }
105
+ groups.at(-1).nodes.push(node);
106
+ }
107
+ const out = [...groups[0].nodes];
108
+ const lastElement = () => [...out].reverse().find((node) => node.type === 'element');
109
+ for (const group of groups.slice(1)) {
110
+ const properties = stepProperties(group.step, group.pause);
111
+ const nodes = group.nodes.filter((node) => !isWhitespace(node));
112
+ const [first, ...rest] = nodes;
113
+ const previous = lastElement();
114
+ // Two lists the step mark splits are one list: its later items are the step.
115
+ if (
116
+ first?.type === 'element' &&
117
+ (first.tagName === 'ul' || first.tagName === 'ol') &&
118
+ previous?.tagName === first.tagName
119
+ ) {
120
+ let marked = false;
121
+ for (const item of first.children ?? []) {
122
+ if (item.type !== 'element') continue;
123
+ item.properties = {
124
+ ...item.properties,
125
+ className: [...classes(item), 'deck-step'],
126
+ ...(marked ? { 'data-step': String(group.step) } : properties),
127
+ };
128
+ marked = true;
129
+ }
130
+ previous.children.push(...(first.children ?? []));
131
+ if (rest.length > 0) out.push(element('div', { className: ['deck-step'], 'data-step': String(group.step) }, rest));
132
+ continue;
133
+ }
134
+ // A step with nothing to show still drives its figure or lights its symbol.
135
+ out.push(element('div', { className: ['deck-step'], ...properties }, nodes));
136
+ }
137
+ return out;
138
+ }
139
+
140
+ function frameShell({ number, kind, id, frameId, chapter, extra = {}, canvas, notes = [] }) {
141
+ return element(
142
+ 'section',
143
+ {
144
+ className: ['deck-frame', `deck-frame--${kind}`],
145
+ id: `slide-${number}`,
146
+ 'data-frame': String(number),
147
+ ...(frameId ? { 'data-frame-id': frameId } : {}),
148
+ ...(chapter ? { 'data-chapter': chapter } : {}),
149
+ 'data-kind': kind,
150
+ role: 'group',
151
+ 'aria-roledescription': 'slide',
152
+ ...(id ? { 'data-frame-anchor': id } : {}),
153
+ ...extra,
154
+ },
155
+ // The stage takes the frame's size on the page; the canvas inside it is
156
+ // laid out at the course's canvas size and scaled to the stage, so a frame
157
+ // looks the same at every width (`docs/diagram-scaling.md`).
158
+ [element('div', { className: ['deck-stage'] }, [element('div', { className: ['deck-canvas'] }, canvas)]), ...notes],
159
+ );
160
+ }
161
+
162
+ function foot(number, sectionTitle) {
163
+ return element('div', { className: ['deck-frame__foot'], 'aria-hidden': 'true' }, [
164
+ ...(sectionTitle ? [element('span', { className: ['deck-frame__section'] }, [{ type: 'text', value: sectionTitle }])] : []),
165
+ element('span', { className: ['deck-frame__number'] }, [{ type: 'text', value: String(number) }]),
166
+ ]);
167
+ }
168
+
169
+ /**
170
+ * Turn a deck page's flow into its frames, in place. `deck` is
171
+ * `file.data.explicoDeck`: the ratio, the canvas, who sees the notes, each
172
+ * frame's id and, on a part's deck, each frame's chapter.
173
+ */
174
+ export function frameDeck(tree, deck) {
175
+ const esm = [];
176
+ const frames = [];
177
+ let current;
178
+ let sectionTitle;
179
+ let lastNumber = 0;
180
+ const frameIdOf = (number) => deck.frameIds?.[number - 1];
181
+ const chapterOf = (number) => deck.frameChapters?.[number - 1] ?? undefined;
182
+
183
+ const close = () => {
184
+ if (!current) return;
185
+ const { heading, kind, body } = current;
186
+ current = undefined;
187
+ const markers = stripMarkers(heading);
188
+ const stamped = Number(prop(heading, 'data-deck-frame'));
189
+ const number = Number.isFinite(stamped) && stamped > 0 ? stamped : lastNumber + 1;
190
+ lastNumber = number;
191
+ const id = prop(heading, 'id');
192
+ heading.properties = {
193
+ ...heading.properties,
194
+ className: [...classes(heading), 'deck-frame__title'],
195
+ };
196
+ for (const name of ['data-deck-frame', 'data-deck-kind', 'data-deck-steps', 'data-deck-morph']) {
197
+ delete heading.properties[name];
198
+ delete heading.properties[name.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())];
199
+ }
200
+ if (kind === 'section') {
201
+ sectionTitle = textOf(heading).trim();
202
+ frames.push(frameShell({ number, kind, id, frameId: frameIdOf(number), chapter: chapterOf(number), canvas: [heading, foot(number)] }));
203
+ return;
204
+ }
205
+ const notes = [];
206
+ const legend = [];
207
+ const content = [];
208
+ for (const node of body) {
209
+ if (isJsx(node, 'Notes')) {
210
+ // A landmark each, so each needs a name of its own (axe: landmark-unique).
211
+ notes.push(element('aside', { className: ['deck-notes'], 'aria-label': `Speaker notes for slide ${number}` }, node.children ?? []));
212
+ } else if (node?.type === 'element' && classes(node).includes('deck-legend')) {
213
+ legend.push(node);
214
+ } else {
215
+ content.push(node);
216
+ }
217
+ }
218
+ const steps = content.filter((node) => isJsx(node, 'Pause')).length + 1;
219
+ frames.push(
220
+ frameShell({
221
+ number,
222
+ kind,
223
+ id,
224
+ frameId: frameIdOf(number),
225
+ chapter: chapterOf(number),
226
+ extra: {
227
+ 'data-steps': String(steps),
228
+ ...(markers.morph || prop(heading, 'data-deck-morph') !== undefined ? { 'data-morph': '' } : {}),
229
+ },
230
+ canvas: [
231
+ heading,
232
+ element('div', { className: ['deck-frame__body'] }, kind === 'frame' ? stepped(content) : content),
233
+ ...legend,
234
+ foot(number, kind === 'frame' ? sectionTitle : undefined),
235
+ ],
236
+ notes: deck.notes === 'off' ? [] : notes,
237
+ }),
238
+ );
239
+ };
240
+
241
+ for (const node of tree.children ?? []) {
242
+ if (node?.type === 'mdxjsEsm') {
243
+ esm.push(node);
244
+ continue;
245
+ }
246
+ if (isGeneratedFrame(node)) {
247
+ close();
248
+ const number = Number(prop(node, 'data-deck-frame')) || lastNumber + 1;
249
+ lastNumber = number;
250
+ const kind = prop(node, 'data-deck-kind') ?? 'frame';
251
+ frames.push(frameShell({ number, kind, frameId: frameIdOf(number), chapter: chapterOf(number), canvas: [...(node.children ?? []), foot(number)] }));
252
+ continue;
253
+ }
254
+ const kind = frameKindOf(node);
255
+ if (kind) {
256
+ close();
257
+ current = { heading: node, kind, body: [] };
258
+ continue;
259
+ }
260
+ if (current) current.body.push(node);
261
+ else if (!isWhitespace(node)) frames.push(node);
262
+ }
263
+ close();
264
+
265
+ const total = frames.filter((node) => isGeneratedFrame(node)).length;
266
+ for (const frame of frames) {
267
+ if (!isGeneratedFrame(frame)) continue;
268
+ frame.properties['aria-label'] = `${frame.properties['data-frame']} of ${total}`;
269
+ }
270
+ const canvas = deck.canvas ?? { width: 1280, height: 720 };
271
+ tree.children = [
272
+ ...esm,
273
+ element(
274
+ 'div',
275
+ {
276
+ className: ['deck'],
277
+ 'data-deck': deck.slug ?? '',
278
+ 'data-aspect': deck.aspectRatio ?? '16:9',
279
+ 'data-notes': deck.notes ?? 'reader',
280
+ 'data-frames': String(total),
281
+ style: `--deck-canvas-width: ${canvas.width}px; --deck-canvas-height: ${canvas.height}px;`,
282
+ },
283
+ frames,
284
+ ),
285
+ ];
286
+ }
@@ -12,4 +12,24 @@ export function stripHeadingMarker(text: any): string;
12
12
  * in `stripMarkers`.
13
13
  */
14
14
  export function headingCardTier(text: any): any;
15
+ /** True when a heading's source text carries the deck's `[morph]` marker. */
16
+ export function headingMorphs(text: any): boolean;
17
+ /**
18
+ * The chapter a part deck's section names, `## Title [chapter: <slug>]`, as
19
+ * written, or `null` without the marker. With two, the one written last wins.
20
+ */
21
+ export function headingChapter(text: any): string | null;
22
+ /**
23
+ * Strip every trailing marker off a hast heading and report them as
24
+ * `{ collapse, card, morph, chapter }` — the collapse marker and the card
25
+ * tier, each `null` when absent, whether a deck frame morphs, and the chapter
26
+ * a deck's section names (`null` without one). With two of one kind, the one
27
+ * written last wins.
28
+ */
29
+ export function stripMarkers(headingNode: any): {
30
+ collapse: null;
31
+ card: null;
32
+ morph: boolean;
33
+ chapter: null;
34
+ };
15
35
  export default function rehypeHeadingAnchors(options?: {}): (tree: any, file: any) => void;
@@ -60,6 +60,7 @@
60
60
  * Astro's slug, renamed only if it would repeat an id given upstream.
61
61
  */
62
62
 
63
+ import { frameDeck } from './rehype-deck.mjs';
63
64
  import { rehypeHeadingIds } from '@astrojs/markdown-remark';
64
65
 
65
66
  const HEADING_TAG = /^h([1-6])$/;
@@ -103,7 +104,12 @@ function headingText(node) {
103
104
  // (e.g. `{#id .class}`) — this codebase has no remark-attr-style plugin, and
104
105
  // a handful of purpose-built tokens are enough for what an author needs to
105
106
  // say about a heading here.
106
- const HEADING_MARKER = /\s*\[(collapsible|collapsed|no-collapsible|card|card=meta)\]\s*$/i;
107
+ // `[morph]` and `[chapter: <slug>]` are a deck's: on a frame title `[morph]`
108
+ // asks for the frame to morph from the one before it, and on a part's deck a
109
+ // section's `[chapter: <slug>]` names the chapter it covers
110
+ // (`docs/authoring/deck.md`); a chapter ignores both.
111
+ const HEADING_MARKER = /\s*\[(collapsible|collapsed|no-collapsible|card|card=meta|morph|chapter:[^\]]*)\]\s*$/i;
112
+ const CHAPTER_MARKER = /^chapter:\s*(.*?)\s*$/i;
107
113
 
108
114
  const CARD_TIER = { card: 'accent', 'card=meta': 'meta' };
109
115
 
@@ -138,13 +144,39 @@ export function headingCardTier(text) {
138
144
  return tier;
139
145
  }
140
146
 
147
+ /** True when a heading's source text carries the deck's `[morph]` marker. */
148
+ export function headingMorphs(text) {
149
+ let rest = String(text);
150
+ for (let match = HEADING_MARKER.exec(rest); match; match = HEADING_MARKER.exec(rest)) {
151
+ if (match[1].toLowerCase() === 'morph') return true;
152
+ rest = rest.slice(0, match.index);
153
+ }
154
+ return false;
155
+ }
156
+
157
+ /**
158
+ * The chapter a part deck's section names, `## Title [chapter: <slug>]`, as
159
+ * written, or `null` without the marker. With two, the one written last wins.
160
+ */
161
+ export function headingChapter(text) {
162
+ let rest = String(text);
163
+ for (let match = HEADING_MARKER.exec(rest); match; match = HEADING_MARKER.exec(rest)) {
164
+ const chapter = CHAPTER_MARKER.exec(match[1]);
165
+ if (chapter) return chapter[1];
166
+ rest = rest.slice(0, match.index);
167
+ }
168
+ return null;
169
+ }
170
+
141
171
  /**
142
172
  * Strip every trailing marker off a hast heading and report them as
143
- * `{ collapse, card }` — the collapse marker and the card tier, each `null`
144
- * when absent. With two of one kind, the one written last wins.
173
+ * `{ collapse, card, morph, chapter }` — the collapse marker and the card
174
+ * tier, each `null` when absent, whether a deck frame morphs, and the chapter
175
+ * a deck's section names (`null` without one). With two of one kind, the one
176
+ * written last wins.
145
177
  */
146
- function stripMarkers(headingNode) {
147
- const found = { collapse: null, card: null };
178
+ export function stripMarkers(headingNode) {
179
+ const found = { collapse: null, card: null, morph: false, chapter: null };
148
180
  const children = headingNode.children;
149
181
  for (let index = children.length - 1; index >= 0; index--) {
150
182
  const node = children[index];
@@ -152,7 +184,10 @@ function stripMarkers(headingNode) {
152
184
  const match = HEADING_MARKER.exec(node.value);
153
185
  if (match) {
154
186
  const marker = match[1].toLowerCase();
187
+ const chapter = CHAPTER_MARKER.exec(match[1]);
155
188
  if (marker in CARD_TIER) found.card ??= CARD_TIER[marker];
189
+ else if (marker === 'morph') found.morph = true;
190
+ else if (chapter) found.chapter ??= chapter[1];
156
191
  else found.collapse ??= marker;
157
192
  node.value = node.value.slice(0, match.index);
158
193
  if (!node.value) children.splice(index, 1);
@@ -163,7 +198,7 @@ function stripMarkers(headingNode) {
163
198
  // trailing whitespace-only text node: keep looking further back
164
199
  }
165
200
  const last = children.at(-1);
166
- if ((found.collapse || found.card) && last?.type === 'text') {
201
+ if ((found.collapse || found.card || found.morph || found.chapter !== null) && last?.type === 'text') {
167
202
  last.value = last.value.trimEnd();
168
203
  }
169
204
  return found;
@@ -244,6 +279,13 @@ export default function rehypeHeadingAnchors(options = {}) {
244
279
  const assignHeadingIds = rehypeHeadingIds();
245
280
 
246
281
  return function transform(tree, file) {
282
+ // A deck's page is its frames (`rehype-deck.mjs`): its headings are frame
283
+ // titles, anchored by the manifest, never collapsible sections.
284
+ if (file?.data?.explicoDeck) {
285
+ frameDeck(tree, file.data.explicoDeck);
286
+ assignHeadingIds(tree, file);
287
+ return;
288
+ }
247
289
  // Markers must be stripped *before* `rehypeHeadingIds` runs — otherwise
248
290
  // the generated slug is built from text that still contains it (e.g.
249
291
  // `two-ways-to-name-the-same-point-collapsed` instead of the clean