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
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The live end of a figure's caption line (docs/plans/figures-and-short-captions.md,
3
+ * decisions 3 and 8). A lab states the case its plot shows after the long
4
+ * caption — "Shown for v₀ = 20 m/s and θ = 30°: range 35.3 m." — in
5
+ * `span.float-caption__state[data-figure-state]` (`FloatCaption.astro`). That
6
+ * span sits outside `.float-caption__text`, so every reader of the long
7
+ * caption (a preview card, a fold's bar, the list of figures) still reads the
8
+ * caption alone, and it prints with the caption.
9
+ *
10
+ * A lab passes its clause as runs of text, never HTML: a string is plain
11
+ * text, `{ text, italic: true }` an italic run (a symbol), `{ sub }` a
12
+ * subscript. There is no `aria-live`: the controls already announce their
13
+ * values.
14
+ */
15
+
16
+ export type FigureStateRun =
17
+ | string
18
+ | { readonly text: string; readonly italic?: boolean }
19
+ | { readonly sub: string };
20
+
21
+ type NodeFactory = Pick<Document, 'createElement' | 'createTextNode'>;
22
+
23
+ /** The nodes for `runs`, made by `doc`. Markup in a run stays text. */
24
+ export function figureStateNodes(
25
+ runs: readonly FigureStateRun[],
26
+ doc: NodeFactory,
27
+ ): Node[] {
28
+ return runs.map((run) => {
29
+ if (typeof run === 'string') return doc.createTextNode(run);
30
+ if ('sub' in run) {
31
+ const sub = doc.createElement('sub');
32
+ sub.textContent = run.sub;
33
+ return sub;
34
+ }
35
+ if (!run.italic) return doc.createTextNode(run.text);
36
+ const italic = doc.createElement('i');
37
+ italic.textContent = run.text;
38
+ return italic;
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Write the case a lab shows at the end of its figure's caption line. `from`
44
+ * is any element inside the `<Figure>` — the lab's own root. An empty list
45
+ * clears the clause. Returns whether it found the figure's caption.
46
+ */
47
+ export function setFigureState(from: Element, runs: readonly FigureStateRun[]): boolean {
48
+ const state = from
49
+ .closest('figure.keyed-figure')
50
+ ?.querySelector<HTMLElement>(':scope > figcaption > [data-figure-state]');
51
+ if (!state) return false;
52
+ // A slider drag writes on every tick; leave the line alone when nothing changed.
53
+ const key = JSON.stringify(runs);
54
+ if (state.dataset.figureStateKey === key) return true;
55
+ state.dataset.figureStateKey = key;
56
+ state.replaceChildren(...figureStateNodes(runs, state.ownerDocument));
57
+ return true;
58
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The fold of a lab's tools (docs/plans/figures-and-short-captions.md,
3
+ * decisions 3–6). A `<Figure>` with a `tools` slot renders it after the
4
+ * caption line, in a tray inside a `<details>` (so persistence, find-in-page
5
+ * and print-as-shown keep working on it). The fold button ⌃ / ⌄ at the end
6
+ * of the caption line drives it; folded, a "Hidden here: 2 sliders, 1
7
+ * equation" line under the caption says what is gone, and unfolds it on a
8
+ * click. Only the tools fold: the picture and the caption always show.
9
+ *
10
+ * `describeHiddenTools` is split from the DOM walk so the wording is
11
+ * unit-testable, as `../starlight/section-summary.ts` does for sections.
12
+ */
13
+ import { wirePersistedDetails } from '../disclosure/persisted-disclosure';
14
+
15
+ export interface ToolCounts {
16
+ readonly sliders: number;
17
+ readonly menus: number;
18
+ readonly equations: number;
19
+ }
20
+
21
+ const LABELS: readonly (readonly [keyof ToolCounts, string, string])[] = [
22
+ ['sliders', 'slider', 'sliders'],
23
+ ['menus', 'menu', 'menus'],
24
+ ['equations', 'equation', 'equations'],
25
+ ];
26
+
27
+ /** `"Hidden here: 2 sliders, 1 equation"`, or `"Hidden here: the controls"`. */
28
+ export function describeHiddenTools(counts: ToolCounts): string {
29
+ const parts = LABELS.filter(([key]) => counts[key] > 0).map(
30
+ ([key, singular, plural]) => `${counts[key]} ${counts[key] === 1 ? singular : plural}`,
31
+ );
32
+ return `Hidden here: ${parts.length > 0 ? parts.join(', ') : 'the controls'}`;
33
+ }
34
+
35
+ export function countTools(tray: ParentNode): ToolCounts {
36
+ return {
37
+ sliders: tray.querySelectorAll('input[type="range"]').length,
38
+ menus: tray.querySelectorAll('select').length,
39
+ equations: tray.querySelectorAll('.katex-display').length,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Wire one figure's tools: its fold button, its "Hidden here" line and the
45
+ * reader's remembered choice (`figure-tools:<figure id>`, per page). The
46
+ * button and the line follow every change to the `<details>`, whoever makes
47
+ * it — a click, the browser's find-in-page, a restore.
48
+ */
49
+ export function wireFigureTools(figure: HTMLElement): void {
50
+ const details = figure.querySelector<HTMLDetailsElement>(
51
+ ':scope > details.keyed-figure__tools-fold',
52
+ );
53
+ const toggle = figure.querySelector<HTMLButtonElement>(
54
+ ':scope > figcaption [data-figure-tools-toggle]',
55
+ );
56
+ const stub = figure.querySelector<HTMLButtonElement>(
57
+ ':scope > [data-figure-tools-stub]',
58
+ );
59
+ if (!details || !toggle || !stub || !figure.id) return;
60
+
61
+ // The SSR `open` attribute carries the first-visit state (`defaultOpen`,
62
+ // else the course's `disclosure.figures`); read it before anything moves it.
63
+ const defaultOpen = details.open;
64
+ if (details.dataset.rememberState === 'true') {
65
+ wirePersistedDetails(details, `figure-tools:${figure.id}`, defaultOpen);
66
+ }
67
+
68
+ const name = figure.dataset.figNumber ? `Figure ${figure.dataset.figNumber}` : 'this figure';
69
+ const sync = () => {
70
+ const open = details.open;
71
+ toggle.setAttribute('aria-expanded', String(open));
72
+ toggle.setAttribute('aria-label', `${open ? 'Hide' : 'Show'} the controls of ${name}`);
73
+ toggle.title = open ? 'Hide the controls' : 'Show the controls';
74
+ if (!open) stub.textContent = describeHiddenTools(countTools(details));
75
+ stub.hidden = open;
76
+ };
77
+
78
+ toggle.addEventListener('click', () => {
79
+ details.open = !details.open;
80
+ });
81
+ stub.addEventListener('click', () => {
82
+ details.open = true;
83
+ toggle.focus({ preventScroll: true });
84
+ });
85
+ details.addEventListener('toggle', sync);
86
+ sync();
87
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * A numbered float's reader-facing name: its kind word and number, then its
3
+ * short caption when it has one — "Diagram 2.1.1 Steps of the range
4
+ * derivation" (docs/plans/figures-and-short-captions.md, decisions 9 and 11).
5
+ * A `<Fold>`'s bar names the floats inside it this way.
6
+ *
7
+ * A float carries both halves on its root element, as `data-<kind>-number`
8
+ * and `data-<kind>-short` (`Figure.astro`, `Table.astro`, `Diagram.astro`);
9
+ * nothing else in the page carries those attributes.
10
+ */
11
+
12
+ export type FloatKind = 'fig' | 'tab' | 'dia';
13
+
14
+ export const FLOAT_KIND_WORDS: Readonly<Record<FloatKind, string>> = {
15
+ fig: 'Figure',
16
+ tab: 'Table',
17
+ dia: 'Diagram',
18
+ };
19
+
20
+ export interface NamedFloat {
21
+ readonly kind: FloatKind;
22
+ readonly number: string;
23
+ readonly short?: string;
24
+ /** Its anchor (`fig-…`, `tab-…`, `dia-…`), when the scan found one. */
25
+ readonly anchorId?: string;
26
+ }
27
+
28
+ /** `Figure 2.1.1 Projectile trajectory explorer`, or `Figure 2.1.1` with no short caption. */
29
+ export function floatName(float: NamedFloat): string {
30
+ const label = `${FLOAT_KIND_WORDS[float.kind]} ${float.number}`;
31
+ return float.short ? `${label} ${float.short}` : label;
32
+ }
33
+
34
+ /**
35
+ * The bold title that opens a caption line when the float has a short
36
+ * caption (docs/plans/figures-and-short-captions.md, decision 9.1): the
37
+ * short caption with a closing period, unless it already ends in one — the
38
+ * journal-legend style, "Fig. 1 | Title. Description".
39
+ */
40
+ export function captionTitle(short: string): string {
41
+ const title = short.trim();
42
+ return /[.!?…:;]$/u.test(title) ? title : `${title}.`;
43
+ }
44
+
45
+ /**
46
+ * A numbered float's name, read off its root element in the page — the same
47
+ * `data-<kind>-number` / `data-<kind>-short` pair `floatsInHtml` reads from
48
+ * markup. `null` for an element that is no numbered float.
49
+ */
50
+ export function floatOf(element: Element): NamedFloat | null {
51
+ for (const kind of ['fig', 'tab', 'dia'] as const) {
52
+ const number = element.getAttribute(`data-${kind}-number`);
53
+ if (!number) continue;
54
+ const short = element.getAttribute(`data-${kind}-short`) ?? undefined;
55
+ return {
56
+ kind,
57
+ number,
58
+ ...(short ? { short } : {}),
59
+ ...(element.id ? { anchorId: element.id } : {}),
60
+ };
61
+ }
62
+ return null;
63
+ }
64
+
65
+ /** Astro writes `&` and `"` in an attribute value as numeric references. */
66
+ function decodeAttribute(value: string): string {
67
+ return value
68
+ .replace(/&#(\d+);/g, (_, code: string) => String.fromCodePoint(Number(code)))
69
+ .replace(/&#x([\da-f]+);/gi, (_, code: string) =>
70
+ String.fromCodePoint(Number.parseInt(code, 16)),
71
+ )
72
+ .replace(/&quot;/g, '"')
73
+ .replace(/&lt;/g, '<')
74
+ .replace(/&gt;/g, '>')
75
+ .replace(/&amp;/g, '&');
76
+ }
77
+
78
+ /**
79
+ * Every numbered float in rendered HTML, in document order — for a build-time
80
+ * component that renders its slot first (`<Fold>`). Reads the start tags
81
+ * only: attribute values are double-quoted, with `"` escaped, so a tag ends
82
+ * at the first `>` outside one.
83
+ */
84
+ export function floatsInHtml(html: string): NamedFloat[] {
85
+ const floats: NamedFloat[] = [];
86
+ for (const tag of html.matchAll(/<[A-Za-z][\w-]*((?:\s+[^\s"'>/=]+(?:="[^"]*")?)*)\s*\/?>/g)) {
87
+ const attributes = tag[1] ?? '';
88
+ const number = /\sdata-(fig|tab|dia)-number="([^"]*)"/.exec(attributes);
89
+ if (!number?.[2]) continue;
90
+ const kind = number[1] as FloatKind;
91
+ const short = new RegExp(`\\sdata-${kind}-short="([^"]*)"`).exec(attributes)?.[1];
92
+ const anchorId = /\sid="([^"]*)"/.exec(attributes)?.[1];
93
+ floats.push({
94
+ kind,
95
+ number: decodeAttribute(number[2]),
96
+ ...(short ? { short: decodeAttribute(short) } : {}),
97
+ ...(anchorId ? { anchorId: decodeAttribute(anchorId) } : {}),
98
+ });
99
+ }
100
+ return floats;
101
+ }
@@ -0,0 +1,125 @@
1
+ ---
2
+ /**
3
+ * `<Fold>` — collapsible content with no heading, number or marker
4
+ * (docs/plans/figures-and-short-captions.md, decision 11). A stretch of the
5
+ * lesson a reader can fold away: a diagram, a figure, a table, a few
6
+ * paragraphs. A float never folds by itself (decision 1), so outside a
7
+ * collapsible section this is how one becomes fully collapsible.
8
+ *
9
+ * It folds the way a memo does (`../memo/Memo.astro`): it is a section of its
10
+ * own that `CollapsibleSections.astro` folds, with the section chevron, the
11
+ * hover peek, the "Hidden here: …" line, the reader's remembered choice, the
12
+ * reveal on a deep link and print-as-shown. It is none of a memo's other
13
+ * things: no marker or letter in the text, no `[[…]]` key, no aside frame —
14
+ * it is the plain tier, like the figure it holds. Its bar is an
15
+ * `<h6 role="none">` this component draws, so it is not in the outline, "On
16
+ * this page", the heading rules or the accessibility tree's headings, and
17
+ * takes no number. `data-section-contained` / `data-section-to-end` make it a
18
+ * section that never ends one around it (`../starlight/section-parts.ts`).
19
+ *
20
+ * The bar is the `label`, set at body-text size. Without one it names the
21
+ * numbered floats inside, "Diagram 2.1.1 Steps of the range derivation"
22
+ * (`../float-name.ts`), read from the rendered content; with neither it
23
+ * throws, as `fold-unnamed` reports at `validate:content`. A bar that names
24
+ * the floats (`data-names-floats`) is their name here: their caption lines
25
+ * drop the bold short-caption title that would repeat it, and a folded
26
+ * fold's "Hidden here" counts them. The floats keep their numbers: the
27
+ * numbering scans see through a fold (`TRANSPARENT_COMPONENTS`).
28
+ *
29
+ * First state: `defaultOpen`, else the course's `disclosure.folds` (closed by
30
+ * default); `rememberState` (default `true`) keeps the reader's choice, per
31
+ * page.
32
+ */
33
+ import { courseDisclosure } from '../disclosure/course-disclosure';
34
+ import { floatName, floatsInHtml } from '../float-name';
35
+ import { foldId, foldSlug } from './fold-id';
36
+
37
+ export interface Props {
38
+ /** The bar's text. Default: the numbers and short captions of the floats inside. */
39
+ label?: string;
40
+ /** State for a reader with nothing saved yet. Default: the course's `disclosure.folds`. */
41
+ defaultOpen?: boolean;
42
+ /** Persist the reader's open/closed choice across visits, per page. Default: `true`. */
43
+ rememberState?: boolean;
44
+ }
45
+
46
+ const { label, defaultOpen, rememberState = true } = Astro.props;
47
+
48
+ if (label !== undefined && (typeof label !== 'string' || !label.trim())) {
49
+ throw new Error('<Fold> `label` must be a non-empty string when given.');
50
+ }
51
+
52
+ // Rendered first, to read the floats it holds; injected below unchanged.
53
+ const content = await Astro.slots.render('default');
54
+ const floats = floatsInHtml(content);
55
+ const bar = label?.trim() || floats.map(floatName).join('; ');
56
+ if (!bar) {
57
+ throw new Error(
58
+ '<Fold> has no `label` and no numbered <Figure>, <Table> or <Diagram> inside it, so its bar has nothing to name it by: give it a `label`, or fold a numbered float with it (fold-unnamed).',
59
+ );
60
+ }
61
+
62
+ const id = foldId(
63
+ Astro.locals,
64
+ label ? foldSlug(label) : (floats[0]?.anchorId ?? foldSlug(bar)),
65
+ );
66
+
67
+ const open = defaultOpen ?? (await courseDisclosure()).folds.defaultOpen;
68
+ ---
69
+
70
+ <div class="fold" data-fold data-names-floats={label ? undefined : ''} data-section-contained>
71
+ <div
72
+ class="sl-heading-wrapper level-h6 fold__bar"
73
+ data-collapsible="true"
74
+ data-default-open={String(open)}
75
+ data-remember-state={String(rememberState)}
76
+ data-section-to-end
77
+ data-bar-names-floats={label ? undefined : ''}
78
+ >
79
+ <h6 id={id} class="fold__label" role="none">{bar}</h6>
80
+ </div>
81
+ <div class="fold__body"><Fragment set:html={content} /></div>
82
+ </div>
83
+
84
+ <style>
85
+ /* The plain tier (`surfaces.css`): no frame, no tint — only the section
86
+ machinery's chevron in the page gutter, and a semibold bar at the size
87
+ of the paragraphs it may fold, never smaller. The folding itself is the
88
+ section wrapper's (`CollapsibleSections.astro`, `global.css`). */
89
+ .fold {
90
+ margin-block: 1rem;
91
+ }
92
+
93
+ .fold > :global(*) {
94
+ margin-block: 0;
95
+ }
96
+
97
+ .fold__label {
98
+ margin: 0;
99
+ font-size: var(--sl-text-body);
100
+ font-weight: 600;
101
+ line-height: 1.5;
102
+ color: var(--sl-color-gray-2);
103
+ scroll-margin-block-start: 5rem;
104
+ }
105
+
106
+ /* One line box for the bar, also where a narrow screen sets the label
107
+ inline and wraps it; the gutter chevron centres on its first line
108
+ (`--sl-section-title-size` × `--sl-line-height-headings`). */
109
+ .fold :global(.fold__bar > .sl-section-collapsible > .sl-section-summary) {
110
+ --sl-section-title-size: var(--sl-text-body);
111
+ --sl-line-height-headings: 1.5;
112
+ line-height: 1.5;
113
+ }
114
+
115
+ .fold__body > :global(:first-child) {
116
+ margin-block-start: 0.5rem;
117
+ }
118
+
119
+ /* A bar that names the floats inside is their title: their caption lines
120
+ go without the bold short caption that would say it a second time
121
+ (`FloatCaption.astro`, `Table.astro`), open, peeking or printed. */
122
+ .fold[data-names-floats] :global(.float-caption__title) {
123
+ display: none;
124
+ }
125
+ </style>
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The id of a `<Fold>`'s bar (`Fold.astro`). It keys the reader's saved
3
+ * open/closed choice (`section:<id>`) and a `#fold-…` link, so it comes from
4
+ * what the fold holds rather than its position: the first float's anchor
5
+ * when the bar names floats (`fold-dia-range-derivation`), else the label
6
+ * (`fold-worked-derivation`), made unique on the page.
7
+ */
8
+
9
+ const usedByPage = new WeakMap<object, Set<string>>();
10
+
11
+ /** `"Steps of the derivation"` → `steps-of-the-derivation`. */
12
+ export function foldSlug(text: string): string {
13
+ return (
14
+ text
15
+ .normalize('NFKD')
16
+ .replace(/[\u0300-\u036f]/g, '')
17
+ .toLowerCase()
18
+ .replace(/[^a-z0-9]+/g, '-')
19
+ .replace(/^-+|-+$/g, '')
20
+ .slice(0, 60)
21
+ .replace(/-+$/, '') || 'content'
22
+ );
23
+ }
24
+
25
+ /**
26
+ * `fold-<base>`, with `-2`, `-3`, … when a fold earlier on the same page took
27
+ * it. `page` is any object unique to one page render — `Astro.locals`.
28
+ */
29
+ export function foldId(page: object, base: string): string {
30
+ let used = usedByPage.get(page);
31
+ if (!used) {
32
+ used = new Set();
33
+ usedByPage.set(page, used);
34
+ }
35
+ const first = `fold-${base}`;
36
+ let id = first;
37
+ for (let n = 2; used.has(id); n++) id = `${first}-${n}`;
38
+ used.add(id);
39
+ return id;
40
+ }