explico 0.13.1 → 0.14.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 +879 -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 +43 -0
- package/dist/compiler/cli-main.js +392 -0
- package/dist/compiler/cli.d.ts +424 -9
- package/dist/compiler/cli.js +1289 -8
- package/dist/compiler/collections.d.ts +88 -1
- package/dist/compiler/collections.js +170 -25
- 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 +359 -58
- package/dist/compiler/page-versions.d.ts +160 -0
- package/dist/compiler/page-versions.js +198 -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 +76 -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 +1 -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 +221 -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 +30 -7
- package/dist/rules/rules.schema.json +259 -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 +29 -9
- package/docs/authoring/agents.md +63 -16
- package/docs/authoring/components.md +15 -0
- package/docs/authoring/deck.md +588 -0
- package/docs/authoring/migrating.md +1045 -16
- package/docs/authoring/notation.md +49 -6
- package/docs/authoring/structure.md +80 -1
- package/docs/authoring/versioning.md +54 -2
- package/docs/authoring/visualizations.md +7 -2
- package/docs/course-agent/AGENTS.md +37 -0
- package/docs/course-agent/agents/rules-reviewer.md +33 -0
- package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
- package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
- package/docs/course-agent/skills/release-content/SKILL.md +24 -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 +469 -0
- package/src/compiler/cli.ts +1905 -16
- package/src/compiler/collections.ts +284 -25
- 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 +558 -63
- package/src/compiler/page-versions.ts +359 -0
- package/src/compiler/resolution-report.ts +38 -7
- package/src/compiler/validate-content.ts +212 -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 +126 -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-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 +257 -4
- package/src/rules/course-rules.ts +21 -1
- package/src/rules/in-effect.ts +54 -8
- 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
|
@@ -3,9 +3,11 @@ import { basename, dirname, extname, join } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
5
|
import type { Manifest, ManifestLesson, ManifestPart } from '../manifest';
|
|
6
|
-
import {
|
|
6
|
+
import { newestSince, versionPhrase } from '../page-versions';
|
|
7
|
+
import { courseFileStem, deckFileStem, deckOwnerSlug, deckSaveName } from './export-pages';
|
|
7
8
|
import {
|
|
8
9
|
consoleLogger,
|
|
10
|
+
deckBlocks,
|
|
9
11
|
diagramsOf,
|
|
10
12
|
figuresOf,
|
|
11
13
|
imageExtension,
|
|
@@ -18,8 +20,9 @@ import {
|
|
|
18
20
|
type PreparedChapters,
|
|
19
21
|
} from './export-chapters';
|
|
20
22
|
import type { CapturedFigure } from './latex/capture';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
+
import { deckSymbols } from './latex/deck';
|
|
24
|
+
import { checkItems, deckCheckItems } from './latex/document';
|
|
25
|
+
import { serializeMarkdownNodes, serializeMarkdownPage, serializeMarpDeck } from './markdown/serialize';
|
|
23
26
|
import type { StaticPage } from './static-tree';
|
|
24
27
|
import { zip, type ZipEntry } from './zip';
|
|
25
28
|
|
|
@@ -33,6 +36,8 @@ import { zip, type ZipEntry } from './zip';
|
|
|
33
36
|
* contents), one file per chapter, and shared `assets/`.
|
|
34
37
|
* - `downloads/course-markdown.zip`: `index.md` (the course and its
|
|
35
38
|
* contents), each part's page, one file per chapter, and `assets/`.
|
|
39
|
+
* - `downloads/<lesson>-slides-markdown.zip`: a deck as Marp's Markdown,
|
|
40
|
+
* `<name>-slides-markdown/slides.md` and its `assets/`.
|
|
36
41
|
*
|
|
37
42
|
* A figure a browser draws is a PNG at two device pixels per CSS pixel; an
|
|
38
43
|
* image is the site's own file; a `<Diagram>` is a PNG with its boxes and
|
|
@@ -112,16 +117,22 @@ export async function writeMarkdownExports(
|
|
|
112
117
|
if (!manifest.lessonExports.markdown) return;
|
|
113
118
|
const logger = options.logger ?? consoleLogger;
|
|
114
119
|
const outputRoot = fileURLToPath(directory);
|
|
115
|
-
const { base, chapters, captures } =
|
|
120
|
+
const { base, chapters, decks, captures } =
|
|
116
121
|
options.prepared ?? (await prepareChapters(outputRoot, manifest, { logger, projectRoot: options.projectRoot }));
|
|
117
122
|
const canonicalBaseUrl = manifest.lessonExports.canonicalBaseUrl;
|
|
118
123
|
const course = manifest.about.material.name;
|
|
119
|
-
const
|
|
120
|
-
|
|
124
|
+
const current = manifest.about.knowledge.version;
|
|
125
|
+
// The line under a title: the version the document's content is at (rule C5).
|
|
126
|
+
const source = (route: string, since: string | null) => ({
|
|
121
127
|
course,
|
|
122
|
-
|
|
128
|
+
since: versionPhrase(since, current),
|
|
123
129
|
...(canonicalBaseUrl ? { url: `${canonicalBaseUrl}${route ? `${route}/` : ''}` } : {}),
|
|
124
130
|
});
|
|
131
|
+
const partSince = (entry: { part: ManifestPart; chapters: readonly { lesson: ManifestLesson }[] }) =>
|
|
132
|
+
newestSince([
|
|
133
|
+
...(entry.part.overview ? [entry.part.overview.since] : []),
|
|
134
|
+
...entry.chapters.map(({ lesson }) => lesson.since),
|
|
135
|
+
]);
|
|
125
136
|
|
|
126
137
|
/** One chapter's Markdown and pictures, seen from `documentFiles`. */
|
|
127
138
|
const chapterFiles = async (
|
|
@@ -139,7 +150,7 @@ export async function writeMarkdownExports(
|
|
|
139
150
|
...(canonicalBaseUrl ? { canonicalBaseUrl } : {}),
|
|
140
151
|
pictures: new Map([...pictures].map(([key, picture]) => [key, picture.path])),
|
|
141
152
|
checkItems: checkItems(manifest, lesson),
|
|
142
|
-
source: source(page.route),
|
|
153
|
+
source: source(page.route, lesson.since),
|
|
143
154
|
},
|
|
144
155
|
{ number: `${lesson.part}.${lesson.chapter}` },
|
|
145
156
|
);
|
|
@@ -168,6 +179,40 @@ export async function writeMarkdownExports(
|
|
|
168
179
|
);
|
|
169
180
|
}
|
|
170
181
|
|
|
182
|
+
// Each deck as Marp's Markdown: a figure or diagram it recalls is its chapter's picture.
|
|
183
|
+
for (const { deck, page, figureRoutes } of decks) {
|
|
184
|
+
if (deck.part === undefined && !manifest.lessons.some((entry) => entry.id === deck.lessonId)) continue;
|
|
185
|
+
const owner = deckOwnerSlug(deck, manifest);
|
|
186
|
+
const captured = new Map<string, CapturedFigure>();
|
|
187
|
+
for (const [anchor, route] of figureRoutes) {
|
|
188
|
+
const figure = captures.get(route)?.get(anchor);
|
|
189
|
+
if (figure) captured.set(anchor, figure);
|
|
190
|
+
}
|
|
191
|
+
const blocks = [...deckBlocks(page), ...page.frames.flatMap((frame) => frame.notes)];
|
|
192
|
+
const pictures = await pagePictures(
|
|
193
|
+
outputRoot,
|
|
194
|
+
base,
|
|
195
|
+
{ slug: page.slug, route: page.route, title: page.title, blocks, notation: [], notationRemarks: [] },
|
|
196
|
+
captured,
|
|
197
|
+
'assets',
|
|
198
|
+
);
|
|
199
|
+
const items = deckCheckItems(manifest, deck);
|
|
200
|
+
const markdown = serializeMarpDeck({
|
|
201
|
+
deck: page,
|
|
202
|
+
documentFiles: new Map([[page.route, '']]),
|
|
203
|
+
...(canonicalBaseUrl ? { canonicalBaseUrl } : {}),
|
|
204
|
+
pictures: new Map([...pictures].map(([key, picture]) => [key, picture.path])),
|
|
205
|
+
checkItems: items,
|
|
206
|
+
source: source(page.route, deck.since),
|
|
207
|
+
symbols: deckSymbols({ deck: page, documentRoutes: new Set([page.route]), pictures: new Map(), checkItems: items }),
|
|
208
|
+
size: manifest.deck.aspectRatio === '4:3' ? '4:3' : '16:9',
|
|
209
|
+
});
|
|
210
|
+
pack(`${deckFileStem(owner)}-markdown.zip`, `${deckSaveName(owner)}-markdown`, [
|
|
211
|
+
{ path: 'slides.md', content: markdown },
|
|
212
|
+
...[...pictures.values()].map((picture) => ({ path: picture.path, content: picture.content })),
|
|
213
|
+
]);
|
|
214
|
+
}
|
|
215
|
+
|
|
171
216
|
const byId = new Map(chapters.map((chapter) => [chapter.lesson.id, chapter]));
|
|
172
217
|
const pageSlugs = new Set(manifest.pages.map((page) => page.slug));
|
|
173
218
|
const parts = manifest.parts
|
|
@@ -196,7 +241,7 @@ export async function writeMarkdownExports(
|
|
|
196
241
|
...(canonicalBaseUrl ? { canonicalBaseUrl } : {}),
|
|
197
242
|
pictures: new Map([...pictures].map(([key, picture]) => [key, picture.path])),
|
|
198
243
|
checkItems: new Map(),
|
|
199
|
-
source: source(page.route),
|
|
244
|
+
source: source(page.route, partSince(entry)),
|
|
200
245
|
},
|
|
201
246
|
{ number: `Part ${entry.part.ordinal}:` },
|
|
202
247
|
);
|
|
@@ -268,7 +313,7 @@ export async function writeMarkdownExports(
|
|
|
268
313
|
{
|
|
269
314
|
type: 'paragraph',
|
|
270
315
|
children: [
|
|
271
|
-
{ type: 'text', value: `Content
|
|
316
|
+
{ type: 'text', value: `Content ${versionPhrase(newestSince(parts.map(partSince)), current)}.` },
|
|
272
317
|
...(canonicalBaseUrl
|
|
273
318
|
? [
|
|
274
319
|
{ type: 'text' as const, value: ' Interactive version: ' },
|
|
@@ -322,7 +367,7 @@ export async function writeMarkdownExports(
|
|
|
322
367
|
await Promise.all(pending.map((file) => rm(`${file.target}.tmp-${process.pid}`, { force: true })));
|
|
323
368
|
}
|
|
324
369
|
logger.info(
|
|
325
|
-
`Markdown export: ${chapters.length} chapter(s), ${parts.length} part(s)` +
|
|
370
|
+
`Markdown export: ${chapters.length} chapter(s), ${decks.length} deck(s), ${parts.length} part(s)` +
|
|
326
371
|
(parts.length > 0 ? ' and the course' : '') +
|
|
327
372
|
'.',
|
|
328
373
|
);
|
|
@@ -12,7 +12,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
12
12
|
import { parse, serialize } from 'parse5';
|
|
13
13
|
|
|
14
14
|
import type { Manifest } from '../manifest';
|
|
15
|
-
import { exportPageRoute,
|
|
15
|
+
import { exportPageRoute, standaloneDownloads } from './export-pages';
|
|
16
16
|
import {
|
|
17
17
|
attribute,
|
|
18
18
|
elements,
|
|
@@ -910,10 +910,11 @@ export async function pageSourceFile(outputRoot: string, slug: string): Promise<
|
|
|
910
910
|
}
|
|
911
911
|
|
|
912
912
|
/**
|
|
913
|
-
* Emit the opt-in standalone HTML artifact of every chapter
|
|
914
|
-
* (`
|
|
915
|
-
* `downloads/<docs slug>.html`. A page
|
|
916
|
-
* part's and the course's files
|
|
913
|
+
* Emit the opt-in standalone HTML artifact of every chapter and deck
|
|
914
|
+
* (`standaloneDownloads`) from Astro's completed static output, at
|
|
915
|
+
* `downloads/<docs slug>.html` and `downloads/<lesson>-slides.html`. A page
|
|
916
|
+
* that is not a chapter is in its part's and the course's files
|
|
917
|
+
* (`combined-html.ts`).
|
|
917
918
|
*/
|
|
918
919
|
export async function writeStandaloneLessonExports(
|
|
919
920
|
directory: URL,
|
|
@@ -922,9 +923,9 @@ export async function writeStandaloneLessonExports(
|
|
|
922
923
|
if (!manifest.lessonExports.standaloneHtml) return;
|
|
923
924
|
const outputRoot = fileURLToPath(directory);
|
|
924
925
|
const pending: { target: string; temporary: string; html: string }[] = [];
|
|
925
|
-
for (const slug of
|
|
926
|
+
for (const { slug, download } of standaloneDownloads(manifest)) {
|
|
926
927
|
const sourceFile = await pageSourceFile(outputRoot, slug);
|
|
927
|
-
const target = join(outputRoot, 'downloads', `${
|
|
928
|
+
const target = join(outputRoot, 'downloads', `${download}.html`);
|
|
928
929
|
pending.push({
|
|
929
930
|
target,
|
|
930
931
|
temporary: `${target}.tmp-${process.pid}`,
|
|
@@ -130,6 +130,8 @@ export type Block =
|
|
|
130
130
|
readonly ordered: boolean;
|
|
131
131
|
readonly start?: number;
|
|
132
132
|
readonly items: readonly (readonly Block[])[];
|
|
133
|
+
/** On a deck, the step each item appears at (1 for the first), when an item is a step of its own. */
|
|
134
|
+
readonly itemSteps?: readonly number[];
|
|
133
135
|
}
|
|
134
136
|
| {
|
|
135
137
|
readonly kind: 'equation';
|
|
@@ -137,6 +139,8 @@ export type Block =
|
|
|
137
139
|
readonly number: string;
|
|
138
140
|
readonly key?: string;
|
|
139
141
|
readonly tex: string;
|
|
142
|
+
/** On a deck, a recalled equation's place in its chapter. */
|
|
143
|
+
readonly recall?: string;
|
|
140
144
|
}
|
|
141
145
|
| { readonly kind: 'displayMath'; readonly tex: string }
|
|
142
146
|
| (FloatBase & {
|
|
@@ -694,6 +698,7 @@ function readBlock(node: HtmlElement, context: ReadContext): Block[] | undefined
|
|
|
694
698
|
number: attribute(node, 'data-eq-number') ?? '',
|
|
695
699
|
...(key ? { key } : {}),
|
|
696
700
|
tex: (katex && texOf(katex)) ?? '',
|
|
701
|
+
...(attribute(node, 'data-eq-recall') ? { recall: attribute(node, 'data-eq-recall')! } : {}),
|
|
697
702
|
},
|
|
698
703
|
];
|
|
699
704
|
}
|
|
@@ -788,6 +793,19 @@ function readBlock(node: HtmlElement, context: ReadContext): Block[] | undefined
|
|
|
788
793
|
if (node.tagName === 'compact-examples' || hasClass(node, 'compact-examples')) {
|
|
789
794
|
return [readExamples(node, context)];
|
|
790
795
|
}
|
|
796
|
+
// A deck's question frame labels its check instead of heading it.
|
|
797
|
+
if (hasClass(node, 'deck-check__label') && attribute(node, 'data-block-kind') === 'check') {
|
|
798
|
+
const anchor = attribute(node, 'id') ?? '';
|
|
799
|
+
return [
|
|
800
|
+
{
|
|
801
|
+
kind: 'check',
|
|
802
|
+
anchor,
|
|
803
|
+
number: attribute(node, 'data-check-number') ?? '',
|
|
804
|
+
assessmentId: anchor.replace(/^check-/u, ''),
|
|
805
|
+
title: [{ kind: 'text', value: attribute(node, 'data-check-title') ?? '' }],
|
|
806
|
+
},
|
|
807
|
+
];
|
|
808
|
+
}
|
|
791
809
|
if (hasClass(node, 'sl-heading-wrapper')) {
|
|
792
810
|
const heading = headingOf(node);
|
|
793
811
|
if (!heading) return readBlocks(node, context);
|
|
@@ -918,6 +936,22 @@ export function readStaticPage(html: string, slug: string, route: string): Stati
|
|
|
918
936
|
const title = elements(main, (node) => node.tagName === 'h1')[0];
|
|
919
937
|
const content = elements(main, (node) => hasClass(node, 'sl-markdown-content'))[0];
|
|
920
938
|
if (!content) throw new Error(`Built page ${slug} has no .sl-markdown-content element.`);
|
|
939
|
+
const { notation, notationRemarks } = readPageNotation(main, context);
|
|
940
|
+
return {
|
|
941
|
+
slug,
|
|
942
|
+
route,
|
|
943
|
+
title: title ? collapse(readableText(title)).trim() : slug,
|
|
944
|
+
blocks: readBlocks(content, context),
|
|
945
|
+
notation,
|
|
946
|
+
notationRemarks,
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** The page's notation list (its notation layer's entries) and its page-specific remarks. */
|
|
951
|
+
function readPageNotation(
|
|
952
|
+
main: HtmlElement,
|
|
953
|
+
context: ReadContext,
|
|
954
|
+
): Pick<StaticPage, 'notation' | 'notationRemarks'> {
|
|
921
955
|
const layer = elements(
|
|
922
956
|
main,
|
|
923
957
|
(node) => hasClass(node, 'notation-layer') && attribute(node, 'data-notation-layer-id') === 'page-notation',
|
|
@@ -933,12 +967,150 @@ export function readStaticPage(html: string, slug: string, route: string): Stati
|
|
|
933
967
|
.map((item) => inlineContent(item, context)),
|
|
934
968
|
)
|
|
935
969
|
: [];
|
|
970
|
+
return { notation: entries.map((entry) => readNotationEntry(entry, context)), notationRemarks: remarks };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// --- decks ------------------------------------------------------------------
|
|
974
|
+
|
|
975
|
+
/** One frame of a deck page (docs/authoring/deck.md), as print exports read it. */
|
|
976
|
+
export interface DeckFrame {
|
|
977
|
+
readonly number: number;
|
|
978
|
+
readonly kind: 'title' | 'section' | 'frame' | 'outcomes' | 'references';
|
|
979
|
+
/** The frame heading's anchor. */
|
|
980
|
+
readonly anchor?: string;
|
|
981
|
+
readonly title: readonly Inline[];
|
|
982
|
+
/** The frame's content, a run of blocks per step, in reading order: step 1 is what shows first. */
|
|
983
|
+
readonly segments: readonly { readonly step: number; readonly blocks: readonly Block[] }[];
|
|
984
|
+
readonly steps: number;
|
|
985
|
+
readonly notes: readonly Block[];
|
|
986
|
+
/** The legend line: each symbol's TeX and its name. */
|
|
987
|
+
readonly legend: readonly { readonly tex: string; readonly label: readonly Inline[] }[];
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
export interface StaticDeck {
|
|
991
|
+
readonly slug: string;
|
|
992
|
+
readonly route: string;
|
|
993
|
+
readonly title: string;
|
|
994
|
+
readonly frames: readonly DeckFrame[];
|
|
995
|
+
/**
|
|
996
|
+
* The page's notation list: its lesson's symbols, its chapter's included.
|
|
997
|
+
* The PDF's appendix keeps the ones its frames use.
|
|
998
|
+
*/
|
|
999
|
+
readonly notation: readonly NotationEntry[];
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/** Each item's step, for a list a step mark split, or `undefined` when no item is a step. */
|
|
1003
|
+
function listItemSteps(list: HtmlElement): number[] | undefined {
|
|
1004
|
+
const items = childrenOf(list).filter((child): child is HtmlElement => isElement(child) && child.tagName === 'li');
|
|
1005
|
+
let step = 1;
|
|
1006
|
+
let any = false;
|
|
1007
|
+
const steps = items.map((item) => {
|
|
1008
|
+
const at = attribute(item, 'data-step');
|
|
1009
|
+
if (at !== undefined) {
|
|
1010
|
+
step = Number(at) || step;
|
|
1011
|
+
any = true;
|
|
1012
|
+
}
|
|
1013
|
+
return step;
|
|
1014
|
+
});
|
|
1015
|
+
return any ? steps : undefined;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/** A frame body's content, step by step. */
|
|
1019
|
+
function readSegments(body: HtmlElement, context: ReadContext): { step: number; blocks: Block[] }[] {
|
|
1020
|
+
const segments: { step: number; blocks: Block[] }[] = [];
|
|
1021
|
+
const add = (step: number, blocks: readonly Block[]) => {
|
|
1022
|
+
if (blocks.length === 0) return;
|
|
1023
|
+
const last = segments.at(-1);
|
|
1024
|
+
if (last && last.step === step) last.blocks.push(...blocks);
|
|
1025
|
+
else segments.push({ step, blocks: [...blocks] });
|
|
1026
|
+
};
|
|
1027
|
+
let pending: HtmlNode[] = [];
|
|
1028
|
+
const flush = () => {
|
|
1029
|
+
if (pending.length === 0) return;
|
|
1030
|
+
const holder = { nodeName: 'div', tagName: 'div', attrs: [], childNodes: pending } as unknown as HtmlElement;
|
|
1031
|
+
add(1, withItemSteps(holder, readBlocks(holder, context)));
|
|
1032
|
+
pending = [];
|
|
1033
|
+
};
|
|
1034
|
+
for (const child of childrenOf(body)) {
|
|
1035
|
+
if (isElement(child) && hasClass(child, 'deck-step') && attribute(child, 'data-step') !== undefined) {
|
|
1036
|
+
flush();
|
|
1037
|
+
add(Number(attribute(child, 'data-step')) || 1, withItemSteps(child, readBlocks(child, context)));
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
pending.push(child);
|
|
1041
|
+
}
|
|
1042
|
+
flush();
|
|
1043
|
+
return segments;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/** The list blocks of `node`'s own lists, given the step of each item. */
|
|
1047
|
+
function withItemSteps(node: HtmlElement, blocks: Block[]): Block[] {
|
|
1048
|
+
const lists = childrenOf(node).filter(
|
|
1049
|
+
(child): child is HtmlElement => isElement(child) && (child.tagName === 'ul' || child.tagName === 'ol'),
|
|
1050
|
+
);
|
|
1051
|
+
let index = 0;
|
|
1052
|
+
return blocks.map((block) => {
|
|
1053
|
+
if (block.kind !== 'list') return block;
|
|
1054
|
+
const list = lists[index++];
|
|
1055
|
+
const itemSteps = list ? listItemSteps(list) : undefined;
|
|
1056
|
+
return itemSteps ? { ...block, itemSteps } : block;
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/** Read the frames of one built deck page. */
|
|
1061
|
+
export function readStaticDeck(html: string, slug: string, route: string): StaticDeck {
|
|
1062
|
+
const document = parse(html);
|
|
1063
|
+
const base = inferBasePrefix(document);
|
|
1064
|
+
const context: ReadContext = { route, base };
|
|
1065
|
+
const main = elements(document, (node) => node.tagName === 'main')[0];
|
|
1066
|
+
if (!main) throw new Error(`Built page ${slug} has no <main> element.`);
|
|
1067
|
+
const deck = elements(main, (node) => hasClass(node, 'deck') && attribute(node, 'data-deck') !== undefined)[0];
|
|
1068
|
+
if (!deck) throw new Error(`Built page ${slug} has no deck (.deck[data-deck]).`);
|
|
1069
|
+
const title = elements(main, (node) => node.tagName === 'h1')[0];
|
|
1070
|
+
const frames = childrenOf(deck)
|
|
1071
|
+
.filter((child): child is HtmlElement => isElement(child) && hasClass(child, 'deck-frame'))
|
|
1072
|
+
.map((frame): DeckFrame => {
|
|
1073
|
+
const canvas = firstByClass(frame, 'deck-canvas');
|
|
1074
|
+
const heading = canvas ? firstByClass(canvas, 'deck-frame__title') ?? firstByClass(canvas, 'deck-title__title') : undefined;
|
|
1075
|
+
const body = canvas ? firstByClass(canvas, 'deck-frame__body') : undefined;
|
|
1076
|
+
const notes = childrenOf(frame).find(
|
|
1077
|
+
(child): child is HtmlElement => isElement(child) && hasClass(child, 'deck-notes'),
|
|
1078
|
+
);
|
|
1079
|
+
const legend = canvas ? firstByClass(canvas, 'deck-legend') : undefined;
|
|
1080
|
+
const kind = (attribute(frame, 'data-kind') ?? 'frame') as DeckFrame['kind'];
|
|
1081
|
+
// A generated frame has no body of its own: its content is the canvas,
|
|
1082
|
+
// less its title and its foot.
|
|
1083
|
+
const generatedContent =
|
|
1084
|
+
!body && canvas
|
|
1085
|
+
? readBlocks(
|
|
1086
|
+
canvas,
|
|
1087
|
+
context,
|
|
1088
|
+
(child) =>
|
|
1089
|
+
!(isElement(child) && (hasClass(child, 'deck-frame__title') || hasClass(child, 'deck-frame__foot') || hasClass(child, 'deck-title__title'))),
|
|
1090
|
+
)
|
|
1091
|
+
: [];
|
|
1092
|
+
return {
|
|
1093
|
+
number: Number(attribute(frame, 'data-frame')) || 0,
|
|
1094
|
+
kind,
|
|
1095
|
+
...(heading && attribute(heading, 'id') ? { anchor: attribute(heading, 'id')! } : {}),
|
|
1096
|
+
title: heading ? inlineContent(heading, context) : [],
|
|
1097
|
+
segments: body ? readSegments(body, context) : generatedContent.length > 0 ? [{ step: 1, blocks: generatedContent }] : [],
|
|
1098
|
+
steps: Number(attribute(frame, 'data-steps')) || 1,
|
|
1099
|
+
notes: notes ? readBlocks(notes, context) : [],
|
|
1100
|
+
legend: legend
|
|
1101
|
+
? elements(legend, (node) => hasClass(node, 'deck-legend__item')).map((item) => {
|
|
1102
|
+
const katex = elements(item, (node) => hasClass(node, 'katex'))[0];
|
|
1103
|
+
const label = firstByClass(item, 'deck-legend__label');
|
|
1104
|
+
return { tex: (katex && texOf(katex)) ?? '', label: label ? inlineContent(label, context) : [] };
|
|
1105
|
+
})
|
|
1106
|
+
: [],
|
|
1107
|
+
};
|
|
1108
|
+
});
|
|
936
1109
|
return {
|
|
937
1110
|
slug,
|
|
938
1111
|
route,
|
|
939
1112
|
title: title ? collapse(readableText(title)).trim() : slug,
|
|
940
|
-
|
|
941
|
-
notation:
|
|
942
|
-
notationRemarks: remarks,
|
|
1113
|
+
frames,
|
|
1114
|
+
notation: readPageNotation(main, context).notation,
|
|
943
1115
|
};
|
|
944
1116
|
}
|
|
@@ -23,10 +23,18 @@ export interface KnowledgeChangelogEntry {
|
|
|
23
23
|
readonly notes: readonly string[];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** The released entries and, apart from them, the bullets `## [Unreleased]` holds. */
|
|
27
|
+
export interface KnowledgeChangelog {
|
|
28
|
+
readonly entries: readonly KnowledgeChangelogEntry[];
|
|
29
|
+
/** Bullet lines under `## [Unreleased]`, any `###` group included, marker stripped. */
|
|
30
|
+
readonly unreleased: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
export type KnowledgeVersionCode =
|
|
27
34
|
| 'knowledge-changelog-missing'
|
|
28
35
|
| 'knowledge-version-invalid'
|
|
29
|
-
| 'knowledge-version-mismatch'
|
|
36
|
+
| 'knowledge-version-mismatch'
|
|
37
|
+
| 'page-versions-invalid';
|
|
30
38
|
|
|
31
39
|
export interface KnowledgeVersionDiagnostic {
|
|
32
40
|
readonly code: KnowledgeVersionCode;
|
|
@@ -41,6 +49,15 @@ export interface KnowledgeVersion {
|
|
|
41
49
|
readonly version: string;
|
|
42
50
|
/** The newest released `## [x.y.z]` entry, when the changelog has one. */
|
|
43
51
|
readonly latest?: KnowledgeChangelogEntry;
|
|
52
|
+
/** Every released entry, newest first; empty without a changelog. */
|
|
53
|
+
readonly entries: readonly KnowledgeChangelogEntry[];
|
|
54
|
+
/**
|
|
55
|
+
* The bullets `## [Unreleased]` holds: a content change not yet released
|
|
56
|
+
* (rule C1). A page changed while there are any is "not yet released"
|
|
57
|
+
* (rule C5); with none, a change is the release's own, or one rule C1 lets
|
|
58
|
+
* go unlisted.
|
|
59
|
+
*/
|
|
60
|
+
readonly unreleased: readonly string[];
|
|
44
61
|
readonly diagnostics: readonly KnowledgeVersionDiagnostic[];
|
|
45
62
|
}
|
|
46
63
|
|
|
@@ -56,29 +73,32 @@ export function isKnowledgeSemver(value: string): boolean {
|
|
|
56
73
|
}
|
|
57
74
|
|
|
58
75
|
/**
|
|
59
|
-
*
|
|
60
|
-
* first, by Keep a Changelog convention)
|
|
76
|
+
* A `content/CHANGELOG.md` body: every released entry in file order (newest
|
|
77
|
+
* first, by Keep a Changelog convention), and the bullets of
|
|
78
|
+
* `## [Unreleased]`. A released entry's notes end at a `###` group heading,
|
|
79
|
+
* as the About panel has always shown them; the unreleased bullets are
|
|
80
|
+
* counted through every group, since what they answer is whether anything
|
|
81
|
+
* is waiting for a release.
|
|
61
82
|
*/
|
|
62
|
-
export function
|
|
63
|
-
text: string,
|
|
64
|
-
): KnowledgeChangelogEntry[] {
|
|
83
|
+
export function scanKnowledgeChangelog(text: string): KnowledgeChangelog {
|
|
65
84
|
const lines = text.split(/\r?\n/);
|
|
66
85
|
const entries: {
|
|
67
86
|
version: string;
|
|
68
87
|
date?: string;
|
|
69
88
|
notes: string[];
|
|
70
89
|
}[] = [];
|
|
90
|
+
const unreleased: string[] = [];
|
|
71
91
|
let current: { version: string; date?: string; notes: string[] } | null =
|
|
72
92
|
null;
|
|
93
|
+
let inUnreleased = false;
|
|
73
94
|
|
|
74
95
|
for (const line of lines) {
|
|
75
96
|
const heading = HEADING_RE.exec(line);
|
|
76
97
|
if (heading) {
|
|
77
98
|
const token = (heading[1] ?? '').trim();
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
99
|
+
current = null;
|
|
100
|
+
inUnreleased = /^unreleased$/i.test(token);
|
|
101
|
+
if (inUnreleased) continue;
|
|
82
102
|
current = { version: token, notes: [] };
|
|
83
103
|
const date = (heading[2] ?? '').trim();
|
|
84
104
|
if (date) current.date = date;
|
|
@@ -88,18 +108,33 @@ export function parseKnowledgeChangelog(
|
|
|
88
108
|
// A top-level `#` or a deeper `###` heading ends the current entry's notes.
|
|
89
109
|
if (/^#{1,3}\s/.test(line) && !line.startsWith('## [')) {
|
|
90
110
|
current = null;
|
|
111
|
+
if (/^#{1,2}\s/.test(line)) inUnreleased = false;
|
|
91
112
|
continue;
|
|
92
113
|
}
|
|
93
|
-
if (!current) continue;
|
|
94
114
|
const bullet = BULLET_RE.exec(line);
|
|
95
|
-
if (bullet)
|
|
115
|
+
if (!bullet) continue;
|
|
116
|
+
if (current) current.notes.push(bullet[1] ?? '');
|
|
117
|
+
else if (inUnreleased) unreleased.push(bullet[1] ?? '');
|
|
96
118
|
}
|
|
97
119
|
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
return {
|
|
121
|
+
entries: entries.map((entry) => ({
|
|
122
|
+
version: entry.version,
|
|
123
|
+
...(entry.date === undefined ? {} : { date: entry.date }),
|
|
124
|
+
notes: [...entry.notes],
|
|
125
|
+
})),
|
|
126
|
+
unreleased,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Every released entry in a `content/CHANGELOG.md` body, in file order (newest
|
|
132
|
+
* first, by Keep a Changelog convention). `## [Unreleased]` is skipped.
|
|
133
|
+
*/
|
|
134
|
+
export function parseKnowledgeChangelog(
|
|
135
|
+
text: string,
|
|
136
|
+
): KnowledgeChangelogEntry[] {
|
|
137
|
+
return [...scanKnowledgeChangelog(text).entries];
|
|
103
138
|
}
|
|
104
139
|
|
|
105
140
|
/**
|
|
@@ -133,17 +168,17 @@ export function resolveKnowledgeVersion(options: {
|
|
|
133
168
|
'knowledge-changelog-missing',
|
|
134
169
|
`No ${file}. Add a Keep a Changelog file whose newest \`## [x.y.z]\` heading is ${JSON.stringify(declaredVersion)} (rule C1).`,
|
|
135
170
|
);
|
|
136
|
-
return { version: declaredVersion, diagnostics };
|
|
171
|
+
return { version: declaredVersion, entries: [], unreleased: [], diagnostics };
|
|
137
172
|
}
|
|
138
173
|
|
|
139
|
-
const entries =
|
|
174
|
+
const { entries, unreleased } = scanKnowledgeChangelog(changelogText);
|
|
140
175
|
const latest = entries[0];
|
|
141
176
|
if (!latest) {
|
|
142
177
|
warn(
|
|
143
178
|
'knowledge-changelog-missing',
|
|
144
179
|
`${file} has no released \`## [x.y.z]\` entry — only \`## [Unreleased]\`. Cut a release: move it to \`## [${declaredVersion}] - <date>\` (rule C3).`,
|
|
145
180
|
);
|
|
146
|
-
return { version: declaredVersion, diagnostics };
|
|
181
|
+
return { version: declaredVersion, entries, unreleased, diagnostics };
|
|
147
182
|
}
|
|
148
183
|
|
|
149
184
|
if (isKnowledgeSemver(declaredVersion) && latest.version !== declaredVersion) {
|
|
@@ -153,5 +188,5 @@ export function resolveKnowledgeVersion(options: {
|
|
|
153
188
|
);
|
|
154
189
|
}
|
|
155
190
|
|
|
156
|
-
return { version: declaredVersion, latest, diagnostics };
|
|
191
|
+
return { version: declaredVersion, latest, entries, unreleased, diagnostics };
|
|
157
192
|
}
|