explico 0.7.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.
- package/CHANGELOG.md +1409 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +54 -3
- package/dist/compiler/cli.js +80 -6
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +84 -8
- package/dist/compiler/course-config.d.ts +3 -194
- package/dist/compiler/course-config.js +78 -5
- package/dist/compiler/exports/standalone-html.d.ts +6 -1
- package/dist/compiler/knowledge-version.js +1 -1
- package/dist/compiler/lesson-derivation.d.ts +27 -10
- package/dist/compiler/lesson-derivation.js +23 -8
- package/dist/compiler/logging.d.ts +14 -1
- package/dist/compiler/logging.js +6 -2
- package/dist/compiler/manifest.d.ts +204 -31
- package/dist/compiler/manifest.js +270 -55
- package/dist/compiler/resolution-report.d.ts +6 -1
- package/dist/course-config-Dsnri9Tk.d.ts +464 -0
- package/dist/reference/base-accents.d.mts +26 -0
- package/dist/reference/base-accents.mjs +31 -0
- package/dist/reference/base-roman-labels.d.mts +2 -2
- package/dist/reference/base-roman-labels.mjs +2 -2
- package/dist/reference/consistency.d.ts +14 -2
- package/dist/reference/consistency.js +108 -4
- package/dist/reference/delimiter-size.d.ts +21 -11
- package/dist/reference/delimiter-size.js +15 -39
- package/dist/reference/diagrams-validate.d.ts +7 -2
- package/dist/reference/diagrams-validate.js +9 -1
- package/dist/reference/equations-validate.d.ts +8 -5
- package/dist/reference/equations-validate.js +1 -0
- package/dist/reference/equations.d.mts +69 -34
- package/dist/reference/equations.mjs +90 -44
- package/dist/reference/examples-validate.d.ts +31 -0
- package/dist/reference/examples-validate.js +48 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/figures-validate.js +4 -1
- package/dist/reference/folds-validate.d.ts +33 -0
- package/dist/reference/folds-validate.js +32 -0
- package/dist/reference/gate-abbreviations.d.ts +3 -1
- package/dist/reference/gate-abbreviations.js +3 -2
- package/dist/reference/gate-course-rules.d.ts +32 -0
- package/dist/reference/gate-course-rules.js +85 -0
- package/dist/reference/gate-heading-length.d.ts +5 -2
- package/dist/reference/gate-heading-length.js +47 -23
- package/dist/reference/gate-heading-structure.d.ts +41 -0
- package/dist/reference/gate-heading-structure.js +113 -0
- package/dist/reference/gate-math-punctuation.d.ts +71 -0
- package/dist/reference/gate-math-punctuation.js +251 -0
- package/dist/reference/gate-math.d.ts +44 -6
- package/dist/reference/gate-math.js +63 -10
- package/dist/reference/gate-part-pages.d.ts +50 -0
- package/dist/reference/gate-part-pages.js +118 -0
- package/dist/reference/gate-prose-notation.d.ts +32 -11
- package/dist/reference/gate-prose-notation.js +30 -8
- package/dist/reference/katex-delimiters.d.mts +15 -0
- package/dist/reference/katex-delimiters.mjs +138 -0
- package/dist/reference/katex-options.mjs +4 -0
- package/dist/reference/keyed-float-validate.d.ts +27 -10
- package/dist/reference/keyed-float-validate.js +15 -1
- package/dist/reference/keyed-jsx-floats.d.mts +11 -5
- package/dist/reference/keyed-jsx-floats.mjs +73 -20
- package/dist/reference/knowledge-checks-validate.d.ts +1 -1
- package/dist/reference/knowledge-checks.d.mts +3 -1
- package/dist/reference/knowledge-checks.mjs +9 -4
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-slot.d.ts +34 -0
- package/dist/reference/lab-math-slot.js +6 -0
- package/dist/reference/math-glyphs.d.mts +80 -4
- package/dist/reference/math-glyphs.mjs +588 -39
- package/dist/reference/page-notation.d.ts +6 -1
- package/dist/reference/raw-floats-validate.js +1 -1
- package/dist/reference/raw-floats.mjs +1 -1
- package/dist/reference/rehype-heading-anchors.d.mts +14 -0
- package/dist/reference/rehype-heading-anchors.mjs +116 -17
- package/dist/reference/rehype-table-header-labels.d.mts +6 -1
- package/dist/reference/rehype-table-header-labels.mjs +21 -3
- package/dist/reference/remark-asides.d.mts +3 -0
- package/dist/reference/remark-asides.mjs +180 -0
- package/dist/reference/remark-memo.d.mts +3 -1
- package/dist/reference/remark-memo.mjs +42 -11
- package/dist/reference/remark-notation.d.mts +60 -10
- package/dist/reference/remark-notation.mjs +387 -36
- package/dist/reference/render-lab-math.d.ts +4 -26
- package/dist/reference/render-lab-math.js +5 -5
- package/dist/reference/sections-validate.d.ts +27 -0
- package/dist/reference/sections-validate.js +41 -0
- package/dist/reference/sections.d.mts +97 -0
- package/dist/reference/sections.mjs +306 -0
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +24 -1
- package/dist/reference/unit-words.d.ts +37 -0
- package/dist/reference/unit-words.js +170 -0
- package/dist/rules/catalog.d.ts +176 -0
- package/dist/rules/catalog.js +983 -0
- package/dist/rules/course-rules.d.ts +115 -0
- package/dist/rules/course-rules.js +292 -0
- package/dist/rules/course-rules.schema.json +138 -0
- package/dist/rules/in-effect.d.ts +53 -0
- package/dist/rules/in-effect.js +142 -0
- package/dist/rules/rules.schema.json +1072 -0
- package/dist/rules/settings.d.ts +3 -0
- package/dist/rules/settings.js +290 -0
- package/dist/rules/waivers.d.ts +53 -0
- package/dist/rules/waivers.js +132 -0
- package/dist/session/PreferenceStore.d.ts +5 -0
- package/dist/session/PreferenceStore.js +9 -0
- package/docs/VERSIONING.md +2 -2
- package/docs/authoring/README.md +232 -40
- package/docs/authoring/agents.md +55 -9
- package/docs/authoring/components.md +178 -22
- package/docs/authoring/migrating.md +1324 -2
- package/docs/authoring/notation.md +616 -344
- package/docs/authoring/prose.md +551 -0
- package/docs/authoring/retired.md +35 -0
- package/docs/authoring/structure-rules.md +422 -40
- package/docs/authoring/structure.md +445 -62
- package/docs/authoring/versioning.md +20 -28
- package/docs/authoring/visualizations.md +142 -20
- package/docs/katex-bracket-scaling.md +191 -0
- package/package.json +3 -2
- package/src/compiler/cli.ts +139 -6
- package/src/compiler/collections.ts +144 -10
- package/src/compiler/course-config.ts +273 -34
- package/src/compiler/knowledge-version.ts +1 -1
- package/src/compiler/lesson-derivation.ts +54 -15
- package/src/compiler/logging.ts +21 -4
- package/src/compiler/manifest.ts +573 -104
- package/src/components/BlockHeading.astro +113 -0
- package/src/components/CourseContents.astro +221 -0
- package/src/components/CurriculumMap.astro +5 -6
- package/src/components/FloatIndex.astro +200 -0
- package/src/components/HoverPanel.astro +431 -79
- package/src/components/PartBridge.astro +27 -0
- package/src/components/PartOverview.astro +401 -0
- package/src/components/PartScope.astro +27 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
- package/src/components/assessments/AssessmentRunner.tsx +8 -5
- package/src/components/assessments/AssessmentSet.astro +23 -26
- package/src/components/block-heading.ts +146 -0
- package/src/components/card-arrow.ts +243 -0
- package/src/components/card-drag.ts +631 -0
- package/src/components/card-jump.ts +45 -0
- package/src/components/card-manager.ts +1589 -0
- package/src/components/card-memory.ts +177 -0
- package/src/components/card-placement.ts +504 -0
- package/src/components/card-side.ts +336 -0
- package/src/components/card-travel.ts +197 -0
- package/src/components/citation/CitationLayer.astro +77 -76
- package/src/components/diagram/Diagram.astro +161 -316
- package/src/components/disclosure/course-disclosure.ts +24 -0
- package/src/components/disclosure/hover-preview.ts +122 -42
- package/src/components/disclosure/persisted-disclosure.ts +3 -3
- package/src/components/disclosure/print-as-shown.ts +252 -0
- package/src/components/examples/CompactExample.astro +3 -1
- package/src/components/examples/CompactExamples.astro +101 -94
- package/src/components/figure/Figure.astro +174 -274
- package/src/components/figure/FloatCaption.astro +87 -0
- package/src/components/figure/figure-state.ts +58 -0
- package/src/components/figure/figure-tools.ts +87 -0
- package/src/components/float-name.ts +101 -0
- package/src/components/fold/Fold.astro +125 -0
- package/src/components/fold/fold-id.ts +40 -0
- package/src/components/memo/Memo.astro +162 -231
- package/src/components/memo/ShortNote.astro +6 -6
- package/src/components/notation/NotationFormula.astro +5 -1
- package/src/components/notation/NotationGlossary.astro +24 -14
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/notation/render-notation-math.ts +111 -10
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +22 -6
- package/src/components/starlight/CollapsibleSections.astro +207 -93
- package/src/components/starlight/DiagramEnhancer.astro +15 -138
- package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
- package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
- package/src/components/starlight/LayoutHeader.astro +298 -19
- package/src/components/starlight/LayoutPageSidebar.astro +99 -12
- package/src/components/starlight/LayoutPageTitle.astro +120 -0
- package/src/components/starlight/LayoutSidebar.astro +229 -11
- package/src/components/starlight/LessonExportActions.astro +53 -72
- package/src/components/starlight/LessonFooter.astro +26 -4
- package/src/components/starlight/section-parts.ts +15 -2
- package/src/components/starlight/section-summary.ts +181 -0
- package/src/components/table/Table.astro +95 -76
- package/src/components/table/TableOverflowExpander.astro +65 -28
- package/src/content-config.ts +40 -4
- package/src/reference/base-accents.mjs +31 -0
- package/src/reference/base-roman-labels.mjs +2 -2
- package/src/reference/consistency.ts +176 -5
- package/src/reference/delimiter-size.ts +40 -61
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +90 -44
- package/src/reference/examples-validate.ts +97 -0
- package/src/reference/figures-validate.ts +6 -1
- package/src/reference/folds-validate.ts +65 -0
- package/src/reference/gate-abbreviations.ts +3 -2
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +68 -37
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +119 -12
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +74 -13
- package/src/reference/katex-delimiters.mjs +138 -0
- package/src/reference/katex-options.mjs +4 -0
- package/src/reference/keyed-float-validate.ts +45 -11
- package/src/reference/keyed-jsx-floats.mjs +73 -20
- package/src/reference/knowledge-checks-validate.ts +1 -1
- package/src/reference/knowledge-checks.mjs +9 -4
- package/src/reference/lab-math-slot.ts +36 -0
- package/src/reference/math-glyphs.mjs +588 -39
- package/src/reference/raw-floats-validate.ts +1 -1
- package/src/reference/raw-floats.mjs +1 -1
- package/src/reference/rehype-heading-anchors.mjs +116 -17
- package/src/reference/rehype-table-header-labels.mjs +21 -3
- package/src/reference/remark-asides.mjs +180 -0
- package/src/reference/remark-memo.mjs +42 -11
- package/src/reference/remark-notation.mjs +387 -36
- package/src/reference/render-lab-math.ts +17 -32
- package/src/reference/sections-validate.ts +81 -0
- package/src/reference/sections.mjs +306 -0
- package/src/reference/tables-validate.ts +2 -1
- package/src/reference/types.ts +24 -0
- package/src/reference/unit-words.ts +233 -0
- package/src/rules/catalog.ts +1298 -0
- package/src/rules/course-rules.ts +456 -0
- package/src/rules/in-effect.ts +235 -0
- package/src/rules/settings.ts +448 -0
- package/src/rules/waivers.ts +249 -0
- package/src/session/PreferenceStore.ts +15 -0
- package/src/styles/global.css +1471 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- 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(/"/g, '"')
|
|
73
|
+
.replace(/</g, '<')
|
|
74
|
+
.replace(/>/g, '>')
|
|
75
|
+
.replace(/&/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
|
+
}
|