explico 0.8.0 → 0.11.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 (230) hide show
  1. package/CHANGELOG.md +1239 -1
  2. package/README.md +60 -15
  3. package/dist/compiler/astro.d.ts +6 -1
  4. package/dist/compiler/cli.d.ts +46 -2
  5. package/dist/compiler/cli.js +70 -4
  6. package/dist/compiler/collections.d.ts +35 -2
  7. package/dist/compiler/collections.js +68 -5
  8. package/dist/compiler/course-config.d.ts +3 -194
  9. package/dist/compiler/course-config.js +78 -5
  10. package/dist/compiler/exports/standalone-html.d.ts +6 -1
  11. package/dist/compiler/knowledge-version.js +1 -1
  12. package/dist/compiler/lesson-derivation.d.ts +27 -10
  13. package/dist/compiler/lesson-derivation.js +23 -8
  14. package/dist/compiler/logging.d.ts +4 -1
  15. package/dist/compiler/logging.js +4 -1
  16. package/dist/compiler/manifest.d.ts +204 -31
  17. package/dist/compiler/manifest.js +270 -55
  18. package/dist/compiler/resolution-report.d.ts +6 -1
  19. package/dist/course-config-Dsnri9Tk.d.ts +464 -0
  20. package/dist/reference/base-accents.d.mts +26 -0
  21. package/dist/reference/base-accents.mjs +31 -0
  22. package/dist/reference/base-roman-labels.d.mts +2 -2
  23. package/dist/reference/base-roman-labels.mjs +2 -2
  24. package/dist/reference/consistency.d.ts +14 -2
  25. package/dist/reference/consistency.js +108 -4
  26. package/dist/reference/delimiter-size.d.ts +3 -2
  27. package/dist/reference/diagrams-validate.d.ts +7 -2
  28. package/dist/reference/diagrams-validate.js +9 -1
  29. package/dist/reference/equations-validate.d.ts +8 -5
  30. package/dist/reference/equations-validate.js +1 -0
  31. package/dist/reference/equations.d.mts +60 -31
  32. package/dist/reference/equations.mjs +83 -43
  33. package/dist/reference/examples-validate.d.ts +31 -0
  34. package/dist/reference/examples-validate.js +48 -0
  35. package/dist/reference/figures-validate.d.ts +1 -1
  36. package/dist/reference/figures-validate.js +4 -1
  37. package/dist/reference/folds-validate.d.ts +33 -0
  38. package/dist/reference/folds-validate.js +32 -0
  39. package/dist/reference/gate-abbreviations.d.ts +3 -1
  40. package/dist/reference/gate-abbreviations.js +1 -0
  41. package/dist/reference/gate-course-rules.d.ts +32 -0
  42. package/dist/reference/gate-course-rules.js +85 -0
  43. package/dist/reference/gate-heading-length.d.ts +5 -2
  44. package/dist/reference/gate-heading-length.js +45 -21
  45. package/dist/reference/gate-heading-structure.d.ts +41 -0
  46. package/dist/reference/gate-heading-structure.js +113 -0
  47. package/dist/reference/gate-math-punctuation.d.ts +71 -0
  48. package/dist/reference/gate-math-punctuation.js +251 -0
  49. package/dist/reference/gate-math.d.ts +22 -6
  50. package/dist/reference/gate-math.js +42 -8
  51. package/dist/reference/gate-part-pages.d.ts +50 -0
  52. package/dist/reference/gate-part-pages.js +118 -0
  53. package/dist/reference/gate-prose-notation.d.ts +21 -11
  54. package/dist/reference/gate-prose-notation.js +25 -7
  55. package/dist/reference/katex-delimiters.mjs +20 -7
  56. package/dist/reference/keyed-float-validate.d.ts +27 -10
  57. package/dist/reference/keyed-float-validate.js +15 -1
  58. package/dist/reference/keyed-jsx-floats.d.mts +11 -5
  59. package/dist/reference/keyed-jsx-floats.mjs +73 -20
  60. package/dist/reference/knowledge-checks-validate.d.ts +1 -1
  61. package/dist/reference/knowledge-checks.d.mts +3 -1
  62. package/dist/reference/knowledge-checks.mjs +9 -4
  63. package/dist/reference/lab-math-scope.d.ts +1 -0
  64. package/dist/reference/lab-math-slot.d.ts +34 -0
  65. package/dist/reference/lab-math-slot.js +6 -0
  66. package/dist/reference/math-glyphs.d.mts +80 -4
  67. package/dist/reference/math-glyphs.mjs +572 -37
  68. package/dist/reference/page-notation.d.ts +6 -1
  69. package/dist/reference/raw-floats-validate.js +1 -1
  70. package/dist/reference/raw-floats.mjs +1 -1
  71. package/dist/reference/rehype-heading-anchors.d.mts +14 -0
  72. package/dist/reference/rehype-heading-anchors.mjs +116 -17
  73. package/dist/reference/rehype-table-header-labels.d.mts +6 -1
  74. package/dist/reference/rehype-table-header-labels.mjs +21 -3
  75. package/dist/reference/remark-asides.d.mts +3 -0
  76. package/dist/reference/remark-asides.mjs +180 -0
  77. package/dist/reference/remark-memo.d.mts +3 -1
  78. package/dist/reference/remark-memo.mjs +42 -11
  79. package/dist/reference/remark-notation.d.mts +60 -10
  80. package/dist/reference/remark-notation.mjs +387 -36
  81. package/dist/reference/render-lab-math.d.ts +4 -26
  82. package/dist/reference/render-lab-math.js +4 -4
  83. package/dist/reference/sections-validate.d.ts +27 -0
  84. package/dist/reference/sections-validate.js +41 -0
  85. package/dist/reference/sections.d.mts +97 -0
  86. package/dist/reference/sections.mjs +306 -0
  87. package/dist/reference/tables-validate.d.ts +1 -1
  88. package/dist/reference/types.d.ts +17 -1
  89. package/dist/reference/unit-words.d.ts +37 -0
  90. package/dist/reference/unit-words.js +170 -0
  91. package/dist/rules/catalog.d.ts +176 -0
  92. package/dist/rules/catalog.js +983 -0
  93. package/dist/rules/course-rules.d.ts +115 -0
  94. package/dist/rules/course-rules.js +292 -0
  95. package/dist/rules/course-rules.schema.json +138 -0
  96. package/dist/rules/in-effect.d.ts +53 -0
  97. package/dist/rules/in-effect.js +142 -0
  98. package/dist/rules/rules.schema.json +1072 -0
  99. package/dist/rules/settings.d.ts +3 -0
  100. package/dist/rules/settings.js +290 -0
  101. package/dist/rules/waivers.d.ts +53 -0
  102. package/dist/rules/waivers.js +132 -0
  103. package/dist/session/PreferenceStore.d.ts +5 -0
  104. package/dist/session/PreferenceStore.js +9 -0
  105. package/docs/VERSIONING.md +2 -2
  106. package/docs/authoring/README.md +232 -40
  107. package/docs/authoring/agents.md +55 -9
  108. package/docs/authoring/components.md +178 -22
  109. package/docs/authoring/migrating.md +1096 -2
  110. package/docs/authoring/notation.md +576 -322
  111. package/docs/authoring/prose.md +551 -0
  112. package/docs/authoring/retired.md +35 -0
  113. package/docs/authoring/structure-rules.md +422 -40
  114. package/docs/authoring/structure.md +445 -62
  115. package/docs/authoring/versioning.md +20 -28
  116. package/docs/authoring/visualizations.md +142 -20
  117. package/package.json +2 -2
  118. package/src/compiler/cli.ts +116 -6
  119. package/src/compiler/collections.ts +111 -7
  120. package/src/compiler/course-config.ts +273 -34
  121. package/src/compiler/knowledge-version.ts +1 -1
  122. package/src/compiler/lesson-derivation.ts +54 -15
  123. package/src/compiler/logging.ts +8 -2
  124. package/src/compiler/manifest.ts +573 -104
  125. package/src/components/BlockHeading.astro +113 -0
  126. package/src/components/CourseContents.astro +221 -0
  127. package/src/components/CurriculumMap.astro +5 -6
  128. package/src/components/FloatIndex.astro +200 -0
  129. package/src/components/HoverPanel.astro +431 -79
  130. package/src/components/PartBridge.astro +27 -0
  131. package/src/components/PartOverview.astro +401 -0
  132. package/src/components/PartScope.astro +27 -0
  133. package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
  134. package/src/components/assessments/AssessmentRunner.tsx +8 -5
  135. package/src/components/assessments/AssessmentSet.astro +23 -26
  136. package/src/components/block-heading.ts +146 -0
  137. package/src/components/card-arrow.ts +243 -0
  138. package/src/components/card-drag.ts +631 -0
  139. package/src/components/card-jump.ts +45 -0
  140. package/src/components/card-manager.ts +1589 -0
  141. package/src/components/card-memory.ts +177 -0
  142. package/src/components/card-placement.ts +504 -0
  143. package/src/components/card-side.ts +336 -0
  144. package/src/components/card-travel.ts +197 -0
  145. package/src/components/citation/CitationLayer.astro +77 -76
  146. package/src/components/diagram/Diagram.astro +161 -316
  147. package/src/components/disclosure/course-disclosure.ts +24 -0
  148. package/src/components/disclosure/hover-preview.ts +122 -42
  149. package/src/components/disclosure/persisted-disclosure.ts +3 -3
  150. package/src/components/disclosure/print-as-shown.ts +252 -0
  151. package/src/components/examples/CompactExample.astro +3 -1
  152. package/src/components/examples/CompactExamples.astro +101 -94
  153. package/src/components/figure/Figure.astro +174 -274
  154. package/src/components/figure/FloatCaption.astro +87 -0
  155. package/src/components/figure/figure-state.ts +58 -0
  156. package/src/components/figure/figure-tools.ts +87 -0
  157. package/src/components/float-name.ts +101 -0
  158. package/src/components/fold/Fold.astro +125 -0
  159. package/src/components/fold/fold-id.ts +40 -0
  160. package/src/components/memo/Memo.astro +162 -231
  161. package/src/components/memo/ShortNote.astro +6 -6
  162. package/src/components/notation/NotationFormula.astro +5 -1
  163. package/src/components/notation/NotationGlossary.astro +3 -4
  164. package/src/components/notation/NotationLayer.astro +169 -162
  165. package/src/components/notation/NotationMath.astro +10 -1
  166. package/src/components/reference/FloatRefLayer.astro +485 -423
  167. package/src/components/starlight/AboutPanel.astro +5 -4
  168. package/src/components/starlight/CollapsibleSections.astro +207 -93
  169. package/src/components/starlight/DiagramEnhancer.astro +15 -138
  170. package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
  171. package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
  172. package/src/components/starlight/LayoutHeader.astro +298 -19
  173. package/src/components/starlight/LayoutPageSidebar.astro +99 -12
  174. package/src/components/starlight/LayoutPageTitle.astro +120 -0
  175. package/src/components/starlight/LayoutSidebar.astro +229 -11
  176. package/src/components/starlight/LessonExportActions.astro +53 -72
  177. package/src/components/starlight/LessonFooter.astro +26 -4
  178. package/src/components/starlight/section-parts.ts +15 -2
  179. package/src/components/starlight/section-summary.ts +181 -0
  180. package/src/components/table/Table.astro +95 -76
  181. package/src/components/table/TableOverflowExpander.astro +65 -28
  182. package/src/content-config.ts +40 -4
  183. package/src/reference/base-accents.mjs +31 -0
  184. package/src/reference/base-roman-labels.mjs +2 -2
  185. package/src/reference/consistency.ts +176 -5
  186. package/src/reference/delimiter-size.ts +3 -2
  187. package/src/reference/diagrams-validate.ts +17 -1
  188. package/src/reference/equations-validate.ts +10 -5
  189. package/src/reference/equations.mjs +83 -43
  190. package/src/reference/examples-validate.ts +97 -0
  191. package/src/reference/figures-validate.ts +6 -1
  192. package/src/reference/folds-validate.ts +65 -0
  193. package/src/reference/gate-abbreviations.ts +1 -0
  194. package/src/reference/gate-course-rules.ts +134 -0
  195. package/src/reference/gate-heading-length.ts +66 -35
  196. package/src/reference/gate-heading-structure.ts +193 -0
  197. package/src/reference/gate-math-punctuation.ts +381 -0
  198. package/src/reference/gate-math.ts +69 -10
  199. package/src/reference/gate-part-pages.ts +217 -0
  200. package/src/reference/gate-prose-notation.ts +59 -12
  201. package/src/reference/katex-delimiters.mjs +20 -7
  202. package/src/reference/keyed-float-validate.ts +45 -11
  203. package/src/reference/keyed-jsx-floats.mjs +73 -20
  204. package/src/reference/knowledge-checks-validate.ts +1 -1
  205. package/src/reference/knowledge-checks.mjs +9 -4
  206. package/src/reference/lab-math-slot.ts +36 -0
  207. package/src/reference/math-glyphs.mjs +572 -37
  208. package/src/reference/raw-floats-validate.ts +1 -1
  209. package/src/reference/raw-floats.mjs +1 -1
  210. package/src/reference/rehype-heading-anchors.mjs +116 -17
  211. package/src/reference/rehype-table-header-labels.mjs +21 -3
  212. package/src/reference/remark-asides.mjs +180 -0
  213. package/src/reference/remark-memo.mjs +42 -11
  214. package/src/reference/remark-notation.mjs +387 -36
  215. package/src/reference/render-lab-math.ts +16 -31
  216. package/src/reference/sections-validate.ts +81 -0
  217. package/src/reference/sections.mjs +306 -0
  218. package/src/reference/tables-validate.ts +2 -1
  219. package/src/reference/types.ts +17 -0
  220. package/src/reference/unit-words.ts +233 -0
  221. package/src/rules/catalog.ts +1298 -0
  222. package/src/rules/course-rules.ts +456 -0
  223. package/src/rules/in-effect.ts +235 -0
  224. package/src/rules/settings.ts +448 -0
  225. package/src/rules/waivers.ts +249 -0
  226. package/src/session/PreferenceStore.ts +15 -0
  227. package/src/styles/global.css +1471 -163
  228. package/src/styles/surfaces.css +182 -0
  229. package/src/components/hover-panel.ts +0 -339
  230. package/src/components/popup-stack.ts +0 -345
