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
|
@@ -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
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
><<<</span
|
|
35
|
+
>
|
|
36
|
+
<span class="layout-edge-glyph layout-edge-glyph-restore" aria-hidden="true"
|
|
37
|
+
>>>></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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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?: {
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
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
|
}
|