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
@@ -6,8 +6,8 @@
6
6
  * quick look at the source without leaving the reader's place.
7
7
  *
8
8
  * It reads everything it shows from the page's own static markup — the
9
- * marker's `data-*` and the matching `#cite-n` reference-list item — so no
10
- * page serializes a second copy of the source database. Opening, pinning,
9
+ * marker's `data-*` and the reference-list item it links to (`#cite-n`) —
10
+ * so no page serializes a second copy of the source database. Opening, pinning,
11
11
  * Escape and placement come from the card manager (`../card-manager.ts`); a
12
12
  * card per source, several at once.
13
13
  */
@@ -21,12 +21,11 @@ import HoverPanel from '../HoverPanel.astro';
21
21
  </p>
22
22
  <div class="citation-panel-body">
23
23
  <p class="citation-panel-locator" data-panel-locator hidden></p>
24
- <p class="citation-panel-status" data-panel-status hidden></p>
25
24
  <p class="citation-panel-links">
26
- <a class="citation-panel-link" data-panel-link hidden
25
+ <a class="citation-panel-link hover-panel-action" data-panel-link hidden
27
26
  >Go to the source<span aria-hidden="true" data-jump-arrow> ↗</span></a
28
27
  >
29
- <a class="citation-panel-jump" data-panel-jump
28
+ <a class="citation-panel-jump hover-panel-action" data-panel-jump
30
29
  >Go to the reference list<span aria-hidden="true" data-jump-arrow> ↓</span></a
31
30
  >
32
31
  </p>
@@ -35,6 +34,7 @@ import HoverPanel from '../HoverPanel.astro';
35
34
 
36
35
  <script>
37
36
  import { cloneCardShell, registerCardSource } from '../card-manager';
37
+ import { fetchDoc, hrefFrom } from '../card-fetch';
38
38
  import { wireJumpArrow } from '../card-jump';
39
39
  import { referenceLabelFromText } from '../../reference/citation-format.mjs';
40
40
 
@@ -49,10 +49,11 @@ import HoverPanel from '../HoverPanel.astro';
49
49
 
50
50
  function setup() {
51
51
  const template = document.getElementById('citation-panel');
52
+ // Every page registers, with markers or none: a citation's card carried
53
+ // in from another page is built here (rule K5).
52
54
  if (
53
55
  !(template instanceof HTMLTemplateElement) ||
54
- template.dataset.cardSourceReady === 'true' ||
55
- !document.querySelector('a.citation-ref[data-citation-id]')
56
+ template.dataset.cardSourceReady === 'true'
56
57
  ) {
57
58
  return;
58
59
  }
@@ -66,27 +67,34 @@ import HoverPanel from '../HoverPanel.astro';
66
67
  return candidate && !candidate.closest('[data-card-foreign]') ? candidate : null;
67
68
  };
68
69
 
