explico 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/README.md +108 -0
  2. package/dist/analytics/AnalyticsEmitter.d.ts +22 -0
  3. package/dist/analytics/AnalyticsEmitter.js +5 -0
  4. package/dist/compiler/cli.d.ts +189 -0
  5. package/dist/compiler/cli.js +552 -0
  6. package/dist/compiler/collections.d.ts +70 -0
  7. package/dist/compiler/collections.js +383 -0
  8. package/dist/compiler/course-config.d.ts +31 -0
  9. package/dist/compiler/course-config.js +0 -0
  10. package/dist/compiler/lesson-derivation.d.ts +89 -0
  11. package/dist/compiler/lesson-derivation.js +91 -0
  12. package/dist/compiler/manifest.d.ts +178 -0
  13. package/dist/compiler/manifest.js +345 -0
  14. package/dist/compiler/resolution-report.d.ts +45 -0
  15. package/dist/compiler/resolution-report.js +97 -0
  16. package/dist/curriculum/validation.d.ts +76 -0
  17. package/dist/curriculum/validation.js +320 -0
  18. package/dist/progress/ProgressRepository.d.ts +53 -0
  19. package/dist/progress/ProgressRepository.js +12 -0
  20. package/dist/reference/base-library.mjs +27 -0
  21. package/dist/reference/citation-format.mjs +133 -0
  22. package/dist/reference/consistency.d.ts +64 -0
  23. package/dist/reference/consistency.js +234 -0
  24. package/dist/reference/curriculum-alignment.d.ts +7 -0
  25. package/dist/reference/curriculum-alignment.js +166 -0
  26. package/dist/reference/equations-validate.d.ts +50 -0
  27. package/dist/reference/equations-validate.js +90 -0
  28. package/dist/reference/equations.mjs +144 -0
  29. package/dist/reference/gate-math.d.ts +41 -0
  30. package/dist/reference/gate-math.js +151 -0
  31. package/dist/reference/katex-options.mjs +91 -0
  32. package/dist/reference/lab-math-scope.d.ts +21 -0
  33. package/dist/reference/lab-math-scope.js +28 -0
  34. package/dist/reference/label.d.ts +11 -0
  35. package/dist/reference/label.js +38 -0
  36. package/dist/reference/lint-ignore.d.ts +32 -0
  37. package/dist/reference/lint-ignore.js +39 -0
  38. package/dist/reference/math-glyphs.mjs +830 -0
  39. package/dist/reference/prose.d.ts +15 -0
  40. package/dist/reference/prose.js +12 -0
  41. package/dist/reference/references.d.ts +17 -0
  42. package/dist/reference/references.js +42 -0
  43. package/dist/reference/registry.d.ts +6 -0
  44. package/dist/reference/registry.js +548 -0
  45. package/dist/reference/rehype-fail-katex-errors.mjs +55 -0
  46. package/dist/reference/remark-citation.mjs +0 -0
  47. package/dist/reference/remark-notation.mjs +753 -0
  48. package/dist/reference/render-lab-math.d.ts +21 -0
  49. package/dist/reference/render-lab-math.js +58 -0
  50. package/dist/reference/types.d.ts +151 -0
  51. package/dist/reference/types.js +0 -0
  52. package/dist/reference/units-vocab.d.ts +26 -0
  53. package/dist/reference/units-vocab.js +119 -0
  54. package/dist/session/PreferenceStore.d.ts +28 -0
  55. package/dist/session/PreferenceStore.js +30 -0
  56. package/dist/session/user-context.d.ts +7 -0
  57. package/dist/session/user-context.js +10 -0
  58. package/dist/session/user.d.ts +21 -0
  59. package/dist/session/user.js +8 -0
  60. package/package.json +53 -0
  61. package/src/analytics/AnalyticsEmitter.ts +23 -0
  62. package/src/compiler/cli.ts +866 -0
  63. package/src/compiler/collections.ts +554 -0
  64. package/src/compiler/course-config.ts +32 -0
  65. package/src/compiler/lesson-derivation.ts +186 -0
  66. package/src/compiler/manifest.ts +601 -0
  67. package/src/compiler/resolution-report.ts +150 -0
  68. package/src/components/CurriculumMap.astro +208 -0
  69. package/src/components/HoverPanel.astro +67 -0
  70. package/src/components/assessments/AssessmentRunner.tsx +320 -0
  71. package/src/components/assessments/AssessmentSet.astro +54 -0
  72. package/src/components/citation/CitationLayer.astro +173 -0
  73. package/src/components/examples/CompactExample.astro +34 -0
  74. package/src/components/examples/CompactExamples.astro +253 -0
  75. package/src/components/hover-panel.ts +273 -0
  76. package/src/components/notation/NotationGlossary.astro +412 -0
  77. package/src/components/notation/NotationLayer.astro +594 -0
  78. package/src/components/notation/NotationMath.astro +24 -0
  79. package/src/components/notation/SafeMathNode.astro +23 -0
  80. package/src/components/notation/render-notation-math.ts +223 -0
  81. package/src/components/starlight/EquationEnhancer.astro +72 -0
  82. package/src/components/starlight/LayoutHeader.astro +102 -0
  83. package/src/components/starlight/LayoutPageSidebar.astro +33 -0
  84. package/src/components/starlight/LayoutSidebar.astro +29 -0
  85. package/src/components/starlight/LessonFooter.astro +58 -0
  86. package/src/content-config.ts +259 -0
  87. package/src/curriculum/validation.ts +481 -0
  88. package/src/progress/ProgressRepository.ts +64 -0
  89. package/src/reference/base-library.mjs +27 -0
  90. package/src/reference/citation-format.mjs +133 -0
  91. package/src/reference/consistency.ts +375 -0
  92. package/src/reference/curriculum-alignment.ts +228 -0
  93. package/src/reference/equations-validate.ts +167 -0
  94. package/src/reference/equations.mjs +144 -0
  95. package/src/reference/gate-math.ts +250 -0
  96. package/src/reference/katex-options.mjs +91 -0
  97. package/src/reference/lab-math-scope.ts +54 -0
  98. package/src/reference/label.ts +47 -0
  99. package/src/reference/lint-ignore.ts +75 -0
  100. package/src/reference/math-glyphs.mjs +830 -0
  101. package/src/reference/prose.ts +21 -0
  102. package/src/reference/references.ts +82 -0
  103. package/src/reference/registry.ts +723 -0
  104. package/src/reference/rehype-fail-katex-errors.mjs +55 -0
  105. package/src/reference/remark-citation.mjs +0 -0
  106. package/src/reference/remark-notation.mjs +753 -0
  107. package/src/reference/render-lab-math.ts +99 -0
  108. package/src/reference/types.ts +182 -0
  109. package/src/reference/units-vocab.ts +143 -0
  110. package/src/session/PreferenceStore.ts +54 -0
  111. package/src/session/user-context.ts +18 -0
  112. package/src/session/user.ts +24 -0
  113. package/src/styles/global.css +763 -0
