explico 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/CHANGELOG.md +1292 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +11 -4
  4. package/dist/compiler/astro.js +48 -15
  5. package/dist/compiler/cli.d.ts +27 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +33 -2
  8. package/dist/compiler/collections.js +91 -19
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +15 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +115 -8
  51. package/dist/compiler/manifest.js +221 -39
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +3 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +8 -2
  61. package/dist/reference/base-operators.mjs +8 -10
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figure-symbols.d.ts +32 -0
  88. package/dist/reference/figure-symbols.js +55 -0
  89. package/dist/reference/figures-validate.d.ts +1 -1
  90. package/dist/reference/gate-abbreviations.d.ts +1 -1
  91. package/dist/reference/gate-abbreviations.js +1 -1
  92. package/dist/reference/gate-course-rules.d.ts +4 -3
  93. package/dist/reference/gate-course-rules.js +9 -4
  94. package/dist/reference/gate-heading-length.d.ts +2 -0
  95. package/dist/reference/gate-heading-length.js +7 -5
  96. package/dist/reference/gate-heading-structure.js +26 -0
  97. package/dist/reference/gate-indices.d.ts +35 -0
  98. package/dist/reference/gate-indices.js +299 -0
  99. package/dist/reference/gate-math.d.ts +32 -2
  100. package/dist/reference/gate-math.js +68 -12
  101. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  102. package/dist/reference/gate-mdx-expression.js +29 -0
  103. package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
  104. package/dist/reference/gate-non-chapter-pages.js +156 -0
  105. package/dist/reference/gate-part-pages.d.ts +4 -2
  106. package/dist/reference/gate-part-pages.js +2 -1
  107. package/dist/reference/gate-prop-text.d.ts +28 -0
  108. package/dist/reference/gate-prop-text.js +106 -0
  109. package/dist/reference/gate-prose-notation.d.ts +3 -1
  110. package/dist/reference/gate-prose-notation.js +4 -2
  111. package/dist/reference/gate-retired-keys.d.ts +35 -0
  112. package/dist/reference/gate-retired-keys.js +58 -0
  113. package/dist/reference/gloss.d.ts +2 -0
  114. package/dist/reference/katex-measure.d.ts +17 -0
  115. package/dist/reference/katex-measure.js +223 -0
  116. package/dist/reference/keyed-float-validate.js +12 -0
  117. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  118. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  119. package/dist/reference/lab-katex-validate.d.ts +43 -0
  120. package/dist/reference/lab-katex-validate.js +96 -0
  121. package/dist/reference/lab-math-scope.d.ts +1 -0
  122. package/dist/reference/lab-math-scope.js +13 -2
  123. package/dist/reference/lint-ignore.js +1 -1
  124. package/dist/reference/math-glyphs.d.mts +94 -2
  125. package/dist/reference/math-glyphs.mjs +851 -46
  126. package/dist/reference/mixed-content.d.ts +8 -3
  127. package/dist/reference/mixed-content.js +4 -2
  128. package/dist/reference/page-notation.d.ts +39 -7
  129. package/dist/reference/page-notation.js +62 -10
  130. package/dist/reference/raw-floats.mjs +37 -13
  131. package/dist/reference/references.d.ts +11 -1
  132. package/dist/reference/references.js +6 -0
  133. package/dist/reference/registry.js +115 -11
  134. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  135. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  136. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  137. package/dist/reference/remark-citation.mjs +0 -0
  138. package/dist/reference/remark-notation.mjs +275 -23
  139. package/dist/reference/render-lab-math.d.ts +13 -2
  140. package/dist/reference/render-lab-math.js +18 -6
  141. package/dist/reference/sections-validate.d.ts +28 -3
  142. package/dist/reference/sections-validate.js +31 -4
  143. package/dist/reference/sections.d.mts +24 -6
  144. package/dist/reference/sections.mjs +92 -17
  145. package/dist/reference/tables-validate.d.ts +1 -1
  146. package/dist/reference/tables-validate.js +24 -1
  147. package/dist/reference/text-measure.d.ts +10 -0
  148. package/dist/reference/text-measure.js +41 -0
  149. package/dist/reference/text-widths.d.ts +10 -0
  150. package/dist/reference/text-widths.js +1238 -0
  151. package/dist/reference/types.d.ts +64 -8
  152. package/dist/rules/catalog.js +223 -32
  153. package/dist/rules/in-effect.d.ts +1 -1
  154. package/dist/rules/rules.schema.json +100 -27
  155. package/dist/rules/settings.d.ts +1 -1
  156. package/dist/rules/settings.js +7 -0
  157. package/dist/session/PreferenceStore.d.ts +18 -1
  158. package/dist/session/PreferenceStore.js +29 -1
  159. package/docs/authoring/README.md +33 -21
  160. package/docs/authoring/agents.md +38 -7
  161. package/docs/authoring/components.md +115 -10
  162. package/docs/authoring/migrating.md +878 -0
  163. package/docs/authoring/notation.md +285 -30
  164. package/docs/authoring/retired.md +1 -0
  165. package/docs/authoring/structure-rules.md +84 -45
  166. package/docs/authoring/structure.md +67 -2
  167. package/docs/authoring/visualizations.md +167 -25
  168. package/package.json +12 -7
  169. package/src/compiler/astro.ts +72 -21
  170. package/src/compiler/cli.ts +143 -35
  171. package/src/compiler/collections.ts +158 -27
  172. package/src/compiler/content-schema.ts +108 -0
  173. package/src/compiler/content-schemas.ts +391 -0
  174. package/src/compiler/course-config.ts +86 -15
  175. package/src/compiler/exports/combined-html.ts +669 -0
  176. package/src/compiler/exports/download-index.ts +36 -0
  177. package/src/compiler/exports/export-chapters.ts +233 -0
  178. package/src/compiler/exports/export-pages.ts +44 -0
  179. package/src/compiler/exports/html-tree.ts +62 -0
  180. package/src/compiler/exports/latex/capture.ts +651 -0
  181. package/src/compiler/exports/latex/compile.ts +168 -0
  182. package/src/compiler/exports/latex/document.ts +479 -0
  183. package/src/compiler/exports/latex/explico.cls +415 -0
  184. package/src/compiler/exports/latex/numbering.ts +74 -0
  185. package/src/compiler/exports/latex/serialize.ts +787 -0
  186. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  187. package/src/compiler/exports/latex-export.ts +255 -0
  188. package/src/compiler/exports/markdown/serialize.ts +632 -0
  189. package/src/compiler/exports/markdown-export.ts +329 -0
  190. package/src/compiler/exports/standalone-html.ts +39 -70
  191. package/src/compiler/exports/static-tree.ts +944 -0
  192. package/src/compiler/exports/zip.ts +88 -0
  193. package/src/compiler/lesson-derivation.ts +13 -8
  194. package/src/compiler/manifest.ts +387 -40
  195. package/src/compiler/render-diagnostics.ts +111 -0
  196. package/src/components/CourseContents.astro +6 -0
  197. package/src/components/HoverPanel.astro +204 -6
  198. package/src/components/PartOverview.astro +18 -9
  199. package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
  200. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  201. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  202. package/src/components/assessments/AssessmentSet.astro +14 -1
  203. package/src/components/block-heading.ts +38 -0
  204. package/src/components/card-arrow.ts +18 -4
  205. package/src/components/card-carry.ts +406 -0
  206. package/src/components/card-drag.ts +1 -30
  207. package/src/components/card-fetch.ts +87 -0
  208. package/src/components/card-manager.ts +1222 -38
  209. package/src/components/card-memory.ts +5 -1
  210. package/src/components/card-placement.ts +211 -13
  211. package/src/components/card-side.ts +281 -31
  212. package/src/components/citation/CitationLayer.astro +45 -40
  213. package/src/components/diagram/Diagram.astro +123 -44
  214. package/src/components/disclosure/print-as-shown.ts +42 -6
  215. package/src/components/figure/Figure.astro +17 -1
  216. package/src/components/figure/FloatCaption.astro +4 -17
  217. package/src/components/figure/figure-state.ts +26 -17
  218. package/src/components/float-toolbar.ts +274 -0
  219. package/src/components/memo/Memo.astro +21 -10
  220. package/src/components/memo/ShortNote.astro +4 -0
  221. package/src/components/notation/NotationGlossary.astro +22 -17
  222. package/src/components/notation/NotationLayer.astro +300 -79
  223. package/src/components/notation/NotationMath.astro +8 -1
  224. package/src/components/notation/render-notation-math.ts +51 -5
  225. package/src/components/readable-text.ts +25 -0
  226. package/src/components/reference/FloatRefLayer.astro +162 -89
  227. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  228. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  229. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  230. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  231. package/src/components/starlight/LessonExportActions.astro +384 -310
  232. package/src/components/starlight/LessonFooter.astro +66 -8
  233. package/src/components/starlight/PrintController.astro +28 -0
  234. package/src/components/starlight/TextSizeControl.astro +137 -0
  235. package/src/components/starlight/print-controller.ts +380 -0
  236. package/src/components/starlight/section-summary.ts +13 -5
  237. package/src/components/table/Table.astro +14 -22
  238. package/src/components/table/table-truncation.ts +15 -0
  239. package/src/content-config.ts +20 -312
  240. package/src/curriculum/validation.ts +72 -24
  241. package/src/reference/base-abbreviations.mjs +2 -2
  242. package/src/reference/base-operators.mjs +8 -10
  243. package/src/reference/base-roman-labels.mjs +4 -3
  244. package/src/reference/citation-format.mjs +74 -0
  245. package/src/reference/consistency.ts +47 -61
  246. package/src/reference/curriculum-alignment.ts +4 -41
  247. package/src/reference/derived-entries.ts +68 -0
  248. package/src/reference/derived-notation.mjs +121 -0
  249. package/src/reference/diagram-fit.ts +104 -0
  250. package/src/reference/diagram-layout.ts +182 -51
  251. package/src/reference/diagrams-validate.ts +2 -1
  252. package/src/reference/equations-validate.ts +41 -2
  253. package/src/reference/equations.mjs +120 -22
  254. package/src/reference/examples-validate.ts +1 -0
  255. package/src/reference/figure-fit.ts +139 -0
  256. package/src/reference/figure-pictures.ts +96 -0
  257. package/src/reference/figure-symbols.ts +117 -0
  258. package/src/reference/figures-validate.ts +2 -1
  259. package/src/reference/gate-abbreviations.ts +1 -1
  260. package/src/reference/gate-course-rules.ts +22 -7
  261. package/src/reference/gate-heading-length.ts +25 -7
  262. package/src/reference/gate-heading-structure.ts +40 -1
  263. package/src/reference/gate-indices.ts +470 -0
  264. package/src/reference/gate-math.ts +102 -13
  265. package/src/reference/gate-mdx-expression.ts +45 -0
  266. package/src/reference/gate-non-chapter-pages.ts +241 -0
  267. package/src/reference/gate-part-pages.ts +1 -1
  268. package/src/reference/gate-prop-text.ts +145 -0
  269. package/src/reference/gate-prose-notation.ts +19 -5
  270. package/src/reference/gate-retired-keys.ts +101 -0
  271. package/src/reference/gloss.ts +2 -0
  272. package/src/reference/katex-measure.ts +384 -0
  273. package/src/reference/keyed-float-validate.ts +22 -0
  274. package/src/reference/keyed-jsx-floats.mjs +11 -14
  275. package/src/reference/lab-katex-validate.ts +156 -0
  276. package/src/reference/lab-math-scope.ts +15 -1
  277. package/src/reference/lint-ignore.ts +4 -1
  278. package/src/reference/math-glyphs.mjs +851 -46
  279. package/src/reference/mixed-content.ts +11 -2
  280. package/src/reference/page-notation.ts +114 -14
  281. package/src/reference/raw-floats.mjs +37 -13
  282. package/src/reference/references.ts +14 -0
  283. package/src/reference/registry.ts +170 -9
  284. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  285. package/src/reference/rehype-heading-anchors.mjs +11 -1
  286. package/src/reference/remark-citation.mjs +0 -0
  287. package/src/reference/remark-notation.mjs +275 -23
  288. package/src/reference/render-lab-math.ts +40 -5
  289. package/src/reference/sections-validate.ts +67 -3
  290. package/src/reference/sections.mjs +92 -17
  291. package/src/reference/tables-validate.ts +40 -2
  292. package/src/reference/text-measure.ts +63 -0
  293. package/src/reference/text-widths.ts +133 -0
  294. package/src/reference/types.ts +75 -8
  295. package/src/rules/catalog.ts +241 -31
  296. package/src/rules/settings.ts +10 -0
  297. package/src/session/PreferenceStore.ts +53 -0
  298. package/src/styles/global.css +322 -83