@@ -1,6 +1,24 @@
1
1
  ---
2
- import { Icon } from '@astrojs/starlight/components';
3
2
  import DefaultSidebar from '@astrojs/starlight/components/Sidebar.astro';
3
+ import { loadManifest } from '../../compiler/manifest';
4
+
5
+ /**
6
+ * The header control that opens a part's own page. Starlight's sidebar group
7
+ * is a label and a list with no link of its own, and the label here already
8
+ * owns a click (it collapses the part), so the control is added beside the
9
+ * caret rather than taking either. It is an enhancement on top of the
10
+ * `Overview` row the compiler puts in the group's list: that row is in the
11
+ * HTML, so the part page is reachable, keyboard-operable and crawlable with
12
+ * this script disabled.
13
+ */
14
+ const manifest = await loadManifest();
15
+ const base = import.meta.env.BASE_URL.replace(/\/$/, '');
16
+ const partLinks = manifest.parts
17
+ .filter((part) => part.overview !== undefined)
18
+ .map((part) => ({ label: part.label, href: `${base}${part.link}` }));
19
+ // `<` keeps a label that happens to contain `</script>` from ending the
20
+ // element early. It is still the same JSON to `JSON.parse`.
21
+ const partLinkPayload = JSON.stringify(partLinks).replace(/</g, '\\u003c');
4
22
  ---
