explico 0.4.0 → 0.5.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 +273 -1
- package/README.md +103 -8
- package/dist/compiler/astro.d.ts +9 -1
- package/dist/compiler/astro.js +182 -3
- package/dist/compiler/cli.d.ts +13 -0
- package/dist/compiler/cli.js +28 -4
- package/dist/compiler/collections.d.ts +3 -2
- package/dist/compiler/collections.js +28 -1
- package/dist/compiler/course-config.d.ts +58 -1
- package/dist/compiler/course-config.js +28 -1
- package/dist/compiler/exports/standalone-html.d.ts +34 -0
- package/dist/compiler/exports/standalone-html.js +803 -0
- package/dist/compiler/manifest.d.ts +90 -6
- package/dist/compiler/manifest.js +150 -17
- package/dist/compiler/resolution-report.d.ts +4 -0
- package/dist/reference/base-abbreviations.d.mts +30 -0
- package/dist/reference/base-abbreviations.mjs +60 -0
- package/dist/reference/base-operators.d.mts +15 -0
- package/dist/reference/base-operators.mjs +40 -0
- package/dist/reference/base-roman-labels.d.mts +35 -0
- package/dist/reference/base-roman-labels.mjs +43 -0
- package/dist/reference/consistency.d.ts +8 -2
- package/dist/reference/consistency.js +50 -3
- package/dist/reference/diagrams-validate.d.ts +21 -0
- package/dist/reference/diagrams-validate.js +13 -0
- package/dist/reference/equations.d.mts +10 -15
- package/dist/reference/equations.mjs +67 -14
- package/dist/reference/gate-abbreviations.d.ts +17 -0
- package/dist/reference/gate-abbreviations.js +64 -0
- package/dist/reference/gate-math.d.ts +28 -5
- package/dist/reference/gate-math.js +5 -2
- package/dist/reference/gate-prose-notation.d.ts +60 -0
- package/dist/reference/gate-prose-notation.js +125 -0
- package/dist/reference/keyed-float-validate.d.ts +5 -5
- package/dist/reference/keyed-jsx-floats.d.mts +7 -12
- package/dist/reference/keyed-jsx-floats.mjs +50 -25
- package/dist/reference/knowledge-checks-validate.d.ts +35 -0
- package/dist/reference/knowledge-checks-validate.js +78 -0
- package/dist/reference/knowledge-checks.d.mts +25 -0
- package/dist/reference/knowledge-checks.mjs +45 -0
- package/dist/reference/lab-math-scope.d.ts +9 -1
- package/dist/reference/lab-math-scope.js +32 -4
- package/dist/reference/math-glyphs.d.mts +30 -0
- package/dist/reference/math-glyphs.mjs +367 -0
- package/dist/reference/mdx-attributes.d.mts +13 -0
- package/dist/reference/mdx-attributes.mjs +38 -0
- package/dist/reference/mixed-content.d.ts +17 -0
- package/dist/reference/mixed-content.js +29 -0
- package/dist/reference/page-notation.d.ts +52 -0
- package/dist/reference/page-notation.js +171 -0
- package/dist/reference/raw-floats-validate.d.ts +18 -0
- package/dist/reference/raw-floats-validate.js +53 -0
- package/dist/reference/raw-floats.d.mts +18 -0
- package/dist/reference/raw-floats.mjs +129 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -1
- package/dist/reference/rehype-heading-anchors.mjs +107 -1
- package/dist/reference/rehype-table-header-labels.d.mts +26 -0
- package/dist/reference/rehype-table-header-labels.mjs +152 -0
- package/dist/reference/remark-abbreviations.d.mts +7 -0
- package/dist/reference/remark-abbreviations.mjs +174 -0
- package/dist/reference/remark-memo.d.mts +16 -0
- package/dist/reference/remark-memo.mjs +339 -0
- package/dist/reference/remark-notation.d.mts +57 -9
- package/dist/reference/remark-notation.mjs +354 -42
- package/dist/reference/remark-short-note.d.mts +4 -0
- package/dist/reference/remark-short-note.mjs +237 -0
- package/dist/reference/types.d.ts +16 -1
- package/dist/reference/units-vocab.js +4 -0
- package/docs/VERSIONING.md +33 -4
- package/docs/authoring/README.md +8 -0
- package/docs/authoring/agents.md +17 -2
- package/docs/authoring/components.md +135 -0
- package/docs/authoring/migrating.md +92 -7
- package/docs/authoring/notation.md +560 -1
- package/docs/authoring/structure.md +120 -8
- package/docs/authoring/visualizations.md +36 -3
- package/package.json +5 -2
- package/src/compiler/astro.ts +208 -3
- package/src/compiler/cli.ts +29 -2
- package/src/compiler/collections.ts +45 -4
- package/src/compiler/course-config.ts +92 -0
- package/src/compiler/exports/standalone-html.ts +963 -0
- package/src/compiler/manifest.ts +268 -22
- package/src/components/HoverPanel.astro +45 -4
- package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
- package/src/components/assessments/AssessmentRunner.tsx +52 -17
- package/src/components/assessments/AssessmentSet.astro +98 -14
- package/src/components/assessments/RenderedContent.tsx +55 -0
- package/src/components/assessments/assessment-randomization.ts +16 -0
- package/src/components/diagram/Diagram.astro +444 -83
- package/src/components/disclosure/hover-preview.ts +202 -0
- package/src/components/disclosure/persisted-disclosure.ts +66 -0
- package/src/components/examples/CompactExamples.astro +2 -0
- package/src/components/figure/Figure.astro +279 -10
- package/src/components/memo/Memo.astro +453 -0
- package/src/components/memo/ShortNote.astro +223 -0
- package/src/components/notation/NotationFormula.astro +18 -1
- package/src/components/notation/NotationLayer.astro +105 -8
- package/src/components/notation/render-notation-math.ts +39 -2
- package/src/components/notation/safe-math-to-react.ts +50 -0
- package/src/components/reference/FloatRefLayer.astro +233 -15
- package/src/components/starlight/CollapsibleSections.astro +461 -0
- package/src/components/starlight/DiagramEnhancer.astro +385 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
- package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
- package/src/components/starlight/LayoutHeader.astro +50 -0
- package/src/components/starlight/LayoutSocialIcons.astro +41 -2
- package/src/components/starlight/LessonExportActions.astro +436 -0
- package/src/components/starlight/LessonFooter.astro +27 -2
- package/src/components/starlight/section-parts.ts +31 -0
- package/src/components/table/Table.astro +293 -7
- package/src/components/table/TableOverflowExpander.astro +489 -0
- package/src/content-config.ts +20 -0
- package/src/reference/base-abbreviations.mjs +60 -0
- package/src/reference/base-operators.mjs +40 -0
- package/src/reference/base-roman-labels.mjs +43 -0
- package/src/reference/consistency.ts +73 -2
- package/src/reference/diagrams-validate.ts +40 -0
- package/src/reference/equations.mjs +67 -14
- package/src/reference/gate-abbreviations.ts +128 -0
- package/src/reference/gate-math.ts +27 -7
- package/src/reference/gate-prose-notation.ts +224 -0
- package/src/reference/keyed-float-validate.ts +5 -5
- package/src/reference/keyed-jsx-floats.mjs +50 -25
- package/src/reference/knowledge-checks-validate.ts +130 -0
- package/src/reference/knowledge-checks.mjs +45 -0
- package/src/reference/lab-math-scope.ts +45 -3
- package/src/reference/math-glyphs.mjs +367 -0
- package/src/reference/mdx-attributes.mjs +38 -0
- package/src/reference/mixed-content.ts +69 -0
- package/src/reference/page-notation.ts +252 -0
- package/src/reference/raw-floats-validate.ts +103 -0
- package/src/reference/raw-floats.mjs +129 -0
- package/src/reference/rehype-heading-anchors.mjs +107 -1
- package/src/reference/rehype-table-header-labels.mjs +152 -0
- package/src/reference/remark-abbreviations.mjs +174 -0
- package/src/reference/remark-memo.mjs +339 -0
- package/src/reference/remark-notation.mjs +354 -42
- package/src/reference/remark-short-note.mjs +237 -0
- package/src/reference/types.ts +16 -0
- package/src/reference/units-vocab.ts +4 -0
- package/src/styles/global.css +996 -50
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Table column narrowing + expand-on-overflow
|
|
4
|
+
* (docs/plans/collapsible-disclosure.md Part 4) — a runtime enhancer, wired
|
|
5
|
+
* from `LessonFooter.astro` alongside the other lesson-page enhancers, the
|
|
6
|
+
* same progressive-enhancement approach `CollapsibleSections.astro` and
|
|
7
|
+
* `HeadingAnchorEnhancer.astro` already use.
|
|
8
|
+
*
|
|
9
|
+
* **Structure**: each candidate container (`.keyed-table__body` from
|
|
10
|
+
* `Table.astro`, or `.table-overflow-body` — created at runtime around a
|
|
11
|
+
* bare markdown table that has none) is turned into a flex row. Normally
|
|
12
|
+
* that row is `[.table-overflow-scroll, .table-overflow-expand-btn]` — the
|
|
13
|
+
* button, present only once the table actually overflows
|
|
14
|
+
* (`evaluateOverflow` toggles `hidden`), is a plain flex sibling rather than
|
|
15
|
+
* an overlay, so `align-items: stretch` (the flex default) makes it exactly
|
|
16
|
+
* as tall as the table for free, and it can never sit on top of table
|
|
17
|
+
* content. While expanded, a second button is prepended — the row becomes
|
|
18
|
+
* `[collapse, .table-overflow-scroll, collapse]`, the "0th and n+1th
|
|
19
|
+
* column" — so *both* collapse affordances are real layout columns the
|
|
20
|
+
* table's own content is pushed clear of, never floating overlays that
|
|
21
|
+
* could cover a cell. `.table-overflow-expand-btn:first-child`/`:last-child`
|
|
22
|
+
* in `global.css` flip which side gets the divider border; the two buttons
|
|
23
|
+
* otherwise share one class. Row striping/hover-highlight
|
|
24
|
+
* (`tbody tr:nth-child(even)`/`:hover`) is a separate, unconditional rule in
|
|
25
|
+
* `global.css` — plain CSS, so it applies with or without this script.
|
|
26
|
+
*
|
|
27
|
+
* **Why a dedicated `.table-overflow-scroll` wrapper**: Starlight's own
|
|
28
|
+
* `markdown.css` gives *every* `<table>` under `.sl-markdown-content` its
|
|
29
|
+
* own `display: block; overflow: auto` — its own independent scroll
|
|
30
|
+
* container. Left in place inside `.table-overflow-scroll`, that inner rule
|
|
31
|
+
* would win first and clip/scroll the table before this wrapper ever saw
|
|
32
|
+
* overflow, leaving `scrollWidth > clientWidth` on it permanently false.
|
|
33
|
+
* `global.css` undoes that per-table rule specifically *inside*
|
|
34
|
+
* `.table-overflow-scroll`, so there is exactly one real scroll surface —
|
|
35
|
+
* the one this component measures and, on click, enlarges. A table with no
|
|
36
|
+
* JS at all never gets this wrapper and keeps Starlight's default per-table
|
|
37
|
+
* scroll as its fallback, unmodified. Column *sizing* was never the bug —
|
|
38
|
+
* `table-layout` is `auto` either way, so columns already size to content —
|
|
39
|
+
* the bug was only about which element ends up owning the scrollbar.
|
|
40
|
+
*
|
|
41
|
+
* A bare markdown table gets the same `.table-overflow-scroll` treatment
|
|
42
|
+
* inside a runtime-created `.table-overflow-body`, skipping anything
|
|
43
|
+
* already inside a `<Table>` or a lab island's own `.lab-data-table`
|
|
44
|
+
* (`ProjectileRangeExplorer.tsx` and friends already give that a dedicated,
|
|
45
|
+
* deliberately-always-scrolling container; wrapping it again would nest two
|
|
46
|
+
* independent scroll containers around the same rows).
|
|
47
|
+
*
|
|
48
|
+
* **Expand**: on click, the actual node that moves to `<body>` and becomes
|
|
49
|
+
* `position: fixed` is `target` — the whole `<figure class="keyed-table">`
|
|
50
|
+
* when there is one (so its `<figcaption>` travels with the expanded view
|
|
51
|
+
* instead of being left behind, orphaned, in the narrow column), or the
|
|
52
|
+
* flex-row container itself for a bare table with no figure. `target`'s
|
|
53
|
+
* left edge lands at the live `.sidebar-pane` edge — not the
|
|
54
|
+
* `--sl-sidebar-width`/`--__toc-width` CSS vars, which don't reflect the
|
|
55
|
+
* sidebar's real, responsive visibility, per `FloatRefLayer.astro`'s
|
|
56
|
+
* `horizontalRegion()`. Its width is only as wide as the table and its two
|
|
57
|
+
* button columns actually need (`.table-overflow-scroll`'s `scrollWidth`,
|
|
58
|
+
* measured before anything moves — that reports the full unclipped content
|
|
59
|
+
* width regardless of how narrow the wrapper currently renders it, plus
|
|
60
|
+
* both `SIDE_COLUMN_PX` button widths). Unlike `FloatRefLayer.astro`'s own
|
|
61
|
+
* card, which deliberately ignores the "On this page" rail's column
|
|
62
|
+
* entirely, this prefers *not* claiming that space (`tocLeftBound()`):
|
|
63
|
+
* offered the choice, it centers itself between the left nav and the
|
|
64
|
+
* rail's left edge, and only spills past that edge — as far as the
|
|
65
|
+
* viewport edge, if it must — once the table genuinely doesn't fit without
|
|
66
|
+
* doing so. That handoff is a continuous function of window width, not a
|
|
67
|
+
* branch between two different layouts: see the comment on `rightBound` in
|
|
68
|
+
* `reposition()` for why nothing jumps at the point where the table starts
|
|
69
|
+
* spilling past the rail. A zero-height placeholder left behind at
|
|
70
|
+
* the original DOM position keeps
|
|
71
|
+
* the page from reflowing, and is swapped back in on collapse
|
|
72
|
+
* (`placeholder.replaceWith(target)`), restoring the exact original spot.
|
|
73
|
+
* Moving the real node rather than `position: fixed`-ing it in place is
|
|
74
|
+
* required, not stylistic: `.main-pane` sets `isolation: isolate`
|
|
75
|
+
* (`TwoColumnContent.astro`), which traps any z-index attempt to paint over
|
|
76
|
+
* the "On this page" rail — `FloatRefLayer.astro` hit the identical trap
|
|
77
|
+
* and documents the same escape.
|
|
78
|
+
*
|
|
79
|
+
* Moving `target` out to `<body>` also moves it out of `.sl-markdown-content`
|
|
80
|
+
* — which is where every bit of Starlight's own table styling (row-line
|
|
81
|
+
* borders, header color) and this file's own zebra-stripe/hover rule live,
|
|
82
|
+
* since they're all written as `.sl-markdown-content …` descendant
|
|
83
|
+
* selectors. Rather than duplicate that styling, `target` is wrapped in a
|
|
84
|
+
* throwaway `<div class="sl-markdown-content">` (itself `display: contents`,
|
|
85
|
+
* so it never generates a box of its own or affects layout) before being
|
|
86
|
+
* appended to `<body>` — a genuine ancestor for CSS purposes, torn down
|
|
87
|
+
* again on collapse.
|
|
88
|
+
*
|
|
89
|
+
* The expanded table tracks the *placeholder's* live position on
|
|
90
|
+
* scroll/resize (where the table would be, were it still in flow) exactly,
|
|
91
|
+
* unclamped — it scrolls with the page the same as the in-flow element it
|
|
92
|
+
* stands in for, sliding up past the header and out the top of the
|
|
93
|
+
* viewport rather than sticking there. Only once that position has
|
|
94
|
+
* scrolled entirely out of the visible band does the expanded table hide
|
|
95
|
+
* (`visibility: hidden`) outright — there is no reason to keep any part of
|
|
96
|
+
* it rendered once the reader has scrolled past it.
|
|
97
|
+
*
|
|
98
|
+
* Only one table is ever expanded at a time, and expanding one closes
|
|
99
|
+
* whichever other overlay — a hover-preview card, a notation popup — was
|
|
100
|
+
* open, via the same `claimActiveSurface`/`releaseActiveSurface` arbiter
|
|
101
|
+
* `FloatRefLayer.astro` and the citation/notation layers already share. That
|
|
102
|
+
* claim is released again immediately rather than held, though: unlike
|
|
103
|
+
* those layers, the table doesn't stay registered as the arbiter's active
|
|
104
|
+
* surface while expanded, so a hover card opening *afterward* — over an
|
|
105
|
+
* explainable quantity inside the table itself, say — can't turn around and
|
|
106
|
+
* force the table closed the way claiming the arbiter normally would. The
|
|
107
|
+
* table closes only through its own explicit gestures — the collapse
|
|
108
|
+
* buttons or Escape — and deliberately *not* an outside click: a reader
|
|
109
|
+
* following a link in the "On this page" rail, or clicking anywhere else to
|
|
110
|
+
* keep reading, should find the table exactly as they left it, not
|
|
111
|
+
* collapsed back to its normal size out from under them.
|
|
112
|
+
*
|
|
113
|
+
* No persisted state here (unlike Parts 1/3): expand/collapse is transient
|
|
114
|
+
* per-view UI, not a reader preference worth remembering across visits.
|
|
115
|
+
*/
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
<script>
|
|
119
|
+
import { claimActiveSurface, releaseActiveSurface } from "../hover-panel";
|
|
120
|
+
|
|
121
|
+
const GROUP = "table-overflow";
|
|
122
|
+
const EDGE_PX = 16; // ~1em at the default root font size
|
|
123
|
+
const WIDTH_BUFFER_PX = 2; // rounding slack so the minimal width never re-triggers a scrollbar
|
|
124
|
+
const SIDE_COLUMN_PX = 24; // must match `.table-overflow-expand-btn`'s `width: 1.5rem`
|
|
125
|
+
|
|
126
|
+
interface Expansion {
|
|
127
|
+
container: HTMLElement;
|
|
128
|
+
target: HTMLElement;
|
|
129
|
+
scroll: HTMLElement;
|
|
130
|
+
placeholder: HTMLElement;
|
|
131
|
+
spoofWrapper: HTMLElement;
|
|
132
|
+
button: HTMLButtonElement;
|
|
133
|
+
leadingButton: HTMLButtonElement;
|
|
134
|
+
reposition: () => void;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let active: Expansion | null = null;
|
|
138
|
+
|
|
139
|
+
function sidebarLeftBound(): number {
|
|
140
|
+
const sidebar = document.querySelector<HTMLElement>(".sidebar-pane");
|
|
141
|
+
const visible =
|
|
142
|
+
!!sidebar && getComputedStyle(sidebar).visibility !== "hidden";
|
|
143
|
+
return visible ? sidebar!.getBoundingClientRect().right + EDGE_PX : EDGE_PX;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Left edge of the "On this page" rail's own rendered box (not
|
|
147
|
+
// `.right-sidebar-container`, the flex row's reserved-but-empty column
|
|
148
|
+
// for it — `.right-sidebar` is `position: fixed`, so its real box can sit
|
|
149
|
+
// narrower than that reservation, and it's the real box this needs to
|
|
150
|
+
// stay clear of), but only while it's actually shown — not the desktop
|
|
151
|
+
// breakpoint's collapsed edge rail (`data-page-sidebar-hidden`, mirroring
|
|
152
|
+
// the left nav's own collapsed-rail affordance) and not the narrow-
|
|
153
|
+
// viewport layout, where there's no real right column at all. `null`
|
|
154
|
+
// means "no rail to avoid" — the table may use the full
|
|
155
|
+
// sidebar-to-viewport-edge region, same as before.
|
|
156
|
+
function tocLeftBound(): number | null {
|
|
157
|
+
const toc = document.querySelector<HTMLElement>(".right-sidebar");
|
|
158
|
+
if (!toc) return null;
|
|
159
|
+
if (document.documentElement.hasAttribute("data-page-sidebar-hidden")) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
if (!window.matchMedia("(min-width: 72rem)").matches) return null;
|
|
163
|
+
return toc.getBoundingClientRect().left;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function captionFor(container: HTMLElement): string {
|
|
167
|
+
const figure = container.closest<HTMLElement>(".keyed-table");
|
|
168
|
+
const text = figure
|
|
169
|
+
?.querySelector(".keyed-table__caption-text")
|
|
170
|
+
?.textContent?.trim();
|
|
171
|
+
const number = figure?.dataset.tabNumber;
|
|
172
|
+
if (number && text) return `Table ${number} — ${text}`;
|
|
173
|
+
return text || "this table";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function updateButtonLabel(
|
|
177
|
+
container: HTMLElement,
|
|
178
|
+
button: HTMLButtonElement,
|
|
179
|
+
expanded: boolean,
|
|
180
|
+
) {
|
|
181
|
+
button.textContent = expanded ? "⟨" : "⇔";
|
|
182
|
+
button.setAttribute(
|
|
183
|
+
"aria-label",
|
|
184
|
+
expanded
|
|
185
|
+
? `Collapse ${captionFor(container)}`
|
|
186
|
+
: `Expand ${captionFor(container)} to full width`,
|
|
187
|
+
);
|
|
188
|
+
button.title = expanded ? "Collapse" : "Expand to full width";
|
|
189
|
+
button.setAttribute("aria-expanded", String(expanded));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// A scrollable region must itself be reachable by keyboard (WCAG 2.1.1 —
|
|
193
|
+
// axe's `scrollable-region-focusable`), so arrow keys can scroll it once
|
|
194
|
+
// focused; a `tabindex` is only added while it is actually scrollable, not
|
|
195
|
+
// unconditionally.
|
|
196
|
+
function updateScrollFocusability(
|
|
197
|
+
scroll: HTMLElement,
|
|
198
|
+
container: HTMLElement,
|
|
199
|
+
) {
|
|
200
|
+
if (scroll.scrollWidth > scroll.clientWidth + 1) {
|
|
201
|
+
scroll.tabIndex = 0;
|
|
202
|
+
scroll.setAttribute(
|
|
203
|
+
"aria-label",
|
|
204
|
+
`Scrollable table: ${captionFor(container)}`,
|
|
205
|
+
);
|
|
206
|
+
} else {
|
|
207
|
+
scroll.removeAttribute("tabindex");
|
|
208
|
+
scroll.removeAttribute("aria-label");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function evaluateOverflow(
|
|
213
|
+
scroll: HTMLElement,
|
|
214
|
+
button: HTMLButtonElement,
|
|
215
|
+
container: HTMLElement,
|
|
216
|
+
) {
|
|
217
|
+
if (active?.scroll === scroll) return; // never hide the one currently expanded
|
|
218
|
+
button.hidden = scroll.scrollWidth <= scroll.clientWidth + 1;
|
|
219
|
+
updateScrollFocusability(scroll, container);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function collapse() {
|
|
223
|
+
if (!active) return;
|
|
224
|
+
const {
|
|
225
|
+
container,
|
|
226
|
+
target,
|
|
227
|
+
scroll,
|
|
228
|
+
placeholder,
|
|
229
|
+
spoofWrapper,
|
|
230
|
+
button,
|
|
231
|
+
leadingButton,
|
|
232
|
+
reposition,
|
|
233
|
+
} = active;
|
|
234
|
+
window.removeEventListener("scroll", reposition, true);
|
|
235
|
+
window.removeEventListener("resize", reposition);
|
|
236
|
+
target.classList.remove("table-overflow-expanded");
|
|
237
|
+
target.style.cssText = "";
|
|
238
|
+
leadingButton.remove();
|
|
239
|
+
placeholder.replaceWith(target);
|
|
240
|
+
spoofWrapper.remove();
|
|
241
|
+
active = null;
|
|
242
|
+
updateButtonLabel(container, button, false);
|
|
243
|
+
evaluateOverflow(scroll, button, container);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function expand(
|
|
247
|
+
container: HTMLElement,
|
|
248
|
+
button: HTMLButtonElement,
|
|
249
|
+
scroll: HTMLElement,
|
|
250
|
+
) {
|
|
251
|
+
if (active) collapse();
|
|
252
|
+
|
|
253
|
+
// Close whatever other overlay (a hover-preview card, a notation popup)
|
|
254
|
+
// was open, via the same shared arbiter every layer uses — but
|
|
255
|
+
// immediately release the claim rather than leaving the table
|
|
256
|
+
// registered as the arbiter's active surface. If it stayed registered,
|
|
257
|
+
// a hover card opening *afterward* over the table's own content (an
|
|
258
|
+
// explainable quantity inside it, say) would claim the arbiter in turn
|
|
259
|
+
// and force this table closed the same way a genuinely different
|
|
260
|
+
// overlay replacing it would — which is exactly backwards: the card
|
|
261
|
+
// should simply float above the still-open table (it already
|
|
262
|
+
// out-ranks it on z-index), and the table should close only through
|
|
263
|
+
// its own explicit gestures (the collapse buttons, an outside click, or
|
|
264
|
+
// Escape — all wired below, independent of this arbiter).
|
|
265
|
+
claimActiveSurface(GROUP, () => {});
|
|
266
|
+
releaseActiveSurface(GROUP);
|
|
267
|
+
|
|
268
|
+
// Measured *before* anything moves: `scrollWidth` reports the table's
|
|
269
|
+
// full, unclipped content width regardless of how narrow this wrapper
|
|
270
|
+
// currently renders it. Expanded, the row gains *two* button columns —
|
|
271
|
+
// the always-present trailing one plus the prepended leading one — so
|
|
272
|
+
// both must be budgeted for, not just one; short-changing this by even
|
|
273
|
+
// one column leaves `.table-overflow-scroll` a hair narrower than its
|
|
274
|
+
// content, which forces a needless horizontal scrollbar — and that
|
|
275
|
+
// scrollbar's own thickness then eats into the row's height, which can
|
|
276
|
+
// in turn push the whole figure past `maxHeight` and trigger a needless
|
|
277
|
+
// *vertical* scrollbar too.
|
|
278
|
+
const neededWidth =
|
|
279
|
+
scroll.scrollWidth + WIDTH_BUFFER_PX + SIDE_COLUMN_PX * 2;
|
|
280
|
+
|
|
281
|
+
// The whole referenceable figure (table + caption) moves together, not
|
|
282
|
+
// just its scrolling body — otherwise the caption is left behind,
|
|
283
|
+
// orphaned, in the narrow column while the table pops out beside it.
|
|
284
|
+
const target = container.closest<HTMLElement>(".keyed-table") ?? container;
|
|
285
|
+
|
|
286
|
+
const rect = target.getBoundingClientRect();
|
|
287
|
+
const placeholder = document.createElement("div");
|
|
288
|
+
placeholder.className = "table-overflow-placeholder";
|
|
289
|
+
placeholder.style.height = `${rect.height}px`;
|
|
290
|
+
target.replaceWith(placeholder);
|
|
291
|
+
|
|
292
|
+
// A genuine CSS ancestor (not a clone/relabel of `target` itself) so
|
|
293
|
+
// every `.sl-markdown-content …` rule — Starlight's row/border styling,
|
|
294
|
+
// this file's own zebra-stripe/hover rule — still applies once `target`
|
|
295
|
+
// is no longer a real descendant of the page's actual
|
|
296
|
+
// `.sl-markdown-content`. `display: contents` keeps it from generating
|
|
297
|
+
// any box of its own or affecting layout.
|
|
298
|
+
const spoofWrapper = document.createElement("div");
|
|
299
|
+
spoofWrapper.className = "sl-markdown-content";
|
|
300
|
+
spoofWrapper.setAttribute("role", "region");
|
|
301
|
+
spoofWrapper.setAttribute("aria-label", `Expanded ${captionFor(container)}`);
|
|
302
|
+
spoofWrapper.style.display = "contents";
|
|
303
|
+
document.body.appendChild(spoofWrapper);
|
|
304
|
+
spoofWrapper.appendChild(target);
|
|
305
|
+
|
|
306
|
+
target.classList.add("table-overflow-expanded");
|
|
307
|
+
// Preserve the target's native semantics while it is moved. The generated
|
|
308
|
+
// ancestor above supplies the landmark axe expects outside `<main>`;
|
|
309
|
+
// applying `role="region"` directly to a keyed table's `<figure>` would be
|
|
310
|
+
// invalid ARIA. Its `<figcaption>` continues to name the figure itself.
|
|
311
|
+
|
|
312
|
+
// The "0th column" — a second collapse button, prepended so the row
|
|
313
|
+
// reads as `[collapse, scroll, collapse]`, exactly mirroring the
|
|
314
|
+
// always-present trailing button. Both are real flex items, so table
|
|
315
|
+
// content is pushed clear of them rather than floating an overlay on
|
|
316
|
+
// top of it.
|
|
317
|
+
const leadingButton = document.createElement("button");
|
|
318
|
+
leadingButton.type = "button";
|
|
319
|
+
leadingButton.className = "table-overflow-expand-btn";
|
|
320
|
+
leadingButton.textContent = "⟩";
|
|
321
|
+
leadingButton.setAttribute(
|
|
322
|
+
"aria-label",
|
|
323
|
+
`Collapse ${captionFor(container)}`,
|
|
324
|
+
);
|
|
325
|
+
leadingButton.title = "Collapse";
|
|
326
|
+
leadingButton.addEventListener("click", collapse);
|
|
327
|
+
container.prepend(leadingButton);
|
|
328
|
+
|
|
329
|
+
const reposition = () => {
|
|
330
|
+
const headerBottom =
|
|
331
|
+
document.querySelector<HTMLElement>("header.header")?.getBoundingClientRect()
|
|
332
|
+
.bottom ?? 0;
|
|
333
|
+
const verticalStart = Math.max(EDGE_PX, headerBottom + EDGE_PX);
|
|
334
|
+
const placeholderRect = placeholder.getBoundingClientRect();
|
|
335
|
+
|
|
336
|
+
// Track where the table would be in normal flow; once that position
|
|
337
|
+
// has scrolled entirely past the visible band, there is nothing to
|
|
338
|
+
// keep on screen — hide rather than cling to the viewport edge.
|
|
339
|
+
const inView =
|
|
340
|
+
placeholderRect.bottom > verticalStart &&
|
|
341
|
+
placeholderRect.top < window.innerHeight;
|
|
342
|
+
target.style.visibility = inView ? "" : "hidden";
|
|
343
|
+
if (!inView) return;
|
|
344
|
+
|
|
345
|
+
// Tracks the placeholder's own top exactly, unclamped — the table
|
|
346
|
+
// scrolls with the page like the in-flow element it stands in for,
|
|
347
|
+
// sliding up and off the top edge as the reader scrolls past, rather
|
|
348
|
+
// than sticking at `verticalStart` and then vanishing outright once
|
|
349
|
+
// `inView` above finally goes false.
|
|
350
|
+
const top = placeholderRect.top;
|
|
351
|
+
const sidebarEdge = sidebarLeftBound();
|
|
352
|
+
const viewportEdge = window.innerWidth - EDGE_PX;
|
|
353
|
+
// The "On this page" rail's left edge is a *soft* boundary, not a hard
|
|
354
|
+
// one: the table would rather stay clear of it, but is willing to
|
|
355
|
+
// spill past it, as far as `viewportEdge`, if `neededWidth` doesn't
|
|
356
|
+
// fit before it. `Math.max(tocBoundary, sidebarEdge + neededWidth)`
|
|
357
|
+
// is what makes that soft — as the window narrows and `tocBoundary`
|
|
358
|
+
// creeps left of `sidebarEdge + neededWidth`, this expression stops
|
|
359
|
+
// tracking `tocBoundary` and holds steady at `sidebarEdge +
|
|
360
|
+
// neededWidth` instead (the table's own width doesn't change), so the
|
|
361
|
+
// handoff between "centered before the rail" and "spilling past it"
|
|
362
|
+
// has no seam to jump across — `left`/`width` below vary continuously
|
|
363
|
+
// with window size the whole way through, never snapping between two
|
|
364
|
+
// different formulas.
|
|
365
|
+
const tocBoundary = tocLeftBound();
|
|
366
|
+
const rightBound = Math.min(
|
|
367
|
+
viewportEdge,
|
|
368
|
+
Math.max(
|
|
369
|
+
tocBoundary !== null ? tocBoundary - EDGE_PX : viewportEdge,
|
|
370
|
+
sidebarEdge + neededWidth,
|
|
371
|
+
),
|
|
372
|
+
);
|
|
373
|
+
const availableWidth = Math.max(rightBound - sidebarEdge, 80);
|
|
374
|
+
// Only as wide as the table (and its two button columns) needs, up to
|
|
375
|
+
// `rightBound` — never wider just because the room is there.
|
|
376
|
+
const width = Math.min(neededWidth, availableWidth);
|
|
377
|
+
// Centered between `sidebarEdge` and `rightBound`. While the table
|
|
378
|
+
// fits before the rail, that's a true center within the sidebar-to-
|
|
379
|
+
// rail gap. Once it doesn't, `rightBound` above has already opened up
|
|
380
|
+
// to exactly `sidebarEdge + neededWidth` (or `viewportEdge`, if even
|
|
381
|
+
// that overflows), so `availableWidth` equals `width` and this
|
|
382
|
+
// reduces to `sidebarEdge` — flush against the nav, extending only as
|
|
383
|
+
// far right as it has to, rather than centered across room it isn't
|
|
384
|
+
// using.
|
|
385
|
+
const left = sidebarEdge + (availableWidth - width) / 2;
|
|
386
|
+
target.style.position = "fixed";
|
|
387
|
+
target.style.top = `${top}px`;
|
|
388
|
+
target.style.left = `${left}px`;
|
|
389
|
+
target.style.width = `${width}px`;
|
|
390
|
+
// Sized against the visible top of the band (never above
|
|
391
|
+
// `verticalStart`, however far off-screen `top` itself has scrolled)
|
|
392
|
+
// so this stays a sane ceiling on the box's own height rather than
|
|
393
|
+
// ballooning once the table starts sliding above the header.
|
|
394
|
+
const visibleTop = Math.max(verticalStart, top);
|
|
395
|
+
target.style.maxHeight = `${Math.max(window.innerHeight - visibleTop - EDGE_PX, 80)}px`;
|
|
396
|
+
// Re-check: at its (possibly capped) expanded width the table may or
|
|
397
|
+
// may not still need to scroll internally.
|
|
398
|
+
updateScrollFocusability(scroll, container);
|
|
399
|
+
};
|
|
400
|
+
reposition();
|
|
401
|
+
window.addEventListener("scroll", reposition, {
|
|
402
|
+
passive: true,
|
|
403
|
+
capture: true,
|
|
404
|
+
});
|
|
405
|
+
window.addEventListener("resize", reposition, { passive: true });
|
|
406
|
+
|
|
407
|
+
active = {
|
|
408
|
+
container,
|
|
409
|
+
target,
|
|
410
|
+
scroll,
|
|
411
|
+
placeholder,
|
|
412
|
+
spoofWrapper,
|
|
413
|
+
button,
|
|
414
|
+
leadingButton,
|
|
415
|
+
reposition,
|
|
416
|
+
};
|
|
417
|
+
updateButtonLabel(container, button, true);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function wrapBareTable(table: HTMLTableElement): HTMLElement {
|
|
421
|
+
const wrapper = document.createElement("div");
|
|
422
|
+
wrapper.className = "table-overflow-body";
|
|
423
|
+
table.replaceWith(wrapper);
|
|
424
|
+
wrapper.append(table);
|
|
425
|
+
return wrapper;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function wire(container: HTMLElement) {
|
|
429
|
+
// The table itself moves into a dedicated scroll clip, freeing the
|
|
430
|
+
// button to be a full-height flex sibling instead of an overlay
|
|
431
|
+
// floating on top of table content.
|
|
432
|
+
const scroll = document.createElement("div");
|
|
433
|
+
scroll.className = "table-overflow-scroll";
|
|
434
|
+
while (container.firstChild) scroll.append(container.firstChild);
|
|
435
|
+
container.append(scroll);
|
|
436
|
+
|
|
437
|
+
const button = document.createElement("button");
|
|
438
|
+
button.type = "button";
|
|
439
|
+
button.className = "table-overflow-expand-btn";
|
|
440
|
+
button.hidden = true;
|
|
441
|
+
updateButtonLabel(container, button, false);
|
|
442
|
+
container.append(button);
|
|
443
|
+
|
|
444
|
+
button.addEventListener("click", () => {
|
|
445
|
+
if (active?.container === container) collapse();
|
|
446
|
+
else expand(container, button, scroll);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
evaluateOverflow(scroll, button, container);
|
|
450
|
+
|
|
451
|
+
let resizeTimer: number | null = null;
|
|
452
|
+
window.addEventListener("resize", () => {
|
|
453
|
+
if (resizeTimer !== null) window.clearTimeout(resizeTimer);
|
|
454
|
+
resizeTimer = window.setTimeout(
|
|
455
|
+
() => evaluateOverflow(scroll, button, container),
|
|
456
|
+
150,
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function setup() {
|
|
462
|
+
const root = document.querySelector(".sl-markdown-content");
|
|
463
|
+
if (!root) return;
|
|
464
|
+
|
|
465
|
+
const containers = new Set<HTMLElement>();
|
|
466
|
+
root
|
|
467
|
+
.querySelectorAll<HTMLElement>(".keyed-table__body")
|
|
468
|
+
.forEach((el) => containers.add(el));
|
|
469
|
+
root
|
|
470
|
+
.querySelectorAll<HTMLTableElement>(
|
|
471
|
+
"table:not(.keyed-table__body table):not(.lab-shell table)",
|
|
472
|
+
)
|
|
473
|
+
.forEach((table) => containers.add(wrapBareTable(table)));
|
|
474
|
+
|
|
475
|
+
if (containers.size === 0) return;
|
|
476
|
+
containers.forEach(wire);
|
|
477
|
+
|
|
478
|
+
document.addEventListener("keydown", (event) => {
|
|
479
|
+
if (event.key === "Escape" && active) collapse();
|
|
480
|
+
});
|
|
481
|
+
// Deliberately no "click outside collapses it": a reader following a
|
|
482
|
+
// link in the "On this page" rail, or clicking anywhere else on the
|
|
483
|
+
// page to keep reading, is not asking for the table back at its normal
|
|
484
|
+
// size — only Escape or the collapse buttons themselves are.
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
setup();
|
|
488
|
+
window.addEventListener("explico:prepare-static-export", collapse);
|
|
489
|
+
</script>
|
package/src/content-config.ts
CHANGED
|
@@ -287,6 +287,25 @@ const notation = defineCollection({
|
|
|
287
287
|
}),
|
|
288
288
|
});
|
|
289
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Abbreviations table (rule N14) — deliberately a lighter tier than a
|
|
292
|
+
* `notation` card: just a name resolution, no `formula`, `units`, or
|
|
293
|
+
* curriculum `alignment`. `abbr` is the canonical written form (`OLS`);
|
|
294
|
+
* `aliases` are other written forms of the same abbreviation that should
|
|
295
|
+
* resolve the same way (`w.r.t.` might alias `wrt`).
|
|
296
|
+
*/
|
|
297
|
+
const abbreviations = defineCollection({
|
|
298
|
+
loader: glob({ pattern: '**/*.md', base: './content/abbreviations' }),
|
|
299
|
+
schema: z.object({
|
|
300
|
+
key: id,
|
|
301
|
+
abbr: z.string().min(1),
|
|
302
|
+
expansion: notationProse,
|
|
303
|
+
aliases: z.array(z.string().min(1)).default([]),
|
|
304
|
+
domain: id.optional(),
|
|
305
|
+
editorialStatus: editorialStatus.default('draft'),
|
|
306
|
+
}),
|
|
307
|
+
});
|
|
308
|
+
|
|
290
309
|
export const collections = {
|
|
291
310
|
docs,
|
|
292
311
|
competencies,
|
|
@@ -294,4 +313,5 @@ export const collections = {
|
|
|
294
313
|
tracks,
|
|
295
314
|
sources,
|
|
296
315
|
notation,
|
|
316
|
+
abbreviations,
|
|
297
317
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base abbreviation baseline: acronyms and dotted abbreviations every
|
|
3
|
+
* reader is assumed to know, so a lesson never has to introduce them with an
|
|
4
|
+
* abbreviations-table entry. The `\operatorname{…}` / `\mathrm{…}` twins are
|
|
5
|
+
* `base-operators.mjs` (rule N8) / `base-roman-labels.mjs` (rule N10); this is
|
|
6
|
+
* the plain-prose equivalent for rule N14.
|
|
7
|
+
*
|
|
8
|
+
* Kept intentionally small, same rationale as `BASE_ROMAN_LABELS`: a
|
|
9
|
+
* course-specific acronym (`OLS`, `SSR`, `MSE`) is exactly the thing a reader
|
|
10
|
+
* benefits from having spelled out, so it belongs in the course's own
|
|
11
|
+
* `content/abbreviations/*.md` table, not here. This baseline only holds
|
|
12
|
+
* abbreviations so common in ordinary English/academic writing that spelling
|
|
13
|
+
* them out on every course would be noise, not help. A course adds a real
|
|
14
|
+
* one via `abbreviations.allow` in `course.config.ts`, the same shape as
|
|
15
|
+
* `operators.allow` / `romanLabels.allow`.
|
|
16
|
+
*/
|
|
17
|
+
export const BASE_ABBREVIATIONS = Object.freeze(
|
|
18
|
+
new Set([
|
|
19
|
+
// dotted Latin/English abbreviations
|
|
20
|
+
'e.g.',
|
|
21
|
+
'i.e.',
|
|
22
|
+
'etc.',
|
|
23
|
+
'cf.',
|
|
24
|
+
'vs.',
|
|
25
|
+
'a.m.',
|
|
26
|
+
'p.m.',
|
|
27
|
+
'a.k.a.',
|
|
28
|
+
'et al.',
|
|
29
|
+
// generic acronyms with no domain-specific meaning in this engine
|
|
30
|
+
'AI',
|
|
31
|
+
'ID',
|
|
32
|
+
'OK',
|
|
33
|
+
'URL',
|
|
34
|
+
'FAQ',
|
|
35
|
+
'US',
|
|
36
|
+
'UK',
|
|
37
|
+
'EU',
|
|
38
|
+
]),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The abbreviations a course treats as common, given its `course.config.ts`
|
|
43
|
+
* `abbreviations` block: the shipped baseline, plus `allow`, minus `deny`.
|
|
44
|
+
*
|
|
45
|
+
* @param {{ allow?: readonly string[], deny?: readonly string[] } | undefined} abbreviations
|
|
46
|
+
* @returns {Set<string>}
|
|
47
|
+
*/
|
|
48
|
+
export function effectiveAbbreviationAllowlist(abbreviations) {
|
|
49
|
+
const allow = abbreviations?.allow ?? [];
|
|
50
|
+
const deny = new Set(abbreviations?.deny ?? []);
|
|
51
|
+
const effective = new Set(BASE_ABBREVIATIONS);
|
|
52
|
+
for (const name of allow) effective.add(name);
|
|
53
|
+
for (const name of deny) effective.delete(name);
|
|
54
|
+
return effective;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** True when `token` needs no abbreviations-table entry for this course. */
|
|
58
|
+
export function isBaseAbbreviation(token, abbreviations) {
|
|
59
|
+
return effectiveAbbreviationAllowlist(abbreviations).has(token);
|
|
60
|
+
}
|
|
@@ -81,6 +81,46 @@ export const BASE_OPERATORS = Object.freeze(
|
|
|
81
81
|
]),
|
|
82
82
|
);
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* The subset of {@link BASE_OPERATORS} that must never appear with a bare
|
|
86
|
+
* (unparenthesized) argument: every trigonometric, inverse-trigonometric, and
|
|
87
|
+
* hyperbolic function, plus the exponential and logarithms (N13). `\sin x`
|
|
88
|
+
* leaves how far the argument extends ambiguous the moment anything follows
|
|
89
|
+
* it (`\sin x + 1` — is the `+1` inside or outside the sine?); `\sin(x)`
|
|
90
|
+
* never does, even for a single-symbol argument. Kept separate from
|
|
91
|
+
* `BASE_OPERATORS`: this is a formatting rule layered on top of the "does it
|
|
92
|
+
* need a notation card" question that set answers, so an operator can be
|
|
93
|
+
* universal (N8) and still owe a parenthesized argument. Enforced by
|
|
94
|
+
* `resolveMathGlyphs` in `math-glyphs.mjs`, which every rendered-math context
|
|
95
|
+
* (lesson body, notation `formula`, assessments, `Diagram` nodes, lab math)
|
|
96
|
+
* already calls.
|
|
97
|
+
*/
|
|
98
|
+
export const PARENTHESIZED_FUNCTIONS = Object.freeze(
|
|
99
|
+
new Set([
|
|
100
|
+
// exponential / logarithms
|
|
101
|
+
'exp',
|
|
102
|
+
'log',
|
|
103
|
+
'ln',
|
|
104
|
+
'lg',
|
|
105
|
+
// trigonometric
|
|
106
|
+
'sin',
|
|
107
|
+
'cos',
|
|
108
|
+
'tan',
|
|
109
|
+
'cot',
|
|
110
|
+
'sec',
|
|
111
|
+
'csc',
|
|
112
|
+
// inverse trigonometric
|
|
113
|
+
'arcsin',
|
|
114
|
+
'arccos',
|
|
115
|
+
'arctan',
|
|
116
|
+
// hyperbolic
|
|
117
|
+
'sinh',
|
|
118
|
+
'cosh',
|
|
119
|
+
'tanh',
|
|
120
|
+
'coth',
|
|
121
|
+
]),
|
|
122
|
+
);
|
|
123
|
+
|
|
84
124
|
/**
|
|
85
125
|
* The operators a course treats as common, given its `course.config.ts`
|
|
86
126
|
* `operators` block: the shipped baseline, plus `allow`, minus `deny`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base roman-label baseline: `\mathrm{…}` multi-letter labels every
|
|
3
|
+
* reader is assumed to know, so a lesson never has to introduce them with a
|
|
4
|
+
* notation entry. The `\operatorname{…}` counterpart is `base-operators.mjs`
|
|
5
|
+
* (rule N8); this is its `\mathrm{…}` twin (rule N10).
|
|
6
|
+
*
|
|
7
|
+
* Rationale: the completeness gate has no opinion about `\mathrm{…}` bodies —
|
|
8
|
+
* they are a typographic label to KaTeX, not a bindable symbol, and N3
|
|
9
|
+
* requires the format without asking whether the label is ever explained.
|
|
10
|
+
* The `roman-label-explained` consistency check fills that gap: a label a
|
|
11
|
+
* lesson uses is either on this list (or the course's additions to it) or it
|
|
12
|
+
* carries a notation entry the reader can hover.
|
|
13
|
+
*
|
|
14
|
+
* Kept intentionally small — unlike `sin`/`exp`, an upright label is almost
|
|
15
|
+
* always a course-specific named quantity (`KE`, `PV`, `Var`-as-a-label) that
|
|
16
|
+
* genuinely benefits from a hover card, so the shipped baseline holds only
|
|
17
|
+
* the qualifiers that function like an operator's extremum subscript
|
|
18
|
+
* (`x_{\mathrm{opt}}`) rather than naming a quantity of their own. A course
|
|
19
|
+
* adds a real one via `romanLabels.allow` in `course.config.ts`, the same
|
|
20
|
+
* shape as `operators.allow`.
|
|
21
|
+
*/
|
|
22
|
+
export const BASE_ROMAN_LABELS = Object.freeze(new Set(['opt']));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The roman labels a course treats as common, given its `course.config.ts`
|
|
26
|
+
* `romanLabels` block: the shipped baseline, plus `allow`, minus `deny`.
|
|
27
|
+
*
|
|
28
|
+
* @param {{ allow?: readonly string[], deny?: readonly string[] } | undefined} romanLabels
|
|
29
|
+
* @returns {Set<string>}
|
|
30
|
+
*/
|
|
31
|
+
export function effectiveRomanLabelAllowlist(romanLabels) {
|
|
32
|
+
const allow = romanLabels?.allow ?? [];
|
|
33
|
+
const deny = new Set(romanLabels?.deny ?? []);
|
|
34
|
+
const effective = new Set(BASE_ROMAN_LABELS);
|
|
35
|
+
for (const name of allow) effective.add(name);
|
|
36
|
+
for (const name of deny) effective.delete(name);
|
|
37
|
+
return effective;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True when `name` needs no notation entry for this course. */
|
|
41
|
+
export function isBaseRomanLabel(name, romanLabels) {
|
|
42
|
+
return effectiveRomanLabelAllowlist(romanLabels).has(name);
|
|
43
|
+
}
|