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.
- package/CHANGELOG.md +879 -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 +43 -0
- package/dist/compiler/cli-main.js +392 -0
- package/dist/compiler/cli.d.ts +424 -9
- package/dist/compiler/cli.js +1289 -8
- package/dist/compiler/collections.d.ts +88 -1
- package/dist/compiler/collections.js +170 -25
- 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 +359 -58
- package/dist/compiler/page-versions.d.ts +160 -0
- package/dist/compiler/page-versions.js +198 -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 +76 -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 +1 -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 +221 -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 +30 -7
- package/dist/rules/rules.schema.json +259 -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 +29 -9
- package/docs/authoring/agents.md +63 -16
- package/docs/authoring/components.md +15 -0
- package/docs/authoring/deck.md +588 -0
- package/docs/authoring/migrating.md +1045 -16
- package/docs/authoring/notation.md +49 -6
- package/docs/authoring/structure.md +80 -1
- package/docs/authoring/versioning.md +54 -2
- package/docs/authoring/visualizations.md +7 -2
- package/docs/course-agent/AGENTS.md +37 -0
- package/docs/course-agent/agents/rules-reviewer.md +33 -0
- package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
- package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
- package/docs/course-agent/skills/release-content/SKILL.md +24 -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 +469 -0
- package/src/compiler/cli.ts +1905 -16
- package/src/compiler/collections.ts +284 -25
- 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 +558 -63
- package/src/compiler/page-versions.ts +359 -0
- package/src/compiler/resolution-report.ts +38 -7
- package/src/compiler/validate-content.ts +212 -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 +126 -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-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 +257 -4
- package/src/rules/course-rules.ts +21 -1
- package/src/rules/in-effect.ts +54 -8
- 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
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A course's decks, as the manifest publishes them (`docs/authoring/deck.md`):
|
|
3
|
+
* each lesson's slide edition read as frames, numbered as the deck page, the
|
|
4
|
+
* presenter and the PDF number them, with the frames the engine writes
|
|
5
|
+
* itself — the title frame first, the outcomes frame and the references
|
|
6
|
+
* frame last — and the deck rules run over them (`decks-validate.ts`).
|
|
7
|
+
*
|
|
8
|
+
* Pure: `compileManifest` hands in the files, the notation scope of each
|
|
9
|
+
* deck's page and the chapter's numbers, and takes back the decks and their
|
|
10
|
+
* diagnostics.
|
|
11
|
+
*/
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
|
|
14
|
+
import type { DeckFile } from './collections';
|
|
15
|
+
import type { NormalizedDeck } from './course-config';
|
|
16
|
+
import { readDeckSource, type DeckFrameSource, type DeckSource } from '../reference/deck-source';
|
|
17
|
+
import {
|
|
18
|
+
chapterNumberOf,
|
|
19
|
+
resolvePartRecalls,
|
|
20
|
+
validateDecks,
|
|
21
|
+
type ChapterFloats,
|
|
22
|
+
type DeckRuleSettings,
|
|
23
|
+
type DeckValidationInput,
|
|
24
|
+
type PartDeckFacts,
|
|
25
|
+
} from '../reference/decks-validate';
|
|
26
|
+
import { mathSpans, type MathGateDiagnostic, type ScopeEntry } from '../reference/gate-math';
|
|
27
|
+
import { resolveMathGlyphs } from '../reference/math-glyphs.mjs';
|
|
28
|
+
import { citedSourceIds } from './lesson-derivation';
|
|
29
|
+
|
|
30
|
+
/** One frame of a deck, in the order the deck shows it. */
|
|
31
|
+
export interface ManifestDeckFrame {
|
|
32
|
+
/** 1-based: the generated title frame is 1. */
|
|
33
|
+
readonly number: number;
|
|
34
|
+
/**
|
|
35
|
+
* What the frame shows, as 12 hex digits of a SHA-256 (v35), as rule C5's
|
|
36
|
+
* hash names a page's content. A reader's record of the frames they saw
|
|
37
|
+
* keeps it, so a frame that changed since is told from one they never saw
|
|
38
|
+
* (`session/deck-memory.ts`). An authored frame hashes its source,
|
|
39
|
+
* notes included, and the chapter's number for each float it recalls; the
|
|
40
|
+
* title frame its title, course and authors, not its version, which every
|
|
41
|
+
* release moves; the outcomes frame its outcomes; the references frame the
|
|
42
|
+
* sources it cites. Another frame's change leaves it as it is.
|
|
43
|
+
*/
|
|
44
|
+
readonly id: string;
|
|
45
|
+
/**
|
|
46
|
+
* `title` and `outcomes` and `references` are generated; `section` is a
|
|
47
|
+
* `##` divider; `frame` is a `###` frame.
|
|
48
|
+
*/
|
|
49
|
+
readonly kind: 'title' | 'section' | 'frame' | 'outcomes' | 'references';
|
|
50
|
+
/** What the reader sees as its title. */
|
|
51
|
+
readonly title: string;
|
|
52
|
+
/** The heading's anchor (a section or a frame). */
|
|
53
|
+
readonly slug?: string;
|
|
54
|
+
/** A frame's section, by its anchor. */
|
|
55
|
+
readonly section?: string;
|
|
56
|
+
/** How many steps the frame shows in: one more than its `<Pause />` marks. */
|
|
57
|
+
readonly steps: number;
|
|
58
|
+
/** `### Title [morph]`: the frame morphs from the frame before it. */
|
|
59
|
+
readonly morph?: true;
|
|
60
|
+
/** The chapter floats the frame recalls, as written (`eq-range`, `fig-…`). */
|
|
61
|
+
readonly recalls?: readonly string[];
|
|
62
|
+
/** The frame carries speaker notes. */
|
|
63
|
+
readonly notes?: true;
|
|
64
|
+
/** The notation keys the frame shows for the first time in the deck — its legend line's first-use list. */
|
|
65
|
+
readonly symbols?: readonly string[];
|
|
66
|
+
/**
|
|
67
|
+
* On a part's deck, the chapter the frame's section covers (v36), by its
|
|
68
|
+
* lesson slug: on the section's divider and on each of its frames.
|
|
69
|
+
*/
|
|
70
|
+
readonly chapter?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One deck, a lesson's or a part's, as the builder makes it (`ManifestDeck` adds its notation). */
|
|
74
|
+
export interface BuiltDeck {
|
|
75
|
+
/** The lesson the deck is the slide edition of; a part's deck belongs to none (v36). */
|
|
76
|
+
readonly lessonId?: string;
|
|
77
|
+
/**
|
|
78
|
+
* The deck page's id: `<lessonId>.slides`, the lesson's own id when it has
|
|
79
|
+
* no chapter, or a part's `<part>.slides`.
|
|
80
|
+
*/
|
|
81
|
+
readonly pageId: string;
|
|
82
|
+
/** Its route: `<lesson>/slides`, the lesson's own for a slides-first lesson, or a part's `<part>/slides`. */
|
|
83
|
+
readonly slug: string;
|
|
84
|
+
readonly file: string;
|
|
85
|
+
readonly title: string;
|
|
86
|
+
readonly description: string;
|
|
87
|
+
/** The lesson's chapter slug, when the deck stands beside one. */
|
|
88
|
+
readonly chapter?: string;
|
|
89
|
+
/** A part's deck (`<part>/index.deck.mdx`, v36): the part's directory, `motion`. */
|
|
90
|
+
readonly part?: string;
|
|
91
|
+
/**
|
|
92
|
+
* Each chapter a part's deck covers, in deck order (v36): its lesson slug,
|
|
93
|
+
* and the number of the frame its section opens on, the divider — the
|
|
94
|
+
* chapter's address in the deck, `#slide-N`. Empty on a lesson's deck.
|
|
95
|
+
*/
|
|
96
|
+
readonly covers: readonly { readonly chapter: string; readonly frame: number }[];
|
|
97
|
+
/**
|
|
98
|
+
* A part's deck (v36): the chapter whose float each recall shows, by its
|
|
99
|
+
* ref (`eq-range` → `projectiles/projectile-range`), which its number
|
|
100
|
+
* links to. A lesson's deck recalls from its {@link chapter}.
|
|
101
|
+
*/
|
|
102
|
+
readonly recalledFrom?: Readonly<Record<string, string>>;
|
|
103
|
+
readonly frames: readonly ManifestDeckFrame[];
|
|
104
|
+
/** The deck's own labelled equations, numbered (1), (2)… in the deck (rule D8). */
|
|
105
|
+
readonly equations: readonly { readonly key: string; readonly number: string }[];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** What the builder needs about each deck's lesson. */
|
|
109
|
+
export interface DeckLessonFacts {
|
|
110
|
+
readonly title: string;
|
|
111
|
+
/** Competencies the lesson teaches; the outcomes frame lists their outcomes. */
|
|
112
|
+
readonly teaches: readonly string[];
|
|
113
|
+
/** Those outcomes, in order: what the outcomes frame shows. */
|
|
114
|
+
readonly outcomes?: readonly string[];
|
|
115
|
+
/** The chapter's numbered floats, by kind, key → number. Absent: no chapter. */
|
|
116
|
+
readonly chapterFloats?: ChapterFloats;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** One chapter of a part, as a part's deck draws on it. */
|
|
120
|
+
export interface DeckChapterFacts {
|
|
121
|
+
/** Its name in the part's directory, what `[chapter: <name>]` writes: `constant-acceleration`. */
|
|
122
|
+
readonly name: string;
|
|
123
|
+
/** Its lesson slug: `motion/constant-acceleration`. */
|
|
124
|
+
readonly slug: string;
|
|
125
|
+
/** Competencies it teaches; the outcomes frame lists those of every chapter covered. */
|
|
126
|
+
readonly teaches: readonly string[];
|
|
127
|
+
/** Their outcomes, in order. */
|
|
128
|
+
readonly outcomes: readonly string[];
|
|
129
|
+
/** What it numbers, which a section of the deck may recall. */
|
|
130
|
+
readonly floats: ChapterFloats;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** What the builder needs about each part that has a deck. */
|
|
134
|
+
export interface DeckPartFacts {
|
|
135
|
+
/** The part's label: the title of its deck when the deck sets none. */
|
|
136
|
+
readonly label: string;
|
|
137
|
+
/** The part's chapters, in reading order. */
|
|
138
|
+
readonly chapters: readonly DeckChapterFacts[];
|
|
139
|
+
/** The part's lessons that are only slides, by name: a section cannot cover one. */
|
|
140
|
+
readonly slidesFirst: readonly string[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface BuildDecksInput {
|
|
144
|
+
readonly decks: readonly DeckFile[];
|
|
145
|
+
readonly settings: NormalizedDeck;
|
|
146
|
+
readonly rules: DeckRuleSettings;
|
|
147
|
+
/** The lesson behind each lesson's deck, by lesson id. */
|
|
148
|
+
readonly lessons: ReadonlyMap<string, DeckLessonFacts>;
|
|
149
|
+
/** Each part the course configures that has a deck, by its directory. */
|
|
150
|
+
readonly parts?: ReadonlyMap<string, DeckPartFacts>;
|
|
151
|
+
/** What every title frame shows under the deck's title: the course's name and its authors. */
|
|
152
|
+
readonly course?: { readonly name?: string; readonly authors: readonly string[] };
|
|
153
|
+
/** The glyph table of each deck's page, by page id (`pageScope`). */
|
|
154
|
+
readonly scopeOf: (pageId: string) => readonly ScopeEntry[];
|
|
155
|
+
/** Decorations a deck's page suspends (rule N22), by page id. */
|
|
156
|
+
readonly suspendedOf?: (pageId: string) => readonly string[];
|
|
157
|
+
/** A `[[key]]` term's label in a heading, as the deck's page reads it. */
|
|
158
|
+
readonly labelFor?: (pageId: string, key: string) => string | undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const NOTES_BLOCK = /<Notes\s*>[\s\S]*?<\/Notes\s*>/g;
|
|
162
|
+
const TERM = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
|
|
163
|
+
const EXPLAIN = /\\explain\s*\{([^{}]+)\}/g;
|
|
164
|
+
const EQ_LABEL = /\\label\{eq:([^{}]+)\}/g;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The notation keys a frame shows: its `[[key]]` terms, every bound symbol in
|
|
168
|
+
* its math, and those of each chapter equation it recalls (`recalled`, by key).
|
|
169
|
+
*/
|
|
170
|
+
function frameKeys(
|
|
171
|
+
frame: DeckFrameSource,
|
|
172
|
+
scope: readonly ScopeEntry[],
|
|
173
|
+
suspended: readonly string[],
|
|
174
|
+
recalled: ReadonlyMap<string, string> = new Map(),
|
|
175
|
+
): string[] {
|
|
176
|
+
const text = frame.body.replace(NOTES_BLOCK, (block) => block.replace(/[^\n]/g, ' '));
|
|
177
|
+
const keys: string[] = [];
|
|
178
|
+
const add = (key: string) => {
|
|
179
|
+
if (!keys.includes(key)) keys.push(key);
|
|
180
|
+
};
|
|
181
|
+
const known = new Set(scope.map((entry) => entry.key));
|
|
182
|
+
for (const match of text.matchAll(TERM)) {
|
|
183
|
+
if (known.has(match[1] ?? '')) add(match[1] ?? '');
|
|
184
|
+
}
|
|
185
|
+
const latexes = [
|
|
186
|
+
...mathSpans(text).map((span) => span.latex),
|
|
187
|
+
...frame.recalls.flatMap((recall) => {
|
|
188
|
+
const latex = recall.ref?.startsWith('eq-') ? recalled.get(recall.ref.slice(3)) : undefined;
|
|
189
|
+
return latex === undefined ? [] : [latex];
|
|
190
|
+
}),
|
|
191
|
+
];
|
|
192
|
+
for (const latex of latexes) {
|
|
193
|
+
for (const match of latex.matchAll(EXPLAIN)) add((match[1] ?? '').trim());
|
|
194
|
+
try {
|
|
195
|
+
const result = resolveMathGlyphs(latex.replace(EQ_LABEL, ''), [...scope], { suspended: [...suspended] }) as {
|
|
196
|
+
bindings?: readonly { key: string }[];
|
|
197
|
+
};
|
|
198
|
+
for (const binding of result.bindings ?? []) add(binding.key);
|
|
199
|
+
} catch {
|
|
200
|
+
// A span the resolver rejects is the math gate's finding, not a legend's.
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return keys;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The deck's own `\label{eq:…}` keys, in order, outside its notes. */
|
|
207
|
+
function ownEquationKeys(source: DeckSource): string[] {
|
|
208
|
+
const keys: string[] = [];
|
|
209
|
+
for (const frame of source.frames) {
|
|
210
|
+
for (const math of frame.displayMath) {
|
|
211
|
+
for (const match of math.latex.matchAll(EQ_LABEL)) {
|
|
212
|
+
const key = (match[1] ?? '').trim();
|
|
213
|
+
if (key && !keys.includes(key)) keys.push(key);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return keys;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* A frame's id: 12 hex digits of a SHA-256 over its kind and what it shows,
|
|
222
|
+
* one string each (`ManifestDeckFrame.id`).
|
|
223
|
+
*/
|
|
224
|
+
export function deckFrameId(kind: ManifestDeckFrame['kind'], shows: readonly string[]): string {
|
|
225
|
+
return createHash('sha256').update(JSON.stringify([kind, ...shows])).digest('hex').slice(0, 12);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** An authored frame's source, heading to the next heading; line ends and trailing blanks do not count. */
|
|
229
|
+
function frameSource(body: string, frame: DeckFrameSource): string {
|
|
230
|
+
return body
|
|
231
|
+
.slice(frame.index, frame.end)
|
|
232
|
+
.replace(/\r\n?/gu, '\n')
|
|
233
|
+
.replace(/[ \t]+$/gmu, '')
|
|
234
|
+
.trimEnd();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* A lesson's deck or a part's, as the deck's frames and generated frames
|
|
239
|
+
* need it: where a recall is numbered, what the outcomes frame lists, and
|
|
240
|
+
* the title it falls back on.
|
|
241
|
+
*/
|
|
242
|
+
interface DeckOwner {
|
|
243
|
+
/** What a frame shows of a float it recalls: its chapter's number and, for an equation, its LaTeX. */
|
|
244
|
+
readonly recalled: (ref: string) => { readonly number?: string; readonly latex?: string };
|
|
245
|
+
/** The chapter a frame's section covers, by the frame's place in the source (a part's deck). */
|
|
246
|
+
readonly frameChapter: (position: number) => string | undefined;
|
|
247
|
+
readonly teaches: readonly string[];
|
|
248
|
+
readonly outcomes: readonly string[];
|
|
249
|
+
/** The title frame's title when the deck file sets none. */
|
|
250
|
+
readonly fallbackTitle: string;
|
|
251
|
+
/** What `BuiltDeck` says of whose deck it is. */
|
|
252
|
+
readonly identity: Pick<BuiltDeck, 'lessonId' | 'chapter' | 'part' | 'covers' | 'recalledFrom'>;
|
|
253
|
+
readonly validation: Pick<DeckValidationInput, 'hasChapter' | 'inPart' | 'chapter' | 'part'>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** What a lesson's deck draws on: its chapter, when it has one. */
|
|
257
|
+
function lessonOwner(deck: Extract<DeckFile, { scope: 'lesson' }>, lesson: DeckLessonFacts | undefined): DeckOwner {
|
|
258
|
+
const floats = lesson?.chapterFloats;
|
|
259
|
+
return {
|
|
260
|
+
recalled: (ref) => {
|
|
261
|
+
const number = chapterNumberOf(ref, floats);
|
|
262
|
+
const latex = ref.startsWith('eq-') ? floats?.equations.get(ref.slice(3))?.latex : undefined;
|
|
263
|
+
return { ...(number === undefined ? {} : { number }), ...(latex === undefined ? {} : { latex }) };
|
|
264
|
+
},
|
|
265
|
+
frameChapter: () => undefined,
|
|
266
|
+
teaches: lesson?.teaches ?? [],
|
|
267
|
+
outcomes: lesson?.outcomes ?? [],
|
|
268
|
+
fallbackTitle: lesson?.title ?? deck.lessonId,
|
|
269
|
+
identity: {
|
|
270
|
+
lessonId: deck.lessonId,
|
|
271
|
+
...(deck.chapterPath === undefined ? {} : { chapter: deck.lessonSlug }),
|
|
272
|
+
covers: [],
|
|
273
|
+
},
|
|
274
|
+
validation: {
|
|
275
|
+
hasChapter: deck.chapterPath !== undefined,
|
|
276
|
+
inPart: deck.inPart,
|
|
277
|
+
...(floats === undefined ? {} : { chapter: floats }),
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* What a part's deck draws on: the chapters its sections cover. A recall is
|
|
284
|
+
* the float of the chapter `resolvePartRecalls` finds; the outcomes frame
|
|
285
|
+
* lists every covered chapter's outcomes, in reading order.
|
|
286
|
+
*/
|
|
287
|
+
function partOwner(
|
|
288
|
+
deck: Extract<DeckFile, { scope: 'part' }>,
|
|
289
|
+
part: DeckPartFacts | undefined,
|
|
290
|
+
source: DeckSource,
|
|
291
|
+
): DeckOwner {
|
|
292
|
+
const facts: PartDeckFacts = {
|
|
293
|
+
dir: deck.partDir,
|
|
294
|
+
chapters: (part?.chapters ?? []).map((chapter) => ({ name: chapter.name, slug: chapter.slug, floats: chapter.floats })),
|
|
295
|
+
slidesFirst: part?.slidesFirst ?? [],
|
|
296
|
+
};
|
|
297
|
+
const recalls = resolvePartRecalls(source, facts);
|
|
298
|
+
const covered = new Set(recalls.covered.map((chapter) => chapter.name));
|
|
299
|
+
const inOrder = (part?.chapters ?? []).filter((chapter) => covered.has(chapter.name));
|
|
300
|
+
const teaches = [...new Set(inOrder.flatMap((chapter) => chapter.teaches))];
|
|
301
|
+
return {
|
|
302
|
+
recalled: (ref) => {
|
|
303
|
+
const floats = recalls.chapterOf.get(ref)?.floats;
|
|
304
|
+
const number = chapterNumberOf(ref, floats);
|
|
305
|
+
const latex = ref.startsWith('eq-') ? floats?.equations.get(ref.slice(3))?.latex : undefined;
|
|
306
|
+
return { ...(number === undefined ? {} : { number }), ...(latex === undefined ? {} : { latex }) };
|
|
307
|
+
},
|
|
308
|
+
frameChapter: (position) => recalls.frameChapters[position]?.slug,
|
|
309
|
+
teaches,
|
|
310
|
+
outcomes: [...new Set(inOrder.flatMap((chapter) => chapter.outcomes))],
|
|
311
|
+
fallbackTitle: part?.label ?? deck.partDir,
|
|
312
|
+
identity: {
|
|
313
|
+
part: deck.partDir,
|
|
314
|
+
covers: recalls.covered.map((chapter) => ({ chapter: chapter.slug, frame: chapter.position + 2 })),
|
|
315
|
+
recalledFrom: Object.fromEntries(
|
|
316
|
+
[...recalls.chapterOf].sort(([left], [right]) => left.localeCompare(right)).map(([ref, chapter]) => [ref, chapter.slug]),
|
|
317
|
+
),
|
|
318
|
+
},
|
|
319
|
+
validation: { hasChapter: false, inPart: part !== undefined, part: facts },
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function buildDecks(input: BuildDecksInput): {
|
|
324
|
+
decks: BuiltDeck[];
|
|
325
|
+
diagnostics: MathGateDiagnostic[];
|
|
326
|
+
} {
|
|
327
|
+
const decks: BuiltDeck[] = [];
|
|
328
|
+
const validation: DeckValidationInput[] = [];
|
|
329
|
+
for (const deck of input.decks) {
|
|
330
|
+
const source = readDeckSource(deck.body, {
|
|
331
|
+
labelFor: (key) => input.labelFor?.(deck.pageId, key),
|
|
332
|
+
});
|
|
333
|
+
const owner =
|
|
334
|
+
deck.scope === 'part'
|
|
335
|
+
? partOwner(deck, input.parts?.get(deck.partDir), source)
|
|
336
|
+
: lessonOwner(deck, input.lessons.get(deck.lessonId));
|
|
337
|
+
const scope = input.scopeOf(deck.pageId);
|
|
338
|
+
const suspended = input.suspendedOf?.(deck.pageId) ?? [];
|
|
339
|
+
const seen = new Set<string>();
|
|
340
|
+
// A recalled equation's LaTeX, by its key: the symbols it shows count for the frame's legend.
|
|
341
|
+
const recalled = new Map(
|
|
342
|
+
source.frames.flatMap((frame) =>
|
|
343
|
+
frame.recalls.flatMap((recall): [string, string][] => {
|
|
344
|
+
if (!recall.ref?.startsWith('eq-')) return [];
|
|
345
|
+
const latex = owner.recalled(recall.ref).latex;
|
|
346
|
+
return latex === undefined ? [] : [[recall.ref.slice(3), latex]];
|
|
347
|
+
}),
|
|
348
|
+
),
|
|
349
|
+
);
|
|
350
|
+
const newSymbols = source.frames.map((frame) => {
|
|
351
|
+
const fresh = frameKeys(frame, scope, suspended, recalled).filter((key) => !seen.has(key));
|
|
352
|
+
for (const key of fresh) seen.add(key);
|
|
353
|
+
return fresh;
|
|
354
|
+
});
|
|
355
|
+
validation.push({
|
|
356
|
+
file: deck.file,
|
|
357
|
+
pageId: deck.pageId,
|
|
358
|
+
body: deck.body,
|
|
359
|
+
bodyLine: deck.bodyLine,
|
|
360
|
+
data: deck.data,
|
|
361
|
+
source,
|
|
362
|
+
...owner.validation,
|
|
363
|
+
newSymbols,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const title =
|
|
367
|
+
typeof deck.data.title === 'string' && deck.data.title.trim() !== ''
|
|
368
|
+
? deck.data.title
|
|
369
|
+
: owner.fallbackTitle;
|
|
370
|
+
const frames: ManifestDeckFrame[] = [
|
|
371
|
+
{
|
|
372
|
+
number: 1,
|
|
373
|
+
id: deckFrameId('title', [title, input.course?.name ?? '', ...(input.course?.authors ?? [])]),
|
|
374
|
+
kind: 'title',
|
|
375
|
+
title,
|
|
376
|
+
steps: 1,
|
|
377
|
+
},
|
|
378
|
+
];
|
|
379
|
+
source.frames.forEach((frame, index) => {
|
|
380
|
+
const symbols = newSymbols[index] ?? [];
|
|
381
|
+
const recalls = frame.recalls.flatMap((recall) => (recall.ref === undefined ? [] : [recall.ref]));
|
|
382
|
+
const chapter = owner.frameChapter(index);
|
|
383
|
+
frames.push({
|
|
384
|
+
number: frames.length + 1,
|
|
385
|
+
id: deckFrameId(frame.kind, [
|
|
386
|
+
frameSource(deck.body, frame),
|
|
387
|
+
...recalls.map((ref) => `${ref} ${owner.recalled(ref).number ?? '?'}`),
|
|
388
|
+
]),
|
|
389
|
+
kind: frame.kind,
|
|
390
|
+
title: frame.title,
|
|
391
|
+
slug: frame.slug,
|
|
392
|
+
...(frame.section === undefined ? {} : { section: frame.section }),
|
|
393
|
+
steps: frame.pauses.length + 1,
|
|
394
|
+
...(frame.morph ? { morph: true as const } : {}),
|
|
395
|
+
...(recalls.length === 0 ? {} : { recalls }),
|
|
396
|
+
...(frame.notes.length === 0 ? {} : { notes: true as const }),
|
|
397
|
+
...(symbols.length === 0 ? {} : { symbols }),
|
|
398
|
+
...(chapter === undefined ? {} : { chapter }),
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
if (input.settings.outcomesFrame && owner.teaches.length > 0) {
|
|
402
|
+
frames.push({
|
|
403
|
+
number: frames.length + 1,
|
|
404
|
+
id: deckFrameId('outcomes', owner.outcomes),
|
|
405
|
+
kind: 'outcomes',
|
|
406
|
+
title: 'What you can now do',
|
|
407
|
+
steps: 1,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
const cited = [...citedSourceIds(deck.body)].sort();
|
|
411
|
+
if (cited.length > 0) {
|
|
412
|
+
frames.push({
|
|
413
|
+
number: frames.length + 1,
|
|
414
|
+
id: deckFrameId('references', cited),
|
|
415
|
+
kind: 'references',
|
|
416
|
+
title: 'References',
|
|
417
|
+
steps: 1,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
decks.push({
|
|
422
|
+
...(owner.identity.lessonId === undefined ? {} : { lessonId: owner.identity.lessonId }),
|
|
423
|
+
pageId: deck.pageId,
|
|
424
|
+
slug: deck.route,
|
|
425
|
+
file: deck.file,
|
|
426
|
+
title,
|
|
427
|
+
description: typeof deck.data.description === 'string' ? deck.data.description : '',
|
|
428
|
+
...(owner.identity.chapter === undefined ? {} : { chapter: owner.identity.chapter }),
|
|
429
|
+
...(owner.identity.part === undefined ? {} : { part: owner.identity.part }),
|
|
430
|
+
covers: owner.identity.covers,
|
|
431
|
+
...(owner.identity.recalledFrom === undefined ? {} : { recalledFrom: owner.identity.recalledFrom }),
|
|
432
|
+
frames,
|
|
433
|
+
equations: ownEquationKeys(source).map((key, index) => ({ key, number: String(index + 1) })),
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return {
|
|
437
|
+
decks: decks.sort((left, right) => left.slug.localeCompare(right.slug)),
|
|
438
|
+
diagnostics: validateDecks(validation, input.rules),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Each labelled display equation of a chapter body, by key: its LaTeX (what
|
|
444
|
+
* rule D8 compares a deck's equation with).
|
|
445
|
+
*/
|
|
446
|
+
export function chapterEquationLatex(body: string): Map<string, string> {
|
|
447
|
+
const latex = new Map<string, string>();
|
|
448
|
+
for (const span of mathSpans(body)) {
|
|
449
|
+
for (const match of span.latex.matchAll(EQ_LABEL)) {
|
|
450
|
+
const key = (match[1] ?? '').trim();
|
|
451
|
+
if (key && !latex.has(key)) latex.set(key, span.latex);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return latex;
|
|
455
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule D2's rendered half: every frame of every deck fits its canvas.
|
|
3
|
+
*
|
|
4
|
+
* Only a browser knows how tall a frame's content is — its fonts, its math,
|
|
5
|
+
* its hydrated labs — so after Astro has built the site this opens each deck
|
|
6
|
+
* page in the course's Chromium, lets its controller lay the frames out at the
|
|
7
|
+
* canvas size (`components/deck/deck-controller.ts`), and reports each frame
|
|
8
|
+
* whose content runs past its canvas: `frame-overflow`, an error, which fails
|
|
9
|
+
* the build. The canvas has the same size on every screen, so one measure
|
|
10
|
+
* holds for every reader. Without a browser it warns and checks nothing;
|
|
11
|
+
* in CI (`CI`, `EXPLICO_LATEX_STRICT=1`) a missing browser fails the build.
|
|
12
|
+
*/
|
|
13
|
+
import { parse } from 'parse5';
|
|
14
|
+
import { readFile } from 'node:fs/promises';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { inferBasePrefix } from './html-tree';
|
|
18
|
+
import { courseChromium, serve } from './latex/capture';
|
|
19
|
+
import { strictMode, type ExportLogger } from './export-chapters';
|
|
20
|
+
import type { Manifest } from '../manifest';
|
|
21
|
+
|
|
22
|
+
export interface FrameFitFinding {
|
|
23
|
+
readonly code: 'frame-overflow';
|
|
24
|
+
readonly severity: 'error';
|
|
25
|
+
/** The deck file. */
|
|
26
|
+
readonly file: string;
|
|
27
|
+
readonly frame: number;
|
|
28
|
+
readonly title: string;
|
|
29
|
+
/** How far past the canvas the content runs, in canvas pixels. */
|
|
30
|
+
readonly overflow: number;
|
|
31
|
+
readonly message: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The measure the page runs: each frame whose content is taller or wider than its box. */
|
|
35
|
+
function measureFrames(): { frame: number; title: string; overflow: number }[] {
|
|
36
|
+
const found: { frame: number; title: string; overflow: number }[] = [];
|
|
37
|
+
for (const frame of document.querySelectorAll<HTMLElement>('.deck .deck-frame')) {
|
|
38
|
+
const body = frame.querySelector<HTMLElement>('.deck-frame__body');
|
|
39
|
+
const canvas = frame.querySelector<HTMLElement>('.deck-canvas');
|
|
40
|
+
const over = Math.max(
|
|
41
|
+
body ? body.scrollHeight - body.clientHeight : 0,
|
|
42
|
+
body ? body.scrollWidth - body.clientWidth : 0,
|
|
43
|
+
canvas ? canvas.scrollHeight - canvas.clientHeight : 0,
|
|
44
|
+
);
|
|
45
|
+
if (over > 1) {
|
|
46
|
+
found.push({
|
|
47
|
+
frame: Number(frame.dataset.frame),
|
|
48
|
+
title: frame.querySelector('.deck-frame__title, .deck-title__title')?.textContent?.trim() ?? '',
|
|
49
|
+
overflow: Math.round(over),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function checkFrameFit(
|
|
57
|
+
outputRoot: string,
|
|
58
|
+
manifest: Pick<Manifest, 'decks'>,
|
|
59
|
+
options: { readonly logger: ExportLogger; readonly projectRoot?: string; readonly timeoutMs?: number },
|
|
60
|
+
): Promise<FrameFitFinding[]> {
|
|
61
|
+
const decks = manifest.decks ?? [];
|
|
62
|
+
if (decks.length === 0) return [];
|
|
63
|
+
const chromium = await courseChromium(options.projectRoot ?? process.cwd());
|
|
64
|
+
if (!chromium) {
|
|
65
|
+
const message =
|
|
66
|
+
'frame-overflow (rule D2): no Chromium from Playwright in this course, so no frame was measured. ' +
|
|
67
|
+
'Install it (`pnpm exec playwright install chromium`) to check that every frame fits its canvas.';
|
|
68
|
+
if (strictMode()) throw new Error(message);
|
|
69
|
+
options.logger.warn(message);
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const firstPage = await readFile(join(outputRoot, decks[0]!.slug, 'index.html'), 'utf8');
|
|
73
|
+
const base = inferBasePrefix(parse(firstPage));
|
|
74
|
+
const { origin, server } = await serve(outputRoot, base);
|
|
75
|
+
const findings: FrameFitFinding[] = [];
|
|
76
|
+
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined;
|
|
77
|
+
try {
|
|
78
|
+
browser = await chromium.launch();
|
|
79
|
+
for (const deck of decks) {
|
|
80
|
+
const context = await browser.newContext({
|
|
81
|
+
viewport: { width: 1280, height: 900 },
|
|
82
|
+
colorScheme: 'light',
|
|
83
|
+
reducedMotion: 'reduce',
|
|
84
|
+
locale: 'en-US',
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
const page = await context.newPage();
|
|
88
|
+
await page.goto(`${origin}${base}${deck.slug}/`, { waitUntil: 'load' });
|
|
89
|
+
// Until the controller has laid the frames out and the fonts are in;
|
|
90
|
+
// then each frame comes into view once, for its labs to hydrate.
|
|
91
|
+
await page.evaluate(async (timeout: number) => {
|
|
92
|
+
const start = performance.now();
|
|
93
|
+
while (!(document.querySelector('.deck[data-deck-ready]') && document.fonts.status === 'loaded')) {
|
|
94
|
+
if (performance.now() - start > timeout) break;
|
|
95
|
+
await new Promise((done) => setTimeout(done, 50));
|
|
96
|
+
}
|
|
97
|
+
for (const frame of document.querySelectorAll<HTMLElement>('.deck .deck-frame')) {
|
|
98
|
+
frame.scrollIntoView({ block: 'center' });
|
|
99
|
+
await new Promise((done) => requestAnimationFrame(() => setTimeout(done, 30)));
|
|
100
|
+
}
|
|
101
|
+
await new Promise((done) => setTimeout(done, 400));
|
|
102
|
+
// The controller fits each figure to its frame, as a reader's page does.
|
|
103
|
+
(window as Window & { explicoDeckOverflow?: () => number[] }).explicoDeckOverflow?.();
|
|
104
|
+
}, options.timeoutMs ?? 30_000);
|
|
105
|
+
for (const { frame, title, overflow } of (await page.evaluate(measureFrames)) as ReturnType<typeof measureFrames>) {
|
|
106
|
+
findings.push({
|
|
107
|
+
code: 'frame-overflow',
|
|
108
|
+
severity: 'error',
|
|
109
|
+
file: deck.file,
|
|
110
|
+
frame,
|
|
111
|
+
title,
|
|
112
|
+
overflow,
|
|
113
|
+
message:
|
|
114
|
+
`${deck.file}: slide ${frame}${title ? ` “${title}”` : ''} does not fit its canvas — its content runs ` +
|
|
115
|
+
`${overflow}px past it (rule D2). Split the frame in two, move its argument into <Notes>, or recall less.`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
await context.close();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} finally {
|
|
123
|
+
await browser?.close();
|
|
124
|
+
await new Promise<void>((done) => server.close(() => done()));
|
|
125
|
+
}
|
|
126
|
+
return findings;
|
|
127
|
+
}
|