explico 0.11.1 → 0.13.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 +1292 -1
- package/README.md +91 -13
- package/dist/compiler/astro.d.ts +11 -4
- package/dist/compiler/astro.js +48 -15
- package/dist/compiler/cli.d.ts +27 -12
- package/dist/compiler/cli.js +96 -25
- package/dist/compiler/collections.d.ts +33 -2
- package/dist/compiler/collections.js +91 -19
- package/dist/compiler/content-schema.d.ts +31 -0
- package/dist/compiler/content-schema.js +82 -0
- package/dist/compiler/content-schemas.d.ts +422 -0
- package/dist/compiler/content-schemas.js +229 -0
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +30 -2
- package/dist/compiler/exports/combined-html.d.ts +97 -0
- package/dist/compiler/exports/combined-html.js +492 -0
- package/dist/compiler/exports/download-index.d.ts +9 -0
- package/dist/compiler/exports/download-index.js +33 -0
- package/dist/compiler/exports/export-chapters.d.ts +98 -0
- package/dist/compiler/exports/export-chapters.js +164 -0
- package/dist/compiler/exports/export-pages.d.ts +48 -0
- package/dist/compiler/exports/export-pages.js +22 -0
- package/dist/compiler/exports/html-tree.d.ts +22 -0
- package/dist/compiler/exports/html-tree.js +47 -0
- package/dist/compiler/exports/latex/capture.d.ts +106 -0
- package/dist/compiler/exports/latex/capture.js +435 -0
- package/dist/compiler/exports/latex/compile.d.ts +73 -0
- package/dist/compiler/exports/latex/compile.js +115 -0
- package/dist/compiler/exports/latex/document.d.ts +102 -0
- package/dist/compiler/exports/latex/document.js +349 -0
- package/dist/compiler/exports/latex/explico.cls +415 -0
- package/dist/compiler/exports/latex/numbering.d.ts +18 -0
- package/dist/compiler/exports/latex/numbering.js +57 -0
- package/dist/compiler/exports/latex/serialize.d.ts +112 -0
- package/dist/compiler/exports/latex/serialize.js +606 -0
- package/dist/compiler/exports/latex-export.d.ts +44 -0
- package/dist/compiler/exports/latex-export.js +169 -0
- package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
- package/dist/compiler/exports/markdown/serialize.js +527 -0
- package/dist/compiler/exports/markdown-export.d.ts +39 -0
- package/dist/compiler/exports/markdown-export.js +240 -0
- package/dist/compiler/exports/standalone-html.d.ts +15 -5
- package/dist/compiler/exports/standalone-html.js +23 -51
- package/dist/compiler/exports/static-tree.d.ts +253 -0
- package/dist/compiler/exports/static-tree.js +625 -0
- package/dist/compiler/exports/zip.d.ts +16 -0
- package/dist/compiler/exports/zip.js +72 -0
- package/dist/compiler/lesson-derivation.d.ts +8 -2
- package/dist/compiler/lesson-derivation.js +6 -7
- package/dist/compiler/manifest.d.ts +115 -8
- package/dist/compiler/manifest.js +221 -39
- package/dist/compiler/render-diagnostics.d.ts +65 -0
- package/dist/compiler/render-diagnostics.js +56 -0
- package/dist/compiler/resolution-report.d.ts +3 -1
- package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
- package/dist/curriculum/validation.d.ts +9 -3
- package/dist/curriculum/validation.js +35 -17
- package/dist/reference/base-abbreviations.d.mts +2 -2
- package/dist/reference/base-abbreviations.mjs +2 -2
- package/dist/reference/base-operators.d.mts +8 -2
- package/dist/reference/base-operators.mjs +8 -10
- package/dist/reference/base-roman-labels.d.mts +4 -3
- package/dist/reference/base-roman-labels.mjs +4 -3
- package/dist/reference/citation-format.d.mts +35 -0
- package/dist/reference/citation-format.mjs +74 -0
- package/dist/reference/consistency.d.ts +5 -9
- package/dist/reference/consistency.js +34 -38
- package/dist/reference/curriculum-alignment.js +3 -24
- package/dist/reference/derived-entries.d.ts +45 -0
- package/dist/reference/derived-entries.js +50 -0
- package/dist/reference/derived-notation.d.mts +125 -0
- package/dist/reference/derived-notation.mjs +121 -0
- package/dist/reference/diagram-fit.d.ts +45 -0
- package/dist/reference/diagram-fit.js +54 -0
- package/dist/reference/diagram-layout.d.ts +59 -10
- package/dist/reference/diagram-layout.js +85 -24
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +1 -1
- package/dist/reference/equations-validate.js +26 -1
- package/dist/reference/equations.d.mts +27 -0
- package/dist/reference/equations.mjs +120 -22
- package/dist/reference/examples-validate.d.ts +1 -1
- package/dist/reference/figure-fit.d.ts +70 -0
- package/dist/reference/figure-fit.js +60 -0
- package/dist/reference/figure-pictures.d.ts +27 -0
- package/dist/reference/figure-pictures.js +49 -0
- package/dist/reference/figure-symbols.d.ts +32 -0
- package/dist/reference/figure-symbols.js +55 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-abbreviations.js +1 -1
- package/dist/reference/gate-course-rules.d.ts +4 -3
- package/dist/reference/gate-course-rules.js +9 -4
- package/dist/reference/gate-heading-length.d.ts +2 -0
- package/dist/reference/gate-heading-length.js +7 -5
- package/dist/reference/gate-heading-structure.js +26 -0
- package/dist/reference/gate-indices.d.ts +35 -0
- package/dist/reference/gate-indices.js +299 -0
- package/dist/reference/gate-math.d.ts +32 -2
- package/dist/reference/gate-math.js +68 -12
- package/dist/reference/gate-mdx-expression.d.ts +17 -0
- package/dist/reference/gate-mdx-expression.js +29 -0
- package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
- package/dist/reference/gate-non-chapter-pages.js +156 -0
- package/dist/reference/gate-part-pages.d.ts +4 -2
- package/dist/reference/gate-part-pages.js +2 -1
- package/dist/reference/gate-prop-text.d.ts +28 -0
- package/dist/reference/gate-prop-text.js +106 -0
- package/dist/reference/gate-prose-notation.d.ts +3 -1
- package/dist/reference/gate-prose-notation.js +4 -2
- package/dist/reference/gate-retired-keys.d.ts +35 -0
- package/dist/reference/gate-retired-keys.js +58 -0
- package/dist/reference/gloss.d.ts +2 -0
- package/dist/reference/katex-measure.d.ts +17 -0
- package/dist/reference/katex-measure.js +223 -0
- package/dist/reference/keyed-float-validate.js +12 -0
- package/dist/reference/keyed-jsx-floats.d.mts +2 -2
- package/dist/reference/keyed-jsx-floats.mjs +11 -14
- package/dist/reference/lab-katex-validate.d.ts +43 -0
- package/dist/reference/lab-katex-validate.js +96 -0
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-scope.js +13 -2
- package/dist/reference/lint-ignore.js +1 -1
- package/dist/reference/math-glyphs.d.mts +94 -2
- package/dist/reference/math-glyphs.mjs +851 -46
- package/dist/reference/mixed-content.d.ts +8 -3
- package/dist/reference/mixed-content.js +4 -2
- package/dist/reference/page-notation.d.ts +39 -7
- package/dist/reference/page-notation.js +62 -10
- package/dist/reference/raw-floats.mjs +37 -13
- package/dist/reference/references.d.ts +11 -1
- package/dist/reference/references.js +6 -0
- package/dist/reference/registry.js +115 -11
- package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
- package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
- package/dist/reference/rehype-heading-anchors.mjs +11 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-notation.mjs +275 -23
- package/dist/reference/render-lab-math.d.ts +13 -2
- package/dist/reference/render-lab-math.js +18 -6
- package/dist/reference/sections-validate.d.ts +28 -3
- package/dist/reference/sections-validate.js +31 -4
- package/dist/reference/sections.d.mts +24 -6
- package/dist/reference/sections.mjs +92 -17
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/tables-validate.js +24 -1
- package/dist/reference/text-measure.d.ts +10 -0
- package/dist/reference/text-measure.js +41 -0
- package/dist/reference/text-widths.d.ts +10 -0
- package/dist/reference/text-widths.js +1238 -0
- package/dist/reference/types.d.ts +64 -8
- package/dist/rules/catalog.js +223 -32
- package/dist/rules/in-effect.d.ts +1 -1
- package/dist/rules/rules.schema.json +100 -27
- package/dist/rules/settings.d.ts +1 -1
- package/dist/rules/settings.js +7 -0
- package/dist/session/PreferenceStore.d.ts +18 -1
- package/dist/session/PreferenceStore.js +29 -1
- package/docs/authoring/README.md +33 -21
- package/docs/authoring/agents.md +38 -7
- package/docs/authoring/components.md +115 -10
- package/docs/authoring/migrating.md +878 -0
- package/docs/authoring/notation.md +285 -30
- package/docs/authoring/retired.md +1 -0
- package/docs/authoring/structure-rules.md +84 -45
- package/docs/authoring/structure.md +67 -2
- package/docs/authoring/visualizations.md +167 -25
- package/package.json +12 -7
- package/src/compiler/astro.ts +72 -21
- package/src/compiler/cli.ts +143 -35
- package/src/compiler/collections.ts +158 -27
- package/src/compiler/content-schema.ts +108 -0
- package/src/compiler/content-schemas.ts +391 -0
- package/src/compiler/course-config.ts +86 -15
- package/src/compiler/exports/combined-html.ts +669 -0
- package/src/compiler/exports/download-index.ts +36 -0
- package/src/compiler/exports/export-chapters.ts +233 -0
- package/src/compiler/exports/export-pages.ts +44 -0
- package/src/compiler/exports/html-tree.ts +62 -0
- package/src/compiler/exports/latex/capture.ts +651 -0
- package/src/compiler/exports/latex/compile.ts +168 -0
- package/src/compiler/exports/latex/document.ts +479 -0
- package/src/compiler/exports/latex/explico.cls +415 -0
- package/src/compiler/exports/latex/numbering.ts +74 -0
- package/src/compiler/exports/latex/serialize.ts +787 -0
- package/src/compiler/exports/latex/texlive-packages.txt +76 -0
- package/src/compiler/exports/latex-export.ts +255 -0
- package/src/compiler/exports/markdown/serialize.ts +632 -0
- package/src/compiler/exports/markdown-export.ts +329 -0
- package/src/compiler/exports/standalone-html.ts +39 -70
- package/src/compiler/exports/static-tree.ts +944 -0
- package/src/compiler/exports/zip.ts +88 -0
- package/src/compiler/lesson-derivation.ts +13 -8
- package/src/compiler/manifest.ts +387 -40
- package/src/compiler/render-diagnostics.ts +111 -0
- package/src/components/CourseContents.astro +6 -0
- package/src/components/HoverPanel.astro +204 -6
- package/src/components/PartOverview.astro +18 -9
- package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
- package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
- package/src/components/assessments/AssessmentRunner.tsx +7 -1
- package/src/components/assessments/AssessmentSet.astro +14 -1
- package/src/components/block-heading.ts +38 -0
- package/src/components/card-arrow.ts +18 -4
- package/src/components/card-carry.ts +406 -0
- package/src/components/card-drag.ts +1 -30
- package/src/components/card-fetch.ts +87 -0
- package/src/components/card-manager.ts +1222 -38
- package/src/components/card-memory.ts +5 -1
- package/src/components/card-placement.ts +211 -13
- package/src/components/card-side.ts +281 -31
- package/src/components/citation/CitationLayer.astro +45 -40
- package/src/components/diagram/Diagram.astro +123 -44
- package/src/components/disclosure/print-as-shown.ts +42 -6
- package/src/components/figure/Figure.astro +17 -1
- package/src/components/figure/FloatCaption.astro +4 -17
- package/src/components/figure/figure-state.ts +26 -17
- package/src/components/float-toolbar.ts +274 -0
- package/src/components/memo/Memo.astro +21 -10
- package/src/components/memo/ShortNote.astro +4 -0
- package/src/components/notation/NotationGlossary.astro +22 -17
- package/src/components/notation/NotationLayer.astro +300 -79
- package/src/components/notation/NotationMath.astro +8 -1
- package/src/components/notation/render-notation-math.ts +51 -5
- package/src/components/readable-text.ts +25 -0
- package/src/components/reference/FloatRefLayer.astro +162 -89
- package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
- package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
- package/src/components/starlight/LayoutPageSidebar.astro +17 -3
- package/src/components/starlight/LayoutSocialIcons.astro +79 -29
- package/src/components/starlight/LessonExportActions.astro +384 -310
- package/src/components/starlight/LessonFooter.astro +66 -8
- package/src/components/starlight/PrintController.astro +28 -0
- package/src/components/starlight/TextSizeControl.astro +137 -0
- package/src/components/starlight/print-controller.ts +380 -0
- package/src/components/starlight/section-summary.ts +13 -5
- package/src/components/table/Table.astro +14 -22
- package/src/components/table/table-truncation.ts +15 -0
- package/src/content-config.ts +20 -312
- package/src/curriculum/validation.ts +72 -24
- package/src/reference/base-abbreviations.mjs +2 -2
- package/src/reference/base-operators.mjs +8 -10
- package/src/reference/base-roman-labels.mjs +4 -3
- package/src/reference/citation-format.mjs +74 -0
- package/src/reference/consistency.ts +47 -61
- package/src/reference/curriculum-alignment.ts +4 -41
- package/src/reference/derived-entries.ts +68 -0
- package/src/reference/derived-notation.mjs +121 -0
- package/src/reference/diagram-fit.ts +104 -0
- package/src/reference/diagram-layout.ts +182 -51
- package/src/reference/diagrams-validate.ts +2 -1
- package/src/reference/equations-validate.ts +41 -2
- package/src/reference/equations.mjs +120 -22
- package/src/reference/examples-validate.ts +1 -0
- package/src/reference/figure-fit.ts +139 -0
- package/src/reference/figure-pictures.ts +96 -0
- package/src/reference/figure-symbols.ts +117 -0
- package/src/reference/figures-validate.ts +2 -1
- package/src/reference/gate-abbreviations.ts +1 -1
- package/src/reference/gate-course-rules.ts +22 -7
- package/src/reference/gate-heading-length.ts +25 -7
- package/src/reference/gate-heading-structure.ts +40 -1
- package/src/reference/gate-indices.ts +470 -0
- package/src/reference/gate-math.ts +102 -13
- package/src/reference/gate-mdx-expression.ts +45 -0
- package/src/reference/gate-non-chapter-pages.ts +241 -0
- package/src/reference/gate-part-pages.ts +1 -1
- package/src/reference/gate-prop-text.ts +145 -0
- package/src/reference/gate-prose-notation.ts +19 -5
- package/src/reference/gate-retired-keys.ts +101 -0
- package/src/reference/gloss.ts +2 -0
- package/src/reference/katex-measure.ts +384 -0
- package/src/reference/keyed-float-validate.ts +22 -0
- package/src/reference/keyed-jsx-floats.mjs +11 -14
- package/src/reference/lab-katex-validate.ts +156 -0
- package/src/reference/lab-math-scope.ts +15 -1
- package/src/reference/lint-ignore.ts +4 -1
- package/src/reference/math-glyphs.mjs +851 -46
- package/src/reference/mixed-content.ts +11 -2
- package/src/reference/page-notation.ts +114 -14
- package/src/reference/raw-floats.mjs +37 -13
- package/src/reference/references.ts +14 -0
- package/src/reference/registry.ts +170 -9
- package/src/reference/rehype-fail-katex-errors.mjs +19 -0
- package/src/reference/rehype-heading-anchors.mjs +11 -1
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-notation.mjs +275 -23
- package/src/reference/render-lab-math.ts +40 -5
- package/src/reference/sections-validate.ts +67 -3
- package/src/reference/sections.mjs +92 -17
- package/src/reference/tables-validate.ts +40 -2
- package/src/reference/text-measure.ts +63 -0
- package/src/reference/text-widths.ts +133 -0
- package/src/reference/types.ts +75 -8
- package/src/rules/catalog.ts +241 -31
- package/src/rules/settings.ts +10 -0
- package/src/session/PreferenceStore.ts +53 -0
- package/src/styles/global.css +322 -83
|
@@ -73,11 +73,30 @@ function normalizeBlockLabels(config) {
|
|
|
73
73
|
}
|
|
74
74
|
return labels;
|
|
75
75
|
}
|
|
76
|
+
const CARDS_DEFAULTS = { carry: false };
|
|
77
|
+
function normalizeCards(config) {
|
|
78
|
+
const carry = config.cards?.carry ?? CARDS_DEFAULTS.carry;
|
|
79
|
+
if (typeof carry !== "boolean") {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`CourseConfig cards.carry must be true or false; received ${JSON.stringify(carry)}.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return { carry };
|
|
85
|
+
}
|
|
76
86
|
function normalizeLessonExports(config) {
|
|
77
87
|
const standaloneHtml = config.lessonExports?.standaloneHtml === true;
|
|
78
88
|
const print = config.lessonExports?.print === true;
|
|
89
|
+
const latex = config.lessonExports?.latex === true;
|
|
90
|
+
const rawPaper = config.lessonExports?.latexPaper ?? "letter";
|
|
91
|
+
if (rawPaper !== "letter" && rawPaper !== "a4") {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`CourseConfig lessonExports.latexPaper must be "letter" or "a4"; received ${JSON.stringify(rawPaper)}.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
const latexPaper = rawPaper;
|
|
97
|
+
const markdown = config.lessonExports?.markdown === true;
|
|
79
98
|
const rawBase = config.lessonExports?.canonicalBaseUrl;
|
|
80
|
-
if (rawBase === void 0) return { standaloneHtml, print };
|
|
99
|
+
if (rawBase === void 0) return { standaloneHtml, print, latex, latexPaper, markdown };
|
|
81
100
|
let parsed;
|
|
82
101
|
try {
|
|
83
102
|
parsed = new URL(rawBase);
|
|
@@ -97,7 +116,14 @@ function normalizeLessonExports(config) {
|
|
|
97
116
|
);
|
|
98
117
|
}
|
|
99
118
|
if (!parsed.pathname.endsWith("/")) parsed.pathname += "/";
|
|
100
|
-
return {
|
|
119
|
+
return {
|
|
120
|
+
standaloneHtml,
|
|
121
|
+
print,
|
|
122
|
+
latex,
|
|
123
|
+
latexPaper,
|
|
124
|
+
markdown,
|
|
125
|
+
canonicalBaseUrl: parsed.toString()
|
|
126
|
+
};
|
|
101
127
|
}
|
|
102
128
|
function coursePartsOf(config) {
|
|
103
129
|
const parts = config.parts ?? config.sections;
|
|
@@ -110,9 +136,11 @@ function coursePartsOf(config) {
|
|
|
110
136
|
}
|
|
111
137
|
export {
|
|
112
138
|
BLOCK_LABELS_DEFAULT,
|
|
139
|
+
CARDS_DEFAULTS,
|
|
113
140
|
DISCLOSURE_DEFAULTS,
|
|
114
141
|
coursePartsOf,
|
|
115
142
|
normalizeBlockLabels,
|
|
143
|
+
normalizeCards,
|
|
116
144
|
normalizeDisclosure,
|
|
117
145
|
normalizeLessonExports,
|
|
118
146
|
normalizeNumbering
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Manifest } from '../manifest.js';
|
|
2
|
+
import '../../curriculum/validation.js';
|
|
3
|
+
import '../../reference/consistency.js';
|
|
4
|
+
import '../../course-config-rRtME5UV.js';
|
|
5
|
+
import '../../rules/catalog.js';
|
|
6
|
+
import '../../rules/course-rules.js';
|
|
7
|
+
import '../collections.js';
|
|
8
|
+
import '../../reference/types.js';
|
|
9
|
+
import '../../reference/gate-retired-keys.js';
|
|
10
|
+
import '../../reference/lint-ignore.js';
|
|
11
|
+
import '../../reference/equations-validate.js';
|
|
12
|
+
import '../../reference/tables-validate.js';
|
|
13
|
+
import '../../reference/keyed-float-validate.js';
|
|
14
|
+
import '../../reference/figures-validate.js';
|
|
15
|
+
import '../../reference/diagrams-validate.js';
|
|
16
|
+
import '../../reference/examples-validate.js';
|
|
17
|
+
import '../../reference/folds-validate.js';
|
|
18
|
+
import '../../reference/knowledge-checks-validate.js';
|
|
19
|
+
import '../../reference/raw-floats-validate.js';
|
|
20
|
+
import '../../reference/lab-katex-validate.js';
|
|
21
|
+
import '../../reference/gate-math.js';
|
|
22
|
+
import '../../reference/mixed-content.js';
|
|
23
|
+
import '../../reference/gloss.js';
|
|
24
|
+
import '../../rules/waivers.js';
|
|
25
|
+
import '../knowledge-version.js';
|
|
26
|
+
import '../lesson-derivation.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One offline HTML file of several built pages (decision D7): a part — its
|
|
30
|
+
* overview page, then its chapters — or the whole course.
|
|
31
|
+
*
|
|
32
|
+
* It is assembled from the pages Astro already built, not rendered again:
|
|
33
|
+
* each page keeps everything explico rendered for it, its body and the card
|
|
34
|
+
* layers at its foot (notation, citations, abbreviations, float references),
|
|
35
|
+
* so every number, card and check is the website's. Each page becomes one
|
|
36
|
+
* `<section>`, and so that ids stay unique, every id in it gains the page's
|
|
37
|
+
* suffix (`eq-range` → `eq-range--projectiles-projectile-range`), with every
|
|
38
|
+
* reference to it: fragment links, ARIA id lists, `for`, `popovertarget`,
|
|
39
|
+
* SVG `href` and `url(#…)`. A suffix, not a prefix, keeps the kind an id
|
|
40
|
+
* starts with (`eq-`, `fig-`, `cite-`), which the card layers read. A link
|
|
41
|
+
* to another page of the document becomes a link into it, so its hover
|
|
42
|
+
* preview works offline. The standalone exporter then inlines the document
|
|
43
|
+
* as it does one page.
|
|
44
|
+
*/
|
|
45
|
+
interface CombinedPage {
|
|
46
|
+
/** The page's docs slug: `projectiles/index`, `projectiles/projectile-range`. */
|
|
47
|
+
readonly slug: string;
|
|
48
|
+
/** Astro's built HTML of the page. */
|
|
49
|
+
readonly html: string;
|
|
50
|
+
}
|
|
51
|
+
/** A line of the document's contents; its title is the page's own `<h1>`. */
|
|
52
|
+
interface ContentsEntry {
|
|
53
|
+
readonly slug: string;
|
|
54
|
+
/** `Part 2`, `2.1`; none for a page outside the numbered parts. */
|
|
55
|
+
readonly number?: string;
|
|
56
|
+
readonly children?: readonly ContentsEntry[];
|
|
57
|
+
}
|
|
58
|
+
interface CombinedDocumentInput {
|
|
59
|
+
/** The document's title: the part's label or the course's name. */
|
|
60
|
+
readonly title: string;
|
|
61
|
+
/** The pages in reading order. */
|
|
62
|
+
readonly pages: readonly CombinedPage[];
|
|
63
|
+
/** The contents, set after the first page; none for a single page. */
|
|
64
|
+
readonly contents?: readonly ContentsEntry[];
|
|
65
|
+
}
|
|
66
|
+
/** The suffix a page's ids gain: its route, `/` as `-`; the landing page is `home`. */
|
|
67
|
+
declare function pageKey(slug: string): string;
|
|
68
|
+
/** The id of a page's section in the combined document. */
|
|
69
|
+
declare function pageSectionId(slug: string): string;
|
|
70
|
+
/**
|
|
71
|
+
* The combined document's HTML: the first page's `<head>` and shell, each
|
|
72
|
+
* page's `<main>` content in a section of its own, the head's stylesheets
|
|
73
|
+
* and scripts the union of every page's.
|
|
74
|
+
*/
|
|
75
|
+
declare function combinePages(input: CombinedDocumentInput): string;
|
|
76
|
+
/** One combined download: where it goes, its title, its pages and contents. */
|
|
77
|
+
interface CombinedDownload {
|
|
78
|
+
/** Below `downloads/`: `part/projectiles.html`, `course.html`. */
|
|
79
|
+
readonly path: string;
|
|
80
|
+
readonly title: string;
|
|
81
|
+
readonly slugs: readonly string[];
|
|
82
|
+
readonly contents: readonly ContentsEntry[];
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The part and course downloads of a course: each part is its overview page
|
|
86
|
+
* (when it has one), then its chapters; the course is the landing page, the
|
|
87
|
+
* front matter (the curriculum map, the figures and tables), every part, and
|
|
88
|
+
* the glossary last, as a book ends with its glossary.
|
|
89
|
+
*/
|
|
90
|
+
declare function combinedDownloads(manifest: Pick<Manifest, 'lessons' | 'pages' | 'parts' | 'about'>): CombinedDownload[];
|
|
91
|
+
/**
|
|
92
|
+
* Write every part's and the course's offline HTML file under `downloads/`
|
|
93
|
+
* (`lessonExports.standaloneHtml`), from Astro's finished output.
|
|
94
|
+
*/
|
|
95
|
+
declare function writeCombinedExports(directory: URL, manifest: Manifest): Promise<void>;
|
|
96
|
+
|
|
97
|
+
export { type CombinedDocumentInput, type CombinedDownload, type CombinedPage, type ContentsEntry, combinePages, combinedDownloads, pageKey, pageSectionId, writeCombinedExports };
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parse, serialize } from "parse5";
|
|
5
|
+
import { exportPageRoute } from "./export-pages.js";
|
|
6
|
+
import {
|
|
7
|
+
attribute,
|
|
8
|
+
childrenOf,
|
|
9
|
+
elements,
|
|
10
|
+
hasClass,
|
|
11
|
+
inferBasePrefix
|
|
12
|
+
} from "./html-tree.js";
|
|
13
|
+
import { buildStandaloneLessonHtml, pageSourceFile } from "./standalone-html.js";
|
|
14
|
+
const ID_ATTRIBUTES = [
|
|
15
|
+
"for",
|
|
16
|
+
"list",
|
|
17
|
+
"form",
|
|
18
|
+
"popovertarget",
|
|
19
|
+
"commandfor",
|
|
20
|
+
"aria-activedescendant",
|
|
21
|
+
"data-notation-layer-id"
|
|
22
|
+
];
|
|
23
|
+
const ID_LIST_ATTRIBUTES = [
|
|
24
|
+
"aria-controls",
|
|
25
|
+
"aria-describedby",
|
|
26
|
+
"aria-details",
|
|
27
|
+
"aria-errormessage",
|
|
28
|
+
"aria-flowto",
|
|
29
|
+
"aria-labelledby",
|
|
30
|
+
"aria-owns",
|
|
31
|
+
"headers"
|
|
32
|
+
];
|
|
33
|
+
const URL_ATTRIBUTES = ["href", "xlink:href", "data-notation-href"];
|
|
34
|
+
const SHARED_TEMPLATE_IDS = /* @__PURE__ */ new Set(["citation-panel", "abbreviation-panel", "float-ref-panel"]);
|
|
35
|
+
const COMBINED_STYLE = `
|
|
36
|
+
.explico-combined-page {
|
|
37
|
+
content-visibility: auto;
|
|
38
|
+
contain-intrinsic-size: auto 1200px;
|
|
39
|
+
}
|
|
40
|
+
.explico-combined-page + .explico-combined-page,
|
|
41
|
+
.explico-combined-contents + .explico-combined-page {
|
|
42
|
+
border-top: 1px solid var(--sl-color-hairline);
|
|
43
|
+
}
|
|
44
|
+
.explico-combined-contents ol {
|
|
45
|
+
list-style: none;
|
|
46
|
+
padding-inline-start: 0;
|
|
47
|
+
}
|
|
48
|
+
.explico-combined-contents ol ol {
|
|
49
|
+
padding-inline-start: 1.5rem;
|
|
50
|
+
}
|
|
51
|
+
.explico-combined-contents li {
|
|
52
|
+
margin-block: 0.25rem;
|
|
53
|
+
}
|
|
54
|
+
.explico-combined-contents li:has(> ol) {
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
}
|
|
57
|
+
.explico-combined-contents li li {
|
|
58
|
+
font-weight: normal;
|
|
59
|
+
}
|
|
60
|
+
.explico-combined-contents__number {
|
|
61
|
+
display: inline-block;
|
|
62
|
+
min-width: 3.5em;
|
|
63
|
+
font-variant-numeric: tabular-nums;
|
|
64
|
+
}
|
|
65
|
+
@media print {
|
|
66
|
+
.explico-combined-page {
|
|
67
|
+
content-visibility: visible;
|
|
68
|
+
}
|
|
69
|
+
.explico-combined-page + .explico-combined-page,
|
|
70
|
+
.explico-combined-contents + .explico-combined-page {
|
|
71
|
+
border-top: 0;
|
|
72
|
+
break-before: page;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
function pageKey(slug) {
|
|
77
|
+
return exportPageRoute(slug).replace(/\//gu, "-") || "home";
|
|
78
|
+
}
|
|
79
|
+
function pageSectionId(slug) {
|
|
80
|
+
return `page--${pageKey(slug)}`;
|
|
81
|
+
}
|
|
82
|
+
function setAttribute(node, name, value) {
|
|
83
|
+
const existing = node.attrs.find((entry) => entry.name === name);
|
|
84
|
+
if (existing) existing.value = value;
|
|
85
|
+
else node.attrs.push({ name, value });
|
|
86
|
+
}
|
|
87
|
+
function firstElement(root, tagName) {
|
|
88
|
+
const found = elements(root, (node) => node.tagName === tagName)[0];
|
|
89
|
+
if (!found) throw new Error(`Combined HTML source has no <${tagName}> element.`);
|
|
90
|
+
return found;
|
|
91
|
+
}
|
|
92
|
+
function detach(node) {
|
|
93
|
+
const parent = node.parentNode;
|
|
94
|
+
if (!parent) return;
|
|
95
|
+
const index = parent.childNodes.indexOf(node);
|
|
96
|
+
if (index !== -1) parent.childNodes.splice(index, 1);
|
|
97
|
+
node.parentNode = null;
|
|
98
|
+
}
|
|
99
|
+
function appendChild(parent, child) {
|
|
100
|
+
detach(child);
|
|
101
|
+
parent.childNodes.push(child);
|
|
102
|
+
child.parentNode = parent;
|
|
103
|
+
}
|
|
104
|
+
function element(tagName, attrs, children = []) {
|
|
105
|
+
const node = {
|
|
106
|
+
nodeName: tagName,
|
|
107
|
+
tagName,
|
|
108
|
+
attrs: Object.entries(attrs).map(([name, value]) => ({ name, value })),
|
|
109
|
+
namespaceURI: "http://www.w3.org/1999/xhtml",
|
|
110
|
+
parentNode: null,
|
|
111
|
+
childNodes: []
|
|
112
|
+
};
|
|
113
|
+
for (const child of children) appendChild(node, child);
|
|
114
|
+
return node;
|
|
115
|
+
}
|
|
116
|
+
function text(value) {
|
|
117
|
+
return { nodeName: "#text", value, parentNode: null };
|
|
118
|
+
}
|
|
119
|
+
function textOf(node) {
|
|
120
|
+
if ("value" in node && node.nodeName === "#text") return String(node.value);
|
|
121
|
+
return childrenOf(node).map((child) => textOf(child)).join("");
|
|
122
|
+
}
|
|
123
|
+
function routeOfPath(pathname, base) {
|
|
124
|
+
if (!pathname.startsWith(base)) return void 0;
|
|
125
|
+
let route;
|
|
126
|
+
try {
|
|
127
|
+
route = decodeURIComponent(pathname.slice(base.length));
|
|
128
|
+
} catch {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
return route.replace(/(?:^|\/)index\.html$/u, "").replace(/\.html$/u, "").replace(/^\/+|\/+$/gu, "");
|
|
132
|
+
}
|
|
133
|
+
function absolutizeUrls(page, base) {
|
|
134
|
+
const location = new URL(`https://explico.invalid${base}${page.route ? `${page.route}/` : ""}`);
|
|
135
|
+
const absolute = (value) => {
|
|
136
|
+
const trimmed = value.trim();
|
|
137
|
+
if (!trimmed || /^(?:[a-z][a-z\d+.-]*:|\/|#)/iu.test(trimmed)) return value;
|
|
138
|
+
const url = new URL(trimmed, location);
|
|
139
|
+
return `${url.pathname}${url.search}${url.hash}`;
|
|
140
|
+
};
|
|
141
|
+
for (const node of elements(page.document)) {
|
|
142
|
+
for (const name of ["href", "src", "poster", "data", "component-url", "renderer-url", "before-hydration-url"]) {
|
|
143
|
+
const value = attribute(node, name);
|
|
144
|
+
if (value) setAttribute(node, name, absolute(value));
|
|
145
|
+
}
|
|
146
|
+
for (const name of ["srcset", "imagesrcset"]) {
|
|
147
|
+
const value = attribute(node, name);
|
|
148
|
+
if (!value) continue;
|
|
149
|
+
setAttribute(
|
|
150
|
+
node,
|
|
151
|
+
name,
|
|
152
|
+
value.split(",").map((candidate) => {
|
|
153
|
+
const [url = "", ...rest] = candidate.trim().split(/\s+/u);
|
|
154
|
+
return [absolute(url), ...rest].join(" ");
|
|
155
|
+
}).join(", ")
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function rewriteCssFragments(value, ids) {
|
|
161
|
+
return value.replace(/url\(\s*(["']?)#([^"')\s]+)\1\s*\)/gu, (match, quote, id) => {
|
|
162
|
+
const next = ids.get(id);
|
|
163
|
+
return next ? `url(${quote}#${next}${quote})` : match;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function rewriteUrl(value, page, pages, base) {
|
|
167
|
+
const trimmed = value.trim();
|
|
168
|
+
if (trimmed.startsWith("#")) {
|
|
169
|
+
let id2;
|
|
170
|
+
try {
|
|
171
|
+
id2 = decodeURIComponent(trimmed.slice(1));
|
|
172
|
+
} catch {
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
const next2 = page.ids.get(id2);
|
|
176
|
+
return next2 ? `#${next2}` : value;
|
|
177
|
+
}
|
|
178
|
+
if (!trimmed.startsWith("/") || trimmed.startsWith("//")) return value;
|
|
179
|
+
const url = new URL(trimmed, "https://explico.invalid/");
|
|
180
|
+
const route = routeOfPath(url.pathname, base);
|
|
181
|
+
const target = route === void 0 ? void 0 : pages.get(route);
|
|
182
|
+
if (!target) return value;
|
|
183
|
+
if (!url.hash) return `#${pageSectionId(target.slug)}`;
|
|
184
|
+
let id;
|
|
185
|
+
try {
|
|
186
|
+
id = decodeURIComponent(url.hash.slice(1));
|
|
187
|
+
} catch {
|
|
188
|
+
return `#${pageSectionId(target.slug)}`;
|
|
189
|
+
}
|
|
190
|
+
const next = target.ids.get(id);
|
|
191
|
+
return next ? `#${next}` : `#${pageSectionId(target.slug)}`;
|
|
192
|
+
}
|
|
193
|
+
function rewriteReferences(page, pages, base) {
|
|
194
|
+
for (const node of elements(page.main)) {
|
|
195
|
+
for (const name of URL_ATTRIBUTES) {
|
|
196
|
+
const value = attribute(node, name);
|
|
197
|
+
if (value) setAttribute(node, name, rewriteUrl(value, page, pages, base));
|
|
198
|
+
}
|
|
199
|
+
for (const name of ID_ATTRIBUTES) {
|
|
200
|
+
const value = attribute(node, name);
|
|
201
|
+
const next = value ? page.ids.get(value) : void 0;
|
|
202
|
+
if (next) setAttribute(node, name, next);
|
|
203
|
+
}
|
|
204
|
+
for (const name of ID_LIST_ATTRIBUTES) {
|
|
205
|
+
const value = attribute(node, name);
|
|
206
|
+
if (!value) continue;
|
|
207
|
+
setAttribute(
|
|
208
|
+
node,
|
|
209
|
+
name,
|
|
210
|
+
value.split(/\s+/u).filter(Boolean).map((id) => page.ids.get(id) ?? id).join(" ")
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
for (const entry of node.attrs) {
|
|
214
|
+
if (entry.value.includes("url(")) entry.value = rewriteCssFragments(entry.value, page.ids);
|
|
215
|
+
}
|
|
216
|
+
if (node.tagName === "style") {
|
|
217
|
+
for (const child of childrenOf(node)) {
|
|
218
|
+
if ("value" in child) child.value = rewriteCssFragments(String(child.value), page.ids);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function islandContents(root) {
|
|
224
|
+
const inside = /* @__PURE__ */ new Set();
|
|
225
|
+
for (const island of elements(root, (node) => node.tagName === "astro-island")) {
|
|
226
|
+
for (const node of elements(island)) if (node !== island) inside.add(node);
|
|
227
|
+
}
|
|
228
|
+
return inside;
|
|
229
|
+
}
|
|
230
|
+
function collectIds(page, first) {
|
|
231
|
+
const ids = /* @__PURE__ */ new Map();
|
|
232
|
+
const islands = islandContents(page.main);
|
|
233
|
+
for (const node of elements(page.main)) {
|
|
234
|
+
const id = attribute(node, "id");
|
|
235
|
+
if (!id || ids.has(id) || islands.has(node)) continue;
|
|
236
|
+
if (node.tagName === "template" && SHARED_TEMPLATE_IDS.has(id)) continue;
|
|
237
|
+
ids.set(id, first && id === "_top" ? id : `${id}--${page.key}`);
|
|
238
|
+
}
|
|
239
|
+
return ids;
|
|
240
|
+
}
|
|
241
|
+
function uniqueIslandPrefixes(pages) {
|
|
242
|
+
const used = /* @__PURE__ */ new Set();
|
|
243
|
+
pages.forEach((page, index) => {
|
|
244
|
+
for (const island of elements(page.main, (node) => node.tagName === "astro-island")) {
|
|
245
|
+
const prefix = attribute(island, "prefix");
|
|
246
|
+
if (!prefix) continue;
|
|
247
|
+
if (!used.has(prefix)) {
|
|
248
|
+
used.add(prefix);
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const next = `${prefix}p${index}`;
|
|
252
|
+
used.add(next);
|
|
253
|
+
setAttribute(island, "prefix", next);
|
|
254
|
+
const escaped = prefix.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
255
|
+
const reactId = new RegExp(`(^|[^A-Za-z0-9])${escaped}R`, "gu");
|
|
256
|
+
for (const node of elements(island)) {
|
|
257
|
+
if (node === island) continue;
|
|
258
|
+
for (const entry of node.attrs) {
|
|
259
|
+
if (entry.name === "id" || ID_ATTRIBUTES.includes(entry.name) || ID_LIST_ATTRIBUTES.includes(entry.name)) {
|
|
260
|
+
entry.value = entry.value.replace(reactId, `$1${next}R`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function isExecutable(node) {
|
|
268
|
+
const type = (attribute(node, "type") ?? "").trim().toLowerCase();
|
|
269
|
+
return type === "" || type === "module" || type === "text/javascript" || type === "application/javascript";
|
|
270
|
+
}
|
|
271
|
+
function panelClasses(main) {
|
|
272
|
+
const panel = elements(main, (node) => hasClass(node, "content-panel"))[0];
|
|
273
|
+
const container = elements(main, (node) => hasClass(node, "sl-container"))[0];
|
|
274
|
+
return {
|
|
275
|
+
panel: attribute(panel ?? main, "class") ?? "content-panel",
|
|
276
|
+
container: attribute(container ?? main, "class") ?? "sl-container"
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function contentsList(entries, titles) {
|
|
280
|
+
return element(
|
|
281
|
+
"ol",
|
|
282
|
+
{},
|
|
283
|
+
entries.map(
|
|
284
|
+
(entry) => element("li", {}, [
|
|
285
|
+
...entry.number ? [element("span", { class: "explico-combined-contents__number" }, [text(entry.number)]), text(" ")] : [],
|
|
286
|
+
element("a", { href: `#${pageSectionId(entry.slug)}` }, [text(titles.get(entry.slug) ?? entry.slug)]),
|
|
287
|
+
...entry.children && entry.children.length > 0 ? [contentsList(entry.children, titles)] : []
|
|
288
|
+
])
|
|
289
|
+
)
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
function combinePages(input) {
|
|
293
|
+
if (input.pages.length === 0) throw new Error("A combined document needs at least one page.");
|
|
294
|
+
const documents = input.pages.map((page) => parse(page.html));
|
|
295
|
+
const shell = documents[0];
|
|
296
|
+
const base = inferBasePrefix(shell);
|
|
297
|
+
const parts = input.pages.map((page, index) => {
|
|
298
|
+
const document = documents[index];
|
|
299
|
+
const partial = {
|
|
300
|
+
slug: page.slug,
|
|
301
|
+
route: exportPageRoute(page.slug),
|
|
302
|
+
key: pageKey(page.slug),
|
|
303
|
+
document,
|
|
304
|
+
main: firstElement(document, "main")
|
|
305
|
+
};
|
|
306
|
+
return { ...partial, ids: collectIds(partial, index === 0) };
|
|
307
|
+
});
|
|
308
|
+
const titles = new Map(
|
|
309
|
+
parts.map((page) => {
|
|
310
|
+
const heading = elements(page.main, (node) => node.tagName === "h1")[0];
|
|
311
|
+
return [page.slug, heading ? textOf(heading).replace(/\s+/gu, " ").trim() : page.slug];
|
|
312
|
+
})
|
|
313
|
+
);
|
|
314
|
+
const byRoute = new Map(parts.map((page) => [page.route, page]));
|
|
315
|
+
uniqueIslandPrefixes(parts);
|
|
316
|
+
for (const page of parts) absolutizeUrls(page, base);
|
|
317
|
+
for (const page of parts) {
|
|
318
|
+
for (const node of elements(page.main)) {
|
|
319
|
+
const id = attribute(node, "id");
|
|
320
|
+
const next = id ? page.ids.get(id) : void 0;
|
|
321
|
+
if (next) setAttribute(node, "id", next);
|
|
322
|
+
}
|
|
323
|
+
rewriteReferences(page, byRoute, base);
|
|
324
|
+
}
|
|
325
|
+
const head = firstElement(shell, "head");
|
|
326
|
+
const headKeys = new Set(
|
|
327
|
+
elements(head).map((node) => `${node.tagName} ${attribute(node, "href") ?? attribute(node, "src") ?? ""}`)
|
|
328
|
+
);
|
|
329
|
+
for (const page of parts.slice(1)) {
|
|
330
|
+
for (const node of elements(firstElement(page.document, "head"))) {
|
|
331
|
+
const isStylesheet = node.tagName === "link" && (attribute(node, "rel") ?? "").split(/\s+/u).includes("stylesheet");
|
|
332
|
+
const isModule = node.tagName === "script" && !!attribute(node, "src");
|
|
333
|
+
if (!isStylesheet && !isModule) continue;
|
|
334
|
+
const key = `${node.tagName} ${attribute(node, "href") ?? attribute(node, "src") ?? ""}`;
|
|
335
|
+
if (headKeys.has(key)) continue;
|
|
336
|
+
headKeys.add(key);
|
|
337
|
+
appendChild(head, node);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const title = elements(head, (node) => node.tagName === "title")[0];
|
|
341
|
+
if (title) {
|
|
342
|
+
title.childNodes.splice(0, title.childNodes.length);
|
|
343
|
+
appendChild(title, text(input.title));
|
|
344
|
+
}
|
|
345
|
+
appendChild(head, element("style", { "data-explico-combined-style": "" }, [text(COMBINED_STYLE)]));
|
|
346
|
+
const main = firstElement(shell, "main");
|
|
347
|
+
const classes = panelClasses(main);
|
|
348
|
+
const seen = /* @__PURE__ */ new Set();
|
|
349
|
+
const sections = parts.map((page) => {
|
|
350
|
+
const heading = page.ids.get("_top");
|
|
351
|
+
const section = element("section", {
|
|
352
|
+
class: "explico-combined-page",
|
|
353
|
+
id: pageSectionId(page.slug),
|
|
354
|
+
"data-explico-page": page.slug,
|
|
355
|
+
...heading ? { "aria-labelledby": heading } : {}
|
|
356
|
+
});
|
|
357
|
+
for (const child of [...page.main.childNodes]) appendChild(section, child);
|
|
358
|
+
for (const node of elements(section)) {
|
|
359
|
+
const id = attribute(node, "id");
|
|
360
|
+
let key;
|
|
361
|
+
if (node.tagName === "script" && !id && isExecutable(node)) {
|
|
362
|
+
key = `script ${attribute(node, "src") ?? textOf(node)}`;
|
|
363
|
+
} else if (node.tagName === "style" && !id) {
|
|
364
|
+
key = `style ${textOf(node)}`;
|
|
365
|
+
} else if (node.tagName === "template" && id && SHARED_TEMPLATE_IDS.has(id)) {
|
|
366
|
+
key = `template ${id}`;
|
|
367
|
+
}
|
|
368
|
+
if (key === void 0) continue;
|
|
369
|
+
if (seen.has(key)) detach(node);
|
|
370
|
+
else seen.add(key);
|
|
371
|
+
}
|
|
372
|
+
return section;
|
|
373
|
+
});
|
|
374
|
+
main.childNodes.splice(0, main.childNodes.length);
|
|
375
|
+
sections.forEach((section, index) => {
|
|
376
|
+
appendChild(main, section);
|
|
377
|
+
if (index !== 0 || !input.contents || input.contents.length === 0) return;
|
|
378
|
+
appendChild(
|
|
379
|
+
main,
|
|
380
|
+
element("div", { class: `${classes.panel} explico-combined-contents` }, [
|
|
381
|
+
element("div", { class: classes.container }, [
|
|
382
|
+
element("nav", { class: "sl-markdown-content", "aria-labelledby": "explico-contents" }, [
|
|
383
|
+
element("h2", { id: "explico-contents" }, [text("Contents")]),
|
|
384
|
+
contentsList(input.contents, titles)
|
|
385
|
+
])
|
|
386
|
+
])
|
|
387
|
+
])
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
setAttribute(main, "data-explico-combined", "");
|
|
391
|
+
return serialize(shell);
|
|
392
|
+
}
|
|
393
|
+
function combinedDownloads(manifest) {
|
|
394
|
+
const pageSlugs = new Set(manifest.pages.map((page) => page.slug));
|
|
395
|
+
const lessonsById = new Map(manifest.lessons.map((lesson) => [lesson.id, lesson]));
|
|
396
|
+
const partPages = manifest.parts.map((part) => {
|
|
397
|
+
const overview = pageSlugs.has(`${part.dir}/index`) ? `${part.dir}/index` : void 0;
|
|
398
|
+
const lessons = part.lessons.flatMap((id) => {
|
|
399
|
+
const lesson = lessonsById.get(id);
|
|
400
|
+
return lesson ? [lesson] : [];
|
|
401
|
+
});
|
|
402
|
+
return { part, overview, lessons };
|
|
403
|
+
});
|
|
404
|
+
const downloads = partPages.flatMap(({ part, overview, lessons }) => {
|
|
405
|
+
if (lessons.length === 0) return [];
|
|
406
|
+
return [
|
|
407
|
+
{
|
|
408
|
+
path: `part/${part.dir}.html`,
|
|
409
|
+
title: part.label,
|
|
410
|
+
slugs: [...overview ? [overview] : [], ...lessons.map((lesson) => lesson.slug)],
|
|
411
|
+
contents: lessons.map((lesson) => ({ slug: lesson.slug, number: `${lesson.part}.${lesson.chapter}` }))
|
|
412
|
+
}
|
|
413
|
+
];
|
|
414
|
+
});
|
|
415
|
+
const partSlugs = new Set(partPages.flatMap(({ overview }) => overview ? [overview] : []));
|
|
416
|
+
const front = ["curriculum-map", "figures-and-tables"].filter((slug) => pageSlugs.has(slug));
|
|
417
|
+
const back = manifest.pages.map((page) => page.slug).filter((slug) => slug !== "index" && slug !== "glossary" && !partSlugs.has(slug) && !front.includes(slug)).concat(pageSlugs.has("glossary") ? ["glossary"] : []);
|
|
418
|
+
const parts = partPages.filter(({ lessons }) => lessons.length > 0);
|
|
419
|
+
const slugs = [
|
|
420
|
+
...pageSlugs.has("index") ? ["index"] : [],
|
|
421
|
+
...front,
|
|
422
|
+
...parts.flatMap(({ overview, lessons }) => [...overview ? [overview] : [], ...lessons.map((lesson) => lesson.slug)]),
|
|
423
|
+
...back
|
|
424
|
+
];
|
|
425
|
+
if (slugs.length > 0 && parts.length > 0) {
|
|
426
|
+
downloads.push({
|
|
427
|
+
path: "course.html",
|
|
428
|
+
title: manifest.about.material.name,
|
|
429
|
+
slugs,
|
|
430
|
+
contents: [
|
|
431
|
+
...front.map((slug) => ({ slug })),
|
|
432
|
+
...parts.map(({ part, overview, lessons }) => ({
|
|
433
|
+
slug: overview ?? lessons[0].slug,
|
|
434
|
+
number: `Part ${part.ordinal}`,
|
|
435
|
+
children: lessons.map((lesson) => ({ slug: lesson.slug, number: `${lesson.part}.${lesson.chapter}` }))
|
|
436
|
+
})),
|
|
437
|
+
...back.map((slug) => ({ slug }))
|
|
438
|
+
]
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
return downloads;
|
|
442
|
+
}
|
|
443
|
+
async function writeCombinedExports(directory, manifest) {
|
|
444
|
+
if (!manifest.lessonExports.standaloneHtml) return;
|
|
445
|
+
const outputRoot = fileURLToPath(directory);
|
|
446
|
+
const pending = [];
|
|
447
|
+
const sources = /* @__PURE__ */ new Map();
|
|
448
|
+
const source = (slug) => {
|
|
449
|
+
let read = sources.get(slug);
|
|
450
|
+
if (!read) {
|
|
451
|
+
read = pageSourceFile(outputRoot, slug).then(async (file) => ({ file, html: await readFile(file, "utf8") }));
|
|
452
|
+
sources.set(slug, read);
|
|
453
|
+
}
|
|
454
|
+
return read;
|
|
455
|
+
};
|
|
456
|
+
for (const download of combinedDownloads(manifest)) {
|
|
457
|
+
const pages = await Promise.all(download.slugs.map(async (slug) => ({ slug, ...await source(slug) })));
|
|
458
|
+
const html = combinePages({
|
|
459
|
+
title: download.title,
|
|
460
|
+
pages: pages.map(({ slug, html: page }) => ({ slug, html: page })),
|
|
461
|
+
contents: download.contents
|
|
462
|
+
});
|
|
463
|
+
const target = join(outputRoot, "downloads", download.path);
|
|
464
|
+
pending.push({
|
|
465
|
+
target,
|
|
466
|
+
temporary: `${target}.tmp-${process.pid}`,
|
|
467
|
+
html: await buildStandaloneLessonHtml({
|
|
468
|
+
html,
|
|
469
|
+
sourceFile: pages[0].file,
|
|
470
|
+
outputRoot,
|
|
471
|
+
lessonSlug: pages[0].slug,
|
|
472
|
+
canonicalBaseUrl: manifest.lessonExports.canonicalBaseUrl
|
|
473
|
+
})
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
for (const file of pending) {
|
|
478
|
+
await mkdir(dirname(file.target), { recursive: true });
|
|
479
|
+
await writeFile(file.temporary, file.html, "utf8");
|
|
480
|
+
}
|
|
481
|
+
for (const file of pending) await rename(file.temporary, file.target);
|
|
482
|
+
} finally {
|
|
483
|
+
await Promise.all(pending.map((file) => rm(file.temporary, { force: true })));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
export {
|
|
487
|
+
combinePages,
|
|
488
|
+
combinedDownloads,
|
|
489
|
+
pageKey,
|
|
490
|
+
pageSectionId,
|
|
491
|
+
writeCombinedExports
|
|
492
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `downloads/index.json`: every file the exports wrote, by its path below
|
|
3
|
+
* `downloads/`, with its size in bytes. The Download menu
|
|
4
|
+
* (`LessonExportActions.astro`) reads it to show sizes and to hide a download
|
|
5
|
+
* the build did not write — a PDF on a machine without TeX, say.
|
|
6
|
+
*/
|
|
7
|
+
declare function writeDownloadIndex(directory: URL): Promise<void>;
|
|
8
|
+
|
|
9
|
+
export { writeDownloadIndex };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
async function writeDownloadIndex(directory) {
|
|
5
|
+
const root = join(fileURLToPath(directory), "downloads");
|
|
6
|
+
const files = [];
|
|
7
|
+
const walk = async (folder) => {
|
|
8
|
+
let entries;
|
|
9
|
+
try {
|
|
10
|
+
entries = await readdir(folder, { withFileTypes: true });
|
|
11
|
+
} catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
const path = join(folder, entry.name);
|
|
16
|
+
if (entry.isDirectory()) await walk(path);
|
|
17
|
+
else if (entry.isFile() && !(folder === root && entry.name === "index.json")) {
|
|
18
|
+
files.push([relative(root, path).split(sep).join("/"), (await stat(path)).size]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
await walk(root);
|
|
23
|
+
if (files.length === 0) return;
|
|
24
|
+
files.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
25
|
+
await writeFile(
|
|
26
|
+
join(root, "index.json"),
|
|
27
|
+
`${JSON.stringify({ files: Object.fromEntries(files) }, null, 2)}
|
|
28
|
+
`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
writeDownloadIndex
|
|
33
|
+
};
|