69
- // Fill a card from the marker plus its `#cite-n` reference-list item —
70
- // never from a serialized source table.
71
- const render = (number: string, marker: HTMLElement) => {
72
- const item = document.getElementById(`cite-${number}`);
70
+ // The reference-list item a marker links to (`#cite-n`). Followed, not
71
+ // built from the number: a downloaded part or course holds one list per
72
+ // chapter, each numbered from 1, with the chapter in its items' ids.
73
+ const itemIdOf = (marker: HTMLElement) => {
74
+ const href = marker.getAttribute('href') ?? '';
75
+ return href.startsWith('#') && href.length > 1 ? decodeURIComponent(href.slice(1)) : null;
76
+ };
77
+
78
+ // Fill a card from the marker plus its reference-list item — never from
79
+ // a serialized source table. On another page (K5), both come from the
80
+ // page the card was opened on, and its links go there.
81
+ const render = (itemId: string, marker: HTMLElement, from?: Document, page?: string) => {
82
+ const item = (from ?? document).getElementById(itemId);
83
+ const number = marker.dataset.citationNumber ?? '';
73
84
  const el = cloneCardShell(template);
74
85
  if (!item || !el) return null;
75
86
  const field = (name: string) => el.querySelector<HTMLElement>(`[data-panel-${name}]`);
76
87
  const markerEl = field('marker');
77
88
  const label = field('label');
78
89
  const locator = field('locator');
79
- const status = field('status');
80
90
  const link = el.querySelector<HTMLAnchorElement>('[data-panel-link]');
81
91
  const jump = el.querySelector<HTMLAnchorElement>('[data-panel-jump]');
82
- if (!markerEl || !label || !locator || !status || !link || !jump) return null;
92
+ if (!markerEl || !label || !locator || !link || !jump) return null;
83
93
 
84
94
  // The reference list is a tight list, so the `<li>` has no `<p>`
85
- // wrapper: clone it and drop the trailing link/status chrome.
86
- const line = item.cloneNode(true) as HTMLElement;
87
- for (const extra of line.querySelectorAll(
88
- '.citation-url, .citation-status, .citation-backref',
89
- )) {
95
+ // wrapper: clone it and drop the trailing link chrome.
96
+ const line = document.importNode(item, true);
97
+ for (const extra of line.querySelectorAll('.citation-url, .citation-backref')) {
90
98
  extra.remove();
91
99
  }
92
100
  const referenceText = line.textContent ?? '';
@@ -97,11 +105,6 @@ import HoverPanel from '../HoverPanel.astro';
97
105
  setText(locator, loc);
98
106
  el.setAttribute('aria-label', `Source citation ${number}`);
99
107
 
100
- const sourceStatus = item
101
- .querySelector('.citation-status')
102
- ?.textContent?.trim();
103
- setText(status, sourceStatus ? `Source record: ${sourceStatus}` : '');
104
-
105
108
  const sourceUrl = item.querySelector<HTMLAnchorElement>('a.citation-url');
106
109
  if (sourceUrl) {
107
110
  link.href = sourceUrl.href;
@@ -109,19 +112,34 @@ import HoverPanel from '../HoverPanel.astro';
109
112
  link.hidden = false;
110
113
  }
111
114
 
112
- jump.href = marker.getAttribute('href') ?? '#references';
115
+ const jumpTo = marker.getAttribute('href') ?? '#references';
116
+ jump.href = page ? hrefFrom(jumpTo, page) : jumpTo;
113
117
  wireJumpArrow(jump);
114
118
  return el;
115
119
  };
116
120
 
121
+ /** A reference-list item names its source only on its own page. */
122
+ const idOf = (item: string) => item.split('@')[0] ?? item;
123
+
117
124
  registerCardSource({
118
125
  layer: 'citation',
119
126
  triggerFrom: markerFrom,
120
127
  itemFor: (marker) => {
121
- const number = marker.dataset.citationNumber;
122
- return number && document.getElementById(`cite-${number}`) ? number : null;
128
+ const id = itemIdOf(marker);
129
+ return id && marker.dataset.citationNumber && document.getElementById(id) ? id : null;
123
130
  },
124
- render,
131
+ render: (id, marker) => render(id, marker),
132
+ // K5: on another page, `[3]` is that page's third source.
133
+ awayItem: (id, page) => `${idOf(id)}@${page}`,
134
+ build: (id, page) =>
135
+ fetchDoc(page).then((doc) => {
136
+ const marker = [
137
+ ...doc.querySelectorAll<HTMLElement>('a.citation-ref[data-citation-number]'),
138
+ ].find((candidate) => itemIdOf(candidate) === idOf(id));
139
+ const el = marker ? render(idOf(id), marker, doc, page) : null;
140
+ el?.setAttribute('data-card-foreign', '');
141
+ return el;
142
+ }),
125
143
  });
126
144
  }
127
145
 
@@ -155,21 +173,8 @@ import HoverPanel from '../HoverPanel.astro';
155
173
  color: var(--sl-color-gray-2);
156
174
  }
157
175
 
158
- .citation-panel-status {
159
- color: var(--sl-color-gray-3);
160
- font-size: var(--sl-text-xs);
161
- text-transform: lowercase;
162
- }
163
-
164
176
  .citation-panel-body .citation-panel-links {
165
177
  margin: 0;
166
178
  }
167
-
168
- .citation-panel-link,
169
- .citation-panel-jump {
170
- display: inline-block;
171
- margin-inline-end: 0.9rem;
172
- font-size: var(--sl-text-xs);
173
- }
174
179
  }
175
180
  </style>
@@ -17,9 +17,20 @@
17
17
  * client script**: `NotationLayer`'s trigger scan already queries the whole
18
18
  * document, not just its own subtree.
