explico 0.8.0 → 0.11.1
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 +1257 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +46 -2
- package/dist/compiler/cli.js +70 -4
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +68 -5
- package/dist/compiler/course-config.d.ts +3 -194
- package/dist/compiler/course-config.js +78 -5
- package/dist/compiler/exports/standalone-html.d.ts +6 -1
- package/dist/compiler/knowledge-version.js +1 -1
- package/dist/compiler/lesson-derivation.d.ts +27 -10
- package/dist/compiler/lesson-derivation.js +23 -8
- package/dist/compiler/logging.d.ts +4 -1
- package/dist/compiler/logging.js +4 -1
- package/dist/compiler/manifest.d.ts +204 -31
- package/dist/compiler/manifest.js +270 -55
- package/dist/compiler/resolution-report.d.ts +6 -1
- package/dist/course-config-Dsnri9Tk.d.ts +464 -0
- package/dist/reference/base-accents.d.mts +26 -0
- package/dist/reference/base-accents.mjs +31 -0
- package/dist/reference/base-roman-labels.d.mts +2 -2
- package/dist/reference/base-roman-labels.mjs +2 -2
- package/dist/reference/consistency.d.ts +14 -2
- package/dist/reference/consistency.js +108 -4
- package/dist/reference/delimiter-size.d.ts +3 -2
- package/dist/reference/diagrams-validate.d.ts +7 -2
- package/dist/reference/diagrams-validate.js +9 -1
- package/dist/reference/equations-validate.d.ts +8 -5
- package/dist/reference/equations-validate.js +1 -0
- package/dist/reference/equations.d.mts +60 -31
- package/dist/reference/equations.mjs +83 -43
- package/dist/reference/examples-validate.d.ts +31 -0
- package/dist/reference/examples-validate.js +48 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/figures-validate.js +4 -1
- package/dist/reference/folds-validate.d.ts +33 -0
- package/dist/reference/folds-validate.js +32 -0
- package/dist/reference/gate-abbreviations.d.ts +3 -1
- package/dist/reference/gate-abbreviations.js +1 -0
- package/dist/reference/gate-course-rules.d.ts +32 -0
- package/dist/reference/gate-course-rules.js +85 -0
- package/dist/reference/gate-heading-length.d.ts +5 -2
- package/dist/reference/gate-heading-length.js +45 -21
- package/dist/reference/gate-heading-structure.d.ts +41 -0
- package/dist/reference/gate-heading-structure.js +113 -0
- package/dist/reference/gate-math-punctuation.d.ts +71 -0
- package/dist/reference/gate-math-punctuation.js +251 -0
- package/dist/reference/gate-math.d.ts +22 -6
- package/dist/reference/gate-math.js +42 -8
- package/dist/reference/gate-part-pages.d.ts +50 -0
- package/dist/reference/gate-part-pages.js +118 -0
- package/dist/reference/gate-prose-notation.d.ts +21 -11
- package/dist/reference/gate-prose-notation.js +25 -7
- package/dist/reference/katex-delimiters.mjs +20 -7
- package/dist/reference/keyed-float-validate.d.ts +27 -10
- package/dist/reference/keyed-float-validate.js +15 -1
- package/dist/reference/keyed-jsx-floats.d.mts +11 -5
- package/dist/reference/keyed-jsx-floats.mjs +73 -20
- package/dist/reference/knowledge-checks-validate.d.ts +1 -1
- package/dist/reference/knowledge-checks.d.mts +3 -1
- package/dist/reference/knowledge-checks.mjs +9 -4
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-slot.d.ts +34 -0
- package/dist/reference/lab-math-slot.js +6 -0
- package/dist/reference/math-glyphs.d.mts +80 -4
- package/dist/reference/math-glyphs.mjs +572 -37
- package/dist/reference/page-notation.d.ts +6 -1
- package/dist/reference/raw-floats-validate.js +1 -1
- package/dist/reference/raw-floats.mjs +1 -1
- package/dist/reference/rehype-heading-anchors.d.mts +14 -0
- package/dist/reference/rehype-heading-anchors.mjs +116 -17
- package/dist/reference/rehype-table-header-labels.d.mts +6 -1
- package/dist/reference/rehype-table-header-labels.mjs +21 -3
- package/dist/reference/remark-asides.d.mts +3 -0
- package/dist/reference/remark-asides.mjs +180 -0
- package/dist/reference/remark-memo.d.mts +3 -1
- package/dist/reference/remark-memo.mjs +42 -11
- package/dist/reference/remark-notation.d.mts +60 -10
- package/dist/reference/remark-notation.mjs +387 -36
- package/dist/reference/render-lab-math.d.ts +4 -26
- package/dist/reference/render-lab-math.js +4 -4
- package/dist/reference/sections-validate.d.ts +27 -0
- package/dist/reference/sections-validate.js +41 -0
- package/dist/reference/sections.d.mts +97 -0
- package/dist/reference/sections.mjs +306 -0
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +17 -1
- package/dist/reference/unit-words.d.ts +37 -0
- package/dist/reference/unit-words.js +170 -0
- package/dist/rules/catalog.d.ts +176 -0
- package/dist/rules/catalog.js +983 -0
- package/dist/rules/course-rules.d.ts +115 -0
- package/dist/rules/course-rules.js +292 -0
- package/dist/rules/course-rules.schema.json +138 -0
- package/dist/rules/in-effect.d.ts +53 -0
- package/dist/rules/in-effect.js +142 -0
- package/dist/rules/rules.schema.json +1072 -0
- package/dist/rules/settings.d.ts +3 -0
- package/dist/rules/settings.js +290 -0
- package/dist/rules/waivers.d.ts +53 -0
- package/dist/rules/waivers.js +132 -0
- package/dist/session/PreferenceStore.d.ts +5 -0
- package/dist/session/PreferenceStore.js +9 -0
- package/docs/VERSIONING.md +5 -3
- package/docs/authoring/README.md +232 -40
- package/docs/authoring/agents.md +55 -9
- package/docs/authoring/components.md +178 -22
- package/docs/authoring/migrating.md +1152 -2
- package/docs/authoring/notation.md +576 -322
- package/docs/authoring/prose.md +551 -0
- package/docs/authoring/retired.md +35 -0
- package/docs/authoring/structure-rules.md +422 -40
- package/docs/authoring/structure.md +445 -62
- package/docs/authoring/versioning.md +20 -28
- package/docs/authoring/visualizations.md +142 -20
- package/package.json +2 -2
- package/src/compiler/cli.ts +116 -6
- package/src/compiler/collections.ts +111 -7
- package/src/compiler/course-config.ts +273 -34
- package/src/compiler/knowledge-version.ts +1 -1
- package/src/compiler/lesson-derivation.ts +54 -15
- package/src/compiler/logging.ts +8 -2
- package/src/compiler/manifest.ts +573 -104
- package/src/components/BlockHeading.astro +113 -0
- package/src/components/CourseContents.astro +221 -0
- package/src/components/CurriculumMap.astro +5 -6
- package/src/components/FloatIndex.astro +200 -0
- package/src/components/HoverPanel.astro +431 -79
- package/src/components/PartBridge.astro +27 -0
- package/src/components/PartOverview.astro +401 -0
- package/src/components/PartScope.astro +27 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
- package/src/components/assessments/AssessmentRunner.tsx +8 -5
- package/src/components/assessments/AssessmentSet.astro +23 -26
- package/src/components/block-heading.ts +146 -0
- package/src/components/card-arrow.ts +243 -0
- package/src/components/card-drag.ts +631 -0
- package/src/components/card-jump.ts +45 -0
- package/src/components/card-manager.ts +1589 -0
- package/src/components/card-memory.ts +177 -0
- package/src/components/card-placement.ts +504 -0
- package/src/components/card-side.ts +336 -0
- package/src/components/card-travel.ts +197 -0
- package/src/components/citation/CitationLayer.astro +77 -76
- package/src/components/diagram/Diagram.astro +161 -316
- package/src/components/disclosure/course-disclosure.ts +24 -0
- package/src/components/disclosure/hover-preview.ts +122 -42
- package/src/components/disclosure/persisted-disclosure.ts +3 -3
- package/src/components/disclosure/print-as-shown.ts +252 -0
- package/src/components/examples/CompactExample.astro +3 -1
- package/src/components/examples/CompactExamples.astro +101 -94
- package/src/components/figure/Figure.astro +174 -274
- package/src/components/figure/FloatCaption.astro +87 -0
- package/src/components/figure/figure-state.ts +58 -0
- package/src/components/figure/figure-tools.ts +87 -0
- package/src/components/float-name.ts +101 -0
- package/src/components/fold/Fold.astro +125 -0
- package/src/components/fold/fold-id.ts +40 -0
- package/src/components/memo/Memo.astro +162 -231
- package/src/components/memo/ShortNote.astro +6 -6
- package/src/components/notation/NotationFormula.astro +5 -1
- package/src/components/notation/NotationGlossary.astro +3 -4
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +5 -4
- package/src/components/starlight/CollapsibleSections.astro +207 -93
- package/src/components/starlight/DiagramEnhancer.astro +15 -138
- package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
- package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
- package/src/components/starlight/LayoutHeader.astro +298 -19
- package/src/components/starlight/LayoutPageSidebar.astro +99 -12
- package/src/components/starlight/LayoutPageTitle.astro +120 -0
- package/src/components/starlight/LayoutSidebar.astro +229 -11
- package/src/components/starlight/LessonExportActions.astro +53 -72
- package/src/components/starlight/LessonFooter.astro +26 -4
- package/src/components/starlight/section-parts.ts +15 -2
- package/src/components/starlight/section-summary.ts +181 -0
- package/src/components/table/Table.astro +95 -76
- package/src/components/table/TableOverflowExpander.astro +65 -28
- package/src/content-config.ts +40 -4
- package/src/reference/base-accents.mjs +31 -0
- package/src/reference/base-roman-labels.mjs +2 -2
- package/src/reference/consistency.ts +176 -5
- package/src/reference/delimiter-size.ts +3 -2
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +83 -43
- package/src/reference/examples-validate.ts +97 -0
- package/src/reference/figures-validate.ts +6 -1
- package/src/reference/folds-validate.ts +65 -0
- package/src/reference/gate-abbreviations.ts +1 -0
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +66 -35
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +69 -10
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +59 -12
- package/src/reference/katex-delimiters.mjs +20 -7
- package/src/reference/keyed-float-validate.ts +45 -11
- package/src/reference/keyed-jsx-floats.mjs +73 -20
- package/src/reference/knowledge-checks-validate.ts +1 -1
- package/src/reference/knowledge-checks.mjs +9 -4
- package/src/reference/lab-math-slot.ts +36 -0
- package/src/reference/math-glyphs.mjs +572 -37
- package/src/reference/raw-floats-validate.ts +1 -1
- package/src/reference/raw-floats.mjs +1 -1
- package/src/reference/rehype-heading-anchors.mjs +116 -17
- package/src/reference/rehype-table-header-labels.mjs +21 -3
- package/src/reference/remark-asides.mjs +180 -0
- package/src/reference/remark-memo.mjs +42 -11
- package/src/reference/remark-notation.mjs +387 -36
- package/src/reference/render-lab-math.ts +16 -31
- package/src/reference/sections-validate.ts +81 -0
- package/src/reference/sections.mjs +306 -0
- package/src/reference/tables-validate.ts +2 -1
- package/src/reference/types.ts +17 -0
- package/src/reference/unit-words.ts +233 -0
- package/src/rules/catalog.ts +1298 -0
- package/src/rules/course-rules.ts +456 -0
- package/src/rules/in-effect.ts +235 -0
- package/src/rules/settings.ts +448 -0
- package/src/rules/waivers.ts +249 -0
- package/src/session/PreferenceStore.ts +15 -0
- package/src/styles/global.css +1467 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- package/src/components/popup-stack.ts +0 -345
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
`V` rules govern anything a lesson draws: diagrams, static charts, and hydrated
|
|
4
4
|
interactive figures. The engine assumes no single plotting approach — these
|
|
5
|
-
rules make the choice per figure predictable
|
|
6
|
-
and deterministic.
|
|
5
|
+
rules make the choice per figure predictable, keep every figure's math
|
|
6
|
+
gate-checked, and keep its drawing deterministic.
|
|
7
7
|
|
|
8
8
|
See [README.md](README.md) for what "strong recommendation", "Build-enforced",
|
|
9
9
|
and "Review-only" mean here, and how to amend a rule.
|
|
@@ -47,15 +47,21 @@ an independent diagram number, a `#dia-<id>` anchor, and references of the form
|
|
|
47
47
|
`[[dia-<id>]]` or `[[lesson/slug#dia-<id>]]`. Diagram numbers do not advance the
|
|
48
48
|
equation, table, or figure sequences.
|
|
49
49
|
|
|
50
|
+
A diagram is named like every float: a long `caption`, shown below the graph
|
|
51
|
+
in the same caption line a figure has (`Diagram 2.1.1`, a chain icon that
|
|
52
|
+
copies the diagram's link, the caption), and an optional `short` caption for
|
|
53
|
+
the `[[dia-…]]` preview card
|
|
54
|
+
([components.md § The caption pair](components.md#the-caption-pair)).
|
|
55
|
+
|
|
50
56
|
The component preserves the build-time canvas size and scrolls horizontally
|
|
51
|
-
when the work area is narrower.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
restores
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
when the work area is narrower. Then a widen button ↔ at the end of the
|
|
58
|
+
caption line — the boxed button a table's caption has — expands the original
|
|
59
|
+
diagram toward the full in-page region and shrinks it back; it is not modal,
|
|
60
|
+
follows the diagram's document position while the page scrolls, and adds
|
|
61
|
+
internal vertical scrolling when the viewport is too short. The button again,
|
|
62
|
+
or Escape, restores its original width. A diagram never folds by itself: it
|
|
63
|
+
is its graph and then its caption line, in every state. To let a reader fold
|
|
64
|
+
it away, put it in a collapsible section.
|
|
59
65
|
|
|
60
66
|
By default a laid-out width above `800px` emits a compile-time warning. Treat
|
|
61
67
|
that warning as a prompt to try
|
|
@@ -103,8 +109,8 @@ _Exceptions._ None — this rule _is_ the exception process.
|
|
|
103
109
|
|
|
104
110
|
## V3
|
|
105
111
|
|
|
106
|
-
**Interactive figures: a hydrated island
|
|
107
|
-
equations
|
|
112
|
+
**Interactive figures: a hydrated island and the `renderLabMath` seam for
|
|
113
|
+
equations.**
|
|
108
114
|
|
|
109
115
|
An interactive figure is a React island, hydrated `client:visible` (or
|
|
110
116
|
`client:idle`). It carries:
|
|
@@ -119,18 +125,60 @@ An interactive figure is a React island, hydrated `client:visible` (or
|
|
|
119
125
|
in this page's popup payload. See
|
|
120
126
|
`playground/content/labs/ProjectileRangeExplorer.tsx` +
|
|
121
127
|
`ProjectileRangeMath.astro`.
|
|
122
|
-
- **
|
|
123
|
-
|
|
124
|
-
- **A text alternative.** A `<details>` block with the model's assumptions and a
|
|
125
|
-
small table of representative values, so the figure is usable with no JS and
|
|
126
|
-
by a screen reader.
|
|
127
|
-
- **Labelled controls.** Every control is a labelled `<input>`; where precision
|
|
128
|
+
- **Labelled controls.** Every control is a labelled `<input>` — the visible
|
|
129
|
+
label is how any reader knows what a slider sets; where precision
|
|
128
130
|
matters, pair a `range` slider with a `number` field (see `RangeField` in
|
|
129
131
|
`ProjectileRangeExplorer.tsx`).
|
|
132
|
+
- **No title or caption of its own.** The island sits in a `<Figure>`, and the
|
|
133
|
+
figure's `caption` (with an optional `short`) names the whole lab. The lab
|
|
134
|
+
adds no heading, no inner `<figure>` / `<figcaption>`, and no
|
|
135
|
+
`aria-labelledby` pointing at one: one object has one name. A sentence
|
|
136
|
+
that describes the plot ("the dot marks the landing point") belongs in the
|
|
137
|
+
figure's caption.
|
|
138
|
+
- **Picture, then caption, then tools.** A lab is laid out as a LaTeX figure
|
|
139
|
+
with its controls after it (docs/plans/figures-and-short-captions.md,
|
|
140
|
+
decision 3). The plot goes in the `<Figure>`'s default slot — its
|
|
141
|
+
picture — and the controls in its `tools` slot: an instruction line, the
|
|
142
|
+
sliders and fields, the live equation. The two are separate islands that
|
|
143
|
+
share their values. The tools render after the caption line, in a tray of
|
|
144
|
+
their own, and are the only part that folds: the fold button ⌃ / ⌄ ends the
|
|
145
|
+
caption line, and folded tools leave a "Hidden here: 2 sliders, 1
|
|
146
|
+
equation" line that unfolds them. Whether they start open is the course's
|
|
147
|
+
`disclosure.figures`, or `defaultOpen` on the one figure; the reader's
|
|
148
|
+
choice is remembered per page. Print and export show the picture and the
|
|
149
|
+
caption; the tools never print.
|
|
150
|
+
- **A live end to the caption.** The caption ends with a clause stating the
|
|
151
|
+
case shown — "Shown for v₀ = 20 m/s and θ = 30°: range 35.3 m, time of
|
|
152
|
+
flight 2.04 s." The lab writes it with `setFigureState(element, runs)`
|
|
153
|
+
(`explico/components/figure/figure-state.ts`), `element` being any element
|
|
154
|
+
of its own inside the figure and `runs` plain text, `{ text, italic: true }`
|
|
155
|
+
or `{ sub }` — never HTML. The clause prints with the caption; a reader
|
|
156
|
+
of the long caption elsewhere (a preview card, a fold's bar) sees the
|
|
157
|
+
caption alone.
|
|
158
|
+
- **Which text goes where.** The caption names the figure and says what it
|
|
159
|
+
shows — its subject, its marks and symbols, the case shown — and may state
|
|
160
|
+
the one thing to notice. The argument drawn from the figure is lesson
|
|
161
|
+
text, after the figure. Instructions for the controls belong to the
|
|
162
|
+
controls: the tools' first line, not the caption.
|
|
163
|
+
- **A preview shows the plot.** A same-page `[[fig-…]]` reference to a lab
|
|
164
|
+
previews a still copy of its plot as drawn; the tools are never copied. A
|
|
165
|
+
plot not drawn yet — or one on another page, drawn only in the browser —
|
|
166
|
+
leaves the card to the caption and number, so a lab no longer needs
|
|
167
|
+
`previewable={false}`.
|
|
168
|
+
|
|
169
|
+
The figure itself never hides; a lab that should fold away whole, with the
|
|
170
|
+
prose around it, goes in a `<Fold>` or a collapsible section.
|
|
171
|
+
|
|
172
|
+
The lab writes no accessible name, description or text alternative: the
|
|
173
|
+
`<Figure>` renders a real `<figure>` and `<figcaption>`, so its caption is
|
|
174
|
+
already the figure's accessible name
|
|
175
|
+
([Accessibility](README.md#accessibility)). A plot option such as
|
|
176
|
+
`ariaLabel` / `ariaDescription` / `ariaHidden` may stay where it is written;
|
|
177
|
+
nothing asks for one.
|
|
130
178
|
|
|
131
179
|
_Rationale._ The seam is the only reason interactive math is not a silent
|
|
132
|
-
exception to the completeness gate.
|
|
133
|
-
|
|
180
|
+
exception to the completeness gate. One name per object means the reader, a
|
|
181
|
+
`[[fig-…]]` reference and a screen reader all call the lab the same thing.
|
|
134
182
|
|
|
135
183
|
_How to apply._ Keep the island's job to "patch numbers and redraw". Domain
|
|
136
184
|
math lives in a plain module (`playground/content/domain/motion.ts`), not in the
|
|
@@ -226,3 +274,77 @@ _Enforcement._ Review-only.
|
|
|
226
274
|
_Exceptions._ A figure where the meaningful range is a known physical bound
|
|
227
275
|
(a probability on `[0, 1]`, an angle on `[0°, 360°]`) uses that bound instead —
|
|
228
276
|
state it in a comment.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## V6
|
|
281
|
+
|
|
282
|
+
**Every quantitative axis of a chart shows at least three labelled values.**
|
|
283
|
+
|
|
284
|
+
A chart that draws an x axis and a y axis — a hand-drawn SVG, an Observable
|
|
285
|
+
Plot, an interactive island, a printed pgfplots figure — labels at least three
|
|
286
|
+
tick values on each quantitative axis, as numbers at their positions. An axis
|
|
287
|
+
_name_ (`feature x`, `density`) does not count, and neither does an annotation
|
|
288
|
+
of one special point (`\mu`, `\bar{x}`, a threshold).
|
|
289
|
+
|
|
290
|
+
_Rationale._ Values are what make a chart readable rather than decorative.
|
|
291
|
+
One value pins a single point. Two values give a scale only if the reader
|
|
292
|
+
already knows the axis is linear, and two labels on a log axis look exactly
|
|
293
|
+
like two on a linear one. Three values show where the axis starts, how big a
|
|
294
|
+
unit is, and whether the spacing is linear or logarithmic. Only then can a
|
|
295
|
+
reader read a value off the curve, compare two panels, or check the figure
|
|
296
|
+
against a number in the prose beside it.
|
|
297
|
+
|
|
298
|
+
_How to apply._
|
|
299
|
+
|
|
300
|
+
- Use round values: steps of 1, 2, 2.5, or 5 times a power of ten, inside the
|
|
301
|
+
axis domain. Let the library choose them where it can. Observable Plot's
|
|
302
|
+
automatic ticks satisfy the rule at any ordinary chart size; `ticks: 5`
|
|
303
|
+
pins a count. In pgfplots the automatic ticks satisfy it; never set
|
|
304
|
+
`xtick=\empty` or `ytick=\empty` on a quantitative axis.
|
|
305
|
+
- A hand-drawn SVG computes its values from the domain and falls back to a
|
|
306
|
+
finer step until at least three fit:
|
|
307
|
+
|
|
308
|
+
```ts
|
|
309
|
+
const NICE_STEPS = [1, 2, 2.5, 5];
|
|
310
|
+
|
|
311
|
+
function axisTicks([lo, hi]: readonly [number, number], target = 5): number[] {
|
|
312
|
+
let exponent = Math.floor(Math.log10((hi - lo) / target));
|
|
313
|
+
let index = NICE_STEPS.findIndex((n) => n * 10 ** exponent >= (hi - lo) / target);
|
|
314
|
+
if (index < 0) [index, exponent] = [0, exponent + 1];
|
|
315
|
+
for (;;) {
|
|
316
|
+
const step = NICE_STEPS[index] * 10 ** exponent;
|
|
317
|
+
const values: number[] = [];
|
|
318
|
+
for (let v = Math.ceil(lo / step - 1e-9) * step; v <= hi + step * 1e-9; v += step) {
|
|
319
|
+
values.push(Number(v.toFixed(10)));
|
|
320
|
+
}
|
|
321
|
+
if (values.length >= 3) return values;
|
|
322
|
+
if (--index < 0) [index, exponent] = [NICE_STEPS.length - 1, exponent - 1];
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Draw a short tick mark at each value and the number beside it.
|
|
328
|
+
- The values follow the domain. When a control changes an axis range
|
|
329
|
+
([V5](#v5)), recompute the values from the new range in the same step.
|
|
330
|
+
- A density or likelihood axis gets values too. "The vertical scale does not
|
|
331
|
+
matter here" is a reason to say so in the caption, not to hide the scale.
|
|
332
|
+
- A log axis labels its decades (`10`, `100`, `1000`).
|
|
333
|
+
- Special-point annotations stay, next to the values rather than instead of
|
|
334
|
+
them.
|
|
335
|
+
- Stacked panels that share an axis, aligned and said to be shared, may
|
|
336
|
+
label it once, on the panel nearest the axis. Every other axis of every
|
|
337
|
+
panel carries its own values.
|
|
338
|
+
|
|
339
|
+
_Enforcement._ Review-only. The engine cannot tell a chart from any other
|
|
340
|
+
drawing. A course can make it build-enforced in its own browser tests by
|
|
341
|
+
marking each axis's value group and counting the values in it.
|
|
342
|
+
|
|
343
|
+
_Exceptions._
|
|
344
|
+
|
|
345
|
+
- A figure without axes has nothing to label: a node-edge diagram, a
|
|
346
|
+
bullseye target, an illustration.
|
|
347
|
+
- A categorical axis shows its categories instead of numbers, even if there
|
|
348
|
+
are only two.
|
|
349
|
+
- A sketch that draws axes is a chart. If the axes are worth drawing, their
|
|
350
|
+
scale is worth labelling.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explico",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
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",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"vitest": "4.1.11"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
|
-
"build": "tsup && node scripts/copy-assets.mjs && node scripts/fix-esm-extensions.mjs && tsc -p tsconfig.dts-mjs.json",
|
|
67
|
+
"build": "tsup && node scripts/copy-assets.mjs && node scripts/fix-esm-extensions.mjs && tsc -p tsconfig.dts-mjs.json && node scripts/gen-rules-schema.mjs",
|
|
68
68
|
"check:release-migration": "node scripts/check-release-migration.mjs",
|
|
69
69
|
"test": "vitest run",
|
|
70
70
|
"test:watch": "vitest"
|
package/src/compiler/cli.ts
CHANGED
|
@@ -52,6 +52,13 @@ import type { DiagnosticSeverity } from '../reference/types';
|
|
|
52
52
|
import type { EquationLink } from '../reference/gate-math';
|
|
53
53
|
import type { Manifest, ManifestLesson } from './manifest';
|
|
54
54
|
import { isLessonSlug, lessonIdFromSlug } from './lesson-derivation';
|
|
55
|
+
import { isCourseRuleId, ownerOf, ELEMENTS, type Element, type Enforcement } from '../rules/catalog';
|
|
56
|
+
import {
|
|
57
|
+
renderRulesInEffect,
|
|
58
|
+
rulesInEffect,
|
|
59
|
+
type RuleInEffect,
|
|
60
|
+
type RulesFilter,
|
|
61
|
+
} from '../rules/in-effect';
|
|
55
62
|
|
|
56
63
|
const REPOSITORY_ROOT = process.cwd();
|
|
57
64
|
// Content and its course math are the top-level `content/` folder (Phase F).
|
|
@@ -86,6 +93,12 @@ export class ContentCliError extends Error {
|
|
|
86
93
|
/** One normalized diagnostic. `code` is `<source>:<kind>` and always stable. */
|
|
87
94
|
export interface CliDiagnostic {
|
|
88
95
|
readonly code: string;
|
|
96
|
+
/**
|
|
97
|
+
* The catalog entry that owns the code — a rule ID (`N9`) or an invariant
|
|
98
|
+
* slug (`keyed-floats`). Absent only for a code the catalog does not know,
|
|
99
|
+
* which `tests/rules/catalog.test.ts` treats as a bug.
|
|
100
|
+
*/
|
|
101
|
+
readonly rule?: string;
|
|
89
102
|
readonly severity: DiagnosticSeverity;
|
|
90
103
|
readonly message: string;
|
|
91
104
|
readonly file?: string;
|
|
@@ -104,6 +117,21 @@ function diagnosticSeverity(value: unknown): DiagnosticSeverity {
|
|
|
104
117
|
return value === 'info' || value === 'warning' ? value : 'error';
|
|
105
118
|
}
|
|
106
119
|
|
|
120
|
+
/**
|
|
121
|
+
* `{ rule }` for a `<source>:<code>` (or bare) code the catalog owns — or for a
|
|
122
|
+
* course rule, whose code is its own ID (`X1`).
|
|
123
|
+
*/
|
|
124
|
+
function ownerFields(code: string): { readonly rule?: string } {
|
|
125
|
+
const bare = code.slice(code.indexOf(':') + 1);
|
|
126
|
+
const owner = ownerOf(bare);
|
|
127
|
+
if (owner !== undefined) return { rule: owner.id };
|
|
128
|
+
return isCourseRuleId(bare) ? { rule: bare } : {};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function withOwner(diagnostic: CliDiagnostic): CliDiagnostic {
|
|
132
|
+
return { ...diagnostic, ...ownerFields(diagnostic.code) };
|
|
133
|
+
}
|
|
134
|
+
|
|
107
135
|
function mentions(message: string, lessonId: string, file: string): boolean {
|
|
108
136
|
return (
|
|
109
137
|
message.includes(lessonId) ||
|
|
@@ -167,7 +195,7 @@ export function lessonDiagnostics(
|
|
|
167
195
|
));
|
|
168
196
|
if (!forLesson) continue;
|
|
169
197
|
out.push({
|
|
170
|
-
code: `math:${diagnostic.code
|
|
198
|
+
code: `math:${diagnostic.code}`,
|
|
171
199
|
severity: diagnostic.severity,
|
|
172
200
|
message: diagnostic.message,
|
|
173
201
|
file: diagnostic.file,
|
|
@@ -200,6 +228,8 @@ export function lessonDiagnostics(
|
|
|
200
228
|
...manifest.diagnostics.figures,
|
|
201
229
|
...manifest.diagnostics.diagrams,
|
|
202
230
|
...manifest.diagnostics.checks,
|
|
231
|
+
...manifest.diagnostics.examples,
|
|
232
|
+
...manifest.diagnostics.folds,
|
|
203
233
|
...manifest.diagnostics.rawFloats,
|
|
204
234
|
]) {
|
|
205
235
|
if (
|
|
@@ -227,10 +257,12 @@ export function lessonDiagnostics(
|
|
|
227
257
|
});
|
|
228
258
|
}
|
|
229
259
|
|
|
230
|
-
return out
|
|
231
|
-
(
|
|
232
|
-
|
|
233
|
-
|
|
260
|
+
return out
|
|
261
|
+
.map(withOwner)
|
|
262
|
+
.sort(
|
|
263
|
+
(a, b) =>
|
|
264
|
+
a.code.localeCompare(b.code) || a.message.localeCompare(b.message),
|
|
265
|
+
);
|
|
234
266
|
}
|
|
235
267
|
|
|
236
268
|
// --- renderability -------------------------------------------------------
|
|
@@ -512,6 +544,7 @@ export interface StatusReport {
|
|
|
512
544
|
readonly keyedTables: number;
|
|
513
545
|
readonly keyedFigures: number;
|
|
514
546
|
readonly keyedDiagrams: number;
|
|
547
|
+
readonly keyedExamples: number;
|
|
515
548
|
};
|
|
516
549
|
readonly draftDebt: {
|
|
517
550
|
readonly lessons: readonly string[];
|
|
@@ -539,6 +572,7 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
539
572
|
.filter((diagnostic) => diagnostic.code === 'unused-definition')
|
|
540
573
|
.map((diagnostic) => ({
|
|
541
574
|
code: `notation:${diagnostic.code}`,
|
|
575
|
+
...ownerFields(diagnostic.code),
|
|
542
576
|
severity: diagnosticSeverity(diagnostic.severity),
|
|
543
577
|
message: diagnostic.message,
|
|
544
578
|
...(diagnostic.source?.file === undefined
|
|
@@ -557,9 +591,13 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
557
591
|
...manifest.diagnostics.figures.map((d) => d.severity),
|
|
558
592
|
...manifest.diagnostics.diagrams.map((d) => d.severity),
|
|
559
593
|
...manifest.diagnostics.checks.map((d) => d.severity),
|
|
594
|
+
...manifest.diagnostics.examples.map((d) => d.severity),
|
|
595
|
+
...manifest.diagnostics.folds.map((d) => d.severity),
|
|
560
596
|
...manifest.diagnostics.rawFloats.map((d) => d.severity),
|
|
561
597
|
...manifest.diagnostics.consistency.map((d) => d.severity),
|
|
562
598
|
...manifest.diagnostics.knowledge.map((d) => d.severity),
|
|
599
|
+
...manifest.diagnostics.config.map((d) => d.severity),
|
|
600
|
+
...manifest.diagnostics.waivers.map((d) => d.severity),
|
|
563
601
|
];
|
|
564
602
|
|
|
565
603
|
const keyedEquations = manifest.lessons.reduce(
|
|
@@ -578,6 +616,10 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
578
616
|
(count, lesson) => count + lesson.diagrams.length,
|
|
579
617
|
0,
|
|
580
618
|
);
|
|
619
|
+
const keyedExamples = manifest.lessons.reduce(
|
|
620
|
+
(count, lesson) => count + lesson.examples.length,
|
|
621
|
+
0,
|
|
622
|
+
);
|
|
581
623
|
const assessments = new Set(
|
|
582
624
|
manifest.lessons.flatMap((lesson) => lesson.assessments),
|
|
583
625
|
).size;
|
|
@@ -595,6 +637,7 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
595
637
|
keyedTables,
|
|
596
638
|
keyedFigures,
|
|
597
639
|
keyedDiagrams,
|
|
640
|
+
keyedExamples,
|
|
598
641
|
},
|
|
599
642
|
draftDebt: { lessons: draftLessons, notation: draftNotation },
|
|
600
643
|
orphans,
|
|
@@ -709,6 +752,7 @@ export function contentDiagnostics(
|
|
|
709
752
|
const equation = equationLink(diagnostic.equation);
|
|
710
753
|
diagnostics.push({
|
|
711
754
|
code: `${group}:${detail}`,
|
|
755
|
+
...ownerFields(detail),
|
|
712
756
|
severity,
|
|
713
757
|
message: String(diagnostic.message ?? detail),
|
|
714
758
|
...(file === undefined ? {} : { file }),
|
|
@@ -742,6 +786,61 @@ export function contentWarnings(manifest: Manifest): WarningsReport {
|
|
|
742
786
|
return { count: warnings.length, warnings };
|
|
743
787
|
}
|
|
744
788
|
|
|
789
|
+
// --- content rules -------------------------------------------------------
|
|
790
|
+
|
|
791
|
+
export interface RulesReport {
|
|
792
|
+
readonly filter: RulesFilter;
|
|
793
|
+
readonly count: number;
|
|
794
|
+
readonly rules: readonly RuleInEffect[];
|
|
795
|
+
/** The same selection as the page `validate:content` writes. */
|
|
796
|
+
readonly markdown: string;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Parse `--element a,b` / `--enforcement script` into a filter, throwing a
|
|
801
|
+
* usage error on a value outside the vocabulary.
|
|
802
|
+
*/
|
|
803
|
+
export function parseRulesFilter(input: {
|
|
804
|
+
readonly element?: string;
|
|
805
|
+
readonly enforcement?: string;
|
|
806
|
+
}): RulesFilter {
|
|
807
|
+
const filter: { elements?: Element[]; enforcement?: Enforcement | 'locked' } = {};
|
|
808
|
+
if (input.element !== undefined) {
|
|
809
|
+
const elements = input.element.split(',').map((e) => e.trim()).filter(Boolean);
|
|
810
|
+
for (const element of elements) {
|
|
811
|
+
if (!(ELEMENTS as readonly string[]).includes(element)) {
|
|
812
|
+
throw new ContentCliError(
|
|
813
|
+
'usage',
|
|
814
|
+
`--element ${element}: not an element. One of: ${ELEMENTS.join(', ')}.`,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
filter.elements = elements as Element[];
|
|
819
|
+
}
|
|
820
|
+
if (input.enforcement !== undefined) {
|
|
821
|
+
const kinds = ['script', 'partial', 'guide', 'locked'];
|
|
822
|
+
if (!kinds.includes(input.enforcement)) {
|
|
823
|
+
throw new ContentCliError(
|
|
824
|
+
'usage',
|
|
825
|
+
`--enforcement ${input.enforcement}: one of ${kinds.join(', ')}.`,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
filter.enforcement = input.enforcement as Enforcement | 'locked';
|
|
829
|
+
}
|
|
830
|
+
return filter;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/** The rules this course has in effect, optionally narrowed. */
|
|
834
|
+
export function contentRules(manifest: Manifest, filter: RulesFilter = {}): RulesReport {
|
|
835
|
+
const rules = rulesInEffect(manifest, filter);
|
|
836
|
+
return {
|
|
837
|
+
filter,
|
|
838
|
+
count: rules.length,
|
|
839
|
+
rules,
|
|
840
|
+
markdown: renderRulesInEffect(manifest, filter),
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
745
844
|
// --- content context ---------------------------------------------------
|
|
746
845
|
|
|
747
846
|
export interface ContextReport {
|
|
@@ -772,7 +871,16 @@ export interface ContextReport {
|
|
|
772
871
|
readonly tables: readonly { key: string; number: string }[];
|
|
773
872
|
readonly figures: readonly { key: string; number: string }[];
|
|
774
873
|
readonly diagrams: readonly { key: string; number: string }[];
|
|
775
|
-
|
|
874
|
+
/** Worked-example sets: `short` is the title `[[ex-key]]` names. */
|
|
875
|
+
readonly examples: readonly { key: string; number: string; short?: string }[];
|
|
876
|
+
readonly knowledgeChecks: readonly { key: string; number: string; title?: string }[];
|
|
877
|
+
/** The top-level headings: `slug` is what `[[sec-slug]]` names. */
|
|
878
|
+
readonly sections: readonly {
|
|
879
|
+
slug: string;
|
|
880
|
+
text: string;
|
|
881
|
+
level: number;
|
|
882
|
+
number?: string;
|
|
883
|
+
}[];
|
|
776
884
|
readonly sources: readonly {
|
|
777
885
|
id: string;
|
|
778
886
|
title: string;
|
|
@@ -844,7 +952,9 @@ export function contentContext(
|
|
|
844
952
|
tables: lesson.tables.map((table) => ({ ...table })),
|
|
845
953
|
figures: lesson.figures.map((figure) => ({ ...figure })),
|
|
846
954
|
diagrams: lesson.diagrams.map((diagram) => ({ ...diagram })),
|
|
955
|
+
examples: lesson.examples.map((example) => ({ ...example })),
|
|
847
956
|
knowledgeChecks: lesson.knowledgeChecks.map((check) => ({ ...check })),
|
|
957
|
+
sections: lesson.headings.map((heading) => ({ ...heading })),
|
|
848
958
|
sources: lesson.sources.map((id) => {
|
|
849
959
|
const record = sourceById.get(id) as Record<string, unknown> | undefined;
|
|
850
960
|
return {
|
|
@@ -31,15 +31,21 @@ import type {
|
|
|
31
31
|
NotationLessonInput,
|
|
32
32
|
NotationReferenceInput,
|
|
33
33
|
NotationRegistryInput,
|
|
34
|
+
NotationSuspension,
|
|
34
35
|
SharedNotationDefinitionInput,
|
|
35
36
|
SourceSpan,
|
|
36
37
|
NotationGlossEntry,
|
|
37
38
|
} from '../reference/types';
|
|
38
39
|
import { isSubstantiveMeaning } from '../reference/prose';
|
|
39
40
|
import { parseEquationRef } from '../reference/equations.mjs';
|
|
41
|
+
import {
|
|
42
|
+
DECORATION_NAMES,
|
|
43
|
+
decorationOfEntry,
|
|
44
|
+
} from '../reference/math-glyphs.mjs';
|
|
40
45
|
import { parseKeyedFloatRef } from '../reference/keyed-jsx-floats.mjs';
|
|
41
46
|
import { parseKnowledgeCheckRef } from '../reference/knowledge-checks.mjs';
|
|
42
47
|
import { parseMemoRef } from '../reference/remark-memo.mjs';
|
|
48
|
+
import { parseSectionRef } from '../reference/sections.mjs';
|
|
43
49
|
import { resolveLabel } from '../reference/label';
|
|
44
50
|
import { resolveGlosses } from '../reference/gloss';
|
|
45
51
|
import {
|
|
@@ -263,16 +269,19 @@ export function extractNotationReferences(
|
|
|
263
269
|
for (const match of searchable.matchAll(pattern)) {
|
|
264
270
|
const key = (match[1] ?? '').trim();
|
|
265
271
|
// `[[eq-key]]` / `[[slug#eq-key]]` are equation references (D7), and
|
|
266
|
-
// `[[tab-key]]`/`[[fig-key]]`/`[[dia-key]]` (and their
|
|
267
|
-
// are keyed-float references, `[[check-key]]`
|
|
268
|
-
// assessment list,
|
|
269
|
-
//
|
|
272
|
+
// `[[tab-key]]`/`[[fig-key]]`/`[[dia-key]]`/`[[ex-key]]` (and their
|
|
273
|
+
// cross-page forms) are keyed-float references, `[[check-key]]`
|
|
274
|
+
// targets a lesson's assessment list, `[[sec-slug]]` a heading, and
|
|
275
|
+
// `[[memo-key]]` is a page-local memo reference — all resolved by the
|
|
276
|
+
// remark pass and the manifest, not notation keys.
|
|
270
277
|
if (
|
|
271
278
|
parseEquationRef(key) ||
|
|
272
279
|
parseKeyedFloatRef(key, { prefix: 'tab' }) ||
|
|
273
280
|
parseKeyedFloatRef(key, { prefix: 'fig' }) ||
|
|
274
281
|
parseKeyedFloatRef(key, { prefix: 'dia' }) ||
|
|
282
|
+
parseKeyedFloatRef(key, { prefix: 'ex' }) ||
|
|
275
283
|
parseKnowledgeCheckRef(key) ||
|
|
284
|
+
parseSectionRef(key) ||
|
|
276
285
|
parseMemoRef(key)
|
|
277
286
|
) {
|
|
278
287
|
continue;
|
|
@@ -323,6 +332,57 @@ function lessonCheckIds(lessonPath: string): string[] {
|
|
|
323
332
|
: [];
|
|
324
333
|
}
|
|
325
334
|
|
|
335
|
+
/**
|
|
336
|
+
* One part's **overview page** — `content/docs/<dir>/index.mdx`, the page the
|
|
337
|
+
* part's own directory URL resolves to. `isLessonSlug` already excludes any
|
|
338
|
+
* `<dir>/index`, so this page is not a chapter: it carries no competencies, no
|
|
339
|
+
* knowledge check, and no float numbers, and nothing in the curriculum graph
|
|
340
|
+
* changes when a course adds one.
|
|
341
|
+
*/
|
|
342
|
+
export interface PartOverviewPage {
|
|
343
|
+
readonly dir: string;
|
|
344
|
+
readonly slug: string;
|
|
345
|
+
readonly file: string;
|
|
346
|
+
readonly title: string;
|
|
347
|
+
readonly description?: string;
|
|
348
|
+
/** MDX after the frontmatter — what the S2 gate (`gate-part-pages.ts`) reads. */
|
|
349
|
+
readonly body: string;
|
|
350
|
+
/** 1-based file line `body` starts on, for diagnostic locations. */
|
|
351
|
+
readonly bodyLine: number;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The overview page of every part that has one, keyed by the part's directory.
|
|
356
|
+
* A part without one is simply absent — overview pages are opt-in, one part at
|
|
357
|
+
* a time.
|
|
358
|
+
*/
|
|
359
|
+
export function loadPartOverviews(
|
|
360
|
+
parts: readonly { readonly dir: string }[],
|
|
361
|
+
): PartOverviewPage[] {
|
|
362
|
+
const pages: PartOverviewPage[] = [];
|
|
363
|
+
for (const { dir } of parts) {
|
|
364
|
+
for (const extension of ['.mdx', '.md']) {
|
|
365
|
+
const path = join(CONTENT_ROOT, 'docs', dir, `index${extension}`);
|
|
366
|
+
if (!existsSync(path)) continue;
|
|
367
|
+
const { data, file, body, bodyLine } = parseDocument(path);
|
|
368
|
+
const title = typeof data.title === 'string' ? data.title : dir;
|
|
369
|
+
const description =
|
|
370
|
+
typeof data.description === 'string' ? data.description : undefined;
|
|
371
|
+
pages.push({
|
|
372
|
+
dir,
|
|
373
|
+
slug: `${dir}/index`,
|
|
374
|
+
file,
|
|
375
|
+
title,
|
|
376
|
+
...(description === undefined ? {} : { description }),
|
|
377
|
+
body,
|
|
378
|
+
bodyLine,
|
|
379
|
+
});
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return pages;
|
|
384
|
+
}
|
|
385
|
+
|
|
326
386
|
function lessonEntries(
|
|
327
387
|
prerequisitesById: ReadonlyMap<string, readonly string[]>,
|
|
328
388
|
): LessonDefinition[] {
|
|
@@ -508,6 +568,48 @@ export async function loadAbbreviationRegistryInput(): Promise<
|
|
|
508
568
|
return abbreviationEntries();
|
|
509
569
|
}
|
|
510
570
|
|
|
571
|
+
/**
|
|
572
|
+
* A lesson's `notation.suspend` (rule N22): decorations that are not
|
|
573
|
+
* operators on this page. A name outside `DECORATION_NAMES`, a decoration
|
|
574
|
+
* suspended twice, and a `notation.local` operator entry for a suspended
|
|
575
|
+
* decoration (the page would contradict itself) each fail the load.
|
|
576
|
+
*/
|
|
577
|
+
export function notationSuspensions(
|
|
578
|
+
raw: unknown,
|
|
579
|
+
localDefinitions: readonly LocalNotationDefinitionInput[],
|
|
580
|
+
file: string,
|
|
581
|
+
): NotationSuspension[] {
|
|
582
|
+
if (raw === undefined) return [];
|
|
583
|
+
if (!Array.isArray(raw)) {
|
|
584
|
+
throw new TypeError(`${file} notation.suspend must be an array`);
|
|
585
|
+
}
|
|
586
|
+
const seen = new Set<string>();
|
|
587
|
+
return raw.map((item, index) => {
|
|
588
|
+
const at = `${file} notation.suspend[${index}]`;
|
|
589
|
+
const data = record(item, at);
|
|
590
|
+
const operator = string(data.operator, `${at}.operator`).trim();
|
|
591
|
+
if (!DECORATION_NAMES.has(operator)) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`${at}.operator ${JSON.stringify(operator)} is not a decoration; use one of ${[...DECORATION_NAMES].join(', ')}`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
if (seen.has(operator)) {
|
|
597
|
+
throw new Error(`${at} suspends ${JSON.stringify(operator)} a second time`);
|
|
598
|
+
}
|
|
599
|
+
seen.add(operator);
|
|
600
|
+
const local = localDefinitions.find(
|
|
601
|
+
(definition) => decorationOfEntry(definition.notation) === operator,
|
|
602
|
+
);
|
|
603
|
+
if (local) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
`${at} suspends ${JSON.stringify(operator)}, but notation.local entry ${local.key} explains it as an operator (latex ${JSON.stringify(local.notation)}); drop one of them`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const reason = optionalString(data.reason, `${at}.reason`);
|
|
609
|
+
return reason === undefined ? { operator } : { operator, reason: reason.trim() };
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
511
613
|
function notationLessonEntries(): NotationLessonInput[] {
|
|
512
614
|
const lessons: NotationLessonInput[] = [];
|
|
513
615
|
for (const path of filesBelow(join(CONTENT_ROOT, 'docs'), ['.md', '.mdx'])) {
|
|
@@ -516,12 +618,14 @@ function notationLessonEntries(): NotationLessonInput[] {
|
|
|
516
618
|
const { file, data, body, bodyLine } = parseDocument(path);
|
|
517
619
|
const notation = record(data.notation ?? {}, `${file} notation`);
|
|
518
620
|
const local = Array.isArray(notation.local) ? notation.local : [];
|
|
621
|
+
const localDefinitions = local.map((entry, index) =>
|
|
622
|
+
localDefinition(entry, file, index),
|
|
623
|
+
);
|
|
519
624
|
lessons.push({
|
|
520
625
|
lessonId: lessonIdFromSlug(slug),
|
|
521
626
|
status: status(data.editorialStatus, `${file} editorialStatus`),
|
|
522
|
-
localDefinitions
|
|
523
|
-
|
|
524
|
-
),
|
|
627
|
+
localDefinitions,
|
|
628
|
+
suspended: notationSuspensions(notation.suspend, localDefinitions, file),
|
|
525
629
|
references: extractNotationReferences(body, file, undefined),
|
|
526
630
|
body,
|
|
527
631
|
bodyLine,
|