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.
Files changed (239) hide show
  1. package/CHANGELOG.md +894 -1
  2. package/README.md +200 -30
  3. package/dist/compiler/astro.d.ts +15 -2
  4. package/dist/compiler/astro.js +40 -11
  5. package/dist/compiler/cli-main.d.ts +43 -0
  6. package/dist/compiler/cli-main.js +392 -0
  7. package/dist/compiler/cli.d.ts +424 -9
  8. package/dist/compiler/cli.js +1289 -8
  9. package/dist/compiler/collections.d.ts +88 -1
  10. package/dist/compiler/collections.js +170 -25
  11. package/dist/compiler/course-config.d.ts +1 -1
  12. package/dist/compiler/course-config.js +105 -1
  13. package/dist/compiler/decks.d.ts +161 -0
  14. package/dist/compiler/decks.js +235 -0
  15. package/dist/compiler/exports/combined-html.d.ts +5 -1
  16. package/dist/compiler/exports/deck-fit.d.ts +55 -0
  17. package/dist/compiler/exports/deck-fit.js +90 -0
  18. package/dist/compiler/exports/export-chapters.d.ts +37 -6
  19. package/dist/compiler/exports/export-chapters.js +64 -5
  20. package/dist/compiler/exports/export-pages.d.ts +30 -3
  21. package/dist/compiler/exports/export-pages.js +24 -1
  22. package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  23. package/dist/compiler/exports/latex/cache.d.ts +78 -0
  24. package/dist/compiler/exports/latex/cache.js +129 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +10 -1
  26. package/dist/compiler/exports/latex/capture.js +3 -1
  27. package/dist/compiler/exports/latex/compile.d.ts +13 -2
  28. package/dist/compiler/exports/latex/compile.js +12 -1
  29. package/dist/compiler/exports/latex/deck.d.ts +92 -0
  30. package/dist/compiler/exports/latex/deck.js +349 -0
  31. package/dist/compiler/exports/latex/document.d.ts +51 -6
  32. package/dist/compiler/exports/latex/document.js +71 -12
  33. package/dist/compiler/exports/latex/explico-common.sty +198 -0
  34. package/dist/compiler/exports/latex/explico.cls +13 -183
  35. package/dist/compiler/exports/latex/serialize.d.ts +60 -4
  36. package/dist/compiler/exports/latex/serialize.js +4 -2
  37. package/dist/compiler/exports/latex-export.d.ts +24 -2
  38. package/dist/compiler/exports/latex-export.js +107 -26
  39. package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
  40. package/dist/compiler/exports/markdown/serialize.js +85 -8
  41. package/dist/compiler/exports/markdown-export.d.ts +6 -1
  42. package/dist/compiler/exports/markdown-export.js +50 -11
  43. package/dist/compiler/exports/standalone-html.d.ts +10 -5
  44. package/dist/compiler/exports/standalone-html.js +3 -3
  45. package/dist/compiler/exports/static-tree.d.ts +38 -1
  46. package/dist/compiler/exports/static-tree.js +120 -4
  47. package/dist/compiler/knowledge-version.d.ts +26 -2
  48. package/dist/compiler/knowledge-version.js +27 -17
  49. package/dist/compiler/lesson-derivation.d.ts +143 -6
  50. package/dist/compiler/lesson-derivation.js +145 -13
  51. package/dist/compiler/manifest.d.ts +133 -4
  52. package/dist/compiler/manifest.js +359 -58
  53. package/dist/compiler/page-versions.d.ts +160 -0
  54. package/dist/compiler/page-versions.js +198 -0
  55. package/dist/compiler/render-diagnostics.d.ts +5 -1
  56. package/dist/compiler/resolution-report.d.ts +24 -6
  57. package/dist/compiler/resolution-report.js +22 -4
  58. package/dist/compiler/validate-content.d.ts +7 -0
  59. package/dist/compiler/validate-content.js +160 -0
  60. package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
  61. package/dist/curriculum/competency-graph.d.ts +54 -0
  62. package/dist/curriculum/competency-graph.js +41 -0
  63. package/dist/reference/base-library.d.mts +8 -0
  64. package/dist/reference/base-library.mjs +19 -0
  65. package/dist/reference/consistency.d.ts +8 -4
  66. package/dist/reference/consistency.js +76 -26
  67. package/dist/reference/deck-remark.d.mts +123 -0
  68. package/dist/reference/deck-remark.mjs +600 -0
  69. package/dist/reference/deck-source.d.ts +107 -0
  70. package/dist/reference/deck-source.js +140 -0
  71. package/dist/reference/decks-validate.d.ts +133 -0
  72. package/dist/reference/decks-validate.js +359 -0
  73. package/dist/reference/derived-entries.d.ts +5 -1
  74. package/dist/reference/diagram-fit.d.ts +1 -1
  75. package/dist/reference/figure-symbols.d.ts +23 -6
  76. package/dist/reference/figure-symbols.js +51 -15
  77. package/dist/reference/float-name.d.ts +43 -0
  78. package/dist/reference/float-name.js +58 -0
  79. package/dist/reference/folds-validate.d.ts +1 -1
  80. package/dist/reference/gate-abbreviations.d.ts +1 -1
  81. package/dist/reference/gate-indices.js +5 -4
  82. package/dist/reference/gate-math.d.ts +29 -2
  83. package/dist/reference/gate-math.js +99 -18
  84. package/dist/reference/gate-part-pages.d.ts +1 -1
  85. package/dist/reference/lab-katex-validate.d.ts +10 -6
  86. package/dist/reference/lab-katex-validate.js +17 -9
  87. package/dist/reference/lab-math-scope.js +29 -4
  88. package/dist/reference/math-glyphs.d.mts +25 -0
  89. package/dist/reference/math-glyphs.mjs +189 -25
  90. package/dist/reference/page-notation.d.ts +22 -3
  91. package/dist/reference/page-notation.js +32 -10
  92. package/dist/reference/references.d.ts +14 -1
  93. package/dist/reference/references.js +13 -0
  94. package/dist/reference/registry-scope.d.mts +33 -0
  95. package/dist/reference/registry-scope.mjs +34 -0
  96. package/dist/reference/registry.js +29 -10
  97. package/dist/reference/rehype-deck.d.mts +6 -0
  98. package/dist/reference/rehype-deck.mjs +286 -0
  99. package/dist/reference/rehype-heading-anchors.d.mts +20 -0
  100. package/dist/reference/rehype-heading-anchors.mjs +48 -6
  101. package/dist/reference/remark-notation.mjs +249 -40
  102. package/dist/reference/source-lines.d.mts +12 -0
  103. package/dist/reference/source-lines.mjs +65 -0
  104. package/dist/reference/types.d.ts +28 -0
  105. package/dist/rules/catalog.d.ts +21 -4
  106. package/dist/rules/catalog.js +221 -2
  107. package/dist/rules/course-rules.d.ts +5 -1
  108. package/dist/rules/course-rules.js +17 -3
  109. package/dist/rules/course-rules.schema.json +12 -0
  110. package/dist/rules/in-effect.d.ts +13 -3
  111. package/dist/rules/in-effect.js +30 -7
  112. package/dist/rules/rules.schema.json +259 -1
  113. package/dist/rules/settings.d.ts +1 -1
  114. package/dist/session/PreferenceStore.d.ts +6 -0
  115. package/dist/session/PreferenceStore.js +18 -1
  116. package/dist/session/deck-memory.d.ts +116 -0
  117. package/dist/session/deck-memory.js +143 -0
  118. package/dist/session/reading-memory.d.ts +96 -0
  119. package/dist/session/reading-memory.js +133 -0
  120. package/docs/VERSIONING.md +21 -18
  121. package/docs/authoring/README.md +29 -9
  122. package/docs/authoring/agents.md +63 -16
  123. package/docs/authoring/components.md +15 -0
  124. package/docs/authoring/deck.md +588 -0
  125. package/docs/authoring/migrating.md +1087 -14
  126. package/docs/authoring/notation.md +49 -6
  127. package/docs/authoring/structure.md +80 -1
  128. package/docs/authoring/versioning.md +54 -2
  129. package/docs/authoring/visualizations.md +7 -2
  130. package/docs/course-agent/AGENTS.md +37 -0
  131. package/docs/course-agent/agents/rules-reviewer.md +33 -0
  132. package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
  133. package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
  134. package/docs/course-agent/skills/release-content/SKILL.md +24 -0
  135. package/docs/diagram-scaling.md +148 -0
  136. package/docs/privacy.md +59 -0
  137. package/package.json +8 -2
  138. package/src/compiler/astro.ts +60 -21
  139. package/src/compiler/cli-main.ts +469 -0
  140. package/src/compiler/cli.ts +1905 -16
  141. package/src/compiler/collections.ts +284 -25
  142. package/src/compiler/course-config.ts +267 -0
  143. package/src/compiler/decks.ts +455 -0
  144. package/src/compiler/exports/deck-fit.ts +127 -0
  145. package/src/compiler/exports/export-chapters.ts +105 -6
  146. package/src/compiler/exports/export-pages.ts +46 -2
  147. package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
  148. package/src/compiler/exports/latex/cache.ts +183 -0
  149. package/src/compiler/exports/latex/capture.ts +5 -2
  150. package/src/compiler/exports/latex/compile.ts +18 -0
  151. package/src/compiler/exports/latex/deck.ts +419 -0
  152. package/src/compiler/exports/latex/document.ts +117 -21
  153. package/src/compiler/exports/latex/explico-common.sty +198 -0
  154. package/src/compiler/exports/latex/explico.cls +13 -183
  155. package/src/compiler/exports/latex/serialize.ts +25 -21
  156. package/src/compiler/exports/latex/texlive-packages.txt +6 -2
  157. package/src/compiler/exports/latex-export.ts +187 -45
  158. package/src/compiler/exports/markdown/serialize.ts +118 -9
  159. package/src/compiler/exports/markdown-export.ts +56 -11
  160. package/src/compiler/exports/standalone-html.ts +8 -7
  161. package/src/compiler/exports/static-tree.ts +175 -3
  162. package/src/compiler/knowledge-version.ts +56 -21
  163. package/src/compiler/lesson-derivation.ts +282 -16
  164. package/src/compiler/manifest.ts +558 -63
  165. package/src/compiler/page-versions.ts +359 -0
  166. package/src/compiler/resolution-report.ts +38 -7
  167. package/src/compiler/validate-content.ts +212 -0
  168. package/src/components/CourseContents.astro +124 -6
  169. package/src/components/CurriculumMap.astro +49 -1
  170. package/src/components/FloatIndex.astro +1 -1
  171. package/src/components/PartOverview.astro +51 -5
  172. package/src/components/assessments/AssessmentSet.astro +41 -15
  173. package/src/components/block-heading.ts +15 -6
  174. package/src/components/card-carry.ts +7 -4
  175. package/src/components/card-manager.ts +14 -2
  176. package/src/components/card-memory.ts +13 -9
  177. package/src/components/deck/DeckEnhancer.astro +13 -0
  178. package/src/components/deck/deck-context.ts +104 -0
  179. package/src/components/deck/deck-controller.ts +1287 -0
  180. package/src/components/deck/deck-map.ts +91 -0
  181. package/src/components/deck-lane.ts +163 -0
  182. package/src/components/diagram/Diagram.astro +131 -86
  183. package/src/components/disclosure/persisted-disclosure.ts +9 -1
  184. package/src/components/figure/Figure.astro +7 -2
  185. package/src/components/figure/FloatCaption.astro +5 -3
  186. package/src/components/fold/Fold.astro +2 -2
  187. package/src/components/notation/NotationFormula.astro +81 -11
  188. package/src/components/notation/NotationGlossary.astro +132 -1
  189. package/src/components/notation/NotationLayer.astro +29 -0
  190. package/src/components/starlight/AboutPanel.astro +186 -3
  191. package/src/components/starlight/CollapsibleSections.astro +24 -3
  192. package/src/components/starlight/DiagramEnhancer.astro +5 -1
  193. package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
  194. package/src/components/starlight/LayoutHeader.astro +17 -0
  195. package/src/components/starlight/LayoutPageTitle.astro +110 -0
  196. package/src/components/starlight/LayoutSidebar.astro +807 -21
  197. package/src/components/starlight/LayoutSocialIcons.astro +62 -6
  198. package/src/components/starlight/LessonExportActions.astro +27 -9
  199. package/src/components/starlight/LessonFooter.astro +59 -0
  200. package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
  201. package/src/components/starlight/TextSizeControl.astro +24 -13
  202. package/src/components/starlight/page-marks.ts +139 -0
  203. package/src/components/starlight/section-summary.ts +1 -1
  204. package/src/components/starlight/sidebar-editions.ts +245 -0
  205. package/src/components/table/Table.astro +15 -7
  206. package/src/content-config.ts +87 -1
  207. package/src/curriculum/competency-graph.ts +95 -0
  208. package/src/reference/base-library.mjs +19 -0
  209. package/src/reference/consistency.ts +126 -39
  210. package/src/reference/deck-remark.mjs +600 -0
  211. package/src/reference/deck-source.ts +273 -0
  212. package/src/reference/decks-validate.ts +604 -0
  213. package/src/reference/diagram-layout.ts +3 -1
  214. package/src/reference/figure-symbols.ts +108 -31
  215. package/src/reference/folds-validate.ts +1 -1
  216. package/src/reference/gate-indices.ts +5 -3
  217. package/src/reference/gate-math.ts +169 -25
  218. package/src/reference/lab-katex-validate.ts +30 -15
  219. package/src/reference/lab-math-scope.ts +34 -3
  220. package/src/reference/math-glyphs.mjs +189 -25
  221. package/src/reference/page-notation.ts +69 -15
  222. package/src/reference/references.ts +28 -0
  223. package/src/reference/registry-scope.mjs +34 -0
  224. package/src/reference/registry.ts +41 -11
  225. package/src/reference/rehype-deck.mjs +286 -0
  226. package/src/reference/rehype-heading-anchors.mjs +48 -6
  227. package/src/reference/remark-notation.mjs +249 -40
  228. package/src/reference/source-lines.mjs +65 -0
  229. package/src/reference/types.ts +28 -0
  230. package/src/rules/catalog.ts +257 -4
  231. package/src/rules/course-rules.ts +21 -1
  232. package/src/rules/in-effect.ts +54 -8
  233. package/src/session/PreferenceStore.ts +23 -0
  234. package/src/session/deck-memory.ts +263 -0
  235. package/src/session/reading-memory.ts +243 -0
  236. package/src/styles/deck.css +1150 -0
  237. package/src/styles/global.css +958 -27
  238. package/src/styles/zones.css +277 -0
  239. /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-rRtME5UV.js';
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
  /**
@@ -1,4 +1,4 @@
1
- import { M as ManifestRules } from '../course-config-rRtME5UV.js';
1
+ import { M as ManifestRules } from '../course-config-BLtIdkei.js';
2
2
  import '../rules/catalog.js';
3
3
  import '../rules/course-rules.js';
4
4
 
@@ -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`). `compiler/astro.ts` runs it on every
15
- * built page, and a course's browser test can run it on a hydrated figure's
16
- * `outerHTML`, which also holds what an island draws only in the browser.
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 whose rendered page shows a symbol in a figure that opens no
28
- * card, naming the figure, the symbols, and the way to bind them.
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, figure, inKatex, covered) => {
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
- figure = attribute(node, "id") ?? "(a figure with no id)";
37
+ next = { ...next, figure: attribute(node, "id") ?? "(a figure with no id)" };
27
38
  }
28
- if (names.includes("katex-html")) inKatex = true;
29
- if (attribute(node, "data-notation-key") !== void 0 || inKatex && names.some((name) => UPRIGHT.has(name))) {
30
- covered = true;
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
- } else if (node.nodeName === "#text" && figure !== void 0 && inKatex && !covered) {
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
- if (LETTER.test(text)) {
35
- const symbols = found.get(figure) ?? [];
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
- for (const child of childrenOf(node)) visit(child, figure, inKatex, covered);
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), void 0, false, false);
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
- function assertFigureSymbolsBound(page, html) {
46
- const unbound = unboundFigureSymbols(html);
47
- if (unbound.length === 0) return;
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
- `Explico figure symbols without a notation card on rendered page "${page}": ` + unbound.map(({ figure, symbols }) => `#${figure} shows ${symbols.map((s) => `"${s}"`).join(", ")}`).join("; ") + ". 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}`."
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(/&quot;/g, '"').replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/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 (`../components/float-name.ts`), so a fold needs one or the other:
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 { h as CourseAbbreviations } from '../course-config-rRtME5UV.js';
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