5
23
 
6
24
  <div class="layout-navigation-panel" data-layout-panel="navigation">
@@ -12,22 +30,127 @@ import DefaultSidebar from '@astrojs/starlight/components/Sidebar.astro';
12
30
  data-layout-toggle="navigation"
13
31
  hidden
14
32
  >
15
- <Icon
16
- name="left-caret"
17
- class="layout-edge-icon layout-edge-icon-hide"
18
- size="1rem"
19
- />
20
- <Icon
21
- name="right-caret"
22
- class="layout-edge-icon layout-edge-icon-restore"
23
- size="1rem"
24
- />
33
+ <span class="layout-edge-glyph layout-edge-glyph-hide" aria-hidden="true"
34
+ >&lt;&lt;&lt;</span
35
+ >
36
+ <span class="layout-edge-glyph layout-edge-glyph-restore" aria-hidden="true"
37
+ >&gt;&gt;&gt;</span
38
+ >
25
39
  </button>
40
+ <div
41
+ class="layout-edge-resizer layout-navigation-resizer"
42
+ role="separator"
43
+ aria-orientation="vertical"
44
+ aria-controls="starlight__navigation-content"
45
+ aria-label="Navigation width"
46
+ tabindex="0"
47
+ data-layout-resizer="navigation"
48
+ hidden
49
+ >
50
+ </div>
26
51
  <div
