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
package/src/content-config.ts
CHANGED
|
@@ -124,19 +124,31 @@ const notationEntryShape = z.object({
|
|
|
124
124
|
|
|
125
125
|
const localNotationDefinition = notationEntryShape;
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* One `notation.suspend` item (rule N22): a decoration (`'`, `\hat`, …) that
|
|
129
|
+
* is not an operator on this page. The loader (`collections.ts`) checks the
|
|
130
|
+
* name against the decorations the resolver knows.
|
|
131
|
+
*/
|
|
132
|
+
const notationSuspension = z.object({
|
|
133
|
+
operator: z.string().min(1),
|
|
134
|
+
reason: z.string().min(1).optional(),
|
|
135
|
+
});
|
|
136
|
+
|
|
127
137
|
const lessonNotation = z
|
|
128
138
|
.object({
|
|
129
139
|
// `notation.uses` is retired (D3): a lesson pulls a shared key into scope
|
|
130
140
|
// by referencing it in prose (`[[key]]`) or math (`\explain`).
|
|
131
141
|
local: z.array(localNotationDefinition).default([]),
|
|
142
|
+
suspend: z.array(notationSuspension).default([]),
|
|
132
143
|
})
|
|
133
|
-
.default({ local: [] });
|
|
144
|
+
.default({ local: [], suspend: [] });
|
|
134
145
|
|
|
135
146
|
/**
|
|
136
147
|
* Lesson frontmatter — reduced shape (Phase C1 schema, C2 codemod applied).
|
|
137
148
|
*
|
|
138
149
|
* Authored: `title`, `description` (Starlight base), `teaches` (ordered — the
|
|
139
150
|
* curriculum contract, never derived), `assumptions`,
|
|
151
|
+
* `studyMinutes` (optional reader-facing estimate),
|
|
140
152
|
* `notation.local` (page-local symbols only).
|
|
141
153
|
* Artifact: `editorialStatus` (human-set trust flag).
|
|
142
154
|
* Derived (in `content/collections.ts` + `content/lesson-derivation.ts`, never
|
|
@@ -144,10 +156,33 @@ const lessonNotation = z
|
|
|
144
156
|
* prerequisites of `teaches`, minus `teaches`), `sources` (`[@…]`
|
|
145
157
|
* occurrences), `assessments` (colocated `<lesson>.checks.yml`),
|
|
146
158
|
* `sidebar` order (track order).
|
|
147
|
-
* Dropped: `aiAssisted`, `lastReviewed`, `riskTier
|
|
148
|
-
*
|
|
149
|
-
* `
|
|
159
|
+
* Dropped: `aiAssisted`, `lastReviewed`, `riskTier` (git history +
|
|
160
|
+
* `editorialStatus` + `NEEDS_SOURCE` carry provenance);
|
|
161
|
+
* `estimatedMinutes`, whose reader-facing half returned as the
|
|
162
|
+
* optional `studyMinutes` once part pages had somewhere to total
|
|
163
|
+
* it; `notation.uses` (D3 — a shared symbol is imported by using
|
|
164
|
+
* it).
|
|
165
|
+
*/
|
|
166
|
+
/**
|
|
167
|
+
* How long this chapter takes to study: `40`, or a `[low, high]` range in
|
|
168
|
+
* minutes. It is a reader-facing estimate the author makes, aggregated by
|
|
169
|
+
* `<PartOverview>` and `<CourseContents>` into a part and course total — not
|
|
170
|
+
* the provenance field `estimatedMinutes` used to be, and nothing derives it.
|
|
171
|
+
*
|
|
172
|
+
* A part only reports a total when every one of its chapters states one, so a
|
|
173
|
+
* half-annotated course shows the chapters it knows about and no misleading
|
|
174
|
+
* sum.
|
|
150
175
|
*/
|
|
176
|
+
const studyMinutes = z
|
|
177
|
+
.union([
|
|
178
|
+
z.number().int().positive(),
|
|
179
|
+
z.tuple([z.number().int().positive(), z.number().int().positive()]),
|
|
180
|
+
])
|
|
181
|
+
.refine((value) => !Array.isArray(value) || value[0] <= value[1], {
|
|
182
|
+
message:
|
|
183
|
+
'studyMinutes as a range is [low, high]; the low end cannot exceed the high end.',
|
|
184
|
+
});
|
|
185
|
+
|
|
151
186
|
const docs = defineCollection({
|
|
152
187
|
loader: docsGlob,
|
|
153
188
|
schema: docsSchema({
|
|
@@ -155,6 +190,7 @@ const docs = defineCollection({
|
|
|
155
190
|
editorialStatus: editorialStatus.default('draft'),
|
|
156
191
|
teaches: z.array(id).default([]),
|
|
157
192
|
assumptions: z.array(z.string().min(1)).default([]),
|
|
193
|
+
studyMinutes: studyMinutes.optional(),
|
|
158
194
|
notation: lessonNotation,
|
|
159
195
|
}),
|
|
160
196
|
}),
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base accent baseline: decorations every reader is assumed to know, so a
|
|
3
|
+
* lesson never has to explain them (rule N22). The named-operator counterpart
|
|
4
|
+
* is `base-operators.mjs` (rule N8).
|
|
5
|
+
*
|
|
6
|
+
* It ships **empty**, because no decoration has one meaning across fields:
|
|
7
|
+
* `\hat` can mean an estimator, a unit vector or a Fourier transform; `\bar`
|
|
8
|
+
* a mean, a conjugate or a closure; `'` a derivative or "another one". A
|
|
9
|
+
* course that uses one consistently says so with `rules.N22.allow` (a physics
|
|
10
|
+
* course might allow `\dot` and `\vec`); anything else is explained by an
|
|
11
|
+
* operator entry (`latex: \hat{}`) or a whole-form entry (`latex:
|
|
12
|
+
* \hat{\beta}`). Names are the canonical ones in `DECORATION_NAMES`
|
|
13
|
+
* (`math-glyphs.mjs`): `\hat`, `\overline`, `'`, `*`, `\dagger`, ….
|
|
14
|
+
*/
|
|
15
|
+
export const BASE_ACCENTS = Object.freeze(new Set());
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The decorations a course treats as universal, given its `rules.N22` block:
|
|
19
|
+
* the shipped baseline, plus `allow`, minus `deny`.
|
|
20
|
+
*
|
|
21
|
+
* @param {{ allow?: readonly string[], deny?: readonly string[] } | undefined} accents
|
|
22
|
+
* @returns {Set<string>}
|
|
23
|
+
*/
|
|
24
|
+
export function effectiveAccentAllowlist(accents) {
|
|
25
|
+
const allow = accents?.allow ?? [];
|
|
26
|
+
const deny = new Set(accents?.deny ?? []);
|
|
27
|
+
const effective = new Set(BASE_ACCENTS);
|
|
28
|
+
for (const name of allow) effective.add(name);
|
|
29
|
+
for (const name of deny) effective.delete(name);
|
|
30
|
+
return effective;
|
|
31
|
+
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* (rule N8); this is its `\mathrm{…}` twin (rule N10).
|
|
6
6
|
*
|
|
7
7
|
* Rationale: the completeness gate has no opinion about `\mathrm{…}` bodies —
|
|
8
|
-
* they are a typographic label to KaTeX, not a bindable symbol,
|
|
9
|
-
*
|
|
8
|
+
* they are a typographic label to KaTeX, not a bindable symbol, so the
|
|
9
|
+
* format half of rule N10 says nothing about whether the label is explained.
|
|
10
10
|
* The `roman-label-explained` consistency check fills that gap: a label a
|
|
11
11
|
* lesson uses is either on this list (or the course's additions to it) or it
|
|
12
12
|
* carries a notation entry the reader can hover.
|
|
@@ -24,12 +24,21 @@
|
|
|
24
24
|
* uses is on the shipped/course roman-label
|
|
25
25
|
* allowlist or carries a notation entry
|
|
26
26
|
* (rule N10)
|
|
27
|
+
* (j) accent-explained a decoration a lesson composes (`\hat`,
|
|
28
|
+
* `'`, `^*`, …) is on the course accent
|
|
29
|
+
* allowlist, explained by an operator entry,
|
|
30
|
+
* or part of a whole-form entry (rule N22)
|
|
31
|
+
* (k) operator-suspended / a `notation.suspend` item, with or without
|
|
32
|
+
* operator-suspension-reason its reason, and a whole-form entry that
|
|
33
|
+
* / operator-shadowed shadows an operator the page does not
|
|
34
|
+
* suspend (rule N22)
|
|
27
35
|
*
|
|
28
36
|
* Check (d) — the committed per-lesson resolution report — lives in
|
|
29
37
|
* `src/content/resolution-report.ts`.
|
|
30
38
|
*/
|
|
31
39
|
|
|
32
40
|
import type {
|
|
41
|
+
CourseAccents,
|
|
33
42
|
CourseConventions,
|
|
34
43
|
CourseOperators,
|
|
35
44
|
CourseRomanLabels,
|
|
@@ -37,9 +46,19 @@ import type {
|
|
|
37
46
|
import type { CurriculumCatalog } from '../curriculum/validation';
|
|
38
47
|
import type { NotationSpec } from '../compiler/collections';
|
|
39
48
|
import type { LintIgnore } from './lint-ignore';
|
|
49
|
+
import { effectiveAccentAllowlist } from './base-accents.mjs';
|
|
40
50
|
import { effectiveOperatorAllowlist } from './base-operators.mjs';
|
|
41
51
|
import { effectiveRomanLabelAllowlist } from './base-roman-labels.mjs';
|
|
42
|
-
import {
|
|
52
|
+
import { stripEquationLabels } from './equations.mjs';
|
|
53
|
+
import { pageScope } from './gate-math';
|
|
54
|
+
import {
|
|
55
|
+
collectDecorations,
|
|
56
|
+
collectOperators,
|
|
57
|
+
collectRomanLabels,
|
|
58
|
+
decorationOfEntry,
|
|
59
|
+
operatorEntryLatex,
|
|
60
|
+
resolveMathGlyphs,
|
|
61
|
+
} from './math-glyphs.mjs';
|
|
43
62
|
import { offVocabularyTokens } from './units-vocab';
|
|
44
63
|
import type {
|
|
45
64
|
NotationDefinitionRecord,
|
|
@@ -58,7 +77,11 @@ export type ConsistencyCode =
|
|
|
58
77
|
| 'card-wants-demoting'
|
|
59
78
|
| 'gloss-name-shape'
|
|
60
79
|
| 'operator-explained'
|
|
61
|
-
| 'roman-label-explained'
|
|
80
|
+
| 'roman-label-explained'
|
|
81
|
+
| 'accent-explained'
|
|
82
|
+
| 'operator-suspended'
|
|
83
|
+
| 'operator-suspension-reason'
|
|
84
|
+
| 'operator-shadowed';
|
|
62
85
|
|
|
63
86
|
export interface ConsistencyDiagnostic {
|
|
64
87
|
readonly code: ConsistencyCode;
|
|
@@ -85,6 +108,8 @@ export interface ConsistencyInput {
|
|
|
85
108
|
readonly operators?: CourseOperators;
|
|
86
109
|
/** This course's edits to the roman-label baseline (rule N10). */
|
|
87
110
|
readonly romanLabels?: CourseRomanLabels;
|
|
111
|
+
/** This course's edits to the (empty) accent baseline (rule N22). */
|
|
112
|
+
readonly accents?: CourseAccents;
|
|
88
113
|
}
|
|
89
114
|
|
|
90
115
|
const CONVENTION_RE =
|
|
@@ -102,11 +127,17 @@ interface MathSpan {
|
|
|
102
127
|
readonly latex: string;
|
|
103
128
|
}
|
|
104
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Every `$$…$$` then `$…$` span in `body`, outside code. A `\label{eq:…}`
|
|
132
|
+
* (D7) is stripped: it is an identity marker KaTeX cannot parse, and the
|
|
133
|
+
* scans below treat an unparseable span as having nothing in it — so left in,
|
|
134
|
+
* every labelled display equation was invisible to rules N8, N10 and N22.
|
|
135
|
+
*/
|
|
105
136
|
function mathSpans(body: string): MathSpan[] {
|
|
106
137
|
const searchable = stripCode(body);
|
|
107
138
|
const spans: MathSpan[] = [];
|
|
108
139
|
for (const match of searchable.matchAll(/\$\$([\s\S]+?)\$\$/g)) {
|
|
109
|
-
spans.push({ latex: match[1] ?? '' });
|
|
140
|
+
spans.push({ latex: stripEquationLabels(match[1] ?? '') });
|
|
110
141
|
}
|
|
111
142
|
for (const match of searchable
|
|
112
143
|
.replace(/\$\$([\s\S]+?)\$\$/g, (block) => block.replace(/[^\n]/g, ' '))
|
|
@@ -508,8 +539,8 @@ function checkOperatorExplained(
|
|
|
508
539
|
|
|
509
540
|
/**
|
|
510
541
|
* (i) roman-label-explained (rule N10) — the `\mathrm{…}` twin of (h)
|
|
511
|
-
* above:
|
|
512
|
-
*
|
|
542
|
+
* above: N10's format half (`\mathrm{KE}`, never bare `KE`) says nothing
|
|
543
|
+
* about whether the label is ever explained, because the glyph
|
|
513
544
|
* resolver skips a `\mathrm{…}` body whole. This is the check that looks at
|
|
514
545
|
* them; it reads `collectRomanLabels`, which parses with the same pinned
|
|
515
546
|
* KaTeX as the completeness gate.
|
|
@@ -566,6 +597,140 @@ function checkRomanLabelExplained(
|
|
|
566
597
|
}
|
|
567
598
|
}
|
|
568
599
|
|
|
600
|
+
function sharedScopeByKey(
|
|
601
|
+
input: ConsistencyInput,
|
|
602
|
+
): Map<string, { key: string; notation: string }> {
|
|
603
|
+
return new Map(
|
|
604
|
+
input.notationInput.sharedDefinitions.map((definition) => [
|
|
605
|
+
definition.key,
|
|
606
|
+
{ key: definition.key, notation: definition.notation },
|
|
607
|
+
]),
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* (j) accent-explained (rule N22) — the decoration twin of (h): an accent or
|
|
613
|
+
* superscript mark a lesson *composes* (`\hat{\beta}` read as "hat applied to
|
|
614
|
+
* β") is universal for the course (`rules.N22.allow`; the shipped list is
|
|
615
|
+
* empty), explained by an operator entry in the page's scope (`latex:
|
|
616
|
+
* \hat{}`), or not composing at all because a whole-form entry covers it
|
|
617
|
+
* (`latex: \hat{\beta}`). Only the resolver knows which whole form covered an
|
|
618
|
+
* occurrence (`\theta^*` and `\theta^{*}` are one form), so this runs it over
|
|
619
|
+
* each span with the same page scope as the completeness gate. A span that
|
|
620
|
+
* will not resolve is the gate's to report, not this check's.
|
|
621
|
+
*/
|
|
622
|
+
function checkAccentExplained(
|
|
623
|
+
input: ConsistencyInput,
|
|
624
|
+
out: ConsistencyDiagnostic[],
|
|
625
|
+
): void {
|
|
626
|
+
const allowlist = effectiveAccentAllowlist(input.accents);
|
|
627
|
+
const sharedByKey = sharedScopeByKey(input);
|
|
628
|
+
|
|
629
|
+
for (const lesson of input.notationInput.lessons) {
|
|
630
|
+
const scope = pageScope(lesson, sharedByKey);
|
|
631
|
+
const suspended = (lesson.suspended ?? []).map((item) => item.operator);
|
|
632
|
+
const reported = new Set<string>();
|
|
633
|
+
for (const span of mathSpans(lesson.body)) {
|
|
634
|
+
let decorations;
|
|
635
|
+
try {
|
|
636
|
+
({ decorations } = resolveMathGlyphs(span.latex, scope, { suspended }));
|
|
637
|
+
} catch {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
for (const decoration of decorations) {
|
|
641
|
+
if (decoration.wholeForm !== null || decoration.operator !== null) continue;
|
|
642
|
+
// A suspended decoration never composes; the gate fails it instead.
|
|
643
|
+
if (suspended.includes(decoration.name)) continue;
|
|
644
|
+
if (allowlist.has(decoration.name)) continue;
|
|
645
|
+
if (reported.has(decoration.name)) continue;
|
|
646
|
+
reported.add(decoration.name);
|
|
647
|
+
if (input.lintIgnore.matches('accent-explained', decoration.name)) {
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
out.push({
|
|
651
|
+
code: 'accent-explained',
|
|
652
|
+
severity: 'warning',
|
|
653
|
+
lessonId: lesson.lessonId,
|
|
654
|
+
file: lesson.source.file,
|
|
655
|
+
glyph: decoration.name,
|
|
656
|
+
message: `${lesson.lessonId}: decoration ${JSON.stringify(decoration.name)} (in ${JSON.stringify(decoration.token)}) is an operator that is neither universal for this course nor explained; give it an operator entry (latex ${JSON.stringify(operatorEntryLatex(decoration.name))}) introduced with [[key]] or in notation.local, give the whole form its own entry (latex ${JSON.stringify(decoration.token)}), or add ${JSON.stringify(decoration.name)} to rules.N22.allow`,
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* (k) operator-suspended / operator-suspension-reason / operator-shadowed
|
|
665
|
+
* (rule N22). A `notation.suspend` item is a deliberate departure from N22 on
|
|
666
|
+
* one page: with its `reason` it is recorded (info), without one it is a
|
|
667
|
+
* warning — the reason is what a reviewer needs. And a whole-form entry whose
|
|
668
|
+
* decoration is explained or universal on its page, but not suspended there,
|
|
669
|
+
* is noted (info): the entry's card wins for that form, which is right for a
|
|
670
|
+
* refinement ("β̂ is the OLS estimate") and a smell when the decoration never
|
|
671
|
+
* means the operator on that page.
|
|
672
|
+
*/
|
|
673
|
+
function checkOperatorSuspension(
|
|
674
|
+
input: ConsistencyInput,
|
|
675
|
+
out: ConsistencyDiagnostic[],
|
|
676
|
+
): void {
|
|
677
|
+
const allowlist = effectiveAccentAllowlist(input.accents);
|
|
678
|
+
const sharedByKey = sharedScopeByKey(input);
|
|
679
|
+
|
|
680
|
+
for (const lesson of input.notationInput.lessons) {
|
|
681
|
+
const suspensions = lesson.suspended ?? [];
|
|
682
|
+
for (const { operator, reason } of suspensions) {
|
|
683
|
+
const code =
|
|
684
|
+
reason === undefined ? 'operator-suspension-reason' : 'operator-suspended';
|
|
685
|
+
if (input.lintIgnore.matches(code, operator)) continue;
|
|
686
|
+
out.push({
|
|
687
|
+
code,
|
|
688
|
+
severity: reason === undefined ? 'warning' : 'info',
|
|
689
|
+
lessonId: lesson.lessonId,
|
|
690
|
+
file: lesson.source.file,
|
|
691
|
+
glyph: operator,
|
|
692
|
+
message:
|
|
693
|
+
reason === undefined
|
|
694
|
+
? `${lesson.lessonId}: notation.suspend suspends ${JSON.stringify(operator)} with no reason; say why it is not an operator on this page (reason: …)`
|
|
695
|
+
: `${lesson.lessonId}: ${JSON.stringify(operator)} is not an operator on this page (notation.suspend): ${reason}`,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const suspended = new Set(suspensions.map((item) => item.operator));
|
|
700
|
+
const scope = pageScope(lesson, sharedByKey);
|
|
701
|
+
const explained = new Set(
|
|
702
|
+
scope
|
|
703
|
+
.map((entry) => decorationOfEntry(entry.notation))
|
|
704
|
+
.filter((name): name is string => name !== undefined),
|
|
705
|
+
);
|
|
706
|
+
for (const entry of scope) {
|
|
707
|
+
if (decorationOfEntry(entry.notation) !== undefined) continue;
|
|
708
|
+
const names = new Set(
|
|
709
|
+
collectDecorations(entry.notation).map((decoration) => decoration.name),
|
|
710
|
+
);
|
|
711
|
+
for (const name of names) {
|
|
712
|
+
if (suspended.has(name)) continue;
|
|
713
|
+
const how = explained.has(name)
|
|
714
|
+
? 'explained by an operator entry'
|
|
715
|
+
: allowlist.has(name)
|
|
716
|
+
? 'universal for this course'
|
|
717
|
+
: undefined;
|
|
718
|
+
if (how === undefined) continue;
|
|
719
|
+
if (input.lintIgnore.matches('operator-shadowed', entry.key)) continue;
|
|
720
|
+
out.push({
|
|
721
|
+
code: 'operator-shadowed',
|
|
722
|
+
severity: 'info',
|
|
723
|
+
lessonId: lesson.lessonId,
|
|
724
|
+
file: lesson.source.file,
|
|
725
|
+
key: entry.key,
|
|
726
|
+
glyph: name,
|
|
727
|
+
message: `${lesson.lessonId}: ${entry.key} (${entry.notation}) has its own entry, so its card wins over the ${JSON.stringify(name)} operator (${how}) for that form; if ${JSON.stringify(name)} never means the operator on this page, suspend it (notation.suspend)`,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
569
734
|
const CHECKS: ((
|
|
570
735
|
input: ConsistencyInput,
|
|
571
736
|
out: ConsistencyDiagnostic[],
|
|
@@ -580,6 +745,8 @@ const CHECKS: ((
|
|
|
580
745
|
checkGlossNameShape,
|
|
581
746
|
checkOperatorExplained,
|
|
582
747
|
checkRomanLabelExplained,
|
|
748
|
+
checkAccentExplained,
|
|
749
|
+
checkOperatorSuspension,
|
|
583
750
|
];
|
|
584
751
|
|
|
585
752
|
/**
|
|
@@ -618,6 +785,10 @@ export function consistencyCounts(
|
|
|
618
785
|
'gloss-name-shape': 0,
|
|
619
786
|
'operator-explained': 0,
|
|
620
787
|
'roman-label-explained': 0,
|
|
788
|
+
'accent-explained': 0,
|
|
789
|
+
'operator-suspended': 0,
|
|
790
|
+
'operator-suspension-reason': 0,
|
|
791
|
+
'operator-shadowed': 0,
|
|
621
792
|
} satisfies Record<ConsistencyCode, number>;
|
|
622
793
|
for (const diagnostic of diagnostics) counts[diagnostic.code] += 1;
|
|
623
794
|
return counts;
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Rule N17: nested scalable
|
|
2
|
+
* Rule N17: a nested scalable delimiter is sized by the engine, never by hand.
|
|
3
|
+
*
|
|
4
|
+
* The engine installs a KaTeX whose `\delimitershortfall` is `-1pt`
|
|
5
|
+
* (`katex-delimiters.mjs`), so a `\left`/`\right` pair is always
|
|
6
|
+
* strictly taller than what it wraps. Nesting depth therefore shows up on its
|
|
7
|
+
* own, and a hand-picked `\bigl`…`\Biggr` inside a nest only replaces a size
|
|
8
|
+
* that tracks the content with a fixed glyph that does not.
|
|
3
9
|
*
|
|
4
10
|
* This deliberately parses only sizing syntax. It does not decide whether a
|
|
5
11
|
* parenthesis, bracket, or brace is semantic, and it never recommends changing
|
|
6
|
-
* one delimiter shape into another
|
|
7
|
-
*
|
|
12
|
+
* one delimiter shape into another — N17's other half, that a delimiter's shape
|
|
13
|
+
* is fixed by its meaning and never alternates to show depth, stays a
|
|
14
|
+
* review-only authoring concern (which is why N17 is `partial`).
|
|
8
15
|
*/
|
|
9
16
|
|
|
10
|
-
export const DELIMITER_SIZE_DIAGNOSTIC_CODE =
|
|
11
|
-
'delimiter-size-hierarchy' as const;
|
|
17
|
+
export const DELIMITER_SIZE_DIAGNOSTIC_CODE = 'delimiter-manual-size' as const;
|
|
12
18
|
|
|
13
19
|
export type DelimiterSizeName =
|
|
14
20
|
| 'automatic'
|
|
@@ -18,7 +24,6 @@ export type DelimiterSizeName =
|
|
|
18
24
|
| 'Bigg';
|
|
19
25
|
|
|
20
26
|
export interface DelimiterSizeSuggestion {
|
|
21
|
-
readonly size: Exclude<DelimiterSizeName, 'automatic' | 'big'>;
|
|
22
27
|
readonly open: string;
|
|
23
28
|
readonly close: string;
|
|
24
29
|
readonly latex: string;
|
|
@@ -30,9 +35,9 @@ export interface DelimiterSizeDiagnostic {
|
|
|
30
35
|
readonly offset: number;
|
|
31
36
|
readonly line: number;
|
|
32
37
|
readonly column: number;
|
|
33
|
-
|
|
34
|
-
readonly
|
|
35
|
-
readonly suggestion
|
|
38
|
+
/** The hand-picked size that should become `\left`/`\right`. */
|
|
39
|
+
readonly size: Exclude<DelimiterSizeName, 'automatic'>;
|
|
40
|
+
readonly suggestion: DelimiterSizeSuggestion;
|
|
36
41
|
readonly message: string;
|
|
37
42
|
}
|
|
38
43
|
|
|
@@ -62,7 +67,6 @@ interface DelimiterPair {
|
|
|
62
67
|
readonly open: DelimiterToken;
|
|
63
68
|
close?: DelimiterToken;
|
|
64
69
|
readonly children: DelimiterPair[];
|
|
65
|
-
requiredRank?: number;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
const SIZE_SPECS: readonly SizeSpec[] = [
|
|
@@ -114,10 +118,6 @@ const COMMAND_SPECS: readonly CommandSpec[] = SIZE_SPECS.flatMap((spec) => [
|
|
|
114
118
|
{ ...spec, command: spec.closeCommand, tokenKind: 'close' as const },
|
|
115
119
|
]).sort((left, right) => right.command.length - left.command.length);
|
|
116
120
|
|
|
117
|
-
const SUGGESTION_BY_RANK = new Map(
|
|
118
|
-
SIZE_SPECS.filter((spec) => spec.rank >= 2).map((spec) => [spec.rank, spec]),
|
|
119
|
-
);
|
|
120
|
-
|
|
121
121
|
function delimiterAt(
|
|
122
122
|
latex: string,
|
|
123
123
|
offset: number,
|
|
@@ -226,31 +226,22 @@ function lineAndColumn(
|
|
|
226
226
|
};
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
|
|
230
|
-
return SUGGESTION_BY_RANK.get(rank)?.name ?? 'Bigg';
|
|
231
|
-
}
|
|
229
|
+
const SPEC_BY_SIZE = new Map(SIZE_SPECS.map((spec) => [spec.name, spec]));
|
|
232
230
|
|
|
233
|
-
function suggestionFor(
|
|
234
|
-
pair
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const close = pair.close;
|
|
238
|
-
const spec = SUGGESTION_BY_RANK.get(requiredRank);
|
|
239
|
-
if (!close || !spec || spec.name === 'automatic' || spec.name === 'big') {
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
size: spec.name,
|
|
244
|
-
open: `${spec.openCommand}${pair.open.delimiter}`,
|
|
245
|
-
close: `${spec.closeCommand}${close.delimiter}`,
|
|
246
|
-
latex: `${spec.openCommand}${pair.open.delimiter} ... ${spec.closeCommand}${close.delimiter}`,
|
|
247
|
-
};
|
|
231
|
+
function suggestionFor(pair: DelimiterPair): DelimiterSizeSuggestion {
|
|
232
|
+
const open = `\\left${pair.open.delimiter}`;
|
|
233
|
+
const close = `\\right${pair.close?.delimiter ?? pair.open.delimiter}`;
|
|
234
|
+
return { open, close, latex: `${open} ... ${close}` };
|
|
248
235
|
}
|
|
249
236
|
|
|
250
237
|
/**
|
|
251
|
-
* Warn when a
|
|
252
|
-
*
|
|
253
|
-
*
|
|
238
|
+
* Warn when a pair that takes part in a nest of scalable delimiters is sized
|
|
239
|
+
* by hand instead of by `\left`/`\right`.
|
|
240
|
+
*
|
|
241
|
+
* A pair takes part in a nest when it contains, or sits inside, another pair
|
|
242
|
+
* this linter tracks. A lone hand-sized pair around otherwise bare content —
|
|
243
|
+
* `\bigl(x(t),\ y(t)\bigr)` for a tuple — is a deliberate typographic
|
|
244
|
+
* choice at a single level, has no hierarchy to get wrong, and is left alone.
|
|
254
245
|
*
|
|
255
246
|
* Only `()`, `[]`, and `\{\}` are parsed. Bare delimiters are intentionally
|
|
256
247
|
* ignored: N15 owns that separate error, while ordinary semantic notation
|
|
@@ -261,41 +252,29 @@ export function lintDelimiterSizes(latex: string): DelimiterSizeDiagnostic[] {
|
|
|
261
252
|
if (!roots) return [];
|
|
262
253
|
const diagnostics: DelimiterSizeDiagnostic[] = [];
|
|
263
254
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
for (const child of pair.children) {
|
|
268
|
-
const childRequired = assess(child);
|
|
269
|
-
if (childRequired > largestNestedRequirement) {
|
|
270
|
-
largestNestedRequirement = childRequired;
|
|
271
|
-
representativeNestedSize = sizeName(childRequired);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const requiredRank =
|
|
276
|
-
largestNestedRequirement === 0
|
|
277
|
-
? pair.open.rank
|
|
278
|
-
: largestNestedRequirement + 1;
|
|
279
|
-
pair.requiredRank = Math.max(pair.open.rank, requiredRank);
|
|
280
|
-
if (largestNestedRequirement > 0 && pair.open.rank < requiredRank) {
|
|
255
|
+
const visit = (pair: DelimiterPair, nested: boolean): void => {
|
|
256
|
+
const inNest = nested || pair.children.length > 0;
|
|
257
|
+
if (inNest && pair.open.pairKind === 'manual') {
|
|
281
258
|
const location = lineAndColumn(latex, pair.open.offset);
|
|
282
|
-
const suggestion = suggestionFor(pair
|
|
259
|
+
const suggestion = suggestionFor(pair);
|
|
283
260
|
diagnostics.push({
|
|
284
261
|
code: DELIMITER_SIZE_DIAGNOSTIC_CODE,
|
|
285
262
|
severity: 'warning',
|
|
286
263
|
offset: pair.open.offset,
|
|
287
264
|
...location,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
265
|
+
size: pair.open.size as Exclude<DelimiterSizeName, 'automatic'>,
|
|
266
|
+
suggestion,
|
|
267
|
+
message:
|
|
268
|
+
`A nested delimiter pair is sized by hand with ` +
|
|
269
|
+
`${SPEC_BY_SIZE.get(pair.open.size)?.openCommand ?? pair.open.size}` +
|
|
270
|
+
`${pair.open.delimiter}. Use ` +
|
|
271
|
+
`${suggestion.latex} and let the engine size the nest; preserve the ` +
|
|
272
|
+
`original opening and closing delimiter shapes (rule N17).`,
|
|
294
273
|
});
|
|
295
274
|
}
|
|
296
|
-
|
|
275
|
+
for (const child of pair.children) visit(child, true);
|
|
297
276
|
};
|
|
298
277
|
|
|
299
|
-
for (const root of roots)
|
|
278
|
+
for (const root of roots) visit(root, false);
|
|
300
279
|
return diagnostics.sort((left, right) => left.offset - right.offset);
|
|
301
280
|
}
|
|
@@ -11,7 +11,8 @@ export type DiagramDiagnosticCode =
|
|
|
11
11
|
| 'dia-invalid-key'
|
|
12
12
|
| 'dia-duplicate-key'
|
|
13
13
|
| 'dia-ref-resolves'
|
|
14
|
-
| 'dia-key-unused'
|
|
14
|
+
| 'dia-key-unused'
|
|
15
|
+
| 'dia-removed-prop';
|
|
15
16
|
|
|
16
17
|
export interface DiagramDiagnostic {
|
|
17
18
|
readonly code: DiagramDiagnosticCode;
|
|
@@ -29,6 +30,13 @@ export interface DiagramValidation extends KeyedFloatValidation {
|
|
|
29
30
|
readonly diagnostics: readonly DiagramDiagnostic[];
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The fix for a compaction prop: a diagram is always its graph and then its
|
|
35
|
+
* caption line (docs/plans/figures-and-short-captions.md, decision 7).
|
|
36
|
+
*/
|
|
37
|
+
export const DIAGRAM_NO_LONGER_FOLDS =
|
|
38
|
+
'a diagram no longer folds by itself — delete the prop. To let a reader fold it away, wrap it in a `<Fold>` or put it in a collapsible section.';
|
|
39
|
+
|
|
32
40
|
export function validateDiagrams(
|
|
33
41
|
lessons: readonly DiagramLessonInput[],
|
|
34
42
|
): DiagramValidation {
|
|
@@ -36,5 +44,13 @@ export function validateDiagrams(
|
|
|
36
44
|
tagName: 'Diagram',
|
|
37
45
|
prefix: 'dia',
|
|
38
46
|
noun: 'diagram',
|
|
47
|
+
removedProps: {
|
|
48
|
+
label:
|
|
49
|
+
'rename it `caption` (shown below the diagram with its number), and add an optional `short` caption if the caption is long.',
|
|
50
|
+
title:
|
|
51
|
+
'a diagram is named by its `caption` (shown below it with its number) and an optional `short` caption.',
|
|
52
|
+
defaultOpen: DIAGRAM_NO_LONGER_FOLDS,
|
|
53
|
+
rememberState: DIAGRAM_NO_LONGER_FOLDS,
|
|
54
|
+
},
|
|
39
55
|
}) as DiagramValidation;
|
|
40
56
|
}
|
|
@@ -15,6 +15,8 @@ import { parseEquationRef, scanEquationLabels } from './equations.mjs';
|
|
|
15
15
|
export interface EquationLabelRecord {
|
|
16
16
|
readonly key: string;
|
|
17
17
|
readonly number: string;
|
|
18
|
+
/** The equation's `\caption{…}` short caption, when it has one. */
|
|
19
|
+
readonly short?: string;
|
|
18
20
|
readonly lessonId: string;
|
|
19
21
|
readonly slug: string;
|
|
20
22
|
readonly section: number;
|
|
@@ -54,12 +56,13 @@ export interface EquationLessonInput {
|
|
|
54
56
|
readonly file: string;
|
|
55
57
|
readonly body: string;
|
|
56
58
|
/**
|
|
57
|
-
* `{ prefix, offset }` — the
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* legacy `##`-section
|
|
59
|
+
* `{ prefix, offset }` — the lesson's float-number prefix (`‹part›.‹chapter›`,
|
|
60
|
+
* or `‹part›` under `numbering.floats: "part"`) and how many display
|
|
61
|
+
* equations precede the lesson in that scope. When set, equations are
|
|
62
|
+
* numbered `‹prefix›.‹offset + n›`; when omitted, the legacy `##`-section
|
|
63
|
+
* scheme. See `docs/authoring/structure.md`.
|
|
61
64
|
*/
|
|
62
|
-
readonly numbering?: { readonly prefix: number; readonly offset: number };
|
|
65
|
+
readonly numbering?: { readonly prefix: number | string; readonly offset: number };
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
const BRACKET_REF = /\[\[\s*([^\][]+?)\s*\]\]/g;
|
|
@@ -100,6 +103,7 @@ export function validateEquations(
|
|
|
100
103
|
for (const raw of scanEquationLabels(lesson.body, lesson.numbering) as {
|
|
101
104
|
key: string;
|
|
102
105
|
number: string;
|
|
106
|
+
short: string | null;
|
|
103
107
|
section: number;
|
|
104
108
|
indexInSection: number;
|
|
105
109
|
}[]) {
|
|
@@ -119,6 +123,7 @@ export function validateEquations(
|
|
|
119
123
|
labels.push({
|
|
120
124
|
key: raw.key,
|
|
121
125
|
number: raw.number,
|
|
126
|
+
...(raw.short ? { short: raw.short } : {}),
|
|
122
127
|
lessonId: lesson.lessonId,
|
|
123
128
|
slug: lesson.slug,
|
|
124
129
|
section: raw.section,
|