explico 0.6.0 → 0.7.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 +180 -1
- package/README.md +16 -2
- package/dist/compiler/astro.d.ts +13 -2
- package/dist/compiler/astro.js +39 -0
- package/dist/compiler/cli.d.ts +22 -3
- package/dist/compiler/cli.js +71 -10
- package/dist/compiler/exports/standalone-html.js +13 -0
- package/dist/compiler/logging.d.ts +109 -0
- package/dist/compiler/logging.js +107 -0
- package/dist/compiler/manifest.d.ts +7 -3
- package/dist/compiler/manifest.js +1 -1
- package/dist/reference/base-abbreviations.mjs +8 -2
- package/dist/reference/consistency.d.ts +8 -10
- package/dist/reference/consistency.js +3 -47
- package/dist/reference/delimiter-size.d.ts +39 -0
- package/dist/reference/delimiter-size.js +177 -0
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +2 -2
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-math.d.ts +2 -0
- package/dist/reference/gate-math.js +28 -6
- package/dist/reference/keyed-float-validate.d.ts +2 -2
- package/dist/reference/keyed-float-validate.js +1 -1
- package/dist/reference/references.js +2 -1
- package/dist/reference/registry.js +1 -1
- package/dist/reference/rehype-base-links.d.mts +23 -0
- package/dist/reference/rehype-base-links.mjs +40 -0
- package/dist/reference/remark-abbreviations.mjs +10 -12
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/render-lab-math.d.ts +18 -1
- package/dist/reference/render-lab-math.js +10 -3
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +1 -1
- package/docs/VERSIONING.md +2 -2
- package/docs/authoring/README.md +12 -2
- package/docs/authoring/agents.md +7 -3
- package/docs/authoring/migrating.md +286 -0
- package/docs/authoring/notation.md +139 -7
- package/docs/authoring/prose.md +64 -0
- package/package.json +1 -1
- package/src/compiler/astro.ts +89 -0
- package/src/compiler/cli.ts +144 -12
- package/src/compiler/exports/standalone-html.ts +15 -0
- package/src/compiler/logging.ts +248 -0
- package/src/compiler/manifest.ts +7 -3
- package/src/components/memo/Memo.astro +2 -5
- package/src/components/memo/ShortNote.astro +2 -5
- package/src/components/notation/NotationFormula.astro +199 -10
- package/src/components/notation/NotationGlossary.astro +89 -58
- package/src/components/notation/NotationLayer.astro +83 -81
- package/src/components/scroll-animation.ts +160 -0
- package/src/components/starlight/AnchorScrollEnhancer.astro +102 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +2 -4
- package/src/components/starlight/KeyedFloatEnhancer.astro +17 -16
- package/src/components/starlight/LessonFooter.astro +2 -0
- package/src/reference/base-abbreviations.mjs +8 -2
- package/src/reference/consistency.ts +10 -66
- package/src/reference/delimiter-size.ts +301 -0
- package/src/reference/diagrams-validate.ts +1 -1
- package/src/reference/equations-validate.ts +3 -3
- package/src/reference/figures-validate.ts +1 -1
- package/src/reference/gate-math.ts +41 -6
- package/src/reference/keyed-float-validate.ts +3 -3
- package/src/reference/references.ts +2 -1
- package/src/reference/registry.ts +1 -1
- package/src/reference/rehype-base-links.mjs +40 -0
- package/src/reference/remark-abbreviations.mjs +10 -12
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/render-lab-math.ts +24 -1
- package/src/reference/tables-validate.ts +1 -1
- package/src/reference/types.ts +1 -1
- package/src/styles/global.css +119 -14
|
@@ -5,7 +5,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
|
|
|
5
5
|
type FigureDiagnosticCode = 'fig-invalid-key' | 'fig-duplicate-key' | 'fig-ref-resolves' | 'fig-key-unused';
|
|
6
6
|
interface FigureDiagnostic {
|
|
7
7
|
readonly code: FigureDiagnosticCode;
|
|
8
|
-
readonly severity: 'error' | 'warning';
|
|
8
|
+
readonly severity: 'error' | 'warning' | 'info';
|
|
9
9
|
readonly message: string;
|
|
10
10
|
readonly file?: string;
|
|
11
11
|
readonly lessonId?: string;
|
|
@@ -26,8 +26,10 @@ interface MathGateDiagnostic {
|
|
|
26
26
|
* than adding a second array the CLI has to know about.
|
|
27
27
|
*/
|
|
28
28
|
readonly severity: 'error' | 'warning';
|
|
29
|
+
readonly code?: string;
|
|
29
30
|
readonly file: string;
|
|
30
31
|
readonly line?: number;
|
|
32
|
+
readonly column?: number;
|
|
31
33
|
readonly token?: string;
|
|
32
34
|
readonly message: string;
|
|
33
35
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveMathGlyphs, GlyphResolutionError } from "./math-glyphs.mjs";
|
|
2
2
|
import { stripEquationLabels } from "./equations.mjs";
|
|
3
|
+
import { lintDelimiterSizes } from "./delimiter-size.js";
|
|
3
4
|
import { formulaGlyphScope, unknownFormulaScopeKeys } from "./gloss.js";
|
|
4
5
|
const SEVERITY = {
|
|
5
6
|
body: "error",
|
|
@@ -26,10 +27,14 @@ function mathSpans(body) {
|
|
|
26
27
|
pattern.lastIndex = 0;
|
|
27
28
|
let match;
|
|
28
29
|
while (match = pattern.exec(searchable)) {
|
|
30
|
+
const latex = match[1] ?? "";
|
|
31
|
+
const latexOffset = match.index + match[0].indexOf(latex);
|
|
32
|
+
const before = body.slice(0, latexOffset);
|
|
33
|
+
const lastNewline = before.lastIndexOf("\n");
|
|
29
34
|
spans.push({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
latex,
|
|
36
|
+
line: lineOf(body, latexOffset),
|
|
37
|
+
column: latexOffset - lastNewline
|
|
33
38
|
});
|
|
34
39
|
}
|
|
35
40
|
}
|
|
@@ -53,10 +58,23 @@ function pageScope(lesson, sharedByKey) {
|
|
|
53
58
|
}
|
|
54
59
|
return scope;
|
|
55
60
|
}
|
|
56
|
-
function resolveInto(latex, scope, context, file, line, fix, out) {
|
|
61
|
+
function resolveInto(latex, scope, context, file, line, column, fix, out) {
|
|
62
|
+
for (const diagnostic of lintDelimiterSizes(latex)) {
|
|
63
|
+
const locatedLine = line === void 0 ? void 0 : line + diagnostic.line - 1;
|
|
64
|
+
const locatedColumn = column === void 0 ? void 0 : diagnostic.line === 1 ? column + diagnostic.column - 1 : diagnostic.column;
|
|
65
|
+
out.push({
|
|
66
|
+
context,
|
|
67
|
+
code: diagnostic.code,
|
|
68
|
+
severity: "warning",
|
|
69
|
+
file,
|
|
70
|
+
...locatedLine === void 0 ? {} : { line: locatedLine },
|
|
71
|
+
...locatedColumn === void 0 ? {} : { column: locatedColumn },
|
|
72
|
+
message: diagnostic.message
|
|
73
|
+
});
|
|
74
|
+
}
|
|
57
75
|
let result;
|
|
58
76
|
try {
|
|
59
|
-
result = resolveMathGlyphs(latex, [...scope]);
|
|
77
|
+
result = resolveMathGlyphs(stripEquationLabels(latex), [...scope]);
|
|
60
78
|
} catch (error) {
|
|
61
79
|
out.push({
|
|
62
80
|
context,
|
|
@@ -145,6 +163,7 @@ function gateContentMath(input) {
|
|
|
145
163
|
"body",
|
|
146
164
|
file,
|
|
147
165
|
span.line,
|
|
166
|
+
span.column,
|
|
148
167
|
BODY_FIX,
|
|
149
168
|
diagnostics
|
|
150
169
|
);
|
|
@@ -163,6 +182,7 @@ function gateContentMath(input) {
|
|
|
163
182
|
"formula",
|
|
164
183
|
file,
|
|
165
184
|
void 0,
|
|
185
|
+
void 0,
|
|
166
186
|
FORMULA_FIX,
|
|
167
187
|
diagnostics
|
|
168
188
|
);
|
|
@@ -183,6 +203,7 @@ function gateContentMath(input) {
|
|
|
183
203
|
"formula",
|
|
184
204
|
definition.source.file,
|
|
185
205
|
void 0,
|
|
206
|
+
void 0,
|
|
186
207
|
SHARED_FORMULA_FIX,
|
|
187
208
|
diagnostics
|
|
188
209
|
);
|
|
@@ -209,7 +230,8 @@ function gateContentMath(input) {
|
|
|
209
230
|
scope,
|
|
210
231
|
"assessment",
|
|
211
232
|
`${assessmentId} (${item.id ?? "?"})`,
|
|
212
|
-
|
|
233
|
+
span.line,
|
|
234
|
+
span.column,
|
|
213
235
|
ASSESSMENT_FIX,
|
|
214
236
|
diagnostics
|
|
215
237
|
);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - `<prefix>-duplicate-key` (error): an `id` is unique per lesson.
|
|
8
8
|
* - `<prefix>-ref-resolves` (error): every `[[<prefix>-key]]` /
|
|
9
9
|
* `[[slug#<prefix>-key]]` points at the matching keyed float with that `id`.
|
|
10
|
-
* - `<prefix>-key-unused` (
|
|
10
|
+
* - `<prefix>-key-unused` (info): a keyed float is never referenced.
|
|
11
11
|
*
|
|
12
12
|
* `tables-validate.ts`, `figures-validate.ts`, and `diagrams-validate.ts` are
|
|
13
13
|
* thin wrappers over this so callers import a kind-specific validator with
|
|
@@ -38,7 +38,7 @@ interface KeyedFloatRefRecord {
|
|
|
38
38
|
}
|
|
39
39
|
interface KeyedFloatDiagnostic {
|
|
40
40
|
readonly code: string;
|
|
41
|
-
readonly severity: 'error' | 'warning';
|
|
41
|
+
readonly severity: 'error' | 'warning' | 'info';
|
|
42
42
|
readonly message: string;
|
|
43
43
|
readonly file?: string;
|
|
44
44
|
readonly lessonId?: string;
|
|
@@ -93,7 +93,7 @@ function validateKeyedFloats(lessons, kind) {
|
|
|
93
93
|
if (referenced.has(`${label.slug}#${label.key}`)) continue;
|
|
94
94
|
diagnostics.push({
|
|
95
95
|
code: `${prefix}-key-unused`,
|
|
96
|
-
severity: "
|
|
96
|
+
severity: "info",
|
|
97
97
|
message: `${label.lessonId} has a ${noun} id="${label.key}" (${label.number}) but nothing references it with [[${prefix}-${label.key}]]`,
|
|
98
98
|
file: label.slug,
|
|
99
99
|
lessonId: label.lessonId,
|
|
@@ -28,7 +28,8 @@ function compareDiagnostics(left, right) {
|
|
|
28
28
|
return severityRank(left.severity) - severityRank(right.severity) || left.code.localeCompare(right.code) || (left.lessonId ?? "").localeCompare(right.lessonId ?? "") || (left.key ?? "").localeCompare(right.key ?? "") || (left.definitionId ?? "").localeCompare(right.definitionId ?? "") || compareSourceSpans(left.source, right.source) || left.message.localeCompare(right.message);
|
|
29
29
|
}
|
|
30
30
|
function severityRank(severity) {
|
|
31
|
-
|
|
31
|
+
if (severity === "error") return 0;
|
|
32
|
+
return severity === "warning" ? 1 : 2;
|
|
32
33
|
}
|
|
33
34
|
export {
|
|
34
35
|
NOTATION_KEY_PATTERN,
|
|
@@ -144,7 +144,7 @@ function buildNotationRegistry(rawInput) {
|
|
|
144
144
|
if (!reachableDefinitions.has(candidate.id)) {
|
|
145
145
|
diagnostics.push({
|
|
146
146
|
code: "unused-definition",
|
|
147
|
-
severity: "
|
|
147
|
+
severity: "info",
|
|
148
148
|
message: `${candidate.id} is not reachable from any lesson reference`,
|
|
149
149
|
key: candidate.input.key,
|
|
150
150
|
lessonId: candidate.kind === "local" ? candidate.lesson.lessonId : void 0,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefixes root-relative `<a href="/...">` links in rendered lesson content
|
|
3
|
+
* with the course's Astro `base` path.
|
|
4
|
+
*
|
|
5
|
+
* A course deployed under a subpath (any GitHub Pages *project* site, e.g.
|
|
6
|
+
* `base: '/my-course'`) gets that prefix applied automatically to Starlight's
|
|
7
|
+
* own generated navigation — sidebar, pagination, breadcrumbs — because those
|
|
8
|
+
* are rendered by Starlight's own `.astro` components, which call its
|
|
9
|
+
* internal `pathWithBase()` helper. They are **not** applied to anything
|
|
10
|
+
* produced by the Markdown/MDX → rehype pipeline: a lesson's own prose links
|
|
11
|
+
* (`[text](/other-lesson/)`), `remark-notation.mjs`'s equation cross-
|
|
12
|
+
* references, and `remark-citation.mjs`'s citation links are all root-
|
|
13
|
+
* relative absolute paths written or generated without the base in mind, so
|
|
14
|
+
* without this plugin every one of them 404s on a subpath deployment. A
|
|
15
|
+
* lesson's Starlight frontmatter (e.g. `hero.actions[].link`) is untouched
|
|
16
|
+
* either way — that YAML never enters this pipeline — so write those as
|
|
17
|
+
* paths relative to the current page instead.
|
|
18
|
+
*
|
|
19
|
+
* A no-op when the course has no base (`base` omitted or `'/'`, e.g. local
|
|
20
|
+
* dev or a GitHub *user/org* Pages site), so a course can wire this in
|
|
21
|
+
* unconditionally and it only does anything on the deployments that need it.
|
|
22
|
+
*/
|
|
23
|
+
export default function rehypeBaseLinks(base: any): (tree: any) => void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prefixes root-relative `<a href="/...">` links in rendered lesson content
|
|
3
|
+
* with the course's Astro `base` path.
|
|
4
|
+
*
|
|
5
|
+
* A course deployed under a subpath (any GitHub Pages *project* site, e.g.
|
|
6
|
+
* `base: '/my-course'`) gets that prefix applied automatically to Starlight's
|
|
7
|
+
* own generated navigation — sidebar, pagination, breadcrumbs — because those
|
|
8
|
+
* are rendered by Starlight's own `.astro` components, which call its
|
|
9
|
+
* internal `pathWithBase()` helper. They are **not** applied to anything
|
|
10
|
+
* produced by the Markdown/MDX → rehype pipeline: a lesson's own prose links
|
|
11
|
+
* (`[text](/other-lesson/)`), `remark-notation.mjs`'s equation cross-
|
|
12
|
+
* references, and `remark-citation.mjs`'s citation links are all root-
|
|
13
|
+
* relative absolute paths written or generated without the base in mind, so
|
|
14
|
+
* without this plugin every one of them 404s on a subpath deployment. A
|
|
15
|
+
* lesson's Starlight frontmatter (e.g. `hero.actions[].link`) is untouched
|
|
16
|
+
* either way — that YAML never enters this pipeline — so write those as
|
|
17
|
+
* paths relative to the current page instead.
|
|
18
|
+
*
|
|
19
|
+
* A no-op when the course has no base (`base` omitted or `'/'`, e.g. local
|
|
20
|
+
* dev or a GitHub *user/org* Pages site), so a course can wire this in
|
|
21
|
+
* unconditionally and it only does anything on the deployments that need it.
|
|
22
|
+
*/
|
|
23
|
+
export default function rehypeBaseLinks(base) {
|
|
24
|
+
const prefix = base && base !== '/' ? base.replace(/\/+$/, '') : '';
|
|
25
|
+
|
|
26
|
+
return function transform(tree) {
|
|
27
|
+
if (!prefix) return;
|
|
28
|
+
visit(tree);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function visit(node) {
|
|
32
|
+
if (node.type === 'element' && node.tagName === 'a') {
|
|
33
|
+
const href = node.properties?.href;
|
|
34
|
+
if (typeof href === 'string' && href.startsWith('/') && !href.startsWith('//')) {
|
|
35
|
+
node.properties.href = prefix + href;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(node.children)) node.children.forEach(visit);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* requires every abbreviation-shaped word in prose to either be universal or
|
|
6
6
|
* carry a `content/abbreviations/*.md` entry, so the literal written form
|
|
7
7
|
* (`abbr`, or one of its `aliases`) is itself the trigger. This plugin scans
|
|
8
|
-
* ordinary prose for that literal text and wraps
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* ordinary prose for that literal text and wraps **every** occurrence in a
|
|
9
|
+
* real link (`/glossary/#abbr-<key>`, so it still navigates with no JS)
|
|
10
|
+
* carrying the `data-abbr-key` an island script reads to render a hover/click
|
|
11
|
+
* card, exactly the way `remarkCitation` wraps `[@id]` markers. A reader who
|
|
12
|
+
* deep-links into a mid-page heading — the sidebar TOC, a heading anchor, an
|
|
13
|
+
* external link — sees the same hoverable cue no matter which occurrence they
|
|
14
|
+
* land on first, rather than only the earliest one in document order.
|
|
15
|
+
* `AbbreviationLayer.astro` already resolves every marker independently by
|
|
16
|
+
* `data-abbr-key`, so wrapping every occurrence needs no island-side change.
|
|
14
17
|
*
|
|
15
18
|
* Matches only ever come from the known table by construction, so unlike
|
|
16
19
|
* citation's `[@id]` there is no "unknown id" failure path here.
|
|
@@ -118,10 +121,6 @@ export default function remarkAbbreviations(options = {}) {
|
|
|
118
121
|
const { regex, keyByMatch } = buildMatcher(suppliedAbbreviations);
|
|
119
122
|
if (!regex) return;
|
|
120
123
|
|
|
121
|
-
// Fresh per file, same lifetime as citation's own `pairs` Map: only the
|
|
122
|
-
// first occurrence of a given key on this page gets wrapped.
|
|
123
|
-
const usedKeys = new Set();
|
|
124
|
-
|
|
125
124
|
const replaceText = (value) => {
|
|
126
125
|
regex.lastIndex = 0;
|
|
127
126
|
const output = [];
|
|
@@ -131,13 +130,12 @@ export default function remarkAbbreviations(options = {}) {
|
|
|
131
130
|
|
|
132
131
|
while ((match = regex.exec(value))) {
|
|
133
132
|
const key = keyByMatch.get(match[0]);
|
|
134
|
-
if (!key
|
|
133
|
+
if (!key) continue;
|
|
135
134
|
|
|
136
135
|
if (match.index > cursor) {
|
|
137
136
|
output.push({ type: 'text', value: value.slice(cursor, match.index) });
|
|
138
137
|
}
|
|
139
138
|
output.push(termNode(key, match[0], base));
|
|
140
|
-
usedKeys.add(key);
|
|
141
139
|
changed = true;
|
|
142
140
|
cursor = match.index + match[0].length;
|
|
143
141
|
}
|
|
Binary file
|
|
@@ -9,7 +9,24 @@ interface RenderedLabMath {
|
|
|
9
9
|
readonly slots: readonly string[];
|
|
10
10
|
/** Notation keys the template resolved to, for diagnostics. */
|
|
11
11
|
readonly keys: readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* Plain, copyable LaTeX — every explained glyph resolved to its bare
|
|
14
|
+
* symbol, every `\slot{name}{…}` replaced by `slotPlaceholder(name)` — so
|
|
15
|
+
* the island can substitute each slot's *current* live value in and keep
|
|
16
|
+
* `[data-notation-formula]`'s `data-latex` accurate as the reader drags a
|
|
17
|
+
* control, the same "click the equation to copy it" gesture a static
|
|
18
|
+
* lesson or glossary equation gets (`NotationLayer.astro`).
|
|
19
|
+
*/
|
|
20
|
+
readonly plainLatexTemplate: string;
|
|
12
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* A slot's placeholder inside `plainLatexTemplate` — distinct from the
|
|
24
|
+
* numeric sentinel `sentinelFor` below uses internally (that one only has to
|
|
25
|
+
* survive a single notation-binding pass at build time; this one is the
|
|
26
|
+
* stable, name-keyed token an island substitutes with a live value at
|
|
27
|
+
* runtime, so it's exported for that purpose).
|
|
28
|
+
*/
|
|
29
|
+
declare function slotPlaceholder(name: string): string;
|
|
13
30
|
/**
|
|
14
31
|
* Compile a developer-authored lab math template the same way lesson `$$`
|
|
15
32
|
* math is compiled: every identifier must resolve to a notation key in the
|
|
@@ -18,4 +35,4 @@ interface RenderedLabMath {
|
|
|
18
35
|
*/
|
|
19
36
|
declare function renderLabMath(template: string, scope: readonly LabMathScopeDefinition[]): RenderedLabMath;
|
|
20
37
|
|
|
21
|
-
export { type LabMathScopeDefinition, type RenderedLabMath, renderLabMath };
|
|
38
|
+
export { type LabMathScopeDefinition, type RenderedLabMath, renderLabMath, slotPlaceholder };
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import katex from "katex";
|
|
2
|
-
import { resolveMathGlyphs } from "./math-glyphs.mjs";
|
|
2
|
+
import { resolveMathGlyphs, stripNotationMarkers } from "./math-glyphs.mjs";
|
|
3
3
|
import { createNotationKatexOptions } from "./katex-options.mjs";
|
|
4
|
+
function slotPlaceholder(name) {
|
|
5
|
+
return `\u27E6${name}\u27E7`;
|
|
6
|
+
}
|
|
4
7
|
const SLOT_PATTERN = /\\slot\{([a-z][a-z0-9]*)\}\{((?:[^{}]|\{[^{}]*\})*)\}/g;
|
|
5
8
|
const sentinelFor = (index) => `909909${index}.909909`;
|
|
6
9
|
function renderLabMath(template, scope) {
|
|
@@ -30,6 +33,7 @@ function renderLabMath(template, scope) {
|
|
|
30
33
|
);
|
|
31
34
|
}
|
|
32
35
|
let bound = result.latex;
|
|
36
|
+
let copyBound = result.latex;
|
|
33
37
|
slots.forEach(({ name, fallback }, index) => {
|
|
34
38
|
const sentinel = sentinelFor(index);
|
|
35
39
|
if (!bound.includes(sentinel)) {
|
|
@@ -42,6 +46,7 @@ function renderLabMath(template, scope) {
|
|
|
42
46
|
sentinel,
|
|
43
47
|
`\\htmlData{lab-slot=${name}}{${fallback}}`
|
|
44
48
|
);
|
|
49
|
+
copyBound = copyBound.replace(sentinel, slotPlaceholder(name));
|
|
45
50
|
});
|
|
46
51
|
const html = katex.renderToString(
|
|
47
52
|
bound,
|
|
@@ -50,9 +55,11 @@ function renderLabMath(template, scope) {
|
|
|
50
55
|
return {
|
|
51
56
|
html,
|
|
52
57
|
slots: slots.map(({ name }) => name),
|
|
53
|
-
keys: [...new Set(result.bindings.map(({ key }) => key))]
|
|
58
|
+
keys: [...new Set(result.bindings.map(({ key }) => key))],
|
|
59
|
+
plainLatexTemplate: stripNotationMarkers(copyBound)
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
export {
|
|
57
|
-
renderLabMath
|
|
63
|
+
renderLabMath,
|
|
64
|
+
slotPlaceholder
|
|
58
65
|
};
|
|
@@ -5,7 +5,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
|
|
|
5
5
|
type TableDiagnosticCode = 'tab-invalid-key' | 'tab-duplicate-key' | 'tab-ref-resolves' | 'tab-key-unused';
|
|
6
6
|
interface TableDiagnostic {
|
|
7
7
|
readonly code: TableDiagnosticCode;
|
|
8
|
-
readonly severity: 'error' | 'warning';
|
|
8
|
+
readonly severity: 'error' | 'warning' | 'info';
|
|
9
9
|
readonly message: string;
|
|
10
10
|
readonly file?: string;
|
|
11
11
|
readonly lessonId?: string;
|
|
@@ -100,7 +100,7 @@ interface AbbreviationDefinitionInput {
|
|
|
100
100
|
readonly source: SourceSpan;
|
|
101
101
|
}
|
|
102
102
|
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';
|
|
103
|
-
type DiagnosticSeverity = 'error' | 'warning';
|
|
103
|
+
type DiagnosticSeverity = 'error' | 'warning' | 'info';
|
|
104
104
|
interface NotationDiagnostic {
|
|
105
105
|
readonly code: NotationDiagnosticCode;
|
|
106
106
|
readonly severity: DiagnosticSeverity;
|
package/docs/VERSIONING.md
CHANGED
|
@@ -38,8 +38,8 @@ passed now fails.
|
|
|
38
38
|
`CHANGELOG.md`).
|
|
39
39
|
|
|
40
40
|
**PATCH** — a bug fix, a docs change, an internal refactor with no surface
|
|
41
|
-
change, or a new **Tier-3 warning** (
|
|
42
|
-
one does not break a consumer).
|
|
41
|
+
change, or a new **Tier-3 warning/info diagnostic** (neither fails a build, so
|
|
42
|
+
surfacing one does not break a consumer).
|
|
43
43
|
|
|
44
44
|
### Pre-1.0
|
|
45
45
|
|
package/docs/authoring/README.md
CHANGED
|
@@ -22,6 +22,12 @@ Two kinds of rule live here:
|
|
|
22
22
|
- **Review-only** — nothing checks it automatically; a reviewer does. Breaking
|
|
23
23
|
one is a review comment, not a broken build.
|
|
24
24
|
|
|
25
|
+
Automated diagnostics use three severities. An **error** means the content is
|
|
26
|
+
incorrect, ambiguous, unexplainable, or cannot render reliably and therefore
|
|
27
|
+
blocks validation. A **warning** is non-blocking but marks a rule, readability,
|
|
28
|
+
or maintenance problem that should be repaired. **Info** is optional advice;
|
|
29
|
+
leaving it in place can be intentional and creates no repair debt.
|
|
30
|
+
|
|
25
31
|
Every rule is written to be falsifiable: you can look at a lesson and say
|
|
26
32
|
whether it complies.
|
|
27
33
|
|
|
@@ -43,7 +49,7 @@ speak in: what a part, a chapter, a section, and a subsection are, and which of
|
|
|
43
49
|
them an equation number counts.
|
|
44
50
|
|
|
45
51
|
Each rule has a stable ID — a letter for its area (`N` notation,
|
|
46
|
-
`V` visualizations, `C` versioning) plus a number — and this shape:
|
|
52
|
+
`V` visualizations, `C` versioning, `S` structure, `P` prose) plus a number — and this shape:
|
|
47
53
|
|
|
48
54
|
> **N1 — One-line imperative.**
|
|
49
55
|
> _Rationale._ Why the rule exists; the failure it prevents.
|
|
@@ -75,6 +81,7 @@ file table.
|
|
|
75
81
|
| [visualizations.md](visualizations.md) | `V` | diagrams, charts, interactive figures, randomness |
|
|
76
82
|
| [versioning.md](versioning.md) | `C` | the knowledge version and `content/CHANGELOG.md` |
|
|
77
83
|
| [structure-rules.md](structure-rules.md) | `S` | heading and figure/diagram/memo/short-note title length |
|
|
84
|
+
| [prose.md](prose.md) | `P` | how a lesson's sentences are worded |
|
|
78
85
|
| [components.md](components.md) | — | _catalog_: inline/block features available in a lesson body — tooltips, tables, figures, diagrams, memos, abbreviations, citations |
|
|
79
86
|
| [agents.md](agents.md) | — | _guide_: working on a course repo (with an AI agent) |
|
|
80
87
|
| [migrating.md](migrating.md) | — | _guide_: per-version steps when bumping the `explico` dep |
|
|
@@ -97,7 +104,9 @@ file table.
|
|
|
97
104
|
| [N12](notation.md#n12) | Math syntax never carries the sentence's own trailing punctuation | Review-only |
|
|
98
105
|
| [N13](notation.md#n13) | A nonlinear function always takes a parenthesized argument | Build-enforced (`resolveMathGlyphs`) |
|
|
99
106
|
| [N14](notation.md#n14) | An abbreviation-shaped word in prose is universal, or explained | Build-enforced (`gate-abbreviations.ts`) |
|
|
100
|
-
| [N15](notation.md#n15) | A nested `(`, `[`, `{`, or `\|` must use
|
|
107
|
+
| [N15](notation.md#n15) | A nested `(`, `[`, `{`, or `\|` must use scalable sizing | Build-enforced (`resolveMathGlyphs`) |
|
|
108
|
+
| [N16](notation.md#n16) | AI-generated stylistic grouping alternates delimiter types | Review-only |
|
|
109
|
+
| [N17](notation.md#n17) | Nested scalable delimiters grow strictly from inside to outside | Tier-3 warning (`delimiter-size-hierarchy`) |
|
|
101
110
|
| [V1](visualizations.md#v1) | Graphs and flowcharts use the `Diagram` component | Review-only |
|
|
102
111
|
| [V2](visualizations.md#v2) | Static charts: SVG → D3 → Observable Plot, chosen per figure | Review-only |
|
|
103
112
|
| [V3](visualizations.md#v3) | Interactive figures: island + `renderLabMath` seam + text alt | Review-only |
|
|
@@ -108,3 +117,4 @@ file table.
|
|
|
108
117
|
| [C3](versioning.md#c3) | `about.version` equals the newest `content/CHANGELOG.md` heading | Build-enforced (Tier-3 `knowledge-version-mismatch`) |
|
|
109
118
|
| [C4](versioning.md#c4) | On release, move `[Unreleased]` and bump `about.version` together | Review-only |
|
|
110
119
|
| [S1](structure-rules.md#s1) | No heading, or figure/diagram/memo/short-note title, over 65 characters | Build-enforced (`gate-heading-length.ts`) for body/floats + review for `frontmatter.title` |
|
|
120
|
+
| [P1](prose.md#p1) | "or" never introduces a second name for the same thing | Review-only |
|
package/docs/authoring/agents.md
CHANGED
|
@@ -7,7 +7,9 @@ particular to your layout.
|
|
|
7
7
|
|
|
8
8
|
This is a guide, not a rule area. The enforceable rules are in
|
|
9
9
|
[`notation.md`](notation.md) (`N`), [`visualizations.md`](visualizations.md)
|
|
10
|
-
(`V`),
|
|
10
|
+
(`V`), [`versioning.md`](versioning.md) (`C`),
|
|
11
|
+
[`structure-rules.md`](structure-rules.md) (`S`), and [`prose.md`](prose.md)
|
|
12
|
+
(`P`). When the engine's behaviour
|
|
11
13
|
and a rule disagree, the engine wins — see [`README.md`](README.md).
|
|
12
14
|
|
|
13
15
|
## The loop
|
|
@@ -15,8 +17,10 @@ and a rule disagree, the engine wins — see [`README.md`](README.md).
|
|
|
15
17
|
- **Start from the CLI, not a file hunt.** `pnpm --filter <course> content
|
|
16
18
|
context <lesson>` prints a lesson plus exactly its notation, references, and
|
|
17
19
|
assumptions. `pnpm content status` is the draft-debt overview. `pnpm content
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
diagnostics info` lists everything; `warning` and `error` raise the minimum
|
|
21
|
+
severity. `pnpm content warnings` lists warnings only.
|
|
22
|
+
`pnpm content check <lesson> | --changed` is schema + refs + MDX→KaTeX, exits
|
|
23
|
+
non-zero on an error. Read those before opening files.
|
|
20
24
|
- **Validate, twice.** `pnpm --filter <course> validate:content` regenerates
|
|
21
25
|
`build/resolution/*` and then fails on the same run that changed them. Run it
|
|
22
26
|
again; the second run is clean. Commit the regenerated `build/` and
|