explico 0.11.1 → 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 (298) hide show
  1. package/CHANGELOG.md +1292 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +11 -4
  4. package/dist/compiler/astro.js +48 -15
  5. package/dist/compiler/cli.d.ts +27 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +33 -2
  8. package/dist/compiler/collections.js +91 -19
  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 +15 -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 +115 -8
  51. package/dist/compiler/manifest.js +221 -39
  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 +3 -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 +8 -2
  61. package/dist/reference/base-operators.mjs +8 -10
  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/figure-symbols.d.ts +32 -0
  88. package/dist/reference/figure-symbols.js +55 -0
  89. package/dist/reference/figures-validate.d.ts +1 -1
  90. package/dist/reference/gate-abbreviations.d.ts +1 -1
  91. package/dist/reference/gate-abbreviations.js +1 -1
  92. package/dist/reference/gate-course-rules.d.ts +4 -3
  93. package/dist/reference/gate-course-rules.js +9 -4
  94. package/dist/reference/gate-heading-length.d.ts +2 -0
  95. package/dist/reference/gate-heading-length.js +7 -5
  96. package/dist/reference/gate-heading-structure.js +26 -0
  97. package/dist/reference/gate-indices.d.ts +35 -0
  98. package/dist/reference/gate-indices.js +299 -0
  99. package/dist/reference/gate-math.d.ts +32 -2
  100. package/dist/reference/gate-math.js +68 -12
  101. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  102. package/dist/reference/gate-mdx-expression.js +29 -0
  103. package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
  104. package/dist/reference/gate-non-chapter-pages.js +156 -0
  105. package/dist/reference/gate-part-pages.d.ts +4 -2
  106. package/dist/reference/gate-part-pages.js +2 -1
  107. package/dist/reference/gate-prop-text.d.ts +28 -0
  108. package/dist/reference/gate-prop-text.js +106 -0
  109. package/dist/reference/gate-prose-notation.d.ts +3 -1
  110. package/dist/reference/gate-prose-notation.js +4 -2
  111. package/dist/reference/gate-retired-keys.d.ts +35 -0
  112. package/dist/reference/gate-retired-keys.js +58 -0
  113. package/dist/reference/gloss.d.ts +2 -0
  114. package/dist/reference/katex-measure.d.ts +17 -0
  115. package/dist/reference/katex-measure.js +223 -0
  116. package/dist/reference/keyed-float-validate.js +12 -0
  117. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  118. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  119. package/dist/reference/lab-katex-validate.d.ts +43 -0
  120. package/dist/reference/lab-katex-validate.js +96 -0
  121. package/dist/reference/lab-math-scope.d.ts +1 -0
  122. package/dist/reference/lab-math-scope.js +13 -2
  123. package/dist/reference/lint-ignore.js +1 -1
  124. package/dist/reference/math-glyphs.d.mts +94 -2
  125. package/dist/reference/math-glyphs.mjs +851 -46
  126. package/dist/reference/mixed-content.d.ts +8 -3
  127. package/dist/reference/mixed-content.js +4 -2
  128. package/dist/reference/page-notation.d.ts +39 -7
  129. package/dist/reference/page-notation.js +62 -10
  130. package/dist/reference/raw-floats.mjs +37 -13
  131. package/dist/reference/references.d.ts +11 -1
  132. package/dist/reference/references.js +6 -0
  133. package/dist/reference/registry.js +115 -11
  134. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  135. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  136. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  137. package/dist/reference/remark-citation.mjs +0 -0
  138. package/dist/reference/remark-notation.mjs +275 -23
  139. package/dist/reference/render-lab-math.d.ts +13 -2
  140. package/dist/reference/render-lab-math.js +18 -6
  141. package/dist/reference/sections-validate.d.ts +28 -3
  142. package/dist/reference/sections-validate.js +31 -4
  143. package/dist/reference/sections.d.mts +24 -6
  144. package/dist/reference/sections.mjs +92 -17
  145. package/dist/reference/tables-validate.d.ts +1 -1
  146. package/dist/reference/tables-validate.js +24 -1
  147. package/dist/reference/text-measure.d.ts +10 -0
  148. package/dist/reference/text-measure.js +41 -0
  149. package/dist/reference/text-widths.d.ts +10 -0
  150. package/dist/reference/text-widths.js +1238 -0
  151. package/dist/reference/types.d.ts +64 -8
  152. package/dist/rules/catalog.js +223 -32
  153. package/dist/rules/in-effect.d.ts +1 -1
  154. package/dist/rules/rules.schema.json +100 -27
  155. package/dist/rules/settings.d.ts +1 -1
  156. package/dist/rules/settings.js +7 -0
  157. package/dist/session/PreferenceStore.d.ts +18 -1
  158. package/dist/session/PreferenceStore.js +29 -1
  159. package/docs/authoring/README.md +33 -21
  160. package/docs/authoring/agents.md +38 -7
  161. package/docs/authoring/components.md +115 -10
  162. package/docs/authoring/migrating.md +878 -0
  163. package/docs/authoring/notation.md +285 -30
  164. package/docs/authoring/retired.md +1 -0
  165. package/docs/authoring/structure-rules.md +84 -45
  166. package/docs/authoring/structure.md +67 -2
  167. package/docs/authoring/visualizations.md +167 -25
  168. package/package.json +12 -7
  169. package/src/compiler/astro.ts +72 -21
  170. package/src/compiler/cli.ts +143 -35
  171. package/src/compiler/collections.ts +158 -27
  172. package/src/compiler/content-schema.ts +108 -0
  173. package/src/compiler/content-schemas.ts +391 -0
  174. package/src/compiler/course-config.ts +86 -15
  175. package/src/compiler/exports/combined-html.ts +669 -0
  176. package/src/compiler/exports/download-index.ts +36 -0
  177. package/src/compiler/exports/export-chapters.ts +233 -0
  178. package/src/compiler/exports/export-pages.ts +44 -0
  179. package/src/compiler/exports/html-tree.ts +62 -0
  180. package/src/compiler/exports/latex/capture.ts +651 -0
  181. package/src/compiler/exports/latex/compile.ts +168 -0
  182. package/src/compiler/exports/latex/document.ts +479 -0
  183. package/src/compiler/exports/latex/explico.cls +415 -0
  184. package/src/compiler/exports/latex/numbering.ts +74 -0
  185. package/src/compiler/exports/latex/serialize.ts +787 -0
  186. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  187. package/src/compiler/exports/latex-export.ts +255 -0
  188. package/src/compiler/exports/markdown/serialize.ts +632 -0
  189. package/src/compiler/exports/markdown-export.ts +329 -0
  190. package/src/compiler/exports/standalone-html.ts +39 -70
  191. package/src/compiler/exports/static-tree.ts +944 -0
  192. package/src/compiler/exports/zip.ts +88 -0
  193. package/src/compiler/lesson-derivation.ts +13 -8
  194. package/src/compiler/manifest.ts +387 -40
  195. package/src/compiler/render-diagnostics.ts +111 -0
  196. package/src/components/CourseContents.astro +6 -0
  197. package/src/components/HoverPanel.astro +204 -6
  198. package/src/components/PartOverview.astro +18 -9
  199. package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
  200. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  201. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  202. package/src/components/assessments/AssessmentSet.astro +14 -1
  203. package/src/components/block-heading.ts +38 -0
  204. package/src/components/card-arrow.ts +18 -4
  205. package/src/components/card-carry.ts +406 -0
  206. package/src/components/card-drag.ts +1 -30
  207. package/src/components/card-fetch.ts +87 -0
  208. package/src/components/card-manager.ts +1222 -38
  209. package/src/components/card-memory.ts +5 -1
  210. package/src/components/card-placement.ts +211 -13
  211. package/src/components/card-side.ts +281 -31
  212. package/src/components/citation/CitationLayer.astro +45 -40
  213. package/src/components/diagram/Diagram.astro +123 -44
  214. package/src/components/disclosure/print-as-shown.ts +42 -6
  215. package/src/components/figure/Figure.astro +17 -1
  216. package/src/components/figure/FloatCaption.astro +4 -17
  217. package/src/components/figure/figure-state.ts +26 -17
  218. package/src/components/float-toolbar.ts +274 -0
  219. package/src/components/memo/Memo.astro +21 -10
  220. package/src/components/memo/ShortNote.astro +4 -0
  221. package/src/components/notation/NotationGlossary.astro +22 -17
  222. package/src/components/notation/NotationLayer.astro +300 -79
  223. package/src/components/notation/NotationMath.astro +8 -1
  224. package/src/components/notation/render-notation-math.ts +51 -5
  225. package/src/components/readable-text.ts +25 -0
  226. package/src/components/reference/FloatRefLayer.astro +162 -89
  227. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  228. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  229. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  230. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  231. package/src/components/starlight/LessonExportActions.astro +384 -310
  232. package/src/components/starlight/LessonFooter.astro +66 -8
  233. package/src/components/starlight/PrintController.astro +28 -0
  234. package/src/components/starlight/TextSizeControl.astro +137 -0
  235. package/src/components/starlight/print-controller.ts +380 -0
  236. package/src/components/starlight/section-summary.ts +13 -5
  237. package/src/components/table/Table.astro +14 -22
  238. package/src/components/table/table-truncation.ts +15 -0
  239. package/src/content-config.ts +20 -312
  240. package/src/curriculum/validation.ts +72 -24
  241. package/src/reference/base-abbreviations.mjs +2 -2
  242. package/src/reference/base-operators.mjs +8 -10
  243. package/src/reference/base-roman-labels.mjs +4 -3
  244. package/src/reference/citation-format.mjs +74 -0
  245. package/src/reference/consistency.ts +47 -61
  246. package/src/reference/curriculum-alignment.ts +4 -41
  247. package/src/reference/derived-entries.ts +68 -0
  248. package/src/reference/derived-notation.mjs +121 -0
  249. package/src/reference/diagram-fit.ts +104 -0
  250. package/src/reference/diagram-layout.ts +182 -51
  251. package/src/reference/diagrams-validate.ts +2 -1
  252. package/src/reference/equations-validate.ts +41 -2
  253. package/src/reference/equations.mjs +120 -22
  254. package/src/reference/examples-validate.ts +1 -0
  255. package/src/reference/figure-fit.ts +139 -0
  256. package/src/reference/figure-pictures.ts +96 -0
  257. package/src/reference/figure-symbols.ts +117 -0
  258. package/src/reference/figures-validate.ts +2 -1
  259. package/src/reference/gate-abbreviations.ts +1 -1
  260. package/src/reference/gate-course-rules.ts +22 -7
  261. package/src/reference/gate-heading-length.ts +25 -7
  262. package/src/reference/gate-heading-structure.ts +40 -1
  263. package/src/reference/gate-indices.ts +470 -0
  264. package/src/reference/gate-math.ts +102 -13
  265. package/src/reference/gate-mdx-expression.ts +45 -0
  266. package/src/reference/gate-non-chapter-pages.ts +241 -0
  267. package/src/reference/gate-part-pages.ts +1 -1
  268. package/src/reference/gate-prop-text.ts +145 -0
  269. package/src/reference/gate-prose-notation.ts +19 -5
  270. package/src/reference/gate-retired-keys.ts +101 -0
  271. package/src/reference/gloss.ts +2 -0
  272. package/src/reference/katex-measure.ts +384 -0
  273. package/src/reference/keyed-float-validate.ts +22 -0
  274. package/src/reference/keyed-jsx-floats.mjs +11 -14
  275. package/src/reference/lab-katex-validate.ts +156 -0
  276. package/src/reference/lab-math-scope.ts +15 -1
  277. package/src/reference/lint-ignore.ts +4 -1
  278. package/src/reference/math-glyphs.mjs +851 -46
  279. package/src/reference/mixed-content.ts +11 -2
  280. package/src/reference/page-notation.ts +114 -14
  281. package/src/reference/raw-floats.mjs +37 -13
  282. package/src/reference/references.ts +14 -0
  283. package/src/reference/registry.ts +170 -9
  284. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  285. package/src/reference/rehype-heading-anchors.mjs +11 -1
  286. package/src/reference/remark-citation.mjs +0 -0
  287. package/src/reference/remark-notation.mjs +275 -23
  288. package/src/reference/render-lab-math.ts +40 -5
  289. package/src/reference/sections-validate.ts +67 -3
  290. package/src/reference/sections.mjs +92 -17
  291. package/src/reference/tables-validate.ts +40 -2
  292. package/src/reference/text-measure.ts +63 -0
  293. package/src/reference/text-widths.ts +133 -0
  294. package/src/reference/types.ts +75 -8
  295. package/src/rules/catalog.ts +241 -31
  296. package/src/rules/settings.ts +10 -0
  297. package/src/session/PreferenceStore.ts +53 -0
  298. package/src/styles/global.css +322 -83
