explico 0.3.0 → 0.4.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 +72 -1
- package/README.md +21 -2
- package/dist/compiler/astro.d.ts +30 -0
- package/dist/compiler/astro.js +79 -0
- package/dist/compiler/cli.js +5 -9
- package/dist/compiler/course-config.d.ts +29 -4
- package/dist/compiler/course-config.js +12 -0
- package/dist/compiler/lesson-derivation.d.ts +1 -1
- package/dist/compiler/lesson-derivation.js +2 -2
- package/dist/compiler/manifest.d.ts +25 -4
- package/dist/compiler/manifest.js +59 -27
- package/dist/reference/equations-validate.d.ts +8 -3
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +58 -22
- package/dist/reference/equations.mjs +75 -26
- package/dist/reference/keyed-float-validate.d.ts +9 -3
- package/dist/reference/keyed-float-validate.js +1 -1
- package/dist/reference/keyed-jsx-floats.d.mts +18 -10
- package/dist/reference/keyed-jsx-floats.mjs +42 -18
- package/dist/reference/remark-notation.d.mts +54 -5
- package/dist/reference/remark-notation.mjs +161 -17
- package/docs/authoring/README.md +4 -0
- package/docs/authoring/migrating.md +62 -0
- package/docs/authoring/notation.md +8 -7
- package/docs/authoring/structure.md +139 -0
- package/package.json +1 -1
- package/src/compiler/astro.ts +127 -0
- package/src/compiler/cli.ts +5 -9
- package/src/compiler/course-config.ts +39 -3
- package/src/compiler/lesson-derivation.ts +2 -2
- package/src/compiler/manifest.ts +108 -36
- package/src/components/notation/NotationLayer.astro +11 -0
- package/src/components/starlight/LayoutSidebar.astro +62 -1
- package/src/components/starlight/LessonFooter.astro +11 -0
- package/src/components/table/Table.astro +3 -2
- package/src/reference/equations-validate.ts +6 -4
- package/src/reference/equations.mjs +75 -26
- package/src/reference/keyed-float-validate.ts +7 -4
- package/src/reference/keyed-jsx-floats.mjs +42 -18
- package/src/reference/remark-notation.mjs +161 -17
- package/src/styles/global.css +129 -0
|
@@ -7,13 +7,25 @@
|
|
|
7
7
|
* supplies its own value of this shape.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* One **part** — a sidebar group, holding the chapters (lessons) whose slugs
|
|
12
|
+
* start with `dir`. A part's 1-based position in `CourseConfig.parts` is the
|
|
13
|
+
* number before the dot in every equation, table, and figure number inside it.
|
|
14
|
+
* See `docs/authoring/structure.md` for the full level taxonomy.
|
|
15
|
+
*/
|
|
16
|
+
export interface CoursePart {
|
|
11
17
|
/** Lesson-slug prefix, e.g. `bonds` for `bonds/yield-to-maturity`. */
|
|
12
18
|
readonly dir: string;
|
|
13
19
|
/** Sidebar group heading. */
|
|
14
20
|
readonly label: string;
|
|
15
21
|
}
|
|
16
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated Renamed to {@link CoursePart}. "Section" now only ever means an
|
|
25
|
+
* `##` heading inside a lesson. Removed in the next MINOR release.
|
|
26
|
+
*/
|
|
27
|
+
export type CourseSection = CoursePart;
|
|
28
|
+
|
|
17
29
|
export interface CourseConventions {
|
|
18
30
|
/** Notation key a lesson's stated sign convention must resolve to. */
|
|
19
31
|
readonly signConventionKey: string;
|
|
@@ -74,8 +86,16 @@ export interface CourseAbout {
|
|
|
74
86
|
export interface CourseConfig {
|
|
75
87
|
readonly title: string;
|
|
76
88
|
readonly description: string;
|
|
77
|
-
/**
|
|
78
|
-
|
|
89
|
+
/**
|
|
90
|
+
* The course's parts (sidebar groups), in order. Exactly one of `parts` or
|
|
91
|
+
* the deprecated `sections` must be given.
|
|
92
|
+
*/
|
|
93
|
+
readonly parts?: readonly CoursePart[];
|
|
94
|
+
/**
|
|
95
|
+
* @deprecated Renamed to {@link CourseConfig.parts}. Still honoured, so a
|
|
96
|
+
* course on the old name keeps building; removed in the next MINOR release.
|
|
97
|
+
*/
|
|
98
|
+
readonly sections?: readonly CoursePart[];
|
|
79
99
|
/** Namespaces used by competency / notation `domain` fields. */
|
|
80
100
|
readonly domains: readonly string[];
|
|
81
101
|
readonly conventions: CourseConventions;
|
|
@@ -84,3 +104,19 @@ export interface CourseConfig {
|
|
|
84
104
|
/** Edits to the shipped named-operator baseline (rule N8). Optional. */
|
|
85
105
|
readonly operators?: CourseOperators;
|
|
86
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The course's parts, from whichever field the config uses. Throws rather than
|
|
110
|
+
* silently producing a course with no sidebar groups — a config with neither
|
|
111
|
+
* field would otherwise number every float by the `##`-section fallback.
|
|
112
|
+
*/
|
|
113
|
+
export function coursePartsOf(config: CourseConfig): readonly CoursePart[] {
|
|
114
|
+
const parts = config.parts ?? config.sections;
|
|
115
|
+
if (!parts || parts.length === 0) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
'CourseConfig needs at least one entry in `parts` (a sidebar group: ' +
|
|
118
|
+
'{ dir, label }). The old name `sections` is still accepted.',
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return parts;
|
|
122
|
+
}
|
|
@@ -156,7 +156,7 @@ export type SidebarGroup =
|
|
|
156
156
|
export function buildSidebar(
|
|
157
157
|
tracks: readonly TrackLike[],
|
|
158
158
|
lessonSlugs: readonly string[],
|
|
159
|
-
|
|
159
|
+
parts: readonly SidebarSection[],
|
|
160
160
|
): SidebarGroup[] {
|
|
161
161
|
const rank = new Map(
|
|
162
162
|
mergeTrackOrder(tracks).map((lessonId, index) => [lessonId, index]),
|
|
@@ -164,7 +164,7 @@ export function buildSidebar(
|
|
|
164
164
|
const positionOf = (slug: string) =>
|
|
165
165
|
rank.get(lessonIdFromSlug(slug)) ?? Number.POSITIVE_INFINITY;
|
|
166
166
|
|
|
167
|
-
const groups: SidebarGroup[] =
|
|
167
|
+
const groups: SidebarGroup[] = parts.map(({ dir, label }) => ({
|
|
168
168
|
label,
|
|
169
169
|
items: lessonSlugs
|
|
170
170
|
.filter((slug) => slug.startsWith(`${dir}/`))
|
package/src/compiler/manifest.ts
CHANGED
|
@@ -47,6 +47,8 @@ import {
|
|
|
47
47
|
type ConsistencyDiagnostic,
|
|
48
48
|
} from '../reference/consistency';
|
|
49
49
|
import { validateNotationAlignment } from '../reference/curriculum-alignment';
|
|
50
|
+
import { countDisplayEquations } from '../reference/equations.mjs';
|
|
51
|
+
import { countKeyedJsxFloats } from '../reference/keyed-jsx-floats.mjs';
|
|
50
52
|
import {
|
|
51
53
|
validateEquations,
|
|
52
54
|
type EquationDiagnostic,
|
|
@@ -80,7 +82,7 @@ import type {
|
|
|
80
82
|
NotationLessonInput,
|
|
81
83
|
NotationPageBundle,
|
|
82
84
|
} from '../reference/types';
|
|
83
|
-
import type
|
|
85
|
+
import { coursePartsOf, type CourseConfig } from './course-config';
|
|
84
86
|
import {
|
|
85
87
|
resolveKnowledgeVersion,
|
|
86
88
|
type KnowledgeVersionDiagnostic,
|
|
@@ -116,8 +118,12 @@ import {
|
|
|
116
118
|
* against `content/CHANGELOG.md`) and `diagnostics.knowledge`.
|
|
117
119
|
* v6 (chapter numbering): `lessons[].chapter` — the lesson's 1-based course
|
|
118
120
|
* ordinal, the chapter in every equation/table/figure number (`(2.1)`).
|
|
121
|
+
* v7 (part numbering): `lessons[].part` (the sidebar group's 1-based ordinal,
|
|
122
|
+
* now the number before the dot), `lessons[].chapter` redefined as the
|
|
123
|
+
* lesson's ordinal *within its part*, and `lessons[].numbering` — the
|
|
124
|
+
* per-kind offsets that make `n` run continuously across a part.
|
|
119
125
|
*/
|
|
120
|
-
export const MANIFEST_SCHEMA_VERSION =
|
|
126
|
+
export const MANIFEST_SCHEMA_VERSION = 7;
|
|
121
127
|
|
|
122
128
|
const REPOSITORY_ROOT = process.cwd();
|
|
123
129
|
// Content is the top-level `content/` folder; the build output is `build/`
|
|
@@ -206,11 +212,28 @@ export interface ManifestLesson {
|
|
|
206
212
|
/** Docs-loader slug, e.g. `bonds/price-yield-relationship`. */
|
|
207
213
|
readonly slug: string;
|
|
208
214
|
/**
|
|
209
|
-
* The
|
|
210
|
-
*
|
|
211
|
-
* figure number on the page
|
|
215
|
+
* The 1-based ordinal of the **part** this lesson sits in — its sidebar
|
|
216
|
+
* group's position in `courseConfig.parts`. It is the number before the dot
|
|
217
|
+
* in every equation, table, and figure number on the page: `(2.1)` is the
|
|
218
|
+
* first equation of part 2. See `docs/authoring/structure.md`.
|
|
219
|
+
*/
|
|
220
|
+
readonly part: number;
|
|
221
|
+
/**
|
|
222
|
+
* The lesson's 1-based ordinal **within its part** (merged track order, then
|
|
223
|
+
* any unranked lessons by slug) — its chapter number. This orders the
|
|
224
|
+
* sidebar and the reading path; it does not appear in a float's number.
|
|
212
225
|
*/
|
|
213
226
|
readonly chapter: number;
|
|
227
|
+
/**
|
|
228
|
+
* How many floats of each kind the earlier lessons of this part already
|
|
229
|
+
* used. The lesson's own floats continue from here, so a part's sequence
|
|
230
|
+
* runs unbroken across its chapters instead of restarting on every page.
|
|
231
|
+
*/
|
|
232
|
+
readonly numbering: {
|
|
233
|
+
readonly equations: number;
|
|
234
|
+
readonly tables: number;
|
|
235
|
+
readonly figures: number;
|
|
236
|
+
};
|
|
214
237
|
readonly file: string;
|
|
215
238
|
readonly title: string;
|
|
216
239
|
readonly description: string;
|
|
@@ -586,45 +609,84 @@ export async function compileManifest(
|
|
|
586
609
|
const sidebar = buildSidebar(
|
|
587
610
|
catalog.tracks.map((track) => ({ id: track.id, lessons: track.lessons })),
|
|
588
611
|
lessonSlugs,
|
|
589
|
-
courseConfig
|
|
612
|
+
coursePartsOf(courseConfig),
|
|
590
613
|
);
|
|
591
614
|
|
|
592
|
-
//
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
|
|
598
|
-
|
|
615
|
+
// Part and chapter, read straight off the sidebar so there is one
|
|
616
|
+
// lesson-ordering source and not two: a part is a sidebar group (parts in
|
|
617
|
+
// config order), a chapter is a lesson's position inside its part (merged
|
|
618
|
+
// track order, then slug). The part is what prefixes every equation / table
|
|
619
|
+
// / figure number — `(2.1)` is the first equation of part 2 — while `n`
|
|
620
|
+
// continues across the part's chapters rather than restarting on each page,
|
|
621
|
+
// so every lesson also needs the running count that precedes it.
|
|
622
|
+
// See `docs/authoring/structure.md`.
|
|
623
|
+
const bodyByLessonId = new Map(
|
|
624
|
+
notationInput.lessons.map((lesson) => [lesson.lessonId, lesson.body]),
|
|
625
|
+
);
|
|
626
|
+
const placementByLessonId = new Map<
|
|
627
|
+
string,
|
|
628
|
+
{
|
|
629
|
+
part: number;
|
|
630
|
+
chapter: number;
|
|
631
|
+
numbering: { equations: number; tables: number; figures: number };
|
|
632
|
+
}
|
|
633
|
+
>();
|
|
634
|
+
let partCursor = 0;
|
|
599
635
|
for (const group of sidebar) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
636
|
+
const lessonIds = group.items
|
|
637
|
+
.filter((item): item is string => typeof item === 'string')
|
|
638
|
+
.map(lessonIdFromSlug);
|
|
639
|
+
if (lessonIds.length === 0) continue; // the static Reference group
|
|
640
|
+
partCursor += 1;
|
|
641
|
+
// Running totals for this part, advanced one chapter at a time.
|
|
642
|
+
const used = { equations: 0, tables: 0, figures: 0 };
|
|
643
|
+
let chapterCursor = 0;
|
|
644
|
+
for (const lessonId of lessonIds) {
|
|
645
|
+
const body = bodyByLessonId.get(lessonId) ?? '';
|
|
646
|
+
placementByLessonId.set(lessonId, {
|
|
647
|
+
part: partCursor,
|
|
648
|
+
chapter: ++chapterCursor,
|
|
649
|
+
numbering: { ...used },
|
|
650
|
+
});
|
|
651
|
+
used.equations += countDisplayEquations(body);
|
|
652
|
+
used.tables += countKeyedJsxFloats(body, { tagName: 'Table' });
|
|
653
|
+
used.figures += countKeyedJsxFloats(body, { tagName: 'Figure' });
|
|
603
654
|
}
|
|
604
655
|
}
|
|
605
656
|
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
657
|
+
const floatInputs = (kind: 'equations' | 'tables' | 'figures') =>
|
|
658
|
+
notationInput.lessons.map((lesson) => {
|
|
659
|
+
const placement = placementByLessonId.get(lesson.lessonId);
|
|
660
|
+
return {
|
|
661
|
+
lessonId: lesson.lessonId,
|
|
662
|
+
slug: lesson.lessonId.replace(/\./g, '/'),
|
|
663
|
+
file: lesson.source.file,
|
|
664
|
+
body: lesson.body,
|
|
665
|
+
...(placement
|
|
666
|
+
? {
|
|
667
|
+
numbering: {
|
|
668
|
+
prefix: placement.part,
|
|
669
|
+
offset: placement.numbering[kind],
|
|
670
|
+
},
|
|
671
|
+
}
|
|
672
|
+
: {}),
|
|
673
|
+
};
|
|
674
|
+
});
|
|
675
|
+
const equationValidation = validateEquations(floatInputs('equations'));
|
|
614
676
|
const equationsBySlug = new Map(
|
|
615
677
|
Object.entries(equationValidation.numbersBySlug).map(([slug, table]) => [
|
|
616
678
|
slug,
|
|
617
679
|
Object.entries(table).map(([key, number]) => ({ key, number })),
|
|
618
680
|
]),
|
|
619
681
|
);
|
|
620
|
-
const tableValidation = validateTables(
|
|
682
|
+
const tableValidation = validateTables(floatInputs('tables'));
|
|
621
683
|
const tablesBySlug = new Map(
|
|
622
684
|
Object.entries(tableValidation.numbersBySlug).map(([slug, table]) => [
|
|
623
685
|
slug,
|
|
624
686
|
Object.entries(table).map(([key, number]) => ({ key, number })),
|
|
625
687
|
]),
|
|
626
688
|
);
|
|
627
|
-
const figureValidation = validateFigures(
|
|
689
|
+
const figureValidation = validateFigures(floatInputs('figures'));
|
|
628
690
|
const figuresBySlug = new Map(
|
|
629
691
|
Object.entries(figureValidation.numbersBySlug).map(([slug, table]) => [
|
|
630
692
|
slug,
|
|
@@ -645,10 +707,16 @@ export async function compileManifest(
|
|
|
645
707
|
lessons.push({
|
|
646
708
|
id,
|
|
647
709
|
slug: id.replace(/\./g, '/'),
|
|
648
|
-
// 0 = the lesson is not placed in any configured
|
|
710
|
+
// 0 = the lesson is not placed in any configured part; a curriculum
|
|
649
711
|
// diagnostic covers that separately, and both numbering paths then fall
|
|
650
712
|
// back to the legacy `##`-section scheme.
|
|
651
|
-
|
|
713
|
+
part: placementByLessonId.get(id)?.part ?? 0,
|
|
714
|
+
chapter: placementByLessonId.get(id)?.chapter ?? 0,
|
|
715
|
+
numbering: placementByLessonId.get(id)?.numbering ?? {
|
|
716
|
+
equations: 0,
|
|
717
|
+
tables: 0,
|
|
718
|
+
figures: 0,
|
|
719
|
+
},
|
|
652
720
|
file: notationLesson.source.file,
|
|
653
721
|
title: frontmatter.title,
|
|
654
722
|
description: frontmatter.description,
|
|
@@ -842,13 +910,17 @@ export function readManifestSync(): Manifest {
|
|
|
842
910
|
export async function loadManifest(
|
|
843
911
|
courseConfig?: CourseConfig,
|
|
844
912
|
): Promise<Manifest> {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
913
|
+
// A caller that has the course config is an authoring/build entry point. It
|
|
914
|
+
// must compile the current content tree even when a persisted manifest is
|
|
915
|
+
// present: using that file here can split the page in two, with remark
|
|
916
|
+
// seeing current frontmatter while Astro components render stale cards.
|
|
917
|
+
if (courseConfig) {
|
|
918
|
+
const manifest = await compileManifest(courseConfig);
|
|
919
|
+
writeManifest(manifest);
|
|
920
|
+
return manifest;
|
|
850
921
|
}
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
922
|
+
if (existsSync(MANIFEST_PATH)) return readManifestSync();
|
|
923
|
+
throw new Error(
|
|
924
|
+
`${relative(REPOSITORY_ROOT, MANIFEST_PATH)} not found. Run \`pnpm validate:content\` first.`,
|
|
925
|
+
);
|
|
854
926
|
}
|
|
@@ -711,6 +711,17 @@ for (const gloss of glossItems) {
|
|
|
711
711
|
pointer-events: none;
|
|
712
712
|
}
|
|
713
713
|
|
|
714
|
+
/* KaTeX draws a radical sign, an extensible arrow (`\xrightarrow`), and
|
|
715
|
+
other stretchy glyphs as an absolutely-positioned SVG layer that is
|
|
716
|
+
painted after — and on top of — the content it decorates. Without this,
|
|
717
|
+
a pointer over a symbol inside `\sqrt{…}` or under an arrow hits that
|
|
718
|
+
overlay instead of the notation marker beneath it. The radicand lives in
|
|
719
|
+
the sibling `.svg-align` layer, which stays interactive. */
|
|
720
|
+
:global(.katex-html .sqrt .vlist > span:not(.svg-align)),
|
|
721
|
+
:global(.katex-html .hide-tail) {
|
|
722
|
+
pointer-events: none;
|
|
723
|
+
}
|
|
724
|
+
|
|
714
725
|
:global(.katex-html [data-notation-key]) {
|
|
715
726
|
cursor: help;
|
|
716
727
|
}
|
|
@@ -23,7 +23,68 @@ import DefaultSidebar from '@astrojs/starlight/components/Sidebar.astro';
|
|
|
23
23
|
size="1rem"
|
|
24
24
|
/>
|
|
25
25
|
</button>
|
|
26
|
-
<div
|
|
26
|
+
<div
|
|
27
|
+
id="starlight__navigation-content"
|
|
28
|
+
class="layout-navigation-content layout-compact-sidebar"
|
|
29
|
+
>
|
|
27
30
|
<DefaultSidebar />
|
|
31
|
+
<script>
|
|
32
|
+
import { createBrowserPreferenceStore } from '../../session/PreferenceStore';
|
|
33
|
+
|
|
34
|
+
(() => {
|
|
35
|
+
if (!matchMedia('(min-width: 50rem)').matches) return;
|
|
36
|
+
const root = document.querySelector('#starlight__navigation-content');
|
|
37
|
+
const stateRoot = root?.querySelector('sl-sidebar-state-persist');
|
|
38
|
+
if (!stateRoot) return;
|
|
39
|
+
|
|
40
|
+
const groups = [
|
|
41
|
+
...stateRoot.querySelectorAll(':scope > ul > li > details'),
|
|
42
|
+
];
|
|
43
|
+
const storageKey = 'explico-sidebar-groups';
|
|
44
|
+
const preferences = createBrowserPreferenceStore();
|
|
45
|
+
let compact = groups.map(() => true);
|
|
46
|
+
try {
|
|
47
|
+
const saved = JSON.parse(preferences.get(storageKey) || 'null');
|
|
48
|
+
if (
|
|
49
|
+
saved?.hash === stateRoot.dataset.hash &&
|
|
50
|
+
Array.isArray(saved.compact)
|
|
51
|
+
) {
|
|
52
|
+
compact = groups.map((_, index) => saved.compact[index] !== false);
|
|
53
|
+
}
|
|
54
|
+
} catch {}
|
|
55
|
+
|
|
56
|
+
const save = () => {
|
|
57
|
+
try {
|
|
58
|
+
preferences.set(
|
|
59
|
+
storageKey,
|
|
60
|
+
JSON.stringify({
|
|
61
|
+
hash: stateRoot.dataset.hash,
|
|
62
|
+
compact: groups.map((group) => group.hasAttribute('data-compact')),
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
} catch {}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
groups.forEach((group, index) => {
|
|
69
|
+
// The links remain rendered and operable in compact mode; only their
|
|
70
|
+
// labels collapse. Keep <details> open so this stays true for assistive
|
|
71
|
+
// technology as well as pointer users.
|
|
72
|
+
group.setAttribute('open', '');
|
|
73
|
+
group.toggleAttribute('data-compact', compact[index]);
|
|
74
|
+
const summary = group.querySelector(':scope > summary');
|
|
75
|
+
summary?.setAttribute('aria-expanded', String(!compact[index]));
|
|
76
|
+
summary?.addEventListener('click', (event) => {
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
group.toggleAttribute('data-compact');
|
|
79
|
+
group.setAttribute('open', '');
|
|
80
|
+
summary.setAttribute(
|
|
81
|
+
'aria-expanded',
|
|
82
|
+
String(!group.hasAttribute('data-compact')),
|
|
83
|
+
);
|
|
84
|
+
save();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
})();
|
|
88
|
+
</script>
|
|
28
89
|
</div>
|
|
29
90
|
</div>
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* D5); this component never rebuilds the registry.
|
|
10
10
|
*/
|
|
11
11
|
import DefaultFooter from '@astrojs/starlight/components/Footer.astro';
|
|
12
|
+
import { assertLocalNotationParity } from '../../compiler/astro';
|
|
12
13
|
import { loadManifest } from '../../compiler/manifest';
|
|
13
14
|
import {
|
|
14
15
|
isLessonSlug,
|
|
@@ -34,6 +35,16 @@ const definitions: NotationLayerDefinition[] = [];
|
|
|
34
35
|
if (lessonId) {
|
|
35
36
|
const manifest = await loadManifest();
|
|
36
37
|
const lesson = manifest.lessons.find((entry) => entry.id === lessonId);
|
|
38
|
+
const routeData = Astro.locals.starlightRoute.entry.data as {
|
|
39
|
+
notation?: { local?: readonly { key?: unknown }[] };
|
|
40
|
+
};
|
|
41
|
+
const authoredLocalKeys = (routeData.notation?.local ?? [])
|
|
42
|
+
.map((entry) => entry.key)
|
|
43
|
+
.filter((key): key is string => typeof key === 'string');
|
|
44
|
+
const renderedLocalKeys = (lesson?.notation.definitions ?? [])
|
|
45
|
+
.filter((definition) => definition.scope === 'local')
|
|
46
|
+
.map((definition) => definition.key);
|
|
47
|
+
assertLocalNotationParity(lessonId, authoredLocalKeys, renderedLocalKeys);
|
|
37
48
|
for (const definition of lesson?.notation.definitions ?? []) {
|
|
38
49
|
definitions.push({
|
|
39
50
|
key: definition.key,
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* A referenceable table (F-tables-figures) — the table equivalent of a
|
|
4
4
|
* keyed display equation. `remark-notation.mjs` numbers every `<Table>` on a
|
|
5
|
-
* lesson page `‹
|
|
6
|
-
* sequential over
|
|
5
|
+
* lesson page `‹part›.‹n›` (the part is the sidebar group's ordinal, `n`
|
|
6
|
+
* sequential over every `<Table>` in that part — see
|
|
7
|
+
* `docs/authoring/structure.md`), keyed or not, and injects the resulting
|
|
7
8
|
* `number`/`anchorId` as props; this component only renders what it's given
|
|
8
9
|
* — it never computes its own number, same as `equationNode()`'s output for
|
|
9
10
|
* equations.
|
|
@@ -54,10 +54,12 @@ export interface EquationLessonInput {
|
|
|
54
54
|
readonly file: string;
|
|
55
55
|
readonly body: string;
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* `{ prefix, offset }` — the part's 1-based ordinal among the sidebar groups,
|
|
58
|
+
* and how many display equations earlier lessons of that part already used.
|
|
59
|
+
* When set, equations are numbered `‹part›.‹offset + n›`; when omitted, the
|
|
60
|
+
* legacy `##`-section scheme. See `docs/authoring/structure.md`.
|
|
59
61
|
*/
|
|
60
|
-
readonly
|
|
62
|
+
readonly numbering?: { readonly prefix: number; readonly offset: number };
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
const BRACKET_REF = /\[\[\s*([^\][]+?)\s*\]\]/g;
|
|
@@ -95,7 +97,7 @@ export function validateEquations(
|
|
|
95
97
|
)) {
|
|
96
98
|
const seen = new Set<string>();
|
|
97
99
|
const table: Record<string, string> = {};
|
|
98
|
-
for (const raw of scanEquationLabels(lesson.body, lesson.
|
|
100
|
+
for (const raw of scanEquationLabels(lesson.body, lesson.numbering) as {
|
|
99
101
|
key: string;
|
|
100
102
|
number: string;
|
|
101
103
|
section: number;
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
* Equation identity (Step D7, extended F3) — shared, framework-free helpers.
|
|
3
3
|
*
|
|
4
4
|
* Every display equation (`$$…$$`) in a lesson is numbered and given an `id`
|
|
5
|
-
* from appearance order. When the caller supplies
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* the
|
|
10
|
-
*
|
|
5
|
+
* from appearance order. When the caller supplies `numbering`
|
|
6
|
+
* (`{ prefix, offset }` — see `resolveNumbering`), the number is
|
|
7
|
+
* `‹part›.‹offset + n›`: the prefix is the **part**'s ordinal among the sidebar
|
|
8
|
+
* groups, `n` counts every display equation in the lesson, and `offset` carries
|
|
9
|
+
* the count from earlier lessons of the same part so the sequence runs
|
|
10
|
+
* continuously across a part rather than restarting on every page. `##` and
|
|
11
|
+
* `###` headings never affect it — `(2.4)` is the fourth display equation of
|
|
12
|
+
* part 2, wherever in that part it sits. See `docs/authoring/structure.md` for
|
|
13
|
+
* the level names. With no `numbering` the number falls back to the
|
|
14
|
+
* `##`-section scheme `‹section›.‹indexInSection›` (used by hand-built test
|
|
15
|
+
* trees and non-lesson pages). An author additionally
|
|
11
16
|
* marks an equation with a trailing `\label{eq:<key>}` inside its `$$…$$`
|
|
12
17
|
* block (inside math, so the `:` is safe and the form matches amsmath) when
|
|
13
18
|
* they want a *stable* anchor that survives edits and cross-page references:
|
|
@@ -63,19 +68,42 @@ function equationNumber(section, index) {
|
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
71
|
+
* Normalise the numbering input to `{ prefix, offset }`.
|
|
72
|
+
*
|
|
73
|
+
* `prefix` is the number before the dot — the **part**'s ordinal (its position
|
|
74
|
+
* among the sidebar groups). `offset` is how many display equations already
|
|
75
|
+
* appeared in earlier lessons of that same part, so `n` runs continuously
|
|
76
|
+
* across a part instead of restarting on every page. A bare number is the
|
|
77
|
+
* legacy per-lesson form (`prefix = n`, `offset = 0`) and is still accepted so
|
|
78
|
+
* hand-built trees and unit tests keep working. `undefined`/`null`/`0` ⇒ the
|
|
79
|
+
* `##`-section fallback for non-lesson pages.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveNumbering(input) {
|
|
82
|
+
if (input == null) return { prefix: undefined, offset: 0 };
|
|
83
|
+
if (typeof input === 'number') {
|
|
84
|
+
return { prefix: input || undefined, offset: 0 };
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
prefix: input.prefix || undefined,
|
|
88
|
+
offset: Number(input.offset) || 0,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The visible number for one display equation. With a `prefix` (the part's
|
|
94
|
+
* ordinal) the scheme is `‹part›.‹offset + indexInLesson›` and `##` headings
|
|
68
95
|
* are ignored; without it, the legacy `##`-section scheme.
|
|
69
96
|
*/
|
|
70
97
|
export function displayEquationNumber({
|
|
71
|
-
|
|
98
|
+
prefix,
|
|
99
|
+
offset = 0,
|
|
72
100
|
section,
|
|
73
101
|
indexInSection,
|
|
74
102
|
indexInLesson,
|
|
75
103
|
}) {
|
|
76
|
-
return
|
|
104
|
+
return prefix == null
|
|
77
105
|
? equationNumber(section, indexInSection)
|
|
78
|
-
: `${
|
|
106
|
+
: `${prefix}.${offset + indexInLesson}`;
|
|
79
107
|
}
|
|
80
108
|
|
|
81
109
|
/** The positional anchor id for an unkeyed equation, e.g. `2.4` → `eq-2-4`. */
|
|
@@ -109,15 +137,18 @@ export function stripJsxComponents(markdown, keep) {
|
|
|
109
137
|
}
|
|
110
138
|
|
|
111
139
|
/**
|
|
112
|
-
* Every
|
|
113
|
-
* its assigned number. `$$…$$` blocks only. With `
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
140
|
+
* Every display equation in a Markdown body, keyed or not, in appearance order
|
|
141
|
+
* with its assigned number. `$$…$$` blocks only. With `numbering`
|
|
142
|
+
* (`{ prefix, offset }`, the part's ordinal and the count of display equations
|
|
143
|
+
* in earlier lessons of that part) the number is `‹part›.‹offset + n›`, `n`
|
|
144
|
+
* sequential over every display block in the lesson; without it, `##` headings
|
|
145
|
+
* advance a section counter and the number is `‹section›.‹indexInSection›`.
|
|
146
|
+
* Either way every display block advances the index, so a keyed equation's
|
|
147
|
+
* number reflects its position among *all* display equations, not only the
|
|
148
|
+
* keyed ones.
|
|
119
149
|
*/
|
|
120
|
-
export function
|
|
150
|
+
export function scanDisplayEquations(markdown, numbering) {
|
|
151
|
+
const { prefix, offset } = resolveNumbering(numbering);
|
|
121
152
|
const text = stripJsxComponents(stripFencedCode(markdown));
|
|
122
153
|
const tokens = [];
|
|
123
154
|
const heading = /^[ \t]{0,3}##[ \t]+\S/gm;
|
|
@@ -148,11 +179,11 @@ export function scanEquationLabels(markdown, chapter) {
|
|
|
148
179
|
}
|
|
149
180
|
indexInSection += 1;
|
|
150
181
|
indexInLesson += 1;
|
|
151
|
-
if (token.key == null) continue;
|
|
152
182
|
labels.push({
|
|
153
183
|
key: token.key,
|
|
154
184
|
number: displayEquationNumber({
|
|
155
|
-
|
|
185
|
+
prefix,
|
|
186
|
+
offset,
|
|
156
187
|
section,
|
|
157
188
|
indexInSection,
|
|
158
189
|
indexInLesson,
|
|
@@ -164,14 +195,31 @@ export function scanEquationLabels(markdown, chapter) {
|
|
|
164
195
|
return labels;
|
|
165
196
|
}
|
|
166
197
|
|
|
198
|
+
/** The keyed subset of `scanDisplayEquations` — the manifest builder's view. */
|
|
199
|
+
export function scanEquationLabels(markdown, numbering) {
|
|
200
|
+
return scanDisplayEquations(markdown, numbering).filter(
|
|
201
|
+
(equation) => equation.key != null,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* How many display equations a lesson body holds. This is what makes `n` run
|
|
207
|
+
* continuously across a part: a lesson's `offset` is the sum of this over every
|
|
208
|
+
* earlier lesson in the same part.
|
|
209
|
+
*/
|
|
210
|
+
export function countDisplayEquations(markdown) {
|
|
211
|
+
return scanDisplayEquations(markdown).length;
|
|
212
|
+
}
|
|
213
|
+
|
|
167
214
|
/**
|
|
168
215
|
* The mdast equivalent of `scanEquationLabels` for *every* display equation:
|
|
169
216
|
* walk the tree in document order, advancing the section counter on every
|
|
170
217
|
* depth-2 heading and emitting a record for every `math` node. `key` is the
|
|
171
|
-
* `\label{eq:…}` payload when present, else `null`. `
|
|
218
|
+
* `\label{eq:…}` payload when present, else `null`. `numbering` is threaded the
|
|
172
219
|
* same way as in `scanEquationLabels`.
|
|
173
220
|
*/
|
|
174
|
-
export function collectDisplayEquations(tree,
|
|
221
|
+
export function collectDisplayEquations(tree, numbering) {
|
|
222
|
+
const { prefix, offset } = resolveNumbering(numbering);
|
|
175
223
|
const equations = [];
|
|
176
224
|
let section = 0;
|
|
177
225
|
let indexInSection = 0;
|
|
@@ -196,7 +244,8 @@ export function collectDisplayEquations(tree, chapter) {
|
|
|
196
244
|
equations.push({
|
|
197
245
|
key: label ? label[1] : null,
|
|
198
246
|
number: displayEquationNumber({
|
|
199
|
-
|
|
247
|
+
prefix,
|
|
248
|
+
offset,
|
|
200
249
|
section,
|
|
201
250
|
indexInSection,
|
|
202
251
|
indexInLesson,
|
|
@@ -214,8 +263,8 @@ export function collectDisplayEquations(tree, chapter) {
|
|
|
214
263
|
* The keyed subset of `collectDisplayEquations` — the tree-walk counterpart of
|
|
215
264
|
* `scanEquationLabels`. Numbers reflect position among all display equations.
|
|
216
265
|
*/
|
|
217
|
-
export function collectEquationLabels(tree,
|
|
218
|
-
return collectDisplayEquations(tree,
|
|
266
|
+
export function collectEquationLabels(tree, numbering) {
|
|
267
|
+
return collectDisplayEquations(tree, numbering).filter(
|
|
219
268
|
(equation) => equation.key != null,
|
|
220
269
|
);
|
|
221
270
|
}
|
|
@@ -69,10 +69,13 @@ export interface KeyedFloatLessonInput {
|
|
|
69
69
|
readonly file: string;
|
|
70
70
|
readonly body: string;
|
|
71
71
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
72
|
+
* `{ prefix, offset }` — the part's 1-based ordinal among the sidebar groups,
|
|
73
|
+
* and how many floats of this kind earlier lessons of that part already used.
|
|
74
|
+
* When set, floats are numbered `‹part›.‹offset + n›`; when omitted, the
|
|
75
|
+
* legacy `##`-section scheme. Each kind counts on its own, so the caller
|
|
76
|
+
* passes a different `offset` for `<Table>` than for `<Figure>`.
|
|
74
77
|
*/
|
|
75
|
-
readonly
|
|
78
|
+
readonly numbering?: { readonly prefix: number; readonly offset: number };
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
const BRACKET_REF = /\[\[\s*([^\][]+?)\s*\]\]/g;
|
|
@@ -117,7 +120,7 @@ export function validateKeyedFloats(
|
|
|
117
120
|
const table: Record<string, string> = {};
|
|
118
121
|
for (const raw of scanKeyedJsxFloats(lesson.body, {
|
|
119
122
|
tagName,
|
|
120
|
-
|
|
123
|
+
numbering: lesson.numbering,
|
|
121
124
|
}) as {
|
|
122
125
|
key: string | null;
|
|
123
126
|
number: string;
|