@@ -22,10 +22,17 @@ export interface NotationLayerDefinition {
22
22
  /** Symbols the formula names that have no card of their own. */
23
23
  glosses?: readonly ResolvedGloss[];
24
24
  units?: string;
25
+ /** Rule N23: the set a running index letter runs over, shown on its card. */
26
+ range?: string;
25
27
  seeAlso?: readonly string[];
26
28
  alignment?: NotationAlignment;
27
- editorialStatus?: 'draft' | 'in-review' | 'reviewed';
28
- scope?: 'local' | 'shared';
29
+ /**
30
+ * `derived`: a derived card (rule N23) — the card of an indexed symbol
31
+ * (`\mathbf{v}_0`) composed from its base and an index entry. It is a
32
+ * hidden source, never a row of the list: the list names the base and the
33
+ * index entry already.
34
+ */
35
+ scope?: 'local' | 'shared' | 'derived';
29
36
  href?: string;
30
37
  }
31
38
 
@@ -52,6 +59,13 @@ export interface Props {
52
59
  * derivative is told it means something else here.
53
60
  */
54
61
  suspended?: readonly NotationSuspension[];
62
+ /**
63
+ * The page's route slug (`projectiles/optimal-launch-angle`). A symbol of
64
+ * the page's own (`scope: 'local'`) is carried under a key scoped to it,
65
+ * `theta-star@projectiles/optimal-launch-angle` (rule K6, `card-carry.ts`),
66
+ * so it never stands for another page's symbol of the same name.
67
+ */
68
+ page?: string;
55
69
  }