19
19
  *
20
- * Box sizes are a generous estimate (`reference/diagram-layout.ts`) unless a
21
- * node gives an explicit `width`/`height` — there is no DOM at build time to
22
- * measure a real render against.
20
+ * Each box is sized from what it holds (`nodeBoxSize` in
21
+ * `reference/diagram-layout.ts`): the label measured in DejaVu Sans, the
22
+ * widest common system font, and wrapped to a line count that sets the
23
+ * height, and the formula measured from KaTeX's own layout tree
24
+ * (`reference/katex-measure.ts`). A node's `width` or `height` overrides
25
+ * either; with only a `width`, the label wraps inside it and the height
26
+ * follows. A diagram wider than the lesson column first wraps its labels
27
+ * narrower until it fits (`fitDiagramLayout`).
28
+ *
29
+ * One that still does not fit (rule V8, `rules.V8.columnWidth`, or the
30
+ * diagram's own `warnAboveWidth`) is marked `data-diagram-too-wide`; the
31
+ * build reports it as a `dia-too-wide` warning with the lesson file and this
32
+ * id (`reference/diagram-fit.ts`), and `astro dev` prints the same warning as
33
+ * it renders.
23
34
  *
24
35
  * Named like every float (docs/plans/titles-and-containers.md Part 3): the
25
36
  * long `caption` sits below the graph with its number, in the same caption
@@ -31,7 +42,10 @@
31
42
  * reader fold a diagram away, wrap it in a `<Fold>` or put it in a
32
43
  * collapsible section.
33
44
  */
34
- import { estimateBoxSize, layoutDiagram } from '../../reference/diagram-layout';
45
+ import { fitDiagramLayout, FORMULA_FONT_PX } from '../../reference/diagram-layout';
46
+ import { measureFormula } from '../../reference/katex-measure';
47
+ import { diagramTooWideMessage, diagramWidthLimit } from '../../reference/diagram-fit';
48
+ import { loadManifest } from '../../compiler/manifest';
35
49
  import { renderNotationFormula } from '../notation/render-notation-math';
36
50
  import type { SafeMathNode as SafeMathNodeType } from '../notation/render-notation-math';
37
51
  import SafeMathNode from '../notation/SafeMathNode.astro';
@@ -40,8 +54,11 @@ import { DIAGRAM_NO_LONGER_FOLDS } from '../../reference/diagrams-validate';
40
54
  import type { GlyphScopeEntry } from '../../reference/gloss';
41
55
  import {
42
56
  notationKeysInMathNodes,
57
+ registerPageDerivedNotation,
43
58
  registerPageNotationDependencies,
44
59
  } from '../../reference/page-notation';
60
+ import { indexFindings } from '../../reference/gate-indices';
61
+ import type { DerivedUse } from '../../reference/types';
45
62
 
46
63
  export interface DiagramNode {
47
64
  readonly id: string;
@@ -49,8 +66,9 @@ export interface DiagramNode {
49
66
  readonly text?: string;
50
67
  /** Raw LaTeX, resolved against `scope` before rendering. */
51
68
  readonly formula?: string;
52
- /** Overrides the size estimate — use for a label the heuristic gets wrong. */
69
+ /** The box's width in px; the label wraps inside it and the height follows. */
53
70
  readonly width?: number;
71
+ /** The box's height in px. */
54
72
  readonly height?: number;
55
73
  }
56
74
 
@@ -76,8 +94,9 @@ export interface Props {
76
94
  /** Computed by `remark-notation.mjs` for a top-level lesson diagram. */
77
95
  anchorId?: string;
78
96
  /**
79
- * Emit a build warning when the laid-out SVG is wider than this many pixels.
80
- * Set `false` only when horizontal overflow is deliberate. Default: `800`.
97
+ * The widest this diagram may lay out, in px, before the build warns
98
+ * (`dia-too-wide`, rule V8). Default: the course's `rules.V8.columnWidth`,
99
+ * the lesson column. Set `false` only when horizontal overflow is deliberate.
81
100
  */
82
101
  warnAboveWidth?: number | false;
83
102
  }
@@ -92,7 +111,7 @@ const {
92
111
  direction = 'TB',
93
112
  number,
94
113
  anchorId,
95
- warnAboveWidth = 800,
114
+ warnAboveWidth,
96
115
  } = Astro.props;
97
116
 
98
117
  const idPattern = /^[A-Za-z][A-Za-z0-9_-]*$/;
@@ -128,6 +147,7 @@ if ((number && !anchorId) || (!number && anchorId)) {
128
147
  );
129
148
  }
130
149
  if (
150
+ warnAboveWidth !== undefined &&
131
151
  warnAboveWidth !== false &&
132
152
  (!Number.isFinite(warnAboveWidth) || warnAboveWidth <= 0)
133
153
  ) {
@@ -136,26 +156,47 @@ if (
136
156
 
137
157
  interface RenderedNode extends DiagramNode {
138
158
  readonly mathNodes?: readonly SafeMathNodeType[];
159
+ /** The LaTeX `mathNodes` were rendered from, glyphs bound. */
160
+ readonly boundFormula?: string;
139
161
  }
140
162
 
163
+ // The course's rule settings; a page rendered without a manifest (a
164
+ // component test) checks against the defaults.
165
+ const rules = await loadManifest().then(
166
+ (manifest) => manifest.rules,
167
+ () => undefined,
168
+ );
169
+
170
+ // Rule N23: a diagram's formula is page math, so its indexed symbols take
171
+ // derived cards like the lesson's, and are held to the rule.
172
+ const derivedUses: DerivedUse[] = [];
141
173
  const rendered: RenderedNode[] = nodes.map((node) => {
142
174
  if (!node.text && !node.formula) {
143
175
  throw new Error(`Diagram "${id}" node "${node.id}" has neither text nor formula.`);
144
176
  }
145
177
  if (!node.formula) return { ...node };
146
178
 
147
- const { nodes: mathNodes, unresolved, degraded } = renderNotationFormula(
148
- node.formula,
149
- scope,
150
- );
179
+ const {
180
+ nodes: mathNodes,
181
+ latex,
182
+ unresolved,
183
+ degraded,
184
+ reason,
185
+ derived,
186
+ indices,
187
+ } = renderNotationFormula(node.formula, scope, { derive: true });
151
188
  if (degraded) {
189
+ // The resolver says what went wrong; an empty `scope` is the usual cause
190
+ // when the diagram is written straight into MDX, where nothing passes it.
191
+ const emptyScope =
192
+ scope.length === 0
193
+ ? ' The diagram has no `scope`, so no symbol can resolve: pass the page\'s ' +
194
+ 'notation scope (`labMathScope`), as a wrapper component does.'
195
+ : '';
152
196
  throw new Error(
153
197
  `Diagram "${id}" node "${node.id}" formula "${node.formula}" could not be ` +
154
- `bound to the notation scope: the LaTeX did not parse, or a bare glyph is ` +
155
- `the base of more than one page symbol. Wrap each symbol in ` +
156
- `\\explain{key}{latex}, or brace a subscripted base (write {v_0}^{2}, not ` +
157
- `v_0^{2}), so every glyph resolves — a diagram formula is never allowed to ` +
158
- `render as silent, unexplained math.`,
198
+ `bound to the notation scope: ${(reason ?? 'the LaTeX did not parse').replace(/\.$/, '')}.${emptyScope} ` +
199
+ `A diagram formula is never allowed to render as silent, unexplained math.`,
159
200
  );
160
201
  }
161
202
  if (unresolved.length > 0) {
@@ -166,33 +207,55 @@ const rendered: RenderedNode[] = nodes.map((node) => {
166
207
  `or pass it in the diagram's scope.`,
167
208
  );
168
209
  }
169
- return { ...node, mathNodes };
210
+ const findings = rules?.disabled.includes('N23') ? [] : indexFindings(indices);
211
+ if (findings.length > 0) {
212
+ throw new Error(
213
+ `Diagram "${id}" node "${node.id}" formula "${node.formula}" has an index no card ` +
214
+ `explains (rule N23): ${findings.join('; ')}.`,
215
+ );
216
+ }
217
+ derivedUses.push(...derived);
218
+ return { ...node, mathNodes, boundFormula: latex };
170
219
  });
171
220
 
221
+ const derivedKeys = new Set(derivedUses.map((use) => use.key));
222
+ registerPageDerivedNotation(Astro.locals, derivedUses, `Diagram "${id}"`);
172
223
  for (const node of rendered) {
173
224
  if (!node.mathNodes) continue;
174
225
  registerPageNotationDependencies(
175
226
  Astro.locals,
176
- notationKeysInMathNodes(node.mathNodes),
227
+ // A derived card is no definition: it ships through its own registry.
228
+ notationKeysInMathNodes(node.mathNodes).filter((key) => !derivedKeys.has(key)),
177
229
  `Diagram "${id}" node "${node.id}" formula "${node.formula}"`,
178
230
  );
179
231
  }
180
232
 
181
- const sized = rendered.map((node) => ({
182
- node,
183
- box:
184
- node.width && node.height
185
- ? { width: node.width, height: node.height }
186
- : estimateBoxSize(
187
- node.formula ?? node.text ?? node.id,
188
- node.formula ? 'formula' : 'text',
189
- ),
190
- }));
191
-
192
- const layout = layoutDiagram(
193
- sized.map(({ node, box }) => ({ id: node.id, width: box.width, height: box.height })),
233
+ for (const node of rendered) {
234
+ for (const side of ['width', 'height'] as const) {
235
+ const value = node[side];
236
+ if (value !== undefined && !(Number.isFinite(value) && value > 0)) {
237
+ throw new Error(`Diagram "${id}" node "${node.id}" needs a positive \`${side}\`.`);
238
+ }
239
+ }
240
+ }
241
+
242
+ // Rule V8's settings, from the course's rules above.
243
+ const widthLimit = diagramWidthLimit(warnAboveWidth, rules);
244
+
245
+ const { layout } = fitDiagramLayout(
246
+ rendered.map((node) => ({
247
+ id: node.id,
248
+ content: {
249
+ ...(node.text ? { text: node.text } : {}),
250
+ ...(node.boundFormula
251
+ ? { formula: measureFormula(node.boundFormula, FORMULA_FONT_PX) }
252
+ : {}),
253
+ },
254
+ ...(node.width !== undefined ? { width: node.width } : {}),
255
+ ...(node.height !== undefined ? { height: node.height } : {}),
256
+ })),
194
257
  edges.map((edge) => ({ from: edge.from, to: edge.to })),
195
- { direction },
258
+ { direction, ...(widthLimit !== undefined ? { fitWidth: widthLimit } : {}) },
196
259
  );
197
260
 
198
261
  const nodeById = new Map(rendered.map((node) => [node.id, node]));
@@ -203,13 +266,13 @@ const renderedAnchorId = anchorId ?? legacyAnchorId;
203
266
  const viewportId = `${legacyAnchorId}-viewport`;
204
267
  const captionTextId = `${legacyAnchorId}-caption`;
205
268
 
206
- if (warnAboveWidth !== false && layout.width > warnAboveWidth) {
269
+ const tooWide = widthLimit !== undefined && layout.width > widthLimit;
270
+ if (tooWide && import.meta.env.DEV) {
271
+ const entry = (Astro.locals as { starlightRoute?: { entry?: { filePath?: string; id?: string } } })
272
+ .starlightRoute?.entry;
207
273
  console.warn(
208
- `[explico/Diagram] "${id}" lays out at ${layout.width}px wide, above the ` +
209
- `${warnAboveWidth}px authoring threshold. It will scroll and can be expanded ` +
210
- `by readers, but consider recreating the graph with direction="TB", shorter ` +
211
- `labels, or explicit node sizes. Set warnAboveWidth={false} only when the ` +
212
- `horizontal overflow is intentional.`,
274
+ `[dia-too-wide] ${entry?.filePath ?? entry?.id ?? Astro.url.pathname}: ` +
275
+ diagramTooWideMessage(id, layout.width, widthLimit),
213
276
  );
214
277
  }
215
278
 
@@ -227,6 +290,7 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
227
290
  data-dia-key={number ? id : undefined}
228
291
  data-dia-short={short}
229
292
  data-diagram-width={layout.width}
293
+ data-diagram-too-wide={tooWide ? widthLimit : undefined}
230
294
  data-diagram-expandable
231
295
  >
232
296
  <div class="diagram__body">
@@ -426,23 +490,38 @@ function pathFrom(points: readonly { x: number; y: number }[]): string {
426
490
  stroke-width: 1;
427
491
  }
428
492
 
493
+ /* `NODE_FONT_PX` and the insets in `reference/diagram-layout.ts` size each
494
+ box from these values; change them together. The size is in px, not
495
+ rem: the boxes are measured at build time for 13.6px text, so text that
496
+ followed a reader's larger default font size (20px, Chrome's "Large")
497
+ outgrew its box and was cut off. Scaling the whole drawing instead is no
498
+ way out — WebKit on iOS scales the SVG but not the HTML in a
499
+ `foreignObject`. Browser zoom still enlarges it with everything else. */
429
500
  .diagram-node-content {
430
501
  display: flex;
431
502
  height: 100%;
432
503
  flex-direction: column;
433
504
  align-items: center;
434
505
  justify-content: center;
435
- gap: 0.15rem;
436
- padding: 0.2rem 0.5rem;
506
+ gap: 2.4px;
507
+ padding: 3.2px 8px;
437
508
  color: var(--sl-color-text);
438
509
  font-family: var(--sl-font);
439
- font-size: 0.85rem;
510
+ font-size: 13.6px;
440
511
  line-height: 1.25;
441
512
  text-align: center;
442
513
  }
443
514
 
444
- .diagram-node-formula :global(.katex) {
445
- font-size: 1em;
515
+ /* The box was sized for its label wrapped evenly; a narrower font
516
+ balances it the same way rather than leaving one word alone. */
517
+ .diagram-node-text {
518
+ text-wrap: balance;
519
+ }
520
+
521
+ /* The box is as wide as the formula on one line (`nodeBoxSize`); KaTeX's
522
+ inline math would otherwise break after a `+` or `=` and outgrow it. */
523
+ .diagram-node-formula {
524
+ white-space: nowrap;
446
525
  }
447
526
 
448
527
  .diagram-edge {
@@ -263,6 +263,17 @@ const name = `Figure ${number}`;
263
263
  .keyed-figure__tools-stub {
264
264
  display: none !important;
265
265
  }
266
+
267
+ /* A lab the reader never scrolled to has not drawn yet. The Print button
268
+ and Ctrl+P load it first; the browser's own File → Print cannot wait,
269
+ so the empty box says how to print the figure. */
270
+ .keyed-figure__body:has(astro-island[ssr])::after {
271
+ content: 'Interactive figure not loaded: print with the page’s Print button or Ctrl+P to include it.';
272
+ display: block;
273
+ font-size: 0.85em;
274
+ font-style: italic;
275
+ padding: 0.5rem 0;
276
+ }
266
277
  }
267
278
  </style>
268
279
 
@@ -4,11 +4,11 @@
4
4
  * (docs/plans/figures-and-short-captions.md, decisions 1 and 4). One
5
5
  * anatomy, below the picture, the LaTeX figure convention:
6
6
  *
7
- * Figure 2.2.1 [chain] **‹short›.** ‹caption› ‹live end› [buttons]
7
+ * Figure 2.2.1 **‹short›.** ‹caption› ‹live end› [buttons]
8
8
  *
9
- * The number links to the float, the chain icon copies that link
10
- * (`HeadingAnchorEnhancer.astro` reads `data-copy-link`) and then navigates
11
- * the same way, and the caption is the float's one long caption. A float
9
+ * The number links to the float; hovering or focusing it shows its toolbar,
10
+ * whose Copy link copies that link (`float-toolbar.ts`). The caption is the
11
+ * float's one long caption. A float
12
12
  * with a short caption opens the caption with it as a bold title,
13
13
  * `b.float-caption__title` (decision 9.1), outside `.float-caption__text`, so
14
14
  * a reader of the long caption (a preview card, a fold's bar) still reads
@@ -65,19 +65,6 @@ const tools = Astro.slots.has('default') ? (await Astro.slots.render('default'))
65
65
  </a>
66
66
  )
67
67
  }
68
- <a
69
- class="float-caption__anchor"
70
- href={`#${anchorId}`}
71
- aria-label={`Copy the link to ${name}`}
72
- title="Copy link"
73
- data-copy-link={name}
74
- >
75
- <svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
76
- ><path
77
- d="m12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42"
78
- ></path></svg
79
- >
80
- </a>
81
68
  {short && <b class="float-caption__title">{captionTitle(short)}</b>}{short && ' '}<span
82
69
  class:list={['float-caption__text', `${prefix}__caption-text`]}
83
70
  id={textId}