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.
Files changed (298) hide show
  1. package/CHANGELOG.md +1292 -1
  2. package/README.md +91 -13
  3. package/dist/compiler/astro.d.ts +11 -4
  4. package/dist/compiler/astro.js +48 -15
  5. package/dist/compiler/cli.d.ts +27 -12
  6. package/dist/compiler/cli.js +96 -25
  7. package/dist/compiler/collections.d.ts +33 -2
  8. package/dist/compiler/collections.js +91 -19
  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 +15 -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 +115 -8
  51. package/dist/compiler/manifest.js +221 -39
  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 +3 -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 +8 -2
  61. package/dist/reference/base-operators.mjs +8 -10
  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/figure-symbols.d.ts +32 -0
  88. package/dist/reference/figure-symbols.js +55 -0
  89. package/dist/reference/figures-validate.d.ts +1 -1
  90. package/dist/reference/gate-abbreviations.d.ts +1 -1
  91. package/dist/reference/gate-abbreviations.js +1 -1
  92. package/dist/reference/gate-course-rules.d.ts +4 -3
  93. package/dist/reference/gate-course-rules.js +9 -4
  94. package/dist/reference/gate-heading-length.d.ts +2 -0
  95. package/dist/reference/gate-heading-length.js +7 -5
  96. package/dist/reference/gate-heading-structure.js +26 -0
  97. package/dist/reference/gate-indices.d.ts +35 -0
  98. package/dist/reference/gate-indices.js +299 -0
  99. package/dist/reference/gate-math.d.ts +32 -2
  100. package/dist/reference/gate-math.js +68 -12
  101. package/dist/reference/gate-mdx-expression.d.ts +17 -0
  102. package/dist/reference/gate-mdx-expression.js +29 -0
  103. package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
  104. package/dist/reference/gate-non-chapter-pages.js +156 -0
  105. package/dist/reference/gate-part-pages.d.ts +4 -2
  106. package/dist/reference/gate-part-pages.js +2 -1
  107. package/dist/reference/gate-prop-text.d.ts +28 -0
  108. package/dist/reference/gate-prop-text.js +106 -0
  109. package/dist/reference/gate-prose-notation.d.ts +3 -1
  110. package/dist/reference/gate-prose-notation.js +4 -2
  111. package/dist/reference/gate-retired-keys.d.ts +35 -0
  112. package/dist/reference/gate-retired-keys.js +58 -0
  113. package/dist/reference/gloss.d.ts +2 -0
  114. package/dist/reference/katex-measure.d.ts +17 -0
  115. package/dist/reference/katex-measure.js +223 -0
  116. package/dist/reference/keyed-float-validate.js +12 -0
  117. package/dist/reference/keyed-jsx-floats.d.mts +2 -2
  118. package/dist/reference/keyed-jsx-floats.mjs +11 -14
  119. package/dist/reference/lab-katex-validate.d.ts +43 -0
  120. package/dist/reference/lab-katex-validate.js +96 -0
  121. package/dist/reference/lab-math-scope.d.ts +1 -0
  122. package/dist/reference/lab-math-scope.js +13 -2
  123. package/dist/reference/lint-ignore.js +1 -1
  124. package/dist/reference/math-glyphs.d.mts +94 -2
  125. package/dist/reference/math-glyphs.mjs +851 -46
  126. package/dist/reference/mixed-content.d.ts +8 -3
  127. package/dist/reference/mixed-content.js +4 -2
  128. package/dist/reference/page-notation.d.ts +39 -7
  129. package/dist/reference/page-notation.js +62 -10
  130. package/dist/reference/raw-floats.mjs +37 -13
  131. package/dist/reference/references.d.ts +11 -1
  132. package/dist/reference/references.js +6 -0
  133. package/dist/reference/registry.js +115 -11
  134. package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
  135. package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
  136. package/dist/reference/rehype-heading-anchors.mjs +11 -1
  137. package/dist/reference/remark-citation.mjs +0 -0
  138. package/dist/reference/remark-notation.mjs +275 -23
  139. package/dist/reference/render-lab-math.d.ts +13 -2
  140. package/dist/reference/render-lab-math.js +18 -6
  141. package/dist/reference/sections-validate.d.ts +28 -3
  142. package/dist/reference/sections-validate.js +31 -4
  143. package/dist/reference/sections.d.mts +24 -6
  144. package/dist/reference/sections.mjs +92 -17
  145. package/dist/reference/tables-validate.d.ts +1 -1
  146. package/dist/reference/tables-validate.js +24 -1
  147. package/dist/reference/text-measure.d.ts +10 -0
  148. package/dist/reference/text-measure.js +41 -0
  149. package/dist/reference/text-widths.d.ts +10 -0
  150. package/dist/reference/text-widths.js +1238 -0
  151. package/dist/reference/types.d.ts +64 -8
  152. package/dist/rules/catalog.js +223 -32
  153. package/dist/rules/in-effect.d.ts +1 -1
  154. package/dist/rules/rules.schema.json +100 -27
  155. package/dist/rules/settings.d.ts +1 -1
  156. package/dist/rules/settings.js +7 -0
  157. package/dist/session/PreferenceStore.d.ts +18 -1
  158. package/dist/session/PreferenceStore.js +29 -1
  159. package/docs/authoring/README.md +33 -21
  160. package/docs/authoring/agents.md +38 -7
  161. package/docs/authoring/components.md +115 -10
  162. package/docs/authoring/migrating.md +878 -0
  163. package/docs/authoring/notation.md +285 -30
  164. package/docs/authoring/retired.md +1 -0
  165. package/docs/authoring/structure-rules.md +84 -45
  166. package/docs/authoring/structure.md +67 -2
  167. package/docs/authoring/visualizations.md +167 -25
  168. package/package.json +12 -7
  169. package/src/compiler/astro.ts +72 -21
  170. package/src/compiler/cli.ts +143 -35
  171. package/src/compiler/collections.ts +158 -27
  172. package/src/compiler/content-schema.ts +108 -0
  173. package/src/compiler/content-schemas.ts +391 -0
  174. package/src/compiler/course-config.ts +86 -15
  175. package/src/compiler/exports/combined-html.ts +669 -0
  176. package/src/compiler/exports/download-index.ts +36 -0
  177. package/src/compiler/exports/export-chapters.ts +233 -0
  178. package/src/compiler/exports/export-pages.ts +44 -0
  179. package/src/compiler/exports/html-tree.ts +62 -0
  180. package/src/compiler/exports/latex/capture.ts +651 -0
  181. package/src/compiler/exports/latex/compile.ts +168 -0
  182. package/src/compiler/exports/latex/document.ts +479 -0
  183. package/src/compiler/exports/latex/explico.cls +415 -0
  184. package/src/compiler/exports/latex/numbering.ts +74 -0
  185. package/src/compiler/exports/latex/serialize.ts +787 -0
  186. package/src/compiler/exports/latex/texlive-packages.txt +76 -0
  187. package/src/compiler/exports/latex-export.ts +255 -0
  188. package/src/compiler/exports/markdown/serialize.ts +632 -0
  189. package/src/compiler/exports/markdown-export.ts +329 -0
  190. package/src/compiler/exports/standalone-html.ts +39 -70
  191. package/src/compiler/exports/static-tree.ts +944 -0
  192. package/src/compiler/exports/zip.ts +88 -0
  193. package/src/compiler/lesson-derivation.ts +13 -8
  194. package/src/compiler/manifest.ts +387 -40
  195. package/src/compiler/render-diagnostics.ts +111 -0
  196. package/src/components/CourseContents.astro +6 -0
  197. package/src/components/HoverPanel.astro +204 -6
  198. package/src/components/PartOverview.astro +18 -9
  199. package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
  200. package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
  201. package/src/components/assessments/AssessmentRunner.tsx +7 -1
  202. package/src/components/assessments/AssessmentSet.astro +14 -1
  203. package/src/components/block-heading.ts +38 -0
  204. package/src/components/card-arrow.ts +18 -4
  205. package/src/components/card-carry.ts +406 -0
  206. package/src/components/card-drag.ts +1 -30
  207. package/src/components/card-fetch.ts +87 -0
  208. package/src/components/card-manager.ts +1222 -38
  209. package/src/components/card-memory.ts +5 -1
  210. package/src/components/card-placement.ts +211 -13
  211. package/src/components/card-side.ts +281 -31
  212. package/src/components/citation/CitationLayer.astro +45 -40
  213. package/src/components/diagram/Diagram.astro +123 -44
  214. package/src/components/disclosure/print-as-shown.ts +42 -6
  215. package/src/components/figure/Figure.astro +17 -1
  216. package/src/components/figure/FloatCaption.astro +4 -17
  217. package/src/components/figure/figure-state.ts +26 -17
  218. package/src/components/float-toolbar.ts +274 -0
  219. package/src/components/memo/Memo.astro +21 -10
  220. package/src/components/memo/ShortNote.astro +4 -0
  221. package/src/components/notation/NotationGlossary.astro +22 -17
  222. package/src/components/notation/NotationLayer.astro +300 -79
  223. package/src/components/notation/NotationMath.astro +8 -1
  224. package/src/components/notation/render-notation-math.ts +51 -5
  225. package/src/components/readable-text.ts +25 -0
  226. package/src/components/reference/FloatRefLayer.astro +162 -89
  227. package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
  228. package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
  229. package/src/components/starlight/LayoutPageSidebar.astro +17 -3
  230. package/src/components/starlight/LayoutSocialIcons.astro +79 -29
  231. package/src/components/starlight/LessonExportActions.astro +384 -310
  232. package/src/components/starlight/LessonFooter.astro +66 -8
  233. package/src/components/starlight/PrintController.astro +28 -0
  234. package/src/components/starlight/TextSizeControl.astro +137 -0
  235. package/src/components/starlight/print-controller.ts +380 -0
  236. package/src/components/starlight/section-summary.ts +13 -5
  237. package/src/components/table/Table.astro +14 -22
  238. package/src/components/table/table-truncation.ts +15 -0
  239. package/src/content-config.ts +20 -312
  240. package/src/curriculum/validation.ts +72 -24
  241. package/src/reference/base-abbreviations.mjs +2 -2
  242. package/src/reference/base-operators.mjs +8 -10
  243. package/src/reference/base-roman-labels.mjs +4 -3
  244. package/src/reference/citation-format.mjs +74 -0
  245. package/src/reference/consistency.ts +47 -61
  246. package/src/reference/curriculum-alignment.ts +4 -41
  247. package/src/reference/derived-entries.ts +68 -0
  248. package/src/reference/derived-notation.mjs +121 -0
  249. package/src/reference/diagram-fit.ts +104 -0
  250. package/src/reference/diagram-layout.ts +182 -51
  251. package/src/reference/diagrams-validate.ts +2 -1
  252. package/src/reference/equations-validate.ts +41 -2
  253. package/src/reference/equations.mjs +120 -22
  254. package/src/reference/examples-validate.ts +1 -0
  255. package/src/reference/figure-fit.ts +139 -0
  256. package/src/reference/figure-pictures.ts +96 -0
  257. package/src/reference/figure-symbols.ts +117 -0
  258. package/src/reference/figures-validate.ts +2 -1
  259. package/src/reference/gate-abbreviations.ts +1 -1
  260. package/src/reference/gate-course-rules.ts +22 -7
  261. package/src/reference/gate-heading-length.ts +25 -7
  262. package/src/reference/gate-heading-structure.ts +40 -1
  263. package/src/reference/gate-indices.ts +470 -0
  264. package/src/reference/gate-math.ts +102 -13
  265. package/src/reference/gate-mdx-expression.ts +45 -0
  266. package/src/reference/gate-non-chapter-pages.ts +241 -0
  267. package/src/reference/gate-part-pages.ts +1 -1
  268. package/src/reference/gate-prop-text.ts +145 -0
  269. package/src/reference/gate-prose-notation.ts +19 -5
  270. package/src/reference/gate-retired-keys.ts +101 -0
  271. package/src/reference/gloss.ts +2 -0
  272. package/src/reference/katex-measure.ts +384 -0
  273. package/src/reference/keyed-float-validate.ts +22 -0
  274. package/src/reference/keyed-jsx-floats.mjs +11 -14
  275. package/src/reference/lab-katex-validate.ts +156 -0
  276. package/src/reference/lab-math-scope.ts +15 -1
  277. package/src/reference/lint-ignore.ts +4 -1
  278. package/src/reference/math-glyphs.mjs +851 -46
  279. package/src/reference/mixed-content.ts +11 -2
  280. package/src/reference/page-notation.ts +114 -14
  281. package/src/reference/raw-floats.mjs +37 -13
  282. package/src/reference/references.ts +14 -0
  283. package/src/reference/registry.ts +170 -9
  284. package/src/reference/rehype-fail-katex-errors.mjs +19 -0
  285. package/src/reference/rehype-heading-anchors.mjs +11 -1
  286. package/src/reference/remark-citation.mjs +0 -0
  287. package/src/reference/remark-notation.mjs +275 -23
  288. package/src/reference/render-lab-math.ts +40 -5
  289. package/src/reference/sections-validate.ts +67 -3
  290. package/src/reference/sections.mjs +92 -17
  291. package/src/reference/tables-validate.ts +40 -2
  292. package/src/reference/text-measure.ts +63 -0
  293. package/src/reference/text-widths.ts +133 -0
  294. package/src/reference/types.ts +75 -8
  295. package/src/rules/catalog.ts +241 -31
  296. package/src/rules/settings.ts +10 -0
  297. package/src/session/PreferenceStore.ts +53 -0
  298. package/src/styles/global.css +322 -83
