explico 0.13.1 → 0.15.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 +990 -1
- package/README.md +200 -30
- package/dist/compiler/astro.d.ts +15 -2
- package/dist/compiler/astro.js +40 -11
- package/dist/compiler/cli-main.d.ts +100 -0
- package/dist/compiler/cli-main.js +433 -0
- package/dist/compiler/cli-reference.d.ts +50 -0
- package/dist/compiler/cli-reference.js +213 -0
- package/dist/compiler/cli-verbs.d.ts +45 -0
- package/dist/compiler/cli-verbs.js +224 -0
- package/dist/compiler/cli-verify.d.ts +51 -0
- package/dist/compiler/cli-verify.js +151 -0
- package/dist/compiler/cli.d.ts +408 -24
- package/dist/compiler/cli.js +1328 -8
- package/dist/compiler/collections.d.ts +88 -1
- package/dist/compiler/collections.js +170 -25
- package/dist/compiler/content-release.d.ts +49 -0
- package/dist/compiler/content-release.js +63 -0
- package/dist/compiler/content-schemas.d.ts +2 -2
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +105 -1
- package/dist/compiler/decks.d.ts +161 -0
- package/dist/compiler/decks.js +235 -0
- package/dist/compiler/exports/combined-html.d.ts +5 -1
- package/dist/compiler/exports/deck-fit.d.ts +55 -0
- package/dist/compiler/exports/deck-fit.js +90 -0
- package/dist/compiler/exports/export-chapters.d.ts +37 -6
- package/dist/compiler/exports/export-chapters.js +64 -5
- package/dist/compiler/exports/export-pages.d.ts +30 -3
- package/dist/compiler/exports/export-pages.js +24 -1
- package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/dist/compiler/exports/latex/cache.d.ts +78 -0
- package/dist/compiler/exports/latex/cache.js +129 -0
- package/dist/compiler/exports/latex/capture.d.ts +10 -1
- package/dist/compiler/exports/latex/capture.js +3 -1
- package/dist/compiler/exports/latex/compile.d.ts +13 -2
- package/dist/compiler/exports/latex/compile.js +12 -1
- package/dist/compiler/exports/latex/deck.d.ts +92 -0
- package/dist/compiler/exports/latex/deck.js +349 -0
- package/dist/compiler/exports/latex/document.d.ts +51 -6
- package/dist/compiler/exports/latex/document.js +71 -12
- package/dist/compiler/exports/latex/explico-common.sty +198 -0
- package/dist/compiler/exports/latex/explico.cls +13 -183
- package/dist/compiler/exports/latex/serialize.d.ts +60 -4
- package/dist/compiler/exports/latex/serialize.js +4 -2
- package/dist/compiler/exports/latex-export.d.ts +24 -2
- package/dist/compiler/exports/latex-export.js +107 -26
- package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
- package/dist/compiler/exports/markdown/serialize.js +85 -8
- package/dist/compiler/exports/markdown-export.d.ts +6 -1
- package/dist/compiler/exports/markdown-export.js +50 -11
- package/dist/compiler/exports/standalone-html.d.ts +10 -5
- package/dist/compiler/exports/standalone-html.js +3 -3
- package/dist/compiler/exports/static-tree.d.ts +38 -1
- package/dist/compiler/exports/static-tree.js +120 -4
- package/dist/compiler/knowledge-version.d.ts +26 -2
- package/dist/compiler/knowledge-version.js +27 -17
- package/dist/compiler/lesson-derivation.d.ts +143 -6
- package/dist/compiler/lesson-derivation.js +145 -13
- package/dist/compiler/manifest.d.ts +133 -4
- package/dist/compiler/manifest.js +364 -58
- package/dist/compiler/page-versions.d.ts +184 -0
- package/dist/compiler/page-versions.js +217 -0
- package/dist/compiler/render-diagnostics.d.ts +5 -1
- package/dist/compiler/resolution-report.d.ts +24 -6
- package/dist/compiler/resolution-report.js +22 -4
- package/dist/compiler/validate-content.d.ts +7 -0
- package/dist/compiler/validate-content.js +160 -0
- package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
- package/dist/curriculum/competency-graph.d.ts +54 -0
- package/dist/curriculum/competency-graph.js +41 -0
- package/dist/reference/base-library.d.mts +8 -0
- package/dist/reference/base-library.mjs +19 -0
- package/dist/reference/consistency.d.ts +8 -4
- package/dist/reference/consistency.js +82 -26
- package/dist/reference/deck-remark.d.mts +123 -0
- package/dist/reference/deck-remark.mjs +600 -0
- package/dist/reference/deck-source.d.ts +107 -0
- package/dist/reference/deck-source.js +140 -0
- package/dist/reference/decks-validate.d.ts +133 -0
- package/dist/reference/decks-validate.js +359 -0
- package/dist/reference/derived-entries.d.ts +5 -1
- package/dist/reference/diagram-fit.d.ts +6 -2
- package/dist/reference/diagram-fit.js +5 -1
- package/dist/reference/figure-symbols.d.ts +23 -6
- package/dist/reference/figure-symbols.js +51 -15
- package/dist/reference/float-name.d.ts +43 -0
- package/dist/reference/float-name.js +58 -0
- package/dist/reference/folds-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-indices.js +5 -4
- package/dist/reference/gate-math.d.ts +29 -2
- package/dist/reference/gate-math.js +99 -18
- package/dist/reference/gate-part-pages.d.ts +1 -1
- package/dist/reference/lab-katex-validate.d.ts +10 -6
- package/dist/reference/lab-katex-validate.js +17 -9
- package/dist/reference/lab-math-scope.js +29 -4
- package/dist/reference/math-glyphs.d.mts +25 -0
- package/dist/reference/math-glyphs.mjs +189 -25
- package/dist/reference/page-notation.d.ts +22 -3
- package/dist/reference/page-notation.js +32 -10
- package/dist/reference/references.d.ts +14 -1
- package/dist/reference/references.js +13 -0
- package/dist/reference/registry-scope.d.mts +33 -0
- package/dist/reference/registry-scope.mjs +34 -0
- package/dist/reference/registry.js +29 -10
- package/dist/reference/rehype-deck.d.mts +6 -0
- package/dist/reference/rehype-deck.mjs +286 -0
- package/dist/reference/rehype-heading-anchors.d.mts +20 -0
- package/dist/reference/rehype-heading-anchors.mjs +48 -6
- package/dist/reference/remark-notation.mjs +249 -40
- package/dist/reference/source-lines.d.mts +12 -0
- package/dist/reference/source-lines.mjs +65 -0
- package/dist/reference/types.d.ts +28 -0
- package/dist/rules/catalog.d.ts +21 -4
- package/dist/rules/catalog.js +237 -2
- package/dist/rules/course-rules.d.ts +5 -1
- package/dist/rules/course-rules.js +17 -3
- package/dist/rules/course-rules.schema.json +12 -0
- package/dist/rules/in-effect.d.ts +13 -3
- package/dist/rules/in-effect.js +34 -10
- package/dist/rules/rules.schema.json +280 -1
- package/dist/rules/settings.d.ts +1 -1
- package/dist/session/PreferenceStore.d.ts +6 -0
- package/dist/session/PreferenceStore.js +18 -1
- package/dist/session/deck-memory.d.ts +116 -0
- package/dist/session/deck-memory.js +143 -0
- package/dist/session/reading-memory.d.ts +96 -0
- package/dist/session/reading-memory.js +133 -0
- package/docs/VERSIONING.md +21 -18
- package/docs/authoring/README.md +31 -9
- package/docs/authoring/agents.md +72 -18
- package/docs/authoring/cli-reference.json +2172 -0
- package/docs/authoring/cli.md +887 -0
- package/docs/authoring/components.md +15 -0
- package/docs/authoring/deck.md +612 -0
- package/docs/authoring/migrating.md +1195 -16
- package/docs/authoring/notation.md +49 -6
- package/docs/authoring/structure.md +80 -1
- package/docs/authoring/versioning.md +56 -3
- package/docs/authoring/visualizations.md +7 -2
- package/docs/course-agent/AGENTS.md +40 -0
- package/docs/course-agent/agents/rules-reviewer.md +33 -0
- package/docs/course-agent/skills/draft-lesson/SKILL.md +38 -0
- package/docs/course-agent/skills/migrate-course/SKILL.md +32 -0
- package/docs/course-agent/skills/release-content/SKILL.md +26 -0
- package/docs/diagram-scaling.md +148 -0
- package/docs/privacy.md +59 -0
- package/package.json +8 -2
- package/src/compiler/astro.ts +60 -21
- package/src/compiler/cli-main.ts +557 -0
- package/src/compiler/cli-reference.ts +285 -0
- package/src/compiler/cli-verbs.ts +270 -0
- package/src/compiler/cli-verify.ts +241 -0
- package/src/compiler/cli.ts +2119 -170
- package/src/compiler/collections.ts +284 -25
- package/src/compiler/content-release.ts +91 -0
- package/src/compiler/course-config.ts +267 -0
- package/src/compiler/decks.ts +455 -0
- package/src/compiler/exports/deck-fit.ts +127 -0
- package/src/compiler/exports/export-chapters.ts +105 -6
- package/src/compiler/exports/export-pages.ts +46 -2
- package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/src/compiler/exports/latex/cache.ts +183 -0
- package/src/compiler/exports/latex/capture.ts +5 -2
- package/src/compiler/exports/latex/compile.ts +18 -0
- package/src/compiler/exports/latex/deck.ts +419 -0
- package/src/compiler/exports/latex/document.ts +117 -21
- package/src/compiler/exports/latex/explico-common.sty +198 -0
- package/src/compiler/exports/latex/explico.cls +13 -183
- package/src/compiler/exports/latex/serialize.ts +25 -21
- package/src/compiler/exports/latex/texlive-packages.txt +6 -2
- package/src/compiler/exports/latex-export.ts +187 -45
- package/src/compiler/exports/markdown/serialize.ts +118 -9
- package/src/compiler/exports/markdown-export.ts +56 -11
- package/src/compiler/exports/standalone-html.ts +8 -7
- package/src/compiler/exports/static-tree.ts +175 -3
- package/src/compiler/knowledge-version.ts +56 -21
- package/src/compiler/lesson-derivation.ts +282 -16
- package/src/compiler/manifest.ts +568 -63
- package/src/compiler/page-versions.ts +401 -0
- package/src/compiler/resolution-report.ts +38 -7
- package/src/compiler/validate-content.ts +215 -0
- package/src/components/CourseContents.astro +124 -6
- package/src/components/CurriculumMap.astro +49 -1
- package/src/components/FloatIndex.astro +1 -1
- package/src/components/PartOverview.astro +51 -5
- package/src/components/assessments/AssessmentSet.astro +41 -15
- package/src/components/block-heading.ts +15 -6
- package/src/components/card-carry.ts +7 -4
- package/src/components/card-manager.ts +14 -2
- package/src/components/card-memory.ts +13 -9
- package/src/components/deck/DeckEnhancer.astro +13 -0
- package/src/components/deck/deck-context.ts +104 -0
- package/src/components/deck/deck-controller.ts +1287 -0
- package/src/components/deck/deck-map.ts +91 -0
- package/src/components/deck-lane.ts +163 -0
- package/src/components/diagram/Diagram.astro +131 -86
- package/src/components/disclosure/persisted-disclosure.ts +9 -1
- package/src/components/figure/Figure.astro +7 -2
- package/src/components/figure/FloatCaption.astro +5 -3
- package/src/components/fold/Fold.astro +2 -2
- package/src/components/notation/NotationFormula.astro +81 -11
- package/src/components/notation/NotationGlossary.astro +132 -1
- package/src/components/notation/NotationLayer.astro +15 -7
- package/src/components/starlight/AboutPanel.astro +186 -3
- package/src/components/starlight/CollapsibleSections.astro +24 -3
- package/src/components/starlight/DiagramEnhancer.astro +5 -1
- package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
- package/src/components/starlight/LayoutHeader.astro +17 -0
- package/src/components/starlight/LayoutPageTitle.astro +110 -0
- package/src/components/starlight/LayoutSidebar.astro +807 -21
- package/src/components/starlight/LayoutSocialIcons.astro +62 -6
- package/src/components/starlight/LessonExportActions.astro +27 -9
- package/src/components/starlight/LessonFooter.astro +59 -0
- package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
- package/src/components/starlight/TextSizeControl.astro +24 -13
- package/src/components/starlight/page-marks.ts +139 -0
- package/src/components/starlight/section-summary.ts +1 -1
- package/src/components/starlight/sidebar-editions.ts +245 -0
- package/src/components/table/Table.astro +15 -7
- package/src/content-config.ts +87 -1
- package/src/curriculum/competency-graph.ts +95 -0
- package/src/reference/base-library.mjs +19 -0
- package/src/reference/consistency.ts +137 -39
- package/src/reference/deck-remark.mjs +600 -0
- package/src/reference/deck-source.ts +273 -0
- package/src/reference/decks-validate.ts +604 -0
- package/src/reference/diagram-fit.ts +18 -4
- package/src/reference/diagram-layout.ts +3 -1
- package/src/reference/figure-symbols.ts +108 -31
- package/src/reference/folds-validate.ts +1 -1
- package/src/reference/gate-indices.ts +5 -3
- package/src/reference/gate-math.ts +169 -25
- package/src/reference/lab-katex-validate.ts +30 -15
- package/src/reference/lab-math-scope.ts +34 -3
- package/src/reference/math-glyphs.mjs +189 -25
- package/src/reference/page-notation.ts +69 -15
- package/src/reference/references.ts +28 -0
- package/src/reference/registry-scope.mjs +34 -0
- package/src/reference/registry.ts +41 -11
- package/src/reference/rehype-deck.mjs +286 -0
- package/src/reference/rehype-heading-anchors.mjs +48 -6
- package/src/reference/remark-notation.mjs +249 -40
- package/src/reference/source-lines.mjs +65 -0
- package/src/reference/types.ts +28 -0
- package/src/rules/catalog.ts +274 -4
- package/src/rules/course-rules.ts +21 -1
- package/src/rules/in-effect.ts +58 -11
- package/src/session/PreferenceStore.ts +23 -0
- package/src/session/deck-memory.ts +263 -0
- package/src/session/reading-memory.ts +243 -0
- package/src/styles/deck.css +1150 -0
- package/src/styles/global.css +958 -27
- package/src/styles/zones.css +277 -0
- /package/src/{components → reference}/float-name.ts +0 -0
|
@@ -51,11 +51,19 @@ import { indexEntryFormula } from '../reference/derived-notation.mjs';
|
|
|
51
51
|
import { isIndexEntryLatex } from './content-schemas';
|
|
52
52
|
import type { ContentRecord } from '../reference/gate-retired-keys';
|
|
53
53
|
import {
|
|
54
|
+
deckLessonSlug,
|
|
55
|
+
deckPageId,
|
|
56
|
+
deckRouteSlug,
|
|
54
57
|
deriveRequires,
|
|
55
58
|
deriveSources,
|
|
59
|
+
isDeckSlug,
|
|
56
60
|
isLessonSlug,
|
|
61
|
+
isPartDeckSlug,
|
|
57
62
|
lessonIdFromSlug,
|
|
58
63
|
parseCheckList,
|
|
64
|
+
partDeckDir,
|
|
65
|
+
partDeckPageId,
|
|
66
|
+
partDeckRouteSlug,
|
|
59
67
|
} from './lesson-derivation';
|
|
60
68
|
|
|
61
69
|
// Every invocation — the CLI, `astro build`/`check`, and vitest — runs with the
|
|
@@ -325,14 +333,166 @@ function docSlug(path: string): string {
|
|
|
325
333
|
return relative(join(CONTENT_ROOT, 'docs'), path).replace(/\\/g, '/');
|
|
326
334
|
}
|
|
327
335
|
|
|
328
|
-
/**
|
|
336
|
+
/**
|
|
337
|
+
* Assessment ids from a lesson's colocated `<name>.checks.yml`, or `[]`. A
|
|
338
|
+
* deck file (`<name>.deck.mdx`) reads the same file as its chapter: the checks
|
|
339
|
+
* belong to the lesson, whichever edition shows them.
|
|
340
|
+
*/
|
|
329
341
|
function lessonCheckIds(lessonPath: string): string[] {
|
|
330
|
-
const checksPath = lessonPath.replace(
|
|
342
|
+
const checksPath = lessonPath.replace(/(?:\.deck)?\.mdx?$/, '.checks.yml');
|
|
331
343
|
return existsSync(checksPath)
|
|
332
344
|
? parseCheckList(readFileSync(checksPath, 'utf8'))
|
|
333
345
|
: [];
|
|
334
346
|
}
|
|
335
347
|
|
|
348
|
+
/** Every `.md` / `.mdx` file under `content/docs/`, sorted. */
|
|
349
|
+
function docsFiles(): string[] {
|
|
350
|
+
const root = join(CONTENT_ROOT, 'docs');
|
|
351
|
+
return existsSync(root) ? filesBelow(root, ['.md', '.mdx']) : [];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** What every deck file is, whichever it belongs to ({@link DeckFile}). */
|
|
355
|
+
interface DeckFileBase {
|
|
356
|
+
/** Absolute path of the deck file. */
|
|
357
|
+
readonly path: string;
|
|
358
|
+
/** Repository-relative path, the `file` of its diagnostics. */
|
|
359
|
+
readonly file: string;
|
|
360
|
+
/** Docs slug of the deck file, extension included: `motion/range.deck.mdx`. */
|
|
361
|
+
readonly slug: string;
|
|
362
|
+
/** Where the deck is served: `motion/range/slides`, `motion/range` alone, or a part's `motion/slides`. */
|
|
363
|
+
readonly route: string;
|
|
364
|
+
/** The id of the deck's page: `motion.range.slides`, the lesson's id alone, or a part's `motion.slides`. */
|
|
365
|
+
readonly pageId: string;
|
|
366
|
+
/** The part directory the deck sits in: `motion`; empty for a deck outside one. */
|
|
367
|
+
readonly partDir: string;
|
|
368
|
+
/** True when the deck sits in a part's directory, as a lesson does. */
|
|
369
|
+
readonly inPart: boolean;
|
|
370
|
+
/** The deck file's frontmatter. */
|
|
371
|
+
readonly data: Record<string, unknown>;
|
|
372
|
+
readonly body: string;
|
|
373
|
+
/** 1-based file line {@link body} starts on. */
|
|
374
|
+
readonly bodyLine: number;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* A lesson's deck (`<lesson>.deck.mdx`): its slide edition, beside the
|
|
379
|
+
* lesson's chapter or, for a lesson that is only slides, alone. See
|
|
380
|
+
* `docs/authoring/deck.md`.
|
|
381
|
+
*/
|
|
382
|
+
export interface LessonDeckFile extends DeckFileBase {
|
|
383
|
+
readonly scope: 'lesson';
|
|
384
|
+
/** The lesson's slug: `motion/range`. */
|
|
385
|
+
readonly lessonSlug: string;
|
|
386
|
+
readonly lessonId: string;
|
|
387
|
+
/** The lesson's chapter file, when it has one. */
|
|
388
|
+
readonly chapterPath?: string;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* A part's deck (`<part>/index.deck.mdx`, beside the part's page): the
|
|
393
|
+
* slides of the part, whose sections may name the chapters they cover. It
|
|
394
|
+
* belongs to no lesson.
|
|
395
|
+
*/
|
|
396
|
+
export interface PartDeckFile extends DeckFileBase {
|
|
397
|
+
readonly scope: 'part';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** A deck file: a lesson's slide edition, or a part's deck. */
|
|
401
|
+
export type DeckFile = LessonDeckFile | PartDeckFile;
|
|
402
|
+
|
|
403
|
+
/** A lesson's chapter file, `.mdx` before `.md`, if it has one. */
|
|
404
|
+
function chapterPathOf(lessonSlug: string): string | undefined {
|
|
405
|
+
for (const extension of ['.mdx', '.md']) {
|
|
406
|
+
const path = join(CONTENT_ROOT, 'docs', `${lessonSlug}${extension}`);
|
|
407
|
+
if (existsSync(path)) return path;
|
|
408
|
+
}
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Every deck file, by slug: each lesson's, and each part's. A part's deck
|
|
414
|
+
* (`<part>/index.deck.mdx`) is never read as the deck of a lesson
|
|
415
|
+
* `<part>/index` whose chapter is the part's page.
|
|
416
|
+
*/
|
|
417
|
+
export function loadDeckFiles(): DeckFile[] {
|
|
418
|
+
const decks: DeckFile[] = [];
|
|
419
|
+
for (const path of docsFiles()) {
|
|
420
|
+
const slug = docSlug(path);
|
|
421
|
+
if (!isDeckSlug(slug)) continue;
|
|
422
|
+
const { file, data, body, bodyLine } = parseDocument(path);
|
|
423
|
+
if (isPartDeckSlug(slug)) {
|
|
424
|
+
const partDir = partDeckDir(slug);
|
|
425
|
+
decks.push({
|
|
426
|
+
scope: 'part',
|
|
427
|
+
path,
|
|
428
|
+
file,
|
|
429
|
+
slug,
|
|
430
|
+
route: partDeckRouteSlug(partDir),
|
|
431
|
+
pageId: partDeckPageId(partDir),
|
|
432
|
+
partDir,
|
|
433
|
+
inPart: true,
|
|
434
|
+
data,
|
|
435
|
+
body,
|
|
436
|
+
bodyLine,
|
|
437
|
+
});
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const lessonSlug = deckLessonSlug(slug);
|
|
441
|
+
const lessonId = lessonIdFromSlug(lessonSlug);
|
|
442
|
+
const chapterPath = chapterPathOf(lessonSlug);
|
|
443
|
+
const hasChapter = chapterPath !== undefined;
|
|
444
|
+
decks.push({
|
|
445
|
+
scope: 'lesson',
|
|
446
|
+
path,
|
|
447
|
+
file,
|
|
448
|
+
slug,
|
|
449
|
+
lessonSlug,
|
|
450
|
+
lessonId,
|
|
451
|
+
...(chapterPath === undefined ? {} : { chapterPath }),
|
|
452
|
+
route: deckRouteSlug(lessonSlug, hasChapter),
|
|
453
|
+
pageId: deckPageId(lessonId, hasChapter),
|
|
454
|
+
partDir: lessonSlug.includes('/') ? lessonSlug.slice(0, lessonSlug.indexOf('/')) : '',
|
|
455
|
+
inPart: lessonSlug.includes('/') && !lessonSlug.endsWith('/index'),
|
|
456
|
+
data,
|
|
457
|
+
body,
|
|
458
|
+
bodyLine,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
return decks;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* A file that defines a lesson: every chapter, and every deck whose lesson has
|
|
466
|
+
* no chapter (a slides-first lesson). Its frontmatter is the lesson's —
|
|
467
|
+
* `teaches`, `assumptions`, `studyMinutes`, `notation` — and its body is what
|
|
468
|
+
* the lesson's numbering and curriculum passes read.
|
|
469
|
+
*/
|
|
470
|
+
export interface LessonFile {
|
|
471
|
+
readonly path: string;
|
|
472
|
+
/** Docs slug of the file, extension included. */
|
|
473
|
+
readonly slug: string;
|
|
474
|
+
readonly lessonSlug: string;
|
|
475
|
+
readonly lessonId: string;
|
|
476
|
+
readonly edition: 'chapter' | 'deck';
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Every lesson's defining file, sorted by lesson slug. */
|
|
480
|
+
export function loadLessonFiles(): LessonFile[] {
|
|
481
|
+
const lessons: LessonFile[] = [];
|
|
482
|
+
for (const path of docsFiles()) {
|
|
483
|
+
const slug = docSlug(path);
|
|
484
|
+
if (isLessonSlug(slug)) {
|
|
485
|
+
const lessonSlug = slug.replace(/\.(md|mdx)$/, '');
|
|
486
|
+
lessons.push({ path, slug, lessonSlug, lessonId: lessonIdFromSlug(slug), edition: 'chapter' });
|
|
487
|
+
} else if (isDeckSlug(slug) && slug.includes('/') && !isPartDeckSlug(slug)) {
|
|
488
|
+
const lessonSlug = deckLessonSlug(slug);
|
|
489
|
+
if (chapterPathOf(lessonSlug) !== undefined) continue;
|
|
490
|
+
lessons.push({ path, slug, lessonSlug, lessonId: lessonIdFromSlug(lessonSlug), edition: 'deck' });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return lessons.sort((left, right) => left.lessonSlug.localeCompare(right.lessonSlug));
|
|
494
|
+
}
|
|
495
|
+
|
|
336
496
|
/**
|
|
337
497
|
* One part's **overview page** — `content/docs/<dir>/index.mdx`, the page the
|
|
338
498
|
* part's own directory URL resolves to. `isLessonSlug` already excludes any
|
|
@@ -388,17 +548,26 @@ function lessonEntries(
|
|
|
388
548
|
prerequisitesById: ReadonlyMap<string, readonly string[]>,
|
|
389
549
|
): LessonDefinition[] {
|
|
390
550
|
const lessons: LessonDefinition[] = [];
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
551
|
+
// A deck beside its chapter cites what its chapter does not need to: its
|
|
552
|
+
// sources count for the lesson too.
|
|
553
|
+
const deckBodies = new Map(
|
|
554
|
+
loadDeckFiles()
|
|
555
|
+
.filter((deck): deck is LessonDeckFile => deck.scope === 'lesson' && deck.chapterPath !== undefined)
|
|
556
|
+
.map((deck) => [deck.lessonId, parseDocument(deck.path).body]),
|
|
557
|
+
);
|
|
558
|
+
for (const { path, lessonId } of loadLessonFiles()) {
|
|
394
559
|
const { data, body } = parseDocument(path);
|
|
395
560
|
const teaches = (data.teaches as string[] | undefined) ?? [];
|
|
561
|
+
const deckBody = deckBodies.get(lessonId);
|
|
396
562
|
lessons.push({
|
|
397
|
-
id:
|
|
563
|
+
id: lessonId,
|
|
398
564
|
requires: deriveRequires(teaches, prerequisitesById),
|
|
399
565
|
teaches,
|
|
400
566
|
assessments: lessonCheckIds(path),
|
|
401
|
-
sources:
|
|
567
|
+
sources:
|
|
568
|
+
deckBody === undefined
|
|
569
|
+
? deriveSources(body)
|
|
570
|
+
: [...new Set([...deriveSources(body), ...deriveSources(deckBody)])].sort(),
|
|
402
571
|
assumptions: (data.assumptions as string[] | undefined) ?? [],
|
|
403
572
|
body,
|
|
404
573
|
});
|
|
@@ -538,7 +707,7 @@ function localDefinition(
|
|
|
538
707
|
function sharedNotationEntries(): SharedNotationDefinitionInput[] {
|
|
539
708
|
const directory = join(CONTENT_ROOT, 'notation');
|
|
540
709
|
return filesBelow(directory, ['.md']).map((path) => {
|
|
541
|
-
const { file, data, body } = parseDocument(path);
|
|
710
|
+
const { file, data, body, bodyLine } = parseDocument(path);
|
|
542
711
|
const key = string(data.key, `${file} key`);
|
|
543
712
|
const filenameKey = basename(path, '.md');
|
|
544
713
|
if (key !== filenameKey) {
|
|
@@ -566,6 +735,7 @@ function sharedNotationEntries(): SharedNotationDefinitionInput[] {
|
|
|
566
735
|
? false
|
|
567
736
|
: boolean(data.aiAssisted, `${file} aiAssisted`),
|
|
568
737
|
body,
|
|
738
|
+
bodyLine,
|
|
569
739
|
references: extractNotationReferences(body, file, undefined),
|
|
570
740
|
glosses: glossEntries(data.glosses, key, `${file} glosses`),
|
|
571
741
|
source: { file },
|
|
@@ -652,21 +822,31 @@ export function notationSuspensions(
|
|
|
652
822
|
});
|
|
653
823
|
}
|
|
654
824
|
|
|
825
|
+
/** A lesson file's `notation.local` entries and `notation.suspend` list, parsed. */
|
|
826
|
+
function lessonNotationOf(
|
|
827
|
+
file: string,
|
|
828
|
+
data: Record<string, unknown>,
|
|
829
|
+
): { localDefinitions: LocalNotationDefinitionInput[]; suspended: NotationSuspension[] } {
|
|
830
|
+
const notation = record(data.notation ?? {}, `${file} notation`);
|
|
831
|
+
const local = Array.isArray(notation.local) ? notation.local : [];
|
|
832
|
+
const localDefinitions = local.map((entry, index) =>
|
|
833
|
+
localDefinition(entry, file, index),
|
|
834
|
+
);
|
|
835
|
+
return {
|
|
836
|
+
localDefinitions,
|
|
837
|
+
suspended: notationSuspensions(notation.suspend, localDefinitions, file),
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
655
841
|
function notationLessonEntries(): NotationLessonInput[] {
|
|
656
842
|
const lessons: NotationLessonInput[] = [];
|
|
657
|
-
for (const path of
|
|
658
|
-
const slug = docSlug(path);
|
|
659
|
-
if (!isLessonSlug(slug)) continue;
|
|
843
|
+
for (const { path, lessonId } of loadLessonFiles()) {
|
|
660
844
|
const { file, data, body, bodyLine } = parseDocument(path);
|
|
661
|
-
const
|
|
662
|
-
const local = Array.isArray(notation.local) ? notation.local : [];
|
|
663
|
-
const localDefinitions = local.map((entry, index) =>
|
|
664
|
-
localDefinition(entry, file, index),
|
|
665
|
-
);
|
|
845
|
+
const { localDefinitions, suspended } = lessonNotationOf(file, data);
|
|
666
846
|
lessons.push({
|
|
667
|
-
lessonId
|
|
847
|
+
lessonId,
|
|
668
848
|
localDefinitions,
|
|
669
|
-
suspended
|
|
849
|
+
suspended,
|
|
670
850
|
references: extractNotationReferences(body, file, undefined),
|
|
671
851
|
body,
|
|
672
852
|
bodyLine,
|
|
@@ -676,6 +856,87 @@ function notationLessonEntries(): NotationLessonInput[] {
|
|
|
676
856
|
return lessons;
|
|
677
857
|
}
|
|
678
858
|
|
|
859
|
+
/**
|
|
860
|
+
* A deck beside its chapter, as a page of the notation registry: its own id
|
|
861
|
+
* (`<lessonId>.slides`), no entries of its own, and its chapter's scope — the
|
|
862
|
+
* chapter's `notation.local` entries are the lesson's, so a deck names them
|
|
863
|
+
* as the chapter does, and it suspends what the chapter suspends. A part's
|
|
864
|
+
* deck (`<part>.slides`) has its part's scope: every chapter of the part, in
|
|
865
|
+
* the order the tracks read them, a key one chapter defines locally binding
|
|
866
|
+
* to the first that does; it suspends what any of them suspends. A deck that
|
|
867
|
+
* is a lesson's only edition is not here: it is that lesson's page
|
|
868
|
+
* (`loadNotationRegistryInput`).
|
|
869
|
+
*/
|
|
870
|
+
export interface DeckNotationPage {
|
|
871
|
+
readonly deck: DeckFile;
|
|
872
|
+
readonly data: Record<string, unknown>;
|
|
873
|
+
readonly notation: NotationLessonInput;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export function loadDeckNotationPages(
|
|
877
|
+
decks: readonly DeckFile[] = loadDeckFiles(),
|
|
878
|
+
{
|
|
879
|
+
order = (lessonIds) => lessonIds,
|
|
880
|
+
}: {
|
|
881
|
+
/** The lessons of a part in reading order (the tracks'); by slug without it. */
|
|
882
|
+
readonly order?: (lessonIds: readonly string[]) => readonly string[];
|
|
883
|
+
} = {},
|
|
884
|
+
): DeckNotationPage[] {
|
|
885
|
+
const pages: DeckNotationPage[] = [];
|
|
886
|
+
const chapterFiles = loadLessonFiles().filter((lesson) => lesson.edition === 'chapter');
|
|
887
|
+
for (const deck of decks) {
|
|
888
|
+
let chapters: { readonly lessonId: string; readonly path: string }[];
|
|
889
|
+
if (deck.scope === 'part') {
|
|
890
|
+
const inPart = chapterFiles.filter((lesson) => lesson.lessonSlug.startsWith(`${deck.partDir}/`));
|
|
891
|
+
const pathById = new Map(inPart.map((lesson) => [lesson.lessonId, lesson.path]));
|
|
892
|
+
chapters = order(inPart.map((lesson) => lesson.lessonId)).flatMap((lessonId) => {
|
|
893
|
+
const path = pathById.get(lessonId);
|
|
894
|
+
return path === undefined ? [] : [{ lessonId, path }];
|
|
895
|
+
});
|
|
896
|
+
} else if (deck.chapterPath !== undefined) {
|
|
897
|
+
chapters = [{ lessonId: deck.lessonId, path: deck.chapterPath }];
|
|
898
|
+
} else {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
const inheritedLocalDefinitions: LocalNotationDefinitionInput[] = [];
|
|
902
|
+
const suspended: NotationSuspension[] = [];
|
|
903
|
+
const inheritedKeys = new Set<string>();
|
|
904
|
+
for (const { path } of chapters) {
|
|
905
|
+
const chapter = parseDocument(path);
|
|
906
|
+
const notation = lessonNotationOf(chapter.file, chapter.data);
|
|
907
|
+
for (const definition of notation.localDefinitions) {
|
|
908
|
+
if (!inheritedLocalDefinitions.some((known) => known.key === definition.key)) {
|
|
909
|
+
inheritedLocalDefinitions.push(definition);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
for (const suspension of notation.suspended) {
|
|
913
|
+
if (!suspended.some((known) => known.operator === suspension.operator)) suspended.push(suspension);
|
|
914
|
+
}
|
|
915
|
+
for (const reference of extractNotationReferences(chapter.body, chapter.file, undefined)) {
|
|
916
|
+
inheritedKeys.add(reference.key);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const { file, data, body, bodyLine } = deck;
|
|
920
|
+
pages.push({
|
|
921
|
+
deck,
|
|
922
|
+
data,
|
|
923
|
+
notation: {
|
|
924
|
+
lessonId: deck.pageId,
|
|
925
|
+
localDefinitions: [],
|
|
926
|
+
suspended,
|
|
927
|
+
scopeLessonIds: chapters.map((chapter) => chapter.lessonId),
|
|
928
|
+
inheritedLocalDefinitions,
|
|
929
|
+
inheritedKeys: [...inheritedKeys].sort(),
|
|
930
|
+
references: extractNotationReferences(body, file, undefined),
|
|
931
|
+
body,
|
|
932
|
+
bodyLine,
|
|
933
|
+
source: { file },
|
|
934
|
+
},
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
return pages;
|
|
938
|
+
}
|
|
939
|
+
|
|
679
940
|
/**
|
|
680
941
|
* A docs page that is **not a chapter** (rule S10): the landing page, a part's
|
|
681
942
|
* overview page, and any other page outside a part directory — every slug
|
|
@@ -701,9 +962,10 @@ export interface NonChapterPage {
|
|
|
701
962
|
|
|
702
963
|
export function loadNonChapterPages(): NonChapterPage[] {
|
|
703
964
|
const pages: NonChapterPage[] = [];
|
|
704
|
-
for (const path of
|
|
965
|
+
for (const path of docsFiles()) {
|
|
705
966
|
const slug = docSlug(path);
|
|
706
|
-
|
|
967
|
+
// A deck is an edition of a lesson (`loadDeckFiles`), not a page of its own kind.
|
|
968
|
+
if (isLessonSlug(slug) || isDeckSlug(slug)) continue;
|
|
707
969
|
const { file, data, body, bodyLine } = parseDocument(path);
|
|
708
970
|
const id = lessonIdFromSlug(slug);
|
|
709
971
|
pages.push({
|
|
@@ -799,7 +1061,7 @@ export function loadSourceRecords(): Record<string, unknown>[] {
|
|
|
799
1061
|
*/
|
|
800
1062
|
export function loadLessonChecks(lessonId: string): string[] {
|
|
801
1063
|
const slug = lessonId.replace(/\./g, '/');
|
|
802
|
-
for (const extension of ['.mdx', '.md']) {
|
|
1064
|
+
for (const extension of ['.mdx', '.md', '.deck.mdx', '.deck.md']) {
|
|
803
1065
|
const lessonPath = join(CONTENT_ROOT, 'docs', `${slug}${extension}`);
|
|
804
1066
|
if (existsSync(lessonPath)) return lessonCheckIds(lessonPath);
|
|
805
1067
|
}
|
|
@@ -861,11 +1123,8 @@ export function loadNotationSpecs(): NotationSpec[] {
|
|
|
861
1123
|
});
|
|
862
1124
|
}
|
|
863
1125
|
|
|
864
|
-
for (const path of
|
|
865
|
-
const slug = docSlug(path);
|
|
866
|
-
if (!isLessonSlug(slug)) continue;
|
|
1126
|
+
for (const { path, lessonId } of loadLessonFiles()) {
|
|
867
1127
|
const { file, data } = parseDocument(path);
|
|
868
|
-
const lessonId = lessonIdFromSlug(slug);
|
|
869
1128
|
const local = (data as { notation?: { local?: unknown } }).notation?.local;
|
|
870
1129
|
if (!Array.isArray(local)) continue;
|
|
871
1130
|
for (const raw of local) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `content release [major|minor|patch]` — cut a course content release (rule
|
|
3
|
+
* C3): `## [Unreleased]` in `content/CHANGELOG.md` becomes a dated
|
|
4
|
+
* `## [x.y.z]` heading and `content/course.config.ts` `about.version` becomes
|
|
5
|
+
* `x.y.z`, in one plan, shown as a diff and written only on `--write`, like
|
|
6
|
+
* the other edit verbs. The bump defaults to what the section's own headings
|
|
7
|
+
* imply (rule C2): `### Breaking` or `### Removed` a MAJOR, `### Added` a
|
|
8
|
+
* MINOR, anything else a PATCH.
|
|
9
|
+
*
|
|
10
|
+
* Each page's own version (rule C5) follows on the next `validate:content`,
|
|
11
|
+
* which records the release in `content/versions.json`; the release commit
|
|
12
|
+
* carries that file too.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { ContentCliError, type EditPlan } from './cli';
|
|
18
|
+
|
|
19
|
+
export type ReleaseBump = 'major' | 'minor' | 'patch';
|
|
20
|
+
|
|
21
|
+
const CHANGELOG = 'content/CHANGELOG.md';
|
|
22
|
+
const CONFIG = 'content/course.config.ts';
|
|
23
|
+
|
|
24
|
+
export interface ReleaseEdit {
|
|
25
|
+
readonly from: string;
|
|
26
|
+
readonly to: string;
|
|
27
|
+
readonly bump: ReleaseBump;
|
|
28
|
+
/** True when the bump was read from the section's headings, not asked for. */
|
|
29
|
+
readonly inferred: boolean;
|
|
30
|
+
readonly changelog: string;
|
|
31
|
+
readonly config: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The release of these two files' text on `date` (`YYYY-MM-DD`); `null` when `[Unreleased]` has no entry. */
|
|
35
|
+
export function releaseEdit(changelog: string, config: string, date: string, requested?: ReleaseBump): ReleaseEdit | null {
|
|
36
|
+
const unreleased = /^## \[Unreleased\]\r?$/m.exec(changelog);
|
|
37
|
+
if (!unreleased) throw new ContentCliError('no-changelog', `${CHANGELOG} has no "## [Unreleased]" heading.`);
|
|
38
|
+
const bodyStart = unreleased.index + unreleased[0].length;
|
|
39
|
+
const released = /^## \[\d+\.\d+\.\d+\](?: - .*?)?\r?$/m.exec(changelog.slice(bodyStart));
|
|
40
|
+
if (!released) throw new ContentCliError('no-changelog', `${CHANGELOG} has no released "## [x.y.z]" heading below "## [Unreleased]".`);
|
|
41
|
+
const bodyEnd = bodyStart + released.index;
|
|
42
|
+
const body = changelog.slice(bodyStart, bodyEnd);
|
|
43
|
+
if (!/^-\s+/m.test(body)) return null;
|
|
44
|
+
|
|
45
|
+
const version = /(about:\s*\{[\s\S]*?\bversion:\s*['"])(\d+)\.(\d+)\.(\d+)(['"])/.exec(config);
|
|
46
|
+
if (!version) throw new ContentCliError('unknown-version', `No about.version "x.y.z" in ${CONFIG}.`);
|
|
47
|
+
const [major, minor, patch] = version.slice(2, 5).map(Number) as [number, number, number];
|
|
48
|
+
const implied: ReleaseBump = /^### (Breaking|Removed)\s*$/m.test(body) ? 'major' : /^### Added\s*$/m.test(body) ? 'minor' : 'patch';
|
|
49
|
+
const bump = requested ?? implied;
|
|
50
|
+
const to = bump === 'major' ? `${major + 1}.0.0` : bump === 'minor' ? `${major}.${minor + 1}.0` : `${major}.${minor}.${patch + 1}`;
|
|
51
|
+
return {
|
|
52
|
+
from: `${major}.${minor}.${patch}`,
|
|
53
|
+
to,
|
|
54
|
+
bump,
|
|
55
|
+
inferred: requested === undefined,
|
|
56
|
+
changelog: `${changelog.slice(0, bodyStart)}\n\n## [${to}] - ${date}${body}${changelog.slice(bodyEnd)}`,
|
|
57
|
+
config: config.replace(version[0], `${version[1]}${to}${version[5]}`),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The release as an edit plan over the course in the working directory. */
|
|
62
|
+
/** Today where the release is cut, `YYYY-MM-DD`: a release at 9 pm is dated that day, not the next one in UTC. */
|
|
63
|
+
function localDate(now = new Date()): string {
|
|
64
|
+
return [now.getFullYear(), now.getMonth() + 1, now.getDate()].map((n, i) => String(n).padStart(i === 0 ? 4 : 2, '0')).join('-');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function planContentRelease(requested?: string, date = localDate()): EditPlan {
|
|
68
|
+
if (requested !== undefined && requested !== 'major' && requested !== 'minor' && requested !== 'patch') {
|
|
69
|
+
throw new ContentCliError('usage', `content release takes major, minor or patch; got ${requested}.`);
|
|
70
|
+
}
|
|
71
|
+
for (const file of [CHANGELOG, CONFIG]) {
|
|
72
|
+
if (!existsSync(join(process.cwd(), file))) throw new ContentCliError('no-changelog', `No ${file} in this course.`);
|
|
73
|
+
}
|
|
74
|
+
const changelog = readFileSync(join(process.cwd(), CHANGELOG), 'utf8');
|
|
75
|
+
const config = readFileSync(join(process.cwd(), CONFIG), 'utf8');
|
|
76
|
+
const edit = releaseEdit(changelog, config, date, requested);
|
|
77
|
+
if (edit === null) {
|
|
78
|
+
return { summary: ['## [Unreleased] has no entries: nothing to release'], edits: [], next: [] };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
summary: [
|
|
82
|
+
`about.version ${edit.from} → ${edit.to} (${edit.bump}${edit.inferred ? ", from the section's headings" : ''})`,
|
|
83
|
+
`${CHANGELOG}: ## [Unreleased] → ## [${edit.to}] - ${date}, a fresh ## [Unreleased] above it`,
|
|
84
|
+
],
|
|
85
|
+
edits: [
|
|
86
|
+
{ file: CHANGELOG, before: changelog, after: edit.changelog },
|
|
87
|
+
{ file: CONFIG, before: config, after: edit.config },
|
|
88
|
+
],
|
|
89
|
+
next: ['pnpm validate:content (records the release in content/versions.json)', 'commit the changelog, the config and content/versions.json together'],
|
|
90
|
+
};
|
|
91
|
+
}
|