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.
- package/CHANGELOG.md +1239 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +46 -2
- package/dist/compiler/cli.js +70 -4
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +68 -5
- 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 +4 -1
- package/dist/compiler/logging.js +4 -1
- 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 +3 -2
- 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 +60 -31
- package/dist/reference/equations.mjs +83 -43
- 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 +1 -0
- 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 +45 -21
- 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 +22 -6
- package/dist/reference/gate-math.js +42 -8
- 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 +21 -11
- package/dist/reference/gate-prose-notation.js +25 -7
- package/dist/reference/katex-delimiters.mjs +20 -7
- 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 +572 -37
- 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 +4 -4
- 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 +17 -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 +1096 -2
- package/docs/authoring/notation.md +576 -322
- 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/package.json +2 -2
- package/src/compiler/cli.ts +116 -6
- package/src/compiler/collections.ts +111 -7
- 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 +8 -2
- 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 +3 -4
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +5 -4
- 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 +3 -2
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +83 -43
- 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 +1 -0
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +66 -35
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +69 -10
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +59 -12
- package/src/reference/katex-delimiters.mjs +20 -7
- 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 +572 -37
- 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 +16 -31
- 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 +17 -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
|
@@ -10,6 +10,24 @@
|
|
|
10
10
|
* sticky-header `scroll-margin`.
|
|
11
11
|
*
|
|
12
12
|
* This script only adds what CSS and anchors cannot:
|
|
13
|
+
* - the chain icon copies the section's link: one click puts the page URL
|
|
14
|
+
* with the heading's named anchor (never its number) on the clipboard and
|
|
15
|
+
* says so beside the icon — "Link copied · § 1.1.2 · Velocity over time",
|
|
16
|
+
* the number read off the heading's `data-sec-number`
|
|
17
|
+
* (docs/plans/titles-and-containers.md, Q2/Q3). On a narrow screen, where
|
|
18
|
+
* headings show no number, this is where the number surfaces. The link is
|
|
19
|
+
* a named one (docs/plans/figures-and-short-captions.md, decision 9.3):
|
|
20
|
+
* beside the URL (`text/plain`), the clipboard holds an `<a>` reading
|
|
21
|
+
* "§ 1.1.2 Velocity over time" (`text/html`) where the browser has
|
|
22
|
+
* `ClipboardItem`, so a paste into a document keeps the name. The click
|
|
23
|
+
* still navigates, so the hash and the highlight below follow as before;
|
|
24
|
+
* a clipboard that is missing or refuses leaves exactly that navigation.
|
|
25
|
+
* On hover the icon says "Copy link", a title this script sets, since
|
|
26
|
+
* only with it does a click copy. A float's chain icon in its caption
|
|
27
|
+
* line (`a[data-copy-link]`, `FloatCaption.astro`) copies the same way,
|
|
28
|
+
* named by its number and short caption — "Link copied · Figure 2.2.1 ·
|
|
29
|
+
* Projectile trajectory explorer", pasted as "Figure 2.2.1, Projectile
|
|
30
|
+
* trajectory explorer"; its landing is `KeyedFloatEnhancer`'s;
|
|
13
31
|
* - a smooth, header-aware scroll on an in-page jump;
|
|
14
32
|
* - a brief highlight of the section body (the heading's following siblings up
|
|
15
33
|
* to the next heading of equal or higher rank), so a deep link lands the
|
|
@@ -38,6 +56,7 @@
|
|
|
38
56
|
<script>
|
|
39
57
|
import { HEADINGS, sectionParts } from './section-parts';
|
|
40
58
|
import { animatedScrollIntoView } from '../scroll-animation';
|
|
59
|
+
import { floatOf } from '../float-name';
|
|
41
60
|
|
|
42
61
|
(() => {
|
|
43
62
|
const root = document.querySelector('.sl-markdown-content');
|
|
@@ -90,14 +109,187 @@
|
|
|
90
109
|
}
|
|
91
110
|
};
|
|
92
111
|
|
|
112
|
+
// The link a reader can share: the page's canonical URL when it declares
|
|
113
|
+
// one (Starlight does once the site has a `site` URL), else this page
|
|
114
|
+
// without its query — and the heading's named anchor, never a number.
|
|
115
|
+
const shareableUrl = (id: string): string => {
|
|
116
|
+
const canonical = document.querySelector<HTMLLinkElement>(
|
|
117
|
+
'link[rel="canonical"]',
|
|
118
|
+
)?.href;
|
|
119
|
+
const url = new URL(canonical || location.href);
|
|
120
|
+
if (!canonical) url.search = '';
|
|
121
|
+
url.hash = id;
|
|
122
|
+
return url.href;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Screen readers hear the confirmation from one polite live region; the
|
|
126
|
+
// bubble beside the icon is for the eye only.
|
|
127
|
+
const status = document.createElement('div');
|
|
128
|
+
status.className = 'sr-only';
|
|
129
|
+
status.setAttribute('role', 'status');
|
|
130
|
+
document.body.append(status);
|
|
131
|
+
|
|
132
|
+
const COPIED_MS = 1800;
|
|
133
|
+
let bubble: HTMLElement | null = null;
|
|
134
|
+
let bubbleTimer = 0;
|
|
135
|
+
|
|
136
|
+
// What a link is called: its kind and number ("§ 1.1.2", "Figure 2.2.1")
|
|
137
|
+
// and its title — a heading's text, a check's or example set's topic, a
|
|
138
|
+
// float's short caption. Either can be missing.
|
|
139
|
+
interface LinkName {
|
|
140
|
+
readonly label: string;
|
|
141
|
+
readonly title: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const headingName = (heading: HTMLElement): LinkName => {
|
|
145
|
+
const { secNumber, checkNumber, exNumber, memoNumber } = heading.dataset;
|
|
146
|
+
const label = secNumber
|
|
147
|
+
? `§ ${secNumber}`
|
|
148
|
+
: checkNumber
|
|
149
|
+
? `Knowledge check ${checkNumber}`
|
|
150
|
+
: exNumber
|
|
151
|
+
? `Example ${exNumber}`
|
|
152
|
+
: memoNumber
|
|
153
|
+
? `Memo ${memoNumber}`
|
|
154
|
+
: '';
|
|
155
|
+
// A memo goes by its short caption where it has one, as its marker's
|
|
156
|
+
// tooltip does; a check's or example set's heading also draws its kind
|
|
157
|
+
// and number, so only its topic counts.
|
|
158
|
+
const short = memoNumber
|
|
159
|
+
? heading.closest<HTMLElement>('[data-memo-short]')?.dataset.memoShort
|
|
160
|
+
: undefined;
|
|
161
|
+
const titled = heading.querySelector('.block-heading__title') ?? heading;
|
|
162
|
+
return {
|
|
163
|
+
label,
|
|
164
|
+
title: short || (titled.textContent?.replace(/\s+/g, ' ').trim() ?? ''),
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const floatName = (link: HTMLElement): LinkName => {
|
|
169
|
+
const root = link.closest('figure');
|
|
170
|
+
return {
|
|
171
|
+
label: link.dataset.copyLink ?? '',
|
|
172
|
+
title: (root && floatOf(root)?.short) || '',
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const confirmation = ({ label, title }: LinkName): string =>
|
|
177
|
+
['Link copied', label, title].filter(Boolean).join(' · ');
|
|
178
|
+
|
|
179
|
+
// "§ 1.1.2 Velocity over time", "Figure 2.2.1, Projectile trajectory
|
|
180
|
+
// explorer": the text a pasted link reads.
|
|
181
|
+
const linkText = ({ label, title }: LinkName): string =>
|
|
182
|
+
!title ? label : !label ? title : label.startsWith('§') ? `${label} ${title}` : `${label}, ${title}`;
|
|
183
|
+
|
|
184
|
+
// `below`: the viewport y the bubble hangs from — under the icon, or
|
|
185
|
+
// under a heading's whole title when that wraps lower.
|
|
186
|
+
const confirmCopied = (link: HTMLElement, message: string, below: number) => {
|
|
187
|
+
status.textContent = message;
|
|
188
|
+
|
|
189
|
+
bubble?.remove();
|
|
190
|
+
window.clearTimeout(bubbleTimer);
|
|
191
|
+
const shown = document.createElement('span');
|
|
192
|
+
shown.className = 'sl-anchor-copied';
|
|
193
|
+
shown.setAttribute('aria-hidden', 'true');
|
|
194
|
+
shown.textContent = message;
|
|
195
|
+
document.body.append(shown);
|
|
196
|
+
// Under the title — a long one wraps below its icon — with its end edge
|
|
197
|
+
// on the icon's, and never past either side of the text column: a short
|
|
198
|
+
// heading's icon sits near the start.
|
|
199
|
+
const icon = link.getBoundingClientRect();
|
|
200
|
+
const column = root.getBoundingClientRect();
|
|
201
|
+
const width = shown.offsetWidth;
|
|
202
|
+
const left = Math.max(
|
|
203
|
+
column.left,
|
|
204
|
+
Math.min(icon.right - width, column.right - width),
|
|
205
|
+
);
|
|
206
|
+
// Page coordinates, so the bubble travels with the heading while the
|
|
207
|
+
// click's own navigation scrolls it into place.
|
|
208
|
+
shown.style.left = `${Math.round(left + window.scrollX)}px`;
|
|
209
|
+
shown.style.top = `${Math.round(below + window.scrollY + 4)}px`;
|
|
210
|
+
bubble = shown;
|
|
211
|
+
bubbleTimer = window.setTimeout(() => {
|
|
212
|
+
shown.remove();
|
|
213
|
+
if (bubble === shown) bubble = null;
|
|
214
|
+
}, COPIED_MS);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// The URL as plain text, and — where the browser can hold two flavours —
|
|
218
|
+
// a named link beside it, for a paste into a document.
|
|
219
|
+
const copyLink = (id: string, name: LinkName, confirm: () => void) => {
|
|
220
|
+
const clipboard = navigator.clipboard;
|
|
221
|
+
if (!clipboard?.writeText) return;
|
|
222
|
+
const url = shareableUrl(id);
|
|
223
|
+
const text = linkText(name);
|
|
224
|
+
let written: Promise<void>;
|
|
225
|
+
if (text && typeof ClipboardItem === 'function' && typeof clipboard.write === 'function') {
|
|
226
|
+
const named = document.createElement('a');
|
|
227
|
+
named.href = url;
|
|
228
|
+
named.textContent = text;
|
|
229
|
+
written = clipboard.write([
|
|
230
|
+
new ClipboardItem({
|
|
231
|
+
'text/plain': new Blob([url], { type: 'text/plain' }),
|
|
232
|
+
'text/html': new Blob([named.outerHTML], { type: 'text/html' }),
|
|
233
|
+
}),
|
|
234
|
+
]);
|
|
235
|
+
} else {
|
|
236
|
+
written = clipboard.writeText(url);
|
|
237
|
+
}
|
|
238
|
+
written.then(confirm, () => {
|
|
239
|
+
/* Refused: the navigation still happens, as it always has. */
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// A heading's chain icon says on hover that a click copies, as a float's
|
|
244
|
+
// does in its own markup (`FloatCaption.astro`). The markup cannot say it
|
|
245
|
+
// here: without this script, or without a clipboard, the icon only
|
|
246
|
+
// navigates. So the title goes on exactly the icons the click handler
|
|
247
|
+
// below copies from — every heading's, including the ones
|
|
248
|
+
// `BlockHeading.astro` and `Memo.astro` draw.
|
|
249
|
+
if (typeof navigator.clipboard?.writeText === 'function') {
|
|
250
|
+
for (const link of root.querySelectorAll<HTMLElement>('a.sl-anchor-link')) {
|
|
251
|
+
const href = link.getAttribute('href') || '';
|
|
252
|
+
if (href.startsWith('#') && headingForId(href.slice(1))) {
|
|
253
|
+
link.title = 'Copy link';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
93
258
|
root.addEventListener('click', (event) => {
|
|
94
259
|
const origin = event.target instanceof Element ? event.target : null;
|
|
95
|
-
|
|
260
|
+
|
|
261
|
+
// A float's chain icon: copy, confirm, and let the click navigate.
|
|
262
|
+
const floatLink = origin?.closest<HTMLElement>('a[data-copy-link]');
|
|
263
|
+
const floatHref = floatLink?.getAttribute('href') || '';
|
|
264
|
+
if (floatLink && floatHref.startsWith('#')) {
|
|
265
|
+
const name = floatName(floatLink);
|
|
266
|
+
copyLink(floatHref.slice(1), name, () =>
|
|
267
|
+
confirmCopied(
|
|
268
|
+
floatLink,
|
|
269
|
+
confirmation(name),
|
|
270
|
+
floatLink.getBoundingClientRect().bottom,
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const link = origin?.closest<HTMLElement>('a.sl-anchor-link');
|
|
96
277
|
if (!link) return;
|
|
97
278
|
const href = link.getAttribute('href') || '';
|
|
98
279
|
if (!href.startsWith('#')) return;
|
|
99
280
|
const heading = headingForId(href.slice(1));
|
|
100
281
|
if (!heading) return;
|
|
282
|
+
const name = headingName(heading);
|
|
283
|
+
copyLink(heading.id, name, () =>
|
|
284
|
+
confirmCopied(
|
|
285
|
+
link,
|
|
286
|
+
confirmation(name),
|
|
287
|
+
Math.max(
|
|
288
|
+
link.getBoundingClientRect().bottom,
|
|
289
|
+
heading.getBoundingClientRect().bottom,
|
|
290
|
+
),
|
|
291
|
+
),
|
|
292
|
+
);
|
|
101
293
|
// Only the already-on-this-anchor click needs handling here — see
|
|
102
294
|
// `KeyedFloatEnhancer.astro`. Every other click is left to the browser's
|
|
103
295
|
// own fragment navigation, picked up below via `hashchange`.
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* activated again — the one case a plain anchor click does nothing for,
|
|
21
21
|
* since the browser won't navigate to a fragment that's already current;
|
|
22
22
|
* - moving keyboard focus onto the float so screen-reader users land on it;
|
|
23
|
+
* - making a display equation that overflows its column keyboard-scrollable;
|
|
23
24
|
* - for an equation only, marking its number permanently visible
|
|
24
25
|
* (`.eq-number-revealed`, styled in `global.css`) once the reader has
|
|
25
26
|
* actually arrived at it, so it doesn't fade back down once the
|
|
@@ -36,9 +37,31 @@
|
|
|
36
37
|
*
|
|
37
38
|
* Everything here is gated on `prefers-reduced-motion` and degrades to the
|
|
38
39
|
* native anchor behaviour if it throws.
|
|
40
|
+
*
|
|
41
|
+
* **Number aliases** (docs/plans/titles-and-containers.md, Q4). With the
|
|
42
|
+
* course's `numbering.aliases` on (the `aliases` prop, from the manifest), a
|
|
43
|
+
* fragment a reader types in number form — `#sec-1.1.2`, `#fig-2.2.1`,
|
|
44
|
+
* `#eq-2.2.4`, `#tab-…`, `#dia-…`, `#check-…`, `#ex-…` — is rewritten to the named
|
|
45
|
+
* anchor of the element that prints that number here. The map is the page
|
|
46
|
+
* itself: every numbered element already carries `data-‹kind›-number` beside
|
|
47
|
+
* its `id`, so an alias resolves exactly when the reader can see its number
|
|
48
|
+
* on this page, and never disagrees with it. Inbound only: nothing the site
|
|
49
|
+
* shows or copies is a number form, and an alias is only as stable as the
|
|
50
|
+
* content version the About panel names. This script runs before
|
|
51
|
+
* `CollapsibleSections` and `HeadingAnchorEnhancer` (see `LessonFooter`), so
|
|
52
|
+
* on arrival the rewrite is in place before either reads the hash — an alias
|
|
53
|
+
* lands exactly like a named deep link.
|
|
39
54
|
*/
|
|
55
|
+
interface Props {
|
|
56
|
+
/** `manifest.numbering.aliases`: resolve number-form fragments. */
|
|
57
|
+
aliases?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { aliases = false } = Astro.props;
|
|
40
61
|
---
|
|
41
62
|
|
|
63
|
+
{aliases && <div hidden data-explico-number-aliases />}
|
|
64
|
+
|
|
42
65
|
<script>
|
|
43
66
|
import { animatedScrollIntoView } from '../scroll-animation';
|
|
44
67
|
|
|
@@ -46,6 +69,21 @@
|
|
|
46
69
|
const root = document.querySelector('.sl-markdown-content');
|
|
47
70
|
if (!root) return;
|
|
48
71
|
|
|
72
|
+
// `#sec-1.1.2` → the element printing that number, if this page has one.
|
|
73
|
+
const ALIAS = /^#(sec|eq|tab|fig|dia|check|ex)-(\d+(?:\.\d+)+)$/;
|
|
74
|
+
const aliasesOn = document.querySelector('[data-explico-number-aliases]') !== null;
|
|
75
|
+
const aliasTarget = (hash: string): HTMLElement | null => {
|
|
76
|
+
const match = aliasesOn ? ALIAS.exec(hash) : null;
|
|
77
|
+
if (!match) return null;
|
|
78
|
+
return root.querySelector<HTMLElement>(
|
|
79
|
+
`[id][data-${match[1]}-number="${match[2]}"]`,
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
// On arrival: rewrite without an event, so every enhancer's own
|
|
83
|
+
// first-hash handling sees the named anchor.
|
|
84
|
+
const arrived = aliasTarget(location.hash);
|
|
85
|
+
if (arrived) history.replaceState(history.state, '', `#${arrived.id}`);
|
|
86
|
+
|
|
49
87
|
interface Kind {
|
|
50
88
|
readonly prefix: string;
|
|
51
89
|
readonly triggerSelector: string;
|
|
@@ -60,21 +98,23 @@
|
|
|
60
98
|
numberSelector: '.keyed-equation__number',
|
|
61
99
|
reveal: true,
|
|
62
100
|
},
|
|
101
|
+
// A float's caption line links to it twice: its number, and the chain
|
|
102
|
+
// icon beside it (which also copies the link, `HeadingAnchorEnhancer`).
|
|
63
103
|
{
|
|
64
104
|
prefix: '#tab-',
|
|
65
|
-
triggerSelector: 'a.table-ref, a.keyed-table__number',
|
|
105
|
+
triggerSelector: 'a.table-ref, a.keyed-table__number, a.float-caption__anchor',
|
|
66
106
|
numberSelector: '.keyed-table__number',
|
|
67
107
|
reveal: false,
|
|
68
108
|
},
|
|
69
109
|
{
|
|
70
110
|
prefix: '#fig-',
|
|
71
|
-
triggerSelector: 'a.figure-ref, a.keyed-figure__number',
|
|
111
|
+
triggerSelector: 'a.figure-ref, a.keyed-figure__number, a.float-caption__anchor',
|
|
72
112
|
numberSelector: '.keyed-figure__number',
|
|
73
113
|
reveal: false,
|
|
74
114
|
},
|
|
75
115
|
{
|
|
76
116
|
prefix: '#dia-',
|
|
77
|
-
triggerSelector: 'a.diagram-ref, a.
|
|
117
|
+
triggerSelector: 'a.diagram-ref, a.diagram__number, a.float-caption__anchor',
|
|
78
118
|
numberSelector: '.diagram__number',
|
|
79
119
|
reveal: false,
|
|
80
120
|
},
|
|
@@ -178,12 +218,14 @@
|
|
|
178
218
|
|
|
179
219
|
// A tooltip for the gesture above, and only for the part of the equation
|
|
180
220
|
// it actually applies to — an explained symbol has its own hover card
|
|
181
|
-
// (`NotationLayer.astro`) and needs no tooltip layered on top of it.
|
|
221
|
+
// (`NotationLayer.astro`) and needs no tooltip layered on top of it. It
|
|
222
|
+
// names the equation by its `\caption{}` short caption when it has one —
|
|
223
|
+
// "Copy LaTeX — Range formula", the same title as its copy button.
|
|
182
224
|
// `mouseover`/`mouseout` (not `mouseenter`/`mouseleave`) because they
|
|
183
225
|
// bubble, which delegation on `root` needs.
|
|
184
226
|
root.addEventListener('mouseover', (event) => {
|
|
185
227
|
const origin = event.target instanceof Element ? event.target : null;
|
|
186
|
-
if (!origin || origin.getAttribute('title')
|
|
228
|
+
if (!origin || origin.getAttribute('title')?.startsWith('Copy LaTeX')) return;
|
|
187
229
|
if (
|
|
188
230
|
origin.closest(
|
|
189
231
|
'[data-notation-key], [data-eq-copy], a.keyed-equation__number',
|
|
@@ -191,18 +233,67 @@
|
|
|
191
233
|
) {
|
|
192
234
|
return;
|
|
193
235
|
}
|
|
194
|
-
|
|
195
|
-
|
|
236
|
+
const equation = origin.closest<HTMLElement>('.keyed-equation, .inline-equation');
|
|
237
|
+
if (!equation) return;
|
|
238
|
+
const short = equation.dataset.eqShort;
|
|
239
|
+
origin.setAttribute('title', short ? `Copy LaTeX — ${short}` : 'Copy LaTeX');
|
|
240
|
+
origin.setAttribute('data-copy-hint', '');
|
|
196
241
|
});
|
|
197
242
|
|
|
198
243
|
root.addEventListener('mouseout', (event) => {
|
|
199
244
|
const origin = event.target instanceof Element ? event.target : null;
|
|
200
|
-
|
|
245
|
+
// Only a title this script added — the copy button's own static
|
|
246
|
+
// `title` must survive the pointer leaving it (or crossing onto its icon).
|
|
247
|
+
if (origin?.hasAttribute('data-copy-hint')) {
|
|
201
248
|
origin.removeAttribute('title');
|
|
249
|
+
origin.removeAttribute('data-copy-hint');
|
|
202
250
|
}
|
|
203
251
|
});
|
|
204
252
|
|
|
205
|
-
|
|
253
|
+
// A display equation wider than the column scrolls sideways
|
|
254
|
+
// (`.katex-display { overflow-x: auto }` in `global.css`) — on a phone,
|
|
255
|
+
// with a numbered equation's controls beside it, that is common. A
|
|
256
|
+
// scrollable region must be keyboard-reachable (WCAG 2.1.1, axe's
|
|
257
|
+
// `scrollable-region-focusable`), so, the same way
|
|
258
|
+
// `TableOverflowExpander.astro` and `DiagramEnhancer.astro` treat their
|
|
259
|
+
// scrollers, it gets a `tabindex` and a name only while it overflows. A
|
|
260
|
+
// `ResizeObserver` catches every case that changes that: first layout, a
|
|
261
|
+
// viewport change, and a collapsed section opening (no layout box while
|
|
262
|
+
// closed, so no overflow either).
|
|
263
|
+
const updateEquationScroll = (display: HTMLElement) => {
|
|
264
|
+
if (display.scrollWidth > display.clientWidth + 1) {
|
|
265
|
+
const number = display
|
|
266
|
+
.closest<HTMLElement>('.keyed-equation')
|
|
267
|
+
?.dataset.eqNumber;
|
|
268
|
+
display.tabIndex = 0;
|
|
269
|
+
display.setAttribute('role', 'group');
|
|
270
|
+
display.setAttribute(
|
|
271
|
+
'aria-label',
|
|
272
|
+
number ? `Scrollable equation (${number})` : 'Scrollable equation',
|
|
273
|
+
);
|
|
274
|
+
} else {
|
|
275
|
+
display.removeAttribute('tabindex');
|
|
276
|
+
display.removeAttribute('role');
|
|
277
|
+
display.removeAttribute('aria-label');
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const equationResize = new ResizeObserver((entries) => {
|
|
281
|
+
for (const entry of entries) updateEquationScroll(entry.target as HTMLElement);
|
|
282
|
+
});
|
|
283
|
+
for (const display of root.querySelectorAll<HTMLElement>('.katex-display')) {
|
|
284
|
+
equationResize.observe(display);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
window.addEventListener('hashchange', (event) => {
|
|
288
|
+
// A number form typed into the address bar of a loaded page: navigate
|
|
289
|
+
// on to the named anchor. The listeners registered after this one
|
|
290
|
+
// (sections, headings) see only that second navigation, not this one.
|
|
291
|
+
const aliased = aliasTarget(location.hash);
|
|
292
|
+
if (aliased) {
|
|
293
|
+
event.stopImmediatePropagation();
|
|
294
|
+
location.replace(`#${aliased.id}`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
206
297
|
const kind = kindForHash(location.hash);
|
|
207
298
|
if (!kind) return;
|
|
208
299
|
activate(document.getElementById(location.hash.slice(1)), true, kind);
|