explico 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +990 -1
- package/README.md +200 -30
- package/dist/compiler/astro.d.ts +15 -2
- package/dist/compiler/astro.js +40 -11
- package/dist/compiler/cli-main.d.ts +100 -0
- package/dist/compiler/cli-main.js +433 -0
- package/dist/compiler/cli-reference.d.ts +50 -0
- package/dist/compiler/cli-reference.js +213 -0
- package/dist/compiler/cli-verbs.d.ts +45 -0
- package/dist/compiler/cli-verbs.js +224 -0
- package/dist/compiler/cli-verify.d.ts +51 -0
- package/dist/compiler/cli-verify.js +151 -0
- package/dist/compiler/cli.d.ts +408 -24
- package/dist/compiler/cli.js +1328 -8
- package/dist/compiler/collections.d.ts +88 -1
- package/dist/compiler/collections.js +170 -25
- package/dist/compiler/content-release.d.ts +49 -0
- package/dist/compiler/content-release.js +63 -0
- package/dist/compiler/content-schemas.d.ts +2 -2
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +105 -1
- package/dist/compiler/decks.d.ts +161 -0
- package/dist/compiler/decks.js +235 -0
- package/dist/compiler/exports/combined-html.d.ts +5 -1
- package/dist/compiler/exports/deck-fit.d.ts +55 -0
- package/dist/compiler/exports/deck-fit.js +90 -0
- package/dist/compiler/exports/export-chapters.d.ts +37 -6
- package/dist/compiler/exports/export-chapters.js +64 -5
- package/dist/compiler/exports/export-pages.d.ts +30 -3
- package/dist/compiler/exports/export-pages.js +24 -1
- package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/dist/compiler/exports/latex/cache.d.ts +78 -0
- package/dist/compiler/exports/latex/cache.js +129 -0
- package/dist/compiler/exports/latex/capture.d.ts +10 -1
- package/dist/compiler/exports/latex/capture.js +3 -1
- package/dist/compiler/exports/latex/compile.d.ts +13 -2
- package/dist/compiler/exports/latex/compile.js +12 -1
- package/dist/compiler/exports/latex/deck.d.ts +92 -0
- package/dist/compiler/exports/latex/deck.js +349 -0
- package/dist/compiler/exports/latex/document.d.ts +51 -6
- package/dist/compiler/exports/latex/document.js +71 -12
- package/dist/compiler/exports/latex/explico-common.sty +198 -0
- package/dist/compiler/exports/latex/explico.cls +13 -183
- package/dist/compiler/exports/latex/serialize.d.ts +60 -4
- package/dist/compiler/exports/latex/serialize.js +4 -2
- package/dist/compiler/exports/latex-export.d.ts +24 -2
- package/dist/compiler/exports/latex-export.js +107 -26
- package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
- package/dist/compiler/exports/markdown/serialize.js +85 -8
- package/dist/compiler/exports/markdown-export.d.ts +6 -1
- package/dist/compiler/exports/markdown-export.js +50 -11
- package/dist/compiler/exports/standalone-html.d.ts +10 -5
- package/dist/compiler/exports/standalone-html.js +3 -3
- package/dist/compiler/exports/static-tree.d.ts +38 -1
- package/dist/compiler/exports/static-tree.js +120 -4
- package/dist/compiler/knowledge-version.d.ts +26 -2
- package/dist/compiler/knowledge-version.js +27 -17
- package/dist/compiler/lesson-derivation.d.ts +143 -6
- package/dist/compiler/lesson-derivation.js +145 -13
- package/dist/compiler/manifest.d.ts +133 -4
- package/dist/compiler/manifest.js +364 -58
- package/dist/compiler/page-versions.d.ts +184 -0
- package/dist/compiler/page-versions.js +217 -0
- package/dist/compiler/render-diagnostics.d.ts +5 -1
- package/dist/compiler/resolution-report.d.ts +24 -6
- package/dist/compiler/resolution-report.js +22 -4
- package/dist/compiler/validate-content.d.ts +7 -0
- package/dist/compiler/validate-content.js +160 -0
- package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
- package/dist/curriculum/competency-graph.d.ts +54 -0
- package/dist/curriculum/competency-graph.js +41 -0
- package/dist/reference/base-library.d.mts +8 -0
- package/dist/reference/base-library.mjs +19 -0
- package/dist/reference/consistency.d.ts +8 -4
- package/dist/reference/consistency.js +82 -26
- package/dist/reference/deck-remark.d.mts +123 -0
- package/dist/reference/deck-remark.mjs +600 -0
- package/dist/reference/deck-source.d.ts +107 -0
- package/dist/reference/deck-source.js +140 -0
- package/dist/reference/decks-validate.d.ts +133 -0
- package/dist/reference/decks-validate.js +359 -0
- package/dist/reference/derived-entries.d.ts +5 -1
- package/dist/reference/diagram-fit.d.ts +6 -2
- package/dist/reference/diagram-fit.js +5 -1
- package/dist/reference/figure-symbols.d.ts +23 -6
- package/dist/reference/figure-symbols.js +51 -15
- package/dist/reference/float-name.d.ts +43 -0
- package/dist/reference/float-name.js +58 -0
- package/dist/reference/folds-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-indices.js +5 -4
- package/dist/reference/gate-math.d.ts +29 -2
- package/dist/reference/gate-math.js +99 -18
- package/dist/reference/gate-part-pages.d.ts +1 -1
- package/dist/reference/lab-katex-validate.d.ts +10 -6
- package/dist/reference/lab-katex-validate.js +17 -9
- package/dist/reference/lab-math-scope.js +29 -4
- package/dist/reference/math-glyphs.d.mts +25 -0
- package/dist/reference/math-glyphs.mjs +189 -25
- package/dist/reference/page-notation.d.ts +22 -3
- package/dist/reference/page-notation.js +32 -10
- package/dist/reference/references.d.ts +14 -1
- package/dist/reference/references.js +13 -0
- package/dist/reference/registry-scope.d.mts +33 -0
- package/dist/reference/registry-scope.mjs +34 -0
- package/dist/reference/registry.js +29 -10
- package/dist/reference/rehype-deck.d.mts +6 -0
- package/dist/reference/rehype-deck.mjs +286 -0
- package/dist/reference/rehype-heading-anchors.d.mts +20 -0
- package/dist/reference/rehype-heading-anchors.mjs +48 -6
- package/dist/reference/remark-notation.mjs +249 -40
- package/dist/reference/source-lines.d.mts +12 -0
- package/dist/reference/source-lines.mjs +65 -0
- package/dist/reference/types.d.ts +28 -0
- package/dist/rules/catalog.d.ts +21 -4
- package/dist/rules/catalog.js +237 -2
- package/dist/rules/course-rules.d.ts +5 -1
- package/dist/rules/course-rules.js +17 -3
- package/dist/rules/course-rules.schema.json +12 -0
- package/dist/rules/in-effect.d.ts +13 -3
- package/dist/rules/in-effect.js +34 -10
- package/dist/rules/rules.schema.json +280 -1
- package/dist/rules/settings.d.ts +1 -1
- package/dist/session/PreferenceStore.d.ts +6 -0
- package/dist/session/PreferenceStore.js +18 -1
- package/dist/session/deck-memory.d.ts +116 -0
- package/dist/session/deck-memory.js +143 -0
- package/dist/session/reading-memory.d.ts +96 -0
- package/dist/session/reading-memory.js +133 -0
- package/docs/VERSIONING.md +21 -18
- package/docs/authoring/README.md +31 -9
- package/docs/authoring/agents.md +72 -18
- package/docs/authoring/cli-reference.json +2172 -0
- package/docs/authoring/cli.md +887 -0
- package/docs/authoring/components.md +15 -0
- package/docs/authoring/deck.md +612 -0
- package/docs/authoring/migrating.md +1195 -16
- package/docs/authoring/notation.md +49 -6
- package/docs/authoring/structure.md +80 -1
- package/docs/authoring/versioning.md +56 -3
- package/docs/authoring/visualizations.md +7 -2
- package/docs/course-agent/AGENTS.md +40 -0
- package/docs/course-agent/agents/rules-reviewer.md +33 -0
- package/docs/course-agent/skills/draft-lesson/SKILL.md +38 -0
- package/docs/course-agent/skills/migrate-course/SKILL.md +32 -0
- package/docs/course-agent/skills/release-content/SKILL.md +26 -0
- package/docs/diagram-scaling.md +148 -0
- package/docs/privacy.md +59 -0
- package/package.json +8 -2
- package/src/compiler/astro.ts +60 -21
- package/src/compiler/cli-main.ts +557 -0
- package/src/compiler/cli-reference.ts +285 -0
- package/src/compiler/cli-verbs.ts +270 -0
- package/src/compiler/cli-verify.ts +241 -0
- package/src/compiler/cli.ts +2119 -170
- package/src/compiler/collections.ts +284 -25
- package/src/compiler/content-release.ts +91 -0
- package/src/compiler/course-config.ts +267 -0
- package/src/compiler/decks.ts +455 -0
- package/src/compiler/exports/deck-fit.ts +127 -0
- package/src/compiler/exports/export-chapters.ts +105 -6
- package/src/compiler/exports/export-pages.ts +46 -2
- package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/src/compiler/exports/latex/cache.ts +183 -0
- package/src/compiler/exports/latex/capture.ts +5 -2
- package/src/compiler/exports/latex/compile.ts +18 -0
- package/src/compiler/exports/latex/deck.ts +419 -0
- package/src/compiler/exports/latex/document.ts +117 -21
- package/src/compiler/exports/latex/explico-common.sty +198 -0
- package/src/compiler/exports/latex/explico.cls +13 -183
- package/src/compiler/exports/latex/serialize.ts +25 -21
- package/src/compiler/exports/latex/texlive-packages.txt +6 -2
- package/src/compiler/exports/latex-export.ts +187 -45
- package/src/compiler/exports/markdown/serialize.ts +118 -9
- package/src/compiler/exports/markdown-export.ts +56 -11
- package/src/compiler/exports/standalone-html.ts +8 -7
- package/src/compiler/exports/static-tree.ts +175 -3
- package/src/compiler/knowledge-version.ts +56 -21
- package/src/compiler/lesson-derivation.ts +282 -16
- package/src/compiler/manifest.ts +568 -63
- package/src/compiler/page-versions.ts +401 -0
- package/src/compiler/resolution-report.ts +38 -7
- package/src/compiler/validate-content.ts +215 -0
- package/src/components/CourseContents.astro +124 -6
- package/src/components/CurriculumMap.astro +49 -1
- package/src/components/FloatIndex.astro +1 -1
- package/src/components/PartOverview.astro +51 -5
- package/src/components/assessments/AssessmentSet.astro +41 -15
- package/src/components/block-heading.ts +15 -6
- package/src/components/card-carry.ts +7 -4
- package/src/components/card-manager.ts +14 -2
- package/src/components/card-memory.ts +13 -9
- package/src/components/deck/DeckEnhancer.astro +13 -0
- package/src/components/deck/deck-context.ts +104 -0
- package/src/components/deck/deck-controller.ts +1287 -0
- package/src/components/deck/deck-map.ts +91 -0
- package/src/components/deck-lane.ts +163 -0
- package/src/components/diagram/Diagram.astro +131 -86
- package/src/components/disclosure/persisted-disclosure.ts +9 -1
- package/src/components/figure/Figure.astro +7 -2
- package/src/components/figure/FloatCaption.astro +5 -3
- package/src/components/fold/Fold.astro +2 -2
- package/src/components/notation/NotationFormula.astro +81 -11
- package/src/components/notation/NotationGlossary.astro +132 -1
- package/src/components/notation/NotationLayer.astro +15 -7
- package/src/components/starlight/AboutPanel.astro +186 -3
- package/src/components/starlight/CollapsibleSections.astro +24 -3
- package/src/components/starlight/DiagramEnhancer.astro +5 -1
- package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
- package/src/components/starlight/LayoutHeader.astro +17 -0
- package/src/components/starlight/LayoutPageTitle.astro +110 -0
- package/src/components/starlight/LayoutSidebar.astro +807 -21
- package/src/components/starlight/LayoutSocialIcons.astro +62 -6
- package/src/components/starlight/LessonExportActions.astro +27 -9
- package/src/components/starlight/LessonFooter.astro +59 -0
- package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
- package/src/components/starlight/TextSizeControl.astro +24 -13
- package/src/components/starlight/page-marks.ts +139 -0
- package/src/components/starlight/section-summary.ts +1 -1
- package/src/components/starlight/sidebar-editions.ts +245 -0
- package/src/components/table/Table.astro +15 -7
- package/src/content-config.ts +87 -1
- package/src/curriculum/competency-graph.ts +95 -0
- package/src/reference/base-library.mjs +19 -0
- package/src/reference/consistency.ts +137 -39
- package/src/reference/deck-remark.mjs +600 -0
- package/src/reference/deck-source.ts +273 -0
- package/src/reference/decks-validate.ts +604 -0
- package/src/reference/diagram-fit.ts +18 -4
- package/src/reference/diagram-layout.ts +3 -1
- package/src/reference/figure-symbols.ts +108 -31
- package/src/reference/folds-validate.ts +1 -1
- package/src/reference/gate-indices.ts +5 -3
- package/src/reference/gate-math.ts +169 -25
- package/src/reference/lab-katex-validate.ts +30 -15
- package/src/reference/lab-math-scope.ts +34 -3
- package/src/reference/math-glyphs.mjs +189 -25
- package/src/reference/page-notation.ts +69 -15
- package/src/reference/references.ts +28 -0
- package/src/reference/registry-scope.mjs +34 -0
- package/src/reference/registry.ts +41 -11
- package/src/reference/rehype-deck.mjs +286 -0
- package/src/reference/rehype-heading-anchors.mjs +48 -6
- package/src/reference/remark-notation.mjs +249 -40
- package/src/reference/source-lines.mjs +65 -0
- package/src/reference/types.ts +28 -0
- package/src/rules/catalog.ts +274 -4
- package/src/rules/course-rules.ts +21 -1
- package/src/rules/in-effect.ts +58 -11
- package/src/session/PreferenceStore.ts +23 -0
- package/src/session/deck-memory.ts +263 -0
- package/src/session/reading-memory.ts +243 -0
- package/src/styles/deck.css +1150 -0
- package/src/styles/global.css +958 -27
- package/src/styles/zones.css +277 -0
- /package/src/{components → reference}/float-name.ts +0 -0
|
@@ -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';
|
|
@@ -14,7 +16,6 @@ import type {
|
|
|
14
16
|
LocalNotationDefinitionRecord,
|
|
15
17
|
NotationBacklink,
|
|
16
18
|
NotationDefinitionRecord,
|
|
17
|
-
NotationDefinitionScope,
|
|
18
19
|
NotationDiagnostic,
|
|
19
20
|
NotationLessonInput,
|
|
20
21
|
NotationPageBundle,
|
|
@@ -124,16 +125,16 @@ export function buildNotationRegistry(
|
|
|
124
125
|
const directReferences = new Map<string, ResolvedNotationReference[]>();
|
|
125
126
|
for (const lesson of lessons.values()) {
|
|
126
127
|
const resolved: ResolvedNotationReference[] = [];
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
128
|
+
// A deck resolves in its lessons' scopes: a deck beside its chapter in
|
|
129
|
+
// the chapter's, whose `notation.local` entries are the lesson's; a
|
|
130
|
+
// part's deck in its chapters', the first to define a key binding it.
|
|
131
|
+
const scopes = pageScopeLessonIds(lesson);
|
|
131
132
|
|
|
132
133
|
for (const reference of sortedReferences(lesson.references)) {
|
|
133
134
|
if (!validatePageReferenceKey(reference, lesson, diagnostics)) continue;
|
|
134
|
-
const definitionId =
|
|
135
|
+
const definitionId = resolvePageReferenceId(
|
|
135
136
|
reference.key,
|
|
136
|
-
|
|
137
|
+
scopes,
|
|
137
138
|
availableDefinitionIds,
|
|
138
139
|
);
|
|
139
140
|
if (definitionId === undefined) {
|
|
@@ -203,19 +204,45 @@ export function buildNotationRegistry(
|
|
|
203
204
|
const reachableDefinitions = new Set<string>();
|
|
204
205
|
|
|
205
206
|
for (const lesson of lessons.values()) {
|
|
206
|
-
|
|
207
|
+
// A deck shows its lessons' symbols: its bundle holds what its chapters
|
|
208
|
+
// name as well as what it names itself. A key is bound as the deck
|
|
209
|
+
// binds it, then as the first of its chapters does.
|
|
210
|
+
const scopes = pageScopeLessonIds(lesson);
|
|
211
|
+
const references = [
|
|
212
|
+
...(directReferences.get(lesson.lessonId) ?? []),
|
|
213
|
+
// A chapter's reference, bound as this page binds its key: on a part's
|
|
214
|
+
// deck, a key two of its chapters define is the first one's.
|
|
215
|
+
...scopes
|
|
216
|
+
.filter((lessonId) => lessonId !== lesson.lessonId)
|
|
217
|
+
.flatMap((lessonId) => directReferences.get(lessonId) ?? [])
|
|
218
|
+
.flatMap((reference) => {
|
|
219
|
+
const definitionId = resolvePageReferenceId(reference.key, scopes, availableDefinitionIds);
|
|
220
|
+
return definitionId === undefined ? [] : [{ ...reference, definitionId }];
|
|
221
|
+
}),
|
|
222
|
+
];
|
|
207
223
|
const directIds = new Set(
|
|
208
224
|
references.map((reference) => reference.definitionId),
|
|
209
225
|
);
|
|
226
|
+
// Every local entry in scope seeds the bundle, the first of a key only.
|
|
227
|
+
const scopedLocalIds: string[] = [];
|
|
228
|
+
const scopedLocalKeys = new Set<string>();
|
|
229
|
+
for (const lessonId of scopes) {
|
|
230
|
+
for (const id of localIdsByLesson.get(lessonId) ?? []) {
|
|
231
|
+
const key = definitions.get(id)?.input.key;
|
|
232
|
+
if (key === undefined || scopedLocalKeys.has(key)) continue;
|
|
233
|
+
scopedLocalKeys.add(key);
|
|
234
|
+
scopedLocalIds.push(id);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
210
237
|
const definitionIds = transitiveClosure(
|
|
211
|
-
new Set([...directIds, ...
|
|
238
|
+
new Set([...directIds, ...scopedLocalIds]),
|
|
212
239
|
edgeIndex,
|
|
213
240
|
);
|
|
214
241
|
for (const id of definitionIds) reachableDefinitions.add(id);
|
|
215
242
|
|
|
216
243
|
const bindingByKey = new Map<string, string>();
|
|
217
244
|
for (const reference of references) {
|
|
218
|
-
bindingByKey.set(reference.key, reference.definitionId);
|
|
245
|
+
if (!bindingByKey.has(reference.key)) bindingByKey.set(reference.key, reference.definitionId);
|
|
219
246
|
}
|
|
220
247
|
|
|
221
248
|
bundles.push({
|
|
@@ -231,7 +258,10 @@ export function buildNotationRegistry(
|
|
|
231
258
|
definitionIds,
|
|
232
259
|
});
|
|
233
260
|
|
|
234
|
-
|
|
261
|
+
// A deck shows its chapters' symbols again: the glossary names each
|
|
262
|
+
// chapter once, not its slides, or its part's, as a page of its own.
|
|
263
|
+
const isDeck = lesson.scopeLessonIds !== undefined;
|
|
264
|
+
for (const definitionId of isDeck ? [] : definitionIds) {
|
|
235
265
|
const definition = definitions.get(definitionId);
|
|
236
266
|
if (definition === undefined) continue;
|
|
237
267
|
backlinks.push({
|
|
@@ -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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
|
144
|
-
* when absent
|
|
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
|