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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, extname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import type { Manifest, ManifestLesson, ManifestPart } from '../manifest';
|
|
6
|
+
import { courseFileStem } from './export-pages';
|
|
7
|
+
import {
|
|
8
|
+
consoleLogger,
|
|
9
|
+
diagramsOf,
|
|
10
|
+
figuresOf,
|
|
11
|
+
imageExtension,
|
|
12
|
+
imageFile,
|
|
13
|
+
imagesOf,
|
|
14
|
+
needsCapture,
|
|
15
|
+
prepareChapters,
|
|
16
|
+
readBuiltPage,
|
|
17
|
+
type ExportLogger,
|
|
18
|
+
type PreparedChapters,
|
|
19
|
+
} from './export-chapters';
|
|
20
|
+
import type { CapturedFigure } from './latex/capture';
|
|
21
|
+
import { checkItems } from './latex/document';
|
|
22
|
+
import { serializeMarkdownNodes, serializeMarkdownPage } from './markdown/serialize';
|
|
23
|
+
import type { StaticPage } from './static-tree';
|
|
24
|
+
import { zip, type ZipEntry } from './zip';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The Markdown export (`lessonExports.markdown`, decision D2): built with the
|
|
28
|
+
* site, from the same reading trees and figure capture as the LaTeX export.
|
|
29
|
+
*
|
|
30
|
+
* - `downloads/<slug>-markdown.zip`: `<name>-markdown/lesson.md` and its
|
|
31
|
+
* `assets/`.
|
|
32
|
+
* - `downloads/part/<dir>-markdown.zip`: `index.md` (the part's page and its
|
|
33
|
+
* contents), one file per chapter, and shared `assets/`.
|
|
34
|
+
* - `downloads/course-markdown.zip`: `index.md` (the course and its
|
|
35
|
+
* contents), each part's page, one file per chapter, and `assets/`.
|
|
36
|
+
*
|
|
37
|
+
* A figure a browser draws is a PNG at two device pixels per CSS pixel; an
|
|
38
|
+
* image is the site's own file; a `<Diagram>` is a PNG with its boxes and
|
|
39
|
+
* arrows listed as text. Links between files of a download are relative.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
interface Picture {
|
|
43
|
+
readonly path: string;
|
|
44
|
+
readonly content: Uint8Array;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Every picture a page shows, by float anchor or image `src`, under `assetDir`. */
|
|
48
|
+
async function pagePictures(
|
|
49
|
+
outputRoot: string,
|
|
50
|
+
base: string,
|
|
51
|
+
page: StaticPage,
|
|
52
|
+
captured: ReadonlyMap<string, CapturedFigure> | undefined,
|
|
53
|
+
assetDir: string,
|
|
54
|
+
): Promise<Map<string, Picture>> {
|
|
55
|
+
const pictures = new Map<string, Picture>();
|
|
56
|
+
const copy = async (key: string, src: string, name: string) => {
|
|
57
|
+
const file = imageFile(outputRoot, base, page.route, src);
|
|
58
|
+
pictures.set(key, { path: `${assetDir}/${name}${extname(file).toLowerCase()}`, content: await readFile(file) });
|
|
59
|
+
};
|
|
60
|
+
for (const figure of figuresOf(page.blocks)) {
|
|
61
|
+
// An image file stays itself, SVG included: Markdown shows it as it is.
|
|
62
|
+
if (figure.picture.kind === 'image' && (!needsCapture(figure) || imageExtension(figure.picture.src) === '.svg')) {
|
|
63
|
+
await copy(figure.anchor, figure.picture.src, figure.anchor);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const png = captured?.get(figure.anchor)?.png;
|
|
67
|
+
if (png) pictures.set(figure.anchor, { path: `${assetDir}/${figure.anchor}.png`, content: png });
|
|
68
|
+
}
|
|
69
|
+
for (const diagram of diagramsOf(page.blocks)) {
|
|
70
|
+
const png = captured?.get(diagram.anchor)?.png;
|
|
71
|
+
if (png) pictures.set(diagram.anchor, { path: `${assetDir}/${diagram.anchor}.png`, content: png });
|
|
72
|
+
}
|
|
73
|
+
for (const image of imagesOf(page.blocks)) {
|
|
74
|
+
if (pictures.has(image.src)) continue;
|
|
75
|
+
const name = basename(new URL(image.src, 'https://x.invalid/').pathname, extname(image.src)) || 'image';
|
|
76
|
+
await copy(image.src, image.src, name);
|
|
77
|
+
}
|
|
78
|
+
return pictures;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function twoDigits(value: number): string {
|
|
82
|
+
return String(value).padStart(2, '0');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A chapter's file in a part's or the course's download: `02-01-projectile-range.md`. */
|
|
86
|
+
function chapterFile(lesson: ManifestLesson): string {
|
|
87
|
+
return `${twoDigits(lesson.part)}-${twoDigits(lesson.chapter)}-${lesson.slug.split('/').at(-1) ?? lesson.slug}.md`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A part's page in the course's download: `02-00-projectiles.md`. */
|
|
91
|
+
function partFile(part: ManifestPart): string {
|
|
92
|
+
return `${twoDigits(part.ordinal)}-00-${part.dir}.md`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The pages of a download, by route, with the file each is written to. */
|
|
96
|
+
type DocumentFiles = ReadonlyMap<string, string>;
|
|
97
|
+
|
|
98
|
+
/** The same map, seen from one file of the download: its own route maps to `''`. */
|
|
99
|
+
function relativeTo(files: DocumentFiles, route: string): Map<string, string> {
|
|
100
|
+
return new Map([...files].map(([key, file]) => [key, key === route ? '' : file]));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function writeMarkdownExports(
|
|
104
|
+
directory: URL,
|
|
105
|
+
manifest: Manifest,
|
|
106
|
+
options: {
|
|
107
|
+
readonly logger?: ExportLogger;
|
|
108
|
+
readonly projectRoot?: string;
|
|
109
|
+
readonly prepared?: PreparedChapters;
|
|
110
|
+
} = {},
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
if (!manifest.lessonExports.markdown) return;
|
|
113
|
+
const logger = options.logger ?? consoleLogger;
|
|
114
|
+
const outputRoot = fileURLToPath(directory);
|
|
115
|
+
const { base, chapters, captures } =
|
|
116
|
+
options.prepared ?? (await prepareChapters(outputRoot, manifest, { logger, projectRoot: options.projectRoot }));
|
|
117
|
+
const canonicalBaseUrl = manifest.lessonExports.canonicalBaseUrl;
|
|
118
|
+
const course = manifest.about.material.name;
|
|
119
|
+
const version = manifest.about.knowledge.version;
|
|
120
|
+
const source = (route: string) => ({
|
|
121
|
+
course,
|
|
122
|
+
version,
|
|
123
|
+
...(canonicalBaseUrl ? { url: `${canonicalBaseUrl}${route ? `${route}/` : ''}` } : {}),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/** One chapter's Markdown and pictures, seen from `documentFiles`. */
|
|
127
|
+
const chapterFiles = async (
|
|
128
|
+
chapter: (typeof chapters)[number],
|
|
129
|
+
file: string,
|
|
130
|
+
assetDir: string,
|
|
131
|
+
documentFiles: DocumentFiles,
|
|
132
|
+
): Promise<ZipEntry[]> => {
|
|
133
|
+
const { lesson, page } = chapter;
|
|
134
|
+
const pictures = await pagePictures(outputRoot, base, page, captures.get(page.route), assetDir);
|
|
135
|
+
const markdown = serializeMarkdownPage(
|
|
136
|
+
{
|
|
137
|
+
page,
|
|
138
|
+
documentFiles: relativeTo(documentFiles, page.route),
|
|
139
|
+
...(canonicalBaseUrl ? { canonicalBaseUrl } : {}),
|
|
140
|
+
pictures: new Map([...pictures].map(([key, picture]) => [key, picture.path])),
|
|
141
|
+
checkItems: checkItems(manifest, lesson),
|
|
142
|
+
source: source(page.route),
|
|
143
|
+
},
|
|
144
|
+
{ number: `${lesson.part}.${lesson.chapter}` },
|
|
145
|
+
);
|
|
146
|
+
return [
|
|
147
|
+
{ path: file, content: markdown },
|
|
148
|
+
...[...pictures.values()].map((picture) => ({ path: picture.path, content: picture.content })),
|
|
149
|
+
];
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const pending: { target: string; content: Uint8Array }[] = [];
|
|
153
|
+
const pack = (target: string, folder: string, entries: readonly ZipEntry[]) => {
|
|
154
|
+
const unique = new Map(entries.map((entry) => [entry.path, entry]));
|
|
155
|
+
pending.push({
|
|
156
|
+
target: join(outputRoot, 'downloads', target),
|
|
157
|
+
content: zip([...unique.values()].map((entry) => ({ path: `${folder}/${entry.path}`, content: entry.content }))),
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Each chapter on its own.
|
|
162
|
+
for (const chapter of chapters) {
|
|
163
|
+
const name = chapter.lesson.slug.split('/').at(-1) ?? chapter.lesson.slug;
|
|
164
|
+
pack(
|
|
165
|
+
`${chapter.lesson.slug}-markdown.zip`,
|
|
166
|
+
`${name}-markdown`,
|
|
167
|
+
await chapterFiles(chapter, 'lesson.md', 'assets', new Map([[chapter.page.route, 'lesson.md']])),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const byId = new Map(chapters.map((chapter) => [chapter.lesson.id, chapter]));
|
|
172
|
+
const pageSlugs = new Set(manifest.pages.map((page) => page.slug));
|
|
173
|
+
const parts = manifest.parts
|
|
174
|
+
.map((part) => ({
|
|
175
|
+
part,
|
|
176
|
+
chapters: part.lessons.flatMap((id) => (byId.has(id) ? [byId.get(id)!] : [])),
|
|
177
|
+
overview: pageSlugs.has(`${part.dir}/index`) ? `${part.dir}/index` : undefined,
|
|
178
|
+
}))
|
|
179
|
+
.filter((entry) => entry.chapters.length > 0);
|
|
180
|
+
|
|
181
|
+
/** A part's page as Markdown, or its label alone when it has none. */
|
|
182
|
+
const overviewMarkdown = async (
|
|
183
|
+
entry: (typeof parts)[number],
|
|
184
|
+
documentFiles: DocumentFiles,
|
|
185
|
+
assetDir: string,
|
|
186
|
+
): Promise<{ markdown: string; pictures: ZipEntry[] }> => {
|
|
187
|
+
if (!entry.overview) {
|
|
188
|
+
return { markdown: serializeMarkdownNodes([{ type: 'heading', depth: 1, children: [{ type: 'text', value: entry.part.label }] }]), pictures: [] };
|
|
189
|
+
}
|
|
190
|
+
const { page } = await readBuiltPage(outputRoot, entry.overview);
|
|
191
|
+
const pictures = await pagePictures(outputRoot, base, page, undefined, assetDir);
|
|
192
|
+
const markdown = serializeMarkdownPage(
|
|
193
|
+
{
|
|
194
|
+
page,
|
|
195
|
+
documentFiles: relativeTo(documentFiles, page.route),
|
|
196
|
+
...(canonicalBaseUrl ? { canonicalBaseUrl } : {}),
|
|
197
|
+
pictures: new Map([...pictures].map(([key, picture]) => [key, picture.path])),
|
|
198
|
+
checkItems: new Map(),
|
|
199
|
+
source: source(page.route),
|
|
200
|
+
},
|
|
201
|
+
{ number: `Part ${entry.part.ordinal}:` },
|
|
202
|
+
);
|
|
203
|
+
return { markdown, pictures: [...pictures.values()].map((picture) => ({ path: picture.path, content: picture.content })) };
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const contents = (entry: (typeof parts)[number], files: DocumentFiles) =>
|
|
207
|
+
entry.chapters.map((chapter) => ({
|
|
208
|
+
type: 'listItem' as const,
|
|
209
|
+
spread: false,
|
|
210
|
+
children: [
|
|
211
|
+
{
|
|
212
|
+
type: 'paragraph' as const,
|
|
213
|
+
children: [
|
|
214
|
+
{
|
|
215
|
+
type: 'link' as const,
|
|
216
|
+
url: files.get(chapter.page.route) ?? '',
|
|
217
|
+
children: [{ type: 'text' as const, value: `${chapter.lesson.part}.${chapter.lesson.chapter} ${chapter.page.title}` }],
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
}));
|
|
223
|
+
|
|
224
|
+
// Each part: its page as index.md, then its chapters.
|
|
225
|
+
for (const entry of parts) {
|
|
226
|
+
const files: DocumentFiles = new Map([
|
|
227
|
+
...(entry.overview ? [[entry.part.dir, 'index.md'] as const] : []),
|
|
228
|
+
...entry.chapters.map((chapter) => [chapter.page.route, chapterFile(chapter.lesson)] as const),
|
|
229
|
+
]);
|
|
230
|
+
const overview = await overviewMarkdown(entry, files, 'assets/index');
|
|
231
|
+
const index =
|
|
232
|
+
`${overview.markdown}\n` +
|
|
233
|
+
serializeMarkdownNodes([
|
|
234
|
+
{ type: 'heading', depth: 2, children: [{ type: 'text', value: 'Contents' }] },
|
|
235
|
+
{ type: 'list', ordered: false, spread: false, children: contents(entry, files) },
|
|
236
|
+
]);
|
|
237
|
+
const entries: ZipEntry[] = [{ path: 'index.md', content: index }, ...overview.pictures];
|
|
238
|
+
for (const chapter of entry.chapters) {
|
|
239
|
+
const file = chapterFile(chapter.lesson);
|
|
240
|
+
entries.push(...(await chapterFiles(chapter, file, `assets/${file.replace(/\.md$/u, '')}`, files)));
|
|
241
|
+
}
|
|
242
|
+
pack(`part/${entry.part.dir}-markdown.zip`, `${entry.part.dir}-markdown`, entries);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// The course: index.md, then each part's page and chapters.
|
|
246
|
+
if (parts.length > 0) {
|
|
247
|
+
const files: DocumentFiles = new Map([
|
|
248
|
+
['', 'index.md'],
|
|
249
|
+
...parts.flatMap((entry) => [
|
|
250
|
+
...(entry.overview ? [[entry.part.dir, partFile(entry.part)] as const] : []),
|
|
251
|
+
...entry.chapters.map((chapter) => [chapter.page.route, chapterFile(chapter.lesson)] as const),
|
|
252
|
+
]),
|
|
253
|
+
]);
|
|
254
|
+
const entries: ZipEntry[] = [];
|
|
255
|
+
for (const entry of parts) {
|
|
256
|
+
if (entry.overview) {
|
|
257
|
+
const file = partFile(entry.part);
|
|
258
|
+
const overview = await overviewMarkdown(entry, files, `assets/${file.replace(/\.md$/u, '')}`);
|
|
259
|
+
entries.push({ path: file, content: overview.markdown }, ...overview.pictures);
|
|
260
|
+
}
|
|
261
|
+
for (const chapter of entry.chapters) {
|
|
262
|
+
const file = chapterFile(chapter.lesson);
|
|
263
|
+
entries.push(...(await chapterFiles(chapter, file, `assets/${file.replace(/\.md$/u, '')}`, files)));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const index = serializeMarkdownNodes([
|
|
267
|
+
{ type: 'heading', depth: 1, children: [{ type: 'text', value: course }] },
|
|
268
|
+
{
|
|
269
|
+
type: 'paragraph',
|
|
270
|
+
children: [
|
|
271
|
+
{ type: 'text', value: `Content version ${version}.` },
|
|
272
|
+
...(canonicalBaseUrl
|
|
273
|
+
? [
|
|
274
|
+
{ type: 'text' as const, value: ' Interactive version: ' },
|
|
275
|
+
{ type: 'link' as const, url: canonicalBaseUrl, children: [{ type: 'text' as const, value: canonicalBaseUrl }] },
|
|
276
|
+
{ type: 'text' as const, value: '.' },
|
|
277
|
+
]
|
|
278
|
+
: []),
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
...(manifest.about.material.summary
|
|
282
|
+
? [{ type: 'paragraph' as const, children: [{ type: 'text' as const, value: manifest.about.material.summary }] }]
|
|
283
|
+
: []),
|
|
284
|
+
{ type: 'heading', depth: 2, children: [{ type: 'text', value: 'Contents' }] },
|
|
285
|
+
{
|
|
286
|
+
type: 'list',
|
|
287
|
+
ordered: false,
|
|
288
|
+
spread: false,
|
|
289
|
+
children: parts.map((entry) => ({
|
|
290
|
+
type: 'listItem' as const,
|
|
291
|
+
spread: false,
|
|
292
|
+
children: [
|
|
293
|
+
{
|
|
294
|
+
type: 'paragraph' as const,
|
|
295
|
+
children: entry.overview
|
|
296
|
+
? [
|
|
297
|
+
{
|
|
298
|
+
type: 'link' as const,
|
|
299
|
+
url: partFile(entry.part),
|
|
300
|
+
children: [{ type: 'text' as const, value: `Part ${entry.part.ordinal}: ${entry.part.label}` }],
|
|
301
|
+
},
|
|
302
|
+
]
|
|
303
|
+
: [{ type: 'text' as const, value: `Part ${entry.part.ordinal}: ${entry.part.label}` }],
|
|
304
|
+
},
|
|
305
|
+
{ type: 'list' as const, ordered: false, spread: false, children: contents(entry, files) },
|
|
306
|
+
],
|
|
307
|
+
})),
|
|
308
|
+
},
|
|
309
|
+
]);
|
|
310
|
+
entries.push({ path: 'index.md', content: index });
|
|
311
|
+
pack('course-markdown.zip', `${courseFileStem(manifest)}-markdown`, entries);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Nothing is published until every file is ready.
|
|
315
|
+
try {
|
|
316
|
+
for (const file of pending) {
|
|
317
|
+
await mkdir(dirname(file.target), { recursive: true });
|
|
318
|
+
await writeFile(`${file.target}.tmp-${process.pid}`, file.content);
|
|
319
|
+
}
|
|
320
|
+
for (const file of pending) await rename(`${file.target}.tmp-${process.pid}`, file.target);
|
|
321
|
+
} finally {
|
|
322
|
+
await Promise.all(pending.map((file) => rm(`${file.target}.tmp-${process.pid}`, { force: true })));
|
|
323
|
+
}
|
|
324
|
+
logger.info(
|
|
325
|
+
`Markdown export: ${chapters.length} chapter(s), ${parts.length} part(s)` +
|
|
326
|
+
(parts.length > 0 ? ' and the course' : '') +
|
|
327
|
+
'.',
|
|
328
|
+
);
|
|
329
|
+
}
|
|
@@ -9,19 +9,21 @@ import {
|
|
|
9
9
|
import { Buffer } from 'node:buffer';
|
|
10
10
|
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import {
|
|
13
|
-
parse,
|
|
14
|
-
serialize,
|
|
15
|
-
type DefaultTreeAdapterMap,
|
|
16
|
-
} from 'parse5';
|
|
12
|
+
import { parse, serialize } from 'parse5';
|
|
17
13
|
|
|
18
14
|
import type { Manifest } from '../manifest';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
import { exportPageRoute, standalonePageSlugs } from './export-pages';
|
|
16
|
+
import {
|
|
17
|
+
attribute,
|
|
18
|
+
elements,
|
|
19
|
+
hasClass,
|
|
20
|
+
inferBasePrefix,
|
|
21
|
+
type HtmlChild,
|
|
22
|
+
type HtmlElement,
|
|
23
|
+
type HtmlNode,
|
|
24
|
+
type HtmlParent,
|
|
25
|
+
type HtmlText,
|
|
26
|
+
} from './html-tree';
|
|
25
27
|
|
|
26
28
|
const HTML_NAMESPACE =
|
|
27
29
|
'http://www.w3.org/1999/xhtml' as HtmlElement['namespaceURI'];
|
|
@@ -115,11 +117,12 @@ const STANDALONE_THEME_SCRIPT = `(() => {
|
|
|
115
117
|
apply('auto');
|
|
116
118
|
})();`;
|
|
117
119
|
|
|
118
|
-
/** Inputs for producing one standalone
|
|
120
|
+
/** Inputs for producing one standalone page from Astro's final HTML. */
|
|
119
121
|
export interface StandaloneLessonInput {
|
|
120
122
|
readonly html: string;
|
|
121
123
|
readonly sourceFile: string;
|
|
122
124
|
readonly outputRoot: string;
|
|
125
|
+
/** The page's docs slug: a chapter's, or `index`, `motion/index`, `glossary`. */
|
|
123
126
|
readonly lessonSlug: string;
|
|
124
127
|
readonly canonicalBaseUrl?: string;
|
|
125
128
|
}
|
|
@@ -143,45 +146,12 @@ interface ExportContext {
|
|
|
143
146
|
readonly assetData: Map<string, string>;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
function isElement(node: HtmlNode): node is HtmlElement {
|
|
147
|
-
return 'tagName' in node;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function childrenOf(node: HtmlNode): readonly HtmlChild[] {
|
|
151
|
-
return 'childNodes' in node ? node.childNodes : [];
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function walk(node: HtmlNode, visit: (node: HtmlNode) => void): void {
|
|
155
|
-
visit(node);
|
|
156
|
-
for (const child of childrenOf(node)) walk(child, visit);
|
|
157
|
-
if (isElement(node) && node.tagName === 'template') {
|
|
158
|
-
const template = node as DefaultTreeAdapterMap['template'];
|
|
159
|
-
for (const child of template.content.childNodes) walk(child, visit);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function elements(root: HtmlNode, predicate?: (node: HtmlElement) => boolean): HtmlElement[] {
|
|
164
|
-
const found: HtmlElement[] = [];
|
|
165
|
-
walk(root, (node) => {
|
|
166
|
-
if (isElement(node) && (predicate?.(node) ?? true)) found.push(node);
|
|
167
|
-
});
|
|
168
|
-
return found;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
149
|
function firstElement(root: HtmlNode, tagName: string): HtmlElement {
|
|
172
150
|
const found = elements(root, (node) => node.tagName === tagName)[0];
|
|
173
151
|
if (!found) throw new Error(`Standalone HTML source has no <${tagName}> element.`);
|
|
174
152
|
return found;
|
|
175
153
|
}
|
|
176
154
|
|
|
177
|
-
function attribute(node: HtmlElement, name: string): string | undefined {
|
|
178
|
-
return node.attrs.find((entry) => entry.name === name)?.value;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function hasClass(node: HtmlElement, name: string): boolean {
|
|
182
|
-
return (attribute(node, 'class') ?? '').split(/\s+/u).includes(name);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
155
|
function setAttribute(node: HtmlElement, name: string, value = ''): void {
|
|
186
156
|
const existing = node.attrs.find((entry) => entry.name === name);
|
|
187
157
|
if (existing) existing.value = value;
|
|
@@ -489,7 +459,7 @@ async function bundleBrowserGraph(
|
|
|
489
459
|
});
|
|
490
460
|
const javascript = result.outputFiles.find((file) => file.path.endsWith('.js'));
|
|
491
461
|
if (!javascript) {
|
|
492
|
-
throw new Error('Browser bundler produced no JavaScript for the standalone
|
|
462
|
+
throw new Error('Browser bundler produced no JavaScript for the standalone page.');
|
|
493
463
|
}
|
|
494
464
|
const unsupported = result.outputFiles.filter(
|
|
495
465
|
(file) => !file.path.endsWith('.js') && !file.path.endsWith('.css'),
|
|
@@ -509,18 +479,6 @@ async function bundleBrowserGraph(
|
|
|
509
479
|
};
|
|
510
480
|
}
|
|
511
481
|
|
|
512
|
-
function inferBasePrefix(document: HtmlNode): string {
|
|
513
|
-
for (const node of elements(document)) {
|
|
514
|
-
for (const name of ['href', 'src', 'component-url', 'renderer-url']) {
|
|
515
|
-
const value = attribute(node, name);
|
|
516
|
-
if (!value?.startsWith('/')) continue;
|
|
517
|
-
const marker = value.indexOf('/astro-assets/');
|
|
518
|
-
if (marker !== -1) return `${value.slice(0, marker)}/`;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
return '/';
|
|
522
|
-
}
|
|
523
|
-
|
|
524
482
|
function normalizedPagePath(pathname: string): string {
|
|
525
483
|
return pathname.replace(/\/index\.html$/iu, '/').replace(/\.html$/iu, '').replace(/\/+$/u, '');
|
|
526
484
|
}
|
|
@@ -538,14 +496,17 @@ function rewriteCourseLink(
|
|
|
538
496
|
throw new Error(`Standalone HTML cannot publish local navigation URL ${JSON.stringify(href)}.`);
|
|
539
497
|
}
|
|
540
498
|
|
|
541
|
-
const pagePath = `${basePrefix}${lessonSlug}/`.replace(
|
|
499
|
+
const pagePath = `${basePrefix}${exportPageRoute(lessonSlug)}/`.replace(
|
|
500
|
+
/\/{2,}/gu,
|
|
501
|
+
'/',
|
|
502
|
+
);
|
|
542
503
|
const resolved = new URL(trimmed, `https://explico.invalid${pagePath}`);
|
|
543
504
|
if (normalizedPagePath(resolved.pathname) === normalizedPagePath(pagePath)) {
|
|
544
505
|
return `${resolved.search}${resolved.hash}` || '#_top';
|
|
545
506
|
}
|
|
546
507
|
if (!canonicalBaseUrl) {
|
|
547
508
|
throw new Error(
|
|
548
|
-
`
|
|
509
|
+
`Page ${lessonSlug} links outside itself (${JSON.stringify(href)}), so ` +
|
|
549
510
|
'`lessonExports.canonicalBaseUrl` is required for standalone HTML.',
|
|
550
511
|
);
|
|
551
512
|
}
|
|
@@ -900,14 +861,14 @@ function auditStandaloneHtml(html: string, lessonSlug: string): void {
|
|
|
900
861
|
}
|
|
901
862
|
}
|
|
902
863
|
|
|
903
|
-
/** Build one fully inlined
|
|
864
|
+
/** Build one fully inlined page document without writing it. */
|
|
904
865
|
export async function buildStandaloneLessonHtml(
|
|
905
866
|
input: StandaloneLessonInput,
|
|
906
867
|
): Promise<string> {
|
|
907
868
|
const outputRoot = resolve(input.outputRoot);
|
|
908
869
|
const sourceFile = resolve(input.sourceFile);
|
|
909
870
|
if (!insideRoot(sourceFile, outputRoot)) {
|
|
910
|
-
throw new Error('Standalone
|
|
871
|
+
throw new Error('Standalone page source must be inside Astro build output.');
|
|
911
872
|
}
|
|
912
873
|
const context: ExportContext = {
|
|
913
874
|
outputRoot,
|
|
@@ -936,8 +897,11 @@ export async function buildStandaloneLessonHtml(
|
|
|
936
897
|
return html;
|
|
937
898
|
}
|
|
938
899
|
|
|
939
|
-
|
|
940
|
-
|
|
900
|
+
/** Astro's built HTML file of a docs slug. */
|
|
901
|
+
export async function pageSourceFile(outputRoot: string, slug: string): Promise<string> {
|
|
902
|
+
const route = exportPageRoute(slug);
|
|
903
|
+
const candidates = [join(outputRoot, route, 'index.html')];
|
|
904
|
+
if (route) candidates.push(join(outputRoot, `${route}.html`));
|
|
941
905
|
for (const candidate of candidates) if (await isFile(candidate)) return candidate;
|
|
942
906
|
throw new Error(
|
|
943
907
|
`Standalone HTML could not find Astro's built page for ${slug}; tried ` +
|
|
@@ -945,7 +909,12 @@ async function lessonSourceFile(outputRoot: string, slug: string): Promise<strin
|
|
|
945
909
|
);
|
|
946
910
|
}
|
|
947
911
|
|
|
948
|
-
/**
|
|
912
|
+
/**
|
|
913
|
+
* Emit the opt-in standalone HTML artifact of every chapter
|
|
914
|
+
* (`standalonePageSlugs`) from Astro's completed static output, at
|
|
915
|
+
* `downloads/<docs slug>.html`. A page that is not a chapter is in its
|
|
916
|
+
* part's and the course's files (`combined-html.ts`).
|
|
917
|
+
*/
|
|
949
918
|
export async function writeStandaloneLessonExports(
|
|
950
919
|
directory: URL,
|
|
951
920
|
manifest: Manifest,
|
|
@@ -953,9 +922,9 @@ export async function writeStandaloneLessonExports(
|
|
|
953
922
|
if (!manifest.lessonExports.standaloneHtml) return;
|
|
954
923
|
const outputRoot = fileURLToPath(directory);
|
|
955
924
|
const pending: { target: string; temporary: string; html: string }[] = [];
|
|
956
|
-
for (const
|
|
957
|
-
const sourceFile = await
|
|
958
|
-
const target = join(outputRoot, 'downloads', `${
|
|
925
|
+
for (const slug of standalonePageSlugs(manifest)) {
|
|
926
|
+
const sourceFile = await pageSourceFile(outputRoot, slug);
|
|
927
|
+
const target = join(outputRoot, 'downloads', `${slug}.html`);
|
|
959
928
|
pending.push({
|
|
960
929
|
target,
|
|
961
930
|
temporary: `${target}.tmp-${process.pid}`,
|
|
@@ -963,13 +932,13 @@ export async function writeStandaloneLessonExports(
|
|
|
963
932
|
html: await readFile(sourceFile, 'utf8'),
|
|
964
933
|
sourceFile,
|
|
965
934
|
outputRoot,
|
|
966
|
-
lessonSlug:
|
|
935
|
+
lessonSlug: slug,
|
|
967
936
|
canonicalBaseUrl: manifest.lessonExports.canonicalBaseUrl,
|
|
968
937
|
}),
|
|
969
938
|
});
|
|
970
939
|
}
|
|
971
940
|
|
|
972
|
-
// Nothing is published until every
|
|
941
|
+
// Nothing is published until every page has passed the asset audit.
|
|
973
942
|
for (const artifact of pending) {
|
|
974
943
|
await mkdir(dirname(artifact.target), { recursive: true });
|
|
975
944
|
await writeFile(artifact.temporary, artifact.html, 'utf8');
|