explico 0.13.0 → 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 +894 -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 +1087 -14
- 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 +29 -0
- 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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
import DefaultSidebar from '@astrojs/starlight/components/Sidebar.astro';
|
|
3
|
+
import { SIDEBAR_EDITIONS_DEFAULTS, SIDEBAR_MARKS_DEFAULTS } from '../../compiler/course-config';
|
|
3
4
|
import { loadManifest } from '../../compiler/manifest';
|
|
5
|
+
import { pageMarkId, pageMarksKey, type PageMarksPayload } from './page-marks';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* The header control that opens a part's own page. Starlight's sidebar group
|
|
@@ -19,6 +21,57 @@ const partLinks = manifest.parts
|
|
|
19
21
|
// `<` keeps a label that happens to contain `</script>` from ending the
|
|
20
22
|
// element early. It is still the same JSON to `JSON.parse`.
|
|
21
23
|
const partLinkPayload = JSON.stringify(partLinks).replace(/</g, '\\u003c');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Every chapter and part page the sidebar can mark for a reader
|
|
27
|
+
* (`page-marks.ts`): its link's path and the identity of its content now,
|
|
28
|
+
* from the record rule C5 keeps. Pages outside it (the landing page, the
|
|
29
|
+
* glossary) are never marked.
|
|
30
|
+
*/
|
|
31
|
+
const marks = manifest.sidebarMarks ?? SIDEBAR_MARKS_DEFAULTS;
|
|
32
|
+
const pageMarks: PageMarksPayload | undefined =
|
|
33
|
+
(marks.unopened || marks.changed) &&
|
|
34
|
+
(manifest.readingMemory ?? 'ask') === 'ask' &&
|
|
35
|
+
manifest.pageVersions
|
|
36
|
+
? {
|
|
37
|
+
key: pageMarksKey(import.meta.env.BASE_URL),
|
|
38
|
+
base: import.meta.env.BASE_URL,
|
|
39
|
+
unopened: marks.unopened,
|
|
40
|
+
changed: marks.changed,
|
|
41
|
+
pages: Object.entries(manifest.pageVersions.record.pages).map(
|
|
42
|
+
([slug, entry]) => ({
|
|
43
|
+
href: `${base}/${slug.replace(/\/index$/u, '')}/`,
|
|
44
|
+
slug,
|
|
45
|
+
id: pageMarkId(entry.pending ?? entry.source),
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
48
|
+
// Every deck's frames, by number, for the legend's two actions (D9).
|
|
49
|
+
decks: (manifest.decks ?? []).map((deck) => ({
|
|
50
|
+
slug: deck.slug,
|
|
51
|
+
frames: deck.frames.map((frame) => frame.id),
|
|
52
|
+
})),
|
|
53
|
+
}
|
|
54
|
+
: undefined;
|
|
55
|
+
const pageMarksPayload =
|
|
56
|
+
pageMarks && pageMarks.pages.length > 0
|
|
57
|
+
? JSON.stringify(pageMarks).replace(/</g, '\\u003c')
|
|
58
|
+
: undefined;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The course's default for a lesson's editions in the sidebar
|
|
62
|
+
* (`sidebar-editions.ts`), for a course with a deck in it: without one,
|
|
63
|
+
* every lesson is a chapter and there is nothing to choose.
|
|
64
|
+
*/
|
|
65
|
+
const editions = manifest.sidebarEditions ?? SIDEBAR_EDITIONS_DEFAULTS;
|
|
66
|
+
const hasDeck = manifest.sidebar.some((group) =>
|
|
67
|
+
group.items.some(
|
|
68
|
+
(item) => typeof item === 'object' && 'slug' in item && item.attrs?.['data-edition'] === 'slides',
|
|
69
|
+
),
|
|
70
|
+
);
|
|
71
|
+
// The site's base, for the address of a chapter's section of its part's deck.
|
|
72
|
+
const editionsPayload = hasDeck
|
|
73
|
+
? JSON.stringify({ lead: editions.lead, other: editions.other, base: import.meta.env.BASE_URL }).replace(/</g, '\\u003c')
|
|
74
|
+
: undefined;
|
|
22
75
|
---
|
|
23
76
|
|
|
24
77
|
<div class="layout-navigation-panel" data-layout-panel="navigation">
|
|
@@ -63,6 +116,26 @@ const partLinkPayload = JSON.stringify(partLinks).replace(/</g, '\\u003c');
|
|
|
63
116
|
/>
|
|
64
117
|
)
|
|
65
118
|
}
|
|
119
|
+
{
|
|
120
|
+
pageMarksPayload && (
|
|
121
|
+
<script
|
|
122
|
+
is:inline
|
|
123
|
+
type="application/json"
|
|
124
|
+
data-explico-page-marks
|
|
125
|
+
set:html={pageMarksPayload}
|
|
126
|
+
/>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
{
|
|
130
|
+
editionsPayload && (
|
|
131
|
+
<script
|
|
132
|
+
is:inline
|
|
133
|
+
type="application/json"
|
|
134
|
+
data-explico-sidebar-editions
|
|
135
|
+
set:html={editionsPayload}
|
|
136
|
+
/>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
66
139
|
<script>
|
|
67
140
|
/**
|
|
68
141
|
* Give each part header a control that opens the part's own page. The
|
|
@@ -118,37 +191,745 @@ const partLinkPayload = JSON.stringify(partLinks).replace(/</g, '\\u003c');
|
|
|
118
191
|
</script>
|
|
119
192
|
<script>
|
|
120
193
|
/**
|
|
121
|
-
*
|
|
122
|
-
* the
|
|
194
|
+
* A lesson with a chapter and a deck is one row (`sidebar-editions.ts`):
|
|
195
|
+
* the row opens the lead edition under the lesson's title, and the
|
|
196
|
+
* other edition is an icon at the row's end, a second tile joined to
|
|
197
|
+
* the first in a compact part, or is not listed. The compiler lists
|
|
198
|
+
* both editions, each link naming its edition, lesson and title
|
|
199
|
+
* (`data-edition`, `data-lesson`, `data-title`), so without this
|
|
200
|
+
* script the sidebar is what it wrote: the chapter, then its "Slides"
|
|
201
|
+
* row.
|
|
202
|
+
*
|
|
203
|
+
* Two controls in the panel's foot set the state: "Lessons open as"
|
|
204
|
+
* (Chapter or Slides) and "List the other edition too", over the
|
|
205
|
+
* course's default (`sidebarEditions`). The reader's choice is a
|
|
206
|
+
* setting, kept whatever they answered about reading memory. A change
|
|
207
|
+
* rebuilds the rows in place, from the lists as the compiler wrote
|
|
208
|
+
* them, and tells the progress rail and the marks, which run after
|
|
209
|
+
* this script and read the rows it leaves.
|
|
210
|
+
*
|
|
211
|
+
* A chapter its part's deck covers, with no deck of its own, carries
|
|
212
|
+
* its section's address (`data-deck-href`): that section is the
|
|
213
|
+
* chapter's other edition, a link this script adds beside it. While
|
|
214
|
+
* the part's deck is open, the section of the chapter on screen is lit
|
|
215
|
+
* (`aria-current="location"`), as the deck's page tells it
|
|
216
|
+
* (`DECK_FRAME_EVENT`).
|
|
217
|
+
*
|
|
218
|
+
* A part's Overview and its deck (`data-part-edition`) are the part's
|
|
219
|
+
* one row the same way, Overview standing where a chapter stands.
|
|
220
|
+
* Both keep their own glyph in every place.
|
|
221
|
+
*/
|
|
222
|
+
import { createBrowserPreferenceStore } from '../../session/PreferenceStore';
|
|
223
|
+
import {
|
|
224
|
+
DECK_CHAPTER_ATTRIBUTE,
|
|
225
|
+
DECK_FRAME_EVENT,
|
|
226
|
+
DECK_SECTION_ATTRIBUTE,
|
|
227
|
+
EDITION_DETAIL_CLASS,
|
|
228
|
+
EDITION_OTHER_CLASS,
|
|
229
|
+
GLYPH_CLASS,
|
|
230
|
+
PART_EDITION_ATTRIBUTE,
|
|
231
|
+
PART_ROW_CLASSES,
|
|
232
|
+
ROW_CLASSES,
|
|
233
|
+
SIDEBAR_EDITIONS_EVENT,
|
|
234
|
+
SIDEBAR_EDITIONS_KEY,
|
|
235
|
+
decodeEditionsChoice,
|
|
236
|
+
deckSectionHref,
|
|
237
|
+
editionLabel,
|
|
238
|
+
encodeEditionsChoice,
|
|
239
|
+
lessonRow,
|
|
240
|
+
partDeckDetail,
|
|
241
|
+
partEditionLabel,
|
|
242
|
+
partRow,
|
|
243
|
+
resolveEditions,
|
|
244
|
+
sectionDetail,
|
|
245
|
+
type Edition,
|
|
246
|
+
type EditionsChoice,
|
|
247
|
+
type EditionsState,
|
|
248
|
+
type PartEdition,
|
|
249
|
+
} from './sidebar-editions';
|
|
250
|
+
|
|
251
|
+
(() => {
|
|
252
|
+
const root = document.querySelector<HTMLElement>(
|
|
253
|
+
'#starlight__navigation-content',
|
|
254
|
+
);
|
|
255
|
+
const payload = root?.querySelector('[data-explico-sidebar-editions]');
|
|
256
|
+
if (!root || !payload?.textContent) return;
|
|
257
|
+
const { lead: courseLead, other: courseOther } = decodeEditionsChoice(
|
|
258
|
+
payload.textContent,
|
|
259
|
+
);
|
|
260
|
+
if (!courseLead || !courseOther) return;
|
|
261
|
+
const course: EditionsState = { lead: courseLead, other: courseOther };
|
|
262
|
+
let base = '/';
|
|
263
|
+
try {
|
|
264
|
+
const parsed: unknown = JSON.parse(payload.textContent);
|
|
265
|
+
const given = (parsed as { base?: unknown } | null)?.base;
|
|
266
|
+
if (typeof given === 'string') base = given;
|
|
267
|
+
} catch {}
|
|
268
|
+
|
|
269
|
+
interface Link {
|
|
270
|
+
readonly a: HTMLAnchorElement;
|
|
271
|
+
readonly li: HTMLLIElement;
|
|
272
|
+
readonly edition: Edition;
|
|
273
|
+
}
|
|
274
|
+
interface Lesson {
|
|
275
|
+
readonly title: string;
|
|
276
|
+
readonly links: Link[];
|
|
277
|
+
}
|
|
278
|
+
interface PartLink {
|
|
279
|
+
readonly a: HTMLAnchorElement;
|
|
280
|
+
readonly li: HTMLLIElement;
|
|
281
|
+
readonly edition: PartEdition;
|
|
282
|
+
}
|
|
283
|
+
interface Part {
|
|
284
|
+
/** The part's directory, `data-part`. */
|
|
285
|
+
readonly dir: string;
|
|
286
|
+
/** The part's label, from its deck's `data-title`. */
|
|
287
|
+
title: string;
|
|
288
|
+
/** Overview's own label, as the compiler wrote it. */
|
|
289
|
+
overviewLabel: string;
|
|
290
|
+
readonly links: PartLink[];
|
|
291
|
+
}
|
|
292
|
+
type Entry = HTMLLIElement | Lesson | Part;
|
|
293
|
+
// Not `'part' in entry`: every element has a `part` of its own.
|
|
294
|
+
const isPart = (entry: Entry): entry is Part =>
|
|
295
|
+
!(entry instanceof HTMLLIElement) && 'overviewLabel' in entry;
|
|
296
|
+
const labelOf = (a: HTMLAnchorElement) =>
|
|
297
|
+
a.querySelector<HTMLElement>(':scope > span');
|
|
298
|
+
/** A link's name, and its second line when it has one (`aria-description`). */
|
|
299
|
+
const setLabel = (a: HTMLAnchorElement, text: string) => {
|
|
300
|
+
const span = labelOf(a);
|
|
301
|
+
if (!span) return;
|
|
302
|
+
span.textContent = text;
|
|
303
|
+
const detail = a.getAttribute('aria-description');
|
|
304
|
+
if (!detail) return;
|
|
305
|
+
const line = document.createElement('small');
|
|
306
|
+
line.className = EDITION_DETAIL_CLASS;
|
|
307
|
+
line.setAttribute('aria-hidden', 'true');
|
|
308
|
+
line.textContent = detail;
|
|
309
|
+
span.append(line);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Every chapter's section link, lit while its section is on screen.
|
|
313
|
+
const sections: HTMLAnchorElement[] = [];
|
|
314
|
+
|
|
315
|
+
// Every list as the compiler wrote it, a lesson where its first
|
|
316
|
+
// edition stood and a part where its Overview stood, so each
|
|
317
|
+
// rebuild starts from the same rows.
|
|
318
|
+
const lists = new Map<HTMLUListElement, Entry[]>();
|
|
319
|
+
for (const list of root.querySelectorAll<HTMLUListElement>('ul')) {
|
|
320
|
+
let entries: Entry[] = [];
|
|
321
|
+
const lessons = new Map<string, Lesson>();
|
|
322
|
+
const parts = new Map<string, Part>();
|
|
323
|
+
for (const li of list.querySelectorAll<HTMLLIElement>(':scope > li')) {
|
|
324
|
+
const own = li.querySelector<HTMLAnchorElement>(`:scope > a[${PART_EDITION_ATTRIBUTE}]`);
|
|
325
|
+
const partEdition = own?.getAttribute(PART_EDITION_ATTRIBUTE);
|
|
326
|
+
if (own && (partEdition === 'overview' || partEdition === 'slides')) {
|
|
327
|
+
const dir = own.dataset.part ?? '';
|
|
328
|
+
let part = parts.get(dir);
|
|
329
|
+
if (!part) {
|
|
330
|
+
part = { dir, title: '', overviewLabel: 'Overview', links: [] };
|
|
331
|
+
parts.set(dir, part);
|
|
332
|
+
entries.push(part);
|
|
333
|
+
}
|
|
334
|
+
part.links.push({ a: own, li, edition: partEdition });
|
|
335
|
+
if (partEdition === 'overview') {
|
|
336
|
+
part.overviewLabel = labelOf(own)?.textContent?.trim() || part.overviewLabel;
|
|
337
|
+
} else {
|
|
338
|
+
part.title = own.dataset.title ?? part.title;
|
|
339
|
+
const frames = Number(own.dataset.frames);
|
|
340
|
+
if (Number.isInteger(frames) && frames > 0) {
|
|
341
|
+
own.setAttribute('aria-description', partDeckDetail(own.dataset.chapters ?? '', frames));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const a = li.querySelector<HTMLAnchorElement>(':scope > a[data-lesson]');
|
|
347
|
+
const edition = a?.dataset.edition;
|
|
348
|
+
if (!a || (edition !== 'chapter' && edition !== 'slides')) {
|
|
349
|
+
entries.push(li);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
const slug = a.dataset.lesson ?? '';
|
|
353
|
+
const known = lessons.get(slug);
|
|
354
|
+
if (known) {
|
|
355
|
+
known.links.push({ a, li, edition });
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
const lesson: Lesson = {
|
|
359
|
+
title: a.dataset.title ?? labelOf(a)?.textContent?.trim() ?? '',
|
|
360
|
+
links: [{ a, li, edition }],
|
|
361
|
+
};
|
|
362
|
+
lessons.set(slug, lesson);
|
|
363
|
+
entries.push(lesson);
|
|
364
|
+
}
|
|
365
|
+
// A chapter with no deck of its own, whose section in its part's
|
|
366
|
+
// deck is its other edition: a link like the chapter's, to it.
|
|
367
|
+
for (const lesson of lessons.values()) {
|
|
368
|
+
const [only] = lesson.links;
|
|
369
|
+
const href = only?.a.dataset.deckHref;
|
|
370
|
+
if (!only || lesson.links.length !== 1 || only.edition !== 'chapter' || !href) continue;
|
|
371
|
+
const section = only.a.cloneNode(false) as HTMLAnchorElement;
|
|
372
|
+
section.href = deckSectionHref(base, href);
|
|
373
|
+
section.removeAttribute('aria-current');
|
|
374
|
+
section.removeAttribute('data-deck-href');
|
|
375
|
+
section.setAttribute(DECK_SECTION_ATTRIBUTE, '');
|
|
376
|
+
section.dataset.edition = 'slides';
|
|
377
|
+
const range = only.a.dataset.deckRange;
|
|
378
|
+
if (range) section.setAttribute('aria-description', sectionDetail(range));
|
|
379
|
+
section.append(document.createElement('span'));
|
|
380
|
+
lesson.links.push({ a: section, li: only.li, edition: 'slides' });
|
|
381
|
+
sections.push(section);
|
|
382
|
+
}
|
|
383
|
+
// A part with no deck keeps its Overview's item as the compiler wrote it.
|
|
384
|
+
entries = entries.map((entry) =>
|
|
385
|
+
isPart(entry) && !entry.links.some((link) => link.edition === 'slides')
|
|
386
|
+
? entry.links[0]!.li
|
|
387
|
+
: entry,
|
|
388
|
+
);
|
|
389
|
+
if (lessons.size > 0 || entries.some(isPart)) lists.set(list, entries);
|
|
390
|
+
}
|
|
391
|
+
if (lists.size === 0) return;
|
|
392
|
+
|
|
393
|
+
// The other edition's own mark, on its icon's corner: its `::before`
|
|
394
|
+
// and `::after` draw its glyph.
|
|
395
|
+
const badge = () => {
|
|
396
|
+
const mark = document.createElement('i');
|
|
397
|
+
mark.className = 'layout-edition-mark';
|
|
398
|
+
mark.setAttribute('aria-hidden', 'true');
|
|
399
|
+
return mark;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
/** A part's one row, or none: its deck's own row, unlisted. */
|
|
403
|
+
const partItems = (entry: Part, state: EditionsState): HTMLLIElement[] => {
|
|
404
|
+
const open = entry.links.find(
|
|
405
|
+
(link) => link.a.getAttribute('aria-current') === 'page',
|
|
406
|
+
)?.edition;
|
|
407
|
+
const row = partRow(
|
|
408
|
+
entry.links.some((link) => link.edition === 'overview'),
|
|
409
|
+
state,
|
|
410
|
+
open,
|
|
411
|
+
);
|
|
412
|
+
for (const { a } of entry.links) {
|
|
413
|
+
a.classList.remove(...PART_ROW_CLASSES);
|
|
414
|
+
a.querySelector(':scope > .layout-edition-mark')?.remove();
|
|
415
|
+
}
|
|
416
|
+
if (!row) return [];
|
|
417
|
+
const lead =
|
|
418
|
+
entry.links.find((link) => link.edition === row.lead) ?? entry.links[0]!;
|
|
419
|
+
const other = entry.links.find((link) => link.edition === row.other);
|
|
420
|
+
setLabel(lead.a, partEditionLabel(lead.edition, entry.title, 'lead', entry.overviewLabel));
|
|
421
|
+
lead.li.replaceChildren(lead.a);
|
|
422
|
+
if (other) {
|
|
423
|
+
other.a.classList.add(EDITION_OTHER_CLASS);
|
|
424
|
+
setLabel(other.a, partEditionLabel(other.edition, entry.title, 'other', entry.overviewLabel));
|
|
425
|
+
other.a.append(badge());
|
|
426
|
+
lead.li.append(other.a);
|
|
427
|
+
}
|
|
428
|
+
lead.li.dataset.editions = other ? 'pair' : 'single';
|
|
429
|
+
return [lead.li];
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const arrange = (state: EditionsState) => {
|
|
433
|
+
for (const [list, entries] of lists) {
|
|
434
|
+
list.replaceChildren(
|
|
435
|
+
...entries.flatMap((entry) => {
|
|
436
|
+
if (entry instanceof HTMLLIElement) return [entry];
|
|
437
|
+
if (isPart(entry)) return partItems(entry, state);
|
|
438
|
+
const open = entry.links.find(
|
|
439
|
+
(link) => link.a.getAttribute('aria-current') === 'page',
|
|
440
|
+
)?.edition;
|
|
441
|
+
const row = lessonRow(
|
|
442
|
+
entry.links.map((link) => link.edition),
|
|
443
|
+
state,
|
|
444
|
+
open,
|
|
445
|
+
);
|
|
446
|
+
const lead =
|
|
447
|
+
entry.links.find((link) => link.edition === row.lead) ??
|
|
448
|
+
entry.links[0]!;
|
|
449
|
+
const other = entry.links.find((link) => link.edition === row.other);
|
|
450
|
+
for (const { a } of entry.links) {
|
|
451
|
+
a.classList.remove(...ROW_CLASSES);
|
|
452
|
+
a.querySelector(':scope > .layout-edition-mark')?.remove();
|
|
453
|
+
}
|
|
454
|
+
setLabel(lead.a, editionLabel(lead.edition, entry.title, 'lead'));
|
|
455
|
+
if (row.glyph) lead.a.classList.add(GLYPH_CLASS[row.glyph]);
|
|
456
|
+
lead.li.replaceChildren(lead.a);
|
|
457
|
+
if (other) {
|
|
458
|
+
other.a.classList.add(EDITION_OTHER_CLASS);
|
|
459
|
+
setLabel(other.a, editionLabel(other.edition, entry.title, 'other'));
|
|
460
|
+
other.a.append(badge());
|
|
461
|
+
lead.li.append(other.a);
|
|
462
|
+
}
|
|
463
|
+
// Lets the CSS join a pair's tiles without `:has()`.
|
|
464
|
+
lead.li.dataset.editions = other ? 'pair' : 'single';
|
|
465
|
+
return [lead.li];
|
|
466
|
+
}),
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
/** The section of the chapter on screen in the part's deck open here, lit; none elsewhere. */
|
|
472
|
+
const light = () => {
|
|
473
|
+
const chapter = document.documentElement.getAttribute(DECK_CHAPTER_ATTRIBUTE) ?? '';
|
|
474
|
+
for (const section of sections) {
|
|
475
|
+
if (chapter !== '' && section.dataset.lesson === chapter) {
|
|
476
|
+
section.setAttribute('aria-current', 'location');
|
|
477
|
+
} else {
|
|
478
|
+
section.removeAttribute('aria-current');
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// The controls, first in the panel's foot, above the marks' line.
|
|
484
|
+
const sidebarList = root.querySelector('sl-sidebar-state-persist');
|
|
485
|
+
let foot = root.querySelector<HTMLElement>('.explico-sidebar-foot');
|
|
486
|
+
if (!foot) {
|
|
487
|
+
foot = document.createElement('div');
|
|
488
|
+
foot.className = 'explico-sidebar-foot';
|
|
489
|
+
if (sidebarList) sidebarList.after(foot);
|
|
490
|
+
else root.append(foot);
|
|
491
|
+
}
|
|
492
|
+
const controls = document.createElement('div');
|
|
493
|
+
controls.className = 'explico-sidebar-editions';
|
|
494
|
+
const group = document.createElement('fieldset');
|
|
495
|
+
group.className = 'explico-sidebar-editions__lead';
|
|
496
|
+
const legend = document.createElement('legend');
|
|
497
|
+
legend.textContent = 'Lessons open as';
|
|
498
|
+
const segments = document.createElement('span');
|
|
499
|
+
segments.className = 'explico-sidebar-editions__segments';
|
|
500
|
+
group.append(legend, segments);
|
|
501
|
+
const leads = new Map<Edition, HTMLInputElement>();
|
|
502
|
+
for (const [edition, text] of [
|
|
503
|
+
['chapter', 'Chapter'],
|
|
504
|
+
['slides', 'Slides'],
|
|
505
|
+
] as const) {
|
|
506
|
+
const option = document.createElement('label');
|
|
507
|
+
option.className = 'explico-sidebar-editions__segment';
|
|
508
|
+
const input = document.createElement('input');
|
|
509
|
+
input.type = 'radio';
|
|
510
|
+
input.name = 'explico-sidebar-editions-lead';
|
|
511
|
+
input.value = edition;
|
|
512
|
+
input.addEventListener('change', () => {
|
|
513
|
+
if (input.checked) choose({ lead: edition });
|
|
514
|
+
});
|
|
515
|
+
const name = document.createElement('span');
|
|
516
|
+
name.textContent = text;
|
|
517
|
+
option.append(input, name);
|
|
518
|
+
segments.append(option);
|
|
519
|
+
leads.set(edition, input);
|
|
520
|
+
}
|
|
521
|
+
const listed = document.createElement('input');
|
|
522
|
+
listed.type = 'checkbox';
|
|
523
|
+
listed.addEventListener('change', () =>
|
|
524
|
+
choose({ other: listed.checked ? 'listed' : 'hidden' }),
|
|
525
|
+
);
|
|
526
|
+
const listedOption = document.createElement('label');
|
|
527
|
+
listedOption.className = 'explico-sidebar-editions__other';
|
|
528
|
+
listedOption.append(listed, 'List the other edition too');
|
|
529
|
+
controls.append(group, listedOption);
|
|
530
|
+
foot.prepend(controls);
|
|
531
|
+
|
|
532
|
+
const store = createBrowserPreferenceStore();
|
|
533
|
+
const show = (state: EditionsState) => {
|
|
534
|
+
arrange(state);
|
|
535
|
+
light();
|
|
536
|
+
const lead = leads.get(state.lead);
|
|
537
|
+
if (lead) lead.checked = true;
|
|
538
|
+
listed.checked = state.other === 'listed';
|
|
539
|
+
window.dispatchEvent(
|
|
540
|
+
new CustomEvent<EditionsState>(SIDEBAR_EDITIONS_EVENT, { detail: state }),
|
|
541
|
+
);
|
|
542
|
+
};
|
|
543
|
+
/** The reader's change, kept with the fields they set before: a field never set keeps the course's default. */
|
|
544
|
+
const choose = (change: EditionsChoice) => {
|
|
545
|
+
const choice = {
|
|
546
|
+
...decodeEditionsChoice(store.get(SIDEBAR_EDITIONS_KEY)),
|
|
547
|
+
...change,
|
|
548
|
+
};
|
|
549
|
+
store.set(SIDEBAR_EDITIONS_KEY, encodeEditionsChoice(choice));
|
|
550
|
+
show(resolveEditions(course, choice));
|
|
551
|
+
};
|
|
552
|
+
show(resolveEditions(course, decodeEditionsChoice(store.get(SIDEBAR_EDITIONS_KEY))));
|
|
553
|
+
// Before the rail's listener, which counts to the section lit here.
|
|
554
|
+
window.addEventListener(DECK_FRAME_EVENT, light);
|
|
555
|
+
})();
|
|
556
|
+
</script>
|
|
557
|
+
<script>
|
|
558
|
+
/**
|
|
559
|
+
* Mark every row the reader has already passed, so the rail beside the
|
|
560
|
+
* chapter list can show how far into the course this page is.
|
|
123
561
|
*
|
|
124
562
|
* The sidebar's document order *is* the reading order — the compiler
|
|
125
563
|
* built it from the merged track order — so "covered" is simply
|
|
126
564
|
* "earlier in this list than the current page". A part whose chapters
|
|
127
565
|
* are all covered is covered itself, which is what makes the rail read
|
|
128
566
|
* as one run rather than as a mark per group.
|
|
567
|
+
*
|
|
568
|
+
* A row is a list item with a link of its own. A lesson's row holds its
|
|
569
|
+
* editions (`sidebar-editions.ts`) and is the open page's row when any
|
|
570
|
+
* of them is the open page, so a lesson whose deck is open is not
|
|
571
|
+
* behind the reader. The rows are counted again when the reader
|
|
572
|
+
* switches the editions. With a part's deck open, the current row is
|
|
573
|
+
* the one whose section is on screen (`aria-current="location"`),
|
|
574
|
+
* counted again as the deck moves.
|
|
129
575
|
*/
|
|
576
|
+
import { DECK_FRAME_EVENT, SIDEBAR_EDITIONS_EVENT } from './sidebar-editions';
|
|
577
|
+
|
|
130
578
|
(() => {
|
|
131
|
-
const root = document.querySelector(
|
|
132
|
-
|
|
133
|
-
(link) => !link.classList.contains('layout-part-open'),
|
|
134
|
-
);
|
|
135
|
-
const current = links.findIndex(
|
|
136
|
-
(link) => link.getAttribute('aria-current') === 'page',
|
|
579
|
+
const root = document.querySelector<HTMLElement>(
|
|
580
|
+
'#starlight__navigation-content',
|
|
137
581
|
);
|
|
138
|
-
if (
|
|
582
|
+
if (!root) return;
|
|
139
583
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
for (const group of root?.querySelectorAll('details') ?? []) {
|
|
144
|
-
const chapters = [...group.querySelectorAll(':scope > ul > li')];
|
|
145
|
-
if (
|
|
146
|
-
chapters.length > 0 &&
|
|
147
|
-
chapters.every((item) => item.hasAttribute('data-covered'))
|
|
148
|
-
) {
|
|
149
|
-
group.setAttribute('data-covered', '');
|
|
584
|
+
const cover = () => {
|
|
585
|
+
for (const item of root.querySelectorAll('[data-covered]')) {
|
|
586
|
+
item.removeAttribute('data-covered');
|
|
150
587
|
}
|
|
588
|
+
const rows = [...root.querySelectorAll('li')].filter((item) =>
|
|
589
|
+
item.querySelector(':scope > a'),
|
|
590
|
+
);
|
|
591
|
+
const at = (current: string) =>
|
|
592
|
+
rows.findIndex((item) =>
|
|
593
|
+
item.querySelector(`:scope > a[aria-current="${current}"]`),
|
|
594
|
+
);
|
|
595
|
+
const lit = at('location');
|
|
596
|
+
const current = lit === -1 ? at('page') : lit;
|
|
597
|
+
if (current < 1) return;
|
|
598
|
+
|
|
599
|
+
for (const row of rows.slice(0, current)) {
|
|
600
|
+
row.setAttribute('data-covered', '');
|
|
601
|
+
}
|
|
602
|
+
for (const group of root.querySelectorAll('details')) {
|
|
603
|
+
const chapters = [...group.querySelectorAll(':scope > ul > li')];
|
|
604
|
+
if (
|
|
605
|
+
chapters.length > 0 &&
|
|
606
|
+
chapters.every((item) => item.hasAttribute('data-covered'))
|
|
607
|
+
) {
|
|
608
|
+
group.setAttribute('data-covered', '');
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
cover();
|
|
613
|
+
window.addEventListener(SIDEBAR_EDITIONS_EVENT, cover);
|
|
614
|
+
window.addEventListener(DECK_FRAME_EVENT, cover);
|
|
615
|
+
})();
|
|
616
|
+
</script>
|
|
617
|
+
<script>
|
|
618
|
+
/**
|
|
619
|
+
* Mark, for this reader, every chapter and part page they have not
|
|
620
|
+
* opened yet (a quiet ring) and every one whose content changed since
|
|
621
|
+
* they last opened it (an accent dot), then record this page as seen.
|
|
622
|
+
* What a mark means is in `page-marks.ts`; how it looks, in
|
|
623
|
+
* `global.css`: at the end of a labelled row, on the corner of a
|
|
624
|
+
* compact tile, after a closed part's label, and on the collapsed
|
|
625
|
+
* panel's restore control.
|
|
626
|
+
*
|
|
627
|
+
* Each marked link names its mark as its description
|
|
628
|
+
* (`aria-describedby`), and a line at the end of the panel says what
|
|
629
|
+
* the marks on screen mean, with "Mark all as seen" and "Mark all as
|
|
630
|
+
* unseen". The line stays while the record is kept, marks or none, so
|
|
631
|
+
* the reader can always go back. Both actions cover every deck's
|
|
632
|
+
* frames too (D9, `../../session/deck-memory.ts`), whose marks a deck's own
|
|
633
|
+
* page shows. On a page that changed since the reader last opened
|
|
634
|
+
* it, a line under its title says so.
|
|
635
|
+
*
|
|
636
|
+
* A lesson's row (`sidebar-editions.ts`) carries its own link's mark;
|
|
637
|
+
* the other edition's icon carries its own, on its corner. The marks
|
|
638
|
+
* follow the rows when the reader switches the editions. A chapter's
|
|
639
|
+
* section of its part's deck carries none: it is no page, and the
|
|
640
|
+
* part's deck's row carries the deck page's.
|
|
641
|
+
*
|
|
642
|
+
* The pages a reader opened are reading memory
|
|
643
|
+
* (`session/reading-memory.ts`): recorded, and marked, only after they
|
|
644
|
+
* said yes to it. Before an answer, after a no, and without storage,
|
|
645
|
+
* nothing is recorded and nothing is marked. An answer given on this
|
|
646
|
+
* page takes effect at once.
|
|
647
|
+
*/
|
|
648
|
+
import { createBrowserPreferenceStore } from '../../session/PreferenceStore';
|
|
649
|
+
import {
|
|
650
|
+
deckPositionsKey,
|
|
651
|
+
READING_MEMORY_EVENT,
|
|
652
|
+
readingMemoryStore,
|
|
653
|
+
rememberReading,
|
|
654
|
+
} from '../../session/reading-memory';
|
|
655
|
+
import {
|
|
656
|
+
DECK_MEMORY_EVENT,
|
|
657
|
+
decodeDeckMemory,
|
|
658
|
+
encodeDeckMemory,
|
|
659
|
+
frameMark,
|
|
660
|
+
withEveryDeckSeen,
|
|
661
|
+
withEveryDeckUnseen,
|
|
662
|
+
type DeckMemory,
|
|
663
|
+
} from '../../session/deck-memory';
|
|
664
|
+
import {
|
|
665
|
+
decodeSeen,
|
|
666
|
+
encodeSeen,
|
|
667
|
+
markOf,
|
|
668
|
+
samePath,
|
|
669
|
+
withAllSeen,
|
|
670
|
+
withNoneSeen,
|
|
671
|
+
withVisit,
|
|
672
|
+
type PageMark,
|
|
673
|
+
type PageMarksPayload,
|
|
674
|
+
} from './page-marks';
|
|
675
|
+
import { DECK_SECTION_ATTRIBUTE, EDITION_OTHER_CLASS, SIDEBAR_EDITIONS_EVENT } from './sidebar-editions';
|
|
676
|
+
|
|
677
|
+
(() => {
|
|
678
|
+
const payloadElement = document.querySelector(
|
|
679
|
+
'[data-explico-page-marks]',
|
|
680
|
+
);
|
|
681
|
+
const root = document.querySelector<HTMLElement>(
|
|
682
|
+
'#starlight__navigation-content',
|
|
683
|
+
);
|
|
684
|
+
if (!payloadElement?.textContent || !root) return;
|
|
685
|
+
let payload: PageMarksPayload;
|
|
686
|
+
try {
|
|
687
|
+
payload = JSON.parse(payloadElement.textContent);
|
|
688
|
+
} catch {
|
|
689
|
+
return;
|
|
151
690
|
}
|
|
691
|
+
|
|
692
|
+
const store = createBrowserPreferenceStore();
|
|
693
|
+
const pathOf = (href: string) =>
|
|
694
|
+
samePath(new URL(href, location.href).pathname);
|
|
695
|
+
const byPath = new Map(
|
|
696
|
+
payload.pages.map((page) => [samePath(page.href), page]),
|
|
697
|
+
);
|
|
698
|
+
const current = byPath.get(pathOf(location.href));
|
|
699
|
+
const changedNote = document.querySelector<HTMLElement>(
|
|
700
|
+
'[data-explico-page-changed]',
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
const TEXT: Record<PageMark, string> = {
|
|
704
|
+
unopened: 'Not opened yet',
|
|
705
|
+
changed: 'Changed since you last opened it',
|
|
706
|
+
};
|
|
707
|
+
const KINDS: readonly PageMark[] = ['unopened', 'changed'];
|
|
708
|
+
const panel = root.closest<HTMLElement>('.layout-navigation-panel');
|
|
709
|
+
// In the panel's foot, right under the chapter list, after the
|
|
710
|
+
// editions' controls: on a phone, Starlight's own controls follow it
|
|
711
|
+
// in the same menu, and the legend belongs with the marks.
|
|
712
|
+
const list = root.querySelector('sl-sidebar-state-persist');
|
|
713
|
+
const place = (element: HTMLElement) => {
|
|
714
|
+
let foot = root.querySelector<HTMLElement>('.explico-sidebar-foot');
|
|
715
|
+
if (!foot) {
|
|
716
|
+
foot = document.createElement('div');
|
|
717
|
+
foot.className = 'explico-sidebar-foot';
|
|
718
|
+
if (list) list.after(foot);
|
|
719
|
+
else root.append(foot);
|
|
720
|
+
}
|
|
721
|
+
foot.append(element);
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const setMarks = (element: HTMLElement | null, marks: Set<PageMark>) => {
|
|
725
|
+
if (!element) return;
|
|
726
|
+
if (marks.size > 0) element.dataset.pageMarks = [...marks].sort().join(' ');
|
|
727
|
+
else delete element.dataset.pageMarks;
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// Every deck's frames as the decks' pages number them (`deck-memory.ts`).
|
|
731
|
+
const decks = (payload.decks ?? []).map((deck) => ({
|
|
732
|
+
slug: deck.slug,
|
|
733
|
+
frames: deck.frames.map((id, index) => ({ number: index + 1, id })),
|
|
734
|
+
}));
|
|
735
|
+
const deckKey = deckPositionsKey(payload.base);
|
|
736
|
+
const readDecks = () => decodeDeckMemory(readingMemoryStore(payload.base).get(deckKey));
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* What the decks' frames hold for the two actions: a frame the
|
|
740
|
+
* course would mark, and a frame recorded as seen.
|
|
741
|
+
*/
|
|
742
|
+
const deckState = () => {
|
|
743
|
+
const memory = readDecks();
|
|
744
|
+
const marked = decks.some((deck) =>
|
|
745
|
+
deck.frames.some((frame) => {
|
|
746
|
+
const mark = frameMark(frame, memory.get(deck.slug));
|
|
747
|
+
return mark === 'unseen' ? payload.unopened : mark === 'changed' && payload.changed;
|
|
748
|
+
}),
|
|
749
|
+
);
|
|
750
|
+
const seen = decks.some((deck) => (memory.get(deck.slug)?.seen.size ?? 0) > 0);
|
|
751
|
+
return { marked, seen };
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
// The two actions, in this order; each covers every deck's frames
|
|
755
|
+
// too (D9), which the decks' pages mark.
|
|
756
|
+
const ACTIONS = {
|
|
757
|
+
seen: {
|
|
758
|
+
label: 'Mark all as seen',
|
|
759
|
+
done: decks.length > 0 ? 'Every page and slide is marked as seen.' : 'Every page is marked as seen.',
|
|
760
|
+
record: () => withAllSeen(payload.pages),
|
|
761
|
+
decks: (memory: DeckMemory) => withEveryDeckSeen(memory, decks),
|
|
762
|
+
},
|
|
763
|
+
unseen: {
|
|
764
|
+
label: 'Mark all as unseen',
|
|
765
|
+
done:
|
|
766
|
+
decks.length > 0
|
|
767
|
+
? 'Every page is marked as not opened yet, and every slide as not shown yet.'
|
|
768
|
+
: 'Every page is marked as not opened yet.',
|
|
769
|
+
// The open page is recorded again: it is open.
|
|
770
|
+
record: () => withVisit(withNoneSeen(), current, payload.pages),
|
|
771
|
+
decks: (memory: DeckMemory) => withEveryDeckUnseen(memory),
|
|
772
|
+
},
|
|
773
|
+
} as const;
|
|
774
|
+
type Action = keyof typeof ACTIONS;
|
|
775
|
+
|
|
776
|
+
/** A button out of use keeps its place and its focus, so it is `aria-disabled`, not `disabled`. */
|
|
777
|
+
const setUsable = (button: HTMLButtonElement | undefined, usable: boolean) => {
|
|
778
|
+
if (usable) button?.removeAttribute('aria-disabled');
|
|
779
|
+
else button?.setAttribute('aria-disabled', 'true');
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
const buildLegend = () => {
|
|
783
|
+
const line = document.createElement('div');
|
|
784
|
+
line.className = 'explico-page-marks-legend';
|
|
785
|
+
const row = document.createElement('div');
|
|
786
|
+
row.className = 'explico-page-marks-legend__row';
|
|
787
|
+
const items = new Map<PageMark, HTMLElement>();
|
|
788
|
+
for (const mark of KINDS) {
|
|
789
|
+
// A marked link's description: every kind on screen has its text here.
|
|
790
|
+
const item = document.createElement('span');
|
|
791
|
+
item.id = `explico-page-mark-${mark}`;
|
|
792
|
+
item.className = 'explico-page-marks-legend__item';
|
|
793
|
+
item.dataset.pageMark = mark;
|
|
794
|
+
item.textContent = TEXT[mark];
|
|
795
|
+
item.hidden = true;
|
|
796
|
+
items.set(mark, item);
|
|
797
|
+
row.append(item);
|
|
798
|
+
}
|
|
799
|
+
// There from the start, empty, so a screen reader hears what it gains.
|
|
800
|
+
const status = document.createElement('p');
|
|
801
|
+
status.className = 'explico-page-marks-legend__status';
|
|
802
|
+
status.setAttribute('role', 'status');
|
|
803
|
+
const actions = document.createElement('span');
|
|
804
|
+
actions.className = 'explico-page-marks-legend__actions';
|
|
805
|
+
const buttons = new Map<Action, HTMLButtonElement>();
|
|
806
|
+
for (const action of Object.keys(ACTIONS) as Action[]) {
|
|
807
|
+
const button = document.createElement('button');
|
|
808
|
+
button.type = 'button';
|
|
809
|
+
button.className = 'explico-page-marks-legend__action';
|
|
810
|
+
button.dataset.pageMarksAction = action;
|
|
811
|
+
button.textContent = ACTIONS[action].label;
|
|
812
|
+
button.addEventListener('click', () => {
|
|
813
|
+
if (button.getAttribute('aria-disabled') === 'true') return;
|
|
814
|
+
const record = ACTIONS[action].record();
|
|
815
|
+
store.set(payload.key, encodeSeen(record));
|
|
816
|
+
if (decks.length > 0) {
|
|
817
|
+
readingMemoryStore(payload.base).set(deckKey, encodeDeckMemory(ACTIONS[action].decks(readDecks())));
|
|
818
|
+
}
|
|
819
|
+
apply(record);
|
|
820
|
+
// In the line, which stays: focus stays on the button.
|
|
821
|
+
status.textContent = ACTIONS[action].done;
|
|
822
|
+
// A deck's page open here marks its frames again.
|
|
823
|
+
if (decks.length > 0) window.dispatchEvent(new CustomEvent(DECK_MEMORY_EVENT));
|
|
824
|
+
});
|
|
825
|
+
buttons.set(action, button);
|
|
826
|
+
actions.append(button);
|
|
827
|
+
}
|
|
828
|
+
row.append(actions);
|
|
829
|
+
line.append(row, status);
|
|
830
|
+
place(line);
|
|
831
|
+
return { line, items, buttons };
|
|
832
|
+
};
|
|
833
|
+
let legend: ReturnType<typeof buildLegend> | undefined;
|
|
834
|
+
|
|
835
|
+
/** The line for the marks on screen, while `record` is kept; none without it. */
|
|
836
|
+
const showLegend = (
|
|
837
|
+
record: ReadonlyMap<string, string> | null,
|
|
838
|
+
shown: Set<PageMark>,
|
|
839
|
+
) => {
|
|
840
|
+
if (!record) {
|
|
841
|
+
legend?.line.remove();
|
|
842
|
+
legend = undefined;
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
legend ??= buildLegend();
|
|
846
|
+
for (const [mark, item] of legend.items) item.hidden = !shown.has(mark);
|
|
847
|
+
const frames = decks.length > 0 ? deckState() : { marked: false, seen: false };
|
|
848
|
+
setUsable(legend.buttons.get('seen'), shown.size > 0 || frames.marked);
|
|
849
|
+
setUsable(
|
|
850
|
+
legend.buttons.get('unseen'),
|
|
851
|
+
[...record.keys()].some((slug) => slug !== current?.slug) || frames.seen,
|
|
852
|
+
);
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
/** The record the marks on screen show, for a rebuild of the rows. */
|
|
856
|
+
let applied: ReadonlyMap<string, string> | null = null;
|
|
857
|
+
|
|
858
|
+
/** The marks for `record`, or none at all with `null`. */
|
|
859
|
+
const apply = (record: ReadonlyMap<string, string> | null) => {
|
|
860
|
+
applied = record;
|
|
861
|
+
const shown = new Set<PageMark>();
|
|
862
|
+
for (const link of root.querySelectorAll<HTMLAnchorElement>('li > a[href]')) {
|
|
863
|
+
// A row's mark is on its item, for its own link; the other
|
|
864
|
+
// edition's icon holds its own. A link that was that icon
|
|
865
|
+
// before a switch drops the mark it held.
|
|
866
|
+
const other = link.classList.contains(EDITION_OTHER_CLASS);
|
|
867
|
+
const holder = other ? link : link.parentElement!;
|
|
868
|
+
if (!other) delete link.dataset.pageMark;
|
|
869
|
+
// A chapter's section of its part's deck is no page of its own.
|
|
870
|
+
const page = link.hasAttribute(DECK_SECTION_ATTRIBUTE) ? undefined : byPath.get(pathOf(link.href));
|
|
871
|
+
const mark =
|
|
872
|
+
record && page && page !== current
|
|
873
|
+
? markOf(page, record, payload)
|
|
874
|
+
: undefined;
|
|
875
|
+
if (mark) {
|
|
876
|
+
holder.dataset.pageMark = mark;
|
|
877
|
+
link.setAttribute('aria-describedby', `explico-page-mark-${mark}`);
|
|
878
|
+
shown.add(mark);
|
|
879
|
+
} else if (holder.dataset.pageMark || link.hasAttribute('aria-describedby')) {
|
|
880
|
+
delete holder.dataset.pageMark;
|
|
881
|
+
link.removeAttribute('aria-describedby');
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
for (const group of root.querySelectorAll<HTMLElement>('details')) {
|
|
885
|
+
setMarks(
|
|
886
|
+
group,
|
|
887
|
+
new Set(
|
|
888
|
+
[
|
|
889
|
+
...group.querySelectorAll<HTMLElement>(
|
|
890
|
+
'li[data-page-mark], a[data-page-mark]',
|
|
891
|
+
),
|
|
892
|
+
].map((item) => item.dataset.pageMark as PageMark),
|
|
893
|
+
),
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
setMarks(panel, shown);
|
|
897
|
+
showLegend(record, shown);
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
let recorded = false;
|
|
901
|
+
const run = () => {
|
|
902
|
+
if (!rememberReading(payload.base)) {
|
|
903
|
+
apply(null);
|
|
904
|
+
if (changedNote) changedNote.hidden = true;
|
|
905
|
+
recorded = false;
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const before = decodeSeen(store.get(payload.key));
|
|
909
|
+
const seen = withVisit(before, current, payload.pages);
|
|
910
|
+
store.set(payload.key, encodeSeen(seen));
|
|
911
|
+
// Without storage nothing could ever be recorded: mark nothing.
|
|
912
|
+
if (store.get(payload.key) === null) return apply(null);
|
|
913
|
+
// The note says what this visit found, once: a yes given on this
|
|
914
|
+
// page records it and has nothing earlier to compare with.
|
|
915
|
+
if (changedNote && current && !recorded) {
|
|
916
|
+
const previous = before.get(current.slug);
|
|
917
|
+
changedNote.hidden = !(
|
|
918
|
+
payload.changed &&
|
|
919
|
+
previous !== undefined &&
|
|
920
|
+
previous !== current.id
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
recorded = true;
|
|
924
|
+
apply(seen);
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
window.addEventListener(READING_MEMORY_EVENT, run);
|
|
928
|
+
// The rows were rebuilt: the same record, on the links they hold now.
|
|
929
|
+
window.addEventListener(SIDEBAR_EDITIONS_EVENT, () => apply(applied));
|
|
930
|
+
// A deck's page recorded frames or reset its marks: the two actions may change use.
|
|
931
|
+
window.addEventListener(DECK_MEMORY_EVENT, () => apply(applied));
|
|
932
|
+
run();
|
|
152
933
|
})();
|
|
153
934
|
</script>
|
|
154
935
|
<script>
|
|
@@ -222,7 +1003,13 @@ const partLinkPayload = JSON.stringify(partLinks).replace(/</g, '\\u003c');
|
|
|
222
1003
|
* `position: fixed` (see `global.css`) and the anchor's own viewport
|
|
223
1004
|
* coordinates from here. The tooltip stays the link's accessible name in
|
|
224
1005
|
* the markup either way — this only moves where it is painted.
|
|
1006
|
+
*
|
|
1007
|
+
* The other edition's icon at the end of a labelled row
|
|
1008
|
+
* (`sidebar-editions.ts`) has no label beside it either: its name
|
|
1009
|
+
* ("Slides: …") is shown the same way.
|
|
225
1010
|
*/
|
|
1011
|
+
import { EDITION_OTHER_CLASS } from './sidebar-editions';
|
|
1012
|
+
|
|
226
1013
|
(() => {
|
|
227
1014
|
if (!matchMedia('(min-width: 50rem)').matches) return;
|
|
228
1015
|
const panel = document.querySelector<HTMLElement>(
|
|
@@ -230,8 +1017,7 @@ const partLinkPayload = JSON.stringify(partLinks).replace(/</g, '\\u003c');
|
|
|
230
1017
|
);
|
|
231
1018
|
if (!panel) return;
|
|
232
1019
|
|
|
233
|
-
const COMPACT_LINK =
|
|
234
|
-
'details[data-compact] > ul:not(.top-level) > li > a';
|
|
1020
|
+
const COMPACT_LINK = `details[data-compact] > ul:not(.top-level) > li > a, li > a.${EDITION_OTHER_CLASS}`;
|
|
235
1021
|
const GAP = 7;
|
|
236
1022
|
const EDGE = 8;
|
|
237
1023
|
let open: HTMLAnchorElement | null = null;
|