explico 0.4.0 → 0.6.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 (150) hide show
  1. package/CHANGELOG.md +382 -1
  2. package/README.md +103 -8
  3. package/dist/compiler/astro.d.ts +9 -1
  4. package/dist/compiler/astro.js +182 -3
  5. package/dist/compiler/cli.d.ts +13 -0
  6. package/dist/compiler/cli.js +28 -4
  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 +88 -1
  10. package/dist/compiler/course-config.js +35 -1
  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/manifest.d.ts +98 -6
  14. package/dist/compiler/manifest.js +154 -17
  15. package/dist/compiler/resolution-report.d.ts +4 -0
  16. package/dist/reference/base-abbreviations.d.mts +30 -0
  17. package/dist/reference/base-abbreviations.mjs +60 -0
  18. package/dist/reference/base-operators.d.mts +15 -0
  19. package/dist/reference/base-operators.mjs +40 -0
  20. package/dist/reference/base-roman-labels.d.mts +35 -0
  21. package/dist/reference/base-roman-labels.mjs +43 -0
  22. package/dist/reference/consistency.d.ts +8 -2
  23. package/dist/reference/consistency.js +50 -3
  24. package/dist/reference/diagrams-validate.d.ts +21 -0
  25. package/dist/reference/diagrams-validate.js +13 -0
  26. package/dist/reference/equations.d.mts +10 -15
  27. package/dist/reference/equations.mjs +67 -14
  28. package/dist/reference/gate-abbreviations.d.ts +17 -0
  29. package/dist/reference/gate-abbreviations.js +64 -0
  30. package/dist/reference/gate-heading-length.d.ts +12 -0
  31. package/dist/reference/gate-heading-length.js +79 -0
  32. package/dist/reference/gate-math.d.ts +32 -5
  33. package/dist/reference/gate-math.js +6 -2
  34. package/dist/reference/gate-prose-notation.d.ts +60 -0
  35. package/dist/reference/gate-prose-notation.js +125 -0
  36. package/dist/reference/keyed-float-validate.d.ts +5 -5
  37. package/dist/reference/keyed-jsx-floats.d.mts +7 -12
  38. package/dist/reference/keyed-jsx-floats.mjs +50 -25
  39. package/dist/reference/knowledge-checks-validate.d.ts +35 -0
  40. package/dist/reference/knowledge-checks-validate.js +78 -0
  41. package/dist/reference/knowledge-checks.d.mts +25 -0
  42. package/dist/reference/knowledge-checks.mjs +45 -0
  43. package/dist/reference/lab-math-scope.d.ts +9 -1
  44. package/dist/reference/lab-math-scope.js +32 -4
  45. package/dist/reference/math-glyphs.d.mts +42 -0
  46. package/dist/reference/math-glyphs.mjs +548 -40
  47. package/dist/reference/mdx-attributes.d.mts +13 -0
  48. package/dist/reference/mdx-attributes.mjs +38 -0
  49. package/dist/reference/mixed-content.d.ts +17 -0
  50. package/dist/reference/mixed-content.js +29 -0
  51. package/dist/reference/page-notation.d.ts +52 -0
  52. package/dist/reference/page-notation.js +171 -0
  53. package/dist/reference/raw-floats-validate.d.ts +18 -0
  54. package/dist/reference/raw-floats-validate.js +53 -0
  55. package/dist/reference/raw-floats.d.mts +18 -0
  56. package/dist/reference/raw-floats.mjs +129 -0
  57. package/dist/reference/rehype-heading-anchors.d.mts +1 -1
  58. package/dist/reference/rehype-heading-anchors.mjs +107 -1
  59. package/dist/reference/rehype-table-header-labels.d.mts +26 -0
  60. package/dist/reference/rehype-table-header-labels.mjs +152 -0
  61. package/dist/reference/remark-abbreviations.d.mts +7 -0
  62. package/dist/reference/remark-abbreviations.mjs +174 -0
  63. package/dist/reference/remark-citation.d.mts +5 -1
  64. package/dist/reference/remark-citation.mjs +0 -0
  65. package/dist/reference/remark-memo.d.mts +16 -0
  66. package/dist/reference/remark-memo.mjs +339 -0
  67. package/dist/reference/remark-notation.d.mts +57 -9
  68. package/dist/reference/remark-notation.mjs +354 -42
  69. package/dist/reference/remark-short-note.d.mts +4 -0
  70. package/dist/reference/remark-short-note.mjs +237 -0
  71. package/dist/reference/types.d.ts +16 -1
  72. package/dist/reference/units-vocab.js +4 -0
  73. package/docs/VERSIONING.md +53 -4
  74. package/docs/authoring/README.md +10 -0
  75. package/docs/authoring/agents.md +17 -2
  76. package/docs/authoring/components.md +138 -0
  77. package/docs/authoring/migrating.md +275 -7
  78. package/docs/authoring/notation.md +615 -1
  79. package/docs/authoring/structure-rules.md +69 -0
  80. package/docs/authoring/structure.md +120 -8
  81. package/docs/authoring/visualizations.md +36 -3
  82. package/package.json +5 -2
  83. package/src/compiler/astro.ts +208 -3
  84. package/src/compiler/cli.ts +29 -2
  85. package/src/compiler/collections.ts +45 -4
  86. package/src/compiler/course-config.ts +132 -0
  87. package/src/compiler/exports/standalone-html.ts +963 -0
  88. package/src/compiler/manifest.ts +281 -22
  89. package/src/components/HoverPanel.astro +45 -4
  90. package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
  91. package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
  92. package/src/components/assessments/AssessmentRunner.tsx +52 -17
  93. package/src/components/assessments/AssessmentSet.astro +98 -14
  94. package/src/components/assessments/RenderedContent.tsx +55 -0
  95. package/src/components/assessments/assessment-randomization.ts +16 -0
  96. package/src/components/diagram/Diagram.astro +409 -103
  97. package/src/components/disclosure/hover-preview.ts +202 -0
  98. package/src/components/disclosure/persisted-disclosure.ts +66 -0
  99. package/src/components/examples/CompactExamples.astro +2 -0
  100. package/src/components/figure/Figure.astro +279 -10
  101. package/src/components/memo/Memo.astro +453 -0
  102. package/src/components/memo/ShortNote.astro +223 -0
  103. package/src/components/notation/NotationFormula.astro +18 -1
  104. package/src/components/notation/NotationLayer.astro +260 -83
  105. package/src/components/notation/render-notation-math.ts +39 -2
  106. package/src/components/notation/safe-math-to-react.ts +50 -0
  107. package/src/components/reference/FloatRefLayer.astro +233 -15
  108. package/src/components/starlight/CollapsibleSections.astro +461 -0
  109. package/src/components/starlight/DiagramEnhancer.astro +385 -0
  110. package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
  111. package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
  112. package/src/components/starlight/LayoutHeader.astro +50 -0
  113. package/src/components/starlight/LayoutSocialIcons.astro +41 -2
  114. package/src/components/starlight/LessonExportActions.astro +436 -0
  115. package/src/components/starlight/LessonFooter.astro +30 -3
  116. package/src/components/starlight/section-parts.ts +31 -0
  117. package/src/components/table/Table.astro +293 -7
  118. package/src/components/table/TableOverflowExpander.astro +489 -0
  119. package/src/content-config.ts +36 -1
  120. package/src/reference/base-abbreviations.mjs +60 -0
  121. package/src/reference/base-operators.mjs +40 -0
  122. package/src/reference/base-roman-labels.mjs +43 -0
  123. package/src/reference/consistency.ts +73 -2
  124. package/src/reference/diagrams-validate.ts +40 -0
  125. package/src/reference/equations.mjs +67 -14
  126. package/src/reference/gate-abbreviations.ts +128 -0
  127. package/src/reference/gate-heading-length.ts +131 -0
  128. package/src/reference/gate-math.ts +32 -7
  129. package/src/reference/gate-prose-notation.ts +224 -0
  130. package/src/reference/keyed-float-validate.ts +5 -5
  131. package/src/reference/keyed-jsx-floats.mjs +50 -25
  132. package/src/reference/knowledge-checks-validate.ts +130 -0
  133. package/src/reference/knowledge-checks.mjs +45 -0
  134. package/src/reference/lab-math-scope.ts +45 -3
  135. package/src/reference/math-glyphs.mjs +548 -40
  136. package/src/reference/mdx-attributes.mjs +38 -0
  137. package/src/reference/mixed-content.ts +69 -0
  138. package/src/reference/page-notation.ts +252 -0
  139. package/src/reference/raw-floats-validate.ts +103 -0
  140. package/src/reference/raw-floats.mjs +129 -0
  141. package/src/reference/rehype-heading-anchors.mjs +107 -1
  142. package/src/reference/rehype-table-header-labels.mjs +152 -0
  143. package/src/reference/remark-abbreviations.mjs +174 -0
  144. package/src/reference/remark-citation.mjs +0 -0
  145. package/src/reference/remark-memo.mjs +339 -0
  146. package/src/reference/remark-notation.mjs +354 -42
  147. package/src/reference/remark-short-note.mjs +237 -0
  148. package/src/reference/types.ts +16 -0
  149. package/src/reference/units-vocab.ts +4 -0
  150. package/src/styles/global.css +996 -50
