explico 0.3.0 → 0.5.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.
Files changed (151) hide show
  1. package/CHANGELOG.md +344 -1
  2. package/README.md +117 -3
  3. package/dist/compiler/astro.d.ts +38 -0
  4. package/dist/compiler/astro.js +258 -0
  5. package/dist/compiler/cli.d.ts +13 -0
  6. package/dist/compiler/cli.js +32 -12
  7. package/dist/compiler/collections.d.ts +3 -2
  8. package/dist/compiler/collections.js +28 -1
  9. package/dist/compiler/course-config.d.ts +86 -4
  10. package/dist/compiler/course-config.js +39 -0
  11. package/dist/compiler/exports/standalone-html.d.ts +34 -0
  12. package/dist/compiler/exports/standalone-html.js +803 -0
  13. package/dist/compiler/lesson-derivation.d.ts +1 -1
  14. package/dist/compiler/lesson-derivation.js +2 -2
  15. package/dist/compiler/manifest.d.ts +112 -7
  16. package/dist/compiler/manifest.js +205 -40
  17. package/dist/compiler/resolution-report.d.ts +4 -0
  18. package/dist/reference/base-abbreviations.d.mts +30 -0
  19. package/dist/reference/base-abbreviations.mjs +60 -0
  20. package/dist/reference/base-operators.d.mts +15 -0
  21. package/dist/reference/base-operators.mjs +40 -0
  22. package/dist/reference/base-roman-labels.d.mts +35 -0
  23. package/dist/reference/base-roman-labels.mjs +43 -0
  24. package/dist/reference/consistency.d.ts +8 -2
  25. package/dist/reference/consistency.js +50 -3
  26. package/dist/reference/diagrams-validate.d.ts +21 -0
  27. package/dist/reference/diagrams-validate.js +13 -0
  28. package/dist/reference/equations-validate.d.ts +8 -3
  29. package/dist/reference/equations-validate.js +1 -1
  30. package/dist/reference/equations.d.mts +66 -35
  31. package/dist/reference/equations.mjs +142 -40
  32. package/dist/reference/gate-abbreviations.d.ts +17 -0
  33. package/dist/reference/gate-abbreviations.js +64 -0
  34. package/dist/reference/gate-math.d.ts +28 -5
  35. package/dist/reference/gate-math.js +5 -2
  36. package/dist/reference/gate-prose-notation.d.ts +60 -0
  37. package/dist/reference/gate-prose-notation.js +125 -0
  38. package/dist/reference/keyed-float-validate.d.ts +14 -8
  39. package/dist/reference/keyed-float-validate.js +1 -1
  40. package/dist/reference/keyed-jsx-floats.d.mts +24 -21
  41. package/dist/reference/keyed-jsx-floats.mjs +90 -41
  42. package/dist/reference/knowledge-checks-validate.d.ts +35 -0
  43. package/dist/reference/knowledge-checks-validate.js +78 -0
  44. package/dist/reference/knowledge-checks.d.mts +25 -0
  45. package/dist/reference/knowledge-checks.mjs +45 -0
  46. package/dist/reference/lab-math-scope.d.ts +9 -1
  47. package/dist/reference/lab-math-scope.js +32 -4
  48. package/dist/reference/math-glyphs.d.mts +30 -0
  49. package/dist/reference/math-glyphs.mjs +367 -0
  50. package/dist/reference/mdx-attributes.d.mts +13 -0
  51. package/dist/reference/mdx-attributes.mjs +38 -0
  52. package/dist/reference/mixed-content.d.ts +17 -0
  53. package/dist/reference/mixed-content.js +29 -0
  54. package/dist/reference/page-notation.d.ts +52 -0
  55. package/dist/reference/page-notation.js +171 -0
  56. package/dist/reference/raw-floats-validate.d.ts +18 -0
  57. package/dist/reference/raw-floats-validate.js +53 -0
  58. package/dist/reference/raw-floats.d.mts +18 -0
  59. package/dist/reference/raw-floats.mjs +129 -0
  60. package/dist/reference/rehype-heading-anchors.d.mts +1 -1
  61. package/dist/reference/rehype-heading-anchors.mjs +107 -1
  62. package/dist/reference/rehype-table-header-labels.d.mts +26 -0
  63. package/dist/reference/rehype-table-header-labels.mjs +152 -0
  64. package/dist/reference/remark-abbreviations.d.mts +7 -0
  65. package/dist/reference/remark-abbreviations.mjs +174 -0
  66. package/dist/reference/remark-memo.d.mts +16 -0
  67. package/dist/reference/remark-memo.mjs +339 -0
  68. package/dist/reference/remark-notation.d.mts +102 -5
  69. package/dist/reference/remark-notation.mjs +506 -50
  70. package/dist/reference/remark-short-note.d.mts +4 -0
  71. package/dist/reference/remark-short-note.mjs +237 -0
  72. package/dist/reference/types.d.ts +16 -1
  73. package/dist/reference/units-vocab.js +4 -0
  74. package/docs/VERSIONING.md +33 -4
  75. package/docs/authoring/README.md +12 -0
  76. package/docs/authoring/agents.md +17 -2
  77. package/docs/authoring/components.md +135 -0
  78. package/docs/authoring/migrating.md +154 -7
  79. package/docs/authoring/notation.md +568 -8
  80. package/docs/authoring/structure.md +251 -0
  81. package/docs/authoring/visualizations.md +36 -3
  82. package/package.json +5 -2
  83. package/src/compiler/astro.ts +332 -0
  84. package/src/compiler/cli.ts +33 -10
  85. package/src/compiler/collections.ts +45 -4
  86. package/src/compiler/course-config.ts +131 -3
  87. package/src/compiler/exports/standalone-html.ts +963 -0
  88. package/src/compiler/lesson-derivation.ts +2 -2
  89. package/src/compiler/manifest.ts +367 -49
  90. package/src/components/HoverPanel.astro +45 -4
  91. package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
  92. package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
  93. package/src/components/assessments/AssessmentRunner.tsx +52 -17
  94. package/src/components/assessments/AssessmentSet.astro +98 -14
  95. package/src/components/assessments/RenderedContent.tsx +55 -0
  96. package/src/components/assessments/assessment-randomization.ts +16 -0
  97. package/src/components/diagram/Diagram.astro +444 -83
  98. package/src/components/disclosure/hover-preview.ts +202 -0
  99. package/src/components/disclosure/persisted-disclosure.ts +66 -0
  100. package/src/components/examples/CompactExamples.astro +2 -0
  101. package/src/components/figure/Figure.astro +279 -10
  102. package/src/components/memo/Memo.astro +453 -0
  103. package/src/components/memo/ShortNote.astro +223 -0
  104. package/src/components/notation/NotationFormula.astro +18 -1
  105. package/src/components/notation/NotationLayer.astro +116 -8
  106. package/src/components/notation/render-notation-math.ts +39 -2
  107. package/src/components/notation/safe-math-to-react.ts +50 -0
  108. package/src/components/reference/FloatRefLayer.astro +233 -15
  109. package/src/components/starlight/CollapsibleSections.astro +461 -0
  110. package/src/components/starlight/DiagramEnhancer.astro +385 -0
  111. package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
  112. package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
  113. package/src/components/starlight/LayoutHeader.astro +50 -0
  114. package/src/components/starlight/LayoutSidebar.astro +62 -1
  115. package/src/components/starlight/LayoutSocialIcons.astro +41 -2
  116. package/src/components/starlight/LessonExportActions.astro +436 -0
  117. package/src/components/starlight/LessonFooter.astro +38 -2
  118. package/src/components/starlight/section-parts.ts +31 -0
  119. package/src/components/table/Table.astro +296 -9
  120. package/src/components/table/TableOverflowExpander.astro +489 -0
  121. package/src/content-config.ts +20 -0
  122. package/src/reference/base-abbreviations.mjs +60 -0
  123. package/src/reference/base-operators.mjs +40 -0
  124. package/src/reference/base-roman-labels.mjs +43 -0
  125. package/src/reference/consistency.ts +73 -2
  126. package/src/reference/diagrams-validate.ts +40 -0
  127. package/src/reference/equations-validate.ts +6 -4
  128. package/src/reference/equations.mjs +142 -40
  129. package/src/reference/gate-abbreviations.ts +128 -0
  130. package/src/reference/gate-math.ts +27 -7
  131. package/src/reference/gate-prose-notation.ts +224 -0
  132. package/src/reference/keyed-float-validate.ts +12 -9
  133. package/src/reference/keyed-jsx-floats.mjs +90 -41
  134. package/src/reference/knowledge-checks-validate.ts +130 -0
  135. package/src/reference/knowledge-checks.mjs +45 -0
  136. package/src/reference/lab-math-scope.ts +45 -3
  137. package/src/reference/math-glyphs.mjs +367 -0
  138. package/src/reference/mdx-attributes.mjs +38 -0
  139. package/src/reference/mixed-content.ts +69 -0
  140. package/src/reference/page-notation.ts +252 -0
  141. package/src/reference/raw-floats-validate.ts +103 -0
  142. package/src/reference/raw-floats.mjs +129 -0
  143. package/src/reference/rehype-heading-anchors.mjs +107 -1
  144. package/src/reference/rehype-table-header-labels.mjs +152 -0
  145. package/src/reference/remark-abbreviations.mjs +174 -0
  146. package/src/reference/remark-memo.mjs +339 -0
  147. package/src/reference/remark-notation.mjs +506 -50
  148. package/src/reference/remark-short-note.mjs +237 -0
  149. package/src/reference/types.ts +16 -0
  150. package/src/reference/units-vocab.ts +4 -0
  151. package/src/styles/global.css +1125 -50
