explico 0.13.0 → 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 +894 -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 +1087 -14
- 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 +29 -0
- 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
|
@@ -2,7 +2,11 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import { extname, join, resolve } from "node:path";
|
|
3
3
|
import { parse } from "parse5";
|
|
4
4
|
import { inferBasePrefix } from "./html-tree.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
readInlineFragment,
|
|
7
|
+
readStaticDeck,
|
|
8
|
+
readStaticPage
|
|
9
|
+
} from "./static-tree.js";
|
|
6
10
|
import {
|
|
7
11
|
captureFigures,
|
|
8
12
|
courseChromium,
|
|
@@ -17,8 +21,9 @@ function strictMode() {
|
|
|
17
21
|
const ci = process.env.CI;
|
|
18
22
|
return !!ci && ci !== "false" && ci !== "0" || process.env.EXPLICO_LATEX_STRICT === "1";
|
|
19
23
|
}
|
|
20
|
-
function epochOf(manifest) {
|
|
21
|
-
const date = manifest.about.knowledge
|
|
24
|
+
function epochOf(manifest, since) {
|
|
25
|
+
const { releases, date: newest } = manifest.about.knowledge;
|
|
26
|
+
const date = (since == null ? void 0 : (releases ?? []).find((release) => release.version === since)?.date) ?? newest;
|
|
22
27
|
const parsed = date ? Date.parse(`${date}T00:00:00Z`) : Number.NaN;
|
|
23
28
|
return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : 0;
|
|
24
29
|
}
|
|
@@ -78,6 +83,28 @@ function withLiveClauses(page, captured, base) {
|
|
|
78
83
|
});
|
|
79
84
|
return { ...page, blocks: update(page.blocks) };
|
|
80
85
|
}
|
|
86
|
+
async function readBuiltDeck(outputRoot, route) {
|
|
87
|
+
const html = await readFile(join(outputRoot, route, "index.html"), "utf8");
|
|
88
|
+
return { html, page: readStaticDeck(html, route, route) };
|
|
89
|
+
}
|
|
90
|
+
function deckBlocks(page) {
|
|
91
|
+
return page.frames.flatMap((frame) => frame.segments.flatMap((segment) => segment.blocks));
|
|
92
|
+
}
|
|
93
|
+
function deckWithLiveClauses(page, captures, figureRoutes, base) {
|
|
94
|
+
const fill = (blocks) => blocks.map((block) => {
|
|
95
|
+
if (block.kind !== "figure") return block;
|
|
96
|
+
const route = figureRoutes.get(block.anchor);
|
|
97
|
+
const live = route === void 0 ? void 0 : captures.get(route)?.get(block.anchor)?.liveHtml;
|
|
98
|
+
return live ? { ...block, live: readInlineFragment(live, page.route, base) } : block;
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
...page,
|
|
102
|
+
frames: page.frames.map((frame) => ({
|
|
103
|
+
...frame,
|
|
104
|
+
segments: frame.segments.map((segment) => ({ ...segment, blocks: fill(segment.blocks) }))
|
|
105
|
+
}))
|
|
106
|
+
};
|
|
107
|
+
}
|
|
81
108
|
async function readBuiltPage(outputRoot, slug) {
|
|
82
109
|
const route = slug.replace(/(?:^|\/)index$/u, "");
|
|
83
110
|
const html = await readFile(join(outputRoot, route, "index.html"), "utf8");
|
|
@@ -90,17 +117,42 @@ async function prepareChapters(outputRoot, manifest, options = {}) {
|
|
|
90
117
|
const read = [];
|
|
91
118
|
let base = "/";
|
|
92
119
|
for (const lesson of manifest.lessons) {
|
|
120
|
+
if (lesson.editions && !lesson.editions.includes("chapter")) continue;
|
|
93
121
|
const { html, page } = await readBuiltPage(outputRoot, lesson.slug);
|
|
94
122
|
if (read.length === 0) base = inferBasePrefix(parse(html));
|
|
95
123
|
read.push({ lesson, page });
|
|
96
124
|
}
|
|
97
|
-
const
|
|
125
|
+
const decksRead = [];
|
|
126
|
+
const pictured = (blocks) => [...figuresOf(blocks), ...diagramsOf(blocks)].map((float) => float.anchor);
|
|
127
|
+
for (const deck of manifest.decks ?? []) {
|
|
128
|
+
const { html, page } = await readBuiltDeck(outputRoot, deck.slug);
|
|
129
|
+
if (read.length === 0 && decksRead.length === 0) base = inferBasePrefix(parse(html));
|
|
130
|
+
const routeOf = (anchor) => {
|
|
131
|
+
const slug = deck.recalledFrom?.[anchor] ?? deck.chapter;
|
|
132
|
+
const chapter = slug === void 0 ? void 0 : read.find((entry) => entry.lesson.slug === slug);
|
|
133
|
+
return chapter && pictured(chapter.page.blocks).includes(anchor) ? chapter.page.route : page.route;
|
|
134
|
+
};
|
|
135
|
+
const figureRoutes = new Map(pictured(deckBlocks(page)).map((anchor) => [anchor, routeOf(anchor)]));
|
|
136
|
+
decksRead.push({ deck, page, figureRoutes });
|
|
137
|
+
}
|
|
138
|
+
const requests = (latex || markdown ? read : []).map(({ lesson, page }) => ({
|
|
98
139
|
route: page.route,
|
|
140
|
+
// A figure's PDF carries its chapter's date (rule C5), as the chapter does.
|
|
141
|
+
epoch: epochOf(manifest, lesson.since),
|
|
99
142
|
anchors: [
|
|
100
143
|
...figuresOf(page.blocks).filter(needsCapture).map((figure) => figure.anchor),
|
|
101
144
|
...markdown ? diagramsOf(page.blocks).map((diagram) => diagram.anchor) : []
|
|
102
145
|
]
|
|
103
146
|
})).filter((request) => request.anchors.length > 0);
|
|
147
|
+
if (latex || markdown) {
|
|
148
|
+
for (const { deck, page, figureRoutes } of decksRead) {
|
|
149
|
+
const anchors = [
|
|
150
|
+
...figuresOf(deckBlocks(page)).filter(needsCapture).map((figure) => figure.anchor),
|
|
151
|
+
...markdown ? diagramsOf(deckBlocks(page)).map((diagram) => diagram.anchor) : []
|
|
152
|
+
].filter((anchor) => figureRoutes.get(anchor) === page.route);
|
|
153
|
+
if (anchors.length > 0) requests.push({ route: page.route, epoch: epochOf(manifest, deck.since), anchors });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
104
156
|
let captures = /* @__PURE__ */ new Map();
|
|
105
157
|
if (requests.length > 0) {
|
|
106
158
|
const chromium = await courseChromium(options.projectRoot ?? process.cwd());
|
|
@@ -145,11 +197,17 @@ async function prepareChapters(outputRoot, manifest, options = {}) {
|
|
|
145
197
|
chapters: read.map(({ lesson, page }) => {
|
|
146
198
|
const captured = captures.get(page.route);
|
|
147
199
|
return { lesson, page: captured ? withLiveClauses(page, captured, base) : page };
|
|
148
|
-
})
|
|
200
|
+
}),
|
|
201
|
+
decks: decksRead.map(({ deck, page, figureRoutes }) => ({
|
|
202
|
+
deck,
|
|
203
|
+
page: deckWithLiveClauses(page, captures, figureRoutes, base),
|
|
204
|
+
figureRoutes
|
|
205
|
+
}))
|
|
149
206
|
};
|
|
150
207
|
}
|
|
151
208
|
export {
|
|
152
209
|
consoleLogger,
|
|
210
|
+
deckBlocks,
|
|
153
211
|
diagramsOf,
|
|
154
212
|
epochOf,
|
|
155
213
|
figuresOf,
|
|
@@ -158,6 +216,7 @@ export {
|
|
|
158
216
|
imagesOf,
|
|
159
217
|
needsCapture,
|
|
160
218
|
prepareChapters,
|
|
219
|
+
readBuiltDeck,
|
|
161
220
|
readBuiltPage,
|
|
162
221
|
strictMode,
|
|
163
222
|
withLiveClauses
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Manifest } from '../manifest.js';
|
|
1
|
+
import { Manifest, ManifestDeck } from '../manifest.js';
|
|
2
2
|
import '../../curriculum/validation.js';
|
|
3
3
|
import '../../reference/consistency.js';
|
|
4
|
-
import '../../course-config-
|
|
4
|
+
import '../../course-config-BLtIdkei.js';
|
|
5
5
|
import '../../rules/catalog.js';
|
|
6
6
|
import '../../rules/course-rules.js';
|
|
7
7
|
import '../collections.js';
|
|
@@ -23,6 +23,10 @@ import '../../reference/mixed-content.js';
|
|
|
23
23
|
import '../../reference/gloss.js';
|
|
24
24
|
import '../../rules/waivers.js';
|
|
25
25
|
import '../knowledge-version.js';
|
|
26
|
+
import '../page-versions.js';
|
|
27
|
+
import '../decks.js';
|
|
28
|
+
import '../../reference/decks-validate.js';
|
|
29
|
+
import '../../reference/deck-source.js';
|
|
26
30
|
import '../lesson-derivation.js';
|
|
27
31
|
|
|
28
32
|
/**
|
|
@@ -42,7 +46,30 @@ declare function standaloneFilename(slug: string): string;
|
|
|
42
46
|
* is in its part's file and the course's (`combined-html.ts`).
|
|
43
47
|
*/
|
|
44
48
|
declare function standalonePageSlugs(manifest: Pick<Manifest, 'lessons'>): string[];
|
|
49
|
+
/**
|
|
50
|
+
* Every page with an HTML download of its own: its route (`slug`) and where
|
|
51
|
+
* it downloads below `downloads/`, without `.html` (`download`). A chapter
|
|
52
|
+
* downloads at its slug; a deck at its owner's {@link deckFileStem}, its
|
|
53
|
+
* Present mode and notes working offline.
|
|
54
|
+
*/
|
|
55
|
+
declare function standaloneDownloads(manifest: Pick<Manifest, 'lessons'> & Partial<Pick<Manifest, 'decks'>>): {
|
|
56
|
+
readonly slug: string;
|
|
57
|
+
readonly download: string;
|
|
58
|
+
}[];
|
|
59
|
+
/**
|
|
60
|
+
* Where a deck downloads below `downloads/`, by its owner's slug — a
|
|
61
|
+
* lesson's (`projectiles/projectile-range`), or a part's directory
|
|
62
|
+
* (`motion`): `projectiles/projectile-range-slides` + `.pdf` (the slides),
|
|
63
|
+
* `-notes.pdf` (each slide with its speaker notes) and `-latex.zip` (their
|
|
64
|
+
* source); a part's deck `motion-slides` + the same. A slides-first lesson's
|
|
65
|
+
* deck is named the same way as a chapter's.
|
|
66
|
+
*/
|
|
67
|
+
declare function deckFileStem(ownerSlug: string): string;
|
|
68
|
+
/** Whose a deck is, as its downloads name it: its lesson's slug, or its part's directory. */
|
|
69
|
+
declare function deckOwnerSlug(deck: Pick<ManifestDeck, 'lessonId' | 'part' | 'slug'>, manifest: Pick<Manifest, 'lessons'>): string;
|
|
70
|
+
/** The name a deck's files are saved under: `projectile-range-slides`, a part's `motion-slides`. */
|
|
71
|
+
declare function deckSaveName(ownerSlug: string): string;
|
|
45
72
|
/** The name the course's files are saved under: `Explico Playground — Motion` → `explico-playground-motion`. */
|
|
46
73
|
declare function courseFileStem(manifest: Pick<Manifest, 'about'>): string;
|
|
47
74
|
|
|
48
|
-
export { courseFileStem, exportPageRoute, exportPageSlugs, standaloneFilename, standalonePageSlugs };
|
|
75
|
+
export { courseFileStem, deckFileStem, deckOwnerSlug, deckSaveName, exportPageRoute, exportPageSlugs, standaloneDownloads, standaloneFilename, standalonePageSlugs };
|
|
@@ -8,15 +8,38 @@ function standaloneFilename(slug) {
|
|
|
8
8
|
return `${exportPageRoute(slug).split("/").at(-1) || "index"}.html`;
|
|
9
9
|
}
|
|
10
10
|
function standalonePageSlugs(manifest) {
|
|
11
|
-
return manifest.lessons.map((lesson) => lesson.slug);
|
|
11
|
+
return manifest.lessons.filter((lesson) => (lesson.editions ?? ["chapter"]).includes("chapter")).map((lesson) => lesson.slug);
|
|
12
|
+
}
|
|
13
|
+
function standaloneDownloads(manifest) {
|
|
14
|
+
return [
|
|
15
|
+
...standalonePageSlugs(manifest).map((slug) => ({ slug, download: slug })),
|
|
16
|
+
...(manifest.decks ?? []).map((deck) => ({
|
|
17
|
+
slug: deck.slug,
|
|
18
|
+
download: deckFileStem(deckOwnerSlug(deck, manifest))
|
|
19
|
+
}))
|
|
20
|
+
];
|
|
21
|
+
}
|
|
22
|
+
function deckFileStem(ownerSlug) {
|
|
23
|
+
return `${ownerSlug}-slides`;
|
|
24
|
+
}
|
|
25
|
+
function deckOwnerSlug(deck, manifest) {
|
|
26
|
+
if (deck.part !== void 0) return deck.part;
|
|
27
|
+
return manifest.lessons.find((lesson) => lesson.id === deck.lessonId)?.slug ?? deck.slug;
|
|
28
|
+
}
|
|
29
|
+
function deckSaveName(ownerSlug) {
|
|
30
|
+
return `${ownerSlug.split("/").at(-1) ?? ownerSlug}-slides`;
|
|
12
31
|
}
|
|
13
32
|
function courseFileStem(manifest) {
|
|
14
33
|
return manifest.about.material.name.normalize("NFKD").replace(/[\u0300-\u036f]/gu, "").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "") || "course";
|
|
15
34
|
}
|
|
16
35
|
export {
|
|
17
36
|
courseFileStem,
|
|
37
|
+
deckFileStem,
|
|
38
|
+
deckOwnerSlug,
|
|
39
|
+
deckSaveName,
|
|
18
40
|
exportPageRoute,
|
|
19
41
|
exportPageSlugs,
|
|
42
|
+
standaloneDownloads,
|
|
20
43
|
standaloneFilename,
|
|
21
44
|
standalonePageSlugs
|
|
22
45
|
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
%% beamerthemeexplico.sty — the beamer theme of an explico deck.
|
|
2
|
+
%%
|
|
3
|
+
%% explico writes this file next to every deck's LaTeX download
|
|
4
|
+
%% (docs/authoring/deck.md), and builds the deck's PDFs from that same folder:
|
|
5
|
+
%% `main.tex` (the slides) and `handout.tex` (each slide followed by its notes).
|
|
6
|
+
%% Edit it freely in a download.
|
|
7
|
+
%%
|
|
8
|
+
%% A frame ends on its legend line, the symbols it introduces (\ExplicoLegend,
|
|
9
|
+
%% which the deck file sets per frame), above a strip with the section's name
|
|
10
|
+
%% and the frame's number; a thin bar under it is the deck's progress.
|
|
11
|
+
%% Engines: LuaLaTeX (preferred) or pdfLaTeX, TeX Live 2025 or later.
|
|
12
|
+
\NeedsTeXFormat{LaTeX2e}
|
|
13
|
+
\ProvidesPackage{beamerthemeexplico}[2026/10/10 explico deck theme]
|
|
14
|
+
\DeclareOptionBeamer{scripts}{\PassOptionsToPackage{scripts}{explico-common}}
|
|
15
|
+
\ProcessOptionsBeamer
|
|
16
|
+
|
|
17
|
+
%% unicode-math sets the math fonts; beamer must leave them alone.
|
|
18
|
+
\usefonttheme{professionalfonts}
|
|
19
|
+
\RequirePackage{explico-common}
|
|
20
|
+
\RequirePackage{etoolbox}
|
|
21
|
+
|
|
22
|
+
\setbeamertemplate{navigation symbols}{}
|
|
23
|
+
\setbeamercovered{invisible}
|
|
24
|
+
\setbeamercolor{structure}{fg=explicoaccent}
|
|
25
|
+
\setbeamercolor{title}{fg=explicoaccent}
|
|
26
|
+
\setbeamercolor{frametitle}{fg=explicoaccent,bg=}
|
|
27
|
+
\setbeamerfont{title}{size=\LARGE,series=\bfseries}
|
|
28
|
+
\setbeamerfont{frametitle}{size=\Large,series=\bfseries}
|
|
29
|
+
\setbeamertemplate{frametitle}{%
|
|
30
|
+
\vskip0.7em
|
|
31
|
+
{\usebeamerfont{frametitle}\usebeamercolor[fg]{frametitle}\insertframetitle\par}%
|
|
32
|
+
\vskip0.15em}
|
|
33
|
+
%% A frame that breaks over several (the notation appendix): "Notation", then
|
|
34
|
+
%% "Notation (continued)".
|
|
35
|
+
\setbeamertemplate{frametitle continuation}[from second][(continued)]
|
|
36
|
+
\setbeamercolor{block title}{fg=black,bg=explicoback!60!white}
|
|
37
|
+
\setbeamercolor{block body}{fg=black,bg=explicoback}
|
|
38
|
+
\setbeamercolor{block title alerted}{fg=black,bg=explicoasidecautionback!80!explicoasidecaution}
|
|
39
|
+
\setbeamercolor{block body alerted}{fg=black,bg=explicoasidecautionback}
|
|
40
|
+
\setbeamertemplate{blocks}[rounded]
|
|
41
|
+
\setbeamertemplate{itemize items}[circle]
|
|
42
|
+
\setbeamertemplate{enumerate items}[default]
|
|
43
|
+
\setbeamerfont{caption}{size=\scriptsize}
|
|
44
|
+
|
|
45
|
+
%% The title frame: the deck's title, its course, its authors and version.
|
|
46
|
+
\setbeamertemplate{title page}{%
|
|
47
|
+
\vfill
|
|
48
|
+
{\usebeamerfont{title}\usebeamercolor[fg]{title}\inserttitle\par}%
|
|
49
|
+
\ifx\insertsubtitle\@empty\else\vskip0.6em{\large\insertsubtitle\par}\fi
|
|
50
|
+
\vskip1.2em
|
|
51
|
+
{\small\color{black!65}\insertauthor\par}%
|
|
52
|
+
\vskip0.3em
|
|
53
|
+
{\small\color{black!65}\insertdate\par}%
|
|
54
|
+
\vfill}
|
|
55
|
+
|
|
56
|
+
%% A section's divider frame: its title over a rule.
|
|
57
|
+
\newcommand{\ExplicoSectionPage}{%
|
|
58
|
+
\vfill
|
|
59
|
+
{\usebeamerfont{title}\usebeamercolor[fg]{title}\insertsectionhead\par}%
|
|
60
|
+
\vskip0.3em{\color{explicoaccent}\rule{0.5\linewidth}{1.5pt}}%
|
|
61
|
+
\vfill}
|
|
62
|
+
|
|
63
|
+
%% The deck's own frames (deck.tex sets their count): the progress bar runs
|
|
64
|
+
%% over them, and the notation appendix after them shows none.
|
|
65
|
+
\def\explico@deckframes{\inserttotalframenumber}
|
|
66
|
+
\newcommand{\ExplicoDeckFrames}[1]{\gdef\explico@deckframes{#1}}
|
|
67
|
+
|
|
68
|
+
%% The legend line: the symbols a frame introduces. The frame's body sets it
|
|
69
|
+
%% on every slide of the frame; the footline prints it and clears it.
|
|
70
|
+
\gdef\explico@legend{}
|
|
71
|
+
\newcommand{\ExplicoLegend}[1]{\gdef\explico@legend{#1}}
|
|
72
|
+
\setbeamertemplate{footline}{%
|
|
73
|
+
\leavevmode
|
|
74
|
+
\ifx\explico@legend\@empty\else
|
|
75
|
+
\hbox to \paperwidth{\hskip1.6em\parbox[b]{\dimexpr\paperwidth-3.2em}{%
|
|
76
|
+
\raggedright\scriptsize\color{black!70}\explico@legend}}\par\vskip3pt
|
|
77
|
+
\fi
|
|
78
|
+
\global\let\explico@legend\@empty
|
|
79
|
+
\hbox to \paperwidth{\hskip1.6em{\tiny\color{black!55}\insertsectionhead}\hfill
|
|
80
|
+
{\tiny\color{black!55}\insertframenumber}\hskip1.2em}\par\vskip3pt
|
|
81
|
+
\ifnum\c@framenumber>\explico@deckframes\relax\else
|
|
82
|
+
{\color{explicoaccent}\rule{\dimexpr\paperwidth*\c@framenumber/\explico@deckframes\relax}{1.2pt}}%
|
|
83
|
+
\fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
%% A note page of the handout (handout.tex): the slide's number and title
|
|
87
|
+
%% beside a small copy of the slide, then its speaker notes.
|
|
88
|
+
\setbeamerfont{note page}{size=\small}
|
|
89
|
+
\setbeamertemplate{note page}{%
|
|
90
|
+
\vskip1em
|
|
91
|
+
\hbox to \textwidth{%
|
|
92
|
+
\vtop{\hsize=\dimexpr\textwidth-0.3\paperwidth-1.5em\relax
|
|
93
|
+
\vskip0pt
|
|
94
|
+
{\usebeamercolor[fg]{frametitle}\normalsize\bfseries Slide~\insertframenumber\par}%
|
|
95
|
+
\vskip0.3em
|
|
96
|
+
{\small\raggedright\insertframetitle\par}}%
|
|
97
|
+
\hfill
|
|
98
|
+
\vtop{\vskip0pt\hbox{\setlength{\fboxsep}{0pt}\fcolorbox{black!25}{white}{\insertslideintonotes{0.3}}}}}%
|
|
99
|
+
\vskip1.2em
|
|
100
|
+
{\usebeamerfont{note page}\raggedright\insertnote\par}%
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
%% A figure's, a table's or a diagram's caption on a frame: its number (a
|
|
104
|
+
%% float recalled from the chapter keeps the chapter's), its short caption in
|
|
105
|
+
%% bold, then the long one.
|
|
106
|
+
\newcommand{\ExplicoDeckCaption}[3]{%
|
|
107
|
+
\par\smallskip{\usebeamerfont{caption}%
|
|
108
|
+
\if\relax\detokenize{#1}\relax\else{\color{explicoaccent}\textbf{#1}}\enspace\fi
|
|
109
|
+
\if\relax\detokenize{#2}\relax\else\textbf{#2.}\space\fi#3\par}}
|
|
110
|
+
|
|
111
|
+
%% A deck's references frame: the list its citations number, as on the
|
|
112
|
+
%% slides. The frame's title names it, so the list has no heading of its own.
|
|
113
|
+
\newenvironment{ExplicoReferences}[1]{%
|
|
114
|
+
\small
|
|
115
|
+
\list{\@biblabel{\@arabic\c@enumiv}}{%
|
|
116
|
+
\settowidth\labelwidth{\@biblabel{#1}}%
|
|
117
|
+
\leftmargin\labelwidth\advance\leftmargin\labelsep
|
|
118
|
+
\usecounter{enumiv}\let\p@enumiv\@empty
|
|
119
|
+
\renewcommand\theenumiv{\@arabic\c@enumiv}}%
|
|
120
|
+
\sloppy\raggedright
|
|
121
|
+
}{\endlist}
|
|
122
|
+
|
|
123
|
+
\pdfstringdefDisableCommands{\let\ExplicoGreekWord\@firstofone}
|
|
124
|
+
\endinput
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ExportLogger } from '../export-chapters.js';
|
|
2
|
+
import { LatexEngine, CompiledLatex, compileLatex } from './compile.js';
|
|
3
|
+
import { SourceFile } from './document.js';
|
|
4
|
+
import '../../manifest.js';
|
|
5
|
+
import '../../../curriculum/validation.js';
|
|
6
|
+
import '../../../reference/consistency.js';
|
|
7
|
+
import '../../../course-config-BLtIdkei.js';
|
|
8
|
+
import '../../../rules/catalog.js';
|
|
9
|
+
import '../../../rules/course-rules.js';
|
|
10
|
+
import '../../collections.js';
|
|
11
|
+
import '../../../reference/types.js';
|
|
12
|
+
import '../../../reference/gate-retired-keys.js';
|
|
13
|
+
import '../../../reference/lint-ignore.js';
|
|
14
|
+
import '../../../reference/equations-validate.js';
|
|
15
|
+
import '../../../reference/tables-validate.js';
|
|
16
|
+
import '../../../reference/keyed-float-validate.js';
|
|
17
|
+
import '../../../reference/figures-validate.js';
|
|
18
|
+
import '../../../reference/diagrams-validate.js';
|
|
19
|
+
import '../../../reference/examples-validate.js';
|
|
20
|
+
import '../../../reference/folds-validate.js';
|
|
21
|
+
import '../../../reference/knowledge-checks-validate.js';
|
|
22
|
+
import '../../../reference/raw-floats-validate.js';
|
|
23
|
+
import '../../../reference/lab-katex-validate.js';
|
|
24
|
+
import '../../../reference/gate-math.js';
|
|
25
|
+
import '../../../reference/mixed-content.js';
|
|
26
|
+
import '../../../reference/gloss.js';
|
|
27
|
+
import '../../../rules/waivers.js';
|
|
28
|
+
import '../../knowledge-version.js';
|
|
29
|
+
import '../../page-versions.js';
|
|
30
|
+
import '../../decks.js';
|
|
31
|
+
import '../../../reference/decks-validate.js';
|
|
32
|
+
import '../../../reference/deck-source.js';
|
|
33
|
+
import '../../lesson-derivation.js';
|
|
34
|
+
import '../static-tree.js';
|
|
35
|
+
import '../html-tree.js';
|
|
36
|
+
import 'parse5';
|
|
37
|
+
import './capture.js';
|
|
38
|
+
import 'node:http';
|
|
39
|
+
import './serialize.js';
|
|
40
|
+
|
|
41
|
+
interface CacheContext {
|
|
42
|
+
readonly engine: LatexEngine['name'];
|
|
43
|
+
/** `texFingerprint`'s description of the TeX installation. */
|
|
44
|
+
readonly fingerprint: string;
|
|
45
|
+
readonly epoch: number;
|
|
46
|
+
readonly salt: string;
|
|
47
|
+
}
|
|
48
|
+
/** The key of one compile: a hex SHA-256 over its context and its files in path order. */
|
|
49
|
+
declare function cacheKey(files: readonly SourceFile[], context: CacheContext): string;
|
|
50
|
+
interface LatexCache {
|
|
51
|
+
readonly directory: string;
|
|
52
|
+
/** The document compiled, or reused from an entry with the same key. */
|
|
53
|
+
compile(files: readonly SourceFile[], options: {
|
|
54
|
+
readonly epoch: number;
|
|
55
|
+
readonly name?: string;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
readonly result: CompiledLatex;
|
|
58
|
+
readonly reused: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
/** Delete the entries no build has used for `KEEP_DAYS`; how many went. */
|
|
61
|
+
prune(now?: Date): Promise<number>;
|
|
62
|
+
}
|
|
63
|
+
declare function openLatexCache(options: {
|
|
64
|
+
readonly directory: string;
|
|
65
|
+
readonly engine: LatexEngine;
|
|
66
|
+
readonly fingerprint: string;
|
|
67
|
+
readonly salt?: string;
|
|
68
|
+
readonly logger?: ExportLogger;
|
|
69
|
+
/** The compile behind a miss; `compileLatex` but in tests. */
|
|
70
|
+
readonly compile?: typeof compileLatex;
|
|
71
|
+
}): LatexCache;
|
|
72
|
+
/**
|
|
73
|
+
* The cache `EXPLICO_LATEX_CACHE` names, or `undefined` when it is unset
|
|
74
|
+
* (today's build: every document compiles) or the directory cannot be made.
|
|
75
|
+
*/
|
|
76
|
+
declare function latexCacheFromEnvironment(engine: LatexEngine, logger: ExportLogger): Promise<LatexCache | undefined>;
|
|
77
|
+
|
|
78
|
+
export { type CacheContext, type LatexCache, cacheKey, latexCacheFromEnvironment, openLatexCache };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, readdir, rename, rm, stat, utimes, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { compileLatex, texFingerprint } from "./compile.js";
|
|
5
|
+
const FORMAT = "explico-latex-cache 1";
|
|
6
|
+
const KEEP_DAYS = 7;
|
|
7
|
+
const ENTRY = /^[0-9a-f]{64}$/u;
|
|
8
|
+
function cacheKey(files, context) {
|
|
9
|
+
const hash = createHash("sha256");
|
|
10
|
+
for (const line of [FORMAT, context.engine, context.fingerprint, String(context.epoch), context.salt]) {
|
|
11
|
+
hash.update(`${line.length}:${line}
|
|
12
|
+
`);
|
|
13
|
+
}
|
|
14
|
+
for (const file of [...files].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) {
|
|
15
|
+
const content = typeof file.content === "string" ? Buffer.from(file.content, "utf8") : file.content;
|
|
16
|
+
hash.update(`${Buffer.byteLength(file.path)}:${file.path}
|
|
17
|
+
${content.byteLength}
|
|
18
|
+
`);
|
|
19
|
+
hash.update(content);
|
|
20
|
+
}
|
|
21
|
+
return hash.digest("hex");
|
|
22
|
+
}
|
|
23
|
+
function encodeEntry(result) {
|
|
24
|
+
const parts = [result.pdf, Buffer.from(result.aux, "utf8"), Buffer.from(result.log, "utf8")];
|
|
25
|
+
const payload = Buffer.concat(parts);
|
|
26
|
+
const header = {
|
|
27
|
+
pdf: parts[0].byteLength,
|
|
28
|
+
aux: parts[1].byteLength,
|
|
29
|
+
log: parts[2].byteLength,
|
|
30
|
+
sha256: createHash("sha256").update(payload).digest("hex")
|
|
31
|
+
};
|
|
32
|
+
return Buffer.concat([Buffer.from(`${FORMAT}
|
|
33
|
+
${JSON.stringify(header)}
|
|
34
|
+
`, "utf8"), payload]);
|
|
35
|
+
}
|
|
36
|
+
function decodeEntry(bytes) {
|
|
37
|
+
const formatEnd = bytes.indexOf(10);
|
|
38
|
+
const headerEnd = formatEnd < 0 ? -1 : bytes.indexOf(10, formatEnd + 1);
|
|
39
|
+
if (headerEnd < 0 || bytes.subarray(0, formatEnd).toString("utf8") !== FORMAT) return void 0;
|
|
40
|
+
let header;
|
|
41
|
+
try {
|
|
42
|
+
header = JSON.parse(bytes.subarray(formatEnd + 1, headerEnd).toString("utf8"));
|
|
43
|
+
} catch {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
const { pdf, aux, log, sha256 } = header;
|
|
47
|
+
if (typeof pdf !== "number" || typeof aux !== "number" || typeof log !== "number") return void 0;
|
|
48
|
+
const payload = bytes.subarray(headerEnd + 1);
|
|
49
|
+
if (payload.byteLength !== pdf + aux + log) return void 0;
|
|
50
|
+
if (createHash("sha256").update(payload).digest("hex") !== sha256) return void 0;
|
|
51
|
+
return {
|
|
52
|
+
pdf: Buffer.from(payload.subarray(0, pdf)),
|
|
53
|
+
aux: payload.subarray(pdf, pdf + aux).toString("utf8"),
|
|
54
|
+
log: payload.subarray(pdf + aux).toString("utf8")
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function openLatexCache(options) {
|
|
58
|
+
const { directory, engine, fingerprint } = options;
|
|
59
|
+
const compile = options.compile ?? compileLatex;
|
|
60
|
+
let warned = false;
|
|
61
|
+
const warnOnce = (message) => {
|
|
62
|
+
if (warned) return;
|
|
63
|
+
warned = true;
|
|
64
|
+
options.logger?.warn(message);
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
directory,
|
|
68
|
+
async compile(files, compileOptions) {
|
|
69
|
+
const key = cacheKey(files, { engine: engine.name, fingerprint, epoch: compileOptions.epoch, salt: options.salt ?? "" });
|
|
70
|
+
const path = join(directory, key);
|
|
71
|
+
const cached = await readFile(path).then(decodeEntry, () => void 0);
|
|
72
|
+
if (cached) {
|
|
73
|
+
const now = /* @__PURE__ */ new Date();
|
|
74
|
+
await utimes(path, now, now).catch(() => void 0);
|
|
75
|
+
return { result: cached, reused: true };
|
|
76
|
+
}
|
|
77
|
+
const result = await compile(files, engine, compileOptions);
|
|
78
|
+
const temporary = `${path}.tmp-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
79
|
+
try {
|
|
80
|
+
await writeFile(temporary, encodeEntry(result));
|
|
81
|
+
await rename(temporary, path);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
await rm(temporary, { force: true });
|
|
84
|
+
warnOnce(
|
|
85
|
+
`LaTeX export: could not store a compile in ${directory} (${error instanceof Error ? error.message : String(error)}); the build goes on without it.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return { result, reused: false };
|
|
89
|
+
},
|
|
90
|
+
async prune(now = /* @__PURE__ */ new Date()) {
|
|
91
|
+
const cutoff = now.getTime() - KEEP_DAYS * 24 * 60 * 60 * 1e3;
|
|
92
|
+
let removed = 0;
|
|
93
|
+
for (const name of await readdir(directory).catch(() => [])) {
|
|
94
|
+
if (!ENTRY.test(name) && !/^[0-9a-f]{64}\.tmp-/u.test(name)) continue;
|
|
95
|
+
const path = join(directory, name);
|
|
96
|
+
const modified = await stat(path).then((entry) => entry.mtimeMs, () => void 0);
|
|
97
|
+
if (modified === void 0 || modified >= cutoff) continue;
|
|
98
|
+
await rm(path, { force: true });
|
|
99
|
+
removed += 1;
|
|
100
|
+
}
|
|
101
|
+
return removed;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function latexCacheFromEnvironment(engine, logger) {
|
|
106
|
+
const setting = process.env.EXPLICO_LATEX_CACHE?.trim();
|
|
107
|
+
if (!setting) return void 0;
|
|
108
|
+
const directory = resolve(setting);
|
|
109
|
+
try {
|
|
110
|
+
await mkdir(directory, { recursive: true });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
logger.warn(
|
|
113
|
+
`LaTeX export: EXPLICO_LATEX_CACHE names ${directory}, which cannot be made (${error instanceof Error ? error.message : String(error)}); every document compiles.`
|
|
114
|
+
);
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
return openLatexCache({
|
|
118
|
+
directory,
|
|
119
|
+
engine,
|
|
120
|
+
fingerprint: await texFingerprint(engine),
|
|
121
|
+
salt: process.env.EXPLICO_LATEX_CACHE_SALT ?? "",
|
|
122
|
+
logger
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export {
|
|
126
|
+
cacheKey,
|
|
127
|
+
latexCacheFromEnvironment,
|
|
128
|
+
openLatexCache
|
|
129
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Server } from 'node:http';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* The capture pass: draw each figure a browser has to draw (a hydrated lab, a
|
|
3
5
|
* course component's SVG) and print it as a vector PDF for the LaTeX source.
|
|
@@ -24,6 +26,8 @@ interface CaptureRequest {
|
|
|
24
26
|
readonly route: string;
|
|
25
27
|
/** Anchor ids of the figures and diagrams to capture (`fig-…`, `dia-…`). */
|
|
26
28
|
readonly anchors: readonly string[];
|
|
29
|
+
/** This page's date for its figure PDFs, over the capture's `epoch`. */
|
|
30
|
+
readonly epoch?: number;
|
|
27
31
|
}
|
|
28
32
|
interface CapturedFigure {
|
|
29
33
|
/** The figure's picture as a one-page vector PDF. */
|
|
@@ -67,6 +71,11 @@ interface PageLike {
|
|
|
67
71
|
}
|
|
68
72
|
/** Playwright's Chromium from the course's own dependencies, or `undefined`. */
|
|
69
73
|
declare function courseChromium(projectRoot: string): Promise<ChromiumLike | undefined>;
|
|
74
|
+
/** Serve `root` on loopback at `base`; resolves with the origin once listening. */
|
|
75
|
+
declare function serve(root: string, base: string): Promise<{
|
|
76
|
+
origin: string;
|
|
77
|
+
server: Server;
|
|
78
|
+
}>;
|
|
70
79
|
/** The size a figure's most common label prints at, as `\footnotesize` in the book's 11 pt. */
|
|
71
80
|
declare const LABEL_PT = 9;
|
|
72
81
|
/** The book's measure, in PostScript points: the most a figure is set at. */
|
|
@@ -103,4 +112,4 @@ interface CaptureOptions {
|
|
|
103
112
|
/** Capture every requested figure, one browser for the whole build. */
|
|
104
113
|
declare function captureFigures(requests: readonly CaptureRequest[], options: CaptureOptions): Promise<CaptureResults>;
|
|
105
114
|
|
|
106
|
-
export { type CaptureOptions, type CaptureRequest, type CaptureResults, type CapturedFigure, type FigureMeasure, LABEL_PT, captureFigures, courseChromium, figureMeasure, stampPdfDates };
|
|
115
|
+
export { type CaptureOptions, type CaptureRequest, type CaptureResults, type CapturedFigure, type FigureMeasure, LABEL_PT, captureFigures, courseChromium, figureMeasure, serve, stampPdfDates };
|
|
@@ -408,7 +408,8 @@ async function captureFigures(requests, options) {
|
|
|
408
408
|
options.measure ?? figureMeasure("letter"),
|
|
409
409
|
options.png ?? false
|
|
410
410
|
);
|
|
411
|
-
const
|
|
411
|
+
const epoch = request.epoch ?? options.epoch;
|
|
412
|
+
const pdf = epoch === void 0 ? printed.pdf : stampPdfDates(printed.pdf, epoch);
|
|
412
413
|
figures.set(anchor, { ...printed, pdf, liveHtml: snapshot.live });
|
|
413
414
|
}
|
|
414
415
|
results.set(request.route, figures);
|
|
@@ -431,5 +432,6 @@ export {
|
|
|
431
432
|
captureFigures,
|
|
432
433
|
courseChromium,
|
|
433
434
|
figureMeasure,
|
|
435
|
+
serve,
|
|
434
436
|
stampPdfDates
|
|
435
437
|
};
|
|
@@ -2,7 +2,7 @@ import { SourceFile } from './document.js';
|
|
|
2
2
|
import '../../manifest.js';
|
|
3
3
|
import '../../../curriculum/validation.js';
|
|
4
4
|
import '../../../reference/consistency.js';
|
|
5
|
-
import '../../../course-config-
|
|
5
|
+
import '../../../course-config-BLtIdkei.js';
|
|
6
6
|
import '../../../rules/catalog.js';
|
|
7
7
|
import '../../../rules/course-rules.js';
|
|
8
8
|
import '../../collections.js';
|
|
@@ -24,6 +24,10 @@ import '../../../reference/mixed-content.js';
|
|
|
24
24
|
import '../../../reference/gloss.js';
|
|
25
25
|
import '../../../rules/waivers.js';
|
|
26
26
|
import '../../knowledge-version.js';
|
|
27
|
+
import '../../page-versions.js';
|
|
28
|
+
import '../../decks.js';
|
|
29
|
+
import '../../../reference/decks-validate.js';
|
|
30
|
+
import '../../../reference/deck-source.js';
|
|
27
31
|
import '../../lesson-derivation.js';
|
|
28
32
|
import '../static-tree.js';
|
|
29
33
|
import '../html-tree.js';
|
|
@@ -53,6 +57,13 @@ interface CompiledLatex {
|
|
|
53
57
|
declare function findLatexEngine(): Promise<LatexEngine | {
|
|
54
58
|
readonly missing: string;
|
|
55
59
|
}>;
|
|
60
|
+
/**
|
|
61
|
+
* What the compile cache knows of the TeX installation: the engine's and
|
|
62
|
+
* latexmk's banners and the LaTeX kernel's date and patch level. A TeX Live
|
|
63
|
+
* release or a kernel update changes it; a package updated with `tlmgr` does
|
|
64
|
+
* not, which is what `EXPLICO_LATEX_CACHE_SALT` is for.
|
|
65
|
+
*/
|
|
66
|
+
declare function texFingerprint(engine: LatexEngine): Promise<string>;
|
|
56
67
|
/**
|
|
57
68
|
* The characters a compile could not draw, in first-seen order: LuaLaTeX
|
|
58
69
|
* leaves a blank where the font has no glyph and only logs `Missing
|
|
@@ -70,4 +81,4 @@ declare function compileLatex(files: readonly SourceFile[], engine: LatexEngine,
|
|
|
70
81
|
readonly name?: string;
|
|
71
82
|
}): Promise<CompiledLatex>;
|
|
72
83
|
|
|
73
|
-
export { type CompiledLatex, type LatexEngine, compileLatex, findLatexEngine, missingCharacters };
|
|
84
|
+
export { type CompiledLatex, type LatexEngine, compileLatex, findLatexEngine, missingCharacters, texFingerprint };
|
|
@@ -42,6 +42,16 @@ async function findLatexEngine() {
|
|
|
42
42
|
}
|
|
43
43
|
return requested === "lualatex" ? { name: "lualatex", latexmkOption: "-lualatex" } : { name: "pdflatex", latexmkOption: "-pdf" };
|
|
44
44
|
}
|
|
45
|
+
async function texFingerprint(engine) {
|
|
46
|
+
const firstLine = (output, pattern) => output.split("\n").find((line) => pattern.test(line))?.trim() ?? "";
|
|
47
|
+
const banner = firstLine((await run(engine.name, ["--version"])).output, /\S/u);
|
|
48
|
+
const latexmk = firstLine((await run("latexmk", ["-v"])).output, /Version/u);
|
|
49
|
+
const kernelFile = (await run("kpsewhich", ["latex.ltx"])).output.trim();
|
|
50
|
+
const kernel = kernelFile ? await readFile(kernelFile, "utf8").catch(() => "") : "";
|
|
51
|
+
const date = /\\edef\\fmtversion\s*\{([^}]*)\}/u.exec(kernel)?.[1] ?? "";
|
|
52
|
+
const patch = /\\def\\patch@level\{([^}]*)\}/u.exec(kernel)?.[1] ?? "";
|
|
53
|
+
return [banner, latexmk, `LaTeX2e ${date} patch level ${patch}`].join("\n");
|
|
54
|
+
}
|
|
45
55
|
function latexErrors(log) {
|
|
46
56
|
const lines = log.split("\n");
|
|
47
57
|
const found = [];
|
|
@@ -111,5 +121,6 @@ async function compileLatex(files, engine, options = {}) {
|
|
|
111
121
|
export {
|
|
112
122
|
compileLatex,
|
|
113
123
|
findLatexEngine,
|
|
114
|
-
missingCharacters
|
|
124
|
+
missingCharacters,
|
|
125
|
+
texFingerprint
|
|
115
126
|
};
|