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
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Carrying the kept cards (rules K1–K8). With "Carry cards" on, a card the
3
+ * reader keeps — pinned, docked or moved — stays on screen as the page
4
+ * scrolls, and comes along to every page of the course.
5
+ *
6
+ * K1. Carrying is the reader's switch, "Carry cards", remembered across pages
7
+ * and visits. It is off until the reader turns it on, unless the course turns
8
+ * it on by default (`cards.carry` in the course settings, D8).
9
+ *
10
+ * K2–K4 hold a card at the window's edges: `heldAt` in
11
+ * `./card-placement.ts`, which the card manager (`./card-manager.ts`)
12
+ * applies.
13
+ *
14
+ * K5. Wherever the reader goes in the course — a link, the sidebar, search,
15
+ * Next and Previous, Back and Forward, a reload, a typed address — the kept
16
+ * cards come along. On the page a card was opened on (or last attached to,
17
+ * C8) it attaches to its trigger again, the same occurrence, at its place;
18
+ * on any other page it is rebuilt from that page, fetched
19
+ * (`./card-fetch.ts`, `CardSource.build`), and held at the edge it was at
20
+ * when the reader left, at the same place across the window. A figure's plot
21
+ * comes as it was drawn. A card's journey ends when it is closed; turning
22
+ * carrying off ends the journeys of the cards from other pages. Each tab
23
+ * keeps its own set (session storage), and every change also writes it as
24
+ * the course's last set (local storage): a tab with no set of its own — a new
25
+ * tab, a browser restarted without its tabs — takes the last set (D9). Card
26
+ * memory per page (`./card-memory.ts`) rests while carrying is on.
27
+ *
28
+ * K6 (D4). A symbol of a page's own (`notation.local`) travels under a key
29
+ * scoped to its page, `theta-star@projectiles/optimal-launch-angle`, so it
30
+ * never stands for another page's symbol of the same name. On any other
31
+ * page its card opens with "Defined in ‹page title› · means this only
32
+ * there", and its arrow to a symbol there whose entry has the same LaTeX is
33
+ * dashed and grey: the same letters, defined elsewhere. A shared symbol's
34
+ * arrow stays solid. A symbol inside a card cloned from another page opens
35
+ * this page's card only when both pages share its meaning
36
+ * (`NotationLayer.astro`).
37
+ *
38
+ * K7 (D15). While carrying is on, every kept card links back to where it
39
+ * was opened, beside its "Go to …": "Opened at § 2.1.1", or "Opened at
40
+ * (2.1.3)" when it was opened from inside a numbered equation. With
41
+ * carrying off a kept card stays by its trigger, so it has no such link,
42
+ * but where it was opened is still recorded, for a journey started later.
43
+ * Its arrow says where that is, as every
44
+ * card link's does (`./card-jump.ts`), and its tooltip says it in full:
45
+ * "Back to where you opened this card: Projectile Range, § 2.1.1 From launch
46
+ * to landing". The link is the section's (or the equation's) anchor, so it
47
+ * works in a new tab and without JavaScript; with it, the page scrolls to
48
+ * the trigger itself and flashes it, after loading when the card was opened
49
+ * on another page. A card opened from inside another goes back to where the
50
+ * outermost one was opened. Where a card was opened travels with it.
51
+ *
52
+ * K8. While carrying is on, a kept card shows, at the left of its header's
53
+ * buttons, whether its meaning holds on every page — a globe: shared
54
+ * notation, citations, abbreviations, numbered references — or only on its
55
+ * own page: a page and "this page", for one of that page's own symbols.
56
+ * Never the pin's icon. Its tooltip says which (`syncMark`).
57
+ *
58
+ * The stored set is plain JSON checked field by field on the way back
59
+ * (`decodeCarried`), like card memory's, so `tests/unit/card-carry.test.ts`
60
+ * checks it in Node.
61
+ */
62
+ import {
63
+ createBrowserPreferenceStore,
64
+ createSessionPreferenceStore,
65
+ } from '../session/PreferenceStore';
66
+ import { decodeCard, keyOf, type RememberedCard } from './card-memory';
67
+ import type { HoldEdge } from './card-placement';
68
+
69
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
70
+ typeof value === 'object' && value !== null && !Array.isArray(value);
71
+
72
+ /** K1, remembered across pages and visits, for every course on this site. */
73
+ const CARRY_KEY = 'explico:cards:carry';
74
+
75
+ // The one seam for per-viewer UI preferences; this module never touches
76
+ // browser storage itself.
77
+ const preferences = createBrowserPreferenceStore();
78
+
79
+ /** K1: whether the reader carries cards: their own choice, else the course's default. */
80
+ export function readCarry(courseDefault: boolean): boolean {
81
+ const stored = preferences.get(CARRY_KEY);
82
+ if (stored === 'true') return true;
83
+ if (stored === 'false') return false;
84
+ return courseDefault;
85
+ }
86
+
87
+ export const writeCarry = (on: boolean): void => preferences.set(CARRY_KEY, String(on));
88
+
89
+ /**
90
+ * What the course says about its cards, from the element `LessonFooter.astro`
91
+ * renders on every page: whether carrying starts on (K1), and its base path.
92
+ */
93
+ export interface CardConfig {
94
+ readonly carry: boolean;
95
+ /** The course's base path, which keeps the carried sets of courses on one origin apart. */
96
+ readonly base: string;
97
+ }
98
+
99
+ export function readCardConfig(): CardConfig {
100
+ const config = document.querySelector<HTMLElement>('[data-explico-card-config]');
101
+ return { carry: config?.dataset.carry === 'true', base: config?.dataset.base || '/' };
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // K5: the carried set, pure.
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /** K7: where a card was opened: the page, the place on it, and the trigger there. */
109
+ export interface OpenedAt {
110
+ /** The page's path. */
111
+ readonly page: string;
112
+ /** The page's title, for the link's tooltip. */
113
+ readonly title: string;
114
+ /** The anchor the link goes to: the section's heading or the equation; `_top` before any. */
115
+ readonly anchor: string;
116
+ /**
117
+ * The link's short label: `§ 2.1.1`, `(2.1.3)`, `Example 1.2.1`, the
118
+ * section's title when it has no number, `the start` before the first
119
+ * section, `the notation list`.
120
+ */
121
+ readonly label: string;
122
+ /** The section's or the equation's own title, for the tooltip; may be empty. */
123
+ readonly name: string;
124
+ /** The trigger itself: its layer, its item, which occurrence on the page. */
125
+ readonly layer: string;
126
+ readonly item: string;
127
+ readonly occurrence: number;
128
+ }
129
+
130
+ /** K7: an opened-at record checked field by field; `null` for anything malformed. */
131
+ export function decodeOpenedAt(value: unknown): OpenedAt | null {
132
+ if (!isRecord(value)) return null;
133
+ const { page, title, anchor, label, name, layer, item, occurrence } = value;
134
+ const strings = [title, anchor, label, name, layer, item];
135
+ if (typeof page !== 'string' || !page.startsWith('/')) return null;
136
+ if (!strings.every((field) => typeof field === 'string')) return null;
137
+ if (anchor === '' || label === '' || layer === '' || item === '') return null;
138
+ if (typeof occurrence !== 'number' || !Number.isInteger(occurrence) || occurrence < 0) return null;
139
+ return {
140
+ page,
141
+ title: title as string,
142
+ anchor: anchor as string,
143
+ label: label as string,
144
+ name: name as string,
145
+ layer: layer as string,
146
+ item: item as string,
147
+ occurrence,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * How "Opened at" names a section heading (K7): by its number — "§ 2.1.1",
153
+ * "Knowledge check 2.1.1", "Example 2.1.1" — with its title as the name, or,
154
+ * where the course shows sections unnumbered, by its title in quotes, the way
155
+ * a reference to it prints (`remark-notation.mjs`).
156
+ */
157
+ export function sectionPlace(
158
+ numbers: { readonly secNumber?: string; readonly checkNumber?: string; readonly exNumber?: string },
159
+ title: string,
160
+ ): { readonly label: string; readonly name: string } {
161
+ const { secNumber, checkNumber, exNumber } = numbers;
162
+ const label = secNumber
163
+ ? `§\u00a0${secNumber}`
164
+ : checkNumber
165
+ ? `Knowledge check\u00a0${checkNumber}`
166
+ : exNumber
167
+ ? `Example\u00a0${exNumber}`
168
+ : '';
169
+ return label ? { label, name: title } : { label: `\u201c${title}\u201d`, name: '' };
170
+ }
171
+
172
+ /**
173
+ * The short label and the tooltip of a card's "Opened at" link (K7): "Opened
174
+ * at § 2.1.1", and the page and the section's title in full. Before a page's
175
+ * first section, its anchor is the page's own heading (`_top`): "Opened at
176
+ * the start".
177
+ */
178
+ export function openedAtText(at: OpenedAt): { readonly label: string; readonly title: string } {
179
+ const where =
180
+ at.anchor === '_top'
181
+ ? `the start of ${at.title}`
182
+ : `${at.title}, ${at.label}${at.name ? ` ${at.name}` : ''}`;
183
+ return { label: `Opened at ${at.label}`, title: `Back to where you opened this card: ${where}` };
184
+ }
185
+
186
+ /** A carried card: a remembered card (`./card-memory.ts`) that knows its page. */
187
+ export interface CarriedCard extends RememberedCard {
188
+ /**
189
+ * The path of the page its trigger is on: where it was opened, or the page
190
+ * it was attached to last. Its occurrence counts there, and another page
191
+ * rebuilds it from there.
192
+ */
193
+ readonly page: string;
194
+ /** K2: the edge it is held at on another page. */
195
+ readonly edge: HoldEdge;
196
+ /** Its left edge against the article column's, where it goes across the window on another page (px). */
197
+ readonly dx: number;
198
+ /** A figure card's plot as it was drawn: an `<svg>`'s markup. */
199
+ readonly picture?: string;
200
+ /** K7: where it was opened. */
201
+ readonly openedAt?: OpenedAt;
202
+ }
203
+
204
+ export interface CarriedSet {
205
+ /** In the order they were first opened (C10, K3). */
206
+ readonly cards: readonly CarriedCard[];
207
+ /** S2: the docked card pointed at last, by its stored key. */
208
+ readonly pointed: string | null;
209
+ }
210
+
211
+ /** Bumped whenever the stored shape changes; a set of another version is dropped. */
212
+ const VERSION = 1;
213
+
214
+ export const EMPTY_SET: CarriedSet = { cards: [], pointed: null };
215
+
216
+ /** A carried card's name in the set: its page and its key there. */
217
+ export const carriedKeyOf = (card: Pick<CarriedCard, 'page' | 'layer' | 'item'>): string =>
218
+ `${card.page} ${keyOf(card)}`;
219
+
220
+ export function encodeCarried(set: CarriedSet): string {
221
+ return JSON.stringify({ v: VERSION, cards: set.cards, pointed: set.pointed });
222
+ }
223
+
224
+ /** A picture longer than this is not carried: the card shows the page's own. */
225
+ export const PICTURE_LIMIT = 200_000;
226
+
227
+ function decodeCarriedCard(value: unknown): CarriedCard | null {
228
+ const card = decodeCard(value);
229
+ if (!card || !isRecord(value)) return null;
230
+ const { page, edge, dx, picture } = value;
231
+ const openedAt = decodeOpenedAt(value.openedAt);
232
+ if (typeof page !== 'string' || !page.startsWith('/')) return null;
233
+ if (edge !== 'top' && edge !== 'bottom') return null;
234
+ if (typeof dx !== 'number' || !Number.isFinite(dx)) return null;
235
+ return {
236
+ ...card,
237
+ page,
238
+ edge,
239
+ dx,
240
+ ...(typeof picture === 'string' && picture.length > 0 && picture.length <= PICTURE_LIMIT
241
+ ? { picture }
242
+ : {}),
243
+ ...(openedAt ? { openedAt } : {}),
244
+ };
245
+ }
246
+
247
+ /**
248
+ * The set a stored value holds, or `null` when it holds none: no value, other
249
+ * JSON, another version. An empty set is a set: a tab whose reader closed
250
+ * every card keeps it. Malformed cards are dropped one by one, and so is a
251
+ * card whose parent is not carried before it, on its page.
252
+ */
253
+ export function decodeCarried(text: string | null): CarriedSet | null {
254
+ if (!text) return null;
255
+ let value: unknown;
256
+ try {
257
+ value = JSON.parse(text);
258
+ } catch {
259
+ return null;
260
+ }
261
+ if (!isRecord(value) || value.v !== VERSION || !Array.isArray(value.cards)) return null;
262
+ const cards: CarriedCard[] = [];
263
+ const seen = new Set<string>();
264
+ for (const entry of value.cards) {
265
+ const card = decodeCarriedCard(entry);
266
+ if (!card || seen.has(carriedKeyOf(card))) continue;
267
+ if (card.parentKey !== null && !seen.has(`${card.page} ${card.parentKey}`)) continue;
268
+ seen.add(carriedKeyOf(card));
269
+ cards.push(card);
270
+ }
271
+ const pointed =
272
+ typeof value.pointed === 'string' && cards.some((card) => keyOf(card) === value.pointed)
273
+ ? value.pointed
274
+ : null;
275
+ return { cards, pointed };
276
+ }
277
+
278
+ // ---------------------------------------------------------------------------
279
+ // The DOM layer.
280
+ // ---------------------------------------------------------------------------
281
+
282
+ // This tab's own set; the course's last set is in `preferences` (local).
283
+ const tab = createSessionPreferenceStore();
284
+
285
+ /** K7: an "Opened at" link followed to another page, which scrolls to the trigger there once it loads. */
286
+ const JUMP_KEY = 'explico:cards:opened-at';
287
+
288
+ export const stashOpenedAt = (at: OpenedAt): void => tab.set(JUMP_KEY, JSON.stringify(at));
289
+
290
+ /** K7: the trigger an "Opened at" link brought the reader to this page for, once. */
291
+ export function takeOpenedAt(): OpenedAt | null {
292
+ const text = tab.get(JUMP_KEY);
293
+ if (text === null) return null;
294
+ if (tab.remove) tab.remove(JUMP_KEY);
295
+ else tab.set(JUMP_KEY, '');
296
+ try {
297
+ return decodeOpenedAt(JSON.parse(text));
298
+ } catch {
299
+ return null;
300
+ }
301
+ }
302
+
303
+ /** One set per course: the base keeps courses on one origin apart. */
304
+ const setKey = (base: string) => `explico:cards:carried:${base}`;
305
+
306
+ /**
307
+ * K5, D9: this tab's own set; a tab with none takes the course's last set,
308
+ * which is its own from then on.
309
+ */
310
+ export function readCarried(base: string): CarriedSet {
311
+ const own = decodeCarried(tab.get(setKey(base)));
312
+ if (own) return own;
313
+ const last = decodeCarried(preferences.get(setKey(base))) ?? EMPTY_SET;
314
+ tab.set(setKey(base), encodeCarried(last));
315
+ return last;
316
+ }
317
+
318
+ /** K5: this tab's set, written as the course's last set too. Returns what was stored. */
319
+ export function writeCarried(base: string, set: CarriedSet): string {
320
+ const text = encodeCarried(set);
321
+ tab.set(setKey(base), text);
322
+ preferences.set(setKey(base), text);
323
+ return text;
324
+ }
325
+
326
+ /** K5: with carrying off, nothing is carried, in this tab or the next. */
327
+ export function forgetCarried(base: string): void {
328
+ for (const store of [tab, preferences]) {
329
+ if (store.remove) store.remove(setKey(base));
330
+ else store.set(setKey(base), '');
331
+ }
332
+ }
333
+
334
+ const SVG_NS = 'http://www.w3.org/2000/svg';
335
+
336
+ /** A card's picture: its first drawing that is not math. */
337
+ function pictureIn(card: HTMLElement): SVGSVGElement | null {
338
+ for (const svg of card.querySelectorAll<SVGSVGElement>('[data-panel-content] svg')) {
339
+ if (!svg.closest('.katex') && !svg.parentElement?.closest('svg')) return svg;
340
+ }
341
+ return null;
342
+ }
343
+
344
+ /** K5: a figure card's plot as it is drawn now, to carry; `undefined` for any other card. */
345
+ export function capturePicture(card: HTMLElement): string | undefined {
346
+ if (!card.hasAttribute('data-panel-picture')) return undefined;
347
+ const markup = pictureIn(card)?.outerHTML;
348
+ return markup && markup.length <= PICTURE_LIMIT ? markup : undefined;
349
+ }
350
+
351
+ let pictureSerial = 0;
352
+
353
+ /**
354
+ * K5: put a carried plot back in a card rebuilt from its page, in place of
355
+ * the picture the page's build drew. Only an inert `<svg>` comes back: no
356
+ * script, no foreign content, no handler, no link off the drawing, and ids of
357
+ * its own, so it never collides with this page's.
358
+ */
359
+ export function restorePicture(card: HTMLElement, markup: string): void {
360
+ // Parsed as HTML, as `outerHTML` wrote it: an `<svg>` there has no
361
+ // `xmlns`, and the HTML parser puts it in the SVG namespace. The parsed
362
+ // document is inert: nothing in it runs.
363
+ const parsed = new DOMParser().parseFromString(markup, 'text/html');
364
+ const svg = parsed.body.firstElementChild;
365
+ if (!svg || svg.namespaceURI !== SVG_NS || svg.localName !== 'svg') return;
366
+ if (parsed.body.childElementCount !== 1) return;
367
+ const banned = new Set(['script', 'foreignObject', 'iframe', 'object', 'embed', 'animate', 'set']);
368
+ const prefix = `carried-${++pictureSerial}-`;
369
+ const ids = new Map<string, string>();
370
+ for (const el of [svg, ...Array.from(svg.querySelectorAll('*'))]) {
371
+ if (banned.has(el.localName)) {
372
+ el.remove();
373
+ continue;
374
+ }
375
+ for (const attribute of Array.from(el.attributes)) {
376
+ const name = attribute.name.toLowerCase();
377
+ const isLink = name === 'href' || name === 'xlink:href';
378
+ if (name.startsWith('on') || (isLink && !attribute.value.startsWith('#'))) {
379
+ el.removeAttribute(attribute.name);
380
+ }
381
+ }
382
+ if (el.id) {
383
+ ids.set(el.id, `${prefix}${el.id}`);
384
+ el.id = `${prefix}${el.id}`;
385
+ }
386
+ }
387
+ if (ids.size > 0) {
388
+ for (const el of [svg, ...Array.from(svg.querySelectorAll('*'))]) {
389
+ for (const attribute of Array.from(el.attributes)) {
390
+ const value = attribute.value.replace(
391
+ /url\(#([^)]+)\)|^#(.+)$/g,
392
+ (whole, inUrl?: string, bare?: string) => {
393
+ const id = ids.get(inUrl ?? bare ?? '');
394
+ if (!id) return whole;
395
+ return inUrl ? `url(#${id})` : `#${id}`;
396
+ },
397
+ );
398
+ if (value !== attribute.value) el.setAttribute(attribute.name, value);
399
+ }
400
+ }
401
+ }
402
+ const node = document.importNode(svg, true);
403
+ const target = pictureIn(card);
404
+ if (target) target.replaceWith(node);
405
+ else card.querySelector('[data-panel-content]')?.append(node);
406
+ }
@@ -21,7 +21,7 @@
21
21
  * where they go once dropped.
22
22
  */
23
23
  import { moveTo } from './card-placement';
24
- import { headerBottom, railLeftEdge, remToPx } from './card-side';
24
+ import { headerBottom, sidebarsBetween } from './card-side';
25
25
  import type { Box, Point } from './card-travel';
26
26
 
27
27
  const clamp = (value: number, low: number, high: number) =>
@@ -207,35 +207,6 @@ export const movedAt = (spot: MovedSpot, columnLeft: number, lineTop: number): P
207
207
  // The DOM layer.
208
208
  // ---------------------------------------------------------------------------
209
209
 
210
- /**
211
- * The navigation's right edge: collapsed, its edge strip's, since the panel
212
- * itself widens over the article while it peeks. `null` without a visible
213
- * navigation (a phone, a page without one).
214
- */
215
- function navigationRightEdge(): number | null {
216
- const pane = document.querySelector<HTMLElement>('.sidebar-pane');
217
- if (!pane || pane.getClientRects().length === 0) return null;
218
- if (getComputedStyle(pane).visibility === 'hidden') return null;
219
- const rect = pane.getBoundingClientRect();
220
- if (rect.width === 0) return null;
221
- const root = document.documentElement;
222
- if (root.hasAttribute('data-navigation-hidden')) {
223
- const strip = getComputedStyle(root).getPropertyValue('--layout-edge-rail-width').trim();
224
- const width = strip ? remToPx(strip) : Number.NaN;
225
- if (Number.isFinite(width) && width > 0) return rect.left + width;
226
- }
227
- return rect.right;
228
- }
229
-
230
- /** D2: the room between the navigation and the rail, each `edge` px away, in viewport x. */
231
- export function sidebarsBetween(edge: number): Between {
232
- const nav = navigationRightEdge();
233
- return {
234
- left: (nav ?? 0) + edge,
235
- right: railLeftEdge() - edge,
236
- };
237
- }
238
-
239
210
  /** D1, D2: where a dragged card may go: between the sidebars, below the header, inside the window. */
240
211
  export function dragBounds(edge: number): DragBounds {
241
212
  return {
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Another page of the course, fetched and parsed once, for a card built from
3
+ * it: a cross-page reference's preview (`reference/FloatRefLayer.astro`), and
4
+ * every carried card rebuilt on a page other than the one it was opened on
5
+ * (rule K5, `./card-carry.ts`). Every card layer builds from the same cache.
6
+ */
7
+
8
+ /**
9
+ * Parsed pages, keyed by path. A page is fetched once and reused for every
10
+ * later card on this page; a failed fetch drops its entry so a later card can
11
+ * retry.
12
+ */
13
+ const pageCache = new Map<string, Promise<Document>>();
14
+
15
+ /** A path as a page, whatever its trailing slash or `index.html`. */
16
+ export const pagePath = (path: string): string =>
17
+ path.replace(/\/index\.html$/, '/').replace(/\/+$/, '') || '/';
18
+
19
+ /** Whether `page` is the page the reader is on. */
20
+ export const isThisPage = (page: string): boolean =>
21
+ pagePath(page) === pagePath(window.location.pathname);
22
+
23
+ /** `page`, parsed; this page is its own document and is never fetched. */
24
+ export function fetchDoc(page: string): Promise<Document> {
25
+ if (isThisPage(page)) return Promise.resolve(document);
26
+ let pending = pageCache.get(page);
27
+ if (!pending) {
28
+ pending = fetch(page, { credentials: 'same-origin' })
29
+ .then((response) => {
30
+ if (!response.ok) throw new Error(`${response.status} for ${page}`);
31
+ return response.text();
32
+ })
33
+ .then((html) => new DOMParser().parseFromString(html, 'text/html'))
34
+ .catch((error: unknown) => {
35
+ pageCache.delete(page);
36
+ throw error;
37
+ });
38
+ pageCache.set(page, pending);
39
+ }
40
+ return pending;
41
+ }
42
+
43
+ /**
44
+ * A card cloned from another page may hold math, which needs KaTeX's
45
+ * stylesheet. Every page that authors math already loads it; this only
46
+ * matters for a page without math of its own, so copy the `<link>` from the
47
+ * fetched document when this one has none.
48
+ */
49
+ export function ensureKatexStylesheet(sourceDoc: Document): void {
50
+ const selector = 'link[rel="stylesheet"][href*="katex"]';
51
+ if (document.querySelector(selector)) return;
52
+ const link = sourceDoc.querySelector<HTMLLinkElement>(selector);
53
+ if (link) document.head.appendChild(document.importNode(link, true));
54
+ }
55
+
56
+ /** A page's title, as its own heading shows it. */
57
+ export const pageTitleOf = (doc: Document): string | undefined =>
58
+ doc.getElementById('_top')?.textContent?.replace(/\s+/g, ' ').trim() || undefined;
59
+
60
+ /** A link read off `page`, usable from any page: a fragment gets the page's path. */
61
+ export const hrefFrom = (href: string, page: string): string =>
62
+ href.startsWith('#') ? `${page}${href}` : href;
63
+
64
+ /** The route slug a page's own symbols are scoped by (`NotationLayer.astro`), or `''`. */
65
+ export const notationPageOf = (doc: Document): string =>
66
+ [...doc.querySelectorAll<HTMLElement>('[data-notation-page]')]
67
+ .map((layer) => layer.dataset.notationPage ?? '')
68
+ .find(Boolean) ?? '';
69
+
70
+ /**
71
+ * K6: `el`, a card built from `doc`, the page at `page`, is that page's, and
72
+ * a symbol in it means what it means there. Says so on the card
73
+ * (`data-card-foreign`), with the page it came from (`data-card-from`) and
74
+ * that page's notation slug (`data-card-from-slug`), and marks each symbol
75
+ * in it with its scope there (`data-notation-scope`).
76
+ */
77
+ export function markForeign(el: HTMLElement, doc: Document, page: string): void {
78
+ el.setAttribute('data-card-foreign', '');
79
+ el.dataset.cardFrom = page;
80
+ el.dataset.cardFromSlug = notationPageOf(doc);
81
+ for (const symbol of el.querySelectorAll<HTMLElement>('[data-notation-key]')) {
82
+ const there = doc.querySelector<HTMLElement>(
83
+ `[data-notation-entry="${CSS.escape(symbol.dataset.notationKey ?? '')}"]`,
84
+ );
85
+ symbol.dataset.notationScope = there?.dataset.notationScope ?? 'shared';
86
+ }
87
+ }