27
52
  id="starlight__navigation-content"
28
53
  class="layout-navigation-content layout-compact-sidebar"
29
54
  >
30
55
  <DefaultSidebar />
56
+ {
57
+ partLinks.length > 0 && (
58
+ <script
59
+ is:inline
60
+ type="application/json"
61
+ data-explico-part-links
62
+ set:html={partLinkPayload}
63
+ />
64
+ )
65
+ }
66
+ <script>
67
+ /**
68
+ * Give each part header a control that opens the part's own page. The
69
+ * labels come from the manifest, so a group is matched by the text
70
+ * Starlight rendered for it rather than by position — the Reference
71
+ * group and any part without an overview page simply never match.
72
+ *
73
+ * The link is a sibling of `<summary>`, drawn over the end of the header
74
+ * row, not a child of it: `<summary>` is itself an interactive control,
75
+ * and a link inside one is `nested-interactive` — announced
76
+ * inconsistently by screen readers, and a focus trap for some assistive
77
+ * technology. Out here it is an ordinary link that happens to sit in the
78
+ * header, and a click on it never reaches the summary's collapse.
79
+ */
80
+ (() => {
81
+ const payload = document.querySelector('[data-explico-part-links]');
82
+ if (!payload?.textContent) return;
83
+
84
+ let links: { label: string; href: string }[] = [];
85
+ try {
86
+ links = JSON.parse(payload.textContent);
87
+ } catch {
88
+ return;
89
+ }
90
+ const hrefByLabel = new Map(
91
+ links.map((link) => [link.label.trim(), link.href]),
92
+ );
93
+
94
+ const root = document.querySelector('#starlight__navigation-content');
95
+ for (const summary of root?.querySelectorAll('summary') ?? []) {
96
+ const group = summary.parentElement;
97
+ const label = summary
98
+ .querySelector('.group-label')
99
+ ?.textContent?.trim();
100
+ const href = label ? hrefByLabel.get(label) : undefined;
101
+ if (!group || !href || group.querySelector('.layout-part-open')) {
102
+ continue;
103
+ }
104
+
105
+ const link = document.createElement('a');
106
+ link.className = 'layout-part-open';
107
+ link.href = href;
108
+ link.title = 'Open part overview';
109
+ link.setAttribute('aria-label', `Open part overview: ${label}`);
110
+ link.textContent = '»';
111
+ // Immediately after the header it belongs to: the reading order is
112
+ // the part, then its page, then its chapters, and CSS lifts it back
113
+ // into the header row.
114
+ summary.insertAdjacentElement('afterend', link);
115
+ group.classList.add('layout-part-has-open');
116
+ }
117
+ })();
118
+ </script>
119
+ <script>
120
+ /**
121
+ * Mark every chapter the reader has already passed, so the rail beside
122
+ * the chapter list can show how far into the course this page is.
123
+ *
124
+ * The sidebar's document order *is* the reading order — the compiler
125
+ * built it from the merged track order — so "covered" is simply
126
+ * "earlier in this list than the current page". A part whose chapters
127
+ * are all covered is covered itself, which is what makes the rail read
128
+ * as one run rather than as a mark per group.
129
+ */
130
+ (() => {
131
+ const root = document.querySelector('#starlight__navigation-content');
132
+ const links = [...(root?.querySelectorAll('a') ?? [])].filter(
133
+ (link) => !link.classList.contains('layout-part-open'),
134
+ );
135
+ const current = links.findIndex(
136
+ (link) => link.getAttribute('aria-current') === 'page',
137
+ );
138
+ if (current < 1) return;
139
+
140
+ for (const link of links.slice(0, current)) {
141
+ link.parentElement?.setAttribute('data-covered', '');
142
+ }
143
+ for (const group of root?.querySelectorAll('details') ?? []) {
144
+ const chapters = [...group.querySelectorAll(':scope > ul > li')];
145
+ if (
146
+ chapters.length > 0 &&
147
+ chapters.every((item) => item.hasAttribute('data-covered'))
148
+ ) {
149
+ group.setAttribute('data-covered', '');
150
+ }
151
+ }
152
+ })();
153
+ </script>
31
154
  <script>
