explico 0.7.0 → 0.11.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 +1409 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +54 -3
- package/dist/compiler/cli.js +80 -6
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +84 -8
- package/dist/compiler/course-config.d.ts +3 -194
- package/dist/compiler/course-config.js +78 -5
- package/dist/compiler/exports/standalone-html.d.ts +6 -1
- package/dist/compiler/knowledge-version.js +1 -1
- package/dist/compiler/lesson-derivation.d.ts +27 -10
- package/dist/compiler/lesson-derivation.js +23 -8
- package/dist/compiler/logging.d.ts +14 -1
- package/dist/compiler/logging.js +6 -2
- package/dist/compiler/manifest.d.ts +204 -31
- package/dist/compiler/manifest.js +270 -55
- package/dist/compiler/resolution-report.d.ts +6 -1
- package/dist/course-config-Dsnri9Tk.d.ts +464 -0
- package/dist/reference/base-accents.d.mts +26 -0
- package/dist/reference/base-accents.mjs +31 -0
- package/dist/reference/base-roman-labels.d.mts +2 -2
- package/dist/reference/base-roman-labels.mjs +2 -2
- package/dist/reference/consistency.d.ts +14 -2
- package/dist/reference/consistency.js +108 -4
- package/dist/reference/delimiter-size.d.ts +21 -11
- package/dist/reference/delimiter-size.js +15 -39
- package/dist/reference/diagrams-validate.d.ts +7 -2
- package/dist/reference/diagrams-validate.js +9 -1
- package/dist/reference/equations-validate.d.ts +8 -5
- package/dist/reference/equations-validate.js +1 -0
- package/dist/reference/equations.d.mts +69 -34
- package/dist/reference/equations.mjs +90 -44
- package/dist/reference/examples-validate.d.ts +31 -0
- package/dist/reference/examples-validate.js +48 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/figures-validate.js +4 -1
- package/dist/reference/folds-validate.d.ts +33 -0
- package/dist/reference/folds-validate.js +32 -0
- package/dist/reference/gate-abbreviations.d.ts +3 -1
- package/dist/reference/gate-abbreviations.js +3 -2
- package/dist/reference/gate-course-rules.d.ts +32 -0
- package/dist/reference/gate-course-rules.js +85 -0
- package/dist/reference/gate-heading-length.d.ts +5 -2
- package/dist/reference/gate-heading-length.js +47 -23
- package/dist/reference/gate-heading-structure.d.ts +41 -0
- package/dist/reference/gate-heading-structure.js +113 -0
- package/dist/reference/gate-math-punctuation.d.ts +71 -0
- package/dist/reference/gate-math-punctuation.js +251 -0
- package/dist/reference/gate-math.d.ts +44 -6
- package/dist/reference/gate-math.js +63 -10
- package/dist/reference/gate-part-pages.d.ts +50 -0
- package/dist/reference/gate-part-pages.js +118 -0
- package/dist/reference/gate-prose-notation.d.ts +32 -11
- package/dist/reference/gate-prose-notation.js +30 -8
- package/dist/reference/katex-delimiters.d.mts +15 -0
- package/dist/reference/katex-delimiters.mjs +138 -0
- package/dist/reference/katex-options.mjs +4 -0
- package/dist/reference/keyed-float-validate.d.ts +27 -10
- package/dist/reference/keyed-float-validate.js +15 -1
- package/dist/reference/keyed-jsx-floats.d.mts +11 -5
- package/dist/reference/keyed-jsx-floats.mjs +73 -20
- package/dist/reference/knowledge-checks-validate.d.ts +1 -1
- package/dist/reference/knowledge-checks.d.mts +3 -1
- package/dist/reference/knowledge-checks.mjs +9 -4
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-slot.d.ts +34 -0
- package/dist/reference/lab-math-slot.js +6 -0
- package/dist/reference/math-glyphs.d.mts +80 -4
- package/dist/reference/math-glyphs.mjs +588 -39
- package/dist/reference/page-notation.d.ts +6 -1
- package/dist/reference/raw-floats-validate.js +1 -1
- package/dist/reference/raw-floats.mjs +1 -1
- package/dist/reference/rehype-heading-anchors.d.mts +14 -0
- package/dist/reference/rehype-heading-anchors.mjs +116 -17
- package/dist/reference/rehype-table-header-labels.d.mts +6 -1
- package/dist/reference/rehype-table-header-labels.mjs +21 -3
- package/dist/reference/remark-asides.d.mts +3 -0
- package/dist/reference/remark-asides.mjs +180 -0
- package/dist/reference/remark-memo.d.mts +3 -1
- package/dist/reference/remark-memo.mjs +42 -11
- package/dist/reference/remark-notation.d.mts +60 -10
- package/dist/reference/remark-notation.mjs +387 -36
- package/dist/reference/render-lab-math.d.ts +4 -26
- package/dist/reference/render-lab-math.js +5 -5
- package/dist/reference/sections-validate.d.ts +27 -0
- package/dist/reference/sections-validate.js +41 -0
- package/dist/reference/sections.d.mts +97 -0
- package/dist/reference/sections.mjs +306 -0
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +24 -1
- package/dist/reference/unit-words.d.ts +37 -0
- package/dist/reference/unit-words.js +170 -0
- package/dist/rules/catalog.d.ts +176 -0
- package/dist/rules/catalog.js +983 -0
- package/dist/rules/course-rules.d.ts +115 -0
- package/dist/rules/course-rules.js +292 -0
- package/dist/rules/course-rules.schema.json +138 -0
- package/dist/rules/in-effect.d.ts +53 -0
- package/dist/rules/in-effect.js +142 -0
- package/dist/rules/rules.schema.json +1072 -0
- package/dist/rules/settings.d.ts +3 -0
- package/dist/rules/settings.js +290 -0
- package/dist/rules/waivers.d.ts +53 -0
- package/dist/rules/waivers.js +132 -0
- package/dist/session/PreferenceStore.d.ts +5 -0
- package/dist/session/PreferenceStore.js +9 -0
- package/docs/VERSIONING.md +2 -2
- package/docs/authoring/README.md +232 -40
- package/docs/authoring/agents.md +55 -9
- package/docs/authoring/components.md +178 -22
- package/docs/authoring/migrating.md +1324 -2
- package/docs/authoring/notation.md +616 -344
- package/docs/authoring/prose.md +551 -0
- package/docs/authoring/retired.md +35 -0
- package/docs/authoring/structure-rules.md +422 -40
- package/docs/authoring/structure.md +445 -62
- package/docs/authoring/versioning.md +20 -28
- package/docs/authoring/visualizations.md +142 -20
- package/docs/katex-bracket-scaling.md +191 -0
- package/package.json +3 -2
- package/src/compiler/cli.ts +139 -6
- package/src/compiler/collections.ts +144 -10
- package/src/compiler/course-config.ts +273 -34
- package/src/compiler/knowledge-version.ts +1 -1
- package/src/compiler/lesson-derivation.ts +54 -15
- package/src/compiler/logging.ts +21 -4
- package/src/compiler/manifest.ts +573 -104
- package/src/components/BlockHeading.astro +113 -0
- package/src/components/CourseContents.astro +221 -0
- package/src/components/CurriculumMap.astro +5 -6
- package/src/components/FloatIndex.astro +200 -0
- package/src/components/HoverPanel.astro +431 -79
- package/src/components/PartBridge.astro +27 -0
- package/src/components/PartOverview.astro +401 -0
- package/src/components/PartScope.astro +27 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
- package/src/components/assessments/AssessmentRunner.tsx +8 -5
- package/src/components/assessments/AssessmentSet.astro +23 -26
- package/src/components/block-heading.ts +146 -0
- package/src/components/card-arrow.ts +243 -0
- package/src/components/card-drag.ts +631 -0
- package/src/components/card-jump.ts +45 -0
- package/src/components/card-manager.ts +1589 -0
- package/src/components/card-memory.ts +177 -0
- package/src/components/card-placement.ts +504 -0
- package/src/components/card-side.ts +336 -0
- package/src/components/card-travel.ts +197 -0
- package/src/components/citation/CitationLayer.astro +77 -76
- package/src/components/diagram/Diagram.astro +161 -316
- package/src/components/disclosure/course-disclosure.ts +24 -0
- package/src/components/disclosure/hover-preview.ts +122 -42
- package/src/components/disclosure/persisted-disclosure.ts +3 -3
- package/src/components/disclosure/print-as-shown.ts +252 -0
- package/src/components/examples/CompactExample.astro +3 -1
- package/src/components/examples/CompactExamples.astro +101 -94
- package/src/components/figure/Figure.astro +174 -274
- package/src/components/figure/FloatCaption.astro +87 -0
- package/src/components/figure/figure-state.ts +58 -0
- package/src/components/figure/figure-tools.ts +87 -0
- package/src/components/float-name.ts +101 -0
- package/src/components/fold/Fold.astro +125 -0
- package/src/components/fold/fold-id.ts +40 -0
- package/src/components/memo/Memo.astro +162 -231
- package/src/components/memo/ShortNote.astro +6 -6
- package/src/components/notation/NotationFormula.astro +5 -1
- package/src/components/notation/NotationGlossary.astro +24 -14
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/notation/render-notation-math.ts +111 -10
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +22 -6
- package/src/components/starlight/CollapsibleSections.astro +207 -93
- package/src/components/starlight/DiagramEnhancer.astro +15 -138
- package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
- package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
- package/src/components/starlight/LayoutHeader.astro +298 -19
- package/src/components/starlight/LayoutPageSidebar.astro +99 -12
- package/src/components/starlight/LayoutPageTitle.astro +120 -0
- package/src/components/starlight/LayoutSidebar.astro +229 -11
- package/src/components/starlight/LessonExportActions.astro +53 -72
- package/src/components/starlight/LessonFooter.astro +26 -4
- package/src/components/starlight/section-parts.ts +15 -2
- package/src/components/starlight/section-summary.ts +181 -0
- package/src/components/table/Table.astro +95 -76
- package/src/components/table/TableOverflowExpander.astro +65 -28
- package/src/content-config.ts +40 -4
- package/src/reference/base-accents.mjs +31 -0
- package/src/reference/base-roman-labels.mjs +2 -2
- package/src/reference/consistency.ts +176 -5
- package/src/reference/delimiter-size.ts +40 -61
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +90 -44
- package/src/reference/examples-validate.ts +97 -0
- package/src/reference/figures-validate.ts +6 -1
- package/src/reference/folds-validate.ts +65 -0
- package/src/reference/gate-abbreviations.ts +3 -2
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +68 -37
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +119 -12
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +74 -13
- package/src/reference/katex-delimiters.mjs +138 -0
- package/src/reference/katex-options.mjs +4 -0
- package/src/reference/keyed-float-validate.ts +45 -11
- package/src/reference/keyed-jsx-floats.mjs +73 -20
- package/src/reference/knowledge-checks-validate.ts +1 -1
- package/src/reference/knowledge-checks.mjs +9 -4
- package/src/reference/lab-math-slot.ts +36 -0
- package/src/reference/math-glyphs.mjs +588 -39
- package/src/reference/raw-floats-validate.ts +1 -1
- package/src/reference/raw-floats.mjs +1 -1
- package/src/reference/rehype-heading-anchors.mjs +116 -17
- package/src/reference/rehype-table-header-labels.mjs +21 -3
- package/src/reference/remark-asides.mjs +180 -0
- package/src/reference/remark-memo.mjs +42 -11
- package/src/reference/remark-notation.mjs +387 -36
- package/src/reference/render-lab-math.ts +17 -32
- package/src/reference/sections-validate.ts +81 -0
- package/src/reference/sections.mjs +306 -0
- package/src/reference/tables-validate.ts +2 -1
- package/src/reference/types.ts +24 -0
- package/src/reference/unit-words.ts +233 -0
- package/src/rules/catalog.ts +1298 -0
- package/src/rules/course-rules.ts +456 -0
- package/src/rules/in-effect.ts +235 -0
- package/src/rules/settings.ts +448 -0
- package/src/rules/waivers.ts +249 -0
- package/src/session/PreferenceStore.ts +15 -0
- package/src/styles/global.css +1471 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- package/src/components/popup-stack.ts +0 -345
package/src/compiler/cli.ts
CHANGED
|
@@ -49,8 +49,16 @@ import remarkNotation, {
|
|
|
49
49
|
} from '../reference/remark-notation.mjs';
|
|
50
50
|
import remarkShortNote from '../reference/remark-short-note.mjs';
|
|
51
51
|
import type { DiagnosticSeverity } from '../reference/types';
|
|
52
|
+
import type { EquationLink } from '../reference/gate-math';
|
|
52
53
|
import type { Manifest, ManifestLesson } from './manifest';
|
|
53
54
|
import { isLessonSlug, lessonIdFromSlug } from './lesson-derivation';
|
|
55
|
+
import { isCourseRuleId, ownerOf, ELEMENTS, type Element, type Enforcement } from '../rules/catalog';
|
|
56
|
+
import {
|
|
57
|
+
renderRulesInEffect,
|
|
58
|
+
rulesInEffect,
|
|
59
|
+
type RuleInEffect,
|
|
60
|
+
type RulesFilter,
|
|
61
|
+
} from '../rules/in-effect';
|
|
54
62
|
|
|
55
63
|
const REPOSITORY_ROOT = process.cwd();
|
|
56
64
|
// Content and its course math are the top-level `content/` folder (Phase F).
|
|
@@ -85,17 +93,45 @@ export class ContentCliError extends Error {
|
|
|
85
93
|
/** One normalized diagnostic. `code` is `<source>:<kind>` and always stable. */
|
|
86
94
|
export interface CliDiagnostic {
|
|
87
95
|
readonly code: string;
|
|
96
|
+
/**
|
|
97
|
+
* The catalog entry that owns the code — a rule ID (`N9`) or an invariant
|
|
98
|
+
* slug (`keyed-floats`). Absent only for a code the catalog does not know,
|
|
99
|
+
* which `tests/rules/catalog.test.ts` treats as a bug.
|
|
100
|
+
*/
|
|
101
|
+
readonly rule?: string;
|
|
88
102
|
readonly severity: DiagnosticSeverity;
|
|
89
103
|
readonly message: string;
|
|
90
104
|
readonly file?: string;
|
|
91
105
|
readonly line?: number;
|
|
92
106
|
readonly column?: number;
|
|
107
|
+
/**
|
|
108
|
+
* The keyed equation the finding sits in, when the math carries a
|
|
109
|
+
* `\label{eq:…}` — `{ key, href }`, the href being the anchor on the
|
|
110
|
+
* rendered page. A size-only finding inside a long `aligned` block is
|
|
111
|
+
* otherwise anonymous.
|
|
112
|
+
*/
|
|
113
|
+
readonly equation?: EquationLink;
|
|
93
114
|
}
|
|
94
115
|
|
|
95
116
|
function diagnosticSeverity(value: unknown): DiagnosticSeverity {
|
|
96
117
|
return value === 'info' || value === 'warning' ? value : 'error';
|
|
97
118
|
}
|
|
98
119
|
|
|
120
|
+
/**
|
|
121
|
+
* `{ rule }` for a `<source>:<code>` (or bare) code the catalog owns — or for a
|
|
122
|
+
* course rule, whose code is its own ID (`X1`).
|
|
123
|
+
*/
|
|
124
|
+
function ownerFields(code: string): { readonly rule?: string } {
|
|
125
|
+
const bare = code.slice(code.indexOf(':') + 1);
|
|
126
|
+
const owner = ownerOf(bare);
|
|
127
|
+
if (owner !== undefined) return { rule: owner.id };
|
|
128
|
+
return isCourseRuleId(bare) ? { rule: bare } : {};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function withOwner(diagnostic: CliDiagnostic): CliDiagnostic {
|
|
132
|
+
return { ...diagnostic, ...ownerFields(diagnostic.code) };
|
|
133
|
+
}
|
|
134
|
+
|
|
99
135
|
function mentions(message: string, lessonId: string, file: string): boolean {
|
|
100
136
|
return (
|
|
101
137
|
message.includes(lessonId) ||
|
|
@@ -159,12 +195,15 @@ export function lessonDiagnostics(
|
|
|
159
195
|
));
|
|
160
196
|
if (!forLesson) continue;
|
|
161
197
|
out.push({
|
|
162
|
-
code: `math:${diagnostic.code
|
|
198
|
+
code: `math:${diagnostic.code}`,
|
|
163
199
|
severity: diagnostic.severity,
|
|
164
200
|
message: diagnostic.message,
|
|
165
201
|
file: diagnostic.file,
|
|
166
202
|
...(diagnostic.line === undefined ? {} : { line: diagnostic.line }),
|
|
167
203
|
...(diagnostic.column === undefined ? {} : { column: diagnostic.column }),
|
|
204
|
+
...(diagnostic.equation === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: { equation: diagnostic.equation }),
|
|
168
207
|
});
|
|
169
208
|
}
|
|
170
209
|
|
|
@@ -189,6 +228,8 @@ export function lessonDiagnostics(
|
|
|
189
228
|
...manifest.diagnostics.figures,
|
|
190
229
|
...manifest.diagnostics.diagrams,
|
|
191
230
|
...manifest.diagnostics.checks,
|
|
231
|
+
...manifest.diagnostics.examples,
|
|
232
|
+
...manifest.diagnostics.folds,
|
|
192
233
|
...manifest.diagnostics.rawFloats,
|
|
193
234
|
]) {
|
|
194
235
|
if (
|
|
@@ -216,10 +257,12 @@ export function lessonDiagnostics(
|
|
|
216
257
|
});
|
|
217
258
|
}
|
|
218
259
|
|
|
219
|
-
return out
|
|
220
|
-
(
|
|
221
|
-
|
|
222
|
-
|
|
260
|
+
return out
|
|
261
|
+
.map(withOwner)
|
|
262
|
+
.sort(
|
|
263
|
+
(a, b) =>
|
|
264
|
+
a.code.localeCompare(b.code) || a.message.localeCompare(b.message),
|
|
265
|
+
);
|
|
223
266
|
}
|
|
224
267
|
|
|
225
268
|
// --- renderability -------------------------------------------------------
|
|
@@ -501,6 +544,7 @@ export interface StatusReport {
|
|
|
501
544
|
readonly keyedTables: number;
|
|
502
545
|
readonly keyedFigures: number;
|
|
503
546
|
readonly keyedDiagrams: number;
|
|
547
|
+
readonly keyedExamples: number;
|
|
504
548
|
};
|
|
505
549
|
readonly draftDebt: {
|
|
506
550
|
readonly lessons: readonly string[];
|
|
@@ -528,6 +572,7 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
528
572
|
.filter((diagnostic) => diagnostic.code === 'unused-definition')
|
|
529
573
|
.map((diagnostic) => ({
|
|
530
574
|
code: `notation:${diagnostic.code}`,
|
|
575
|
+
...ownerFields(diagnostic.code),
|
|
531
576
|
severity: diagnosticSeverity(diagnostic.severity),
|
|
532
577
|
message: diagnostic.message,
|
|
533
578
|
...(diagnostic.source?.file === undefined
|
|
@@ -546,9 +591,13 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
546
591
|
...manifest.diagnostics.figures.map((d) => d.severity),
|
|
547
592
|
...manifest.diagnostics.diagrams.map((d) => d.severity),
|
|
548
593
|
...manifest.diagnostics.checks.map((d) => d.severity),
|
|
594
|
+
...manifest.diagnostics.examples.map((d) => d.severity),
|
|
595
|
+
...manifest.diagnostics.folds.map((d) => d.severity),
|
|
549
596
|
...manifest.diagnostics.rawFloats.map((d) => d.severity),
|
|
550
597
|
...manifest.diagnostics.consistency.map((d) => d.severity),
|
|
551
598
|
...manifest.diagnostics.knowledge.map((d) => d.severity),
|
|
599
|
+
...manifest.diagnostics.config.map((d) => d.severity),
|
|
600
|
+
...manifest.diagnostics.waivers.map((d) => d.severity),
|
|
552
601
|
];
|
|
553
602
|
|
|
554
603
|
const keyedEquations = manifest.lessons.reduce(
|
|
@@ -567,6 +616,10 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
567
616
|
(count, lesson) => count + lesson.diagrams.length,
|
|
568
617
|
0,
|
|
569
618
|
);
|
|
619
|
+
const keyedExamples = manifest.lessons.reduce(
|
|
620
|
+
(count, lesson) => count + lesson.examples.length,
|
|
621
|
+
0,
|
|
622
|
+
);
|
|
570
623
|
const assessments = new Set(
|
|
571
624
|
manifest.lessons.flatMap((lesson) => lesson.assessments),
|
|
572
625
|
).size;
|
|
@@ -584,6 +637,7 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
584
637
|
keyedTables,
|
|
585
638
|
keyedFigures,
|
|
586
639
|
keyedDiagrams,
|
|
640
|
+
keyedExamples,
|
|
587
641
|
},
|
|
588
642
|
draftDebt: { lessons: draftLessons, notation: draftNotation },
|
|
589
643
|
orphans,
|
|
@@ -621,8 +675,18 @@ type DiagnosticRecord = Record<string, unknown> & {
|
|
|
621
675
|
readonly line?: unknown;
|
|
622
676
|
readonly column?: unknown;
|
|
623
677
|
readonly source?: unknown;
|
|
678
|
+
readonly equation?: unknown;
|
|
624
679
|
};
|
|
625
680
|
|
|
681
|
+
/** `{ key, href }` as it survives the untyped manifest walk above. */
|
|
682
|
+
function equationLink(value: unknown): EquationLink | undefined {
|
|
683
|
+
if (value === null || typeof value !== 'object') return undefined;
|
|
684
|
+
const { key, href } = value as Record<string, unknown>;
|
|
685
|
+
return typeof key === 'string' && typeof href === 'string'
|
|
686
|
+
? { key, href }
|
|
687
|
+
: undefined;
|
|
688
|
+
}
|
|
689
|
+
|
|
626
690
|
function numericLocation(value: unknown): number | undefined {
|
|
627
691
|
return typeof value === 'number' && Number.isInteger(value) && value > 0
|
|
628
692
|
? value
|
|
@@ -685,13 +749,16 @@ export function contentDiagnostics(
|
|
|
685
749
|
: typeof diagnostic.kind === 'string'
|
|
686
750
|
? diagnostic.kind
|
|
687
751
|
: 'diagnostic';
|
|
752
|
+
const equation = equationLink(diagnostic.equation);
|
|
688
753
|
diagnostics.push({
|
|
689
754
|
code: `${group}:${detail}`,
|
|
755
|
+
...ownerFields(detail),
|
|
690
756
|
severity,
|
|
691
757
|
message: String(diagnostic.message ?? detail),
|
|
692
758
|
...(file === undefined ? {} : { file }),
|
|
693
759
|
...(line === undefined ? {} : { line }),
|
|
694
760
|
...(column === undefined ? {} : { column }),
|
|
761
|
+
...(equation === undefined ? {} : { equation }),
|
|
695
762
|
});
|
|
696
763
|
}
|
|
697
764
|
}
|
|
@@ -719,6 +786,61 @@ export function contentWarnings(manifest: Manifest): WarningsReport {
|
|
|
719
786
|
return { count: warnings.length, warnings };
|
|
720
787
|
}
|
|
721
788
|
|
|
789
|
+
// --- content rules -------------------------------------------------------
|
|
790
|
+
|
|
791
|
+
export interface RulesReport {
|
|
792
|
+
readonly filter: RulesFilter;
|
|
793
|
+
readonly count: number;
|
|
794
|
+
readonly rules: readonly RuleInEffect[];
|
|
795
|
+
/** The same selection as the page `validate:content` writes. */
|
|
796
|
+
readonly markdown: string;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/**
|
|
800
|
+
* Parse `--element a,b` / `--enforcement script` into a filter, throwing a
|
|
801
|
+
* usage error on a value outside the vocabulary.
|
|
802
|
+
*/
|
|
803
|
+
export function parseRulesFilter(input: {
|
|
804
|
+
readonly element?: string;
|
|
805
|
+
readonly enforcement?: string;
|
|
806
|
+
}): RulesFilter {
|
|
807
|
+
const filter: { elements?: Element[]; enforcement?: Enforcement | 'locked' } = {};
|
|
808
|
+
if (input.element !== undefined) {
|
|
809
|
+
const elements = input.element.split(',').map((e) => e.trim()).filter(Boolean);
|
|
810
|
+
for (const element of elements) {
|
|
811
|
+
if (!(ELEMENTS as readonly string[]).includes(element)) {
|
|
812
|
+
throw new ContentCliError(
|
|
813
|
+
'usage',
|
|
814
|
+
`--element ${element}: not an element. One of: ${ELEMENTS.join(', ')}.`,
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
filter.elements = elements as Element[];
|
|
819
|
+
}
|
|
820
|
+
if (input.enforcement !== undefined) {
|
|
821
|
+
const kinds = ['script', 'partial', 'guide', 'locked'];
|
|
822
|
+
if (!kinds.includes(input.enforcement)) {
|
|
823
|
+
throw new ContentCliError(
|
|
824
|
+
'usage',
|
|
825
|
+
`--enforcement ${input.enforcement}: one of ${kinds.join(', ')}.`,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
filter.enforcement = input.enforcement as Enforcement | 'locked';
|
|
829
|
+
}
|
|
830
|
+
return filter;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/** The rules this course has in effect, optionally narrowed. */
|
|
834
|
+
export function contentRules(manifest: Manifest, filter: RulesFilter = {}): RulesReport {
|
|
835
|
+
const rules = rulesInEffect(manifest, filter);
|
|
836
|
+
return {
|
|
837
|
+
filter,
|
|
838
|
+
count: rules.length,
|
|
839
|
+
rules,
|
|
840
|
+
markdown: renderRulesInEffect(manifest, filter),
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
722
844
|
// --- content context ---------------------------------------------------
|
|
723
845
|
|
|
724
846
|
export interface ContextReport {
|
|
@@ -749,7 +871,16 @@ export interface ContextReport {
|
|
|
749
871
|
readonly tables: readonly { key: string; number: string }[];
|
|
750
872
|
readonly figures: readonly { key: string; number: string }[];
|
|
751
873
|
readonly diagrams: readonly { key: string; number: string }[];
|
|
752
|
-
|
|
874
|
+
/** Worked-example sets: `short` is the title `[[ex-key]]` names. */
|
|
875
|
+
readonly examples: readonly { key: string; number: string; short?: string }[];
|
|
876
|
+
readonly knowledgeChecks: readonly { key: string; number: string; title?: string }[];
|
|
877
|
+
/** The top-level headings: `slug` is what `[[sec-slug]]` names. */
|
|
878
|
+
readonly sections: readonly {
|
|
879
|
+
slug: string;
|
|
880
|
+
text: string;
|
|
881
|
+
level: number;
|
|
882
|
+
number?: string;
|
|
883
|
+
}[];
|
|
753
884
|
readonly sources: readonly {
|
|
754
885
|
id: string;
|
|
755
886
|
title: string;
|
|
@@ -821,7 +952,9 @@ export function contentContext(
|
|
|
821
952
|
tables: lesson.tables.map((table) => ({ ...table })),
|
|
822
953
|
figures: lesson.figures.map((figure) => ({ ...figure })),
|
|
823
954
|
diagrams: lesson.diagrams.map((diagram) => ({ ...diagram })),
|
|
955
|
+
examples: lesson.examples.map((example) => ({ ...example })),
|
|
824
956
|
knowledgeChecks: lesson.knowledgeChecks.map((check) => ({ ...check })),
|
|
957
|
+
sections: lesson.headings.map((heading) => ({ ...heading })),
|
|
825
958
|
sources: lesson.sources.map((id) => {
|
|
826
959
|
const record = sourceById.get(id) as Record<string, unknown> | undefined;
|
|
827
960
|
return {
|
|
@@ -31,15 +31,21 @@ import type {
|
|
|
31
31
|
NotationLessonInput,
|
|
32
32
|
NotationReferenceInput,
|
|
33
33
|
NotationRegistryInput,
|
|
34
|
+
NotationSuspension,
|
|
34
35
|
SharedNotationDefinitionInput,
|
|
35
36
|
SourceSpan,
|
|
36
37
|
NotationGlossEntry,
|
|
37
38
|
} from '../reference/types';
|
|
38
39
|
import { isSubstantiveMeaning } from '../reference/prose';
|
|
39
40
|
import { parseEquationRef } from '../reference/equations.mjs';
|
|
41
|
+
import {
|
|
42
|
+
DECORATION_NAMES,
|
|
43
|
+
decorationOfEntry,
|
|
44
|
+
} from '../reference/math-glyphs.mjs';
|
|
40
45
|
import { parseKeyedFloatRef } from '../reference/keyed-jsx-floats.mjs';
|
|
41
46
|
import { parseKnowledgeCheckRef } from '../reference/knowledge-checks.mjs';
|
|
42
47
|
import { parseMemoRef } from '../reference/remark-memo.mjs';
|
|
48
|
+
import { parseSectionRef } from '../reference/sections.mjs';
|
|
43
49
|
import { resolveLabel } from '../reference/label';
|
|
44
50
|
import { resolveGlosses } from '../reference/gloss';
|
|
45
51
|
import {
|
|
@@ -80,6 +86,32 @@ interface ParsedDocument {
|
|
|
80
86
|
readonly data: Record<string, unknown>;
|
|
81
87
|
/** Markdown body, byte-identical to the previous `gray-matter` output. */
|
|
82
88
|
readonly body: string;
|
|
89
|
+
/**
|
|
90
|
+
* 1-based line *in the file* where {@link body} starts — the frontmatter
|
|
91
|
+
* block's height plus one. Every gate scans the body, so a finding's index
|
|
92
|
+
* is body-relative while the `file` it is reported against is not; without
|
|
93
|
+
* this offset a `file:line:column` diagnostic points at the wrong line (by
|
|
94
|
+
* the height of the frontmatter) and no editor can jump to it.
|
|
95
|
+
*/
|
|
96
|
+
readonly bodyLine: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The line `body` starts on in `raw`. `parseFrontmatter` returns the body as a
|
|
101
|
+
* suffix of the file, so the offset is what precedes it; the `indexOf` branch
|
|
102
|
+
* is a guard for a future parser that normalizes the text rather than slicing.
|
|
103
|
+
*/
|
|
104
|
+
function bodyStartLine(raw: string, body: string): number {
|
|
105
|
+
const offset = raw.endsWith(body)
|
|
106
|
+
? raw.length - body.length
|
|
107
|
+
: raw.indexOf(body);
|
|
108
|
+
if (offset <= 0) return 1;
|
|
109
|
+
|
|
110
|
+
let line = 1;
|
|
111
|
+
for (let index = 0; index < offset; index += 1) {
|
|
112
|
+
if (raw.charCodeAt(index) === 10) line += 1;
|
|
113
|
+
}
|
|
114
|
+
return line;
|
|
83
115
|
}
|
|
84
116
|
|
|
85
117
|
/**
|
|
@@ -87,11 +119,14 @@ interface ParsedDocument {
|
|
|
87
119
|
* did not; strip it so body offsets in diagnostics are unchanged.
|
|
88
120
|
*/
|
|
89
121
|
function parseDocument(path: string): ParsedDocument {
|
|
90
|
-
const
|
|
122
|
+
const raw = readFileSync(path, 'utf8');
|
|
123
|
+
const parsed = parseFrontmatter(raw);
|
|
124
|
+
const body = parsed.content.replace(/^\n/, '');
|
|
91
125
|
return {
|
|
92
126
|
file: relative(REPOSITORY_ROOT, path),
|
|
93
127
|
data: (parsed.frontmatter ?? {}) as Record<string, unknown>,
|
|
94
|
-
body
|
|
128
|
+
body,
|
|
129
|
+
bodyLine: bodyStartLine(raw, body),
|
|
95
130
|
};
|
|
96
131
|
}
|
|
97
132
|
|
|
@@ -234,16 +269,19 @@ export function extractNotationReferences(
|
|
|
234
269
|
for (const match of searchable.matchAll(pattern)) {
|
|
235
270
|
const key = (match[1] ?? '').trim();
|
|
236
271
|
// `[[eq-key]]` / `[[slug#eq-key]]` are equation references (D7), and
|
|
237
|
-
// `[[tab-key]]`/`[[fig-key]]`/`[[dia-key]]` (and their
|
|
238
|
-
// are keyed-float references, `[[check-key]]`
|
|
239
|
-
// assessment list,
|
|
240
|
-
//
|
|
272
|
+
// `[[tab-key]]`/`[[fig-key]]`/`[[dia-key]]`/`[[ex-key]]` (and their
|
|
273
|
+
// cross-page forms) are keyed-float references, `[[check-key]]`
|
|
274
|
+
// targets a lesson's assessment list, `[[sec-slug]]` a heading, and
|
|
275
|
+
// `[[memo-key]]` is a page-local memo reference — all resolved by the
|
|
276
|
+
// remark pass and the manifest, not notation keys.
|
|
241
277
|
if (
|
|
242
278
|
parseEquationRef(key) ||
|
|
243
279
|
parseKeyedFloatRef(key, { prefix: 'tab' }) ||
|
|
244
280
|
parseKeyedFloatRef(key, { prefix: 'fig' }) ||
|
|
245
281
|
parseKeyedFloatRef(key, { prefix: 'dia' }) ||
|
|
282
|
+
parseKeyedFloatRef(key, { prefix: 'ex' }) ||
|
|
246
283
|
parseKnowledgeCheckRef(key) ||
|
|
284
|
+
parseSectionRef(key) ||
|
|
247
285
|
parseMemoRef(key)
|
|
248
286
|
) {
|
|
249
287
|
continue;
|
|
@@ -294,6 +332,57 @@ function lessonCheckIds(lessonPath: string): string[] {
|
|
|
294
332
|
: [];
|
|
295
333
|
}
|
|
296
334
|
|
|
335
|
+
/**
|
|
336
|
+
* One part's **overview page** — `content/docs/<dir>/index.mdx`, the page the
|
|
337
|
+
* part's own directory URL resolves to. `isLessonSlug` already excludes any
|
|
338
|
+
* `<dir>/index`, so this page is not a chapter: it carries no competencies, no
|
|
339
|
+
* knowledge check, and no float numbers, and nothing in the curriculum graph
|
|
340
|
+
* changes when a course adds one.
|
|
341
|
+
*/
|
|
342
|
+
export interface PartOverviewPage {
|
|
343
|
+
readonly dir: string;
|
|
344
|
+
readonly slug: string;
|
|
345
|
+
readonly file: string;
|
|
346
|
+
readonly title: string;
|
|
347
|
+
readonly description?: string;
|
|
348
|
+
/** MDX after the frontmatter — what the S2 gate (`gate-part-pages.ts`) reads. */
|
|
349
|
+
readonly body: string;
|
|
350
|
+
/** 1-based file line `body` starts on, for diagnostic locations. */
|
|
351
|
+
readonly bodyLine: number;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The overview page of every part that has one, keyed by the part's directory.
|
|
356
|
+
* A part without one is simply absent — overview pages are opt-in, one part at
|
|
357
|
+
* a time.
|
|
358
|
+
*/
|
|
359
|
+
export function loadPartOverviews(
|
|
360
|
+
parts: readonly { readonly dir: string }[],
|
|
361
|
+
): PartOverviewPage[] {
|
|
362
|
+
const pages: PartOverviewPage[] = [];
|
|
363
|
+
for (const { dir } of parts) {
|
|
364
|
+
for (const extension of ['.mdx', '.md']) {
|
|
365
|
+
const path = join(CONTENT_ROOT, 'docs', dir, `index${extension}`);
|
|
366
|
+
if (!existsSync(path)) continue;
|
|
367
|
+
const { data, file, body, bodyLine } = parseDocument(path);
|
|
368
|
+
const title = typeof data.title === 'string' ? data.title : dir;
|
|
369
|
+
const description =
|
|
370
|
+
typeof data.description === 'string' ? data.description : undefined;
|
|
371
|
+
pages.push({
|
|
372
|
+
dir,
|
|
373
|
+
slug: `${dir}/index`,
|
|
374
|
+
file,
|
|
375
|
+
title,
|
|
376
|
+
...(description === undefined ? {} : { description }),
|
|
377
|
+
body,
|
|
378
|
+
bodyLine,
|
|
379
|
+
});
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return pages;
|
|
384
|
+
}
|
|
385
|
+
|
|
297
386
|
function lessonEntries(
|
|
298
387
|
prerequisitesById: ReadonlyMap<string, readonly string[]>,
|
|
299
388
|
): LessonDefinition[] {
|
|
@@ -479,22 +568,67 @@ export async function loadAbbreviationRegistryInput(): Promise<
|
|
|
479
568
|
return abbreviationEntries();
|
|
480
569
|
}
|
|
481
570
|
|
|
571
|
+
/**
|
|
572
|
+
* A lesson's `notation.suspend` (rule N22): decorations that are not
|
|
573
|
+
* operators on this page. A name outside `DECORATION_NAMES`, a decoration
|
|
574
|
+
* suspended twice, and a `notation.local` operator entry for a suspended
|
|
575
|
+
* decoration (the page would contradict itself) each fail the load.
|
|
576
|
+
*/
|
|
577
|
+
export function notationSuspensions(
|
|
578
|
+
raw: unknown,
|
|
579
|
+
localDefinitions: readonly LocalNotationDefinitionInput[],
|
|
580
|
+
file: string,
|
|
581
|
+
): NotationSuspension[] {
|
|
582
|
+
if (raw === undefined) return [];
|
|
583
|
+
if (!Array.isArray(raw)) {
|
|
584
|
+
throw new TypeError(`${file} notation.suspend must be an array`);
|
|
585
|
+
}
|
|
586
|
+
const seen = new Set<string>();
|
|
587
|
+
return raw.map((item, index) => {
|
|
588
|
+
const at = `${file} notation.suspend[${index}]`;
|
|
589
|
+
const data = record(item, at);
|
|
590
|
+
const operator = string(data.operator, `${at}.operator`).trim();
|
|
591
|
+
if (!DECORATION_NAMES.has(operator)) {
|
|
592
|
+
throw new Error(
|
|
593
|
+
`${at}.operator ${JSON.stringify(operator)} is not a decoration; use one of ${[...DECORATION_NAMES].join(', ')}`,
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
if (seen.has(operator)) {
|
|
597
|
+
throw new Error(`${at} suspends ${JSON.stringify(operator)} a second time`);
|
|
598
|
+
}
|
|
599
|
+
seen.add(operator);
|
|
600
|
+
const local = localDefinitions.find(
|
|
601
|
+
(definition) => decorationOfEntry(definition.notation) === operator,
|
|
602
|
+
);
|
|
603
|
+
if (local) {
|
|
604
|
+
throw new Error(
|
|
605
|
+
`${at} suspends ${JSON.stringify(operator)}, but notation.local entry ${local.key} explains it as an operator (latex ${JSON.stringify(local.notation)}); drop one of them`,
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const reason = optionalString(data.reason, `${at}.reason`);
|
|
609
|
+
return reason === undefined ? { operator } : { operator, reason: reason.trim() };
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
|
|
482
613
|
function notationLessonEntries(): NotationLessonInput[] {
|
|
483
614
|
const lessons: NotationLessonInput[] = [];
|
|
484
615
|
for (const path of filesBelow(join(CONTENT_ROOT, 'docs'), ['.md', '.mdx'])) {
|
|
485
616
|
const slug = docSlug(path);
|
|
486
617
|
if (!isLessonSlug(slug)) continue;
|
|
487
|
-
const { file, data, body } = parseDocument(path);
|
|
618
|
+
const { file, data, body, bodyLine } = parseDocument(path);
|
|
488
619
|
const notation = record(data.notation ?? {}, `${file} notation`);
|
|
489
620
|
const local = Array.isArray(notation.local) ? notation.local : [];
|
|
621
|
+
const localDefinitions = local.map((entry, index) =>
|
|
622
|
+
localDefinition(entry, file, index),
|
|
623
|
+
);
|
|
490
624
|
lessons.push({
|
|
491
625
|
lessonId: lessonIdFromSlug(slug),
|
|
492
626
|
status: status(data.editorialStatus, `${file} editorialStatus`),
|
|
493
|
-
localDefinitions
|
|
494
|
-
|
|
495
|
-
),
|
|
627
|
+
localDefinitions,
|
|
628
|
+
suspended: notationSuspensions(notation.suspend, localDefinitions, file),
|
|
496
629
|
references: extractNotationReferences(body, file, undefined),
|
|
497
630
|
body,
|
|
631
|
+
bodyLine,
|
|
498
632
|
source: { file },
|
|
499
633
|
});
|
|
500
634
|
}
|