explico 0.7.0 → 0.11.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 +1409 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +54 -3
- package/dist/compiler/cli.js +80 -6
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +84 -8
- 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 +14 -1
- package/dist/compiler/logging.js +6 -2
- 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 +21 -11
- package/dist/reference/delimiter-size.js +15 -39
- 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 +69 -34
- package/dist/reference/equations.mjs +90 -44
- 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 +3 -2
- 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 +47 -23
- 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 +44 -6
- package/dist/reference/gate-math.js +63 -10
- 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 +32 -11
- package/dist/reference/gate-prose-notation.js +30 -8
- package/dist/reference/katex-delimiters.d.mts +15 -0
- package/dist/reference/katex-delimiters.mjs +138 -0
- package/dist/reference/katex-options.mjs +4 -0
- 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 +588 -39
- 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 +5 -5
- 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 +24 -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 +2 -2
- 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 +1324 -2
- package/docs/authoring/notation.md +616 -344
- 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/docs/katex-bracket-scaling.md +191 -0
- package/package.json +3 -2
- package/src/compiler/cli.ts +139 -6
- package/src/compiler/collections.ts +144 -10
- 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 +21 -4
- 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 +24 -14
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/notation/render-notation-math.ts +111 -10
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +22 -6
- 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 +40 -61
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +90 -44
- 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 +3 -2
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +68 -37
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +119 -12
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +74 -13
- package/src/reference/katex-delimiters.mjs +138 -0
- package/src/reference/katex-options.mjs +4 -0
- 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 +588 -39
- 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 +17 -32
- 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 +24 -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 +1471 -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.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# Auto-scaling brackets in KaTeX (and MathJax)
|
|
2
|
+
|
|
3
|
+
> **Status in this repo.** Solution 2 is implemented, but as *code* rather than
|
|
4
|
+
> as a `patches/` file: [`src/reference/katex-delimiters.mjs`](../src/reference/katex-delimiters.mjs)
|
|
5
|
+
> applies the same one-constant change at import time, so it ships inside the
|
|
6
|
+
> published `explico` package and a downstream course inherits it with no
|
|
7
|
+
> install step. Rule [N17](authoring/notation.md#n17) depends on it. The
|
|
8
|
+
> sections below are the derivation; see
|
|
9
|
+
> [How this repo applies it](#how-this-repo-applies-it).
|
|
10
|
+
|
|
11
|
+
## The problem
|
|
12
|
+
|
|
13
|
+
In LaTeX, `\left[ ... \right]` sizes a bracket to fit its content — but nested brackets often come out **the same size**, because the sizing rule only requires the delimiter to be *close enough* to the content's height, not strictly bigger.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
|
|
17
|
+
```latex
|
|
18
|
+
$$\left[ \left[ \sqrt{\frac{\int\limits_0^{10}\sqrt{a^2+b^2}}{2}} \right] + 1 \right]$$
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The outer `[` and inner `[` render at the same size, even though the outer one wraps strictly more content. The alternative — manually choosing `\bigl`, `\Bigl`, `\biggl`, `\Biggl` — fixes the sizing but abandons `\left`/`\right`'s automatic "match the height of everything inside" behavior, since those are fixed-size delimiters that don't adapt to content.
|
|
22
|
+
|
|
23
|
+
So there are two properties you want simultaneously, and the two standard tools each give you only one:
|
|
24
|
+
|
|
25
|
+
1. **Strict growth outward** — each nesting level's bracket must be visibly bigger than the one inside it.
|
|
26
|
+
2. **Full coverage** — every bracket must span the full height of its content, with no gap.
|
|
27
|
+
|
|
28
|
+
`\bigl`/`\Bigl`/`\biggl`/`\Biggl` give you (1) but not (2) (fixed sizes don't track arbitrary content). `\left`/`\right` give you (2) but not (1) (the sizing rule allows equal-looking sizes at different nesting depths).
|
|
29
|
+
|
|
30
|
+
## Why it happens: the underlying rule
|
|
31
|
+
|
|
32
|
+
TeX sizes a `\left`/`\right` pair using this rule (TeXbook, Appendix G, rule 19):
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
s_min = max( 2·δ₀ · f / 1000 , 2·δ₀ − l )
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
TeX then picks the smallest available delimiter glyph whose total size `s` satisfies `s ≥ s_min`.
|
|
39
|
+
|
|
40
|
+
| Symbol | Meaning |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `h` | height of the content: how far it reaches above the baseline |
|
|
43
|
+
| `d` | depth of the content: how far it reaches below the baseline |
|
|
44
|
+
| `a` | axis height — the line fraction bars and minus signs sit on; delimiters are centered on this line |
|
|
45
|
+
| `δ₀` | `max(h − a, d + a)` — how far the content reaches from the axis, on whichever side is farther |
|
|
46
|
+
| `f` | `\delimiterfactor`, default **901** |
|
|
47
|
+
| `l` | `\delimitershortfall`, default **5pt** |
|
|
48
|
+
| `s_min` | the smallest total bracket size TeX will accept |
|
|
49
|
+
| `s` | the size of the delimiter TeX actually picks |
|
|
50
|
+
|
|
51
|
+
With the default `l = 5pt`, `s_min` can be *smaller* than the content's own height (since `901/1000 ≈ 0.9`), so a delimiter that already matches the inner bracket's height passes the test for the outer bracket too — no forced growth. The same slack also means a delimiter can legally fall up to 5pt short of the content, i.e. requirement (2) isn't fully guaranteed by default either.
|
|
52
|
+
|
|
53
|
+
**The fix in real LaTeX:** set `\delimitershortfall` to a *negative* value (e.g. `-1pt`). Then `s_min = 2δ₀ + 1pt`, which is always larger than the content's own extent — forcing outward growth at every level, automatically, with no per-formula work. This is a document-wide setting, not something you write per equation:
|
|
54
|
+
|
|
55
|
+
```latex
|
|
56
|
+
\delimitershortfall=-1pt
|
|
57
|
+
\left[ \left[ \sqrt{\frac{\int\limits_0^{10}\sqrt{a^2+b^2}}{2}} \right] + 1 \right]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Verified by rendering with `pdflatex`: default nesting produces equal-sized brackets; the same source with `\delimitershortfall=-1pt` produces strictly increasing sizes at every depth, and small content (`(x)`, `((x))`, `(((x)))`) is unaffected since the correction is tiny relative to normal delimiter jumps.
|
|
61
|
+
|
|
62
|
+
## Why it doesn't carry over to the web
|
|
63
|
+
|
|
64
|
+
`\delimitershortfall` is a TeX engine primitive. **Neither KaTeX nor MathJax expose it or honor it** — I tested both directly:
|
|
65
|
+
|
|
66
|
+
- KaTeX: `\delimitershortfall=-1pt` throws `ParseError: Expected group after '\delimitershortfall'` (undefined control sequence).
|
|
67
|
+
- MathJax v3: the command is silently dropped/renders as an error span; no effect on sizing.
|
|
68
|
+
|
|
69
|
+
So the web renderers need a different fix.
|
|
70
|
+
|
|
71
|
+
## Solution 1: a `\grow` macro (portable, works in *any* KaTeX/MathJax page)
|
|
72
|
+
|
|
73
|
+
Since neither engine's *rule* can be changed from user-level TeX, the workaround changes the **content** being measured instead. Wrap everything inside a bracket pair (except the innermost level) in an invisible padding macro that makes the content measure as slightly taller and deeper than it visually is. Then the normal `\left`/`\right` sizing — which *does* work correctly in both engines — is forced to pick a bigger glyph at each level, because the content it measures really is bigger.
|
|
74
|
+
|
|
75
|
+
**Mechanism:** for content `#1`, stack a raised zero-width phantom copy 0.5em above it and a lowered zero-width phantom copy 0.5em below it. `\vphantom{#1}` takes zero horizontal space and is invisible, but claims the same height/depth as `#1`. Raising and lowering copies of it extends the *measured* bounding box by 0.5em on each side — 1em of forced growth per wrap — without changing what's visually drawn.
|
|
76
|
+
|
|
77
|
+
**KaTeX** (verified — `\raisebox` only accepts a text-mode argument, so the phantom must be re-entered as math and forced to `\displaystyle`; without that it silently shrinks to inline size and the trick does nothing):
|
|
78
|
+
|
|
79
|
+
```latex
|
|
80
|
+
\newcommand{\grow}[1]{#1\raisebox{0.5em}{$\displaystyle\vphantom{#1}$}\raisebox{-0.5em}{$\displaystyle\vphantom{#1}$}}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**MathJax** (verified — `\raisebox` doesn't exist here; plain-TeX `\raise`/`\lower` take a math argument directly and preserve the current style):
|
|
84
|
+
|
|
85
|
+
```latex
|
|
86
|
+
\newcommand{\grow}[1]{#1\raise{0.5em}{\vphantom{#1}}\lower{0.5em}{\vphantom{#1}}}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Usage** — wrap the content of every bracket pair except the innermost:
|
|
90
|
+
|
|
91
|
+
```latex
|
|
92
|
+
\left[ \grow{ \left[ \sqrt{\frac{\int\limits_0^{10}\sqrt{a^2+b^2}}{2}} \right] + 1 } \right]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Verified by rendering with real KaTeX 0.18.7 and MathJax 3.2.2 (headless Chromium screenshots): both show strictly increasing bracket sizes across 2–3 nesting levels, matching the pdfLaTeX reference.
|
|
96
|
+
|
|
97
|
+
**Trade-offs:**
|
|
98
|
+
- Manual — you place `\grow{}` yourself at each level; it isn't automatic like the LaTeX-engine setting.
|
|
99
|
+
- The two spellings aren't interchangeable: `\raisebox` errors out in MathJax, `\raise`/`\lower` don't exist in KaTeX. Pick the one matching your renderer.
|
|
100
|
+
- Can be defined once (in MathJax's `tex.macros` config, or KaTeX's `macros` render option) so you only write `\grow{...}` per formula, not the full definition each time.
|
|
101
|
+
|
|
102
|
+
## Solution 2: patch KaTeX directly (better fit for a site you build/host yourself)
|
|
103
|
+
|
|
104
|
+
Since you control the build (an Astro site rendering KaTeX server-side), you don't need the macro workaround at all — you can restore the actual `\delimitershortfall` behavior by editing the one place KaTeX hard-codes it.
|
|
105
|
+
|
|
106
|
+
**The code.** KaTeX's delimiter-sizing function (`makeLeftRightDelim`, in the compiled bundle since no per-file TS source ships in the npm package) contains:
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
const delimiterFactor = 901; // TeX's \delimiterfactor
|
|
110
|
+
const delimiterExtend = 5.0 / options.fontMetrics().ptPerEm; // TeX's \delimitershortfall = 5pt
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
This is the exact same formula from the TeXbook rule above, with the same two constants. Changing `5.0` to `-1.0` is precisely `\delimitershortfall=-1pt` — `delimiterFactor` doesn't need to change, since a negative shortfall always makes the second term of the `max(...)` dominate anyway.
|
|
114
|
+
|
|
115
|
+
**Verified:** I applied this one-line edit to the installed `katex` package and rendered plain, unmodified `\left[...\right]` source (no `\grow` macro anywhere) through the patched build. Result: strict growth across 4 nesting levels, small content (`(x)`, `((x))`, `(((x)))`) unaffected, and — critically — the LaTeX source embedded in KaTeX's output for copy/paste (`<annotation encoding="application/x-tex">`) stays exactly what you typed, since only the *sizing*, not the *content*, is touched.
|
|
116
|
+
|
|
117
|
+
**Where the line lives** (npm package, KaTeX 0.16.47):
|
|
118
|
+
|
|
119
|
+
| File | Used by | Patched here |
|
|
120
|
+
|---|---|---|
|
|
121
|
+
| `src/delimiter.ts` | the readable source the bundles are built from | yes |
|
|
122
|
+
| `dist/katex.mjs` | ESM import — what an Astro/Vite build resolves to | yes |
|
|
123
|
+
| `dist/katex.js` | CJS `require` | yes |
|
|
124
|
+
| `dist/katex.min.js` | only if KaTeX is also loaded client-side via `<script>` | **no** — see below |
|
|
125
|
+
|
|
126
|
+
`dist/katex.min.js` is left alone on purpose. explico renders every formula at
|
|
127
|
+
build time and ships only `katex.min.css` plus fonts to the browser, so nothing
|
|
128
|
+
here loads that bundle — and because it is a single ~270 KB line, including it
|
|
129
|
+
turns a 2 KB patch into a 550 KB one that no reviewer can read and no KaTeX
|
|
130
|
+
upgrade can rebase. A course that *does* render KaTeX in the browser has to
|
|
131
|
+
patch it too; the minified form of the same line is
|
|
132
|
+
`l=5/n.fontMetrics().ptPerEm` inside `makeLeftRightDelim`.
|
|
133
|
+
|
|
134
|
+
## How this repo applies it
|
|
135
|
+
|
|
136
|
+
A `pnpm patch` would work, and is what the section above describes, but it is
|
|
137
|
+
**project-local**: pnpm records it in the consuming repo's
|
|
138
|
+
`pnpm-workspace.yaml`, so it cannot travel inside the published `explico`
|
|
139
|
+
package. Every downstream course would have to reapply it, and a course that
|
|
140
|
+
forgot would render flat nests while its linter told authors the engine handled
|
|
141
|
+
sizing. Documenting that in a migration guide only narrows the window for the
|
|
142
|
+
mistake; it does not close it.
|
|
143
|
+
|
|
144
|
+
So the engine does the same edit in code, at import time, in
|
|
145
|
+
[`src/reference/katex-delimiters.mjs`](../src/reference/katex-delimiters.mjs):
|
|
146
|
+
|
|
147
|
+
1. read KaTeX's own ESM bundle off disk (`katex/dist/katex.mjs`);
|
|
148
|
+
2. rewrite that one constant, throwing if it is not found verbatim;
|
|
149
|
+
3. `import()` the rewritten source as a `data:` URL — the bundle has no imports
|
|
150
|
+
of its own, so it needs no base to resolve against;
|
|
151
|
+
4. move `render`, `renderToString`, `__renderToDomTree` and
|
|
152
|
+
`__renderToHTMLTree` onto the shared `katex` object every consumer in the
|
|
153
|
+
process already imported;
|
|
154
|
+
5. render a known nest and throw unless the sizes actually grew.
|
|
155
|
+
|
|
156
|
+
Step 4 is what makes it reach lesson-body math. `rehype-katex` does its own
|
|
157
|
+
`import katex from 'katex'`, but it calls `katex.renderToString(...)` — a
|
|
158
|
+
property read at call time — and KaTeX's default export is a plain mutable
|
|
159
|
+
object. Replacing the function on that object is therefore enough; the plugin
|
|
160
|
+
does not have to be forked or aliased.
|
|
161
|
+
|
|
162
|
+
**A course inherits this with no changes.** `astro.config.mjs` already imports
|
|
163
|
+
`createNotationKatexOptions` from `explico/reference/katex-options.mjs`, and
|
|
164
|
+
that module imports the installer for its side effect, so the sizing is in
|
|
165
|
+
place before Astro renders anything. The engine's own non-Astro paths
|
|
166
|
+
(`math-glyphs.mjs`, `render-lab-math.ts`, and therefore `validate:content`)
|
|
167
|
+
import it directly.
|
|
168
|
+
|
|
169
|
+
**It cannot fail quietly.** Steps 2 and 5 throw. A KaTeX upgrade that moves the
|
|
170
|
+
constant, or anything that renders before the installer runs, stops the build
|
|
171
|
+
with an explicit message instead of silently reverting to stock spacing — which
|
|
172
|
+
is the failure mode a patch file plus a migration note could not rule out.
|
|
173
|
+
|
|
174
|
+
**Trade-offs.** It is a monkey-patch of a third-party module object, so it
|
|
175
|
+
rests on three upstream properties: the constant is a literal in the shipped
|
|
176
|
+
bundle, the bundle is self-contained, and the default export is mutable. All
|
|
177
|
+
three are asserted at install time rather than assumed. It also holds a second
|
|
178
|
+
copy of the KaTeX module in memory (~600 KB of source, ~50 ms to load) for the
|
|
179
|
+
duration of a build, which is not worth optimising away.
|
|
180
|
+
|
|
181
|
+
## Why the patch is the better fit here
|
|
182
|
+
|
|
183
|
+
Your original idea — keep writing `\left[` everywhere, run a build-time preprocessing step that inserts `\grow{}` at each nesting level, then swap in the original source for a "copy" button — would work, but has two costs the patch avoids:
|
|
184
|
+
|
|
185
|
+
1. **The preprocessor needs a real TeX parser**, not a regex. It has to track bracket nesting depth correctly across `\text{...}`, comments, `\right.` (null right delimiters), and — critically — macros. If you ever define something like `\norm{...}` that *expands* to `\left\lVert...\right\rVert`, a source-level preprocessor can't see the nesting hidden inside the macro and will miscount levels. The patch operates after macro expansion, inside KaTeX's own layout engine, so it can't miss anything.
|
|
186
|
+
2. **The copy-button problem disappears.** KaTeX already embeds your literal input in the hidden MathML `<annotation>` tag — that's what the standard `copy-tex` extension reads. With preprocessing, that annotation would contain the `\grow`-polluted string, so you'd have to intercept the copy handler and substitute the original text yourself. With the patch, the *displayed* math and the *embedded source* never diverge in the first place, because nothing was ever inserted into the source.
|
|
187
|
+
|
|
188
|
+
## Caveats
|
|
189
|
+
|
|
190
|
+
- If your pipeline actually renders to **SVG** rather than HTML+CSS, you're using MathJax's `tex-svg` output, not KaTeX (KaTeX renders to HTML spans, not SVG). MathJax's stretchy-delimiter logic lives in the output stage rather than one constants block, so the equivalent patch would be far more invasive there — the `\grow` macro is the practical option for MathJax.
|
|
191
|
+
- On an Astro build, KaTeX runs at build time (via `remark-math`/`rehype-katex` or similar) and only sends finished HTML + `katex.css` + fonts to the browser — the patched JS never ships to visitors, it only needs to be present in your build environment.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explico",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"README.md",
|
|
31
31
|
"CHANGELOG.md",
|
|
32
32
|
"docs/VERSIONING.md",
|
|
33
|
+
"docs/katex-bracket-scaling.md",
|
|
33
34
|
"docs/authoring"
|
|
34
35
|
],
|
|
35
36
|
"dependencies": {
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
"vitest": "4.1.11"
|
|
64
65
|
},
|
|
65
66
|
"scripts": {
|
|
66
|
-
"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",
|
|
67
68
|
"check:release-migration": "node scripts/check-release-migration.mjs",
|
|
68
69
|
"test": "vitest run",
|
|
69
70
|
"test:watch": "vitest"
|