@@ -0,0 +1,258 @@
1
+ import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { spawn } from "node:child_process";
6
+ import {
7
+ compileManifest,
8
+ writeManifest
9
+ } from "./manifest.js";
10
+ import { missingNotationPayloadKeys } from "../reference/page-notation.js";
11
+ import { writeStandaloneLessonExports } from "./exports/standalone-html.js";
12
+ const watchedServers = /* @__PURE__ */ new WeakSet();
13
+ function normalizedBasePath(base) {
14
+ const segment = base.replace(/^\/+|\/+$/gu, "");
15
+ return segment ? `/${segment}/` : "/";
16
+ }
17
+ function matchStandaloneDownload(pathname, base, lessonSlugs) {
18
+ const prefix = `${normalizedBasePath(base)}downloads/`;
19
+ if (!pathname.startsWith(prefix) || !pathname.endsWith(".html")) return void 0;
20
+ const slug = pathname.slice(prefix.length, -".html".length);
21
+ return lessonSlugs.includes(slug) ? slug : void 0;
22
+ }
23
+ async function buildDevelopmentDownloads(config, lessonSlugs) {
24
+ const temporaryRoot = await mkdtemp(join(tmpdir(), "explico-standalone-"));
25
+ const outputRoot = join(temporaryRoot, "dist");
26
+ try {
27
+ const projectRoot = fileURLToPath(config.root);
28
+ const astroEntry = fileURLToPath(import.meta.resolve("astro"));
29
+ const astroCli = resolve(dirname(astroEntry), "..", "bin", "astro.mjs");
30
+ await new Promise((resolveBuild, rejectBuild) => {
31
+ const child = spawn(
32
+ process.execPath,
33
+ [
34
+ astroCli,
35
+ "build",
36
+ "--root",
37
+ projectRoot,
38
+ "--outDir",
39
+ outputRoot,
40
+ "--silent"
41
+ ],
42
+ {
43
+ cwd: projectRoot,
44
+ env: { ...process.env, NODE_ENV: "production" },
45
+ stdio: ["ignore", "pipe", "pipe"]
46
+ }
47
+ );
48
+ let diagnostic = "";
49
+ const capture = (chunk) => {
50
+ if (diagnostic.length < 16e3) diagnostic += chunk.toString("utf8");
51
+ };
52
+ child.stdout.on("data", capture);
53
+ child.stderr.on("data", capture);
54
+ child.once("error", rejectBuild);
55
+ child.once("close", (code) => {
56
+ if (code === 0) {
57
+ resolveBuild();
58
+ return;
59
+ }
60
+ rejectBuild(
61
+ new Error(
62
+ `Astro's standalone development build exited with code ${code}.` + (diagnostic.trim() ? `
63
+ ${diagnostic.trim()}` : "")
64
+ )
65
+ );
66
+ });
67
+ });
68
+ return new Map(
69
+ await Promise.all(
70
+ lessonSlugs.map(async (slug) => [
71
+ slug,
72
+ await readFile(join(outputRoot, "downloads", `${slug}.html`))
73
+ ])
74
+ )
75
+ );
76
+ } finally {
77
+ await rm(temporaryRoot, { recursive: true, force: true });
78
+ }
79
+ }
80
+ function assertLocalNotationParity(lessonId, authoredKeys, renderedKeys) {
81
+ const authored = [...new Set(authoredKeys)].sort();
82
+ const rendered = [...new Set(renderedKeys)].sort();
83
+ if (authored.length === rendered.length && authored.every((key, index) => key === rendered[index])) {
84
+ return;
85
+ }
86
+ throw new Error(
87
+ `Stale Explico manifest for ${lessonId}: lesson notation.local has [${authored.join(", ")}], but the page notation bundle has [${rendered.join(", ")}]. Pass courseConfig to loadManifest() and add explicoContentRefresh() to the Astro integrations.`
88
+ );
89
+ }
90
+ function assertNotationPayloadHtml(page, html) {
91
+ const missing = missingNotationPayloadKeys(html);
92
+ if (missing.length === 0) return;
93
+ throw new Error(
94
+ `Explico notation payload invariant failed for rendered page "${page}": ${missing.map((key) => `data-notation-key="${key}"`).join(", ")} ${missing.length === 1 ? "has" : "have"} no matching \`data-notation-entry\` popup definition. A nested Astro component that emits notation must register its formula dependencies; use Diagram, NotationFormula, or labMathScope(..., { locals: Astro.locals, context }).`
95
+ );
96
+ }
97
+ async function validateGeneratedNotationPayloads(directory) {
98
+ const root = fileURLToPath(directory);
99
+ const walk = async (path) => {
100
+ for (const entry of await readdir(path, { withFileTypes: true })) {
101
+ const child = join(path, entry.name);
102
+ if (entry.isDirectory()) {
103
+ await walk(child);
104
+ } else if (entry.isFile() && entry.name.endsWith(".html")) {
105
+ assertNotationPayloadHtml(relative(root, child), await readFile(child, "utf8"));
106
+ }
107
+ }
108
+ };
109
+ await walk(root);
110
+ }
111
+ function replaceInPlace(target, source) {
112
+ if (Array.isArray(target) && Array.isArray(source)) {
113
+ target.splice(0, target.length, ...source);
114
+ return;
115
+ }
116
+ if (!target || !source || typeof target !== "object" || typeof source !== "object") {
117
+ return;
118
+ }
119
+ const mutable = target;
120
+ const fresh = source;
121
+ for (const key of Object.keys(mutable)) {
122
+ if (!(key in fresh)) delete mutable[key];
123
+ }
124
+ for (const [key, value] of Object.entries(fresh)) {
125
+ const current = mutable[key];
126
+ if (Array.isArray(current) && Array.isArray(value) || current && value && typeof current === "object" && typeof value === "object" && !Array.isArray(current) && !Array.isArray(value)) {
127
+ replaceInPlace(current, value);
128
+ } else {
129
+ mutable[key] = value;
130
+ }
131
+ }
132
+ }
133
+ function explicoContentRefresh(courseConfig, manifest) {
134
+ let developmentConfig;
135
+ return {
136
+ name: "explico-content-refresh",
137
+ hooks: {
138
+ "astro:config:setup": ({ command, config }) => {
139
+ if (command === "dev") {
140
+ developmentConfig = { root: config.root, base: config.base };
141
+ }
142
+ },
143
+ "astro:server:setup": ({ server, logger }) => {
144
+ if (watchedServers.has(server)) return;
145
+ watchedServers.add(server);
146
+ const contentRoot = `${resolve(process.cwd(), "content")}${sep}`;
147
+ let timer;
148
+ let restarting = false;
149
+ let developmentDownloads;
150
+ const developmentRoot = developmentConfig ? fileURLToPath(developmentConfig.root) : process.cwd();
151
+ const generatedDevelopmentRoots = [
152
+ ".astro",
153
+ "build",
154
+ "dist",
155
+ "node_modules"
156
+ ].map((directory) => `${resolve(developmentRoot, directory)}${sep}`);
157
+ const invalidateDevelopmentDownloads = (path) => {
158
+ const absolute = resolve(path);
159
+ if (generatedDevelopmentRoots.some((root) => absolute.startsWith(root))) {
160
+ return;
161
+ }
162
+ developmentDownloads = void 0;
163
+ };
164
+ const refresh = (path) => {
165
+ const absolute = resolve(path);
166
+ if (!absolute.startsWith(contentRoot) || restarting) return;
167
+ if (timer) clearTimeout(timer);
168
+ timer = setTimeout(async () => {
169
+ restarting = true;
170
+ try {
171
+ const fresh = await compileManifest(courseConfig);
172
+ writeManifest(fresh);
173
+ replaceInPlace(manifest, fresh);
174
+ await server.restart();
175
+ } catch (error) {
176
+ server.config.logger.error(
177
+ `Explico could not refresh the content manifest: ${error instanceof Error ? error.message : String(error)}`
178
+ );
179
+ } finally {
180
+ restarting = false;
181
+ }
182
+ }, 50);
183
+ };
184
+ server.watcher.on("add", refresh);
185
+ server.watcher.on("change", refresh);
186
+ server.watcher.on("unlink", refresh);
187
+ const configuredDevelopment = developmentConfig;
188
+ if (manifest.lessonExports?.standaloneHtml && configuredDevelopment) {
189
+ server.watcher.on("add", invalidateDevelopmentDownloads);
190
+ server.watcher.on("change", invalidateDevelopmentDownloads);
191
+ server.watcher.on("unlink", invalidateDevelopmentDownloads);
192
+ server.middlewares.use(async (request, response, next) => {
193
+ if (request.method !== "GET" && request.method !== "HEAD") return next();
194
+ let pathname;
195
+ try {
196
+ pathname = decodeURIComponent(
197
+ new URL(request.url ?? "/", "http://localhost").pathname
198
+ );
199
+ } catch {
200
+ return next();
201
+ }
202
+ const slug = matchStandaloneDownload(
203
+ pathname,
204
+ configuredDevelopment.base,
205
+ manifest.lessons.map((lesson) => lesson.slug)
206
+ );
207
+ if (!slug) return next();
208
+ try {
209
+ logger.info(
210
+ developmentDownloads ? `Serving cached standalone lesson ${slug}.html` : "Building standalone lesson downloads for local development\u2026"
211
+ );
212
+ developmentDownloads ??= buildDevelopmentDownloads(
213
+ configuredDevelopment,
214
+ manifest.lessons.map((lesson) => lesson.slug)
215
+ );
216
+ const currentDownloads = developmentDownloads;
217
+ currentDownloads.catch(() => {
218
+ if (developmentDownloads === currentDownloads) {
219
+ developmentDownloads = void 0;
220
+ }
221
+ });
222
+ const artifact = (await currentDownloads).get(slug);
223
+ if (!artifact) return next();
224
+ const filename = `${slug.split("/").at(-1) ?? "lesson"}.html`;
225
+ response.statusCode = 200;
226
+ response.setHeader("Content-Type", "text/html; charset=utf-8");
227
+ response.setHeader("Content-Length", artifact.byteLength);
228
+ response.setHeader(
229
+ "Content-Disposition",
230
+ `attachment; filename="${filename}"`
231
+ );
232
+ response.setHeader("Cache-Control", "no-store");
233
+ response.end(request.method === "HEAD" ? void 0 : artifact);
234
+ } catch (error) {
235
+ const message = error instanceof Error ? error.message : String(error);
236
+ logger.error(`Could not build standalone lesson downloads: ${message}`);
237
+ response.statusCode = 500;
238
+ response.setHeader("Content-Type", "text/plain; charset=utf-8");
239
+ response.end(`Could not build standalone lesson download.
240
+ ${message}
241
+ `);
242
+ }
243
+ });
244
+ }
245
+ },
246
+ "astro:build:done": async ({ dir }) => {
247
+ await validateGeneratedNotationPayloads(dir);
248
+ await writeStandaloneLessonExports(dir, manifest);
249
+ }
250
+ }
251
+ };
252
+ }
253
+ export {
254
+ assertLocalNotationParity,
255
+ assertNotationPayloadHtml,
256
+ explicoContentRefresh,
257
+ matchStandaloneDownload
258
+ };
@@ -9,7 +9,11 @@ import '../reference/equations-validate.js';
9
9
  import '../reference/tables-validate.js';