@@ -5,6 +5,13 @@
5
5
  * footer. Both layers are optional progressive enhancements over static
6
6
  * markup that the remark adapters and the content collections already emit.
7
7
  *
8
+ * A page that is not a chapter (rule S10: the landing page, a part's overview
9
+ * page) gets the card layers too — notation, citation, abbreviation and float
10
+ * reference — from its own manifest bundle. Every layer registers on every
11
+ * course page, with nothing of its own to show or not, so a card carried in
12
+ * from another page can be built there (rule K5, `card-carry.ts`). The layers
13
+ * that serve a chapter's numbered floats and outline stay on chapters.
14
+ *
8
15
  * The resolved notation bundle comes from the one compiler manifest (Phase
9
16
  * D5); this component never rebuilds the registry.
10
17
  */
@@ -14,6 +21,7 @@ import { loadManifest } from '../../compiler/manifest';
14
21
  import {
15
22
  isLessonSlug,
16
23
  lessonIdFromSlug,
24
+ docsSlugsForRoute,
17
25
  } from '../../compiler/lesson-derivation';
18
26
  import NotationLayer, {
19
27
  type NotationLayerDefinition,
@@ -27,7 +35,9 @@ import AnchorScrollEnhancer from './AnchorScrollEnhancer.astro';
27
35
  import CollapsibleSections from './CollapsibleSections.astro';
28
36
  import TableOverflowExpander from '../table/TableOverflowExpander.astro';
29
37
  import DiagramEnhancer from './DiagramEnhancer.astro';
38
+ import PrintController from './PrintController.astro';
30
39
  import {
40
+ derivedNotationForPage,
31
41
  notationDefinitionsForRenderedPage,
32
42
  pageNotationDependencies,
33
43
  } from '../../reference/page-notation';
@@ -38,6 +48,11 @@ const routeSlug = Astro.locals.starlightRoute.entry.id;
38
48
  const lessonId = isLessonSlug(routeSlug)
39
49
  ? lessonIdFromSlug(routeSlug)
40
50
  : undefined;
51
+ const manifest = await loadManifest();
52
+ // A chapter, or a non-chapter page the manifest knows (not the 404 route).
53
+ const routeSlugs = docsSlugsForRoute(routeSlug);
54
+ const pageId =
55
+ lessonId ?? manifest.pages.find((page) => routeSlugs.includes(page.slug))?.id;
41
56
 
42
57
  const glossaryBase = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/glossary/`;
43
58
 
@@ -45,10 +60,11 @@ const definitions: NotationLayerDefinition[] = [];
45
60
  let notationDefaultOpen = false;
46
61
  let numberAliases = false;
47
62
  let suspended: NotationSuspension[] = [];
48
- if (lessonId) {
49
- const manifest = await loadManifest();
63
+ if (pageId) {
50
64
  notationDefaultOpen = manifest.disclosure.notation.defaultOpen;
51
65
  numberAliases = manifest.numbering.aliases;
66
+ }
67
+ if (lessonId) {
52
68
  const lesson = manifest.lessons.find((entry) => entry.id === lessonId);
53
69
  const routeData = Astro.locals.starlightRoute.entry.data as {
54
70
  notation?: {
@@ -74,10 +90,15 @@ if (lessonId) {
74
90
  .filter((definition) => definition.scope === 'local')
75
91
  .map((definition) => definition.key);
76
92
  assertLocalNotationParity(lessonId, authoredLocalKeys, renderedLocalKeys);
93
+ }
94
+ if (pageId) {
95
+ // A non-chapter page renders the bundle its body compiled to, and nothing a
96
+ // component registered: the glossary's formulas are its own layer's cards,
97
+ // and rule S10 keeps the floats that would register more off the page.
77
98
  const renderedDefinitions = notationDefinitionsForRenderedPage(
78
99
  manifest,
79
- lessonId,
80
- pageNotationDependencies(Astro.locals),
100
+ pageId,
101
+ lessonId ? pageNotationDependencies(Astro.locals) : [],
81
102
  );
82
103
  for (const definition of renderedDefinitions) {
83
104
  definitions.push({
@@ -89,8 +110,8 @@ if (lessonId) {
89
110
  formulaScope: definition.formulaScope,
90
111
  glosses: definition.glosses,
91
112
  units: definition.units,
113
+ range: definition.range,
92
114
  seeAlso: definition.seeAlso,
93
- editorialStatus: definition.editorialStatus,
94
115
  scope: definition.scope,
95
116
  href:
96
117
  definition.scope === 'shared'
@@ -98,23 +119,60 @@ if (lessonId) {
98
119
  : `#notation-${definition.key}`,
99
120
  });
