explico 0.12.0 → 0.13.1
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 +1137 -1
- package/README.md +91 -13
- package/dist/compiler/astro.d.ts +4 -3
- package/dist/compiler/astro.js +45 -15
- package/dist/compiler/cli.d.ts +26 -12
- package/dist/compiler/cli.js +96 -25
- package/dist/compiler/collections.d.ts +9 -1
- package/dist/compiler/collections.js +55 -16
- 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 +14 -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 +87 -6
- package/dist/compiler/manifest.js +174 -31
- package/dist/compiler/render-diagnostics.d.ts +65 -0
- package/dist/compiler/render-diagnostics.js +56 -0
- package/dist/compiler/resolution-report.d.ts +2 -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 +2 -2
- package/dist/reference/base-operators.mjs +2 -2
- 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/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-part-pages.d.ts +1 -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-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-scope.js +13 -2
- 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 +34 -4
- package/dist/reference/page-notation.js +54 -2
- package/dist/reference/references.d.ts +11 -1
- package/dist/reference/references.js +6 -0
- package/dist/reference/registry.js +104 -10
- 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 +84 -20
- package/dist/reference/render-lab-math.d.ts +2 -0
- package/dist/reference/render-lab-math.js +17 -5
- 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 +191 -30
- package/dist/rules/in-effect.d.ts +1 -1
- package/dist/rules/rules.schema.json +79 -24
- 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 +32 -21
- package/docs/authoring/agents.md +38 -7
- package/docs/authoring/components.md +111 -6
- package/docs/authoring/migrating.md +707 -0
- package/docs/authoring/notation.md +281 -29
- package/docs/authoring/retired.md +1 -0
- package/docs/authoring/structure-rules.md +10 -41
- package/docs/authoring/structure.md +53 -2
- package/docs/authoring/visualizations.md +144 -18
- package/package.json +12 -7
- package/src/compiler/astro.ts +57 -17
- package/src/compiler/cli.ts +143 -35
- package/src/compiler/collections.ts +92 -19
- 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 +299 -28
- package/src/compiler/render-diagnostics.ts +111 -0
- package/src/components/HoverPanel.astro +204 -6
- package/src/components/PartOverview.astro +12 -9
- 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/figure/Figure.astro +11 -0
- package/src/components/figure/FloatCaption.astro +4 -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 +321 -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 +47 -9
- 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 +13 -21
- 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 +2 -2
- 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/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-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-math-scope.ts +15 -1
- package/src/reference/math-glyphs.mjs +851 -46
- package/src/reference/mixed-content.ts +11 -2
- package/src/reference/page-notation.ts +100 -3
- package/src/reference/references.ts +14 -0
- package/src/reference/registry.ts +154 -8
- 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 +84 -20
- package/src/reference/render-lab-math.ts +29 -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 +205 -29
- package/src/rules/settings.ts +10 -0
- package/src/session/PreferenceStore.ts +53 -0
- package/src/styles/global.css +291 -75
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* A page that is not a chapter (rule S10: the landing page, a part's overview
|
|
9
9
|
* page) gets the card layers too — notation, citation, abbreviation and float
|
|
10
|
-
* reference — from its own manifest bundle.
|
|
11
|
-
*
|
|
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.
|
|
12
14
|
*
|
|
13
15
|
* The resolved notation bundle comes from the one compiler manifest (Phase
|
|
14
16
|
* D5); this component never rebuilds the registry.
|
|
@@ -19,6 +21,7 @@ import { loadManifest } from '../../compiler/manifest';
|
|
|
19
21
|
import {
|
|
20
22
|
isLessonSlug,
|
|
21
23
|
lessonIdFromSlug,
|
|
24
|
+
docsSlugsForRoute,
|
|
22
25
|
} from '../../compiler/lesson-derivation';
|
|
23
26
|
import NotationLayer, {
|
|
24
27
|
type NotationLayerDefinition,
|
|
@@ -32,7 +35,9 @@ import AnchorScrollEnhancer from './AnchorScrollEnhancer.astro';
|
|
|
32
35
|
import CollapsibleSections from './CollapsibleSections.astro';
|
|
33
36
|
import TableOverflowExpander from '../table/TableOverflowExpander.astro';
|
|
34
37
|
import DiagramEnhancer from './DiagramEnhancer.astro';
|
|
38
|
+
import PrintController from './PrintController.astro';
|
|
35
39
|
import {
|
|
40
|
+
derivedNotationForPage,
|
|
36
41
|
notationDefinitionsForRenderedPage,
|
|
37
42
|
pageNotationDependencies,
|
|
38
43
|
} from '../../reference/page-notation';
|
|
@@ -45,13 +50,9 @@ const lessonId = isLessonSlug(routeSlug)
|
|
|
45
50
|
: undefined;
|
|
46
51
|
const manifest = await loadManifest();
|
|
47
52
|
// A chapter, or a non-chapter page the manifest knows (not the 404 route).
|
|
48
|
-
|
|
49
|
-
// the landing page an empty one, so match the page's docs slug either way.
|
|
50
|
-
const routeSlugs = new Set(
|
|
51
|
-
routeSlug === '' ? ['index'] : [routeSlug, `${routeSlug}/index`],
|
|
52
|
-
);
|
|
53
|
+
const routeSlugs = docsSlugsForRoute(routeSlug);
|
|
53
54
|
const pageId =
|
|
54
|
-
lessonId ?? manifest.pages.find((page) => routeSlugs.
|
|
55
|
+
lessonId ?? manifest.pages.find((page) => routeSlugs.includes(page.slug))?.id;
|
|
55
56
|
|
|
56
57
|
const glossaryBase = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/glossary/`;
|
|
57
58
|
|
|
@@ -109,8 +110,8 @@ if (pageId) {
|
|
|
109
110
|
formulaScope: definition.formulaScope,
|
|
110
111
|
glosses: definition.glosses,
|
|
111
112
|
units: definition.units,
|
|
113
|
+
range: definition.range,
|
|
112
114
|
seeAlso: definition.seeAlso,
|
|
113
|
-
editorialStatus: definition.editorialStatus,
|
|
114
115
|
scope: definition.scope,
|
|
115
116
|
href:
|
|
116
117
|
definition.scope === 'shared'
|
|
@@ -118,13 +119,42 @@ if (pageId) {
|
|
|
118
119
|
: `#notation-${definition.key}`,
|
|
119
120
|
});
|
|
120
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
|
+
}
|
|
121
142
|
}
|
|
122
143
|
---
|
|
123
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>
|
|
124
153
|
<NotationLayer
|
|
125
154
|
definitions={definitions}
|
|
126
155
|
defaultOpen={notationDefaultOpen}
|
|
127
156
|
suspended={suspended}
|
|
157
|
+
page={routeSlug}
|
|
128
158
|
/>
|
|
129
159
|
<AnchorScrollEnhancer />
|
|
130
160
|
{pageId && <CitationLayer />}
|
|
@@ -135,6 +165,14 @@ if (pageId) {
|
|
|
135
165
|
{lessonId && <TableOverflowExpander />}
|
|
136
166
|
<DiagramEnhancer />
|
|
137
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
|
+
}
|
|
138
176
|
<div class="explico-site-footer" data-explico-export-chrome="site-footer">
|
|
139
177
|
<DefaultFooter />
|
|
140
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
|
+
}
|