explico 0.12.0 → 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.
Files changed (279) hide show
  1. package/CHANGELOG.md +1122 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +4 -3
  4. package/dist/compiler/astro.js +45 -15
  5. package/dist/compiler/cli.d.ts +26 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +9 -1
  8. package/dist/compiler/collections.js +55 -16
  9. package/dist/compiler/content-schema.d.ts +31 -0
  10. package/dist/compiler/content-schema.js +82 -0
  11. package/dist/compiler/content-schemas.d.ts +422 -0
  12. package/dist/compiler/content-schemas.js +229 -0
  13. package/dist/compiler/course-config.d.ts +1 -1
  14. package/dist/compiler/course-config.js +30 -2
  15. package/dist/compiler/exports/combined-html.d.ts +97 -0
  16. package/dist/compiler/exports/combined-html.js +492 -0
  17. package/dist/compiler/exports/download-index.d.ts +9 -0
  18. package/dist/compiler/exports/download-index.js +33 -0
  19. package/dist/compiler/exports/export-chapters.d.ts +98 -0
  20. package/dist/compiler/exports/export-chapters.js +164 -0
  21. package/dist/compiler/exports/export-pages.d.ts +48 -0
  22. package/dist/compiler/exports/export-pages.js +22 -0
  23. package/dist/compiler/exports/html-tree.d.ts +22 -0
  24. package/dist/compiler/exports/html-tree.js +47 -0
  25. package/dist/compiler/exports/latex/capture.d.ts +106 -0
  26. package/dist/compiler/exports/latex/capture.js +435 -0
  27. package/dist/compiler/exports/latex/compile.d.ts +73 -0
  28. package/dist/compiler/exports/latex/compile.js +115 -0
  29. package/dist/compiler/exports/latex/document.d.ts +102 -0
  30. package/dist/compiler/exports/latex/document.js +349 -0
  31. package/dist/compiler/exports/latex/explico.cls +415 -0
  32. package/dist/compiler/exports/latex/numbering.d.ts +18 -0
  33. package/dist/compiler/exports/latex/numbering.js +57 -0
  34. package/dist/compiler/exports/latex/serialize.d.ts +112 -0
  35. package/dist/compiler/exports/latex/serialize.js +606 -0
  36. package/dist/compiler/exports/latex-export.d.ts +44 -0
  37. package/dist/compiler/exports/latex-export.js +169 -0
  38. package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
  39. package/dist/compiler/exports/markdown/serialize.js +527 -0
  40. package/dist/compiler/exports/markdown-export.d.ts +39 -0
  41. package/dist/compiler/exports/markdown-export.js +240 -0
  42. package/dist/compiler/exports/standalone-html.d.ts +14 -5
  43. package/dist/compiler/exports/standalone-html.js +23 -51
  44. package/dist/compiler/exports/static-tree.d.ts +253 -0
  45. package/dist/compiler/exports/static-tree.js +625 -0
  46. package/dist/compiler/exports/zip.d.ts +16 -0
  47. package/dist/compiler/exports/zip.js +72 -0
  48. package/dist/compiler/lesson-derivation.d.ts +8 -2
  49. package/dist/compiler/lesson-derivation.js +6 -7
  50. package/dist/compiler/manifest.d.ts +87 -6
  51. package/dist/compiler/manifest.js +174 -31
  52. package/dist/compiler/render-diagnostics.d.ts +65 -0
  53. package/dist/compiler/render-diagnostics.js +56 -0
  54. package/dist/compiler/resolution-report.d.ts +2 -1
  55. package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
  56. package/dist/curriculum/validation.d.ts +9 -3
  57. package/dist/curriculum/validation.js +35 -17
  58. package/dist/reference/base-abbreviations.d.mts +2 -2
  59. package/dist/reference/base-abbreviations.mjs +2 -2
  60. package/dist/reference/base-operators.d.mts +2 -2
  61. package/dist/reference/base-operators.mjs +2 -2
  62. package/dist/reference/base-roman-labels.d.mts +4 -3
  63. package/dist/reference/base-roman-labels.mjs +4 -3
  64. package/dist/reference/citation-format.d.mts +35 -0
  65. package/dist/reference/citation-format.mjs +74 -0
  66. package/dist/reference/consistency.d.ts +5 -9
  67. package/dist/reference/consistency.js +34 -38
  68. package/dist/reference/curriculum-alignment.js +3 -24
  69. package/dist/reference/derived-entries.d.ts +45 -0
  70. package/dist/reference/derived-entries.js +50 -0
  71. package/dist/reference/derived-notation.d.mts +125 -0
  72. package/dist/reference/derived-notation.mjs +121 -0
  73. package/dist/reference/diagram-fit.d.ts +45 -0
  74. package/dist/reference/diagram-fit.js +54 -0
  75. package/dist/reference/diagram-layout.d.ts +59 -10
  76. package/dist/reference/diagram-layout.js +85 -24
  77. package/dist/reference/diagrams-validate.d.ts +1 -1
  78. package/dist/reference/equations-validate.d.ts +1 -1
  79. package/dist/reference/equations-validate.js +26 -1
  80. package/dist/reference/equations.d.mts +27 -0
  81. package/dist/reference/equations.mjs +120 -22
  82. package/dist/reference/examples-validate.d.ts +1 -1
  83. package/dist/reference/figure-fit.d.ts +70 -0
  84. package/dist/reference/figure-fit.js +60 -0
  85. package/dist/reference/figure-pictures.d.ts +27 -0
  86. package/dist/reference/figure-pictures.js +49 -0
  87. package/dist/reference/figures-validate.d.ts +1 -1
  88. package/dist/reference/gate-abbreviations.d.ts +1 -1
  89. package/dist/reference/gate-abbreviations.js +1 -1
  90. package/dist/reference/gate-course-rules.d.ts +4 -3
  91. package/dist/reference/gate-course-rules.js +9 -4
  92. package/dist/reference/gate-heading-length.d.ts +2 -0
  93. package/dist/reference/gate-heading-length.js +7 -5
  94. package/dist/reference/gate-heading-structure.js +26 -0
  95. package/dist/reference/gate-indices.d.ts +35 -0
  96. package/dist/reference/gate-indices.js +299 -0
  97. package/dist/reference/gate-math.d.ts +32 -2
  98. package/dist/reference/gate-math.js +68 -12
  99. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  100. package/dist/reference/gate-mdx-expression.js +29 -0
  101. package/dist/reference/gate-part-pages.d.ts +1 -1
  102. package/dist/reference/gate-prop-text.d.ts +28 -0
  103. package/dist/reference/gate-prop-text.js +106 -0
  104. package/dist/reference/gate-prose-notation.d.ts +3 -1
  105. package/dist/reference/gate-prose-notation.js +4 -2
  106. package/dist/reference/gate-retired-keys.d.ts +35 -0
  107. package/dist/reference/gate-retired-keys.js +58 -0
  108. package/dist/reference/gloss.d.ts +2 -0
  109. package/dist/reference/katex-measure.d.ts +17 -0
  110. package/dist/reference/katex-measure.js +223 -0
  111. package/dist/reference/keyed-float-validate.js +12 -0
  112. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  113. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  114. package/dist/reference/lab-math-scope.d.ts +1 -0
  115. package/dist/reference/lab-math-scope.js +13 -2
  116. package/dist/reference/math-glyphs.d.mts +94 -2
  117. package/dist/reference/math-glyphs.mjs +851 -46
  118. package/dist/reference/mixed-content.d.ts +8 -3
  119. package/dist/reference/mixed-content.js +4 -2
  120. package/dist/reference/page-notation.d.ts +34 -4
  121. package/dist/reference/page-notation.js +54 -2
  122. package/dist/reference/references.d.ts +11 -1
  123. package/dist/reference/references.js +6 -0
  124. package/dist/reference/registry.js +104 -10
  125. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  126. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  127. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  128. package/dist/reference/remark-citation.mjs +0 -0
  129. package/dist/reference/remark-notation.mjs +84 -20
  130. package/dist/reference/render-lab-math.d.ts +2 -0
  131. package/dist/reference/render-lab-math.js +17 -5
  132. package/dist/reference/sections-validate.d.ts +28 -3
  133. package/dist/reference/sections-validate.js +31 -4
  134. package/dist/reference/sections.d.mts +24 -6
  135. package/dist/reference/sections.mjs +92 -17
  136. package/dist/reference/tables-validate.d.ts +1 -1
  137. package/dist/reference/tables-validate.js +24 -1
  138. package/dist/reference/text-measure.d.ts +10 -0
  139. package/dist/reference/text-measure.js +41 -0
  140. package/dist/reference/text-widths.d.ts +10 -0
  141. package/dist/reference/text-widths.js +1238 -0
  142. package/dist/reference/types.d.ts +64 -8
  143. package/dist/rules/catalog.js +191 -30
  144. package/dist/rules/in-effect.d.ts +1 -1
  145. package/dist/rules/rules.schema.json +79 -24
  146. package/dist/rules/settings.d.ts +1 -1
  147. package/dist/rules/settings.js +7 -0
  148. package/dist/session/PreferenceStore.d.ts +18 -1
  149. package/dist/session/PreferenceStore.js +29 -1
  150. package/docs/authoring/README.md +32 -21
  151. package/docs/authoring/agents.md +38 -7
  152. package/docs/authoring/components.md +111 -6
  153. package/docs/authoring/migrating.md +663 -0
  154. package/docs/authoring/notation.md +281 -29
  155. package/docs/authoring/retired.md +1 -0
  156. package/docs/authoring/structure-rules.md +10 -41
  157. package/docs/authoring/structure.md +53 -2
  158. package/docs/authoring/visualizations.md +144 -18
  159. package/package.json +12 -7
  160. package/src/compiler/astro.ts +57 -17
  161. package/src/compiler/cli.ts +143 -35
  162. package/src/compiler/collections.ts +92 -19
  163. package/src/compiler/content-schema.ts +108 -0
  164. package/src/compiler/content-schemas.ts +391 -0
  165. package/src/compiler/course-config.ts +86 -15
  166. package/src/compiler/exports/combined-html.ts +669 -0
  167. package/src/compiler/exports/download-index.ts +36 -0
  168. package/src/compiler/exports/export-chapters.ts +233 -0
  169. package/src/compiler/exports/export-pages.ts +44 -0
  170. package/src/compiler/exports/html-tree.ts +62 -0
  171. package/src/compiler/exports/latex/capture.ts +651 -0
  172. package/src/compiler/exports/latex/compile.ts +168 -0
  173. package/src/compiler/exports/latex/document.ts +479 -0
  174. package/src/compiler/exports/latex/explico.cls +415 -0
  175. package/src/compiler/exports/latex/numbering.ts +74 -0
  176. package/src/compiler/exports/latex/serialize.ts +787 -0
  177. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  178. package/src/compiler/exports/latex-export.ts +255 -0
  179. package/src/compiler/exports/markdown/serialize.ts +632 -0
  180. package/src/compiler/exports/markdown-export.ts +329 -0
  181. package/src/compiler/exports/standalone-html.ts +39 -70
  182. package/src/compiler/exports/static-tree.ts +944 -0
  183. package/src/compiler/exports/zip.ts +88 -0
  184. package/src/compiler/lesson-derivation.ts +13 -8
  185. package/src/compiler/manifest.ts +299 -28
  186. package/src/compiler/render-diagnostics.ts +111 -0
  187. package/src/components/HoverPanel.astro +204 -6
  188. package/src/components/PartOverview.astro +12 -9
  189. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  190. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  191. package/src/components/assessments/AssessmentSet.astro +14 -1
  192. package/src/components/block-heading.ts +38 -0
  193. package/src/components/card-arrow.ts +18 -4
  194. package/src/components/card-carry.ts +406 -0
  195. package/src/components/card-drag.ts +1 -30
  196. package/src/components/card-fetch.ts +87 -0
  197. package/src/components/card-manager.ts +1222 -38
  198. package/src/components/card-memory.ts +5 -1
  199. package/src/components/card-placement.ts +211 -13
  200. package/src/components/card-side.ts +281 -31
  201. package/src/components/citation/CitationLayer.astro +45 -40
  202. package/src/components/diagram/Diagram.astro +123 -44
  203. package/src/components/figure/Figure.astro +11 -0
  204. package/src/components/figure/FloatCaption.astro +4 -17
  205. package/src/components/float-toolbar.ts +274 -0
  206. package/src/components/memo/Memo.astro +21 -10
  207. package/src/components/memo/ShortNote.astro +4 -0
  208. package/src/components/notation/NotationGlossary.astro +22 -17
  209. package/src/components/notation/NotationLayer.astro +300 -79
  210. package/src/components/notation/NotationMath.astro +8 -1
  211. package/src/components/notation/render-notation-math.ts +51 -5
  212. package/src/components/readable-text.ts +25 -0
  213. package/src/components/reference/FloatRefLayer.astro +162 -89
  214. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  215. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  216. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  217. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  218. package/src/components/starlight/LessonExportActions.astro +384 -310
  219. package/src/components/starlight/LessonFooter.astro +47 -9
  220. package/src/components/starlight/PrintController.astro +28 -0
  221. package/src/components/starlight/TextSizeControl.astro +137 -0
  222. package/src/components/starlight/print-controller.ts +380 -0
  223. package/src/components/starlight/section-summary.ts +13 -5
  224. package/src/components/table/Table.astro +13 -21
  225. package/src/components/table/table-truncation.ts +15 -0
  226. package/src/content-config.ts +20 -312
  227. package/src/curriculum/validation.ts +72 -24
  228. package/src/reference/base-abbreviations.mjs +2 -2
  229. package/src/reference/base-operators.mjs +2 -2
  230. package/src/reference/base-roman-labels.mjs +4 -3
  231. package/src/reference/citation-format.mjs +74 -0
  232. package/src/reference/consistency.ts +47 -61
  233. package/src/reference/curriculum-alignment.ts +4 -41
  234. package/src/reference/derived-entries.ts +68 -0
  235. package/src/reference/derived-notation.mjs +121 -0
  236. package/src/reference/diagram-fit.ts +104 -0
  237. package/src/reference/diagram-layout.ts +182 -51
  238. package/src/reference/diagrams-validate.ts +2 -1
  239. package/src/reference/equations-validate.ts +41 -2
  240. package/src/reference/equations.mjs +120 -22
  241. package/src/reference/examples-validate.ts +1 -0
  242. package/src/reference/figure-fit.ts +139 -0
  243. package/src/reference/figure-pictures.ts +96 -0
  244. package/src/reference/figures-validate.ts +2 -1
  245. package/src/reference/gate-abbreviations.ts +1 -1
  246. package/src/reference/gate-course-rules.ts +22 -7
  247. package/src/reference/gate-heading-length.ts +25 -7
  248. package/src/reference/gate-heading-structure.ts +40 -1
  249. package/src/reference/gate-indices.ts +470 -0
  250. package/src/reference/gate-math.ts +102 -13
  251. package/src/reference/gate-mdx-expression.ts +45 -0
  252. package/src/reference/gate-prop-text.ts +145 -0
  253. package/src/reference/gate-prose-notation.ts +19 -5
  254. package/src/reference/gate-retired-keys.ts +101 -0
  255. package/src/reference/gloss.ts +2 -0
  256. package/src/reference/katex-measure.ts +384 -0
  257. package/src/reference/keyed-float-validate.ts +22 -0
  258. package/src/reference/keyed-jsx-floats.mjs +11 -14
  259. package/src/reference/lab-math-scope.ts +15 -1
  260. package/src/reference/math-glyphs.mjs +851 -46
  261. package/src/reference/mixed-content.ts +11 -2
  262. package/src/reference/page-notation.ts +100 -3
  263. package/src/reference/references.ts +14 -0
  264. package/src/reference/registry.ts +154 -8
  265. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  266. package/src/reference/rehype-heading-anchors.mjs +11 -1
  267. package/src/reference/remark-citation.mjs +0 -0
  268. package/src/reference/remark-notation.mjs +84 -20
  269. package/src/reference/render-lab-math.ts +29 -5
  270. package/src/reference/sections-validate.ts +67 -3
  271. package/src/reference/sections.mjs +92 -17
  272. package/src/reference/tables-validate.ts +40 -2
  273. package/src/reference/text-measure.ts +63 -0
  274. package/src/reference/text-widths.ts +133 -0
  275. package/src/reference/types.ts +75 -8
  276. package/src/rules/catalog.ts +205 -29
  277. package/src/rules/settings.ts +10 -0
  278. package/src/session/PreferenceStore.ts +53 -0
  279. package/src/styles/global.css +291 -75
@@ -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 { standaloneHtml, print, canonicalBaseUrl: parsed.toString() };
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
+ };