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,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The base roman-label baseline: `\mathrm{…}` multi-letter labels every
|
|
3
|
+
* reader is assumed to know, so a lesson never has to introduce them with a
|
|
4
|
+
* notation entry. The `\operatorname{…}` counterpart is `base-operators.mjs`
|
|
5
|
+
* (rule N8); this is its `\mathrm{…}` twin (rule N10).
|
|
6
|
+
*
|
|
7
|
+
* Rationale: the completeness gate has no opinion about `\mathrm{…}` bodies —
|
|
8
|
+
* they are a typographic label to KaTeX, not a bindable symbol, and N3
|
|
9
|
+
* requires the format without asking whether the label is ever explained.
|
|
10
|
+
* The `roman-label-explained` consistency check fills that gap: a label a
|
|
11
|
+
* lesson uses is either on this list (or the course's additions to it) or it
|
|
12
|
+
* carries a notation entry the reader can hover.
|
|
13
|
+
*
|
|
14
|
+
* Kept intentionally small — unlike `sin`/`exp`, an upright label is almost
|
|
15
|
+
* always a course-specific named quantity (`KE`, `PV`, `Var`-as-a-label) that
|
|
16
|
+
* genuinely benefits from a hover card, so the shipped baseline holds only
|
|
17
|
+
* the qualifiers that function like an operator's extremum subscript
|
|
18
|
+
* (`x_{\mathrm{opt}}`) rather than naming a quantity of their own. A course
|
|
19
|
+
* adds a real one via `romanLabels.allow` in `course.config.ts`, the same
|
|
20
|
+
* shape as `operators.allow`.
|
|
21
|
+
*/
|
|
22
|
+
export const BASE_ROMAN_LABELS = Object.freeze(new Set(['opt']));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The roman labels a course treats as common, given its `course.config.ts`
|
|
26
|
+
* `romanLabels` block: the shipped baseline, plus `allow`, minus `deny`.
|
|
27
|
+
*
|
|
28
|
+
* @param {{ allow?: readonly string[], deny?: readonly string[] } | undefined} romanLabels
|
|
29
|
+
* @returns {Set<string>}
|
|
30
|
+
*/
|
|
31
|
+
export function effectiveRomanLabelAllowlist(romanLabels) {
|
|
32
|
+
const allow = romanLabels?.allow ?? [];
|
|
33
|
+
const deny = new Set(romanLabels?.deny ?? []);
|
|
34
|
+
const effective = new Set(BASE_ROMAN_LABELS);
|
|
35
|
+
for (const name of allow) effective.add(name);
|
|
36
|
+
for (const name of deny) effective.delete(name);
|
|
37
|
+
return effective;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** True when `name` needs no notation entry for this course. */
|
|
41
|
+
export function isBaseRomanLabel(name, romanLabels) {
|
|
42
|
+
return effectiveRomanLabelAllowlist(romanLabels).has(name);
|
|
43
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CourseConventions, CourseOperators } from '../compiler/course-config.js';
|
|
1
|
+
import { CourseConventions, CourseOperators, CourseRomanLabels } from '../compiler/course-config.js';
|
|
2
2
|
import { CurriculumCatalog } from '../curriculum/validation.js';
|
|
3
3
|
import { NotationSpec } from '../compiler/collections.js';
|
|
4
4
|
import { LintIgnore } from './lint-ignore.js';
|
|
@@ -28,12 +28,16 @@ import { NotationRegistry, NotationRegistryInput } from './types.js';
|
|
|
28
28
|
* (h) operator-explained a named operator a lesson uses is on the
|
|
29
29
|
* shipped/course operator allowlist or
|
|
30
30
|
* carries a notation entry (rule N8)
|
|
31
|
+
* (i) roman-label-explained a `\mathrm{…}` multi-letter label a lesson
|
|
32
|
+
* uses is on the shipped/course roman-label
|
|
33
|
+
* allowlist or carries a notation entry
|
|
34
|
+
* (rule N10)
|
|
31
35
|
*
|
|
32
36
|
* Check (d) — the committed per-lesson resolution report — lives in
|
|
33
37
|
* `src/content/resolution-report.ts`.
|
|
34
38
|
*/
|
|
35
39
|
|
|
36
|
-
type ConsistencyCode = 'glyph-unique-in-corpus' | 'notation-units' | 'units-vocab' | 'convention-single-definition' | 'numerals-tagged' | 'weak-local' | 'notation-source-locator' | 'gloss-wants-promoting' | 'card-wants-demoting' | 'gloss-name-shape' | 'operator-explained';
|
|
40
|
+
type ConsistencyCode = 'glyph-unique-in-corpus' | 'notation-units' | 'units-vocab' | 'convention-single-definition' | 'numerals-tagged' | 'weak-local' | 'notation-source-locator' | 'gloss-wants-promoting' | 'card-wants-demoting' | 'gloss-name-shape' | 'operator-explained' | 'roman-label-explained';
|
|
37
41
|
interface ConsistencyDiagnostic {
|
|
38
42
|
readonly code: ConsistencyCode;
|
|
39
43
|
/** Tier 3 — every finding here is a warning until content is brought clean. */
|
|
@@ -56,6 +60,8 @@ interface ConsistencyInput {
|
|
|
56
60
|
readonly conventions: CourseConventions;
|
|
57
61
|
/** This course's edits to the named-operator baseline (rule N8). */
|
|
58
62
|
readonly operators?: CourseOperators;
|
|
63
|
+
/** This course's edits to the roman-label baseline (rule N10). */
|
|
64
|
+
readonly romanLabels?: CourseRomanLabels;
|
|
59
65
|
}
|
|
60
66
|
/**
|
|
61
67
|
* Run every consistency check. The result is sorted deterministically (code,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { effectiveOperatorAllowlist } from "./base-operators.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { effectiveRomanLabelAllowlist } from "./base-roman-labels.mjs";
|
|
3
|
+
import { collectOperators, collectRomanLabels } from "./math-glyphs.mjs";
|
|
3
4
|
import { offVocabularyTokens } from "./units-vocab.js";
|
|
4
5
|
const CONVENTION_RE = /\bsign convention\b|\bcash-flow perspective\b|\bholder(?:'s)?(?:\s+\w+){0,2}\s+perspective\b|\bfrom (?:a|one|the|its)(?:\s+\w+){0,3}\s+perspective\b|\breverses? every sign\b|\bpositive (?:means|amounts?)\b/i;
|
|
5
6
|
function stripCode(text) {
|
|
@@ -332,6 +333,50 @@ function checkOperatorExplained(input, out) {
|
|
|
332
333
|
}
|
|
333
334
|
}
|
|
334
335
|
}
|
|
336
|
+
function checkRomanLabelExplained(input, out) {
|
|
337
|
+
const allowlist = effectiveRomanLabelAllowlist(input.romanLabels);
|
|
338
|
+
const byId = new Map(
|
|
339
|
+
input.registry.definitions.map((definition) => [definition.id, definition])
|
|
340
|
+
);
|
|
341
|
+
const bindingsByLesson = new Map(
|
|
342
|
+
input.registry.bundles.map((bundle) => [bundle.lessonId, bundle.bindings])
|
|
343
|
+
);
|
|
344
|
+
for (const lesson of input.notationInput.lessons) {
|
|
345
|
+
const explained = /* @__PURE__ */ new Set();
|
|
346
|
+
const noteEntryLabels = (latex) => {
|
|
347
|
+
for (const label of collectRomanLabels(latex)) {
|
|
348
|
+
explained.add(label.name);
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
for (const definition of lesson.localDefinitions) {
|
|
352
|
+
noteEntryLabels(definition.notation);
|
|
353
|
+
}
|
|
354
|
+
for (const binding of bindingsByLesson.get(lesson.lessonId) ?? []) {
|
|
355
|
+
const definition = byId.get(binding.definitionId);
|
|
356
|
+
if (definition) noteEntryLabels(definition.notation);
|
|
357
|
+
}
|
|
358
|
+
const reported = /* @__PURE__ */ new Set();
|
|
359
|
+
for (const span of mathSpans(lesson.body)) {
|
|
360
|
+
for (const label of collectRomanLabels(span.latex)) {
|
|
361
|
+
if (allowlist.has(label.name)) continue;
|
|
362
|
+
if (explained.has(label.name)) continue;
|
|
363
|
+
if (reported.has(label.name)) continue;
|
|
364
|
+
reported.add(label.name);
|
|
365
|
+
if (input.lintIgnore.matches("roman-label-explained", label.name)) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
out.push({
|
|
369
|
+
code: "roman-label-explained",
|
|
370
|
+
severity: "warning",
|
|
371
|
+
lessonId: lesson.lessonId,
|
|
372
|
+
file: lesson.source.file,
|
|
373
|
+
glyph: label.name,
|
|
374
|
+
message: `${lesson.lessonId}: roman label ${JSON.stringify(`\\mathrm{${label.name}}`)} is neither on the course roman-label allowlist nor explained by a notation entry; give it a card or notation.local entry (latex ${JSON.stringify(`\\mathrm{${label.name}}`)}) introduced with [[key]], or add ${JSON.stringify(label.name)} to romanLabels.allow in course.config.ts`
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
335
380
|
const CHECKS = [
|
|
336
381
|
checkGlyphUniqueness,
|
|
337
382
|
checkNotationUnits,
|
|
@@ -342,7 +387,8 @@ const CHECKS = [
|
|
|
342
387
|
checkGlossWantsPromoting,
|
|
343
388
|
checkCardWantsDemoting,
|
|
344
389
|
checkGlossNameShape,
|
|
345
|
-
checkOperatorExplained
|
|
390
|
+
checkOperatorExplained,
|
|
391
|
+
checkRomanLabelExplained
|
|
346
392
|
];
|
|
347
393
|
function runConsistencyChecks(input) {
|
|
348
394
|
const diagnostics = [];
|
|
@@ -363,7 +409,8 @@ function consistencyCounts(diagnostics) {
|
|
|
363
409
|
"gloss-wants-promoting": 0,
|
|
364
410
|
"card-wants-demoting": 0,
|
|
365
411
|
"gloss-name-shape": 0,
|
|
366
|
-
"operator-explained": 0
|
|
412
|
+
"operator-explained": 0,
|
|
413
|
+
"roman-label-explained": 0
|
|
367
414
|
};
|
|
368
415
|
for (const diagnostic of diagnostics) counts[diagnostic.code] += 1;
|
|
369
416
|
return counts;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } from './keyed-float-validate.js';
|
|
2
|
+
|
|
3
|
+
/** `validateKeyedFloats` for `<Diagram>` — see `keyed-float-validate.ts`. */
|
|
4
|
+
|
|
5
|
+
type DiagramDiagnosticCode = 'dia-invalid-key' | 'dia-duplicate-key' | 'dia-ref-resolves' | 'dia-key-unused';
|
|
6
|
+
interface DiagramDiagnostic {
|
|
7
|
+
readonly code: DiagramDiagnosticCode;
|
|
8
|
+
readonly severity: 'error' | 'warning';
|
|
9
|
+
readonly message: string;
|
|
10
|
+
readonly file?: string;
|
|
11
|
+
readonly lessonId?: string;
|
|
12
|
+
readonly key?: string;
|
|
13
|
+
}
|
|
14
|
+
type DiagramLabelRecord = KeyedFloatLabelRecord;
|
|
15
|
+
type DiagramLessonInput = KeyedFloatLessonInput;
|
|
16
|
+
interface DiagramValidation extends KeyedFloatValidation {
|
|
17
|
+
readonly diagnostics: readonly DiagramDiagnostic[];
|
|
18
|
+
}
|
|
19
|
+
declare function validateDiagrams(lessons: readonly DiagramLessonInput[]): DiagramValidation;
|
|
20
|
+
|
|
21
|
+
export { type DiagramDiagnostic, type DiagramDiagnosticCode, type DiagramLabelRecord, type DiagramLessonInput, type DiagramValidation, validateDiagrams };
|
|
@@ -51,8 +51,15 @@ export function stripFencedCode(markdown: any): string;
|
|
|
51
51
|
* `keep`, when given, leaves a component of that name untouched instead of
|
|
52
52
|
* blanking it — `keyed-jsx-floats.mjs` uses this to blank every *other*
|
|
53
53
|
* component while still finding `<Table>`/`<Figure>` tags at the top level.
|
|
54
|
+
*
|
|
55
|
+
* `transparent`, when given, removes only those components' opening/closing
|
|
56
|
+
* tags and exposes their body to the later blanking pass. `<Memo>` and
|
|
57
|
+
* `<ShortNote>` are intentional uses: unlike a worked-example/presentation
|
|
58
|
+
* wrapper, their bodies are details in the lesson's reading order, so numbered
|
|
59
|
+
* equations/floats in them stay in the official sequence. Section headings in
|
|
60
|
+
* a transparent body do not change the surrounding page's section counter.
|
|
54
61
|
*/
|
|
55
|
-
export function stripJsxComponents(markdown: any, keep: any): string;
|
|
62
|
+
export function stripJsxComponents(markdown: any, keep: any, transparent?: any[]): string;
|
|
56
63
|
/**
|
|
57
64
|
* Every display equation in a Markdown body, keyed or not, in appearance order
|
|
58
65
|
* with its assigned number. `$$…$$` blocks only. With `numbering`
|
|
@@ -90,24 +97,12 @@ export function countDisplayEquations(markdown: any): number;
|
|
|
90
97
|
* `\label{eq:…}` payload when present, else `null`. `numbering` is threaded the
|
|
91
98
|
* same way as in `scanEquationLabels`.
|
|
92
99
|
*/
|
|
93
|
-
export function collectDisplayEquations(tree: any, numbering: any):
|
|
94
|
-
key: string | null;
|
|
95
|
-
number: string;
|
|
96
|
-
section: number;
|
|
97
|
-
indexInSection: number;
|
|
98
|
-
node: any;
|
|
99
|
-
}[];
|
|
100
|
+
export function collectDisplayEquations(tree: any, numbering: any): any[];
|
|
100
101
|
/**
|
|
101
102
|
* The keyed subset of `collectDisplayEquations` — the tree-walk counterpart of
|
|
102
103
|
* `scanEquationLabels`. Numbers reflect position among all display equations.
|
|
103
104
|
*/
|
|
104
|
-
export function collectEquationLabels(tree: any, numbering: any):
|
|
105
|
-
key: string | null;
|
|
106
|
-
number: string;
|
|
107
|
-
section: number;
|
|
108
|
-
indexInSection: number;
|
|
109
|
-
node: any;
|
|
110
|
-
}[];
|
|
105
|
+
export function collectEquationLabels(tree: any, numbering: any): any[];
|
|
111
106
|
/**
|
|
112
107
|
* Equation identity (Step D7, extended F3) — shared, framework-free helpers.
|
|
113
108
|
*
|
|
@@ -128,9 +128,36 @@ export function stripFencedCode(markdown) {
|
|
|
128
128
|
* `keep`, when given, leaves a component of that name untouched instead of
|
|
129
129
|
* blanking it — `keyed-jsx-floats.mjs` uses this to blank every *other*
|
|
130
130
|
* component while still finding `<Table>`/`<Figure>` tags at the top level.
|
|
131
|
+
*
|
|
132
|
+
* `transparent`, when given, removes only those components' opening/closing
|
|
133
|
+
* tags and exposes their body to the later blanking pass. `<Memo>` and
|
|
134
|
+
* `<ShortNote>` are intentional uses: unlike a worked-example/presentation
|
|
135
|
+
* wrapper, their bodies are details in the lesson's reading order, so numbered
|
|
136
|
+
* equations/floats in them stay in the official sequence. Section headings in
|
|
137
|
+
* a transparent body do not change the surrounding page's section counter.
|
|
131
138
|
*/
|
|
132
|
-
export function stripJsxComponents(markdown, keep) {
|
|
133
|
-
|
|
139
|
+
export function stripJsxComponents(markdown, keep, transparent = []) {
|
|
140
|
+
let source = String(markdown);
|
|
141
|
+
const attributes = String.raw`(?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}|[^>"'])*`;
|
|
142
|
+
for (const name of transparent) {
|
|
143
|
+
const component = new RegExp(
|
|
144
|
+
`<${name}\\b${attributes}>([\\s\\S]*?)<\\/${name}\\s*>`,
|
|
145
|
+
'g',
|
|
146
|
+
);
|
|
147
|
+
source = source.replace(component, (block, body) => {
|
|
148
|
+
const bodyStart = block.indexOf(body);
|
|
149
|
+
const visibleBody = body.replace(
|
|
150
|
+
/(^|\n)([ \t]{0,3})##(?=[ \t]+\S)/g,
|
|
151
|
+
(match) => match.replace(/#/g, ' '),
|
|
152
|
+
);
|
|
153
|
+
return (
|
|
154
|
+
block.slice(0, bodyStart).replace(/[^\n]/g, ' ') +
|
|
155
|
+
visibleBody +
|
|
156
|
+
block.slice(bodyStart + body.length).replace(/[^\n]/g, ' ')
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return source.replace(
|
|
134
161
|
/<([A-Z][A-Za-z0-9]*)(?:\s[^>]*)?>[\s\S]*?<\/\1>/g,
|
|
135
162
|
(block, name) => (name === keep ? block : block.replace(/[^\n]/g, ' ')),
|
|
136
163
|
);
|
|
@@ -149,7 +176,10 @@ export function stripJsxComponents(markdown, keep) {
|
|
|
149
176
|
*/
|
|
150
177
|
export function scanDisplayEquations(markdown, numbering) {
|
|
151
178
|
const { prefix, offset } = resolveNumbering(numbering);
|
|
152
|
-
const text = stripJsxComponents(stripFencedCode(markdown)
|
|
179
|
+
const text = stripJsxComponents(stripFencedCode(markdown), undefined, [
|
|
180
|
+
'Memo',
|
|
181
|
+
'ShortNote',
|
|
182
|
+
]);
|
|
153
183
|
const tokens = [];
|
|
154
184
|
const heading = /^[ \t]{0,3}##[ \t]+\S/gm;
|
|
155
185
|
const display = /\$\$([\s\S]+?)\$\$/g;
|
|
@@ -225,17 +255,13 @@ export function collectDisplayEquations(tree, numbering) {
|
|
|
225
255
|
let indexInSection = 0;
|
|
226
256
|
let indexInLesson = 0;
|
|
227
257
|
|
|
228
|
-
//
|
|
229
|
-
// `<CompactExample>` worked example, an `<Aside>`)
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (node
|
|
235
|
-
section += 1;
|
|
236
|
-
indexInSection = 0;
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
258
|
+
// Top-level blocks plus the bodies of `<Memo>` and `<ShortNote>`: other MDX
|
|
259
|
+
// components (a `<CompactExample>` worked example, an `<Aside>`) remain
|
|
260
|
+
// illustrative and outside the lesson's official equation sequence. These
|
|
261
|
+
// two detail components are deliberately transparent because they occupy an
|
|
262
|
+
// exact point in the reading order, even while visually collapsed.
|
|
263
|
+
const collectFromDetail = (node) => {
|
|
264
|
+
if (!node || typeof node !== 'object') return;
|
|
239
265
|
if (node.type === 'math') {
|
|
240
266
|
indexInSection += 1;
|
|
241
267
|
indexInLesson += 1;
|
|
@@ -254,6 +280,33 @@ export function collectDisplayEquations(tree, numbering) {
|
|
|
254
280
|
indexInSection,
|
|
255
281
|
node,
|
|
256
282
|
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (
|
|
286
|
+
node.type === 'mdxJsxFlowElement' &&
|
|
287
|
+
node.name !== 'Memo' &&
|
|
288
|
+
node.name !== 'ShortNote'
|
|
289
|
+
) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!Array.isArray(node.children)) return;
|
|
293
|
+
for (const child of node.children) collectFromDetail(child);
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const children = Array.isArray(tree?.children) ? tree.children : [];
|
|
297
|
+
for (const node of children) {
|
|
298
|
+
if (!node || typeof node !== 'object') continue;
|
|
299
|
+
if (node.type === 'heading' && node.depth === 2) {
|
|
300
|
+
section += 1;
|
|
301
|
+
indexInSection = 0;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (node.type === 'math') collectFromDetail(node);
|
|
305
|
+
else if (
|
|
306
|
+
node.type === 'mdxJsxFlowElement' &&
|
|
307
|
+
(node.name === 'Memo' || node.name === 'ShortNote')
|
|
308
|
+
) {
|
|
309
|
+
collectFromDetail(node);
|
|
257
310
|
}
|
|
258
311
|
}
|
|
259
312
|
return equations;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MathGateDiagnostic } from './gate-math.js';
|
|
2
|
+
import { NotationRegistryInput, AbbreviationDefinitionInput } from './types.js';
|
|
3
|
+
import { CourseAbbreviations } from '../compiler/course-config.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scan every lesson body for an abbreviation-shaped token outside `$…$` that
|
|
7
|
+
* is neither universal nor explained. Returns one `MathGateDiagnostic`
|
|
8
|
+
* (context `'abbreviation'`, always an error) per unresolved token, deduped
|
|
9
|
+
* per lesson so a term used many times on one page is reported once.
|
|
10
|
+
*/
|
|
11
|
+
declare function gateAbbreviations(input: {
|
|
12
|
+
readonly notationInput: NotationRegistryInput;
|
|
13
|
+
readonly abbreviations: readonly AbbreviationDefinitionInput[];
|
|
14
|
+
readonly courseAbbreviations?: CourseAbbreviations;
|
|
15
|
+
}): MathGateDiagnostic[];
|
|
16
|
+
|
|
17
|
+
export { gateAbbreviations };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { collectOperators, collectRomanLabels } from "./math-glyphs.mjs";
|
|
2
|
+
import { isBaseAbbreviation } from "./base-abbreviations.mjs";
|
|
3
|
+
import { pageScope } from "./gate-math.js";
|
|
4
|
+
import { lineOf, proseOnly } from "./gate-prose-notation.js";
|
|
5
|
+
const CAPS_RUN = /\b([A-Z]{2,})(?:'s|s)?\b/g;
|
|
6
|
+
const DOTTED_ABBR = /\b(?:[A-Za-z]\.){2,}/g;
|
|
7
|
+
function findAbbreviationLikeTokens(text) {
|
|
8
|
+
const out = [];
|
|
9
|
+
for (const match of text.matchAll(CAPS_RUN)) {
|
|
10
|
+
out.push({ token: match[1] ?? match[0], index: match.index ?? 0 });
|
|
11
|
+
}
|
|
12
|
+
for (const match of text.matchAll(DOTTED_ABBR)) {
|
|
13
|
+
out.push({ token: match[0], index: match.index ?? 0 });
|
|
14
|
+
}
|
|
15
|
+
return out.sort((a, b) => a.index - b.index);
|
|
16
|
+
}
|
|
17
|
+
const FIX = "Add it to an abbreviations-table entry (content/abbreviations/*.md), allowlist it in abbreviations.allow in course.config.ts if it is common, non-technical usage, or \u2014 if it names a math quantity \u2014 introduce it as \\mathrm{\u2026} (or \\operatorname{\u2026}) bound to a notation entry the reader can hover. See docs/authoring/notation.md#n14.";
|
|
18
|
+
function gateAbbreviations(input) {
|
|
19
|
+
const { notationInput, abbreviations, courseAbbreviations } = input;
|
|
20
|
+
const tableTokens = /* @__PURE__ */ new Set();
|
|
21
|
+
for (const entry of abbreviations) {
|
|
22
|
+
tableTokens.add(entry.abbr);
|
|
23
|
+
for (const alias of entry.aliases) tableTokens.add(alias);
|
|
24
|
+
}
|
|
25
|
+
const sharedByKey = new Map(
|
|
26
|
+
notationInput.sharedDefinitions.map((definition) => [
|
|
27
|
+
definition.key,
|
|
28
|
+
{ key: definition.key, notation: definition.notation }
|
|
29
|
+
])
|
|
30
|
+
);
|
|
31
|
+
const diagnostics = [];
|
|
32
|
+
for (const lesson of notationInput.lessons) {
|
|
33
|
+
const mathQuantityNames = /* @__PURE__ */ new Set();
|
|
34
|
+
for (const entry of pageScope(lesson, sharedByKey)) {
|
|
35
|
+
for (const label of collectRomanLabels(entry.notation)) {
|
|
36
|
+
mathQuantityNames.add(label.name);
|
|
37
|
+
}
|
|
38
|
+
for (const operator of collectOperators(entry.notation)) {
|
|
39
|
+
mathQuantityNames.add(operator.name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const text = proseOnly(lesson.body);
|
|
43
|
+
const reported = /* @__PURE__ */ new Set();
|
|
44
|
+
for (const { token, index } of findAbbreviationLikeTokens(text)) {
|
|
45
|
+
if (reported.has(token)) continue;
|
|
46
|
+
if (isBaseAbbreviation(token, courseAbbreviations)) continue;
|
|
47
|
+
if (tableTokens.has(token)) continue;
|
|
48
|
+
if (mathQuantityNames.has(token)) continue;
|
|
49
|
+
reported.add(token);
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
context: "abbreviation",
|
|
52
|
+
severity: "error",
|
|
53
|
+
file: lesson.source.file,
|
|
54
|
+
line: lineOf(lesson.body, index),
|
|
55
|
+
token,
|
|
56
|
+
message: `Unexplained abbreviation ${JSON.stringify(token)} outside math. ${FIX}`
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return diagnostics;
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
gateAbbreviations
|
|
64
|
+
};
|
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import { NotationRegistryInput } from './types.js';
|
|
1
|
+
import { NotationRegistryInput, NotationLessonInput } from './types.js';
|
|
2
2
|
|
|
3
3
|
interface MathGateDiagnostic {
|
|
4
|
-
readonly context: 'body' | 'formula' | 'assessment';
|
|
4
|
+
readonly context: 'body' | 'formula' | 'assessment' | 'prose' | 'abbreviation';
|
|
5
5
|
/**
|
|
6
6
|
* Body / component-slot math blocks the build (the render path needs every
|
|
7
7
|
* glyph resolved for the KaTeX trust callback). `formula` joined it at D15:
|
|
8
8
|
* a formula's glyphs are now rendered live and explorable, so an unresolved
|
|
9
9
|
* one is a hole the reader can see, and the corpus was brought to zero when
|
|
10
|
-
* the tier was promoted. `assessment`
|
|
11
|
-
*
|
|
10
|
+
* the tier was promoted. `assessment` joined the same way when
|
|
11
|
+
* `AssessmentRunner` started rendering `prompt` / `explanation` through
|
|
12
|
+
* KaTeX (`mixed-content.ts`) instead of showing them as raw text: an
|
|
13
|
+
* unresolved glyph is now a visible hole in the assessment UI, not an
|
|
14
|
+
* invisible warning, so the corpus was brought to zero and the tier
|
|
15
|
+
* promoted in the same change — see `CHANGELOG.md`. `prose` (N9,
|
|
16
|
+
* `gate-prose-notation.ts`) is the complementary direction — plain-text
|
|
17
|
+
* notation sitting *outside* `$…$` — and ships straight as an error: it has
|
|
18
|
+
* no partial corpus to absorb, since every prior instance was fixed in the
|
|
19
|
+
* same change that introduced the check. `abbreviation` (N14,
|
|
20
|
+
* `gate-abbreviations.ts`) is the same "ships straight as an error" story
|
|
21
|
+
* for a different shape of plain-text token — an unexplained acronym or
|
|
22
|
+
* dotted abbreviation, rather than a mis-typed quantity/unit.
|
|
12
23
|
*/
|
|
13
24
|
readonly severity: 'error' | 'warning';
|
|
14
25
|
readonly file: string;
|
|
@@ -29,6 +40,18 @@ interface LessonAssessmentLink {
|
|
|
29
40
|
readonly lessonId: string;
|
|
30
41
|
readonly assessmentIds: readonly string[];
|
|
31
42
|
}
|
|
43
|
+
type ScopeEntry = {
|
|
44
|
+
key: string;
|
|
45
|
+
notation: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A lesson's flat glyph table: its own `notation.local` entries plus every
|
|
49
|
+
* shared card it introduces with `[[key]]` / `\explain{key}`. Exported so a
|
|
50
|
+
* caller that needs the *same* scope for a different purpose — rendering an
|
|
51
|
+
* assessment's math (`mixed-content.ts`), say — doesn't reconstruct it by
|
|
52
|
+
* hand and risk drifting from what the completeness gate actually checks.
|
|
53
|
+
*/
|
|
54
|
+
declare function pageScope(lesson: NotationLessonInput, sharedByKey: ReadonlyMap<string, ScopeEntry>): ScopeEntry[];
|
|
32
55
|
/**
|
|
33
56
|
* Resolve every rendered-math context against the page glyph table. Returns one
|
|
34
57
|
* diagnostic per unresolved token; an empty array means the gate passed.
|
|
@@ -39,4 +62,4 @@ declare function gateContentMath(input: {
|
|
|
39
62
|
readonly lessonAssessments: readonly LessonAssessmentLink[];
|
|
40
63
|
}): MathGateDiagnostic[];
|
|
41
64
|
|
|
42
|
-
export { type MathGateDiagnostic, gateContentMath };
|
|
65
|
+
export { type MathGateDiagnostic, type ScopeEntry, gateContentMath, pageScope };
|
|
@@ -4,7 +4,9 @@ import { formulaGlyphScope, unknownFormulaScopeKeys } from "./gloss.js";
|
|
|
4
4
|
const SEVERITY = {
|
|
5
5
|
body: "error",
|
|
6
6
|
formula: "error",
|
|
7
|
-
assessment: "
|
|
7
|
+
assessment: "error",
|
|
8
|
+
prose: "error",
|
|
9
|
+
abbreviation: "error"
|
|
8
10
|
};
|
|
9
11
|
const TERM_KEY = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
|
|
10
12
|
const EXPLAIN_KEY = /\\explain\s*\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}/g;
|
|
@@ -218,5 +220,6 @@ function gateContentMath(input) {
|
|
|
218
220
|
return diagnostics;
|
|
219
221
|
}
|
|
220
222
|
export {
|
|
221
|
-
gateContentMath
|
|
223
|
+
gateContentMath,
|
|
224
|
+
pageScope
|
|
222
225
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { MathGateDiagnostic } from './gate-math.js';
|
|
2
|
+
import { NotationLessonInput } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The plain-text-notation gate (rule N9), run from the CLI alongside the
|
|
6
|
+
* completeness gate (`gate-math.ts`). Where that gate asks "does every glyph
|
|
7
|
+
* *inside* `$…$` resolve", this one asks the complementary question: "is
|
|
8
|
+
* anything that reads as math sitting *outside* `$…$` in the first place" —
|
|
9
|
+
* a unit abbreviation, a division of two variable-like symbols, an escaped
|
|
10
|
+
* absolute-value bar, a hand-typed sub/superscript. Both gates share one
|
|
11
|
+
* `MathGateDiagnostic` shape and one output array (`manifest.diagnostics.math`)
|
|
12
|
+
* so the CLI needs no separate wiring.
|
|
13
|
+
*
|
|
14
|
+
* Two entry points, one set of checks (`findingsIn`): `gateProseNotation`
|
|
15
|
+
* scans `lesson.body` with fenced/inline code and every `$…$` / `$$…$$` span
|
|
16
|
+
* already blanked out (same `stripCode` + math-span erasure `gate-math.ts`
|
|
17
|
+
* uses), so it only ever sees prose and table cells. `gateProseNotationInAssessments`
|
|
18
|
+
* scans an assessment item's `prompt` / `explanation` the same way — a plain
|
|
19
|
+
* JSON string field, not Markdown, but one that renders through the same
|
|
20
|
+
* KaTeX pipeline as a lesson body since `AssessmentRunner` picked it up
|
|
21
|
+
* (`mixed-content.ts`), so the same rule applies.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
interface AssessmentItemLike {
|
|
25
|
+
readonly id?: string;
|
|
26
|
+
readonly prompt?: string;
|
|
27
|
+
readonly explanation?: string;
|
|
28
|
+
}
|
|
29
|
+
interface AssessmentLike {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly items?: readonly AssessmentItemLike[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Blank code, math, reference, and JSX-tag spans so offsets don't shift.
|
|
35
|
+
* Exported so `gate-abbreviations.ts` (N14) scans exactly the same "is this
|
|
36
|
+
* really prose" surface as N9, instead of drifting with a second copy of
|
|
37
|
+
* these regexes.
|
|
38
|
+
*/
|
|
39
|
+
declare function proseOnly(body: string): string;
|
|
40
|
+
/** Exported for the same reason as {@link proseOnly}. */
|
|
41
|
+
declare function lineOf(text: string, index: number): number;
|
|
42
|
+
/**
|
|
43
|
+
* Scan every lesson body for plain-text math notation outside `$…$`. Returns
|
|
44
|
+
* one `MathGateDiagnostic` (context `'prose'`, always an error — N9 has no
|
|
45
|
+
* escape hatch) per finding.
|
|
46
|
+
*/
|
|
47
|
+
declare function gateProseNotation(lessons: readonly NotationLessonInput[]): MathGateDiagnostic[];
|
|
48
|
+
/**
|
|
49
|
+
* Scan every assessment `prompt` / `explanation` for the same plain-text
|
|
50
|
+
* notation N9 forbids in a lesson body — `AssessmentRunner` renders both
|
|
51
|
+
* through the same KaTeX pipeline now (`mixed-content.ts`), so text sitting
|
|
52
|
+
* outside `$…$` there is just as invisible to the reader's hover cards as it
|
|
53
|
+
* would be in a lesson. No line number: an assessment item is one JSON string
|
|
54
|
+
* field, not a multi-line source file, so `file` names the item instead —
|
|
55
|
+
* same `"<assessmentId> (<itemId>)"` shape `gate-math.ts` already uses for
|
|
56
|
+
* this context.
|
|
57
|
+
*/
|
|
58
|
+
declare function gateProseNotationInAssessments(assessments: readonly AssessmentLike[]): MathGateDiagnostic[];
|
|
59
|
+
|
|
60
|
+
export { gateProseNotation, gateProseNotationInAssessments, lineOf, proseOnly };
|