100
121
  }
122
+ // Rule N23: a derived card goes to its index entry's full definition.
123
+ for (const derived of derivedNotationForPage(
124
+ manifest,
125
+ pageId,
126
+ lessonId ? Astro.locals : undefined,
127
+ )) {
128
+ const index = definitions.find((definition) => definition.key === derived.index);
129
+ definitions.push({
130
+ key: derived.key,
131
+ notation: derived.notation,
132
+ label: derived.label,
133
+ summary: derived.summary,
134
+ formula: derived.formula,
135
+ formulaScope: derived.formulaScope,
136
+ glosses: derived.glosses,
137
+ units: derived.units,
138
+ scope: 'derived',
139
+ ...(index?.href === undefined ? {} : { href: index.href }),
140
+ });
141
+ }
101
142
  }
102
143
  ---
103
144
 
145
+ {/* What the course says about its cards (`card-carry.ts`): carrying (K1),
146
+ and the base its carried set is stored under (K5). */}
147
+ <div
148
+ hidden
149
+ data-explico-card-config
150
+ data-carry={String(manifest.cards.carry)}
151
+ data-base={import.meta.env.BASE_URL}
152
+ ></div>
104
153
  <NotationLayer
105
154
  definitions={definitions}
106
155
  defaultOpen={notationDefaultOpen}
