explico 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +879 -1
- package/README.md +200 -30
- package/dist/compiler/astro.d.ts +15 -2
- package/dist/compiler/astro.js +40 -11
- package/dist/compiler/cli-main.d.ts +43 -0
- package/dist/compiler/cli-main.js +392 -0
- package/dist/compiler/cli.d.ts +424 -9
- package/dist/compiler/cli.js +1289 -8
- package/dist/compiler/collections.d.ts +88 -1
- package/dist/compiler/collections.js +170 -25
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +105 -1
- package/dist/compiler/decks.d.ts +161 -0
- package/dist/compiler/decks.js +235 -0
- package/dist/compiler/exports/combined-html.d.ts +5 -1
- package/dist/compiler/exports/deck-fit.d.ts +55 -0
- package/dist/compiler/exports/deck-fit.js +90 -0
- package/dist/compiler/exports/export-chapters.d.ts +37 -6
- package/dist/compiler/exports/export-chapters.js +64 -5
- package/dist/compiler/exports/export-pages.d.ts +30 -3
- package/dist/compiler/exports/export-pages.js +24 -1
- package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/dist/compiler/exports/latex/cache.d.ts +78 -0
- package/dist/compiler/exports/latex/cache.js +129 -0
- package/dist/compiler/exports/latex/capture.d.ts +10 -1
- package/dist/compiler/exports/latex/capture.js +3 -1
- package/dist/compiler/exports/latex/compile.d.ts +13 -2
- package/dist/compiler/exports/latex/compile.js +12 -1
- package/dist/compiler/exports/latex/deck.d.ts +92 -0
- package/dist/compiler/exports/latex/deck.js +349 -0
- package/dist/compiler/exports/latex/document.d.ts +51 -6
- package/dist/compiler/exports/latex/document.js +71 -12
- package/dist/compiler/exports/latex/explico-common.sty +198 -0
- package/dist/compiler/exports/latex/explico.cls +13 -183
- package/dist/compiler/exports/latex/serialize.d.ts +60 -4
- package/dist/compiler/exports/latex/serialize.js +4 -2
- package/dist/compiler/exports/latex-export.d.ts +24 -2
- package/dist/compiler/exports/latex-export.js +107 -26
- package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
- package/dist/compiler/exports/markdown/serialize.js +85 -8
- package/dist/compiler/exports/markdown-export.d.ts +6 -1
- package/dist/compiler/exports/markdown-export.js +50 -11
- package/dist/compiler/exports/standalone-html.d.ts +10 -5
- package/dist/compiler/exports/standalone-html.js +3 -3
- package/dist/compiler/exports/static-tree.d.ts +38 -1
- package/dist/compiler/exports/static-tree.js +120 -4
- package/dist/compiler/knowledge-version.d.ts +26 -2
- package/dist/compiler/knowledge-version.js +27 -17
- package/dist/compiler/lesson-derivation.d.ts +143 -6
- package/dist/compiler/lesson-derivation.js +145 -13
- package/dist/compiler/manifest.d.ts +133 -4
- package/dist/compiler/manifest.js +359 -58
- package/dist/compiler/page-versions.d.ts +160 -0
- package/dist/compiler/page-versions.js +198 -0
- package/dist/compiler/render-diagnostics.d.ts +5 -1
- package/dist/compiler/resolution-report.d.ts +24 -6
- package/dist/compiler/resolution-report.js +22 -4
- package/dist/compiler/validate-content.d.ts +7 -0
- package/dist/compiler/validate-content.js +160 -0
- package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
- package/dist/curriculum/competency-graph.d.ts +54 -0
- package/dist/curriculum/competency-graph.js +41 -0
- package/dist/reference/base-library.d.mts +8 -0
- package/dist/reference/base-library.mjs +19 -0
- package/dist/reference/consistency.d.ts +8 -4
- package/dist/reference/consistency.js +76 -26
- package/dist/reference/deck-remark.d.mts +123 -0
- package/dist/reference/deck-remark.mjs +600 -0
- package/dist/reference/deck-source.d.ts +107 -0
- package/dist/reference/deck-source.js +140 -0
- package/dist/reference/decks-validate.d.ts +133 -0
- package/dist/reference/decks-validate.js +359 -0
- package/dist/reference/derived-entries.d.ts +5 -1
- package/dist/reference/diagram-fit.d.ts +1 -1
- package/dist/reference/figure-symbols.d.ts +23 -6
- package/dist/reference/figure-symbols.js +51 -15
- package/dist/reference/float-name.d.ts +43 -0
- package/dist/reference/float-name.js +58 -0
- package/dist/reference/folds-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-indices.js +5 -4
- package/dist/reference/gate-math.d.ts +29 -2
- package/dist/reference/gate-math.js +99 -18
- package/dist/reference/gate-part-pages.d.ts +1 -1
- package/dist/reference/lab-katex-validate.d.ts +10 -6
- package/dist/reference/lab-katex-validate.js +17 -9
- package/dist/reference/lab-math-scope.js +29 -4
- package/dist/reference/math-glyphs.d.mts +25 -0
- package/dist/reference/math-glyphs.mjs +189 -25
- package/dist/reference/page-notation.d.ts +22 -3
- package/dist/reference/page-notation.js +32 -10
- package/dist/reference/references.d.ts +14 -1
- package/dist/reference/references.js +13 -0
- package/dist/reference/registry-scope.d.mts +33 -0
- package/dist/reference/registry-scope.mjs +34 -0
- package/dist/reference/registry.js +29 -10
- package/dist/reference/rehype-deck.d.mts +6 -0
- package/dist/reference/rehype-deck.mjs +286 -0
- package/dist/reference/rehype-heading-anchors.d.mts +20 -0
- package/dist/reference/rehype-heading-anchors.mjs +48 -6
- package/dist/reference/remark-notation.mjs +249 -40
- package/dist/reference/source-lines.d.mts +12 -0
- package/dist/reference/source-lines.mjs +65 -0
- package/dist/reference/types.d.ts +28 -0
- package/dist/rules/catalog.d.ts +21 -4
- package/dist/rules/catalog.js +221 -2
- package/dist/rules/course-rules.d.ts +5 -1
- package/dist/rules/course-rules.js +17 -3
- package/dist/rules/course-rules.schema.json +12 -0
- package/dist/rules/in-effect.d.ts +13 -3
- package/dist/rules/in-effect.js +30 -7
- package/dist/rules/rules.schema.json +259 -1
- package/dist/rules/settings.d.ts +1 -1
- package/dist/session/PreferenceStore.d.ts +6 -0
- package/dist/session/PreferenceStore.js +18 -1
- package/dist/session/deck-memory.d.ts +116 -0
- package/dist/session/deck-memory.js +143 -0
- package/dist/session/reading-memory.d.ts +96 -0
- package/dist/session/reading-memory.js +133 -0
- package/docs/VERSIONING.md +21 -18
- package/docs/authoring/README.md +29 -9
- package/docs/authoring/agents.md +63 -16
- package/docs/authoring/components.md +15 -0
- package/docs/authoring/deck.md +588 -0
- package/docs/authoring/migrating.md +1045 -16
- package/docs/authoring/notation.md +49 -6
- package/docs/authoring/structure.md +80 -1
- package/docs/authoring/versioning.md +54 -2
- package/docs/authoring/visualizations.md +7 -2
- package/docs/course-agent/AGENTS.md +37 -0
- package/docs/course-agent/agents/rules-reviewer.md +33 -0
- package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
- package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
- package/docs/course-agent/skills/release-content/SKILL.md +24 -0
- package/docs/diagram-scaling.md +148 -0
- package/docs/privacy.md +59 -0
- package/package.json +8 -2
- package/src/compiler/astro.ts +60 -21
- package/src/compiler/cli-main.ts +469 -0
- package/src/compiler/cli.ts +1905 -16
- package/src/compiler/collections.ts +284 -25
- package/src/compiler/course-config.ts +267 -0
- package/src/compiler/decks.ts +455 -0
- package/src/compiler/exports/deck-fit.ts +127 -0
- package/src/compiler/exports/export-chapters.ts +105 -6
- package/src/compiler/exports/export-pages.ts +46 -2
- package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/src/compiler/exports/latex/cache.ts +183 -0
- package/src/compiler/exports/latex/capture.ts +5 -2
- package/src/compiler/exports/latex/compile.ts +18 -0
- package/src/compiler/exports/latex/deck.ts +419 -0
- package/src/compiler/exports/latex/document.ts +117 -21
- package/src/compiler/exports/latex/explico-common.sty +198 -0
- package/src/compiler/exports/latex/explico.cls +13 -183
- package/src/compiler/exports/latex/serialize.ts +25 -21
- package/src/compiler/exports/latex/texlive-packages.txt +6 -2
- package/src/compiler/exports/latex-export.ts +187 -45
- package/src/compiler/exports/markdown/serialize.ts +118 -9
- package/src/compiler/exports/markdown-export.ts +56 -11
- package/src/compiler/exports/standalone-html.ts +8 -7
- package/src/compiler/exports/static-tree.ts +175 -3
- package/src/compiler/knowledge-version.ts +56 -21
- package/src/compiler/lesson-derivation.ts +282 -16
- package/src/compiler/manifest.ts +558 -63
- package/src/compiler/page-versions.ts +359 -0
- package/src/compiler/resolution-report.ts +38 -7
- package/src/compiler/validate-content.ts +212 -0
- package/src/components/CourseContents.astro +124 -6
- package/src/components/CurriculumMap.astro +49 -1
- package/src/components/FloatIndex.astro +1 -1
- package/src/components/PartOverview.astro +51 -5
- package/src/components/assessments/AssessmentSet.astro +41 -15
- package/src/components/block-heading.ts +15 -6
- package/src/components/card-carry.ts +7 -4
- package/src/components/card-manager.ts +14 -2
- package/src/components/card-memory.ts +13 -9
- package/src/components/deck/DeckEnhancer.astro +13 -0
- package/src/components/deck/deck-context.ts +104 -0
- package/src/components/deck/deck-controller.ts +1287 -0
- package/src/components/deck/deck-map.ts +91 -0
- package/src/components/deck-lane.ts +163 -0
- package/src/components/diagram/Diagram.astro +131 -86
- package/src/components/disclosure/persisted-disclosure.ts +9 -1
- package/src/components/figure/Figure.astro +7 -2
- package/src/components/figure/FloatCaption.astro +5 -3
- package/src/components/fold/Fold.astro +2 -2
- package/src/components/notation/NotationFormula.astro +81 -11
- package/src/components/notation/NotationGlossary.astro +132 -1
- package/src/components/notation/NotationLayer.astro +15 -7
- package/src/components/starlight/AboutPanel.astro +186 -3
- package/src/components/starlight/CollapsibleSections.astro +24 -3
- package/src/components/starlight/DiagramEnhancer.astro +5 -1
- package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
- package/src/components/starlight/LayoutHeader.astro +17 -0
- package/src/components/starlight/LayoutPageTitle.astro +110 -0
- package/src/components/starlight/LayoutSidebar.astro +807 -21
- package/src/components/starlight/LayoutSocialIcons.astro +62 -6
- package/src/components/starlight/LessonExportActions.astro +27 -9
- package/src/components/starlight/LessonFooter.astro +59 -0
- package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
- package/src/components/starlight/TextSizeControl.astro +24 -13
- package/src/components/starlight/page-marks.ts +139 -0
- package/src/components/starlight/section-summary.ts +1 -1
- package/src/components/starlight/sidebar-editions.ts +245 -0
- package/src/components/table/Table.astro +15 -7
- package/src/content-config.ts +87 -1
- package/src/curriculum/competency-graph.ts +95 -0
- package/src/reference/base-library.mjs +19 -0
- package/src/reference/consistency.ts +126 -39
- package/src/reference/deck-remark.mjs +600 -0
- package/src/reference/deck-source.ts +273 -0
- package/src/reference/decks-validate.ts +604 -0
- package/src/reference/diagram-layout.ts +3 -1
- package/src/reference/figure-symbols.ts +108 -31
- package/src/reference/folds-validate.ts +1 -1
- package/src/reference/gate-indices.ts +5 -3
- package/src/reference/gate-math.ts +169 -25
- package/src/reference/lab-katex-validate.ts +30 -15
- package/src/reference/lab-math-scope.ts +34 -3
- package/src/reference/math-glyphs.mjs +189 -25
- package/src/reference/page-notation.ts +69 -15
- package/src/reference/references.ts +28 -0
- package/src/reference/registry-scope.mjs +34 -0
- package/src/reference/registry.ts +41 -11
- package/src/reference/rehype-deck.mjs +286 -0
- package/src/reference/rehype-heading-anchors.mjs +48 -6
- package/src/reference/remark-notation.mjs +249 -40
- package/src/reference/source-lines.mjs +65 -0
- package/src/reference/types.ts +28 -0
- package/src/rules/catalog.ts +257 -4
- package/src/rules/course-rules.ts +21 -1
- package/src/rules/in-effect.ts +54 -8
- package/src/session/PreferenceStore.ts +23 -0
- package/src/session/deck-memory.ts +263 -0
- package/src/session/reading-memory.ts +243 -0
- package/src/styles/deck.css +1150 -0
- package/src/styles/global.css +958 -27
- package/src/styles/zones.css +277 -0
- /package/src/{components → reference}/float-name.ts +0 -0
|
@@ -4,11 +4,12 @@ import { dirname, extname, join } from 'node:path';
|
|
|
4
4
|
import { performance } from 'node:perf_hooks';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
|
-
import type { Manifest } from '../manifest';
|
|
8
|
-
import type {
|
|
7
|
+
import type { Manifest, ManifestLesson } from '../manifest';
|
|
8
|
+
import type { Block } from './static-tree';
|
|
9
9
|
import { zip } from './zip';
|
|
10
10
|
import {
|
|
11
11
|
consoleLogger,
|
|
12
|
+
deckBlocks,
|
|
12
13
|
epochOf,
|
|
13
14
|
figuresOf,
|
|
14
15
|
imageFile,
|
|
@@ -19,9 +20,21 @@ import {
|
|
|
19
20
|
type ExportLogger,
|
|
20
21
|
type PreparedChapters,
|
|
21
22
|
} from './export-chapters';
|
|
23
|
+
import type { CaptureResults } from './latex/capture';
|
|
24
|
+
import { latexCacheFromEnvironment } from './latex/cache';
|
|
22
25
|
import { compileLatex, missingCharacters, findLatexEngine } from './latex/compile';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
26
|
+
import { deckNumberingProblems, deckOverflows, deckSource } from './latex/deck';
|
|
27
|
+
import {
|
|
28
|
+
bookSince,
|
|
29
|
+
bookSource,
|
|
30
|
+
chapterFolderName,
|
|
31
|
+
chapterSource,
|
|
32
|
+
deckCheckItems,
|
|
33
|
+
type BookPart,
|
|
34
|
+
type BookSlidesLesson,
|
|
35
|
+
type SourceFile,
|
|
36
|
+
} from './latex/document';
|
|
37
|
+
import { courseFileStem, deckFileStem, deckOwnerSlug, deckSaveName } from './export-pages';
|
|
25
38
|
import { auxLabels, numberingMismatches, websiteNumbers } from './latex/numbering';
|
|
26
39
|
|
|
27
40
|
export type { ExportLogger } from './export-chapters';
|
|
@@ -32,61 +45,107 @@ export type { ExportLogger } from './export-chapters';
|
|
|
32
45
|
* figures — is zipped to `downloads/<slug>-latex.zip`, and the same folder is
|
|
33
46
|
* compiled to `downloads/<slug>.pdf`. Each part and the course are books
|
|
34
47
|
* (`bookSource`): `downloads/part/<dir>-latex.zip` and `.pdf`,
|
|
35
|
-
* `downloads/course-latex.zip` and `.pdf`.
|
|
36
|
-
*
|
|
48
|
+
* `downloads/course-latex.zip` and `.pdf`. Each deck is beamer slides
|
|
49
|
+
* (`latex/deck.ts`): `downloads/<lesson>-slides.pdf`, its handout
|
|
50
|
+
* `-slides-notes.pdf` and its source `-slides-latex.zip`; a book keeps a
|
|
51
|
+
* slides-first lesson's place with a short chapter. Every document compiles at once,
|
|
52
|
+
* up to one per core, the longest first. With `EXPLICO_LATEX_CACHE` set, a
|
|
53
|
+
* document whose folder a build already compiled is reused (`latex/cache.ts`).
|
|
37
54
|
*
|
|
38
|
-
* The build fails when a compiled number differs from the website's
|
|
39
|
-
* LaTeX reports an error, and — in CI (`CI` set) or with
|
|
55
|
+
* The build fails when a compiled number differs from the website's (a
|
|
56
|
+
* deck's frame numbers included), when LaTeX reports an error, and — in CI (`CI` set) or with
|
|
40
57
|
* `EXPLICO_LATEX_STRICT=1` — when the TeX engine or the browser for figures
|
|
41
58
|
* is missing. On a developer's machine without them it warns and still
|
|
42
59
|
* writes the source zips.
|
|
43
60
|
*/
|
|
44
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The pictures of a page's figures, by anchor: what the capture pass drew,
|
|
64
|
+
* or the image the page shows. `drawnOn` says which page drew a figure (a
|
|
65
|
+
* deck's recalled figure is its chapter's); `folder` is where the pictures
|
|
66
|
+
* go in the document's folder.
|
|
67
|
+
*/
|
|
45
68
|
async function pagePictures(
|
|
46
69
|
outputRoot: string,
|
|
47
70
|
base: string,
|
|
48
|
-
page:
|
|
49
|
-
|
|
71
|
+
page: { readonly route: string; readonly blocks: readonly Block[] },
|
|
72
|
+
captures: CaptureResults,
|
|
73
|
+
drawnOn: (anchor: string) => string = () => page.route,
|
|
74
|
+
folder = `figures/${page.route}`,
|
|
50
75
|
): Promise<Map<string, { path: string; content: Uint8Array }>> {
|
|
51
76
|
const pictures = new Map<string, { path: string; content: Uint8Array }>();
|
|
52
77
|
for (const figure of figuresOf(page.blocks)) {
|
|
53
|
-
const capturedFigure =
|
|
78
|
+
const capturedFigure = captures.get(drawnOn(figure.anchor))?.get(figure.anchor);
|
|
54
79
|
if (capturedFigure) {
|
|
55
|
-
pictures.set(figure.anchor, {
|
|
56
|
-
path: `figures/${page.route}/${figure.anchor}.pdf`,
|
|
57
|
-
content: capturedFigure.pdf,
|
|
58
|
-
});
|
|
80
|
+
pictures.set(figure.anchor, { path: `${folder}/${figure.anchor}.pdf`, content: capturedFigure.pdf });
|
|
59
81
|
continue;
|
|
60
82
|
}
|
|
61
83
|
if (figure.picture.kind !== 'image' || needsCapture(figure)) continue;
|
|
62
84
|
const file = imageFile(outputRoot, base, page.route, figure.picture.src);
|
|
63
85
|
pictures.set(figure.anchor, {
|
|
64
|
-
path:
|
|
86
|
+
path: `${folder}/${figure.anchor}${extname(file).toLowerCase()}`,
|
|
65
87
|
content: await readFile(file),
|
|
66
88
|
});
|
|
67
89
|
}
|
|
68
90
|
return pictures;
|
|
69
91
|
}
|
|
70
92
|
|
|
93
|
+
/** What a slides-first lesson's place in a book says: its outcomes and where its slides are. */
|
|
94
|
+
function slidesLesson(manifest: Manifest, lesson: ManifestLesson): BookSlidesLesson {
|
|
95
|
+
const byId = new Map(manifest.competencies.map((competency) => [competency.id, competency]));
|
|
96
|
+
const outcomes = lesson.teaches.flatMap((id) => {
|
|
97
|
+
const outcome = byId.get(id)?.outcome;
|
|
98
|
+
return typeof outcome === 'string' && outcome.trim() !== '' ? [outcome.trim()] : [];
|
|
99
|
+
});
|
|
100
|
+
const base = manifest.lessonExports.canonicalBaseUrl;
|
|
101
|
+
return { lesson, outcomes, ...(base ? { slidesUrl: `${base}${lesson.slug}/` } : {}) };
|
|
102
|
+
}
|
|
103
|
+
|
|
71
104
|
interface PendingFile {
|
|
72
105
|
readonly target: string;
|
|
73
106
|
readonly content: Uint8Array;
|
|
74
107
|
}
|
|
75
108
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
109
|
+
/**
|
|
110
|
+
* `work` over every item, at most `limit` at a time, started in `order` (the
|
|
111
|
+
* items' indices, by default their own order); results in the items' order.
|
|
112
|
+
*/
|
|
113
|
+
export async function mapWithLimit<T, R>(
|
|
114
|
+
items: readonly T[],
|
|
115
|
+
limit: number,
|
|
116
|
+
work: (item: T) => Promise<R>,
|
|
117
|
+
order: readonly number[] = items.map((_item, index) => index),
|
|
118
|
+
): Promise<R[]> {
|
|
78
119
|
const results: R[] = new Array(items.length);
|
|
79
120
|
let next = 0;
|
|
80
121
|
const worker = async () => {
|
|
81
|
-
while (next <
|
|
82
|
-
const index = next
|
|
122
|
+
while (next < order.length) {
|
|
123
|
+
const index = order[next++]!;
|
|
83
124
|
results[index] = await work(items[index]!);
|
|
84
125
|
}
|
|
85
126
|
};
|
|
86
|
-
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit,
|
|
127
|
+
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, order.length)) }, worker));
|
|
87
128
|
return results;
|
|
88
129
|
}
|
|
89
130
|
|
|
131
|
+
/**
|
|
132
|
+
* The order to start documents in: the course, then the parts, then the
|
|
133
|
+
* chapters, the largest folder first within each. The course takes longest
|
|
134
|
+
* and is the one document a cache never reuses after a change; started last,
|
|
135
|
+
* it ran alone at the end while the other cores had nothing left to do.
|
|
136
|
+
*/
|
|
137
|
+
export function startOrder(
|
|
138
|
+
jobs: readonly { readonly kind: 'chapter' | 'deck' | 'part' | 'course'; readonly files: readonly SourceFile[] }[],
|
|
139
|
+
): number[] {
|
|
140
|
+
const rank = { course: 0, part: 1, chapter: 2, deck: 2 } as const;
|
|
141
|
+
const size = jobs.map(({ files }) =>
|
|
142
|
+
files.reduce((sum, file) => sum + (typeof file.content === 'string' ? Buffer.byteLength(file.content) : file.content.byteLength), 0),
|
|
143
|
+
);
|
|
144
|
+
return jobs
|
|
145
|
+
.map((_job, index) => index)
|
|
146
|
+
.sort((a, b) => rank[jobs[a]!.kind] - rank[jobs[b]!.kind] || size[b]! - size[a]! || a - b);
|
|
147
|
+
}
|
|
148
|
+
|
|
90
149
|
/**
|
|
91
150
|
* Write each chapter's, each part's and the course's LaTeX source zip and PDF
|
|
92
151
|
* under `downloads/`, from the chapters `prepareChapters` read and drew (read
|
|
@@ -105,7 +164,7 @@ export async function writeLatexExports(
|
|
|
105
164
|
const logger = options.logger ?? consoleLogger;
|
|
106
165
|
const strict = strictMode();
|
|
107
166
|
const outputRoot = fileURLToPath(directory);
|
|
108
|
-
const { base, chapters, captures } =
|
|
167
|
+
const { base, chapters, decks, captures } =
|
|
109
168
|
options.prepared ?? (await prepareChapters(outputRoot, manifest, { logger, projectRoot: options.projectRoot }));
|
|
110
169
|
|
|
111
170
|
const engine = await findLatexEngine();
|
|
@@ -116,17 +175,25 @@ export async function writeLatexExports(
|
|
|
116
175
|
|
|
117
176
|
/** One LaTeX document: its folder, where its zip and PDF go, and the numbers it must print. */
|
|
118
177
|
interface Job {
|
|
119
|
-
readonly kind: 'chapter' | 'part' | 'course';
|
|
178
|
+
readonly kind: 'chapter' | 'deck' | 'part' | 'course';
|
|
120
179
|
readonly name: string;
|
|
121
180
|
readonly files: SourceFile[];
|
|
122
181
|
readonly target: string;
|
|
123
182
|
readonly folder: string;
|
|
124
|
-
|
|
183
|
+
/** No source zip of its own: a deck's handout is built from its slides' folder. */
|
|
184
|
+
readonly noZip?: boolean;
|
|
185
|
+
/** Where LaTeX's numbers differ from the website's, from the compiled `.aux`. */
|
|
186
|
+
readonly numbering: (aux: string) => string[];
|
|
187
|
+
/** What else its log says that a reader would see: a deck's frames that run over on paper. */
|
|
188
|
+
readonly review?: (log: string) => void;
|
|
189
|
+
/** The version the document's content is at (rule C5): its date, and its cache key through its files. */
|
|
190
|
+
readonly since: string | null;
|
|
125
191
|
}
|
|
126
192
|
const jobs: Job[] = [];
|
|
193
|
+
const againstWebsite = (website: Map<string, string>) => (aux: string) => numberingMismatches(website, auxLabels(aux));
|
|
127
194
|
const pictures = new Map<string, Awaited<ReturnType<typeof pagePictures>>>();
|
|
128
195
|
for (const { lesson, page } of chapters) {
|
|
129
|
-
const chapterPictures = await pagePictures(outputRoot, base, page, captures
|
|
196
|
+
const chapterPictures = await pagePictures(outputRoot, base, page, captures);
|
|
130
197
|
pictures.set(lesson.id, chapterPictures);
|
|
131
198
|
jobs.push({
|
|
132
199
|
kind: 'chapter',
|
|
@@ -134,8 +201,57 @@ export async function writeLatexExports(
|
|
|
134
201
|
files: chapterSource({ manifest, lesson, page, documentRoutes: new Set([page.route]), pictures: chapterPictures }),
|
|
135
202
|
target: lesson.slug,
|
|
136
203
|
folder: chapterFolderName(lesson),
|
|
137
|
-
|
|
204
|
+
numbering: againstWebsite(websiteNumbers(page, `${lesson.part}.${lesson.chapter}`)),
|
|
205
|
+
since: lesson.since,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Each deck as beamer slides, and as a handout of each slide with its notes:
|
|
210
|
+
// a lesson's, named after the lesson, and a part's, after the part.
|
|
211
|
+
const lessonsById = new Map(manifest.lessons.map((lesson) => [lesson.id, lesson]));
|
|
212
|
+
for (const { deck, page, figureRoutes } of decks) {
|
|
213
|
+
if (deck.part === undefined && !lessonsById.has(deck.lessonId ?? '')) continue;
|
|
214
|
+
const owner = deckOwnerSlug(deck, manifest);
|
|
215
|
+
const deckPictures = await pagePictures(
|
|
216
|
+
outputRoot,
|
|
217
|
+
base,
|
|
218
|
+
{ route: page.route, blocks: deckBlocks(page) },
|
|
219
|
+
captures,
|
|
220
|
+
(anchor) => figureRoutes.get(anchor) ?? page.route,
|
|
221
|
+
'figures',
|
|
222
|
+
);
|
|
223
|
+
const { files, frameEnds } = deckSource({ manifest, deck, page, checkItems: deckCheckItems(manifest, deck), pictures: deckPictures });
|
|
224
|
+
const stem = deckFileStem(owner);
|
|
225
|
+
const numbering = (aux: string) => deckNumberingProblems(aux, page.frames.length);
|
|
226
|
+
const common = { kind: 'deck', folder: `${deckSaveName(owner)}-latex`, numbering, since: deck.since } as const;
|
|
227
|
+
jobs.push({
|
|
228
|
+
...common,
|
|
229
|
+
name: stem,
|
|
230
|
+
files,
|
|
231
|
+
target: stem,
|
|
232
|
+
review: (log) => {
|
|
233
|
+
const over = deckOverflows(log, frameEnds);
|
|
234
|
+
if (over.length === 0) return;
|
|
235
|
+
logger.warn(
|
|
236
|
+
`LaTeX export: on paper, ${over.length === 1 ? 'a slide' : `${over.length} slides`} of ${stem} ` +
|
|
237
|
+
`run${over.length === 1 ? 's' : ''} over the bottom: ` +
|
|
238
|
+
`${over.map(({ frame, points }) => `slide ${frame} by ${points} pt`).join(', ')}. ` +
|
|
239
|
+
'The web page fits them (rule D2), but the PDF cuts them off. Shorten the slide, or split it.',
|
|
240
|
+
);
|
|
241
|
+
},
|
|
138
242
|
});
|
|
243
|
+
if (manifest.deck.handoutPdf && manifest.deck.notes !== 'off') {
|
|
244
|
+
const handout = files.find((file) => file.path === 'handout.tex')!;
|
|
245
|
+
jobs.push({
|
|
246
|
+
...common,
|
|
247
|
+
name: `${stem}-notes`,
|
|
248
|
+
files: files.flatMap((file) =>
|
|
249
|
+
file.path === 'handout.tex' ? [] : file.path === 'main.tex' ? [{ path: 'main.tex', content: handout.content }] : [file],
|
|
250
|
+
),
|
|
251
|
+
target: `${stem}-notes`,
|
|
252
|
+
noZip: true,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
139
255
|
}
|
|
140
256
|
|
|
141
257
|
// Each part, and the course, as a book (plan step 6).
|
|
@@ -147,11 +263,16 @@ export async function writeLatexExports(
|
|
|
147
263
|
const chapter = byId.get(id);
|
|
148
264
|
return chapter ? [{ ...chapter, pictures: pictures.get(id)! }] : [];
|
|
149
265
|
});
|
|
266
|
+
// A slides-first lesson keeps its place: the chapters after it keep their numbers.
|
|
267
|
+
const slides = part.lessons.flatMap((id) => {
|
|
268
|
+
const lesson = lessonsById.get(id);
|
|
269
|
+
return lesson && !lesson.editions.includes('chapter') ? [slidesLesson(manifest, lesson)] : [];
|
|
270
|
+
});
|
|
150
271
|
if (partChapters.length === 0) continue;
|
|
151
272
|
const overview = pageSlugs.has(`${part.dir}/index`)
|
|
152
273
|
? (await readBuiltPage(outputRoot, `${part.dir}/index`)).page
|
|
153
274
|
: undefined;
|
|
154
|
-
parts.push({ part, ...(overview ? { overview } : {}), chapters: partChapters });
|
|
275
|
+
parts.push({ part, ...(overview ? { overview } : {}), chapters: partChapters, ...(slides.length > 0 ? { slides } : {}) });
|
|
155
276
|
}
|
|
156
277
|
const bookNumbers = (bookParts: readonly BookPart[]) =>
|
|
157
278
|
new Map(
|
|
@@ -173,7 +294,8 @@ export async function writeLatexExports(
|
|
|
173
294
|
}),
|
|
174
295
|
target: `part/${entry.part.dir}`,
|
|
175
296
|
folder: `${entry.part.dir}-latex`,
|
|
176
|
-
|
|
297
|
+
numbering: againstWebsite(bookNumbers([entry])),
|
|
298
|
+
since: bookSince([entry]),
|
|
177
299
|
});
|
|
178
300
|
}
|
|
179
301
|
if (parts.length > 0) {
|
|
@@ -183,31 +305,44 @@ export async function writeLatexExports(
|
|
|
183
305
|
files: bookSource({ manifest, parts, document: 'course', title: course }),
|
|
184
306
|
target: 'course',
|
|
185
307
|
folder: `${courseFileStem(manifest)}-latex`,
|
|
186
|
-
|
|
308
|
+
numbering: againstWebsite(bookNumbers(parts)),
|
|
309
|
+
since: bookSince(parts),
|
|
187
310
|
});
|
|
188
311
|
}
|
|
189
312
|
|
|
190
|
-
const pending: PendingFile[] = jobs
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
313
|
+
const pending: PendingFile[] = jobs
|
|
314
|
+
.filter((job) => !job.noZip)
|
|
315
|
+
.map((job) => ({
|
|
316
|
+
target: join(outputRoot, 'downloads', `${job.target}-latex.zip`),
|
|
317
|
+
content: zip(job.files.map((file) => ({ path: `${job.folder}/${file.path}`, content: file.content }))),
|
|
318
|
+
}));
|
|
194
319
|
|
|
195
|
-
// Compile every document at once, up to one per core
|
|
196
|
-
// every chapter, so each is checked against the website
|
|
320
|
+
// Compile every document at once, up to one per core, the longest first;
|
|
321
|
+
// a book numbers every chapter, so each is checked against the website
|
|
322
|
+
// whole. A document reused from the cache is checked the same way.
|
|
197
323
|
const durations: { kind: Job['kind']; seconds: number }[] = [];
|
|
324
|
+
const cache = 'missing' in engine ? undefined : await latexCacheFromEnvironment(engine, logger);
|
|
325
|
+
let reused = 0;
|
|
198
326
|
const started = performance.now();
|
|
199
327
|
if (!('missing' in engine)) {
|
|
200
328
|
const compiled = await mapWithLimit(jobs, availableParallelism(), async (job) => {
|
|
201
329
|
const jobStarted = performance.now();
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
330
|
+
const options = { epoch: epochOf(manifest, job.since), name: job.name };
|
|
331
|
+
const compile = cache
|
|
332
|
+
? await cache.compile(job.files, options)
|
|
333
|
+
: { result: await compileLatex(job.files, engine, options), reused: false };
|
|
334
|
+
const { result } = compile;
|
|
335
|
+
if (compile.reused) reused += 1;
|
|
336
|
+
else durations.push({ kind: job.kind, seconds: (performance.now() - jobStarted) / 1000 });
|
|
337
|
+
const problems = job.numbering(result.aux);
|
|
205
338
|
if (problems.length > 0) {
|
|
206
339
|
throw new Error(`LaTeX numbers ${job.name} differently from the website:\n ${problems.join('\n ')}`);
|
|
207
340
|
}
|
|
208
|
-
|
|
209
|
-
//
|
|
210
|
-
|
|
341
|
+
job.review?.(result.log);
|
|
342
|
+
// A part's and the course's PDFs repeat their chapters' text, and a
|
|
343
|
+
// handout its slides', so each character is reported once, by the
|
|
344
|
+
// document that has it first.
|
|
345
|
+
const missing = (job.kind === 'chapter' || job.kind === 'deck') && !job.noZip ? missingCharacters(result.log) : [];
|
|
211
346
|
if (missing.length > 0) {
|
|
212
347
|
logger.warn(
|
|
213
348
|
`LaTeX export: the PDF of ${job.name} leaves ${missing.length} character(s) blank, ` +
|
|
@@ -220,7 +355,7 @@ export async function writeLatexExports(
|
|
|
220
355
|
);
|
|
221
356
|
}
|
|
222
357
|
return result.pdf;
|
|
223
|
-
});
|
|
358
|
+
}, startOrder(jobs));
|
|
224
359
|
jobs.forEach((job, index) => {
|
|
225
360
|
pending.push({ target: join(outputRoot, 'downloads', `${job.target}.pdf`), content: compiled[index]! });
|
|
226
361
|
});
|
|
@@ -238,15 +373,22 @@ export async function writeLatexExports(
|
|
|
238
373
|
} finally {
|
|
239
374
|
await Promise.all(temporary.map((file) => rm(file.temporary, { force: true })));
|
|
240
375
|
}
|
|
376
|
+
const pruned = cache ? await cache.prune() : 0;
|
|
241
377
|
const pdfs = pending.filter((file) => file.target.endsWith('.pdf')).length;
|
|
242
378
|
const seconds = (kind: Job['kind']) =>
|
|
243
379
|
durations.filter((entry) => entry.kind === kind).reduce((sum, entry) => sum + entry.seconds, 0);
|
|
244
380
|
logger.info(
|
|
245
|
-
`LaTeX export: ${chapters.length} chapter(s), ${
|
|
246
|
-
`${
|
|
381
|
+
`LaTeX export: ${chapters.length} chapter(s), ${decks.length} deck(s), ` +
|
|
382
|
+
`${parts.length} part(s)${parts.length > 0 ? ' and the course' : ''}: ` +
|
|
383
|
+
`${jobs.filter((job) => !job.noZip).length} source zip(s)` +
|
|
247
384
|
(pdfs > 0
|
|
248
385
|
? `, ${pdfs} PDF(s) with ${'name' in engine ? engine.name : ''} in ${wall.toFixed(1)} s on up to ` +
|
|
249
|
-
`${availableParallelism()} core(s) (
|
|
386
|
+
`${availableParallelism()} core(s) (` +
|
|
387
|
+
(cache
|
|
388
|
+
? `${pdfs - reused} compiled, ${reused} reused from ${cache.directory}` +
|
|
389
|
+
`${pruned > 0 ? `, ${pruned} unused entr${pruned === 1 ? 'y' : 'ies'} deleted` : ''}; `
|
|
390
|
+
: '') +
|
|
391
|
+
`compile time: chapters ${seconds('chapter').toFixed(1)} s, decks ${seconds('deck').toFixed(1)} s, ` +
|
|
250
392
|
`parts ${seconds('part').toFixed(1)} s, course ${seconds('course').toFixed(1)} s)`
|
|
251
393
|
: '') +
|
|
252
394
|
(captures.size > 0 ? `, figures from ${captures.size} page(s) drawn as vector PDF` : '') +
|
|
@@ -15,7 +15,7 @@ import { gfmTableToMarkdown } from 'mdast-util-gfm-table';
|
|
|
15
15
|
import { mathToMarkdown } from 'mdast-util-math';
|
|
16
16
|
import { toMarkdown } from 'mdast-util-to-markdown';
|
|
17
17
|
|
|
18
|
-
import { captionTitle } from '../../../
|
|
18
|
+
import { captionTitle } from '../../../reference/float-name';
|
|
19
19
|
import { stripNotationMarkers } from '../../../reference/math-glyphs.mjs';
|
|
20
20
|
import type { ContentSegment } from '../../../reference/mixed-content';
|
|
21
21
|
import type { CheckItem } from '../latex/serialize';
|
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
DiagramGeometry,
|
|
26
26
|
Inline,
|
|
27
27
|
NotationEntry,
|
|
28
|
+
StaticDeck,
|
|
28
29
|
StaticPage,
|
|
29
30
|
TableCell,
|
|
30
31
|
} from '../static-tree';
|
|
@@ -55,8 +56,11 @@ export interface MarkdownChapterContext {
|
|
|
55
56
|
readonly pictures: ReadonlyMap<string, string>;
|
|
56
57
|
/** Answers of this page's checks, by assessment id, in authored order. */
|
|
57
58
|
readonly checkItems: ReadonlyMap<string, readonly CheckItem[]>;
|
|
58
|
-
/**
|
|
59
|
-
|
|
59
|
+
/**
|
|
60
|
+
* The line under the title: the course, the version the page's content is
|
|
61
|
+
* at (`versionPhrase`, rule C5) and its live URL.
|
|
62
|
+
*/
|
|
63
|
+
readonly source?: { readonly course: string; readonly since?: string; readonly url?: string };
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
type Phrasing = PhrasingContent;
|
|
@@ -526,16 +530,19 @@ class MarkdownWriter {
|
|
|
526
530
|
];
|
|
527
531
|
}
|
|
528
532
|
|
|
529
|
-
|
|
533
|
+
/**
|
|
534
|
+
* The answers to `checks`: under their own heading at a page's end, or
|
|
535
|
+
* bare (`inline`), as a deck's speaker notes give them.
|
|
536
|
+
*/
|
|
537
|
+
answers(checks: readonly Extract<Block, { kind: 'check' }>[], inline = false): RootContent[] {
|
|
530
538
|
if (checks.length === 0) return [];
|
|
531
539
|
return [
|
|
532
|
-
{ type: 'heading', depth: 2, children: [anchor('answers'), text('Answers to the knowledge checks')] },
|
|
540
|
+
...(inline ? [] : [{ type: 'heading', depth: 2, children: [anchor('answers'), text('Answers to the knowledge checks')] } as RootContent]),
|
|
533
541
|
...checks.flatMap((check): RootContent[] => {
|
|
534
542
|
const items = this.context.checkItems.get(check.assessmentId) ?? [];
|
|
543
|
+
const name = text(`Knowledge check ${check.number}`);
|
|
535
544
|
return [
|
|
536
|
-
paragraph([
|
|
537
|
-
strong([link(`#${check.anchor}`, [text(`Knowledge check ${check.number}`)]), text(': '), ...this.inlines(check.title)]),
|
|
538
|
-
]),
|
|
545
|
+
paragraph([strong([inline ? name : link(`#${check.anchor}`, [name]), text(': '), ...this.inlines(check.title)])]),
|
|
539
546
|
{
|
|
540
547
|
type: 'list',
|
|
541
548
|
ordered: true,
|
|
@@ -608,7 +615,7 @@ export function serializeMarkdownPage(
|
|
|
608
615
|
? [
|
|
609
616
|
text('From '),
|
|
610
617
|
emphasis([text(source.course)]),
|
|
611
|
-
...(source.
|
|
618
|
+
...(source.since ? [text(`, ${source.since}`)] : []),
|
|
612
619
|
text('.'),
|
|
613
620
|
...(source.url ? [text(' Interactive version: '), link(source.url, [text(source.url)]), text('.')] : []),
|
|
614
621
|
]
|
|
@@ -626,6 +633,108 @@ export function serializeMarkdownPage(
|
|
|
626
633
|
]);
|
|
627
634
|
}
|
|
628
635
|
|
|
636
|
+
/** The slide sizes Marp's built-in themes have. */
|
|
637
|
+
export type MarpSize = '16:9' | '4:3';
|
|
638
|
+
|
|
639
|
+
export interface MarpDeckContext extends Omit<MarkdownChapterContext, 'page'> {
|
|
640
|
+
readonly deck: StaticDeck;
|
|
641
|
+
/** The symbols its frames use, by first use: its closing notation slide. */
|
|
642
|
+
readonly symbols: readonly NotationEntry[];
|
|
643
|
+
readonly size: MarpSize;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* A deck in Marp's Markdown (decision D10): front matter that turns Marp on,
|
|
648
|
+
* one slide per frame between `---` lines, numbered as the deck is, each
|
|
649
|
+
* frame's notes and its check's answers as an HTML comment (Marp's speaker
|
|
650
|
+
* notes), and a closing slide of the symbols the frames use. A step shows
|
|
651
|
+
* its frame's final state, as the handout does. The file reads as plain
|
|
652
|
+
* Markdown too; Marp, its VS Code extension and Deckset present it.
|
|
653
|
+
*/
|
|
654
|
+
export function serializeMarpDeck(context: MarpDeckContext): string {
|
|
655
|
+
const { deck, source } = context;
|
|
656
|
+
const page: StaticPage = {
|
|
657
|
+
slug: deck.slug,
|
|
658
|
+
route: deck.route,
|
|
659
|
+
title: deck.title,
|
|
660
|
+
blocks: [],
|
|
661
|
+
notation: deck.notation,
|
|
662
|
+
notationRemarks: [],
|
|
663
|
+
};
|
|
664
|
+
const writer = new MarkdownWriter({ ...context, page });
|
|
665
|
+
const heading = (depth: 1 | 2, children: Phrasing[]): RootContent => ({ type: 'heading', depth, children });
|
|
666
|
+
// Marp's own class for a slide whose content is centred: the title and a divider.
|
|
667
|
+
const lead: RootContent = { type: 'html', value: '<!-- _class: lead -->' };
|
|
668
|
+
const notes = (blocks: RootContent[]): RootContent[] =>
|
|
669
|
+
blocks.length === 0
|
|
670
|
+
? []
|
|
671
|
+
: [{ type: 'html', value: `<!--\n${serializeNodes(blocks).trim().replace(/--/gu, '- -')}\n-->` }];
|
|
672
|
+
const slides: RootContent[][] = deck.frames.map((frame) => {
|
|
673
|
+
const title = writer.inlines(frame.title);
|
|
674
|
+
if (frame.kind === 'title') {
|
|
675
|
+
return [
|
|
676
|
+
lead,
|
|
677
|
+
heading(1, title),
|
|
678
|
+
...(source
|
|
679
|
+
? [
|
|
680
|
+
paragraph([
|
|
681
|
+
emphasis([text(source.course)]),
|
|
682
|
+
...(source.since ? [text(`, ${source.since}`)] : []),
|
|
683
|
+
text('.'),
|
|
684
|
+
...(source.url ? [text(' Interactive version: '), link(source.url, [text(source.url)]), text('.')] : []),
|
|
685
|
+
]),
|
|
686
|
+
]
|
|
687
|
+
: []),
|
|
688
|
+
];
|
|
689
|
+
}
|
|
690
|
+
if (frame.kind === 'section') return [lead, heading(1, title)];
|
|
691
|
+
const blocks = frame.segments.flatMap((segment) => segment.blocks);
|
|
692
|
+
const legend = frame.legend.filter((item) => item.tex);
|
|
693
|
+
return [
|
|
694
|
+
heading(2, title),
|
|
695
|
+
...writer.blocks(blocks),
|
|
696
|
+
...(legend.length > 0
|
|
697
|
+
? [
|
|
698
|
+
paragraph([
|
|
699
|
+
emphasis(
|
|
700
|
+
legend.flatMap((item, index): Phrasing[] => [
|
|
701
|
+
...(index > 0 ? [text(' · ')] : []),
|
|
702
|
+
{ type: 'inlineMath', value: plainTex(item.tex) } as Phrasing,
|
|
703
|
+
text(' '),
|
|
704
|
+
...writer.inlines(item.label),
|
|
705
|
+
]),
|
|
706
|
+
),
|
|
707
|
+
]),
|
|
708
|
+
]
|
|
709
|
+
: []),
|
|
710
|
+
...notes([...writer.blocks(frame.notes), ...writer.answers(checksOf(blocks), true)]),
|
|
711
|
+
];
|
|
712
|
+
});
|
|
713
|
+
if (context.symbols.length > 0) {
|
|
714
|
+
slides.push([heading(2, [text('Notation')]), ...writer.notation(context.symbols, []).slice(1)]);
|
|
715
|
+
}
|
|
716
|
+
const body = serializeNodes(
|
|
717
|
+
slides.flatMap((slide, index): RootContent[] => [...(index > 0 ? [{ type: 'thematicBreak' } as RootContent] : []), ...slide]),
|
|
718
|
+
);
|
|
719
|
+
const frontMatter = [
|
|
720
|
+
'---',
|
|
721
|
+
'marp: true',
|
|
722
|
+
`title: ${JSON.stringify(deck.title)}`,
|
|
723
|
+
`size: ${context.size}`,
|
|
724
|
+
'paginate: true',
|
|
725
|
+
'math: katex',
|
|
726
|
+
'---',
|
|
727
|
+
'',
|
|
728
|
+
].join('\n');
|
|
729
|
+
// Marp shows raw HTML as text: the `<a id>` anchors a page's links land on
|
|
730
|
+
// go, and a link to one of them keeps its words.
|
|
731
|
+
const markdown = body
|
|
732
|
+
.replace(/<a id="[^"]*"><\/a> ?/gu, '')
|
|
733
|
+
.replace(/(?<!!)\[([^\]]*)\]\(#[^)]*\)/gu, '$1')
|
|
734
|
+
.replace(/\n{3,}/gu, '\n\n');
|
|
735
|
+
return `${frontMatter}\n${markdown}`;
|
|
736
|
+
}
|
|
737
|
+
|
|
629
738
|
/** Markdown of a few blocks of mdast, written the same way (for a download's `index.md`). */
|
|
630
739
|
export function serializeMarkdownNodes(children: RootContent[]): string {
|
|
631
740
|
return serializeNodes(children);
|