@@ -1,9 +1,82 @@
1
- import { resolve, sep } from "node:path";
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";
2
6
  import {
3
7
  compileManifest,
4
8
  writeManifest
5
9
  } from "./manifest.js";
10
+ import { missingNotationPayloadKeys } from "../reference/page-notation.js";
11
+ import { writeStandaloneLessonExports } from "./exports/standalone-html.js";
6
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
+ }
7
80
  function assertLocalNotationParity(lessonId, authoredKeys, renderedKeys) {
8
81
  const authored = [...new Set(authoredKeys)].sort();
9
82
  const rendered = [...new Set(renderedKeys)].sort();
@@ -14,6 +87,27 @@ function assertLocalNotationParity(lessonId, authoredKeys, renderedKeys) {
14
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.`
15
88
  );
16
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
+ }
17
111
  function replaceInPlace(target, source) {
18
112
  if (Array.isArray(target) && Array.isArray(source)) {
19
113
  target.splice(0, target.length, ...source);
@@ -37,15 +131,36 @@ function replaceInPlace(target, source) {
37
131
  }
38
132
  }
39
133
  function explicoContentRefresh(courseConfig, manifest) {
134
+ let developmentConfig;
40
135
  return {
41
136
  name: "explico-content-refresh",
42
137
  hooks: {
43
- "astro:server:setup": ({ server }) => {
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 }) => {
44
144
  if (watchedServers.has(server)) return;
45
145
  watchedServers.add(server);
46
146
  const contentRoot = `${resolve(process.cwd(), "content")}${sep}`;
47
147
  let timer;
48
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
+ };
49
164
  const refresh = (path) => {
50
165
  const absolute = resolve(path);
51
166
  if (!absolute.startsWith(contentRoot) || restarting) return;
@@ -69,11 +184,75 @@ function explicoContentRefresh(courseConfig, manifest) {
69
184
  server.watcher.on("add", refresh);
70
185
  server.watcher.on("change", refresh);
71
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);
72
249
  }
73
250
  }
74
251
  };
75
252
  }
76
253
  export {
77
254
  assertLocalNotationParity,
78
- explicoContentRefresh
255
+ assertNotationPayloadHtml,
256
+ explicoContentRefresh,
257
+ matchStandaloneDownload
79
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,11 +128,18 @@ function processorFor(manifest) {
121
128
  // shape here that it does there (D7/F-tables-figures parity).
122
129
  remarkMdx,
123
130
  remarkMath,
131
+ remarkShortNote,
132
+ remarkMemo,
124
133
  // `manifest` carries the number tables *and* the part/offset placement
125
134
  // together, so this check numbers a page exactly as the Astro build
126
135
  // does. Passing the tables alone silently numbered pages by `##` section
127
136
  // here while cross-page references read the part-scheme manifest.
128
- [remarkNotation, { definitions: manifest.notation.raw, 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)],
129
143
  [remarkCitation, { sources: manifest.sources }]
130
144
  ],
131
145
  rehypePlugins: [
@@ -299,6 +313,9 @@ function contentStatus(manifest) {
299
313
  ...manifest.diagnostics.equations.map((d) => d.severity),
300
314
  ...manifest.diagnostics.tables.map((d) => d.severity),
301
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),
302
319
  ...manifest.diagnostics.consistency.map(() => "warning"),
303
320
  ...manifest.diagnostics.knowledge.map((d) => d.severity)
304
321
  ];
@@ -314,6 +331,10 @@ function contentStatus(manifest) {
314
331
  (count, lesson) => count + lesson.figures.length,
315
332
  0
316
333
  );
334
+ const keyedDiagrams = manifest.lessons.reduce(
335
+ (count, lesson) => count + lesson.diagrams.length,
336
+ 0
337
+ );
317
338
  const assessments = new Set(
318
339
  manifest.lessons.flatMap((lesson) => lesson.assessments)
319
340
  ).size;
@@ -328,7 +349,8 @@ function contentStatus(manifest) {
328
349
  notationDefinitions: manifest.notation.definitions.length,
329
350
  keyedEquations,
330
351
  keyedTables,
331
- keyedFigures
352
+ keyedFigures,
353
+ keyedDiagrams
332
354
  },
333
355
  draftDebt: { lessons: draftLessons, notation: draftNotation },
334
356
  orphans,
@@ -384,6 +406,8 @@ function contentContext(manifest, rawId) {
384
406
  equations: lesson.equations.map((equation) => ({ ...equation })),
385
407
  tables: lesson.tables.map((table) => ({ ...table })),
386
408
  figures: lesson.figures.map((figure) => ({ ...figure })),
409
+ diagrams: lesson.diagrams.map((diagram) => ({ ...diagram })),
410
+ knowledgeChecks: lesson.knowledgeChecks.map((check) => ({ ...check })),
387
411
  sources: lesson.sources.map((id) => {
388
412
  const record = sourceById.get(id);
389
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,
@@ -44,6 +44,37 @@ interface CourseOperators {
44
44
  /** Baseline operators this course wants explained anyway. */
45
45
  readonly deny?: readonly string[];
46
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
+ }
47
78
  /** One person or team credited for the study material. */
48
79
  interface CourseAuthor {
49
80
  readonly name: string;
@@ -76,6 +107,51 @@ interface CourseAbout {
76
107
  /** Link to the published content changelog (repo blob, docs page, …). */
77
108
  readonly changelogHref?: string;
78
109
  }
110
+ /**
111
+ * A course's chosen initial open/closed state for the engine's own built-in
112
+ * collapsible sections — the "Page notation" panel (`NotationLayer.astro`)
113
+ * and the generated "References" section (`remark-citation.mjs`). Each is
114
+ * independent of `defaultCollapsible.sections`/`.subsections` (an author's
115
+ * own `##` headings): both of these are engine-authored, not written by a
116
+ * lesson, so they need their own default rather than inheriting the site's
117
+ * general heading default.
118
+ *
119
+ * `true` starts open, `false` (or omitted) starts closed — the engine's own
120
+ * default is closed either way, matching a long panel a reader usually
121
+ * doesn't need open on arrival.
122
+ */
123
+ interface CourseDisclosureDefaults {
124
+ /** Initial state of the "Page notation" panel. Default: closed. */
125
+ readonly notation?: boolean;
126
+ /** Initial state of the generated "References" section. Default: closed. */
127
+ readonly references?: boolean;
128
+ }
129
+ /** The stable, non-optional form `NormalizedLessonExports` mirrors. */
130
+ interface NormalizedDisclosureDefaults {
131
+ readonly notation: boolean;
132
+ readonly references: boolean;
133
+ }
134
+ /** Validate and canonicalize `CourseConfig.disclosureDefaults`. Absent (or
135
+ * an absent sub-field) resolves to closed, the engine's own default. */
136
+ declare function normalizeDisclosureDefaults(config: CourseConfig): NormalizedDisclosureDefaults;
137
+ /** Optional, build-time lesson artifacts exposed to readers. */
138
+ interface LessonExportsConfig {
139
+ /** Emit one self-contained, offline-capable HTML file per lesson. */
140
+ readonly standaloneHtml?: boolean;
141
+ /** Offer the browser's native print dialog with explico's lesson print layout. */
142
+ readonly print?: boolean;
143
+ /**
144
+ * Published course root used for links that intentionally leave the exported
145
+ * lesson. Required at export time when such a link is present.
146
+ */
147
+ readonly canonicalBaseUrl?: string;
148
+ }
149
+ /** The stable, non-optional export settings carried by the manifest. */
150
+ interface NormalizedLessonExports {
151
+ readonly standaloneHtml: boolean;
152
+ readonly print: boolean;
153
+ readonly canonicalBaseUrl?: string;
154
+ }
79
155
  interface CourseConfig {
80
156
  readonly title: string;
81
157
  readonly description: string;
@@ -96,7 +172,18 @@ interface CourseConfig {
96
172
  readonly about: CourseAbout;
97
173
  /** Edits to the shipped named-operator baseline (rule N8). Optional. */
98
174
  readonly operators?: CourseOperators;
175
+ /** Edits to the shipped roman-label baseline (rule N10). Optional. */
176
+ readonly romanLabels?: CourseRomanLabels;
177
+ /** Edits to the shipped abbreviation baseline (rule N14). Optional. */
178
+ readonly abbreviations?: CourseAbbreviations;
179
+ /** Per-lesson export actions and downloadable artifacts. Disabled when omitted. */
180
+ readonly lessonExports?: LessonExportsConfig;
181
+ /** Initial open/closed state for the engine's own built-in collapsible
182
+ * sections ("Page notation", "References"). Closed when omitted. */
183
+ readonly disclosureDefaults?: CourseDisclosureDefaults;
99
184
  }
185
+ /** Validate and canonicalize the build-facing lesson-export configuration. */
186
+ declare function normalizeLessonExports(config: CourseConfig): NormalizedLessonExports;
100
187
  /**
101
188
  * The course's parts, from whichever field the config uses. Throws rather than
102
189
  * silently producing a course with no sidebar groups — a config with neither
@@ -104,4 +191,4 @@ interface CourseConfig {
104
191
  */
105
192
  declare function coursePartsOf(config: CourseConfig): readonly CoursePart[];
106
193
 
107
- export { type CourseAbout, type CourseAuthor, type CourseConfig, type CourseConventions, type CourseOperators, type CoursePart, type CourseSection, coursePartsOf };
194
+ export { type CourseAbbreviations, type CourseAbout, type CourseAuthor, type CourseConfig, type CourseConventions, type CourseDisclosureDefaults, type CourseOperators, type CoursePart, type CourseRomanLabels, type CourseSection, type LessonExportsConfig, type NormalizedDisclosureDefaults, type NormalizedLessonExports, coursePartsOf, normalizeDisclosureDefaults, normalizeLessonExports };
@@ -1,3 +1,35 @@
1
+ function normalizeDisclosureDefaults(config) {
2
+ return {
3
+ notation: config.disclosureDefaults?.notation === true,
4
+ references: config.disclosureDefaults?.references === true
5
+ };
6
+ }
7
+ function normalizeLessonExports(config) {
8
+ const standaloneHtml = config.lessonExports?.standaloneHtml === true;
9
+ const print = config.lessonExports?.print === true;
10
+ const rawBase = config.lessonExports?.canonicalBaseUrl;
11
+ if (rawBase === void 0) return { standaloneHtml, print };
12
+ let parsed;
13
+ try {
14
+ parsed = new URL(rawBase);
15
+ } catch {
16
+ throw new Error(
17
+ `CourseConfig lessonExports.canonicalBaseUrl must be an absolute http(s) URL; received ${JSON.stringify(rawBase)}.`
18
+ );
19
+ }
20
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
21
+ throw new Error(
22
+ `CourseConfig lessonExports.canonicalBaseUrl must use http(s); received ${JSON.stringify(rawBase)}.`
23
+ );
24
+ }
25
+ if (parsed.search || parsed.hash) {
26
+ throw new Error(
27
+ "CourseConfig lessonExports.canonicalBaseUrl cannot contain a query or fragment."
28
+ );
29
+ }
30
+ if (!parsed.pathname.endsWith("/")) parsed.pathname += "/";
31
+ return { standaloneHtml, print, canonicalBaseUrl: parsed.toString() };
32
+ }
1
33
  function coursePartsOf(config) {
2
34
  const parts = config.parts ?? config.sections;
3
35
  if (!parts || parts.length === 0) {
@@ -8,5 +40,7 @@ function coursePartsOf(config) {
8
40
  return parts;
9
41
  }
10
42
  export {
11
- coursePartsOf
43
+ coursePartsOf,
44
+ normalizeDisclosureDefaults,
45
+ normalizeLessonExports
12
46
  };
@@ -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 };