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
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lesson's editions in the sidebar (`sidebarEditions` in the course
|
|
3
|
+
* settings): a lesson with a chapter and a deck is one row. The row opens the
|
|
4
|
+
* **lead** edition under the lesson's title, and the **other** edition is an
|
|
5
|
+
* icon at the row's end, the second tile of a joined pair in a compact part,
|
|
6
|
+
* or not listed. A lesson with one edition is one row in every state; only
|
|
7
|
+
* when that edition is not the lead does the row carry its glyph, so the
|
|
8
|
+
* exception is marked and nothing else is.
|
|
9
|
+
*
|
|
10
|
+
* The course sets the default; the reader's own choice, made with the two
|
|
11
|
+
* controls in the sidebar's foot, is a setting kept in their browser
|
|
12
|
+
* whatever they answered about reading memory (`explico-sidebar-editions`,
|
|
13
|
+
* `session/reading-memory.ts`). It is kept field by field: a field the
|
|
14
|
+
* reader never set keeps the course's default, so a course that changes its
|
|
15
|
+
* default moves the readers who did not choose.
|
|
16
|
+
*
|
|
17
|
+
* Pure: `LayoutSidebar.astro` does the DOM and the storage, and
|
|
18
|
+
* `tests/unit/sidebar-editions.test.ts` checks this in Node.
|
|
19
|
+
*/
|
|
20
|
+
import type {
|
|
21
|
+
NormalizedSidebarEditions,
|
|
22
|
+
SidebarEditionLead,
|
|
23
|
+
SidebarEditionOther,
|
|
24
|
+
} from '../../compiler/course-config';
|
|
25
|
+
|
|
26
|
+
/** A lesson's edition, as `data-edition` names it on its sidebar link: its chapter or its deck. */
|
|
27
|
+
export type Edition = SidebarEditionLead;
|
|
28
|
+
|
|
29
|
+
/** What the sidebar shows: the edition a lesson's row opens, and whether it lists the other. */
|
|
30
|
+
export type EditionsState = NormalizedSidebarEditions;
|
|
31
|
+
|
|
32
|
+
/** The reader's choice: a field they never set is left out. */
|
|
33
|
+
export type EditionsChoice = Partial<EditionsState>;
|
|
34
|
+
|
|
35
|
+
/** Where the reader's choice is kept: one key for every explico course on the site, like `explico-sidebar-groups`. */
|
|
36
|
+
export const SIDEBAR_EDITIONS_KEY = 'explico-sidebar-editions';
|
|
37
|
+
|
|
38
|
+
/** Told on `window` once the rows are rebuilt, so the marks and the progress rail follow them. */
|
|
39
|
+
export const SIDEBAR_EDITIONS_EVENT = 'explico:sidebar-editions';
|
|
40
|
+
|
|
41
|
+
/** The other edition's link: the icon at a row's end, or a pair's second tile. */
|
|
42
|
+
export const EDITION_OTHER_CLASS = 'layout-edition-other';
|
|
43
|
+
|
|
44
|
+
/** A chapter that is a lesson's only edition while slides lead: its row carries a sheet. */
|
|
45
|
+
export const CHAPTER_LESSON_CLASS = 'explico-chapter-lesson';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Marks a chapter's section of its part's deck: the link the script makes
|
|
49
|
+
* a chapter's other edition from the chapter link's `data-deck-href` (the
|
|
50
|
+
* compiler's, `buildSidebar`) when the chapter has no deck of its own. It
|
|
51
|
+
* is no page of its own, so the marks pass it by; the part's deck's own
|
|
52
|
+
* row carries the deck page's.
|
|
53
|
+
*/
|
|
54
|
+
export const DECK_SECTION_ATTRIBUTE = 'data-deck-section';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A chapter's section of its part's deck, under the site's base: the
|
|
58
|
+
* compiler writes it without one (`/motion/slides/#slide-9`), as Starlight's
|
|
59
|
+
* own links are written.
|
|
60
|
+
*/
|
|
61
|
+
export function deckSectionHref(base: string, deckHref: string): string {
|
|
62
|
+
return `${base.replace(/\/+$/u, '')}${deckHref.startsWith('/') ? deckHref : `/${deckHref}`}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The compiler's classes for a deck's link, `DECK_ROW_CLASS` (a chapter's
|
|
67
|
+
* "Slides" row) and `DECK_LESSON_CLASS` (a slides-first lesson), from
|
|
68
|
+
* `compiler/lesson-derivation.ts`: spelt out here so the browser bundle does
|
|
69
|
+
* not load the compiler.
|
|
70
|
+
*/
|
|
71
|
+
const DECK_ROW_CLASS = 'explico-deck-row';
|
|
72
|
+
const DECK_LESSON_CLASS = 'explico-deck-lesson';
|
|
73
|
+
|
|
74
|
+
/** The glyph class a lesson's one edition takes when it is not the lead. */
|
|
75
|
+
export const GLYPH_CLASS: Readonly<Record<Edition, string>> = {
|
|
76
|
+
chapter: CHAPTER_LESSON_CLASS,
|
|
77
|
+
slides: DECK_LESSON_CLASS,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Every class a lesson's link can carry for its place in a row, the compiler's included: a rebuild starts from none. */
|
|
81
|
+
export const ROW_CLASSES: readonly string[] = [EDITION_OTHER_CLASS, CHAPTER_LESSON_CLASS, DECK_LESSON_CLASS, DECK_ROW_CLASS];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The same for a part's link: its own class (`explico-part-overview`,
|
|
85
|
+
* `explico-part-deck`) stays, since it draws the part's glyph in every place.
|
|
86
|
+
*/
|
|
87
|
+
export const PART_ROW_CLASSES: readonly string[] = [EDITION_OTHER_CLASS, DECK_ROW_CLASS];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A link's second line, inside its `<span>` after its name: shown where the
|
|
91
|
+
* span is a tooltip, hidden where it is a row's text (`global.css`). The
|
|
92
|
+
* link's `aria-description` says it to assistive technology.
|
|
93
|
+
*/
|
|
94
|
+
export const EDITION_DETAIL_CLASS = 'layout-edition-detail';
|
|
95
|
+
|
|
96
|
+
const EDITIONS: readonly Edition[] = ['chapter', 'slides'];
|
|
97
|
+
const OTHERS: readonly SidebarEditionOther[] = ['listed', 'hidden'];
|
|
98
|
+
|
|
99
|
+
const isEdition = (value: unknown): value is Edition => EDITIONS.includes(value as Edition);
|
|
100
|
+
const isOther = (value: unknown): value is SidebarEditionOther => OTHERS.includes(value as SidebarEditionOther);
|
|
101
|
+
|
|
102
|
+
/** A stored choice, or the course's payload: each field it holds with a value it may have; anything else is left out. */
|
|
103
|
+
export function decodeEditionsChoice(raw: string | null): EditionsChoice {
|
|
104
|
+
if (!raw) return {};
|
|
105
|
+
let parsed: unknown;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(raw);
|
|
108
|
+
} catch {
|
|
109
|
+
return {};
|
|
110
|
+
}
|
|
111
|
+
if (!parsed || typeof parsed !== 'object') return {};
|
|
112
|
+
const { lead, other } = parsed as { lead?: unknown; other?: unknown };
|
|
113
|
+
return { ...(isEdition(lead) ? { lead } : {}), ...(isOther(other) ? { other } : {}) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The stored text of a choice: `{"lead":"slides"}`, `{"lead":"slides","other":"listed"}`. */
|
|
117
|
+
export function encodeEditionsChoice(choice: EditionsChoice): string {
|
|
118
|
+
return JSON.stringify({
|
|
119
|
+
...(choice.lead === undefined ? {} : { lead: choice.lead }),
|
|
120
|
+
...(choice.other === undefined ? {} : { other: choice.other }),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** What the sidebar shows: the reader's choice, field by field, over the course's default. */
|
|
125
|
+
export function resolveEditions(course: EditionsState, choice: EditionsChoice): EditionsState {
|
|
126
|
+
return { lead: choice.lead ?? course.lead, other: choice.other ?? course.other };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The row a lesson takes. */
|
|
130
|
+
export interface LessonRow {
|
|
131
|
+
/** The edition the row opens, under the lesson's title. */
|
|
132
|
+
readonly lead: Edition;
|
|
133
|
+
/** The edition at the row's end, or the pair's second tile: none while hidden, or with one edition. */
|
|
134
|
+
readonly other?: Edition;
|
|
135
|
+
/** The glyph before the title: the lesson's one edition, when it is not the lead. */
|
|
136
|
+
readonly glyph?: Edition;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The row of a lesson with `editions` (one or both), in `state`. `open` is
|
|
141
|
+
* the edition that is the page open now, if it is one of this lesson's: a
|
|
142
|
+
* hidden edition is listed while it is open, so the open page is never
|
|
143
|
+
* missing from the sidebar.
|
|
144
|
+
*/
|
|
145
|
+
export function lessonRow(editions: readonly Edition[], state: EditionsState, open?: Edition): LessonRow {
|
|
146
|
+
const has = new Set(editions);
|
|
147
|
+
if (has.size < 2) {
|
|
148
|
+
const only = editions[0] ?? state.lead;
|
|
149
|
+
return only === state.lead ? { lead: only } : { lead: only, glyph: only };
|
|
150
|
+
}
|
|
151
|
+
const other: Edition = state.lead === 'chapter' ? 'slides' : 'chapter';
|
|
152
|
+
return state.other === 'listed' || open === other ? { lead: state.lead, other } : { lead: state.lead };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A link's text: the lesson's title on the row's own link, and on the other
|
|
157
|
+
* edition's icon the title it stands for ("Slides: Projectile Range"), which
|
|
158
|
+
* is its accessible name and its tooltip.
|
|
159
|
+
*/
|
|
160
|
+
export function editionLabel(edition: Edition, title: string, role: 'lead' | 'other'): string {
|
|
161
|
+
if (role === 'lead') return title;
|
|
162
|
+
return `${edition === 'slides' ? 'Slides' : 'Chapter'}: ${title}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A part's two links (`buildSidebar`): its Overview and its deck, each
|
|
167
|
+
* naming which it is. The script makes them the part's one row, as a
|
|
168
|
+
* lesson's chapter and deck are one: Overview stands where a chapter stands.
|
|
169
|
+
*/
|
|
170
|
+
export const PART_EDITION_ATTRIBUTE = 'data-part-edition';
|
|
171
|
+
|
|
172
|
+
/** One of a part's two links, as `data-part-edition` names it. */
|
|
173
|
+
export type PartEdition = 'overview' | 'slides';
|
|
174
|
+
|
|
175
|
+
/** The row a part takes: the link it opens, and the link at its end. */
|
|
176
|
+
export interface PartRow {
|
|
177
|
+
readonly lead: PartEdition;
|
|
178
|
+
readonly other?: PartEdition;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The row of a part with a deck, in `state`: with an Overview, a lesson's
|
|
183
|
+
* row with Overview for its chapter; without one, the deck's own row,
|
|
184
|
+
* listed unless chapters lead with the other edition unlisted. `open` is
|
|
185
|
+
* the part's link that is the page open now: it is always listed.
|
|
186
|
+
*/
|
|
187
|
+
export function partRow(hasOverview: boolean, state: EditionsState, open?: PartEdition): PartRow | undefined {
|
|
188
|
+
if (!hasOverview) {
|
|
189
|
+
return state.lead === 'chapter' && state.other === 'hidden' && open !== 'slides' ? undefined : { lead: 'slides' };
|
|
190
|
+
}
|
|
191
|
+
const asPart = (edition: Edition): PartEdition => (edition === 'chapter' ? 'overview' : 'slides');
|
|
192
|
+
const row = lessonRow(['chapter', 'slides'], state, open === undefined ? undefined : open === 'overview' ? 'chapter' : 'slides');
|
|
193
|
+
return row.other === undefined ? { lead: asPart(row.lead) } : { lead: asPart(row.lead), other: asPart(row.other) };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* A part's link's text: Overview's own label (`overviewLabel`, "Overview" by
|
|
198
|
+
* default) or "Slides for this part" leading the row, and the part's name
|
|
199
|
+
* after either at its end, which is the icon's name and its tooltip.
|
|
200
|
+
*/
|
|
201
|
+
export function partEditionLabel(edition: PartEdition, part: string, role: 'lead' | 'other', overviewLabel = 'Overview'): string {
|
|
202
|
+
if (edition === 'overview') return role === 'lead' ? overviewLabel : `Overview: ${part}`;
|
|
203
|
+
return role === 'lead' ? 'Slides for this part' : `Slides for this part: ${part}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The part's deck's second line: the chapters it covers (`1–2`, `1, 3`, as
|
|
208
|
+
* `data-chapters` writes them) and its slides. `Chapters 1–2 · 26 slides`,
|
|
209
|
+
* `Chapter 1 · 9 slides`.
|
|
210
|
+
*/
|
|
211
|
+
export function partDeckDetail(chapters: string, frames: number): string {
|
|
212
|
+
const slides = `${frames} ${frames === 1 ? 'slide' : 'slides'}`;
|
|
213
|
+
if (chapters.trim() === '') return slides;
|
|
214
|
+
return `${/[–,]/u.test(chapters) ? 'Chapters' : 'Chapter'} ${chapters} · ${slides}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* A chapter's section's second line: its frames in the part's deck, as
|
|
219
|
+
* `data-deck-range` writes them. `Slides 17–26 of the part's slides`,
|
|
220
|
+
* `Slide 5 of the part's slides`.
|
|
221
|
+
*/
|
|
222
|
+
export function sectionDetail(range: string): string {
|
|
223
|
+
return `${range.includes('–') ? 'Slides' : 'Slide'} ${range} of the part's slides`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Told on `window` by a deck's page when the chapter on screen changes
|
|
228
|
+
* (`deck-controller.ts`): `{ deck, chapter }`, the deck's slug and the
|
|
229
|
+
* chapter's lesson slug, empty on a frame of no chapter. The sidebar lights
|
|
230
|
+
* that chapter's section link and moves the rail to it.
|
|
231
|
+
*/
|
|
232
|
+
export const DECK_FRAME_EVENT = 'explico:deck-frame';
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The chapter on screen, on `<html>`, as {@link DECK_FRAME_EVENT} tells it:
|
|
236
|
+
* whichever of the deck's script and the sidebar's runs first, the sidebar
|
|
237
|
+
* reads the state from here.
|
|
238
|
+
*/
|
|
239
|
+
export const DECK_CHAPTER_ATTRIBUTE = 'data-deck-chapter';
|
|
240
|
+
|
|
241
|
+
/** What {@link DECK_FRAME_EVENT} tells. */
|
|
242
|
+
export interface DeckFrameDetail {
|
|
243
|
+
readonly deck: string;
|
|
244
|
+
readonly chapter: string;
|
|
245
|
+
}
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* a table keeps that caption line and its header row (decision Q6).
|
|
22
22
|
*/
|
|
23
23
|
import { courseDisclosure } from '../disclosure/course-disclosure';
|
|
24
|
-
import {
|
|
24
|
+
import { deckFloat } from '../deck/deck-context';
|
|
25
|
+
import { captionTitle } from '../../reference/float-name';
|
|
25
26
|
|
|
26
27
|
export interface Props {
|
|
27
28
|
/** Stable key, e.g. `id="kinematics-summary"`. Omit for a positional-only table. */
|
|
@@ -37,6 +38,9 @@ export interface Props {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
const { id, caption, short, number, anchorId } = Astro.props;
|
|
41
|
+
// On a deck, the deck's own table carries no number and a recalled one's
|
|
42
|
+
// number links to its chapter (rule D8).
|
|
43
|
+
const { onDeck, numberHref } = await deckFloat(Astro.locals, anchorId, import.meta.env.BASE_URL);
|
|
40
44
|
// A table longer than this starts truncated (the course's
|
|
41
45
|
// `disclosure.tables.truncateAfter`, default 6).
|
|
42
46
|
const { truncateAfter } = (await courseDisclosure()).tables;
|
|
@@ -47,7 +51,7 @@ if (typeof caption !== 'string' || !caption.trim()) {
|
|
|
47
51
|
if (short !== undefined && (typeof short !== 'string' || !short.trim())) {
|
|
48
52
|
throw new Error('Table `short` must be a non-empty string when given.');
|
|
49
53
|
}
|
|
50
|
-
if (!number || !anchorId) {
|
|
54
|
+
if ((!number && !onDeck) || !anchorId) {
|
|
51
55
|
throw new Error(
|
|
52
56
|
`Table is missing its computed \`number\`/\`anchorId\`. remark-notation numbers a <Table> only on a lesson page, at its top level or in a <Memo>, <ShortNote> or <Fold>: elsewhere (a worked example) write a plain Markdown table, and on a page that is not a chapter (rule S10) link the chapter's table. At a lesson's top level, this page is not running remark-notation with the manifest's \`tables\` (see astro.config.mjs).`,
|
|
53
57
|
);
|
|
@@ -63,11 +67,15 @@ if (!number || !anchorId) {
|
|
|
63
67
|
data-truncate-after={truncateAfter}
|
|
64
68
|
>
|
|
65
69
|
<figcaption class="keyed-table__caption float-caption">
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
{
|
|
71
|
+
number && (
|
|
72
|
+
<a
|
|
73
|
+
class="keyed-table__number float-caption__number"
|
|
74
|
+
href={numberHref ?? `#${anchorId}`}
|
|
75
|
+
aria-label={`Table ${number}`}>Table {number}</a
|
|
76
|
+
>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
71
79
|
{short && <b class="float-caption__title">{captionTitle(short)}</b>}{short && ' '}<span
|
|
72
80
|
class="keyed-table__caption-text float-caption__text">{caption}</span
|
|
73
81
|
>
|
package/src/content-config.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
1
4
|
import { defineCollection } from 'astro:content';
|
|
2
5
|
import { glob } from 'astro/loaders';
|
|
3
|
-
import {
|
|
6
|
+
import type { Loader } from 'astro/loaders';
|
|
7
|
+
import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
|
|
8
|
+
import { slug as githubSlug } from 'github-slugger';
|
|
4
9
|
import {
|
|
5
10
|
abbreviationSchema,
|
|
6
11
|
assessmentSchema,
|
|
@@ -10,6 +15,7 @@ import {
|
|
|
10
15
|
sourceSchema,
|
|
11
16
|
trackSchema,
|
|
12
17
|
} from './compiler/content-schemas';
|
|
18
|
+
import { isPartDeckSlug, partDeckDir } from './compiler/lesson-derivation';
|
|
13
19
|
|
|
14
20
|
// The schemas live in `compiler/content-schemas.ts`, where the manifest walk
|
|
15
21
|
// checks every record against them too (`validate:content` rejects what this
|
|
@@ -19,9 +25,46 @@ import {
|
|
|
19
25
|
// `docsLoader()` is hard-wired to `<srcDir>/content/docs`, so the `docs`
|
|
20
26
|
// collection uses a plain glob with the same extensions and `_`-prefix ignore.
|
|
21
27
|
const CONTENT_DIR = './content';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Astro's own id for a docs file (`generateIdDefault` in `astro/loaders`):
|
|
31
|
+
* the frontmatter `slug` if any, else each path segment slugged, the
|
|
32
|
+
* extension and a trailing `/index` dropped.
|
|
33
|
+
*/
|
|
34
|
+
function defaultDocsId(entry: string, data: Record<string, unknown>): string {
|
|
35
|
+
if (data.slug) return String(data.slug);
|
|
36
|
+
return entry
|
|
37
|
+
.replace(/\.(md|mdx)$/, '')
|
|
38
|
+
.split('/')
|
|
39
|
+
.map((segment) => githubSlug(segment))
|
|
40
|
+
.join('/')
|
|
41
|
+
.replace(/\/index$/, '');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A lesson's deck, `<lesson>.deck.mdx`, is served at `<lesson>/slides/`
|
|
46
|
+
* beside its chapter, and at the lesson's own URL when the lesson has no
|
|
47
|
+
* chapter (`deckRouteSlug` in `compiler/lesson-derivation.ts`, which the
|
|
48
|
+
* manifest uses for the same route). A part's deck, `<part>/index.deck.mdx`,
|
|
49
|
+
* is served at `<part>/slides/` (`partDeckRouteSlug`). Every other page
|
|
50
|
+
* keeps Astro's id.
|
|
51
|
+
*/
|
|
52
|
+
function docsId({ entry, base, data }: { entry: string; base: URL; data: Record<string, unknown> }): string {
|
|
53
|
+
const deck = /^(.*)\.deck\.(md|mdx)$/.exec(entry);
|
|
54
|
+
if (!deck || data.slug) return defaultDocsId(entry, data);
|
|
55
|
+
if (isPartDeckSlug(entry)) return `${defaultDocsId(`${partDeckDir(entry)}.mdx`, {})}/slides`;
|
|
56
|
+
const lesson = deck[1] ?? '';
|
|
57
|
+
const hasChapter = ['.mdx', '.md'].some((extension) =>
|
|
58
|
+
existsSync(fileURLToPath(new URL(`./${encodeURI(lesson)}${extension}`, base))),
|
|
59
|
+
);
|
|
60
|
+
const lessonId = defaultDocsId(`${lesson}.mdx`, {});
|
|
61
|
+
return hasChapter ? `${lessonId}/slides` : lessonId;
|
|
62
|
+
}
|
|
63
|
+
|
|
22
64
|
const docsGlob = glob({
|
|
23
65
|
base: `${CONTENT_DIR}/docs`,
|
|
24
66
|
pattern: '**/[^_]*.{md,mdx}',
|
|
67
|
+
generateId: docsId,
|
|
25
68
|
});
|
|
26
69
|
|
|
27
70
|
const docs = defineCollection({
|
|
@@ -29,6 +72,48 @@ const docs = defineCollection({
|
|
|
29
72
|
schema: docsSchema({ extend: lessonFrontmatterSchema }),
|
|
30
73
|
});
|
|
31
74
|
|
|
75
|
+
const i18nGlob = glob({
|
|
76
|
+
base: `${CONTENT_DIR}/i18n`,
|
|
77
|
+
pattern: '**/[^_]*.{json,yml,yaml}',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Starlight reads its optional `i18n` collection even in a single-language
|
|
82
|
+
* course. Astro logs a warning when that collection is absent or empty, so
|
|
83
|
+
* supply an empty English entry until a course authors translations of its
|
|
84
|
+
* own. Avoid invoking `glob()` for a missing/empty directory: the loader would
|
|
85
|
+
* emit a second warning before Starlight reads the collection.
|
|
86
|
+
*/
|
|
87
|
+
const i18nLoader: Loader = {
|
|
88
|
+
name: 'explico-i18n-loader',
|
|
89
|
+
async load(context) {
|
|
90
|
+
const directory = new URL('content/i18n/', context.config.root);
|
|
91
|
+
let hasTranslations = false;
|
|
92
|
+
try {
|
|
93
|
+
const entries = await readdir(directory, { recursive: true });
|
|
94
|
+
hasTranslations = entries.some((entry) =>
|
|
95
|
+
/(^|[/\\])[^_/\\][^/\\]*\.(json|ya?ml)$/.test(entry),
|
|
96
|
+
);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (hasTranslations) {
|
|
102
|
+
await i18nGlob.load(context);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
context.store.clear();
|
|
107
|
+
const data = await context.parseData({ id: 'en', data: {} });
|
|
108
|
+
context.store.set({ id: 'en', data, digest: context.generateDigest(data) });
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const i18n = defineCollection({
|
|
113
|
+
loader: i18nLoader,
|
|
114
|
+
schema: i18nSchema(),
|
|
115
|
+
});
|
|
116
|
+
|
|
32
117
|
const competencies = defineCollection({
|
|
33
118
|
loader: glob({
|
|
34
119
|
pattern: '**/*.json',
|
|
@@ -67,6 +152,7 @@ const abbreviations = defineCollection({
|
|
|
67
152
|
|
|
68
153
|
export const collections = {
|
|
69
154
|
docs,
|
|
155
|
+
i18n,
|
|
70
156
|
competencies,
|
|
71
157
|
assessments,
|
|
72
158
|
tracks,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The competency dependency graph, cut into one piece per part
|
|
3
|
+
* (`<CurriculumMap graph="per-part">`).
|
|
4
|
+
*
|
|
5
|
+
* The whole-course graph is one diagram, and a course with many competencies
|
|
6
|
+
* lays it out far wider than the lesson column (rule V8) — narrower boxes do
|
|
7
|
+
* not help, since the widest rank holds a dozen competencies. One diagram per
|
|
8
|
+
* part keeps each to the competencies a reader meets together.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface GraphNode {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly text: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface GraphEdge {
|
|
17
|
+
readonly from: string;
|
|
18
|
+
readonly to: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PartGraph {
|
|
22
|
+
/** The diagram's id, unique on the page: `competency-graph-<part dir>`. */
|
|
23
|
+
readonly id: string;
|
|
24
|
+
/** The part's overview title, its label when it has none; the untaught group's name. */
|
|
25
|
+
readonly title: string;
|
|
26
|
+
/** The competencies this group teaches, then one box per earlier competency they build on. */
|
|
27
|
+
readonly nodes: readonly GraphNode[];
|
|
28
|
+
/** An arrow from each prerequisite to a competency the group teaches. */
|
|
29
|
+
readonly edges: readonly GraphEdge[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PartInput {
|
|
33
|
+
readonly dir: string;
|
|
34
|
+
readonly label: string;
|
|
35
|
+
/** Lesson ids in chapter order. */
|
|
36
|
+
readonly lessons: readonly string[];
|
|
37
|
+
readonly overview?: { readonly title: string };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface CompetencyInput {
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly title: string;
|
|
43
|
+
readonly prerequisites: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const UNTAUGHT_GRAPH_ID = 'competency-graph-untaught';
|
|
47
|
+
export const UNTAUGHT_TITLE = 'Competencies no lesson teaches';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One graph per part, in `parts` order, then a last one for competencies no
|
|
51
|
+
* lesson teaches, so every competency is drawn once as a taught box. A
|
|
52
|
+
* competency belongs to the first part whose lessons teach it. A prerequisite
|
|
53
|
+
* taught in another part is drawn in this one as a box labelled
|
|
54
|
+
* `<part label>: <title>`, so the arrow into the competency still shows.
|
|
55
|
+
* A part that teaches no competency has no graph.
|
|
56
|
+
*/
|
|
57
|
+
export function competencyGraphsByPart(
|
|
58
|
+
parts: readonly PartInput[],
|
|
59
|
+
teachesByLesson: ReadonlyMap<string, readonly string[]>,
|
|
60
|
+
competencies: readonly CompetencyInput[],
|
|
61
|
+
): PartGraph[] {
|
|
62
|
+
const partOf = new Map<string, PartInput>();
|
|
63
|
+
for (const part of parts) {
|
|
64
|
+
for (const lessonId of part.lessons) {
|
|
65
|
+
for (const id of teachesByLesson.get(lessonId) ?? []) {
|
|
66
|
+
if (!partOf.has(id)) partOf.set(id, part);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const titleOf = new Map(competencies.map((competency) => [competency.id, competency.title]));
|
|
71
|
+
|
|
72
|
+
const graphs: PartGraph[] = [];
|
|
73
|
+
for (const part of [...parts, undefined]) {
|
|
74
|
+
const taught = competencies.filter((competency) => partOf.get(competency.id) === part);
|
|
75
|
+
if (taught.length === 0) continue;
|
|
76
|
+
const earlier = [...new Set(taught.flatMap((competency) => competency.prerequisites))].filter(
|
|
77
|
+
(id) => partOf.get(id) !== part,
|
|
78
|
+
);
|
|
79
|
+
graphs.push({
|
|
80
|
+
id: part ? `competency-graph-${part.dir.replace(/[^A-Za-z0-9_-]/g, '-')}` : UNTAUGHT_GRAPH_ID,
|
|
81
|
+
title: part ? (part.overview?.title ?? part.label) : UNTAUGHT_TITLE,
|
|
82
|
+
nodes: [
|
|
83
|
+
...taught.map((competency) => ({ id: competency.id, text: competency.title })),
|
|
84
|
+
...earlier.map((id) => ({
|
|
85
|
+
id,
|
|
86
|
+
text: `${partOf.get(id)?.label ?? 'Not taught'}: ${titleOf.get(id) ?? id}`,
|
|
87
|
+
})),
|
|
88
|
+
],
|
|
89
|
+
edges: taught.flatMap((competency) =>
|
|
90
|
+
competency.prerequisites.map((from) => ({ from, to: competency.id })),
|
|
91
|
+
),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return graphs;
|
|
95
|
+
}
|
|
@@ -25,3 +25,22 @@ export const BASE_LIBRARY = Object.freeze(
|
|
|
25
25
|
export function isBaseLibraryAtom(token) {
|
|
26
26
|
return BASE_LIBRARY.has(token);
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
// An atom's LaTeX is its own glyph unless it is a command: `\pi` draws `π`.
|
|
30
|
+
// `tests/notation/figure-symbols.test.ts` renders every atom and holds this
|
|
31
|
+
// table to what KaTeX draws, so a new command atom cannot be forgotten here.
|
|
32
|
+
const DRAWN_AS = Object.freeze({ '\\pi': 'π' });
|
|
33
|
+
|
|
34
|
+
/** What KaTeX draws for each base-library atom: `d`, `e`, `i`, `π`. */
|
|
35
|
+
export const BASE_LIBRARY_GLYPHS = Object.freeze(
|
|
36
|
+
new Set([...BASE_LIBRARY].map((atom) => DRAWN_AS[atom] ?? atom)),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* True when `glyph`, as a rendered page shows it, is a base-library atom. The
|
|
41
|
+
* rendered-page check (`figure-symbols.ts`) reads glyphs, not LaTeX, and
|
|
42
|
+
* exempts the same library the math gates do.
|
|
43
|
+
*/
|
|
44
|
+
export function isBaseLibraryGlyph(glyph) {
|
|
45
|
+
return BASE_LIBRARY_GLYPHS.has(glyph);
|
|
46
|
+
}
|