explico 0.13.0 → 0.14.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 +894 -1
- package/README.md +200 -30
- package/dist/compiler/astro.d.ts +15 -2
- package/dist/compiler/astro.js +40 -11
- package/dist/compiler/cli-main.d.ts +43 -0
- package/dist/compiler/cli-main.js +392 -0
- package/dist/compiler/cli.d.ts +424 -9
- package/dist/compiler/cli.js +1289 -8
- package/dist/compiler/collections.d.ts +88 -1
- package/dist/compiler/collections.js +170 -25
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +105 -1
- package/dist/compiler/decks.d.ts +161 -0
- package/dist/compiler/decks.js +235 -0
- package/dist/compiler/exports/combined-html.d.ts +5 -1
- package/dist/compiler/exports/deck-fit.d.ts +55 -0
- package/dist/compiler/exports/deck-fit.js +90 -0
- package/dist/compiler/exports/export-chapters.d.ts +37 -6
- package/dist/compiler/exports/export-chapters.js +64 -5
- package/dist/compiler/exports/export-pages.d.ts +30 -3
- package/dist/compiler/exports/export-pages.js +24 -1
- package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/dist/compiler/exports/latex/cache.d.ts +78 -0
- package/dist/compiler/exports/latex/cache.js +129 -0
- package/dist/compiler/exports/latex/capture.d.ts +10 -1
- package/dist/compiler/exports/latex/capture.js +3 -1
- package/dist/compiler/exports/latex/compile.d.ts +13 -2
- package/dist/compiler/exports/latex/compile.js +12 -1
- package/dist/compiler/exports/latex/deck.d.ts +92 -0
- package/dist/compiler/exports/latex/deck.js +349 -0
- package/dist/compiler/exports/latex/document.d.ts +51 -6
- package/dist/compiler/exports/latex/document.js +71 -12
- package/dist/compiler/exports/latex/explico-common.sty +198 -0
- package/dist/compiler/exports/latex/explico.cls +13 -183
- package/dist/compiler/exports/latex/serialize.d.ts +60 -4
- package/dist/compiler/exports/latex/serialize.js +4 -2
- package/dist/compiler/exports/latex-export.d.ts +24 -2
- package/dist/compiler/exports/latex-export.js +107 -26
- package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
- package/dist/compiler/exports/markdown/serialize.js +85 -8
- package/dist/compiler/exports/markdown-export.d.ts +6 -1
- package/dist/compiler/exports/markdown-export.js +50 -11
- package/dist/compiler/exports/standalone-html.d.ts +10 -5
- package/dist/compiler/exports/standalone-html.js +3 -3
- package/dist/compiler/exports/static-tree.d.ts +38 -1
- package/dist/compiler/exports/static-tree.js +120 -4
- package/dist/compiler/knowledge-version.d.ts +26 -2
- package/dist/compiler/knowledge-version.js +27 -17
- package/dist/compiler/lesson-derivation.d.ts +143 -6
- package/dist/compiler/lesson-derivation.js +145 -13
- package/dist/compiler/manifest.d.ts +133 -4
- package/dist/compiler/manifest.js +359 -58
- package/dist/compiler/page-versions.d.ts +160 -0
- package/dist/compiler/page-versions.js +198 -0
- package/dist/compiler/render-diagnostics.d.ts +5 -1
- package/dist/compiler/resolution-report.d.ts +24 -6
- package/dist/compiler/resolution-report.js +22 -4
- package/dist/compiler/validate-content.d.ts +7 -0
- package/dist/compiler/validate-content.js +160 -0
- package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
- package/dist/curriculum/competency-graph.d.ts +54 -0
- package/dist/curriculum/competency-graph.js +41 -0
- package/dist/reference/base-library.d.mts +8 -0
- package/dist/reference/base-library.mjs +19 -0
- package/dist/reference/consistency.d.ts +8 -4
- package/dist/reference/consistency.js +76 -26
- package/dist/reference/deck-remark.d.mts +123 -0
- package/dist/reference/deck-remark.mjs +600 -0
- package/dist/reference/deck-source.d.ts +107 -0
- package/dist/reference/deck-source.js +140 -0
- package/dist/reference/decks-validate.d.ts +133 -0
- package/dist/reference/decks-validate.js +359 -0
- package/dist/reference/derived-entries.d.ts +5 -1
- package/dist/reference/diagram-fit.d.ts +1 -1
- package/dist/reference/figure-symbols.d.ts +23 -6
- package/dist/reference/figure-symbols.js +51 -15
- package/dist/reference/float-name.d.ts +43 -0
- package/dist/reference/float-name.js +58 -0
- package/dist/reference/folds-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-indices.js +5 -4
- package/dist/reference/gate-math.d.ts +29 -2
- package/dist/reference/gate-math.js +99 -18
- package/dist/reference/gate-part-pages.d.ts +1 -1
- package/dist/reference/lab-katex-validate.d.ts +10 -6
- package/dist/reference/lab-katex-validate.js +17 -9
- package/dist/reference/lab-math-scope.js +29 -4
- package/dist/reference/math-glyphs.d.mts +25 -0
- package/dist/reference/math-glyphs.mjs +189 -25
- package/dist/reference/page-notation.d.ts +22 -3
- package/dist/reference/page-notation.js +32 -10
- package/dist/reference/references.d.ts +14 -1
- package/dist/reference/references.js +13 -0
- package/dist/reference/registry-scope.d.mts +33 -0
- package/dist/reference/registry-scope.mjs +34 -0
- package/dist/reference/registry.js +29 -10
- package/dist/reference/rehype-deck.d.mts +6 -0
- package/dist/reference/rehype-deck.mjs +286 -0
- package/dist/reference/rehype-heading-anchors.d.mts +20 -0
- package/dist/reference/rehype-heading-anchors.mjs +48 -6
- package/dist/reference/remark-notation.mjs +249 -40
- package/dist/reference/source-lines.d.mts +12 -0
- package/dist/reference/source-lines.mjs +65 -0
- package/dist/reference/types.d.ts +28 -0
- package/dist/rules/catalog.d.ts +21 -4
- package/dist/rules/catalog.js +221 -2
- package/dist/rules/course-rules.d.ts +5 -1
- package/dist/rules/course-rules.js +17 -3
- package/dist/rules/course-rules.schema.json +12 -0
- package/dist/rules/in-effect.d.ts +13 -3
- package/dist/rules/in-effect.js +30 -7
- package/dist/rules/rules.schema.json +259 -1
- package/dist/rules/settings.d.ts +1 -1
- package/dist/session/PreferenceStore.d.ts +6 -0
- package/dist/session/PreferenceStore.js +18 -1
- package/dist/session/deck-memory.d.ts +116 -0
- package/dist/session/deck-memory.js +143 -0
- package/dist/session/reading-memory.d.ts +96 -0
- package/dist/session/reading-memory.js +133 -0
- package/docs/VERSIONING.md +21 -18
- package/docs/authoring/README.md +29 -9
- package/docs/authoring/agents.md +63 -16
- package/docs/authoring/components.md +15 -0
- package/docs/authoring/deck.md +588 -0
- package/docs/authoring/migrating.md +1087 -14
- package/docs/authoring/notation.md +49 -6
- package/docs/authoring/structure.md +80 -1
- package/docs/authoring/versioning.md +54 -2
- package/docs/authoring/visualizations.md +7 -2
- package/docs/course-agent/AGENTS.md +37 -0
- package/docs/course-agent/agents/rules-reviewer.md +33 -0
- package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
- package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
- package/docs/course-agent/skills/release-content/SKILL.md +24 -0
- package/docs/diagram-scaling.md +148 -0
- package/docs/privacy.md +59 -0
- package/package.json +8 -2
- package/src/compiler/astro.ts +60 -21
- package/src/compiler/cli-main.ts +469 -0
- package/src/compiler/cli.ts +1905 -16
- package/src/compiler/collections.ts +284 -25
- package/src/compiler/course-config.ts +267 -0
- package/src/compiler/decks.ts +455 -0
- package/src/compiler/exports/deck-fit.ts +127 -0
- package/src/compiler/exports/export-chapters.ts +105 -6
- package/src/compiler/exports/export-pages.ts +46 -2
- package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/src/compiler/exports/latex/cache.ts +183 -0
- package/src/compiler/exports/latex/capture.ts +5 -2
- package/src/compiler/exports/latex/compile.ts +18 -0
- package/src/compiler/exports/latex/deck.ts +419 -0
- package/src/compiler/exports/latex/document.ts +117 -21
- package/src/compiler/exports/latex/explico-common.sty +198 -0
- package/src/compiler/exports/latex/explico.cls +13 -183
- package/src/compiler/exports/latex/serialize.ts +25 -21
- package/src/compiler/exports/latex/texlive-packages.txt +6 -2
- package/src/compiler/exports/latex-export.ts +187 -45
- package/src/compiler/exports/markdown/serialize.ts +118 -9
- package/src/compiler/exports/markdown-export.ts +56 -11
- package/src/compiler/exports/standalone-html.ts +8 -7
- package/src/compiler/exports/static-tree.ts +175 -3
- package/src/compiler/knowledge-version.ts +56 -21
- package/src/compiler/lesson-derivation.ts +282 -16
- package/src/compiler/manifest.ts +558 -63
- package/src/compiler/page-versions.ts +359 -0
- package/src/compiler/resolution-report.ts +38 -7
- package/src/compiler/validate-content.ts +212 -0
- package/src/components/CourseContents.astro +124 -6
- package/src/components/CurriculumMap.astro +49 -1
- package/src/components/FloatIndex.astro +1 -1
- package/src/components/PartOverview.astro +51 -5
- package/src/components/assessments/AssessmentSet.astro +41 -15
- package/src/components/block-heading.ts +15 -6
- package/src/components/card-carry.ts +7 -4
- package/src/components/card-manager.ts +14 -2
- package/src/components/card-memory.ts +13 -9
- package/src/components/deck/DeckEnhancer.astro +13 -0
- package/src/components/deck/deck-context.ts +104 -0
- package/src/components/deck/deck-controller.ts +1287 -0
- package/src/components/deck/deck-map.ts +91 -0
- package/src/components/deck-lane.ts +163 -0
- package/src/components/diagram/Diagram.astro +131 -86
- package/src/components/disclosure/persisted-disclosure.ts +9 -1
- package/src/components/figure/Figure.astro +7 -2
- package/src/components/figure/FloatCaption.astro +5 -3
- package/src/components/fold/Fold.astro +2 -2
- package/src/components/notation/NotationFormula.astro +81 -11
- package/src/components/notation/NotationGlossary.astro +132 -1
- package/src/components/notation/NotationLayer.astro +29 -0
- package/src/components/starlight/AboutPanel.astro +186 -3
- package/src/components/starlight/CollapsibleSections.astro +24 -3
- package/src/components/starlight/DiagramEnhancer.astro +5 -1
- package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
- package/src/components/starlight/LayoutHeader.astro +17 -0
- package/src/components/starlight/LayoutPageTitle.astro +110 -0
- package/src/components/starlight/LayoutSidebar.astro +807 -21
- package/src/components/starlight/LayoutSocialIcons.astro +62 -6
- package/src/components/starlight/LessonExportActions.astro +27 -9
- package/src/components/starlight/LessonFooter.astro +59 -0
- package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
- package/src/components/starlight/TextSizeControl.astro +24 -13
- package/src/components/starlight/page-marks.ts +139 -0
- package/src/components/starlight/section-summary.ts +1 -1
- package/src/components/starlight/sidebar-editions.ts +245 -0
- package/src/components/table/Table.astro +15 -7
- package/src/content-config.ts +87 -1
- package/src/curriculum/competency-graph.ts +95 -0
- package/src/reference/base-library.mjs +19 -0
- package/src/reference/consistency.ts +126 -39
- package/src/reference/deck-remark.mjs +600 -0
- package/src/reference/deck-source.ts +273 -0
- package/src/reference/decks-validate.ts +604 -0
- package/src/reference/diagram-layout.ts +3 -1
- package/src/reference/figure-symbols.ts +108 -31
- package/src/reference/folds-validate.ts +1 -1
- package/src/reference/gate-indices.ts +5 -3
- package/src/reference/gate-math.ts +169 -25
- package/src/reference/lab-katex-validate.ts +30 -15
- package/src/reference/lab-math-scope.ts +34 -3
- package/src/reference/math-glyphs.mjs +189 -25
- package/src/reference/page-notation.ts +69 -15
- package/src/reference/references.ts +28 -0
- package/src/reference/registry-scope.mjs +34 -0
- package/src/reference/registry.ts +41 -11
- package/src/reference/rehype-deck.mjs +286 -0
- package/src/reference/rehype-heading-anchors.mjs +48 -6
- package/src/reference/remark-notation.mjs +249 -40
- package/src/reference/source-lines.mjs +65 -0
- package/src/reference/types.ts +28 -0
- package/src/rules/catalog.ts +257 -4
- package/src/rules/course-rules.ts +21 -1
- package/src/rules/in-effect.ts +54 -8
- package/src/session/PreferenceStore.ts +23 -0
- package/src/session/deck-memory.ts +263 -0
- package/src/session/reading-memory.ts +243 -0
- package/src/styles/deck.css +1150 -0
- package/src/styles/global.css +958 -27
- package/src/styles/zones.css +277 -0
- /package/src/{components → reference}/float-name.ts +0 -0
|
@@ -11,13 +11,22 @@
|
|
|
11
11
|
* `figure.keyed-figure` (a lab's tools included), every letter KaTeX draws
|
|
12
12
|
* must sit in a `[data-notation-key]`. Upright text is not a symbol:
|
|
13
13
|
* `\text{…}`, `\mathrm{…}` (the resolver skips its body, rule N10) and a
|
|
14
|
-
* named operator (`\sin`, `\max`).
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* named operator (`\sin`, `\max`). Nor is a plain base-library atom (`d`,
|
|
15
|
+
* `e`, `i`, `\pi`; `base-library.mjs`), which the resolver leaves without a
|
|
16
|
+
* card in lab math as in a lesson's, because every reader knows it — in a
|
|
17
|
+
* power too (`e^{i\pi}`). A styled one (`\mathbf{e}`), a decorated one
|
|
18
|
+
* (`\hat{e}`, rule N22) and an index (`x_i`, `e_i`, rule N23) are other
|
|
19
|
+
* symbols and still need a card; a subscript is told from a power by
|
|
20
|
+
* KaTeX's `vlist-t2`, the vertical list that has a lowered row.
|
|
21
|
+
* `compiler/astro.ts` runs it on every built page, and a course's browser
|
|
22
|
+
* test can run it on a hydrated figure's `outerHTML`, which also holds what
|
|
23
|
+
* an island draws only in the browser.
|
|
17
24
|
*/
|
|
18
25
|
|
|
19
26
|
import { parse, type DefaultTreeAdapterMap } from 'parse5';
|
|
20
27
|
|
|
28
|
+
import { isBaseLibraryGlyph } from './base-library.mjs';
|
|
29
|
+
|
|
21
30
|
type HtmlNode = DefaultTreeAdapterMap['node'];
|
|
22
31
|
type HtmlElement = DefaultTreeAdapterMap['element'];
|
|
23
32
|
|
|
@@ -30,6 +39,10 @@ export interface UnboundFigureSymbols {
|
|
|
30
39
|
|
|
31
40
|
/** KaTeX classes of upright text, which is never a symbol. */
|
|
32
41
|
const UPRIGHT = new Set(['text', 'mathrm', 'textrm', 'mop']);
|
|
42
|
+
/** KaTeX classes of a letter in its plain italic form, `\mathit{e}` included. */
|
|
43
|
+
const PLAIN = new Set(['mathnormal', 'mathit']);
|
|
44
|
+
/** KaTeX class of an accent: the letter under it is not the plain atom. */
|
|
45
|
+
const DECORATED = new Set(['accent']);
|
|
33
46
|
const LETTER = /\p{L}/u;
|
|
34
47
|
|
|
35
48
|
function isElement(node: HtmlNode): node is HtmlElement {
|
|
@@ -59,59 +72,123 @@ export function unboundFigureSymbols(html: string): UnboundFigureSymbols[] {
|
|
|
59
72
|
if (!html.includes('keyed-figure') || !html.includes('katex-html')) return [];
|
|
60
73
|
const found = new Map<string, string[]>();
|
|
61
74
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
75
|
+
interface Context {
|
|
76
|
+
readonly figure: string | undefined;
|
|
77
|
+
readonly inKatex: boolean;
|
|
78
|
+
readonly covered: boolean;
|
|
79
|
+
/** Under an accent or in a subscript: not the plain atom. */
|
|
80
|
+
readonly decorated: boolean;
|
|
81
|
+
/** The element that holds this text draws it in plain italic. */
|
|
82
|
+
readonly plain: boolean;
|
|
83
|
+
/** In an `msupsub` with a subscript (`vlist-t2`), whose first row it is. */
|
|
84
|
+
readonly lowered: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const visit = (node: HtmlNode, context: Context, row?: number): void => {
|
|
88
|
+
let next = context;
|
|
68
89
|
if (isElement(node)) {
|
|
69
90
|
const names = classes(node);
|
|
91
|
+
next = {
|
|
92
|
+
...context,
|
|
93
|
+
// A letter's own element says how it is drawn; its ancestors do not.
|
|
94
|
+
plain: names.some((name) => PLAIN.has(name)),
|
|
95
|
+
// Only the rows of a script's own vertical list are its sub and sup.
|
|
96
|
+
lowered: names.includes('msupsub')
|
|
97
|
+
? childrenOf(node).some((child) => isElement(child) && classes(child).includes('vlist-t2'))
|
|
98
|
+
: row === undefined
|
|
99
|
+
? context.lowered
|
|
100
|
+
: false,
|
|
101
|
+
};
|
|
70
102
|
if (node.tagName === 'figure' && names.includes('keyed-figure')) {
|
|
71
|
-
|
|
103
|
+
next = { ...next, figure: attribute(node, 'id') ?? '(a figure with no id)' };
|
|
104
|
+
}
|
|
105
|
+
if (names.includes('katex-html')) next = { ...next, inKatex: true };
|
|
106
|
+
if (names.some((name) => DECORATED.has(name)) || (row === 0 && context.lowered)) {
|
|
107
|
+
next = { ...next, decorated: true };
|
|
72
108
|
}
|
|
73
|
-
if (names.includes('katex-html')) inKatex = true;
|
|
74
109
|
if (
|
|
75
110
|
attribute(node, 'data-notation-key') !== undefined ||
|
|
76
|
-
(inKatex && names.some((name) => UPRIGHT.has(name)))
|
|
111
|
+
(next.inKatex && names.some((name) => UPRIGHT.has(name)))
|
|
77
112
|
) {
|
|
78
|
-
|
|
113
|
+
next = { ...next, covered: true };
|
|
79
114
|
}
|
|
80
115
|
} else if (
|
|
81
116
|
node.nodeName === '#text' &&
|
|
82
|
-
figure !== undefined &&
|
|
83
|
-
inKatex &&
|
|
84
|
-
!covered
|
|
117
|
+
context.figure !== undefined &&
|
|
118
|
+
context.inKatex &&
|
|
119
|
+
!context.covered
|
|
85
120
|
) {
|
|
86
121
|
const text = (node as DefaultTreeAdapterMap['textNode']).value.trim();
|
|
87
|
-
|
|
88
|
-
|
|
122
|
+
// KaTeX draws a run of plain letters as one text node (`iπ` of `e^{i\pi}`).
|
|
123
|
+
const library =
|
|
124
|
+
context.plain &&
|
|
125
|
+
!context.decorated &&
|
|
126
|
+
[...text].every((glyph) => !LETTER.test(glyph) || isBaseLibraryGlyph(glyph));
|
|
127
|
+
if (LETTER.test(text) && !library) {
|
|
128
|
+
const symbols = found.get(context.figure) ?? [];
|
|
89
129
|
if (!symbols.includes(text)) symbols.push(text);
|
|
90
|
-
found.set(figure, symbols);
|
|
130
|
+
found.set(context.figure, symbols);
|
|
91
131
|
}
|
|
92
132
|
}
|
|
93
|
-
|
|
133
|
+
// The rows of a `vlist` are its element children; the first of a lowered
|
|
134
|
+
// one is the subscript, the second a superscript over it.
|
|
135
|
+
const isVlist = isElement(node) && classes(node).includes('vlist');
|
|
136
|
+
let rows = 0;
|
|
137
|
+
for (const child of childrenOf(node)) {
|
|
138
|
+
const isRow = isVlist && isElement(child);
|
|
139
|
+
visit(child, next, isRow ? rows : undefined);
|
|
140
|
+
if (isRow) rows += 1;
|
|
141
|
+
}
|
|
94
142
|
};
|
|
95
143
|
|
|
96
|
-
visit(parse(html),
|
|
144
|
+
visit(parse(html), {
|
|
145
|
+
figure: undefined,
|
|
146
|
+
inKatex: false,
|
|
147
|
+
covered: false,
|
|
148
|
+
decorated: false,
|
|
149
|
+
plain: false,
|
|
150
|
+
lowered: false,
|
|
151
|
+
});
|
|
97
152
|
return [...found].map(([figure, symbols]) => ({ figure, symbols }));
|
|
98
153
|
}
|
|
99
154
|
|
|
155
|
+
/** One rendered page and the figures on it that draw a symbol with no card. */
|
|
156
|
+
export interface PageUnboundFigureSymbols {
|
|
157
|
+
/** The page's path in the build output, e.g. `lecture-5/chernoff/index.html`. */
|
|
158
|
+
readonly page: string;
|
|
159
|
+
readonly figures: readonly UnboundFigureSymbols[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const describeFigures = (figures: readonly UnboundFigureSymbols[]): string =>
|
|
163
|
+
figures
|
|
164
|
+
.map(({ figure, symbols }) => `#${figure} shows ${symbols.map((s) => `"${s}"`).join(', ')}`)
|
|
165
|
+
.join('; ');
|
|
166
|
+
|
|
100
167
|
/**
|
|
101
|
-
* Fail a build
|
|
102
|
-
* card, naming
|
|
168
|
+
* Fail a build over the pages that show a symbol in a figure that opens no
|
|
169
|
+
* card, naming every page, its figures and symbols, and the way to bind them.
|
|
170
|
+
* A build collects the whole list first (`compiler/astro.ts`), so it reports
|
|
171
|
+
* every page at once instead of one per run.
|
|
103
172
|
*/
|
|
104
|
-
export function
|
|
105
|
-
const
|
|
106
|
-
|
|
173
|
+
export function assertNoUnboundFigureSymbols(pages: readonly PageUnboundFigureSymbols[]): void {
|
|
174
|
+
const found = pages.filter(({ figures }) => figures.length > 0);
|
|
175
|
+
const [only] = found;
|
|
176
|
+
if (!only) return;
|
|
177
|
+
const head =
|
|
178
|
+
found.length === 1
|
|
179
|
+
? `Explico figure symbols without a notation card on rendered page "${only.page}": ` +
|
|
180
|
+
describeFigures(only.figures)
|
|
181
|
+
: `Explico figure symbols without a notation card on ${found.length} rendered pages:\n` +
|
|
182
|
+
found.map(({ page, figures }) => ` "${page}": ${describeFigures(figures)}`).join('\n');
|
|
107
183
|
throw new Error(
|
|
108
|
-
|
|
109
|
-
unbound
|
|
110
|
-
.map(({ figure, symbols }) => `#${figure} shows ${symbols.map((s) => `"${s}"`).join(', ')}`)
|
|
111
|
-
.join('; ') +
|
|
112
|
-
'. Every symbol a figure shows opens its card (rule N4): render its math ' +
|
|
184
|
+
`${head}. Every symbol a figure shows opens its card (rule N4): render its math ` +
|
|
113
185
|
'with renderLabMath (`{ display: false }` for a legend, an axis title or ' +
|
|
114
186
|
'a label; a `{ math }` run for the caption clause), never with katex ' +
|
|
115
187
|
'directly. Write a unit as `\\text{…}`.',
|
|
116
188
|
);
|
|
117
189
|
}
|
|
190
|
+
|
|
191
|
+
/** Fail a build whose rendered page shows a symbol in a figure that opens no card. */
|
|
192
|
+
export function assertFigureSymbolsBound(page: string, html: string): void {
|
|
193
|
+
assertNoUnboundFigureSymbols([{ page, figures: unboundFigureSymbols(html) }]);
|
|
194
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `<Fold>` validation, run inside `compileManifest()`
|
|
3
3
|
* (docs/plans/figures-and-short-captions.md, decision 11). A fold's bar is
|
|
4
4
|
* its `label` or, without one, the numbers and short captions of the floats
|
|
5
|
-
* it holds (
|
|
5
|
+
* it holds (`./float-name.ts`), so a fold needs one or the other:
|
|
6
6
|
*
|
|
7
7
|
* - `fold-unnamed` (error): a `<Fold>` with no `label` and no `<Figure>`,
|
|
8
8
|
* `<Table>` or `<Diagram>` that takes a number inside it — its bar would
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
type LessonAssessmentLink,
|
|
35
35
|
type MathGateDiagnostic,
|
|
36
36
|
type ScopeEntry,
|
|
37
|
+
scopeLocals,
|
|
37
38
|
} from './gate-math';
|
|
38
39
|
import { formulaGlyphScope, type GlyphScopeEntry, type ResolvedGloss } from './gloss';
|
|
39
40
|
import { indexOfEntry } from './math-glyphs.mjs';
|
|
@@ -136,7 +137,8 @@ function message(
|
|
|
136
137
|
`${lessonId}: the running index ${details.letter} (first in ${JSON.stringify(details.token)}) ` +
|
|
137
138
|
`has no stated set. Give ${details.where} a range (range: "1, \\\\dots, n"), state it after a ` +
|
|
138
139
|
`\\quad on the first display line that uses it (\\quad ${details.letter} = 1, \\dots, n), or ` +
|
|
139
|
-
`mark the card fixedIndex: true if ${details.letter} is a label there, not a running index
|
|
140
|
+
`mark the card fixedIndex: true if ${details.letter} is a label there, not a running index; ` +
|
|
141
|
+
`a family indexed by ${details.letter} then explains none of its instances (rule N23).`
|
|
140
142
|
);
|
|
141
143
|
case 'derived-key-taken':
|
|
142
144
|
return (
|
|
@@ -173,7 +175,7 @@ export function gateIndices(
|
|
|
173
175
|
const bodyOffset = (lesson.bodyLine ?? 1) - 1;
|
|
174
176
|
const scope = pageScope(lesson, entries);
|
|
175
177
|
const local = new Map<string, IndexEntryFacts>(
|
|
176
|
-
lesson.
|
|
178
|
+
scopeLocals(lesson).map((definition) => [definition.key, definition]),
|
|
177
179
|
);
|
|
178
180
|
const factsOf = (key: string | null): IndexEntryFacts | undefined =>
|
|
179
181
|
key === null ? undefined : (local.get(key) ?? entries.get(key));
|
|
@@ -325,7 +327,7 @@ export function gateIndices(
|
|
|
325
327
|
if (!lesson) continue;
|
|
326
328
|
const scope = pageScope(lesson, entries);
|
|
327
329
|
const local = new Map<string, IndexEntryFacts>(
|
|
328
|
-
lesson.
|
|
330
|
+
scopeLocals(lesson).map((definition) => [definition.key, definition]),
|
|
329
331
|
);
|
|
330
332
|
const factsOf = (key: string | null): IndexEntryFacts | undefined =>
|
|
331
333
|
key === null ? undefined : (local.get(key) ?? entries.get(key));
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The completeness gate, run from the CLI (`pnpm validate:content`) over every
|
|
3
3
|
* rendered-math context — lesson body `$…$` / `$$…$$` (including math inside
|
|
4
|
-
* component slots), each `notation.local`
|
|
5
|
-
*
|
|
4
|
+
* component slots), a notation entry's Markdown body, each `notation.local`
|
|
5
|
+
* and shared `formula`, and any `$…$` in an assessment `prompt` /
|
|
6
|
+
* `explanation`. It reuses the one resolver the Astro
|
|
6
7
|
* remark pass uses (`resolveMathGlyphs`), so there is a single glyph table and
|
|
7
8
|
* a single diagnostic shape; `astro build` re-runs the same resolver on the
|
|
8
9
|
* render path as a backstop.
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
* Every finding names its source and the fix. N17's size-only findings are
|
|
11
12
|
* warnings and include a column when the math came from lesson/assessment text.
|
|
12
13
|
*/
|
|
13
|
-
import { resolveMathGlyphs, GlyphResolutionError } from './math-glyphs.mjs';
|
|
14
|
+
import { resolveMathGlyphs, GlyphResolutionError, fixedLenderMessage, mathExcerpt } from './math-glyphs.mjs';
|
|
14
15
|
import {
|
|
15
16
|
EQ_LABEL_PATTERN,
|
|
16
17
|
equationAnchorId,
|
|
@@ -25,6 +26,7 @@ import type {
|
|
|
25
26
|
SharedNotationDefinitionInput,
|
|
26
27
|
} from './types';
|
|
27
28
|
import { blankPropStrings } from './gate-prop-text';
|
|
29
|
+
import { REGISTRY_BODY_FIX, registryGlyphScope } from './registry-scope.mjs';
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* The equation a finding sits in, when its math carries a `\label{eq:…}`.
|
|
@@ -77,7 +79,7 @@ export interface MathGateDiagnostic {
|
|
|
77
79
|
* one the engine ships; its severity is the course's choice, so it is the one
|
|
78
80
|
* context whose severity is not fixed here.
|
|
79
81
|
*/
|
|
80
|
-
readonly severity: 'error' | 'warning';
|
|
82
|
+
readonly severity: 'error' | 'warning' | 'info';
|
|
81
83
|
/** Owned by exactly one rule or invariant in `src/rules/catalog.ts`. */
|
|
82
84
|
readonly code: string;
|
|
83
85
|
readonly file: string;
|
|
@@ -192,14 +194,76 @@ export function scopeEntry(definition: {
|
|
|
192
194
|
* assessment's math (`mixed-content.ts`), say — doesn't reconstruct it by
|
|
193
195
|
* hand and risk drifting from what the completeness gate actually checks.
|
|
194
196
|
*/
|
|
197
|
+
/**
|
|
198
|
+
* The `notation.local` entries in scope on a page: its own, and for a deck
|
|
199
|
+
* beside its chapter the chapter's, which are the lesson's
|
|
200
|
+
* (`NotationLessonInput.inheritedLocalDefinitions`).
|
|
201
|
+
*/
|
|
202
|
+
export function scopeLocals(
|
|
203
|
+
lesson: NotationLessonInput,
|
|
204
|
+
): readonly NotationLessonInput['localDefinitions'][number][] {
|
|
205
|
+
const inherited = lesson.inheritedLocalDefinitions ?? [];
|
|
206
|
+
return inherited.length === 0 ? lesson.localDefinitions : [...lesson.localDefinitions, ...inherited];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const EXPLAIN_CALL = /\\explain\{([^{}]+)\}\{((?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*)\}/g;
|
|
210
|
+
|
|
211
|
+
/** Every `\explain{key}{inner}` in a span, in source order, with its offset. */
|
|
212
|
+
export function explainCalls(latex: string): { key: string; inner: string; index: number }[] {
|
|
213
|
+
return [...latex.matchAll(EXPLAIN_CALL)].map((match) => ({
|
|
214
|
+
key: match[1]!,
|
|
215
|
+
inner: match[2]!,
|
|
216
|
+
index: match.index ?? 0,
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The `\explain` wraps in a span that bind nothing new (rule N4): the bare
|
|
222
|
+
* inner form already resolves to the same card, with no glyph left over and
|
|
223
|
+
* no decoration left unexplained (rule N22). Such a wrap costs an author
|
|
224
|
+
* tokens on every use and the resolver nothing; `\explain` is for a compound
|
|
225
|
+
* or a colliding glyph.
|
|
226
|
+
*/
|
|
227
|
+
export function redundantExplainWraps(
|
|
228
|
+
latex: string,
|
|
229
|
+
scope: readonly ScopeEntry[],
|
|
230
|
+
options: { readonly suspended?: readonly string[]; readonly derive?: boolean } = {},
|
|
231
|
+
): { key: string; inner: string; index: number }[] {
|
|
232
|
+
const redundant: { key: string; inner: string; index: number }[] = [];
|
|
233
|
+
for (const call of explainCalls(latex)) {
|
|
234
|
+
let bare;
|
|
235
|
+
try {
|
|
236
|
+
bare = resolveMathGlyphs(call.inner, [...scope], {
|
|
237
|
+
suspended: options.suspended ?? [],
|
|
238
|
+
derive: options.derive ?? false,
|
|
239
|
+
}) as {
|
|
240
|
+
bindings: readonly { key: string; level: string }[];
|
|
241
|
+
unresolved: readonly unknown[];
|
|
242
|
+
decorations: readonly { wholeForm: string | null; operator: string | null }[];
|
|
243
|
+
};
|
|
244
|
+
} catch {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (
|
|
248
|
+
bare.unresolved.length === 0 &&
|
|
249
|
+
bare.bindings.length > 0 &&
|
|
250
|
+
bare.bindings.every((binding) => binding.key === call.key && binding.level !== 'explicit') &&
|
|
251
|
+
bare.decorations.every((decoration) => decoration.wholeForm !== null || decoration.operator !== null)
|
|
252
|
+
) {
|
|
253
|
+
redundant.push(call);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return redundant;
|
|
257
|
+
}
|
|
258
|
+
|
|
195
259
|
export function pageScope(
|
|
196
260
|
lesson: NotationLessonInput,
|
|
197
261
|
sharedByKey: ReadonlyMap<string, ScopeEntry>,
|
|
198
262
|
): ScopeEntry[] {
|
|
199
|
-
const local = lesson.
|
|
263
|
+
const local = scopeLocals(lesson).map(scopeEntry);
|
|
200
264
|
const localKeys = new Set(local.map((entry) => entry.key));
|
|
201
265
|
|
|
202
|
-
const referenced = new Set<string>();
|
|
266
|
+
const referenced = new Set<string>(lesson.inheritedKeys ?? []);
|
|
203
267
|
for (const pattern of [TERM_KEY, EXPLAIN_KEY]) {
|
|
204
268
|
for (const match of lesson.body.matchAll(pattern)) referenced.add(match[1]);
|
|
205
269
|
}
|
|
@@ -237,6 +301,15 @@ export function suspendedOperators(lesson: NotationLessonInput): string[] {
|
|
|
237
301
|
return (lesson.suspended ?? []).map((item) => item.operator);
|
|
238
302
|
}
|
|
239
303
|
|
|
304
|
+
/** A family that would explain a subscripted symbol, were its index not fixed (rule N23). */
|
|
305
|
+
interface FixedLender {
|
|
306
|
+
readonly key: string;
|
|
307
|
+
/** What fixed it: the entry's own `fixedIndex`, a letter card's, or an index entry. */
|
|
308
|
+
readonly via: 'entry' | 'fixedIndex' | 'index-entry';
|
|
309
|
+
readonly card?: string;
|
|
310
|
+
readonly letter?: string;
|
|
311
|
+
}
|
|
312
|
+
|
|
240
313
|
function resolveInto(
|
|
241
314
|
latex: string,
|
|
242
315
|
scope: readonly ScopeEntry[],
|
|
@@ -300,6 +373,24 @@ function resolveInto(
|
|
|
300
373
|
});
|
|
301
374
|
return;
|
|
302
375
|
}
|
|
376
|
+
// Rule N4: an `\explain` wrap whose bare symbol already resolves to the
|
|
377
|
+
// same card binds nothing new. In a formula the wrap is what puts the card
|
|
378
|
+
// in scope (`formulaGlyphScope`), so the check does not apply there.
|
|
379
|
+
if (context !== 'formula') {
|
|
380
|
+
for (const wrap of redundantExplainWraps(stripEquationLabels(latex), scope, { suspended, derive })) {
|
|
381
|
+
out.push({
|
|
382
|
+
context,
|
|
383
|
+
code: 'explain-redundant',
|
|
384
|
+
severity: 'info',
|
|
385
|
+
file,
|
|
386
|
+
line,
|
|
387
|
+
...(column === undefined ? {} : { column }),
|
|
388
|
+
token: wrap.inner,
|
|
389
|
+
...at,
|
|
390
|
+
message: `\\explain{${wrap.key}}{${wrap.inner}} wraps what ${wrap.inner} already resolves to on this page; write it bare — \\explain is for a compound or a colliding glyph (rule N4)`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
303
394
|
for (const item of result.unresolved) {
|
|
304
395
|
const { token } = item;
|
|
305
396
|
if ('suspended' in item && typeof item.suspended === 'string') {
|
|
@@ -320,10 +411,23 @@ function resolveInto(
|
|
|
320
411
|
'font' in item && typeof item.font === 'string'
|
|
321
412
|
? ` ${token} is a different symbol from the plain letter: give it its own entry (latex: ${token}) or wrap it in \\explain{key}{${token}}.`
|
|
322
413
|
: '';
|
|
414
|
+
// Rule N23: a family whose index letter a card fixed would have explained
|
|
415
|
+
// this `r_2`; blaming "a bare r" for it sends the author to the wrong repair.
|
|
416
|
+
const fixedLenders: readonly FixedLender[] =
|
|
417
|
+
'fixedLenders' in item && Array.isArray(item.fixedLenders)
|
|
418
|
+
? (item.fixedLenders as FixedLender[])
|
|
419
|
+
: [];
|
|
420
|
+
const instance = 'instance' in item && typeof item.instance === 'string' ? item.instance : token;
|
|
421
|
+
const fixedHint = fixedLenders.map((lender) => ` ${fixedLenderMessage(lender, instance)}`).join('');
|
|
323
422
|
// Rule N23: an indexed entry with this base explains no bare letter.
|
|
423
|
+
const fixedKeys = new Set(fixedLenders.map((lender) => lender.key));
|
|
424
|
+
const lenders =
|
|
425
|
+
'lenders' in item && Array.isArray(item.lenders)
|
|
426
|
+
? (item.lenders as string[]).filter((key) => !fixedKeys.has(key))
|
|
427
|
+
: [];
|
|
324
428
|
const lenderHint =
|
|
325
|
-
|
|
326
|
-
? ` ${
|
|
429
|
+
lenders.length > 0
|
|
430
|
+
? ` ${lenders.join(', ')} ${lenders.length === 1 ? 'has' : 'have'} ${token} with a subscript, and an indexed entry explains its own instances only, never a bare ${token} (rule N4). If ${token} is a different object, give it its own entry (latex: ${token}); if it is shorthand for that entry, wrap it in \\explain{${lenders[0]}}{${token}}.`
|
|
327
431
|
: '';
|
|
328
432
|
out.push({
|
|
329
433
|
context,
|
|
@@ -333,7 +437,7 @@ function resolveInto(
|
|
|
333
437
|
line,
|
|
334
438
|
token,
|
|
335
439
|
...at,
|
|
336
|
-
message: `Unresolved notation ${JSON.stringify(token)} in ${context} math.${fontHint}${lenderHint} ${fix}`,
|
|
440
|
+
message: `Unresolved notation ${JSON.stringify(token)} in ${context} math, in ${mathExcerpt(latex)}.${fontHint}${fixedHint}${lenderHint} ${fix}`,
|
|
337
441
|
});
|
|
338
442
|
}
|
|
339
443
|
}
|
|
@@ -372,6 +476,31 @@ export function currencyDollars(body: string): MathSpan[] {
|
|
|
372
476
|
return spans;
|
|
373
477
|
}
|
|
374
478
|
|
|
479
|
+
/** A body's `$5 to $6`: money remark-math would render as math. */
|
|
480
|
+
function reportCurrencyDollars(
|
|
481
|
+
body: string,
|
|
482
|
+
file: string,
|
|
483
|
+
bodyOffset: number,
|
|
484
|
+
out: MathGateDiagnostic[],
|
|
485
|
+
): void {
|
|
486
|
+
for (const span of currencyDollars(body)) {
|
|
487
|
+
const shown = span.latex.replace(/\s+$/, '');
|
|
488
|
+
out.push({
|
|
489
|
+
context: 'body',
|
|
490
|
+
code: 'currency-dollar',
|
|
491
|
+
severity: SEVERITY.body,
|
|
492
|
+
file,
|
|
493
|
+
line: span.line + bodyOffset,
|
|
494
|
+
column: span.column,
|
|
495
|
+
token: `$${span.latex}$`,
|
|
496
|
+
message:
|
|
497
|
+
`"$${span.latex}$" reads as math: remark-math pairs any two dollars on a line, so the ` +
|
|
498
|
+
`page shows "${shown}" without them. Write a currency dollar as \\$ (\\$5 to \\$6), ` +
|
|
499
|
+
'or the amount in words.',
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
375
504
|
const BODY_FIX =
|
|
376
505
|
'Introduce the symbol with [[key]], define it in notation.local, or wrap it in \\explain{key}{latex}.';
|
|
377
506
|
function dedupeScope(scope: readonly GlyphScopeEntry[]): GlyphScopeEntry[] {
|
|
@@ -491,22 +620,7 @@ export function gateContentMath(input: {
|
|
|
491
620
|
);
|
|
492
621
|
}
|
|
493
622
|
|
|
494
|
-
|
|
495
|
-
const shown = span.latex.replace(/\s+$/, '');
|
|
496
|
-
diagnostics.push({
|
|
497
|
-
context: 'body',
|
|
498
|
-
code: 'currency-dollar',
|
|
499
|
-
severity: SEVERITY.body,
|
|
500
|
-
file,
|
|
501
|
-
line: span.line + bodyOffset,
|
|
502
|
-
column: span.column,
|
|
503
|
-
token: `$${span.latex}$`,
|
|
504
|
-
message:
|
|
505
|
-
`"$${span.latex}$" reads as math: remark-math pairs any two dollars on a line, so the ` +
|
|
506
|
-
`page shows "${shown}" without them. Write a currency dollar as \\$ (\\$5 to \\$6), ` +
|
|
507
|
-
'or the amount in words.',
|
|
508
|
-
});
|
|
509
|
-
}
|
|
623
|
+
reportCurrencyDollars(lesson.body, file, bodyOffset, diagnostics);
|
|
510
624
|
|
|
511
625
|
for (const definition of lesson.localDefinitions) {
|
|
512
626
|
if (!definition.formula) continue;
|
|
@@ -533,6 +647,36 @@ export function gateContentMath(input: {
|
|
|
533
647
|
}
|
|
534
648
|
}
|
|
535
649
|
|
|
650
|
+
// A shared entry's Markdown body renders in the glossary through the same
|
|
651
|
+
// remark pass as a lesson, against the whole registry (`registry-scope.mjs`).
|
|
652
|
+
// A symbol it misses fails that render, and Astro's content layer only logs
|
|
653
|
+
// the failure and ships the entry without its body, so this is the check
|
|
654
|
+
// that stops the build.
|
|
655
|
+
const registryScope: ScopeEntry[] = registryGlyphScope(
|
|
656
|
+
notationInput.sharedDefinitions,
|
|
657
|
+
);
|
|
658
|
+
for (const definition of notationInput.sharedDefinitions) {
|
|
659
|
+
const file = definition.source.file;
|
|
660
|
+
const bodyOffset = (definition.bodyLine ?? 1) - 1;
|
|
661
|
+
for (const span of mathSpans(definition.body)) {
|
|
662
|
+
resolveInto(
|
|
663
|
+
span.latex,
|
|
664
|
+
registryScope,
|
|
665
|
+
'body',
|
|
666
|
+
file,
|
|
667
|
+
span.line + bodyOffset,
|
|
668
|
+
span.column,
|
|
669
|
+
REGISTRY_BODY_FIX,
|
|
670
|
+
diagnostics,
|
|
671
|
+
undefined,
|
|
672
|
+
// An entry's body suspends nothing and derives no cards.
|
|
673
|
+
[],
|
|
674
|
+
false,
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
reportCurrencyDollars(definition.body, file, bodyOffset, diagnostics);
|
|
678
|
+
}
|
|
679
|
+
|
|
536
680
|
// Shared cards carry their own rigorous `formula`. It resolves against the
|
|
537
681
|
// entry-scoped table (D15) — the card, its glosses, and the cards the
|
|
538
682
|
// formula names with `\explain` — not the registry-wide table the card's
|
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* Starting at each lesson's own imports, this follows every relative import
|
|
8
8
|
* — the course's visuals, wherever they live — and reports each file that
|
|
9
9
|
* imports `katex` as `lab-katex-direct`, with the chain of imports that
|
|
10
|
-
* reaches it.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* reaches it. The answer is per lesson: a helper that three lessons reach is
|
|
11
|
+
* reported three times, each with that lesson's own chain, and a lesson that
|
|
12
|
+
* reaches none is clean — so `content check <lesson>` can say so. A package
|
|
13
|
+
* import (`explico/…`, `react`) is not followed: explico's own KaTeX use is
|
|
14
|
+
* the seam. A type-only import and the KaTeX stylesheet draw nothing and are
|
|
15
|
+
* not reported.
|
|
13
16
|
*
|
|
14
17
|
* `figure-symbols.ts` checks the same promise on the rendered page, which
|
|
15
18
|
* also catches math a lab draws some other way.
|
|
@@ -26,7 +29,7 @@ export interface LabKatexDiagnostic {
|
|
|
26
29
|
readonly message: string;
|
|
27
30
|
/** The file that imports `katex`, repository-relative. */
|
|
28
31
|
readonly file: string;
|
|
29
|
-
/** The
|
|
32
|
+
/** The lesson whose imports reach it; one diagnostic per lesson and file. */
|
|
30
33
|
readonly lessonId: string;
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -94,26 +97,35 @@ function resolveRelative(fromFile: string, specifier: string): string | undefine
|
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
/**
|
|
97
|
-
* Each course file that
|
|
98
|
-
* `root` is the repository root the lesson
|
|
100
|
+
* Each course file that imports `katex`, once for every lesson whose imports
|
|
101
|
+
* reach it, by the shortest chain. `root` is the repository root the lesson
|
|
102
|
+
* paths are relative to.
|
|
99
103
|
*/
|
|
100
104
|
export function validateLabKatexImports(
|
|
101
105
|
lessons: readonly LabKatexLessonInput[],
|
|
102
106
|
root: string = process.cwd(),
|
|
103
107
|
): LabKatexDiagnostic[] {
|
|
108
|
+
// A component many lessons import is read once.
|
|
109
|
+
const sources = new Map<string, string | undefined>();
|
|
104
110
|
const read = (path: string): string | undefined => {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
if (!sources.has(path)) {
|
|
112
|
+
try {
|
|
113
|
+
sources.set(path, readFileSync(path, 'utf8'));
|
|
114
|
+
} catch {
|
|
115
|
+
sources.set(path, undefined);
|
|
116
|
+
}
|
|
109
117
|
}
|
|
118
|
+
return sources.get(path);
|
|
110
119
|
};
|
|
111
120
|
const repoPath = (path: string) => relative(root, path).split(sep).join('/');
|
|
112
121
|
|
|
113
|
-
const diagnostics =
|
|
114
|
-
const seen = new Set<string>();
|
|
122
|
+
const diagnostics: LabKatexDiagnostic[] = [];
|
|
115
123
|
for (const lesson of [...lessons].sort((a, b) => a.lessonId.localeCompare(b.lessonId))) {
|
|
116
124
|
const lessonPath = join(root, lesson.file);
|
|
125
|
+
// What this lesson reaches; another lesson reaching it too is not an answer
|
|
126
|
+
// for this one.
|
|
127
|
+
const reported = new Set<string>();
|
|
128
|
+
const seen = new Set<string>();
|
|
117
129
|
// Breadth first, so a reported chain is the shortest one.
|
|
118
130
|
const queue: { path: string; source: string; chain: readonly string[] }[] = [
|
|
119
131
|
{ path: lessonPath, source: stripFencedCode(lesson.body), chain: [lesson.file] },
|
|
@@ -123,8 +135,9 @@ export function validateLabKatexImports(
|
|
|
123
135
|
for (const specifier of importSpecifiers(source)) {
|
|
124
136
|
if (isKatexModule(specifier)) {
|
|
125
137
|
const file = repoPath(path);
|
|
126
|
-
if (!
|
|
127
|
-
|
|
138
|
+
if (!reported.has(file)) {
|
|
139
|
+
reported.add(file);
|
|
140
|
+
diagnostics.push({
|
|
128
141
|
code: 'lab-katex-direct',
|
|
129
142
|
severity: 'error',
|
|
130
143
|
file,
|
|
@@ -152,5 +165,7 @@ export function validateLabKatexImports(
|
|
|
152
165
|
}
|
|
153
166
|
}
|
|
154
167
|
}
|
|
155
|
-
return
|
|
168
|
+
return diagnostics.sort(
|
|
169
|
+
(a, b) => a.file.localeCompare(b.file) || a.lessonId.localeCompare(b.lessonId),
|
|
170
|
+
);
|
|
156
171
|
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
bindScopeToPage,
|
|
5
5
|
registerPageNotationDependencies,
|
|
6
6
|
routeIdFromLocals,
|
|
7
|
+
scopeLessonsOf,
|
|
7
8
|
} from './page-notation';
|
|
8
9
|
|
|
9
10
|
interface LessonNotationFrontmatter {
|
|
@@ -37,14 +38,45 @@ export async function labMathScope(
|
|
|
37
38
|
body = '',
|
|
38
39
|
component?: { readonly locals: object; readonly context: string },
|
|
39
40
|
): Promise<LabMathScopeDefinition[]> {
|
|
40
|
-
const
|
|
41
|
+
const manifest = await loadManifest();
|
|
42
|
+
// On a deck beside its chapter (`docs/authoring/deck.md`), a lab shows its
|
|
43
|
+
// lesson's symbols: the chapter's `notation.local` and the keys it names,
|
|
44
|
+
// as the deck's own math does. On a part's deck, its chapters' symbols,
|
|
45
|
+
// the first chapter to define a key binding it.
|
|
46
|
+
const routeId = component ? routeIdFromLocals(component.locals) : undefined;
|
|
47
|
+
const deck = routeId === undefined
|
|
48
|
+
? undefined
|
|
49
|
+
: manifest.decks?.find(
|
|
50
|
+
(entry) => entry.slug === routeId && (entry.chapter !== undefined || entry.part !== undefined),
|
|
51
|
+
);
|
|
52
|
+
const scopeLessons = deck ? scopeLessonsOf(manifest, deck.pageId) : [];
|
|
53
|
+
const lessonLocal: { key: string; latex: string; fixedIndex?: boolean }[] = [];
|
|
54
|
+
for (const lessonId of scopeLessons) {
|
|
55
|
+
for (const definition of manifest.notation.definitions) {
|
|
56
|
+
if (definition.kind !== 'local' || !definition.id.startsWith(`page:${lessonId}:`)) continue;
|
|
57
|
+
if (lessonLocal.some((known) => known.key === definition.key)) continue;
|
|
58
|
+
lessonLocal.push({
|
|
59
|
+
key: definition.key,
|
|
60
|
+
latex: definition.notation,
|
|
61
|
+
...(definition.fixedIndex ? { fixedIndex: true } : {}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const lessonKeys = scopeLessons.flatMap((lessonId) =>
|
|
66
|
+
(manifest.lessons.find((lesson) => lesson.id === lessonId)?.notation.bindings ?? []).map((binding) => binding.key),
|
|
67
|
+
);
|
|
68
|
+
const ownLocal = notation?.local ?? [];
|
|
69
|
+
const local = [
|
|
70
|
+
...ownLocal,
|
|
71
|
+
...lessonLocal.filter((definition) => !ownLocal.some((own) => own.key === definition.key)),
|
|
72
|
+
];
|
|
41
73
|
const scope: LabMathScopeDefinition[] = local.map((definition) => ({
|
|
42
74
|
key: definition.key,
|
|
43
75
|
notation: definition.latex,
|
|
44
76
|
...(definition.fixedIndex === true ? { fixedIndex: true as const } : {}),
|
|
45
77
|
}));
|
|
46
78
|
|
|
47
|
-
const referenced = new Set<string>();
|
|
79
|
+
const referenced = new Set<string>(lessonKeys);
|
|
48
80
|
const explicitlyExplained = new Set<string>();
|
|
49
81
|
for (const pattern of [TERM_KEY, EXPLAIN_KEY]) {
|
|
50
82
|
for (const match of body.matchAll(pattern)) {
|
|
@@ -54,7 +86,6 @@ export async function labMathScope(
|
|
|
54
86
|
}
|
|
55
87
|
|
|
56
88
|
const localKeys = new Set(local.map((definition) => definition.key));
|
|
57
|
-
const manifest = await loadManifest();
|
|
58
89
|
const sharedByKey = new Map(
|
|
59
90
|
manifest.notation.definitions
|
|
60
91
|
.filter((definition) => definition.kind === 'shared')
|