explico 0.7.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 +1409 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +54 -3
- package/dist/compiler/cli.js +80 -6
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +84 -8
- 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 +14 -1
- package/dist/compiler/logging.js +6 -2
- 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 +21 -11
- package/dist/reference/delimiter-size.js +15 -39
- 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 +69 -34
- package/dist/reference/equations.mjs +90 -44
- 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 +3 -2
- 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 +47 -23
- 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 +44 -6
- package/dist/reference/gate-math.js +63 -10
- 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 +32 -11
- package/dist/reference/gate-prose-notation.js +30 -8
- package/dist/reference/katex-delimiters.d.mts +15 -0
- package/dist/reference/katex-delimiters.mjs +138 -0
- package/dist/reference/katex-options.mjs +4 -0
- 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 +588 -39
- 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 +5 -5
- 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 +24 -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 +1324 -2
- package/docs/authoring/notation.md +616 -344
- 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/docs/katex-bracket-scaling.md +191 -0
- package/package.json +3 -2
- package/src/compiler/cli.ts +139 -6
- package/src/compiler/collections.ts +144 -10
- 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 +21 -4
- 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 +24 -14
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/notation/render-notation-math.ts +111 -10
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +22 -6
- 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 +40 -61
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +90 -44
- 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 +3 -2
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +68 -37
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +119 -12
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +74 -13
- package/src/reference/katex-delimiters.mjs +138 -0
- package/src/reference/katex-options.mjs +4 -0
- 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 +588 -39
- 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 +17 -32
- 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 +24 -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,32 +1,10 @@
|
|
|
1
|
+
import { RenderedLabMath } from './lab-math-slot.js';
|
|
2
|
+
export { slotPlaceholder } from './lab-math-slot.js';
|
|
3
|
+
|
|
1
4
|
interface LabMathScopeDefinition {
|
|
2
5
|
readonly key: string;
|
|
3
6
|
readonly notation: string;
|
|
4
7
|
}
|
|
5
|
-
interface RenderedLabMath {
|
|
6
|
-
/** KaTeX HTML+MathML with `data-notation-key` and `data-lab-slot` spans. */
|
|
7
|
-
readonly html: string;
|
|
8
|
-
/** Slot names, in template order, the island must supply at runtime. */
|
|
9
|
-
readonly slots: readonly string[];
|
|
10
|
-
/** Notation keys the template resolved to, for diagnostics. */
|
|
11
|
-
readonly keys: readonly string[];
|
|
12
|
-
/**
|
|
13
|
-
* Plain, copyable LaTeX — every explained glyph resolved to its bare
|
|
14
|
-
* symbol, every `\slot{name}{…}` replaced by `slotPlaceholder(name)` — so
|
|
15
|
-
* the island can substitute each slot's *current* live value in and keep
|
|
16
|
-
* `[data-notation-formula]`'s `data-latex` accurate as the reader drags a
|
|
17
|
-
* control, the same "click the equation to copy it" gesture a static
|
|
18
|
-
* lesson or glossary equation gets (`NotationLayer.astro`).
|
|
19
|
-
*/
|
|
20
|
-
readonly plainLatexTemplate: string;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* A slot's placeholder inside `plainLatexTemplate` — distinct from the
|
|
24
|
-
* numeric sentinel `sentinelFor` below uses internally (that one only has to
|
|
25
|
-
* survive a single notation-binding pass at build time; this one is the
|
|
26
|
-
* stable, name-keyed token an island substitutes with a live value at
|
|
27
|
-
* runtime, so it's exported for that purpose).
|
|
28
|
-
*/
|
|
29
|
-
declare function slotPlaceholder(name: string): string;
|
|
30
8
|
/**
|
|
31
9
|
* Compile a developer-authored lab math template the same way lesson `$$`
|
|
32
10
|
* math is compiled: every identifier must resolve to a notation key in the
|
|
@@ -35,4 +13,4 @@ declare function slotPlaceholder(name: string): string;
|
|
|
35
13
|
*/
|
|
36
14
|
declare function renderLabMath(template: string, scope: readonly LabMathScopeDefinition[]): RenderedLabMath;
|
|
37
15
|
|
|
38
|
-
export { type LabMathScopeDefinition,
|
|
16
|
+
export { type LabMathScopeDefinition, RenderedLabMath, renderLabMath };
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import katex from "katex";
|
|
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
|
-
|
|
5
|
-
return `\u27E6${name}\u27E7`;
|
|
6
|
-
}
|
|
4
|
+
import { slotPlaceholder } from "./lab-math-slot.js";
|
|
7
5
|
const SLOT_PATTERN = /\\slot\{([a-z][a-z0-9]*)\}\{((?:[^{}]|\{[^{}]*\})*)\}/g;
|
|
8
6
|
const sentinelFor = (index) => `909909${index}.909909`;
|
|
9
7
|
function renderLabMath(template, scope) {
|
|
@@ -27,8 +25,10 @@ function renderLabMath(template, scope) {
|
|
|
27
25
|
}
|
|
28
26
|
if (result.unresolved.length > 0) {
|
|
29
27
|
const list = result.unresolved.map(({ token }) => `"${token}"`).join(", ");
|
|
28
|
+
const styled = result.unresolved.filter(({ font }) => font !== void 0);
|
|
29
|
+
const fontHint = styled.length === 0 ? "" : ` ${styled.map(({ token }) => token).join(", ")} ${styled.length === 1 ? "is a different symbol" : "are different symbols"} from the plain letter and need${styled.length === 1 ? "s" : ""} an entry of ${styled.length === 1 ? "its" : "their"} own.`;
|
|
30
30
|
throw new Error(
|
|
31
|
-
`Lab math uses undefined notation ${list}. Every symbol must resolve to a notation entry in this page's glyph scope (a [[key]] reference or notation.local)
|
|
31
|
+
`Lab math uses undefined notation ${list}. Every symbol must resolve to a notation entry in this page's glyph scope (a [[key]] reference or notation.local).${fontHint}
|
|
32
32
|
template: ${template.trim()}`
|
|
33
33
|
);
|
|
34
34
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MathGateDiagnostic } from './gate-math.js';
|
|
2
|
+
import './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `[[sec-…]]` references (docs/plans/titles-and-containers.md Part 1): every
|
|
6
|
+
* `[[sec-slug]]` and `[[lesson/slug#sec-slug]]` must name a top-level heading
|
|
7
|
+
* of that lesson — `sec-ref-resolves`, an error owned by the `keyed-floats`
|
|
8
|
+
* invariant, the same promise `[[eq-…]]` and `[[fig-…]]` make.
|
|
9
|
+
*
|
|
10
|
+
* The finding goes into `manifest.diagnostics.math` with context `structure`,
|
|
11
|
+
* like S1 and S2 (see `MathGateDiagnostic`), so every reporter that already
|
|
12
|
+
* reads that array fails the build on it without learning a new one.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
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
|
+
/** One `sec-ref-resolves` error per `[[sec-…]]` that names no heading. */
|
|
25
|
+
declare function validateSectionRefs(lessons: readonly SectionRefLessonInput[]): MathGateDiagnostic[];
|
|
26
|
+
|
|
27
|
+
export { type SectionRefLessonInput, validateSectionRefs };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { lessonLine } from "./gate-prose-notation.js";
|
|
2
|
+
import { parseSectionRef } from "./sections.mjs";
|
|
3
|
+
const BRACKET_REF = /\[\[\s*([^\][]+?)\s*\]\]/g;
|
|
4
|
+
function withoutCode(text) {
|
|
5
|
+
const blank = (block) => block.replace(/[^\n]/g, " ");
|
|
6
|
+
return text.replace(/```[\s\S]*?```/g, blank).replace(/~~~[\s\S]*?~~~/g, blank).replace(/`[^`\n]*`/g, blank);
|
|
7
|
+
}
|
|
8
|
+
function unresolved(ref, lessonExists) {
|
|
9
|
+
const written = ref.lesson ? `[[${ref.lesson}#sec-${ref.key}]]` : `[[sec-${ref.key}]]`;
|
|
10
|
+
if (ref.lesson && !lessonExists) {
|
|
11
|
+
return `${written} names lesson ${ref.lesson}, which does not exist.`;
|
|
12
|
+
}
|
|
13
|
+
return `${written} names no heading: ${ref.lesson ? `lesson ${ref.lesson}` : "this lesson"} has no top-level heading anchored #${ref.key}. A heading's anchor is its text in lowercase with spaces as hyphens and punctuation dropped: \`## Two ways to name the same point\` is \`[[sec-two-ways-to-name-the-same-point]]\`.`;
|
|
14
|
+
}
|
|
15
|
+
function validateSectionRefs(lessons) {
|
|
16
|
+
const headingsByLesson = new Map(
|
|
17
|
+
lessons.map((lesson) => [lesson.slug, new Set(lesson.headingSlugs)])
|
|
18
|
+
);
|
|
19
|
+
const diagnostics = [];
|
|
20
|
+
for (const lesson of lessons) {
|
|
21
|
+
for (const match of withoutCode(lesson.body).matchAll(BRACKET_REF)) {
|
|
22
|
+
const ref = parseSectionRef(match[1] ?? "");
|
|
23
|
+
if (!ref) continue;
|
|
24
|
+
const headings = headingsByLesson.get(ref.lesson ?? lesson.slug);
|
|
25
|
+
if (headings?.has(ref.key)) continue;
|
|
26
|
+
diagnostics.push({
|
|
27
|
+
context: "structure",
|
|
28
|
+
code: "sec-ref-resolves",
|
|
29
|
+
severity: "error",
|
|
30
|
+
file: lesson.file,
|
|
31
|
+
line: lessonLine(lesson, match.index ?? 0),
|
|
32
|
+
token: match[0],
|
|
33
|
+
message: unresolved(ref, headings !== void 0)
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return diagnostics;
|
|
38
|
+
}
|
|
39
|
+
export {
|
|
40
|
+
validateSectionRefs
|
|
41
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export function parseSectionRef(raw: any): {
|
|
2
|
+
lesson: string;
|
|
3
|
+
key: string;
|
|
4
|
+
} | {
|
|
5
|
+
key: string;
|
|
6
|
+
lesson?: undefined;
|
|
7
|
+
} | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* The number each heading takes, from its level alone: `##` counts within the
|
|
10
|
+
* chapter, `###` within its `##`, and so on, each deeper counter restarting
|
|
11
|
+
* when a shallower heading appears. A level a heading skipped counts as 0
|
|
12
|
+
* (`##` then `####` is `…1.0.1`), the way LaTeX numbers a skipped level. An
|
|
13
|
+
* `#` in a lesson body is never numbered and resets nothing. Headings deeper
|
|
14
|
+
* than `depth`, or any heading when there is no `prefix`, get `undefined`.
|
|
15
|
+
* `unnumbered[i]` marks heading `i` as outside the outline (a meta section):
|
|
16
|
+
* it and the deeper headings under it get `undefined` and move no counter.
|
|
17
|
+
*/
|
|
18
|
+
export function sectionNumbers(levels: any, { prefix, depth, unnumbered }?: {
|
|
19
|
+
depth?: number | undefined;
|
|
20
|
+
unnumbered?: never[] | undefined;
|
|
21
|
+
}): any;
|
|
22
|
+
/**
|
|
23
|
+
* The text a heading's source renders as, near enough to slug it the way
|
|
24
|
+
* Astro would: markup goes, the words stay. Inline code and math keep their
|
|
25
|
+
* source (math renders as KaTeX, whose text is no use as a slug); a notation
|
|
26
|
+
* term keeps its key; a link keeps its text; an image, a citation and an MDX
|
|
27
|
+
* expression contribute nothing; the authoring marker is dropped.
|
|
28
|
+
*/
|
|
29
|
+
export function headingPlainText(source: any): string;
|
|
30
|
+
/**
|
|
31
|
+
* Anchors for a page's headings, in order, from Astro's own `rehypeHeadingIds`
|
|
32
|
+
* run over their plain text: the same slugger with the same duplicate
|
|
33
|
+
* counters (`foo`, `foo-1`) the page would otherwise get.
|
|
34
|
+
*/
|
|
35
|
+
export function slugHeadings(texts: any): any;
|
|
36
|
+
/**
|
|
37
|
+
* Every top-level heading of a lesson body in document order, as the
|
|
38
|
+
* manifest records it: `{ level, text, slug, number? }`. `prefix` is the
|
|
39
|
+
* chapter's `‹part›.‹chapter›`; without one nothing is numbered.
|
|
40
|
+
*/
|
|
41
|
+
export function scanSectionHeadings(markdown: any, options?: {}): {
|
|
42
|
+
number?: any;
|
|
43
|
+
level: number;
|
|
44
|
+
text: string;
|
|
45
|
+
slug: any;
|
|
46
|
+
}[];
|
|
47
|
+
/**
|
|
48
|
+
* The top-level blocks of a lesson body in document order, as the heading
|
|
49
|
+
* rules see them (S6, S7 — `gate-heading-structure.ts`): each outline heading
|
|
50
|
+
* `scanSectionHeadings` numbers, each JSX element by name, and every other
|
|
51
|
+
* line of content. An MDX comment and an `import` / `export` line render
|
|
52
|
+
* nothing, so they are not blocks. `index` is the offset in `markdown`; a
|
|
53
|
+
* heading's `text` is what the manifest records for it.
|
|
54
|
+
*
|
|
55
|
+
* @returns {Array<
|
|
56
|
+
* | { kind: 'heading', level: number, text: string, index: number }
|
|
57
|
+
* | { kind: 'element', name: string, index: number }
|
|
58
|
+
* | { kind: 'content', index: number }
|
|
59
|
+
* >}
|
|
60
|
+
*/
|
|
61
|
+
export function scanOutlineBlocks(markdown: any): Array<{
|
|
62
|
+
kind: "heading";
|
|
63
|
+
level: number;
|
|
64
|
+
text: string;
|
|
65
|
+
index: number;
|
|
66
|
+
} | {
|
|
67
|
+
kind: "element";
|
|
68
|
+
name: string;
|
|
69
|
+
index: number;
|
|
70
|
+
} | {
|
|
71
|
+
kind: "content";
|
|
72
|
+
index: number;
|
|
73
|
+
}>;
|
|
74
|
+
/**
|
|
75
|
+
* The top-level blocks named in `names` — components that draw a heading of
|
|
76
|
+
* their own where their tag stands, a knowledge check's `<AssessmentSet />`
|
|
77
|
+
* and a worked-example set's `<CompactExamples>` — in document order: each
|
|
78
|
+
* with its tag `name`, the literal `id` its tag sets (if any), and `heading`,
|
|
79
|
+
* the position in `scanSectionHeadings`' list of the outline heading it
|
|
80
|
+
* follows (`-1` before the first). The manifest reads this to place the
|
|
81
|
+
* block's heading in "On this page".
|
|
82
|
+
*
|
|
83
|
+
* @returns {Array<{ name: string, heading: number, id?: string }>}
|
|
84
|
+
*/
|
|
85
|
+
export function scanTitledBlocks(markdown: any, names: any): Array<{
|
|
86
|
+
name: string;
|
|
87
|
+
heading: number;
|
|
88
|
+
id?: string;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* The mdast counterpart of `scanSectionHeadings`: the lesson root's own
|
|
92
|
+
* `heading` children, with the number each takes. Slugs are not computed here
|
|
93
|
+
* — the render path takes them from the manifest.
|
|
94
|
+
*/
|
|
95
|
+
export function collectSectionHeadings(tree: any, options?: {}): any;
|
|
96
|
+
/** The deepest heading level numbered when a course sets nothing (`####`). */
|
|
97
|
+
export const DEFAULT_SECTION_DEPTH: 4;
|
|
@@ -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
|
+
}
|
|
@@ -2,7 +2,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
|
|
|
2
2
|
|
|
3
3
|
/** `validateKeyedFloats` for `<Table>` — see `keyed-float-validate.ts`. */
|
|
4
4
|
|
|
5
|
-
type TableDiagnosticCode = 'tab-invalid-key' | 'tab-duplicate-key' | 'tab-ref-resolves' | 'tab-key-unused';
|
|
5
|
+
type TableDiagnosticCode = 'tab-invalid-key' | 'tab-duplicate-key' | 'tab-ref-resolves' | 'tab-key-unused' | 'tab-removed-prop';
|
|
6
6
|
interface TableDiagnostic {
|
|
7
7
|
readonly code: TableDiagnosticCode;
|
|
8
8
|
readonly severity: 'error' | 'warning' | 'info';
|
|
@@ -70,14 +70,37 @@ interface LocalNotationDefinitionInput {
|
|
|
70
70
|
readonly glosses: readonly NotationGlossEntry[];
|
|
71
71
|
readonly source: SourceSpan;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* One `notation.suspend` item: a decoration (`'`, `\hat`, …) this page says
|
|
75
|
+
* is not an operator (rule N22). Every decorated form of it on the page then
|
|
76
|
+
* needs its own entry.
|
|
77
|
+
*/
|
|
78
|
+
interface NotationSuspension {
|
|
79
|
+
/** A canonical name from `DECORATION_NAMES` (`math-glyphs.mjs`). */
|
|
80
|
+
readonly operator: string;
|
|
81
|
+
/** Why; a suspension without one is an `operator-suspension-reason` warning. */
|
|
82
|
+
readonly reason?: string;
|
|
83
|
+
}
|
|
73
84
|
interface NotationLessonInput {
|
|
74
85
|
readonly lessonId: string;
|
|
75
86
|
readonly status: EditorialStatus;
|
|
76
87
|
readonly localDefinitions: readonly LocalNotationDefinitionInput[];
|
|
88
|
+
/**
|
|
89
|
+
* Decorations this page suspends (`notation.suspend`, rule N22). Optional
|
|
90
|
+
* so a hand-built input (a test, an adapter) stays valid.
|
|
91
|
+
*/
|
|
92
|
+
readonly suspended?: readonly NotationSuspension[];
|
|
77
93
|
/** References harvested from lesson prose and equations, excluding definitions. */
|
|
78
94
|
readonly references: readonly NotationReferenceInput[];
|
|
79
95
|
/** Original lesson body, retained for lossless loader adapters. */
|
|
80
96
|
readonly body: string;
|
|
97
|
+
/**
|
|
98
|
+
* 1-based line in `source.file` where `body` starts. Gates scan the body, so
|
|
99
|
+
* every finding's index is body-relative; this is what turns one into a real
|
|
100
|
+
* location in the file. Optional so a hand-built input (a test, an adapter)
|
|
101
|
+
* stays valid — it then behaves as 1, the pre-0.7.1 behavior.
|
|
102
|
+
*/
|
|
103
|
+
readonly bodyLine?: number;
|
|
81
104
|
readonly source: SourceSpan;
|
|
82
105
|
}
|
|
83
106
|
interface NotationRegistryInput {
|
|
@@ -186,4 +209,4 @@ interface NotationRegistry {
|
|
|
186
209
|
readonly diagnostics: readonly NotationDiagnostic[];
|
|
187
210
|
}
|
|
188
211
|
|
|
189
|
-
export type { AbbreviationDefinitionInput, DiagnosticSeverity, EditorialStatus, LocalNotationDefinitionInput, LocalNotationDefinitionRecord, NotationAlignment, NotationBacklink, NotationBinding, NotationDefinitionRecord, NotationDefinitionScope, NotationDiagnostic, NotationDiagnosticCode, NotationGlossEntry, NotationLessonInput, NotationPageBundle, NotationReferenceInput, NotationReferenceKind, NotationRegistry, NotationRegistryInput, ResolvedNotationReference, SharedNotationDefinitionInput, SharedNotationDefinitionRecord, SourceSpan };
|
|
212
|
+
export type { AbbreviationDefinitionInput, DiagnosticSeverity, EditorialStatus, LocalNotationDefinitionInput, LocalNotationDefinitionRecord, NotationAlignment, NotationBacklink, NotationBinding, NotationDefinitionRecord, NotationDefinitionScope, NotationDiagnostic, NotationDiagnosticCode, NotationGlossEntry, NotationLessonInput, NotationPageBundle, NotationReferenceInput, NotationReferenceKind, NotationRegistry, NotationRegistryInput, NotationSuspension, ResolvedNotationReference, SharedNotationDefinitionInput, SharedNotationDefinitionRecord, SourceSpan };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Units written out as English words — "2 meters per second squared",
|
|
3
|
+
* "30 degrees", "$x = 3$ meters" — for rule N9's `spelled-unit` check
|
|
4
|
+
* (`gate-prose-notation.ts`). A unit is an upright symbol inside `$…$`
|
|
5
|
+
* (`$2\ \text{m/s}^2$`, `$30^{\circ}$`, `$x = 3\ \text{m}$`), so a spelled-out
|
|
6
|
+
* unit that quantifies something is the same mistake as `45 m/s` in plain
|
|
7
|
+
* text.
|
|
8
|
+
*
|
|
9
|
+
* A unit word on its own is not flagged — "measured in meters" names the
|
|
10
|
+
* unit, it does not state a quantity. What is flagged is a unit word
|
|
11
|
+
*
|
|
12
|
+
* - after a number, written in digits or as a word, in prose or typeset
|
|
13
|
+
* (`2 meters`, `two-meter`, `$x = 3$ meters`);
|
|
14
|
+
* - after `per`, or before it (`rotations per second`, `meters per unit`);
|
|
15
|
+
* - with `square` / `cubic` before it or `squared` / `cubed` after it.
|
|
16
|
+
*
|
|
17
|
+
* "Degrees of freedom" is not a unit. Calendar time (day, week, month, year)
|
|
18
|
+
* and currency are left out on purpose: they are ordinary words in a finance
|
|
19
|
+
* course's prose, not SI units. `notation` `units:` strings are a different
|
|
20
|
+
* vocabulary (`units-vocab.ts`, rule N19) and are never scanned here.
|
|
21
|
+
*/
|
|
22
|
+
interface SpelledUnit {
|
|
23
|
+
readonly index: number;
|
|
24
|
+
readonly token: string;
|
|
25
|
+
/** What to write instead, as `$…$`. */
|
|
26
|
+
readonly suggestion: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Every spelled-out unit in a lesson or assessment text. `prose` has code,
|
|
30
|
+
* math, references, JSX tags and comments blanked (`proseOnly`); `withMath`
|
|
31
|
+
* is the same text with math left in, for the `$x = 3$ meters` shape. Both
|
|
32
|
+
* must share offsets. A phrase whose unit words are all in `allow`
|
|
33
|
+
* (`rules.N9.allow`, singular or plural) is skipped.
|
|
34
|
+
*/
|
|
35
|
+
declare function findSpelledUnits(prose: string, withMath: string, allow?: readonly string[]): SpelledUnit[];
|
|
36
|
+
|
|
37
|
+
export { type SpelledUnit, findSpelledUnits };
|