@@ -0,0 +1,223 @@
1
+ import rehypeKatex from 'rehype-katex';
2
+ import { createNotationKatexOptions } from '../../reference/katex-options.mjs';
3
+
4
+ interface HastText {
5
+ type: 'text';
6
+ value: string;
7
+ }
8
+
9
+ interface HastElement {
10
+ type: 'element';
11
+ tagName: string;
12
+ properties: Record<string, unknown>;
13
+ children: HastNode[];
14
+ }
15
+
16
+ type HastNode = HastText | HastElement;
17
+
18
+ interface HastRoot {
19
+ type: 'root';
20
+ children: HastNode[];
21
+ }
22
+
23
+ export type SafeMathNode =
24
+ | { type: 'text'; value: string }
25
+ | {
26
+ type: 'element';
27
+ tagName: string;
28
+ properties: Readonly<Record<string, string>>;
29
+ children: readonly SafeMathNode[];
30
+ };
31
+
32
+ const allowedTags = new Set([
33
+ 'annotation',
34
+ 'math',
35
+ 'menclose',
36
+ 'mfrac',
37
+ 'mi',
38
+ 'mn',
39
+ 'mo',
40
+ 'mover',
41
+ 'mpadded',
42
+ 'mphantom',
43
+ 'mroot',
44
+ 'mrow',
45
+ 'mspace',
46
+ 'msqrt',
47
+ 'mstyle',
48
+ 'msub',
49
+ 'msubsup',
50
+ 'msup',
51
+ 'mtable',
52
+ 'mtd',
53
+ 'mtext',
54
+ 'mtr',
55
+ 'munder',
56
+ 'munderover',
57
+ 'semantics',
58
+ 'span',
59
+ 'svg',
60
+ 'path',
61
+ 'line',
62
+ ]);
63
+
64
+ const simpleAttributeNames = new Set([
65
+ 'accent',
66
+ 'accentunder',
67
+ 'columnalign',
68
+ 'columnspacing',
69
+ 'displaystyle',
70
+ 'fence',
71
+ 'height',
72
+ 'lspace',
73
+ 'mathvariant',
74
+ 'preserveaspectratio',
75
+ 'rowspacing',
76
+ 'rspace',
77
+ 'scriptlevel',
78
+ 'separator',
79
+ 'stretchy',
80
+ 'viewbox',
81
+ 'width',
82
+ ]);
83
+
84
+ const safeAttributeValue = /^[ A-Za-z0-9+.,()\-/%]*$/;
85
+ const safeClassName = /^[A-Za-z0-9_-]+$/;
86
+ const safeStyle = /^(?:[a-z-]+:[ A-Za-z0-9+.,()\-/%]*;)*$/;
87
+ const safePathData = /^[\sA-Za-z0-9+.,\-]*$/;
88
+ const unsafeExtensionCommand =
89
+ /\\(?:href|url|includegraphics|htmlClass|htmlData|htmlId|htmlStyle)\b/;
90
+
91
+ function attributeValue(value: unknown, propertyName: string): string {
92
+ if (typeof value === 'boolean') return String(value);
93
+ if (typeof value === 'number' && Number.isFinite(value)) return String(value);
94
+ if (typeof value !== 'string' || !safeAttributeValue.test(value)) {
95
+ throw new Error(`Unsafe KaTeX ${propertyName} value.`);
96
+ }
97
+ return value;
98
+ }
99
+
100
+ function safeProperties(
101
+ element: HastElement,
102
+ ): Readonly<Record<string, string>> {
103
+ const properties: Record<string, string> = {};
104
+
105
+ for (const [name, value] of Object.entries(element.properties)) {
106
+ if (name === 'className') {
107
+ if (
108
+ !Array.isArray(value) ||
109
+ !value.every(
110
+ (className) =>
111
+ typeof className === 'string' && safeClassName.test(className),
112
+ )
113
+ ) {
114
+ throw new Error('Unsafe KaTeX class list.');
115
+ }
116
+ properties.class = value.join(' ');
117
+ continue;
118
+ }
119
+
120
+ if (name === 'ariaHidden' && value === 'true') {
121
+ properties['aria-hidden'] = 'true';
122
+ continue;
123
+ }
124
+
125
+ if (name === 'encoding' && value === 'application/x-tex') {
126
+ properties.encoding = value;
127
+ continue;
128
+ }
129
+
130
+ if (
131
+ name === 'xmlns' &&
132
+ (value === 'http://www.w3.org/1998/Math/MathML' ||
133
+ value === 'http://www.w3.org/2000/svg')
134
+ ) {
135
+ properties.xmlns = value;
136
+ continue;
137
+ }
138
+
139
+ if (
140
+ name === 'style' &&
141
+ typeof value === 'string' &&
142
+ safeStyle.test(value) &&
143
+ !/(?:expression|url)\s*\(/i.test(value)
144
+ ) {
145
+ properties.style = value;
146
+ continue;
147
+ }
148
+
149
+ if (name === 'd' && typeof value === 'string' && safePathData.test(value)) {
150
+ properties.d = value;
151
+ continue;
152
+ }
153
+
154
+ const normalizedName = name.toLowerCase();
155
+ if (simpleAttributeNames.has(normalizedName)) {
156
+ const outputName =
157
+ normalizedName === 'viewbox'
158
+ ? 'viewBox'
159
+ : normalizedName === 'preserveaspectratio'
160
+ ? 'preserveAspectRatio'
161
+ : normalizedName;
162
+ properties[outputName] = attributeValue(value, name);
163
+ continue;
164
+ }
165
+
166
+ throw new Error(`Unexpected KaTeX property: ${name}.`);
167
+ }
168
+
169
+ return Object.freeze(properties);
170
+ }
171
+
172
+ function sanitizeNode(node: HastNode): SafeMathNode {
173
+ if (node.type === 'text') {
174
+ return Object.freeze({ type: 'text', value: node.value });
175
+ }
176
+
177
+ if (node.type !== 'element' || !allowedTags.has(node.tagName)) {
178
+ throw new Error(`Unexpected KaTeX node: ${node.type}.`);
179
+ }
180
+
181
+ return Object.freeze({
182
+ type: 'element',
183
+ tagName: node.tagName,
184
+ properties: safeProperties(node),
185
+ children: Object.freeze(node.children.map(sanitizeNode)),
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Render repository-authored LaTeX through the same build-time KaTeX adapter
191
+ * as lesson math, then expose a constrained structured tree to Astro. No HTML
192
+ * string crosses the component boundary.
193
+ */
194
+ export function renderNotationMath(latex: string): readonly SafeMathNode[] {
195
+ if (latex.trim().length === 0) {
196
+ throw new Error('Notation LaTeX must not be empty.');
197
+ }
198
+ if (unsafeExtensionCommand.test(latex)) {
199
+ throw new Error('Notation LaTeX cannot use a trust-requiring extension.');
200
+ }
201
+
202
+ const root: HastRoot = {
203
+ type: 'root',
204
+ children: [
205
+ {
206
+ type: 'element',
207
+ tagName: 'code',
208
+ properties: { className: ['math-inline'] },
209
+ children: [{ type: 'text', value: latex }],
210
+ },
211
+ ],
212
+ };
213
+
214
+ const transform = rehypeKatex(createNotationKatexOptions());
215
+ const file = {
216
+ message(message: unknown): never {
217
+ throw new Error(String(message));
218
+ },
219
+ } as unknown as Parameters<typeof transform>[1];
220
+
221
+ transform(root as unknown as Parameters<typeof transform>[0], file);
222
+ return Object.freeze(root.children.map(sanitizeNode));
223
+ }
@@ -0,0 +1,72 @@
1
+ ---
2
+ /**
3
+ * Progressive enhancement for keyed equations (Step D7).
4
+ *
5
+ * The no-JS baseline is complete: `remark-notation` emits a real `id="eq-<key>"`
6
+ * container, a focusable number `<a>`, and `[[eq:key]]` renders a plain anchor;
7
+ * `global.css` does the `:target` deep-link highlight (with a
8
+ * `prefers-reduced-motion` fallback) and the sticky-header scroll offset via
9
+ * `scroll-margin`.
10
+ *
11
+ * This script only adds what CSS and anchors cannot:
12
+ * - re-triggering the highlight when the same `#eq-…` is activated again;
13
+ * - a smooth, header-aware scroll on an in-page jump;
14
+ * - moving keyboard focus onto the equation so screen-reader users land on it.
15
+ *
16
+ * Everything here is gated on `prefers-reduced-motion` and degrades to the
17
+ * native anchor behaviour if it throws.
18
+ */
19
+ ---
20
+
21
+ <script>
22
+ (() => {
23
+ const root = document.querySelector('.sl-markdown-content');
24
+ if (!root) return;
25
+
26
+ const reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
27
+
28
+ const activate = (target: HTMLElement | null, doScroll: boolean) => {
29
+ if (!target) return;
30
+ if (!reduce.matches) {
31
+ // Restart the CSS `:target` animation even when the hash is unchanged.
32
+ target.style.animation = 'none';
33
+ void target.offsetWidth;
34
+ target.style.removeProperty('animation');
35
+ }
36
+ if (doScroll) {
37
+ target.scrollIntoView({
38
+ behavior: reduce.matches ? 'auto' : 'smooth',
39
+ block: 'start',
40
+ });
41
+ }
42
+ target.setAttribute('tabindex', '-1');
43
+ try {
44
+ target.focus({ preventScroll: true });
45
+ } catch {
46
+ target.focus();
47
+ }
48
+ };
49
+
50
+ root.addEventListener('click', (event) => {
51
+ const origin = event.target instanceof Element ? event.target : null;
52
+ const link = origin?.closest('a.equation-ref, a.keyed-equation__number');
53
+ if (!link) return;
54
+ const href = link.getAttribute('href') || '';
55
+ if (!href.startsWith('#eq-')) return; // cross-page: let the browser navigate
56
+ const target = document.getElementById(href.slice(1));
57
+ if (!target) return;
58
+ event.preventDefault();
59
+ if (location.hash !== href) history.pushState(null, '', href);
60
+ activate(target, true);
61
+ });
62
+
63
+ window.addEventListener('hashchange', () => {
64
+ if (!location.hash.startsWith('#eq-')) return;
65
+ activate(document.getElementById(location.hash.slice(1)), false);
66
+ });
67
+
68
+ if (location.hash.startsWith('#eq-')) {
69
+ activate(document.getElementById(location.hash.slice(1)), true);
70
+ }
71
+ })();
72
+ </script>
@@ -0,0 +1,102 @@
1
+ ---
2
+ import DefaultHeader from '@astrojs/starlight/components/Header.astro';
3
+ ---
4
+
5
+ <DefaultHeader />
6
+
7
+ <script>
8
+ import { createBrowserPreferenceStore } from '../../session/PreferenceStore';
9
+
10
+ type LayoutRegion = 'navigation' | 'contents';
11
+
12
+ const regions: Record<
13
+ LayoutRegion,
14
+ {
15
+ attribute: string;
16
+ storageKey: string;
17
+ peekSuppressedAttribute: string;
18
+ panelSelector: string;
19
+ shownLabel: string;
20
+ hiddenLabel: string;
21
+ }
22
+ > = {
23
+ navigation: {
24
+ attribute: 'data-navigation-hidden',
25
+ storageKey: 'credit-playground:layout:navigation-hidden',
26
+ peekSuppressedAttribute: 'data-navigation-peek-suppressed',
27
+ panelSelector: '.sidebar-pane',
28
+ shownLabel: 'Hide navigation',
29
+ hiddenLabel: 'Show navigation',
30
+ },
31
+ contents: {
32
+ attribute: 'data-page-sidebar-hidden',
33
+ storageKey: 'credit-playground:layout:contents-hidden',
34
+ peekSuppressedAttribute: 'data-page-sidebar-peek-suppressed',
35
+ panelSelector: '.right-sidebar',
36
+ shownLabel: 'Hide page contents',
37
+ hiddenLabel: 'Show page contents',
38
+ },
39
+ };
40
+
41
+ const root = document.documentElement;
42
+
43
+ // The one seam for per-viewer UI preferences (Phase H1). This component never
44
+ // touches browser storage directly; a future SSR host swaps in a different
45
+ // PreferenceStore and nothing here changes.
46
+ const preferences = createBrowserPreferenceStore();
47
+
48
+ function readPreference(storageKey: string): boolean {
49
+ return preferences.get(storageKey) === 'true';
50
+ }
51
+
52
+ function writePreference(storageKey: string, hidden: boolean): void {
53
+ preferences.set(storageKey, String(hidden));
54
+ }
55
+
56
+ function applyState(
57
+ button: HTMLButtonElement,
58
+ region: LayoutRegion,
59
+ hidden: boolean,
60
+ ): void {
61
+ const settings = regions[region];
62
+ const label = hidden ? settings.hiddenLabel : settings.shownLabel;
63
+ root.toggleAttribute(settings.attribute, hidden);
64
+ if (!hidden) root.removeAttribute(settings.peekSuppressedAttribute);
65
+ button.setAttribute('aria-expanded', String(!hidden));
66
+ button.setAttribute('aria-label', label);
67
+ button.title = label;
68
+ }
69
+
70
+ document
71
+ .querySelectorAll<HTMLButtonElement>('[data-layout-toggle]')
72
+ .forEach((button) => {
73
+ const region = button.dataset.layoutToggle as LayoutRegion;
74
+ const settings = regions[region];
75
+ if (!settings) return;
76
+
77
+ applyState(button, region, readPreference(settings.storageKey));
78
+ button.removeAttribute('hidden');
79
+ button.addEventListener('click', (event) => {
80
+ const hidden = !root.hasAttribute(settings.attribute);
81
+ applyState(button, region, hidden);
82
+ writePreference(settings.storageKey, hidden);
83
+
84
+ // A pointer click should leave a newly collapsed rail collapsed even
85
+ // though the pointer was inside the full-width panel when clicked.
86
+ if (hidden && event.detail > 0) {
87
+ root.setAttribute(settings.peekSuppressedAttribute, '');
88
+ button.blur();
89
+
90
+ const panel = document.querySelector(settings.panelSelector);
91
+ const clearSuppression = () =>
92
+ root.removeAttribute(settings.peekSuppressedAttribute);
93
+ panel?.addEventListener('pointerleave', clearSuppression, {
94
+ once: true,
95
+ });
96
+ window.setTimeout(() => {
97
+ if (!panel?.matches(':hover')) clearSuppression();
98
+ }, 180);
99
+ }
100
+ });
101
+ });
102
+ </script>
@@ -0,0 +1,33 @@
1
+ ---
2
+ import { Icon } from '@astrojs/starlight/components';
3
+ import DefaultPageSidebar from '@astrojs/starlight/components/PageSidebar.astro';
4
+ ---
5
+
6
+ <div
7
+ id="starlight__page-sidebar"
8
+ class="layout-page-sidebar"
9
+ data-layout-panel="contents"
10
+ >
11
+ <button
12
+ class="layout-edge-toggle layout-contents-toggle"
13
+ type="button"
14
+ aria-controls="starlight__page-sidebar-content"
15
+ aria-expanded="true"
16
+ data-layout-toggle="contents"
17
+ hidden
18
+ >
19
+ <Icon
20
+ name="right-caret"
21
+ class="layout-edge-icon layout-edge-icon-hide"
22
+ size="1rem"
23
+ />
24
+ <Icon
25
+ name="left-caret"
26
+ class="layout-edge-icon layout-edge-icon-restore"
27
+ size="1rem"
28
+ />
29
+ </button>
30
+ <div id="starlight__page-sidebar-content" class="layout-page-sidebar-content">
31
+ <DefaultPageSidebar />
32
+ </div>
33
+ </div>
@@ -0,0 +1,29 @@
1
+ ---
2
+ import { Icon } from '@astrojs/starlight/components';
3
+ import DefaultSidebar from '@astrojs/starlight/components/Sidebar.astro';
4
+ ---
5
+
6
+ <div class="layout-navigation-panel" data-layout-panel="navigation">
7
+ <button
8
+ class="layout-edge-toggle layout-navigation-toggle"
9
+ type="button"
10
+ aria-controls="starlight__navigation-content"
11
+ aria-expanded="true"
12
+ data-layout-toggle="navigation"
13
+ hidden
14
+ >
15
+ <Icon
16
+ name="left-caret"
17
+ class="layout-edge-icon layout-edge-icon-hide"
18
+ size="1rem"
19
+ />
20
+ <Icon
21
+ name="right-caret"
22
+ class="layout-edge-icon layout-edge-icon-restore"
23
+ size="1rem"
24
+ />
25
+ </button>
26
+ <div id="starlight__navigation-content" class="layout-navigation-content">
27
+ <DefaultSidebar />
28
+ </div>
29
+ </div>
@@ -0,0 +1,58 @@
1
+ ---
2
+ /**
3
+ * Starlight only allows one `Footer` override, so this component composes the
4
+ * page-notation layer and the source-citation layer ahead of Starlight's own
5
+ * footer. Both layers are optional progressive enhancements over static
6
+ * markup that the remark adapters and the content collections already emit.
7
+ *
8
+ * The resolved notation bundle comes from the one compiler manifest (Phase
9
+ * D5); this component never rebuilds the registry.
10
+ */
11
+ import DefaultFooter from '@astrojs/starlight/components/Footer.astro';
12
+ import { loadManifest } from '../../compiler/manifest';
13
+ import {
14
+ isLessonSlug,
15
+ lessonIdFromSlug,
16
+ } from '../../compiler/lesson-derivation';
17
+ import NotationLayer, {
18
+ type NotationLayerDefinition,
19
+ } from '../notation/NotationLayer.astro';
20
+ import CitationLayer from '../citation/CitationLayer.astro';
21
+ import EquationEnhancer from './EquationEnhancer.astro';
22
+
23
+ // `lessonId` is no longer authored (Phase C2); derive it from the route.
24
+ const routeSlug = Astro.locals.starlightRoute.entry.id;
25
+ const lessonId = isLessonSlug(routeSlug)
26
+ ? lessonIdFromSlug(routeSlug)
27
+ : undefined;
28
+
29
+ const glossaryBase = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/glossary/`;
30
+
31
+ const definitions: NotationLayerDefinition[] = [];
32
+ if (lessonId) {
33
+ const manifest = await loadManifest();
34
+ const lesson = manifest.lessons.find((entry) => entry.id === lessonId);
35
+ for (const definition of lesson?.notation.definitions ?? []) {
36
+ definitions.push({
37
+ key: definition.key,
38
+ notation: definition.notation,
39
+ label: definition.label,
40
+ summary: definition.summary,
41
+ formula: definition.formula,
42
+ units: definition.units,
43
+ seeAlso: definition.seeAlso,
44
+ editorialStatus: definition.editorialStatus,
45
+ scope: definition.scope,
46
+ href:
47
+ definition.scope === 'shared'
48
+ ? `${glossaryBase}#notation-${definition.key}`
49
+ : `#notation-${definition.key}`,
50
+ });
51
+ }
52
+ }
53
+ ---
54
+
55
+ <NotationLayer definitions={definitions} />
56
+ {lessonId && <CitationLayer />}
57
+ {lessonId && <EquationEnhancer />}
58
+ <DefaultFooter />