32
155
  import { createBrowserPreferenceStore } from '../../session/PreferenceStore';
33
156
 
@@ -86,5 +209,100 @@ import DefaultSidebar from '@astrojs/starlight/components/Sidebar.astro';
86
209
  });
87
210
  })();
88
211
  </script>
212
+ <script>
213
+ /**
214
+ * A compact chapter shows its lessons as icons and hands the lesson's own
215
+ * title to a tooltip. That tooltip is a child of the navigation panel,
216
+ * which scrolls (`overflow-y: auto`, which forces `overflow-x` to match),
217
+ * so an absolutely positioned child cannot leave it: any title longer
218
+ * than the panel is wide ended at the panel's edge, unreadable.
219
+ *
220
+ * Nothing in CSS escapes an ancestor's scroll clipping while staying
221
+ * positioned against that ancestor, so the showing tooltip gets
222
+ * `position: fixed` (see `global.css`) and the anchor's own viewport
223
+ * coordinates from here. The tooltip stays the link's accessible name in
224
+ * the markup either way — this only moves where it is painted.
225
+ */
226
+ (() => {
227
+ if (!matchMedia('(min-width: 50rem)').matches) return;
228
+ const panel = document.querySelector<HTMLElement>(
229
+ '#starlight__navigation-content',
230
+ );
231
+ if (!panel) return;
232
+
233
+ const COMPACT_LINK =
234
+ 'details[data-compact] > ul:not(.top-level) > li > a';
235
+ const GAP = 7;
236
+ const EDGE = 8;
237
+ let open: HTMLAnchorElement | null = null;
238
+
239
+ const labelOf = (link: HTMLAnchorElement) =>
240
+ link.querySelector<HTMLElement>(':scope > span');
241
+
242
+ const place = () => {
243
+ if (!open) return;
244
+ const label = labelOf(open);
245
+ if (!label) return;
246
+ const anchor = open.getBoundingClientRect();
247
+ label.style.setProperty(
248
+ '--layout-tooltip-y',
249
+ `${anchor.top + anchor.height / 2}px`,
250
+ );
251
+ // Measured while it is already fixed, so this is the width it will
252
+ // actually be drawn at, wrapping included.
253
+ const width = label.getBoundingClientRect().width;
254
+ const rtl = getComputedStyle(document.documentElement).direction === 'rtl';
255
+ const preferred = rtl
256
+ ? anchor.left - GAP - width
257
+ : anchor.right + GAP;
258
+ const furthest = Math.max(EDGE, window.innerWidth - width - EDGE);
259
+ label.style.setProperty(
260
+ '--layout-tooltip-x',
261
+ `${Math.min(Math.max(EDGE, preferred), furthest)}px`,
262
+ );
263
+ };
264
+
265
+ const hide = () => {
266
+ if (!open) return;
267
+ const label = labelOf(open);
268
+ label?.style.removeProperty('--layout-tooltip-x');
269
+ label?.style.removeProperty('--layout-tooltip-y');
270
+ open.removeAttribute('data-tooltip-open');
271
+ open = null;
272
+ };
273
+
274
+ const show = (link: HTMLAnchorElement) => {
275
+ if (open === link) return;
276
+ hide();
277
+ open = link;
278
+ link.setAttribute('data-tooltip-open', '');
279
+ place();
280
+ };
281
+
282
+ const linkFrom = (target: EventTarget | null) =>
283
+ target instanceof Element
284
+ ? target.closest<HTMLAnchorElement>(COMPACT_LINK)
285
+ : null;
286
+
287
+ panel.addEventListener('pointerover', (event) => {
288
+ const link = linkFrom(event.target);
289
+ if (link) show(link);
290
+ else hide();
291
+ });
292
+ panel.addEventListener('pointerleave', hide);
293
+ panel.addEventListener('focusin', (event) => {
294
+ const link = linkFrom(event.target);
295
+ if (link) show(link);
296
+ else hide();
297
+ });
298
+ panel.addEventListener('focusout', hide);
299
+ // A fixed tooltip no longer travels with the panel it points at.
300
+ window.addEventListener('scroll', place, {
301
+ passive: true,
302
+ capture: true,
303
+ });
304
+ window.addEventListener('resize', place, { passive: true });
305
+ })();
306
+ </script>
89
307
  </div>
