explico 0.12.0 → 0.13.1

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 +1137 -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 +707 -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 +321 -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
@@ -10,6 +10,7 @@ import {
10
10
  stripNotationMarkers,
11
11
  } from '../../reference/math-glyphs.mjs';
12
12
  import type { GlyphScopeEntry } from '../../reference/gloss';
13
+ import type { DerivedUse, IndexedSymbol } from '../../reference/types';
13
14
 
14
15
  interface HastText {
15
16
  type: 'text';
@@ -284,14 +285,27 @@ function sanitizeNode(node: HastNode): SafeMathNode {
284
285
  throw new Error(`Unexpected KaTeX node: ${node.type}.`);
285
286
  }
286
287
 
288
+ const properties = safeProperties(node);
287
289
  return Object.freeze({
288
290
  type: 'element',
289
291
  tagName: node.tagName,
290
- properties: safeProperties(node),
292
+ // KaTeX writes each formula twice, as MathML (with its LaTeX in an
293
+ // `<annotation>`) for assistive technology and as visible glyphs; the
294
+ // site search indexes the glyphs alone (`withSearchOnce`).
295
+ properties: withSearchOnce(properties),
291
296
  children: Object.freeze(node.children.map(sanitizeNode)),
292
297
  });
293
298
  }
294
299
 
300
+ /** `data-pagefind-ignore` on KaTeX's MathML copy of a formula. */
301
+ function withSearchOnce(
302
+ properties: Readonly<Record<string, string>>,
303
+ ): Readonly<Record<string, string>> {
304
+ return properties.class?.split(' ').includes('katex-mathml')
305
+ ? Object.freeze({ ...properties, 'data-pagefind-ignore': '' })
306
+ : properties;
307
+ }
308
+
295
309
  // Re-exported so existing imports of `stripNotationMarkers` from this module
296
310
  // (the notation popup card's "Copy LaTeX" button) keep working. The
297
311
  // implementation lives in `math-glyphs.mjs` because `remark-notation.mjs`