56
70
 
57
71
  import HoverPanel from '../HoverPanel.astro';
@@ -71,6 +85,7 @@ const {
71
85
  defaultOpen = false,
72
86
  rememberState = true,
73
87
  suspended = [],
88
+ page,
74
89
  } = Astro.props;
75
90
 
76
91
  const panelOnly = presentation === 'panel';
@@ -104,11 +119,16 @@ const items = definitions.map((definition) => {
104
119
  return { ...definition, scope, href };
105
120
  });
106
121
 
122
+ // Listed entries, and the derived cards (rule N23) that are only sources.
123
+ const listed = items.filter((definition) => definition.scope !== 'derived');
124
+ const derivedItems = items.filter((definition) => definition.scope === 'derived');
125
+
107
126
  // Glosses (D15) have no card and no anchor, so they need a data source for the
108
127
  // panel. Their names are already visible under the formula they belong to
109
128
  // (`NotationFormula` renders that list, and it is the no-JS baseline), so these
110
- // blocks are hidden and exist only for the enhancement to read.
111
- const glossItems = items.flatMap((definition) => definition.glosses ?? []);
129
+ // blocks are hidden and exist only for the enhancement to read. A derived
130
+ // card's glosses are its index entry's, which that entry emits already.
131
+ const glossItems = listed.flatMap((definition) => definition.glosses ?? []);
112
132
 