@@ -11,8 +11,8 @@
11
11
  * `content/abbreviations/*.md` table, not here. This baseline only holds
12
12
  * abbreviations so common in ordinary English/academic writing that spelling
13
13
  * them out on every course would be noise, not help. A course adds a real
14
- * one via `abbreviations.allow` in `course.config.ts`, the same shape as
15
- * `operators.allow` / `romanLabels.allow`.
14
+ * one via `rules.N14.allow` in its course config, the same shape as
15
+ * `rules.N8.allow` / `rules.N10.allow`.
16
16
  */
17
17
  export const BASE_ABBREVIATIONS = Object.freeze(
18
18
  new Set([
@@ -13,10 +13,16 @@
13
13
  * baseline — trig, the exponential and logarithms, limits, extrema, and the
14
14
  * big operators — kept greppable and small like the base library.
15
15
  *
16
+ * Only the plain limit, `\lim`, is baseline. `\limsup` / `\liminf` and the
17
+ * category-theory `\injlim` / `\projlim` / `\varlimsup` / `\varliminf` /
18
+ * `\varinjlim` / `\varprojlim` read fine to someone who already knows them,
19
+ * but none is "everyone knows this" the way `\lim` or `\max` is — a course
20
+ * that uses one owes it a notation entry, like any other domain operator.
21
+ *
16
22
  * A domain operator (`Var`, `Cov`, `Corr`, `sgn`, `erf`, `Tr`, `argmax`, …) is
17
23
  * written `\operatorname{…}` and is NOT here: it earns a card, a
18
- * `notation.local` entry, or an explicit line in `course.config.ts`
19
- * (`operators.allow`).
24
+ * `notation.local` entry, or an explicit line in the course config
25
+ * (`rules.N8.allow`).
20
26
  *
21
27
  * Entries are the KaTeX command without its backslash (`exp`, not `\exp`) for
22
28
  * `op` nodes, and the verbatim body for `\operatorname{…}` nodes (`Var`).
@@ -47,14 +53,6 @@ export const BASE_OPERATORS = Object.freeze(
47
53
  'coth',
48
54
  // limits
49
55
  'lim',
50
- 'limsup',
51
- 'liminf',
52
- 'injlim',
53
- 'projlim',
54
- 'varlimsup',
55
- 'varliminf',
56
- 'varinjlim',
57
- 'varprojlim',
58
56
  // extrema
59
57
  'max',
60
58
  'min',
@@ -15,9 +15,10 @@
15
15
  * always a course-specific named quantity (`KE`, `PV`, `Var`-as-a-label) that
16
16
  * genuinely benefits from a hover card, so the shipped baseline holds only
17
17
  * the qualifiers that function like an operator's extremum subscript
18
- * (`x_{\mathrm{opt}}`) rather than naming a quantity of their own. A course
19
- * adds a real one via `romanLabels.allow` in `course.config.ts`, the same
20
- * shape as `operators.allow`.
18
+ * (`x_{\mathrm{opt}}`) rather than naming a quantity of their own. That
19
+ * clears rule N10 only: the indexed symbol still needs a card (rule N23).
20
+ * A course adds a real one via `rules.N10.allow` in its course config, the
21
+ * same shape as `rules.N8.allow`.
21
22
  */
22
23
  export const BASE_ROMAN_LABELS = Object.freeze(new Set(['opt']));
23
24
 
@@ -131,3 +131,77 @@ export function formatSourceLabel(record) {
131
131
  .filter(Boolean)
132
132
  .join(', ');
133
133
  }
134
+
135
+ /**
136
+ * One `[@…]` bracket: `[@id]`, `[@id; locator]`, or several citations as
137
+ * Pandoc writes them, `[@a; @b; p. 7]`. Group 1 is the first id, group 2 the
138
+ * rest after the first `;`, read by {@link bracketCitations}.
139
+ */
140
+ export const CITATION_BRACKET = /\[@([a-z0-9][a-z0-9.-]*)(?:;[ \t\r\n]*([^\]]*))?\]/g;
141
+
142
+ /**
143
+ * The citations one bracket holds, in order, as `{ id, locator }`. A
144
+ * `;`-separated part that is only `@id` starts another citation (Pandoc's
145
+ * `[@a; @b]`); any other part belongs to the locator of the citation before
146
+ * it, so a locator may itself contain `;` (`[@a; §1.2; eq. 3]`).
147
+ *
148
+ * @param {string} firstId
149
+ * @param {string | undefined} rest
150
+ * @returns {{ id: string, locator: string }[]}
151
+ */
152
+ export function bracketCitations(firstId, rest) {
153
+ const citations = [{ id: String(firstId).trim(), parts: [] }];
154
+ for (const part of String(rest ?? '').split(';')) {
155
+ const next = /^\s*@([a-z0-9][a-z0-9.-]*)\s*$/.exec(part);
156
+ if (next) citations.push({ id: next[1], parts: [] });
157
+ else if (part.trim()) citations[citations.length - 1].parts.push(part);
158
+ }
159
+ return citations.map(({ id, parts }) => ({
160
+ id,
161
+ locator: parts.join(';').replace(/\s+/g, ' ').trim(),
162
+ }));
163
+ }
164
+
165
+ /** Every source id a text cites, `[@a; @b]` included. */
166
+ export function citedIds(text) {
167
+ const ids = new Set();
168
+ for (const match of String(text).matchAll(CITATION_BRACKET)) {
169
+ for (const { id } of bracketCitations(match[1] ?? '', match[2])) ids.add(id);
170
+ }
171
+ return ids;
172
+ }
173
+
174
+ /**
175
+ * A Markdown link whose text holds a citation, `[as measured in [@id]](url)`:
176
+ * remark-citation leaves link text alone (a link cannot hold the marker's
177
+ * link), so the citation would print as typed. Group 1 is the link text.
178
+ */
179
+ const LINK_WITH_CITATION = /\[((?:[^[\]\n]|\[[^[\]\n]*\])*?\[@[^\]\n]*\](?:[^[\]\n]|\[[^[\]\n]*\])*)\]\(/g;
180
+
181
+ /**
182
+ * Each citation written inside a link's text, as `{ index, text }`: the
183
+ * offset of its `[@` in `text`, and the citation as written.
184
+ */
185
+ export function citationsInLinks(text) {
186
+ const found = [];
187
+ for (const link of String(text).matchAll(LINK_WITH_CITATION)) {
188
+ const start = (link.index ?? 0) + 1;
189
+ for (const citation of (link[1] ?? '').matchAll(/\[@[^\]\n]*\]/g)) {
190
+ found.push({ index: start + (citation.index ?? 0), text: citation[0] });
191
+ }
192
+ }
193
+ return found;
194
+ }
195
+
196
+ /**
197
+ * `text` with every citation inside a link's text blanked, offsets kept: what
198
+ * the page cites, for a scan that counts a lesson's sources.
199
+ */
200
+ export function withoutLinkCitations(text) {
201
+ let out = String(text);
202
+ for (const { index, text: citation } of citationsInLinks(out)) {
203
+ out = out.slice(0, index) + citation.replace(/[^\n]/g, ' ') + out.slice(index + citation.length);
204
+ }
205
+ return out;
206
+ }
207
+
@@ -12,9 +12,7 @@
12
12
  * (a) glyph-unique-in-corpus one rendered glyph → one meaning corpus-wide
13
13
  * (b) notation-units / units-vocab every entry has units or `dimensionless`,
14
14
  * unit strings drawn from a controlled vocab
15
- * (c) convention-single-definition a sign / cash-flow-perspective convention a
16
- * lesson uses resolves to one declaration
17
- * (f) weak-local a `notation.local` symbol used only in its
15
+ * (f) weak-local a `notation.local` symbol used only in its
18
16
  * own defining equation and prose
19
17
  * (g) notation-source-locator every notation `sources` entry has a locator
20
18
  * (h) operator-explained a named operator a lesson uses is on the
@@ -39,18 +37,17 @@
39
37
 
40
38
  import type {
41
39
  CourseAccents,
42
- CourseConventions,
43
40
  CourseOperators,
44
41
  CourseRomanLabels,
45
42
  } from '../compiler/course-config';
46
- import type { CurriculumCatalog } from '../curriculum/validation';
47
43
  import type { NotationSpec } from '../compiler/collections';
48
44
  import type { LintIgnore } from './lint-ignore';
49
45
  import { effectiveAccentAllowlist } from './base-accents.mjs';
50
46
  import { effectiveOperatorAllowlist } from './base-operators.mjs';
51
47
  import { effectiveRomanLabelAllowlist } from './base-roman-labels.mjs';
52
48
  import { stripEquationLabels } from './equations.mjs';
53
- import { pageScope } from './gate-math';
49
+ import { pageScope, scopeEntry, type ScopeEntry } from './gate-math';
50
+ import { isIndexEntryLatex } from '../compiler/content-schemas';
54
51
  import {
55
52
  collectDecorations,
56
53
  collectOperators,
@@ -65,12 +62,12 @@ import type {
65
62
  NotationRegistry,
66
63
  NotationRegistryInput,
67
64
  } from './types';
65
+ import { blankPropStrings } from './gate-prop-text';
68
66
 
69
67
  export type ConsistencyCode =
70
68
  | 'glyph-unique-in-corpus'
71
69
  | 'notation-units'
72
70
  | 'units-vocab'
73
- | 'convention-single-definition'
74
71
  | 'weak-local'
75
72
  | 'notation-source-locator'
76
73
  | 'gloss-wants-promoting'
@@ -79,6 +76,7 @@ export type ConsistencyCode =
79
76
  | 'operator-explained'
80
77
  | 'roman-label-explained'
81
78
  | 'accent-explained'
79
+ | 'accent-swallowed'
82
80
  | 'operator-suspended'
83
81
  | 'operator-suspension-reason'
84
82
  | 'operator-shadowed';
@@ -100,10 +98,7 @@ export interface ConsistencyInput {
100
98
  readonly registry: NotationRegistry;
101
99
  readonly notationInput: NotationRegistryInput;
102
100
  readonly specs: readonly NotationSpec[];
103
- readonly catalog: CurriculumCatalog;
104
101
  readonly lintIgnore: LintIgnore;
105
- /** Convention keys this course anchors on (from `content/course.config.ts`). */
106
- readonly conventions: CourseConventions;
107
102
  /** This course's edits to the named-operator baseline (rule N8). */
108
103
  readonly operators?: CourseOperators;
109
104
  /** This course's edits to the roman-label baseline (rule N10). */
@@ -112,9 +107,6 @@ export interface ConsistencyInput {
112
107
  readonly accents?: CourseAccents;
113
108
  }
114
109
 
115
- const CONVENTION_RE =
116
- /\bsign convention\b|\bcash-flow perspective\b|\bholder(?:'s)?(?:\s+\w+){0,2}\s+perspective\b|\bfrom (?:a|one|the|its)(?:\s+\w+){0,3}\s+perspective\b|\breverses? every sign\b|\bpositive (?:means|amounts?)\b/i;
117
-
118
110
  // --- shared text helpers ------------------------------------------------
119
111
 
120
112
  function stripCode(text: string): string {
@@ -128,13 +120,14 @@ interface MathSpan {
128
120
  }
129
121
 
130
122
  /**
131
- * Every `$$…$$` then `$…$` span in `body`, outside code. A `\label{eq:…}`
123
+ * Every `$$…$$` then `$…$` span in `body`, outside code and quoted JSX props
124
+ * (plain text; invariant `prop-text`). A `\label{eq:…}`
132
125
  * (D7) is stripped: it is an identity marker KaTeX cannot parse, and the
133
126
  * scans below treat an unparseable span as having nothing in it — so left in,
134
127
  * every labelled display equation was invisible to rules N8, N10 and N22.
135
128
  */
136
129
  function mathSpans(body: string): MathSpan[] {
137
- const searchable = stripCode(body);
130
+ const searchable = stripCode(blankPropStrings(body));
138
131
  const spans: MathSpan[] = [];
139
132
  for (const match of searchable.matchAll(/\$\$([\s\S]+?)\$\$/g)) {
140
133
  spans.push({ latex: stripEquationLabels(match[1] ?? '') });
@@ -217,6 +210,9 @@ function checkNotationUnits(
217
210
  out: ConsistencyDiagnostic[],
218
211
  ): void {
219
212
  for (const spec of input.specs) {
213
+ // An index entry (`{}_0`, rule N23) names a subscript, which has no
214
+ // unit; its derived cards take the indexed symbol's.
215
+ if (isIndexEntryLatex(spec.latex)) continue;
220
216
  if (spec.units === undefined && !spec.dimensionless) {
221
217
  if (input.lintIgnore.matches('notation-units', spec.key)) continue;
222
218
  out.push({
@@ -244,42 +240,6 @@ function checkNotationUnits(
244
240
  }
245
241
  }
246
242
 
247
- function checkConventionSingleDefinition(
248
- input: ConsistencyInput,
249
- out: ConsistencyDiagnostic[],
250
- ): void {
251
- const lessonById = new Map(
252
- input.catalog.lessons.map((lesson) => [lesson.id, lesson]),
253
- );
254
- for (const lesson of input.notationInput.lessons) {
255
- const body = lesson.body;
256
- if (!CONVENTION_RE.test(body)) continue;
257
- const meta = lessonById.get(lesson.lessonId);
258
- const { signConventionKey, cashFlowPerspectiveCompetency } =
259
- input.conventions;
260
- const anchoredByCompetency = [
261
- ...(meta?.teaches ?? []),
262
- ...(meta?.requires ?? []),
263
- ].includes(cashFlowPerspectiveCompetency);
264
- const anchoredByTerm = new RegExp(
265
- `\\[\\[\\s*${signConventionKey}\\s*\\]\\]`,
266
- ).test(body);
267
- if (anchoredByCompetency || anchoredByTerm) continue;
268
- if (
269
- input.lintIgnore.matches('convention-single-definition', lesson.lessonId)
270
- ) {
271
- continue;
272
- }
273
- out.push({
274
- code: 'convention-single-definition',
275
- severity: 'warning',
276
- lessonId: lesson.lessonId,
277
- file: lesson.source.file,
278
- message: `${lesson.lessonId} states a sign / cash-flow-perspective convention in prose but does not resolve it to a single declaration ([[${signConventionKey}]] or ${cashFlowPerspectiveCompetency})`,
279
- });
280
- }
281
- }
282
-
283
243
  function checkWeakLocal(
284
244
  input: ConsistencyInput,
285
245
  out: ConsistencyDiagnostic[],
@@ -471,7 +431,7 @@ function checkGlossNameShape(
471
431
 
472
432
  /**
473
433
  * A named operator a lesson uses is either universal — on the shipped
474
- * `BASE_OPERATORS` baseline plus this course's `operators.allow`, minus its
434
+ * `BASE_OPERATORS` baseline plus this course's `rules.N8.allow`, minus its
475
435
  * `operators.deny` — or it is explained: a notation card or `notation.local`
476
436
  * entry the lesson binds with `[[key]]` whose `latex` is that operator, so the
477
437
  * reader can hover it. Anything else (`\operatorname{Var}` with no entry, or a
@@ -530,7 +490,7 @@ function checkOperatorExplained(
530
490
  lessonId: lesson.lessonId,
531
491
  file: lesson.source.file,
532
492
  glyph: operator.name,
533
- message: `${lesson.lessonId}: operator ${JSON.stringify(display)} is neither on the course operator allowlist nor explained by a notation entry; give it a card or notation.local entry (latex ${JSON.stringify(display)}) introduced with [[key]], or add ${JSON.stringify(operator.name)} to operators.allow in course.config.ts`,
493
+ message: `${lesson.lessonId}: operator ${JSON.stringify(display)} is neither on the course operator allowlist nor explained by a notation entry; give it a card or notation.local entry (latex ${JSON.stringify(display)}) introduced with [[key]], or add ${JSON.stringify(operator.name)} to rules.N8.allow in the course config (course.config.ts, or the course.settings.json it reads)`,
534
494
  });
535
495
  }
536
496
  }
@@ -590,20 +550,18 @@ function checkRomanLabelExplained(
590
550
  lessonId: lesson.lessonId,
591
551
  file: lesson.source.file,
592
552
  glyph: label.name,
593
- message: `${lesson.lessonId}: roman label ${JSON.stringify(`\\mathrm{${label.name}}`)} is neither on the course roman-label allowlist nor explained by a notation entry; give it a card or notation.local entry (latex ${JSON.stringify(`\\mathrm{${label.name}}`)}) introduced with [[key]], or add ${JSON.stringify(label.name)} to romanLabels.allow in course.config.ts`,
553
+ message: `${lesson.lessonId}: roman label ${JSON.stringify(`\\mathrm{${label.name}}`)} is neither on the course roman-label allowlist nor explained by a notation entry; give it a card or notation.local entry (latex ${JSON.stringify(`\\mathrm{${label.name}}`)}) introduced with [[key]], or add ${JSON.stringify(label.name)} to rules.N10.allow in the course config (course.config.ts, or the course.settings.json it reads)`,
594
554
  });
595
555
  }
596
556
  }
597
557
  }
598
558
  }
599
559
 
600
- function sharedScopeByKey(
601
- input: ConsistencyInput,
602
- ): Map<string, { key: string; notation: string }> {
560
+ function sharedScopeByKey(input: ConsistencyInput): Map<string, ScopeEntry> {
603
561
  return new Map(
604
562
  input.notationInput.sharedDefinitions.map((definition) => [
605
563
  definition.key,
606
- { key: definition.key, notation: definition.notation },
564
+ scopeEntry(definition),
607
565
  ]),
608
566
  );
609
567
  }
@@ -633,11 +591,40 @@ function checkAccentExplained(
633
591
  for (const span of mathSpans(lesson.body)) {
634
592
  let decorations;
635
593
  try {
636
- ({ decorations } = resolveMathGlyphs(span.latex, scope, { suspended }));
594
+ ({ decorations } = resolveMathGlyphs(span.latex, scope, {
595
+ suspended,
596
+ derive: true,
597
+ }));
637
598
  } catch {
638
599
  continue;
639
600
  }
640
601
  for (const decoration of decorations) {
602
+ // An author's `\explain{key}{\hat{\beta}}` counts as the whole form
603
+ // only when `key`'s own `latex` carries the decoration: the `beta`
604
+ // card says nothing about a hat (the twin of rule N23's
605
+ // `index-swallowed`).
606
+ if (decoration.wholeForm !== null && decoration.wholeFormExplicit) {
607
+ const notation =
608
+ scope.find((entry) => entry.key === decoration.wholeForm)?.notation ?? '';
609
+ const carries = collectDecorations(notation).some(
610
+ (item) => item.name === decoration.name,
611
+ );
612
+ const id = `swallowed:${decoration.token}`;
613
+ if (!carries && !reported.has(id)) {
614
+ reported.add(id);
615
+ if (!input.lintIgnore.matches('accent-swallowed', decoration.name)) {
616
+ out.push({
617
+ code: 'accent-swallowed',
618
+ severity: 'warning',
619
+ lessonId: lesson.lessonId,
620
+ file: lesson.source.file,
621
+ glyph: decoration.name,
622
+ message: `${lesson.lessonId}: \\explain{${decoration.wholeForm}}{${decoration.token}} wraps the decoration ${JSON.stringify(decoration.name)} in ${decoration.wholeForm}, whose latex (${notation}) has none, so its card cannot explain it; wrap only the symbol under it and give the decoration an operator entry (latex ${JSON.stringify(operatorEntryLatex(decoration.name))}), or name an entry for the decorated form (latex ${JSON.stringify(decoration.token)})`,
623
+ });
624
+ }
625
+ }
626
+ continue;
627
+ }
641
628
  if (decoration.wholeForm !== null || decoration.operator !== null) continue;
642
629
  // A suspended decoration never composes; the gate fails it instead.
643
630
  if (suspended.includes(decoration.name)) continue;
@@ -737,7 +724,6 @@ const CHECKS: ((
737
724
  ) => void)[] = [
738
725
  checkGlyphUniqueness,
739
726
  checkNotationUnits,
740
- checkConventionSingleDefinition,
741
727
  checkWeakLocal,
742
728
  checkNotationSourceLocator,
743
729
  checkGlossWantsPromoting,
@@ -777,7 +763,6 @@ export function consistencyCounts(
777
763
  'glyph-unique-in-corpus': 0,
778
764
  'notation-units': 0,
779
765
  'units-vocab': 0,
780
- 'convention-single-definition': 0,
781
766
  'weak-local': 0,
782
767
  'notation-source-locator': 0,
783
768
  'gloss-wants-promoting': 0,
@@ -786,6 +771,7 @@ export function consistencyCounts(
786
771
  'operator-explained': 0,
787
772
  'roman-label-explained': 0,
788
773
  'accent-explained': 0,
774
+ 'accent-swallowed': 0,
789
775
  'operator-suspended': 0,
790
776
  'operator-suspension-reason': 0,
791
777
  'operator-shadowed': 0,
@@ -6,24 +6,16 @@ import type {
6
6
  NotationRegistry,
7
7
  } from './types';
8
8
 
9
- interface EditorialCompetency {
10
- readonly id: string;
11
- readonly editorialStatus?: 'draft' | 'in-review' | 'reviewed';
12
- }
13
-
14
9
  export function validateNotationAlignment(
15
10
  registry: NotationRegistry,
16
11
  catalog: CurriculumCatalog,
17
12
  ): readonly NotationDiagnostic[] {
18
13
  const diagnostics: NotationDiagnostic[] = [];
19
14
  const competencies = new Map(
20
- (catalog.competencies as readonly EditorialCompetency[]).map((entry) => [
21
- entry.id,
22
- entry,
23
- ]),
15
+ catalog.competencies.map((entry) => [entry.id, entry]),
24
16
  );
25
17
  const lessons = new Map(catalog.lessons.map((entry) => [entry.id, entry]));
26
- const sources = new Map(catalog.sources.map((entry) => [entry.id, entry]));
18
+ const sources = new Set(catalog.sources.map((entry) => entry.id));
27
19
  const trackPositions = buildTrackPositions(catalog, diagnostics);
28
20
 
29
21
  for (const definition of registry.definitions) {
@@ -82,20 +74,6 @@ export function validateNotationAlignment(
82
74
  trackPositions,
83
75
  diagnostics,
84
76
  );
85
-
86
- if (
87
- definition.status === 'reviewed' &&
88
- (lesson.status !== 'reviewed' ||
89
- competency?.editorialStatus !== 'reviewed')
90
- ) {
91
- diagnostics.push(
92
- error(
93
- 'alignment-review-state',
94
- `${definition.id} is reviewed while its owning lesson or competency is not reviewed`,
95
- definition,
96
- ),
97
- );
98
- }
99
77
  }
100
78
 
101
79
  return diagnostics.sort(compareDiagnostics);
@@ -177,15 +155,11 @@ function validateIntroductionOrder(
177
155
 
178
156
  function validateSources(
179
157
  definition: NotationDefinitionRecord,
180
- sources: ReadonlyMap<
181
- string,
182
- { readonly editorialStatus: 'draft' | 'in-review' | 'reviewed' }
183
- >,
158
+ sources: ReadonlySet<string>,
184
159
  diagnostics: NotationDiagnostic[],
185
160
  ): void {
186
161
  for (const sourceId of definition.sources) {
187
- const source = sources.get(sourceId);
188
- if (!source) {
162
+ if (!sources.has(sourceId)) {
189
163
  diagnostics.push(
190
164
  error(
191
165
  'alignment-unavailable',
@@ -193,17 +167,6 @@ function validateSources(
193
167
  definition,
194
168
  ),
195
169
  );
196
- } else if (
197
- definition.status === 'reviewed' &&
198
- source.editorialStatus !== 'reviewed'
199
- ) {
200
- diagnostics.push(
201
- error(
202
- 'alignment-review-state',
203
- `${definition.id} is reviewed while source ${sourceId} is ${source.editorialStatus}`,
204
- definition,
205
- ),
206
- );
207
170
  }
208
171
  }
209
172
  }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Derived cards (rule N23) as the page payload carries them: each derived
3
+ * card a page's math uses, composed from its base and index entries by
4
+ * `derived-notation.mjs`, with the glyph table its formula resolves against.
5
+ * The manifest builds the ones lesson-body and assessment math use; the page
6
+ * payload (`page-notation.ts`) builds the ones a lab or a diagram registers
7
+ * while it renders, the same way.
8
+ */
9
+ import type { ManifestDerivedNotation } from '../compiler/manifest';
10
+ import { composeDerivedEntry } from './derived-notation.mjs';
11
+ import { scopeEntry } from './gate-math';
12
+ import { formulaExplainKeys, type GlyphScopeEntry } from './gloss';
13
+ import { baseSymbolLatex, derivedNotationLatex } from './math-glyphs.mjs';
14
+ import type { DerivedUse, NotationDefinitionRecord } from './types';
15
+
16
+ /**
17
+ * The derived cards for `uses`, sorted by key. `records` holds the page's
18
+ * entries by key, local ones winning; a use whose base or index entry is not
19
+ * there is skipped. A card's formula resolves against the derived symbol,
20
+ * the index entry's glosses, the base, and the cards the formula names.
21
+ */
22
+ export function derivedNotationEntries(
23
+ uses: readonly DerivedUse[],
24
+ records: ReadonlyMap<string, NotationDefinitionRecord>,
25
+ cardsByKey: ReadonlyMap<string, GlyphScopeEntry>,
26
+ ): ManifestDerivedNotation[] {
27
+ const derived = new Map<string, ManifestDerivedNotation>();
28
+ for (const use of uses) {
29
+ if (derived.has(use.key)) continue;
30
+ const base = records.get(use.base);
31
+ const index = records.get(use.index);
32
+ if (!base || !index) continue;
33
+ const notation = derivedNotationLatex(base.notation, index.notation);
34
+ const entry = composeDerivedEntry({
35
+ key: use.key,
36
+ notation,
37
+ baseLatex: baseSymbolLatex(base.notation),
38
+ base,
39
+ index: {
40
+ ...index,
41
+ ...(index.formulaTemplate === undefined ? {} : { formula: index.formulaTemplate }),
42
+ },
43
+ });
44
+ const formulaScope: GlyphScopeEntry[] = [
45
+ { key: use.key, notation },
46
+ ...index.glosses.map((gloss) => ({ key: gloss.key, notation: gloss.notation })),
47
+ scopeEntry(base),
48
+ ];
49
+ for (const key of formulaExplainKeys(entry.formula ?? '')) {
50
+ if (formulaScope.some((row) => row.key === key)) continue;
51
+ const card = records.get(key) ?? cardsByKey.get(key);
52
+ if (card) formulaScope.push(scopeEntry(card));
53
+ }
54
+ derived.set(use.key, {
55
+ key: entry.key,
56
+ base: entry.base,
57
+ index: entry.index,
58
+ notation: entry.notation,
59
+ label: entry.label,
60
+ summary: entry.summary,
61
+ glosses: [...index.glosses],
62
+ formulaScope,
63
+ ...(entry.units === undefined ? {} : { units: entry.units }),
64
+ ...(entry.formula === undefined ? {} : { formula: entry.formula }),
65
+ });
66
+ }
67
+ return [...derived.values()].sort((left, right) => left.key.localeCompare(right.key));
68
+ }
@@ -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
+ }