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
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Equation identity (Step D7) — shared, framework-free helpers.
|
|
2
|
+
* Equation identity (Step D7, extended F3) — shared, framework-free helpers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* equation with the reserved `eq-` prefix (a bare `:` in `[[…]]` prose would
|
|
11
|
-
* be eaten by `remark-directive`):
|
|
4
|
+
* Every display equation (`$$…$$`) in a lesson is numbered and given an
|
|
5
|
+
* `id`, from appearance order scoped to the enclosing `##` section (`(2.4)` =
|
|
6
|
+
* fourth display equation under the second section). An author additionally
|
|
7
|
+
* marks an equation with a trailing `\label{eq:<key>}` inside its `$$…$$`
|
|
8
|
+
* block (inside math, so the `:` is safe and the form matches amsmath) when
|
|
9
|
+
* they want a *stable* anchor that survives edits and cross-page references:
|
|
12
10
|
*
|
|
13
11
|
* [[eq-discount-factor-def]] same page → "(2.4)" linked to #eq-…
|
|
14
12
|
* [[foundations/present-value#eq-pv]] cross page → number from the manifest
|
|
15
13
|
*
|
|
14
|
+
* An unkeyed equation still gets a positional anchor (`#eq-2-4`) so it can be
|
|
15
|
+
* deep-linked, but only a keyed equation is referenceable by name.
|
|
16
|
+
*
|
|
16
17
|
* `scanEquationLabels` (raw Markdown) and `collectEquationLabels` (mdast tree)
|
|
17
18
|
* MUST agree — `tests/notation/equations.test.ts` checks that on the real
|
|
18
19
|
* corpus. Both are here so `manifest.ts` and `remark-notation.mjs` share one
|
|
19
|
-
* numbering algorithm.
|
|
20
|
+
* numbering algorithm. `collectDisplayEquations` is the superset the render
|
|
21
|
+
* path uses to number and anchor every display equation, keyed or not.
|
|
20
22
|
*/
|
|
21
23
|
|
|
22
24
|
/** `eq:` key grammar — the notation-key shape without the dotted namespaces. */
|
|
@@ -51,11 +53,16 @@ export function stripEquationLabels(latex) {
|
|
|
51
53
|
.trim();
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
/** The visible number for the `index`-th
|
|
56
|
+
/** The visible number for the `index`-th display equation under `section`. */
|
|
55
57
|
function equationNumber(section, index) {
|
|
56
58
|
return section > 0 ? `${section}.${index}` : String(index);
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
/** The positional anchor id for an unkeyed equation, e.g. `2.4` → `eq-2-4`. */
|
|
62
|
+
export function equationAnchorId({ key, number }) {
|
|
63
|
+
return key ? `eq-${key}` : `eq-${String(number).replace(/\./g, '-')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
59
66
|
function stripFencedCode(markdown) {
|
|
60
67
|
return String(markdown)
|
|
61
68
|
.replace(/```[\s\S]*?```/g, (block) => block.replace(/[^\n]/g, ' '))
|
|
@@ -63,13 +70,29 @@ function stripFencedCode(markdown) {
|
|
|
63
70
|
.replace(/(^|[^`])`[^`\n]*`/g, (match) => match.replace(/[^\n]/g, ' '));
|
|
64
71
|
}
|
|
65
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Blank the body of every block-level MDX component (`<Capitalised …> … </…>`)
|
|
75
|
+
* so a `$$…$$` used to illustrate a worked example inside `<CompactExample>`
|
|
76
|
+
* (or an `<Aside>`, etc.) is not counted as a lesson-body equation. The
|
|
77
|
+
* outermost match wins, so nested components are blanked in one pass;
|
|
78
|
+
* newlines are kept so line/section structure is unchanged.
|
|
79
|
+
*/
|
|
80
|
+
function stripJsxComponents(markdown) {
|
|
81
|
+
return String(markdown).replace(
|
|
82
|
+
/<([A-Z][A-Za-z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g,
|
|
83
|
+
(block) => block.replace(/[^\n]/g, ' '),
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
/**
|
|
67
88
|
* Every keyed display equation in a Markdown body, in appearance order, with
|
|
68
89
|
* its assigned number. `$$…$$` blocks only; `##` headings between them advance
|
|
69
|
-
* the section counter.
|
|
90
|
+
* the section counter. Every display block advances the in-section index, so a
|
|
91
|
+
* keyed equation's number reflects its position among *all* display equations,
|
|
92
|
+
* not only the keyed ones.
|
|
70
93
|
*/
|
|
71
94
|
export function scanEquationLabels(markdown) {
|
|
72
|
-
const text = stripFencedCode(markdown);
|
|
95
|
+
const text = stripJsxComponents(stripFencedCode(markdown));
|
|
73
96
|
const tokens = [];
|
|
74
97
|
const heading = /^[ \t]{0,3}##[ \t]+\S/gm;
|
|
75
98
|
const display = /\$\$([\s\S]+?)\$\$/g;
|
|
@@ -79,9 +102,11 @@ export function scanEquationLabels(markdown) {
|
|
|
79
102
|
for (const match of text.matchAll(display)) {
|
|
80
103
|
EQ_LABEL_PATTERN.lastIndex = 0;
|
|
81
104
|
const label = EQ_LABEL_PATTERN.exec(match[1]);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
tokens.push({
|
|
106
|
+
kind: 'equation',
|
|
107
|
+
index: match.index,
|
|
108
|
+
key: label ? label[1] : null,
|
|
109
|
+
});
|
|
85
110
|
}
|
|
86
111
|
tokens.sort((a, b) => a.index - b.index);
|
|
87
112
|
|
|
@@ -95,6 +120,7 @@ export function scanEquationLabels(markdown) {
|
|
|
95
120
|
continue;
|
|
96
121
|
}
|
|
97
122
|
indexInSection += 1;
|
|
123
|
+
if (token.key == null) continue;
|
|
98
124
|
labels.push({
|
|
99
125
|
key: token.key,
|
|
100
126
|
number: equationNumber(section, indexInSection),
|
|
@@ -106,39 +132,49 @@ export function scanEquationLabels(markdown) {
|
|
|
106
132
|
}
|
|
107
133
|
|
|
108
134
|
/**
|
|
109
|
-
* The mdast equivalent of `scanEquationLabels
|
|
110
|
-
* order, advancing the section counter on every
|
|
111
|
-
*
|
|
135
|
+
* The mdast equivalent of `scanEquationLabels` for *every* display equation:
|
|
136
|
+
* walk the tree in document order, advancing the section counter on every
|
|
137
|
+
* depth-2 heading and emitting a record for every `math` node. `key` is the
|
|
138
|
+
* `\label{eq:…}` payload when present, else `null`.
|
|
112
139
|
*/
|
|
113
|
-
export function
|
|
114
|
-
const
|
|
140
|
+
export function collectDisplayEquations(tree) {
|
|
141
|
+
const equations = [];
|
|
115
142
|
let section = 0;
|
|
116
143
|
let indexInSection = 0;
|
|
117
144
|
|
|
118
|
-
|
|
119
|
-
|
|
145
|
+
// Only top-level blocks: a `$$…$$` nested in an MDX component (a
|
|
146
|
+
// `<CompactExample>` worked example, an `<Aside>`) is illustrative, not part
|
|
147
|
+
// of the lesson's equation sequence, and its container may be collapsed.
|
|
148
|
+
const children = Array.isArray(tree?.children) ? tree.children : [];
|
|
149
|
+
for (const node of children) {
|
|
150
|
+
if (!node || typeof node !== 'object') continue;
|
|
120
151
|
if (node.type === 'heading' && node.depth === 2) {
|
|
121
152
|
section += 1;
|
|
122
153
|
indexInSection = 0;
|
|
123
|
-
|
|
154
|
+
continue;
|
|
124
155
|
}
|
|
125
156
|
if (node.type === 'math') {
|
|
157
|
+
indexInSection += 1;
|
|
126
158
|
EQ_LABEL_PATTERN.lastIndex = 0;
|
|
127
159
|
const label = EQ_LABEL_PATTERN.exec(String(node.value ?? ''));
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
node,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
return;
|
|
160
|
+
equations.push({
|
|
161
|
+
key: label ? label[1] : null,
|
|
162
|
+
number: equationNumber(section, indexInSection),
|
|
163
|
+
section,
|
|
164
|
+
indexInSection,
|
|
165
|
+
node,
|
|
166
|
+
});
|
|
139
167
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
168
|
+
}
|
|
169
|
+
return equations;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The keyed subset of `collectDisplayEquations` — the tree-walk counterpart of
|
|
174
|
+
* `scanEquationLabels`. Numbers reflect position among all display equations.
|
|
175
|
+
*/
|
|
176
|
+
export function collectEquationLabels(tree) {
|
|
177
|
+
return collectDisplayEquations(tree).filter(
|
|
178
|
+
(equation) => equation.key != null,
|
|
179
|
+
);
|
|
144
180
|
}
|
|
@@ -4,10 +4,11 @@ interface MathGateDiagnostic {
|
|
|
4
4
|
readonly context: 'body' | 'formula' | 'assessment';
|
|
5
5
|
/**
|
|
6
6
|
* Body / component-slot math blocks the build (the render path needs every
|
|
7
|
-
* glyph resolved for the KaTeX trust callback). `formula`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* glyph resolved for the KaTeX trust callback). `formula` joined it at D15:
|
|
8
|
+
* a formula's glyphs are now rendered live and explorable, so an unresolved
|
|
9
|
+
* one is a hole the reader can see, and the corpus was brought to zero when
|
|
10
|
+
* the tier was promoted. `assessment` math stays a Tier-3 warning until its
|
|
11
|
+
* own reviewed content pass (D6/D13/D14).
|
|
11
12
|
*/
|
|
12
13
|
readonly severity: 'error' | 'warning';
|
|
13
14
|
readonly file: string;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { resolveMathGlyphs, GlyphResolutionError } from "./math-glyphs.mjs";
|
|
2
2
|
import { stripEquationLabels } from "./equations.mjs";
|
|
3
|
+
import { formulaGlyphScope, unknownFormulaScopeKeys } from "./gloss.js";
|
|
3
4
|
const SEVERITY = {
|
|
4
5
|
body: "error",
|
|
5
|
-
formula: "
|
|
6
|
+
formula: "error",
|
|
6
7
|
assessment: "warning"
|
|
7
8
|
};
|
|
8
9
|
const TERM_KEY = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
|
|
@@ -75,6 +76,33 @@ function resolveInto(latex, scope, context, file, line, fix, out) {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
const BODY_FIX = "Introduce the symbol with [[key]], define it in notation.local, or wrap it in \\explain{key}{latex}.";
|
|
79
|
+
function dedupeScope(scope) {
|
|
80
|
+
const seen = /* @__PURE__ */ new Set();
|
|
81
|
+
const out = [];
|
|
82
|
+
for (const entry of scope) {
|
|
83
|
+
if (seen.has(entry.key)) continue;
|
|
84
|
+
seen.add(entry.key);
|
|
85
|
+
out.push(entry);
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
function reportUnknownScopeKeys(definition, cardKeys, file, out) {
|
|
90
|
+
const glossKeys = new Set(definition.glosses.map((gloss) => gloss.key));
|
|
91
|
+
for (const key of unknownFormulaScopeKeys(
|
|
92
|
+
definition.formula,
|
|
93
|
+
glossKeys,
|
|
94
|
+
cardKeys
|
|
95
|
+
)) {
|
|
96
|
+
out.push({
|
|
97
|
+
context: "formula",
|
|
98
|
+
severity: SEVERITY.formula,
|
|
99
|
+
file,
|
|
100
|
+
token: key,
|
|
101
|
+
message: `${definition.key} formula names unknown notation key ${JSON.stringify(key)}. Name an existing card, or declare the symbol under glosses: on this entry.`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const SHARED_FORMULA_FIX = "Every symbol in a notation entry formula must resolve to the entry itself, one of its glosses:, a card the formula names with \\explain{key}{latex}, or the base library.";
|
|
78
106
|
const FORMULA_FIX = "Every right-hand-side variable of a notation.local formula must resolve to the entry key, another page symbol, or the base library.";
|
|
79
107
|
const ASSESSMENT_FIX = "Introduce the symbol in the owning lesson's prose with [[key]], or wrap it in \\explain{key}{latex}.";
|
|
80
108
|
function gateContentMath(input) {
|
|
@@ -85,6 +113,18 @@ function gateContentMath(input) {
|
|
|
85
113
|
definition
|
|
86
114
|
])
|
|
87
115
|
);
|
|
116
|
+
const sharedCards = new Map(
|
|
117
|
+
notationInput.sharedDefinitions.map((definition) => [
|
|
118
|
+
definition.key,
|
|
119
|
+
{ key: definition.key, notation: definition.notation }
|
|
120
|
+
])
|
|
121
|
+
);
|
|
122
|
+
const cardKeys = /* @__PURE__ */ new Set([
|
|
123
|
+
...sharedCards.keys(),
|
|
124
|
+
...notationInput.lessons.flatMap(
|
|
125
|
+
(lesson) => lesson.localDefinitions.map((definition) => definition.key)
|
|
126
|
+
)
|
|
127
|
+
]);
|
|
88
128
|
const lessonById = new Map(
|
|
89
129
|
notationInput.lessons.map((lesson) => [lesson.lessonId, lesson])
|
|
90
130
|
);
|
|
@@ -110,15 +150,46 @@ function gateContentMath(input) {
|
|
|
110
150
|
if (!definition.formula) continue;
|
|
111
151
|
resolveInto(
|
|
112
152
|
definition.formula,
|
|
113
|
-
|
|
153
|
+
dedupeScope([
|
|
154
|
+
...scope,
|
|
155
|
+
...definition.glosses.map((gloss) => ({
|
|
156
|
+
key: gloss.key,
|
|
157
|
+
notation: gloss.notation
|
|
158
|
+
}))
|
|
159
|
+
]),
|
|
114
160
|
"formula",
|
|
115
161
|
file,
|
|
116
162
|
void 0,
|
|
117
163
|
FORMULA_FIX,
|
|
118
164
|
diagnostics
|
|
119
165
|
);
|
|
166
|
+
reportUnknownScopeKeys(definition, cardKeys, file, diagnostics);
|
|
120
167
|
}
|
|
121
168
|
}
|
|
169
|
+
for (const definition of notationInput.sharedDefinitions) {
|
|
170
|
+
if (!definition.formula) continue;
|
|
171
|
+
resolveInto(
|
|
172
|
+
definition.formula,
|
|
173
|
+
formulaGlyphScope({
|
|
174
|
+
key: definition.key,
|
|
175
|
+
notation: definition.notation,
|
|
176
|
+
formula: definition.formula,
|
|
177
|
+
glosses: definition.glosses,
|
|
178
|
+
cardsByKey: sharedCards
|
|
179
|
+
}),
|
|
180
|
+
"formula",
|
|
181
|
+
definition.source.file,
|
|
182
|
+
void 0,
|
|
183
|
+
SHARED_FORMULA_FIX,
|
|
184
|
+
diagnostics
|
|
185
|
+
);
|
|
186
|
+
reportUnknownScopeKeys(
|
|
187
|
+
definition,
|
|
188
|
+
cardKeys,
|
|
189
|
+
definition.source.file,
|
|
190
|
+
diagnostics
|
|
191
|
+
);
|
|
192
|
+
}
|
|
122
193
|
for (const link of lessonAssessments) {
|
|
123
194
|
const lesson = lessonById.get(link.lessonId);
|
|
124
195
|
if (!lesson) continue;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glosses and formula scope (D15) — the second tier of the notation
|
|
3
|
+
* vocabulary.
|
|
4
|
+
*
|
|
5
|
+
* A **card** is a `content/notation/*.md` entry (or a `notation.local` entry):
|
|
6
|
+
* symbol, name, description, units, sources, curriculum alignment, review
|
|
7
|
+
* state. It gets a glossary card and is a node in the curriculum graph.
|
|
8
|
+
*
|
|
9
|
+
* A **gloss** is declared on a card, in that card's `glosses:` frontmatter,
|
|
10
|
+
* and carries a symbol, a name, and optionally a bare units string. It exists
|
|
11
|
+
* for one reason: so a card's rigorous `formula` can name its own letters
|
|
12
|
+
* without each letter having to become a card. A gloss has no description, no
|
|
13
|
+
* sources, no alignment, no review state, and no glossary card — and, the
|
|
14
|
+
* point of the whole tier, **it creates no curriculum edge**. A card that
|
|
15
|
+
* dragged its formula's letters into the registry would drag them into the
|
|
16
|
+
* lesson bundle of every lesson that uses it, and a foundations lesson would
|
|
17
|
+
* fail `alignment-introduction` on a symbol introduced six lessons later.
|
|
18
|
+
*
|
|
19
|
+
* The editorial convention (not enforced here): a card's formula may introduce
|
|
20
|
+
* glosses; glosses terminate — they have no formula of their own. A symbol
|
|
21
|
+
* that later needs a real definition is **promoted**: delete the gloss line,
|
|
22
|
+
* add `content/notation/<key>.md`. Because a formula spells LaTeX and never a
|
|
23
|
+
* key, nothing else in the corpus changes.
|
|
24
|
+
*/
|
|
25
|
+
interface NotationGlossInput {
|
|
26
|
+
readonly latex: string;
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly units?: string;
|
|
29
|
+
}
|
|
30
|
+
/** A gloss with its derived key and display label. */
|
|
31
|
+
interface ResolvedGloss {
|
|
32
|
+
/** `<card key>.<slug(name)>` — derived, never authored. */
|
|
33
|
+
readonly key: string;
|
|
34
|
+
readonly notation: string;
|
|
35
|
+
readonly label: string;
|
|
36
|
+
readonly units?: string;
|
|
37
|
+
/** The card that declares it. */
|
|
38
|
+
readonly ownerKey: string;
|
|
39
|
+
}
|
|
40
|
+
/** One `{key, notation}` row of the flat table `resolveMathGlyphs` consumes. */
|
|
41
|
+
interface GlyphScopeEntry {
|
|
42
|
+
readonly key: string;
|
|
43
|
+
readonly notation: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The key segment derived from a gloss name: lower-cased, runs of anything
|
|
47
|
+
* that is not a letter or digit collapsed to a single `-`. Returns `undefined`
|
|
48
|
+
* when the name has no usable characters, so the schema can reject it rather
|
|
49
|
+
* than the loader minting an invalid key.
|
|
50
|
+
*/
|
|
51
|
+
declare function glossKeySegment(name: string): string | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* Derive a gloss's key. Dotted onto the owning card's key, so a gloss is
|
|
54
|
+
* entry-scoped by construction: two cards may each gloss `T`, and a gloss can
|
|
55
|
+
* never be confused with a card in the flat registry.
|
|
56
|
+
*/
|
|
57
|
+
declare function glossKey(ownerKey: string, name: string): string;
|
|
58
|
+
/** Resolve a card's authored `glosses:` list into keyed, labelled entries. */
|
|
59
|
+
declare function resolveGlosses(ownerKey: string, glosses: readonly NotationGlossInput[]): ResolvedGloss[];
|
|
60
|
+
declare function formulaExplainKeys(formula: string): string[];
|
|
61
|
+
/**
|
|
62
|
+
* The glyph table a card's `formula` resolves against:
|
|
63
|
+
*
|
|
64
|
+
* {the card itself} ∪ {its glosses} ∪ {cards its formula names} ∪ base library
|
|
65
|
+
*
|
|
66
|
+
* Narrower than the registry-wide table a card's *body* math still uses, and
|
|
67
|
+
* narrower than a lesson page's scope. A card named with `\explain{key}` in
|
|
68
|
+
* the formula joins the table for glyph resolution **only**: its existence is
|
|
69
|
+
* checked, but it deliberately becomes no reference edge, because a
|
|
70
|
+
* definitional letter is not a curriculum prerequisite. See the module note.
|
|
71
|
+
*/
|
|
72
|
+
declare function formulaGlyphScope(input: {
|
|
73
|
+
readonly key: string;
|
|
74
|
+
readonly notation: string;
|
|
75
|
+
readonly formula?: string;
|
|
76
|
+
readonly glosses: readonly ResolvedGloss[];
|
|
77
|
+
/** Every card by key — shared entries, plus page-local ones for a lesson. */
|
|
78
|
+
readonly cardsByKey: ReadonlyMap<string, GlyphScopeEntry>;
|
|
79
|
+
}): GlyphScopeEntry[];
|
|
80
|
+
/**
|
|
81
|
+
* Keys named by a formula that no card defines. The build reports these — an
|
|
82
|
+
* `\explain{…}` pointing at nothing is an authoring mistake, not a silent
|
|
83
|
+
* fallback to an unresolved glyph.
|
|
84
|
+
*/
|
|
85
|
+
declare function unknownFormulaScopeKeys(formula: string | undefined, glossKeys: ReadonlySet<string>, cardKeys: ReadonlySet<string>): string[];
|
|
86
|
+
|
|
87
|
+
export { type GlyphScopeEntry, type NotationGlossInput, type ResolvedGloss, formulaExplainKeys, formulaGlyphScope, glossKey, glossKeySegment, resolveGlosses, unknownFormulaScopeKeys };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
function glossKeySegment(name) {
|
|
2
|
+
const segment = name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3
|
+
return segment.length > 0 ? segment : void 0;
|
|
4
|
+
}
|
|
5
|
+
function glossKey(ownerKey, name) {
|
|
6
|
+
const segment = glossKeySegment(name);
|
|
7
|
+
if (segment === void 0) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
`Gloss name ${JSON.stringify(name)} on ${ownerKey} yields no key segment.`
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
return `${ownerKey}.${segment}`;
|
|
13
|
+
}
|
|
14
|
+
function resolveGlosses(ownerKey, glosses) {
|
|
15
|
+
return glosses.map((gloss) => ({
|
|
16
|
+
key: glossKey(ownerKey, gloss.name),
|
|
17
|
+
notation: gloss.latex,
|
|
18
|
+
label: gloss.name,
|
|
19
|
+
ownerKey,
|
|
20
|
+
...gloss.units === void 0 ? {} : { units: gloss.units }
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
const EXPLAIN_KEY = /\\explain\s*\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}/g;
|
|
24
|
+
function formulaExplainKeys(formula) {
|
|
25
|
+
EXPLAIN_KEY.lastIndex = 0;
|
|
26
|
+
return [...formula.matchAll(EXPLAIN_KEY)].map((match) => match[1] ?? "");
|
|
27
|
+
}
|
|
28
|
+
function formulaGlyphScope(input) {
|
|
29
|
+
const scope = [
|
|
30
|
+
{ key: input.key, notation: input.notation }
|
|
31
|
+
];
|
|
32
|
+
const seen = /* @__PURE__ */ new Set([input.key]);
|
|
33
|
+
for (const gloss of input.glosses) {
|
|
34
|
+
if (seen.has(gloss.key)) continue;
|
|
35
|
+
seen.add(gloss.key);
|
|
36
|
+
scope.push({ key: gloss.key, notation: gloss.notation });
|
|
37
|
+
}
|
|
38
|
+
for (const key of formulaExplainKeys(input.formula ?? "")) {
|
|
39
|
+
if (seen.has(key)) continue;
|
|
40
|
+
const card = input.cardsByKey.get(key);
|
|
41
|
+
if (!card) continue;
|
|
42
|
+
seen.add(key);
|
|
43
|
+
scope.push({ key: card.key, notation: card.notation });
|
|
44
|
+
}
|
|
45
|
+
return scope;
|
|
46
|
+
}
|
|
47
|
+
function unknownFormulaScopeKeys(formula, glossKeys, cardKeys) {
|
|
48
|
+
return formulaExplainKeys(formula ?? "").filter(
|
|
49
|
+
(key) => !cardKeys.has(key) && !glossKeys.has(key)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
formulaExplainKeys,
|
|
54
|
+
formulaGlyphScope,
|
|
55
|
+
glossKey,
|
|
56
|
+
glossKeySegment,
|
|
57
|
+
resolveGlosses,
|
|
58
|
+
unknownFormulaScopeKeys
|
|
59
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KaTeX trust callback that permits exactly one inert marker attribute:
|
|
3
|
+
* `data-notation-key` (a registry key) or `data-lab-slot` (a lab value hole).
|
|
4
|
+
* Links, styles, classes, IDs, protocols, and every other data attribute stay
|
|
5
|
+
* untrusted. Key existence is checked by the remark adapter, the lab-math
|
|
6
|
+
* renderer, and the workspace validator before KaTeX runs.
|
|
7
|
+
*/
|
|
8
|
+
export function trustNotationMarker(context: any): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Add non-security KaTeX settings while keeping the notation macros and trust
|
|
11
|
+
* boundary non-overridable.
|
|
12
|
+
*/
|
|
13
|
+
export function createNotationKatexOptions(overrides?: {}): {
|
|
14
|
+
output: "htmlAndMathml";
|
|
15
|
+
throwOnError: true;
|
|
16
|
+
trust: typeof trustNotationMarker;
|
|
17
|
+
macros: any;
|
|
18
|
+
strict: typeof strictNotation;
|
|
19
|
+
};
|
|
20
|
+
export const EXPLAIN_MACRO: string;
|
|
21
|
+
export const SLOT_MACRO: string;
|
|
22
|
+
export const notationKatexOptions: Readonly<{
|
|
23
|
+
output: "htmlAndMathml";
|
|
24
|
+
throwOnError: true;
|
|
25
|
+
strict: typeof strictNotation;
|
|
26
|
+
trust: typeof trustNotationMarker;
|
|
27
|
+
macros: Readonly<{
|
|
28
|
+
'\\explain': string;
|
|
29
|
+
'\\slot': string;
|
|
30
|
+
}>;
|
|
31
|
+
}>;
|
|
32
|
+
declare function strictNotation(errorCode: any): "ignore" | "warn";
|
|
33
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadManifest } from "../compiler/manifest";
|
|
1
|
+
import { loadManifest } from "../compiler/manifest.js";
|
|
2
2
|
const TERM_KEY = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
|
|
3
3
|
const EXPLAIN_KEY = /\\explain\s*\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}/g;
|
|
4
4
|
async function labMathScope(notation, body = "") {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve and annotate one KaTeX expression against a page's flat glyph table.
|
|
3
|
+
* Source offsets in the result always refer to the original, unmodified LaTeX.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} latex
|
|
6
|
+
* @param {Array<{key: string, notation: string}>} definitions
|
|
7
|
+
*/
|
|
8
|
+
export function resolveMathGlyphs(latex: string, definitions: Array<{
|
|
9
|
+
key: string;
|
|
10
|
+
notation: string;
|
|
11
|
+
}>): {
|
|
12
|
+
latex: string;
|
|
13
|
+
bindings: {
|
|
14
|
+
key: any;
|
|
15
|
+
level: any;
|
|
16
|
+
match: any;
|
|
17
|
+
token: any;
|
|
18
|
+
}[];
|
|
19
|
+
unresolved: any;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Page-level glyph-map resolver (cleanup plan, D1 decision 2026-09-02).
|
|
23
|
+
*
|
|
24
|
+
* A page declares its glyphs once — `notation.local` plus the shared entries it
|
|
25
|
+
* introduces in prose with `[[key]]` — so the glyph table handed in here is a
|
|
26
|
+
* flat list with **one meaning per glyph**. This module parses one `$…$` /
|
|
27
|
+
* `$$…$$` expression, matches each declared symbol against it (whole canonical
|
|
28
|
+
* form first, then a unique base glyph for a shorter or instantiated form),
|
|
29
|
+
* marks every match with a validated `\explain` marker, and reports any
|
|
30
|
+
* remaining identifier atom so the caller can fail at `file:line:token`.
|
|
31
|
+
* `\explain{key}{latex}` in the source stays as the escape hatch for a
|
|
32
|
+
* compound or glyph-colliding symbol.
|
|
33
|
+
*
|
|
34
|
+
* Because the table is unambiguous, matching is a lookup, not a search: no
|
|
35
|
+
* cross-definition candidate solving, no ambiguity permutation. What remains is
|
|
36
|
+
* the tokenizer the page-glyph-map syntax requires — a KaTeX parse plus the
|
|
37
|
+
* structural signatures that let `CF_k` be found as a three-node run and `D` be
|
|
38
|
+
* found as the head of `D(0,2)`. `katex.__parse` is that tokenizer; the version
|
|
39
|
+
* guard below keeps a dependency bump from silently changing its tree.
|
|
40
|
+
*/
|
|
41
|
+
export const SUPPORTED_KATEX_PARSE_VERSION: "0.16.47";
|
|
42
|
+
export class GlyphResolutionError extends Error {
|
|
43
|
+
constructor(code: any, message: any, details?: undefined);
|
|
44
|
+
code: any;
|
|
45
|
+
details: any;
|
|
46
|
+
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
localDefinitionId,
|
|
6
6
|
resolveReferenceId,
|
|
7
7
|
sharedDefinitionId
|
|
8
|
-
} from "./references";
|
|
8
|
+
} from "./references.js";
|
|
9
9
|
function buildNotationRegistry(rawInput) {
|
|
10
10
|
const diagnostics = [];
|
|
11
11
|
const lessons = buildLessonIndex(rawInput.lessons, diagnostics);
|
|
@@ -15,6 +15,7 @@ function buildNotationRegistry(rawInput) {
|
|
|
15
15
|
diagnostics
|
|
16
16
|
);
|
|
17
17
|
const availableDefinitionIds = new Set(definitions.keys());
|
|
18
|
+
validateGlossKeys(definitions, diagnostics);
|
|
18
19
|
for (const candidate of definitions.values()) {
|
|
19
20
|
const references = sortedReferences(candidate.input.references);
|
|
20
21
|
const resolved = [];
|
|
@@ -298,6 +299,26 @@ function normalizeLocalDefinition(input) {
|
|
|
298
299
|
references: sortedReferences(input.references)
|
|
299
300
|
};
|
|
300
301
|
}
|
|
302
|
+
function validateGlossKeys(definitions, diagnostics) {
|
|
303
|
+
const cardKeys = new Set(
|
|
304
|
+
[...definitions.values()].map((candidate) => candidate.input.key)
|
|
305
|
+
);
|
|
306
|
+
for (const candidate of definitions.values()) {
|
|
307
|
+
for (const gloss of candidate.input.glosses) {
|
|
308
|
+
if (!cardKeys.has(gloss.key)) continue;
|
|
309
|
+
diagnostics.push(
|
|
310
|
+
diagnostic({
|
|
311
|
+
code: "gloss-collides-with-card",
|
|
312
|
+
message: `${candidate.id} glosses ${JSON.stringify(gloss.label)} as ${gloss.key}, which is also a notation card; delete the gloss or rename it`,
|
|
313
|
+
key: gloss.key,
|
|
314
|
+
lessonId: candidate.kind === "local" ? candidate.lesson.lessonId : void 0,
|
|
315
|
+
definitionId: candidate.id,
|
|
316
|
+
source: candidate.input.source
|
|
317
|
+
})
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
301
322
|
function validateReferenceKey(reference, owner, diagnostics) {
|
|
302
323
|
if (isNotationKey(reference.key)) return true;
|
|
303
324
|
diagnostics.push(
|
|
@@ -371,6 +392,7 @@ function toDefinitionRecord(candidate) {
|
|
|
371
392
|
status: input2.status,
|
|
372
393
|
aiAssisted: input2.aiAssisted,
|
|
373
394
|
body: input2.body,
|
|
395
|
+
glosses: input2.glosses,
|
|
374
396
|
source: input2.source,
|
|
375
397
|
resolvedReferences: candidate.resolvedReferences,
|
|
376
398
|
...input2.formula === void 0 ? {} : { formula: input2.formula },
|
|
@@ -392,6 +414,7 @@ function toDefinitionRecord(candidate) {
|
|
|
392
414
|
seeAlso: input.seeAlso,
|
|
393
415
|
alignment: input.alignment,
|
|
394
416
|
status: candidate.lesson.status,
|
|
417
|
+
glosses: input.glosses,
|
|
395
418
|
source: input.source,
|
|
396
419
|
resolvedReferences: candidate.resolvedReferences,
|
|
397
420
|
...input.formula === void 0 ? {} : { formula: input.formula },
|
|
@@ -432,6 +455,7 @@ function definitionFingerprint(candidate) {
|
|
|
432
455
|
status: input2.status,
|
|
433
456
|
aiAssisted: input2.aiAssisted,
|
|
434
457
|
body: normalizeText(input2.body),
|
|
458
|
+
glosses: input2.glosses,
|
|
435
459
|
references
|
|
436
460
|
});
|
|
437
461
|
}
|
|
@@ -447,6 +471,7 @@ function definitionFingerprint(candidate) {
|
|
|
447
471
|
sources: [...input.sources].sort(),
|
|
448
472
|
seeAlso: [...input.seeAlso].sort(),
|
|
449
473
|
alignment,
|
|
474
|
+
glosses: input.glosses,
|
|
450
475
|
references
|
|
451
476
|
});
|
|
452
477
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rehype-katex reports parse failures as non-fatal VFile messages and renders
|
|
3
|
+
* a `.katex-error` fallback. Turn those recovered failures into compiler
|
|
4
|
+
* errors so Astro cannot successfully build a lesson containing broken math.
|
|
5
|
+
*/
|
|
6
|
+
export default function rehypeFailKatexErrors(): (tree: any, file: any) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default function rehypeHeadingAnchors(): (tree: any, file: any) => void;
|