explico 0.8.0 → 0.11.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 +1239 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +46 -2
- package/dist/compiler/cli.js +70 -4
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +68 -5
- package/dist/compiler/course-config.d.ts +3 -194
- package/dist/compiler/course-config.js +78 -5
- package/dist/compiler/exports/standalone-html.d.ts +6 -1
- package/dist/compiler/knowledge-version.js +1 -1
- package/dist/compiler/lesson-derivation.d.ts +27 -10
- package/dist/compiler/lesson-derivation.js +23 -8
- package/dist/compiler/logging.d.ts +4 -1
- package/dist/compiler/logging.js +4 -1
- package/dist/compiler/manifest.d.ts +204 -31
- package/dist/compiler/manifest.js +270 -55
- package/dist/compiler/resolution-report.d.ts +6 -1
- package/dist/course-config-Dsnri9Tk.d.ts +464 -0
- package/dist/reference/base-accents.d.mts +26 -0
- package/dist/reference/base-accents.mjs +31 -0
- package/dist/reference/base-roman-labels.d.mts +2 -2
- package/dist/reference/base-roman-labels.mjs +2 -2
- package/dist/reference/consistency.d.ts +14 -2
- package/dist/reference/consistency.js +108 -4
- package/dist/reference/delimiter-size.d.ts +3 -2
- package/dist/reference/diagrams-validate.d.ts +7 -2
- package/dist/reference/diagrams-validate.js +9 -1
- package/dist/reference/equations-validate.d.ts +8 -5
- package/dist/reference/equations-validate.js +1 -0
- package/dist/reference/equations.d.mts +60 -31
- package/dist/reference/equations.mjs +83 -43
- package/dist/reference/examples-validate.d.ts +31 -0
- package/dist/reference/examples-validate.js +48 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/figures-validate.js +4 -1
- package/dist/reference/folds-validate.d.ts +33 -0
- package/dist/reference/folds-validate.js +32 -0
- package/dist/reference/gate-abbreviations.d.ts +3 -1
- package/dist/reference/gate-abbreviations.js +1 -0
- package/dist/reference/gate-course-rules.d.ts +32 -0
- package/dist/reference/gate-course-rules.js +85 -0
- package/dist/reference/gate-heading-length.d.ts +5 -2
- package/dist/reference/gate-heading-length.js +45 -21
- package/dist/reference/gate-heading-structure.d.ts +41 -0
- package/dist/reference/gate-heading-structure.js +113 -0
- package/dist/reference/gate-math-punctuation.d.ts +71 -0
- package/dist/reference/gate-math-punctuation.js +251 -0
- package/dist/reference/gate-math.d.ts +22 -6
- package/dist/reference/gate-math.js +42 -8
- package/dist/reference/gate-part-pages.d.ts +50 -0
- package/dist/reference/gate-part-pages.js +118 -0
- package/dist/reference/gate-prose-notation.d.ts +21 -11
- package/dist/reference/gate-prose-notation.js +25 -7
- package/dist/reference/katex-delimiters.mjs +20 -7
- package/dist/reference/keyed-float-validate.d.ts +27 -10
- package/dist/reference/keyed-float-validate.js +15 -1
- package/dist/reference/keyed-jsx-floats.d.mts +11 -5
- package/dist/reference/keyed-jsx-floats.mjs +73 -20
- package/dist/reference/knowledge-checks-validate.d.ts +1 -1
- package/dist/reference/knowledge-checks.d.mts +3 -1
- package/dist/reference/knowledge-checks.mjs +9 -4
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-slot.d.ts +34 -0
- package/dist/reference/lab-math-slot.js +6 -0
- package/dist/reference/math-glyphs.d.mts +80 -4
- package/dist/reference/math-glyphs.mjs +572 -37
- package/dist/reference/page-notation.d.ts +6 -1
- package/dist/reference/raw-floats-validate.js +1 -1
- package/dist/reference/raw-floats.mjs +1 -1
- package/dist/reference/rehype-heading-anchors.d.mts +14 -0
- package/dist/reference/rehype-heading-anchors.mjs +116 -17
- package/dist/reference/rehype-table-header-labels.d.mts +6 -1
- package/dist/reference/rehype-table-header-labels.mjs +21 -3
- package/dist/reference/remark-asides.d.mts +3 -0
- package/dist/reference/remark-asides.mjs +180 -0
- package/dist/reference/remark-memo.d.mts +3 -1
- package/dist/reference/remark-memo.mjs +42 -11
- package/dist/reference/remark-notation.d.mts +60 -10
- package/dist/reference/remark-notation.mjs +387 -36
- package/dist/reference/render-lab-math.d.ts +4 -26
- package/dist/reference/render-lab-math.js +4 -4
- package/dist/reference/sections-validate.d.ts +27 -0
- package/dist/reference/sections-validate.js +41 -0
- package/dist/reference/sections.d.mts +97 -0
- package/dist/reference/sections.mjs +306 -0
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +17 -1
- package/dist/reference/unit-words.d.ts +37 -0
- package/dist/reference/unit-words.js +170 -0
- package/dist/rules/catalog.d.ts +176 -0
- package/dist/rules/catalog.js +983 -0
- package/dist/rules/course-rules.d.ts +115 -0
- package/dist/rules/course-rules.js +292 -0
- package/dist/rules/course-rules.schema.json +138 -0
- package/dist/rules/in-effect.d.ts +53 -0
- package/dist/rules/in-effect.js +142 -0
- package/dist/rules/rules.schema.json +1072 -0
- package/dist/rules/settings.d.ts +3 -0
- package/dist/rules/settings.js +290 -0
- package/dist/rules/waivers.d.ts +53 -0
- package/dist/rules/waivers.js +132 -0
- package/dist/session/PreferenceStore.d.ts +5 -0
- package/dist/session/PreferenceStore.js +9 -0
- package/docs/VERSIONING.md +2 -2
- package/docs/authoring/README.md +232 -40
- package/docs/authoring/agents.md +55 -9
- package/docs/authoring/components.md +178 -22
- package/docs/authoring/migrating.md +1096 -2
- package/docs/authoring/notation.md +576 -322
- package/docs/authoring/prose.md +551 -0
- package/docs/authoring/retired.md +35 -0
- package/docs/authoring/structure-rules.md +422 -40
- package/docs/authoring/structure.md +445 -62
- package/docs/authoring/versioning.md +20 -28
- package/docs/authoring/visualizations.md +142 -20
- package/package.json +2 -2
- package/src/compiler/cli.ts +116 -6
- package/src/compiler/collections.ts +111 -7
- package/src/compiler/course-config.ts +273 -34
- package/src/compiler/knowledge-version.ts +1 -1
- package/src/compiler/lesson-derivation.ts +54 -15
- package/src/compiler/logging.ts +8 -2
- package/src/compiler/manifest.ts +573 -104
- package/src/components/BlockHeading.astro +113 -0
- package/src/components/CourseContents.astro +221 -0
- package/src/components/CurriculumMap.astro +5 -6
- package/src/components/FloatIndex.astro +200 -0
- package/src/components/HoverPanel.astro +431 -79
- package/src/components/PartBridge.astro +27 -0
- package/src/components/PartOverview.astro +401 -0
- package/src/components/PartScope.astro +27 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
- package/src/components/assessments/AssessmentRunner.tsx +8 -5
- package/src/components/assessments/AssessmentSet.astro +23 -26
- package/src/components/block-heading.ts +146 -0
- package/src/components/card-arrow.ts +243 -0
- package/src/components/card-drag.ts +631 -0
- package/src/components/card-jump.ts +45 -0
- package/src/components/card-manager.ts +1589 -0
- package/src/components/card-memory.ts +177 -0
- package/src/components/card-placement.ts +504 -0
- package/src/components/card-side.ts +336 -0
- package/src/components/card-travel.ts +197 -0
- package/src/components/citation/CitationLayer.astro +77 -76
- package/src/components/diagram/Diagram.astro +161 -316
- package/src/components/disclosure/course-disclosure.ts +24 -0
- package/src/components/disclosure/hover-preview.ts +122 -42
- package/src/components/disclosure/persisted-disclosure.ts +3 -3
- package/src/components/disclosure/print-as-shown.ts +252 -0
- package/src/components/examples/CompactExample.astro +3 -1
- package/src/components/examples/CompactExamples.astro +101 -94
- package/src/components/figure/Figure.astro +174 -274
- package/src/components/figure/FloatCaption.astro +87 -0
- package/src/components/figure/figure-state.ts +58 -0
- package/src/components/figure/figure-tools.ts +87 -0
- package/src/components/float-name.ts +101 -0
- package/src/components/fold/Fold.astro +125 -0
- package/src/components/fold/fold-id.ts +40 -0
- package/src/components/memo/Memo.astro +162 -231
- package/src/components/memo/ShortNote.astro +6 -6
- package/src/components/notation/NotationFormula.astro +5 -1
- package/src/components/notation/NotationGlossary.astro +3 -4
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +5 -4
- package/src/components/starlight/CollapsibleSections.astro +207 -93
- package/src/components/starlight/DiagramEnhancer.astro +15 -138
- package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
- package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
- package/src/components/starlight/LayoutHeader.astro +298 -19
- package/src/components/starlight/LayoutPageSidebar.astro +99 -12
- package/src/components/starlight/LayoutPageTitle.astro +120 -0
- package/src/components/starlight/LayoutSidebar.astro +229 -11
- package/src/components/starlight/LessonExportActions.astro +53 -72
- package/src/components/starlight/LessonFooter.astro +26 -4
- package/src/components/starlight/section-parts.ts +15 -2
- package/src/components/starlight/section-summary.ts +181 -0
- package/src/components/table/Table.astro +95 -76
- package/src/components/table/TableOverflowExpander.astro +65 -28
- package/src/content-config.ts +40 -4
- package/src/reference/base-accents.mjs +31 -0
- package/src/reference/base-roman-labels.mjs +2 -2
- package/src/reference/consistency.ts +176 -5
- package/src/reference/delimiter-size.ts +3 -2
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +83 -43
- package/src/reference/examples-validate.ts +97 -0
- package/src/reference/figures-validate.ts +6 -1
- package/src/reference/folds-validate.ts +65 -0
- package/src/reference/gate-abbreviations.ts +1 -0
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +66 -35
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +69 -10
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +59 -12
- package/src/reference/katex-delimiters.mjs +20 -7
- package/src/reference/keyed-float-validate.ts +45 -11
- package/src/reference/keyed-jsx-floats.mjs +73 -20
- package/src/reference/knowledge-checks-validate.ts +1 -1
- package/src/reference/knowledge-checks.mjs +9 -4
- package/src/reference/lab-math-slot.ts +36 -0
- package/src/reference/math-glyphs.mjs +572 -37
- package/src/reference/raw-floats-validate.ts +1 -1
- package/src/reference/raw-floats.mjs +1 -1
- package/src/reference/rehype-heading-anchors.mjs +116 -17
- package/src/reference/rehype-table-header-labels.mjs +21 -3
- package/src/reference/remark-asides.mjs +180 -0
- package/src/reference/remark-memo.mjs +42 -11
- package/src/reference/remark-notation.mjs +387 -36
- package/src/reference/render-lab-math.ts +16 -31
- package/src/reference/sections-validate.ts +81 -0
- package/src/reference/sections.mjs +306 -0
- package/src/reference/tables-validate.ts +2 -1
- package/src/reference/types.ts +17 -0
- package/src/reference/unit-words.ts +233 -0
- package/src/rules/catalog.ts +1298 -0
- package/src/rules/course-rules.ts +456 -0
- package/src/rules/in-effect.ts +235 -0
- package/src/rules/settings.ts +448 -0
- package/src/rules/waivers.ts +249 -0
- package/src/session/PreferenceStore.ts +15 -0
- package/src/styles/global.css +1471 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- package/src/components/popup-stack.ts +0 -345
|
@@ -1,45 +1,21 @@
|
|
|
1
1
|
import katex from './katex-delimiters.mjs';
|
|
2
2
|
import { resolveMathGlyphs, stripNotationMarkers } from './math-glyphs.mjs';
|
|
3
3
|
import { createNotationKatexOptions } from './katex-options.mjs';
|
|
4
|
+
import { slotPlaceholder, type RenderedLabMath } from './lab-math-slot';
|
|
5
|
+
|
|
6
|
+
// Re-exported so server code keeps one import. An island imports these from
|
|
7
|
+
// `lab-math-slot` instead: this module is server-only.
|
|
8
|
+
export { slotPlaceholder, type RenderedLabMath };
|
|
4
9
|
|
|
5
10
|
export interface LabMathScopeDefinition {
|
|
6
11
|
readonly key: string;
|
|
7
12
|
readonly notation: string;
|
|
8
13
|
}
|
|
9
14
|
|
|
10
|
-
export interface RenderedLabMath {
|
|
11
|
-
/** KaTeX HTML+MathML with `data-notation-key` and `data-lab-slot` spans. */
|
|
12
|
-
readonly html: string;
|
|
13
|
-
/** Slot names, in template order, the island must supply at runtime. */
|
|
14
|
-
readonly slots: readonly string[];
|
|
15
|
-
/** Notation keys the template resolved to, for diagnostics. */
|
|
16
|
-
readonly keys: readonly string[];
|
|
17
|
-
/**
|
|
18
|
-
* Plain, copyable LaTeX — every explained glyph resolved to its bare
|
|
19
|
-
* symbol, every `\slot{name}{…}` replaced by `slotPlaceholder(name)` — so
|
|
20
|
-
* the island can substitute each slot's *current* live value in and keep
|
|
21
|
-
* `[data-notation-formula]`'s `data-latex` accurate as the reader drags a
|
|
22
|
-
* control, the same "click the equation to copy it" gesture a static
|
|
23
|
-
* lesson or glossary equation gets (`NotationLayer.astro`).
|
|
24
|
-
*/
|
|
25
|
-
readonly plainLatexTemplate: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* A slot's placeholder inside `plainLatexTemplate` — distinct from the
|
|
30
|
-
* numeric sentinel `sentinelFor` below uses internally (that one only has to
|
|
31
|
-
* survive a single notation-binding pass at build time; this one is the
|
|
32
|
-
* stable, name-keyed token an island substitutes with a live value at
|
|
33
|
-
* runtime, so it's exported for that purpose).
|
|
34
|
-
*/
|
|
35
|
-
export function slotPlaceholder(name: string): string {
|
|
36
|
-
return `⟦${name}⟧`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
15
|
interface BindResult {
|
|
40
16
|
latex: string;
|
|
41
17
|
bindings: ReadonlyArray<{ key: string }>;
|
|
42
|
-
unresolved: ReadonlyArray<{ token: string; start: number }>;
|
|
18
|
+
unresolved: ReadonlyArray<{ token: string; start: number; font?: string }>;
|
|
43
19
|
}
|
|
44
20
|
|
|
45
21
|
// `\slot{name}{fallback}` — `name` is a lab-slot id, `fallback` may contain one
|
|
@@ -84,10 +60,19 @@ export function renderLabMath(
|
|
|
84
60
|
|
|
85
61
|
if (result.unresolved.length > 0) {
|
|
86
62
|
const list = result.unresolved.map(({ token }) => `"${token}"`).join(', ');
|
|
63
|
+
// Rule N4: `\mathbf{v}` never borrows the card of a scalar `v` in scope.
|
|
64
|
+
const styled = result.unresolved.filter(({ font }) => font !== undefined);
|
|
65
|
+
const fontHint =
|
|
66
|
+
styled.length === 0
|
|
67
|
+
? ''
|
|
68
|
+
: ` ${styled.map(({ token }) => token).join(', ')} ${
|
|
69
|
+
styled.length === 1 ? 'is a different symbol' : 'are different symbols'
|
|
70
|
+
} from the plain letter and need${styled.length === 1 ? 's' : ''} ` +
|
|
71
|
+
`an entry of ${styled.length === 1 ? 'its' : 'their'} own.`;
|
|
87
72
|
throw new Error(
|
|
88
73
|
`Lab math uses undefined notation ${list}. Every symbol must resolve to ` +
|
|
89
74
|
`a notation entry in this page's glyph scope (a [[key]] reference or ` +
|
|
90
|
-
`notation.local)
|
|
75
|
+
`notation.local).${fontHint}\n template: ${template.trim()}`,
|
|
91
76
|
);
|
|
92
77
|
}
|
|
93
78
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `[[sec-…]]` references (docs/plans/titles-and-containers.md Part 1): every
|
|
3
|
+
* `[[sec-slug]]` and `[[lesson/slug#sec-slug]]` must name a top-level heading
|
|
4
|
+
* of that lesson — `sec-ref-resolves`, an error owned by the `keyed-floats`
|
|
5
|
+
* invariant, the same promise `[[eq-…]]` and `[[fig-…]]` make.
|
|
6
|
+
*
|
|
7
|
+
* The finding goes into `manifest.diagnostics.math` with context `structure`,
|
|
8
|
+
* like S1 and S2 (see `MathGateDiagnostic`), so every reporter that already
|
|
9
|
+
* reads that array fails the build on it without learning a new one.
|
|
10
|
+
*/
|
|
11
|
+
import type { MathGateDiagnostic } from './gate-math';
|
|
12
|
+
import { lessonLine } from './gate-prose-notation';
|
|
13
|
+
import { parseSectionRef } from './sections.mjs';
|
|
14
|
+
|
|
15
|
+
export interface SectionRefLessonInput {
|
|
16
|
+
/** Lesson slug, e.g. `motion/position-and-coordinates`. */
|
|
17
|
+
readonly slug: string;
|
|
18
|
+
readonly file: string;
|
|
19
|
+
readonly body: string;
|
|
20
|
+
readonly bodyLine?: number;
|
|
21
|
+
/** The anchors of the lesson's top-level headings (`manifest.lessons[].headings`). */
|
|
22
|
+
readonly headingSlugs: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const BRACKET_REF = /\[\[\s*([^\][]+?)\s*\]\]/g;
|
|
26
|
+
|
|
27
|
+
/** Code blanked in place, so an index still points into the body. */
|
|
28
|
+
function withoutCode(text: string): string {
|
|
29
|
+
const blank = (block: string) => block.replace(/[^\n]/g, ' ');
|
|
30
|
+
return text
|
|
31
|
+
.replace(/```[\s\S]*?```/g, blank)
|
|
32
|
+
.replace(/~~~[\s\S]*?~~~/g, blank)
|
|
33
|
+
.replace(/`[^`\n]*`/g, blank);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function unresolved(
|
|
37
|
+
ref: { readonly lesson?: string; readonly key: string },
|
|
38
|
+
lessonExists: boolean,
|
|
39
|
+
): string {
|
|
40
|
+
const written = ref.lesson
|
|
41
|
+
? `[[${ref.lesson}#sec-${ref.key}]]`
|
|
42
|
+
: `[[sec-${ref.key}]]`;
|
|
43
|
+
if (ref.lesson && !lessonExists) {
|
|
44
|
+
return `${written} names lesson ${ref.lesson}, which does not exist.`;
|
|
45
|
+
}
|
|
46
|
+
return (
|
|
47
|
+
`${written} names no heading: ${ref.lesson ? `lesson ${ref.lesson}` : 'this lesson'} ` +
|
|
48
|
+
`has no top-level heading anchored #${ref.key}. A heading's anchor is its ` +
|
|
49
|
+
'text in lowercase with spaces as hyphens and punctuation dropped: ' +
|
|
50
|
+
'`## Two ways to name the same point` is ' +
|
|
51
|
+
'`[[sec-two-ways-to-name-the-same-point]]`.'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One `sec-ref-resolves` error per `[[sec-…]]` that names no heading. */
|
|
56
|
+
export function validateSectionRefs(
|
|
57
|
+
lessons: readonly SectionRefLessonInput[],
|
|
58
|
+
): MathGateDiagnostic[] {
|
|
59
|
+
const headingsByLesson = new Map(
|
|
60
|
+
lessons.map((lesson) => [lesson.slug, new Set(lesson.headingSlugs)]),
|
|
61
|
+
);
|
|
62
|
+
const diagnostics: MathGateDiagnostic[] = [];
|
|
63
|
+
for (const lesson of lessons) {
|
|
64
|
+
for (const match of withoutCode(lesson.body).matchAll(BRACKET_REF)) {
|
|
65
|
+
const ref = parseSectionRef(match[1] ?? '');
|
|
66
|
+
if (!ref) continue;
|
|
67
|
+
const headings = headingsByLesson.get(ref.lesson ?? lesson.slug);
|
|
68
|
+
if (headings?.has(ref.key)) continue;
|
|
69
|
+
diagnostics.push({
|
|
70
|
+
context: 'structure',
|
|
71
|
+
code: 'sec-ref-resolves',
|
|
72
|
+
severity: 'error',
|
|
73
|
+
file: lesson.file,
|
|
74
|
+
line: lessonLine(lesson, match.index ?? 0),
|
|
75
|
+
token: match[0],
|
|
76
|
+
message: unresolved(ref, headings !== undefined),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return diagnostics;
|
|
81
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Outline numbering (docs/plans/titles-and-containers.md Part 1): every `##`
|
|
3
|
+
* to `######` heading at the top level of a lesson is numbered
|
|
4
|
+
* `‹part›.‹chapter›.‹section›[.‹subsection›…]`, down to the course's
|
|
5
|
+
* `numbering.sections.depth`, and is citable as `[[sec-‹slug›]]`. A
|
|
6
|
+
* `[card=meta]` section is logistics, not a step of the lesson, so it and
|
|
7
|
+
* every heading inside it stay unnumbered — LaTeX's `\section*`, and what
|
|
8
|
+
* References already is — and take no number from the sections after it.
|
|
9
|
+
*
|
|
10
|
+
* Numbers are for a reader to say and see; the anchor stays the heading's
|
|
11
|
+
* slug (`#two-ways-to-name-the-same-point`), never the number.
|
|
12
|
+
*
|
|
13
|
+
* Two walks, same contract as `equations.mjs` and `keyed-jsx-floats.mjs`:
|
|
14
|
+
* `scanSectionHeadings` reads raw Markdown for the manifest, and
|
|
15
|
+
* `collectSectionHeadings` walks the mdast for `remark-notation.mjs`. Both see
|
|
16
|
+
* the same headings — top level only; a heading inside a component (`<Memo>`,
|
|
17
|
+
* `<CompactExample>`, …) is part of that component, not of the outline — and
|
|
18
|
+
* number them with the one `sectionNumbers` below, so the two agree whenever
|
|
19
|
+
* they agree on the level sequence. `remark-notation.mjs` checks exactly that
|
|
20
|
+
* before it trusts the manifest's slugs and numbers.
|
|
21
|
+
*
|
|
22
|
+
* The manifest is where a heading's slug is decided: `headingPlainText` reads
|
|
23
|
+
* the heading's source text and `slugHeadings` slugs it with Astro's own
|
|
24
|
+
* `rehypeHeadingIds`, so a plain-text heading keeps the id it always had. The
|
|
25
|
+
* render path then gives each heading that id before Astro's pass would have
|
|
26
|
+
* picked one, so `[[sec-…]]`, the page, and the alias map cannot disagree.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
|
|
30
|
+
import { headingCardTier, stripHeadingMarker } from './rehype-heading-anchors.mjs';
|
|
31
|
+
|
|
32
|
+
/** The deepest heading level numbered when a course sets nothing (`####`). */
|
|
33
|
+
export const DEFAULT_SECTION_DEPTH = 4;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `[[sec-slug]]`, or `[[lesson/slug#sec-slug]]` across pages. The slug part
|
|
37
|
+
* is whatever the heading's anchor is: github-slugger keeps letters, digits,
|
|
38
|
+
* `_` and `-` from any script, and can double a hyphen.
|
|
39
|
+
*/
|
|
40
|
+
const SECTION_REF =
|
|
41
|
+
/^(?:([a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)*)#)?sec-([\p{L}\p{N}_-]+)$/u;
|
|
42
|
+
|
|
43
|
+
export function parseSectionRef(raw) {
|
|
44
|
+
const match = SECTION_REF.exec(String(raw).trim());
|
|
45
|
+
if (!match) return undefined;
|
|
46
|
+
return match[1] ? { lesson: match[1], key: match[2] } : { key: match[2] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The number each heading takes, from its level alone: `##` counts within the
|
|
51
|
+
* chapter, `###` within its `##`, and so on, each deeper counter restarting
|
|
52
|
+
* when a shallower heading appears. A level a heading skipped counts as 0
|
|
53
|
+
* (`##` then `####` is `…1.0.1`), the way LaTeX numbers a skipped level. An
|
|
54
|
+
* `#` in a lesson body is never numbered and resets nothing. Headings deeper
|
|
55
|
+
* than `depth`, or any heading when there is no `prefix`, get `undefined`.
|
|
56
|
+
* `unnumbered[i]` marks heading `i` as outside the outline (a meta section):
|
|
57
|
+
* it and the deeper headings under it get `undefined` and move no counter.
|
|
58
|
+
*/
|
|
59
|
+
export function sectionNumbers(
|
|
60
|
+
levels,
|
|
61
|
+
{ prefix, depth = DEFAULT_SECTION_DEPTH, unnumbered = [] } = {},
|
|
62
|
+
) {
|
|
63
|
+
const counters = [0, 0, 0, 0, 0, 0, 0];
|
|
64
|
+
// A heading deeper than this level sits inside an unnumbered section.
|
|
65
|
+
let outside = 7;
|
|
66
|
+
return levels.map((level, index) => {
|
|
67
|
+
if (level < 2) return undefined;
|
|
68
|
+
if (level <= outside) outside = 7;
|
|
69
|
+
if (unnumbered[index]) outside = level;
|
|
70
|
+
if (outside < 7) return undefined;
|
|
71
|
+
counters[level] += 1;
|
|
72
|
+
for (let deeper = level + 1; deeper <= 6; deeper++) counters[deeper] = 0;
|
|
73
|
+
if (!prefix || level > depth) return undefined;
|
|
74
|
+
return [prefix, ...counters.slice(2, level + 1)].join('.');
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- raw Markdown ------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
const blank = (block) => block.replace(/[^\n]/g, ' ');
|
|
81
|
+
|
|
82
|
+
// An MDX attribute may be a quoted string, a brace expression (one level of
|
|
83
|
+
// nesting), or unquoted source — the same breadth `stripJsxComponents` allows.
|
|
84
|
+
const ATTRIBUTES = String.raw`(?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}|[^>"'])*`;
|
|
85
|
+
const JSX_BLOCK = new RegExp(
|
|
86
|
+
String.raw`<([A-Za-z][\w.:-]*)(?:\s${ATTRIBUTES})?>[\s\S]*?<\/\1\s*>`,
|
|
87
|
+
'g',
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The body with everything that is not outline blanked, newlines kept: fenced
|
|
92
|
+
* code, MDX comments, and every JSX element's children (a heading in there is
|
|
93
|
+
* not an outline heading). A self-closing element has no children to blank.
|
|
94
|
+
* With `markers`, a code block keeps its opening backtick or tilde and an
|
|
95
|
+
* element its `<Name`, so `scanOutlineBlocks` can see that something stands
|
|
96
|
+
* there; neither can start a heading line, so both forms find the same
|
|
97
|
+
* headings.
|
|
98
|
+
*/
|
|
99
|
+
function outlineSource(markdown, { markers = false } = {}) {
|
|
100
|
+
const fence = markers ? (block) => block[0] + blank(block.slice(1)) : blank;
|
|
101
|
+
const element = markers
|
|
102
|
+
? (block, name) => `<${name}${blank(block.slice(name.length + 1))}`
|
|
103
|
+
: blank;
|
|
104
|
+
return String(markdown)
|
|
105
|
+
.replace(/```[\s\S]*?```/g, fence)
|
|
106
|
+
.replace(/~~~[\s\S]*?~~~/g, fence)
|
|
107
|
+
.replace(/\{\/\*[\s\S]*?\*\/\}/g, blank)
|
|
108
|
+
.replace(JSX_BLOCK, element);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const ATX_HEADING = /^[ \t]{0,3}(#{1,6})(?:[ \t]+([^\n]*?))?(?:[ \t]+#+)?[ \t]*$/gm;
|
|
112
|
+
|
|
113
|
+
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The text a heading's source renders as, near enough to slug it the way
|
|
117
|
+
* Astro would: markup goes, the words stay. Inline code and math keep their
|
|
118
|
+
* source (math renders as KaTeX, whose text is no use as a slug); a notation
|
|
119
|
+
* term keeps its key; a link keeps its text; an image, a citation and an MDX
|
|
120
|
+
* expression contribute nothing; the authoring marker is dropped.
|
|
121
|
+
*/
|
|
122
|
+
export function headingPlainText(source) {
|
|
123
|
+
const literals = [];
|
|
124
|
+
const keep = (value) => `\u0000${literals.push(value) - 1}\u0000`;
|
|
125
|
+
let text = stripHeadingMarker(source ?? '')
|
|
126
|
+
.replace(/(`+)([\s\S]*?[^`])\1(?!`)/g, (_, _ticks, code) => keep(code.trim()))
|
|
127
|
+
.replace(/\$([^$\n]+)\$/g, (_, latex) => keep(latex.trim()))
|
|
128
|
+
.replace(/\[\[\s*([^\][]+?)\s*\]\]/g, (_, key) => keep(key))
|
|
129
|
+
.replace(/\[@[^\]]*\]/g, '')
|
|
130
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, '')
|
|
131
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
|
|
132
|
+
.replace(/\[([^\]]*)\]\[[^\]]*\]/g, '$1')
|
|
133
|
+
.replace(/<(https?:\/\/[^>\s]+)>/g, (_, url) => keep(url))
|
|
134
|
+
.replace(/\{[^{}]*\}/g, '')
|
|
135
|
+
.replace(/<\/?[A-Za-z][^>]*>/g, '')
|
|
136
|
+
.replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g, '$2')
|
|
137
|
+
.replace(/(^|[^\w*])\*(?=\S)([\s\S]*?\S)\*(?![\w*])/g, '$1$2')
|
|
138
|
+
.replace(/(^|[^\w])_(?=\S)([\s\S]*?\S)_(?!\w)/g, '$1$2')
|
|
139
|
+
.replace(/~~(?=\S)([\s\S]*?\S)~~/g, '$1')
|
|
140
|
+
.replace(/\\([!-/:-@[-`{-~])/g, '$1')
|
|
141
|
+
.replace(/&(#x[0-9a-f]+|#[0-9]+|[a-z]+);/gi, (entity, name) => {
|
|
142
|
+
if (name[0] !== '#') return ENTITIES[name.toLowerCase()] ?? entity;
|
|
143
|
+
const code = name[1] === 'x' || name[1] === 'X'
|
|
144
|
+
? Number.parseInt(name.slice(2), 16)
|
|
145
|
+
: Number.parseInt(name.slice(1), 10);
|
|
146
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : entity;
|
|
147
|
+
});
|
|
148
|
+
text = text.replace(/\u0000(\d+)\u0000/g, (_, index) => literals[Number(index)]);
|
|
149
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Anchors for a page's headings, in order, from Astro's own `rehypeHeadingIds`
|
|
154
|
+
* run over their plain text: the same slugger with the same duplicate
|
|
155
|
+
* counters (`foo`, `foo-1`) the page would otherwise get.
|
|
156
|
+
*/
|
|
157
|
+
export function slugHeadings(texts) {
|
|
158
|
+
const tree = {
|
|
159
|
+
type: 'root',
|
|
160
|
+
children: texts.map((value) => ({
|
|
161
|
+
type: 'element',
|
|
162
|
+
tagName: 'h2',
|
|
163
|
+
properties: {},
|
|
164
|
+
children: [{ type: 'text', value }],
|
|
165
|
+
})),
|
|
166
|
+
};
|
|
167
|
+
rehypeHeadingIds()(tree, { history: ['headings.mdx'], data: {} });
|
|
168
|
+
return tree.children.map((node) => String(node.properties.id));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Every top-level heading of a lesson body in document order, as the
|
|
173
|
+
* manifest records it: `{ level, text, slug, number? }`. `prefix` is the
|
|
174
|
+
* chapter's `‹part›.‹chapter›`; without one nothing is numbered.
|
|
175
|
+
*/
|
|
176
|
+
export function scanSectionHeadings(markdown, options = {}) {
|
|
177
|
+
const found = [];
|
|
178
|
+
for (const match of outlineSource(markdown).matchAll(ATX_HEADING)) {
|
|
179
|
+
found.push({
|
|
180
|
+
level: match[1].length,
|
|
181
|
+
text: headingPlainText(match[2] ?? ''),
|
|
182
|
+
meta: headingCardTier(match[2] ?? '') === 'meta',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const slugs = slugHeadings(found.map((heading) => heading.text));
|
|
186
|
+
const numbers = sectionNumbers(
|
|
187
|
+
found.map((heading) => heading.level),
|
|
188
|
+
{ ...options, unnumbered: found.map((heading) => heading.meta) },
|
|
189
|
+
);
|
|
190
|
+
return found.map((heading, index) => ({
|
|
191
|
+
level: heading.level,
|
|
192
|
+
text: heading.text,
|
|
193
|
+
slug: slugs[index],
|
|
194
|
+
...(numbers[index] === undefined ? {} : { number: numbers[index] }),
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const ELEMENT_START = /^[ \t]*<([A-Za-z][\w.:-]*)/;
|
|
199
|
+
const ESM_LINE = /^(?:import|export)\s/;
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The top-level blocks of a lesson body in document order, as the heading
|
|
203
|
+
* rules see them (S6, S7 — `gate-heading-structure.ts`): each outline heading
|
|
204
|
+
* `scanSectionHeadings` numbers, each JSX element by name, and every other
|
|
205
|
+
* line of content. An MDX comment and an `import` / `export` line render
|
|
206
|
+
* nothing, so they are not blocks. `index` is the offset in `markdown`; a
|
|
207
|
+
* heading's `text` is what the manifest records for it.
|
|
208
|
+
*
|
|
209
|
+
* @returns {Array<
|
|
210
|
+
* | { kind: 'heading', level: number, text: string, index: number }
|
|
211
|
+
* | { kind: 'element', name: string, index: number }
|
|
212
|
+
* | { kind: 'content', index: number }
|
|
213
|
+
* >}
|
|
214
|
+
*/
|
|
215
|
+
export function scanOutlineBlocks(markdown) {
|
|
216
|
+
// Same length, same lines: headings come from the form `scanSectionHeadings`
|
|
217
|
+
// reads, everything else from the one that keeps its markers.
|
|
218
|
+
const plain = outlineSource(markdown).split('\n');
|
|
219
|
+
const marked = outlineSource(markdown, { markers: true }).split('\n');
|
|
220
|
+
const heading = new RegExp(ATX_HEADING.source);
|
|
221
|
+
const blocks = [];
|
|
222
|
+
let index = 0;
|
|
223
|
+
marked.forEach((line, row) => {
|
|
224
|
+
const at = index;
|
|
225
|
+
index += line.length + 1;
|
|
226
|
+
const match = heading.exec(plain[row]);
|
|
227
|
+
if (match) {
|
|
228
|
+
blocks.push({
|
|
229
|
+
kind: 'heading',
|
|
230
|
+
level: match[1].length,
|
|
231
|
+
text: headingPlainText(match[2] ?? ''),
|
|
232
|
+
index: at,
|
|
233
|
+
});
|
|
234
|
+
} else if (line.trim() !== '' && !ESM_LINE.test(line)) {
|
|
235
|
+
const element = ELEMENT_START.exec(line);
|
|
236
|
+
blocks.push(element ? { kind: 'element', name: element[1], index: at } : { kind: 'content', index: at });
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
return blocks;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const OPENING_TAG = new RegExp(String.raw`^[ \t]*<[A-Za-z][\w.:-]*(${ATTRIBUTES})\/?>`);
|
|
243
|
+
const ID_ATTRIBUTE = /(?:^|\s)id\s*=\s*(?:"([^"]*)"|'([^']*)')/;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The top-level blocks named in `names` — components that draw a heading of
|
|
247
|
+
* their own where their tag stands, a knowledge check's `<AssessmentSet />`
|
|
248
|
+
* and a worked-example set's `<CompactExamples>` — in document order: each
|
|
249
|
+
* with its tag `name`, the literal `id` its tag sets (if any), and `heading`,
|
|
250
|
+
* the position in `scanSectionHeadings`' list of the outline heading it
|
|
251
|
+
* follows (`-1` before the first). The manifest reads this to place the
|
|
252
|
+
* block's heading in "On this page".
|
|
253
|
+
*
|
|
254
|
+
* @returns {Array<{ name: string, heading: number, id?: string }>}
|
|
255
|
+
*/
|
|
256
|
+
export function scanTitledBlocks(markdown, names) {
|
|
257
|
+
const wanted = new Set(names);
|
|
258
|
+
const source = String(markdown);
|
|
259
|
+
const blocks = [];
|
|
260
|
+
let heading = -1;
|
|
261
|
+
for (const block of scanOutlineBlocks(source)) {
|
|
262
|
+
if (block.kind === 'heading') {
|
|
263
|
+
heading += 1;
|
|
264
|
+
} else if (block.kind === 'element' && wanted.has(block.name)) {
|
|
265
|
+
const attributes = OPENING_TAG.exec(source.slice(block.index))?.[1] ?? '';
|
|
266
|
+
const id = ID_ATTRIBUTE.exec(attributes);
|
|
267
|
+
blocks.push({
|
|
268
|
+
name: block.name,
|
|
269
|
+
heading,
|
|
270
|
+
...(id ? { id: id[1] ?? id[2] } : {}),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return blocks;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// --- mdast -------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
/** A node's literal text, markers included (`[card=meta]` is plain text here). */
|
|
280
|
+
function mdastText(node) {
|
|
281
|
+
if (typeof node?.value === 'string') return node.value;
|
|
282
|
+
return Array.isArray(node?.children) ? node.children.map(mdastText).join('') : '';
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The mdast counterpart of `scanSectionHeadings`: the lesson root's own
|
|
287
|
+
* `heading` children, with the number each takes. Slugs are not computed here
|
|
288
|
+
* — the render path takes them from the manifest.
|
|
289
|
+
*/
|
|
290
|
+
export function collectSectionHeadings(tree, options = {}) {
|
|
291
|
+
const nodes = (Array.isArray(tree?.children) ? tree.children : []).filter(
|
|
292
|
+
(node) => node?.type === 'heading',
|
|
293
|
+
);
|
|
294
|
+
const numbers = sectionNumbers(
|
|
295
|
+
nodes.map((node) => node.depth),
|
|
296
|
+
{
|
|
297
|
+
...options,
|
|
298
|
+
unnumbered: nodes.map((node) => headingCardTier(mdastText(node)) === 'meta'),
|
|
299
|
+
},
|
|
300
|
+
);
|
|
301
|
+
return nodes.map((node, index) => ({
|
|
302
|
+
node,
|
|
303
|
+
level: node.depth,
|
|
304
|
+
...(numbers[index] === undefined ? {} : { number: numbers[index] }),
|
|
305
|
+
}));
|
|
306
|
+
}
|
package/src/reference/types.ts
CHANGED
|
@@ -80,10 +80,27 @@ export interface LocalNotationDefinitionInput {
|
|
|
80
80
|
readonly source: SourceSpan;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* One `notation.suspend` item: a decoration (`'`, `\hat`, …) this page says
|
|
85
|
+
* is not an operator (rule N22). Every decorated form of it on the page then
|
|
86
|
+
* needs its own entry.
|
|
87
|
+
*/
|
|
88
|
+
export interface NotationSuspension {
|
|
89
|
+
/** A canonical name from `DECORATION_NAMES` (`math-glyphs.mjs`). */
|
|
90
|
+
readonly operator: string;
|
|
91
|
+
/** Why; a suspension without one is an `operator-suspension-reason` warning. */
|
|
92
|
+
readonly reason?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
83
95
|
export interface NotationLessonInput {
|
|
84
96
|
readonly lessonId: string;
|
|
85
97
|
readonly status: EditorialStatus;
|
|
86
98
|
readonly localDefinitions: readonly LocalNotationDefinitionInput[];
|
|
99
|
+
/**
|
|
100
|
+
* Decorations this page suspends (`notation.suspend`, rule N22). Optional
|
|
101
|
+
* so a hand-built input (a test, an adapter) stays valid.
|
|
102
|
+
*/
|
|
103
|
+
readonly suspended?: readonly NotationSuspension[];
|
|
87
104
|
/** References harvested from lesson prose and equations, excluding definitions. */
|
|
88
105
|
readonly references: readonly NotationReferenceInput[];
|
|
89
106
|
/** Original lesson body, retained for lossless loader adapters. */
|