90
308
  </div>
@@ -100,15 +100,10 @@ const {
100
100
  </style>
101
101
 
102
102
  <script>
103
- type SavedDetails = {
104
- readonly element: HTMLDetailsElement;
105
- readonly open: boolean;
106
- readonly state: string | undefined;
107
- readonly style: string | null;
108
- readonly body: HTMLElement | null;
109
- readonly bodyStyle: string | null;
110
- readonly temporaryMarker: string | null;
111
- };
103
+ import {
104
+ prepareDisclosures,
105
+ type PreparedDisclosures,
106
+ } from '../disclosure/print-as-shown';
112
107
 
113
108
  type SavedLink = {
114
109
  readonly element: HTMLAnchorElement;
@@ -124,7 +119,7 @@ const {
124
119
 
125
120
  type PreparedStaticExport = {
126
121
  readonly mode: StaticExportMode;
127
- readonly details: readonly SavedDetails[];
122
+ readonly disclosures: PreparedDisclosures;
128
123
  readonly hidden: readonly SavedHidden[];
129
124
  readonly links: readonly SavedLink[];
130
125
  };
@@ -164,19 +159,25 @@ const {
164
159
  return undefined;
165
160
  }
166
161
 
167
- let result = target.href;
168
- if (canonicalBaseUrl && target.origin === current.origin) {
169
- const normalizedSiteBase = `/${siteBaseUrl.replace(/^\/+|\/+$/gu, '')}/`;
170
- const relativePath = target.pathname.startsWith(normalizedSiteBase)
171
- ? target.pathname.slice(normalizedSiteBase.length)
172
- : target.pathname.replace(/^\/+/, '');
173
- result = new URL(
174
- `${relativePath}${target.search}${target.hash}`,
175
- canonicalBaseUrl,
176
- ).href;
177
- }
162
+ return canonicalUrl(target, canonicalBaseUrl, siteBaseUrl);
163
+ }
178
164
 
179
- return result;
165
+ // A same-site URL moved onto the deployed site, so a printed or saved
166
+ // lesson's links don't point at whatever host the reader printed from.
167
+ function canonicalUrl(
168
+ target: URL,
169
+ canonicalBaseUrl: string | undefined,
170
+ siteBaseUrl: string,
171
+ ): string {
172
+ if (!canonicalBaseUrl || target.origin !== location.origin) return target.href;
173
+ const normalizedSiteBase = `/${siteBaseUrl.replace(/^\/+|\/+$/gu, '')}/`;
174
+ const relativePath = target.pathname.startsWith(normalizedSiteBase)
175
+ ? target.pathname.slice(normalizedSiteBase.length)
176
+ : target.pathname.replace(/^\/+/, '');
177
+ return new URL(
178
+ `${relativePath}${target.search}${target.hash}`,
179
+ canonicalBaseUrl,
180
+ ).href;
180
181
  }
181
182
 
182
183
  function prepareStaticExport(mode: StaticExportMode): void {
@@ -187,35 +188,32 @@ const {
187
188
 
188
189
  root.dataset.explicoStaticExport = mode;
189
190
 
190
- const details = Array.from(
191
- document.querySelectorAll<HTMLDetailsElement>('main details'),
192
- (element): SavedDetails => {
193
- const body = element.querySelector<HTMLElement>(
194
- ':scope > .sl-section-body, :scope > .diagram__body',
195
- );
196
- const saved = {
197
- element,
198
- open: element.open,
199
- state: element.dataset.state,
200
- style: element.getAttribute('style'),
201
- body,
202
- bodyStyle: body?.getAttribute('style') ?? null,
203
- temporaryMarker: element.getAttribute('data-explico-temporary-open'),
204
- };
205
- element.dataset.explicoTemporaryOpen = mode;
206
- element.open = true;
207
- if (element.dataset.state !== undefined) element.dataset.state = 'open';
208
- body?.style.removeProperty('max-height');
209
- body?.style.removeProperty('overflow');
210
- body?.style.removeProperty('transition');
211
- return saved;
191
+ const canonicalBaseUrl = printController.dataset.canonicalBaseUrl;
192
+ const siteBaseUrl = printController.dataset.siteBaseUrl ?? '/';
193
+
194
+ // Print follows the screen (see `../disclosure/print-as-shown`); a
195
+ // Markdown export needs every word whatever the reader collapsed.
196
+ const disclosures = prepareDisclosures(
197
+ mode === 'print' ? 'as-shown' : 'expanded',
198
+ {
199
+ noteHref: (anchorId) => {
200
+ const page = new URL(location.href);
201
+ page.hash = anchorId ? `#${anchorId}` : '';
202
+ return canonicalUrl(page, canonicalBaseUrl, siteBaseUrl);
203
+ },
212
204
  },
213
205
  );
214
206
 
207
+ // Rows beyond a long table's fold and an open example set's inactive
208
+ // tabs are hidden only to save screen space, so they are exposed for
209
+ // print too. A closed short note or question is a reader's disclosure:
210
+ // print leaves it closed (with its note), only Markdown export opens it.
211
+ const hiddenSelector =
212
+ mode === 'print'
213
+ ? 'main :is(.compact-example, .keyed-table__body tr:not(.keyed-table__more-row))[hidden]'
214
+ : 'main :is(.assessment-panel, .short-note, .compact-example, .keyed-table__body tr:not(.keyed-table__more-row))[hidden]';
215
215
  const hidden = Array.from(
216
- document.querySelectorAll<HTMLElement>(
217
- 'main :is(.assessment-panel, .short-note, .compact-example, .keyed-table__body tr:not(.keyed-table__more-row))[hidden]',
218
- ),
216
+ document.querySelectorAll<HTMLElement>(hiddenSelector),
219
217
  (element): SavedHidden => {
220
218
  const saved = { element, hidden: element.hidden };
221
219
  element.hidden = false;
@@ -223,8 +221,6 @@ const {
223
221
  },
224
222
  );
225
223
 
226
- const canonicalBaseUrl = printController.dataset.canonicalBaseUrl;
227
- const siteBaseUrl = printController.dataset.siteBaseUrl ?? '/';
228
224
  const links =
229
225
  mode === 'print'
230
226
  ? Array.from(
@@ -248,7 +244,7 @@ const {
248
244
  })
249
245
  : [];
250
246
 
251
- prepared = { mode, details, hidden, links };
247
+ prepared = { mode, disclosures, hidden, links };
252
248
  if (mode === 'print') root.dataset.explicoPrintReady = '';
253
249
  }
254
250
 
@@ -257,17 +253,7 @@ const {
257
253
  if (!saved) return;
258
254
  prepared = null;
259
255
 
260
- for (const state of saved.details) {
261
- state.element.open = state.open;
262
- if (state.state === undefined) delete state.element.dataset.state;
263
- else state.element.dataset.state = state.state;
264
- if (state.style === null) state.element.removeAttribute('style');
265
- else state.element.setAttribute('style', state.style);
266
- if (state.body) {
267
- if (state.bodyStyle === null) state.body.removeAttribute('style');
268
- else state.body.setAttribute('style', state.bodyStyle);
269
- }
270
- }
256
+ saved.disclosures.restore();
271
257
  for (const state of saved.hidden) state.element.hidden = state.hidden;
272
258
  for (const state of saved.links) {
273
259
  if (state.href === null) state.element.removeAttribute('href');
@@ -280,16 +266,7 @@ const {
280
266
  // prepare and restore mutations.
281
267
  const cleanupMarkers = () => {
282
268
  if (pendingMarkerCleanup !== cleanupMarkers) return;
283
- for (const state of saved.details) {
284
- if (state.temporaryMarker === null) {
285
- state.element.removeAttribute('data-explico-temporary-open');
286
- } else {
287
- state.element.setAttribute(
288
- 'data-explico-temporary-open',
289
- state.temporaryMarker,
290
- );
291
- }
292
- }
269
+ saved.disclosures.releaseMarkers();
293
270
  delete root.dataset.explicoStaticExport;
294
271
  pendingMarkerCleanup = null;
295
272
  };
@@ -324,9 +301,13 @@ const {
324
301
  }
325
302
 
326
303
  async function hydrateVisibleIslandsForPrint(): Promise<void> {
304
+ // Only islands that will be on paper: one inside a disclosure the reader
305
+ // left closed (print follows the screen) never gets a layout box, so it
306
+ // can never become visible, and waiting on it would stall the dialog for
307
+ // `waitForIslandHydration`'s full timeout. A lab's tools never print.
327
308
  const islands = Array.from(
328
309
  document.querySelectorAll<HTMLElement>('main astro-island[ssr][client="visible"]'),
329
- );
310
+ ).filter((island) => !island.closest('details:not([open]), [hidden], [data-print-omit]'));
330
311
  if (islands.length === 0) return;
331
312
 
332
313
  const scrollX = window.scrollX;
@@ -31,6 +31,7 @@ import {
31
31
  notationDefinitionsForRenderedPage,
32
32
  pageNotationDependencies,
33
33
  } from '../../reference/page-notation';
34
+ import type { NotationSuspension } from '../../reference/types';
34
35
 
35
36
  // `lessonId` is no longer authored (Phase C2); derive it from the route.
36
37
  const routeSlug = Astro.locals.starlightRoute.entry.id;
@@ -42,13 +43,30 @@ const glossaryBase = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/glossary/`;
42
43
 
43
44
  const definitions: NotationLayerDefinition[] = [];
44
45
  let notationDefaultOpen = false;
46
+ let numberAliases = false;
47
+ let suspended: NotationSuspension[] = [];
45
48
  if (lessonId) {
46
49
  const manifest = await loadManifest();
47
- notationDefaultOpen = manifest.disclosureDefaults.notation;
50
+ notationDefaultOpen = manifest.disclosure.notation.defaultOpen;
51
+ numberAliases = manifest.numbering.aliases;
48
52
  const lesson = manifest.lessons.find((entry) => entry.id === lessonId);
49
53
  const routeData = Astro.locals.starlightRoute.entry.data as {
50
- notation?: { local?: readonly { key?: unknown }[] };
54
+ notation?: {
55
+ local?: readonly { key?: unknown }[];
56
+ suspend?: readonly { operator?: unknown; reason?: unknown }[];
57
+ };
51
58
  };
59
+ // `notation.suspend` (rule N22), already checked by the schema and by
60
+ // `validate:content`; the Page notation panel tells the reader.
61
+ suspended = (routeData.notation?.suspend ?? []).flatMap((item) =>
62
+ typeof item.operator === 'string'
63
+ ? [
64
+ typeof item.reason === 'string'
65
+ ? { operator: item.operator, reason: item.reason }
66
+ : { operator: item.operator },
67
+ ]
68
+ : [],
69
+ );
52
70
  const authoredLocalKeys = (routeData.notation?.local ?? [])
53
71
  .map((entry) => entry.key)
54
72
  .filter((key): key is string => typeof key === 'string');
@@ -83,11 +101,15 @@ if (lessonId) {
83
101
  }
84
102
  ---
85
103
 
86
- <NotationLayer definitions={definitions} defaultOpen={notationDefaultOpen} />
104
+ <NotationLayer
105
+ definitions={definitions}
106
+ defaultOpen={notationDefaultOpen}
107
+ suspended={suspended}
108
+ />
87
109
  <AnchorScrollEnhancer />
88
110
  {lessonId && <CitationLayer />}
89
111
  {lessonId && <AbbreviationLayer />}
90
- {lessonId && <KeyedFloatEnhancer />}
112
+ {lessonId && <KeyedFloatEnhancer aliases={numberAliases} />}
91
113
  {lessonId && <FloatRefLayer />}
92
114
  {lessonId && <CollapsibleSections />}
93
115
  {lessonId && <TableOverflowExpander />}
@@ -16,14 +16,27 @@
16
16
 
17
17
  export const HEADINGS = 'h1, h2, h3, h4, h5, h6';
18
18
 
19
+ /**
20
+ * Two markers for a block that is a section of its own, not a heading in the
21
+ * page's outline — a `<Memo>`, whose bar is a `######`
22
+ * (docs/plans/titles-and-containers.md Part 4), and a `<Fold>`, the same
23
+ * without the memo's marker, letter and frame:
24
+ * - `data-section-to-end` on its heading wrapper: its section is every
25
+ * following sibling in its container, whatever headings its body holds;
26
+ * - `data-section-contained` on its container: the block never ends a
27
+ * section around it — it is body content there, like a figure.
28
+ */
19
29
  export function sectionParts(heading: HTMLElement): Element[] {
20
30
  const wrapper = heading.closest('.sl-heading-wrapper') ?? heading;
21
31
  const rank = Number(heading.tagName[1]);
32
+ const toEnd = wrapper.hasAttribute('data-section-to-end');
22
33
  const parts: Element[] = [wrapper];
23
34
  let node = wrapper.nextElementSibling;
24
35
  while (node) {
25
- const inner = node.matches(HEADINGS) ? node : node.querySelector(HEADINGS);
26
- if (inner && Number(inner.tagName[1]) <= rank) break;
36
+ if (!toEnd && !node.matches('[data-section-contained]')) {
37
+ const inner = node.matches(HEADINGS) ? node : node.querySelector(HEADINGS);
38
+ if (inner && Number(inner.tagName[1]) <= rank) break;
39
+ }
27
40
  parts.push(node);
28
41
  node = node.nextElementSibling;
29
42
  }