explico 0.12.0 → 0.13.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 (279) hide show
  1. package/CHANGELOG.md +1122 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +14 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +2 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +663 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +300 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
@@ -0,0 +1,50 @@
1
+ import { composeDerivedEntry } from "./derived-notation.mjs";
2
+ import { scopeEntry } from "./gate-math.js";
3
+ import { formulaExplainKeys } from "./gloss.js";
4
+ import { baseSymbolLatex, derivedNotationLatex } from "./math-glyphs.mjs";
5
+ function derivedNotationEntries(uses, records, cardsByKey) {
6
+ const derived = /* @__PURE__ */ new Map();
7
+ for (const use of uses) {
8
+ if (derived.has(use.key)) continue;
9
+ const base = records.get(use.base);
10
+ const index = records.get(use.index);
11
+ if (!base || !index) continue;
12
+ const notation = derivedNotationLatex(base.notation, index.notation);
13
+ const entry = composeDerivedEntry({
14
+ key: use.key,
15
+ notation,
16
+ baseLatex: baseSymbolLatex(base.notation),
17
+ base,
18
+ index: {
19
+ ...index,
20
+ ...index.formulaTemplate === void 0 ? {} : { formula: index.formulaTemplate }
21
+ }
22
+ });
23
+ const formulaScope = [
24
+ { key: use.key, notation },
25
+ ...index.glosses.map((gloss) => ({ key: gloss.key, notation: gloss.notation })),
26
+ scopeEntry(base)
27
+ ];
28
+ for (const key of formulaExplainKeys(entry.formula ?? "")) {
29
+ if (formulaScope.some((row) => row.key === key)) continue;
30
+ const card = records.get(key) ?? cardsByKey.get(key);
31
+ if (card) formulaScope.push(scopeEntry(card));
32
+ }
33
+ derived.set(use.key, {
34
+ key: entry.key,
35
+ base: entry.base,
36
+ index: entry.index,
37
+ notation: entry.notation,
38
+ label: entry.label,
39
+ summary: entry.summary,
40
+ glosses: [...index.glosses],
41
+ formulaScope,
42
+ ...entry.units === void 0 ? {} : { units: entry.units },
43
+ ...entry.formula === void 0 ? {} : { formula: entry.formula }
44
+ });
45
+ }
46
+ return [...derived.values()].sort((left, right) => left.key.localeCompare(right.key));
47
+ }
48
+ export {
49
+ derivedNotationEntries
50
+ };
@@ -0,0 +1,125 @@
1
+ /**
2
+ * A label as it reads inside a sentence: its first letter lowercased unless
3
+ * the first word is an acronym (`OLS estimate`) — the same rule
4
+ * `composeHeaderLabel` applies to an operator's operand.
5
+ *
6
+ * @param {string} label
7
+ * @returns {string}
8
+ */
9
+ export function inSentence(label: string): string;
10
+ /**
11
+ * A derived card's title: the index entry's `reads` with `{base}` replaced by
12
+ * the base's label (lowercased in mid-sentence), or `"<index label> of
13
+ * <base label>"` when the index entry gives no `reads`.
14
+ *
15
+ * @param {{ label: string, reads?: string }} index
16
+ * @param {string} baseLabel
17
+ * @returns {string}
18
+ */
19
+ export function derivedLabel(index: {
20
+ label: string;
21
+ reads?: string;
22
+ }, baseLabel: string): string;
23
+ /**
24
+ * An index entry's formula with `{base}` bound: to the base symbol under its
25
+ * own `\explain`, for a derived card, so the reader opens the base's card
26
+ * from it.
27
+ *
28
+ * @param {string} formula
29
+ * @param {string} baseKey
30
+ * @param {string} baseLatex
31
+ * @returns {string}
32
+ */
33
+ export function derivedFormula(formula: string, baseKey: string, baseLatex: string): string;
34
+ /**
35
+ * An index entry's own formula, shown on the index entry's card and checked
36
+ * by the formula gate: `{base}` becomes a placeholder square bound to the
37
+ * entry itself, as an operator entry is shown over a square.
38
+ *
39
+ * @param {string} formula
40
+ * @param {string} ownKey
41
+ * @returns {string}
42
+ */
43
+ export function indexEntryFormula(formula: string, ownKey: string): string;
44
+ /**
45
+ * @typedef {object} DerivedSource
46
+ * @property {string} key
47
+ * @property {string} notation
48
+ * @property {string} label
49
+ * @property {string} [summary]
50
+ * @property {string} [units]
51
+ * @property {string} [reads]
52
+ * @property {string} [formula]
53
+ * @property {readonly { key: string, notation: string, label: string, units?: string, ownerKey: string }[]} [glosses]
54
+ */
55
+ /**
56
+ * The derived entry for one (base, index) pair. `notation` is the indexed
57
+ * symbol, from `derivedNotationLatex`.
58
+ *
59
+ * @param {{ key: string, notation: string, baseLatex: string, base: DerivedSource, index: DerivedSource }} input
60
+ */
61
+ export function composeDerivedEntry({ key, notation, baseLatex, base, index }: {
62
+ key: string;
63
+ notation: string;
64
+ baseLatex: string;
65
+ base: DerivedSource;
66
+ index: DerivedSource;
67
+ }): {
68
+ formula?: string | undefined;
69
+ units?: string | undefined;
70
+ key: string;
71
+ notation: string;
72
+ base: string;
73
+ index: string;
74
+ label: string;
75
+ summary: string;
76
+ glosses: {
77
+ key: string;
78
+ notation: string;
79
+ label: string;
80
+ units?: string;
81
+ ownerKey: string;
82
+ }[];
83
+ };
84
+ /**
85
+ * Derived notation entries (rule N23): the card for a symbol with a subscript
86
+ * that an **index entry** explains — `\mathbf{v}_0` from the base entry
87
+ * `velocity-vector` (`\mathbf{v}`) and the index entry `motion.initial`
88
+ * (`{}_0`, "Initial value").
89
+ *
90
+ * The resolver writes one `\explain` around the whole indexed symbol, with
91
+ * the key `derivedNotationKey(base, index)` (`math-glyphs.mjs`); the build
92
+ * materializes the entry from the two, here, so that everything downstream
93
+ * treats it as one more card. What the reader sees first is about the
94
+ * indexed symbol; the base's own card is one level deeper, through the base
95
+ * symbol on the card's formula line:
96
+ *
97
+ * | Field | From |
98
+ * | ------- | ------------------------------------------- |
99
+ * | symbol | the base's symbol plus the index |
100
+ * | title | the index entry's `reads`, the base's label |
101
+ * | summary | the index entry's `meaning`, never the base's |
102
+ * | formula | the index entry's `formula`, `{base}` bound |
103
+ * | units | the base entry |
104
+ *
105
+ * Plain JavaScript with no parser, so the remark pass and the manifest share
106
+ * it; `derivedNotationLatex` in `math-glyphs.mjs` builds the symbol.
107
+ */
108
+ /** The indexed symbol, in an index entry's `reads` and `formula`. */
109
+ export const BASE_PLACEHOLDER: "{base}";
110
+ export type DerivedSource = {
111
+ key: string;
112
+ notation: string;
113
+ label: string;
114
+ summary?: string | undefined;
115
+ units?: string | undefined;
116
+ reads?: string | undefined;
117
+ formula?: string | undefined;
118
+ glosses?: readonly {
119
+ key: string;
120
+ notation: string;
121
+ label: string;
122
+ units?: string;
123
+ ownerKey: string;
124
+ }[] | undefined;
125
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Derived notation entries (rule N23): the card for a symbol with a subscript
3
+ * that an **index entry** explains — `\mathbf{v}_0` from the base entry
4
+ * `velocity-vector` (`\mathbf{v}`) and the index entry `motion.initial`
5
+ * (`{}_0`, "Initial value").
6
+ *
7
+ * The resolver writes one `\explain` around the whole indexed symbol, with
8
+ * the key `derivedNotationKey(base, index)` (`math-glyphs.mjs`); the build
9
+ * materializes the entry from the two, here, so that everything downstream
10
+ * treats it as one more card. What the reader sees first is about the
11
+ * indexed symbol; the base's own card is one level deeper, through the base
12
+ * symbol on the card's formula line:
13
+ *
14
+ * | Field | From |
15
+ * | ------- | ------------------------------------------- |
16
+ * | symbol | the base's symbol plus the index |
17
+ * | title | the index entry's `reads`, the base's label |
18
+ * | summary | the index entry's `meaning`, never the base's |
19
+ * | formula | the index entry's `formula`, `{base}` bound |
20
+ * | units | the base entry |
21
+ *
22
+ * Plain JavaScript with no parser, so the remark pass and the manifest share
23
+ * it; `derivedNotationLatex` in `math-glyphs.mjs` builds the symbol.
24
+ */
25
+
26
+ /** The indexed symbol, in an index entry's `reads` and `formula`. */
27
+ export const BASE_PLACEHOLDER = '{base}';
28
+
29
+ /**
30
+ * A label as it reads inside a sentence: its first letter lowercased unless
31
+ * the first word is an acronym (`OLS estimate`) — the same rule
32
+ * `composeHeaderLabel` applies to an operator's operand.
33
+ *
34
+ * @param {string} label
35
+ * @returns {string}
36
+ */
37
+ export function inSentence(label) {
38
+ const [first = ''] = label.split(/\s+/u);
39
+ if (first.length > 1 && first === first.toUpperCase()) return label;
40
+ return label.charAt(0).toLowerCase() + label.slice(1);
41
+ }
42
+
43
+ /**
44
+ * A derived card's title: the index entry's `reads` with `{base}` replaced by
45
+ * the base's label (lowercased in mid-sentence), or `"<index label> of
46
+ * <base label>"` when the index entry gives no `reads`.
47
+ *
48
+ * @param {{ label: string, reads?: string }} index
49
+ * @param {string} baseLabel
50
+ * @returns {string}
51
+ */
52
+ export function derivedLabel(index, baseLabel) {
53
+ const reads = index.reads ?? `${index.label} of ${BASE_PLACEHOLDER}`;
54
+ const at = reads.indexOf(BASE_PLACEHOLDER);
55
+ const filled = reads
56
+ .split(BASE_PLACEHOLDER)
57
+ .join(at === 0 ? baseLabel : inSentence(baseLabel));
58
+ // `{base}` first in a later position, or a `reads` that starts lowercase.
59
+ return filled.charAt(0).toUpperCase() + filled.slice(1);
60
+ }
61
+
62
+ /**
63
+ * An index entry's formula with `{base}` bound: to the base symbol under its
64
+ * own `\explain`, for a derived card, so the reader opens the base's card
65
+ * from it.
66
+ *
67
+ * @param {string} formula
68
+ * @param {string} baseKey
69
+ * @param {string} baseLatex
70
+ * @returns {string}
71
+ */
72
+ export function derivedFormula(formula, baseKey, baseLatex) {
73
+ return formula.split(BASE_PLACEHOLDER).join(`\\explain{${baseKey}}{${baseLatex}}`);
74
+ }
75
+
76
+ /**
77
+ * An index entry's own formula, shown on the index entry's card and checked
78
+ * by the formula gate: `{base}` becomes a placeholder square bound to the
79
+ * entry itself, as an operator entry is shown over a square.
80
+ *
81
+ * @param {string} formula
82
+ * @param {string} ownKey
83
+ * @returns {string}
84
+ */
85
+ export function indexEntryFormula(formula, ownKey) {
86
+ return formula.split(BASE_PLACEHOLDER).join(`\\explain{${ownKey}}{\\square}`);
87
+ }
88
+
89
+ /**
90
+ * @typedef {object} DerivedSource
91
+ * @property {string} key
92
+ * @property {string} notation
93
+ * @property {string} label
94
+ * @property {string} [summary]
95
+ * @property {string} [units]
96
+ * @property {string} [reads]
97
+ * @property {string} [formula]
98
+ * @property {readonly { key: string, notation: string, label: string, units?: string, ownerKey: string }[]} [glosses]
99
+ */
100
+
101
+ /**
102
+ * The derived entry for one (base, index) pair. `notation` is the indexed
103
+ * symbol, from `derivedNotationLatex`.
104
+ *
105
+ * @param {{ key: string, notation: string, baseLatex: string, base: DerivedSource, index: DerivedSource }} input
106
+ */
107
+ export function composeDerivedEntry({ key, notation, baseLatex, base, index }) {
108
+ return {
109
+ key,
110
+ notation,
111
+ base: base.key,
112
+ index: index.key,
113
+ label: derivedLabel(index, base.label),
114
+ summary: index.summary ?? '',
115
+ glosses: [...(index.glosses ?? [])],
116
+ ...(base.units === undefined ? {} : { units: base.units }),
117
+ ...(index.formula === undefined
118
+ ? {}
119
+ : { formula: derivedFormula(index.formula, base.key, baseLatex) }),
120
+ };
121
+ }
@@ -0,0 +1,45 @@
1
+ import { M as ManifestRules } from '../course-config-rRtME5UV.js';
2
+ import '../rules/catalog.js';
3
+ import '../rules/course-rules.js';
4
+
5
+ /**
6
+ * Rule V8: a diagram is no wider than the lesson column.
7
+ *
8
+ * `Diagram.astro` lays a diagram out at build time and so knows its width, but
9
+ * not the page it is on or what the course allows, and a diagram drawn by a
10
+ * wrapper component (`<CourseDiagram kind="…">`) never shows its nodes to the
11
+ * manifest walk. So the check runs on what was rendered: the component marks
12
+ * a diagram wider than {@link diagramWidthLimit} with `data-diagram-too-wide`,
13
+ * `compiler/astro.ts` finds each mark with {@link oversizedDiagrams} on every
14
+ * built page and reports a `dia-too-wide` warning with the lesson file and the
15
+ * diagram id, and `content warnings` lists what the last build reported
16
+ * (`compiler/render-diagnostics.ts`). Under `astro dev` the component prints
17
+ * the same message as it renders.
18
+ */
19
+
20
+ /**
21
+ * The lesson column at a 1366px-wide window, the most common laptop screen
22
+ * (632px at 1280, 832px at 1920), unless the course sets
23
+ * `rules.V8.columnWidth`.
24
+ */
25
+ declare const DEFAULT_COLUMN_WIDTH = 707;
26
+ /**
27
+ * The widest a diagram may lay out, or `undefined` when nothing checks it: rule
28
+ * V8 is off, or the diagram says `warnAboveWidth={false}`. A number in
29
+ * `warnAboveWidth` replaces the course's column width for that diagram.
30
+ */
31
+ declare function diagramWidthLimit(warnAboveWidth: number | false | undefined, rules: Pick<ManifestRules, 'disabled' | 'settings'> | undefined): number | undefined;
32
+ /** What to tell the author about one diagram wider than its limit. */
33
+ declare function diagramTooWideMessage(id: string, width: number, limit: number): string;
34
+ /** A diagram on a built page that is wider than its limit. */
35
+ interface OversizedDiagram {
36
+ readonly code: 'dia-too-wide';
37
+ /** The diagram's `id`. */
38
+ readonly key: string;
39
+ readonly width: number;
40
+ readonly limit: number;
41
+ }
42
+ /** The diagrams `Diagram.astro` marked too wide in `html`. */
43
+ declare function oversizedDiagrams(html: string): OversizedDiagram[];
44
+
45
+ export { DEFAULT_COLUMN_WIDTH, type OversizedDiagram, diagramTooWideMessage, diagramWidthLimit, oversizedDiagrams };
@@ -0,0 +1,54 @@
1
+ import { parse } from "parse5";
2
+ const DEFAULT_COLUMN_WIDTH = 707;
3
+ function diagramWidthLimit(warnAboveWidth, rules) {
4
+ if (warnAboveWidth === false || rules?.disabled.includes("V8")) return void 0;
5
+ if (warnAboveWidth !== void 0) return warnAboveWidth;
6
+ const columnWidth = rules?.settings.V8?.columnWidth;
7
+ return typeof columnWidth === "number" ? columnWidth : DEFAULT_COLUMN_WIDTH;
8
+ }
9
+ function diagramTooWideMessage(id, width, limit) {
10
+ return `Diagram "${id}" lays out ${Math.round(width)}px wide, wider than the ${limit}px it may take (rule V8), so a reader has to scroll it or widen it to see it whole. Try direction="TB", a shorter label, or a narrower node \`width\` (its label wraps inside it); set warnAboveWidth={false} only when the diagram is meant to scroll.`;
11
+ }
12
+ function isElement(node) {
13
+ return "tagName" in node;
14
+ }
15
+ function attribute(node, name) {
16
+ return node.attrs.find((attr) => attr.name === name)?.value;
17
+ }
18
+ function viewportKey(node) {
19
+ for (const child of "childNodes" in node ? node.childNodes : []) {
20
+ if (!isElement(child)) continue;
21
+ const match = /^diagram-(.+)-viewport$/.exec(attribute(child, "id") ?? "");
22
+ if (match) return match[1];
23
+ const inner = viewportKey(child);
24
+ if (inner) return inner;
25
+ }
26
+ return void 0;
27
+ }
28
+ function oversizedDiagrams(html) {
29
+ if (!html.includes("data-diagram-too-wide")) return [];
30
+ const found = [];
31
+ const visit = (node) => {
32
+ for (const child of "childNodes" in node ? node.childNodes : []) {
33
+ if (!isElement(child)) continue;
34
+ const limit = attribute(child, "data-diagram-too-wide");
35
+ if (limit !== void 0) {
36
+ found.push({
37
+ code: "dia-too-wide",
38
+ key: viewportKey(child) ?? attribute(child, "id") ?? "",
39
+ width: Number(attribute(child, "data-diagram-width")),
40
+ limit: Number(limit)
41
+ });
42
+ }
43
+ visit(child);
44
+ }
45
+ };
46
+ visit(parse(html));
47
+ return found;
48
+ }
49
+ export {
50
+ DEFAULT_COLUMN_WIDTH,
51
+ diagramTooWideMessage,
52
+ diagramWidthLimit,
53
+ oversizedDiagrams
54
+ };
@@ -45,19 +45,68 @@ interface DiagramLayout {
45
45
  * would otherwise create a phantom zero-size node).
46
46
  */
47
47
  declare function layoutDiagram(nodes: readonly DiagramNodeInput[], edges: readonly DiagramEdgeInput[], options?: DiagramLayoutOptions): DiagramLayout;
48
- interface BoxSizeEstimate {
48
+ /** A node's text font size, in px. */
49
+ declare const NODE_FONT_PX = 13.6;
50
+ /**
51
+ * A node's formula font size, in px: KaTeX's own `.katex { font-size: 1.21em }`
52
+ * of the node's text. `katex.css` is unlayered, so no rule in the component's
53
+ * `@layer` can shrink it, and at that size KaTeX_Main's x-height matches the
54
+ * label's.
55
+ */
56
+ declare const FORMULA_FONT_PX: number;
57
+ /** What a node holds, measured: its label, and its formula's box in px. */
58
+ interface NodeBoxContent {
59
+ readonly text?: string;
60
+ /** `measureFormula(latex, FORMULA_FONT_PX)`. */
61
+ readonly formula?: {
62
+ readonly width: number;
63
+ readonly height: number;
64
+ readonly depth: number;
65
+ };
66
+ }
67
+ interface NodeBoxOptions {
68
+ /** The author's width: the label wraps inside it and the height follows. */
69
+ readonly width?: number;
70
+ /** The author's height. */
71
+ readonly height?: number;
72
+ /** The widest box a label is wrapped to fit before it takes a second line. */
73
+ readonly maxWidth?: number;
74
+ }
75
+ interface NodeBox {
49
76
  readonly width: number;
50
77
  readonly height: number;
78
+ /** The label as it is wrapped in DejaVu Sans. */
79
+ readonly lines: readonly string[];
51
80
  }
52
81
  /**
53
- * Estimate a diagram node's box from its label text (or raw, pre-resolution
54
- * LaTeX for a formula node). Deliberately generous: it is meant to avoid
55
- * clipping, not to match a real render pixel-for-pixel. Pass an explicit
56
- * `width`/`height` on the node to override for a label this estimate gets
57
- * badly wrong.
82
+ * Size a node's box from its measured content: as wide as its widest line of
83
+ * text or its formula, whichever is wider, plus the inset, and as tall as its
84
+ * lines of text, the gap and its formula's line. A label wider than `maxWidth`
85
+ * wraps; a formula never does, so a wide formula widens the box instead and the
86
+ * label wraps to it. An author's `width` or `height` makes the box larger,
87
+ * never too small for its formula or its lines.
58
88
  */
59
- declare function estimateBoxSize(content: string, kind: 'text' | 'formula', options?: {
60
- readonly maxWidth?: number;
61
- }): BoxSizeEstimate;
89
+ declare function nodeBoxSize(content: NodeBoxContent, options?: NodeBoxOptions): NodeBox;
90
+ /** One node to fit: what it holds, and the author's size if any. */
91
+ interface FitNodeInput {
92
+ readonly id: string;
93
+ readonly content: NodeBoxContent;
94
+ readonly width?: number;
95
+ readonly height?: number;
96
+ }
97
+ interface FitOptions extends DiagramLayoutOptions {
98
+ /** The widest the diagram should lay out (rule V8's column); omit to skip fitting. */
99
+ readonly fitWidth?: number;
100
+ }
101
+ /**
102
+ * Size every node from its content and lay the diagram out. When `fitWidth`
103
+ * is set and the diagram comes out wider, wrap the labels of the nodes without
104
+ * an author's `width` narrower, step by step, until it fits; a diagram that
105
+ * still does not fit keeps its narrowest layout, and the caller warns.
106
+ */
107
+ declare function fitDiagramLayout(nodes: readonly FitNodeInput[], edges: readonly DiagramEdgeInput[], options?: FitOptions): {
108
+ readonly layout: DiagramLayout;
109
+ readonly boxes: ReadonlyMap<string, NodeBox>;
110
+ };
62
111
 
63
- export { type BoxSizeEstimate, type DiagramEdgeInput, type DiagramLayout, type DiagramLayoutOptions, type DiagramNodeInput, type LaidOutEdge, type LaidOutNode, type Point, estimateBoxSize, layoutDiagram };
112
+ export { type DiagramEdgeInput, type DiagramLayout, type DiagramLayoutOptions, type DiagramNodeInput, FORMULA_FONT_PX, type FitNodeInput, type FitOptions, type LaidOutEdge, type LaidOutNode, NODE_FONT_PX, type NodeBox, type NodeBoxContent, type NodeBoxOptions, type Point, fitDiagramLayout, layoutDiagram, nodeBoxSize };
@@ -1,4 +1,5 @@
1
1
  import dagre from "@dagrejs/dagre";
2
+ import { textWidth, wrapText } from "./text-measure.js";
2
3
  function layoutDiagram(nodes, edges, options = {}) {
3
4
  const ids = /* @__PURE__ */ new Set();
4
5
  for (const node of nodes) {
@@ -19,6 +20,11 @@ function layoutDiagram(nodes, edges, options = {}) {
19
20
  if (!ids.has(edge.to)) {
20
21
  throw new Error(`Diagram edge references unknown node "${edge.to}".`);
21
22
  }
23
+ if (edge.from === edge.to) {
24
+ throw new Error(
25
+ `Diagram edge from "${edge.from}" to itself: a diagram draws no loop on one node. Name the repetition in the label ("Repeat until it converges"), or draw the step it returns to as its own node.`
26
+ );
27
+ }
22
28
  }
23
29
  const graph = new dagre.graphlib.Graph();
24
30
  graph.setGraph({
@@ -91,32 +97,87 @@ function layoutDiagram(nodes, edges, options = {}) {
91
97
  height: maxY - minY + padding * 2
92
98
  };
93
99
  }
94
- const DEFAULT_MAX_WIDTH = 220;
100
+ const NODE_FONT_PX = 13.6;
101
+ const FORMULA_FONT_PX = NODE_FONT_PX * 1.21;
102
+ const LINE_PX = 17;
103
+ const CSS_PADDING_X = 8;
104
+ const INSET_X = 16;
105
+ const INSET_Y = 14;
106
+ const GAP_PX = 2.4;
107
+ const FORMULA_ASCENT_PX = 14;
108
+ const FORMULA_DESCENT_PX = 5.5;
95
109
  const MIN_WIDTH = 90;
96
- const PADDING_X = 20;
97
- const PADDING_Y = 16;
98
- const LINE_HEIGHT = 22;
99
- const CHAR_WIDTH = {
100
- text: 7.2,
101
- formula: 11
102
- };
103
- function formulaVisibleLength(latex) {
104
- const collapsed = latex.replace(/\\[a-zA-Z]+/g, "x").replace(/[{}$]/g, "").trim();
105
- return Math.max(1, collapsed.length);
110
+ const DEFAULT_MAX_WIDTH = 220;
111
+ function balancedLines(text, maxWidth) {
112
+ const greedy = wrapText(text, maxWidth, NODE_FONT_PX);
113
+ if (greedy.length <= 1) return greedy;
114
+ let low = 1;
115
+ let high = Math.ceil(maxWidth);
116
+ while (low < high) {
117
+ const middle = Math.floor((low + high) / 2);
118
+ if (wrapText(text, middle, NODE_FONT_PX).length <= greedy.length) high = middle;
119
+ else low = middle + 1;
120
+ }
121
+ return wrapText(text, high, NODE_FONT_PX);
122
+ }
123
+ function nodeBoxSize(content, options = {}) {
124
+ const text = content.text?.trim() ?? "";
125
+ const formula = content.formula;
126
+ const formulaWidth = formula?.width ?? 0;
127
+ let width;
128
+ let lines;
129
+ if (options.width !== void 0) {
130
+ width = Math.max(options.width, Math.ceil(formulaWidth + 2 * CSS_PADDING_X));
131
+ lines = text ? wrapText(text, width - 2 * CSS_PADDING_X, NODE_FONT_PX) : [];
132
+ } else {
133
+ const maxContent = Math.max(
134
+ (options.maxWidth ?? DEFAULT_MAX_WIDTH) - 2 * INSET_X,
135
+ formulaWidth
136
+ );
137
+ lines = text ? balancedLines(text, maxContent) : [];
138
+ const contentWidth = Math.max(
139
+ formulaWidth,
140
+ ...lines.map((line) => textWidth(line, NODE_FONT_PX))
141
+ );
142
+ width = Math.max(MIN_WIDTH, Math.ceil(contentWidth + 2 * INSET_X));
143
+ }
144
+ const formulaHeight = formula ? Math.max(FORMULA_ASCENT_PX, formula.height) + Math.max(FORMULA_DESCENT_PX, formula.depth) : 0;
145
+ const gap = lines.length > 0 && formula ? GAP_PX : 0;
146
+ const contentHeight = Math.ceil(lines.length * LINE_PX + gap + formulaHeight + 2 * INSET_Y);
147
+ const height = Math.max(options.height ?? 0, contentHeight);
148
+ return { width, height, lines };
149
+ }
150
+ const FIT_MAX_WIDTHS = [DEFAULT_MAX_WIDTH, 200, 180, 160, 140, 120];
151
+ function fitDiagramLayout(nodes, edges, options = {}) {
152
+ let result;
153
+ for (const maxWidth of FIT_MAX_WIDTHS) {
154
+ const boxes = new Map(
155
+ nodes.map((node) => [
156
+ node.id,
157
+ nodeBoxSize(node.content, {
158
+ maxWidth,
159
+ ...node.width !== void 0 ? { width: node.width } : {},
160
+ ...node.height !== void 0 ? { height: node.height } : {}
161
+ })
162
+ ])
163
+ );
164
+ const layout = layoutDiagram(
165
+ nodes.map((node) => ({ id: node.id, ...sizeOf(boxes.get(node.id)) })),
166
+ edges,
167
+ options
168
+ );
169
+ result = { layout, boxes };
170
+ if (options.fitWidth === void 0 || layout.width <= options.fitWidth) break;
171
+ }
172
+ return result;
106
173
  }
107
- function estimateBoxSize(content, kind, options = {}) {
108
- const maxWidth = options.maxWidth ?? DEFAULT_MAX_WIDTH;
109
- const visibleLength = kind === "formula" ? formulaVisibleLength(content) : content.trim().length || 1;
110
- const charWidth = CHAR_WIDTH[kind];
111
- const naturalWidth = visibleLength * charWidth + PADDING_X * 2;
112
- const width = Math.min(maxWidth, Math.max(MIN_WIDTH, naturalWidth));
113
- const usableWidth = width - PADDING_X * 2;
114
- const charsPerLine = Math.max(1, Math.floor(usableWidth / charWidth));
115
- const lines = Math.max(1, Math.ceil(visibleLength / charsPerLine));
116
- const height = lines * LINE_HEIGHT + PADDING_Y * 2;
117
- return { width: Math.round(width), height: Math.round(height) };
174
+ function sizeOf(box) {
175
+ return { width: box.width, height: box.height };
118
176
  }
119
177
  export {
120
- estimateBoxSize,
121
- layoutDiagram
178
+ FORMULA_FONT_PX,
179
+ NODE_FONT_PX,
180
+ fitDiagramLayout,
181
+ layoutDiagram,
182
+ nodeBoxSize
122
183
  };
@@ -2,7 +2,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
2
2
 
3
3
  /** `validateKeyedFloats` for `<Diagram>` — see `keyed-float-validate.ts`. */
4
4
 
5
- type DiagramDiagnosticCode = 'dia-invalid-key' | 'dia-duplicate-key' | 'dia-ref-resolves' | 'dia-key-unused' | 'dia-removed-prop';
5
+ type DiagramDiagnosticCode = 'dia-invalid-key' | 'dia-duplicate-key' | 'dia-ref-resolves' | 'dia-key-unused' | 'dia-removed-prop' | 'dia-unread-prop';
6
6
  interface DiagramDiagnostic {
7
7
  readonly code: DiagramDiagnosticCode;
8
8
  readonly severity: 'error' | 'warning' | 'info';
@@ -25,7 +25,7 @@ interface EquationRefRecord {
25
25
  /** Target lesson slug for a cross-page reference; `undefined` = same page. */
26
26
  readonly targetSlug?: string;
27
27
  }
28
- type EquationDiagnosticCode = 'eq-duplicate-key' | 'eq-ref-resolves' | 'eq-key-unused';
28
+ type EquationDiagnosticCode = 'eq-duplicate-key' | 'eq-ref-resolves' | 'eq-key-unused' | 'eq-label-repeated' | 'eq-latex-ref';
29
29
  interface EquationDiagnostic {
30
30
  readonly code: EquationDiagnosticCode;
31
31
  readonly severity: 'error' | 'warning' | 'info';
@@ -1,4 +1,5 @@
1
- import { parseEquationRef, scanEquationLabels } from "./equations.mjs";
1
+ import { parseEquationRef, repeatedEquationLabels, scanEquationLabels } from "./equations.mjs";
2
+ import { proseOnly } from "./gate-prose-notation.js";
2
3
  const BRACKET_REF = /\[\[\s*([^\][]+?)\s*\]\]/g;
3
4
  function stripCode(text) {
4
5
  return text.replace(/```[\s\S]*?```/g, " ").replace(/~~~[\s\S]*?~~~/g, " ").replace(/`[^`\n]*`/g, " ");
@@ -16,6 +17,7 @@ function bodyEquationRefs(lesson) {
16
17
  }
17
18
  return refs;
18
19
  }
20
+ const LATEX_REFERENCE = /\\(eqref|ref|autoref|cref|Cref)\s*\{([^{}\n]*)\}/g;
19
21
  function validateEquations(lessons) {
20
22
  const labels = [];
21
23
  const diagnostics = [];
@@ -50,6 +52,29 @@ function validateEquations(lessons) {
50
52
  });
51
53
  }
52
54
  numbersBySlug[lesson.slug] = table;
55
+ for (const match of proseOnly(lesson.body).matchAll(LATEX_REFERENCE)) {
56
+ const [, command, target = ""] = match;
57
+ const [prefix, key] = target.includes(":") ? target.split(":", 2) : ["eq", target];
58
+ diagnostics.push({
59
+ code: "eq-latex-ref",
60
+ severity: "error",
61
+ message: `${lesson.lessonId} writes \\${command}{${target}}, LaTeX's reference: MDX reads its {\u2026} as JavaScript, and the page fails to build. Write [[${prefix}-${key}]].`,
62
+ file: lesson.file,
63
+ lessonId: lesson.lessonId,
64
+ key: key ?? target
65
+ });
66
+ }
67
+ for (const keys of repeatedEquationLabels(lesson.body)) {
68
+ const [first, ...rest] = keys;
69
+ diagnostics.push({
70
+ code: "eq-label-repeated",
71
+ severity: "error",
72
+ message: `${lesson.lessonId} puts ${keys.map((key) => `\\label{eq:${key}}`).join(" and ")} in one display block, which is one numbered equation: eq:${first} names it, and ${rest.map((key) => `[[eq-${key}]]`).join(", ")} would point at nothing. Split the block into one $$\u2026$$ block per equation you refer to.`,
73
+ file: lesson.file,
74
+ lessonId: lesson.lessonId,
75
+ key: rest[0]
76
+ });
77
+ }
53
78
  }
54
79
  const referenced = /* @__PURE__ */ new Set();
55
80
  for (const lesson of lessons) {