107
156
  suspended={suspended}
157
+ page={routeSlug}
108
158
  />
109
159
  <AnchorScrollEnhancer />
110
- {lessonId && <CitationLayer />}
111
- {lessonId && <AbbreviationLayer />}
160
+ {pageId && <CitationLayer />}
161
+ {pageId && <AbbreviationLayer />}
112
162
  {lessonId && <KeyedFloatEnhancer aliases={numberAliases} />}
113
- {lessonId && <FloatRefLayer />}
163
+ {pageId && <FloatRefLayer />}
114
164
  {lessonId && <CollapsibleSections />}
115
165
  {lessonId && <TableOverflowExpander />}
116
166
  <DiagramEnhancer />
117
167
  {lessonId && <HeadingAnchorEnhancer />}
168
+ {
169
+ pageId && manifest.lessonExports.print && (
170
+ <PrintController
171
+ canonicalBaseUrl={manifest.lessonExports.canonicalBaseUrl}
172
+ siteBaseUrl={import.meta.env.BASE_URL}
173
+ />
174
+ )
175
+ }
118
176
  <div class="explico-site-footer" data-explico-export-chrome="site-footer">
119
177
  <DefaultFooter />
120
178
  </div>
@@ -0,0 +1,28 @@
1
+ ---
2
+ /**
3
+ * The page's print controller (`print-controller.ts`): what the Print
4
+ * button, Ctrl+P and Cmd+P run, and the site's URLs it moves links onto
5
+ * when printing. It sits in the page's footer, inside `<main>`, because a
6
+ * downloaded page keeps only `<main>` and must print the same way.
7
+ */
8
+ interface Props {
9
+ canonicalBaseUrl?: string;
10
+ siteBaseUrl: string;
11
+ }
12
+
13
+ const { canonicalBaseUrl, siteBaseUrl } = Astro.props;
14
+ ---
15
+
16
+ <div
17
+ hidden
18
+ data-explico-print-controller
19
+ data-canonical-base-url={canonicalBaseUrl}
20
+ data-site-base-url={siteBaseUrl}
21
+ >
22
+ </div>
23
+
24
+ <script>
25
+ import { installPrintController } from './print-controller';
26
+
27
+ installPrintController();
28
+ </script>
@@ -0,0 +1,137 @@
1
+ ---
2
+ /**
3
+ * The reader's text size on a touch screen: 100%, 90%, 80% or 70% of the
4
+ * site's own. A PC has the browser's zoom for this; a phone's text-size
5
+ * settings are buried in its browser's menus, so the page offers its own.
6
+ * It sits beside the theme switch (in the phone menu, and in the header
7
+ * of a tablet held wide) and shows only where there is no hover.
8
+ *
9
+ * The choice scales the root font size (`data-text-size` on `<html>`,
10
+ * `global.css`), so the text, the math, the spacing and the cards all
11
+ * follow. It is remembered per reader and copied onto the root before the
12
+ * page first paints by the inline script below, so the page never jumps
13
+ * from one size to another. `LayoutSocialIcons.astro` renders this in the
14
+ * header and in the phone menu; both copies stay in step.
15
+ */
16
+ import { Icon } from '@astrojs/starlight/components';
17
+ import { inlineRootAttributeScript } from '../../session/PreferenceStore';
18
+
19
+ const TEXT_SIZE_KEY = 'explico:display:text-size';
20
+ const SIZES = ['100', '90', '80', '70'] as const;
21
+ const boot = inlineRootAttributeScript(TEXT_SIZE_KEY, 'data-text-size', SIZES.slice(1));
22
+ ---
23
+
24
+ <script is:inline set:html={boot} />
25
+ <label class="explico-text-size" data-text-size-control>
26
+ <span class="sr-only">Text size</span>
27
+ <span class="label-icon" aria-hidden="true">Aa</span>
28
+ <select autocomplete="off" data-text-size-select>
29
+ {SIZES.map((size) => <option value={size} selected={size === '100'}>{size}%</option>)}
30
+ </select>
31
+ <Icon name="down-caret" class="caret" />
32
+ </label>
33
+
34
+ <script>
35
+ import { createBrowserPreferenceStore } from '../../session/PreferenceStore';
36
+
37
+ const KEY = 'explico:display:text-size';
38
+ const SIZES = new Set(['100', '90', '80', '70']);
39
+ const preferences = createBrowserPreferenceStore();
40
+ const root = document.documentElement;
41
+ const selects = () =>
42
+ Array.from(document.querySelectorAll<HTMLSelectElement>('[data-text-size-select]'));
43
+
44
+ const current = (): string => {
45
+ const value = root.getAttribute('data-text-size') ?? '100';
46
+ return SIZES.has(value) ? value : '100';
47
+ };
48
+
49
+ const show = (value: string) => {
50
+ for (const select of selects()) select.value = value;
51
+ };
52
+
53
+ const choose = (value: string) => {
54
+ if (!SIZES.has(value)) return;
55
+ if (value === '100') {
56
+ root.removeAttribute('data-text-size');
57
+ preferences.remove?.(KEY);
58
+ } else {
59
+ root.setAttribute('data-text-size', value);
60
+ preferences.set(KEY, value);
61
+ }
62
+ show(value);
63
+ };
64
+
65
+ show(current());
66
+ for (const select of selects()) {
67
+ select.addEventListener('change', () => choose(select.value));
68
+ }
69
+ // A page restored from the back/forward cache keeps its old selects' values.
70
+ window.addEventListener('pageshow', () => show(current()));
71
+ </script>
72
+
73
+ <style>
74
+ @layer starlight.components {
75
+ /* Starlight's own select (`ThemeSelect`), with "Aa" for its icon. */
76
+ .explico-text-size {
77
+ --sl-label-icon-size: 0.875rem;
78
+ --sl-caret-size: 1.25rem;
79
+ --sl-inline-padding: 0.5rem;
80
+ --sl-select-width: 5.5em;
81
+ position: relative;
82
+ display: none;
83
+ align-items: center;
84
+ gap: 0.25rem;
85
+ color: var(--sl-color-gray-2);
86
+ }
87
+
88
+ @media (hover: none) {
89
+ .explico-text-size {
90
+ display: flex;
91
+ }
92
+ }
93
+
94
+ .explico-text-size:hover {
95
+ color: var(--sl-color-white);
96
+ }
97
+
98
+ .label-icon,
99
+ .explico-text-size :global(.caret) {
100
+ position: absolute;
101
+ top: 50%;
102
+ transform: translateY(-50%);
103
+ pointer-events: none;
104
+ }
105
+
106
+ .label-icon {
107
+ inset-inline-start: 0;
108
+ font-size: var(--sl-label-icon-size);
109
+ font-weight: 600;
110
+ letter-spacing: -0.02em;
111
+ }
112
+
113
+ .explico-text-size :global(.caret) {
114
+ inset-inline-end: 0;
115
+ font-size: var(--sl-caret-size);
116
+ }
117
+
118
+ /* As tall as the icon buttons beside it, so the row keeps its height. */
119
+ select {
120
+ border: 0;
121
+ padding-block: 0.25rem;
122
+ padding-inline: calc(var(--sl-label-icon-size) + var(--sl-inline-padding) + 0.5rem)
123
+ calc(var(--sl-caret-size) + var(--sl-inline-padding) + 0.25rem);
124
+ margin-inline: calc(var(--sl-inline-padding) * -1);
125
+ width: calc(var(--sl-select-width) + var(--sl-inline-padding) * 2);
126
+ background-color: transparent;
127
+ color: inherit;
128
+ cursor: pointer;
129
+ appearance: none;
130
+ }
131
+
132
+ option {
133
+ background-color: var(--sl-color-bg-nav);
134
+ color: var(--sl-color-gray-1);
135
+ }
136
+ }
137
+ </style>
@@ -0,0 +1,380 @@
1
+ /**
2
+ * The page's print controller, shared by the header's Print button and the
3
+ * page itself. The button, Ctrl+P and Cmd+P prepare the page as shown
4
+ * (`../disclosure/print-as-shown`), move its links onto the deployed site,
5
+ * load every lab that will be on paper and then open the print dialog; the
6
+ * browser's own File → Print gets the same preparation without loading
7
+ * labs. The figure capture fires `explico:prepare-static-export` in
8
+ * `markdown` mode, which opens everything.
9
+ *
10
+ * `PrintController.astro` installs it from the page's footer, inside
11
+ * `<main>`, with the `[data-explico-print-controller]` element that holds
12
+ * the site's URLs: a downloaded page keeps only `<main>`, so it prints the
13
+ * same way.
14
+ */
15
+ import {
16
+ prepareDisclosures,
17
+ type PreparedDisclosures,
18
+ } from '../disclosure/print-as-shown';
19
+
20
+ type SavedLink = {
21
+ readonly element: HTMLAnchorElement;
22
+ readonly href: string | null;
23
+ };
24
+
25
+ type SavedHidden = {
26
+ readonly element: HTMLElement;
27
+ readonly hidden: HTMLElement['hidden'];
28
+ };
29
+
30
+ type StaticExportMode = 'print' | 'markdown';
31
+
32
+ type PreparedStaticExport = {
33
+ readonly mode: StaticExportMode;
34
+ readonly disclosures: PreparedDisclosures;
35
+ readonly hidden: readonly SavedHidden[];
36
+ readonly links: readonly SavedLink[];
37
+ };
38
+
39
+ const root = document.documentElement;
40
+ let prepared: PreparedStaticExport | null = null;
41
+ let pendingMarkerCleanup: (() => void) | null = null;
42
+
43
+ function controller(): HTMLElement | null {
44
+ return document.querySelector<HTMLElement>('[data-explico-print-controller]');
45
+ }
46
+
47
+ function canonicalPrintHref(
48
+ link: HTMLAnchorElement,
49
+ canonicalBaseUrl: string | undefined,
50
+ siteBaseUrl: string,
51
+ ): string | undefined {
52
+ const rawHref = link.getAttribute('href');
53
+ if (!rawHref || rawHref.startsWith('#') || link.hasAttribute('download')) {
54
+ return undefined;
55
+ }
56
+
57
+ let target: URL;
58
+ try {
59
+ target = new URL(rawHref, location.href);
60
+ } catch {
61
+ return undefined;
62
+ }
63
+ if (target.protocol !== 'http:' && target.protocol !== 'https:') return undefined;
64
+
65
+ const current = new URL(location.href);
66
+ if (
67
+ target.origin === current.origin &&
68
+ target.pathname === current.pathname &&
69
+ target.search === current.search
70
+ ) {
71
+ return undefined;
72
+ }
73
+
74
+ return canonicalUrl(target, canonicalBaseUrl, siteBaseUrl);
75
+ }
76
+
77
+ // A same-site URL moved onto the deployed site, so a printed or saved
78
+ // lesson's links don't point at whatever host the reader printed from.
79
+ function canonicalUrl(
80
+ target: URL,
81
+ canonicalBaseUrl: string | undefined,
82
+ siteBaseUrl: string,
83
+ ): string {
84
+ if (!canonicalBaseUrl || target.origin !== location.origin) return target.href;
85
+ const normalizedSiteBase = `/${siteBaseUrl.replace(/^\/+|\/+$/gu, '')}/`;
86
+ const relativePath = target.pathname.startsWith(normalizedSiteBase)
87
+ ? target.pathname.slice(normalizedSiteBase.length)
88
+ : target.pathname.replace(/^\/+/, '');
89
+ return new URL(
90
+ `${relativePath}${target.search}${target.hash}`,
91
+ canonicalBaseUrl,
92
+ ).href;
93
+ }
94
+
95
+ function prepareStaticExport(mode: StaticExportMode): void {
96
+ if (prepared) return;
97
+ const printController = controller();
98
+ if (!printController) return;
99
+ pendingMarkerCleanup?.();
100
+
101
+ root.dataset.explicoStaticExport = mode;
102
+
103
+ const canonicalBaseUrl = printController.dataset.canonicalBaseUrl;
104
+ const siteBaseUrl = printController.dataset.siteBaseUrl ?? '/';
105
+
106
+ // Print follows the screen (see `../disclosure/print-as-shown`); a
107
+ // Markdown export needs every word whatever the reader collapsed.
108
+ const disclosures = prepareDisclosures(
109
+ mode === 'print' ? 'as-shown' : 'expanded',
110
+ {
111
+ noteHref: (anchorId) => {
112
+ const page = new URL(location.href);
113
+ page.hash = anchorId ? `#${anchorId}` : '';
114
+ return canonicalUrl(page, canonicalBaseUrl, siteBaseUrl);
115
+ },
116
+ },
117
+ );
118
+
119
+ // Rows beyond a long table's fold and an open example set's inactive
120
+ // tabs are hidden only to save screen space, so they are exposed for
121
+ // print too. A closed short note or question is a reader's disclosure:
122
+ // print leaves it closed (with its note), only Markdown export opens it.
123
+ const hiddenSelector =
124
+ mode === 'print'
125
+ ? 'main :is(.compact-example, .keyed-table__body tr:not(.keyed-table__more-row))[hidden]'
126
+ : 'main :is(.assessment-panel, .short-note, .compact-example, .keyed-table__body tr:not(.keyed-table__more-row))[hidden]';
127
+ const hidden = Array.from(
128
+ document.querySelectorAll<HTMLElement>(hiddenSelector),
129
+ (element): SavedHidden => {
130
+ const saved = { element, hidden: element.hidden };
131
+ element.hidden = false;
132
+ return saved;
133
+ },
134
+ );
135
+
136
+ const links =
137
+ mode === 'print'
138
+ ? Array.from(
139
+ document.querySelectorAll<HTMLAnchorElement>('main a[href]'),
140
+ ).flatMap((element): SavedLink[] => {
141
+ const printHref = canonicalPrintHref(
142
+ element,
143
+ canonicalBaseUrl,
144
+ siteBaseUrl,
145
+ );
146
+ if (!printHref) return [];
147
+ const saved = {
148
+ element,
149
+ href: element.getAttribute('href'),
150
+ };
151
+ // PDF viewers retain anchors as clickable annotations. Point the
152
+ // anchor itself at the deployed lesson while printing; no visible
153
+ // URL suffix is needed in the document text.
154
+ element.setAttribute('href', printHref);
155
+ return [saved];
156
+ })
157
+ : [];
158
+
159
+ prepared = { mode, disclosures, hidden, links };
160
+ if (mode === 'print') root.dataset.explicoPrintReady = '';
161
+ }
162
+
163
+ function restoreStaticExport(): void {
164
+ const saved = prepared;
165
+ if (!saved) return;
166
+ prepared = null;
167
+
168
+ saved.disclosures.restore();
169
+ for (const state of saved.hidden) state.element.hidden = state.hidden;
170
+ for (const state of saved.links) {
171
+ if (state.href === null) state.element.removeAttribute('href');
172
+ else state.element.setAttribute('href', state.href);
173
+ }
174
+ delete root.dataset.explicoPrintReady;
175
+
176
+ // Native `toggle` is queued after changing `.open`. Keep the temporary
177
+ // marker through that task so disclosure persistence ignores both the
178
+ // prepare and restore mutations.
179
+ const cleanupMarkers = () => {
180
+ if (pendingMarkerCleanup !== cleanupMarkers) return;
181
+ saved.disclosures.releaseMarkers();
182
+ delete root.dataset.explicoStaticExport;
183
+ pendingMarkerCleanup = null;
184
+ };
185
+ pendingMarkerCleanup = cleanupMarkers;
186
+ window.setTimeout(cleanupMarkers, 0);
187
+ }
188
+
189
+ function afterNextPaint(): Promise<void> {
190
+ return new Promise((resolve) => {
191
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
192
+ });
193
+ }
194
+
195
+ /** `promise`, or nothing after `ms` — a slow resource never holds the dialog back. */
196
+ function atMost(promise: Promise<unknown>, ms: number): Promise<void> {
197
+ return new Promise((resolve) => {
198
+ const timer = window.setTimeout(resolve, ms);
199
+ void promise.then(
200
+ () => {
201
+ window.clearTimeout(timer);
202
+ resolve();
203
+ },
204
+ () => {
205
+ window.clearTimeout(timer);
206
+ resolve();
207
+ },
208
+ );
209
+ });
210
+ }
211
+
212
+ function waitForIslandHydration(island: HTMLElement): Promise<void> {
213
+ if (!island.hasAttribute('ssr')) return Promise.resolve();
214
+
215
+ return new Promise((resolve) => {
216
+ let timeout = 0;
217
+ const finish = () => {
218
+ window.clearTimeout(timeout);
219
+ island.removeEventListener('astro:hydrate', finish);
220
+ island.removeEventListener('astro:hydration-error', finish);
221
+ resolve();
222
+ };
223
+ island.addEventListener('astro:hydrate', finish, { once: true });
224
+ island.addEventListener('astro:hydration-error', finish, { once: true });
225
+ timeout = window.setTimeout(finish, 10_000);
226
+ requestAnimationFrame(() => {
227
+ if (!island.hasAttribute('ssr')) finish();
228
+ });
229
+ });
230
+ }
231
+
232
+ async function hydrateVisibleIslandsForPrint(): Promise<void> {
233
+ // Only islands that will be on paper: one inside a disclosure the reader
234
+ // left closed (print follows the screen) never gets a layout box, so it
235
+ // can never become visible, and waiting on it would stall the dialog for
236
+ // `waitForIslandHydration`'s full timeout. A lab's tools never print.
237
+ const islands = Array.from(
238
+ document.querySelectorAll<HTMLElement>('main astro-island[ssr][client="visible"]'),
239
+ ).filter((island) => !island.closest('details:not([open]), [hidden], [data-print-omit]'));
240
+ if (islands.length === 0) return;
241
+
242
+ const scrollX = window.scrollX;
243
+ const scrollY = window.scrollY;
244
+ const scrollBehavior = root.style.scrollBehavior;
245
+ root.style.scrollBehavior = 'auto';
246
+ try {
247
+ // `client:visible` deliberately does not hydrate an off-screen plot.
248
+ // Visit each island briefly so its normal Astro directive runs before
249
+ // the browser snapshots the document for printing.
250
+ for (const island of islands) {
251
+ if (!island.isConnected || !island.hasAttribute('ssr')) continue;
252
+ const hydrated = waitForIslandHydration(island);
253
+ // Astro's `client:visible` directive observes an island's children,
254
+ // while the custom-element wrapper itself may have no layout box.
255
+ (island.firstElementChild ?? island).scrollIntoView({
256
+ block: 'center',
257
+ inline: 'nearest',
258
+ });
259
+ await hydrated;
260
+ }
261
+ } finally {
262
+ window.scrollTo({ left: scrollX, top: scrollY, behavior: 'auto' });
263
+ root.style.scrollBehavior = scrollBehavior;
264
+ }
265
+ }
266
+
267
+ async function waitForPrintVisuals(): Promise<void> {
268
+ await hydrateVisibleIslandsForPrint();
269
+ await document.fonts?.ready;
270
+ // A lazy image the reader never scrolled to would never load, and the
271
+ // wait below would never end: load it now, and give up after a while.
272
+ const images = Array.from(document.querySelectorAll<HTMLImageElement>('main img'));
273
+ for (const image of images) {
274
+ if (image.loading === 'lazy') image.loading = 'eager';
275
+ }
276
+ await atMost(
277
+ Promise.all(
278
+ images.map((image) => {
279
+ if (!image.complete) {
280
+ return new Promise<void>((resolve) => {
281
+ image.addEventListener('load', () => resolve(), { once: true });
282
+ image.addEventListener('error', () => resolve(), { once: true });
283
+ });
284
+ }
285
+ return typeof image.decode === 'function'
286
+ ? image.decode().catch(() => undefined)
287
+ : Promise.resolve();
288
+ }),
289
+ ),
290
+ 5_000,
291
+ );
292
+ // React chart effects and SVG layout settle after island hydration.
293
+ await afterNextPaint();
294
+ }
295
+
296
+ let printing = false;
297
+
298
+ /** Prepare the page, load what will be on paper, then open the print dialog. */
299
+ export async function printPage(button?: HTMLButtonElement): Promise<void> {
300
+ if (printing) return;
301
+ printing = true;
302
+ if (button) {
303
+ button.disabled = true;
304
+ button.setAttribute('aria-busy', 'true');
305
+ }
306
+ window.dispatchEvent(
307
+ new CustomEvent('explico:prepare-static-export', {
308
+ detail: { mode: 'print' },
309
+ }),
310
+ );
311
+ try {
312
+ await waitForPrintVisuals();
313
+ window.print();
314
+ } finally {
315
+ printing = false;
316
+ if (button) {
317
+ button.disabled = false;
318
+ button.removeAttribute('aria-busy');
319
+ }
320
+ }
321
+ }
322
+
323
+ /**
324
+ * Wire the page once: the prepare and restore events, the browser's own
325
+ * print, and Ctrl+P / Cmd+P, which run the Print button's steps.
326
+ */
327
+ export function installPrintController(): void {
328
+ if (!root.hasAttribute('data-explico-print-controller-wired')) {
329
+ root.dataset.explicoPrintControllerWired = '';
330
+ const printMedia = window.matchMedia('print');
331
+ window.addEventListener('explico:prepare-static-export', (event) => {
332
+ const mode = (event as CustomEvent<{ mode?: unknown }>).detail?.mode;
333
+ prepareStaticExport(mode === 'markdown' ? 'markdown' : 'print');
334
+ });
335
+ window.addEventListener('explico:restore-static-export', restoreStaticExport);
336
+ const requestPrintPreparation = () => {
337
+ window.dispatchEvent(
338
+ new CustomEvent('explico:prepare-static-export', {
339
+ detail: { mode: 'print' },
340
+ }),
341
+ );
342
+ };
343
+ const requestPrintRestore = () => {
344
+ window.dispatchEvent(
345
+ new CustomEvent('explico:restore-static-export', {
346
+ detail: { mode: 'print' },
347
+ }),
348
+ );
349
+ };
350
+ window.addEventListener('beforeprint', requestPrintPreparation);
351
+ window.addEventListener('afterprint', requestPrintRestore);
352
+ printMedia.addEventListener('change', (event) => {
353
+ if (event.matches) requestPrintPreparation();
354
+ else requestPrintRestore();
355
+ });
356
+ if (printMedia.matches) requestPrintPreparation();
357
+
358
+ // Ctrl+P / Cmd+P run the Print button's steps: the browser's own dialog
359
+ // cannot wait for an off-screen lab to load.
360
+ window.addEventListener(
361
+ 'keydown',
362
+ (event) => {
363
+ if (
364
+ !(event.ctrlKey || event.metaKey) ||
365
+ event.altKey ||
366
+ event.shiftKey ||
367
+ event.key.toLowerCase() !== 'p'
368
+ ) {
369
+ return;
370
+ }
371
+ event.preventDefault();
372
+ event.stopImmediatePropagation();
373
+ void printPage(
374
+ document.querySelector<HTMLButtonElement>('[data-explico-print-action]') ?? undefined,
375
+ );
376
+ },
377
+ { capture: true },
378
+ );
379
+ }
380
+ }