@@ -388,19 +402,34 @@ export function renderNotationMath(latex: string): readonly SafeMathNode[] {
388
402
  export function renderNotationFormula(
389
403
  formula: string,
390
404
  scope: readonly GlyphScopeEntry[],
405
+ { derive = false }: { readonly derive?: boolean } = {},
391
406
  ): {
392
407
  readonly nodes: readonly SafeMathNode[];
408
+ /** The LaTeX `nodes` were rendered from: `formula` with its glyphs bound, or as written. */
409
+ readonly latex: string;
393
410
  readonly unresolved: readonly string[];
394
411
  readonly degraded: boolean;
412
+ /** Why the formula could not be bound, when `degraded`: the resolver's own message. */
413
+ readonly reason?: string;
414
+ /** With `derive` (page math, rule N23): the derived cards the formula uses. */
415
+ readonly derived: readonly DerivedUse[];
416
+ /** How each symbol with a subscript is explained (rule N23). */
417
+ readonly indices: readonly IndexedSymbol[];
395
418
  } {
396
419
  let bound = formula;
397
420
  let unresolved: string[] = [];
398
421
  let degraded = false;
422
+ let reason: string | undefined;
423
+ let derived: DerivedUse[] = [];
424
+ let indices: IndexedSymbol[] = [];
399
425
  try {
400
- const result = resolveMathGlyphs(formula, [...scope]);
426
+ const result = resolveMathGlyphs(formula, [...scope], { derive });
401
427
  bound = result.latex;
402
428
  unresolved = result.unresolved.map(({ token }: { token: string }) => token);
429
+ derived = result.derived;
430
+ indices = result.indices;
403
431
  } catch (error) {
432
+ reason = error instanceof Error ? error.message : String(error);
404
433
  // N13 (a nonlinear function with no parenthesized argument) is a hard,
405
434
  // no-exceptions rule like N9 — never degrade it to inert math, so both
406
435
  // `Diagram` (which turns `degraded` into a build error anyway, but with
@@ -417,11 +446,28 @@ export function renderNotationFormula(
417
446
  degraded = true;
418
447
  }
419
448
 
449
+ const why = reason === undefined ? {} : { reason };
420
450
  try {
421
- return { nodes: renderNotationMath(bound), unresolved, degraded };
422
- } catch {
451
+ return {
452
+ nodes: renderNotationMath(bound),
453
+ latex: bound,
454
+ unresolved,
455
+ degraded,
456
+ ...why,
457
+ derived,
458
+ indices,
459
+ };
460
+ } catch (error) {
423
461
  // A marker that KaTeX or the sanitizer rejects must not cost the reader
424
462
  // the equation itself.
425
- return { nodes: renderNotationMath(formula), unresolved, degraded: true };
463
+ return {
464
+ nodes: renderNotationMath(formula),
465
+ latex: formula,
466
+ unresolved,
467
+ degraded: true,
468
+ reason: reason ?? (error instanceof Error ? error.message : String(error)),
469
+ derived: [],
470
+ indices: [],
471
+ };
426
472
  }
427
473
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * An element's text as a reader sees it. KaTeX writes each formula three ways
3
+ * — MathML, its LaTeX `<annotation>` and its `aria-hidden` glyphs — so a
4
+ * heading's `textContent` reads `R\explain{motion.range}{R}R`; this reads the
5
+ * formula once, from its MathML, as `rehype-heading-anchors.mjs` does for the
6
+ * heading link's label.
7
+ */
8
+ export function readableText(element: Element): string {
9
+ let text = '';
10
+ const walk = (node: Node): void => {
11
+ if (node.nodeType === Node.TEXT_NODE) {
12
+ text += node.textContent ?? '';
13
+ return;
14
+ }
15
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
16
+ const child = node as Element;
17
+ if (child.localName === 'annotation' || child.classList.contains('katex-html')) return;
18
+ child.childNodes.forEach(walk);
19
+ };
20
+ walk(element);
21
+ return text
22
+ .replace(/[​⁡-⁤]/g, '')
23
+ .replace(/\s+/g, ' ')
24
+ .trim();
25
+ }
@@ -22,7 +22,10 @@
22
22
  * - otherwise a card floats near the reference showing its number,
23
23
  * caption (table/figure), content, and a "Go to …" link, whose arrow
24
24
  * says where it lands (`../card-jump.ts`): ↑ / ↓ on this page, → with the
25
- * lesson's name on another one. A click on the
25
+ * lesson's name on another one. The float's number heads the card's
26
+ * title, "Equation (1.1.3)", "Figure 2.1.1", and the link then says only
27
+ * "Go to the figure". A figure's picture is 80% of its width in the
28
+ * article. A click on the
26
29
  * reference pins the card, a second one closes it (the card manager's
27
30
  * contract, `../card-manager.ts`); the card's own links navigate.
28
31
  *
@@ -60,7 +63,8 @@
60
63
  *
61
64
  * One card per referenced float, several at once; the card manager places
62
65
  * them clear of every other open card, and a symbol inside a card opens its
63
- * notation card as a child of it.
66
+ * notation card as a child of it — in a card from another page, as that
67
+ * page explains it (`markForeign`), even a symbol this page never uses.
64
68
  */
65
69
  import HoverPanel from '../HoverPanel.astro';
66
70
  ---
@@ -68,23 +72,30 @@ import HoverPanel from '../HoverPanel.astro';
68
72
  <HoverPanel id="float-ref-panel" label="Reference preview" data-float-ref-panel>
69
73
  <p class="float-ref-panel-title" slot="heading">
70
74
  <span class="float-ref-panel-kind" data-panel-kind></span>
75
+ <a class="float-ref-panel-number" data-panel-number></a>
71
76
  <span class="float-ref-panel-heading" data-panel-heading hidden></span>
72
77
  </p>
73
78
  <div class="float-ref-panel-body">
74
- <div class="float-ref-panel-main">
75
- <div class="float-ref-panel-content" data-panel-content></div>
76
- <a class="float-ref-panel-number" data-panel-number></a>
77
- </div>
79
+ <div class="float-ref-panel-content" data-panel-content></div>
78
80
  <p class="float-ref-panel-caption" data-panel-caption></p>
79
- <a class="float-ref-panel-jump" data-panel-jump
80
- >Go to <span data-panel-jump-label></span
81
- ><span aria-hidden="true" data-jump-arrow> →</span></a
82
- >
81
+ <p class="float-ref-panel-links">
82
+ <a class="float-ref-panel-jump hover-panel-action" data-panel-jump
83
+ >Go to <span data-panel-jump-label></span
84
+ ><span aria-hidden="true" data-jump-arrow> →</span></a
85
+ >
86
+ </p>
83
87
  </div>
84
88
  </HoverPanel>
85
89
 
86
90
  <script>
87
91
  import { cloneCardShell, isKept, registerCardSource } from '../card-manager';
92
+ import {
93
+ ensureKatexStylesheet,
94
+ fetchDoc,
95
+ isThisPage,
96
+ markForeign,
97
+ pageTitleOf,
98
+ } from '../card-fetch';
88
99
  import { wireJumpArrow } from '../card-jump';
89
100
 
90
101
  interface KindConfig {
@@ -120,6 +131,7 @@ import HoverPanel from '../HoverPanel.astro';
120
131
  // equation, not a hover preview of it — same principle as the table
121
132
  // preview below never offering its live pagination/expand controls.
122
133
  clone.querySelector('.keyed-equation__controls')?.remove();
134
+ clone.querySelector('[data-eq-copy]')?.remove();
123
135
  return clone;
124
136
  }
125
137
 
@@ -234,8 +246,8 @@ import HoverPanel from '../HoverPanel.astro';
234
246
  * decision 12). The body is the picture alone — a lab's tools sit in a tray
235
247
  * outside it. A hydrated island in it (a lab's plot) becomes plain markup,
236
248
  * so it never hydrates a second time inside the card: its plot is copied as
237
- * drawn. A picture with nothing drawn yet — a plot the browser has not
238
- * drawn, or one on a fetched page, where only the server markup exists —
249
+ * drawn, or, before the island has drawn and on a fetched page, the
250
+ * picture the build drew (rule V3). A picture with nothing drawn at all
239
251
  * gives no copy, and the card shows the caption alone.
240
252
  */
241
253
  function cloneFigureBody(target: HTMLElement): HTMLElement | null {
@@ -248,7 +260,21 @@ import HoverPanel from '../HoverPanel.astro';
248
260
  still.append(...island.childNodes);
249
261
  island.replaceWith(still);
250
262
  }
251
- clone.querySelectorAll('script, template, astro-slot').forEach((node) => node.remove());
263
+ clone.querySelectorAll('script, template').forEach((node) => node.remove());
264
+ // What an island was given as children sits in an `<astro-slot>`: a
265
+ // lab's build-time picture, the one a fetched page or a figure not yet
266
+ // drawn has (rule V3). Keep it, without the wrapper, unless the island
267
+ // has already drawn its own picture beside it.
268
+ for (const slot of clone.querySelectorAll('astro-slot, astro-static-slot')) {
269
+ const drawnBeside = Array.from(slot.parentElement?.children ?? []).some(
270
+ (sibling) =>
271
+ sibling !== slot &&
272
+ !sibling.matches('astro-slot, astro-static-slot') &&
273
+ sibling.querySelector('svg, img, picture, canvas, video') !== null,
274
+ );
275
+ if (drawnBeside) slot.remove();
276
+ else slot.replaceWith(...Array.from(slot.childNodes));
277
+ }
252
278
  const drawn = clone.querySelector('svg, img, picture, canvas, video, table');
253
279
  if (islands.length > 0 && !drawn) return null;
254
280
  for (const control of clone.querySelectorAll<HTMLElement>('input, select, textarea, button')) {
@@ -259,6 +285,35 @@ import HoverPanel from '../HoverPanel.astro';
259
285
  return clone;
260
286
  }
261
287
 
288
+ /**
289
+ * A figure's picture in a card is 80% of its width in the article. On this
290
+ * page that width is measured on the figure. A fetched page has no layout,
291
+ * so its copy is laid out unseen in this page's article, where every
292
+ * lesson's figures take the same width, and measured there.
293
+ */
294
+ const FIGURE_CARD_SCALE = 0.8;
295
+ const PICTURE = ':is(svg:not(.katex *), img, canvas, video)';
296
+
297
+ function figureCardWidth(target: HTMLElement, clone: HTMLElement): number | null {
298
+ let width = 0;
299
+ if (target.ownerDocument === document) {
300
+ width =
301
+ target.querySelector(`.keyed-figure__body ${PICTURE}`)?.getBoundingClientRect().width ?? 0;
302
+ }
303
+ if (!(width > 0)) {
304
+ const article = document.querySelector<HTMLElement>('.main-pane .sl-markdown-content');
305
+ if (article) {
306
+ const probe = document.createElement('div');
307
+ probe.style.cssText = 'height:0;overflow:hidden;visibility:hidden';
308
+ probe.append(clone);
309
+ article.append(probe);
310
+ width = clone.querySelector(PICTURE)?.getBoundingClientRect().width ?? 0;
311
+ probe.remove();
312
+ }
313
+ }
314
+ return width > 0 ? Math.round(width * FIGURE_CARD_SCALE) : null;
315
+ }
316
+
262
317
  /** A copy with no ids, so a preview never collides with the page. */
263
318
  function importWithoutIds(node: Node): Node {
264
319
  const clone = document.importNode(node, true);
@@ -359,6 +414,11 @@ import HoverPanel from '../HoverPanel.astro';
359
414
  fillContent(target, contentEl) {
360
415
  const previewable = target.dataset.figPreviewable !== 'false';
361
416
  const clone = previewable ? cloneFigureBody(target) : null;
417
+ const width = clone ? figureCardWidth(target, clone) : null;
418
+ if (clone && width) {
419
+ clone.style.width = `${width}px`;
420
+ clone.style.maxWidth = '100%';
421
+ }
362
422
  contentEl.replaceChildren(...(clone ? [clone] : []));
363
423
  },
364
424
  captionFor(target) {
@@ -469,9 +529,35 @@ import HoverPanel from '../HoverPanel.astro';
469
529
  return node instanceof Element ? node.closest<HTMLElement>(selector) : null;
470
530
  }
471
531
 
532
+ /**
533
+ * A reference's kind, or the kind of a float whose own number this is (the
534
+ * number's toolbar hangs the float's card from it, `float-toolbar.ts`).
535
+ */
472
536
  function kindOf(trigger: HTMLElement): KindConfig | undefined {
473
- return Object.values(KINDS).find((kind) =>
474
- trigger.classList.contains(kind.triggerClass),
537
+ return Object.values(KINDS).find(
538
+ (kind) =>
539
+ trigger.classList.contains(kind.triggerClass) ||
540
+ (kind.numberClass !== '' && trigger.classList.contains(kind.numberClass)),
541
+ );
542
+ }
543
+
544
+ /** The kind an anchor names: by its prefix, else a section's heading. */
545
+ function kindOfId(id: string): KindConfig {
546
+ return (
547
+ Object.values(KINDS).find(
548
+ (kind) => kind.hrefPrefix !== '#' && id.startsWith(kind.hrefPrefix.slice(1)),
549
+ ) ?? KINDS.section
550
+ );
551
+ }
552
+
553
+ /** A float's own number on this page, for `item` (`eq-range`, `fig-…`). */
554
+ function ownNumber(item: string): HTMLElement | null {
555
+ const kind = Object.values(KINDS).find(
556
+ (candidate) => candidate.numberClass !== '' && item.startsWith(candidate.hrefPrefix.slice(1)),
557
+ );
558
+ if (!kind) return null;
559
+ return (
560
+ document.getElementById(item)?.querySelector<HTMLElement>(`a.${kind.numberClass}`) ?? null
475
561
  );
476
562
  }
477
563
 
@@ -494,61 +580,23 @@ import HoverPanel from '../HoverPanel.astro';
494
580
  return { page: href.slice(0, hash), id };
495
581
  }
496
582
 
497
- /**
498
- * Parsed target pages, keyed by path. A cross-page preview fetches the
499
- * referenced lesson once and reuses it for every later hover on this page.
500
- * A failed fetch drops its entry so a later hover can retry.
501
- */
502
- const pageCache = new Map<string, Promise<Document>>();
503
-
504
583
  /** A standalone lesson deliberately has no other course pages to fetch. */
505
584
  const standalone = document.documentElement.hasAttribute(
506
585
  'data-explico-standalone',
507
586
  );
508
587
 
509
- function fetchDoc(page: string): Promise<Document> {
510
- let pending = pageCache.get(page);
511
- if (!pending) {
512
- pending = fetch(page, { credentials: 'same-origin' })
513
- .then((response) => {
514
- if (!response.ok) throw new Error(`${response.status} for ${page}`);
515
- return response.text();
516
- })
517
- .then((html) => new DOMParser().parseFromString(html, 'text/html'))
518
- .catch((error) => {
519
- pageCache.delete(page);
520
- throw error;
521
- });
522
- pageCache.set(page, pending);
523
- }
524
- return pending;
525
- }
526
-
527
- /**
528
- * A cloned equation preview needs KaTeX's stylesheet to render. Every page
529
- * that *authors* math already loads it; this only matters for a page whose
530
- * sole math is a cross-page reference, so copy the `<link>` from the fetched
531
- * document when this one has none.
532
- */
533
- function ensureKatexStylesheet(sourceDoc: Document): void {
534
- const selector = 'link[rel="stylesheet"][href*="katex"]';
535
- if (document.querySelector(selector)) return;
536
- const link = sourceDoc.querySelector<HTMLLinkElement>(selector);
537
- if (link) document.head.appendChild(document.importNode(link, true));
538
- }
539
-
540
588
  const capitalized = (text: string) =>
541
589
  `${text.charAt(0).toUpperCase()}${text.slice(1)}`;
542
590
 
543
- /** The number link's text: `(2.1)` for an equation, else `Table 2.1`, `§ 1.1.2`, …; empty hides it. */
591
+ /** The number link's text: `Equation (2.1)`, `Table 2.1`, `§ 1.1.2`, …; empty hides it. */
544
592
  function numberText(kind: KindConfig, number: string): string {
545
593
  if (kind.numberLabel) return kind.numberLabel(number);
546
594
  return kind.jumpLabel === 'equation'
547
- ? `(${number})`
595
+ ? `Equation (${number})`
548
596
  : `${capitalized(kind.jumpLabel)} ${number}`;
549
597
  }
550
598
 
551
- /** What the card's "Go to …" link names (C15): `equation (1.4)`, `table 2.1`, `the section`. */
599
+ /** What the card previews, for its label (C15): `equation (1.4)`, `table 2.1`, `the section`. */
552
600
  function goToLabel(kind: KindConfig, number: string): string {
553
601
  if (!number) return `the ${kind.jumpLabel}`;
554
602
  return kind.jumpLabel === 'equation'
@@ -584,8 +632,9 @@ import HoverPanel from '../HoverPanel.astro';
584
632
  return targetFor(trigger, kind) || crossPageHref(trigger, kind) ? trigger : null;
585
633
  };
586
634
 
587
- const refs = Array.from(document.querySelectorAll<HTMLElement>(TRIGGER_SELECTOR));
588
- if (!refs.some((ref) => refFrom(ref))) return;
635
+ // Every page registers, with references or none: its floats' numbers
636
+ // pin and dock their own cards (`float-toolbar.ts`), and a card carried
637
+ // in from another page is built here (rule K5).
589
638
  shell.dataset.cardSourceReady = 'true';
590
639
 
591
640
  /**
@@ -680,19 +729,26 @@ import HoverPanel from '../HoverPanel.astro';
680
729
 
681
730
  const number = target.dataset[kind.numberDataKey] ?? '';
682
731
  const href = trigger.getAttribute('href') ?? '';
683
- const goTo = goToLabel(kind, number);
732
+ const named = goToLabel(kind, number);
684
733
  kindEl.textContent = capitalized(kind.jumpLabel);
685
734
  headingEl.textContent = kind.shortDataKey
686
735
  ? (target.dataset[kind.shortDataKey] ?? '')
687
736
  : '';
688
737
  headingEl.hidden = headingEl.textContent.length === 0;
689
- // The number sits to the right of the content and is itself a link to
690
- // the float — for an equation just "(2.1)", else the labelled form.
738
+ // The float is named by its number in the card's title, "Equation
739
+ // (2.1)", "Figure 2.1", in place of its kind, so the link below need
740
+ // not repeat it. The number is itself a link to the float.
691
741
  numberEl.textContent = numberText(kind, number);
692
742
  numberEl.hidden = numberEl.textContent.length === 0;
693
743
  numberEl.href = href;
694
744
  numberEl.title = `Go to the ${kind.jumpLabel}`;
745
+ kindEl.hidden = !numberEl.hidden;
746
+ const goTo = `the ${kind.jumpLabel}`;
695
747
  kind.fillContent(target, contentEl);
748
+ el.toggleAttribute(
749
+ 'data-panel-picture',
750
+ kind.jumpLabel === 'figure' && !!contentEl.querySelector('svg, img, picture, canvas, video'),
751
+ );
696
752
  // A term cloned from another page still carries its no-JS tooltip.
697
753
  for (const term of contentEl.querySelectorAll('a.notation-term[title]')) {
698
754
  term.removeAttribute('title');
@@ -702,7 +758,7 @@ import HoverPanel from '../HoverPanel.astro';
702
758
  jumpLabelEl.textContent = lesson ? `${goTo} in ${lesson}` : goTo;
703
759
  jumpEl.href = href;
704
760
  wireJumpArrow(jumpEl);
705
- el.setAttribute('aria-label', `Preview of ${goTo}`);
761
+ el.setAttribute('aria-label', `Preview of ${named}`);
706
762
  return el;
707
763
  };
708
764
 
@@ -734,9 +790,8 @@ import HoverPanel from '../HoverPanel.astro';
734
790
  const target = sourceDoc.getElementById(crossPage.id);
735
791
  if (!target) return null;
736
792
  ensureKatexStylesheet(sourceDoc);
737
- const lesson = sourceDoc.getElementById('_top')?.textContent?.trim() || undefined;
738
- const el = buildCard(trigger, target, kind, lesson);
739
- el?.setAttribute('data-card-foreign', '');
793
+ const el = buildCard(trigger, target, kind, pageTitleOf(sourceDoc));
794
+ if (el) markForeign(el, sourceDoc, crossPage.page);
740
795
  return el;
741
796
  });
742
797
  };
@@ -761,6 +816,27 @@ import HoverPanel from '../HoverPanel.astro';
761
816
  return !!target && !isFullyVisible(target);
762
817
  },
763
818
  render,
819
+ ownAnchor: ownNumber,
820
+ // K5: `eq-range` names this page's equation; elsewhere it is
821
+ // `/its/page/#eq-range`, the name a cross-page reference gives it.
822
+ awayItem: (item, page) => (item.includes('#') ? item : `${page}#${item}`),
823
+ build: (item, page) => {
824
+ const hash = item.indexOf('#');
825
+ const targetPage = hash === -1 ? page : item.slice(0, hash);
826
+ const id = hash === -1 ? item : item.slice(hash + 1);
827
+ const kind = kindOfId(id);
828
+ return fetchDoc(targetPage).then((sourceDoc) => {
829
+ const target = sourceDoc.getElementById(id);
830
+ if (!target) return null;
831
+ ensureKatexStylesheet(sourceDoc);
832
+ const here = isThisPage(targetPage);
833
+ const link = document.createElement('a');
834
+ link.setAttribute('href', here ? `#${id}` : `${targetPage}#${id}`);
835
+ const el = buildCard(link, target, kind, here ? undefined : pageTitleOf(sourceDoc));
836
+ if (el && !here) markForeign(el, sourceDoc, targetPage);
837
+ return el;
838
+ });
839
+ },
764
840
  onChange: (open) => {
765
841
  for (const [item, bar] of foldMarks) {
766
842
  if (open.has(item)) continue;
@@ -825,23 +901,11 @@ import HoverPanel from '../HoverPanel.astro';
825
901
  gap: 0.6rem;
826
902
  }
827
903
 
828
- .float-ref-panel-main {
829
- display: flex;
830
- min-width: 0;
831
- /* Center, not baseline: a tall equation's own baseline sits wherever
832
- its glyphs put it, which for a fraction or multi-row expression can
833
- be well above center — baseline-aligning the number to that point
834
- would leave it looking pinned near the top of the card, seemingly
835
- above the equation, instead of beside it. */
836
- align-items: center;
837
- justify-content: space-between;
838
- gap: 0.75rem;
839
- }
840
-
904
+ /* A float's number in the title, in place of its kind: "Equation
905
+ (1.1.3)", "Figure 2.1.1". */
841
906
  .float-ref-panel-number {
842
- flex: none;
843
- margin: 0;
844
- color: var(--sl-color-gray-3);
907
+ color: var(--sl-color-gray-2);
908
+ font-size: var(--sl-text-xs);
845
909
  font-weight: 600;
846
910
  font-variant-numeric: tabular-nums;
847
911
  text-decoration: none;
@@ -857,7 +921,6 @@ import HoverPanel from '../HoverPanel.astro';
857
921
  `viewBox` alone, or a table in its scroll clip, has no width of its
858
922
  own to give a card that sizes itself to its content. */
859
923
  .float-ref-panel-content {
860
- flex: 1 1 auto;
861
924
  min-width: 0;
862
925
  overflow-x: auto;
863
926
  /* `overflow-x: auto` alone makes the block axis compute to `auto` too
@@ -877,10 +940,6 @@ import HoverPanel from '../HoverPanel.astro';
877
940
  min-inline-size: min(22rem, 70vw);
878
941
  }
879
942
 
880
- .float-ref-panel-content :global(.katex-display) {
881
- margin-block: 0;
882
- }
883
-
884
943
  .float-ref-panel-content :global(table) {
885
944
  margin: 0;
886
945
  /* 60% of the real table's own font-size (`global.css`'s
@@ -920,6 +979,12 @@ import HoverPanel from '../HoverPanel.astro';
920
979
  font-size: var(--sl-text-xs);
921
980
  }
922
981
 
982
+ /* A figure card is as wide as its picture: the caption wraps under it
983
+ rather than widening the card to fit its own line. */
984
+ :global(.hover-panel[data-panel-picture]) .float-ref-panel-caption {
985
+ contain: inline-size;
986
+ }
987
+
923
988
  .float-ref-panel-heading {
924
989
  font-weight: 600;
925
990
  font-size: var(--sl-text-sm);
@@ -943,9 +1008,17 @@ import HoverPanel from '../HoverPanel.astro';
943
1008
  font-size: var(--sl-text-sm);
944
1009
  }
945
1010
 
946
- .float-ref-panel-jump {
947
- justify-self: start;
948
- font-size: var(--sl-text-xs);
1011
+ /* The card's links in one row: "Go to …", and a kept card's "Opened
1012
+ at …" beside it (rule K7, `card-manager.ts`). */
1013
+ .float-ref-panel-links {
1014
+ margin: 0;
949
1015
  }
950
1016
  }
1017
+
1018
+ /* Outside the layer: `katex.css` is unlayered, and its
1019
+ `.katex-display { margin: 1em 0 }` beats any layered rule, so inside
1020
+ `@layer` this left the equation 1em of air above and below. */
1021
+ .float-ref-panel-content :global(.katex-display) {
1022
+ margin-block: 0;
1023
+ }
951
1024
  </style>
@@ -23,11 +23,12 @@
23
23
  * still navigates, so the hash and the highlight below follow as before;
24
24
  * a clipboard that is missing or refuses leaves exactly that navigation.
25
25
  * On hover the icon says "Copy link", a title this script sets, since
26
- * only with it does a click copy. A float's chain icon in its caption
27
- * line (`a[data-copy-link]`, `FloatCaption.astro`) copies the same way,
28
- * named by its number and short caption — "Link copied · Figure 2.2.1 ·
29
- * Projectile trajectory explorer", pasted as "Figure 2.2.1, Projectile
30
- * trajectory explorer"; its landing is `KeyedFloatEnhancer`'s;
26
+ * only with it does a click copy. A float's Copy link, in the toolbar
27
+ * its number shows (`a[data-copy-link]`, `float-toolbar.ts`), copies the
28
+ * same way, named by its number and short caption — "Link copied ·
29
+ * Figure 2.2.1 · Projectile trajectory explorer", pasted as "Figure
30
+ * 2.2.1, Projectile trajectory explorer"; its landing is
31
+ * `KeyedFloatEnhancer`'s;
31
32
  * - a smooth, header-aware scroll on an in-page jump;
32
33
  * - a brief highlight of the section body (the heading's following siblings up
33
34
  * to the next heading of equal or higher rank), so a deep link lands the
@@ -57,6 +58,7 @@
57
58
  import { HEADINGS, sectionParts } from './section-parts';
58
59
  import { animatedScrollIntoView } from '../scroll-animation';
59
60
  import { floatOf } from '../float-name';
61
+ import { readableText } from '../readable-text';
60
62
 
61
63
  (() => {
62
64
  const root = document.querySelector('.sl-markdown-content');
@@ -161,15 +163,18 @@
161
163
  const titled = heading.querySelector('.block-heading__title') ?? heading;
162
164
  return {
163
165
  label,
164
- title: short || (titled.textContent?.replace(/\s+/g, ' ').trim() ?? ''),
166
+ title: short || readableText(titled),
165
167
  };
166
168
  };
167
169
 
170
+ // A float's copy link (its number's toolbar, `float-toolbar.ts`): named
171
+ // by its number, then its short caption, an equation's `\caption{}` too.
168
172
  const floatName = (link: HTMLElement): LinkName => {
169
173
  const root = link.closest('figure');
174
+ const equation = link.closest<HTMLElement>('.keyed-equation');
170
175
  return {
171
176
  label: link.dataset.copyLink ?? '',
172
- title: (root && floatOf(root)?.short) || '',
177
+ title: (root && floatOf(root)?.short) || equation?.dataset.eqShort || '',
173
178
  };
174
179
  };
175
180
 
@@ -258,7 +263,7 @@
258
263
  root.addEventListener('click', (event) => {
259
264
  const origin = event.target instanceof Element ? event.target : null;
260
265
 
261
- // A float's chain icon: copy, confirm, and let the click navigate.
266
+ // A float's copy link: copy, confirm, and let the click navigate.
262
267
  const floatLink = origin?.closest<HTMLElement>('a[data-copy-link]');
263
268
  const floatHref = floatLink?.getAttribute('href') || '';
264
269
  if (floatLink && floatHref.startsWith('#')) {