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,9 +2,16 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { extname, join, resolve } from 'node:path';
|
|
3
3
|
import { parse } from 'parse5';
|
|
4
4
|
|
|
5
|
-
import type { Manifest, ManifestLesson } from '../manifest';
|
|
5
|
+
import type { Manifest, ManifestDeck, ManifestLesson } from '../manifest';
|
|
6
6
|
import { inferBasePrefix } from './html-tree';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
readInlineFragment,
|
|
9
|
+
readStaticDeck,
|
|
10
|
+
readStaticPage,
|
|
11
|
+
type Block,
|
|
12
|
+
type StaticDeck,
|
|
13
|
+
type StaticPage,
|
|
14
|
+
} from './static-tree';
|
|
8
15
|
import {
|
|
9
16
|
captureFigures,
|
|
10
17
|
courseChromium,
|
|
@@ -37,9 +44,15 @@ export function strictMode(): boolean {
|
|
|
37
44
|
return (!!ci && ci !== 'false' && ci !== '0') || process.env.EXPLICO_LATEX_STRICT === '1';
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
/**
|
|
48
|
+
* A document's date as seconds since 1970, for its timestamps: the date of
|
|
49
|
+
* the release its content last changed in (`since`, rule C5), so a document
|
|
50
|
+
* keeps its date across the releases that did not touch it; the newest
|
|
51
|
+
* release's date for a change not yet released, and with no `since`.
|
|
52
|
+
*/
|
|
53
|
+
export function epochOf(manifest: Pick<Manifest, 'about'>, since?: string | null): number {
|
|
54
|
+
const { releases, date: newest } = manifest.about.knowledge;
|
|
55
|
+
const date = (since == null ? undefined : (releases ?? []).find((release) => release.version === since)?.date) ?? newest;
|
|
43
56
|
const parsed = date ? Date.parse(`${date}T00:00:00Z`) : Number.NaN;
|
|
44
57
|
return Number.isFinite(parsed) ? Math.floor(parsed / 1000) : 0;
|
|
45
58
|
}
|
|
@@ -130,14 +143,63 @@ export interface PreparedChapter {
|
|
|
130
143
|
readonly page: StaticPage;
|
|
131
144
|
}
|
|
132
145
|
|
|
146
|
+
/** A lesson's deck, read from its page (docs/authoring/deck.md). */
|
|
147
|
+
export interface PreparedDeck {
|
|
148
|
+
readonly deck: ManifestDeck;
|
|
149
|
+
/** Its frames, each lab's live caption clause filled in. */
|
|
150
|
+
readonly page: StaticDeck;
|
|
151
|
+
/**
|
|
152
|
+
* Where each of its figures and diagrams was drawn, by anchor: the deck's
|
|
153
|
+
* own route, or its chapter's for one it recalls (the chapter's capture is
|
|
154
|
+
* reused).
|
|
155
|
+
*/
|
|
156
|
+
readonly figureRoutes: ReadonlyMap<string, string>;
|
|
157
|
+
}
|
|
158
|
+
|
|
133
159
|
export interface PreparedChapters {
|
|
134
160
|
/** The site's base path, `/` or `/course/`. */
|
|
135
161
|
readonly base: string;
|
|
136
162
|
readonly chapters: readonly PreparedChapter[];
|
|
163
|
+
/** Each lesson's deck. */
|
|
164
|
+
readonly decks: readonly PreparedDeck[];
|
|
137
165
|
/** What the capture pass drew, by route and anchor. */
|
|
138
166
|
readonly captures: CaptureResults;
|
|
139
167
|
}
|
|
140
168
|
|
|
169
|
+
/** Read one built deck page into its frames. */
|
|
170
|
+
export async function readBuiltDeck(outputRoot: string, route: string): Promise<{ html: string; page: StaticDeck }> {
|
|
171
|
+
const html = await readFile(join(outputRoot, route, 'index.html'), 'utf8');
|
|
172
|
+
return { html, page: readStaticDeck(html, route, route) };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Every block a deck's frames show, notes aside. */
|
|
176
|
+
export function deckBlocks(page: StaticDeck): Block[] {
|
|
177
|
+
return page.frames.flatMap((frame) => frame.segments.flatMap((segment) => segment.blocks));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The deck with each figure's live caption clause filled in from where it was drawn. */
|
|
181
|
+
function deckWithLiveClauses(
|
|
182
|
+
page: StaticDeck,
|
|
183
|
+
captures: CaptureResults,
|
|
184
|
+
figureRoutes: ReadonlyMap<string, string>,
|
|
185
|
+
base: string,
|
|
186
|
+
): StaticDeck {
|
|
187
|
+
const fill = (blocks: readonly Block[]): Block[] =>
|
|
188
|
+
blocks.map((block) => {
|
|
189
|
+
if (block.kind !== 'figure') return block;
|
|
190
|
+
const route = figureRoutes.get(block.anchor);
|
|
191
|
+
const live = route === undefined ? undefined : captures.get(route)?.get(block.anchor)?.liveHtml;
|
|
192
|
+
return live ? { ...block, live: readInlineFragment(live, page.route, base) } : block;
|
|
193
|
+
});
|
|
194
|
+
return {
|
|
195
|
+
...page,
|
|
196
|
+
frames: page.frames.map((frame) => ({
|
|
197
|
+
...frame,
|
|
198
|
+
segments: frame.segments.map((segment) => ({ ...segment, blocks: fill(segment.blocks) })),
|
|
199
|
+
})),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
141
203
|
/** Read one built page into its reading tree. */
|
|
142
204
|
export async function readBuiltPage(outputRoot: string, slug: string): Promise<{ html: string; page: StaticPage }> {
|
|
143
205
|
const route = slug.replace(/(?:^|\/)index$/u, '');
|
|
@@ -164,20 +226,52 @@ export async function prepareChapters(
|
|
|
164
226
|
const read: { lesson: ManifestLesson; page: StaticPage }[] = [];
|
|
165
227
|
let base = '/';
|
|
166
228
|
for (const lesson of manifest.lessons) {
|
|
229
|
+
// A slides-first lesson has no chapter to print: its deck is read below.
|
|
230
|
+
if (lesson.editions && !lesson.editions.includes('chapter')) continue;
|
|
167
231
|
const { html, page } = await readBuiltPage(outputRoot, lesson.slug);
|
|
168
232
|
if (read.length === 0) base = inferBasePrefix(parse(html));
|
|
169
233
|
read.push({ lesson, page });
|
|
170
234
|
}
|
|
171
235
|
|
|
236
|
+
// Each deck's frames. A figure or diagram it recalls is its chapter's, drawn
|
|
237
|
+
// there: a lesson's deck recalls from its chapter, a part's from the
|
|
238
|
+
// chapter each recall names (`recalledFrom`).
|
|
239
|
+
const decksRead: { deck: ManifestDeck; page: StaticDeck; figureRoutes: Map<string, string> }[] = [];
|
|
240
|
+
const pictured = (blocks: readonly Block[]) => [...figuresOf(blocks), ...diagramsOf(blocks)].map((float) => float.anchor);
|
|
241
|
+
for (const deck of manifest.decks ?? []) {
|
|
242
|
+
const { html, page } = await readBuiltDeck(outputRoot, deck.slug);
|
|
243
|
+
if (read.length === 0 && decksRead.length === 0) base = inferBasePrefix(parse(html));
|
|
244
|
+
const routeOf = (anchor: string) => {
|
|
245
|
+
const slug = deck.recalledFrom?.[anchor] ?? deck.chapter;
|
|
246
|
+
const chapter = slug === undefined ? undefined : read.find((entry) => entry.lesson.slug === slug);
|
|
247
|
+
return chapter && pictured(chapter.page.blocks).includes(anchor) ? chapter.page.route : page.route;
|
|
248
|
+
};
|
|
249
|
+
const figureRoutes = new Map(pictured(deckBlocks(page)).map((anchor) => [anchor, routeOf(anchor)]));
|
|
250
|
+
decksRead.push({ deck, page, figureRoutes });
|
|
251
|
+
}
|
|
252
|
+
|
|
172
253
|
const requests: CaptureRequest[] = (latex || markdown ? read : [])
|
|
173
|
-
.map(({ page }) => ({
|
|
254
|
+
.map(({ lesson, page }) => ({
|
|
174
255
|
route: page.route,
|
|
256
|
+
// A figure's PDF carries its chapter's date (rule C5), as the chapter does.
|
|
257
|
+
epoch: epochOf(manifest, lesson.since),
|
|
175
258
|
anchors: [
|
|
176
259
|
...figuresOf(page.blocks).filter(needsCapture).map((figure) => figure.anchor),
|
|
177
260
|
...(markdown ? diagramsOf(page.blocks).map((diagram) => diagram.anchor) : []),
|
|
178
261
|
],
|
|
179
262
|
}))
|
|
180
263
|
.filter((request) => request.anchors.length > 0);
|
|
264
|
+
// A deck's own figures (and, for Markdown, its own diagrams), drawn on its page.
|
|
265
|
+
if (latex || markdown) {
|
|
266
|
+
for (const { deck, page, figureRoutes } of decksRead) {
|
|
267
|
+
const anchors = [
|
|
268
|
+
...figuresOf(deckBlocks(page)).filter(needsCapture).map((figure) => figure.anchor),
|
|
269
|
+
...(markdown ? diagramsOf(deckBlocks(page)).map((diagram) => diagram.anchor) : []),
|
|
270
|
+
].filter((anchor) => figureRoutes.get(anchor) === page.route);
|
|
271
|
+
// A figure's PDF carries its deck's date (rule C5).
|
|
272
|
+
if (anchors.length > 0) requests.push({ route: page.route, epoch: epochOf(manifest, deck.since), anchors });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
181
275
|
let captures: CaptureResults = new Map();
|
|
182
276
|
if (requests.length > 0) {
|
|
183
277
|
const chromium = await courseChromium(options.projectRoot ?? process.cwd());
|
|
@@ -229,5 +323,10 @@ export async function prepareChapters(
|
|
|
229
323
|
const captured = captures.get(page.route);
|
|
230
324
|
return { lesson, page: captured ? withLiveClauses(page, captured, base) : page };
|
|
231
325
|
}),
|
|
326
|
+
decks: decksRead.map(({ deck, page, figureRoutes }) => ({
|
|
327
|
+
deck,
|
|
328
|
+
page: deckWithLiveClauses(page, captures, figureRoutes, base),
|
|
329
|
+
figureRoutes,
|
|
330
|
+
})),
|
|
232
331
|
};
|
|
233
332
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Manifest } from '../manifest';
|
|
1
|
+
import type { Manifest, ManifestDeck } from '../manifest';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Every page a reader can print or download, by docs slug: each chapter
|
|
@@ -28,7 +28,51 @@ export function standaloneFilename(slug: string): string {
|
|
|
28
28
|
* is in its part's file and the course's (`combined-html.ts`).
|
|
29
29
|
*/
|
|
30
30
|
export function standalonePageSlugs(manifest: Pick<Manifest, 'lessons'>): string[] {
|
|
31
|
-
return manifest.lessons.map((lesson) => lesson.slug);
|
|
31
|
+
return manifest.lessons.filter((lesson) => (lesson.editions ?? ['chapter']).includes('chapter')).map((lesson) => lesson.slug);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Every page with an HTML download of its own: its route (`slug`) and where
|
|
36
|
+
* it downloads below `downloads/`, without `.html` (`download`). A chapter
|
|
37
|
+
* downloads at its slug; a deck at its owner's {@link deckFileStem}, its
|
|
38
|
+
* Present mode and notes working offline.
|
|
39
|
+
*/
|
|
40
|
+
export function standaloneDownloads(
|
|
41
|
+
manifest: Pick<Manifest, 'lessons'> & Partial<Pick<Manifest, 'decks'>>,
|
|
42
|
+
): { readonly slug: string; readonly download: string }[] {
|
|
43
|
+
return [
|
|
44
|
+
...standalonePageSlugs(manifest).map((slug) => ({ slug, download: slug })),
|
|
45
|
+
...(manifest.decks ?? []).map((deck) => ({
|
|
46
|
+
slug: deck.slug,
|
|
47
|
+
download: deckFileStem(deckOwnerSlug(deck, manifest)),
|
|
48
|
+
})),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Where a deck downloads below `downloads/`, by its owner's slug — a
|
|
54
|
+
* lesson's (`projectiles/projectile-range`), or a part's directory
|
|
55
|
+
* (`motion`): `projectiles/projectile-range-slides` + `.pdf` (the slides),
|
|
56
|
+
* `-notes.pdf` (each slide with its speaker notes) and `-latex.zip` (their
|
|
57
|
+
* source); a part's deck `motion-slides` + the same. A slides-first lesson's
|
|
58
|
+
* deck is named the same way as a chapter's.
|
|
59
|
+
*/
|
|
60
|
+
export function deckFileStem(ownerSlug: string): string {
|
|
61
|
+
return `${ownerSlug}-slides`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whose a deck is, as its downloads name it: its lesson's slug, or its part's directory. */
|
|
65
|
+
export function deckOwnerSlug(
|
|
66
|
+
deck: Pick<ManifestDeck, 'lessonId' | 'part' | 'slug'>,
|
|
67
|
+
manifest: Pick<Manifest, 'lessons'>,
|
|
68
|
+
): string {
|
|
69
|
+
if (deck.part !== undefined) return deck.part;
|
|
70
|
+
return manifest.lessons.find((lesson) => lesson.id === deck.lessonId)?.slug ?? deck.slug;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The name a deck's files are saved under: `projectile-range-slides`, a part's `motion-slides`. */
|
|
74
|
+
export function deckSaveName(ownerSlug: string): string {
|
|
75
|
+
return `${ownerSlug.split('/').at(-1) ?? ownerSlug}-slides`;
|
|
32
76
|
}
|
|
33
77
|
|
|
34
78
|
/** The name the course's files are saved under: `Explico Playground — Motion` → `explico-playground-motion`. */
|
|
@@ -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,183 @@
|
|
|
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
|
+
|
|
5
|
+
import type { ExportLogger } from '../export-chapters';
|
|
6
|
+
import { compileLatex, texFingerprint, type CompiledLatex, type LatexEngine } from './compile';
|
|
7
|
+
import type { SourceFile } from './document';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A cache of compiled documents (`EXPLICO_LATEX_CACHE=<directory>`), so a
|
|
11
|
+
* build compiles only the documents whose source changed. A document's key
|
|
12
|
+
* is a SHA-256 over everything its compile reads: the cache's format, the
|
|
13
|
+
* engine and a fingerprint of the TeX installation, the epoch the PDF is
|
|
14
|
+
* dated with, `EXPLICO_LATEX_CACHE_SALT`, and every file of its folder
|
|
15
|
+
* (path, length and bytes). A shared notation card, a renumbered chapter, a
|
|
16
|
+
* new `explico.cls` or a figure all change some file, so the key needs no
|
|
17
|
+
* dependency graph and cannot miss one.
|
|
18
|
+
*
|
|
19
|
+
* An entry keeps what `compileLatex` returns, the PDF, the `.aux` and the
|
|
20
|
+
* log, so a reused document is checked like a compiled one: its numbers
|
|
21
|
+
* against the website's, its log for characters no font set. An entry is
|
|
22
|
+
* written under a temporary name and renamed, a failed compile stores
|
|
23
|
+
* nothing, and an entry that does not read back whole is a miss. A hit
|
|
24
|
+
* touches its entry; entries no build has used for `KEEP_DAYS` are deleted.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const FORMAT = 'explico-latex-cache 1';
|
|
28
|
+
const KEEP_DAYS = 7;
|
|
29
|
+
const ENTRY = /^[0-9a-f]{64}$/u;
|
|
30
|
+
|
|
31
|
+
export interface CacheContext {
|
|
32
|
+
readonly engine: LatexEngine['name'];
|
|
33
|
+
/** `texFingerprint`'s description of the TeX installation. */
|
|
34
|
+
readonly fingerprint: string;
|
|
35
|
+
readonly epoch: number;
|
|
36
|
+
readonly salt: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The key of one compile: a hex SHA-256 over its context and its files in path order. */
|
|
40
|
+
export function cacheKey(files: readonly SourceFile[], context: CacheContext): string {
|
|
41
|
+
const hash = createHash('sha256');
|
|
42
|
+
for (const line of [FORMAT, context.engine, context.fingerprint, String(context.epoch), context.salt]) {
|
|
43
|
+
hash.update(`${line.length}:${line}\n`);
|
|
44
|
+
}
|
|
45
|
+
for (const file of [...files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))) {
|
|
46
|
+
const content = typeof file.content === 'string' ? Buffer.from(file.content, 'utf8') : file.content;
|
|
47
|
+
hash.update(`${Buffer.byteLength(file.path)}:${file.path}\n${content.byteLength}\n`);
|
|
48
|
+
hash.update(content);
|
|
49
|
+
}
|
|
50
|
+
return hash.digest('hex');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** One file per entry: a format line, the parts' lengths and digest as JSON, then the parts. */
|
|
54
|
+
function encodeEntry(result: CompiledLatex): Buffer {
|
|
55
|
+
const parts = [result.pdf, Buffer.from(result.aux, 'utf8'), Buffer.from(result.log, 'utf8')];
|
|
56
|
+
const payload = Buffer.concat(parts);
|
|
57
|
+
const header = {
|
|
58
|
+
pdf: parts[0]!.byteLength,
|
|
59
|
+
aux: parts[1]!.byteLength,
|
|
60
|
+
log: parts[2]!.byteLength,
|
|
61
|
+
sha256: createHash('sha256').update(payload).digest('hex'),
|
|
62
|
+
};
|
|
63
|
+
return Buffer.concat([Buffer.from(`${FORMAT}\n${JSON.stringify(header)}\n`, 'utf8'), payload]);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The compile an entry holds, or `undefined` when it is not whole. */
|
|
67
|
+
function decodeEntry(bytes: Buffer): CompiledLatex | undefined {
|
|
68
|
+
const formatEnd = bytes.indexOf(0x0a);
|
|
69
|
+
const headerEnd = formatEnd < 0 ? -1 : bytes.indexOf(0x0a, formatEnd + 1);
|
|
70
|
+
if (headerEnd < 0 || bytes.subarray(0, formatEnd).toString('utf8') !== FORMAT) return undefined;
|
|
71
|
+
let header: { pdf?: unknown; aux?: unknown; log?: unknown; sha256?: unknown };
|
|
72
|
+
try {
|
|
73
|
+
header = JSON.parse(bytes.subarray(formatEnd + 1, headerEnd).toString('utf8'));
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const { pdf, aux, log, sha256 } = header;
|
|
78
|
+
if (typeof pdf !== 'number' || typeof aux !== 'number' || typeof log !== 'number') return undefined;
|
|
79
|
+
const payload = bytes.subarray(headerEnd + 1);
|
|
80
|
+
if (payload.byteLength !== pdf + aux + log) return undefined;
|
|
81
|
+
if (createHash('sha256').update(payload).digest('hex') !== sha256) return undefined;
|
|
82
|
+
return {
|
|
83
|
+
pdf: Buffer.from(payload.subarray(0, pdf)),
|
|
84
|
+
aux: payload.subarray(pdf, pdf + aux).toString('utf8'),
|
|
85
|
+
log: payload.subarray(pdf + aux).toString('utf8'),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface LatexCache {
|
|
90
|
+
readonly directory: string;
|
|
91
|
+
/** The document compiled, or reused from an entry with the same key. */
|
|
92
|
+
compile(
|
|
93
|
+
files: readonly SourceFile[],
|
|
94
|
+
options: { readonly epoch: number; readonly name?: string },
|
|
95
|
+
): Promise<{ readonly result: CompiledLatex; readonly reused: boolean }>;
|
|
96
|
+
/** Delete the entries no build has used for `KEEP_DAYS`; how many went. */
|
|
97
|
+
prune(now?: Date): Promise<number>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function openLatexCache(options: {
|
|
101
|
+
readonly directory: string;
|
|
102
|
+
readonly engine: LatexEngine;
|
|
103
|
+
readonly fingerprint: string;
|
|
104
|
+
readonly salt?: string;
|
|
105
|
+
readonly logger?: ExportLogger;
|
|
106
|
+
/** The compile behind a miss; `compileLatex` but in tests. */
|
|
107
|
+
readonly compile?: typeof compileLatex;
|
|
108
|
+
}): LatexCache {
|
|
109
|
+
const { directory, engine, fingerprint } = options;
|
|
110
|
+
const compile = options.compile ?? compileLatex;
|
|
111
|
+
let warned = false;
|
|
112
|
+
const warnOnce = (message: string) => {
|
|
113
|
+
if (warned) return;
|
|
114
|
+
warned = true;
|
|
115
|
+
options.logger?.warn(message);
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
directory,
|
|
119
|
+
async compile(files, compileOptions) {
|
|
120
|
+
const key = cacheKey(files, { engine: engine.name, fingerprint, epoch: compileOptions.epoch, salt: options.salt ?? '' });
|
|
121
|
+
const path = join(directory, key);
|
|
122
|
+
const cached = await readFile(path).then(decodeEntry, () => undefined);
|
|
123
|
+
if (cached) {
|
|
124
|
+
const now = new Date();
|
|
125
|
+
await utimes(path, now, now).catch(() => undefined);
|
|
126
|
+
return { result: cached, reused: true };
|
|
127
|
+
}
|
|
128
|
+
const result = await compile(files, engine, compileOptions);
|
|
129
|
+
const temporary = `${path}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`;
|
|
130
|
+
try {
|
|
131
|
+
await writeFile(temporary, encodeEntry(result));
|
|
132
|
+
await rename(temporary, path);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
await rm(temporary, { force: true });
|
|
135
|
+
warnOnce(
|
|
136
|
+
`LaTeX export: could not store a compile in ${directory} (${error instanceof Error ? error.message : String(error)}); ` +
|
|
137
|
+
'the build goes on without it.',
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
return { result, reused: false };
|
|
141
|
+
},
|
|
142
|
+
async prune(now = new Date()) {
|
|
143
|
+
const cutoff = now.getTime() - KEEP_DAYS * 24 * 60 * 60 * 1000;
|
|
144
|
+
let removed = 0;
|
|
145
|
+
for (const name of await readdir(directory).catch(() => [] as string[])) {
|
|
146
|
+
// An entry, or what an interrupted write left behind.
|
|
147
|
+
if (!ENTRY.test(name) && !/^[0-9a-f]{64}\.tmp-/u.test(name)) continue;
|
|
148
|
+
const path = join(directory, name);
|
|
149
|
+
const modified = await stat(path).then((entry) => entry.mtimeMs, () => undefined);
|
|
150
|
+
if (modified === undefined || modified >= cutoff) continue;
|
|
151
|
+
await rm(path, { force: true });
|
|
152
|
+
removed += 1;
|
|
153
|
+
}
|
|
154
|
+
return removed;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The cache `EXPLICO_LATEX_CACHE` names, or `undefined` when it is unset
|
|
161
|
+
* (today's build: every document compiles) or the directory cannot be made.
|
|
162
|
+
*/
|
|
163
|
+
export async function latexCacheFromEnvironment(engine: LatexEngine, logger: ExportLogger): Promise<LatexCache | undefined> {
|
|
164
|
+
const setting = process.env.EXPLICO_LATEX_CACHE?.trim();
|
|
165
|
+
if (!setting) return undefined;
|
|
166
|
+
const directory = resolve(setting);
|
|
167
|
+
try {
|
|
168
|
+
await mkdir(directory, { recursive: true });
|
|
169
|
+
} catch (error) {
|
|
170
|
+
logger.warn(
|
|
171
|
+
`LaTeX export: EXPLICO_LATEX_CACHE names ${directory}, which cannot be made ` +
|
|
172
|
+
`(${error instanceof Error ? error.message : String(error)}); every document compiles.`,
|
|
173
|
+
);
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
return openLatexCache({
|
|
177
|
+
directory,
|
|
178
|
+
engine,
|
|
179
|
+
fingerprint: await texFingerprint(engine),
|
|
180
|
+
salt: process.env.EXPLICO_LATEX_CACHE_SALT ?? '',
|
|
181
|
+
logger,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -32,6 +32,8 @@ export interface CaptureRequest {
|
|
|
32
32
|
readonly route: string;
|
|
33
33
|
/** Anchor ids of the figures and diagrams to capture (`fig-…`, `dia-…`). */
|
|
34
34
|
readonly anchors: readonly string[];
|
|
35
|
+
/** This page's date for its figure PDFs, over the capture's `epoch`. */
|
|
36
|
+
readonly epoch?: number;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export interface CapturedFigure {
|
|
@@ -140,7 +142,7 @@ async function existingFile(path: string): Promise<string | undefined> {
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
/** Serve `root` on loopback at `base`; resolves with the origin once listening. */
|
|
143
|
-
async function serve(root: string, base: string): Promise<{ origin: string; server: Server }> {
|
|
145
|
+
export async function serve(root: string, base: string): Promise<{ origin: string; server: Server }> {
|
|
144
146
|
const rootPath = resolve(root);
|
|
145
147
|
const server = createServer((request, response) => {
|
|
146
148
|
void (async () => {
|
|
@@ -629,7 +631,8 @@ export async function captureFigures(
|
|
|
629
631
|
options.measure ?? figureMeasure('letter'),
|
|
630
632
|
options.png ?? false,
|
|
631
633
|
);
|
|
632
|
-
const
|
|
634
|
+
const epoch = request.epoch ?? options.epoch;
|
|
635
|
+
const pdf = epoch === undefined ? printed.pdf : stampPdfDates(printed.pdf, epoch);
|
|
633
636
|
figures.set(anchor, { ...printed, pdf, liveHtml: snapshot.live });
|
|
634
637
|
}
|
|
635
638
|
results.set(request.route, figures);
|
|
@@ -79,6 +79,24 @@ export async function findLatexEngine(): Promise<LatexEngine | { readonly missin
|
|
|
79
79
|
: { name: 'pdflatex', latexmkOption: '-pdf' };
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* What the compile cache knows of the TeX installation: the engine's and
|
|
84
|
+
* latexmk's banners and the LaTeX kernel's date and patch level. A TeX Live
|
|
85
|
+
* release or a kernel update changes it; a package updated with `tlmgr` does
|
|
86
|
+
* not, which is what `EXPLICO_LATEX_CACHE_SALT` is for.
|
|
87
|
+
*/
|
|
88
|
+
export async function texFingerprint(engine: LatexEngine): Promise<string> {
|
|
89
|
+
const firstLine = (output: string, pattern: RegExp) =>
|
|
90
|
+
output.split('\n').find((line) => pattern.test(line))?.trim() ?? '';
|
|
91
|
+
const banner = firstLine((await run(engine.name, ['--version'])).output, /\S/u);
|
|
92
|
+
const latexmk = firstLine((await run('latexmk', ['-v'])).output, /Version/u);
|
|
93
|
+
const kernelFile = (await run('kpsewhich', ['latex.ltx'])).output.trim();
|
|
94
|
+
const kernel = kernelFile ? await readFile(kernelFile, 'utf8').catch(() => '') : '';
|
|
95
|
+
const date = /\\edef\\fmtversion\s*\{([^}]*)\}/u.exec(kernel)?.[1] ?? '';
|
|
96
|
+
const patch = /\\def\\patch@level\{([^}]*)\}/u.exec(kernel)?.[1] ?? '';
|
|
97
|
+
return [banner, latexmk, `LaTeX2e ${date} patch level ${patch}`].join('\n');
|
|
98
|
+
}
|
|
99
|
+
|
|
82
100
|
/**
|
|
83
101
|
* The last LaTeX errors of a log, with their file and line. A Lua error
|
|
84
102
|
* (`[\directlua]:1: …`, `….lua:12: …`) carries no TeX file and line.
|