explico 0.1.1 → 0.3.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 +102 -0
- package/README.md +46 -5
- package/dist/compiler/cli.d.ts +17 -0
- package/dist/compiler/cli.js +41 -3
- package/dist/compiler/collections.js +31 -1
- package/dist/compiler/course-config.d.ts +52 -1
- package/dist/compiler/knowledge-version.d.ts +58 -0
- package/dist/compiler/knowledge-version.js +80 -0
- package/dist/compiler/manifest.d.ts +87 -3
- package/dist/compiler/manifest.js +133 -20
- package/dist/compiler/resolution-report.d.ts +5 -0
- package/dist/reference/base-operators.d.mts +38 -0
- package/dist/reference/base-operators.mjs +103 -0
- package/dist/reference/consistency.d.ts +7 -2
- package/dist/reference/consistency.js +142 -2
- package/dist/reference/diagram-layout.d.ts +63 -0
- package/dist/reference/diagram-layout.js +122 -0
- package/dist/reference/equations-validate.d.ts +5 -0
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +76 -18
- package/dist/reference/equations.mjs +118 -41
- package/dist/reference/figures-validate.d.ts +21 -0
- package/dist/reference/figures-validate.js +13 -0
- package/dist/reference/gate-math.d.ts +5 -4
- package/dist/reference/gate-math.js +73 -2
- package/dist/reference/gloss.d.ts +87 -0
- package/dist/reference/gloss.js +59 -0
- package/dist/reference/keyed-float-validate.d.ts +67 -0
- package/dist/reference/keyed-float-validate.js +110 -0
- package/dist/reference/keyed-jsx-floats.d.mts +56 -0
- package/dist/reference/keyed-jsx-floats.mjs +167 -0
- package/dist/reference/math-glyphs.d.mts +18 -0
- package/dist/reference/math-glyphs.mjs +92 -0
- package/dist/reference/registry.js +25 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -0
- package/dist/reference/rehype-heading-anchors.mjs +144 -0
- package/dist/reference/remark-notation.d.mts +12 -0
- package/dist/reference/remark-notation.mjs +228 -32
- package/dist/reference/tables-validate.d.ts +21 -0
- package/dist/reference/tables-validate.js +13 -0
- package/dist/reference/types.d.ts +25 -2
- package/dist/reference/units-vocab.js +19 -1
- package/docs/VERSIONING.md +66 -0
- package/docs/authoring/README.md +96 -0
- package/docs/authoring/agents.md +63 -0
- package/docs/authoring/migrating.md +107 -0
- package/docs/authoring/notation.md +296 -0
- package/docs/authoring/versioning.md +116 -0
- package/docs/authoring/visualizations.md +195 -0
- package/package.json +20 -5
- package/src/compiler/cli.ts +49 -0
- package/src/compiler/collections.ts +52 -3
- package/src/compiler/course-config.ts +54 -0
- package/src/compiler/knowledge-version.ts +157 -0
- package/src/compiler/manifest.ts +270 -17
- package/src/components/CurriculumMap.astro +27 -0
- package/src/components/HoverPanel.astro +42 -0
- package/src/components/citation/CitationLayer.astro +1 -0
- package/src/components/diagram/Diagram.astro +317 -0
- package/src/components/examples/CompactExamples.astro +1 -1
- package/src/components/figure/Figure.astro +104 -0
- package/src/components/hover-panel.ts +71 -5
- package/src/components/notation/NotationFormula.astro +154 -0
- package/src/components/notation/NotationGlossary.astro +60 -0
- package/src/components/notation/NotationLayer.astro +248 -103
- package/src/components/notation/render-notation-math.ts +66 -0
- package/src/components/popup-stack.ts +345 -0
- package/src/components/reference/FloatRefLayer.astro +671 -0
- package/src/components/starlight/AboutPanel.astro +255 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +131 -0
- package/src/components/starlight/KeyedFloatEnhancer.astro +137 -0
- package/src/components/starlight/LayoutSocialIcons.astro +12 -0
- package/src/components/starlight/LessonFooter.astro +8 -2
- package/src/components/table/Table.astro +104 -0
- package/src/content-config.ts +38 -0
- package/src/reference/base-operators.mjs +103 -0
- package/src/reference/consistency.ts +236 -2
- package/src/reference/diagram-layout.ts +246 -0
- package/src/reference/equations-validate.ts +6 -1
- package/src/reference/equations.mjs +118 -41
- package/src/reference/figures-validate.ts +40 -0
- package/src/reference/gate-math.ts +104 -7
- package/src/reference/gloss.ts +153 -0
- package/src/reference/keyed-float-validate.ts +207 -0
- package/src/reference/keyed-jsx-floats.mjs +167 -0
- package/src/reference/math-glyphs.mjs +92 -0
- package/src/reference/registry.ts +43 -0
- package/src/reference/rehype-heading-anchors.mjs +144 -0
- package/src/reference/remark-notation.mjs +228 -32
- package/src/reference/tables-validate.ts +40 -0
- package/src/reference/types.ts +26 -1
- package/src/reference/units-vocab.ts +18 -0
- package/src/styles/global.css +193 -3
- package/src/components/starlight/EquationEnhancer.astro +0 -72
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection, render } from 'astro:content';
|
|
3
3
|
import NotationMath from './NotationMath.astro';
|
|
4
|
+
import NotationFormula from './NotationFormula.astro';
|
|
4
5
|
import NotationLayer, {
|
|
5
6
|
type NotationLayerDefinition,
|
|
6
7
|
} from './NotationLayer.astro';
|
|
7
8
|
import { resolveLabel } from '../../reference/label';
|
|
9
|
+
import { formulaGlyphScope, resolveGlosses } from '../../reference/gloss';
|
|
10
|
+
import type { GlyphScopeEntry, ResolvedGloss } from '../../reference/gloss';
|
|
8
11
|
import {
|
|
9
12
|
isLessonSlug,
|
|
10
13
|
lessonIdFromSlug,
|
|
@@ -38,6 +41,35 @@ for (const backlink of manifest.notation.backlinks) {
|
|
|
38
41
|
lessonsByNotationKey.set(backlink.key, current);
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
// The two-tier vocabulary (D15). Every card is a node here; its glosses are
|
|
45
|
+
// not — they are symbol-and-name entries owned by the card whose `formula`
|
|
46
|
+
// needs them, kept out of the registry so a definitional letter never becomes
|
|
47
|
+
// a curriculum edge.
|
|
48
|
+
const cardsByKey = new Map<string, GlyphScopeEntry>(
|
|
49
|
+
notationEntries.map((entry) => [
|
|
50
|
+
entry.data.key,
|
|
51
|
+
{ key: entry.data.key, notation: entry.data.latex },
|
|
52
|
+
]),
|
|
53
|
+
);
|
|
54
|
+
const glossesByKey = new Map<string, ResolvedGloss[]>(
|
|
55
|
+
notationEntries.map((entry) => [
|
|
56
|
+
entry.data.key,
|
|
57
|
+
resolveGlosses(entry.data.key, entry.data.glosses),
|
|
58
|
+
]),
|
|
59
|
+
);
|
|
60
|
+
const formulaScopeByKey = new Map<string, GlyphScopeEntry[]>(
|
|
61
|
+
notationEntries.map((entry) => [
|
|
62
|
+
entry.data.key,
|
|
63
|
+
formulaGlyphScope({
|
|
64
|
+
key: entry.data.key,
|
|
65
|
+
notation: entry.data.latex,
|
|
66
|
+
formula: entry.data.formula,
|
|
67
|
+
glosses: glossesByKey.get(entry.data.key) ?? [],
|
|
68
|
+
cardsByKey,
|
|
69
|
+
}),
|
|
70
|
+
]),
|
|
71
|
+
);
|
|
72
|
+
|
|
41
73
|
const renderedEntries = await Promise.all(
|
|
42
74
|
notationEntries
|
|
43
75
|
.sort(
|
|
@@ -60,6 +92,9 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
|
|
|
60
92
|
notation: entry.data.latex,
|
|
61
93
|
label: labelOf(entry.data),
|
|
62
94
|
summary: entry.data.meaning,
|
|
95
|
+
formula: entry.data.formula,
|
|
96
|
+
formulaScope: formulaScopeByKey.get(entry.data.key),
|
|
97
|
+
glosses: glossesByKey.get(entry.data.key),
|
|
63
98
|
units: entry.data.units,
|
|
64
99
|
seeAlso: entry.data.seeAlso,
|
|
65
100
|
editorialStatus: entry.data.editorialStatus,
|
|
@@ -149,6 +184,19 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
|
|
|
149
184
|
)}
|
|
150
185
|
</dl>
|
|
151
186
|
|
|
187
|
+
<p class="glossary-meaning">{entry.data.meaning}</p>
|
|
188
|
+
|
|
189
|
+
{entry.data.formula && (
|
|
190
|
+
<div class="glossary-formula">
|
|
191
|
+
<NotationFormula
|
|
192
|
+
formula={entry.data.formula}
|
|
193
|
+
scope={formulaScopeByKey.get(entry.data.key) ?? []}
|
|
194
|
+
glosses={glossesByKey.get(entry.data.key)}
|
|
195
|
+
entryLabel={labelOf(entry.data)}
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
|
|
152
200
|
<div class="glossary-definition">
|
|
153
201
|
<Content />
|
|
154
202
|
</div>
|
|
@@ -364,6 +412,18 @@ const panelDefinitions: NotationLayerDefinition[] = notationEntries.map(
|
|
|
364
412
|
margin: 0;
|
|
365
413
|
}
|
|
366
414
|
|
|
415
|
+
.glossary-meaning {
|
|
416
|
+
margin-block: 0 1rem;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.glossary-formula {
|
|
420
|
+
margin-block: 1rem;
|
|
421
|
+
padding: 0.85rem 1rem;
|
|
422
|
+
border: 1px solid var(--sl-color-gray-5);
|
|
423
|
+
border-radius: 0.5rem;
|
|
424
|
+
background: var(--sl-color-bg);
|
|
425
|
+
}
|
|
426
|
+
|
|
367
427
|
.see-also {
|
|
368
428
|
display: flex;
|
|
369
429
|
flex-wrap: wrap;
|
|
@@ -11,8 +11,16 @@ export interface NotationLayerDefinition {
|
|
|
11
11
|
key: string;
|
|
12
12
|
notation: string;
|
|
13
13
|
label: string;
|
|
14
|
-
|
|
14
|
+
/** A card's `meaning`. Absent on a gloss, which is a symbol and a name. */
|
|
15
|
+
summary?: string;
|
|
15
16
|
formula?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Entry-scoped glyph table for `formula` (D15). Without it the formula
|
|
19
|
+
* renders inert, as it did before the two-tier vocabulary.
|
|
20
|
+
*/
|
|
21
|
+
formulaScope?: readonly GlyphScopeEntry[];
|
|
22
|
+
/** Symbols the formula names that have no card of their own. */
|
|
23
|
+
glosses?: readonly ResolvedGloss[];
|
|
16
24
|
units?: string;
|
|
17
25
|
seeAlso?: readonly string[];
|
|
18
26
|
alignment?: NotationAlignment;
|
|
@@ -37,7 +45,8 @@ export interface Props {
|
|
|
37
45
|
}
|
|
38
46
|
|
|
39
47
|
import NotationMath from './NotationMath.astro';
|
|
40
|
-
import
|
|
48
|
+
import NotationFormula from './NotationFormula.astro';
|
|
49
|
+
import type { GlyphScopeEntry, ResolvedGloss } from '../../reference/gloss';
|
|
41
50
|
|
|
42
51
|
const {
|
|
43
52
|
definitions,
|
|
@@ -78,7 +87,21 @@ const items = definitions.map((definition) => {
|
|
|
78
87
|
return { ...definition, scope, href };
|
|
79
88
|
});
|
|
80
89
|
|
|
81
|
-
|
|
90
|
+
// Glosses (D15) have no card and no anchor, so they need a data source for the
|
|
91
|
+
// panel. Their names are already visible under the formula they belong to
|
|
92
|
+
// (`NotationFormula` renders that list, and it is the no-JS baseline), so these
|
|
93
|
+
// blocks are hidden and exist only for the enhancement to read.
|
|
94
|
+
const glossItems = items.flatMap((definition) => definition.glosses ?? []);
|
|
95
|
+
|
|
96
|
+
for (const gloss of glossItems) {
|
|
97
|
+
if (!keyPattern.test(gloss.key)) {
|
|
98
|
+
throw new Error(`Unsafe notation gloss key: ${gloss.key}`);
|
|
99
|
+
}
|
|
100
|
+
if (seen.has(gloss.key)) {
|
|
101
|
+
throw new Error(`Duplicate notation key in NotationLayer: ${gloss.key}`);
|
|
102
|
+
}
|
|
103
|
+
seen.add(gloss.key);
|
|
104
|
+
}
|
|
82
105
|
---
|
|
83
106
|
|
|
84
107
|
{
|
|
@@ -134,12 +157,22 @@ const panelId = `${id}-panel`;
|
|
|
134
157
|
)}
|
|
135
158
|
</dt>
|
|
136
159
|
<dd>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<span class="notation-formula" data-notation-formula>
|
|
140
|
-
<NotationMath latex={definition.formula} />
|
|
141
|
-
</span>
|
|
160
|
+
{definition.summary && (
|
|
161
|
+
<p data-notation-summary>{definition.summary}</p>
|
|
142
162
|
)}
|
|
163
|
+
{definition.formula &&
|
|
164
|
+
(definition.formulaScope ? (
|
|
165
|
+
<NotationFormula
|
|
166
|
+
formula={definition.formula}
|
|
167
|
+
scope={definition.formulaScope}
|
|
168
|
+
glosses={definition.glosses}
|
|
169
|
+
entryLabel={definition.label}
|
|
170
|
+
/>
|
|
171
|
+
) : (
|
|
172
|
+
<span class="notation-formula" data-notation-formula>
|
|
173
|
+
<NotationMath latex={definition.formula} />
|
|
174
|
+
</span>
|
|
175
|
+
))}
|
|
143
176
|
{definition.units && (
|
|
144
177
|
<p class="notation-units" data-notation-units>
|
|
145
178
|
<strong>Units:</strong> {definition.units}
|
|
@@ -149,7 +182,6 @@ const panelId = `${id}-panel`;
|
|
|
149
182
|
class="notation-pin"
|
|
150
183
|
type="button"
|
|
151
184
|
data-notation-control={definition.key}
|
|
152
|
-
aria-controls={panelId}
|
|
153
185
|
aria-expanded="false"
|
|
154
186
|
aria-pressed="false"
|
|
155
187
|
hidden
|
|
@@ -160,34 +192,71 @@ const panelId = `${id}-panel`;
|
|
|
160
192
|
</div>
|
|
161
193
|
))}
|
|
162
194
|
</dl>
|
|
195
|
+
|
|
196
|
+
{glossItems.map((gloss) => (
|
|
197
|
+
<div
|
|
198
|
+
class="notation-gloss-source"
|
|
199
|
+
data-notation-entry={gloss.key}
|
|
200
|
+
hidden
|
|
201
|
+
>
|
|
202
|
+
<span class="notation-symbol-trigger" data-notation-key={gloss.key}>
|
|
203
|
+
<NotationMath class="notation-symbol" latex={gloss.notation} />
|
|
204
|
+
</span>
|
|
205
|
+
<span data-notation-link={gloss.key}>{gloss.label}</span>
|
|
206
|
+
{gloss.units && (
|
|
207
|
+
<span data-notation-units>
|
|
208
|
+
<strong>Units:</strong> {gloss.units}
|
|
209
|
+
</span>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
))}
|
|
163
213
|
</details>
|
|
164
214
|
|
|
165
215
|
{!panelOnly && (
|
|
166
216
|
<p class="notation-enhancement-help" data-notation-help hidden>
|
|
167
217
|
Point to a marked symbol for a quick explanation, or use the list
|
|
168
|
-
above
|
|
169
|
-
|
|
218
|
+
above — pointing at a symbol inside an open card explains that one
|
|
219
|
+
too, alongside it. Click a symbol, or select “Pin explanation”, to
|
|
220
|
+
keep a card open; click the card (or its “×”) to close it, or press
|
|
221
|
+
Escape.
|
|
170
222
|
</p>
|
|
171
223
|
)}
|
|
172
224
|
|
|
173
|
-
<
|
|
174
|
-
<
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
225
|
+
<template data-notation-popup-template>
|
|
226
|
+
<aside
|
|
227
|
+
class="notation-popup"
|
|
228
|
+
role="region"
|
|
229
|
+
aria-atomic="true"
|
|
230
|
+
tabindex="-1"
|
|
231
|
+
>
|
|
232
|
+
<button
|
|
233
|
+
class="notation-popup-close"
|
|
234
|
+
type="button"
|
|
235
|
+
data-popup-close
|
|
236
|
+
aria-label="Close"
|
|
237
|
+
>
|
|
238
|
+
<span aria-hidden="true">×</span>
|
|
239
|
+
</button>
|
|
240
|
+
<div class="notation-panel-body">
|
|
241
|
+
<header class="notation-panel-heading">
|
|
242
|
+
<span class="notation-panel-symbol" data-slot-symbol />
|
|
243
|
+
<h3 data-slot-title />
|
|
244
|
+
</header>
|
|
245
|
+
<p data-slot-summary hidden />
|
|
246
|
+
<div data-slot-formula hidden />
|
|
247
|
+
<p class="notation-units" data-slot-units hidden />
|
|
248
|
+
<a data-slot-link hidden>
|
|
249
|
+
Open the full definition
|
|
250
|
+
</a>
|
|
251
|
+
</div>
|
|
252
|
+
</aside>
|
|
253
|
+
</template>
|
|
185
254
|
</section>
|
|
186
255
|
)
|
|
187
256
|
}
|
|
188
257
|
|
|
189
258
|
<script>
|
|
190
|
-
import {
|
|
259
|
+
import { createPopupStack, closestElement } from '../popup-stack';
|
|
191
260
|
|
|
192
261
|
const triggerSelector = '[data-notation-key]';
|
|
193
262
|
|
|
@@ -209,34 +278,24 @@ const panelId = `${id}-panel`;
|
|
|
209
278
|
entries.delete('');
|
|
210
279
|
if (entries.size === 0) return;
|
|
211
280
|
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
const panelTitle = root.querySelector<HTMLElement>('[data-panel-title]');
|
|
215
|
-
const panelSummary = root.querySelector<HTMLElement>(
|
|
216
|
-
'[data-panel-summary]',
|
|
281
|
+
const template = root.querySelector<HTMLTemplateElement>(
|
|
282
|
+
'[data-notation-popup-template]',
|
|
217
283
|
);
|
|
218
|
-
|
|
219
|
-
'[data-panel-formula]',
|
|
220
|
-
);
|
|
221
|
-
const panelUnits = root.querySelector<HTMLElement>('[data-panel-units]');
|
|
222
|
-
const panelLink =
|
|
223
|
-
root.querySelector<HTMLAnchorElement>('[data-panel-link]');
|
|
224
|
-
if (
|
|
225
|
-
!panel ||
|
|
226
|
-
!panelSymbol ||
|
|
227
|
-
!panelTitle ||
|
|
228
|
-
!panelSummary ||
|
|
229
|
-
!panelFormula ||
|
|
230
|
-
!panelUnits ||
|
|
231
|
-
!panelLink
|
|
232
|
-
) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
284
|
+
if (!template) return;
|
|
235
285
|
|
|
236
286
|
const controls = Array.from(
|
|
237
287
|
root.querySelectorAll<HTMLButtonElement>('[data-notation-control]'),
|
|
238
288
|
);
|
|
239
|
-
|
|
289
|
+
|
|
290
|
+
// Static triggers, computed once: markers embedded in prose or an
|
|
291
|
+
// equation, and the symbol glyphs in the "Notation used on this page"
|
|
292
|
+
// list. These get the always-on `.notation-trigger` cursor/hover
|
|
293
|
+
// treatment. Symbols that appear *inside a popup* (a card's own formula,
|
|
294
|
+
// D15) are not in this list — there's no bound on how many of those can
|
|
295
|
+
// exist — but they still work as triggers: `triggerFrom` below finds
|
|
296
|
+
// them live, and the shared `[data-notation-key]:hover` rule (declared
|
|
297
|
+
// `:global`, further down) already paints them the same way.
|
|
298
|
+
const externalTriggers = Array.from(
|
|
240
299
|
document.querySelectorAll<HTMLElement>(triggerSelector),
|
|
241
300
|
).filter(
|
|
242
301
|
(element) =>
|
|
@@ -244,17 +303,10 @@ const panelId = `${id}-panel`;
|
|
|
244
303
|
!element.closest('.katex-mathml') &&
|
|
245
304
|
entries.has(element.dataset.notationKey ?? ''),
|
|
246
305
|
);
|
|
247
|
-
const triggerSet = new Set(triggers);
|
|
248
|
-
|
|
249
|
-
// Symbol glyphs rendered inside the page-notation list. They live within
|
|
250
|
-
// `root`, so the external-marker filter above deliberately excludes them;
|
|
251
|
-
// wire them as their own trigger set so pointing at a listed symbol opens
|
|
252
|
-
// the same explanation as pointing at one embedded in prose or an equation.
|
|
253
306
|
const symbolTriggers = Array.from(
|
|
254
307
|
root.querySelectorAll<HTMLElement>('[data-notation-symbol-trigger]'),
|
|
255
308
|
).filter((element) => entries.has(element.dataset.notationKey ?? ''));
|
|
256
|
-
const
|
|
257
|
-
const highlightTargets = [...triggers, ...symbolTriggers];
|
|
309
|
+
const highlightTargets = [...externalTriggers, ...symbolTriggers];
|
|
258
310
|
|
|
259
311
|
for (const trigger of highlightTargets) {
|
|
260
312
|
trigger.classList.add('notation-trigger');
|
|
@@ -269,16 +321,6 @@ const panelId = `${id}-panel`;
|
|
|
269
321
|
element?.dataset.notationLink ??
|
|
270
322
|
null;
|
|
271
323
|
|
|
272
|
-
const externalTriggerFrom = (node: EventTarget | null) => {
|
|
273
|
-
const candidate = closestElement(node, triggerSelector);
|
|
274
|
-
return candidate && triggerSet.has(candidate) ? candidate : null;
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
const symbolTriggerFrom = (node: EventTarget | null) => {
|
|
278
|
-
const candidate = closestElement(node, '[data-notation-symbol-trigger]');
|
|
279
|
-
return candidate && symbolTriggerSet.has(candidate) ? candidate : null;
|
|
280
|
-
};
|
|
281
|
-
|
|
282
324
|
const controlFrom = (node: EventTarget | null) => {
|
|
283
325
|
const candidate = closestElement(node, '[data-notation-control]');
|
|
284
326
|
return candidate && root.contains(candidate) ? candidate : null;
|
|
@@ -289,81 +331,125 @@ const panelId = `${id}-panel`;
|
|
|
289
331
|
return candidate && root.contains(candidate) ? candidate : null;
|
|
290
332
|
};
|
|
291
333
|
|
|
334
|
+
// A symbol trigger, wherever it lives: prose, the equation, the visible
|
|
335
|
+
// list, or — the D15 case this exists for — inside an already-open
|
|
336
|
+
// popup's own formula. Unlike the static sets above, this has to be
|
|
337
|
+
// live: a popup's content doesn't exist until the reader opens it.
|
|
338
|
+
const symbolFrom = (node: EventTarget | null) => {
|
|
339
|
+
const candidate = closestElement(node, triggerSelector);
|
|
340
|
+
if (!candidate || candidate.closest('.katex-mathml')) return null;
|
|
341
|
+
return entries.has(candidate.dataset.notationKey ?? '')
|
|
342
|
+
? candidate
|
|
343
|
+
: null;
|
|
344
|
+
};
|
|
345
|
+
|
|
292
346
|
const triggerFrom = (node: EventTarget | null) =>
|
|
293
|
-
controlFrom(node) ??
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
347
|
+
controlFrom(node) ?? staticLinkFrom(node) ?? symbolFrom(node);
|
|
348
|
+
|
|
349
|
+
const labelOf = (entry: HTMLElement, key: string) =>
|
|
350
|
+
entry
|
|
351
|
+
.querySelector<HTMLElement>('[data-notation-link]')
|
|
352
|
+
?.textContent?.trim() ?? key;
|
|
297
353
|
|
|
298
|
-
const
|
|
354
|
+
const createPopup = (key: string, _trigger: HTMLElement) => {
|
|
299
355
|
const entry = entries.get(key);
|
|
300
|
-
if (!entry) return
|
|
356
|
+
if (!entry) return null;
|
|
357
|
+
|
|
358
|
+
const fragment = template.content.cloneNode(true) as DocumentFragment;
|
|
359
|
+
const el = fragment.querySelector<HTMLElement>('.notation-popup');
|
|
360
|
+
const symbolHost =
|
|
361
|
+
fragment.querySelector<HTMLElement>('[data-slot-symbol]');
|
|
362
|
+
const title = fragment.querySelector<HTMLElement>('[data-slot-title]');
|
|
363
|
+
const summary = fragment.querySelector<HTMLElement>(
|
|
364
|
+
'[data-slot-summary]',
|
|
365
|
+
);
|
|
366
|
+
const formulaHost = fragment.querySelector<HTMLElement>(
|
|
367
|
+
'[data-slot-formula]',
|
|
368
|
+
);
|
|
369
|
+
const units = fragment.querySelector<HTMLElement>('[data-slot-units]');
|
|
370
|
+
const link =
|
|
371
|
+
fragment.querySelector<HTMLAnchorElement>('[data-slot-link]');
|
|
372
|
+
if (!el || !symbolHost || !title || !summary || !formulaHost || !units) {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
el.setAttribute('aria-label', `Notation: ${labelOf(entry, key)}`);
|
|
301
377
|
|
|
302
378
|
const sourceSymbol = entry.querySelector<HTMLElement>('.notation-symbol');
|
|
303
|
-
|
|
379
|
+
symbolHost.replaceChildren(
|
|
304
380
|
...(sourceSymbol
|
|
305
381
|
? [sourceSymbol.cloneNode(true)]
|
|
306
382
|
: [document.createTextNode(entry.dataset.notationEntry ?? key)]),
|
|
307
383
|
);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
?.textContent?.trim() ?? key;
|
|
312
|
-
panelSummary.textContent =
|
|
384
|
+
title.textContent = labelOf(entry, key);
|
|
385
|
+
setOptionalText(
|
|
386
|
+
summary,
|
|
313
387
|
entry
|
|
314
388
|
.querySelector<HTMLElement>('[data-notation-summary]')
|
|
315
|
-
?.textContent?.trim() ?? ''
|
|
389
|
+
?.textContent?.trim() ?? '',
|
|
390
|
+
);
|
|
316
391
|
const sourceFormula = entry.querySelector<HTMLElement>(
|
|
317
392
|
'[data-notation-formula]',
|
|
318
393
|
);
|
|
319
|
-
|
|
394
|
+
formulaHost.replaceChildren(
|
|
320
395
|
...(sourceFormula ? [sourceFormula.cloneNode(true)] : []),
|
|
321
396
|
);
|
|
322
|
-
|
|
397
|
+
formulaHost.hidden = sourceFormula === null;
|
|
323
398
|
setOptionalText(
|
|
324
|
-
|
|
399
|
+
units,
|
|
325
400
|
entry
|
|
326
401
|
.querySelector<HTMLElement>('[data-notation-units]')
|
|
327
402
|
?.textContent?.trim() ?? '',
|
|
328
403
|
);
|
|
329
|
-
|
|
330
|
-
|
|
404
|
+
|
|
405
|
+
// A gloss has no card to open. Its own popup is the whole of it.
|
|
406
|
+
const href = entry.dataset.notationHref;
|
|
407
|
+
if (link) {
|
|
408
|
+
link.hidden = href === undefined;
|
|
409
|
+
if (href !== undefined) link.href = href;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return el;
|
|
331
413
|
};
|
|
332
414
|
|
|
333
|
-
const updateControls = (
|
|
415
|
+
const updateControls = (
|
|
416
|
+
open: ReadonlySet<string>,
|
|
417
|
+
pinned: ReadonlySet<string>,
|
|
418
|
+
) => {
|
|
334
419
|
for (const control of controls) {
|
|
335
|
-
const
|
|
420
|
+
const key = control.dataset.notationControl ?? '';
|
|
421
|
+
const selected = pinned.has(key);
|
|
336
422
|
control.setAttribute('aria-pressed', String(selected));
|
|
337
|
-
control.setAttribute('aria-expanded', String(
|
|
423
|
+
control.setAttribute('aria-expanded', String(open.has(key)));
|
|
338
424
|
control.textContent = selected
|
|
339
425
|
? 'Unpin explanation'
|
|
340
426
|
: 'Pin explanation';
|
|
341
427
|
}
|
|
342
428
|
};
|
|
343
429
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
430
|
+
createPopupStack({
|
|
431
|
+
// Popups are `position: fixed` and float above everything regardless
|
|
432
|
+
// of DOM placement — *except* z-index only ever wins within its own
|
|
433
|
+
// stacking context. Nesting them under the article risked landing
|
|
434
|
+
// inside a stacking context an ancestor happens to establish (found
|
|
435
|
+
// live: the Starlight right sidebar then ate their clicks even though
|
|
436
|
+
// its own z-index is far lower). Mounting on `<body>` sidesteps that
|
|
437
|
+
// entirely.
|
|
438
|
+
container: document.body,
|
|
439
|
+
groupId: 'notation',
|
|
347
440
|
triggerFrom,
|
|
348
441
|
keyFor,
|
|
349
|
-
|
|
442
|
+
createPopup,
|
|
350
443
|
pinControlFrom: controlFrom,
|
|
351
|
-
|
|
444
|
+
onChange: (open, pinned) => {
|
|
352
445
|
for (const trigger of highlightTargets) {
|
|
353
446
|
trigger.classList.toggle(
|
|
354
447
|
'is-notation-active',
|
|
355
|
-
trigger.dataset.notationKey
|
|
448
|
+
open.has(trigger.dataset.notationKey ?? ''),
|
|
356
449
|
);
|
|
357
450
|
}
|
|
358
|
-
|
|
359
|
-
},
|
|
360
|
-
onClose: () => {
|
|
361
|
-
delete panel.dataset.activeNotationKey;
|
|
362
|
-
for (const trigger of highlightTargets) {
|
|
363
|
-
trigger.classList.remove('is-notation-active');
|
|
364
|
-
}
|
|
451
|
+
updateControls(open, pinned);
|
|
365
452
|
},
|
|
366
|
-
onSync: ({ pinnedKey, visible }) => updateControls(pinnedKey, visible),
|
|
367
453
|
});
|
|
368
454
|
}
|
|
369
455
|
|
|
@@ -514,8 +600,67 @@ const panelId = `${id}-panel`;
|
|
|
514
600
|
font-size: var(--sl-text-xs);
|
|
515
601
|
}
|
|
516
602
|
|
|
517
|
-
/*
|
|
518
|
-
|
|
603
|
+
/* Each open explanation is its own floating card — see `popup-stack.ts`.
|
|
604
|
+
They're mounted on `<body>`, not here (a stacking-context trap found
|
|
605
|
+
live: an ancestor of this section could otherwise cap how high a
|
|
606
|
+
popup's own z-index reaches). `.notation-panel-body` and its
|
|
607
|
+
descendants below style the fields common to both this static list
|
|
608
|
+
and a popup's cloned content. */
|
|
609
|
+
.notation-popup {
|
|
610
|
+
position: fixed;
|
|
611
|
+
z-index: 100;
|
|
612
|
+
margin: 0;
|
|
613
|
+
width: min(23rem, calc(100vw - 1.5rem));
|
|
614
|
+
overflow: auto;
|
|
615
|
+
padding: 1rem;
|
|
616
|
+
padding-inline-end: 2.4rem;
|
|
617
|
+
border: 1px solid var(--sl-color-gray-5);
|
|
618
|
+
border-top: 0.25rem solid var(--sl-color-accent);
|
|
619
|
+
border-radius: 0.65rem;
|
|
620
|
+
color: var(--sl-color-text);
|
|
621
|
+
background: var(--sl-color-bg);
|
|
622
|
+
box-shadow: var(--sl-shadow-lg);
|
|
623
|
+
font-size: var(--sl-text-sm);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.notation-popup-close {
|
|
627
|
+
position: absolute;
|
|
628
|
+
top: 0.5rem;
|
|
629
|
+
right: 0.5rem;
|
|
630
|
+
width: 1.6rem;
|
|
631
|
+
height: 1.6rem;
|
|
632
|
+
display: flex;
|
|
633
|
+
align-items: center;
|
|
634
|
+
justify-content: center;
|
|
635
|
+
padding: 0;
|
|
636
|
+
border: 0;
|
|
637
|
+
border-radius: 0.35rem;
|
|
638
|
+
color: var(--sl-color-gray-3);
|
|
639
|
+
background: transparent;
|
|
640
|
+
font-size: 1.15rem;
|
|
641
|
+
line-height: 1;
|
|
642
|
+
cursor: pointer;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.notation-popup-close:hover {
|
|
646
|
+
color: var(--sl-color-text);
|
|
647
|
+
background: var(--sl-color-gray-6);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/* Symbols inside a card's formula are click/hover-through triggers for
|
|
651
|
+
another card — the D15 case this whole file exists for. */
|
|
652
|
+
.notation-panel-body [data-slot-formula] :global([data-notation-key]) {
|
|
653
|
+
border-radius: 0.16em;
|
|
654
|
+
cursor: pointer;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.notation-panel-body
|
|
658
|
+
[data-slot-formula]
|
|
659
|
+
:global([data-notation-key]:hover) {
|
|
660
|
+
background: var(--sl-color-accent-low);
|
|
661
|
+
box-shadow: 0 0 0 0.12em var(--sl-color-accent-low);
|
|
662
|
+
}
|
|
663
|
+
|
|
519
664
|
.notation-panel-heading {
|
|
520
665
|
display: flex;
|
|
521
666
|
align-items: baseline;
|
|
@@ -538,7 +683,7 @@ const panelId = `${id}-panel`;
|
|
|
538
683
|
line-height: 1.5;
|
|
539
684
|
}
|
|
540
685
|
|
|
541
|
-
.notation-panel-body [data-
|
|
686
|
+
.notation-panel-body [data-slot-formula] {
|
|
542
687
|
overflow-x: auto;
|
|
543
688
|
margin-block: 0.65rem;
|
|
544
689
|
color: var(--sl-color-text);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import rehypeKatex from 'rehype-katex';
|
|
2
2
|
import { createNotationKatexOptions } from '../../reference/katex-options.mjs';
|
|
3
|
+
import { NOTATION_KEY_PATTERN } from '../../reference/remark-notation.mjs';
|
|
4
|
+
import { resolveMathGlyphs } from '../../reference/math-glyphs.mjs';
|
|
5
|
+
import type { GlyphScopeEntry } from '../../reference/gloss';
|
|
3
6
|
|
|
4
7
|
interface HastText {
|
|
5
8
|
type: 'text';
|
|
@@ -122,6 +125,20 @@ function safeProperties(
|
|
|
122
125
|
continue;
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
// The one marker attribute this renderer emits, and the only one KaTeX's
|
|
129
|
+
// trust callback lets through: `\explain{key}{latex}` expands to
|
|
130
|
+
// `\htmlData{notation-key=key}{latex}` so a resolved `formula` carries
|
|
131
|
+
// live, hoverable glyphs. Re-validate the key here rather than trusting
|
|
132
|
+
// the upstream check — this is the boundary that decides what reaches the
|
|
133
|
+
// page, and it must stay exactly as narrow as `trustNotationMarker`.
|
|
134
|
+
if (name === 'dataNotationKey') {
|
|
135
|
+
if (typeof value !== 'string' || !NOTATION_KEY_PATTERN.test(value)) {
|
|
136
|
+
throw new Error('Unsafe KaTeX notation key.');
|
|
137
|
+
}
|
|
138
|
+
properties['data-notation-key'] = value;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
125
142
|
if (name === 'encoding' && value === 'application/x-tex') {
|
|
126
143
|
properties.encoding = value;
|
|
127
144
|
continue;
|
|
@@ -221,3 +238,52 @@ export function renderNotationMath(latex: string): readonly SafeMathNode[] {
|
|
|
221
238
|
transform(root as unknown as Parameters<typeof transform>[0], file);
|
|
222
239
|
return Object.freeze(root.children.map(sanitizeNode));
|
|
223
240
|
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Render a notation entry's `formula` with its glyphs bound to keys, so every
|
|
244
|
+
* symbol in a rigorous definition is itself explainable (D15).
|
|
245
|
+
*
|
|
246
|
+
* `scope` is the entry-scoped table from `reference/gloss.ts` — the entry, its
|
|
247
|
+
* glosses, and the cards its formula names — not the registry-wide table a
|
|
248
|
+
* card's body math uses. Resolution is best-effort by design: an unresolved
|
|
249
|
+
* glyph renders inert rather than breaking the page, and is reported instead
|
|
250
|
+
* by the `formula` completeness gate (Tier 3) and by the CI test that holds
|
|
251
|
+
* the corpus at zero. A scope error degrades all the way to today's behaviour,
|
|
252
|
+
* an inert but correct equation.
|
|
253
|
+
*
|
|
254
|
+
* `unresolved` lists glyphs that are simply not in `scope`. `degraded` is a
|
|
255
|
+
* coarser, separate signal: it is `true` when resolution *threw* — an ambiguous
|
|
256
|
+
* bare base glyph, LaTeX that did not parse, a marker KaTeX or the sanitizer
|
|
257
|
+
* rejected — so the returned `nodes` fell back to the raw formula and carry no
|
|
258
|
+
* (or only literal `\explain`) markers. A caller that cannot tolerate silent,
|
|
259
|
+
* unmarked math (the `Diagram` component) turns `degraded` into a build error;
|
|
260
|
+
* `NotationFormula` keeps the inert-but-correct fallback.
|
|
261
|
+
*/
|
|
262
|
+
export function renderNotationFormula(
|
|
263
|
+
formula: string,
|
|
264
|
+
scope: readonly GlyphScopeEntry[],
|
|
265
|
+
): {
|
|
266
|
+
readonly nodes: readonly SafeMathNode[];
|
|
267
|
+
readonly unresolved: readonly string[];
|
|
268
|
+
readonly degraded: boolean;
|
|
269
|
+
} {
|
|
270
|
+
let bound = formula;
|
|
271
|
+
let unresolved: string[] = [];
|
|
272
|
+
let degraded = false;
|
|
273
|
+
try {
|
|
274
|
+
const result = resolveMathGlyphs(formula, [...scope]);
|
|
275
|
+
bound = result.latex;
|
|
276
|
+
unresolved = result.unresolved.map(({ token }: { token: string }) => token);
|
|
277
|
+
} catch {
|
|
278
|
+
bound = formula;
|
|
279
|
+
degraded = true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
return { nodes: renderNotationMath(bound), unresolved, degraded };
|
|
284
|
+
} catch {
|
|
285
|
+
// A marker that KaTeX or the sanitizer rejects must not cost the reader
|
|
286
|
+
// the equation itself.
|
|
287
|
+
return { nodes: renderNotationMath(formula), unresolved, degraded: true };
|
|
288
|
+
}
|
|
289
|
+
}
|