explico 0.12.0 → 0.13.1
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 +1137 -1
- package/README.md +91 -13
- package/dist/compiler/astro.d.ts +4 -3
- package/dist/compiler/astro.js +45 -15
- package/dist/compiler/cli.d.ts +26 -12
- package/dist/compiler/cli.js +96 -25
- package/dist/compiler/collections.d.ts +9 -1
- package/dist/compiler/collections.js +55 -16
- package/dist/compiler/content-schema.d.ts +31 -0
- package/dist/compiler/content-schema.js +82 -0
- package/dist/compiler/content-schemas.d.ts +422 -0
- package/dist/compiler/content-schemas.js +229 -0
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +30 -2
- package/dist/compiler/exports/combined-html.d.ts +97 -0
- package/dist/compiler/exports/combined-html.js +492 -0
- package/dist/compiler/exports/download-index.d.ts +9 -0
- package/dist/compiler/exports/download-index.js +33 -0
- package/dist/compiler/exports/export-chapters.d.ts +98 -0
- package/dist/compiler/exports/export-chapters.js +164 -0
- package/dist/compiler/exports/export-pages.d.ts +48 -0
- package/dist/compiler/exports/export-pages.js +22 -0
- package/dist/compiler/exports/html-tree.d.ts +22 -0
- package/dist/compiler/exports/html-tree.js +47 -0
- package/dist/compiler/exports/latex/capture.d.ts +106 -0
- package/dist/compiler/exports/latex/capture.js +435 -0
- package/dist/compiler/exports/latex/compile.d.ts +73 -0
- package/dist/compiler/exports/latex/compile.js +115 -0
- package/dist/compiler/exports/latex/document.d.ts +102 -0
- package/dist/compiler/exports/latex/document.js +349 -0
- package/dist/compiler/exports/latex/explico.cls +415 -0
- package/dist/compiler/exports/latex/numbering.d.ts +18 -0
- package/dist/compiler/exports/latex/numbering.js +57 -0
- package/dist/compiler/exports/latex/serialize.d.ts +112 -0
- package/dist/compiler/exports/latex/serialize.js +606 -0
- package/dist/compiler/exports/latex-export.d.ts +44 -0
- package/dist/compiler/exports/latex-export.js +169 -0
- package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
- package/dist/compiler/exports/markdown/serialize.js +527 -0
- package/dist/compiler/exports/markdown-export.d.ts +39 -0
- package/dist/compiler/exports/markdown-export.js +240 -0
- package/dist/compiler/exports/standalone-html.d.ts +14 -5
- package/dist/compiler/exports/standalone-html.js +23 -51
- package/dist/compiler/exports/static-tree.d.ts +253 -0
- package/dist/compiler/exports/static-tree.js +625 -0
- package/dist/compiler/exports/zip.d.ts +16 -0
- package/dist/compiler/exports/zip.js +72 -0
- package/dist/compiler/lesson-derivation.d.ts +8 -2
- package/dist/compiler/lesson-derivation.js +6 -7
- package/dist/compiler/manifest.d.ts +87 -6
- package/dist/compiler/manifest.js +174 -31
- package/dist/compiler/render-diagnostics.d.ts +65 -0
- package/dist/compiler/render-diagnostics.js +56 -0
- package/dist/compiler/resolution-report.d.ts +2 -1
- package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
- package/dist/curriculum/validation.d.ts +9 -3
- package/dist/curriculum/validation.js +35 -17
- package/dist/reference/base-abbreviations.d.mts +2 -2
- package/dist/reference/base-abbreviations.mjs +2 -2
- package/dist/reference/base-operators.d.mts +2 -2
- package/dist/reference/base-operators.mjs +2 -2
- package/dist/reference/base-roman-labels.d.mts +4 -3
- package/dist/reference/base-roman-labels.mjs +4 -3
- package/dist/reference/citation-format.d.mts +35 -0
- package/dist/reference/citation-format.mjs +74 -0
- package/dist/reference/consistency.d.ts +5 -9
- package/dist/reference/consistency.js +34 -38
- package/dist/reference/curriculum-alignment.js +3 -24
- package/dist/reference/derived-entries.d.ts +45 -0
- package/dist/reference/derived-entries.js +50 -0
- package/dist/reference/derived-notation.d.mts +125 -0
- package/dist/reference/derived-notation.mjs +121 -0
- package/dist/reference/diagram-fit.d.ts +45 -0
- package/dist/reference/diagram-fit.js +54 -0
- package/dist/reference/diagram-layout.d.ts +59 -10
- package/dist/reference/diagram-layout.js +85 -24
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +1 -1
- package/dist/reference/equations-validate.js +26 -1
- package/dist/reference/equations.d.mts +27 -0
- package/dist/reference/equations.mjs +120 -22
- package/dist/reference/examples-validate.d.ts +1 -1
- package/dist/reference/figure-fit.d.ts +70 -0
- package/dist/reference/figure-fit.js +60 -0
- package/dist/reference/figure-pictures.d.ts +27 -0
- package/dist/reference/figure-pictures.js +49 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-abbreviations.js +1 -1
- package/dist/reference/gate-course-rules.d.ts +4 -3
- package/dist/reference/gate-course-rules.js +9 -4
- package/dist/reference/gate-heading-length.d.ts +2 -0
- package/dist/reference/gate-heading-length.js +7 -5
- package/dist/reference/gate-heading-structure.js +26 -0
- package/dist/reference/gate-indices.d.ts +35 -0
- package/dist/reference/gate-indices.js +299 -0
- package/dist/reference/gate-math.d.ts +32 -2
- package/dist/reference/gate-math.js +68 -12
- package/dist/reference/gate-mdx-expression.d.ts +17 -0
- package/dist/reference/gate-mdx-expression.js +29 -0
- package/dist/reference/gate-part-pages.d.ts +1 -1
- package/dist/reference/gate-prop-text.d.ts +28 -0
- package/dist/reference/gate-prop-text.js +106 -0
- package/dist/reference/gate-prose-notation.d.ts +3 -1
- package/dist/reference/gate-prose-notation.js +4 -2
- package/dist/reference/gate-retired-keys.d.ts +35 -0
- package/dist/reference/gate-retired-keys.js +58 -0
- package/dist/reference/gloss.d.ts +2 -0
- package/dist/reference/katex-measure.d.ts +17 -0
- package/dist/reference/katex-measure.js +223 -0
- package/dist/reference/keyed-float-validate.js +12 -0
- package/dist/reference/keyed-jsx-floats.d.mts +2 -2
- package/dist/reference/keyed-jsx-floats.mjs +11 -14
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-scope.js +13 -2
- package/dist/reference/math-glyphs.d.mts +94 -2
- package/dist/reference/math-glyphs.mjs +851 -46
- package/dist/reference/mixed-content.d.ts +8 -3
- package/dist/reference/mixed-content.js +4 -2
- package/dist/reference/page-notation.d.ts +34 -4
- package/dist/reference/page-notation.js +54 -2
- package/dist/reference/references.d.ts +11 -1
- package/dist/reference/references.js +6 -0
- package/dist/reference/registry.js +104 -10
- package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
- package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
- package/dist/reference/rehype-heading-anchors.mjs +11 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-notation.mjs +84 -20
- package/dist/reference/render-lab-math.d.ts +2 -0
- package/dist/reference/render-lab-math.js +17 -5
- package/dist/reference/sections-validate.d.ts +28 -3
- package/dist/reference/sections-validate.js +31 -4
- package/dist/reference/sections.d.mts +24 -6
- package/dist/reference/sections.mjs +92 -17
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/tables-validate.js +24 -1
- package/dist/reference/text-measure.d.ts +10 -0
- package/dist/reference/text-measure.js +41 -0
- package/dist/reference/text-widths.d.ts +10 -0
- package/dist/reference/text-widths.js +1238 -0
- package/dist/reference/types.d.ts +64 -8
- package/dist/rules/catalog.js +191 -30
- package/dist/rules/in-effect.d.ts +1 -1
- package/dist/rules/rules.schema.json +79 -24
- package/dist/rules/settings.d.ts +1 -1
- package/dist/rules/settings.js +7 -0
- package/dist/session/PreferenceStore.d.ts +18 -1
- package/dist/session/PreferenceStore.js +29 -1
- package/docs/authoring/README.md +32 -21
- package/docs/authoring/agents.md +38 -7
- package/docs/authoring/components.md +111 -6
- package/docs/authoring/migrating.md +707 -0
- package/docs/authoring/notation.md +281 -29
- package/docs/authoring/retired.md +1 -0
- package/docs/authoring/structure-rules.md +10 -41
- package/docs/authoring/structure.md +53 -2
- package/docs/authoring/visualizations.md +144 -18
- package/package.json +12 -7
- package/src/compiler/astro.ts +57 -17
- package/src/compiler/cli.ts +143 -35
- package/src/compiler/collections.ts +92 -19
- package/src/compiler/content-schema.ts +108 -0
- package/src/compiler/content-schemas.ts +391 -0
- package/src/compiler/course-config.ts +86 -15
- package/src/compiler/exports/combined-html.ts +669 -0
- package/src/compiler/exports/download-index.ts +36 -0
- package/src/compiler/exports/export-chapters.ts +233 -0
- package/src/compiler/exports/export-pages.ts +44 -0
- package/src/compiler/exports/html-tree.ts +62 -0
- package/src/compiler/exports/latex/capture.ts +651 -0
- package/src/compiler/exports/latex/compile.ts +168 -0
- package/src/compiler/exports/latex/document.ts +479 -0
- package/src/compiler/exports/latex/explico.cls +415 -0
- package/src/compiler/exports/latex/numbering.ts +74 -0
- package/src/compiler/exports/latex/serialize.ts +787 -0
- package/src/compiler/exports/latex/texlive-packages.txt +76 -0
- package/src/compiler/exports/latex-export.ts +255 -0
- package/src/compiler/exports/markdown/serialize.ts +632 -0
- package/src/compiler/exports/markdown-export.ts +329 -0
- package/src/compiler/exports/standalone-html.ts +39 -70
- package/src/compiler/exports/static-tree.ts +944 -0
- package/src/compiler/exports/zip.ts +88 -0
- package/src/compiler/lesson-derivation.ts +13 -8
- package/src/compiler/manifest.ts +299 -28
- package/src/compiler/render-diagnostics.ts +111 -0
- package/src/components/HoverPanel.astro +204 -6
- package/src/components/PartOverview.astro +12 -9
- package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
- package/src/components/assessments/AssessmentRunner.tsx +7 -1
- package/src/components/assessments/AssessmentSet.astro +14 -1
- package/src/components/block-heading.ts +38 -0
- package/src/components/card-arrow.ts +18 -4
- package/src/components/card-carry.ts +406 -0
- package/src/components/card-drag.ts +1 -30
- package/src/components/card-fetch.ts +87 -0
- package/src/components/card-manager.ts +1222 -38
- package/src/components/card-memory.ts +5 -1
- package/src/components/card-placement.ts +211 -13
- package/src/components/card-side.ts +281 -31
- package/src/components/citation/CitationLayer.astro +45 -40
- package/src/components/diagram/Diagram.astro +123 -44
- package/src/components/figure/Figure.astro +11 -0
- package/src/components/figure/FloatCaption.astro +4 -17
- package/src/components/float-toolbar.ts +274 -0
- package/src/components/memo/Memo.astro +21 -10
- package/src/components/memo/ShortNote.astro +4 -0
- package/src/components/notation/NotationGlossary.astro +22 -17
- package/src/components/notation/NotationLayer.astro +321 -79
- package/src/components/notation/NotationMath.astro +8 -1
- package/src/components/notation/render-notation-math.ts +51 -5
- package/src/components/readable-text.ts +25 -0
- package/src/components/reference/FloatRefLayer.astro +162 -89
- package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
- package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
- package/src/components/starlight/LayoutPageSidebar.astro +17 -3
- package/src/components/starlight/LayoutSocialIcons.astro +79 -29
- package/src/components/starlight/LessonExportActions.astro +384 -310
- package/src/components/starlight/LessonFooter.astro +47 -9
- package/src/components/starlight/PrintController.astro +28 -0
- package/src/components/starlight/TextSizeControl.astro +137 -0
- package/src/components/starlight/print-controller.ts +380 -0
- package/src/components/starlight/section-summary.ts +13 -5
- package/src/components/table/Table.astro +13 -21
- package/src/components/table/table-truncation.ts +15 -0
- package/src/content-config.ts +20 -312
- package/src/curriculum/validation.ts +72 -24
- package/src/reference/base-abbreviations.mjs +2 -2
- package/src/reference/base-operators.mjs +2 -2
- package/src/reference/base-roman-labels.mjs +4 -3
- package/src/reference/citation-format.mjs +74 -0
- package/src/reference/consistency.ts +47 -61
- package/src/reference/curriculum-alignment.ts +4 -41
- package/src/reference/derived-entries.ts +68 -0
- package/src/reference/derived-notation.mjs +121 -0
- package/src/reference/diagram-fit.ts +104 -0
- package/src/reference/diagram-layout.ts +182 -51
- package/src/reference/diagrams-validate.ts +2 -1
- package/src/reference/equations-validate.ts +41 -2
- package/src/reference/equations.mjs +120 -22
- package/src/reference/examples-validate.ts +1 -0
- package/src/reference/figure-fit.ts +139 -0
- package/src/reference/figure-pictures.ts +96 -0
- package/src/reference/figures-validate.ts +2 -1
- package/src/reference/gate-abbreviations.ts +1 -1
- package/src/reference/gate-course-rules.ts +22 -7
- package/src/reference/gate-heading-length.ts +25 -7
- package/src/reference/gate-heading-structure.ts +40 -1
- package/src/reference/gate-indices.ts +470 -0
- package/src/reference/gate-math.ts +102 -13
- package/src/reference/gate-mdx-expression.ts +45 -0
- package/src/reference/gate-prop-text.ts +145 -0
- package/src/reference/gate-prose-notation.ts +19 -5
- package/src/reference/gate-retired-keys.ts +101 -0
- package/src/reference/gloss.ts +2 -0
- package/src/reference/katex-measure.ts +384 -0
- package/src/reference/keyed-float-validate.ts +22 -0
- package/src/reference/keyed-jsx-floats.mjs +11 -14
- package/src/reference/lab-math-scope.ts +15 -1
- package/src/reference/math-glyphs.mjs +851 -46
- package/src/reference/mixed-content.ts +11 -2
- package/src/reference/page-notation.ts +100 -3
- package/src/reference/references.ts +14 -0
- package/src/reference/registry.ts +154 -8
- package/src/reference/rehype-fail-katex-errors.mjs +19 -0
- package/src/reference/rehype-heading-anchors.mjs +11 -1
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-notation.mjs +84 -20
- package/src/reference/render-lab-math.ts +29 -5
- package/src/reference/sections-validate.ts +67 -3
- package/src/reference/sections.mjs +92 -17
- package/src/reference/tables-validate.ts +40 -2
- package/src/reference/text-measure.ts +63 -0
- package/src/reference/text-widths.ts +133 -0
- package/src/reference/types.ts +75 -8
- package/src/rules/catalog.ts +205 -29
- package/src/rules/settings.ts +10 -0
- package/src/session/PreferenceStore.ts +53 -0
- package/src/styles/global.css +291 -75
package/src/compiler/cli.ts
CHANGED
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
* on any renderability error (unparseable MDX, KaTeX error, unresolved
|
|
26
26
|
* notation, unknown `[[key]]`, unknown citation), on top of the manifest's
|
|
27
27
|
* schema / ref / math / equation diagnostics for that lesson.
|
|
28
|
+
* - Beside the manifest's diagnostics, every command lists what the last
|
|
29
|
+
* `astro build` found on the rendered pages (`render-diagnostics.ts`),
|
|
30
|
+
* under the `render:` source.
|
|
28
31
|
*/
|
|
29
32
|
|
|
30
33
|
import { execFileSync } from 'node:child_process';
|
|
@@ -37,6 +40,7 @@ import {
|
|
|
37
40
|
parseFrontmatter,
|
|
38
41
|
} from '@astrojs/markdown-remark';
|
|
39
42
|
import rehypeKatex from 'rehype-katex';
|
|
43
|
+
import remarkDirective from 'remark-directive';
|
|
40
44
|
import remarkMath from 'remark-math';
|
|
41
45
|
import remarkMdx from 'remark-mdx';
|
|
42
46
|
|
|
@@ -51,6 +55,7 @@ import remarkShortNote from '../reference/remark-short-note.mjs';
|
|
|
51
55
|
import type { DiagnosticSeverity } from '../reference/types';
|
|
52
56
|
import type { EquationLink } from '../reference/gate-math';
|
|
53
57
|
import type { Manifest, ManifestLesson } from './manifest';
|
|
58
|
+
import { readRenderDiagnostics, type RenderDiagnostic } from './render-diagnostics';
|
|
54
59
|
import { isLessonSlug, lessonIdFromSlug } from './lesson-derivation';
|
|
55
60
|
import { isCourseRuleId, ownerOf, ELEMENTS, type Element, type Enforcement } from '../rules/catalog';
|
|
56
61
|
import {
|
|
@@ -59,6 +64,7 @@ import {
|
|
|
59
64
|
type RuleInEffect,
|
|
60
65
|
type RulesFilter,
|
|
61
66
|
} from '../rules/in-effect';
|
|
67
|
+
import { RESERVED_KEY_PREFIXES, reservedKeyPrefix } from '../reference/references';
|
|
62
68
|
|
|
63
69
|
const REPOSITORY_ROOT = process.cwd();
|
|
64
70
|
// Content and its course math are the top-level `content/` folder (Phase F).
|
|
@@ -132,6 +138,17 @@ function withOwner(diagnostic: CliDiagnostic): CliDiagnostic {
|
|
|
132
138
|
return { ...diagnostic, ...ownerFields(diagnostic.code) };
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
/** A finding the last `astro build` made on a rendered page, as a CLI diagnostic. */
|
|
142
|
+
function renderedCliDiagnostic(diagnostic: RenderDiagnostic): CliDiagnostic {
|
|
143
|
+
return {
|
|
144
|
+
code: `render:${diagnostic.code}`,
|
|
145
|
+
...ownerFields(diagnostic.code),
|
|
146
|
+
severity: diagnosticSeverity(diagnostic.severity),
|
|
147
|
+
message: diagnostic.message,
|
|
148
|
+
file: diagnostic.file,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
135
152
|
function mentions(message: string, lessonId: string, file: string): boolean {
|
|
136
153
|
return (
|
|
137
154
|
message.includes(lessonId) ||
|
|
@@ -149,10 +166,19 @@ function mentions(message: string, lessonId: string, file: string): boolean {
|
|
|
149
166
|
export function lessonDiagnostics(
|
|
150
167
|
manifest: Manifest,
|
|
151
168
|
lesson: ManifestLesson,
|
|
169
|
+
rendered: readonly RenderDiagnostic[] = readRenderDiagnostics(),
|
|
152
170
|
): CliDiagnostic[] {
|
|
153
171
|
const out: CliDiagnostic[] = [];
|
|
154
172
|
const { file, id } = lesson;
|
|
155
173
|
const assessments = new Set(lesson.assessments);
|
|
174
|
+
// The shared entries this page shows a card of: a fault in one (a formula
|
|
175
|
+
// naming an unknown key, say) is the page's too, as `validate:content` says.
|
|
176
|
+
const bundle = new Set(lesson.notation.definitionIds);
|
|
177
|
+
const entryFiles = new Set(
|
|
178
|
+
manifest.notation.definitions
|
|
179
|
+
.filter((definition) => definition.scope.kind === 'shared' && bundle.has(definition.id))
|
|
180
|
+
.map((definition) => definition.source.file),
|
|
181
|
+
);
|
|
156
182
|
const withFile = (value?: string) =>
|
|
157
183
|
value === undefined ? {} : { file: value };
|
|
158
184
|
|
|
@@ -174,6 +200,7 @@ export function lessonDiagnostics(
|
|
|
174
200
|
if (
|
|
175
201
|
entry.lessonId !== id &&
|
|
176
202
|
entryFile !== file &&
|
|
203
|
+
!(entryFile !== undefined && entryFiles.has(entryFile)) &&
|
|
177
204
|
!mentions(entry.message, id, file)
|
|
178
205
|
) {
|
|
179
206
|
continue;
|
|
@@ -189,6 +216,7 @@ export function lessonDiagnostics(
|
|
|
189
216
|
for (const diagnostic of manifest.diagnostics.math) {
|
|
190
217
|
const forLesson =
|
|
191
218
|
diagnostic.file === file ||
|
|
219
|
+
entryFiles.has(diagnostic.file) ||
|
|
192
220
|
(diagnostic.context === 'assessment' &&
|
|
193
221
|
[...assessments].some((assessmentId) =>
|
|
194
222
|
diagnostic.file.startsWith(`${assessmentId} `),
|
|
@@ -257,6 +285,11 @@ export function lessonDiagnostics(
|
|
|
257
285
|
});
|
|
258
286
|
}
|
|
259
287
|
|
|
288
|
+
for (const diagnostic of rendered) {
|
|
289
|
+
if (diagnostic.file !== file && diagnostic.lessonId !== id) continue;
|
|
290
|
+
out.push(renderedCliDiagnostic(diagnostic));
|
|
291
|
+
}
|
|
292
|
+
|
|
260
293
|
return out
|
|
261
294
|
.map(withOwner)
|
|
262
295
|
.sort(
|
|
@@ -280,6 +313,11 @@ function processorFor(manifest: Manifest) {
|
|
|
280
313
|
// as the real Astro/MDX build, so `remarkNotation` sees the same tree
|
|
281
314
|
// shape here that it does there (D7/F-tables-figures parity).
|
|
282
315
|
remarkMdx,
|
|
316
|
+
// Starlight parses `:::note` asides as directives in the real build
|
|
317
|
+
// (on every page, not only its own `src/content/docs/`); without this
|
|
318
|
+
// an aside's body read as top-level lesson flow here, so its equation
|
|
319
|
+
// took a number the build does not give it.
|
|
320
|
+
remarkDirective,
|
|
283
321
|
remarkMath,
|
|
284
322
|
remarkShortNote,
|
|
285
323
|
remarkMemo,
|
|
@@ -303,9 +341,12 @@ function processorFor(manifest: Manifest) {
|
|
|
303
341
|
return processorPromise;
|
|
304
342
|
}
|
|
305
343
|
|
|
306
|
-
/**
|
|
344
|
+
/**
|
|
345
|
+
* Empty `import` / `export` lines so they don't render as stray prose. The
|
|
346
|
+
* line itself stays, so a render error names the line it has in the file.
|
|
347
|
+
*/
|
|
307
348
|
function stripEsm(body: string): string {
|
|
308
|
-
return body.replace(/^(?:import|export)\s[^\n]
|
|
349
|
+
return body.replace(/^(?:import|export)\s[^\n]*/gm, '');
|
|
309
350
|
}
|
|
310
351
|
|
|
311
352
|
/** Classify a render error by which pipeline stage raised it. */
|
|
@@ -347,22 +388,60 @@ export async function renderLesson(
|
|
|
347
388
|
relative(DOCS_ROOT, input.path).replace(/\\/g, '/'),
|
|
348
389
|
);
|
|
349
390
|
try {
|
|
350
|
-
await
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
391
|
+
await withoutParseLogs(() =>
|
|
392
|
+
processor.render(stripEsm(input.body), {
|
|
393
|
+
frontmatter: { ...input.frontmatter, lessonId },
|
|
394
|
+
fileURL: pathToFileURL(input.path),
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
354
397
|
return { rendered: true, diagnostics: [] };
|
|
355
398
|
} catch (error) {
|
|
356
399
|
const message = error instanceof Error ? error.message : String(error);
|
|
400
|
+
// A VFileMessage knows its place in the file (the body keeps the
|
|
401
|
+
// frontmatter's lines, `lessonSource`).
|
|
402
|
+
const { line, column } = (error ?? {}) as { line?: unknown; column?: unknown };
|
|
357
403
|
return {
|
|
358
404
|
rendered: false,
|
|
359
405
|
diagnostics: [
|
|
360
|
-
{
|
|
406
|
+
{
|
|
407
|
+
code: renderErrorCode(message),
|
|
408
|
+
severity: 'error',
|
|
409
|
+
message,
|
|
410
|
+
...(typeof line === 'number' ? { line } : {}),
|
|
411
|
+
...(typeof column === 'number' ? { column } : {}),
|
|
412
|
+
},
|
|
361
413
|
],
|
|
362
414
|
};
|
|
363
415
|
}
|
|
364
416
|
}
|
|
365
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Astro's processor `console.error`s a failed render (the whole VFileMessage,
|
|
420
|
+
* its syntax tree included) before rethrowing it; the check reports the error
|
|
421
|
+
* itself, once, as a diagnostic. Renders run concurrently, so the swap is
|
|
422
|
+
* counted and only that one log is dropped.
|
|
423
|
+
*/
|
|
424
|
+
let quietRenders = 0;
|
|
425
|
+
let consoleError: typeof console.error | undefined;
|
|
426
|
+
async function withoutParseLogs<T>(work: () => Promise<T>): Promise<T> {
|
|
427
|
+
if (quietRenders++ === 0) {
|
|
428
|
+
const original = console.error;
|
|
429
|
+
consoleError = original;
|
|
430
|
+
console.error = (...args: unknown[]) => {
|
|
431
|
+
if (args[0] instanceof Error && args[0].message.startsWith('Failed to parse Markdown file')) return;
|
|
432
|
+
original(...args);
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
return await work();
|
|
437
|
+
} finally {
|
|
438
|
+
if (--quietRenders === 0 && consoleError !== undefined) {
|
|
439
|
+
console.error = consoleError;
|
|
440
|
+
consoleError = undefined;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
366
445
|
// --- token estimate ----------------------------------------------------
|
|
367
446
|
|
|
368
447
|
/** Rough token estimate — ~4 chars/token, the usual GPT-family heuristic. */
|
|
@@ -461,6 +540,22 @@ function lessonRaw(lesson: ManifestLesson): {
|
|
|
461
540
|
};
|
|
462
541
|
}
|
|
463
542
|
|
|
543
|
+
/**
|
|
544
|
+
* A lesson file's frontmatter and its body for rendering: the frontmatter is
|
|
545
|
+
* blanked with spaces, as Astro's MDX plugin does, so line N of the body is
|
|
546
|
+
* line N of the file and a render error points where the build's would.
|
|
547
|
+
*/
|
|
548
|
+
export function lessonSource(text: string): {
|
|
549
|
+
frontmatter: Record<string, unknown>;
|
|
550
|
+
body: string;
|
|
551
|
+
} {
|
|
552
|
+
const parsed = parseFrontmatter(text, { frontmatter: 'empty-with-spaces' });
|
|
553
|
+
return {
|
|
554
|
+
frontmatter: (parsed.frontmatter ?? {}) as Record<string, unknown>,
|
|
555
|
+
body: parsed.content,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
464
559
|
// --- domain signatures ------------------------------------------------
|
|
465
560
|
|
|
466
561
|
/**
|
|
@@ -546,10 +641,6 @@ export interface StatusReport {
|
|
|
546
641
|
readonly keyedDiagrams: number;
|
|
547
642
|
readonly keyedExamples: number;
|
|
548
643
|
};
|
|
549
|
-
readonly draftDebt: {
|
|
550
|
-
readonly lessons: readonly string[];
|
|
551
|
-
readonly notation: readonly string[];
|
|
552
|
-
};
|
|
553
644
|
readonly orphans: readonly CliDiagnostic[];
|
|
554
645
|
readonly diagnostics: {
|
|
555
646
|
readonly errors: number;
|
|
@@ -558,16 +649,10 @@ export interface StatusReport {
|
|
|
558
649
|
};
|
|
559
650
|
}
|
|
560
651
|
|
|
561
|
-
export function contentStatus(
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
.sort();
|
|
566
|
-
const draftNotation = manifest.notation.definitions
|
|
567
|
-
.filter((definition) => definition.status === 'draft')
|
|
568
|
-
.map((definition) => definition.id)
|
|
569
|
-
.sort();
|
|
570
|
-
|
|
652
|
+
export function contentStatus(
|
|
653
|
+
manifest: Manifest,
|
|
654
|
+
rendered: readonly RenderDiagnostic[] = readRenderDiagnostics(),
|
|
655
|
+
): StatusReport {
|
|
571
656
|
const orphans: CliDiagnostic[] = manifest.notation.diagnostics
|
|
572
657
|
.filter((diagnostic) => diagnostic.code === 'unused-definition')
|
|
573
658
|
.map((diagnostic) => ({
|
|
@@ -598,6 +683,7 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
598
683
|
...manifest.diagnostics.knowledge.map((d) => d.severity),
|
|
599
684
|
...manifest.diagnostics.config.map((d) => d.severity),
|
|
600
685
|
...manifest.diagnostics.waivers.map((d) => d.severity),
|
|
686
|
+
...rendered.map((d) => diagnosticSeverity(d.severity)),
|
|
601
687
|
];
|
|
602
688
|
|
|
603
689
|
const keyedEquations = manifest.lessons.reduce(
|
|
@@ -639,7 +725,6 @@ export function contentStatus(manifest: Manifest): StatusReport {
|
|
|
639
725
|
keyedDiagrams,
|
|
640
726
|
keyedExamples,
|
|
641
727
|
},
|
|
642
|
-
draftDebt: { lessons: draftLessons, notation: draftNotation },
|
|
643
728
|
orphans,
|
|
644
729
|
diagnostics: {
|
|
645
730
|
errors: allDiagnostics.filter((severity) => severity === 'error').length,
|
|
@@ -699,10 +784,14 @@ const SEVERITY_RANK: Readonly<Record<DiagnosticSeverity, number>> = {
|
|
|
699
784
|
error: 2,
|
|
700
785
|
};
|
|
701
786
|
|
|
702
|
-
/**
|
|
787
|
+
/**
|
|
788
|
+
* Manifest diagnostics at or above a severity, and those the last
|
|
789
|
+
* `astro build` found on rendered pages, normalized for tooling.
|
|
790
|
+
*/
|
|
703
791
|
export function contentDiagnostics(
|
|
704
792
|
manifest: Manifest,
|
|
705
793
|
minimumSeverity: DiagnosticThreshold = 'info',
|
|
794
|
+
rendered: readonly RenderDiagnostic[] = readRenderDiagnostics(),
|
|
706
795
|
): DiagnosticsReport {
|
|
707
796
|
const lessonFileByHint = new Map<string, string>();
|
|
708
797
|
for (const lesson of manifest.lessons) {
|
|
@@ -762,6 +851,10 @@ export function contentDiagnostics(
|
|
|
762
851
|
});
|
|
763
852
|
}
|
|
764
853
|
}
|
|
854
|
+
for (const diagnostic of rendered) {
|
|
855
|
+
const cli = renderedCliDiagnostic(diagnostic);
|
|
856
|
+
if (SEVERITY_RANK[cli.severity] >= SEVERITY_RANK[minimumSeverity]) diagnostics.push(cli);
|
|
857
|
+
}
|
|
765
858
|
|
|
766
859
|
diagnostics.sort(
|
|
767
860
|
(left, right) =>
|
|
@@ -779,8 +872,11 @@ export function contentDiagnostics(
|
|
|
779
872
|
}
|
|
780
873
|
|
|
781
874
|
/** Every warning exactly; errors and informational advice are excluded. */
|
|
782
|
-
export function contentWarnings(
|
|
783
|
-
|
|
875
|
+
export function contentWarnings(
|
|
876
|
+
manifest: Manifest,
|
|
877
|
+
rendered: readonly RenderDiagnostic[] = readRenderDiagnostics(),
|
|
878
|
+
): WarningsReport {
|
|
879
|
+
const warnings = contentDiagnostics(manifest, 'warning', rendered).diagnostics.filter(
|
|
784
880
|
(diagnostic) => diagnostic.severity === 'warning',
|
|
785
881
|
);
|
|
786
882
|
return { count: warnings.length, warnings };
|
|
@@ -850,7 +946,6 @@ export interface ContextReport {
|
|
|
850
946
|
readonly file: string;
|
|
851
947
|
readonly title: string;
|
|
852
948
|
readonly description: string;
|
|
853
|
-
readonly status: string;
|
|
854
949
|
readonly teaches: readonly string[];
|
|
855
950
|
readonly requires: readonly string[];
|
|
856
951
|
readonly assumptions: readonly string[];
|
|
@@ -928,7 +1023,6 @@ export function contentContext(
|
|
|
928
1023
|
file: lesson.file,
|
|
929
1024
|
title: lesson.title,
|
|
930
1025
|
description: lesson.description,
|
|
931
|
-
status: lesson.status,
|
|
932
1026
|
teaches: lesson.teaches,
|
|
933
1027
|
requires: lesson.requires,
|
|
934
1028
|
assumptions: lesson.assumptions,
|
|
@@ -999,7 +1093,9 @@ async function checkOneLesson(
|
|
|
999
1093
|
lesson: ManifestLesson,
|
|
1000
1094
|
): Promise<CheckLessonResult> {
|
|
1001
1095
|
const manifestDiagnostics = lessonDiagnostics(manifest, lesson);
|
|
1002
|
-
const { frontmatter, body } =
|
|
1096
|
+
const { frontmatter, body } = lessonSource(
|
|
1097
|
+
readFileSync(lessonAbsolutePath(lesson), 'utf8'),
|
|
1098
|
+
);
|
|
1003
1099
|
const render = await renderLesson(manifest, {
|
|
1004
1100
|
body,
|
|
1005
1101
|
frontmatter,
|
|
@@ -1057,20 +1153,26 @@ export function changedLessonIds(): string[] {
|
|
|
1057
1153
|
|
|
1058
1154
|
export async function contentCheck(
|
|
1059
1155
|
manifest: Manifest,
|
|
1060
|
-
options: {
|
|
1156
|
+
options: {
|
|
1157
|
+
lesson?: string;
|
|
1158
|
+
/** Several lessons, each checked; `lesson` is the one-lesson form. */
|
|
1159
|
+
lessons?: readonly string[];
|
|
1160
|
+
changed?: boolean;
|
|
1161
|
+
},
|
|
1061
1162
|
): Promise<CheckReport> {
|
|
1062
1163
|
let targets: ManifestLesson[];
|
|
1164
|
+
const named = [...(options.lesson ? [options.lesson] : []), ...(options.lessons ?? [])];
|
|
1063
1165
|
if (options.changed) {
|
|
1064
1166
|
const ids = changedLessonIds();
|
|
1065
1167
|
targets = manifest.lessons.filter((lesson) => ids.includes(lesson.id));
|
|
1066
1168
|
} else {
|
|
1067
|
-
if (
|
|
1169
|
+
if (named.length === 0) {
|
|
1068
1170
|
throw new ContentCliError(
|
|
1069
1171
|
'usage',
|
|
1070
1172
|
'content check needs a <lesson> id or --changed.',
|
|
1071
1173
|
);
|
|
1072
1174
|
}
|
|
1073
|
-
targets =
|
|
1175
|
+
targets = named.map((id) => findLesson(manifest, id));
|
|
1074
1176
|
}
|
|
1075
1177
|
|
|
1076
1178
|
const lessons = await Promise.all(
|
|
@@ -1129,7 +1231,6 @@ export function scaffoldLesson(rawId: string): Scaffold {
|
|
|
1129
1231
|
const contents = `---
|
|
1130
1232
|
title: ${title}
|
|
1131
1233
|
description: One sentence on what the reader can do after this lesson.
|
|
1132
|
-
editorialStatus: draft
|
|
1133
1234
|
teaches: []
|
|
1134
1235
|
assumptions: []
|
|
1135
1236
|
---
|
|
@@ -1172,6 +1273,14 @@ export function scaffoldTerm(rawKey: string): Scaffold {
|
|
|
1172
1273
|
`A notation key is lower-case, dot/hyphen separated, got ${JSON.stringify(rawKey)}.`,
|
|
1173
1274
|
);
|
|
1174
1275
|
}
|
|
1276
|
+
const reserved = reservedKeyPrefix(key);
|
|
1277
|
+
if (reserved !== undefined) {
|
|
1278
|
+
throw new ContentCliError(
|
|
1279
|
+
'invalid-id',
|
|
1280
|
+
`A notation key may not start with ${RESERVED_KEY_PREFIXES.join(', ')}: [[${key}]] would be ` +
|
|
1281
|
+
`read as a reference to that kind of float. Got ${JSON.stringify(rawKey)}.`,
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1175
1284
|
const relPath = `content/notation/${key}.md`;
|
|
1176
1285
|
if (existsSync(join(REPOSITORY_ROOT, relPath))) {
|
|
1177
1286
|
throw new ContentCliError(
|
|
@@ -1190,13 +1299,12 @@ sources: []
|
|
|
1190
1299
|
seeAlso: []
|
|
1191
1300
|
alignment:
|
|
1192
1301
|
kind: general
|
|
1193
|
-
rationale: Draft scaffold — set a real alignment
|
|
1194
|
-
editorialStatus: draft
|
|
1302
|
+
rationale: Draft scaffold — set a real alignment.
|
|
1195
1303
|
aiAssisted: true
|
|
1196
1304
|
---
|
|
1197
1305
|
|
|
1198
1306
|
Draft definition for [[${key}]]. Replace this body and the placeholder
|
|
1199
|
-
frontmatter
|
|
1307
|
+
frontmatter.
|
|
1200
1308
|
`;
|
|
1201
1309
|
|
|
1202
1310
|
return { path: relPath, contents, companions: [] };
|
|
@@ -25,7 +25,6 @@ import type {
|
|
|
25
25
|
} from '../curriculum/validation';
|
|
26
26
|
import type {
|
|
27
27
|
AbbreviationDefinitionInput,
|
|
28
|
-
EditorialStatus,
|
|
29
28
|
LocalNotationDefinitionInput,
|
|
30
29
|
NotationAlignment,
|
|
31
30
|
NotationLessonInput,
|
|
@@ -48,6 +47,9 @@ import { parseMemoRef } from '../reference/remark-memo.mjs';
|
|
|
48
47
|
import { parseSectionRef } from '../reference/sections.mjs';
|
|
49
48
|
import { resolveLabel } from '../reference/label';
|
|
50
49
|
import { resolveGlosses } from '../reference/gloss';
|
|
50
|
+
import { indexEntryFormula } from '../reference/derived-notation.mjs';
|
|
51
|
+
import { isIndexEntryLatex } from './content-schemas';
|
|
52
|
+
import type { ContentRecord } from '../reference/gate-retired-keys';
|
|
51
53
|
import {
|
|
52
54
|
deriveRequires,
|
|
53
55
|
deriveSources,
|
|
@@ -194,13 +196,6 @@ function boolean(value: unknown, label: string): boolean {
|
|
|
194
196
|
return value;
|
|
195
197
|
}
|
|
196
198
|
|
|
197
|
-
function status(value: unknown, label: string): EditorialStatus {
|
|
198
|
-
if (value !== 'draft' && value !== 'in-review' && value !== 'reviewed') {
|
|
199
|
-
throw new TypeError(`${label} must be draft, in-review, or reviewed`);
|
|
200
|
-
}
|
|
201
|
-
return value;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
199
|
function alignment(value: unknown, label: string): NotationAlignment {
|
|
205
200
|
const data = record(value, label);
|
|
206
201
|
if (data.kind === 'general') {
|
|
@@ -400,7 +395,6 @@ function lessonEntries(
|
|
|
400
395
|
const teaches = (data.teaches as string[] | undefined) ?? [];
|
|
401
396
|
lessons.push({
|
|
402
397
|
id: lessonIdFromSlug(slug),
|
|
403
|
-
status: (data.editorialStatus as LessonDefinition['status']) ?? 'draft',
|
|
404
398
|
requires: deriveRequires(teaches, prerequisitesById),
|
|
405
399
|
teaches,
|
|
406
400
|
assessments: lessonCheckIds(path),
|
|
@@ -471,6 +465,41 @@ function glossEntries(
|
|
|
471
465
|
return glosses;
|
|
472
466
|
}
|
|
473
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Rule N23's optional fields: `reads` (index entry), `range`, `fixedIndex`.
|
|
470
|
+
* An index entry's `formula` names the symbol it indexes as `{base}`: the
|
|
471
|
+
* entry's own card shows it over a square bound to the entry, and the raw
|
|
472
|
+
* template stays as `formulaTemplate` for the derived cards.
|
|
473
|
+
*/
|
|
474
|
+
function indexFields(
|
|
475
|
+
data: Record<string, unknown>,
|
|
476
|
+
at: string,
|
|
477
|
+
key: string,
|
|
478
|
+
latex: string,
|
|
479
|
+
formula: string | undefined,
|
|
480
|
+
): {
|
|
481
|
+
reads?: string;
|
|
482
|
+
range?: string;
|
|
483
|
+
fixedIndex?: true;
|
|
484
|
+
formula?: string;
|
|
485
|
+
formulaTemplate?: string;
|
|
486
|
+
} {
|
|
487
|
+
const reads = optionalString(data.reads, `${at}.reads`);
|
|
488
|
+
const range = optionalString(data.range, `${at}.range`);
|
|
489
|
+
const fixedIndex =
|
|
490
|
+
data.fixedIndex === undefined ? false : boolean(data.fixedIndex, `${at}.fixedIndex`);
|
|
491
|
+
const template =
|
|
492
|
+
formula !== undefined && isIndexEntryLatex(latex) ? formula : undefined;
|
|
493
|
+
return {
|
|
494
|
+
...(reads === undefined ? {} : { reads }),
|
|
495
|
+
...(range === undefined ? {} : { range }),
|
|
496
|
+
...(fixedIndex ? { fixedIndex: true as const } : {}),
|
|
497
|
+
...(template === undefined
|
|
498
|
+
? {}
|
|
499
|
+
: { formula: indexEntryFormula(template, key), formulaTemplate: template }),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
474
503
|
function localDefinition(
|
|
475
504
|
raw: unknown,
|
|
476
505
|
file: string,
|
|
@@ -483,10 +512,6 @@ function localDefinition(
|
|
|
483
512
|
const meaning = meaningText(data.meaning, `${at}.meaning`);
|
|
484
513
|
const formula = optionalString(data.formula, `${at}.formula`);
|
|
485
514
|
const units = optionalString(data.units, `${at}.units`);
|
|
486
|
-
const definitionText = [meaning, formula]
|
|
487
|
-
.filter((value): value is string => value !== undefined)
|
|
488
|
-
.join('\n');
|
|
489
|
-
|
|
490
515
|
return {
|
|
491
516
|
key,
|
|
492
517
|
notation: latex,
|
|
@@ -498,11 +523,15 @@ function localDefinition(
|
|
|
498
523
|
data.alignment === undefined
|
|
499
524
|
? { kind: 'general', rationale: 'Lesson-local symbol.' }
|
|
500
525
|
: alignment(data.alignment, `${at}.alignment`),
|
|
501
|
-
references:
|
|
526
|
+
references: [
|
|
527
|
+
...extractNotationReferences(meaning, file, 'definition'),
|
|
528
|
+
...(formula === undefined ? [] : extractNotationReferences(formula, file, 'formula')),
|
|
529
|
+
],
|
|
502
530
|
glosses: glossEntries(data.glosses, key, `${at}.glosses`),
|
|
503
531
|
source: { file },
|
|
504
532
|
...(formula === undefined ? {} : { formula }),
|
|
505
533
|
...(units === undefined ? {} : { units }),
|
|
534
|
+
...indexFields(data, at, key, latex, formula),
|
|
506
535
|
};
|
|
507
536
|
}
|
|
508
537
|
|
|
@@ -527,15 +556,22 @@ function sharedNotationEntries(): SharedNotationDefinitionInput[] {
|
|
|
527
556
|
domain: string(data.domain, `${file} domain`),
|
|
528
557
|
sources: sourceIds(data.sources, `${file} sources`),
|
|
529
558
|
seeAlso: strings(data.seeAlso, `${file} seeAlso`),
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
559
|
+
// The collection schema's defaults (`compiler/content-schemas.ts`).
|
|
560
|
+
alignment:
|
|
561
|
+
data.alignment === undefined
|
|
562
|
+
? { kind: 'general', rationale: 'General notation.' }
|
|
563
|
+
: alignment(data.alignment, `${file} alignment`),
|
|
564
|
+
aiAssisted:
|
|
565
|
+
data.aiAssisted === undefined
|
|
566
|
+
? false
|
|
567
|
+
: boolean(data.aiAssisted, `${file} aiAssisted`),
|
|
533
568
|
body,
|
|
534
569
|
references: extractNotationReferences(body, file, undefined),
|
|
535
570
|
glosses: glossEntries(data.glosses, key, `${file} glosses`),
|
|
536
571
|
source: { file },
|
|
537
572
|
...(formula === undefined ? {} : { formula }),
|
|
538
573
|
...(units === undefined ? {} : { units }),
|
|
574
|
+
...indexFields(data, file, key, string(data.latex, `${file} latex`), formula),
|
|
539
575
|
};
|
|
540
576
|
});
|
|
541
577
|
}
|
|
@@ -629,7 +665,6 @@ function notationLessonEntries(): NotationLessonInput[] {
|
|
|
629
665
|
);
|
|
630
666
|
lessons.push({
|
|
631
667
|
lessonId: lessonIdFromSlug(slug),
|
|
632
|
-
status: status(data.editorialStatus, `${file} editorialStatus`),
|
|
633
668
|
localDefinitions,
|
|
634
669
|
suspended: notationSuspensions(notation.suspend, localDefinitions, file),
|
|
635
670
|
references: extractNotationReferences(body, file, undefined),
|
|
@@ -680,7 +715,6 @@ export function loadNonChapterPages(): NonChapterPage[] {
|
|
|
680
715
|
bodyLine,
|
|
681
716
|
notation: {
|
|
682
717
|
lessonId: id,
|
|
683
|
-
status: status(data.editorialStatus ?? 'draft', `${file} editorialStatus`),
|
|
684
718
|
localDefinitions: [],
|
|
685
719
|
suspended: [],
|
|
686
720
|
references: extractNotationReferences(body, file, undefined),
|
|
@@ -712,6 +746,45 @@ export function loadNotationDefinitions(): Record<string, unknown>[] {
|
|
|
712
746
|
);
|
|
713
747
|
}
|
|
714
748
|
|
|
749
|
+
/**
|
|
750
|
+
* Every content file's top-level fields and text: the frontmatter of a page,
|
|
751
|
+
* a notation entry or an abbreviation, and the object of a competency,
|
|
752
|
+
* assessment, source or track. The typed loaders above drop keys they do not
|
|
753
|
+
* know; the retired-key gate (`reference/gate-retired-keys.ts`) needs them.
|
|
754
|
+
*/
|
|
755
|
+
export function loadContentRecords(): ContentRecord[] {
|
|
756
|
+
const below = (directory: string, extensions: readonly string[]) => {
|
|
757
|
+
const path = join(CONTENT_ROOT, directory);
|
|
758
|
+
return existsSync(path) ? filesBelow(path, extensions) : [];
|
|
759
|
+
};
|
|
760
|
+
const records: ContentRecord[] = [];
|
|
761
|
+
for (const directory of ['docs', 'notation', 'abbreviations']) {
|
|
762
|
+
for (const path of below(directory, ['.md', '.mdx'])) {
|
|
763
|
+
const text = readFileSync(path, 'utf8');
|
|
764
|
+
records.push({
|
|
765
|
+
file: relative(REPOSITORY_ROOT, path),
|
|
766
|
+
data: (parseFrontmatter(text).frontmatter ?? {}) as Record<string, unknown>,
|
|
767
|
+
text,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
for (const directory of ['competencies', 'assessments', 'sources', 'tracks']) {
|
|
772
|
+
for (const path of below(directory, ['.json'])) {
|
|
773
|
+
const text = readFileSync(path, 'utf8');
|
|
774
|
+
const data: unknown = JSON.parse(text);
|
|
775
|
+
records.push({
|
|
776
|
+
file: relative(REPOSITORY_ROOT, path),
|
|
777
|
+
data:
|
|
778
|
+
data !== null && typeof data === 'object' && !Array.isArray(data)
|
|
779
|
+
? (data as Record<string, unknown>)
|
|
780
|
+
: {},
|
|
781
|
+
text,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return records;
|
|
786
|
+
}
|
|
787
|
+
|
|
715
788
|
/** Parsed `src/content/sources/*.json` records for `remark-citation`. */
|
|
716
789
|
export function loadSourceRecords(): Record<string, unknown>[] {
|
|
717
790
|
return jsonEntries<SourceDefinition & { readonly id: string }>(
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The content-schema check (invariant `content-schema`). Every content file
|
|
3
|
+
* must satisfy its collection's schema — the same zod schema `astro build`
|
|
4
|
+
* loads it with (`content-schemas.ts`) — so `pnpm validate:content` and
|
|
5
|
+
* `pnpm content check` reject what the build would, at the key's line,
|
|
6
|
+
* instead of passing it and leaving `astro build` to fail with
|
|
7
|
+
* `InvalidContentEntryDataError`.
|
|
8
|
+
*/
|
|
9
|
+
import type { CurriculumIssue } from '../curriculum/validation';
|
|
10
|
+
import { keyLine, type ContentRecord } from '../reference/gate-retired-keys';
|
|
11
|
+
import { CONTENT_SCHEMAS } from './content-schemas';
|
|
12
|
+
|
|
13
|
+
export type ContentCollection = keyof typeof CONTENT_SCHEMAS;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The collection a repository-relative content file belongs to, or `null`
|
|
17
|
+
* for one Astro's loaders skip (`src/content-config.ts`): a docs page whose
|
|
18
|
+
* name starts with `_`, a notation or abbreviation file that is not `.md`.
|
|
19
|
+
*/
|
|
20
|
+
export function collectionOf(file: string): ContentCollection | null {
|
|
21
|
+
const parts = file.replace(/\\/g, '/').split('/');
|
|
22
|
+
if (parts[0] !== 'content' || parts.length < 3) return null;
|
|
23
|
+
const directory = parts[1] ?? '';
|
|
24
|
+
const name = parts[parts.length - 1] ?? '';
|
|
25
|
+
if (!Object.hasOwn(CONTENT_SCHEMAS, directory)) return null;
|
|
26
|
+
const collection = directory as ContentCollection;
|
|
27
|
+
if (collection === 'docs') {
|
|
28
|
+
return /\.mdx?$/.test(name) && !name.startsWith('_') ? collection : null;
|
|
29
|
+
}
|
|
30
|
+
if (collection === 'notation' || collection === 'abbreviations') {
|
|
31
|
+
return name.endsWith('.md') ? collection : null;
|
|
32
|
+
}
|
|
33
|
+
return name.endsWith('.json') ? collection : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The 1-based line of the value at `path` in a YAML or JSON `text`: each name
|
|
38
|
+
* is searched for after the previous one, and an array index skips that many
|
|
39
|
+
* `- ` items of a YAML list. Falls back to the deepest line found.
|
|
40
|
+
*/
|
|
41
|
+
export function lineOfPath(
|
|
42
|
+
text: string,
|
|
43
|
+
path: readonly (string | number)[],
|
|
44
|
+
): number | undefined {
|
|
45
|
+
const first = path[0];
|
|
46
|
+
if (typeof first !== 'string') return undefined;
|
|
47
|
+
const start = keyLine(text, first);
|
|
48
|
+
if (start === undefined) return undefined;
|
|
49
|
+
const lines = text.split('\n');
|
|
50
|
+
let at = start - 1;
|
|
51
|
+
for (const segment of path.slice(1)) {
|
|
52
|
+
if (typeof segment === 'number') {
|
|
53
|
+
let seen = -1;
|
|
54
|
+
let found = -1;
|
|
55
|
+
for (let index = at + 1; index < lines.length; index += 1) {
|
|
56
|
+
if (/^\s*-\s/.test(lines[index] ?? '')) seen += 1;
|
|
57
|
+
if (seen === segment) {
|
|
58
|
+
found = index;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (found === -1) break;
|
|
63
|
+
at = found;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const name = segment.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
67
|
+
const pattern = new RegExp(`^[\\s-]*(?:"${name}"|${name})[ \\t]*:`);
|
|
68
|
+
let found = -1;
|
|
69
|
+
for (let index = at; index < lines.length; index += 1) {
|
|
70
|
+
if (pattern.test(lines[index] ?? '')) {
|
|
71
|
+
found = index;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (found === -1) break;
|
|
76
|
+
at = found;
|
|
77
|
+
}
|
|
78
|
+
return at + 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** One `content-schema` issue per schema problem in each record, in record order. */
|
|
82
|
+
export function validateContentSchemas(
|
|
83
|
+
records: readonly ContentRecord[],
|
|
84
|
+
): CurriculumIssue[] {
|
|
85
|
+
const issues: CurriculumIssue[] = [];
|
|
86
|
+
for (const { file, data, text } of records) {
|
|
87
|
+
const collection = collectionOf(file);
|
|
88
|
+
if (!collection) continue;
|
|
89
|
+
const result = CONTENT_SCHEMAS[collection].safeParse(data);
|
|
90
|
+
if (result.success) continue;
|
|
91
|
+
for (const problem of result.error.issues) {
|
|
92
|
+
const path = problem.path.filter(
|
|
93
|
+
(segment): segment is string | number =>
|
|
94
|
+
typeof segment === 'string' || typeof segment === 'number',
|
|
95
|
+
);
|
|
96
|
+
const line = lineOfPath(text, path);
|
|
97
|
+
const at = line === undefined ? file : `${file}:${line}`;
|
|
98
|
+
const field = path.length > 0 ? `${path.join('.')}: ` : '';
|
|
99
|
+
issues.push({
|
|
100
|
+
kind: 'content-schema',
|
|
101
|
+
message: `${at}: ${field}${problem.message} (the ${collection} collection's schema, which astro build applies too)`,
|
|
102
|
+
file,
|
|
103
|
+
...(line === undefined ? {} : { line }),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return issues;
|
|
108
|
+
}
|