explico 0.4.0 → 0.5.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 +273 -1
- package/README.md +103 -8
- package/dist/compiler/astro.d.ts +9 -1
- package/dist/compiler/astro.js +182 -3
- package/dist/compiler/cli.d.ts +13 -0
- package/dist/compiler/cli.js +28 -4
- package/dist/compiler/collections.d.ts +3 -2
- package/dist/compiler/collections.js +28 -1
- package/dist/compiler/course-config.d.ts +58 -1
- package/dist/compiler/course-config.js +28 -1
- package/dist/compiler/exports/standalone-html.d.ts +34 -0
- package/dist/compiler/exports/standalone-html.js +803 -0
- package/dist/compiler/manifest.d.ts +90 -6
- package/dist/compiler/manifest.js +150 -17
- package/dist/compiler/resolution-report.d.ts +4 -0
- package/dist/reference/base-abbreviations.d.mts +30 -0
- package/dist/reference/base-abbreviations.mjs +60 -0
- package/dist/reference/base-operators.d.mts +15 -0
- package/dist/reference/base-operators.mjs +40 -0
- package/dist/reference/base-roman-labels.d.mts +35 -0
- package/dist/reference/base-roman-labels.mjs +43 -0
- package/dist/reference/consistency.d.ts +8 -2
- package/dist/reference/consistency.js +50 -3
- package/dist/reference/diagrams-validate.d.ts +21 -0
- package/dist/reference/diagrams-validate.js +13 -0
- package/dist/reference/equations.d.mts +10 -15
- package/dist/reference/equations.mjs +67 -14
- package/dist/reference/gate-abbreviations.d.ts +17 -0
- package/dist/reference/gate-abbreviations.js +64 -0
- package/dist/reference/gate-math.d.ts +28 -5
- package/dist/reference/gate-math.js +5 -2
- package/dist/reference/gate-prose-notation.d.ts +60 -0
- package/dist/reference/gate-prose-notation.js +125 -0
- package/dist/reference/keyed-float-validate.d.ts +5 -5
- package/dist/reference/keyed-jsx-floats.d.mts +7 -12
- package/dist/reference/keyed-jsx-floats.mjs +50 -25
- package/dist/reference/knowledge-checks-validate.d.ts +35 -0
- package/dist/reference/knowledge-checks-validate.js +78 -0
- package/dist/reference/knowledge-checks.d.mts +25 -0
- package/dist/reference/knowledge-checks.mjs +45 -0
- package/dist/reference/lab-math-scope.d.ts +9 -1
- package/dist/reference/lab-math-scope.js +32 -4
- package/dist/reference/math-glyphs.d.mts +30 -0
- package/dist/reference/math-glyphs.mjs +367 -0
- package/dist/reference/mdx-attributes.d.mts +13 -0
- package/dist/reference/mdx-attributes.mjs +38 -0
- package/dist/reference/mixed-content.d.ts +17 -0
- package/dist/reference/mixed-content.js +29 -0
- package/dist/reference/page-notation.d.ts +52 -0
- package/dist/reference/page-notation.js +171 -0
- package/dist/reference/raw-floats-validate.d.ts +18 -0
- package/dist/reference/raw-floats-validate.js +53 -0
- package/dist/reference/raw-floats.d.mts +18 -0
- package/dist/reference/raw-floats.mjs +129 -0
- package/dist/reference/rehype-heading-anchors.d.mts +1 -1
- package/dist/reference/rehype-heading-anchors.mjs +107 -1
- package/dist/reference/rehype-table-header-labels.d.mts +26 -0
- package/dist/reference/rehype-table-header-labels.mjs +152 -0
- package/dist/reference/remark-abbreviations.d.mts +7 -0
- package/dist/reference/remark-abbreviations.mjs +174 -0
- package/dist/reference/remark-memo.d.mts +16 -0
- package/dist/reference/remark-memo.mjs +339 -0
- package/dist/reference/remark-notation.d.mts +57 -9
- package/dist/reference/remark-notation.mjs +354 -42
- package/dist/reference/remark-short-note.d.mts +4 -0
- package/dist/reference/remark-short-note.mjs +237 -0
- package/dist/reference/types.d.ts +16 -1
- package/dist/reference/units-vocab.js +4 -0
- package/docs/VERSIONING.md +33 -4
- package/docs/authoring/README.md +8 -0
- package/docs/authoring/agents.md +17 -2
- package/docs/authoring/components.md +135 -0
- package/docs/authoring/migrating.md +92 -7
- package/docs/authoring/notation.md +560 -1
- package/docs/authoring/structure.md +120 -8
- package/docs/authoring/visualizations.md +36 -3
- package/package.json +5 -2
- package/src/compiler/astro.ts +208 -3
- package/src/compiler/cli.ts +29 -2
- package/src/compiler/collections.ts +45 -4
- package/src/compiler/course-config.ts +92 -0
- package/src/compiler/exports/standalone-html.ts +963 -0
- package/src/compiler/manifest.ts +268 -22
- package/src/components/HoverPanel.astro +45 -4
- package/src/components/abbreviation/AbbreviationGlossary.astro +70 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +127 -0
- package/src/components/assessments/AssessmentRunner.tsx +52 -17
- package/src/components/assessments/AssessmentSet.astro +98 -14
- package/src/components/assessments/RenderedContent.tsx +55 -0
- package/src/components/assessments/assessment-randomization.ts +16 -0
- package/src/components/diagram/Diagram.astro +444 -83
- package/src/components/disclosure/hover-preview.ts +202 -0
- package/src/components/disclosure/persisted-disclosure.ts +66 -0
- package/src/components/examples/CompactExamples.astro +2 -0
- package/src/components/figure/Figure.astro +279 -10
- package/src/components/memo/Memo.astro +453 -0
- package/src/components/memo/ShortNote.astro +223 -0
- package/src/components/notation/NotationFormula.astro +18 -1
- package/src/components/notation/NotationLayer.astro +105 -8
- package/src/components/notation/render-notation-math.ts +39 -2
- package/src/components/notation/safe-math-to-react.ts +50 -0
- package/src/components/reference/FloatRefLayer.astro +233 -15
- package/src/components/starlight/CollapsibleSections.astro +461 -0
- package/src/components/starlight/DiagramEnhancer.astro +385 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +5 -17
- package/src/components/starlight/KeyedFloatEnhancer.astro +81 -3
- package/src/components/starlight/LayoutHeader.astro +50 -0
- package/src/components/starlight/LayoutSocialIcons.astro +41 -2
- package/src/components/starlight/LessonExportActions.astro +436 -0
- package/src/components/starlight/LessonFooter.astro +27 -2
- package/src/components/starlight/section-parts.ts +31 -0
- package/src/components/table/Table.astro +293 -7
- package/src/components/table/TableOverflowExpander.astro +489 -0
- package/src/content-config.ts +20 -0
- package/src/reference/base-abbreviations.mjs +60 -0
- package/src/reference/base-operators.mjs +40 -0
- package/src/reference/base-roman-labels.mjs +43 -0
- package/src/reference/consistency.ts +73 -2
- package/src/reference/diagrams-validate.ts +40 -0
- package/src/reference/equations.mjs +67 -14
- package/src/reference/gate-abbreviations.ts +128 -0
- package/src/reference/gate-math.ts +27 -7
- package/src/reference/gate-prose-notation.ts +224 -0
- package/src/reference/keyed-float-validate.ts +5 -5
- package/src/reference/keyed-jsx-floats.mjs +50 -25
- package/src/reference/knowledge-checks-validate.ts +130 -0
- package/src/reference/knowledge-checks.mjs +45 -0
- package/src/reference/lab-math-scope.ts +45 -3
- package/src/reference/math-glyphs.mjs +367 -0
- package/src/reference/mdx-attributes.mjs +38 -0
- package/src/reference/mixed-content.ts +69 -0
- package/src/reference/page-notation.ts +252 -0
- package/src/reference/raw-floats-validate.ts +103 -0
- package/src/reference/raw-floats.mjs +129 -0
- package/src/reference/rehype-heading-anchors.mjs +107 -1
- package/src/reference/rehype-table-header-labels.mjs +152 -0
- package/src/reference/remark-abbreviations.mjs +174 -0
- package/src/reference/remark-memo.mjs +339 -0
- package/src/reference/remark-notation.mjs +354 -42
- package/src/reference/remark-short-note.mjs +237 -0
- package/src/reference/types.ts +16 -0
- package/src/reference/units-vocab.ts +4 -0
- package/src/styles/global.css +996 -50
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const CODE_FENCE = /```[\s\S]*?```/g;
|
|
2
|
+
const INLINE_CODE = /(^|[^`])`[^`\n]*`/g;
|
|
3
|
+
const DISPLAY_MATH = /\$\$([\s\S]+?)\$\$/g;
|
|
4
|
+
const INLINE_MATH = /(?<![\\$])\$(?!\s)([^$\n]+?)(?<![\\\s])\$(?!\d)/g;
|
|
5
|
+
const IMPORT_LINE = /^[ \t]*import\b[^\n]*$/gm;
|
|
6
|
+
const KEY_REFERENCE = /\[\[[^\]\n]*\]\]/g;
|
|
7
|
+
const JSX_TAG = /<\/?[A-Za-z][^<>]*>/g;
|
|
8
|
+
function proseOnly(body) {
|
|
9
|
+
const blank = (match) => match.replace(/[^\n]/g, " ");
|
|
10
|
+
return body.replace(CODE_FENCE, blank).replace(INLINE_CODE, (match, lead) => lead + blank(match.slice(lead.length))).replace(DISPLAY_MATH, blank).replace(INLINE_MATH, blank).replace(IMPORT_LINE, blank).replace(KEY_REFERENCE, blank).replace(JSX_TAG, blank);
|
|
11
|
+
}
|
|
12
|
+
function lineOf(text, index) {
|
|
13
|
+
return text.slice(0, index).split("\n").length;
|
|
14
|
+
}
|
|
15
|
+
const UNIT = String.raw`m/s²|m/s|km/h|rad/s|m²/s²|J/kg|m²|s⁻¹|kg|N|J|W|Hz|deg|°|rad|m|s`;
|
|
16
|
+
const NUMERAL_UNIT = new RegExp(
|
|
17
|
+
String.raw`(?<![\w.])-?\d+(?:\.\d+)?\s?(?:${UNIT})(?![A-Za-z])`,
|
|
18
|
+
"g"
|
|
19
|
+
);
|
|
20
|
+
const BARE_UNIT_PARENS = new RegExp(String.raw`\((?:${UNIT})\)`, "g");
|
|
21
|
+
const SLASH_IDIOMS = /* @__PURE__ */ new Set([
|
|
22
|
+
"and/or",
|
|
23
|
+
"his/her",
|
|
24
|
+
"him/her",
|
|
25
|
+
"he/she",
|
|
26
|
+
"she/he",
|
|
27
|
+
"on/off",
|
|
28
|
+
"yes/no",
|
|
29
|
+
"pass/fail",
|
|
30
|
+
"input/output",
|
|
31
|
+
"output/input",
|
|
32
|
+
"read/write",
|
|
33
|
+
"either/or",
|
|
34
|
+
"client/server",
|
|
35
|
+
"pre/post",
|
|
36
|
+
"a/b"
|
|
37
|
+
]);
|
|
38
|
+
const SLASH_TOKENS = /\b([A-Za-z]{1,6})\/([A-Za-z]{1,6})\b/g;
|
|
39
|
+
const FAKE_SCRIPT = /[A-Za-z0-9][_^][A-Za-z0-9{]/g;
|
|
40
|
+
function findAll(text, pattern, toFinding) {
|
|
41
|
+
const out = [];
|
|
42
|
+
const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
43
|
+
let match;
|
|
44
|
+
while (match = re.exec(text)) {
|
|
45
|
+
out.push(toFinding(match));
|
|
46
|
+
if (match[0].length === 0) re.lastIndex++;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
const FIX = "Wrap it in $\u2026$ (or $$\u2026$$) instead \u2014 see docs/authoring/notation.md#n9.";
|
|
51
|
+
function findingsIn(text) {
|
|
52
|
+
return [
|
|
53
|
+
...findAll(text, /\\\|/g, (m) => ({
|
|
54
|
+
index: m.index,
|
|
55
|
+
token: m[0],
|
|
56
|
+
message: `Escaped pipe ${JSON.stringify(m[0])} outside math. Use \\lvert\u2026\\rvert for an absolute value or norm inside $\u2026$, not an escaped table pipe. ${FIX}`
|
|
57
|
+
})),
|
|
58
|
+
...findAll(text, NUMERAL_UNIT, (m) => ({
|
|
59
|
+
index: m.index,
|
|
60
|
+
token: m[0].trim(),
|
|
61
|
+
message: `Numeral with a plain-text unit ${JSON.stringify(m[0].trim())} outside math. ${FIX}`
|
|
62
|
+
})),
|
|
63
|
+
...findAll(text, BARE_UNIT_PARENS, (m) => ({
|
|
64
|
+
index: m.index,
|
|
65
|
+
token: m[0],
|
|
66
|
+
message: `Plain-text unit ${JSON.stringify(m[0])} outside math. ${FIX}`
|
|
67
|
+
})),
|
|
68
|
+
...findAll(text, FAKE_SCRIPT, (m) => ({
|
|
69
|
+
index: m.index,
|
|
70
|
+
token: m[0],
|
|
71
|
+
message: `${JSON.stringify(m[0])} looks like a hand-typed sub/superscript outside math. ${FIX}`
|
|
72
|
+
})),
|
|
73
|
+
...findAll(text, SLASH_TOKENS, (m) => {
|
|
74
|
+
if (SLASH_IDIOMS.has(m[0].toLowerCase())) return null;
|
|
75
|
+
return {
|
|
76
|
+
index: m.index,
|
|
77
|
+
token: m[0],
|
|
78
|
+
message: `${JSON.stringify(m[0])} outside math reads as a division of two symbols/units, not English prose. If it really is prose, add the idiom to SLASH_IDIOMS in gate-prose-notation.ts (reviewed like any other rule change). ${FIX}`
|
|
79
|
+
};
|
|
80
|
+
}).filter(Boolean)
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
function gateProseNotation(lessons) {
|
|
84
|
+
const diagnostics = [];
|
|
85
|
+
for (const lesson of lessons) {
|
|
86
|
+
const text = proseOnly(lesson.body);
|
|
87
|
+
for (const finding of findingsIn(text)) {
|
|
88
|
+
diagnostics.push({
|
|
89
|
+
context: "prose",
|
|
90
|
+
severity: "error",
|
|
91
|
+
file: lesson.source.file,
|
|
92
|
+
line: lineOf(lesson.body, finding.index),
|
|
93
|
+
token: finding.token,
|
|
94
|
+
message: finding.message
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return diagnostics;
|
|
99
|
+
}
|
|
100
|
+
function gateProseNotationInAssessments(assessments) {
|
|
101
|
+
const diagnostics = [];
|
|
102
|
+
for (const assessment of assessments) {
|
|
103
|
+
for (const item of assessment.items ?? []) {
|
|
104
|
+
for (const field of [item.prompt, item.explanation]) {
|
|
105
|
+
if (typeof field !== "string") continue;
|
|
106
|
+
for (const finding of findingsIn(proseOnly(field))) {
|
|
107
|
+
diagnostics.push({
|
|
108
|
+
context: "prose",
|
|
109
|
+
severity: "error",
|
|
110
|
+
file: `${assessment.id} (${item.id ?? "?"})`,
|
|
111
|
+
token: finding.token,
|
|
112
|
+
message: finding.message
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return diagnostics;
|
|
119
|
+
}
|
|
120
|
+
export {
|
|
121
|
+
gateProseNotation,
|
|
122
|
+
gateProseNotationInAssessments,
|
|
123
|
+
lineOf,
|
|
124
|
+
proseOnly
|
|
125
|
+
};
|
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
* - `<prefix>-invalid-key` (error): an `id` must match the key grammar.
|
|
7
7
|
* - `<prefix>-duplicate-key` (error): an `id` is unique per lesson.
|
|
8
8
|
* - `<prefix>-ref-resolves` (error): every `[[<prefix>-key]]` /
|
|
9
|
-
* `[[slug#<prefix>-key]]` points at
|
|
9
|
+
* `[[slug#<prefix>-key]]` points at the matching keyed float with that `id`.
|
|
10
10
|
* - `<prefix>-key-unused` (warning): a keyed float never referenced.
|
|
11
11
|
*
|
|
12
|
-
* `tables-validate.ts` and `
|
|
13
|
-
* so callers import
|
|
14
|
-
* narrow diagnostic-code types, while `equations-validate.ts` stays
|
|
15
|
-
*
|
|
12
|
+
* `tables-validate.ts`, `figures-validate.ts`, and `diagrams-validate.ts` are
|
|
13
|
+
* thin wrappers over this so callers import a kind-specific validator with
|
|
14
|
+
* narrow diagnostic-code types, while `equations-validate.ts` stays untouched
|
|
15
|
+
* — equations predate this and keep their own dedicated path.
|
|
16
16
|
*/
|
|
17
17
|
interface KeyedFloatKind {
|
|
18
18
|
/** The MDX component tag, e.g. `Table` or `Figure`. */
|
|
@@ -44,21 +44,16 @@ export function countKeyedJsxFloats(markdown: any, { tagName }: {
|
|
|
44
44
|
tagName: any;
|
|
45
45
|
}): number;
|
|
46
46
|
/**
|
|
47
|
-
* The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's
|
|
48
|
-
* children in document order, advancing the
|
|
49
|
-
* depth-2 heading and emitting a record for
|
|
50
|
-
* `tagName` — keyed or not, exactly like
|
|
51
|
-
* is threaded the same way as in
|
|
47
|
+
* The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's top-level
|
|
48
|
+
* children (and through `<Memo>` bodies) in document order, advancing the
|
|
49
|
+
* section counter on every top-level depth-2 heading and emitting a record for
|
|
50
|
+
* every `mdxJsxFlowElement` named `tagName` — keyed or not, exactly like
|
|
51
|
+
* `collectDisplayEquations`. `numbering` is threaded the same way as in
|
|
52
|
+
* `scanKeyedJsxFloats`.
|
|
52
53
|
*/
|
|
53
54
|
export function collectKeyedJsxFloats(tree: any, { tagName, numbering }: {
|
|
54
55
|
tagName: any;
|
|
55
56
|
numbering: any;
|
|
56
|
-
}):
|
|
57
|
-
key: any;
|
|
58
|
-
number: string;
|
|
59
|
-
section: number;
|
|
60
|
-
indexInSection: number;
|
|
61
|
-
node: any;
|
|
62
|
-
}[];
|
|
57
|
+
}): any[];
|
|
63
58
|
/** Same shape as `EQ_KEY_PATTERN` — the notation-key grammar without dots. */
|
|
64
59
|
export const KEYED_FLOAT_KEY_PATTERN: RegExp;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Keyed-float identity for a JSX-wrapped table or
|
|
3
|
-
* `<Table
|
|
2
|
+
* Keyed-float identity for a JSX-wrapped table, figure, or diagram, shared
|
|
3
|
+
* between `<Table>`, `<Figure>`, and `<Diagram>`.
|
|
4
4
|
*
|
|
5
5
|
* This mirrors `equations.mjs`, but the key comes from a plain `id` prop on
|
|
6
6
|
* the wrapper component rather than a `\label{eq:key}` embedded in shared
|
|
7
|
-
* LaTeX source — a `<Table>`/`<Figure>` is purpose-built, so there is no
|
|
7
|
+
* LaTeX source — a `<Table>`/`<Figure>`/`<Diagram>` is purpose-built, so there is no
|
|
8
8
|
* ambiguity an embedded marker would need to resolve. An author writes the
|
|
9
9
|
* bare key (`id="kinematics-summary"`); the rendered DOM id and the `[[…]]`
|
|
10
10
|
* reference prefix are computed the same way `\label{eq:key}` becomes
|
|
11
|
-
* `#eq-key` — `keyedFloatAnchorId` prepends `tab-`/`fig-`.
|
|
11
|
+
* `#eq-key` — `keyedFloatAnchorId` prepends `tab-`/`fig-`/`dia-`.
|
|
12
12
|
*
|
|
13
|
-
* Only top-level occurrences count, exactly like `collectDisplayEquations
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* Only top-level occurrences count, exactly like `collectDisplayEquations`,
|
|
14
|
+
* except that `<Memo>` is transparent: a float in a memo remains a side-note
|
|
15
|
+
* in the lesson's reading order. A keyed float nested inside a
|
|
16
|
+
* `<CompactExample>` (or any other component) is illustrative, not part of the
|
|
17
|
+
* official sequence.
|
|
18
18
|
*
|
|
19
19
|
* `scanKeyedJsxFloats` (raw Markdown, for the manifest builder) and
|
|
20
20
|
* `collectKeyedJsxFloats` (mdast, for `remark-notation.mjs`) MUST agree —
|
|
@@ -41,7 +41,7 @@ function floatNumber(section, index) {
|
|
|
41
41
|
* scheme is `‹part›.‹offset + indexInLesson›` and `##` headings are ignored;
|
|
42
42
|
* without it, the legacy `##`-section scheme. Mirrors `displayEquationNumber`
|
|
43
43
|
* in `equations.mjs`. Each kind of float counts on its own — a `<Table>` never
|
|
44
|
-
* advances the `<Figure>` sequence — so `offset` is per kind too.
|
|
44
|
+
* advances the `<Figure>` or `<Diagram>` sequence — so `offset` is per kind too.
|
|
45
45
|
*/
|
|
46
46
|
function keyedFloatNumber({
|
|
47
47
|
prefix,
|
|
@@ -91,7 +91,10 @@ function readIdAttribute(attributesSource) {
|
|
|
91
91
|
*/
|
|
92
92
|
export function scanKeyedJsxFloats(markdown, { tagName, numbering }) {
|
|
93
93
|
const { prefix, offset } = resolveNumbering(numbering);
|
|
94
|
-
const text = stripJsxComponents(stripFencedCode(markdown), tagName
|
|
94
|
+
const text = stripJsxComponents(stripFencedCode(markdown), tagName, [
|
|
95
|
+
'Memo',
|
|
96
|
+
'ShortNote',
|
|
97
|
+
]);
|
|
95
98
|
const tokens = [];
|
|
96
99
|
const heading = /^[ \t]{0,3}##[ \t]+\S/gm;
|
|
97
100
|
const opening = new RegExp(`<${tagName}\\b([^>]*)>`, 'g');
|
|
@@ -141,11 +144,12 @@ export function countKeyedJsxFloats(markdown, { tagName }) {
|
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
/**
|
|
144
|
-
* The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's
|
|
145
|
-
* children in document order, advancing the
|
|
146
|
-
* depth-2 heading and emitting a record for
|
|
147
|
-
* `tagName` — keyed or not, exactly like
|
|
148
|
-
* is threaded the same way as in
|
|
147
|
+
* The mdast equivalent of `scanKeyedJsxFloats`: walk the tree's top-level
|
|
148
|
+
* children (and through `<Memo>` bodies) in document order, advancing the
|
|
149
|
+
* section counter on every top-level depth-2 heading and emitting a record for
|
|
150
|
+
* every `mdxJsxFlowElement` named `tagName` — keyed or not, exactly like
|
|
151
|
+
* `collectDisplayEquations`. `numbering` is threaded the same way as in
|
|
152
|
+
* `scanKeyedJsxFloats`.
|
|
149
153
|
*/
|
|
150
154
|
export function collectKeyedJsxFloats(tree, { tagName, numbering }) {
|
|
151
155
|
const { prefix, offset } = resolveNumbering(numbering);
|
|
@@ -154,14 +158,8 @@ export function collectKeyedJsxFloats(tree, { tagName, numbering }) {
|
|
|
154
158
|
let indexInSection = 0;
|
|
155
159
|
let indexInLesson = 0;
|
|
156
160
|
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
if (!node || typeof node !== 'object') continue;
|
|
160
|
-
if (node.type === 'heading' && node.depth === 2) {
|
|
161
|
-
section += 1;
|
|
162
|
-
indexInSection = 0;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
161
|
+
const collectFromDetail = (node) => {
|
|
162
|
+
if (!node || typeof node !== 'object') return;
|
|
165
163
|
if (node.type === 'mdxJsxFlowElement' && node.name === tagName) {
|
|
166
164
|
indexInSection += 1;
|
|
167
165
|
indexInLesson += 1;
|
|
@@ -184,8 +182,35 @@ export function collectKeyedJsxFloats(tree, { tagName, numbering }) {
|
|
|
184
182
|
indexInSection,
|
|
185
183
|
node,
|
|
186
184
|
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (
|
|
188
|
+
node.type === 'mdxJsxFlowElement' &&
|
|
189
|
+
node.name !== 'Memo' &&
|
|
190
|
+
node.name !== 'ShortNote'
|
|
191
|
+
) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!Array.isArray(node.children)) return;
|
|
195
|
+
for (const child of node.children) collectFromDetail(child);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const children = Array.isArray(tree?.children) ? tree.children : [];
|
|
199
|
+
for (const node of children) {
|
|
200
|
+
if (!node || typeof node !== 'object') continue;
|
|
201
|
+
if (node.type === 'heading' && node.depth === 2) {
|
|
202
|
+
section += 1;
|
|
203
|
+
indexInSection = 0;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (node.type === 'mdxJsxFlowElement' && node.name === tagName) {
|
|
207
|
+
collectFromDetail(node);
|
|
208
|
+
} else if (
|
|
209
|
+
node.type === 'mdxJsxFlowElement' &&
|
|
210
|
+
(node.name === 'Memo' || node.name === 'ShortNote')
|
|
211
|
+
) {
|
|
212
|
+
collectFromDetail(node);
|
|
187
213
|
}
|
|
188
214
|
}
|
|
189
215
|
return floats;
|
|
190
216
|
}
|
|
191
|
-
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Build-time identity and reference validation for knowledge checks. */
|
|
2
|
+
interface KnowledgeCheckLessonInput {
|
|
3
|
+
readonly lessonId: string;
|
|
4
|
+
readonly slug: string;
|
|
5
|
+
readonly file: string;
|
|
6
|
+
readonly body: string;
|
|
7
|
+
readonly assessmentIds: readonly string[];
|
|
8
|
+
readonly numbering?: {
|
|
9
|
+
readonly prefix: number;
|
|
10
|
+
readonly offset: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
interface KnowledgeCheckLabelRecord {
|
|
14
|
+
readonly key: string;
|
|
15
|
+
readonly number: string;
|
|
16
|
+
readonly lessonId: string;
|
|
17
|
+
readonly slug: string;
|
|
18
|
+
readonly indexInLesson: number;
|
|
19
|
+
}
|
|
20
|
+
interface KnowledgeCheckDiagnostic {
|
|
21
|
+
readonly code: 'check-invalid-key' | 'check-duplicate-key' | 'check-ref-resolves';
|
|
22
|
+
readonly severity: 'error';
|
|
23
|
+
readonly message: string;
|
|
24
|
+
readonly file?: string;
|
|
25
|
+
readonly lessonId?: string;
|
|
26
|
+
readonly key?: string;
|
|
27
|
+
}
|
|
28
|
+
interface KnowledgeCheckValidation {
|
|
29
|
+
readonly labels: readonly KnowledgeCheckLabelRecord[];
|
|
30
|
+
readonly numbersBySlug: Record<string, Record<string, string>>;
|
|
31
|
+
readonly diagnostics: readonly KnowledgeCheckDiagnostic[];
|
|
32
|
+
}
|
|
33
|
+
declare function validateKnowledgeChecks(lessons: readonly KnowledgeCheckLessonInput[]): KnowledgeCheckValidation;
|
|
34
|
+
|
|
35
|
+
export { type KnowledgeCheckDiagnostic, type KnowledgeCheckLabelRecord, type KnowledgeCheckLessonInput, type KnowledgeCheckValidation, validateKnowledgeChecks };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
collectKnowledgeChecks,
|
|
3
|
+
KNOWLEDGE_CHECK_KEY_PATTERN,
|
|
4
|
+
parseKnowledgeCheckRef
|
|
5
|
+
} from "./knowledge-checks.mjs";
|
|
6
|
+
const BRACKET_REF = /\[\[\s*([^\][]+?)\s*\]\]/g;
|
|
7
|
+
function stripCode(text) {
|
|
8
|
+
return text.replace(/```[\s\S]*?```/g, " ").replace(/~~~[\s\S]*?~~~/g, " ").replace(/`[^`\n]*`/g, " ");
|
|
9
|
+
}
|
|
10
|
+
function validateKnowledgeChecks(lessons) {
|
|
11
|
+
const labels = [];
|
|
12
|
+
const diagnostics = [];
|
|
13
|
+
const numbersBySlug = {};
|
|
14
|
+
for (const lesson of [...lessons].sort(
|
|
15
|
+
(a, b) => a.lessonId.localeCompare(b.lessonId)
|
|
16
|
+
)) {
|
|
17
|
+
const seen = /* @__PURE__ */ new Set();
|
|
18
|
+
const table = {};
|
|
19
|
+
for (const check of collectKnowledgeChecks(
|
|
20
|
+
lesson.assessmentIds,
|
|
21
|
+
lesson.numbering
|
|
22
|
+
)) {
|
|
23
|
+
if (!KNOWLEDGE_CHECK_KEY_PATTERN.test(check.key)) {
|
|
24
|
+
diagnostics.push({
|
|
25
|
+
code: "check-invalid-key",
|
|
26
|
+
severity: "error",
|
|
27
|
+
message: `${lesson.lessonId} has a knowledge-check id "${check.key}" that is not a valid content key.`,
|
|
28
|
+
file: lesson.file,
|
|
29
|
+
lessonId: lesson.lessonId,
|
|
30
|
+
key: check.key
|
|
31
|
+
});
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (seen.has(check.key)) {
|
|
35
|
+
diagnostics.push({
|
|
36
|
+
code: "check-duplicate-key",
|
|
37
|
+
severity: "error",
|
|
38
|
+
message: `${lesson.lessonId} lists knowledge check "${check.key}" twice; an assessment id is unique per lesson.`,
|
|
39
|
+
file: lesson.file,
|
|
40
|
+
lessonId: lesson.lessonId,
|
|
41
|
+
key: check.key
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
seen.add(check.key);
|
|
46
|
+
table[check.key] = check.number;
|
|
47
|
+
labels.push({
|
|
48
|
+
...check,
|
|
49
|
+
lessonId: lesson.lessonId,
|
|
50
|
+
slug: lesson.slug
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
numbersBySlug[lesson.slug] = table;
|
|
54
|
+
}
|
|
55
|
+
for (const lesson of lessons) {
|
|
56
|
+
for (const match of stripCode(lesson.body).matchAll(BRACKET_REF)) {
|
|
57
|
+
const ref = parseKnowledgeCheckRef((match[1] ?? "").trim());
|
|
58
|
+
if (!ref) continue;
|
|
59
|
+
const targetSlug = ref.lesson ?? lesson.slug;
|
|
60
|
+
if (numbersBySlug[targetSlug]?.[ref.key] !== void 0) continue;
|
|
61
|
+
diagnostics.push({
|
|
62
|
+
code: "check-ref-resolves",
|
|
63
|
+
severity: "error",
|
|
64
|
+
message: ref.lesson ? `${lesson.lessonId} references [[${ref.lesson}#check-${ref.key}]] but that lesson has no knowledge check "${ref.key}"` : `${lesson.lessonId} references [[check-${ref.key}]] but its .checks.yml does not list "${ref.key}"`,
|
|
65
|
+
file: lesson.file,
|
|
66
|
+
lessonId: lesson.lessonId,
|
|
67
|
+
key: ref.key
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
diagnostics.sort(
|
|
72
|
+
(a, b) => a.code.localeCompare(b.code) || (a.lessonId ?? "").localeCompare(b.lessonId ?? "") || (a.key ?? "").localeCompare(b.key ?? "") || a.message.localeCompare(b.message)
|
|
73
|
+
);
|
|
74
|
+
return { labels, numbersBySlug, diagnostics };
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
validateKnowledgeChecks
|
|
78
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** `[[check-id]]`, or `[[lesson/slug#check-id]]` across pages. */
|
|
2
|
+
export function parseKnowledgeCheckRef(raw: any): {
|
|
3
|
+
lesson: string;
|
|
4
|
+
key: string;
|
|
5
|
+
} | {
|
|
6
|
+
key: string;
|
|
7
|
+
lesson?: undefined;
|
|
8
|
+
} | undefined;
|
|
9
|
+
/** The stable DOM fragment for a knowledge check. */
|
|
10
|
+
export function knowledgeCheckAnchorId(key: any): string;
|
|
11
|
+
/**
|
|
12
|
+
* Number an ordered assessment-id list. With manifest placement, the number
|
|
13
|
+
* is `part.offset + index`; without it, the list remains usefully page-local.
|
|
14
|
+
*/
|
|
15
|
+
export function collectKnowledgeChecks(assessmentIds: any, numbering: any): any;
|
|
16
|
+
/**
|
|
17
|
+
* Stable identity and numbering helpers for lesson knowledge checks.
|
|
18
|
+
*
|
|
19
|
+
* A course owns check titles and questions in `content/assessments/*.json`,
|
|
20
|
+
* then places their ids in a lesson's colocated `.checks.yml`. That ordered
|
|
21
|
+
* list is the check sequence: numbers run independently across each part,
|
|
22
|
+
* exactly like the equation/table/figure/diagram streams.
|
|
23
|
+
*/
|
|
24
|
+
/** Assessment ids use the content key grammar, including optional dots. */
|
|
25
|
+
export const KNOWLEDGE_CHECK_KEY_PATTERN: RegExp;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable identity and numbering helpers for lesson knowledge checks.
|
|
3
|
+
*
|
|
4
|
+
* A course owns check titles and questions in `content/assessments/*.json`,
|
|
5
|
+
* then places their ids in a lesson's colocated `.checks.yml`. That ordered
|
|
6
|
+
* list is the check sequence: numbers run independently across each part,
|
|
7
|
+
* exactly like the equation/table/figure/diagram streams.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Assessment ids use the content key grammar, including optional dots. */
|
|
11
|
+
export const KNOWLEDGE_CHECK_KEY_PATTERN =
|
|
12
|
+
/^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
13
|
+
|
|
14
|
+
/** `[[check-id]]`, or `[[lesson/slug#check-id]]` across pages. */
|
|
15
|
+
export function parseKnowledgeCheckRef(raw) {
|
|
16
|
+
const match =
|
|
17
|
+
/^(?:([a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*)#)?check-([a-z0-9]+(?:[.-][a-z0-9]+)*)$/.exec(
|
|
18
|
+
String(raw).trim(),
|
|
19
|
+
);
|
|
20
|
+
if (!match) return undefined;
|
|
21
|
+
return match[1] ? { lesson: match[1], key: match[2] } : { key: match[2] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The stable DOM fragment for a knowledge check. */
|
|
25
|
+
export function knowledgeCheckAnchorId(key) {
|
|
26
|
+
return `check-${key}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Number an ordered assessment-id list. With manifest placement, the number
|
|
31
|
+
* is `part.offset + index`; without it, the list remains usefully page-local.
|
|
32
|
+
*/
|
|
33
|
+
export function collectKnowledgeChecks(assessmentIds, numbering) {
|
|
34
|
+
const prefix = Number.isInteger(numbering?.prefix) && numbering.prefix > 0
|
|
35
|
+
? numbering.prefix
|
|
36
|
+
: undefined;
|
|
37
|
+
const offset = Number.isInteger(numbering?.offset) && numbering.offset >= 0
|
|
38
|
+
? numbering.offset
|
|
39
|
+
: 0;
|
|
40
|
+
return assessmentIds.map((key, index) => ({
|
|
41
|
+
key,
|
|
42
|
+
number: prefix == null ? String(index + 1) : `${prefix}.${offset + index + 1}`,
|
|
43
|
+
indexInLesson: index + 1,
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
@@ -15,7 +15,15 @@ interface LessonNotationFrontmatter {
|
|
|
15
15
|
*
|
|
16
16
|
* Shared definitions are read from the one compiler manifest (Phase D5); this
|
|
17
17
|
* module never re-reads the content tree.
|
|
18
|
+
*
|
|
19
|
+
* A nested component whose `body` contains notation absent from lesson MDX
|
|
20
|
+
* passes `{ locals: Astro.locals, context: 'Component formula name' }`. That
|
|
21
|
+
* request-local declaration lets `LessonFooter` ship just those extra cards and
|
|
22
|
+
* gives an unknown `\explain` key an actionable render-time diagnostic.
|
|
18
23
|
*/
|
|
19
|
-
declare function labMathScope(notation: LessonNotationFrontmatter | undefined, body?: string
|
|
24
|
+
declare function labMathScope(notation: LessonNotationFrontmatter | undefined, body?: string, component?: {
|
|
25
|
+
readonly locals: object;
|
|
26
|
+
readonly context: string;
|
|
27
|
+
}): Promise<LabMathScopeDefinition[]>;
|
|
20
28
|
|
|
21
29
|
export { labMathScope };
|
|
@@ -1,25 +1,53 @@
|
|
|
1
1
|
import { loadManifest } from "../compiler/manifest.js";
|
|
2
|
+
import {
|
|
3
|
+
registerPageNotationDependencies,
|
|
4
|
+
routeIdFromLocals
|
|
5
|
+
} from "./page-notation.js";
|
|
2
6
|
const TERM_KEY = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
|
|
3
7
|
const EXPLAIN_KEY = /\\explain\s*\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}/g;
|
|
4
|
-
async function labMathScope(notation, body = "") {
|
|
8
|
+
async function labMathScope(notation, body = "", component) {
|
|
5
9
|
const local = notation?.local ?? [];
|
|
6
10
|
const scope = local.map((definition) => ({
|
|
7
11
|
key: definition.key,
|
|
8
12
|
notation: definition.latex
|
|
9
13
|
}));
|
|
10
14
|
const referenced = /* @__PURE__ */ new Set();
|
|
15
|
+
const explicitlyExplained = /* @__PURE__ */ new Set();
|
|
11
16
|
for (const pattern of [TERM_KEY, EXPLAIN_KEY]) {
|
|
12
|
-
for (const match of body.matchAll(pattern))
|
|
17
|
+
for (const match of body.matchAll(pattern)) {
|
|
18
|
+
referenced.add(match[1]);
|
|
19
|
+
if (pattern === EXPLAIN_KEY) explicitlyExplained.add(match[1]);
|
|
20
|
+
}
|
|
13
21
|
}
|
|
14
22
|
const localKeys = new Set(local.map((definition) => definition.key));
|
|
15
23
|
const manifest = await loadManifest();
|
|
16
24
|
const sharedByKey = new Map(
|
|
17
25
|
manifest.notation.definitions.filter((definition) => definition.kind === "shared").map((definition) => [definition.key, definition])
|
|
18
26
|
);
|
|
27
|
+
const resolvedKeys = /* @__PURE__ */ new Set();
|
|
19
28
|
for (const key of referenced) {
|
|
20
|
-
if (localKeys.has(key))
|
|
29
|
+
if (localKeys.has(key)) {
|
|
30
|
+
resolvedKeys.add(key);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
21
33
|
const shared = sharedByKey.get(key);
|
|
22
|
-
if (shared)
|
|
34
|
+
if (shared) {
|
|
35
|
+
scope.push({ key: shared.key, notation: shared.notation });
|
|
36
|
+
resolvedKeys.add(key);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (explicitlyExplained.has(key)) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Notation dependency error${component ? ` in lesson "${routeIdFromLocals(component.locals) ?? "unknown"}" (${component.context})` : ""}: unknown key "${key}". Define it in notation.local or the shared notation registry.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (component) {
|
|
46
|
+
registerPageNotationDependencies(
|
|
47
|
+
component.locals,
|
|
48
|
+
resolvedKeys,
|
|
49
|
+
component.context
|
|
50
|
+
);
|
|
23
51
|
}
|
|
24
52
|
return scope;
|
|
25
53
|
}
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip Explico's own `\explain{key}{latex}` / `\slot{key}{latex}` markers
|
|
3
|
+
* (see `katex-options.mjs`) down to the LaTeX they wrap, recursively. Lesson
|
|
4
|
+
* math — both a notation card's `formula` and an equation written directly
|
|
5
|
+
* in prose — carries these markers inline so every symbol is hoverable; a
|
|
6
|
+
* reader copying the equation out of this system wants the plain math
|
|
7
|
+
* underneath, not internal notation-key plumbing that means nothing to
|
|
8
|
+
* KaTeX, MathJax, or LaTeX anywhere else. Shared by `render-notation-math.ts`
|
|
9
|
+
* (a notation card's copy button) and `remark-notation.mjs` (a numbered
|
|
10
|
+
* display equation's copy button) — kept here, rather than in either, so
|
|
11
|
+
* neither module has to import the other.
|
|
12
|
+
*/
|
|
13
|
+
export function stripNotationMarkers(latex: any): string;
|
|
1
14
|
/**
|
|
2
15
|
* Resolve and annotate one KaTeX expression against a page's flat glyph table.
|
|
3
16
|
* Source offsets in the result always refer to the original, unmodified LaTeX.
|
|
@@ -36,6 +49,23 @@ export function collectOperators(latex: string): Array<{
|
|
|
36
49
|
name: string;
|
|
37
50
|
source: "named" | "operatorname";
|
|
38
51
|
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Every multi-letter `\mathrm{…}` label in one KaTeX expression (`KE`, `PV`,
|
|
54
|
+
* `opt`) — the upright-label counterpart to {@link collectOperators}. A
|
|
55
|
+
* single-letter body (`\mathrm{d}`, the N7 differential) is not collected:
|
|
56
|
+
* that one is a fixed typographic operator, not a named quantity a reader
|
|
57
|
+
* would need explained.
|
|
58
|
+
*
|
|
59
|
+
* Pure read: nothing here rewrites the source. Used by the
|
|
60
|
+
* `roman-label-explained` consistency check (rule N10); the glyph resolver
|
|
61
|
+
* still skips `\mathrm{…}` bodies whole ([N3](../../docs/authoring/notation.md#n3)).
|
|
62
|
+
*
|
|
63
|
+
* @param {string} latex
|
|
64
|
+
* @returns {Array<{ name: string }>}
|
|
65
|
+
*/
|
|
66
|
+
export function collectRomanLabels(latex: string): Array<{
|
|
67
|
+
name: string;
|
|
68
|
+
}>;
|
|
39
69
|
/**
|
|
40
70
|
* Page-level glyph-map resolver (cleanup plan, D1 decision 2026-09-02).
|
|
41
71
|
*
|