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
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import { lessonLine } from "./gate-prose-notation.js";
|
|
2
|
+
import { frameAt } from "./deck-source.js";
|
|
3
|
+
const RECALL_TABLE = { eq: "equations", fig: "figures", tab: "tables", dia: "diagrams" };
|
|
4
|
+
function chapterNumberOf(ref, floats) {
|
|
5
|
+
const match = RECALL_REF.exec(ref);
|
|
6
|
+
if (!match || floats === void 0) return void 0;
|
|
7
|
+
const [, kind, key] = match;
|
|
8
|
+
if (kind === "eq") return floats.equations.get(key ?? "")?.number;
|
|
9
|
+
return floats[RECALL_TABLE[kind]].get(key ?? "");
|
|
10
|
+
}
|
|
11
|
+
function coveredSections(source, chapters) {
|
|
12
|
+
const covered = [];
|
|
13
|
+
const frameChapters = [];
|
|
14
|
+
let current;
|
|
15
|
+
source.frames.forEach((frame, position) => {
|
|
16
|
+
if (frame.kind === "section") {
|
|
17
|
+
current = frame.chapter !== void 0 && chapters.has(frame.chapter) ? frame.chapter : void 0;
|
|
18
|
+
if (current !== void 0 && !covered.some((entry) => entry.name === current)) {
|
|
19
|
+
covered.push({ name: current, position });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
frameChapters.push(current);
|
|
23
|
+
});
|
|
24
|
+
return { covered, frameChapters };
|
|
25
|
+
}
|
|
26
|
+
function resolvePartRecalls(source, part) {
|
|
27
|
+
const byName = new Map(part.chapters.map((chapter) => [chapter.name, chapter]));
|
|
28
|
+
const sections = coveredSections(source, new Set(byName.keys()));
|
|
29
|
+
const covered = sections.covered.map(({ name, position }) => ({ ...byName.get(name), position }));
|
|
30
|
+
const frameChapters = sections.frameChapters.map(
|
|
31
|
+
(name) => name === void 0 ? void 0 : covered.find((chapter) => chapter.name === name)
|
|
32
|
+
);
|
|
33
|
+
const chapterOf = /* @__PURE__ */ new Map();
|
|
34
|
+
const problems = /* @__PURE__ */ new Map();
|
|
35
|
+
source.frames.forEach((frame, position) => {
|
|
36
|
+
const own = frameChapters[position];
|
|
37
|
+
for (const recall of frame.recalls) {
|
|
38
|
+
if (recall.ref === void 0 || !RECALL_REF.test(recall.ref)) continue;
|
|
39
|
+
const ref = recall.ref;
|
|
40
|
+
const numbering = covered.filter((chapter) => chapterNumberOf(ref, chapter.floats) !== void 0);
|
|
41
|
+
const from = own !== void 0 && numbering.includes(own) ? own : numbering.length === 1 ? numbering[0] : void 0;
|
|
42
|
+
const earlier = chapterOf.get(ref);
|
|
43
|
+
if (from === void 0) {
|
|
44
|
+
problems.set(
|
|
45
|
+
recall.index,
|
|
46
|
+
numbering.length === 0 ? covered.length === 0 ? `recalls \`${ref}\`, but no section of this deck names a chapter to recall from (\`## Title [chapter: <name>]\`)` : `recalls \`${ref}\`, which no chapter this deck covers numbers (${covered.map((chapter) => chapter.slug).join(", ")})` : `recalls \`${ref}\`, which ${numbering.map((chapter) => chapter.slug).join(" and ")} each number: recall it in the section that names the chapter you mean`
|
|
47
|
+
);
|
|
48
|
+
} else if (earlier !== void 0 && earlier !== from) {
|
|
49
|
+
problems.set(
|
|
50
|
+
recall.index,
|
|
51
|
+
`recalls \`${ref}\` from ${from.slug}, where an earlier frame recalls it from ${earlier.slug}: a deck shows one float under one anchor`
|
|
52
|
+
);
|
|
53
|
+
} else {
|
|
54
|
+
chapterOf.set(ref, from);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
return { covered, frameChapters, chapterOf, problems };
|
|
59
|
+
}
|
|
60
|
+
const DECK_RULE_DEFAULTS = {
|
|
61
|
+
maxWords: 70,
|
|
62
|
+
maxBullets: 7,
|
|
63
|
+
maxTitleLength: 50,
|
|
64
|
+
maxSteps: 4,
|
|
65
|
+
maxNewSymbols: 4
|
|
66
|
+
};
|
|
67
|
+
const LESSON_FIELDS = ["teaches", "assumptions", "studyMinutes", "notation"];
|
|
68
|
+
const RECALL_REF = /^(eq|fig|tab|dia)-([a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
|
69
|
+
const NOTATION_KEY = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
70
|
+
function comparableLatex(latex) {
|
|
71
|
+
let text = String(latex).replace(/\\label\{[^{}]*\}/g, "").replace(/\\caption\{(?:[^{}]|\{[^{}]*\})*\}/g, "");
|
|
72
|
+
for (let guard = 0; guard < 20 && /\\explain\s*\{/.test(text); guard += 1) {
|
|
73
|
+
text = text.replace(/\\explain\s*\{[^{}]*\}\s*\{((?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})*)\}/g, "$1");
|
|
74
|
+
}
|
|
75
|
+
return text.replace(/\\(?:left|right|big|Big|bigg|Bigg)(?![A-Za-z])/g, "").replace(/\\(?:,|;|:|!|quad|qquad)(?![A-Za-z])/g, "").replace(/\s+/g, "");
|
|
76
|
+
}
|
|
77
|
+
function frameName(frame) {
|
|
78
|
+
return `${frame.kind === "section" ? "##" : "###"} ${JSON.stringify(frame.title)}`;
|
|
79
|
+
}
|
|
80
|
+
function validateDecks(decks, settings = DECK_RULE_DEFAULTS) {
|
|
81
|
+
const diagnostics = [];
|
|
82
|
+
for (const deck of decks) {
|
|
83
|
+
let checkFrame2 = function(frame, position) {
|
|
84
|
+
if (frame.words > settings.maxWords || frame.bullets > settings.maxBullets) {
|
|
85
|
+
const over = [
|
|
86
|
+
frame.words > settings.maxWords ? `${frame.words} words (most ${settings.maxWords})` : "",
|
|
87
|
+
frame.bullets > settings.maxBullets ? `${frame.bullets} list items (most ${settings.maxBullets})` : ""
|
|
88
|
+
].filter(Boolean);
|
|
89
|
+
push({
|
|
90
|
+
code: "frame-dense",
|
|
91
|
+
severity: "warning",
|
|
92
|
+
message: `${frameName(frame)} shows ${over.join(" and ")} (rule D2): it will not fit its canvas. Split it into two frames, or move the argument into its \`<Notes>\`.`,
|
|
93
|
+
index: frame.index,
|
|
94
|
+
token: frame.title
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if ((frame.words > 0 || frame.bullets > 0) && frame.notes.length === 0) {
|
|
98
|
+
push({
|
|
99
|
+
code: "frame-notes-missing",
|
|
100
|
+
severity: "warning",
|
|
101
|
+
message: `${frameName(frame)} has text but no \`<Notes>\` (rule D4): write what the speaker says \u2014 the reasoning, the transition, the question to ask \u2014 in \`<Notes>\u2026</Notes>\` on the frame.`,
|
|
102
|
+
index: frame.index,
|
|
103
|
+
token: frame.title
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const steps = frame.pauses.length + 1;
|
|
107
|
+
if (steps > settings.maxSteps) {
|
|
108
|
+
push({
|
|
109
|
+
code: "frame-steps",
|
|
110
|
+
severity: "warning",
|
|
111
|
+
message: `${frameName(frame)} shows in ${steps} steps, more than ${settings.maxSteps} (rule D6, \`rules.D6.maxSteps\`). Make the later steps a frame of their own.`,
|
|
112
|
+
index: frame.index,
|
|
113
|
+
token: frame.title
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const pictures = frame.floats.filter((float) => float.name === "Figure" || float.name === "Table" || float.name === "Diagram").length + frame.recalls.filter((recall) => recall.ref !== void 0 && !recall.ref.startsWith("eq-")).length;
|
|
117
|
+
if (pictures > 1) {
|
|
118
|
+
push({
|
|
119
|
+
code: "frame-floats",
|
|
120
|
+
severity: "warning",
|
|
121
|
+
message: `${frameName(frame)} holds ${pictures} figures, tables or diagrams (rule D7): one per frame, at the canvas width. Give the second its own frame.`,
|
|
122
|
+
index: frame.index,
|
|
123
|
+
token: frame.title
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const drivesSomething = frame.floats.some((float) => float.name === "Figure") || frame.recalls.some((recall) => recall.ref?.startsWith("fig-") === true);
|
|
127
|
+
for (const pause of frame.pauses) {
|
|
128
|
+
const problems = pauseProblems(pause, drivesSomething);
|
|
129
|
+
if (problems.length > 0) {
|
|
130
|
+
push({
|
|
131
|
+
code: "pause-invalid",
|
|
132
|
+
severity: "error",
|
|
133
|
+
message: `<Pause> on ${frameName(frame)}: ${problems.join("; ")}. See docs/authoring/deck.md, "Steps and animation".`,
|
|
134
|
+
index: pause.index
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const recall of frame.recalls) {
|
|
139
|
+
const problem = partRecalls === void 0 ? recallProblem(recall.ref, deck) : recallFormProblem(recall.ref) ?? partRecalls.problems.get(recall.index);
|
|
140
|
+
if (problem !== void 0) {
|
|
141
|
+
push({
|
|
142
|
+
code: "recall-unresolved",
|
|
143
|
+
severity: "error",
|
|
144
|
+
message: `<Recall> on ${frameName(frame)} ${problem}. A recall names a labelled equation (\`eq-<key>\`) or a ` + (partRecalls === void 0 ? "keyed `<Figure>`, `<Table>` or `<Diagram>` of the deck's own chapter." : "keyed `<Figure>`, `<Table>` or `<Diagram>` of a chapter a section of the deck covers."),
|
|
145
|
+
index: recall.index,
|
|
146
|
+
...recall.ref === void 0 ? {} : { token: recall.ref }
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (part !== void 0 && frame.chapter === void 0 && frame.floats.some((float) => float.name === "AssessmentSet")) {
|
|
151
|
+
push({
|
|
152
|
+
code: "deck-check-no-chapter",
|
|
153
|
+
severity: "error",
|
|
154
|
+
message: `${frameName(frame)} holds <AssessmentSet>, but its section names no chapter. On a part's deck a question frame asks the checks of the chapter its section covers: move it into a \`## Title [chapter: <name>]\` section.`,
|
|
155
|
+
index: frame.floats.find((float) => float.name === "AssessmentSet")?.index ?? frame.index,
|
|
156
|
+
token: frame.title
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const chapterEquations = (partRecalls === void 0 ? [...deck.chapter?.equations ?? /* @__PURE__ */ new Map()] : partRecalls.covered.flatMap((chapter) => [...chapter.floats.equations])).filter(([, equation]) => equation.latex !== void 0);
|
|
160
|
+
if (chapterEquations.length > 0) {
|
|
161
|
+
for (const math of frame.displayMath) {
|
|
162
|
+
const mine = comparableLatex(math.latex);
|
|
163
|
+
if (mine === "") continue;
|
|
164
|
+
const same = chapterEquations.find(([, equation]) => comparableLatex(equation.latex ?? "") === mine);
|
|
165
|
+
if (same) {
|
|
166
|
+
push({
|
|
167
|
+
code: "frame-retyped",
|
|
168
|
+
severity: "warning",
|
|
169
|
+
message: `${frameName(frame)} retypes the chapter's equation (${same[1].number}) (rule D8). Write <Recall ref="eq-${same[0]}" /> instead: it shows the chapter's number and links to it.`,
|
|
170
|
+
index: math.index,
|
|
171
|
+
token: `eq-${same[0]}`
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const fresh = deck.newSymbols?.[position] ?? [];
|
|
177
|
+
if (fresh.length > settings.maxNewSymbols) {
|
|
178
|
+
push({
|
|
179
|
+
code: "frame-new-symbols",
|
|
180
|
+
severity: "info",
|
|
181
|
+
message: `${frameName(frame)} introduces ${fresh.length} symbols the deck has not shown before (${fresh.join(", ")}), more than ${settings.maxNewSymbols} (rule D9). Introduce some on an earlier frame.`,
|
|
182
|
+
index: frame.index,
|
|
183
|
+
token: frame.title
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var checkFrame = checkFrame2;
|
|
188
|
+
const lineAt = (index) => lessonLine({ body: deck.body, bodyLine: deck.bodyLine }, index);
|
|
189
|
+
const push = (finding) => {
|
|
190
|
+
const { index, ...rest } = finding;
|
|
191
|
+
diagnostics.push({
|
|
192
|
+
context: "structure",
|
|
193
|
+
file: deck.file,
|
|
194
|
+
...index === void 0 ? {} : { line: lineAt(index) },
|
|
195
|
+
...rest
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
const { source, part } = deck;
|
|
199
|
+
if (!deck.inPart) {
|
|
200
|
+
push({
|
|
201
|
+
code: "deck-outside-part",
|
|
202
|
+
severity: "error",
|
|
203
|
+
message: part !== void 0 ? `${deck.file} is the deck of content/docs/${part.dir}/, which no part in course.settings.json \`parts\` lists. A part's deck sits in a part's directory: add { "dir": "${part.dir}", "label": "\u2026" } to \`parts\`, or move it.` : `${deck.file} is a deck outside a part's directory. A deck is the slide edition of a lesson: put it beside the lesson, \`content/docs/<part>/<lesson>.deck.mdx\`, or make it a part's, \`content/docs/<part>/index.deck.mdx\`.`
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
if (deck.hasChapter || part !== void 0) {
|
|
207
|
+
const set = LESSON_FIELDS.filter((field) => deck.data[field] !== void 0);
|
|
208
|
+
if (set.length > 0) {
|
|
209
|
+
push({
|
|
210
|
+
code: "deck-frontmatter",
|
|
211
|
+
severity: "error",
|
|
212
|
+
message: part !== void 0 ? `${deck.file} sets ${set.map((field) => `\`${field}\``).join(", ")}. A part's deck is no lesson: it sets only \`title\` and \`description\`, and shows its chapters' symbols and outcomes.` : `${deck.file} sets ${set.map((field) => `\`${field}\``).join(", ")}, which the lesson's chapter holds. A deck beside its chapter sets only \`title\` and \`description\`; move the rest into the chapter.`
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const partRecalls = part === void 0 ? void 0 : resolvePartRecalls(source, part);
|
|
217
|
+
const named = /* @__PURE__ */ new Set();
|
|
218
|
+
let previous;
|
|
219
|
+
for (const frame of source.frames) {
|
|
220
|
+
if (frame.kind !== "section") continue;
|
|
221
|
+
const before = previous;
|
|
222
|
+
previous = frame.chapter;
|
|
223
|
+
if (frame.chapter === void 0) continue;
|
|
224
|
+
const chapter = frame.chapter;
|
|
225
|
+
if (part === void 0) {
|
|
226
|
+
push({
|
|
227
|
+
code: "deck-chapter-outside-part",
|
|
228
|
+
severity: "error",
|
|
229
|
+
message: `${frameName(frame)} names the chapter \`${chapter}\`, but this is a lesson's deck, which covers its own lesson. Only a part's deck (\`content/docs/<part>/index.deck.mdx\`) names the chapter a section covers; drop the \`[chapter: \u2026]\` marker.`,
|
|
230
|
+
index: frame.index,
|
|
231
|
+
token: chapter
|
|
232
|
+
});
|
|
233
|
+
} else if (!part.chapters.some((entry) => entry.name === chapter)) {
|
|
234
|
+
push({
|
|
235
|
+
code: "deck-chapter-unknown",
|
|
236
|
+
severity: "error",
|
|
237
|
+
message: (part.slidesFirst ?? []).includes(chapter) ? `${frameName(frame)} names \`${chapter}\`, a lesson of ${part.dir} taught as slides only: it has no chapter to cover. Drop the marker, or name a chapter of the part.` : `${frameName(frame)} names \`${chapter}\`, which is no chapter of ${part.dir}. ` + (part.chapters.length > 0 ? `Its chapters are ${part.chapters.map((entry) => `\`${entry.name}\``).join(", ")}: name one by its file's name.` : "The part has no chapter: drop the marker."),
|
|
238
|
+
index: frame.index,
|
|
239
|
+
token: chapter
|
|
240
|
+
});
|
|
241
|
+
} else if (named.has(chapter) && before !== chapter) {
|
|
242
|
+
push({
|
|
243
|
+
code: "deck-chapter-repeated",
|
|
244
|
+
severity: "error",
|
|
245
|
+
message: `${frameName(frame)} names \`${chapter}\`, which an earlier section of the deck covers already, with a section of another chapter or of the deck's own between them. A chapter's sections follow one another, the first one's divider the chapter's address in the deck: move this section up to the others.`,
|
|
246
|
+
index: frame.index,
|
|
247
|
+
token: chapter
|
|
248
|
+
});
|
|
249
|
+
} else {
|
|
250
|
+
named.add(chapter);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (part !== void 0 && [...part.chapters.map((entry) => entry.name), ...part.slidesFirst ?? []].includes("slides")) {
|
|
254
|
+
push({
|
|
255
|
+
code: "deck-route-taken",
|
|
256
|
+
severity: "error",
|
|
257
|
+
message: `${deck.file} is served at /${part.dir}/slides/, which is the address of the lesson content/docs/${part.dir}/slides. Rename the lesson's file, or the part keeps no deck of its own.`
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
for (const marker of source.misplacedMarkers) {
|
|
261
|
+
push({
|
|
262
|
+
code: "deck-marker-misplaced",
|
|
263
|
+
severity: "error",
|
|
264
|
+
message: marker.marker === "chapter" ? `### ${JSON.stringify(marker.title)} carries \`[chapter: \u2026]\`, which a section's \`##\` heading carries: it names the chapter the whole section covers. Move it to the section.` : `## ${JSON.stringify(marker.title)} carries \`[morph]\`, which a frame's \`###\` heading carries: a section divider shows its title alone and has nothing to morph from. Drop it, or move it to the frame.`,
|
|
265
|
+
index: marker.index,
|
|
266
|
+
token: marker.title
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
const frames = source.frames.filter((frame) => frame.kind === "frame");
|
|
270
|
+
if (frames.length === 0) {
|
|
271
|
+
push({
|
|
272
|
+
code: "deck-empty",
|
|
273
|
+
severity: "error",
|
|
274
|
+
message: `${deck.file} has no frame. A frame is a \`###\` heading and what follows it (rule D1).`
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
for (const heading of source.deepHeadings) {
|
|
278
|
+
push({
|
|
279
|
+
code: "frame-depth",
|
|
280
|
+
severity: "error",
|
|
281
|
+
message: `A ${"#".repeat(heading.level)} heading ${JSON.stringify(heading.text)} in a deck (rule D1): \`##\` opens a section and \`###\` is a frame; nothing else is a heading here. Make it a frame, or bold text in the frame.`,
|
|
282
|
+
index: heading.index,
|
|
283
|
+
token: heading.text
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
for (const loose of source.loose) {
|
|
287
|
+
const section = loose.where === "section" ? frameAt(source, loose.index) : void 0;
|
|
288
|
+
push({
|
|
289
|
+
code: "deck-loose-content",
|
|
290
|
+
severity: "error",
|
|
291
|
+
message: loose.where === "before" ? "Content before the deck's first heading belongs to no frame (rule D1). Put it in a `###` frame; the title frame is generated." : `Content under ${section ? frameName(section) : "a section heading"} before its first \`###\` frame (rule D1): a section is a divider that shows only its title. Put the content in a \`###\` frame.`,
|
|
292
|
+
index: loose.index
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
for (const [position, frame] of source.frames.entries()) {
|
|
296
|
+
if (frame.title.length > settings.maxTitleLength) {
|
|
297
|
+
push({
|
|
298
|
+
code: "frame-title-length",
|
|
299
|
+
severity: "error",
|
|
300
|
+
message: `${frameName(frame)} is ${frame.title.length} characters, over the ${settings.maxTitleLength}-character limit (rule D5, \`rules.D5.maxLength\`): a frame title stays on one line. Shorten it and move the rest into the frame.`,
|
|
301
|
+
index: frame.index,
|
|
302
|
+
token: frame.title
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (frame.kind !== "frame") continue;
|
|
306
|
+
checkFrame2(frame, position);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return diagnostics;
|
|
310
|
+
}
|
|
311
|
+
function pauseProblems(pause, drivesSomething) {
|
|
312
|
+
const problems = [];
|
|
313
|
+
if (pause.unknown.length > 0) {
|
|
314
|
+
problems.push(`takes no ${pause.unknown.map((name) => `\`${name}\``).join(", ")}`);
|
|
315
|
+
}
|
|
316
|
+
const drives = pause.control !== void 0 || pause.to !== void 0 || pause.from !== void 0 || pause.seconds !== void 0;
|
|
317
|
+
if (drives && pause.highlight !== void 0) {
|
|
318
|
+
problems.push("either drives a figure (`control`, `to`) or lights a symbol (`highlight`), not both");
|
|
319
|
+
}
|
|
320
|
+
if (drives && (pause.control === void 0 || pause.control.trim() === "" || pause.to === void 0)) {
|
|
321
|
+
problems.push("drives a figure with both `control` (the label of the lab\u2019s control) and `to`");
|
|
322
|
+
}
|
|
323
|
+
for (const name of ["from", "to", "seconds"]) {
|
|
324
|
+
const value = pause[name];
|
|
325
|
+
if (value !== void 0 && !Number.isFinite(Number(value))) {
|
|
326
|
+
problems.push(`\`${name}="${value}"\` is not a number`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (pause.seconds !== void 0 && Number(pause.seconds) <= 0) {
|
|
330
|
+
problems.push("`seconds` is more than zero");
|
|
331
|
+
}
|
|
332
|
+
if (pause.control !== void 0 && !drivesSomething) {
|
|
333
|
+
problems.push("drives a lab control, but the frame has no `<Figure>` or recalled figure to drive");
|
|
334
|
+
}
|
|
335
|
+
if (pause.highlight !== void 0 && !NOTATION_KEY.test(pause.highlight)) {
|
|
336
|
+
problems.push(`\`highlight="${pause.highlight}"\` is not a notation key`);
|
|
337
|
+
}
|
|
338
|
+
return problems;
|
|
339
|
+
}
|
|
340
|
+
function recallFormProblem(ref) {
|
|
341
|
+
if (ref === void 0) return "has no `ref`";
|
|
342
|
+
return RECALL_REF.test(ref) ? void 0 : `\`ref="${ref}"\` is not \`eq-\`, \`fig-\`, \`tab-\` or \`dia-\` and a key`;
|
|
343
|
+
}
|
|
344
|
+
function recallProblem(ref, deck) {
|
|
345
|
+
const form = recallFormProblem(ref);
|
|
346
|
+
if (form !== void 0 || ref === void 0) return form;
|
|
347
|
+
if (!deck.hasChapter || deck.chapter === void 0) {
|
|
348
|
+
return `recalls \`${ref}\`, but this lesson has no chapter to recall from`;
|
|
349
|
+
}
|
|
350
|
+
return chapterNumberOf(ref, deck.chapter) === void 0 ? `recalls \`${ref}\`, which the chapter does not number` : void 0;
|
|
351
|
+
}
|
|
352
|
+
export {
|
|
353
|
+
DECK_RULE_DEFAULTS,
|
|
354
|
+
chapterNumberOf,
|
|
355
|
+
comparableLatex,
|
|
356
|
+
coveredSections,
|
|
357
|
+
resolvePartRecalls,
|
|
358
|
+
validateDecks
|
|
359
|
+
};
|
|
@@ -3,7 +3,7 @@ import { GlyphScopeEntry } from './gloss.js';
|
|
|
3
3
|
import { DerivedUse, NotationDefinitionRecord } from './types.js';
|
|
4
4
|
import '../curriculum/validation.js';
|
|
5
5
|
import './consistency.js';
|
|
6
|
-
import '../course-config-
|
|
6
|
+
import '../course-config-BLtIdkei.js';
|
|
7
7
|
import '../rules/catalog.js';
|
|
8
8
|
import '../rules/course-rules.js';
|
|
9
9
|
import '../compiler/collections.js';
|
|
@@ -23,6 +23,10 @@ import './gate-math.js';
|
|
|
23
23
|
import './mixed-content.js';
|
|
24
24
|
import '../rules/waivers.js';
|
|
25
25
|
import '../compiler/knowledge-version.js';
|
|
26
|
+
import '../compiler/page-versions.js';
|
|
27
|
+
import '../compiler/decks.js';
|
|
28
|
+
import './decks-validate.js';
|
|
29
|
+
import './deck-source.js';
|
|
26
30
|
import '../compiler/lesson-derivation.js';
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -11,9 +11,16 @@
|
|
|
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
|
interface UnboundFigureSymbols {
|
|
19
26
|
/** The figure's anchor id, e.g. `fig-latent-factor-model`. */
|
|
@@ -23,10 +30,20 @@ interface UnboundFigureSymbols {
|
|
|
23
30
|
}
|
|
24
31
|
/** The figures in `html` that draw a symbol with no card, and those symbols. */
|
|
25
32
|
declare function unboundFigureSymbols(html: string): UnboundFigureSymbols[];
|
|
33
|
+
/** One rendered page and the figures on it that draw a symbol with no card. */
|
|
34
|
+
interface PageUnboundFigureSymbols {
|
|
35
|
+
/** The page's path in the build output, e.g. `lecture-5/chernoff/index.html`. */
|
|
36
|
+
readonly page: string;
|
|
37
|
+
readonly figures: readonly UnboundFigureSymbols[];
|
|
38
|
+
}
|
|
26
39
|
/**
|
|
27
|
-
* Fail a build
|
|
28
|
-
* card, naming
|
|
40
|
+
* Fail a build over the pages that show a symbol in a figure that opens no
|
|
41
|
+
* card, naming every page, its figures and symbols, and the way to bind them.
|
|
42
|
+
* A build collects the whole list first (`compiler/astro.ts`), so it reports
|
|
43
|
+
* every page at once instead of one per run.
|
|
29
44
|
*/
|
|
45
|
+
declare function assertNoUnboundFigureSymbols(pages: readonly PageUnboundFigureSymbols[]): void;
|
|
46
|
+
/** Fail a build whose rendered page shows a symbol in a figure that opens no card. */
|
|
30
47
|
declare function assertFigureSymbolsBound(page: string, html: string): void;
|
|
31
48
|
|
|
32
|
-
export { type UnboundFigureSymbols, assertFigureSymbolsBound, unboundFigureSymbols };
|
|
49
|
+
export { type PageUnboundFigureSymbols, type UnboundFigureSymbols, assertFigureSymbolsBound, assertNoUnboundFigureSymbols, unboundFigureSymbols };
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { parse } from "parse5";
|
|
2
|
+
import { isBaseLibraryGlyph } from "./base-library.mjs";
|
|
2
3
|
const UPRIGHT = /* @__PURE__ */ new Set(["text", "mathrm", "textrm", "mop"]);
|
|
4
|
+
const PLAIN = /* @__PURE__ */ new Set(["mathnormal", "mathit"]);
|
|
5
|
+
const DECORATED = /* @__PURE__ */ new Set(["accent"]);
|
|
3
6
|
const LETTER = new RegExp("\\p{L}", "u");
|
|
4
7
|
function isElement(node) {
|
|
5
8
|
return "tagName" in node;
|
|
@@ -19,37 +22,70 @@ function childrenOf(node) {
|
|
|
19
22
|
function unboundFigureSymbols(html) {
|
|
20
23
|
if (!html.includes("keyed-figure") || !html.includes("katex-html")) return [];
|
|
21
24
|
const found = /* @__PURE__ */ new Map();
|
|
22
|
-
const visit = (node,
|
|
25
|
+
const visit = (node, context, row) => {
|
|
26
|
+
let next = context;
|
|
23
27
|
if (isElement(node)) {
|
|
24
28
|
const names = classes(node);
|
|
29
|
+
next = {
|
|
30
|
+
...context,
|
|
31
|
+
// A letter's own element says how it is drawn; its ancestors do not.
|
|
32
|
+
plain: names.some((name) => PLAIN.has(name)),
|
|
33
|
+
// Only the rows of a script's own vertical list are its sub and sup.
|
|
34
|
+
lowered: names.includes("msupsub") ? childrenOf(node).some((child) => isElement(child) && classes(child).includes("vlist-t2")) : row === void 0 ? context.lowered : false
|
|
35
|
+
};
|
|
25
36
|
if (node.tagName === "figure" && names.includes("keyed-figure")) {
|
|
26
|
-
|
|
37
|
+
next = { ...next, figure: attribute(node, "id") ?? "(a figure with no id)" };
|
|
27
38
|
}
|
|
28
|
-
if (names.includes("katex-html"))
|
|
29
|
-
if (
|
|
30
|
-
|
|
39
|
+
if (names.includes("katex-html")) next = { ...next, inKatex: true };
|
|
40
|
+
if (names.some((name) => DECORATED.has(name)) || row === 0 && context.lowered) {
|
|
41
|
+
next = { ...next, decorated: true };
|
|
31
42
|
}
|
|
32
|
-
|
|
43
|
+
if (attribute(node, "data-notation-key") !== void 0 || next.inKatex && names.some((name) => UPRIGHT.has(name))) {
|
|
44
|
+
next = { ...next, covered: true };
|
|
45
|
+
}
|
|
46
|
+
} else if (node.nodeName === "#text" && context.figure !== void 0 && context.inKatex && !context.covered) {
|
|
33
47
|
const text = node.value.trim();
|
|
34
|
-
|
|
35
|
-
|
|
48
|
+
const library = context.plain && !context.decorated && [...text].every((glyph) => !LETTER.test(glyph) || isBaseLibraryGlyph(glyph));
|
|
49
|
+
if (LETTER.test(text) && !library) {
|
|
50
|
+
const symbols = found.get(context.figure) ?? [];
|
|
36
51
|
if (!symbols.includes(text)) symbols.push(text);
|
|
37
|
-
found.set(figure, symbols);
|
|
52
|
+
found.set(context.figure, symbols);
|
|
38
53
|
}
|
|
39
54
|
}
|
|
40
|
-
|
|
55
|
+
const isVlist = isElement(node) && classes(node).includes("vlist");
|
|
56
|
+
let rows = 0;
|
|
57
|
+
for (const child of childrenOf(node)) {
|
|
58
|
+
const isRow = isVlist && isElement(child);
|
|
59
|
+
visit(child, next, isRow ? rows : void 0);
|
|
60
|
+
if (isRow) rows += 1;
|
|
61
|
+
}
|
|
41
62
|
};
|
|
42
|
-
visit(parse(html),
|
|
63
|
+
visit(parse(html), {
|
|
64
|
+
figure: void 0,
|
|
65
|
+
inKatex: false,
|
|
66
|
+
covered: false,
|
|
67
|
+
decorated: false,
|
|
68
|
+
plain: false,
|
|
69
|
+
lowered: false
|
|
70
|
+
});
|
|
43
71
|
return [...found].map(([figure, symbols]) => ({ figure, symbols }));
|
|
44
72
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
const describeFigures = (figures) => figures.map(({ figure, symbols }) => `#${figure} shows ${symbols.map((s) => `"${s}"`).join(", ")}`).join("; ");
|
|
74
|
+
function assertNoUnboundFigureSymbols(pages) {
|
|
75
|
+
const found = pages.filter(({ figures }) => figures.length > 0);
|
|
76
|
+
const [only] = found;
|
|
77
|
+
if (!only) return;
|
|
78
|
+
const head = found.length === 1 ? `Explico figure symbols without a notation card on rendered page "${only.page}": ` + describeFigures(only.figures) : `Explico figure symbols without a notation card on ${found.length} rendered pages:
|
|
79
|
+
` + found.map(({ page, figures }) => ` "${page}": ${describeFigures(figures)}`).join("\n");
|
|
48
80
|
throw new Error(
|
|
49
|
-
|
|
81
|
+
`${head}. Every symbol a figure shows opens its card (rule N4): render its math with renderLabMath (\`{ display: false }\` for a legend, an axis title or a label; a \`{ math }\` run for the caption clause), never with katex directly. Write a unit as \`\\text{\u2026}\`.`
|
|
50
82
|
);
|
|
51
83
|
}
|
|
84
|
+
function assertFigureSymbolsBound(page, html) {
|
|
85
|
+
assertNoUnboundFigureSymbols([{ page, figures: unboundFigureSymbols(html) }]);
|
|
86
|
+
}
|
|
52
87
|
export {
|
|
53
88
|
assertFigureSymbolsBound,
|
|
89
|
+
assertNoUnboundFigureSymbols,
|
|
54
90
|
unboundFigureSymbols
|
|
55
91
|
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A numbered float's reader-facing name: its kind word and number, then its
|
|
3
|
+
* short caption when it has one — "Diagram 2.1.1 Steps of the range
|
|
4
|
+
* derivation" (docs/plans/figures-and-short-captions.md, decisions 9 and 11).
|
|
5
|
+
* A `<Fold>`'s bar names the floats inside it this way.
|
|
6
|
+
*
|
|
7
|
+
* A float carries both halves on its root element, as `data-<kind>-number`
|
|
8
|
+
* and `data-<kind>-short` (`Figure.astro`, `Table.astro`, `Diagram.astro`);
|
|
9
|
+
* nothing else in the page carries those attributes.
|
|
10
|
+
*/
|
|
11
|
+
type FloatKind = 'fig' | 'tab' | 'dia';
|
|
12
|
+
declare const FLOAT_KIND_WORDS: Readonly<Record<FloatKind, string>>;
|
|
13
|
+
interface NamedFloat {
|
|
14
|
+
readonly kind: FloatKind;
|
|
15
|
+
readonly number: string;
|
|
16
|
+
readonly short?: string;
|
|
17
|
+
/** Its anchor (`fig-…`, `tab-…`, `dia-…`), when the scan found one. */
|
|
18
|
+
readonly anchorId?: string;
|
|
19
|
+
}
|
|
20
|
+
/** `Figure 2.1.1 Projectile trajectory explorer`, or `Figure 2.1.1` with no short caption. */
|
|
21
|
+
declare function floatName(float: NamedFloat): string;
|
|
22
|
+
/**
|
|
23
|
+
* The bold title that opens a caption line when the float has a short
|
|
24
|
+
* caption (docs/plans/figures-and-short-captions.md, decision 9.1): the
|
|
25
|
+
* short caption with a closing period, unless it already ends in one — the
|
|
26
|
+
* journal-legend style, "Fig. 1 | Title. Description".
|
|
27
|
+
*/
|
|
28
|
+
declare function captionTitle(short: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* A numbered float's name, read off its root element in the page — the same
|
|
31
|
+
* `data-<kind>-number` / `data-<kind>-short` pair `floatsInHtml` reads from
|
|
32
|
+
* markup. `null` for an element that is no numbered float.
|
|
33
|
+
*/
|
|
34
|
+
declare function floatOf(element: Element): NamedFloat | null;
|
|
35
|
+
/**
|
|
36
|
+
* Every numbered float in rendered HTML, in document order — for a build-time
|
|
37
|
+
* component that renders its slot first (`<Fold>`). Reads the start tags
|
|
38
|
+
* only: attribute values are double-quoted, with `"` escaped, so a tag ends
|
|
39
|
+
* at the first `>` outside one.
|
|
40
|
+
*/
|
|
41
|
+
declare function floatsInHtml(html: string): NamedFloat[];
|
|
42
|
+
|
|
43
|
+
export { FLOAT_KIND_WORDS, type FloatKind, type NamedFloat, captionTitle, floatName, floatOf, floatsInHtml };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const FLOAT_KIND_WORDS = {
|
|
2
|
+
fig: "Figure",
|
|
3
|
+
tab: "Table",
|
|
4
|
+
dia: "Diagram"
|
|
5
|
+
};
|
|
6
|
+
function floatName(float) {
|
|
7
|
+
const label = `${FLOAT_KIND_WORDS[float.kind]} ${float.number}`;
|
|
8
|
+
return float.short ? `${label} ${float.short}` : label;
|
|
9
|
+
}
|
|
10
|
+
function captionTitle(short) {
|
|
11
|
+
const title = short.trim();
|
|
12
|
+
return /[.!?…:;]$/u.test(title) ? title : `${title}.`;
|
|
13
|
+
}
|
|
14
|
+
function floatOf(element) {
|
|
15
|
+
for (const kind of ["fig", "tab", "dia"]) {
|
|
16
|
+
const number = element.getAttribute(`data-${kind}-number`);
|
|
17
|
+
if (!number) continue;
|
|
18
|
+
const short = element.getAttribute(`data-${kind}-short`) ?? void 0;
|
|
19
|
+
return {
|
|
20
|
+
kind,
|
|
21
|
+
number,
|
|
22
|
+
...short ? { short } : {},
|
|
23
|
+
...element.id ? { anchorId: element.id } : {}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function decodeAttribute(value) {
|
|
29
|
+
return value.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code))).replace(
|
|
30
|
+
/&#x([\da-f]+);/gi,
|
|
31
|
+
(_, code) => String.fromCodePoint(Number.parseInt(code, 16))
|
|
32
|
+
).replace(/"/g, '"').replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
33
|
+
}
|
|
34
|
+
function floatsInHtml(html) {
|
|
35
|
+
const floats = [];
|
|
36
|
+
for (const tag of html.matchAll(/<[A-Za-z][\w-]*((?:\s+[^\s"'>/=]+(?:="[^"]*")?)*)\s*\/?>/g)) {
|
|
37
|
+
const attributes = tag[1] ?? "";
|
|
38
|
+
const number = /\sdata-(fig|tab|dia)-number="([^"]*)"/.exec(attributes);
|
|
39
|
+
if (!number?.[2]) continue;
|
|
40
|
+
const kind = number[1];
|
|
41
|
+
const short = new RegExp(`\\sdata-${kind}-short="([^"]*)"`).exec(attributes)?.[1];
|
|
42
|
+
const anchorId = /\sid="([^"]*)"/.exec(attributes)?.[1];
|
|
43
|
+
floats.push({
|
|
44
|
+
kind,
|
|
45
|
+
number: decodeAttribute(number[2]),
|
|
46
|
+
...short ? { short: decodeAttribute(short) } : {},
|
|
47
|
+
...anchorId ? { anchorId: decodeAttribute(anchorId) } : {}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return floats;
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
FLOAT_KIND_WORDS,
|
|
54
|
+
captionTitle,
|
|
55
|
+
floatName,
|
|
56
|
+
floatOf,
|
|
57
|
+
floatsInHtml
|
|
58
|
+
};
|
|
@@ -4,7 +4,7 @@ import { KeyedFloatLessonInput } from './keyed-float-validate.js';
|
|
|
4
4
|
* `<Fold>` validation, run inside `compileManifest()`
|
|
5
5
|
* (docs/plans/figures-and-short-captions.md, decision 11). A fold's bar is
|
|
6
6
|
* its `label` or, without one, the numbers and short captions of the floats
|
|
7
|
-
* it holds (
|
|
7
|
+
* it holds (`./float-name.ts`), so a fold needs one or the other:
|
|
8
8
|
*
|
|
9
9
|
* - `fold-unnamed` (error): a `<Fold>` with no `label` and no `<Figure>`,
|
|
10
10
|
* `<Table>` or `<Diagram>` that takes a number inside it — its bar would
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MathGateDiagnostic } from './gate-math.js';
|
|
2
2
|
import { NotationRegistryInput, AbbreviationDefinitionInput } from './types.js';
|
|
3
|
-
import {
|
|
3
|
+
import { l as CourseAbbreviations } from '../course-config-BLtIdkei.js';
|
|
4
4
|
import '../rules/catalog.js';
|
|
5
5
|
import '../rules/course-rules.js';
|
|
6
6
|
|