113
133
  for (const gloss of glossItems) {
114
134
  if (!keyPattern.test(gloss.key)) {
@@ -121,14 +141,17 @@ for (const gloss of glossItems) {
121
141
  }
122
142
  ---
123
143
 
124
- {
125
- items.length > 0 && (
126
- <section
127
- class:list={['notation-layer', panelOnly && 'notation-layer--panel-only']}
128
- data-notation-layer
129
- data-notation-layer-id={id}
130
- aria-label={panelOnly ? 'Notation explanations' : label}
131
- >
144
+ {/* A page with no notation still renders the card's template, hidden: a
145
+ card carried here from another page is built from it (rule K5). */}
146
+ <section
147
+ class:list={['notation-layer', panelOnly && 'notation-layer--panel-only']}
148
+ data-notation-layer
149
+ data-notation-layer-id={id}
150
+ data-notation-page={page}
151
+ aria-label={panelOnly ? 'Notation explanations' : label}
152
+ hidden={items.length === 0}
153
+ >
154
+ {items.length > 0 && (
132
155
  <div class="notation-list-titlebar">
133
156
  <details
134
157
  class="notation-list ex-tier-meta"
@@ -138,7 +161,7 @@ for (const gloss of glossItems) {
138
161
  open={defaultOpen}
139
162
  >
140
163
  <summary>
141
- {label} <span class="notation-count">({items.length})</span>
164
+ {label} <span class="notation-count">({listed.length})</span>
142
165
  </summary>
143
166
  {/* How the cards work, inside the panel: shown once the panel is
144
167
  opened, not as three lines under every closed one. */}
@@ -163,12 +186,14 @@ for (const gloss of glossItems) {
163
186
  </ul>
164
187
  )}
165
188
  <dl>
166
- {items.map((definition) => (
189
+ {listed.map((definition) => (
167
190
  <div
168
191
  class="notation-entry"
169
192
  id={panelOnly ? undefined : `notation-${definition.key}`}
170
193
  data-notation-entry={definition.key}
171
194
  data-notation-href={definition.href}
195
+ data-notation-scope={definition.scope}
196
+ data-notation-latex={definition.notation}
172
197
  >
173
198
  <dt>
174
199
  <span
@@ -188,12 +213,6 @@ for (const gloss of glossItems) {
188
213
  >
189
214
  {definition.label}
190
215
  </a>
191
- {definition.editorialStatus &&
192
- definition.editorialStatus !== 'reviewed' && (
193
- <span class="notation-status">
194
- {definition.editorialStatus}
195
- </span>
196
- )}
197
216
  </dt>
198
217
  <dd>
199
218
  {definition.summary && (
@@ -216,6 +235,11 @@ for (const gloss of glossItems) {
216
235
  <NotationMath latex={definition.formula} />
217
236
  </span>
218
237
  ))}
238
+ {definition.range && (
239
+ <p class="notation-range" data-notation-range>
240
+ Runs over <NotationMath latex={definition.range} />
241
+ </p>
242
+ )}
219
243
  {definition.units && (
220
244
  <p class="notation-units" data-notation-units>
221
245
  <strong>Units:</strong> {definition.units}
@@ -226,6 +250,36 @@ for (const gloss of glossItems) {
226
250
  ))}
227
251
  </dl>
228
252
 
253
+ {derivedItems.map((definition) => (
254
+ <div
255
+ class="notation-derived-source"
256
+ data-notation-entry={definition.key}
257
+ data-notation-href={definition.href}
258
+ data-notation-scope="derived"
259
+ data-notation-latex={definition.notation}
260
+ hidden
261
+ >
262
+ <span class="notation-symbol-trigger" data-notation-key={definition.key}>
263
+ <NotationMath class="notation-symbol" latex={definition.notation} />
264
+ </span>
265
+ <span data-notation-link={definition.key}>{definition.label}</span>
266
+ {definition.summary && <p data-notation-summary>{definition.summary}</p>}
267
+ {definition.formula && definition.formulaScope && (
268
+ <NotationFormula
269
+ formula={definition.formula}
270
+ scope={definition.formulaScope}
271
+ glosses={definition.glosses}
272
+ entryLabel={definition.label}
273
+ />
274
+ )}
275
+ {definition.units && (
276
+ <span data-notation-units>
277
+ <strong>Units:</strong> {definition.units}
278
+ </span>
279
+ )}
280
+ </div>
281
+ ))}
282
+
229
283
  {glossItems.map((gloss) => (
230
284
  <div
231
285
  class="notation-gloss-source"
@@ -263,6 +317,7 @@ for (const gloss of glossItems) {
263
317
  </a>
264
318
  )}
265
319
  </div>
320
+ )}
266
321
 
267
322
  <HoverPanel
268
323
  id={`${id}-card`}
@@ -275,17 +330,17 @@ for (const gloss of glossItems) {
275
330
  <h3 data-slot-title></h3>
276
331
  </div>
277
332
  <div class="notation-panel-body">
333
+ <p class="notation-panel-origin" data-slot-origin hidden></p>
278
334
  <p data-slot-summary hidden></p>
279
335
  <div data-slot-formula hidden></div>
336
+ <p class="notation-range" data-slot-range hidden></p>
280
337
  <p class="notation-units" data-slot-units hidden></p>
281
- <a data-slot-link hidden
338
+ <a class="hover-panel-action" data-slot-link hidden
282
339
  >Go to the full definition<span aria-hidden="true" data-jump-arrow> →</span></a
283
340
  >
284
341
  </div>
285
342
  </HoverPanel>
286
- </section>
287
- )
288
- }
343
+ </section>
289
344
 
290
345
  <script>
291
346
  import {
@@ -293,6 +348,15 @@ for (const gloss of glossItems) {
293
348
  isKept,
294
349
  registerCardSource,
295
350
  } from '../card-manager';
351
+ import { readCardConfig } from '../card-carry';
352
+ import {
353
+ ensureKatexStylesheet,
354
+ fetchDoc,
355
+ hrefFrom,
356
+ markForeign,
357
+ notationPageOf,
358
+ pageTitleOf,
359
+ } from '../card-fetch';
296
360
  import { wireJumpArrow } from '../card-jump';
297
361
  import { wirePersistedDetails } from '../disclosure/persisted-disclosure';
298
362
 
@@ -307,23 +371,78 @@ for (const gloss of glossItems) {
307
371
  element.hidden = value.length === 0;
308
372
  }
309
373
 
310
- function setupNotationLayer(root: HTMLElement) {
311
- if (root.dataset.notationEnhanced === 'true') return;
312
- root.dataset.notationEnhanced = 'true';
374
+ /**
375
+ * One card source for every notation layer on the page — the page's own,
376
+ * and the glossary's panel — so the page has one `notation` source to
377
+ * register, even with no notation at all: a card carried here from another
378
+ * page is built from the template (rule K5).
379
+ */
380
+ function setupNotationLayers(roots: readonly HTMLElement[]) {
381
+ if (roots.length === 0 || roots.every((root) => root.dataset.notationEnhanced === 'true')) {
382
+ return;
383
+ }
384
+ for (const root of roots) root.dataset.notationEnhanced = 'true';
313
385
 
314
- const entryElements = Array.from(
315
- root.querySelectorAll<HTMLElement>('[data-notation-entry]'),
316
- );
317
- const entries = new Map(
318
- entryElements.map((entry) => [entry.dataset.notationEntry ?? '', entry]),
319
- );
320
- entries.delete('');
321
- if (entries.size === 0) return;
386
+ const entries = new Map<string, HTMLElement>();
387
+ for (const root of roots) {
388
+ for (const entry of root.querySelectorAll<HTMLElement>('[data-notation-entry]')) {
389
+ const key = entry.dataset.notationEntry ?? '';
390
+ if (key && !entries.has(key)) entries.set(key, entry);
391
+ }
392
+ }
322
393
 
323
- const template = root.querySelector<HTMLTemplateElement>(
324
- 'template[data-card-template]',
325
- );
394
+ const template = roots
395
+ .map((root) => root.querySelector<HTMLTemplateElement>('template[data-card-template]'))
396
+ .find((found): found is HTMLTemplateElement => found !== null);
326
397
  if (!template) return;
398
+ const inLayer = (element: Element) => roots.some((root) => root.contains(element));
399
+
400
+ // K6: the page a symbol of its own belongs to, which scopes its key. An
401
+ // item names a symbol's meaning: `key`, the course's shared one;
402
+ // `key@slug`, page `slug`'s own; `key<slug`, a shared one this page does
403
+ // not explain, read off page `slug`, which says where its card is found.
404
+ const page = roots.map((root) => root.dataset.notationPage).find(Boolean) ?? '';
405
+ const scopeOf = (key: string) => entries.get(key)?.dataset.notationScope;
406
+ const itemOf = (key: string) => (page && scopeOf(key) === 'local' ? `${key}@${page}` : key);
407
+ const keyOf = (item: string) => item.split(/[@<]/)[0] ?? item;
408
+ const slugOf = (item: string) => item.split(/[@<]/)[1] ?? '';
409
+
410
+ /** A notation slug's page: a route slug under the course's base, as Starlight serves it. */
411
+ const pageOfSlug = (slug: string) => {
412
+ const route = slug.replace(/(^|\/)index$/, '');
413
+ return `${readCardConfig().base.replace(/\/+$/, '')}/${route ? `${route}/` : ''}`;
414
+ };
415
+
416
+ /**
417
+ * The page a card cloned from another page (`data-card-foreign`) came
418
+ * from, for a symbol in it (`markForeign`); `null` outside such a card.
419
+ */
420
+ const foreignOf = (symbol: HTMLElement) => {
421
+ const card = symbol.closest<HTMLElement>('[data-card-foreign]');
422
+ return card
423
+ ? { page: card.dataset.cardFrom ?? '', slug: card.dataset.cardFromSlug ?? '' }
424
+ : null;
425
+ };
426
+
427
+ /**
428
+ * K6: whether a symbol means what this page's entry for it says. In a
429
+ * card from another page, only when both pages share the meaning;
430
+ * otherwise its card is that page's (`foreignItem`).
431
+ */
432
+ const meansHere = (symbol: HTMLElement, key: string) =>
433
+ entries.has(key) &&
434
+ (!foreignOf(symbol) ||
435
+ (scopeOf(key) !== 'local' && symbol.dataset.notationScope !== 'local'));
436
+
437
+ /** K6: the item a symbol in a card from another page names there; `null` if unknown. */
438
+ const foreignItem = (symbol: HTMLElement, key: string) => {
439
+ const from = foreignOf(symbol);
440
+ if (!from?.page || !from.slug) return null;
441
+ return symbol.dataset.notationScope === 'local' ? `${key}@${from.slug}` : `${key}<${from.slug}`;
442
+ };
443
+ /** D4: a symbol's glyph, its entry's LaTeX, compared without its spacing. */
444
+ const glyphOf = (entry: HTMLElement | undefined) =>
445
+ entry?.dataset.notationLatex?.replace(/\s+/g, '') || null;
327
446
 
328
447
  // Static triggers, computed once: markers embedded in prose or an
329
448
  // equation, and the symbol glyphs in the "Notation used on this page"
@@ -338,13 +457,15 @@ for (const gloss of glossItems) {
338
457
  document.querySelectorAll<HTMLElement>(triggerSelector),
339
458
  ).filter(
340
459
  (element) =>
341
- !root.contains(element) &&
460
+ !inLayer(element) &&
342
461
  !element.closest('.katex-mathml') &&
343
462
  entries.has(element.dataset.notationKey ?? ''),
344
463
  );
345
- const symbolTriggers = Array.from(
346
- root.querySelectorAll<HTMLElement>('[data-notation-symbol-trigger]'),
347
- ).filter((element) => entries.has(element.dataset.notationKey ?? ''));
464
+ const symbolTriggers = roots
465
+ .flatMap((root) =>
466
+ Array.from(root.querySelectorAll<HTMLElement>('[data-notation-symbol-trigger]')),
467
+ )
468
+ .filter((element) => entries.has(element.dataset.notationKey ?? ''));
348
469
  const highlightTargets = [...externalTriggers, ...symbolTriggers];
349
470
 
350
471
  for (const trigger of highlightTargets) {
@@ -355,8 +476,10 @@ for (const gloss of glossItems) {
355
476
  for (const term of externalTriggers) {
356
477
  if (term.matches('a.notation-term')) term.removeAttribute('title');
357
478
  }
358
- const help = root.querySelector<HTMLElement>('[data-notation-help]');
359
- if (help) help.hidden = false;
479
+ for (const root of roots) {
480
+ const help = root.querySelector<HTMLElement>('[data-notation-help]');
481
+ if (help) help.hidden = false;
482
+ }
360
483
 
361
484
  // The visible list's own symbol glyphs are the only keyboard path to
362
485
  // pinning a card from this list — make them a real, labelled,
@@ -381,13 +504,17 @@ for (const gloss of glossItems) {
381
504
 
382
505
  // A symbol trigger, wherever it lives: prose, the equation, the visible
383
506
  // list, or — the D15 case — inside an open card. This has to be live: a
384
- // card's content doesn't exist until the reader opens it.
507
+ // card's content doesn't exist until the reader opens it. In a card
508
+ // cloned from another page (`data-card-foreign`) a symbol means what it
509
+ // means there: it opens this page's card only when this page shares that
510
+ // meaning (K6), and otherwise that page's, fetched — the equation a
511
+ // reference card brings from another lesson explains its own symbols,
512
+ // even those this page never uses.
385
513
  const symbolFrom = (node: EventTarget | null) => {
386
514
  const candidate = closestElement(node, triggerSelector);
387
515
  if (!candidate || candidate.closest('.katex-mathml')) return null;
388
- return entries.has(candidate.dataset.notationKey ?? '')
389
- ? candidate
390
- : null;
516
+ const key = candidate.dataset.notationKey ?? '';
517
+ return meansHere(candidate, key) || foreignItem(candidate, key) ? candidate : null;
391
518
  };
392
519
 
393
520
  const labelOf = (entry: HTMLElement, key: string) =>
@@ -395,10 +522,11 @@ for (const gloss of glossItems) {
395
522
  .querySelector<HTMLElement>('[data-notation-link]')
396
523
  ?.textContent?.trim() ?? key;
397
524
 
398
- const buildCard = (key: string) => {
399
- const entry = entries.get(key);
400
- if (!entry) return null;
401
-
525
+ /**
526
+ * A card for `key` from its entry: on this page, or on the page `from`
527
+ * (K5), whose links it then makes reachable from here.
528
+ */
529
+ const buildCard = (key: string, entry: HTMLElement, from?: string) => {
402
530
  const el = cloneCardShell(template);
403
531
  const symbolHost = el?.querySelector<HTMLElement>('[data-slot-symbol]');
404
532
  const title = el?.querySelector<HTMLElement>('[data-slot-title]');
@@ -411,12 +539,16 @@ for (const gloss of glossItems) {
411
539
  }
412
540
 
413
541
  el.dataset.popupKey = key;
542
+ el.dataset.cardScope = entry.dataset.notationScope ?? 'shared';
543
+ const glyph = glyphOf(entry);
544
+ if (glyph) el.dataset.cardGlyph = glyph;
414
545
  el.setAttribute('aria-label', `Notation: ${labelOf(entry, key)}`);
415
546
 
547
+ // `importNode`: the entry may come from a fetched page.
416
548
  const sourceSymbol = entry.querySelector<HTMLElement>('.notation-symbol');
417
549
  symbolHost.replaceChildren(
418
550
  ...(sourceSymbol
419
- ? [sourceSymbol.cloneNode(true)]
551
+ ? [document.importNode(sourceSymbol, true)]
420
552
  : [document.createTextNode(entry.dataset.notationEntry ?? key)]),
421
553
  );
422
554
  title.textContent = labelOf(entry, key);
@@ -430,10 +562,22 @@ for (const gloss of glossItems) {
430
562
  '[data-notation-formula]',
431
563
  );
432
564
  formulaHost.replaceChildren(
433
- ...(sourceFormula ? [sourceFormula.cloneNode(true)] : []),
565
+ ...(sourceFormula ? [document.importNode(sourceFormula, true)] : []),
434
566
  );
435
567
  formulaHost.hidden = sourceFormula === null;
436
568
 
569
+ // Rule N23: the set a running index runs over.
570
+ const range = el?.querySelector<HTMLElement>('[data-slot-range]');
571
+ const sourceRange = entry.querySelector<HTMLElement>('[data-notation-range]');
572
+ if (range) {
573
+ range.replaceChildren(
574
+ ...(sourceRange
575
+ ? [...sourceRange.childNodes].map((node) => document.importNode(node, true))
576
+ : []),
577
+ );
578
+ range.hidden = sourceRange === null;
579
+ }
580
+
437
581
  setOptionalText(
438
582
  units,
439
583
  entry
@@ -446,7 +590,7 @@ for (const gloss of glossItems) {
446
590
  if (link) {
447
591
  link.hidden = href === undefined;
448
592
  if (href !== undefined) {
449
- link.href = href;
593
+ link.href = from ? hrefFrom(href, from) : href;
450
594
  wireJumpArrow(link);
451
595
  }
452
596
  }
@@ -454,23 +598,91 @@ for (const gloss of glossItems) {
454
598
  return el;
455
599
  };
456
600
 
601
+ /**
602
+ * The page `item`'s entry is read off: `from`, unless the item names
603
+ * another page's meaning (`key@slug`, `key<slug`) — a symbol of a card
604
+ * from that page, which travels with the page it was opened on (K5).
605
+ */
606
+ const entryPage = async (item: string, from: string) => {
607
+ const slug = slugOf(item);
608
+ const doc = await fetchDoc(from);
609
+ if (!slug || notationPageOf(doc) === slug) return { doc, page: from };
610
+ const page = pageOfSlug(slug);
611
+ return { doc: await fetchDoc(page), page };
612
+ };
613
+
614
+ /** K5, K6: the card for `item` from its entry on the page `from`, or the page it names. */
615
+ const buildFrom = (item: string, from: string) =>
616
+ entryPage(item, from).then(({ doc, page }) => {
617
+ const key = keyOf(item);
618
+ const entry = doc.querySelector<HTMLElement>(
619
+ `[data-notation-entry="${CSS.escape(key)}"]`,
620
+ );
621
+ if (!entry) return null;
622
+ if (doc === document) return buildCard(key, entry);
623
+ ensureKatexStylesheet(doc);
624
+ const el = buildCard(key, entry, page);
625
+ if (!el) return null;
626
+ // K6: each symbol in it, marked with what it means there.
627
+ markForeign(el, doc, page);
628
+ // K6: away from its page, a symbol of that page's own says so first,
629
+ // and its carrying mark names the page (K8).
630
+ const definedIn = pageTitleOf(doc) ?? page;
631
+ el.dataset.cardDefinedIn = definedIn;
632
+ const origin = el.querySelector<HTMLElement>('[data-slot-origin]');
633
+ if (origin && entry.dataset.notationScope === 'local') {
634
+ const title = document.createElement('em');
635
+ title.textContent = definedIn;
636
+ origin.replaceChildren('Defined in ', title, ' · means this only there');
637
+ origin.hidden = false;
638
+ }
639
+ return el;
640
+ });
641
+
457
642
  // The card manager mounts every card in one layer on `<body>`, never in
458
643
  // the article: an ancestor's stacking context once let the Starlight
459
644
  // right sidebar eat a card's clicks.
460
645
  registerCardSource({
461
646
  layer: 'notation',
462
647
  triggerFrom: symbolFrom,
463
- itemFor: (trigger) => trigger.dataset.notationKey ?? null,
464
- render: (key) => buildCard(key),
648
+ itemFor: (trigger) => {
649
+ const key = trigger.dataset.notationKey;
650
+ if (!key) return null;
651
+ return meansHere(trigger, key) ? itemOf(key) : foreignItem(trigger, key);
652
+ },
653
+ render: (item, trigger) => {
654
+ const key = keyOf(item);
655
+ const entry = entries.get(key);
656
+ if (meansHere(trigger, key) && entry) return buildCard(key, entry);
657
+ // K6: a symbol of a card from another page, as that page explains it.
658
+ const from = foreignOf(trigger)?.page;
659
+ return from ? buildFrom(item, from) : null;
660
+ },
661
+ // K5: the entry on the page the card was opened on.
662
+ build: buildFrom,
663
+ // K5: a shared symbol read off another page is this page's when it explains it.
664
+ awayItem: (item) => {
665
+ const key = keyOf(item);
666
+ return item.includes('<') && entries.has(key) && scopeOf(key) !== 'local' ? key : item;
667
+ },
668
+ // D4: the symbols here that look like a card's symbol from another page.
669
+ glyphOf: (trigger) => {
670
+ const key = trigger.dataset.notationKey ?? '';
671
+ return meansHere(trigger, key) ? glyphOf(entries.get(key)) : null;
672
+ },
673
+ lookalikes: (glyph) =>
674
+ externalTriggers.filter(
675
+ (trigger) => glyphOf(entries.get(trigger.dataset.notationKey ?? '')) === glyph,
676
+ ),
465
677
  onChange: (open) => {
466
678
  for (const trigger of highlightTargets) {
467
679
  trigger.classList.toggle(
468
680
  'is-notation-active',
469
- open.has(trigger.dataset.notationKey ?? ''),
681
+ open.has(itemOf(trigger.dataset.notationKey ?? '')),
470
682
  );
471
683
  }
472
684
  for (const trigger of symbolTriggers) {
473
- const mode = open.get(trigger.dataset.notationKey ?? '');
685
+ const mode = open.get(itemOf(trigger.dataset.notationKey ?? ''));
474
686
  trigger.setAttribute('aria-pressed', String(!!mode && isKept(mode)));
475
687
  }
476
688
  },
@@ -497,10 +709,11 @@ for (const gloss of glossItems) {
497
709
  window.addEventListener('hashchange', openIfTargeted);
498
710
  }
499
711
 
500
- for (const root of document.querySelectorAll<HTMLElement>(
501
- '[data-notation-layer]',
502
- )) {
503
- setupNotationLayer(root);
712
+ const roots = Array.from(
713
+ document.querySelectorAll<HTMLElement>('[data-notation-layer]'),
714
+ );
715
+ setupNotationLayers(roots);
716
+ for (const root of roots) {
504
717
  const details = root.querySelector<HTMLDetailsElement>(
505
718
  ':scope > .notation-list-titlebar > .notation-list[id]',
506
719
  );
@@ -759,18 +972,29 @@ for (const gloss of glossItems) {
759
972
  }
760
973
  }
761
974
 
762
- .notation-status {
763
- padding: 0.08rem 0.35rem;
764
- border: 1px solid var(--sl-color-gray-5);
765
- border-radius: 999px;
766
- color: var(--sl-color-gray-3);
975
+ .notation-formula {
976
+ font-size: var(--sl-text-sm);
977
+ }
978
+
979
+ /* A secondary line, a step below the card's text: the units, and the
980
+ set a running index runs over (rule N23). */
981
+ .notation-units,
982
+ .notation-range {
767
983
  font-size: var(--sl-text-xs);
768
- font-weight: 500;
769
984
  }
770
985
 
771
- .notation-formula,
772
- .notation-units {
773
- font-size: var(--sl-text-sm);
986
+ .notation-range[hidden] {
987
+ display: none;
988
+ }
989
+
990
+ /* K6: away from its page, a symbol of that page's own opens with where
991
+ it is defined, and that it means this only there. */
992
+ .notation-panel-origin {
993
+ font-size: var(--sl-text-xs);
994
+ }
995
+
996
+ .notation-panel-origin[hidden] {
997
+ display: none;
774
998
  }
775
999
 
776
1000
  /* Cursor, hover highlight, and "Copied!" flash for this plain-span
@@ -795,13 +1019,10 @@ for (const gloss of glossItems) {
795
1019
 
796
1020
  /* Each open explanation is its own card, cloned from the `HoverPanel`
797
1021
  shell above and mounted by the card manager (`card-manager.ts`) on
798
- `<body>`. Relative to the surrounding prose's own font-size, not a
799
- fixed Starlight step: `%` resolves against the same base the article's
800
- paragraphs use. `.notation-panel-body` and its descendants below style
801
- the fields common to both this static list and a card's content. */
802
- .notation-popup.hover-panel {
803
- font-size: 90%;
804
- }
1022
+ `<body>`. Its text is the size of every card's, `HoverPanel`'s
1023
+ `--sl-text-sm` (14 px, the article's caption size); it used to be 90%
1024
+ here alone. `.notation-panel-body` and its descendants below style the
1025
+ fields common to both this static list and a card's content. */
805
1026
 
806
1027
  /* Symbols inside a card's formula are click/hover-through triggers for
807
1028
  another card — the D15 case this whole file exists for. */
@@ -3,6 +3,8 @@ import { renderNotationMath } from './render-notation-math';
3
3
  import SafeMathNode from './SafeMathNode.astro';
4
4
  import {
5
5
  decorationOfEntry,
6
+ indexDisplayLatex,
7
+ indexOfEntry,
6
8
  operatorDisplayLatex,
7
9
  } from '../../reference/math-glyphs.mjs';
8
10
 
@@ -14,9 +16,14 @@ interface Props {
14
16
  const { latex, class: className } = Astro.props;
15
17
  // An operator entry (`\dot{}`, rule N22) is shown over a placeholder square;
16
18
  // on its own the decoration is a speck.
19
+ // An index entry (`{}_0`, rule N23) the same way: `\square_0`.
17
20
  const operator = decorationOfEntry(latex);
18
21
  const nodes = renderNotationMath(
19
- operator === undefined ? latex : operatorDisplayLatex(operator),
22
+ operator !== undefined
23
+ ? operatorDisplayLatex(operator)
24
+ : indexOfEntry(latex) !== undefined
25
+ ? indexDisplayLatex(latex)
26
+ : latex,
20
27
  );
21
28
  ---
22
29