explico 0.4.0 → 0.6.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 +382 -1
- package/README.md +103 -8
- package/dist/compiler/astro.d.ts +9 -1
- package/dist/compiler/astro.js +182 -3
- package/dist/compiler/cli.d.ts +13 -0
- package/dist/compiler/cli.js +28 -4
- package/dist/compiler/collections.d.ts +3 -2
- package/dist/compiler/collections.js +28 -1
- package/dist/compiler/course-config.d.ts +88 -1
- package/dist/compiler/course-config.js +35 -1
- package/dist/compiler/exports/standalone-html.d.ts +34 -0
- package/dist/compiler/exports/standalone-html.js +803 -0
- package/dist/compiler/manifest.d.ts +98 -6
- package/dist/compiler/manifest.js +154 -17
- package/dist/compiler/resolution-report.d.ts +4 -0
- package/dist/reference/base-abbreviations.d.mts +30 -0
- package/dist/reference/base-abbreviations.mjs +60 -0
- package/dist/reference/base-operators.d.mts +15 -0
- package/dist/reference/base-operators.mjs +40 -0
- package/dist/reference/base-roman-labels.d.mts +35 -0
- package/dist/reference/base-roman-labels.mjs +43 -0
- package/dist/reference/consistency.d.ts +8 -2
- package/dist/reference/consistency.js +50 -3
- package/dist/reference/diagrams-validate.d.ts +21 -0
- package/dist/reference/diagrams-validate.js +13 -0
- package/dist/reference/equations.d.mts +10 -15
- package/dist/reference/equations.mjs +67 -14
- package/dist/reference/gate-abbreviations.d.ts +17 -0
- package/dist/reference/gate-abbreviations.js +64 -0
- package/dist/reference/gate-heading-length.d.ts +12 -0
- package/dist/reference/gate-heading-length.js +79 -0
- package/dist/reference/gate-math.d.ts +32 -5
- package/dist/reference/gate-math.js +6 -2
- package/dist/reference/gate-prose-notation.d.ts +60 -0
- package/dist/reference/gate-prose-notation.js +125 -0
- package/dist/reference/keyed-float-validate.d.ts +5 -5
- package/dist/reference/keyed-jsx-floats.d.mts +7 -12
- package/dist/reference/keyed-jsx-floats.mjs +50 -25
- package/dist/reference/knowledge-checks-validate.d.ts +35 -0
- package/dist/reference/knowledge-checks-validate.js +78 -0
- package/dist/reference/knowledge-checks.d.mts +25 -0
- package/dist/reference/knowledge-checks.mjs +45 -0
- package/dist/reference/lab-math-scope.d.ts +9 -1
- package/dist/reference/lab-math-scope.js +32 -4
- package/dist/reference/math-glyphs.d.mts +42 -0
- package/dist/reference/math-glyphs.mjs +548 -40
- package/dist/reference/mdx-attributes.d.mts +13 -0
- package/dist/reference/mdx-attributes.mjs +38 -0
- package/dist/reference/mixed-content.d.ts +17 -0
- package/dist/reference/mixed-content.js +29 -0
- package/dist/reference/page-notation.d.ts +52 -0
- package/dist/reference/page-notation.js +171 -0
- package/dist/reference/raw-floats-validate.d.ts +18 -0
- package/dist/reference/raw-floats-validate.js +53 -0
- package/dist/reference/raw-floats.d.mts +18 -0
- package/dist/reference/raw-floats.mjs +129 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -1
- package/dist/reference/rehype-heading-anchors.mjs +107 -1
- package/dist/reference/rehype-table-header-labels.d.mts +26 -0
- package/dist/reference/rehype-table-header-labels.mjs +152 -0
- package/dist/reference/remark-abbreviations.d.mts +7 -0
- package/dist/reference/remark-abbreviations.mjs +174 -0
- package/dist/reference/remark-citation.d.mts +5 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-memo.d.mts +16 -0
- package/dist/reference/remark-memo.mjs +339 -0
- package/dist/reference/remark-notation.d.mts +57 -9
- package/dist/reference/remark-notation.mjs +354 -42
- package/dist/reference/remark-short-note.d.mts +4 -0
- package/dist/reference/remark-short-note.mjs +237 -0
- package/dist/reference/types.d.ts +16 -1
- package/dist/reference/units-vocab.js +4 -0
- package/docs/VERSIONING.md +53 -4
- package/docs/authoring/README.md +10 -0
- package/docs/authoring/agents.md +17 -2
- package/docs/authoring/components.md +138 -0
- package/docs/authoring/migrating.md +275 -7
- package/docs/authoring/notation.md +615 -1
- package/docs/authoring/structure-rules.md +69 -0
- package/docs/authoring/structure.md +120 -8
- package/docs/authoring/visualizations.md +36 -3
- package/package.json +5 -2
- package/src/compiler/astro.ts +208 -3
- package/src/compiler/cli.ts +29 -2
- package/src/compiler/collections.ts +45 -4
- package/src/compiler/course-config.ts +132 -0
- package/src/compiler/exports/standalone-html.ts +963 -0
- package/src/compiler/manifest.ts +281 -22
- package/src/components/HoverPanel.astro +45 -4
- package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
- package/src/components/assessments/AssessmentRunner.tsx +52 -17
- package/src/components/assessments/AssessmentSet.astro +98 -14
- package/src/components/assessments/RenderedContent.tsx +55 -0
- package/src/components/assessments/assessment-randomization.ts +16 -0
- package/src/components/diagram/Diagram.astro +409 -103
- package/src/components/disclosure/hover-preview.ts +202 -0
- package/src/components/disclosure/persisted-disclosure.ts +66 -0
- package/src/components/examples/CompactExamples.astro +2 -0
- package/src/components/figure/Figure.astro +279 -10
- package/src/components/memo/Memo.astro +453 -0
- package/src/components/memo/ShortNote.astro +223 -0
- package/src/components/notation/NotationFormula.astro +18 -1
- package/src/components/notation/NotationLayer.astro +260 -83
- package/src/components/notation/render-notation-math.ts +39 -2
- package/src/components/notation/safe-math-to-react.ts +50 -0
- package/src/components/reference/FloatRefLayer.astro +233 -15
- package/src/components/starlight/CollapsibleSections.astro +461 -0
- package/src/components/starlight/DiagramEnhancer.astro +385 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
- package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
- package/src/components/starlight/LayoutHeader.astro +50 -0
- package/src/components/starlight/LayoutSocialIcons.astro +41 -2
- package/src/components/starlight/LessonExportActions.astro +436 -0
- package/src/components/starlight/LessonFooter.astro +30 -3
- package/src/components/starlight/section-parts.ts +31 -0
- package/src/components/table/Table.astro +293 -7
- package/src/components/table/TableOverflowExpander.astro +489 -0
- package/src/content-config.ts +36 -1
- package/src/reference/base-abbreviations.mjs +60 -0
- package/src/reference/base-operators.mjs +40 -0
- package/src/reference/base-roman-labels.mjs +43 -0
- package/src/reference/consistency.ts +73 -2
- package/src/reference/diagrams-validate.ts +40 -0
- package/src/reference/equations.mjs +67 -14
- package/src/reference/gate-abbreviations.ts +128 -0
- package/src/reference/gate-heading-length.ts +131 -0
- package/src/reference/gate-math.ts +32 -7
- package/src/reference/gate-prose-notation.ts +224 -0
- package/src/reference/keyed-float-validate.ts +5 -5
- package/src/reference/keyed-jsx-floats.mjs +50 -25
- package/src/reference/knowledge-checks-validate.ts +130 -0
- package/src/reference/knowledge-checks.mjs +45 -0
- package/src/reference/lab-math-scope.ts +45 -3
- package/src/reference/math-glyphs.mjs +548 -40
- package/src/reference/mdx-attributes.mjs +38 -0
- package/src/reference/mixed-content.ts +69 -0
- package/src/reference/page-notation.ts +252 -0
- package/src/reference/raw-floats-validate.ts +103 -0
- package/src/reference/raw-floats.mjs +129 -0
- package/src/reference/rehype-heading-anchors.mjs +107 -1
- package/src/reference/rehype-table-header-labels.mjs +152 -0
- package/src/reference/remark-abbreviations.mjs +174 -0
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-memo.mjs +339 -0
- package/src/reference/remark-notation.mjs +354 -42
- package/src/reference/remark-short-note.mjs +237 -0
- package/src/reference/types.ts +16 -0
- package/src/reference/units-vocab.ts +4 -0
- package/src/styles/global.css +996 -50
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Structure rules
|
|
2
|
+
|
|
3
|
+
`S` rules govern the shape of a lesson's headings and the short titles on its
|
|
4
|
+
keyed floats — the parts of a page a reader scans before reading prose. This
|
|
5
|
+
is not [structure.md](structure.md), which is the taxonomy these rules (and
|
|
6
|
+
the rest of the engine) speak in, not a rule set itself.
|
|
7
|
+
|
|
8
|
+
See [README.md](README.md) for what "strong recommendation", "Build-enforced",
|
|
9
|
+
and "Review-only" mean here, and how to amend a rule.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## S1
|
|
14
|
+
|
|
15
|
+
**No heading, and no figure/diagram/memo/short-note title, runs past 65
|
|
16
|
+
characters.**
|
|
17
|
+
|
|
18
|
+
This caps every Markdown heading (`##` through `######`), a lesson's own
|
|
19
|
+
`frontmatter.title` (rendered as the page's `<h1>`), and the short `title` /
|
|
20
|
+
`label` prop on `Figure`, `Diagram`, `Memo`, and `ShortNote` — the text shown
|
|
21
|
+
on a collapsed title bar, a diagram's caption line, or a chapter's sidebar
|
|
22
|
+
entry. `Table` is exempt: it has only a `caption`, no separate `title`, and
|
|
23
|
+
that caption is the LaTeX-convention full sentence shown above the table.
|
|
24
|
+
|
|
25
|
+
A heading or float title that runs long is usually doing a caption's job. Move
|
|
26
|
+
the rest of the sentence into the paragraph right after a heading, or into the
|
|
27
|
+
float's own `caption`.
|
|
28
|
+
|
|
29
|
+
```mdx
|
|
30
|
+
<!-- Over the limit: the title carries the whole explanation. -->
|
|
31
|
+
<Figure
|
|
32
|
+
id="ar1-forecast-fan"
|
|
33
|
+
caption="Multi-step AR(1) forecast: the conditional mean (teal) decays geometrically to the unconditional mean while the Gaussian 95% band (shaded) widens toward the stationary dispersion Γ₀; the coral line is one seeded future path."
|
|
34
|
+
>
|
|
35
|
+
|
|
36
|
+
<!-- At the limit: a short label, the rest moved into `caption`. -->
|
|
37
|
+
<Figure
|
|
38
|
+
id="ar1-forecast-fan"
|
|
39
|
+
title="Multi-step AR(1) forecast"
|
|
40
|
+
caption="The conditional mean (teal) decays geometrically to the unconditional mean while the Gaussian 95% band (shaded) widens toward the stationary dispersion Γ₀; the coral line is one seeded future path."
|
|
41
|
+
>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
_Rationale._ A `Figure`'s `title` falls back to `caption` when omitted (see
|
|
45
|
+
`Figure.astro`), so an unset `title` on a long-captioned figure silently
|
|
46
|
+
becomes the text on the always-visible collapsible title bar — the reader
|
|
47
|
+
sees the entire caption twice, once as the "heading" and once again in
|
|
48
|
+
`<figcaption>` once they expand it. The same abuse happens with a heading
|
|
49
|
+
authored as a full sentence instead of a label: the sidebar, the table of
|
|
50
|
+
contents, and every anchor link now carry a paragraph instead of a title.
|
|
51
|
+
Sixty-five characters is roughly one sidebar line at normal zoom before
|
|
52
|
+
Starlight starts wrapping or truncating it.
|
|
53
|
+
|
|
54
|
+
_How to apply._ Write the heading or `title` as a short noun phrase (a topic,
|
|
55
|
+
not a claim about it); put the claim in the prose or the `caption`. If a
|
|
56
|
+
`Figure` needs no distinct title beyond its caption's own first clause,
|
|
57
|
+
extract that clause into `title` explicitly rather than omitting the prop and
|
|
58
|
+
relying on the fallback — an omitted `title` on a long caption is exactly the
|
|
59
|
+
failure this rule exists to catch.
|
|
60
|
+
|
|
61
|
+
_Enforcement._ Build-enforced (`gate-heading-length.ts`, `pnpm
|
|
62
|
+
validate:content`) for every Markdown heading in a lesson body and every
|
|
63
|
+
`Figure`/`Diagram`/`Memo`/`ShortNote` `title`/`label` prop. Review-only for a
|
|
64
|
+
lesson's `frontmatter.title` — the gate scans lesson body text and cannot see
|
|
65
|
+
frontmatter, so a long page title (`<h1>`) is caught by a reviewer, not the
|
|
66
|
+
build.
|
|
67
|
+
|
|
68
|
+
_Exceptions._ None — a heading or float title that must exceed the limit
|
|
69
|
+
should not be a heading or a float title; find the shorter label first.
|
|
@@ -105,12 +105,14 @@ Part 2 Projectile motion
|
|
|
105
105
|
|
|
106
106
|
## Numbering
|
|
107
107
|
|
|
108
|
-
A display equation, `<Table>`,
|
|
108
|
+
A display equation, `<Table>`, `<Figure>`, `<Diagram>`, or knowledge check is
|
|
109
|
+
numbered **`‹part›.‹n›`**:
|
|
109
110
|
|
|
110
111
|
- **`part`** is the part's 1-based position in `courseConfig.parts`.
|
|
111
|
-
- **`n`** counts that kind of
|
|
112
|
-
running unbroken from one chapter into the next. Each kind counts on
|
|
113
|
-
so a `<Table>` never advances the `<Figure
|
|
112
|
+
- **`n`** counts that kind of numbered item across the *whole part*, in document
|
|
113
|
+
order, running unbroken from one chapter into the next. Each kind counts on
|
|
114
|
+
its own, so a `<Table>` never advances the `<Figure>`, `<Diagram>`, or
|
|
115
|
+
knowledge-check sequence.
|
|
114
116
|
- **Sections and subsections do not participate.** Neither `##` nor `###`
|
|
115
117
|
resets `n` or appears in the number.
|
|
116
118
|
- **Keyed or not, everything counts.** An equation with no `\label{eq:…}` still
|
|
@@ -121,8 +123,8 @@ In the playground: part 1 runs `1.1` … `1.7` through chapter 1 and continues
|
|
|
121
123
|
`1.8` in chapter 2; part 2 opens at `2.1`.
|
|
122
124
|
|
|
123
125
|
`manifest.lessons[]` carries what the render path needs to reproduce this:
|
|
124
|
-
`part`, `chapter`, and `numbering.{equations,tables,figures}` —
|
|
125
|
-
count each chapter starts from.
|
|
126
|
+
`part`, `chapter`, and `numbering.{equations,tables,figures,diagrams,checks}` —
|
|
127
|
+
the running count each chapter starts from.
|
|
126
128
|
|
|
127
129
|
### The invariant, and why it is build-enforced
|
|
128
130
|
|
|
@@ -135,5 +137,115 @@ This exists because the disagreement is otherwise silent and shipped: a course
|
|
|
135
137
|
config that passed the manifest number tables without the placement map
|
|
136
138
|
numbered its pages by `##` section while every reference to them read the
|
|
137
139
|
manifest, so one equation was `(2.2)` on its own page and `(4.5)` everywhere
|
|
138
|
-
else. Wire the plugin
|
|
139
|
-
|
|
140
|
+
else. Wire the plugin with `createRemarkNotationOptions`, which reads
|
|
141
|
+
`definitions`, `manifest`, and every numbering input off one manifest in one
|
|
142
|
+
call so they cannot drift apart:
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
import remarkNotation, {
|
|
146
|
+
createRemarkNotationOptions,
|
|
147
|
+
} from 'explico/reference/remark-notation.mjs';
|
|
148
|
+
|
|
149
|
+
// [remarkNotation, createRemarkNotationOptions(manifest, { strict: true })]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
That cross-check only ever caught a disagreement for a *keyed* equation or
|
|
153
|
+
float — an unkeyed one still took a number, but nothing compared it against
|
|
154
|
+
the manifest, so a lesson whose only equations/tables/figures were unkeyed
|
|
155
|
+
could still silently fall back to `##`-section numbering with no error at all.
|
|
156
|
+
`strict: true` closes that: it fails the build when a catalogued lesson
|
|
157
|
+
contains any equation, `<Table>`, or `<Figure>` — keyed or not — but has no
|
|
158
|
+
`numbering` placement or no cross-page number table entry for its slug.
|
|
159
|
+
`<Diagram>` is exempt, since `Diagram.astro` already renders one without a
|
|
160
|
+
number by design when it has none. `strict` defaults to `false`, so existing
|
|
161
|
+
non-strict wiring keeps building unchanged; turn it on once a course's
|
|
162
|
+
manifest covers every catalogued lesson.
|
|
163
|
+
|
|
164
|
+
### Raw and unwrapped floats
|
|
165
|
+
|
|
166
|
+
A raw Markdown pipe table or a raw HTML `<table>` written directly into a
|
|
167
|
+
lesson — never wrapped in `<Table>` — renders fine but never enters keyed-float
|
|
168
|
+
validation: it has no number and `[[tab-…]]` cannot reach it. The same is true
|
|
169
|
+
of an `<Diagram>` with no `id`, and of a `<Diagram>` nested inside anything but
|
|
170
|
+
`<Memo>`/`<ShortNote>`/`<Table>`/`<Figure>` — `keyed-jsx-floats.mjs` only scans
|
|
171
|
+
top-level (and `<Memo>`/`<ShortNote>`-nested) occurrences, so a diagram buried
|
|
172
|
+
inside e.g. `<CompactExample>` never reaches diagram numbering at all.
|
|
173
|
+
|
|
174
|
+
The compiled manifest flags all four as warnings in `diagnostics.rawFloats`
|
|
175
|
+
(`raw-table-markdown`, `raw-table-html`, `diagram-unkeyed`, `diagram-nested`),
|
|
176
|
+
and `remarkNotation` reports the same at render time as a defense-in-depth
|
|
177
|
+
check for content added since the manifest was last compiled. None of these
|
|
178
|
+
fail the build — they are advisory, independent of `strict` — but a course
|
|
179
|
+
that wants them addressed should wrap the table in `<Table id="…">`/
|
|
180
|
+
`<Figure id="…">`, or give the diagram an `id` and move it to the top level (or
|
|
181
|
+
inside `<Memo>`/`<ShortNote>`), or wrap it in `<Figure>`.
|
|
182
|
+
|
|
183
|
+
### Knowledge checks
|
|
184
|
+
|
|
185
|
+
An assessment title is authored once, in `content/assessments/<id>.json`. Put
|
|
186
|
+
that id in the lesson's colocated `.checks.yml`, then render `<AssessmentSet />`;
|
|
187
|
+
do not add a separate “Check yourself” heading. The component renders
|
|
188
|
+
`Knowledge check ‹number› — ‹title›` as a permalinked, collapsible section at
|
|
189
|
+
`#check-<id>`. Cite it on the same page with `[[check-<id>]]`, or from another
|
|
190
|
+
lesson with `[[lesson/slug#check-<id>]]`. Single-choice options are shuffled
|
|
191
|
+
when the browser views the check; option ids, correctness, and authored content
|
|
192
|
+
remain unchanged.
|
|
193
|
+
|
|
194
|
+
## Detailed memos and inline short notes
|
|
195
|
+
|
|
196
|
+
Headings define the page's conceptual hierarchy: they belong in the outline and
|
|
197
|
+
should help every reader navigate the main argument. A `<Memo>` instead holds
|
|
198
|
+
optional depth — a long derivation, proof, robustness check, edge case, or
|
|
199
|
+
alternative method — without pretending that material is another section of
|
|
200
|
+
the lecture. It keeps the subsection-like card, background, and border, but is
|
|
201
|
+
collapsed and independently numbered.
|
|
202
|
+
|
|
203
|
+
Register `remarkShortNote` and then `remarkMemo` after `remarkMath` in
|
|
204
|
+
`astro.config.mjs`. Import `explico/components/memo/Memo.astro`, cite a memo with
|
|
205
|
+
`[[memo-id]]` wherever it is relevant, and place the flow block later:
|
|
206
|
+
|
|
207
|
+
```mdx
|
|
208
|
+
The full calculation is available in [[memo-supporting-check]].
|
|
209
|
+
|
|
210
|
+
Several paragraphs may intervene before the detailed material.
|
|
211
|
+
|
|
212
|
+
<Memo id="supporting-check" title="Supporting check" description="The detailed calculation behind the claim.">
|
|
213
|
+
|
|
214
|
+
The body can contain ordinary Markdown, display equations, `<Table>`, and
|
|
215
|
+
`<Figure>` components.
|
|
216
|
+
|
|
217
|
+
</Memo>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`[[memo-supporting-check]]` renders as `[1]`. Clicking it opens and scrolls to
|
|
221
|
+
the block and updates the URL to `#memo-supporting-check`; the `[1]` shown on
|
|
222
|
+
the memo itself remains a numbered permalink, and the chain icon beside its
|
|
223
|
+
title provides the same explicit anchor affordance used by headings and
|
|
224
|
+
figures. `id`, `title`, and `description` are required literal strings. The
|
|
225
|
+
block is collapsed on a reader's first visit and remembers later open/closed
|
|
226
|
+
choices unless `rememberState={false}` is set. Memo numbering is independent
|
|
227
|
+
of equation, table, figure, and diagram numbering. Those numbered items remain
|
|
228
|
+
in their normal page/part streams inside a memo.
|
|
229
|
+
|
|
230
|
+
For print and PDF export, the memo opens at its reading-order position. It does
|
|
231
|
+
not move to the bottom of the page: browser support for CSS footnote floats is
|
|
232
|
+
not reliable enough to make that layout portable.
|
|
233
|
+
|
|
234
|
+
Use `<ShortNote>` for the smaller, genuinely inline interaction. Import
|
|
235
|
+
`explico/components/memo/ShortNote.astro` and put it exactly where its collapsed
|
|
236
|
+
link belongs:
|
|
237
|
+
|
|
238
|
+
```mdx
|
|
239
|
+
The derivative agrees <ShortNote id="derivative" title="(show how)" description="Show the full calculus calculation.">
|
|
240
|
+
|
|
241
|
+
Take the derivative, set it to zero, and solve. The body may also contain
|
|
242
|
+
display equations, figures, or other block Markdown.
|
|
243
|
+
|
|
244
|
+
</ShortNote>. But symmetry is quicker.
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
While collapsed, the surrounding sentence remains one paragraph and only the
|
|
248
|
+
`title` text is visible. Activating it hides that link, inserts the body at that
|
|
249
|
+
point in the reading flow, and exposes a “Show less” control that restores the
|
|
250
|
+
original sentence. Short notes are for a locally optional explanation, not for
|
|
251
|
+
material that needs a number or citations elsewhere; use a memo for that.
|
|
@@ -31,11 +31,40 @@ identically in print and with JS off.
|
|
|
31
31
|
_How to apply._ Pass `nodes` / `edges` and, if any node has a `formula`, a
|
|
32
32
|
`scope` from `labMathScope` — see
|
|
33
33
|
`playground/content/labs/DerivationDiagram.astro`. A `formula` node's symbols
|
|
34
|
-
must resolve against the page glyph table, same as `NotationFormula`.
|
|
34
|
+
must resolve against the page glyph table, same as `NotationFormula`. Pass the
|
|
35
|
+
component's formula source to `labMathScope`; when it names a shared key that
|
|
36
|
+
the lesson MDX does not, also pass
|
|
37
|
+
`{ locals: Astro.locals, context: '<ComponentName> <formula context>' }` so the
|
|
38
|
+
key joins that page's notation payload. `Diagram` independently registers the
|
|
39
|
+
keys it ultimately emits, which protects wrapper components and supplies a
|
|
40
|
+
node-specific build diagnostic. If
|
|
35
41
|
`Diagram` genuinely cannot express the picture, raise it as an engine gap
|
|
36
42
|
rather than reaching for another library.
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
A top-level lesson diagram must expose an authored `id` on an MDX element named
|
|
45
|
+
`Diagram` (a wrapper component may be imported under that name). It then receives
|
|
46
|
+
an independent diagram number, a `#dia-<id>` anchor, and references of the form
|
|
47
|
+
`[[dia-<id>]]` or `[[lesson/slug#dia-<id>]]`. Diagram numbers do not advance the
|
|
48
|
+
equation, table, or figure sequences.
|
|
49
|
+
|
|
50
|
+
The component preserves the build-time canvas size and scrolls horizontally
|
|
51
|
+
when the work area is narrower. A compact control on the title line can expand
|
|
52
|
+
the original diagram toward the full in-page region and shrink it back; it is
|
|
53
|
+
not modal, follows the diagram's document position while the page scrolls, and
|
|
54
|
+
adds internal vertical scrolling when the viewport is too short. The shrink
|
|
55
|
+
control or Escape restores its original width. Clicking the title minimizes/
|
|
56
|
+
restores the diagram; this state is remembered per page by default
|
|
57
|
+
(`defaultOpen` / `rememberState` configure it).
|
|
58
|
+
The title also carries the same chain-link permalink treatment as headings.
|
|
59
|
+
|
|
60
|
+
By default a laid-out width above `800px` emits a compile-time warning. Treat
|
|
61
|
+
that warning as a prompt to try
|
|
62
|
+
`direction="TB"`, shorter labels, or explicit node sizes before accepting the
|
|
63
|
+
wide layout. Set `warnAboveWidth={false}` only when horizontal overflow is
|
|
64
|
+
intentional, or set a positive pixel value when the course needs a different
|
|
65
|
+
review threshold.
|
|
66
|
+
|
|
67
|
+
_Enforcement._ Review-only, with a compile-time width warning from `Diagram`.
|
|
39
68
|
|
|
40
69
|
_Exceptions._ A one-off illustrative schematic that is not a graph (a labelled
|
|
41
70
|
photo, a geometric figure) is a [V2](#v2) figure, not a `Diagram`.
|
|
@@ -83,7 +112,11 @@ An interactive figure is a React island, hydrated `client:visible` (or
|
|
|
83
112
|
- **Gate-checked math.** Any equation shown in the figure goes through the
|
|
84
113
|
`labMathScope` / `renderLabMath` / `\slot{}` seam so its symbols are resolved
|
|
85
114
|
against the page's real notation at build time and the island only patches
|
|
86
|
-
numbers into pre-rendered KaTeX.
|
|
115
|
+
numbers into pre-rendered KaTeX. A wrapper whose template names notation not
|
|
116
|
+
otherwise present in lesson MDX passes
|
|
117
|
+
`{ locals: Astro.locals, context: '<ComponentName> <template>' }` as the third
|
|
118
|
+
`labMathScope` argument; that is the declaration that ships those definitions
|
|
119
|
+
in this page's popup payload. See
|
|
87
120
|
`playground/content/labs/ProjectileRangeExplorer.tsx` +
|
|
88
121
|
`ProjectileRangeMath.astro`.
|
|
89
122
|
- **An accessible name and description** — `ariaLabel` / `ariaDescription` on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explico",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Build-time engine for prerequisite-aware, notation-rigorous course content: one validated content model, a semantic notation/citation/equation system with a completeness gate, and a deterministic JSON manifest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"docs/authoring"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@dagrejs/dagre": "3.1.1"
|
|
36
|
+
"@dagrejs/dagre": "3.1.1",
|
|
37
|
+
"esbuild": "0.27.7",
|
|
38
|
+
"parse5": "7.3.0"
|
|
37
39
|
},
|
|
38
40
|
"peerDependencies": {
|
|
39
41
|
"@astrojs/markdown-remark": "7.2.4",
|
|
@@ -62,6 +64,7 @@
|
|
|
62
64
|
},
|
|
63
65
|
"scripts": {
|
|
64
66
|
"build": "tsup && node scripts/copy-assets.mjs && node scripts/fix-esm-extensions.mjs && tsc -p tsconfig.dts-mjs.json",
|
|
67
|
+
"check:release-migration": "node scripts/check-release-migration.mjs",
|
|
65
68
|
"test": "vitest run",
|
|
66
69
|
"test:watch": "vitest"
|
|
67
70
|
}
|
package/src/compiler/astro.ts
CHANGED
|
@@ -1,17 +1,101 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
2
6
|
|
|
3
|
-
import type { AstroIntegration } from 'astro';
|
|
7
|
+
import type { AstroConfig, AstroIntegration } from 'astro';
|
|
4
8
|
import type { CourseConfig } from './course-config';
|
|
5
9
|
import {
|
|
6
10
|
compileManifest,
|
|
7
11
|
type Manifest,
|
|
8
12
|
writeManifest,
|
|
9
13
|
} from './manifest';
|
|
14
|
+
import { missingNotationPayloadKeys } from '../reference/page-notation';
|
|
15
|
+
import { writeStandaloneLessonExports } from './exports/standalone-html';
|
|
10
16
|
|
|
11
17
|
// Vite keeps its server object across a restart. Do not accumulate watcher
|
|
12
18
|
// callbacks each time Astro reloads its configuration.
|
|
13
19
|
const watchedServers = new WeakSet<object>();
|
|
14
20
|
|
|
21
|
+
function normalizedBasePath(base: string): string {
|
|
22
|
+
const segment = base.replace(/^\/+|\/+$/gu, '');
|
|
23
|
+
return segment ? `/${segment}/` : '/';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Match only a configured lesson artifact below Astro's deployment base. */
|
|
27
|
+
export function matchStandaloneDownload(
|
|
28
|
+
pathname: string,
|
|
29
|
+
base: string,
|
|
30
|
+
lessonSlugs: readonly string[],
|
|
31
|
+
): string | undefined {
|
|
32
|
+
const prefix = `${normalizedBasePath(base)}downloads/`;
|
|
33
|
+
if (!pathname.startsWith(prefix) || !pathname.endsWith('.html')) return undefined;
|
|
34
|
+
const slug = pathname.slice(prefix.length, -'.html'.length);
|
|
35
|
+
return lessonSlugs.includes(slug) ? slug : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function buildDevelopmentDownloads(
|
|
39
|
+
config: Pick<AstroConfig, 'root'>,
|
|
40
|
+
lessonSlugs: readonly string[],
|
|
41
|
+
): Promise<ReadonlyMap<string, Buffer>> {
|
|
42
|
+
const temporaryRoot = await mkdtemp(join(tmpdir(), 'explico-standalone-'));
|
|
43
|
+
const outputRoot = join(temporaryRoot, 'dist');
|
|
44
|
+
try {
|
|
45
|
+
const projectRoot = fileURLToPath(config.root);
|
|
46
|
+
const astroEntry = fileURLToPath(import.meta.resolve('astro'));
|
|
47
|
+
const astroCli = resolve(dirname(astroEntry), '..', 'bin', 'astro.mjs');
|
|
48
|
+
await new Promise<void>((resolveBuild, rejectBuild) => {
|
|
49
|
+
const child = spawn(
|
|
50
|
+
process.execPath,
|
|
51
|
+
[
|
|
52
|
+
astroCli,
|
|
53
|
+
'build',
|
|
54
|
+
'--root',
|
|
55
|
+
projectRoot,
|
|
56
|
+
'--outDir',
|
|
57
|
+
outputRoot,
|
|
58
|
+
'--silent',
|
|
59
|
+
],
|
|
60
|
+
{
|
|
61
|
+
cwd: projectRoot,
|
|
62
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
63
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
let diagnostic = '';
|
|
67
|
+
const capture = (chunk: Buffer) => {
|
|
68
|
+
if (diagnostic.length < 16_000) diagnostic += chunk.toString('utf8');
|
|
69
|
+
};
|
|
70
|
+
child.stdout.on('data', capture);
|
|
71
|
+
child.stderr.on('data', capture);
|
|
72
|
+
child.once('error', rejectBuild);
|
|
73
|
+
child.once('close', (code) => {
|
|
74
|
+
if (code === 0) {
|
|
75
|
+
resolveBuild();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
rejectBuild(
|
|
79
|
+
new Error(
|
|
80
|
+
`Astro's standalone development build exited with code ${code}.` +
|
|
81
|
+
(diagnostic.trim() ? `\n${diagnostic.trim()}` : ''),
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
return new Map(
|
|
87
|
+
await Promise.all(
|
|
88
|
+
lessonSlugs.map(async (slug) => [
|
|
89
|
+
slug,
|
|
90
|
+
await readFile(join(outputRoot, 'downloads', `${slug}.html`)),
|
|
91
|
+
] as const),
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
} finally {
|
|
95
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
15
99
|
/** Fail loudly instead of rendering math triggers whose local cards are stale. */
|
|
16
100
|
export function assertLocalNotationParity(
|
|
17
101
|
lessonId: string,
|
|
@@ -34,6 +118,35 @@ export function assertLocalNotationParity(
|
|
|
34
118
|
);
|
|
35
119
|
}
|
|
36
120
|
|
|
121
|
+
/** Fail an Astro build if final rendered HTML contains an unusable trigger. */
|
|
122
|
+
export function assertNotationPayloadHtml(page: string, html: string): void {
|
|
123
|
+
const missing = missingNotationPayloadKeys(html);
|
|
124
|
+
if (missing.length === 0) return;
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Explico notation payload invariant failed for rendered page "${page}": ` +
|
|
127
|
+
`${missing.map((key) => `data-notation-key="${key}"`).join(', ')} ` +
|
|
128
|
+
`${missing.length === 1 ? 'has' : 'have'} no matching ` +
|
|
129
|
+
'`data-notation-entry` popup definition. A nested Astro component that ' +
|
|
130
|
+
'emits notation must register its formula dependencies; use Diagram, ' +
|
|
131
|
+
'NotationFormula, or labMathScope(..., { locals: Astro.locals, context }).',
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function validateGeneratedNotationPayloads(directory: URL): Promise<void> {
|
|
136
|
+
const root = fileURLToPath(directory);
|
|
137
|
+
const walk = async (path: string): Promise<void> => {
|
|
138
|
+
for (const entry of await readdir(path, { withFileTypes: true })) {
|
|
139
|
+
const child = join(path, entry.name);
|
|
140
|
+
if (entry.isDirectory()) {
|
|
141
|
+
await walk(child);
|
|
142
|
+
} else if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
143
|
+
assertNotationPayloadHtml(relative(root, child), await readFile(child, 'utf8'));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
await walk(root);
|
|
148
|
+
}
|
|
149
|
+
|
|
37
150
|
function replaceInPlace(target: unknown, source: unknown): void {
|
|
38
151
|
if (Array.isArray(target) && Array.isArray(source)) {
|
|
39
152
|
target.splice(0, target.length, ...source);
|
|
@@ -82,15 +195,43 @@ export function explicoContentRefresh(
|
|
|
82
195
|
courseConfig: CourseConfig,
|
|
83
196
|
manifest: Manifest,
|
|
84
197
|
): AstroIntegration {
|
|
198
|
+
let developmentConfig: Pick<AstroConfig, 'root' | 'base'> | undefined;
|
|
85
199
|
return {
|
|
86
200
|
name: 'explico-content-refresh',
|
|
87
201
|
hooks: {
|
|
88
|
-
'astro:
|
|
202
|
+
'astro:config:setup': ({ command, config }) => {
|
|
203
|
+
if (command === 'dev') {
|
|
204
|
+
developmentConfig = { root: config.root, base: config.base };
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
'astro:server:setup': ({ server, logger }) => {
|
|
89
208
|
if (watchedServers.has(server)) return;
|
|
90
209
|
watchedServers.add(server);
|
|
91
210
|
const contentRoot = `${resolve(process.cwd(), 'content')}${sep}`;
|
|
92
211
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
93
212
|
let restarting = false;
|
|
213
|
+
let developmentDownloads:
|
|
214
|
+
| Promise<ReadonlyMap<string, Buffer>>
|
|
215
|
+
| undefined;
|
|
216
|
+
const developmentRoot = developmentConfig
|
|
217
|
+
? fileURLToPath(developmentConfig.root)
|
|
218
|
+
: process.cwd();
|
|
219
|
+
const generatedDevelopmentRoots = [
|
|
220
|
+
'.astro',
|
|
221
|
+
'build',
|
|
222
|
+
'dist',
|
|
223
|
+
'node_modules',
|
|
224
|
+
].map((directory) => `${resolve(developmentRoot, directory)}${sep}`);
|
|
225
|
+
|
|
226
|
+
const invalidateDevelopmentDownloads = (path: string) => {
|
|
227
|
+
const absolute = resolve(path);
|
|
228
|
+
if (
|
|
229
|
+
generatedDevelopmentRoots.some((root) => absolute.startsWith(root))
|
|
230
|
+
) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
developmentDownloads = undefined;
|
|
234
|
+
};
|
|
94
235
|
|
|
95
236
|
const refresh = (path: string) => {
|
|
96
237
|
const absolute = resolve(path);
|
|
@@ -121,6 +262,70 @@ export function explicoContentRefresh(
|
|
|
121
262
|
server.watcher.on('add', refresh);
|
|
122
263
|
server.watcher.on('change', refresh);
|
|
123
264
|
server.watcher.on('unlink', refresh);
|
|
265
|
+
|
|
266
|
+
const configuredDevelopment = developmentConfig;
|
|
267
|
+
if (manifest.lessonExports?.standaloneHtml && configuredDevelopment) {
|
|
268
|
+
server.watcher.on('add', invalidateDevelopmentDownloads);
|
|
269
|
+
server.watcher.on('change', invalidateDevelopmentDownloads);
|
|
270
|
+
server.watcher.on('unlink', invalidateDevelopmentDownloads);
|
|
271
|
+
server.middlewares.use(async (request, response, next) => {
|
|
272
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') return next();
|
|
273
|
+
let pathname: string;
|
|
274
|
+
try {
|
|
275
|
+
pathname = decodeURIComponent(
|
|
276
|
+
new URL(request.url ?? '/', 'http://localhost').pathname,
|
|
277
|
+
);
|
|
278
|
+
} catch {
|
|
279
|
+
return next();
|
|
280
|
+
}
|
|
281
|
+
const slug = matchStandaloneDownload(
|
|
282
|
+
pathname,
|
|
283
|
+
configuredDevelopment.base,
|
|
284
|
+
manifest.lessons.map((lesson) => lesson.slug),
|
|
285
|
+
);
|
|
286
|
+
if (!slug) return next();
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
logger.info(
|
|
290
|
+
developmentDownloads
|
|
291
|
+
? `Serving cached standalone lesson ${slug}.html`
|
|
292
|
+
: 'Building standalone lesson downloads for local development…',
|
|
293
|
+
);
|
|
294
|
+
developmentDownloads ??= buildDevelopmentDownloads(
|
|
295
|
+
configuredDevelopment,
|
|
296
|
+
manifest.lessons.map((lesson) => lesson.slug),
|
|
297
|
+
);
|
|
298
|
+
const currentDownloads = developmentDownloads;
|
|
299
|
+
currentDownloads.catch(() => {
|
|
300
|
+
if (developmentDownloads === currentDownloads) {
|
|
301
|
+
developmentDownloads = undefined;
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
const artifact = (await currentDownloads).get(slug);
|
|
305
|
+
if (!artifact) return next();
|
|
306
|
+
const filename = `${slug.split('/').at(-1) ?? 'lesson'}.html`;
|
|
307
|
+
response.statusCode = 200;
|
|
308
|
+
response.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
309
|
+
response.setHeader('Content-Length', artifact.byteLength);
|
|
310
|
+
response.setHeader(
|
|
311
|
+
'Content-Disposition',
|
|
312
|
+
`attachment; filename="${filename}"`,
|
|
313
|
+
);
|
|
314
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
315
|
+
response.end(request.method === 'HEAD' ? undefined : artifact);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
318
|
+
logger.error(`Could not build standalone lesson downloads: ${message}`);
|
|
319
|
+
response.statusCode = 500;
|
|
320
|
+
response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
321
|
+
response.end(`Could not build standalone lesson download.\n${message}\n`);
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
'astro:build:done': async ({ dir }) => {
|
|
327
|
+
await validateGeneratedNotationPayloads(dir);
|
|
328
|
+
await writeStandaloneLessonExports(dir, manifest);
|
|
124
329
|
},
|
|
125
330
|
},
|
|
126
331
|
};
|
package/src/compiler/cli.ts
CHANGED
|
@@ -40,7 +40,11 @@ import remarkMdx from 'remark-mdx';
|
|
|
40
40
|
import { createNotationKatexOptions } from '../reference/katex-options.mjs';
|
|
41
41
|
import rehypeFailKatexErrors from '../reference/rehype-fail-katex-errors.mjs';
|
|
42
42
|
import remarkCitation from '../reference/remark-citation.mjs';
|
|
43
|
-
import
|
|
43
|
+
import remarkMemo from '../reference/remark-memo.mjs';
|
|
44
|
+
import remarkNotation, {
|
|
45
|
+
createRemarkNotationOptions,
|
|
46
|
+
} from '../reference/remark-notation.mjs';
|
|
47
|
+
import remarkShortNote from '../reference/remark-short-note.mjs';
|
|
44
48
|
import type { Manifest, ManifestLesson } from './manifest';
|
|
45
49
|
import { isLessonSlug, lessonIdFromSlug } from './lesson-derivation';
|
|
46
50
|
|
|
@@ -173,6 +177,9 @@ export function lessonDiagnostics(
|
|
|
173
177
|
for (const diagnostic of [
|
|
174
178
|
...manifest.diagnostics.tables,
|
|
175
179
|
...manifest.diagnostics.figures,
|
|
180
|
+
...manifest.diagnostics.diagrams,
|
|
181
|
+
...manifest.diagnostics.checks,
|
|
182
|
+
...manifest.diagnostics.rawFloats,
|
|
176
183
|
]) {
|
|
177
184
|
if (
|
|
178
185
|
diagnostic.lessonId !== id &&
|
|
@@ -221,11 +228,18 @@ function processorFor(manifest: Manifest) {
|
|
|
221
228
|
// shape here that it does there (D7/F-tables-figures parity).
|
|
222
229
|
remarkMdx,
|
|
223
230
|
remarkMath,
|
|
231
|
+
remarkShortNote,
|
|
232
|
+
remarkMemo,
|
|
224
233
|
// `manifest` carries the number tables *and* the part/offset placement
|
|
225
234
|
// together, so this check numbers a page exactly as the Astro build
|
|
226
235
|
// does. Passing the tables alone silently numbered pages by `##` section
|
|
227
236
|
// here while cross-page references read the part-scheme manifest.
|
|
228
|
-
|
|
237
|
+
// No `strict: true` here: `renderLesson()` below is also called
|
|
238
|
+
// directly with a synthetic path/body that has no manifest entry at all
|
|
239
|
+
// (fixture renderability checks — see its own doc comment), which
|
|
240
|
+
// strict mode would reject as "catalogued lesson with no numbering",
|
|
241
|
+
// even though it was never meant to be catalogued in the first place.
|
|
242
|
+
[remarkNotation, createRemarkNotationOptions(manifest)],
|
|
229
243
|
[remarkCitation, { sources: manifest.sources }],
|
|
230
244
|
],
|
|
231
245
|
rehypePlugins: [
|
|
@@ -476,6 +490,7 @@ export interface StatusReport {
|
|
|
476
490
|
readonly keyedEquations: number;
|
|
477
491
|
readonly keyedTables: number;
|
|
478
492
|
readonly keyedFigures: number;
|
|
493
|
+
readonly keyedDiagrams: number;
|
|
479
494
|
};
|
|
480
495
|
readonly draftDebt: {
|
|
481
496
|
readonly lessons: readonly string[];
|
|
@@ -521,6 +536,9 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
521
536
|
...manifest.diagnostics.equations.map((d) => d.severity),
|
|
522
537
|
...manifest.diagnostics.tables.map((d) => d.severity),
|
|
523
538
|
...manifest.diagnostics.figures.map((d) => d.severity),
|
|
539
|
+
...manifest.diagnostics.diagrams.map((d) => d.severity),
|
|
540
|
+
...manifest.diagnostics.checks.map((d) => d.severity),
|
|
541
|
+
...manifest.diagnostics.rawFloats.map((d) => d.severity),
|
|
524
542
|
...manifest.diagnostics.consistency.map(() => 'warning' as const),
|
|
525
543
|
...manifest.diagnostics.knowledge.map((d) => d.severity),
|
|
526
544
|
];
|
|
@@ -537,6 +555,10 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
537
555
|
(count, lesson) => count + lesson.figures.length,
|
|
538
556
|
0,
|
|
539
557
|
);
|
|
558
|
+
const keyedDiagrams = manifest.lessons.reduce(
|
|
559
|
+
(count, lesson) => count + lesson.diagrams.length,
|
|
560
|
+
0,
|
|
561
|
+
);
|
|
540
562
|
const assessments = new Set(
|
|
541
563
|
manifest.lessons.flatMap((lesson) => lesson.assessments),
|
|
542
564
|
).size;
|
|
@@ -553,6 +575,7 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
553
575
|
keyedEquations,
|
|
554
576
|
keyedTables,
|
|
555
577
|
keyedFigures,
|
|
578
|
+
keyedDiagrams,
|
|
556
579
|
},
|
|
557
580
|
draftDebt: { lessons: draftLessons, notation: draftNotation },
|
|
558
581
|
orphans,
|
|
@@ -593,6 +616,8 @@ export interface ContextReport {
|
|
|
593
616
|
readonly equations: readonly { key: string; number: string }[];
|
|
594
617
|
readonly tables: readonly { key: string; number: string }[];
|
|
595
618
|
readonly figures: readonly { key: string; number: string }[];
|
|
619
|
+
readonly diagrams: readonly { key: string; number: string }[];
|
|
620
|
+
readonly knowledgeChecks: readonly { key: string; number: string }[];
|
|
596
621
|
readonly sources: readonly {
|
|
597
622
|
id: string;
|
|
598
623
|
title: string;
|
|
@@ -663,6 +688,8 @@ export function contentContext(
|
|
|
663
688
|
equations: lesson.equations.map((equation) => ({ ...equation })),
|
|
664
689
|
tables: lesson.tables.map((table) => ({ ...table })),
|
|
665
690
|
figures: lesson.figures.map((figure) => ({ ...figure })),
|
|
691
|
+
diagrams: lesson.diagrams.map((diagram) => ({ ...diagram })),
|
|
692
|
+
knowledgeChecks: lesson.knowledgeChecks.map((check) => ({ ...check })),
|
|
666
693
|
sources: lesson.sources.map((id) => {
|
|
667
694
|
const record = sourceById.get(id) as Record<string, unknown> | undefined;
|
|
668
695
|
return {
|