explico 0.13.1 → 0.14.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 +879 -1
- package/README.md +200 -30
- package/dist/compiler/astro.d.ts +15 -2
- package/dist/compiler/astro.js +40 -11
- package/dist/compiler/cli-main.d.ts +43 -0
- package/dist/compiler/cli-main.js +392 -0
- package/dist/compiler/cli.d.ts +424 -9
- package/dist/compiler/cli.js +1289 -8
- package/dist/compiler/collections.d.ts +88 -1
- package/dist/compiler/collections.js +170 -25
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +105 -1
- package/dist/compiler/decks.d.ts +161 -0
- package/dist/compiler/decks.js +235 -0
- package/dist/compiler/exports/combined-html.d.ts +5 -1
- package/dist/compiler/exports/deck-fit.d.ts +55 -0
- package/dist/compiler/exports/deck-fit.js +90 -0
- package/dist/compiler/exports/export-chapters.d.ts +37 -6
- package/dist/compiler/exports/export-chapters.js +64 -5
- package/dist/compiler/exports/export-pages.d.ts +30 -3
- package/dist/compiler/exports/export-pages.js +24 -1
- package/dist/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/dist/compiler/exports/latex/cache.d.ts +78 -0
- package/dist/compiler/exports/latex/cache.js +129 -0
- package/dist/compiler/exports/latex/capture.d.ts +10 -1
- package/dist/compiler/exports/latex/capture.js +3 -1
- package/dist/compiler/exports/latex/compile.d.ts +13 -2
- package/dist/compiler/exports/latex/compile.js +12 -1
- package/dist/compiler/exports/latex/deck.d.ts +92 -0
- package/dist/compiler/exports/latex/deck.js +349 -0
- package/dist/compiler/exports/latex/document.d.ts +51 -6
- package/dist/compiler/exports/latex/document.js +71 -12
- package/dist/compiler/exports/latex/explico-common.sty +198 -0
- package/dist/compiler/exports/latex/explico.cls +13 -183
- package/dist/compiler/exports/latex/serialize.d.ts +60 -4
- package/dist/compiler/exports/latex/serialize.js +4 -2
- package/dist/compiler/exports/latex-export.d.ts +24 -2
- package/dist/compiler/exports/latex-export.js +107 -26
- package/dist/compiler/exports/markdown/serialize.d.ts +24 -4
- package/dist/compiler/exports/markdown/serialize.js +85 -8
- package/dist/compiler/exports/markdown-export.d.ts +6 -1
- package/dist/compiler/exports/markdown-export.js +50 -11
- package/dist/compiler/exports/standalone-html.d.ts +10 -5
- package/dist/compiler/exports/standalone-html.js +3 -3
- package/dist/compiler/exports/static-tree.d.ts +38 -1
- package/dist/compiler/exports/static-tree.js +120 -4
- package/dist/compiler/knowledge-version.d.ts +26 -2
- package/dist/compiler/knowledge-version.js +27 -17
- package/dist/compiler/lesson-derivation.d.ts +143 -6
- package/dist/compiler/lesson-derivation.js +145 -13
- package/dist/compiler/manifest.d.ts +133 -4
- package/dist/compiler/manifest.js +359 -58
- package/dist/compiler/page-versions.d.ts +160 -0
- package/dist/compiler/page-versions.js +198 -0
- package/dist/compiler/render-diagnostics.d.ts +5 -1
- package/dist/compiler/resolution-report.d.ts +24 -6
- package/dist/compiler/resolution-report.js +22 -4
- package/dist/compiler/validate-content.d.ts +7 -0
- package/dist/compiler/validate-content.js +160 -0
- package/dist/{course-config-rRtME5UV.d.ts → course-config-BLtIdkei.d.ts} +164 -1
- package/dist/curriculum/competency-graph.d.ts +54 -0
- package/dist/curriculum/competency-graph.js +41 -0
- package/dist/reference/base-library.d.mts +8 -0
- package/dist/reference/base-library.mjs +19 -0
- package/dist/reference/consistency.d.ts +8 -4
- package/dist/reference/consistency.js +76 -26
- package/dist/reference/deck-remark.d.mts +123 -0
- package/dist/reference/deck-remark.mjs +600 -0
- package/dist/reference/deck-source.d.ts +107 -0
- package/dist/reference/deck-source.js +140 -0
- package/dist/reference/decks-validate.d.ts +133 -0
- package/dist/reference/decks-validate.js +359 -0
- package/dist/reference/derived-entries.d.ts +5 -1
- package/dist/reference/diagram-fit.d.ts +1 -1
- package/dist/reference/figure-symbols.d.ts +23 -6
- package/dist/reference/figure-symbols.js +51 -15
- package/dist/reference/float-name.d.ts +43 -0
- package/dist/reference/float-name.js +58 -0
- package/dist/reference/folds-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-indices.js +5 -4
- package/dist/reference/gate-math.d.ts +29 -2
- package/dist/reference/gate-math.js +99 -18
- package/dist/reference/gate-part-pages.d.ts +1 -1
- package/dist/reference/lab-katex-validate.d.ts +10 -6
- package/dist/reference/lab-katex-validate.js +17 -9
- package/dist/reference/lab-math-scope.js +29 -4
- package/dist/reference/math-glyphs.d.mts +25 -0
- package/dist/reference/math-glyphs.mjs +189 -25
- package/dist/reference/page-notation.d.ts +22 -3
- package/dist/reference/page-notation.js +32 -10
- package/dist/reference/references.d.ts +14 -1
- package/dist/reference/references.js +13 -0
- package/dist/reference/registry-scope.d.mts +33 -0
- package/dist/reference/registry-scope.mjs +34 -0
- package/dist/reference/registry.js +29 -10
- package/dist/reference/rehype-deck.d.mts +6 -0
- package/dist/reference/rehype-deck.mjs +286 -0
- package/dist/reference/rehype-heading-anchors.d.mts +20 -0
- package/dist/reference/rehype-heading-anchors.mjs +48 -6
- package/dist/reference/remark-notation.mjs +249 -40
- package/dist/reference/source-lines.d.mts +12 -0
- package/dist/reference/source-lines.mjs +65 -0
- package/dist/reference/types.d.ts +28 -0
- package/dist/rules/catalog.d.ts +21 -4
- package/dist/rules/catalog.js +221 -2
- package/dist/rules/course-rules.d.ts +5 -1
- package/dist/rules/course-rules.js +17 -3
- package/dist/rules/course-rules.schema.json +12 -0
- package/dist/rules/in-effect.d.ts +13 -3
- package/dist/rules/in-effect.js +30 -7
- package/dist/rules/rules.schema.json +259 -1
- package/dist/rules/settings.d.ts +1 -1
- package/dist/session/PreferenceStore.d.ts +6 -0
- package/dist/session/PreferenceStore.js +18 -1
- package/dist/session/deck-memory.d.ts +116 -0
- package/dist/session/deck-memory.js +143 -0
- package/dist/session/reading-memory.d.ts +96 -0
- package/dist/session/reading-memory.js +133 -0
- package/docs/VERSIONING.md +21 -18
- package/docs/authoring/README.md +29 -9
- package/docs/authoring/agents.md +63 -16
- package/docs/authoring/components.md +15 -0
- package/docs/authoring/deck.md +588 -0
- package/docs/authoring/migrating.md +1045 -16
- package/docs/authoring/notation.md +49 -6
- package/docs/authoring/structure.md +80 -1
- package/docs/authoring/versioning.md +54 -2
- package/docs/authoring/visualizations.md +7 -2
- package/docs/course-agent/AGENTS.md +37 -0
- package/docs/course-agent/agents/rules-reviewer.md +33 -0
- package/docs/course-agent/skills/draft-lesson/SKILL.md +37 -0
- package/docs/course-agent/skills/migrate-course/SKILL.md +31 -0
- package/docs/course-agent/skills/release-content/SKILL.md +24 -0
- package/docs/diagram-scaling.md +148 -0
- package/docs/privacy.md +59 -0
- package/package.json +8 -2
- package/src/compiler/astro.ts +60 -21
- package/src/compiler/cli-main.ts +469 -0
- package/src/compiler/cli.ts +1905 -16
- package/src/compiler/collections.ts +284 -25
- package/src/compiler/course-config.ts +267 -0
- package/src/compiler/decks.ts +455 -0
- package/src/compiler/exports/deck-fit.ts +127 -0
- package/src/compiler/exports/export-chapters.ts +105 -6
- package/src/compiler/exports/export-pages.ts +46 -2
- package/src/compiler/exports/latex/beamerthemeexplico.sty +124 -0
- package/src/compiler/exports/latex/cache.ts +183 -0
- package/src/compiler/exports/latex/capture.ts +5 -2
- package/src/compiler/exports/latex/compile.ts +18 -0
- package/src/compiler/exports/latex/deck.ts +419 -0
- package/src/compiler/exports/latex/document.ts +117 -21
- package/src/compiler/exports/latex/explico-common.sty +198 -0
- package/src/compiler/exports/latex/explico.cls +13 -183
- package/src/compiler/exports/latex/serialize.ts +25 -21
- package/src/compiler/exports/latex/texlive-packages.txt +6 -2
- package/src/compiler/exports/latex-export.ts +187 -45
- package/src/compiler/exports/markdown/serialize.ts +118 -9
- package/src/compiler/exports/markdown-export.ts +56 -11
- package/src/compiler/exports/standalone-html.ts +8 -7
- package/src/compiler/exports/static-tree.ts +175 -3
- package/src/compiler/knowledge-version.ts +56 -21
- package/src/compiler/lesson-derivation.ts +282 -16
- package/src/compiler/manifest.ts +558 -63
- package/src/compiler/page-versions.ts +359 -0
- package/src/compiler/resolution-report.ts +38 -7
- package/src/compiler/validate-content.ts +212 -0
- package/src/components/CourseContents.astro +124 -6
- package/src/components/CurriculumMap.astro +49 -1
- package/src/components/FloatIndex.astro +1 -1
- package/src/components/PartOverview.astro +51 -5
- package/src/components/assessments/AssessmentSet.astro +41 -15
- package/src/components/block-heading.ts +15 -6
- package/src/components/card-carry.ts +7 -4
- package/src/components/card-manager.ts +14 -2
- package/src/components/card-memory.ts +13 -9
- package/src/components/deck/DeckEnhancer.astro +13 -0
- package/src/components/deck/deck-context.ts +104 -0
- package/src/components/deck/deck-controller.ts +1287 -0
- package/src/components/deck/deck-map.ts +91 -0
- package/src/components/deck-lane.ts +163 -0
- package/src/components/diagram/Diagram.astro +131 -86
- package/src/components/disclosure/persisted-disclosure.ts +9 -1
- package/src/components/figure/Figure.astro +7 -2
- package/src/components/figure/FloatCaption.astro +5 -3
- package/src/components/fold/Fold.astro +2 -2
- package/src/components/notation/NotationFormula.astro +81 -11
- package/src/components/notation/NotationGlossary.astro +132 -1
- package/src/components/notation/NotationLayer.astro +15 -7
- package/src/components/starlight/AboutPanel.astro +186 -3
- package/src/components/starlight/CollapsibleSections.astro +24 -3
- package/src/components/starlight/DiagramEnhancer.astro +5 -1
- package/src/components/starlight/HeadingAnchorEnhancer.astro +1 -1
- package/src/components/starlight/LayoutHeader.astro +17 -0
- package/src/components/starlight/LayoutPageTitle.astro +110 -0
- package/src/components/starlight/LayoutSidebar.astro +807 -21
- package/src/components/starlight/LayoutSocialIcons.astro +62 -6
- package/src/components/starlight/LessonExportActions.astro +27 -9
- package/src/components/starlight/LessonFooter.astro +59 -0
- package/src/components/starlight/ReadingMemoryNotice.astro +114 -0
- package/src/components/starlight/TextSizeControl.astro +24 -13
- package/src/components/starlight/page-marks.ts +139 -0
- package/src/components/starlight/section-summary.ts +1 -1
- package/src/components/starlight/sidebar-editions.ts +245 -0
- package/src/components/table/Table.astro +15 -7
- package/src/content-config.ts +87 -1
- package/src/curriculum/competency-graph.ts +95 -0
- package/src/reference/base-library.mjs +19 -0
- package/src/reference/consistency.ts +126 -39
- package/src/reference/deck-remark.mjs +600 -0
- package/src/reference/deck-source.ts +273 -0
- package/src/reference/decks-validate.ts +604 -0
- package/src/reference/diagram-layout.ts +3 -1
- package/src/reference/figure-symbols.ts +108 -31
- package/src/reference/folds-validate.ts +1 -1
- package/src/reference/gate-indices.ts +5 -3
- package/src/reference/gate-math.ts +169 -25
- package/src/reference/lab-katex-validate.ts +30 -15
- package/src/reference/lab-math-scope.ts +34 -3
- package/src/reference/math-glyphs.mjs +189 -25
- package/src/reference/page-notation.ts +69 -15
- package/src/reference/references.ts +28 -0
- package/src/reference/registry-scope.mjs +34 -0
- package/src/reference/registry.ts +41 -11
- package/src/reference/rehype-deck.mjs +286 -0
- package/src/reference/rehype-heading-anchors.mjs +48 -6
- package/src/reference/remark-notation.mjs +249 -40
- package/src/reference/source-lines.mjs +65 -0
- package/src/reference/types.ts +28 -0
- package/src/rules/catalog.ts +257 -4
- package/src/rules/course-rules.ts +21 -1
- package/src/rules/in-effect.ts +54 -8
- package/src/session/PreferenceStore.ts +23 -0
- package/src/session/deck-memory.ts +263 -0
- package/src/session/reading-memory.ts +243 -0
- package/src/styles/deck.css +1150 -0
- package/src/styles/global.css +958 -27
- package/src/styles/zones.css +277 -0
- /package/src/{components → reference}/float-name.ts +0 -0
|
@@ -14,15 +14,26 @@
|
|
|
14
14
|
* part and the course as one file each); with `latex`, as PDF and LaTeX
|
|
15
15
|
* source (a part and the course as a book); with `markdown`, as Markdown. A page that is not a chapter has no download of its own (D8):
|
|
16
16
|
* its content is in its part's and the course's files.
|
|
17
|
+
*
|
|
18
|
+
* A lesson's deck (docs/authoring/deck.md) adds a row on its own page and on
|
|
19
|
+
* its chapter's: the slides as standalone HTML (Present mode offline); with
|
|
20
|
+
* `latex`, as PDF, as a handout (each slide with its speaker notes) and their
|
|
21
|
+
* LaTeX source; with `markdown`, as Marp's Markdown. A slides-first lesson
|
|
22
|
+
* has no chapter row: its deck is all of it. A part's deck adds the same row
|
|
23
|
+
* on its own page and on its part's, above "This part".
|
|
17
24
|
*/
|
|
18
25
|
import DefaultSocialIcons from '@astrojs/starlight/components/SocialIcons.astro';
|
|
19
26
|
import { loadManifest } from '../../compiler/manifest';
|
|
20
27
|
import { docsSlugsForRoute } from '../../compiler/lesson-derivation';
|
|
21
28
|
import {
|
|
22
29
|
courseFileStem,
|
|
30
|
+
deckFileStem,
|
|
31
|
+
deckOwnerSlug,
|
|
32
|
+
deckSaveName,
|
|
23
33
|
exportPageSlugs,
|
|
24
34
|
standaloneFilename,
|
|
25
35
|
} from '../../compiler/exports/export-pages';
|
|
36
|
+
import { chapterSlides, deckOfRoute, partDeckOf } from '../deck/deck-context';
|
|
26
37
|
import AboutPanel from './AboutPanel.astro';
|
|
27
38
|
import LessonExportActions, {
|
|
28
39
|
type DownloadCell,
|
|
@@ -31,19 +42,63 @@ import LessonExportActions, {
|
|
|
31
42
|
import TextSizeControl from './TextSizeControl.astro';
|
|
32
43
|
|
|
33
44
|
const manifest = await loadManifest();
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
)
|
|
45
|
+
const routeId = Astro.locals.starlightRoute.entry.id;
|
|
46
|
+
const routeSlugs = docsSlugsForRoute(routeId);
|
|
47
|
+
// A deck beside its chapter has a route of its own (`<lesson>/slides`); it
|
|
48
|
+
// downloads as its lesson's page does. A part's deck (`<part>/slides`)
|
|
49
|
+
// downloads as its part's page does.
|
|
50
|
+
const pageDeck = deckOfRoute(manifest, routeId);
|
|
51
|
+
const pageSlug =
|
|
52
|
+
exportPageSlugs(manifest).find((slug) => routeSlugs.includes(slug)) ??
|
|
53
|
+
(pageDeck?.part !== undefined
|
|
54
|
+
? `${pageDeck.part}/index`
|
|
55
|
+
: pageDeck
|
|
56
|
+
? manifest.lessons.find((entry) => entry.id === pageDeck.lessonId)?.slug
|
|
57
|
+
: undefined);
|
|
38
58
|
const exports = manifest.lessonExports;
|
|
39
59
|
const printEnabled = pageSlug !== undefined && exports.print;
|
|
40
60
|
const lesson = manifest.lessons.find((entry) => entry.slug === pageSlug);
|
|
41
61
|
const part = lesson
|
|
42
62
|
? manifest.parts.find((entry) => entry.ordinal === lesson.part)
|
|
43
63
|
: manifest.parts.find((entry) => `${entry.dir}/index` === pageSlug);
|
|
64
|
+
// The slides this page lists: its own; its lesson's, or for a chapter with
|
|
65
|
+
// none of its own the part's deck that covers it (`chapterSlides`); or on a
|
|
66
|
+
// part's page the part's.
|
|
67
|
+
const rowDeck =
|
|
68
|
+
pageDeck ??
|
|
69
|
+
(lesson
|
|
70
|
+
? ((manifest.decks ?? []).find((entry) => entry.lessonId === lesson.id) ??
|
|
71
|
+
chapterSlides(manifest, lesson.slug)?.deck)
|
|
72
|
+
: part
|
|
73
|
+
? partDeckOf(manifest, part.dir)
|
|
74
|
+
: undefined);
|
|
44
75
|
|
|
45
76
|
const downloads: DownloadRow[] = [];
|
|
46
|
-
|
|
77
|
+
/** The deck's row: its slides offline, as PDF, as a handout, their source and Marp's Markdown. */
|
|
78
|
+
const deckRow = (): DownloadRow | undefined => {
|
|
79
|
+
if (!rowDeck) return undefined;
|
|
80
|
+
const owner = deckOwnerSlug(rowDeck, manifest);
|
|
81
|
+
const path = deckFileStem(owner);
|
|
82
|
+
const name = deckSaveName(owner);
|
|
83
|
+
const handout = manifest.deck.handoutPdf && manifest.deck.notes !== 'off';
|
|
84
|
+
const cells: DownloadCell[] = [
|
|
85
|
+
...(exports.standaloneHtml ? [{ format: 'html' as const, path: `${path}.html`, filename: `${name}.html` }] : []),
|
|
86
|
+
...(exports.latex
|
|
87
|
+
? [
|
|
88
|
+
{ format: 'pdf' as const, path: `${path}.pdf`, filename: `${name}.pdf` },
|
|
89
|
+
...(handout ? [{ format: 'handout' as const, path: `${path}-notes.pdf`, filename: `${name}-notes.pdf` }] : []),
|
|
90
|
+
{ format: 'latex' as const, path: `${path}-latex.zip`, filename: `${name}-latex.zip` },
|
|
91
|
+
]
|
|
92
|
+
: []),
|
|
93
|
+
...(exports.markdown ? [{ format: 'markdown' as const, path: `${path}-markdown.zip`, filename: `${name}-markdown.zip` }] : []),
|
|
94
|
+
];
|
|
95
|
+
return cells.length === 0
|
|
96
|
+
? undefined
|
|
97
|
+
: { scope: 'deck', label: pageDeck ? 'These slides' : 'Its slides', title: rowDeck.title, cells };
|
|
98
|
+
};
|
|
99
|
+
const slidesRow = deckRow();
|
|
100
|
+
if (slidesRow && pageDeck) downloads.push(slidesRow);
|
|
101
|
+
if (lesson && lesson.editions.includes('chapter')) {
|
|
47
102
|
const name = lesson.slug.split('/').at(-1) ?? lesson.slug;
|
|
48
103
|
const cells: DownloadCell[] = [];
|
|
49
104
|
if (exports.standaloneHtml) {
|
|
@@ -59,9 +114,10 @@ if (lesson) {
|
|
|
59
114
|
cells.push({ format: 'markdown', path: `${lesson.slug}-markdown.zip`, filename: `${name}-markdown.zip` });
|
|
60
115
|
}
|
|
61
116
|
if (cells.length > 0) {
|
|
62
|
-
downloads.push({ scope: 'chapter', label: 'This chapter', title: lesson.title, cells });
|
|
117
|
+
downloads.push({ scope: 'chapter', label: pageDeck ? 'Its chapter' : 'This chapter', title: lesson.title, cells });
|
|
63
118
|
}
|
|
64
119
|
}
|
|
120
|
+
if (slidesRow && !pageDeck) downloads.push(slidesRow);
|
|
65
121
|
if (pageSlug !== undefined) {
|
|
66
122
|
/** A part's or the course's downloads: `stem` names the files a reader saves. */
|
|
67
123
|
const wholeCells = (path: string, stem: string): DownloadCell[] => [
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* plain link to a file the build wrote. The build lists what it wrote, with
|
|
10
10
|
* sizes, in `downloads/index.json`; the menu reads it the first time it
|
|
11
11
|
* opens, shows each size and hides a cell whose file is not there. During
|
|
12
|
-
* `astro dev` only a chapter's HTML
|
|
13
|
-
* are built when the site is deployed.
|
|
12
|
+
* `astro dev` only a chapter's and a deck's HTML build on request; the
|
|
13
|
+
* others say they are built when the site is deployed.
|
|
14
14
|
*
|
|
15
15
|
* **Print.** The button loads every lab that will be on paper before the
|
|
16
16
|
* print dialog opens — the browser's own `beforeprint` cannot wait for that —
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* page's footer too (`PrintController.astro`), so Ctrl+P prepares a
|
|
23
23
|
* downloaded page the same way.
|
|
24
24
|
*/
|
|
25
|
-
export type DownloadFormat = 'html' | 'pdf' | 'latex' | 'markdown';
|
|
25
|
+
export type DownloadFormat = 'html' | 'pdf' | 'handout' | 'latex' | 'markdown';
|
|
26
26
|
|
|
27
27
|
export interface DownloadCell {
|
|
28
28
|
readonly format: DownloadFormat;
|
|
@@ -32,11 +32,11 @@ export interface DownloadCell {
|
|
|
32
32
|
readonly filename: string;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export type DownloadScope = 'chapter' | 'part' | 'course';
|
|
35
|
+
export type DownloadScope = 'deck' | 'chapter' | 'part' | 'course';
|
|
36
36
|
|
|
37
37
|
export interface DownloadRow {
|
|
38
38
|
readonly scope: DownloadScope;
|
|
39
|
-
/** "This chapter", "This part", "Whole course". */
|
|
39
|
+
/** "These slides", "This chapter", "This part", "Whole course". */
|
|
40
40
|
readonly label: string;
|
|
41
41
|
/** The chapter's title, the part's label or the course's name. */
|
|
42
42
|
readonly title: string;
|
|
@@ -61,6 +61,7 @@ const base = siteBaseUrl.replace(/\/$/u, '');
|
|
|
61
61
|
const development = import.meta.env.DEV;
|
|
62
62
|
/** What a download's accessible name calls each scope. */
|
|
63
63
|
const SCOPE_NAMES: Readonly<Record<DownloadScope, string>> = {
|
|
64
|
+
deck: 'the slides',
|
|
64
65
|
chapter: 'this chapter',
|
|
65
66
|
part: 'this part',
|
|
66
67
|
course: 'the whole course',
|
|
@@ -68,9 +69,25 @@ const SCOPE_NAMES: Readonly<Record<DownloadScope, string>> = {
|
|
|
68
69
|
const FORMATS: readonly { readonly format: DownloadFormat; readonly label: string; readonly name: string }[] = [
|
|
69
70
|
{ format: 'html', label: 'HTML', name: 'as standalone HTML' },
|
|
70
71
|
{ format: 'pdf', label: 'PDF', name: 'as PDF' },
|
|
72
|
+
{ format: 'handout', label: 'Handout', name: 'as a handout, each slide with its notes' },
|
|
71
73
|
{ format: 'latex', label: 'LaTeX', name: 'as LaTeX source' },
|
|
72
74
|
{ format: 'markdown', label: 'Markdown', name: 'as Markdown' },
|
|
73
75
|
];
|
|
76
|
+
/**
|
|
77
|
+
* The button's tooltip names the scopes its menu offers on this page, in the
|
|
78
|
+
* menu's own words ("this chapter", "this part", "the whole course"): a chapter
|
|
79
|
+
* has all three, a part's page two, any other page only the course. Its
|
|
80
|
+
* accessible name stays "Download this page": courses select the button by it.
|
|
81
|
+
*/
|
|
82
|
+
const scopeNames = downloads.map((row) => SCOPE_NAMES[row.scope]);
|
|
83
|
+
const downloadTitle =
|
|
84
|
+
scopeNames.length === 0
|
|
85
|
+
? 'Download this page'
|
|
86
|
+
: `Download ${
|
|
87
|
+
scopeNames.length === 1
|
|
88
|
+
? scopeNames[0]
|
|
89
|
+
: `${scopeNames.slice(0, -1).join(', ')} or ${scopeNames[scopeNames.length - 1]}`
|
|
90
|
+
}`;
|
|
74
91
|
const columns = FORMATS.filter(({ format }) =>
|
|
75
92
|
downloads.some((row) => row.cells.some((cell) => cell.format === format)),
|
|
76
93
|
);
|
|
@@ -80,6 +97,7 @@ const notes = [
|
|
|
80
97
|
(columns.some((column) => column.format === 'latex')
|
|
81
98
|
? 'PDF is typeset for print, and LaTeX is its source.'
|
|
82
99
|
: 'PDF is typeset for print.'),
|
|
100
|
+
columns.some((column) => column.format === 'handout') && 'Handout is each slide with its speaker notes.',
|
|
83
101
|
columns.some((column) => column.format === 'markdown') && 'Markdown is plain text with images.',
|
|
84
102
|
].filter(Boolean);
|
|
85
103
|
---
|
|
@@ -96,7 +114,7 @@ const notes = [
|
|
|
96
114
|
class="lesson-export-actions__download"
|
|
97
115
|
popovertarget={menuId}
|
|
98
116
|
aria-label="Download this page"
|
|
99
|
-
title=
|
|
117
|
+
title={downloadTitle}
|
|
100
118
|
>
|
|
101
119
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
102
120
|
<path d="M12 3v12m0 0 4-4m-4 4-4-4M5 19h14" />
|
|
@@ -148,9 +166,9 @@ const notes = [
|
|
|
148
166
|
const cell = row.cells.find((entry) => entry.format === column.format);
|
|
149
167
|
if (!cell) return <td><span class="explico-downloads__none" aria-hidden="true">—</span></td>;
|
|
150
168
|
const name = `Download ${SCOPE_NAMES[row.scope]} ${column.name}`;
|
|
151
|
-
// `astro dev` builds a chapter's
|
|
152
|
-
// exists only once the site is built.
|
|
153
|
-
if (development && !(row.scope === 'chapter' && column.format === 'html')) {
|
|
169
|
+
// `astro dev` builds a chapter's and a deck's HTML on
|
|
170
|
+
// request; the rest exists only once the site is built.
|
|
171
|
+
if (development && !((row.scope === 'chapter' || row.scope === 'deck') && column.format === 'html')) {
|
|
154
172
|
return (
|
|
155
173
|
<td>
|
|
156
174
|
<span
|
|
@@ -19,6 +19,7 @@ import DefaultFooter from '@astrojs/starlight/components/Footer.astro';
|
|
|
19
19
|
import { assertLocalNotationParity } from '../../compiler/astro';
|
|
20
20
|
import { loadManifest } from '../../compiler/manifest';
|
|
21
21
|
import {
|
|
22
|
+
DECK_ROW_CLASS,
|
|
22
23
|
isLessonSlug,
|
|
23
24
|
lessonIdFromSlug,
|
|
24
25
|
docsSlugsForRoute,
|
|
@@ -36,6 +37,8 @@ import CollapsibleSections from './CollapsibleSections.astro';
|
|
|
36
37
|
import TableOverflowExpander from '../table/TableOverflowExpander.astro';
|
|
37
38
|
import DiagramEnhancer from './DiagramEnhancer.astro';
|
|
38
39
|
import PrintController from './PrintController.astro';
|
|
40
|
+
import DeckEnhancer from '../deck/DeckEnhancer.astro';
|
|
41
|
+
import { deckNeighbours, deckOfRoute } from '../deck/deck-context';
|
|
39
42
|
import {
|
|
40
43
|
derivedNotationForPage,
|
|
41
44
|
notationDefinitionsForRenderedPage,
|
|
@@ -54,7 +57,62 @@ const routeSlugs = docsSlugsForRoute(routeSlug);
|
|
|
54
57
|
const pageId =
|
|
55
58
|
lessonId ?? manifest.pages.find((page) => routeSlugs.includes(page.slug))?.id;
|
|
56
59
|
|
|
60
|
+
// Next and Previous step from chapter to chapter: a chapter's slides are a
|
|
61
|
+
// link of their own under it in the sidebar, not a page of the reading order.
|
|
62
|
+
// On a deck's page they step from deck to deck instead, each named
|
|
63
|
+
// "Slides: *title*" (`deckNeighbours`): a reader of the slides reads on in
|
|
64
|
+
// the slides.
|
|
65
|
+
{
|
|
66
|
+
type SidebarEntry = {
|
|
67
|
+
type: string;
|
|
68
|
+
label?: string;
|
|
69
|
+
href?: string;
|
|
70
|
+
attrs?: Record<string, unknown>;
|
|
71
|
+
isCurrent?: boolean;
|
|
72
|
+
entries?: SidebarEntry[];
|
|
73
|
+
};
|
|
74
|
+
const route = Astro.locals.starlightRoute as unknown as {
|
|
75
|
+
sidebar: SidebarEntry[];
|
|
76
|
+
pagination: { prev?: SidebarEntry; next?: SidebarEntry };
|
|
77
|
+
};
|
|
78
|
+
const links: SidebarEntry[] = [];
|
|
79
|
+
const flatten = (entries: readonly SidebarEntry[]) => {
|
|
80
|
+
for (const entry of entries) {
|
|
81
|
+
if (entry.type === 'group') flatten(entry.entries ?? []);
|
|
82
|
+
else links.push(entry);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
flatten(route.sidebar ?? []);
|
|
86
|
+
const isDeckRow = (entry?: SidebarEntry) =>
|
|
87
|
+
String(entry?.attrs?.class ?? '').split(/\s+/).includes(DECK_ROW_CLASS);
|
|
88
|
+
const current = links.findIndex((entry) => entry.isCurrent);
|
|
89
|
+
const beyond = (direction: 1 | -1) => {
|
|
90
|
+
for (let index = current + direction; index >= 0 && index < links.length; index += direction) {
|
|
91
|
+
if (!isDeckRow(links[index])) return links[index];
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
};
|
|
95
|
+
if (current >= 0 && route.pagination) {
|
|
96
|
+
if (links[current]?.attrs?.['data-edition'] === 'slides') {
|
|
97
|
+
const base = import.meta.env.BASE_URL.replace(/\/$/, '');
|
|
98
|
+
const titleByHref = new Map(
|
|
99
|
+
(manifest.decks ?? []).map((deck) => [`${base}/${deck.slug}`, deck.title]),
|
|
100
|
+
);
|
|
101
|
+
const { prev, next } = deckNeighbours(links, current, (link) =>
|
|
102
|
+
titleByHref.get(String(link.href ?? '').replace(/\/$/, '')),
|
|
103
|
+
);
|
|
104
|
+
route.pagination.prev = prev;
|
|
105
|
+
route.pagination.next = next;
|
|
106
|
+
} else {
|
|
107
|
+
if (isDeckRow(route.pagination.next)) route.pagination.next = beyond(1);
|
|
108
|
+
if (isDeckRow(route.pagination.prev)) route.pagination.prev = beyond(-1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
57
113
|
const glossaryBase = `${import.meta.env.BASE_URL.replace(/\/$/, '')}/glossary/`;
|
|
114
|
+
// A lesson's deck: its frames, Present mode and the presenter window.
|
|
115
|
+
const deck = deckOfRoute(manifest, routeSlug);
|
|
58
116
|
|
|
59
117
|
const definitions: NotationLayerDefinition[] = [];
|
|
60
118
|
let notationDefaultOpen = false;
|
|
@@ -164,6 +222,7 @@ if (pageId) {
|
|
|
164
222
|
{lessonId && <CollapsibleSections />}
|
|
165
223
|
{lessonId && <TableOverflowExpander />}
|
|
166
224
|
<DiagramEnhancer />
|
|
225
|
+
{deck && <DeckEnhancer />}
|
|
167
226
|
{lessonId && <HeadingAnchorEnhancer />}
|
|
168
227
|
{
|
|
169
228
|
pageId && manifest.lessonExports.print && (
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* The one question explico asks a reader (`session/reading-memory.ts`): may
|
|
4
|
+
* this course remember their reading in this browser — which chapters they
|
|
5
|
+
* opened, and the explanation cards they keep or carry, across visits?
|
|
6
|
+
*
|
|
7
|
+
* It sits at the bottom of the window, over nothing the reader needs, until
|
|
8
|
+
* they answer, on every page; the two answers are the same size and weight.
|
|
9
|
+
* Before an answer nothing of their reading is kept beyond the tab, so
|
|
10
|
+
* ignoring the question is the same as "Don't remember" for now. The answer
|
|
11
|
+
* stands for 13 months; the `[?]` panel changes it or forgets everything.
|
|
12
|
+
*
|
|
13
|
+
* Rendered once per page by the header, with the course's `readingMemory`
|
|
14
|
+
* setting and base path on `[data-explico-reading-memory]`, which every
|
|
15
|
+
* reading-memory reader on the page looks at. A course with the setting
|
|
16
|
+
* `off` gets the element but never the question. Without JavaScript nothing
|
|
17
|
+
* is stored at all, so the question stays hidden.
|
|
18
|
+
*/
|
|
19
|
+
import { loadManifest } from '../../compiler/manifest';
|
|
20
|
+
|
|
21
|
+
const manifest = await loadManifest();
|
|
22
|
+
const mode = manifest.readingMemory ?? 'ask';
|
|
23
|
+
const base = import.meta.env.BASE_URL;
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<div
|
|
27
|
+
data-explico-reading-memory={mode}
|
|
28
|
+
data-explico-base={base}
|
|
29
|
+
hidden
|
|
30
|
+
>
|
|
31
|
+
</div>
|
|
32
|
+
{
|
|
33
|
+
mode === 'ask' && (
|
|
34
|
+
<section
|
|
35
|
+
class="explico-reading-notice"
|
|
36
|
+
aria-label="Remember your reading?"
|
|
37
|
+
data-explico-reading-notice
|
|
38
|
+
hidden
|
|
39
|
+
>
|
|
40
|
+
<p class="explico-reading-notice__text">
|
|
41
|
+
<strong>Remember your reading?</strong> This course can keep, in this
|
|
42
|
+
browser only, which chapters you open, which slides you present and
|
|
43
|
+
the explanation cards you keep open, so it can mark new and changed
|
|
44
|
+
chapters and slides and bring your cards back next time. No cookies;
|
|
45
|
+
nothing is sent anywhere.
|
|
46
|
+
</p>
|
|
47
|
+
<div class="explico-reading-notice__actions">
|
|
48
|
+
<button type="button" data-reading-answer="yes">
|
|
49
|
+
Remember
|
|
50
|
+
</button>
|
|
51
|
+
<button type="button" data-reading-answer="no">
|
|
52
|
+
Don't remember
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
<p class="explico-reading-notice__note">
|
|
56
|
+
Your settings, such as the theme and the text size, are kept either
|
|
57
|
+
way. Change your answer any time with the{' '}
|
|
58
|
+
<span aria-hidden="true">?</span><span class="sr-only">About</span>{' '}
|
|
59
|
+
button at the top of the page.
|
|
60
|
+
</p>
|
|
61
|
+
</section>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
<script>
|
|
66
|
+
import {
|
|
67
|
+
answerReadingMemory,
|
|
68
|
+
readingAnswer,
|
|
69
|
+
readingMemoryMode,
|
|
70
|
+
READING_MEMORY_EVENT,
|
|
71
|
+
} from '../../session/reading-memory';
|
|
72
|
+
|
|
73
|
+
(() => {
|
|
74
|
+
const config = document.querySelector<HTMLElement>(
|
|
75
|
+
'[data-explico-reading-memory]',
|
|
76
|
+
);
|
|
77
|
+
const notice = document.querySelector<HTMLElement>(
|
|
78
|
+
'[data-explico-reading-notice]',
|
|
79
|
+
);
|
|
80
|
+
if (!config || !notice) return;
|
|
81
|
+
// A standalone export is one page with no other pages to remember.
|
|
82
|
+
if (document.documentElement.hasAttribute('data-explico-standalone')) return;
|
|
83
|
+
const base = config.dataset.explicoBase ?? '/';
|
|
84
|
+
// Out of the header, whose own positioning would otherwise hold it: a
|
|
85
|
+
// fixed box at the window's bottom, last in the reading order.
|
|
86
|
+
document.body.append(notice);
|
|
87
|
+
|
|
88
|
+
const sync = () => {
|
|
89
|
+
notice.hidden =
|
|
90
|
+
readingMemoryMode() !== 'ask' || readingAnswer(base) !== 'unasked';
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
notice.addEventListener('click', (event) => {
|
|
94
|
+
const button =
|
|
95
|
+
event.target instanceof Element
|
|
96
|
+
? event.target.closest<HTMLButtonElement>('button[data-reading-answer]')
|
|
97
|
+
: null;
|
|
98
|
+
if (!button) return;
|
|
99
|
+
const hadFocus = notice.contains(document.activeElement);
|
|
100
|
+
answerReadingMemory(base, button.dataset.readingAnswer === 'yes');
|
|
101
|
+
// The question goes; focus goes back to the page, not to nowhere.
|
|
102
|
+
if (hadFocus) {
|
|
103
|
+
const title = document.querySelector<HTMLElement>('main h1');
|
|
104
|
+
if (title) {
|
|
105
|
+
title.tabIndex = -1;
|
|
106
|
+
title.focus({ preventScroll: true });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
window.addEventListener(READING_MEMORY_EVENT, sync);
|
|
112
|
+
sync();
|
|
113
|
+
})();
|
|
114
|
+
</script>
|
|
@@ -1,24 +1,32 @@
|
|
|
1
1
|
---
|
|
2
2
|
/**
|
|
3
|
-
* The reader's text size on a touch screen:
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* The reader's text size on a touch screen: 50% to 100% of the site's own, in
|
|
4
|
+
* five steps of a geometric series (each 2^(1/4), about 1.19, times the one
|
|
5
|
+
* below: 50, 59, 71, 84, 100). A PC has the browser's zoom for this; a
|
|
6
|
+
* phone's text-size settings are buried in its browser's menus, so the page
|
|
7
|
+
* offers its own. It sits beside the theme switch (in the phone menu, and in
|
|
8
|
+
* the header of a tablet held wide) and shows only where there is no hover.
|
|
8
9
|
*
|
|
9
10
|
* The choice scales the root font size (`data-text-size` on `<html>`,
|
|
10
11
|
* `global.css`), so the text, the math, the spacing and the cards all
|
|
11
|
-
* follow
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* follow, and sets `--explico-text-scale` for what is drawn in px, a
|
|
13
|
+
* diagram, which `Diagram.astro` scales as a whole. `global.css` has one rule per step: the end-to-end
|
|
14
|
+
* test holds it to this list. It is remembered per reader and copied onto
|
|
15
|
+
* the root before the page first paints by the inline script below, so the
|
|
16
|
+
* page never jumps from one size to another. A size an earlier release
|
|
17
|
+
* offered and this one does not (90, 80, 70) is ignored: the page shows 100%. `LayoutSocialIcons.astro` renders this in the
|
|
14
18
|
* header and in the phone menu; both copies stay in step.
|
|
15
19
|
*/
|
|
16
20
|
import { Icon } from '@astrojs/starlight/components';
|
|
17
21
|
import { inlineRootAttributeScript } from '../../session/PreferenceStore';
|
|
18
22
|
|
|
19
23
|
const TEXT_SIZE_KEY = 'explico:display:text-size';
|
|
20
|
-
const
|
|
21
|
-
|
|
24
|
+
const STEPS = 4;
|
|
25
|
+
/** 50, 59, 71, 84, 100: the series' ratio is 2^(1/STEPS), and its end is 100. */
|
|
26
|
+
const SIZES = Array.from({ length: STEPS + 1 }, (_, step) =>
|
|
27
|
+
String(Math.round(100 * 2 ** ((step - STEPS) / STEPS))),
|
|
28
|
+
);
|
|
29
|
+
const boot = inlineRootAttributeScript(TEXT_SIZE_KEY, 'data-text-size', SIZES.slice(0, -1));
|
|
22
30
|
---
|
|
23
31
|
|
|
24
32
|
<script is:inline set:html={boot} />
|
|
@@ -35,15 +43,18 @@ const boot = inlineRootAttributeScript(TEXT_SIZE_KEY, 'data-text-size', SIZES.sl
|
|
|
35
43
|
import { createBrowserPreferenceStore } from '../../session/PreferenceStore';
|
|
36
44
|
|
|
37
45
|
const KEY = 'explico:display:text-size';
|
|
38
|
-
const SIZES = new Set(['100', '90', '80', '70']);
|
|
39
46
|
const preferences = createBrowserPreferenceStore();
|
|
40
47
|
const root = document.documentElement;
|
|
41
48
|
const selects = () =>
|
|
42
49
|
Array.from(document.querySelectorAll<HTMLSelectElement>('[data-text-size-select]'));
|
|
43
50
|
|
|
51
|
+
// The steps are the options the page rendered, so the list lives in one place.
|
|
52
|
+
const sizes = (): Set<string> =>
|
|
53
|
+
new Set(selects().flatMap((select) => Array.from(select.options, (option) => option.value)));
|
|
54
|
+
|
|
44
55
|
const current = (): string => {
|
|
45
56
|
const value = root.getAttribute('data-text-size') ?? '100';
|
|
46
|
-
return
|
|
57
|
+
return sizes().has(value) ? value : '100';
|
|
47
58
|
};
|
|
48
59
|
|
|
49
60
|
const show = (value: string) => {
|
|
@@ -51,7 +62,7 @@ const boot = inlineRootAttributeScript(TEXT_SIZE_KEY, 'data-text-size', SIZES.sl
|
|
|
51
62
|
};
|
|
52
63
|
|
|
53
64
|
const choose = (value: string) => {
|
|
54
|
-
if (!
|
|
65
|
+
if (!sizes().has(value)) return;
|
|
55
66
|
if (value === '100') {
|
|
56
67
|
root.removeAttribute('data-text-size');
|
|
57
68
|
preferences.remove?.(KEY);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sidebar's marks for one reader (`sidebarMarks` in the course settings):
|
|
3
|
+
* a quiet ring beside every chapter and part page they have not opened yet,
|
|
4
|
+
* and an accent dot beside every one whose content changed since they last
|
|
5
|
+
* opened it.
|
|
6
|
+
*
|
|
7
|
+
* A page is named by its slug, and its content by the hash rule C5 keeps for
|
|
8
|
+
* it in `content/versions.json` (the current one, `pending ?? source`), cut to
|
|
9
|
+
* 12 hex digits. That hash moves with every change to what the page shows —
|
|
10
|
+
* its text, its checks, the shared notation and sources it uses, its numbers
|
|
11
|
+
* — and not when a release only stamps a version on it. So "changed" means
|
|
12
|
+
* "not what this reader saw", which an unreleased change on the live site
|
|
13
|
+
* already is, and a release of something they had read is not.
|
|
14
|
+
*
|
|
15
|
+
* What a reader has seen is view state, the same kind as an unread mark, not
|
|
16
|
+
* learning progress: it claims nothing about what they learned. It is one
|
|
17
|
+
* small JSON map per course, `{ "v": 1, "seen": { slug: id } }`, kept through
|
|
18
|
+
* the `PreferenceStore` under a key scoped by the site's base path, since
|
|
19
|
+
* several courses can share one origin. It is reading memory
|
|
20
|
+
* (`../../session/reading-memory.ts`): recorded only after the reader said
|
|
21
|
+
* yes to the course keeping it, and deleted with a no. Pure: `LayoutSidebar.astro` does the
|
|
22
|
+
* DOM and the storage, and `tests/unit/page-marks.test.ts` checks this in Node.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** One chapter or part page the sidebar can mark. */
|
|
26
|
+
export interface TrackedPage {
|
|
27
|
+
/** The sidebar link's path, base included: `/course/motion/constant-acceleration/`. */
|
|
28
|
+
readonly href: string;
|
|
29
|
+
/** The docs slug: `motion/constant-acceleration`, `motion/index`. */
|
|
30
|
+
readonly slug: string;
|
|
31
|
+
/** The content's identity: the first 12 hex digits of its rule C5 hash. */
|
|
32
|
+
readonly id: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type PageMark = 'unopened' | 'changed';
|
|
36
|
+
|
|
37
|
+
/** What `LayoutSidebar.astro` hands its script, as JSON. */
|
|
38
|
+
export interface PageMarksPayload {
|
|
39
|
+
/** The storage key, scoped by the site's base path. */
|
|
40
|
+
readonly key: string;
|
|
41
|
+
/** The site's base path, which names the reader's reading-memory answer. */
|
|
42
|
+
readonly base: string;
|
|
43
|
+
/** The course's `sidebarMarks` setting. */
|
|
44
|
+
readonly unopened: boolean;
|
|
45
|
+
readonly changed: boolean;
|
|
46
|
+
readonly pages: readonly TrackedPage[];
|
|
47
|
+
/**
|
|
48
|
+
* Every deck and its frames' ids now, which the legend's two actions cover
|
|
49
|
+
* too (D9, `../../session/deck-memory.ts`): `frames[i]` is frame `i + 1`'s id.
|
|
50
|
+
*/
|
|
51
|
+
readonly decks?: readonly { readonly slug: string; readonly frames: readonly string[] }[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Bumped whenever the stored shape changes; a record of another version is dropped. */
|
|
55
|
+
const VERSION = 1;
|
|
56
|
+
const ID = /^[0-9a-f]{12}$/u;
|
|
57
|
+
|
|
58
|
+
/** The storage key for a site whose base path is `base` (`/`, `/course/`). */
|
|
59
|
+
export function pageMarksKey(base: string): string {
|
|
60
|
+
return `explico-page-marks:${base}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The identity of a page's content: 12 hex digits of its `sha256:…` hash. */
|
|
64
|
+
export function pageMarkId(hash: string): string {
|
|
65
|
+
return hash.replace(/^sha256:/u, '').slice(0, 12);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The pages the reader has seen, by slug, from the stored text. Anything
|
|
70
|
+
* that is not a record of this version, and any entry that is not a slug
|
|
71
|
+
* and an id, is dropped, never trusted.
|
|
72
|
+
*/
|
|
73
|
+
export function decodeSeen(raw: string | null): Map<string, string> {
|
|
74
|
+
const seen = new Map<string, string>();
|
|
75
|
+
if (!raw) return seen;
|
|
76
|
+
let parsed: unknown;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(raw);
|
|
79
|
+
} catch {
|
|
80
|
+
return seen;
|
|
81
|
+
}
|
|
82
|
+
if (!parsed || typeof parsed !== 'object') return seen;
|
|
83
|
+
const { v, seen: entries } = parsed as { v?: unknown; seen?: unknown };
|
|
84
|
+
if (v !== VERSION || !entries || typeof entries !== 'object' || Array.isArray(entries)) return seen;
|
|
85
|
+
for (const [slug, id] of Object.entries(entries as Record<string, unknown>)) {
|
|
86
|
+
if (typeof id === 'string' && ID.test(id) && slug.length > 0) seen.set(slug, id);
|
|
87
|
+
}
|
|
88
|
+
return seen;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The stored text, slugs sorted so the same state is always the same string. */
|
|
92
|
+
export function encodeSeen(seen: ReadonlyMap<string, string>): string {
|
|
93
|
+
const entries = [...seen].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
94
|
+
return JSON.stringify({ v: VERSION, seen: Object.fromEntries(entries) });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The record after the reader opened `current` (none for a page the sidebar
|
|
99
|
+
* does not mark), keeping only the pages the course still has.
|
|
100
|
+
*/
|
|
101
|
+
export function withVisit(
|
|
102
|
+
seen: ReadonlyMap<string, string>,
|
|
103
|
+
current: TrackedPage | undefined,
|
|
104
|
+
pages: readonly TrackedPage[],
|
|
105
|
+
): Map<string, string> {
|
|
106
|
+
const known = new Set(pages.map((page) => page.slug));
|
|
107
|
+
const next = new Map([...seen].filter(([slug]) => known.has(slug)));
|
|
108
|
+
if (current) next.set(current.slug, current.id);
|
|
109
|
+
return next;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The record with every page seen as it is now: "Mark all as seen". */
|
|
113
|
+
export function withAllSeen(pages: readonly TrackedPage[]): Map<string, string> {
|
|
114
|
+
return new Map(pages.map((page) => [page.slug, page.id]));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The record with no page seen: "Mark all as unseen". The page open when the
|
|
119
|
+
* reader asks is recorded again by {@link withVisit}, since it is open.
|
|
120
|
+
*/
|
|
121
|
+
export function withNoneSeen(): Map<string, string> {
|
|
122
|
+
return new Map();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** A page's mark for a reader who has seen `seen`, under the course's setting. */
|
|
126
|
+
export function markOf(
|
|
127
|
+
page: TrackedPage,
|
|
128
|
+
seen: ReadonlyMap<string, string>,
|
|
129
|
+
options: { readonly unopened: boolean; readonly changed: boolean },
|
|
130
|
+
): PageMark | undefined {
|
|
131
|
+
const id = seen.get(page.slug);
|
|
132
|
+
if (id === undefined) return options.unopened ? 'unopened' : undefined;
|
|
133
|
+
return id !== page.id && options.changed ? 'changed' : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A path as the sidebar compares it: no trailing slash, `/` for the root. */
|
|
137
|
+
export function samePath(href: string): string {
|
|
138
|
+
return href.replace(/\/+$/u, '') || '/';
|
|
139
|
+
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* `hiddenContentParts` / `describeHiddenContent` turn what it found into the
|
|
14
14
|
* reader-facing line.
|
|
15
15
|
*/
|
|
16
|
-
import { FLOAT_KIND_WORDS, floatOf, type NamedFloat } from '
|
|
16
|
+
import { FLOAT_KIND_WORDS, floatOf, type NamedFloat } from '../../reference/float-name';
|
|
17
17
|
|
|
18
18
|
export interface HiddenContentCounts {
|
|
19
19
|
readonly subsections: number;
|