explico 0.1.0 → 0.2.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/dist/compiler/cli.d.ts +1 -0
- package/dist/compiler/cli.js +1 -1
- package/dist/compiler/collections.js +30 -3
- package/dist/compiler/manifest.d.ts +10 -1
- package/dist/compiler/manifest.js +28 -12
- package/dist/compiler/resolution-report.d.ts +1 -0
- package/dist/compiler/resolution-report.js +2 -2
- package/dist/reference/base-library.d.mts +17 -0
- package/dist/reference/citation-format.d.mts +55 -0
- package/dist/reference/consistency.d.ts +1 -1
- package/dist/reference/consistency.js +94 -3
- package/dist/reference/curriculum-alignment.js +1 -1
- package/dist/reference/equations.d.mts +84 -0
- package/dist/reference/equations.mjs +75 -39
- 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/katex-options.d.mts +33 -0
- package/dist/reference/lab-math-scope.js +1 -1
- package/dist/reference/math-glyphs.d.mts +46 -0
- package/dist/reference/registry.js +26 -1
- package/dist/reference/rehype-fail-katex-errors.d.mts +6 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -0
- package/dist/reference/rehype-heading-anchors.mjs +144 -0
- package/dist/reference/remark-citation.d.mts +6 -0
- package/dist/reference/remark-notation.d.mts +49 -0
- package/dist/reference/remark-notation.mjs +30 -24
- package/dist/reference/types.d.ts +25 -2
- package/dist/session/user-context.js +1 -1
- package/package.json +2 -2
- package/src/compiler/collections.ts +40 -0
- package/src/compiler/manifest.ts +31 -0
- package/src/components/HoverPanel.astro +42 -0
- package/src/components/citation/CitationLayer.astro +1 -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 +55 -0
- package/src/components/popup-stack.ts +345 -0
- package/src/components/starlight/EquationEnhancer.astro +20 -5
- package/src/components/starlight/HeadingAnchorEnhancer.astro +131 -0
- package/src/components/starlight/LessonFooter.astro +4 -0
- package/src/content-config.ts +38 -0
- package/src/reference/consistency.ts +154 -1
- package/src/reference/equations.mjs +75 -39
- package/src/reference/gate-math.ts +104 -7
- package/src/reference/gloss.ts +153 -0
- package/src/reference/registry.ts +43 -0
- package/src/reference/rehype-heading-anchors.mjs +144 -0
- package/src/reference/remark-notation.mjs +30 -24
- package/src/reference/types.ts +26 -1
- package/src/styles/global.css +84 -2
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep-linkable content headings (Step F3).
|
|
3
|
+
*
|
|
4
|
+
* The lesson pipeline uses a custom Markdown `processor` (see `astro.config.mjs`),
|
|
5
|
+
* which bypasses Starlight's own heading-link rehype pass. This plugin restores
|
|
6
|
+
* it with the *same* markup Starlight emits, so its bundled `anchor-links.css`
|
|
7
|
+
* (shipped globally because `markdown.headingLinks` defaults on) styles it for
|
|
8
|
+
* free — including the hover/focus reveal of an otherwise invisible anchor:
|
|
9
|
+
*
|
|
10
|
+
* <div class="sl-heading-wrapper level-h2">
|
|
11
|
+
* <h2 id="slug">Heading</h2>
|
|
12
|
+
* <a class="sl-anchor-link" href="#slug"> <icon> <sr-only label> </a>
|
|
13
|
+
* </div>
|
|
14
|
+
*
|
|
15
|
+
* The `:target` flash, sticky-header scroll offset, and the JS niceties
|
|
16
|
+
* (re-trigger on repeat activation, smooth scroll, `history.pushState`, brief
|
|
17
|
+
* section highlight) live in `global.css` + `HeadingAnchorEnhancer.astro`, the
|
|
18
|
+
* same split the keyed-equation feature uses.
|
|
19
|
+
*
|
|
20
|
+
* `rehypeHeadingIds` from `@astrojs/markdown-remark` normally runs *after* the
|
|
21
|
+
* user rehype plugins, so heading `id`s do not exist yet when this runs. It is
|
|
22
|
+
* idempotent (only assigns a missing `id`, and rebuilds `file.data.astro.headings`
|
|
23
|
+
* from an unchanged document order), so this plugin runs it first and Astro's
|
|
24
|
+
* later pass becomes a no-op for these headings.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
|
|
28
|
+
|
|
29
|
+
const HEADING_TAG = /^h([1-6])$/;
|
|
30
|
+
|
|
31
|
+
// Starlight's `link-alt` icon (`components-internals/Icons.ts`), the same glyph
|
|
32
|
+
// its own `rehypeAutolinkHeadings` uses.
|
|
33
|
+
const LINK_ICON_PATH =
|
|
34
|
+
'm12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42';
|
|
35
|
+
|
|
36
|
+
function headingText(node) {
|
|
37
|
+
let text = '';
|
|
38
|
+
const walk = (current) => {
|
|
39
|
+
if (!current || typeof current !== 'object') return;
|
|
40
|
+
if (current.type === 'text' || current.type === 'raw') {
|
|
41
|
+
text += String(current.value ?? '');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(current.children)) current.children.forEach(walk);
|
|
45
|
+
};
|
|
46
|
+
walk(node);
|
|
47
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isHeadingWrapper(node) {
|
|
51
|
+
const className = node?.properties?.className;
|
|
52
|
+
return (
|
|
53
|
+
node?.type === 'element' &&
|
|
54
|
+
Array.isArray(className) &&
|
|
55
|
+
className.includes('sl-heading-wrapper')
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function anchorLink(id, label) {
|
|
60
|
+
return {
|
|
61
|
+
type: 'element',
|
|
62
|
+
tagName: 'a',
|
|
63
|
+
properties: { className: ['sl-anchor-link'], href: `#${id}` },
|
|
64
|
+
children: [
|
|
65
|
+
{
|
|
66
|
+
type: 'element',
|
|
67
|
+
tagName: 'span',
|
|
68
|
+
properties: { ariaHidden: 'true', className: ['sl-anchor-icon'] },
|
|
69
|
+
children: [
|
|
70
|
+
{
|
|
71
|
+
type: 'element',
|
|
72
|
+
tagName: 'svg',
|
|
73
|
+
properties: {
|
|
74
|
+
width: 16,
|
|
75
|
+
height: 16,
|
|
76
|
+
viewBox: '0 0 24 24',
|
|
77
|
+
fill: 'currentColor',
|
|
78
|
+
},
|
|
79
|
+
children: [
|
|
80
|
+
{
|
|
81
|
+
type: 'element',
|
|
82
|
+
tagName: 'path',
|
|
83
|
+
properties: { d: LINK_ICON_PATH },
|
|
84
|
+
children: [],
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: 'element',
|
|
92
|
+
tagName: 'span',
|
|
93
|
+
properties: { className: ['sr-only'], 'data-pagefind-ignore': 'true' },
|
|
94
|
+
children: [{ type: 'text', value: label }],
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default function rehypeHeadingAnchors() {
|
|
101
|
+
const assignHeadingIds = rehypeHeadingIds();
|
|
102
|
+
|
|
103
|
+
return function transform(tree, file) {
|
|
104
|
+
assignHeadingIds(tree, file);
|
|
105
|
+
|
|
106
|
+
const walk = (node) => {
|
|
107
|
+
if (!node || typeof node !== 'object' || !Array.isArray(node.children)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (isHeadingWrapper(node)) return; // already processed
|
|
111
|
+
|
|
112
|
+
for (let index = 0; index < node.children.length; index++) {
|
|
113
|
+
const child = node.children[index];
|
|
114
|
+
const tag =
|
|
115
|
+
child?.type === 'element' && typeof child.tagName === 'string'
|
|
116
|
+
? HEADING_TAG.exec(child.tagName)
|
|
117
|
+
: null;
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
tag &&
|
|
121
|
+
tag[1] !== '1' &&
|
|
122
|
+
typeof child.properties?.id === 'string' &&
|
|
123
|
+
child.properties.id.length > 0
|
|
124
|
+
) {
|
|
125
|
+
const id = child.properties.id;
|
|
126
|
+
const label = `Section titled “${headingText(child)}”`;
|
|
127
|
+
node.children[index] = {
|
|
128
|
+
type: 'element',
|
|
129
|
+
tagName: 'div',
|
|
130
|
+
properties: {
|
|
131
|
+
className: ['sl-heading-wrapper', `level-${child.tagName}`],
|
|
132
|
+
},
|
|
133
|
+
children: [child, anchorLink(id, label)],
|
|
134
|
+
};
|
|
135
|
+
continue; // the heading is a leaf for our purposes
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
walk(child);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
walk(tree);
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{ sources?: Map<string, object> | readonly object[] | (() => Map<string, object> | readonly object[]) }} [options]
|
|
3
|
+
*/
|
|
4
|
+
export default function remarkCitation(options?: {
|
|
5
|
+
sources?: Map<string, object> | readonly object[] | (() => Map<string, object> | readonly object[]);
|
|
6
|
+
}): (tree: any, file: any) => void;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {{
|
|
3
|
+
* definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>),
|
|
4
|
+
* resolve?: (key: string, context: object) => object | undefined,
|
|
5
|
+
* unknown?: 'error' | 'warn' | 'ignore',
|
|
6
|
+
* base?: string,
|
|
7
|
+
* equations?: Record<string, Record<string, string>>,
|
|
8
|
+
* }} [options]
|
|
9
|
+
*/
|
|
10
|
+
export default function remarkNotation(options?: {
|
|
11
|
+
definitions?: Map<string, object> | readonly object[] | Record<string, object> | (() => Map<string, object> | readonly object[] | Record<string, object>);
|
|
12
|
+
resolve?: (key: string, context: object) => object | undefined;
|
|
13
|
+
unknown?: "error" | "warn" | "ignore";
|
|
14
|
+
base?: string;
|
|
15
|
+
equations?: Record<string, Record<string, string>>;
|
|
16
|
+
}): (tree: any, file: any) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Remark adapter for semantic notation references.
|
|
19
|
+
*
|
|
20
|
+
* Author syntax:
|
|
21
|
+
* - Prose introduces a meaning: `[[rates.discount-factor]]`
|
|
22
|
+
* - Later math uses ordinary LaTeX: `D(0,t)`
|
|
23
|
+
* - `\\explain{rates.discount-factor}{D(0,t)}` remains available inside math
|
|
24
|
+
* when lexical scope cannot select one meaning unambiguously.
|
|
25
|
+
*
|
|
26
|
+
* Prose references become ordinary links. Lesson math is resolved against the
|
|
27
|
+
* page glyph table — `notation.local` plus the shared entries the page names
|
|
28
|
+
* with `[[key]]` (or `\\explain{key}{…}`) — and the base library; the
|
|
29
|
+
* adapter then injects the narrowly trusted `\\explain` marker before
|
|
30
|
+
* build-time KaTeX rendering. `notation.uses` is retired: the use is the
|
|
31
|
+
* declaration (D1 decision, 2026-09-02).
|
|
32
|
+
*
|
|
33
|
+
* Definitions can be supplied as a Map, an array, an object keyed by the
|
|
34
|
+
* semantic key, or a loader returning one of those forms. Loaders are
|
|
35
|
+
* evaluated for every document transform so a long-running development
|
|
36
|
+
* server can discover definitions created after startup. A resolver can
|
|
37
|
+
* provide page-local lexical scoping:
|
|
38
|
+
*
|
|
39
|
+
* remarkNotation({
|
|
40
|
+
* definitions: sharedDefinitions,
|
|
41
|
+
* resolve(key, { file, kind, localDefinitions, sharedDefinitions }) {
|
|
42
|
+
* return localDefinitions.get(key) ?? sharedDefinitions.get(key);
|
|
43
|
+
* },
|
|
44
|
+
* })
|
|
45
|
+
*
|
|
46
|
+
* A definition may be either its data object or an Astro collection entry
|
|
47
|
+
* with the data object under `.data`.
|
|
48
|
+
*/
|
|
49
|
+
export const NOTATION_KEY_PATTERN: RegExp;
|
|
@@ -36,7 +36,9 @@ export const NOTATION_KEY_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
|
36
36
|
import { GlyphResolutionError, resolveMathGlyphs } from './math-glyphs.mjs';
|
|
37
37
|
import {
|
|
38
38
|
EQ_LABEL_PATTERN,
|
|
39
|
+
collectDisplayEquations,
|
|
39
40
|
collectEquationLabels,
|
|
41
|
+
equationAnchorId,
|
|
40
42
|
parseEquationRef,
|
|
41
43
|
stripEquationLabels,
|
|
42
44
|
} from './equations.mjs';
|
|
@@ -307,28 +309,31 @@ function defaultHref(definition, key, base = '') {
|
|
|
307
309
|
}
|
|
308
310
|
|
|
309
311
|
/**
|
|
310
|
-
* Wrap a
|
|
311
|
-
* (which splices the inner `math-display` element out).
|
|
312
|
-
* a
|
|
313
|
-
*
|
|
312
|
+
* Wrap a display `$$…$$` `math` node so the container survives `rehype-katex`
|
|
313
|
+
* (which splices the inner `math-display` element out). Every display equation
|
|
314
|
+
* in a lesson is numbered; a keyed one anchors at `#eq-<key>`, an unkeyed one
|
|
315
|
+
* at its positional `#eq-<section>-<index>`. The visible number is a focusable
|
|
316
|
+
* `<a>` sibling — never authored, never inside KaTeX — so it survives print and
|
|
317
|
+
* screen readers name it "Equation 2.4".
|
|
314
318
|
*/
|
|
315
|
-
function
|
|
319
|
+
function equationNode(mathNode, { key, number }) {
|
|
320
|
+
const id = equationAnchorId({ key, number });
|
|
321
|
+
const hProperties = {
|
|
322
|
+
id,
|
|
323
|
+
className: ['keyed-equation'],
|
|
324
|
+
'data-eq-number': number,
|
|
325
|
+
};
|
|
326
|
+
if (key) hProperties['data-eq-key'] = key;
|
|
327
|
+
else hProperties['data-eq-auto'] = '';
|
|
328
|
+
|
|
316
329
|
return {
|
|
317
330
|
type: 'paragraph',
|
|
318
|
-
data: {
|
|
319
|
-
hName: 'div',
|
|
320
|
-
hProperties: {
|
|
321
|
-
id: `eq-${key}`,
|
|
322
|
-
className: ['keyed-equation'],
|
|
323
|
-
'data-eq-key': key,
|
|
324
|
-
'data-eq-number': number,
|
|
325
|
-
},
|
|
326
|
-
},
|
|
331
|
+
data: { hName: 'div', hProperties },
|
|
327
332
|
children: [
|
|
328
333
|
mathNode,
|
|
329
334
|
{
|
|
330
335
|
type: 'link',
|
|
331
|
-
url:
|
|
336
|
+
url: `#${id}`,
|
|
332
337
|
children: [{ type: 'text', value: `(${number})` }],
|
|
333
338
|
data: {
|
|
334
339
|
hProperties: {
|
|
@@ -598,6 +603,14 @@ export default function remarkNotation(options = {}) {
|
|
|
598
603
|
crossPage: options.equations ?? {},
|
|
599
604
|
};
|
|
600
605
|
|
|
606
|
+
// Number and anchor every display equation on a lesson page, keyed or
|
|
607
|
+
// not (F3). Non-lesson pages (notation registry, stray docs) keep the
|
|
608
|
+
// keyed-only behaviour so their body math is left untouched.
|
|
609
|
+
const displayEquations = lessonId ? collectDisplayEquations(tree) : [];
|
|
610
|
+
const equationByNode = new Map(
|
|
611
|
+
displayEquations.map((equation) => [equation.node, equation]),
|
|
612
|
+
);
|
|
613
|
+
|
|
601
614
|
const resolve = (key, kind, node) => {
|
|
602
615
|
const context = {
|
|
603
616
|
file,
|
|
@@ -647,7 +660,6 @@ export default function remarkNotation(options = {}) {
|
|
|
647
660
|
node,
|
|
648
661
|
);
|
|
649
662
|
}
|
|
650
|
-
node.__eqKey = labelMatch[1];
|
|
651
663
|
value = stripEquationLabels(value);
|
|
652
664
|
replaceMathSource(node, value);
|
|
653
665
|
}
|
|
@@ -725,14 +737,8 @@ export default function remarkNotation(options = {}) {
|
|
|
725
737
|
continue;
|
|
726
738
|
}
|
|
727
739
|
visit(child, false);
|
|
728
|
-
if (child && child.
|
|
729
|
-
|
|
730
|
-
delete child.__eqKey;
|
|
731
|
-
node.children[index] = keyedEquationNode(
|
|
732
|
-
child,
|
|
733
|
-
key,
|
|
734
|
-
pageLabels.get(key),
|
|
735
|
-
);
|
|
740
|
+
if (child && child.type === 'math' && equationByNode.has(child)) {
|
|
741
|
+
node.children[index] = equationNode(child, equationByNode.get(child));
|
|
736
742
|
}
|
|
737
743
|
}
|
|
738
744
|
};
|
|
@@ -15,6 +15,19 @@ type NotationAlignment = {
|
|
|
15
15
|
readonly rationale: string;
|
|
16
16
|
};
|
|
17
17
|
type NotationReferenceKind = 'prose' | 'math' | 'definition';
|
|
18
|
+
/**
|
|
19
|
+
* A gloss resolved by the loader (D15). Declared on a card, keyed
|
|
20
|
+
* `<card key>.<slug(name)>`, and deliberately absent from the registry: a
|
|
21
|
+
* gloss is never a definition record, never a reference target, and never a
|
|
22
|
+
* curriculum node. See `reference/gloss.ts`.
|
|
23
|
+
*/
|
|
24
|
+
interface NotationGlossEntry {
|
|
25
|
+
readonly key: string;
|
|
26
|
+
readonly notation: string;
|
|
27
|
+
readonly label: string;
|
|
28
|
+
readonly units?: string;
|
|
29
|
+
readonly ownerKey: string;
|
|
30
|
+
}
|
|
18
31
|
interface NotationReferenceInput {
|
|
19
32
|
readonly key: string;
|
|
20
33
|
readonly kind: NotationReferenceKind;
|
|
@@ -37,6 +50,8 @@ interface SharedNotationDefinitionInput {
|
|
|
37
50
|
readonly body: string;
|
|
38
51
|
/** References harvested from the Markdown body. */
|
|
39
52
|
readonly references: readonly NotationReferenceInput[];
|
|
53
|
+
/** Symbols this entry's `formula` names but does not make cards of. */
|
|
54
|
+
readonly glosses: readonly NotationGlossEntry[];
|
|
40
55
|
readonly source: SourceSpan;
|
|
41
56
|
}
|
|
42
57
|
interface LocalNotationDefinitionInput {
|
|
@@ -51,6 +66,8 @@ interface LocalNotationDefinitionInput {
|
|
|
51
66
|
readonly alignment: NotationAlignment;
|
|
52
67
|
/** References harvested from summary content. */
|
|
53
68
|
readonly references: readonly NotationReferenceInput[];
|
|
69
|
+
/** Symbols this entry's `formula` names but does not make cards of. */
|
|
70
|
+
readonly glosses: readonly NotationGlossEntry[];
|
|
54
71
|
readonly source: SourceSpan;
|
|
55
72
|
}
|
|
56
73
|
interface NotationLessonInput {
|
|
@@ -67,7 +84,7 @@ interface NotationRegistryInput {
|
|
|
67
84
|
readonly sharedDefinitions: readonly SharedNotationDefinitionInput[];
|
|
68
85
|
readonly lessons: readonly NotationLessonInput[];
|
|
69
86
|
}
|
|
70
|
-
type NotationDiagnosticCode = 'invalid-key' | 'duplicate-page' | 'duplicate-definition' | 'conflicting-definition' | 'undefined-reference' | 'reference-cycle' | 'unused-definition' | 'alignment-unknown-competency' | 'alignment-unknown-lesson' | 'alignment-introduction' | 'alignment-unavailable' | 'alignment-review-state';
|
|
87
|
+
type NotationDiagnosticCode = 'invalid-key' | 'duplicate-page' | 'duplicate-definition' | 'conflicting-definition' | 'undefined-reference' | 'reference-cycle' | 'unused-definition' | 'alignment-unknown-competency' | 'alignment-unknown-lesson' | 'alignment-introduction' | 'alignment-unavailable' | 'alignment-review-state' | 'gloss-collides-with-card';
|
|
71
88
|
type DiagnosticSeverity = 'error' | 'warning';
|
|
72
89
|
interface NotationDiagnostic {
|
|
73
90
|
readonly code: NotationDiagnosticCode;
|
|
@@ -95,6 +112,12 @@ interface ResolvedNotationReference {
|
|
|
95
112
|
interface NotationDefinitionRecordBase {
|
|
96
113
|
readonly id: string;
|
|
97
114
|
readonly key: string;
|
|
115
|
+
/**
|
|
116
|
+
* Symbols this entry's `formula` names but does not make cards of (D15).
|
|
117
|
+
* Carried on the record so the manifest can build the entry-scoped formula
|
|
118
|
+
* table; deliberately **not** a definition, a reference target, or an edge.
|
|
119
|
+
*/
|
|
120
|
+
readonly glosses: readonly NotationGlossEntry[];
|
|
98
121
|
readonly scope: NotationDefinitionScope;
|
|
99
122
|
readonly notation: string;
|
|
100
123
|
readonly label: string;
|
|
@@ -148,4 +171,4 @@ interface NotationRegistry {
|
|
|
148
171
|
readonly diagnostics: readonly NotationDiagnostic[];
|
|
149
172
|
}
|
|
150
173
|
|
|
151
|
-
export type { DiagnosticSeverity, EditorialStatus, LocalNotationDefinitionInput, LocalNotationDefinitionRecord, NotationAlignment, NotationBacklink, NotationBinding, NotationDefinitionRecord, NotationDefinitionScope, NotationDiagnostic, NotationDiagnosticCode, NotationLessonInput, NotationPageBundle, NotationReferenceInput, NotationReferenceKind, NotationRegistry, NotationRegistryInput, ResolvedNotationReference, SharedNotationDefinitionInput, SharedNotationDefinitionRecord, SourceSpan };
|
|
174
|
+
export type { DiagnosticSeverity, EditorialStatus, LocalNotationDefinitionInput, LocalNotationDefinitionRecord, NotationAlignment, NotationBacklink, NotationBinding, NotationDefinitionRecord, NotationDefinitionScope, NotationDiagnostic, NotationDiagnosticCode, NotationGlossEntry, NotationLessonInput, NotationPageBundle, NotationReferenceInput, NotationReferenceKind, NotationRegistry, NotationRegistryInput, ResolvedNotationReference, SharedNotationDefinitionInput, SharedNotationDefinitionRecord, SourceSpan };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explico",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Build-time engine for prerequisite-aware, notation-rigorous course content: one validated content model, a semantic notation/citation/equation system with a completeness gate, and a deterministic JSON manifest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -48,6 +48,6 @@
|
|
|
48
48
|
"typescript": "6.0.2"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
|
-
"build": "tsup && node scripts/copy-assets.mjs"
|
|
51
|
+
"build": "tsup && node scripts/copy-assets.mjs && node scripts/fix-esm-extensions.mjs && tsc -p tsconfig.dts-mjs.json"
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -32,10 +32,12 @@ import type {
|
|
|
32
32
|
NotationRegistryInput,
|
|
33
33
|
SharedNotationDefinitionInput,
|
|
34
34
|
SourceSpan,
|
|
35
|
+
NotationGlossEntry,
|
|
35
36
|
} from '../reference/types';
|
|
36
37
|
import { isSubstantiveMeaning } from '../reference/prose';
|
|
37
38
|
import { parseEquationRef } from '../reference/equations.mjs';
|
|
38
39
|
import { resolveLabel } from '../reference/label';
|
|
40
|
+
import { resolveGlosses } from '../reference/gloss';
|
|
39
41
|
import {
|
|
40
42
|
deriveRequires,
|
|
41
43
|
deriveSources,
|
|
@@ -322,6 +324,42 @@ export async function loadCurriculumCatalog(): Promise<CurriculumCatalog> {
|
|
|
322
324
|
|
|
323
325
|
// --- notation registry input -------------------------------------------
|
|
324
326
|
|
|
327
|
+
/**
|
|
328
|
+
* A card's authored `glosses:` list (D15). Keys are derived from the names, so
|
|
329
|
+
* a gloss is entry-scoped and promoting one to a card needs no reference
|
|
330
|
+
* rewrite anywhere in the corpus — see `reference/gloss.ts`.
|
|
331
|
+
*/
|
|
332
|
+
function glossEntries(
|
|
333
|
+
raw: unknown,
|
|
334
|
+
ownerKey: string,
|
|
335
|
+
at: string,
|
|
336
|
+
): NotationGlossEntry[] {
|
|
337
|
+
if (raw === undefined) return [];
|
|
338
|
+
if (!Array.isArray(raw)) throw new Error(`${at} must be an array`);
|
|
339
|
+
|
|
340
|
+
const authored = raw.map((value, index) => {
|
|
341
|
+
const data = record(value, `${at}[${index}]`);
|
|
342
|
+
const units = optionalString(data.units, `${at}[${index}].units`);
|
|
343
|
+
return {
|
|
344
|
+
latex: string(data.latex, `${at}[${index}].latex`),
|
|
345
|
+
name: string(data.name, `${at}[${index}].name`),
|
|
346
|
+
...(units === undefined ? {} : { units }),
|
|
347
|
+
};
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const glosses = resolveGlosses(ownerKey, authored);
|
|
351
|
+
const seen = new Set<string>();
|
|
352
|
+
for (const gloss of glosses) {
|
|
353
|
+
if (seen.has(gloss.key)) {
|
|
354
|
+
throw new Error(
|
|
355
|
+
`${at} declares two glosses named ${JSON.stringify(gloss.label)}`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
seen.add(gloss.key);
|
|
359
|
+
}
|
|
360
|
+
return glosses;
|
|
361
|
+
}
|
|
362
|
+
|
|
325
363
|
function localDefinition(
|
|
326
364
|
raw: unknown,
|
|
327
365
|
file: string,
|
|
@@ -350,6 +388,7 @@ function localDefinition(
|
|
|
350
388
|
? { kind: 'general', rationale: 'Lesson-local symbol.' }
|
|
351
389
|
: alignment(data.alignment, `${at}.alignment`),
|
|
352
390
|
references: extractNotationReferences(definitionText, file, 'definition'),
|
|
391
|
+
glosses: glossEntries(data.glosses, key, `${at}.glosses`),
|
|
353
392
|
source: { file },
|
|
354
393
|
...(formula === undefined ? {} : { formula }),
|
|
355
394
|
...(units === undefined ? {} : { units }),
|
|
@@ -382,6 +421,7 @@ function sharedNotationEntries(): SharedNotationDefinitionInput[] {
|
|
|
382
421
|
aiAssisted: boolean(data.aiAssisted, `${file} aiAssisted`),
|
|
383
422
|
body,
|
|
384
423
|
references: extractNotationReferences(body, file, undefined),
|
|
424
|
+
glosses: glossEntries(data.glosses, key, `${file} glosses`),
|
|
385
425
|
source: { file },
|
|
386
426
|
...(formula === undefined ? {} : { formula }),
|
|
387
427
|
...(units === undefined ? {} : { units }),
|
package/src/compiler/manifest.ts
CHANGED
|
@@ -57,7 +57,10 @@ import {
|
|
|
57
57
|
} from '../reference/gate-math';
|
|
58
58
|
import { loadLintIgnore } from '../reference/lint-ignore';
|
|
59
59
|
import { buildNotationRegistry } from '../reference/registry';
|
|
60
|
+
import { formulaGlyphScope } from '../reference/gloss';
|
|
61
|
+
import type { GlyphScopeEntry } from '../reference/gloss';
|
|
60
62
|
import { resolveReferenceId } from '../reference/references';
|
|
63
|
+
import type { NotationGlossEntry } from '../reference/types';
|
|
61
64
|
import type {
|
|
62
65
|
EditorialStatus,
|
|
63
66
|
NotationBacklink,
|
|
@@ -137,6 +140,14 @@ export interface ManifestLessonNotation {
|
|
|
137
140
|
readonly seeAlso: readonly string[];
|
|
138
141
|
readonly units?: string;
|
|
139
142
|
readonly formula?: string;
|
|
143
|
+
/** Symbols the formula names that have no card of their own (D15). */
|
|
144
|
+
readonly glosses: readonly NotationGlossEntry[];
|
|
145
|
+
/**
|
|
146
|
+
* Entry-scoped glyph table for `formula` — the entry, its glosses, and any
|
|
147
|
+
* card the formula names. Precomputed here because the whole card set lives
|
|
148
|
+
* in the compiler, not in the page component.
|
|
149
|
+
*/
|
|
150
|
+
readonly formulaScope: readonly GlyphScopeEntry[];
|
|
140
151
|
}
|
|
141
152
|
|
|
142
153
|
export interface ManifestLesson {
|
|
@@ -306,6 +317,7 @@ function lessonNotationBundle(
|
|
|
306
317
|
bundle: NotationPageBundle | undefined,
|
|
307
318
|
definitionsById: ReadonlyMap<string, NotationDefinitionRecord>,
|
|
308
319
|
resolution: readonly ManifestNotationResolution[],
|
|
320
|
+
cardsByKey: ReadonlyMap<string, GlyphScopeEntry>,
|
|
309
321
|
): ManifestLesson['notation'] {
|
|
310
322
|
const definitions: ManifestLessonNotation[] = [];
|
|
311
323
|
for (const definitionId of bundle?.definitionIds ?? []) {
|
|
@@ -320,6 +332,16 @@ function lessonNotationBundle(
|
|
|
320
332
|
summary: definition.summary,
|
|
321
333
|
editorialStatus: definition.status,
|
|
322
334
|
seeAlso: [...definition.seeAlso],
|
|
335
|
+
glosses: [...definition.glosses],
|
|
336
|
+
formulaScope: formulaGlyphScope({
|
|
337
|
+
key: definition.key,
|
|
338
|
+
notation: definition.notation,
|
|
339
|
+
glosses: definition.glosses,
|
|
340
|
+
cardsByKey,
|
|
341
|
+
...(definition.formula === undefined
|
|
342
|
+
? {}
|
|
343
|
+
: { formula: definition.formula }),
|
|
344
|
+
}),
|
|
323
345
|
...(definition.units === undefined ? {} : { units: definition.units }),
|
|
324
346
|
...(definition.formula === undefined
|
|
325
347
|
? {}
|
|
@@ -379,6 +401,14 @@ export async function compileManifest(
|
|
|
379
401
|
]);
|
|
380
402
|
|
|
381
403
|
const registry = buildNotationRegistry(notationInput);
|
|
404
|
+
// Cards a `formula` may name with `\explain{key}` (D15). Shared entries
|
|
405
|
+
// only: a page-local symbol is not addressable from another entry.
|
|
406
|
+
const cardsByKey = new Map<string, GlyphScopeEntry>(
|
|
407
|
+
notationInput.sharedDefinitions.map((definition) => [
|
|
408
|
+
definition.key,
|
|
409
|
+
{ key: definition.key, notation: definition.notation },
|
|
410
|
+
]),
|
|
411
|
+
);
|
|
382
412
|
const definitionsById = new Map(
|
|
383
413
|
registry.definitions.map((definition) => [definition.id, definition]),
|
|
384
414
|
);
|
|
@@ -476,6 +506,7 @@ export async function compileManifest(
|
|
|
476
506
|
definitionsById,
|
|
477
507
|
availableDefinitionIds,
|
|
478
508
|
),
|
|
509
|
+
cardsByKey,
|
|
479
510
|
),
|
|
480
511
|
});
|
|
481
512
|
}
|
|
@@ -11,6 +11,14 @@
|
|
|
11
11
|
* Extra attributes (e.g. `data-notation-panel`, `data-citation-panel`) are
|
|
12
12
|
* forwarded to the `<aside>` so each layer keeps the hook its tests and pin
|
|
13
13
|
* controls already reference.
|
|
14
|
+
*
|
|
15
|
+
* `tabindex="-1"`: never in the tab order, but a valid target for
|
|
16
|
+
* `panel.focus()`. A layer that lets the reader click deeper into the panel's
|
|
17
|
+
* own content (D15's in-panel navigation) needs somewhere stable to move
|
|
18
|
+
* focus to before it replaces whatever the reader just clicked — otherwise
|
|
19
|
+
* removing the focused node drops focus to nothing, and the shared
|
|
20
|
+
* `focusout` handler in `./hover-panel.ts` reads that as "the reader left"
|
|
21
|
+
* and closes the panel it was just asked to update.
|
|
14
22
|
*/
|
|
15
23
|
export interface Props {
|
|
16
24
|
/** Element id — pin controls point `aria-controls` at it. */
|
|
@@ -31,8 +39,17 @@ const { id, label, class: className, ...rest } = Astro.props;
|
|
|
31
39
|
aria-live="polite"
|
|
32
40
|
aria-atomic="true"
|
|
33
41
|
hidden
|
|
42
|
+
tabindex="-1"
|
|
34
43
|
{...rest}
|
|
35
44
|
>
|
|
45
|
+
<button
|
|
46
|
+
class="hover-panel-close"
|
|
47
|
+
type="button"
|
|
48
|
+
data-hover-panel-close
|
|
49
|
+
aria-label="Close"
|
|
50
|
+
>
|
|
51
|
+
<span aria-hidden="true">×</span>
|
|
52
|
+
</button>
|
|
36
53
|
<slot />
|
|
37
54
|
</aside>
|
|
38
55
|
|
|
@@ -48,6 +65,7 @@ const { id, label, class: className, ...rest } = Astro.props;
|
|
|
48
65
|
width: min(23rem, calc(100vw - 1.5rem));
|
|
49
66
|
overflow: auto;
|
|
50
67
|
padding: 1rem;
|
|
68
|
+
padding-inline-end: 2.4rem;
|
|
51
69
|
border: 1px solid var(--sl-color-gray-5);
|
|
52
70
|
border-top: 0.25rem solid var(--sl-color-accent);
|
|
53
71
|
border-radius: 0.65rem;
|
|
@@ -57,6 +75,30 @@ const { id, label, class: className, ...rest } = Astro.props;
|
|
|
57
75
|
font-size: var(--sl-text-sm);
|
|
58
76
|
}
|
|
59
77
|
|
|
78
|
+
.hover-panel-close {
|
|
79
|
+
position: absolute;
|
|
80
|
+
top: 0.5rem;
|
|
81
|
+
right: 0.5rem;
|
|
82
|
+
width: 1.6rem;
|
|
83
|
+
height: 1.6rem;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
padding: 0;
|
|
88
|
+
border: 0;
|
|
89
|
+
border-radius: 0.35rem;
|
|
90
|
+
color: var(--sl-color-gray-3);
|
|
91
|
+
background: transparent;
|
|
92
|
+
font-size: 1.15rem;
|
|
93
|
+
line-height: 1;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.hover-panel-close:hover {
|
|
98
|
+
color: var(--sl-color-text);
|
|
99
|
+
background: var(--sl-color-gray-6);
|
|
100
|
+
}
|
|
101
|
+
|
|
60
102
|
/* Slotted fields carry the owning layer's scope, not this component's, so
|
|
61
103
|
the descendant match must be global. */
|
|
62
104
|
.hover-panel :global(:focus-visible) {
|