explico 0.5.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 +116 -7
- package/dist/compiler/course-config.d.ts +31 -1
- package/dist/compiler/course-config.js +7 -0
- package/dist/compiler/manifest.d.ts +10 -2
- package/dist/compiler/manifest.js +7 -3
- 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 +6 -2
- package/dist/reference/gate-math.js +2 -1
- package/dist/reference/math-glyphs.d.mts +12 -0
- package/dist/reference/math-glyphs.mjs +183 -42
- package/dist/reference/remark-citation.d.mts +5 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/docs/VERSIONING.md +22 -2
- package/docs/authoring/README.md +2 -0
- package/docs/authoring/components.md +4 -1
- package/docs/authoring/migrating.md +191 -8
- package/docs/authoring/notation.md +55 -0
- package/docs/authoring/structure-rules.md +69 -0
- package/docs/authoring/visualizations.md +2 -2
- package/package.json +1 -1
- package/src/compiler/course-config.ts +40 -0
- package/src/compiler/manifest.ts +14 -1
- package/src/components/diagram/Diagram.astro +0 -55
- package/src/components/notation/NotationLayer.astro +155 -75
- package/src/components/starlight/LessonFooter.astro +3 -1
- package/src/content-config.ts +16 -1
- package/src/reference/gate-heading-length.ts +131 -0
- package/src/reference/gate-math.ts +7 -2
- package/src/reference/math-glyphs.mjs +183 -42
- package/src/reference/remark-citation.mjs +0 -0
- package/src/styles/global.css +1 -1
|
@@ -33,9 +33,141 @@ required config block throws with the section of this file to read.
|
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
|
+
## 0.5.0 → 0.6.0
|
|
37
|
+
|
|
38
|
+
### 1. Rule S1: no heading or float title runs past 65 characters
|
|
39
|
+
|
|
40
|
+
Every Markdown heading (`##` through `######`), a lesson's own
|
|
41
|
+
`frontmatter.title`, and the `title`/`label` prop on `Figure`, `Diagram`,
|
|
42
|
+
`Memo`, and `ShortNote` is now capped at 65 characters; `Table` is exempt (it
|
|
43
|
+
has only a `caption`). No wiring is required — the check runs as part of
|
|
44
|
+
`pnpm validate:content` / `astro build` the same as N9/N13/N15. Audit for
|
|
45
|
+
existing violations and shorten them, moving the rest of the sentence into
|
|
46
|
+
the paragraph after a heading or into a float's `caption`. See
|
|
47
|
+
[`docs/authoring/structure-rules.md#s1`](structure-rules.md#s1).
|
|
48
|
+
|
|
49
|
+
### 2. Removed markup: `.notation-pin`, `[data-notation-control]`, `.notation-layer-heading`
|
|
50
|
+
|
|
51
|
+
The "Page notation" panel's "Pin explanation" button (`.notation-pin`,
|
|
52
|
+
`[data-notation-control]`) and its separate `<h2>` (`.notation-layer-heading`)
|
|
53
|
+
are both gone from the rendered DOM. Pinning a card now works the same way an
|
|
54
|
+
inline prose symbol always has — click its glyph — and the panel's
|
|
55
|
+
`<summary>` is its only heading. Update any course-owned CSS or end-to-end
|
|
56
|
+
test that targets these selectors; no other action is required.
|
|
57
|
+
|
|
58
|
+
### 3. Removed prop: `<Diagram description>`
|
|
59
|
+
|
|
60
|
+
`Diagram`'s `description` prop and its collapsed "Diagram description"
|
|
61
|
+
fallback block (`.diagram-fallback`) no longer exist. `astro check` fails on
|
|
62
|
+
an excess prop, so remove every `description="…"` passed to `<Diagram>`; fold
|
|
63
|
+
that text into the diagram's `label` (caption) instead.
|
|
64
|
+
|
|
65
|
+
### 4. Optional: collapsible "Page notation" panel and References section
|
|
66
|
+
|
|
67
|
+
`NotationLayer` gains `defaultOpen`/`rememberState` props (mirroring
|
|
68
|
+
`Figure`'s own), and `remark-citation.mjs`'s generated References heading now
|
|
69
|
+
accepts a `defaultOpen` plugin option — both ride the same persisted,
|
|
70
|
+
deep-linkable disclosure machinery `Figure`/`CollapsibleSections` already use,
|
|
71
|
+
in place of the previous always-native-default `<details>`. No action is
|
|
72
|
+
required to keep building: both default to closed, matching prior behavior,
|
|
73
|
+
and the References option only takes effect on a course whose
|
|
74
|
+
`astro.config.mjs` already wires `rehypeHeadingAnchors` into its rehype
|
|
75
|
+
pipeline. To opt into a different default, set `CourseConfig.disclosureDefaults`:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
disclosureDefaults: {
|
|
79
|
+
notation: true, // "Page notation" panel starts open for a reader with nothing saved yet
|
|
80
|
+
references: true, // generated References section starts open
|
|
81
|
+
},
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 5. Manifest schema is now v14 (additive: v13 → v14)
|
|
85
|
+
|
|
86
|
+
`CourseConfig.disclosureDefaults` is new and additive — nothing existing
|
|
87
|
+
changed shape — but a client asserting on `MANIFEST_SCHEMA_VERSION` by exact
|
|
88
|
+
equality needs the bump.
|
|
89
|
+
|
|
90
|
+
### 6. Re-run `pnpm validate:content` — the notation auto-annotator's silent-failure bugs are fixed
|
|
91
|
+
|
|
92
|
+
Several shapes that previously either dropped a symbol's tooltip silently (no
|
|
93
|
+
build error) or produced malformed output are now fixed at the resolver
|
|
94
|
+
(`resolveMathGlyphs`), not just documented as traps to avoid: a bare
|
|
95
|
+
`\mathrm{…}`-only symbol (`\mathrm{SSR}`, `\mathrm{SST}`) can now auto-resolve
|
|
96
|
+
at every later bare occurrence, a bare symbol immediately next to an
|
|
97
|
+
`\explain{…}{…}` call no longer has its span corrupted, and a partial
|
|
98
|
+
`\explain{key}{…}` wrap no longer produces unparseable doubled output. No
|
|
99
|
+
course-side wiring changes, but re-running `pnpm validate:content` may surface
|
|
100
|
+
two new classes of error that previously passed silently:
|
|
101
|
+
|
|
102
|
+
- **"Unresolved notation"** for a symbol that was relying on one of the fixed
|
|
103
|
+
bugs to appear resolved when it was not (rare — the bugs mostly hid symbols
|
|
104
|
+
entirely rather than falsely resolving them) or for the uncovered remainder
|
|
105
|
+
of a partial `\explain{key}{…}` wrap. Fix by explaining the symbol, or by
|
|
106
|
+
wrapping the key's *entire* declared `latex`, not just its base glyph.
|
|
107
|
+
- **"Page glyphs … have structurally identical LaTeX"** for two different
|
|
108
|
+
notation keys whose declared `latex` is the same `\operatorname{…}` or
|
|
109
|
+
`\mathrm{…}` form (e.g. two keys both `\operatorname{Var}`) — this check
|
|
110
|
+
previously silently skipped every such glyph and could not catch this. Give
|
|
111
|
+
the colliding keys distinct canonical forms, or reach for
|
|
112
|
+
`\explain{key}{…}` at every occurrence of whichever one keeps the shared
|
|
113
|
+
form.
|
|
114
|
+
|
|
36
115
|
## 0.4.x → 0.5.0
|
|
37
116
|
|
|
38
|
-
### 1. Rule
|
|
117
|
+
### 1. Rule N9: wrap every plain-text quantity, unit, and operator in `$…$`
|
|
118
|
+
|
|
119
|
+
Every physical quantity, unit abbreviation, or math operator ever typed as
|
|
120
|
+
plain text — in lesson prose, a table cell, or (new in 0.5.0) an assessment
|
|
121
|
+
item's `prompt` / `explanation` string — must now be inside `$…$`. `45 m/s`,
|
|
122
|
+
`t (s)`, `vx`, `\|v\|`, `KE/m` are all the same mistake. This is build-enforced
|
|
123
|
+
with **no `lint-ignore.yml` escape hatch**: `pnpm validate:content` / `astro
|
|
124
|
+
build` fails on every existing violation. Fix per
|
|
125
|
+
[`docs/authoring/notation.md#n9`](notation.md#n9) — wrap the symbol in `$…$`
|
|
126
|
+
and fold its unit into the *same* span as `\text{}` (`$t\ \text{(s)}$`, not two
|
|
127
|
+
adjacent spans).
|
|
128
|
+
|
|
129
|
+
Also wire the accessible-table-header plugin this rule ships with — the build
|
|
130
|
+
works without it, but a table header that is now 100% KaTeX (a bare `$t$`
|
|
131
|
+
column, since its unit moved inside `$…$` too) renders with no
|
|
132
|
+
screen-reader-visible name until this is wired:
|
|
133
|
+
|
|
134
|
+
```diff
|
|
135
|
+
+ import rehypeTableHeaderLabels from 'explico/reference/rehype-table-header-labels.mjs';
|
|
136
|
+
…
|
|
137
|
+
rehypePlugins: [
|
|
138
|
+
[rehypeKatex, createNotationKatexOptions()],
|
|
139
|
+
rehypeFailKatexErrors,
|
|
140
|
+
+ rehypeTableHeaderLabels,
|
|
141
|
+
]
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
It reads the `\explain{key}{…}` key(s) a header's LaTeX names and backfills an
|
|
145
|
+
`sr-only` label from that key's notation metadata — the one prerequisite is
|
|
146
|
+
that the header's symbol resolves to a key at all, so a `\mathrm{…}` compound
|
|
147
|
+
(`\mathrm{KE}/m`) needs each side explicitly `\explain{}`-wrapped.
|
|
148
|
+
|
|
149
|
+
### 2. Rule N13: a nonlinear function always takes a parenthesized argument
|
|
150
|
+
|
|
151
|
+
`\sin x`, `\log t`, `\exp\theta` — every trig/inverse-trig/hyperbolic function
|
|
152
|
+
plus `exp`/`log`/`ln`/`lg` — is now a hard error unless its argument is
|
|
153
|
+
parenthesized: `\sin(x)` or `\sin\left(x\right)` is required. Same enforcement
|
|
154
|
+
shape as N9: build-enforced, no `lint-ignore.yml` escape. It fires on lesson
|
|
155
|
+
body math, `notation.formula`, assessment `prompt`/`explanation`, `Diagram`
|
|
156
|
+
formulas, and interactive lab math alike, since all of them resolve through
|
|
157
|
+
the same glyph walker. Fix every occurrence; see
|
|
158
|
+
[`docs/authoring/notation.md#n13`](notation.md#n13).
|
|
159
|
+
|
|
160
|
+
### 3. Rule N15: size every level of a nested visible delimiter
|
|
161
|
+
|
|
162
|
+
Once a `(`, `[`, `\{`, or `|` sits inside another one of those four — same
|
|
163
|
+
type or mixed, e.g. `(a + [b])` — **every level of that nest** must switch to
|
|
164
|
+
`\left`/`\right` sizing: `\sin(2(90°-\theta))` is now a hard error,
|
|
165
|
+
`\sin\left(2\left(90°-\theta\right)\right)` is required. Same enforcement
|
|
166
|
+
shape as N9/N13: build-enforced, no escape hatch. A single, non-nested bare
|
|
167
|
+
delimiter (`(x)`) is untouched. Fix every nest; see
|
|
168
|
+
[`docs/authoring/notation.md#n15`](notation.md#n15).
|
|
169
|
+
|
|
170
|
+
### 4. Rule N14: catalogue every abbreviation used in lesson prose
|
|
39
171
|
|
|
40
172
|
Every acronym (`OLS`, `MSE`) or dotted abbreviation (`w.r.t.`, `i.i.d.`) used in
|
|
41
173
|
plain prose must now be on the shipped baseline
|
|
@@ -69,14 +201,65 @@ every `abbreviation-explained` diagnostic by adding a
|
|
|
69
201
|
`content/abbreviations/<key>.md` entry or an `allow` entry in
|
|
70
202
|
`course.config.ts`.
|
|
71
203
|
|
|
72
|
-
|
|
204
|
+
The catalog check above runs regardless of Astro wiring, but the inline
|
|
205
|
+
hover/click card on a lesson page's first occurrence of a catalogued
|
|
206
|
+
abbreviation needs its remark plugin wired in too:
|
|
73
207
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
208
|
+
```diff
|
|
209
|
+
+ import remarkAbbreviations from 'explico/reference/remark-abbreviations.mjs';
|
|
210
|
+
…
|
|
211
|
+
remarkPlugins: [
|
|
212
|
+
…
|
|
213
|
+
[remarkCitation, { sources: manifest.sources }],
|
|
214
|
+
+ [remarkAbbreviations, { abbreviations: manifest.abbreviations }],
|
|
215
|
+
]
|
|
216
|
+
```
|
|
78
217
|
|
|
79
|
-
###
|
|
218
|
+
### 5. Optional: rule N10 warns on an unexplained `\mathrm{…}` label
|
|
219
|
+
|
|
220
|
+
A `\mathrm{…}` multi-letter label (`\mathrm{KE}`, `\mathrm{PV}`) not on the
|
|
221
|
+
shipped roman-label baseline (`base-roman-labels.mjs`) or `course.config.ts`'s
|
|
222
|
+
new `romanLabels.allow` now needs a notation entry the reader can hover — the
|
|
223
|
+
`\mathrm{…}` counterpart to N8. Unlike N9/N13/N14/N15, this is a **Tier-3
|
|
224
|
+
warning** (`roman-label-explained`), not a build failure: it never blocks
|
|
225
|
+
`astro build`, and an existing violation can be silenced with a reviewed
|
|
226
|
+
`lint-ignore.yml` line (`roman-label-explained:KE — <reason>`) while content is
|
|
227
|
+
cleaned up. `pnpm validate:content` surfaces it. See
|
|
228
|
+
[`docs/authoring/notation.md#n10`](notation.md#n10).
|
|
229
|
+
|
|
230
|
+
### 6. Only if you render `AssessmentRunner` directly (bypassing `AssessmentSet.astro`)
|
|
231
|
+
|
|
232
|
+
`AssessmentRunner`'s `Item` props changed shape: `prompt` / `explanation`
|
|
233
|
+
(`string`) are now `promptContent` / `explanationContent`
|
|
234
|
+
(`MixedContentNode[]`), rendered with the new `RenderedContent` component,
|
|
235
|
+
because `AssessmentSet.astro` now pre-renders assessment math through the same
|
|
236
|
+
KaTeX pipeline as lesson body math instead of handing `AssessmentRunner` a raw
|
|
237
|
+
string. A course that only uses `<AssessmentSet>` needs no change here — this
|
|
238
|
+
only applies to a course that imports and renders `AssessmentRunner` itself.
|
|
239
|
+
|
|
240
|
+
### 7. Manifest schema is v13 (additive: v7 → v8 → v9 → v10 → v11 → v12 → v13)
|
|
241
|
+
|
|
242
|
+
Every step is additive — nothing existing changed shape — but a client
|
|
243
|
+
asserting on `MANIFEST_SCHEMA_VERSION` by exact equality needs the final
|
|
244
|
+
number:
|
|
245
|
+
|
|
246
|
+
- **v8** — `assessmentContent`: every assessment item's `prompt` /
|
|
247
|
+
`explanation`, pre-split and resolved for `AssessmentSet.astro` (see item 6
|
|
248
|
+
above).
|
|
249
|
+
- **v9** — `diagrams`, `lessons[].diagrams`, `lessons[].numbering.diagrams`,
|
|
250
|
+
`diagnostics.diagrams`: `<Diagram>` becomes a first-class keyed float with
|
|
251
|
+
its own number stream and `[[dia-<id>]]` references.
|
|
252
|
+
- **v10** — `checks`, `lessons[].knowledgeChecks`, `lessons[].numbering.checks`,
|
|
253
|
+
`diagnostics.checks`: numbered, referenceable knowledge checks.
|
|
254
|
+
- **v11** — `lessons[].lessonExports`-derived fields: standalone HTML / print
|
|
255
|
+
export settings (item 8 below).
|
|
256
|
+
- **v12** — `manifest.abbreviations` (rule N14, item 4 above).
|
|
257
|
+
- **v13** — `manifest.diagnostics.rawFloats`: a raw Markdown/HTML table, an
|
|
258
|
+
unkeyed `<Diagram>`, or a `<Diagram>` nested outside
|
|
259
|
+
`<Memo>`/`<ShortNote>`/`<Table>`/`<Figure>` — none of these reach
|
|
260
|
+
keyed-float validation, so this diagnostic flags them separately.
|
|
261
|
+
|
|
262
|
+
### 8. Optional: enable standalone HTML / print lesson exports
|
|
80
263
|
|
|
81
264
|
No action required to keep building. To opt in, add to
|
|
82
265
|
`content/course.config.ts`:
|
|
@@ -89,7 +272,7 @@ lessonExports: {
|
|
|
89
272
|
canonicalBaseUrl: 'https://your-course.example.com',
|
|
90
273
|
```
|
|
91
274
|
|
|
92
|
-
###
|
|
275
|
+
### 9. Optional: turn on strict numbering cross-checks
|
|
93
276
|
|
|
94
277
|
`remarkNotation`'s new `strict: true` option — recommended via
|
|
95
278
|
`createRemarkNotationOptions(manifest, { strict: true })` — fails the build on
|
|
@@ -131,6 +131,61 @@ _How to apply._ See the engine README's notation section and the `notation`
|
|
|
131
131
|
collection schema. A letter that exists only so one entry's own `formula` can
|
|
132
132
|
be stated is a **gloss**, not a card — it creates no curriculum edge.
|
|
133
133
|
|
|
134
|
+
Do not satisfy this rule by dumping every `[[key]]` a page uses into one
|
|
135
|
+
trailing "Notation used on this page" (or similarly named) section near the
|
|
136
|
+
end of the lesson. `LessonFooter.astro` already injects an interactive
|
|
137
|
+
"Page notation" panel — built from the page's actual notation manifest, with
|
|
138
|
+
hover/click cards — into every lesson's footer automatically; a hand-written
|
|
139
|
+
list repeats the same term set as flat, non-interactive prose immediately
|
|
140
|
+
above it. Introduce each shared symbol inline, in the sentence where it is
|
|
141
|
+
first defined or used, the way `[[eq-key]]` and other cross-references are
|
|
142
|
+
already used inline elsewhere in these lessons. The completeness gate only
|
|
143
|
+
checks that a `[[key]]`/`\explain{key}` appears *somewhere* in the page body
|
|
144
|
+
(document order does not matter to it), but inline-at-first-use is what keeps
|
|
145
|
+
the rendered page free of a redundant list — and matches what a reader
|
|
146
|
+
actually wants: the symbol explained where it shows up, not gathered into an
|
|
147
|
+
appendix.
|
|
148
|
+
|
|
149
|
+
A page's own `notation.local` entries (declared in frontmatter) are already in
|
|
150
|
+
scope everywhere on that page for the *math completeness gate* — a bare
|
|
151
|
+
occurrence of a local symbol never trips "Unresolved notation" even with zero
|
|
152
|
+
`[[key]]`/`\explain` references. But a **different** check,
|
|
153
|
+
`assertLocalNotationParity` (run at `astro build`, not `pnpm validate:content`),
|
|
154
|
+
still requires every `notation.local` entry to be *referenced* somewhere in
|
|
155
|
+
the body — otherwise the build fails with "Stale Explico manifest for
|
|
156
|
+
‹lesson›: lesson notation.local has […], but the page notation bundle has
|
|
157
|
+
[…]." So every local entry still needs one `\explain{key}{…}` (or `[[key]]`)
|
|
158
|
+
mention, even though the math gate alone wouldn't ask for it.
|
|
159
|
+
|
|
160
|
+
The auto-annotator (`resolveMathGlyphs` in `math-glyphs.mjs`) re-derives every
|
|
161
|
+
`\explain{…}` marker's insertion point from KaTeX's own parsed offsets, then
|
|
162
|
+
re-applies those offsets to the raw source. Three shapes used to corrupt that
|
|
163
|
+
offset math badly enough to either silently drop a symbol (no tooltip, no
|
|
164
|
+
error) or emit unparseable output: a bare roman-label symbol (`\mathrm{SSR}`)
|
|
165
|
+
could never auto-resolve at all regardless of where it sat on the page; a bare
|
|
166
|
+
symbol immediately adjacent to an explicit `\explain{…}{…}` call could have
|
|
167
|
+
its span swallow the whole following command; and a canonical whole-form
|
|
168
|
+
match could be found _through_ an explicit call's own wrapping and extend
|
|
169
|
+
past its span. All three are fixed at the resolver, not just detected — a
|
|
170
|
+
bare `\mathrm{SST}`, `\epsilon\explain{feature-function}{h}`, and similar
|
|
171
|
+
shapes now resolve correctly with no special authoring care required, and
|
|
172
|
+
`gate-math.ts` exercises the same resolver at `pnpm validate:content` time
|
|
173
|
+
(it reuses `resolveMathGlyphs` directly), so a regression here fails the
|
|
174
|
+
build immediately rather than passing silently through to a render-time
|
|
175
|
+
surprise.
|
|
176
|
+
|
|
177
|
+
One related shape is still worth knowing, because it is a genuine authoring
|
|
178
|
+
mistake rather than a resolver bug: a partial `\explain{key}{…}` wrap that
|
|
179
|
+
covers only part of the key's declared `latex`. If `latex: Z_N` and you write
|
|
180
|
+
`\explain{z-statistic}{Z}_N` (base wrapped, subscript left outside), the
|
|
181
|
+
resolver no longer corrupts the output — but the uncovered remainder (`N`)
|
|
182
|
+
has no binding of its own and correctly fails loudly as "Unresolved
|
|
183
|
+
notation," at `pnpm validate:content` time. Wrap the whole declared form —
|
|
184
|
+
`\explain{z-statistic}{Z_N}` — instead; bare `Z_N` elsewhere on the page then
|
|
185
|
+
resolves automatically. The same applies to accent-wrapped forms
|
|
186
|
+
(`latex: \widehat{\theta}`): wrap the accent and its argument together, not
|
|
187
|
+
just the inner letter.
|
|
188
|
+
|
|
134
189
|
Math authored inside a nested Astro component is outside the lesson MDX scan,
|
|
135
190
|
so the component must declare the formula text at render time. `Diagram` and
|
|
136
191
|
`NotationFormula` do this automatically from the notation keys they actually
|
|
@@ -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.
|
|
@@ -53,8 +53,8 @@ the original diagram toward the full in-page region and shrink it back; it is
|
|
|
53
53
|
not modal, follows the diagram's document position while the page scrolls, and
|
|
54
54
|
adds internal vertical scrolling when the viewport is too short. The shrink
|
|
55
55
|
control or Escape restores its original width. Clicking the title minimizes/
|
|
56
|
-
restores the diagram
|
|
57
|
-
|
|
56
|
+
restores the diagram; this state is remembered per page by default
|
|
57
|
+
(`defaultOpen` / `rememberState` configure it).
|
|
58
58
|
The title also carries the same chain-link permalink treatment as headings.
|
|
59
59
|
|
|
60
60
|
By default a laid-out width above `800px` emits a compile-time warning. Treat
|
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",
|
|
@@ -116,6 +116,43 @@ export interface CourseAbout {
|
|
|
116
116
|
readonly changelogHref?: string;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* A course's chosen initial open/closed state for the engine's own built-in
|
|
121
|
+
* collapsible sections — the "Page notation" panel (`NotationLayer.astro`)
|
|
122
|
+
* and the generated "References" section (`remark-citation.mjs`). Each is
|
|
123
|
+
* independent of `defaultCollapsible.sections`/`.subsections` (an author's
|
|
124
|
+
* own `##` headings): both of these are engine-authored, not written by a
|
|
125
|
+
* lesson, so they need their own default rather than inheriting the site's
|
|
126
|
+
* general heading default.
|
|
127
|
+
*
|
|
128
|
+
* `true` starts open, `false` (or omitted) starts closed — the engine's own
|
|
129
|
+
* default is closed either way, matching a long panel a reader usually
|
|
130
|
+
* doesn't need open on arrival.
|
|
131
|
+
*/
|
|
132
|
+
export interface CourseDisclosureDefaults {
|
|
133
|
+
/** Initial state of the "Page notation" panel. Default: closed. */
|
|
134
|
+
readonly notation?: boolean;
|
|
135
|
+
/** Initial state of the generated "References" section. Default: closed. */
|
|
136
|
+
readonly references?: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The stable, non-optional form `NormalizedLessonExports` mirrors. */
|
|
140
|
+
export interface NormalizedDisclosureDefaults {
|
|
141
|
+
readonly notation: boolean;
|
|
142
|
+
readonly references: boolean;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Validate and canonicalize `CourseConfig.disclosureDefaults`. Absent (or
|
|
146
|
+
* an absent sub-field) resolves to closed, the engine's own default. */
|
|
147
|
+
export function normalizeDisclosureDefaults(
|
|
148
|
+
config: CourseConfig,
|
|
149
|
+
): NormalizedDisclosureDefaults {
|
|
150
|
+
return {
|
|
151
|
+
notation: config.disclosureDefaults?.notation === true,
|
|
152
|
+
references: config.disclosureDefaults?.references === true,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
119
156
|
/** Optional, build-time lesson artifacts exposed to readers. */
|
|
120
157
|
export interface LessonExportsConfig {
|
|
121
158
|
/** Emit one self-contained, offline-capable HTML file per lesson. */
|
|
@@ -162,6 +199,9 @@ export interface CourseConfig {
|
|
|
162
199
|
readonly abbreviations?: CourseAbbreviations;
|
|
163
200
|
/** Per-lesson export actions and downloadable artifacts. Disabled when omitted. */
|
|
164
201
|
readonly lessonExports?: LessonExportsConfig;
|
|
202
|
+
/** Initial open/closed state for the engine's own built-in collapsible
|
|
203
|
+
* sections ("Page notation", "References"). Closed when omitted. */
|
|
204
|
+
readonly disclosureDefaults?: CourseDisclosureDefaults;
|
|
165
205
|
}
|
|
166
206
|
|
|
167
207
|
/** Validate and canonicalize the build-facing lesson-export configuration. */
|
package/src/compiler/manifest.ts
CHANGED
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
gateProseNotationInAssessments,
|
|
89
89
|
} from '../reference/gate-prose-notation';
|
|
90
90
|
import { gateAbbreviations } from '../reference/gate-abbreviations';
|
|
91
|
+
import { gateHeadingLength } from '../reference/gate-heading-length';
|
|
91
92
|
import {
|
|
92
93
|
splitMixedContent,
|
|
93
94
|
type ContentSegment,
|
|
@@ -109,8 +110,10 @@ import type {
|
|
|
109
110
|
import {
|
|
110
111
|
coursePartsOf,
|
|
111
112
|
normalizeLessonExports,
|
|
113
|
+
normalizeDisclosureDefaults,
|
|
112
114
|
type CourseConfig,
|
|
113
115
|
type NormalizedLessonExports,
|
|
116
|
+
type NormalizedDisclosureDefaults,
|
|
114
117
|
} from './course-config';
|
|
115
118
|
import {
|
|
116
119
|
resolveKnowledgeVersion,
|
|
@@ -177,8 +180,14 @@ import {
|
|
|
177
180
|
* validation and can never get a stable number or `[[…]]` reference; this
|
|
178
181
|
* flags it (warning-only — the manifest shape a client already reads is
|
|
179
182
|
* unchanged).
|
|
183
|
+
* v14 (disclosure defaults, additive): `disclosureDefaults` — the resolved
|
|
184
|
+
* initial open/closed state for the "Page notation" panel and the generated
|
|
185
|
+
* "References" section, from `CourseConfig.disclosureDefaults`. Both are
|
|
186
|
+
* engine-authored sections, not written by a lesson, so they get their own
|
|
187
|
+
* default independent of a course's `##` heading default; closed unless the
|
|
188
|
+
* course sets one.
|
|
180
189
|
*/
|
|
181
|
-
export const MANIFEST_SCHEMA_VERSION =
|
|
190
|
+
export const MANIFEST_SCHEMA_VERSION = 14;
|
|
182
191
|
|
|
183
192
|
const REPOSITORY_ROOT = process.cwd();
|
|
184
193
|
// Content is the top-level `content/` folder; the build output is `build/`
|
|
@@ -382,6 +391,8 @@ export interface Manifest {
|
|
|
382
391
|
readonly schemaVersion: number;
|
|
383
392
|
/** Normalized build/UI settings for opt-in per-lesson exports. */
|
|
384
393
|
readonly lessonExports: NormalizedLessonExports;
|
|
394
|
+
/** Initial open/closed state for "Page notation" and "References". */
|
|
395
|
+
readonly disclosureDefaults: NormalizedDisclosureDefaults;
|
|
385
396
|
/** Reader-facing identity + versions (the `[?]` About panel). */
|
|
386
397
|
readonly about: ManifestAbout;
|
|
387
398
|
readonly notation: {
|
|
@@ -703,6 +714,7 @@ export async function compileManifest(
|
|
|
703
714
|
abbreviations: abbreviationsInput,
|
|
704
715
|
courseAbbreviations: courseConfig.abbreviations,
|
|
705
716
|
}),
|
|
717
|
+
...gateHeadingLength(notationInput.lessons),
|
|
706
718
|
];
|
|
707
719
|
|
|
708
720
|
// Pre-render every assessment `prompt` / `explanation` into text/math
|
|
@@ -1052,6 +1064,7 @@ export async function compileManifest(
|
|
|
1052
1064
|
return {
|
|
1053
1065
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
1054
1066
|
lessonExports: normalizeLessonExports(courseConfig),
|
|
1067
|
+
disclosureDefaults: normalizeDisclosureDefaults(courseConfig),
|
|
1055
1068
|
about,
|
|
1056
1069
|
notation: {
|
|
1057
1070
|
definitions: registry.definitions,
|
|
@@ -70,12 +70,6 @@ export interface Props {
|
|
|
70
70
|
* Set `false` only when horizontal overflow is deliberate. Default: `800`.
|
|
71
71
|
*/
|
|
72
72
|
warnAboveWidth?: number | false;
|
|
73
|
-
/**
|
|
74
|
-
* Prose text alternative for the SVG. When omitted, the fallback is
|
|
75
|
-
* generated from the node labels and the edge chain — readable, but a
|
|
76
|
-
* hand-written sentence usually reads better for a screen-reader user.
|
|
77
|
-
*/
|
|
78
|
-
description?: string;
|
|
79
73
|
}
|
|
80
74
|
|
|
81
75
|
const {
|
|
@@ -85,7 +79,6 @@ const {
|
|
|
85
79
|
edges,
|
|
86
80
|
scope = [],
|
|
87
81
|
direction = 'TB',
|
|
88
|
-
description,
|
|
89
82
|
number,
|
|
90
83
|
anchorId,
|
|
91
84
|
defaultOpen = true,
|
|
@@ -190,12 +183,6 @@ if (warnAboveWidth !== false && layout.width > warnAboveWidth) {
|
|
|
190
183
|
);
|
|
191
184
|
}
|
|
192
185
|
|
|
193
|
-
/** A node's human label for the text fallback — never the internal id. */
|
|
194
|
-
function fallbackLabel(nodeId: string): string {
|
|
195
|
-
const node = nodeById.get(nodeId);
|
|
196
|
-
return node?.text ?? node?.formula ?? nodeId;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
186
|
function pathFrom(points: readonly { x: number; y: number }[]): string {
|
|
200
187
|
if (points.length === 0) return '';
|
|
201
188
|
const [first, ...rest] = points;
|
|
@@ -318,33 +305,6 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
|
|
|
318
305
|
</div>
|
|
319
306
|
</div>
|
|
320
307
|
</div>
|
|
321
|
-
|
|
322
|
-
<details class="diagram-fallback">
|
|
323
|
-
<summary>Diagram description</summary>
|
|
324
|
-
{
|
|
325
|
-
description ? (
|
|
326
|
-
<p>{description}</p>
|
|
327
|
-
) : (
|
|
328
|
-
<Fragment>
|
|
329
|
-
<ol>
|
|
330
|
-
{rendered.map((node) => (
|
|
331
|
-
<li>{fallbackLabel(node.id)}</li>
|
|
332
|
-
))}
|
|
333
|
-
</ol>
|
|
334
|
-
{edges.length > 0 && (
|
|
335
|
-
<p class="diagram-fallback-flow">
|
|
336
|
-
{edges.map((edge, index) => (
|
|
337
|
-
<Fragment>
|
|
338
|
-
{index > 0 && '; '}
|
|
339
|
-
{fallbackLabel(edge.from)} → {fallbackLabel(edge.to)}
|
|
340
|
-
</Fragment>
|
|
341
|
-
))}
|
|
342
|
-
</p>
|
|
343
|
-
)}
|
|
344
|
-
</Fragment>
|
|
345
|
-
)
|
|
346
|
-
}
|
|
347
|
-
</details>
|
|
348
308
|
</div>
|
|
349
309
|
</details>
|
|
350
310
|
|
|
@@ -635,21 +595,6 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
|
|
|
635
595
|
fill: var(--sl-color-gray-4);
|
|
636
596
|
}
|
|
637
597
|
|
|
638
|
-
.diagram-fallback {
|
|
639
|
-
margin-block-start: 0.5rem;
|
|
640
|
-
color: var(--sl-color-gray-3);
|
|
641
|
-
font-size: var(--sl-text-sm);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
.diagram-fallback ol,
|
|
645
|
-
.diagram-fallback p {
|
|
646
|
-
margin: 0.35rem 0 0;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
.diagram-fallback ol {
|
|
650
|
-
padding-inline-start: 1.25rem;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
598
|
@media print {
|
|
654
599
|
.diagram-toolbar,
|
|
655
600
|
.diagram__title-anchor,
|