10
10
  import '../reference/keyed-float-validate.js';
11
11
  import '../reference/figures-validate.js';
12
+ import '../reference/diagrams-validate.js';
13
+ import '../reference/knowledge-checks-validate.js';
14
+ import '../reference/raw-floats-validate.js';
12
15
  import '../reference/gate-math.js';
16
+ import '../reference/mixed-content.js';
13
17
  import '../reference/gloss.js';
14
18
  import './knowledge-version.js';
15
19
  import './lesson-derivation.js';
@@ -107,6 +111,7 @@ interface StatusReport {
107
111
  readonly keyedEquations: number;
108
112
  readonly keyedTables: number;
109
113
  readonly keyedFigures: number;
114
+ readonly keyedDiagrams: number;
110
115
  };
111
116
  readonly draftDebt: {
112
117
  readonly lessons: readonly string[];
@@ -161,6 +166,14 @@ interface ContextReport {
161
166
  key: string;
162
167
  number: string;
163
168
  }[];
169
+ readonly diagrams: readonly {
170
+ key: string;
171
+ number: string;
172
+ }[];
173
+ readonly knowledgeChecks: readonly {
174
+ key: string;
175
+ number: string;
176
+ }[];
164
177
  readonly sources: readonly {
165
178
  id: string;
166
179
  title: string;
@@ -12,7 +12,11 @@ import remarkMdx from "remark-mdx";
12
12
  import { createNotationKatexOptions } from "../reference/katex-options.mjs";
13
13
  import rehypeFailKatexErrors from "../reference/rehype-fail-katex-errors.mjs";
14
14
  import remarkCitation from "../reference/remark-citation.mjs";
15
- import remarkNotation from "../reference/remark-notation.mjs";
15
+ import remarkMemo from "../reference/remark-memo.mjs";
16
+ import remarkNotation, {
17
+ createRemarkNotationOptions
18
+ } from "../reference/remark-notation.mjs";
19
+ import remarkShortNote from "../reference/remark-short-note.mjs";
16
20
  import { isLessonSlug, lessonIdFromSlug } from "./lesson-derivation.js";
17
21
  const REPOSITORY_ROOT = process.cwd();
18
22
  const CONTENT_ROOT = join(REPOSITORY_ROOT, "content");
@@ -84,7 +88,10 @@ function lessonDiagnostics(manifest, lesson) {
84
88
  }
85
89
  for (const diagnostic of [
86
90
  ...manifest.diagnostics.tables,
87
- ...manifest.diagnostics.figures
91
+ ...manifest.diagnostics.figures,
92
+ ...manifest.diagnostics.diagrams,
93
+ ...manifest.diagnostics.checks,
94
+ ...manifest.diagnostics.rawFloats
88
95
  ]) {
89
96
  if (diagnostic.lessonId !== id && diagnostic.file !== file && !mentions(diagnostic.message, id, file)) {
90
97
  continue;
@@ -121,15 +128,18 @@ function processorFor(manifest) {
121
128
  // shape here that it does there (D7/F-tables-figures parity).
122
129
  remarkMdx,
123
130
  remarkMath,
124
- [
125
- remarkNotation,
126
- {
127
- definitions: manifest.notation.raw,
128
- equations: manifest.equations.numbersBySlug,
129
- tables: manifest.tables.numbersBySlug,
130
- figures: manifest.figures.numbersBySlug
131
- }
132
- ],
131
+ remarkShortNote,
132
+ remarkMemo,
133
+ // `manifest` carries the number tables *and* the part/offset placement
134
+ // together, so this check numbers a page exactly as the Astro build
135
+ // does. Passing the tables alone silently numbered pages by `##` section
136
+ // here while cross-page references read the part-scheme manifest.
137
+ // No `strict: true` here: `renderLesson()` below is also called
138
+ // directly with a synthetic path/body that has no manifest entry at all
139
+ // (fixture renderability checks — see its own doc comment), which
140
+ // strict mode would reject as "catalogued lesson with no numbering",
141
+ // even though it was never meant to be catalogued in the first place.
142
+ [remarkNotation, createRemarkNotationOptions(manifest)],
133
143
  [remarkCitation, { sources: manifest.sources }]
134
144
  ],
135
145
  rehypePlugins: [
@@ -303,6 +313,9 @@ function contentStatus(manifest) {
303
313
  ...manifest.diagnostics.equations.map((d) => d.severity),
304
314
  ...manifest.diagnostics.tables.map((d) => d.severity),
305
315
  ...manifest.diagnostics.figures.map((d) => d.severity),
316
+ ...manifest.diagnostics.diagrams.map((d) => d.severity),
317
+ ...manifest.diagnostics.checks.map((d) => d.severity),
318
+ ...manifest.diagnostics.rawFloats.map((d) => d.severity),
306
319
  ...manifest.diagnostics.consistency.map(() => "warning"),
307
320
  ...manifest.diagnostics.knowledge.map((d) => d.severity)
308
321
  ];
@@ -318,6 +331,10 @@ function contentStatus(manifest) {
318
331
  (count, lesson) => count + lesson.figures.length,
319
332
  0
320
333
  );
334
+ const keyedDiagrams = manifest.lessons.reduce(
335
+ (count, lesson) => count + lesson.diagrams.length,
336
+ 0
337
+ );
321
338
  const assessments = new Set(
322
339
  manifest.lessons.flatMap((lesson) => lesson.assessments)
323
340
  ).size;
@@ -332,7 +349,8 @@ function contentStatus(manifest) {
332
349
  notationDefinitions: manifest.notation.definitions.length,
333
350
  keyedEquations,
334
351
  keyedTables,
335
- keyedFigures
352
+ keyedFigures,
353
+ keyedDiagrams
336
354
  },
337
355
  draftDebt: { lessons: draftLessons, notation: draftNotation },
338
356
  orphans,
@@ -388,6 +406,8 @@ function contentContext(manifest, rawId) {
388
406
  equations: lesson.equations.map((equation) => ({ ...equation })),
389
407
  tables: lesson.tables.map((table) => ({ ...table })),
390
408
  figures: lesson.figures.map((figure) => ({ ...figure })),
409
+ diagrams: lesson.diagrams.map((diagram) => ({ ...diagram })),
410
+ knowledgeChecks: lesson.knowledgeChecks.map((check) => ({ ...check })),
391
411
  sources: lesson.sources.map((id) => {
392
412
  const record = sourceById.get(id);
393
413
  return {
@@ -1,5 +1,5 @@
1
1
  import { CurriculumCatalog } from '../curriculum/validation.js';
2
- import { NotationReferenceInput, NotationRegistryInput } from '../reference/types.js';
2
+ import { NotationReferenceInput, AbbreviationDefinitionInput, NotationRegistryInput } from '../reference/types.js';
3
3
 
4
4
  /**
5
5
  * The single path from content files to typed data.
@@ -20,6 +20,7 @@ import { NotationReferenceInput, NotationRegistryInput } from '../reference/type
20
20
  */
21
21
  declare function extractNotationReferences(text: string, file: string, kind: NotationReferenceInput['kind'] | undefined): NotationReferenceInput[];
22
22
  declare function loadCurriculumCatalog(): Promise<CurriculumCatalog>;
23
+ declare function loadAbbreviationRegistryInput(): Promise<readonly AbbreviationDefinitionInput[]>;
23
24
  declare function loadNotationRegistryInput(): Promise<NotationRegistryInput>;
24
25
  /**
25
26
  * Notation frontmatter objects for `remark-notation`'s `definitions` option.
@@ -67,4 +68,4 @@ declare function notationSourceLocatorGaps(): {
67
68
  total: number;
68
69
  };
69
70
 
70
- export { type NotationSpec, extractNotationReferences, loadCurriculumCatalog, loadLessonChecks, loadNotationDefinitions, loadNotationRegistryInput, loadNotationSpecs, loadSourceRecords, notationSourceLocatorGaps };
71
+ export { type NotationSpec, extractNotationReferences, loadAbbreviationRegistryInput, loadCurriculumCatalog, loadLessonChecks, loadNotationDefinitions, loadNotationRegistryInput, loadNotationSpecs, loadSourceRecords, notationSourceLocatorGaps };
@@ -4,6 +4,8 @@ import { parseFrontmatter } from "@astrojs/markdown-remark";
4
4
  import { isSubstantiveMeaning } from "../reference/prose.js";
5
5
  import { parseEquationRef } from "../reference/equations.mjs";
6
6
  import { parseKeyedFloatRef } from "../reference/keyed-jsx-floats.mjs";
7
+ import { parseKnowledgeCheckRef } from "../reference/knowledge-checks.mjs";
8
+ import { parseMemoRef } from "../reference/remark-memo.mjs";
7
9
  import { resolveLabel } from "../reference/label.js";
8
10
  import { resolveGlosses } from "../reference/gloss.js";
9
11
  import {
@@ -137,7 +139,7 @@ function extractNotationReferences(text, file, kind) {
137
139
  for (const [pattern, referenceKind] of patterns) {
138
140
  for (const match of searchable.matchAll(pattern)) {
139
141
  const key = (match[1] ?? "").trim();
140
- if (parseEquationRef(key) || parseKeyedFloatRef(key, { prefix: "tab" }) || parseKeyedFloatRef(key, { prefix: "fig" })) {
142
+ if (parseEquationRef(key) || parseKeyedFloatRef(key, { prefix: "tab" }) || parseKeyedFloatRef(key, { prefix: "fig" }) || parseKeyedFloatRef(key, { prefix: "dia" }) || parseKnowledgeCheckRef(key) || parseMemoRef(key)) {
141
143
  continue;
142
144
  }
143
145
  references.push({
@@ -291,6 +293,30 @@ function sharedNotationEntries() {
291
293
  };
292
294
  });
293
295
  }
296
+ function abbreviationEntries() {
297
+ const directory = join(CONTENT_ROOT, "abbreviations");
298
+ if (!existsSync(directory)) return [];
299
+ return filesBelow(directory, [".md"]).map((path) => {
300
+ const { file, data } = parseDocument(path);
301
+ const key = string(data.key, `${file} key`);
302
+ const filenameKey = basename(path, ".md");
303
+ if (key !== filenameKey) {
304
+ throw new Error(`${file} declares key ${key}; expected ${filenameKey}`);
305
+ }
306
+ const domain = optionalString(data.domain, `${file} domain`);
307
+ return {
308
+ key,
309
+ abbr: string(data.abbr, `${file} abbr`),
310
+ expansion: meaningText(data.expansion, `${file} expansion`),
311
+ aliases: strings(data.aliases, `${file} aliases`),
312
+ source: { file },
313
+ ...domain === void 0 ? {} : { domain }
314
+ };
315
+ });
316
+ }
317
+ async function loadAbbreviationRegistryInput() {
318
+ return abbreviationEntries();
319
+ }
294
320
  function notationLessonEntries() {
295
321
  const lessons = [];
296
322
  for (const path of filesBelow(join(CONTENT_ROOT, "docs"), [".md", ".mdx"])) {
@@ -403,6 +429,7 @@ function notationSourceLocatorGaps() {
403
429
  }
404
430
  export {
405
431
  extractNotationReferences,
432
+ loadAbbreviationRegistryInput,
406
433
  loadCurriculumCatalog,
407
434
  loadLessonChecks,
408
435
  loadNotationDefinitions,
@@ -6,12 +6,23 @@
6
6
  * data value and passes it here, typed against `CourseConfig`. A second course
7
7
  * supplies its own value of this shape.
8
8
  */
9
- interface CourseSection {
9
+ /**
10
+ * One **part** — a sidebar group, holding the chapters (lessons) whose slugs
11
+ * start with `dir`. A part's 1-based position in `CourseConfig.parts` is the
12
+ * number before the dot in every equation, table, and figure number inside it.
13
+ * See `docs/authoring/structure.md` for the full level taxonomy.
14
+ */
15
+ interface CoursePart {
10
16
  /** Lesson-slug prefix, e.g. `bonds` for `bonds/yield-to-maturity`. */
11
17
  readonly dir: string;
12
18
  /** Sidebar group heading. */
13
19
  readonly label: string;
14
20
  }
21
+ /**
22
+ * @deprecated Renamed to {@link CoursePart}. "Section" now only ever means an
23
+ * `##` heading inside a lesson. Removed in the next MINOR release.
24
+ */
25
+ type CourseSection = CoursePart;
15
26
  interface CourseConventions {
16
27
  /** Notation key a lesson's stated sign convention must resolve to. */
17
28
  readonly signConventionKey: string;
@@ -33,6 +44,37 @@ interface CourseOperators {
33
44
  /** Baseline operators this course wants explained anyway. */
34
45
  readonly deny?: readonly string[];
35
46
  }
47
+ /**
48
+ * A course's edits to the shipped roman-label baseline (`BASE_ROMAN_LABELS`),
49
+ * consumed by the `roman-label-explained` consistency check (rule N10). A
50
+ * `\mathrm{…}` multi-letter label a lesson uses is fine when it is on the
51
+ * baseline plus `allow` minus `deny`; otherwise it needs a notation entry the
52
+ * reader can hover. Same shape as {@link CourseOperators}, one level down:
53
+ * this governs `\mathrm{KE}`-style labels, not `\operatorname{Var}`-style
54
+ * operators.
55
+ */
56
+ interface CourseRomanLabels {
57
+ /** Labels this course also treats as universal qualifiers (`eff`, `net`, …). */
58
+ readonly allow?: readonly string[];
59
+ /** Baseline labels this course wants explained anyway. */
60
+ readonly deny?: readonly string[];
61
+ }
62
+ /**
63
+ * A course's edits to the shipped abbreviation baseline (`BASE_ABBREVIATIONS`),
64
+ * consumed by the `abbreviation-explained` gate (rule N14). An abbreviation-
65
+ * shaped token — an all-caps acronym or a dotted form like `w.r.t.` — a
66
+ * lesson uses in prose is fine when it is on the baseline plus `allow` minus
67
+ * `deny`; otherwise it needs an entry in the `abbreviations` content
68
+ * collection, or — if it also names a math quantity — a notation entry the
69
+ * reader can hover. Same shape as {@link CourseOperators} / {@link
70
+ * CourseRomanLabels}.
71
+ */
72
+ interface CourseAbbreviations {
73
+ /** Abbreviations this course also treats as universal (`GDP`, `CPU`, …). */
74
+ readonly allow?: readonly string[];
75
+ /** Baseline abbreviations this course wants explained anyway. */
76
+ readonly deny?: readonly string[];
77
+ }
36
78
  /** One person or team credited for the study material. */
37
79
  interface CourseAuthor {
38
80
  readonly name: string;
@@ -65,11 +107,37 @@ interface CourseAbout {
65
107
  /** Link to the published content changelog (repo blob, docs page, …). */
66
108
  readonly changelogHref?: string;
67
109
  }
110
+ /** Optional, build-time lesson artifacts exposed to readers. */
111
+ interface LessonExportsConfig {
112
+ /** Emit one self-contained, offline-capable HTML file per lesson. */
113
+ readonly standaloneHtml?: boolean;
114
+ /** Offer the browser's native print dialog with explico's lesson print layout. */
115
+ readonly print?: boolean;
116
+ /**
117
+ * Published course root used for links that intentionally leave the exported
118
+ * lesson. Required at export time when such a link is present.
119
+ */
120
+ readonly canonicalBaseUrl?: string;
121
+ }
122
+ /** The stable, non-optional export settings carried by the manifest. */
123
+ interface NormalizedLessonExports {
124
+ readonly standaloneHtml: boolean;
125
+ readonly print: boolean;
126
+ readonly canonicalBaseUrl?: string;
127
+ }
68
128
  interface CourseConfig {
69
129
  readonly title: string;
70
130
  readonly description: string;
71
- /** Sidebar section groups, in order. */
72
- readonly sections: readonly CourseSection[];
131
+ /**
132
+ * The course's parts (sidebar groups), in order. Exactly one of `parts` or
133
+ * the deprecated `sections` must be given.
134
+ */
135
+ readonly parts?: readonly CoursePart[];
136
+ /**
137
+ * @deprecated Renamed to {@link CourseConfig.parts}. Still honoured, so a
138
+ * course on the old name keeps building; removed in the next MINOR release.
139
+ */
140
+ readonly sections?: readonly CoursePart[];
73
141
  /** Namespaces used by competency / notation `domain` fields. */
74
142
  readonly domains: readonly string[];
75
143
  readonly conventions: CourseConventions;
@@ -77,6 +145,20 @@ interface CourseConfig {
77
145
  readonly about: CourseAbout;
78
146
  /** Edits to the shipped named-operator baseline (rule N8). Optional. */
79
147
  readonly operators?: CourseOperators;
148
+ /** Edits to the shipped roman-label baseline (rule N10). Optional. */
149
+ readonly romanLabels?: CourseRomanLabels;
150
+ /** Edits to the shipped abbreviation baseline (rule N14). Optional. */
151
+ readonly abbreviations?: CourseAbbreviations;
152
+ /** Per-lesson export actions and downloadable artifacts. Disabled when omitted. */
153
+ readonly lessonExports?: LessonExportsConfig;
80
154
  }
155
+ /** Validate and canonicalize the build-facing lesson-export configuration. */
156
+ declare function normalizeLessonExports(config: CourseConfig): NormalizedLessonExports;
157
+ /**
158
+ * The course's parts, from whichever field the config uses. Throws rather than
159
+ * silently producing a course with no sidebar groups — a config with neither
160
+ * field would otherwise number every float by the `##`-section fallback.
161
+ */
162
+ declare function coursePartsOf(config: CourseConfig): readonly CoursePart[];
81
163
 
82
- export type { CourseAbout, CourseAuthor, CourseConfig, CourseConventions, CourseOperators, CourseSection };
164
+ export { type CourseAbbreviations, type CourseAbout, type CourseAuthor, type CourseConfig, type CourseConventions, type CourseOperators, type CoursePart, type CourseRomanLabels, type CourseSection, type LessonExportsConfig, type NormalizedLessonExports, coursePartsOf, normalizeLessonExports };
@@ -0,0 +1,39 @@
1
+ function normalizeLessonExports(config) {
2
+ const standaloneHtml = config.lessonExports?.standaloneHtml === true;
3
+ const print = config.lessonExports?.print === true;
4
+ const rawBase = config.lessonExports?.canonicalBaseUrl;
5
+ if (rawBase === void 0) return { standaloneHtml, print };
6
+ let parsed;
7
+ try {
8
+ parsed = new URL(rawBase);
9
+ } catch {
10
+ throw new Error(
11
+ `CourseConfig lessonExports.canonicalBaseUrl must be an absolute http(s) URL; received ${JSON.stringify(rawBase)}.`
12
+ );
13
+ }
14
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
15
+ throw new Error(
16
+ `CourseConfig lessonExports.canonicalBaseUrl must use http(s); received ${JSON.stringify(rawBase)}.`
17
+ );
18
+ }
19
+ if (parsed.search || parsed.hash) {
20
+ throw new Error(
21
+ "CourseConfig lessonExports.canonicalBaseUrl cannot contain a query or fragment."
22
+ );
23
+ }
24
+ if (!parsed.pathname.endsWith("/")) parsed.pathname += "/";
25
+ return { standaloneHtml, print, canonicalBaseUrl: parsed.toString() };
26
+ }
27
+ function coursePartsOf(config) {
28
+ const parts = config.parts ?? config.sections;
29
+ if (!parts || parts.length === 0) {
30
+ throw new Error(
31
+ "CourseConfig needs at least one entry in `parts` (a sidebar group: { dir, label }). The old name `sections` is still accepted."
32
+ );
33
+ }
34
+ return parts;
35
+ }
36
+ export {
37
+ coursePartsOf,
38
+ normalizeLessonExports
39
+ };
@@ -0,0 +1,34 @@
1
+ import { Manifest } from '../manifest.js';
2
+ import '../../curriculum/validation.js';
3
+ import '../../reference/consistency.js';
4
+ import '../course-config.js';
5
+ import '../collections.js';
6
+ import '../../reference/types.js';
7
+ import '../../reference/lint-ignore.js';
8
+ import '../../reference/equations-validate.js';
9
+ import '../../reference/tables-validate.js';
10
+ import '../../reference/keyed-float-validate.js';
11
+ import '../../reference/figures-validate.js';
12
+ import '../../reference/diagrams-validate.js';
13
+ import '../../reference/knowledge-checks-validate.js';
14
+ import '../../reference/raw-floats-validate.js';
15
+ import '../../reference/gate-math.js';
16
+ import '../../reference/mixed-content.js';
17
+ import '../../reference/gloss.js';
18
+ import '../knowledge-version.js';
19
+ import '../lesson-derivation.js';
20
+
21
+ /** Inputs for producing one standalone lesson from Astro's final HTML. */
22
+ interface StandaloneLessonInput {
23
+ readonly html: string;
24
+ readonly sourceFile: string;
25
+ readonly outputRoot: string;
26
+ readonly lessonSlug: string;
27
+ readonly canonicalBaseUrl?: string;
28
+ }
29
+ /** Build one fully inlined lesson document without writing it. */
30
+ declare function buildStandaloneLessonHtml(input: StandaloneLessonInput): Promise<string>;
31
+ /** Emit all opt-in lesson HTML artifacts from Astro's completed static output. */
32
+ declare function writeStandaloneLessonExports(directory: URL, manifest: Manifest): Promise<void>;
33
+
34
+ export { type StandaloneLessonInput, buildStandaloneLessonHtml, writeStandaloneLessonExports };