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.
- package/CHANGELOG.md +1122 -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 +663 -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 +300 -79
- package/src/components/notation/NotationMath.astro +8 -1
- package/src/components/notation/render-notation-math.ts +51 -5
- package/src/components/readable-text.ts +25 -0
- package/src/components/reference/FloatRefLayer.astro +162 -89
- package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
- package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
- package/src/components/starlight/LayoutPageSidebar.astro +17 -3
- package/src/components/starlight/LayoutSocialIcons.astro +79 -29
- package/src/components/starlight/LessonExportActions.astro +384 -310
- package/src/components/starlight/LessonFooter.astro +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
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
import { parse, parseFragment } from 'parse5';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
attribute,
|
|
5
|
+
childrenOf,
|
|
6
|
+
elements,
|
|
7
|
+
hasClass,
|
|
8
|
+
inferBasePrefix,
|
|
9
|
+
isElement,
|
|
10
|
+
type HtmlElement,
|
|
11
|
+
type HtmlNode,
|
|
12
|
+
} from './html-tree';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* explico's static reading tree: a built page reduced to what a reader reads,
|
|
16
|
+
* with explico's meaning kept — a float with its number and captions, a
|
|
17
|
+
* cross-reference by its target, a check by its anchor. Print exporters (the
|
|
18
|
+
* LaTeX source now, Markdown later) serialize this tree; none of them
|
|
19
|
+
* re-walks `content/` or recomputes a number.
|
|
20
|
+
*
|
|
21
|
+
* It is read off Astro's static output, where every disclosure's body, every
|
|
22
|
+
* table row and every formula's TeX already sit in the markup, whatever the
|
|
23
|
+
* reader would see collapsed. What the static page does not hold — a lab's
|
|
24
|
+
* drawing, a check's answers — comes from the capture pass and the manifest.
|
|
25
|
+
*
|
|
26
|
+
* Math keeps the author's `\explain{key}{…}` and `\slot{key}{…}` markers (the
|
|
27
|
+
* KaTeX annotation, not the stripped `data-latex`): a printer that understands
|
|
28
|
+
* them (explico's LaTeX class) needs no stripping, and one that doesn't strips
|
|
29
|
+
* them itself.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export type RefKind =
|
|
33
|
+
| 'equation'
|
|
34
|
+
| 'figure'
|
|
35
|
+
| 'table'
|
|
36
|
+
| 'diagram'
|
|
37
|
+
| 'section'
|
|
38
|
+
| 'example'
|
|
39
|
+
| 'check'
|
|
40
|
+
| 'memo'
|
|
41
|
+
| 'anchor'
|
|
42
|
+
| 'page';
|
|
43
|
+
|
|
44
|
+
/** A link target inside the course: a page route (base-relative, no slashes at the ends) and an anchor. */
|
|
45
|
+
export interface CourseTarget {
|
|
46
|
+
readonly route: string;
|
|
47
|
+
readonly anchor: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type Inline =
|
|
51
|
+
| { readonly kind: 'text'; readonly value: string }
|
|
52
|
+
| { readonly kind: 'emphasis'; readonly children: readonly Inline[] }
|
|
53
|
+
| { readonly kind: 'strong'; readonly children: readonly Inline[] }
|
|
54
|
+
| { readonly kind: 'superscript'; readonly children: readonly Inline[] }
|
|
55
|
+
| { readonly kind: 'subscript'; readonly children: readonly Inline[] }
|
|
56
|
+
| { readonly kind: 'code'; readonly value: string }
|
|
57
|
+
| { readonly kind: 'math'; readonly tex: string }
|
|
58
|
+
| { readonly kind: 'break' }
|
|
59
|
+
| { readonly kind: 'link'; readonly href: string; readonly children: readonly Inline[] }
|
|
60
|
+
| {
|
|
61
|
+
readonly kind: 'ref';
|
|
62
|
+
readonly refKind: RefKind;
|
|
63
|
+
readonly target: CourseTarget;
|
|
64
|
+
readonly children: readonly Inline[];
|
|
65
|
+
}
|
|
66
|
+
| { readonly kind: 'term'; readonly key: string; readonly children: readonly Inline[] }
|
|
67
|
+
| {
|
|
68
|
+
readonly kind: 'cite';
|
|
69
|
+
readonly number: string;
|
|
70
|
+
readonly anchor: string;
|
|
71
|
+
readonly sourceId?: string;
|
|
72
|
+
readonly locator?: string;
|
|
73
|
+
}
|
|
74
|
+
| { readonly kind: 'memoMark'; readonly anchor: string; readonly label: string }
|
|
75
|
+
| { readonly kind: 'noteMark'; readonly anchor: string; readonly children: readonly Inline[] };
|
|
76
|
+
|
|
77
|
+
export interface TableCell {
|
|
78
|
+
readonly header: boolean;
|
|
79
|
+
readonly colspan: number;
|
|
80
|
+
readonly rowspan: number;
|
|
81
|
+
readonly align?: 'left' | 'center' | 'right';
|
|
82
|
+
readonly children: readonly Inline[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface DiagramNodeShape {
|
|
86
|
+
readonly x: number;
|
|
87
|
+
readonly y: number;
|
|
88
|
+
readonly width: number;
|
|
89
|
+
readonly height: number;
|
|
90
|
+
readonly radius: number;
|
|
91
|
+
readonly text?: string;
|
|
92
|
+
readonly tex?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface DiagramGeometry {
|
|
96
|
+
readonly width: number;
|
|
97
|
+
readonly height: number;
|
|
98
|
+
readonly nodes: readonly DiagramNodeShape[];
|
|
99
|
+
/** Each edge as its polyline, start to end; the arrowhead sits at the end. */
|
|
100
|
+
readonly edges: readonly (readonly (readonly [number, number])[])[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** What a figure shows: an image file, or something only a browser can draw. */
|
|
104
|
+
export type FigurePicture =
|
|
105
|
+
| { readonly kind: 'image'; readonly src: string; readonly alt: string }
|
|
106
|
+
| { readonly kind: 'diagram'; readonly geometry: DiagramGeometry }
|
|
107
|
+
| { readonly kind: 'capture' };
|
|
108
|
+
|
|
109
|
+
interface FloatBase {
|
|
110
|
+
/** The float's anchor id on its page (`fig-…`, `tab-…`, `dia-…`). */
|
|
111
|
+
readonly anchor: string;
|
|
112
|
+
readonly number: string;
|
|
113
|
+
readonly key?: string;
|
|
114
|
+
readonly short?: string;
|
|
115
|
+
/** The long caption. */
|
|
116
|
+
readonly caption: readonly Inline[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type Block =
|
|
120
|
+
| {
|
|
121
|
+
readonly kind: 'heading';
|
|
122
|
+
readonly level: number;
|
|
123
|
+
readonly anchor?: string;
|
|
124
|
+
readonly number?: string;
|
|
125
|
+
readonly children: readonly Inline[];
|
|
126
|
+
}
|
|
127
|
+
| { readonly kind: 'paragraph'; readonly children: readonly Inline[] }
|
|
128
|
+
| {
|
|
129
|
+
readonly kind: 'list';
|
|
130
|
+
readonly ordered: boolean;
|
|
131
|
+
readonly start?: number;
|
|
132
|
+
readonly items: readonly (readonly Block[])[];
|
|
133
|
+
}
|
|
134
|
+
| {
|
|
135
|
+
readonly kind: 'equation';
|
|
136
|
+
readonly anchor: string;
|
|
137
|
+
readonly number: string;
|
|
138
|
+
readonly key?: string;
|
|
139
|
+
readonly tex: string;
|
|
140
|
+
}
|
|
141
|
+
| { readonly kind: 'displayMath'; readonly tex: string }
|
|
142
|
+
| (FloatBase & {
|
|
143
|
+
readonly kind: 'figure';
|
|
144
|
+
/** The live clause a lab writes after its caption; empty in the static page. */
|
|
145
|
+
readonly live: readonly Inline[];
|
|
146
|
+
readonly picture: FigurePicture;
|
|
147
|
+
/** The lab's own accessible label, when it gives one. */
|
|
148
|
+
readonly alt?: string;
|
|
149
|
+
})
|
|
150
|
+
| (FloatBase & {
|
|
151
|
+
readonly kind: 'table';
|
|
152
|
+
readonly head: readonly (readonly TableCell[])[];
|
|
153
|
+
readonly body: readonly (readonly TableCell[])[];
|
|
154
|
+
})
|
|
155
|
+
| (FloatBase & { readonly kind: 'diagram'; readonly geometry: DiagramGeometry })
|
|
156
|
+
| {
|
|
157
|
+
readonly kind: 'memo';
|
|
158
|
+
readonly anchor: string;
|
|
159
|
+
readonly label: string;
|
|
160
|
+
readonly short?: string;
|
|
161
|
+
readonly title: readonly Inline[];
|
|
162
|
+
readonly children: readonly Block[];
|
|
163
|
+
}
|
|
164
|
+
| {
|
|
165
|
+
readonly kind: 'note';
|
|
166
|
+
readonly anchor: string;
|
|
167
|
+
readonly title: string;
|
|
168
|
+
readonly children: readonly Block[];
|
|
169
|
+
}
|
|
170
|
+
| {
|
|
171
|
+
readonly kind: 'aside';
|
|
172
|
+
readonly variant: string;
|
|
173
|
+
readonly title: readonly Inline[];
|
|
174
|
+
readonly children: readonly Block[];
|
|
175
|
+
}
|
|
176
|
+
| {
|
|
177
|
+
readonly kind: 'fold';
|
|
178
|
+
readonly title?: readonly Inline[];
|
|
179
|
+
readonly children: readonly Block[];
|
|
180
|
+
}
|
|
181
|
+
| {
|
|
182
|
+
readonly kind: 'examples';
|
|
183
|
+
readonly anchor: string;
|
|
184
|
+
readonly number: string;
|
|
185
|
+
readonly title: readonly Inline[];
|
|
186
|
+
readonly items: readonly {
|
|
187
|
+
readonly title: readonly Inline[];
|
|
188
|
+
readonly children: readonly Block[];
|
|
189
|
+
}[];
|
|
190
|
+
}
|
|
191
|
+
| {
|
|
192
|
+
readonly kind: 'check';
|
|
193
|
+
readonly anchor: string;
|
|
194
|
+
readonly number: string;
|
|
195
|
+
readonly assessmentId: string;
|
|
196
|
+
readonly title: readonly Inline[];
|
|
197
|
+
}
|
|
198
|
+
| { readonly kind: 'quote'; readonly children: readonly Block[] }
|
|
199
|
+
| { readonly kind: 'image'; readonly src: string; readonly alt: string }
|
|
200
|
+
| { readonly kind: 'code'; readonly value: string }
|
|
201
|
+
| { readonly kind: 'rule' }
|
|
202
|
+
| {
|
|
203
|
+
readonly kind: 'references';
|
|
204
|
+
readonly items: readonly {
|
|
205
|
+
readonly number: string;
|
|
206
|
+
readonly anchor: string;
|
|
207
|
+
readonly children: readonly Inline[];
|
|
208
|
+
}[];
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export interface NotationEntry {
|
|
212
|
+
readonly key: string;
|
|
213
|
+
readonly anchor: string;
|
|
214
|
+
readonly symbolTex?: string;
|
|
215
|
+
readonly label: readonly Inline[];
|
|
216
|
+
readonly summary: readonly Inline[];
|
|
217
|
+
readonly units?: readonly Inline[];
|
|
218
|
+
readonly formulaTex?: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface StaticPage {
|
|
222
|
+
/** The page's docs slug, e.g. `projectiles/optimal-launch-angle`. */
|
|
223
|
+
readonly slug: string;
|
|
224
|
+
/** Base-relative route of the page, e.g. `projectiles/optimal-launch-angle`. */
|
|
225
|
+
readonly route: string;
|
|
226
|
+
readonly title: string;
|
|
227
|
+
readonly blocks: readonly Block[];
|
|
228
|
+
/** "Notation used on this page", in the page's order. */
|
|
229
|
+
readonly notation: readonly NotationEntry[];
|
|
230
|
+
/** The notation list's page-specific remarks ("On this page, ′ is not an operator …"). */
|
|
231
|
+
readonly notationRemarks: readonly (readonly Inline[])[];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
interface ReadContext {
|
|
235
|
+
readonly route: string;
|
|
236
|
+
readonly base: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const SKIPPED_TAGS = new Set([
|
|
240
|
+
'button',
|
|
241
|
+
'input',
|
|
242
|
+
'nav',
|
|
243
|
+
'noscript',
|
|
244
|
+
'script',
|
|
245
|
+
'select',
|
|
246
|
+
'style',
|
|
247
|
+
'svg',
|
|
248
|
+
'template',
|
|
249
|
+
'textarea',
|
|
250
|
+
'astro-island',
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
/** Page chrome and interaction affordances that never reach paper. */
|
|
254
|
+
const SKIPPED_CLASSES = [
|
|
255
|
+
'sr-only',
|
|
256
|
+
'sl-anchor-link',
|
|
257
|
+
'keyed-equation__controls',
|
|
258
|
+
'float-tools',
|
|
259
|
+
'float-caption__anchor',
|
|
260
|
+
'keyed-figure__tools-stub',
|
|
261
|
+
'short-note__collapse',
|
|
262
|
+
'notation-enhancement-help',
|
|
263
|
+
'pagination-links',
|
|
264
|
+
'explico-site-footer',
|
|
265
|
+
'lesson-breadcrumb',
|
|
266
|
+
// A part page's stats line and chapter list are navigation the site builds;
|
|
267
|
+
// a print export's own contents replace them. Its objectives stay.
|
|
268
|
+
'part-meta',
|
|
269
|
+
'part-chapters',
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
const REF_CLASSES: readonly (readonly [string, RefKind])[] = [
|
|
273
|
+
['equation-ref', 'equation'],
|
|
274
|
+
['figure-ref', 'figure'],
|
|
275
|
+
['table-ref', 'table'],
|
|
276
|
+
['diagram-ref', 'diagram'],
|
|
277
|
+
['section-ref', 'section'],
|
|
278
|
+
['example-ref', 'example'],
|
|
279
|
+
['knowledge-check-ref', 'check'],
|
|
280
|
+
['memo-ref', 'memo'],
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
function skipped(node: HtmlElement): boolean {
|
|
284
|
+
if (SKIPPED_TAGS.has(node.tagName)) return true;
|
|
285
|
+
if (attribute(node, 'aria-hidden') === 'true') return true;
|
|
286
|
+
if (attribute(node, 'data-print-omit') !== undefined) return true;
|
|
287
|
+
return SKIPPED_CLASSES.some((name) => hasClass(node, name));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** All text below `node`, as a reader sees it: no screen-reader-only or hidden helpers. */
|
|
291
|
+
export function readableText(node: HtmlNode): string {
|
|
292
|
+
if (!isElement(node)) {
|
|
293
|
+
return node.nodeName === '#text' ? (node as { value: string }).value : '';
|
|
294
|
+
}
|
|
295
|
+
if (skipped(node)) return '';
|
|
296
|
+
return childrenOf(node).map(readableText).join('');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function collapse(text: string): string {
|
|
300
|
+
return text.replace(/[ \t\r\n\f]+/gu, ' ');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** The TeX source KaTeX was given, from the MathML annotation it keeps. */
|
|
304
|
+
function texOf(katex: HtmlElement): string | undefined {
|
|
305
|
+
const annotation = elements(
|
|
306
|
+
katex,
|
|
307
|
+
(node) =>
|
|
308
|
+
node.tagName === 'annotation' &&
|
|
309
|
+
attribute(node, 'encoding') === 'application/x-tex',
|
|
310
|
+
)[0];
|
|
311
|
+
if (!annotation) return undefined;
|
|
312
|
+
return childrenOf(annotation)
|
|
313
|
+
.map((child) => ('value' in child ? String(child.value) : ''))
|
|
314
|
+
.join('')
|
|
315
|
+
.trim();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function firstByClass(root: HtmlNode, name: string): HtmlElement | undefined {
|
|
319
|
+
return elements(root, (node) => hasClass(node, name))[0];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function cleanRoute(pathname: string, base: string): string {
|
|
323
|
+
let path = pathname;
|
|
324
|
+
if (path.startsWith(base)) path = path.slice(base.length);
|
|
325
|
+
return path
|
|
326
|
+
.replace(/\/index\.html$/u, '')
|
|
327
|
+
.replace(/\.html$/u, '')
|
|
328
|
+
.replace(/^\/+|\/+$/gu, '');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Resolve a link to a course target, or `undefined` when it leaves the course. */
|
|
332
|
+
export function courseTarget(href: string, context: ReadContext): CourseTarget | undefined {
|
|
333
|
+
const trimmed = href.trim();
|
|
334
|
+
if (trimmed.startsWith('#')) {
|
|
335
|
+
return { route: context.route, anchor: decodeURIComponent(trimmed.slice(1)) };
|
|
336
|
+
}
|
|
337
|
+
if (/^[a-z][a-z0-9+.-]*:/iu.test(trimmed) || trimmed.startsWith('//')) return undefined;
|
|
338
|
+
const page = new URL(
|
|
339
|
+
`${context.base}${context.route ? `${context.route}/` : ''}`,
|
|
340
|
+
'https://explico.invalid',
|
|
341
|
+
);
|
|
342
|
+
const url = new URL(trimmed, page);
|
|
343
|
+
return {
|
|
344
|
+
route: cleanRoute(url.pathname, context.base),
|
|
345
|
+
anchor: decodeURIComponent(url.hash.slice(1)),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function trimInlines(inlines: Inline[]): Inline[] {
|
|
350
|
+
const result = [...inlines];
|
|
351
|
+
while (result[0]?.kind === 'text' && !result[0].value.trim()) result.shift();
|
|
352
|
+
while (result.at(-1)?.kind === 'text' && !(result.at(-1) as { value: string }).value.trim()) {
|
|
353
|
+
result.pop();
|
|
354
|
+
}
|
|
355
|
+
const first = result[0];
|
|
356
|
+
if (first?.kind === 'text') result[0] = { kind: 'text', value: first.value.replace(/^ +/u, '') };
|
|
357
|
+
const last = result.at(-1);
|
|
358
|
+
if (last?.kind === 'text') {
|
|
359
|
+
result[result.length - 1] = { kind: 'text', value: last.value.replace(/ +$/u, '') };
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function readLink(node: HtmlElement, context: ReadContext): Inline[] {
|
|
365
|
+
const href = attribute(node, 'href') ?? '';
|
|
366
|
+
const children = readInlines(node, context);
|
|
367
|
+
if (hasClass(node, 'citation-ref')) {
|
|
368
|
+
const number = attribute(node, 'data-citation-number') ?? readableText(node).replace(/\D/gu, '');
|
|
369
|
+
return [
|
|
370
|
+
{
|
|
371
|
+
kind: 'cite',
|
|
372
|
+
number,
|
|
373
|
+
anchor: href.replace(/^#/u, ''),
|
|
374
|
+
sourceId: attribute(node, 'data-citation-id'),
|
|
375
|
+
locator: attribute(node, 'data-citation-locator') || undefined,
|
|
376
|
+
},
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
if (hasClass(node, 'memo-marker')) {
|
|
380
|
+
return [
|
|
381
|
+
{
|
|
382
|
+
kind: 'memoMark',
|
|
383
|
+
anchor: href.replace(/^#/u, ''),
|
|
384
|
+
label: attribute(node, 'data-memo-number') ?? readableText(node).trim(),
|
|
385
|
+
},
|
|
386
|
+
];
|
|
387
|
+
}
|
|
388
|
+
if (hasClass(node, 'short-note-marker')) {
|
|
389
|
+
return [{ kind: 'noteMark', anchor: href.replace(/^#/u, ''), children }];
|
|
390
|
+
}
|
|
391
|
+
if (hasClass(node, 'notation-term')) {
|
|
392
|
+
return [{ kind: 'term', key: attribute(node, 'data-notation-key') ?? '', children }];
|
|
393
|
+
}
|
|
394
|
+
if (!href) return children;
|
|
395
|
+
const target = courseTarget(href, context);
|
|
396
|
+
if (!target) return [{ kind: 'link', href, children }];
|
|
397
|
+
const refKind =
|
|
398
|
+
REF_CLASSES.find(([name]) => hasClass(node, name))?.[1] ??
|
|
399
|
+
(target.route === context.route ? 'anchor' : 'page');
|
|
400
|
+
return [{ kind: 'ref', refKind, target, children }];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function readInline(node: HtmlNode, context: ReadContext): Inline[] {
|
|
404
|
+
if (!isElement(node)) {
|
|
405
|
+
if (node.nodeName !== '#text') return [];
|
|
406
|
+
const value = collapse((node as { value: string }).value);
|
|
407
|
+
return value ? [{ kind: 'text', value }] : [];
|
|
408
|
+
}
|
|
409
|
+
if (hasClass(node, 'katex')) {
|
|
410
|
+
const tex = texOf(node);
|
|
411
|
+
return tex ? [{ kind: 'math', tex }] : [];
|
|
412
|
+
}
|
|
413
|
+
if (skipped(node)) return [];
|
|
414
|
+
switch (node.tagName) {
|
|
415
|
+
case 'em':
|
|
416
|
+
case 'i':
|
|
417
|
+
return [{ kind: 'emphasis', children: readInlines(node, context) }];
|
|
418
|
+
case 'strong':
|
|
419
|
+
case 'b':
|
|
420
|
+
return [{ kind: 'strong', children: readInlines(node, context) }];
|
|
421
|
+
case 'sup':
|
|
422
|
+
if (hasClass(node, 'memo-marker-wrap')) return readInlines(node, context);
|
|
423
|
+
return [{ kind: 'superscript', children: readInlines(node, context) }];
|
|
424
|
+
case 'sub':
|
|
425
|
+
return [{ kind: 'subscript', children: readInlines(node, context) }];
|
|
426
|
+
case 'code':
|
|
427
|
+
case 'kbd':
|
|
428
|
+
return [{ kind: 'code', value: readableText(node) }];
|
|
429
|
+
case 'br':
|
|
430
|
+
return [{ kind: 'break' }];
|
|
431
|
+
case 'a':
|
|
432
|
+
return readLink(node, context);
|
|
433
|
+
default:
|
|
434
|
+
return readInlines(node, context);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function readInlines(node: HtmlNode, context: ReadContext): Inline[] {
|
|
439
|
+
const result: Inline[] = [];
|
|
440
|
+
for (const child of childrenOf(node)) {
|
|
441
|
+
for (const inline of readInline(child, context)) {
|
|
442
|
+
const previous = result.at(-1);
|
|
443
|
+
if (inline.kind === 'text' && previous?.kind === 'text') {
|
|
444
|
+
result[result.length - 1] = {
|
|
445
|
+
kind: 'text',
|
|
446
|
+
value: collapse(previous.value + inline.value),
|
|
447
|
+
};
|
|
448
|
+
} else {
|
|
449
|
+
result.push(inline);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Inline content of `node`, trimmed at both ends as a block's content is. */
|
|
457
|
+
function inlineContent(node: HtmlNode | undefined, context: ReadContext): Inline[] {
|
|
458
|
+
return node ? trimInlines(readInlines(node, context)) : [];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function numberAttribute(node: HtmlElement, name: string): number {
|
|
462
|
+
return Number.parseFloat(attribute(node, name) ?? '0') || 0;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Node boxes and edge polylines of a built `<Diagram>`'s SVG. */
|
|
466
|
+
export function readDiagramGeometry(figure: HtmlElement): DiagramGeometry | undefined {
|
|
467
|
+
const svg = elements(figure, (node) => node.tagName === 'svg' && !!attribute(node, 'viewBox'))
|
|
468
|
+
.find((node) => elements(node, (child) => hasClass(child, 'diagram-node')).length > 0);
|
|
469
|
+
if (!svg) return undefined;
|
|
470
|
+
const [, , width, height] = (attribute(svg, 'viewBox') ?? '0 0 0 0')
|
|
471
|
+
.trim()
|
|
472
|
+
.split(/[\s,]+/u)
|
|
473
|
+
.map(Number);
|
|
474
|
+
const nodes = elements(svg, (node) => hasClass(node, 'diagram-node')).flatMap((group) => {
|
|
475
|
+
const rect = elements(group, (node) => node.tagName === 'rect')[0];
|
|
476
|
+
if (!rect) return [];
|
|
477
|
+
const text = firstByClass(group, 'diagram-node-text');
|
|
478
|
+
const formula = firstByClass(group, 'diagram-node-formula');
|
|
479
|
+
const katex = formula ? elements(formula, (node) => hasClass(node, 'katex'))[0] : undefined;
|
|
480
|
+
const label = text ? collapse(readableText(text)).trim() : '';
|
|
481
|
+
const tex = katex ? texOf(katex) : undefined;
|
|
482
|
+
return [
|
|
483
|
+
{
|
|
484
|
+
x: numberAttribute(rect, 'x'),
|
|
485
|
+
y: numberAttribute(rect, 'y'),
|
|
486
|
+
width: numberAttribute(rect, 'width'),
|
|
487
|
+
height: numberAttribute(rect, 'height'),
|
|
488
|
+
radius: numberAttribute(rect, 'rx'),
|
|
489
|
+
...(label ? { text: label } : {}),
|
|
490
|
+
...(tex ? { tex } : {}),
|
|
491
|
+
},
|
|
492
|
+
];
|
|
493
|
+
});
|
|
494
|
+
const edges = elements(svg, (node) => node.tagName === 'path' && hasClass(node, 'diagram-edge'))
|
|
495
|
+
.map((path) =>
|
|
496
|
+
[...(attribute(path, 'd') ?? '').matchAll(/[ML]\s*(-?[\d.]+)[\s,]+(-?[\d.]+)/gu)].map(
|
|
497
|
+
(match) => [Number(match[1]), Number(match[2])] as const,
|
|
498
|
+
),
|
|
499
|
+
)
|
|
500
|
+
.filter((points) => points.length >= 2);
|
|
501
|
+
return { width: width ?? 0, height: height ?? 0, nodes, edges };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function readCaption(figcaption: HtmlElement | undefined, context: ReadContext): Inline[] {
|
|
505
|
+
if (!figcaption) return [];
|
|
506
|
+
const text = firstByClass(figcaption, 'float-caption__text');
|
|
507
|
+
return inlineContent(text, context);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function floatBase(node: HtmlElement, prefix: string, context: ReadContext): FloatBase {
|
|
511
|
+
const figcaption = elements(node, (child) => child.tagName === 'figcaption')[0];
|
|
512
|
+
const key = attribute(node, `data-${prefix}-key`);
|
|
513
|
+
const short = attribute(node, `data-${prefix}-short`);
|
|
514
|
+
return {
|
|
515
|
+
anchor: attribute(node, 'id') ?? '',
|
|
516
|
+
number: attribute(node, `data-${prefix}-number`) ?? '',
|
|
517
|
+
...(key ? { key } : {}),
|
|
518
|
+
...(short ? { short } : {}),
|
|
519
|
+
caption: readCaption(figcaption, context),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function readFigurePicture(body: HtmlElement | undefined): FigurePicture {
|
|
524
|
+
if (!body) return { kind: 'capture' };
|
|
525
|
+
const diagram = elements(body, (node) => node.tagName === 'figure' && hasClass(node, 'diagram'))[0];
|
|
526
|
+
if (diagram) {
|
|
527
|
+
const geometry = readDiagramGeometry(diagram);
|
|
528
|
+
if (geometry) return { kind: 'diagram', geometry };
|
|
529
|
+
}
|
|
530
|
+
const meaningful = childrenOf(body).filter(
|
|
531
|
+
(child) =>
|
|
532
|
+
(isElement(child) && !['script', 'style'].includes(child.tagName)) ||
|
|
533
|
+
(child.nodeName === '#text' && (child as { value: string }).value.trim()),
|
|
534
|
+
);
|
|
535
|
+
const only = meaningful.length === 1 && isElement(meaningful[0]!) ? meaningful[0] : undefined;
|
|
536
|
+
const image =
|
|
537
|
+
only?.tagName === 'img'
|
|
538
|
+
? only
|
|
539
|
+
: only?.tagName === 'picture'
|
|
540
|
+
? elements(only, (node) => node.tagName === 'img')[0]
|
|
541
|
+
: undefined;
|
|
542
|
+
if (image && attribute(image, 'src')) {
|
|
543
|
+
return { kind: 'image', src: attribute(image, 'src')!, alt: attribute(image, 'alt') ?? '' };
|
|
544
|
+
}
|
|
545
|
+
return { kind: 'capture' };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function readTableCells(row: HtmlElement, context: ReadContext): TableCell[] {
|
|
549
|
+
return childrenOf(row)
|
|
550
|
+
.filter((cell): cell is HtmlElement => isElement(cell) && ['td', 'th'].includes(cell.tagName))
|
|
551
|
+
.map((cell) => {
|
|
552
|
+
const style = attribute(cell, 'style') ?? '';
|
|
553
|
+
const align =
|
|
554
|
+
attribute(cell, 'align') ?? /text-align:\s*(left|center|right)/u.exec(style)?.[1];
|
|
555
|
+
return {
|
|
556
|
+
header: cell.tagName === 'th',
|
|
557
|
+
colspan: Number.parseInt(attribute(cell, 'colspan') ?? '1', 10) || 1,
|
|
558
|
+
rowspan: Number.parseInt(attribute(cell, 'rowspan') ?? '1', 10) || 1,
|
|
559
|
+
...(align === 'left' || align === 'center' || align === 'right' ? { align } : {}),
|
|
560
|
+
children: inlineContent(cell, context),
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function readTable(table: HtmlElement, context: ReadContext) {
|
|
566
|
+
const rows = (section: string) =>
|
|
567
|
+
elements(table, (node) => node.tagName === section)
|
|
568
|
+
.flatMap((group) =>
|
|
569
|
+
childrenOf(group).filter(
|
|
570
|
+
(row): row is HtmlElement =>
|
|
571
|
+
isElement(row) && row.tagName === 'tr' && !hasClass(row, 'keyed-table__more-row'),
|
|
572
|
+
),
|
|
573
|
+
)
|
|
574
|
+
.map((row) => readTableCells(row, context));
|
|
575
|
+
const head = rows('thead');
|
|
576
|
+
const body = rows('tbody');
|
|
577
|
+
if (head.length === 0 && body.length === 0) {
|
|
578
|
+
return {
|
|
579
|
+
head: [],
|
|
580
|
+
body: childrenOf(table)
|
|
581
|
+
.filter((row): row is HtmlElement => isElement(row) && row.tagName === 'tr')
|
|
582
|
+
.map((row) => readTableCells(row, context)),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
return { head, body };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function headingOf(wrapper: HtmlElement): HtmlElement | undefined {
|
|
589
|
+
return childrenOf(wrapper).find(
|
|
590
|
+
(child): child is HtmlElement => isElement(child) && /^h[1-6]$/u.test(child.tagName),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function blockTitle(heading: HtmlElement | undefined, context: ReadContext): Inline[] {
|
|
595
|
+
if (!heading) return [];
|
|
596
|
+
return inlineContent(firstByClass(heading, 'block-heading__title') ?? heading, context);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function readExamples(node: HtmlElement, context: ReadContext): Block {
|
|
600
|
+
const heading = elements(node, (child) => hasClass(child, 'block-heading'))[0];
|
|
601
|
+
const items = elements(node, (child) => hasClass(child, 'compact-example')).map((section) => {
|
|
602
|
+
const title = elements(section, (child) => hasClass(child, 'compact-example__heading'))[0];
|
|
603
|
+
return {
|
|
604
|
+
title: title
|
|
605
|
+
? inlineContent(title, context)
|
|
606
|
+
: [{ kind: 'text', value: attribute(section, 'data-example-label') ?? '' } as const],
|
|
607
|
+
children: readBlocks(section, context, (child) => child !== title),
|
|
608
|
+
};
|
|
609
|
+
});
|
|
610
|
+
return {
|
|
611
|
+
kind: 'examples',
|
|
612
|
+
anchor: attribute(heading ?? node, 'id') ?? '',
|
|
613
|
+
number: (heading && attribute(heading, 'data-ex-number')) ?? '',
|
|
614
|
+
title: blockTitle(heading, context),
|
|
615
|
+
items,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function readNotationEntry(entry: HtmlElement, context: ReadContext): NotationEntry {
|
|
620
|
+
const key = attribute(entry, 'data-notation-entry') ?? '';
|
|
621
|
+
const symbol = elements(entry, (node) => hasClass(node, 'notation-symbol'))[0];
|
|
622
|
+
const symbolKatex = symbol ? elements(symbol, (node) => hasClass(node, 'katex'))[0] : undefined;
|
|
623
|
+
const label = elements(entry, (node) => node.tagName === 'a' && !!attribute(node, 'data-notation-link'))[0];
|
|
624
|
+
const summary = elements(entry, (node) => attribute(node, 'data-notation-summary') !== undefined)[0];
|
|
625
|
+
const units = elements(entry, (node) => attribute(node, 'data-notation-units') !== undefined)[0];
|
|
626
|
+
const formula = elements(entry, (node) => attribute(node, 'data-notation-formula') !== undefined)[0];
|
|
627
|
+
const formulaKatex = formula ? elements(formula, (node) => hasClass(node, 'katex'))[0] : undefined;
|
|
628
|
+
const unitsInlines = units
|
|
629
|
+
? trimInlines(
|
|
630
|
+
readInlines(units, context).filter(
|
|
631
|
+
(inline) => !(inline.kind === 'strong'),
|
|
632
|
+
),
|
|
633
|
+
)
|
|
634
|
+
: undefined;
|
|
635
|
+
const symbolTex = symbolKatex ? texOf(symbolKatex) : undefined;
|
|
636
|
+
const formulaTex = formulaKatex ? texOf(formulaKatex) : undefined;
|
|
637
|
+
return {
|
|
638
|
+
key,
|
|
639
|
+
anchor: attribute(entry, 'id') ?? `notation-${key}`,
|
|
640
|
+
...(symbolTex ? { symbolTex } : {}),
|
|
641
|
+
label: inlineContent(label, context),
|
|
642
|
+
summary: inlineContent(summary, context),
|
|
643
|
+
...(unitsInlines && unitsInlines.length > 0 ? { units: unitsInlines } : {}),
|
|
644
|
+
...(formulaTex ? { formulaTex } : {}),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Convert the block-level children of `node`. Loose inline content between
|
|
650
|
+
* blocks (text directly inside a `<div>`) becomes a paragraph of its own.
|
|
651
|
+
*/
|
|
652
|
+
function readBlocks(
|
|
653
|
+
node: HtmlNode,
|
|
654
|
+
context: ReadContext,
|
|
655
|
+
keep: (child: HtmlNode) => boolean = () => true,
|
|
656
|
+
): Block[] {
|
|
657
|
+
const blocks: Block[] = [];
|
|
658
|
+
let pending: Inline[] = [];
|
|
659
|
+
const flush = () => {
|
|
660
|
+
const content = trimInlines(pending);
|
|
661
|
+
if (content.length > 0) blocks.push({ kind: 'paragraph', children: content });
|
|
662
|
+
pending = [];
|
|
663
|
+
};
|
|
664
|
+
for (const child of childrenOf(node)) {
|
|
665
|
+
if (!keep(child)) continue;
|
|
666
|
+
const block = isElement(child) ? readBlock(child, context) : undefined;
|
|
667
|
+
if (block === undefined) {
|
|
668
|
+
pending.push(...readInline(child, context));
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
flush();
|
|
672
|
+
blocks.push(...block);
|
|
673
|
+
}
|
|
674
|
+
flush();
|
|
675
|
+
return blocks;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/** A block-level element as blocks, or `undefined` when it is inline content. */
|
|
679
|
+
function readBlock(node: HtmlElement, context: ReadContext): Block[] | undefined {
|
|
680
|
+
if (hasClass(node, 'katex-display')) {
|
|
681
|
+
const katex = elements(node, (child) => hasClass(child, 'katex'))[0];
|
|
682
|
+
const tex = katex ? texOf(katex) : undefined;
|
|
683
|
+
return tex ? [{ kind: 'displayMath', tex }] : [];
|
|
684
|
+
}
|
|
685
|
+
if (skipped(node)) return [];
|
|
686
|
+
if (hasClass(node, 'notation-layer')) return [];
|
|
687
|
+
if (hasClass(node, 'keyed-equation')) {
|
|
688
|
+
const katex = elements(node, (child) => hasClass(child, 'katex'))[0];
|
|
689
|
+
const key = attribute(node, 'data-eq-key');
|
|
690
|
+
return [
|
|
691
|
+
{
|
|
692
|
+
kind: 'equation',
|
|
693
|
+
anchor: attribute(node, 'id') ?? '',
|
|
694
|
+
number: attribute(node, 'data-eq-number') ?? '',
|
|
695
|
+
...(key ? { key } : {}),
|
|
696
|
+
tex: (katex && texOf(katex)) ?? '',
|
|
697
|
+
},
|
|
698
|
+
];
|
|
699
|
+
}
|
|
700
|
+
if (node.tagName === 'figure' && hasClass(node, 'keyed-figure')) {
|
|
701
|
+
const body = firstByClass(node, 'keyed-figure__body');
|
|
702
|
+
const island = body ? elements(body, (child) => child.tagName === 'astro-island')[0] : undefined;
|
|
703
|
+
const figcaption = elements(node, (child) => child.tagName === 'figcaption')[0];
|
|
704
|
+
const live = figcaption ? firstByClass(figcaption, 'float-caption__state') : undefined;
|
|
705
|
+
const alt = island ? undefined : body && attribute(body, 'aria-label');
|
|
706
|
+
return [
|
|
707
|
+
{
|
|
708
|
+
kind: 'figure',
|
|
709
|
+
...floatBase(node, 'fig', context),
|
|
710
|
+
live: inlineContent(live, context),
|
|
711
|
+
picture: readFigurePicture(body),
|
|
712
|
+
...(alt ? { alt } : {}),
|
|
713
|
+
},
|
|
714
|
+
];
|
|
715
|
+
}
|
|
716
|
+
if (node.tagName === 'figure' && hasClass(node, 'keyed-table')) {
|
|
717
|
+
const table = elements(node, (child) => child.tagName === 'table')[0];
|
|
718
|
+
return [
|
|
719
|
+
{
|
|
720
|
+
kind: 'table',
|
|
721
|
+
...floatBase(node, 'tab', context),
|
|
722
|
+
...(table ? readTable(table, context) : { head: [], body: [] }),
|
|
723
|
+
},
|
|
724
|
+
];
|
|
725
|
+
}
|
|
726
|
+
if (node.tagName === 'figure' && hasClass(node, 'diagram')) {
|
|
727
|
+
const geometry = readDiagramGeometry(node);
|
|
728
|
+
return [
|
|
729
|
+
{
|
|
730
|
+
kind: 'diagram',
|
|
731
|
+
...floatBase(node, 'dia', context),
|
|
732
|
+
geometry: geometry ?? { width: 0, height: 0, nodes: [], edges: [] },
|
|
733
|
+
},
|
|
734
|
+
];
|
|
735
|
+
}
|
|
736
|
+
if (hasClass(node, 'memo-block')) {
|
|
737
|
+
const heading = elements(node, (child) => hasClass(child, 'memo-block__heading'))[0];
|
|
738
|
+
const body = firstByClass(node, 'memo-block__body');
|
|
739
|
+
const short = attribute(node, 'data-memo-short');
|
|
740
|
+
return [
|
|
741
|
+
{
|
|
742
|
+
kind: 'memo',
|
|
743
|
+
anchor: (heading && attribute(heading, 'id')) ?? `memo-${attribute(node, 'data-memo-key') ?? ''}`,
|
|
744
|
+
label: attribute(node, 'data-memo-number') ?? '',
|
|
745
|
+
...(short ? { short } : {}),
|
|
746
|
+
title: inlineContent(heading, context),
|
|
747
|
+
children: body ? readBlocks(body, context) : [],
|
|
748
|
+
},
|
|
749
|
+
];
|
|
750
|
+
}
|
|
751
|
+
if (node.tagName === 'aside' && hasClass(node, 'short-note')) {
|
|
752
|
+
const body = firstByClass(node, 'short-note__body');
|
|
753
|
+
return [
|
|
754
|
+
{
|
|
755
|
+
kind: 'note',
|
|
756
|
+
anchor: attribute(node, 'id') ?? '',
|
|
757
|
+
title: attribute(node, 'aria-label') ?? '',
|
|
758
|
+
children: body ? readBlocks(body, context) : [],
|
|
759
|
+
},
|
|
760
|
+
];
|
|
761
|
+
}
|
|
762
|
+
if (node.tagName === 'aside' && hasClass(node, 'starlight-aside')) {
|
|
763
|
+
const variant =
|
|
764
|
+
/starlight-aside--([a-z]+)/u.exec(attribute(node, 'class') ?? '')?.[1] ?? 'note';
|
|
765
|
+
const title = firstByClass(node, 'starlight-aside__title');
|
|
766
|
+
const content = firstByClass(node, 'starlight-aside__content');
|
|
767
|
+
return [
|
|
768
|
+
{
|
|
769
|
+
kind: 'aside',
|
|
770
|
+
variant,
|
|
771
|
+
title: inlineContent(title, context),
|
|
772
|
+
children: content ? readBlocks(content, context) : [],
|
|
773
|
+
},
|
|
774
|
+
];
|
|
775
|
+
}
|
|
776
|
+
if (hasClass(node, 'fold')) {
|
|
777
|
+
const label = firstByClass(node, 'fold__label');
|
|
778
|
+
const body = firstByClass(node, 'fold__body');
|
|
779
|
+
const namesFloats = attribute(node, 'data-names-floats') !== undefined;
|
|
780
|
+
return [
|
|
781
|
+
{
|
|
782
|
+
kind: 'fold',
|
|
783
|
+
...(label && !namesFloats ? { title: inlineContent(label, context) } : {}),
|
|
784
|
+
children: body ? readBlocks(body, context) : [],
|
|
785
|
+
},
|
|
786
|
+
];
|
|
787
|
+
}
|
|
788
|
+
if (node.tagName === 'compact-examples' || hasClass(node, 'compact-examples')) {
|
|
789
|
+
return [readExamples(node, context)];
|
|
790
|
+
}
|
|
791
|
+
if (hasClass(node, 'sl-heading-wrapper')) {
|
|
792
|
+
const heading = headingOf(node);
|
|
793
|
+
if (!heading) return readBlocks(node, context);
|
|
794
|
+
if (attribute(heading, 'data-block-kind') === 'check') {
|
|
795
|
+
const anchor = attribute(heading, 'id') ?? '';
|
|
796
|
+
return [
|
|
797
|
+
{
|
|
798
|
+
kind: 'check',
|
|
799
|
+
anchor,
|
|
800
|
+
number: attribute(heading, 'data-check-number') ?? '',
|
|
801
|
+
assessmentId: anchor.replace(/^check-/u, ''),
|
|
802
|
+
title: blockTitle(heading, context),
|
|
803
|
+
},
|
|
804
|
+
];
|
|
805
|
+
}
|
|
806
|
+
return readBlock(heading, context) ?? [];
|
|
807
|
+
}
|
|
808
|
+
if (/^h[1-6]$/u.test(node.tagName)) {
|
|
809
|
+
if (hasClass(node, 'citations-heading')) return [];
|
|
810
|
+
const anchor = attribute(node, 'id');
|
|
811
|
+
const number = attribute(node, 'data-sec-number');
|
|
812
|
+
return [
|
|
813
|
+
{
|
|
814
|
+
kind: 'heading',
|
|
815
|
+
level: Number(node.tagName.slice(1)),
|
|
816
|
+
...(anchor ? { anchor } : {}),
|
|
817
|
+
...(number ? { number } : {}),
|
|
818
|
+
children: inlineContent(node, context),
|
|
819
|
+
},
|
|
820
|
+
];
|
|
821
|
+
}
|
|
822
|
+
if (node.tagName === 'ol' && hasClass(node, 'citations-list')) {
|
|
823
|
+
return [
|
|
824
|
+
{
|
|
825
|
+
kind: 'references',
|
|
826
|
+
items: childrenOf(node)
|
|
827
|
+
.filter((item): item is HtmlElement => isElement(item) && item.tagName === 'li')
|
|
828
|
+
.map((item) => {
|
|
829
|
+
const anchor = attribute(item, 'id') ?? '';
|
|
830
|
+
return {
|
|
831
|
+
number: anchor.replace(/^cite-/u, ''),
|
|
832
|
+
anchor,
|
|
833
|
+
children: trimInlines(
|
|
834
|
+
readInlines(item, context).filter(
|
|
835
|
+
(inline) => !(inline.kind === 'ref' && inline.target.anchor.startsWith('cite-ref-')),
|
|
836
|
+
),
|
|
837
|
+
),
|
|
838
|
+
};
|
|
839
|
+
}),
|
|
840
|
+
},
|
|
841
|
+
];
|
|
842
|
+
}
|
|
843
|
+
switch (node.tagName) {
|
|
844
|
+
case 'p': {
|
|
845
|
+
const children = inlineContent(node, context);
|
|
846
|
+
return children.length > 0 ? [{ kind: 'paragraph', children }] : [];
|
|
847
|
+
}
|
|
848
|
+
case 'ul':
|
|
849
|
+
case 'ol': {
|
|
850
|
+
const start = Number.parseInt(attribute(node, 'start') ?? '', 10);
|
|
851
|
+
return [
|
|
852
|
+
{
|
|
853
|
+
kind: 'list',
|
|
854
|
+
ordered: node.tagName === 'ol',
|
|
855
|
+
...(Number.isFinite(start) ? { start } : {}),
|
|
856
|
+
items: childrenOf(node)
|
|
857
|
+
.filter((item): item is HtmlElement => isElement(item) && item.tagName === 'li')
|
|
858
|
+
.map((item) => readBlocks(item, context)),
|
|
859
|
+
},
|
|
860
|
+
];
|
|
861
|
+
}
|
|
862
|
+
case 'blockquote':
|
|
863
|
+
return [{ kind: 'quote', children: readBlocks(node, context) }];
|
|
864
|
+
case 'pre':
|
|
865
|
+
return [{ kind: 'code', value: readableText(node).replace(/\n$/u, '') }];
|
|
866
|
+
case 'hr':
|
|
867
|
+
return [{ kind: 'rule' }];
|
|
868
|
+
case 'img': {
|
|
869
|
+
const src = attribute(node, 'src');
|
|
870
|
+
return src ? [{ kind: 'image', src, alt: attribute(node, 'alt') ?? '' }] : [];
|
|
871
|
+
}
|
|
872
|
+
case 'table':
|
|
873
|
+
return [
|
|
874
|
+
{
|
|
875
|
+
kind: 'table',
|
|
876
|
+
anchor: attribute(node, 'id') ?? '',
|
|
877
|
+
number: '',
|
|
878
|
+
caption: [],
|
|
879
|
+
...readTable(node, context),
|
|
880
|
+
},
|
|
881
|
+
];
|
|
882
|
+
case 'details':
|
|
883
|
+
return readBlocks(node, context, (child) => !(isElement(child) && child.tagName === 'summary'));
|
|
884
|
+
case 'div':
|
|
885
|
+
case 'section':
|
|
886
|
+
case 'article':
|
|
887
|
+
case 'header':
|
|
888
|
+
case 'footer':
|
|
889
|
+
case 'main':
|
|
890
|
+
case 'figure':
|
|
891
|
+
case 'aside':
|
|
892
|
+
case 'dl':
|
|
893
|
+
case 'dd':
|
|
894
|
+
case 'dt':
|
|
895
|
+
case 'li':
|
|
896
|
+
case 'compact-example':
|
|
897
|
+
return readBlocks(node, context);
|
|
898
|
+
default:
|
|
899
|
+
return undefined;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Inline content from an HTML fragment of a page — a lab's live caption
|
|
905
|
+
* clause, which only exists once the lab has run.
|
|
906
|
+
*/
|
|
907
|
+
export function readInlineFragment(html: string, route: string, base = '/'): Inline[] {
|
|
908
|
+
return inlineContent(parseFragment(html), { route, base });
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/** Read the static reading tree of one built page. */
|
|
912
|
+
export function readStaticPage(html: string, slug: string, route: string): StaticPage {
|
|
913
|
+
const document = parse(html);
|
|
914
|
+
const base = inferBasePrefix(document);
|
|
915
|
+
const context: ReadContext = { route, base };
|
|
916
|
+
const main = elements(document, (node) => node.tagName === 'main')[0];
|
|
917
|
+
if (!main) throw new Error(`Built page ${slug} has no <main> element.`);
|
|
918
|
+
const title = elements(main, (node) => node.tagName === 'h1')[0];
|
|
919
|
+
const content = elements(main, (node) => hasClass(node, 'sl-markdown-content'))[0];
|
|
920
|
+
if (!content) throw new Error(`Built page ${slug} has no .sl-markdown-content element.`);
|
|
921
|
+
const layer = elements(
|
|
922
|
+
main,
|
|
923
|
+
(node) => hasClass(node, 'notation-layer') && attribute(node, 'data-notation-layer-id') === 'page-notation',
|
|
924
|
+
)[0];
|
|
925
|
+
const entries = layer
|
|
926
|
+
? elements(layer, (node) => hasClass(node, 'notation-entry') && attribute(node, 'data-notation-entry') !== undefined)
|
|
927
|
+
: [];
|
|
928
|
+
const remarks = layer
|
|
929
|
+
? elements(layer, (node) => node.tagName === 'ul' && hasClass(node, 'notation-suspended')).flatMap(
|
|
930
|
+
(list) =>
|
|
931
|
+
childrenOf(list)
|
|
932
|
+
.filter((item): item is HtmlElement => isElement(item) && item.tagName === 'li')
|
|
933
|
+
.map((item) => inlineContent(item, context)),
|
|
934
|
+
)
|
|
935
|
+
: [];
|
|
936
|
+
return {
|
|
937
|
+
slug,
|
|
938
|
+
route,
|
|
939
|
+
title: title ? collapse(readableText(title)).trim() : slug,
|
|
940
|
+
blocks: readBlocks(content, context),
|
|
941
|
+
notation: entries.map((entry) => readNotationEntry(entry, context)),
|
|
942
|
+
notationRemarks: remarks,
|
|
943
|
+
};
|
|
944
|
+
}
|