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,632 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BlockContent,
|
|
3
|
+
DefinitionContent,
|
|
4
|
+
List,
|
|
5
|
+
ListItem,
|
|
6
|
+
Paragraph,
|
|
7
|
+
PhrasingContent,
|
|
8
|
+
Root,
|
|
9
|
+
RootContent,
|
|
10
|
+
Table,
|
|
11
|
+
TableCell as MdTableCell,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from 'mdast';
|
|
14
|
+
import { gfmTableToMarkdown } from 'mdast-util-gfm-table';
|
|
15
|
+
import { mathToMarkdown } from 'mdast-util-math';
|
|
16
|
+
import { toMarkdown } from 'mdast-util-to-markdown';
|
|
17
|
+
|
|
18
|
+
import { captionTitle } from '../../../components/float-name';
|
|
19
|
+
import { stripNotationMarkers } from '../../../reference/math-glyphs.mjs';
|
|
20
|
+
import type { ContentSegment } from '../../../reference/mixed-content';
|
|
21
|
+
import type { CheckItem } from '../latex/serialize';
|
|
22
|
+
import type {
|
|
23
|
+
Block,
|
|
24
|
+
CourseTarget,
|
|
25
|
+
DiagramGeometry,
|
|
26
|
+
Inline,
|
|
27
|
+
NotationEntry,
|
|
28
|
+
StaticPage,
|
|
29
|
+
TableCell,
|
|
30
|
+
} from '../static-tree';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Serialize explico's static reading tree to Markdown: GitHub's flavour, with
|
|
34
|
+
* `$…$` and `$$…$$` math for remark-math, KaTeX or MathJax (decisions D2, D3).
|
|
35
|
+
* It builds an mdast tree and lets `mdast-util-to-markdown` write it, so every
|
|
36
|
+
* character that means something to Markdown is escaped where it must be.
|
|
37
|
+
*
|
|
38
|
+
* What a reader of the file needs from the website comes along as text: each
|
|
39
|
+
* heading and float keeps its number, a display equation its `\tag{…}`, and
|
|
40
|
+
* every anchor an `<a id>` the links in the file land on. A reference into a
|
|
41
|
+
* page of the same download is a link into its file; any other goes to the
|
|
42
|
+
* website. The author's `\explain{key}{…}` markers are stripped from the
|
|
43
|
+
* math. Memos, notes, examples and checks are titled quotes, always open;
|
|
44
|
+
* Starlight's asides are GitHub alerts; each chapter ends with its
|
|
45
|
+
* references, its notation and the answers to its checks.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
export interface MarkdownChapterContext {
|
|
49
|
+
readonly page: StaticPage;
|
|
50
|
+
/** Every page of this download by route, with its file relative to this one (`''` for this file). */
|
|
51
|
+
readonly documentFiles: ReadonlyMap<string, string>;
|
|
52
|
+
/** Published course root, for links that leave the download. */
|
|
53
|
+
readonly canonicalBaseUrl?: string;
|
|
54
|
+
/** Picture files written for this page, by float anchor or image `src`, relative to this file. */
|
|
55
|
+
readonly pictures: ReadonlyMap<string, string>;
|
|
56
|
+
/** Answers of this page's checks, by assessment id, in authored order. */
|
|
57
|
+
readonly checkItems: ReadonlyMap<string, readonly CheckItem[]>;
|
|
58
|
+
/** Course title, version and the page's live URL, for the line under its title. */
|
|
59
|
+
readonly source?: { readonly course: string; readonly version?: string; readonly url?: string };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type Phrasing = PhrasingContent;
|
|
63
|
+
|
|
64
|
+
const text = (value: string): Phrasing => ({ type: 'text', value });
|
|
65
|
+
const html = (value: string): Phrasing => ({ type: 'html', value });
|
|
66
|
+
const strong = (children: Phrasing[]): Phrasing => ({ type: 'strong', children });
|
|
67
|
+
const emphasis = (children: Phrasing[]): Phrasing => ({ type: 'emphasis', children });
|
|
68
|
+
const paragraph = (children: Phrasing[]): Paragraph => ({ type: 'paragraph', children });
|
|
69
|
+
const link = (url: string, children: Phrasing[]): Phrasing => ({ type: 'link', url, children });
|
|
70
|
+
const anchor = (id: string): Phrasing => html(`<a id="${id.replace(/"/gu, '"')}"></a>`);
|
|
71
|
+
|
|
72
|
+
/** Math for a Markdown renderer: plain TeX, the author's markers stripped. */
|
|
73
|
+
function plainTex(tex: string): string {
|
|
74
|
+
return stripNotationMarkers(tex).trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A pipe ends a table cell even inside `$…$` (GitHub and remark-gfm split
|
|
79
|
+
* the row first), so math in a cell spells its bars as commands.
|
|
80
|
+
*/
|
|
81
|
+
function cellTex(tex: string): string {
|
|
82
|
+
return tex.replace(/\\\|/gu, '\\Vert ').replace(/\|/gu, '\\vert ');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The GitHub alert a Starlight aside becomes. */
|
|
86
|
+
const ALERTS: Readonly<Record<string, string>> = {
|
|
87
|
+
note: 'NOTE',
|
|
88
|
+
tip: 'TIP',
|
|
89
|
+
caution: 'WARNING',
|
|
90
|
+
danger: 'CAUTION',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** What a phrase says, without markup: a link's text, a caption's words. */
|
|
94
|
+
function plainText(inlines: readonly Inline[]): string {
|
|
95
|
+
return inlines
|
|
96
|
+
.map((inline) => {
|
|
97
|
+
switch (inline.kind) {
|
|
98
|
+
case 'text':
|
|
99
|
+
case 'code':
|
|
100
|
+
return inline.value;
|
|
101
|
+
case 'math':
|
|
102
|
+
return plainTex(inline.tex);
|
|
103
|
+
case 'break':
|
|
104
|
+
return ' ';
|
|
105
|
+
case 'cite':
|
|
106
|
+
return `[${inline.number}]`;
|
|
107
|
+
case 'memoMark':
|
|
108
|
+
return inline.label;
|
|
109
|
+
default:
|
|
110
|
+
return 'children' in inline ? plainText(inline.children) : '';
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.join('');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Each edge of a diagram as the boxes it joins: the nearest box to each end. */
|
|
117
|
+
export function diagramEdges(geometry: DiagramGeometry): [number, number][] {
|
|
118
|
+
const nearest = ([x, y]: readonly [number, number]) => {
|
|
119
|
+
let best = -1;
|
|
120
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
121
|
+
geometry.nodes.forEach((node, index) => {
|
|
122
|
+
const dx = Math.max(node.x - x, 0, x - (node.x + node.width));
|
|
123
|
+
const dy = Math.max(node.y - y, 0, y - (node.y + node.height));
|
|
124
|
+
const distance = Math.hypot(dx, dy);
|
|
125
|
+
if (distance < bestDistance) [best, bestDistance] = [index, distance];
|
|
126
|
+
});
|
|
127
|
+
return best;
|
|
128
|
+
};
|
|
129
|
+
return geometry.edges.flatMap((edge) => {
|
|
130
|
+
const start = edge[0];
|
|
131
|
+
const end = edge.at(-1);
|
|
132
|
+
if (!start || !end) return [];
|
|
133
|
+
const from = nearest(start);
|
|
134
|
+
const to = nearest(end);
|
|
135
|
+
return from === -1 || to === -1 || from === to ? [] : [[from, to] as [number, number]];
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class MarkdownWriter {
|
|
140
|
+
private readonly route: string;
|
|
141
|
+
private readonly notationAnchors: Map<string, string>;
|
|
142
|
+
|
|
143
|
+
constructor(private readonly context: MarkdownChapterContext) {
|
|
144
|
+
this.route = context.page.route;
|
|
145
|
+
this.notationAnchors = new Map(context.page.notation.map((entry) => [entry.key, entry.anchor]));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The link to an anchor on a page: into its file when the download has it, else the website. */
|
|
149
|
+
private targetUrl(target: CourseTarget): string | undefined {
|
|
150
|
+
const file = this.context.documentFiles.get(target.route);
|
|
151
|
+
if (file !== undefined) {
|
|
152
|
+
if (!target.anchor) return file || '#';
|
|
153
|
+
return `${file}#${target.anchor}`;
|
|
154
|
+
}
|
|
155
|
+
const base = this.context.canonicalBaseUrl;
|
|
156
|
+
if (!base) return undefined;
|
|
157
|
+
return `${base}${target.route ? `${target.route}/` : ''}${target.anchor ? `#${target.anchor}` : ''}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
inline(inline: Inline, inTable = false): Phrasing[] {
|
|
161
|
+
switch (inline.kind) {
|
|
162
|
+
case 'text':
|
|
163
|
+
return [text(inline.value)];
|
|
164
|
+
case 'emphasis':
|
|
165
|
+
return [emphasis(this.inlines(inline.children, inTable))];
|
|
166
|
+
case 'strong':
|
|
167
|
+
return [strong(this.inlines(inline.children, inTable))];
|
|
168
|
+
case 'superscript':
|
|
169
|
+
return [html('<sup>'), ...this.inlines(inline.children, inTable), html('</sup>')];
|
|
170
|
+
case 'subscript':
|
|
171
|
+
return [html('<sub>'), ...this.inlines(inline.children, inTable), html('</sub>')];
|
|
172
|
+
case 'code':
|
|
173
|
+
return [{ type: 'inlineCode', value: inline.value }];
|
|
174
|
+
case 'math': {
|
|
175
|
+
const tex = plainTex(inline.tex);
|
|
176
|
+
return tex ? [{ type: 'inlineMath', value: inTable ? cellTex(tex) : tex }] : [];
|
|
177
|
+
}
|
|
178
|
+
case 'break':
|
|
179
|
+
return inTable ? [html('<br>')] : [{ type: 'break' }];
|
|
180
|
+
case 'link':
|
|
181
|
+
return [link(inline.href, this.inlines(inline.children, inTable))];
|
|
182
|
+
case 'ref': {
|
|
183
|
+
const url = this.targetUrl(inline.target);
|
|
184
|
+
const children = this.inlines(inline.children, inTable);
|
|
185
|
+
return url ? [link(url, children)] : children;
|
|
186
|
+
}
|
|
187
|
+
case 'term': {
|
|
188
|
+
const target = this.notationAnchors.get(inline.key);
|
|
189
|
+
const children = this.inlines(inline.children, inTable);
|
|
190
|
+
return target ? [link(`#${target}`, children)] : children;
|
|
191
|
+
}
|
|
192
|
+
case 'cite':
|
|
193
|
+
return [link(`#${inline.anchor}`, [text(`[${inline.number}]`)])];
|
|
194
|
+
case 'memoMark':
|
|
195
|
+
return [html('<sup>'), link(`#${inline.anchor}`, [text(inline.label)]), html('</sup>')];
|
|
196
|
+
case 'noteMark':
|
|
197
|
+
return [link(`#${inline.anchor}`, this.inlines(inline.children, inTable))];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
inlines(inlines: readonly Inline[], inTable = false): Phrasing[] {
|
|
202
|
+
return inlines.flatMap((inline) => this.inline(inline, inTable));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A choice's label, its math as math like the prompt's. */
|
|
206
|
+
private optionLabel(option: NonNullable<CheckItem['options']>[number]): Phrasing[] {
|
|
207
|
+
return option.labelSegments ? this.segments(option.labelSegments) : [text(option.label)];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private segments(segments: readonly ContentSegment[]): Phrasing[] {
|
|
211
|
+
return segments.flatMap((segment): Phrasing[] =>
|
|
212
|
+
segment.type === 'math' ? [{ type: 'inlineMath', value: plainTex(segment.latex) }] : [text(segment.value)],
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** A float's caption line: its number and short caption in bold, then the long one. */
|
|
217
|
+
private captionLine(label: string, block: { short?: string; caption: readonly Inline[] }, extra: readonly Inline[] = []): Phrasing[] {
|
|
218
|
+
const short = block.short?.trim();
|
|
219
|
+
const head = short ? `${label}. ${captionTitle(short)}` : `${label}.`;
|
|
220
|
+
const long = [...this.inlines(block.caption), ...(extra.length > 0 ? [text(' '), ...this.inlines(extra)] : [])];
|
|
221
|
+
return [strong([text(head)]), ...(long.length > 0 ? [text(' '), ...long] : [])];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private picture(key: string, alt: string): Phrasing | undefined {
|
|
225
|
+
const file = this.context.pictures.get(key);
|
|
226
|
+
return file ? { type: 'image', url: file, alt } : undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private table(block: Extract<Block, { kind: 'table' }>): RootContent[] {
|
|
230
|
+
const rows = [...block.head, ...block.body];
|
|
231
|
+
const spans = rows.some((row) => row.some((cell) => cell.colspan > 1 || cell.rowspan > 1));
|
|
232
|
+
const caption = block.number
|
|
233
|
+
? [paragraph([anchor(block.anchor), ...this.captionLine(`Table ${block.number}`, block)])]
|
|
234
|
+
: [];
|
|
235
|
+
if (spans) return [...caption, ...this.htmlTable(block)];
|
|
236
|
+
const columns = Math.max(0, ...rows.map((row) => row.length));
|
|
237
|
+
if (columns === 0) return caption;
|
|
238
|
+
const cell = (source: TableCell | undefined): MdTableCell => ({
|
|
239
|
+
type: 'tableCell',
|
|
240
|
+
children: source ? this.inlines(source.children, true) : [],
|
|
241
|
+
});
|
|
242
|
+
const row = (cells: readonly TableCell[]): TableRow => ({
|
|
243
|
+
type: 'tableRow',
|
|
244
|
+
children: Array.from({ length: columns }, (_, index) => cell(cells[index])),
|
|
245
|
+
});
|
|
246
|
+
const head = block.head[0] ?? [];
|
|
247
|
+
const firstBody = block.body[0] ?? head;
|
|
248
|
+
const table: Table = {
|
|
249
|
+
type: 'table',
|
|
250
|
+
align: Array.from({ length: columns }, (_, index) => (firstBody[index] ?? head[index])?.align ?? null),
|
|
251
|
+
children: [row(head), ...block.head.slice(1).map(row), ...block.body.map(row)],
|
|
252
|
+
};
|
|
253
|
+
return [...caption, table];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* A table with spanning cells as HTML, which pipe tables cannot hold. Each
|
|
258
|
+
* cell's content stands on lines of its own, so Markdown (and its math)
|
|
259
|
+
* still reads it.
|
|
260
|
+
*/
|
|
261
|
+
private htmlTable(block: Extract<Block, { kind: 'table' }>): RootContent[] {
|
|
262
|
+
const cellHtml = (cell: TableCell) => {
|
|
263
|
+
const tag = cell.header ? 'th' : 'td';
|
|
264
|
+
const attributes = [
|
|
265
|
+
...(cell.colspan > 1 ? [`colspan="${cell.colspan}"`] : []),
|
|
266
|
+
...(cell.rowspan > 1 ? [`rowspan="${cell.rowspan}"`] : []),
|
|
267
|
+
...(cell.align ? [`align="${cell.align}"`] : []),
|
|
268
|
+
];
|
|
269
|
+
const content = serializeNodes([paragraph(this.inlines(cell.children))]).trim();
|
|
270
|
+
return [`<${tag}${attributes.length > 0 ? ` ${attributes.join(' ')}` : ''}>`, '', content, '', `</${tag}>`];
|
|
271
|
+
};
|
|
272
|
+
const lines = [
|
|
273
|
+
'<table>',
|
|
274
|
+
...block.head.flatMap((row) => ['<tr>', ...row.flatMap(cellHtml), '</tr>']),
|
|
275
|
+
...block.body.flatMap((row) => ['<tr>', ...row.flatMap(cellHtml), '</tr>']),
|
|
276
|
+
'</table>',
|
|
277
|
+
];
|
|
278
|
+
return [{ type: 'html', value: lines.join('\n') }];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** A diagram's boxes and arrows as a list: what the picture shows, as text. */
|
|
282
|
+
private diagramText(geometry: DiagramGeometry): RootContent[] {
|
|
283
|
+
const label = (index: number): Phrasing[] => {
|
|
284
|
+
const node = geometry.nodes[index]!;
|
|
285
|
+
return [
|
|
286
|
+
...(node.text ? [text(node.text)] : []),
|
|
287
|
+
...(node.text && node.tex ? [text(' ')] : []),
|
|
288
|
+
...(node.tex ? [{ type: 'inlineMath', value: plainTex(node.tex) } as Phrasing] : []),
|
|
289
|
+
];
|
|
290
|
+
};
|
|
291
|
+
const edges = diagramEdges(geometry);
|
|
292
|
+
const joined = new Set(edges.flat());
|
|
293
|
+
const items: ListItem[] = [
|
|
294
|
+
...edges.map(([from, to]): ListItem => ({
|
|
295
|
+
type: 'listItem',
|
|
296
|
+
spread: false,
|
|
297
|
+
children: [paragraph([...label(from), text(' → '), ...label(to)])],
|
|
298
|
+
})),
|
|
299
|
+
...geometry.nodes
|
|
300
|
+
.map((_, index) => index)
|
|
301
|
+
.filter((index) => !joined.has(index))
|
|
302
|
+
.map((index): ListItem => ({ type: 'listItem', spread: false, children: [paragraph(label(index))] })),
|
|
303
|
+
];
|
|
304
|
+
if (items.length === 0) return [];
|
|
305
|
+
return [
|
|
306
|
+
paragraph([emphasis([text('The diagram as text:')])]),
|
|
307
|
+
{ type: 'list', ordered: false, spread: false, children: items },
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** A titled quote: a memo, a short note, an example set, a check. */
|
|
312
|
+
private quote(title: Phrasing[], children: RootContent[], id?: string): RootContent {
|
|
313
|
+
return {
|
|
314
|
+
type: 'blockquote',
|
|
315
|
+
children: [paragraph([...(id ? [anchor(id)] : []), strong(title)]), ...(children as BlockContent[])],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private check(block: Extract<Block, { kind: 'check' }>): RootContent {
|
|
320
|
+
const items = this.context.checkItems.get(block.assessmentId) ?? [];
|
|
321
|
+
const questions: List = {
|
|
322
|
+
type: 'list',
|
|
323
|
+
ordered: true,
|
|
324
|
+
spread: false,
|
|
325
|
+
children: items.map((item): ListItem => ({
|
|
326
|
+
type: 'listItem',
|
|
327
|
+
spread: false,
|
|
328
|
+
children: [
|
|
329
|
+
paragraph(this.segments(item.promptSegments)),
|
|
330
|
+
...(item.type === 'single-choice' && item.options && item.options.length > 0
|
|
331
|
+
? [
|
|
332
|
+
{
|
|
333
|
+
type: 'list',
|
|
334
|
+
ordered: false,
|
|
335
|
+
spread: false,
|
|
336
|
+
children: item.options.map(
|
|
337
|
+
(option, index): ListItem => ({
|
|
338
|
+
type: 'listItem',
|
|
339
|
+
spread: false,
|
|
340
|
+
children: [
|
|
341
|
+
paragraph([text(`(${String.fromCharCode(97 + index)}) `), ...this.optionLabel(option)]),
|
|
342
|
+
],
|
|
343
|
+
}),
|
|
344
|
+
),
|
|
345
|
+
} satisfies List,
|
|
346
|
+
]
|
|
347
|
+
: []),
|
|
348
|
+
],
|
|
349
|
+
})),
|
|
350
|
+
};
|
|
351
|
+
return this.quote(
|
|
352
|
+
[text(`Knowledge check ${block.number}: `), ...this.inlines(block.title)],
|
|
353
|
+
[
|
|
354
|
+
...(items.length > 0 ? [questions] : []),
|
|
355
|
+
paragraph([emphasis([text('Answers: '), link('#answers', [text('end of the chapter')]), text('.')])]),
|
|
356
|
+
],
|
|
357
|
+
block.anchor,
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
block(block: Block, inBox = false): RootContent[] {
|
|
362
|
+
switch (block.kind) {
|
|
363
|
+
case 'heading': {
|
|
364
|
+
const content = [
|
|
365
|
+
...(block.anchor ? [anchor(block.anchor)] : []),
|
|
366
|
+
...(block.number ? [text(`${block.number} `)] : []),
|
|
367
|
+
...this.inlines(block.children),
|
|
368
|
+
];
|
|
369
|
+
if (inBox) return [paragraph([strong(content)])];
|
|
370
|
+
const depth = Math.min(Math.max(block.level, 2), 6) as 2 | 3 | 4 | 5 | 6;
|
|
371
|
+
return [{ type: 'heading', depth, children: content }];
|
|
372
|
+
}
|
|
373
|
+
case 'paragraph': {
|
|
374
|
+
const children = this.inlines(block.children);
|
|
375
|
+
return children.length > 0 ? [paragraph(children)] : [];
|
|
376
|
+
}
|
|
377
|
+
case 'list':
|
|
378
|
+
return [
|
|
379
|
+
{
|
|
380
|
+
type: 'list',
|
|
381
|
+
ordered: block.ordered,
|
|
382
|
+
...(block.ordered && block.start ? { start: block.start } : {}),
|
|
383
|
+
spread: false,
|
|
384
|
+
children: block.items.map((item): ListItem => ({
|
|
385
|
+
type: 'listItem',
|
|
386
|
+
spread: false,
|
|
387
|
+
children: this.blocks(item, inBox) as (BlockContent | DefinitionContent)[],
|
|
388
|
+
})),
|
|
389
|
+
},
|
|
390
|
+
];
|
|
391
|
+
case 'equation':
|
|
392
|
+
return [
|
|
393
|
+
paragraph([anchor(block.anchor)]),
|
|
394
|
+
{ type: 'math', value: `${plainTex(block.tex)}\n\\tag{${block.number}}` },
|
|
395
|
+
];
|
|
396
|
+
case 'displayMath':
|
|
397
|
+
return [{ type: 'math', value: plainTex(block.tex) }];
|
|
398
|
+
case 'figure': {
|
|
399
|
+
const alt =
|
|
400
|
+
block.alt ?? [block.short && captionTitle(block.short), plainText(block.caption)].filter(Boolean).join(' ');
|
|
401
|
+
const image = this.picture(block.anchor, alt || 'Figure');
|
|
402
|
+
const missing: Phrasing[] = [
|
|
403
|
+
emphasis([
|
|
404
|
+
text('Interactive figure'),
|
|
405
|
+
...(this.context.canonicalBaseUrl
|
|
406
|
+
? [text(': '), link(`${this.context.canonicalBaseUrl}${this.route}/#${block.anchor}`, [text('see it on the course website')])]
|
|
407
|
+
: []),
|
|
408
|
+
text('.'),
|
|
409
|
+
]),
|
|
410
|
+
];
|
|
411
|
+
return [
|
|
412
|
+
paragraph([anchor(block.anchor), ...(image ? [image] : missing)]),
|
|
413
|
+
paragraph(this.captionLine(`Figure ${block.number}`, block, block.live)),
|
|
414
|
+
];
|
|
415
|
+
}
|
|
416
|
+
case 'diagram': {
|
|
417
|
+
const image = this.picture(block.anchor, block.short ?? plainText(block.caption) ?? 'Diagram');
|
|
418
|
+
return [
|
|
419
|
+
paragraph([anchor(block.anchor), ...(image ? [image] : [])]),
|
|
420
|
+
paragraph(this.captionLine(`Diagram ${block.number}`, block)),
|
|
421
|
+
...this.diagramText(block.geometry),
|
|
422
|
+
];
|
|
423
|
+
}
|
|
424
|
+
case 'table':
|
|
425
|
+
return this.table(block);
|
|
426
|
+
case 'memo':
|
|
427
|
+
return [
|
|
428
|
+
this.quote(
|
|
429
|
+
[text(`Memo ${block.label}: `), ...this.inlines(block.title)],
|
|
430
|
+
this.blocks(block.children, true),
|
|
431
|
+
block.anchor,
|
|
432
|
+
),
|
|
433
|
+
];
|
|
434
|
+
case 'note':
|
|
435
|
+
return [this.quote([text(block.title)], this.blocks(block.children, true), block.anchor)];
|
|
436
|
+
case 'aside': {
|
|
437
|
+
const title = this.inlines(block.title);
|
|
438
|
+
return [
|
|
439
|
+
{
|
|
440
|
+
type: 'blockquote',
|
|
441
|
+
children: [
|
|
442
|
+
paragraph([
|
|
443
|
+
html(`[!${ALERTS[block.variant] ?? 'NOTE'}]`),
|
|
444
|
+
...(title.length > 0 ? [text('\n'), strong(title)] : []),
|
|
445
|
+
]),
|
|
446
|
+
...(this.blocks(block.children, true) as BlockContent[]),
|
|
447
|
+
],
|
|
448
|
+
},
|
|
449
|
+
];
|
|
450
|
+
}
|
|
451
|
+
case 'fold':
|
|
452
|
+
return [
|
|
453
|
+
...(block.title && block.title.length > 0 ? [paragraph([strong(this.inlines(block.title))])] : []),
|
|
454
|
+
...this.blocks(block.children, inBox),
|
|
455
|
+
];
|
|
456
|
+
case 'examples':
|
|
457
|
+
return [
|
|
458
|
+
this.quote(
|
|
459
|
+
[text(`Example ${block.number}: `), ...this.inlines(block.title)],
|
|
460
|
+
block.items.flatMap((item) => [paragraph([strong(this.inlines(item.title))]), ...this.blocks(item.children, true)]),
|
|
461
|
+
block.anchor,
|
|
462
|
+
),
|
|
463
|
+
];
|
|
464
|
+
case 'check':
|
|
465
|
+
return [this.check(block)];
|
|
466
|
+
case 'quote':
|
|
467
|
+
return [{ type: 'blockquote', children: this.blocks(block.children, inBox) as BlockContent[] }];
|
|
468
|
+
case 'image': {
|
|
469
|
+
const image = this.picture(block.src, block.alt);
|
|
470
|
+
return image ? [paragraph([image])] : [];
|
|
471
|
+
}
|
|
472
|
+
case 'code':
|
|
473
|
+
return [{ type: 'code', value: block.value }];
|
|
474
|
+
case 'rule':
|
|
475
|
+
return [{ type: 'thematicBreak' }];
|
|
476
|
+
case 'references':
|
|
477
|
+
return [
|
|
478
|
+
{ type: 'heading', depth: 2, children: [text('References')] },
|
|
479
|
+
{
|
|
480
|
+
type: 'list',
|
|
481
|
+
ordered: true,
|
|
482
|
+
start: Number.parseInt(block.items[0]?.number ?? '1', 10) || 1,
|
|
483
|
+
spread: false,
|
|
484
|
+
children: block.items.map((item): ListItem => ({
|
|
485
|
+
type: 'listItem',
|
|
486
|
+
spread: false,
|
|
487
|
+
children: [paragraph([anchor(item.anchor), ...this.inlines(item.children)])],
|
|
488
|
+
})),
|
|
489
|
+
},
|
|
490
|
+
];
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
blocks(blocks: readonly Block[], inBox = false): RootContent[] {
|
|
495
|
+
return blocks.flatMap((block) => this.block(block, inBox));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
notation(entries: readonly NotationEntry[], remarks: readonly (readonly Inline[])[]): RootContent[] {
|
|
499
|
+
if (entries.length === 0) return [];
|
|
500
|
+
return [
|
|
501
|
+
{ type: 'heading', depth: 2, children: [text('Notation')] },
|
|
502
|
+
...remarks.map((remark) => paragraph([emphasis(this.inlines(remark))])),
|
|
503
|
+
{
|
|
504
|
+
type: 'list',
|
|
505
|
+
ordered: false,
|
|
506
|
+
spread: false,
|
|
507
|
+
children: entries.map((entry): ListItem => {
|
|
508
|
+
const symbol = entry.symbolTex ? plainTex(entry.symbolTex) : '';
|
|
509
|
+
const line: Phrasing[] = [
|
|
510
|
+
anchor(entry.anchor),
|
|
511
|
+
...(symbol ? [{ type: 'inlineMath', value: symbol } as Phrasing, text(' ')] : []),
|
|
512
|
+
strong([...this.inlines(entry.label), text('.')]),
|
|
513
|
+
...(entry.summary.length > 0 ? [text(' '), ...this.inlines(entry.summary)] : []),
|
|
514
|
+
...(entry.units ? [text(' '), emphasis([text('Units:')]), text(' '), ...this.inlines(entry.units), text('.')] : []),
|
|
515
|
+
];
|
|
516
|
+
return {
|
|
517
|
+
type: 'listItem',
|
|
518
|
+
spread: false,
|
|
519
|
+
children: [
|
|
520
|
+
paragraph(line),
|
|
521
|
+
...(entry.formulaTex ? [{ type: 'math', value: plainTex(entry.formulaTex) } as BlockContent] : []),
|
|
522
|
+
],
|
|
523
|
+
};
|
|
524
|
+
}),
|
|
525
|
+
},
|
|
526
|
+
];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
answers(checks: readonly Extract<Block, { kind: 'check' }>[]): RootContent[] {
|
|
530
|
+
if (checks.length === 0) return [];
|
|
531
|
+
return [
|
|
532
|
+
{ type: 'heading', depth: 2, children: [anchor('answers'), text('Answers to the knowledge checks')] },
|
|
533
|
+
...checks.flatMap((check): RootContent[] => {
|
|
534
|
+
const items = this.context.checkItems.get(check.assessmentId) ?? [];
|
|
535
|
+
return [
|
|
536
|
+
paragraph([
|
|
537
|
+
strong([link(`#${check.anchor}`, [text(`Knowledge check ${check.number}`)]), text(': '), ...this.inlines(check.title)]),
|
|
538
|
+
]),
|
|
539
|
+
{
|
|
540
|
+
type: 'list',
|
|
541
|
+
ordered: true,
|
|
542
|
+
spread: false,
|
|
543
|
+
children: items.map((item): ListItem => {
|
|
544
|
+
const answer: Phrasing[] = [];
|
|
545
|
+
if (item.type === 'single-choice' && item.options) {
|
|
546
|
+
const index = item.options.findIndex((option) => option.id === item.correctOptionId);
|
|
547
|
+
if (index !== -1) {
|
|
548
|
+
answer.push(
|
|
549
|
+
strong([text(`(${String.fromCharCode(97 + index)}) `), ...this.optionLabel(item.options[index]!), text('.')]),
|
|
550
|
+
text(' '),
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
} else if (item.type === 'numeric' && item.answer) {
|
|
554
|
+
answer.push(
|
|
555
|
+
strong([text(String(item.answer.value))]),
|
|
556
|
+
text(` (accepted within ±${item.answer.tolerance}). `),
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
return { type: 'listItem', spread: false, children: [paragraph([...answer, ...this.segments(item.explanationSegments)])] };
|
|
560
|
+
}),
|
|
561
|
+
},
|
|
562
|
+
];
|
|
563
|
+
}),
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function serializeNodes(children: RootContent[]): string {
|
|
569
|
+
const root: Root = { type: 'root', children };
|
|
570
|
+
return toMarkdown(root, {
|
|
571
|
+
bullet: '-',
|
|
572
|
+
emphasis: '*',
|
|
573
|
+
strong: '*',
|
|
574
|
+
fence: '`',
|
|
575
|
+
fences: true,
|
|
576
|
+
rule: '-',
|
|
577
|
+
listItemIndent: 'one',
|
|
578
|
+
incrementListMarker: true,
|
|
579
|
+
extensions: [gfmTableToMarkdown(), mathToMarkdown()],
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Every check of a page, in reading order. */
|
|
584
|
+
function checksOf(blocks: readonly Block[]): Extract<Block, { kind: 'check' }>[] {
|
|
585
|
+
const checks: Extract<Block, { kind: 'check' }>[] = [];
|
|
586
|
+
const visit = (list: readonly Block[]) => {
|
|
587
|
+
for (const block of list) {
|
|
588
|
+
if (block.kind === 'check') checks.push(block);
|
|
589
|
+
if (block.kind === 'list') block.items.forEach(visit);
|
|
590
|
+
if (block.kind === 'examples') block.items.forEach((item) => visit(item.children));
|
|
591
|
+
if ('children' in block && Array.isArray(block.children) && block.kind !== 'paragraph' && block.kind !== 'heading') {
|
|
592
|
+
visit(block.children as readonly Block[]);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
visit(blocks);
|
|
597
|
+
return checks;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** The Markdown of one page: its title, its body, references, notation and answers. */
|
|
601
|
+
export function serializeMarkdownPage(
|
|
602
|
+
context: MarkdownChapterContext,
|
|
603
|
+
options: { readonly number?: string } = {},
|
|
604
|
+
): string {
|
|
605
|
+
const writer = new MarkdownWriter(context);
|
|
606
|
+
const { page, source } = context;
|
|
607
|
+
const sourceLine: Phrasing[] = source
|
|
608
|
+
? [
|
|
609
|
+
text('From '),
|
|
610
|
+
emphasis([text(source.course)]),
|
|
611
|
+
...(source.version ? [text(`, version ${source.version}`)] : []),
|
|
612
|
+
text('.'),
|
|
613
|
+
...(source.url ? [text(' Interactive version: '), link(source.url, [text(source.url)]), text('.')] : []),
|
|
614
|
+
]
|
|
615
|
+
: [];
|
|
616
|
+
return serializeNodes([
|
|
617
|
+
{
|
|
618
|
+
type: 'heading',
|
|
619
|
+
depth: 1,
|
|
620
|
+
children: [anchor('_top'), ...(options.number ? [text(`${options.number} `)] : []), text(page.title)],
|
|
621
|
+
},
|
|
622
|
+
...(sourceLine.length > 0 ? [paragraph(sourceLine)] : []),
|
|
623
|
+
...writer.blocks(page.blocks),
|
|
624
|
+
...writer.notation(page.notation, page.notationRemarks),
|
|
625
|
+
...writer.answers(checksOf(page.blocks)),
|
|
626
|
+
]);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Markdown of a few blocks of mdast, written the same way (for a download's `index.md`). */
|
|
630
|
+
export function serializeMarkdownNodes(children: RootContent[]): string {
|
|
631
|
+
return serializeNodes(children);
|
|
632
|
+
}
|