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
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
stripNotationMarkers,
|
|
40
40
|
} from './math-glyphs.mjs';
|
|
41
41
|
import {
|
|
42
|
+
EQ_CAPTION_PATTERN,
|
|
42
43
|
EQ_LABEL_PATTERN,
|
|
43
44
|
collectDisplayEquations,
|
|
44
45
|
collectEquationLabels,
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
parseKnowledgeCheckRef,
|
|
58
59
|
} from './knowledge-checks.mjs';
|
|
59
60
|
import { injectMdxStringAttributes } from './mdx-attributes.mjs';
|
|
61
|
+
import { collectSectionHeadings, parseSectionRef } from './sections.mjs';
|
|
60
62
|
|
|
61
63
|
const PROSE_SKIP_TYPES = new Set([
|
|
62
64
|
'code',
|
|
@@ -128,6 +130,23 @@ function localDefinitionsFromFile(file) {
|
|
|
128
130
|
return definitionsMap(candidates.find(Array.isArray));
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
/**
|
|
134
|
+
* The decorations a lesson suspends (`notation.suspend`, rule N22), by name.
|
|
135
|
+
* Read here as well as in the gate because `astro dev` renders without
|
|
136
|
+
* running the gate. The loader (`collections.ts`) validates the names.
|
|
137
|
+
*/
|
|
138
|
+
function suspendedOperatorsFromFile(file) {
|
|
139
|
+
const candidates = [
|
|
140
|
+
file?.data?.astro?.frontmatter?.notation?.suspend,
|
|
141
|
+
file?.data?.frontmatter?.notation?.suspend,
|
|
142
|
+
file?.data?.notation?.suspend,
|
|
143
|
+
];
|
|
144
|
+
const suspend = candidates.find(Array.isArray) ?? [];
|
|
145
|
+
return suspend
|
|
146
|
+
.map((item) => (typeof item?.operator === 'string' ? item.operator.trim() : ''))
|
|
147
|
+
.filter((operator) => operator !== '');
|
|
148
|
+
}
|
|
149
|
+
|
|
131
150
|
// Registry definition pages (`src/content/notation/*.md`) carry their own
|
|
132
151
|
// worked equations. Their math is resolved against the whole shared registry:
|
|
133
152
|
// an author writes ordinary LaTeX (or an explicit `\explain{key}{latex}`) and
|
|
@@ -367,7 +386,8 @@ function inlineEquationNode(mathNode) {
|
|
|
367
386
|
* at its positional `#eq-<section>-<index>`. The visible number is a focusable
|
|
368
387
|
* `<a>` sibling — never authored, never inside KaTeX — so it survives print and
|
|
369
388
|
* screen readers name it "Equation 2.4". A "Copy LaTeX" button sits beside it
|
|
370
|
-
* (`.keyed-equation__controls`, styled in `global.css
|
|
389
|
+
* (`.keyed-equation__controls`, styled in `global.css`; titled "Copy LaTeX —
|
|
390
|
+
* ‹short›" when the equation has a `\caption{}`) — the equation's own
|
|
371
391
|
* copy affordance, distinct from the notation popup card's (`NotationLayer.astro`),
|
|
372
392
|
* which only exists for a symbol with a registry entry, not a bare display
|
|
373
393
|
* equation. `mathNode.value` is read here — after the resolver above has
|
|
@@ -376,7 +396,7 @@ function inlineEquationNode(mathNode) {
|
|
|
376
396
|
* author-written one; either way the reader copies plain LaTeX, never this
|
|
377
397
|
* system's own notation-key plumbing (`math-glyphs.mjs:stripNotationMarkers`).
|
|
378
398
|
*/
|
|
379
|
-
function equationNode(mathNode, { key, number }) {
|
|
399
|
+
function equationNode(mathNode, { key, number, short }) {
|
|
380
400
|
const id = equationAnchorId({ key, number });
|
|
381
401
|
const hProperties = {
|
|
382
402
|
id,
|
|
@@ -385,6 +405,9 @@ function equationNode(mathNode, { key, number }) {
|
|
|
385
405
|
};
|
|
386
406
|
if (key) hProperties['data-eq-key'] = key;
|
|
387
407
|
else hProperties['data-eq-auto'] = '';
|
|
408
|
+
// The `\caption{}` short caption names the equation wherever it is shown
|
|
409
|
+
// away from itself: the `[[eq-…]]` preview card and the copy tooltip.
|
|
410
|
+
if (short) hProperties['data-eq-short'] = short;
|
|
388
411
|
|
|
389
412
|
const latex = stripNotationMarkers(String(mathNode.value ?? ''));
|
|
390
413
|
|
|
@@ -416,7 +439,7 @@ function equationNode(mathNode, { key, number }) {
|
|
|
416
439
|
'data-eq-copy': '',
|
|
417
440
|
'data-latex': latex,
|
|
418
441
|
'aria-label': `Copy LaTeX for Equation ${number}`,
|
|
419
|
-
title: 'Copy LaTeX',
|
|
442
|
+
title: short ? `Copy LaTeX — ${short}` : 'Copy LaTeX',
|
|
420
443
|
},
|
|
421
444
|
[copyIconNode()],
|
|
422
445
|
),
|
|
@@ -502,6 +525,47 @@ function resolveKeyedFloatRef(floatRef, ctx, { prefix, tagName }, file, node) {
|
|
|
502
525
|
return { number, href: `#${prefix}-${floatRef.key}` };
|
|
503
526
|
}
|
|
504
527
|
|
|
528
|
+
/**
|
|
529
|
+
* Resolve a `[[sec-slug]]` / `[[lesson/slug#sec-slug]]` reference to the
|
|
530
|
+
* heading's `{ href, text }`: `§ 1.1.2` for a numbered section when the
|
|
531
|
+
* course shows section numbers, else the heading's own title in quotes — a
|
|
532
|
+
* reference never prints a number the reader cannot find on the page.
|
|
533
|
+
*/
|
|
534
|
+
function resolveSectionRef(secRef, ctx, file, node) {
|
|
535
|
+
const heading = secRef.lesson
|
|
536
|
+
? ctx.crossPage?.[secRef.lesson]?.[secRef.key]
|
|
537
|
+
: ctx.pageHeadings.get(secRef.key);
|
|
538
|
+
if (heading === undefined) {
|
|
539
|
+
fail(
|
|
540
|
+
file,
|
|
541
|
+
secRef.lesson
|
|
542
|
+
? `Unknown section reference [[${secRef.lesson}#sec-${secRef.key}]]: lesson ${secRef.lesson} has no top-level heading anchored #${secRef.key}.`
|
|
543
|
+
: `Unknown section reference [[sec-${secRef.key}]]: no top-level heading on this page is anchored #${secRef.key}.`,
|
|
544
|
+
node,
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
href: secRef.lesson
|
|
549
|
+
? `${ctx.base.replace(/\/$/, '')}/${secRef.lesson}/#${secRef.key}`
|
|
550
|
+
: `#${secRef.key}`,
|
|
551
|
+
text:
|
|
552
|
+
ctx.show && heading.number
|
|
553
|
+
? `§ ${heading.number}`
|
|
554
|
+
: `“${heading.text}”`,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* How a reference names a knowledge check or a worked-example set: `‹Kind›
|
|
560
|
+
* 1.1.1` when the course shows section numbers — those blocks' numbers follow
|
|
561
|
+
* the sections' (docs/plans/titles-and-containers.md Q11) — else `‹Kind›
|
|
562
|
+
* “‹title›”`, so a reference never prints a number the page does not show.
|
|
563
|
+
* The number is the fallback when the manifest carries no title.
|
|
564
|
+
*/
|
|
565
|
+
function blockRefText(kind, number, title, show) {
|
|
566
|
+
return show || !title ? `${kind} ${number}` : `${kind} “${title}”`;
|
|
567
|
+
}
|
|
568
|
+
|
|
505
569
|
/** Resolve a numbered knowledge check declared by a lesson's `.checks.yml`. */
|
|
506
570
|
function resolveKnowledgeCheckRef(checkRef, ctx, file, node) {
|
|
507
571
|
if (checkRef.lesson) {
|
|
@@ -515,6 +579,7 @@ function resolveKnowledgeCheckRef(checkRef, ctx, file, node) {
|
|
|
515
579
|
}
|
|
516
580
|
return {
|
|
517
581
|
number,
|
|
582
|
+
title: ctx.titles?.[checkRef.lesson]?.[checkRef.key],
|
|
518
583
|
href: `${ctx.base.replace(/\/$/, '')}/${checkRef.lesson}/#${knowledgeCheckAnchorId(checkRef.key)}`,
|
|
519
584
|
};
|
|
520
585
|
}
|
|
@@ -526,7 +591,11 @@ function resolveKnowledgeCheckRef(checkRef, ctx, file, node) {
|
|
|
526
591
|
node,
|
|
527
592
|
);
|
|
528
593
|
}
|
|
529
|
-
return {
|
|
594
|
+
return {
|
|
595
|
+
number,
|
|
596
|
+
title: ctx.titles?.[ctx.slug]?.[checkRef.key],
|
|
597
|
+
href: `#${knowledgeCheckAnchorId(checkRef.key)}`,
|
|
598
|
+
};
|
|
530
599
|
}
|
|
531
600
|
|
|
532
601
|
function proseNodes(
|
|
@@ -540,6 +609,8 @@ function proseNodes(
|
|
|
540
609
|
figureCtx,
|
|
541
610
|
diagramCtx,
|
|
542
611
|
checkCtx,
|
|
612
|
+
sectionCtx,
|
|
613
|
+
exampleCtx,
|
|
543
614
|
) {
|
|
544
615
|
const base = eqCtx?.base ?? '';
|
|
545
616
|
const strayExplain = firstUnescaped(value, EXPLAIN);
|
|
@@ -571,16 +642,35 @@ function proseNodes(
|
|
|
571
642
|
!eqRef && !tableRef && !figureRef && diagramCtx
|
|
572
643
|
? parseKeyedFloatRef(key, { prefix: 'dia' })
|
|
573
644
|
: undefined;
|
|
645
|
+
// `ex-` (a worked-example set) is reserved the same way as `sec-` below.
|
|
646
|
+
const exampleRef =
|
|
647
|
+
!eqRef && !tableRef && !figureRef && !diagramRef && exampleCtx
|
|
648
|
+
? parseKeyedFloatRef(key, { prefix: 'ex' })
|
|
649
|
+
: undefined;
|
|
574
650
|
const checkRef =
|
|
575
|
-
!eqRef && !tableRef && !figureRef && !diagramRef && checkCtx
|
|
651
|
+
!eqRef && !tableRef && !figureRef && !diagramRef && !exampleRef && checkCtx
|
|
576
652
|
? parseKnowledgeCheckRef(key)
|
|
577
653
|
: undefined;
|
|
654
|
+
// `sec-` is a reserved prefix like the others: `[[sec-foo]]` would also
|
|
655
|
+
// pass as a notation key, so it has to be claimed before that check.
|
|
656
|
+
const secRef =
|
|
657
|
+
!eqRef &&
|
|
658
|
+
!tableRef &&
|
|
659
|
+
!figureRef &&
|
|
660
|
+
!diagramRef &&
|
|
661
|
+
!exampleRef &&
|
|
662
|
+
!checkRef &&
|
|
663
|
+
sectionCtx
|
|
664
|
+
? parseSectionRef(key)
|
|
665
|
+
: undefined;
|
|
578
666
|
if (
|
|
579
667
|
!eqRef &&
|
|
580
668
|
!tableRef &&
|
|
581
669
|
!figureRef &&
|
|
582
670
|
!diagramRef &&
|
|
671
|
+
!exampleRef &&
|
|
583
672
|
!checkRef &&
|
|
673
|
+
!secRef &&
|
|
584
674
|
!NOTATION_KEY_PATTERN.test(key)
|
|
585
675
|
) {
|
|
586
676
|
fail(file, `Invalid notation key "${key}" in [[...]].`, node);
|
|
@@ -613,8 +703,31 @@ function proseNodes(
|
|
|
613
703
|
continue;
|
|
614
704
|
}
|
|
615
705
|
|
|
706
|
+
if (secRef) {
|
|
707
|
+
const { href, text } = resolveSectionRef(secRef, sectionCtx, file, node);
|
|
708
|
+
output.push({
|
|
709
|
+
type: 'link',
|
|
710
|
+
url: href,
|
|
711
|
+
children: [{ type: 'text', value: text }],
|
|
712
|
+
data: {
|
|
713
|
+
hProperties: {
|
|
714
|
+
className: ['section-ref'],
|
|
715
|
+
'data-sec-ref': secRef.key,
|
|
716
|
+
...(secRef.lesson ? { 'data-sec-lesson': secRef.lesson } : {}),
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
});
|
|
720
|
+
references.push({
|
|
721
|
+
key: secRef.key,
|
|
722
|
+
kind: 'section',
|
|
723
|
+
...(secRef.lesson ? { lesson: secRef.lesson } : {}),
|
|
724
|
+
});
|
|
725
|
+
cursor = match.index + match[0].length;
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
|
|
616
729
|
if (checkRef) {
|
|
617
|
-
const { number, href } = resolveKnowledgeCheckRef(
|
|
730
|
+
const { number, title, href } = resolveKnowledgeCheckRef(
|
|
618
731
|
checkRef,
|
|
619
732
|
checkCtx,
|
|
620
733
|
file,
|
|
@@ -623,7 +736,12 @@ function proseNodes(
|
|
|
623
736
|
output.push({
|
|
624
737
|
type: 'link',
|
|
625
738
|
url: href,
|
|
626
|
-
children: [
|
|
739
|
+
children: [
|
|
740
|
+
{
|
|
741
|
+
type: 'text',
|
|
742
|
+
value: blockRefText('Knowledge check', number, title, checkCtx.show),
|
|
743
|
+
},
|
|
744
|
+
],
|
|
627
745
|
data: {
|
|
628
746
|
hProperties: {
|
|
629
747
|
className: ['knowledge-check-ref'],
|
|
@@ -643,6 +761,43 @@ function proseNodes(
|
|
|
643
761
|
continue;
|
|
644
762
|
}
|
|
645
763
|
|
|
764
|
+
if (exampleRef) {
|
|
765
|
+
const { number, href } = resolveKeyedFloatRef(
|
|
766
|
+
exampleRef,
|
|
767
|
+
exampleCtx,
|
|
768
|
+
{ prefix: 'ex', tagName: 'CompactExamples' },
|
|
769
|
+
file,
|
|
770
|
+
node,
|
|
771
|
+
);
|
|
772
|
+
const title = exampleCtx.titles?.[exampleRef.lesson ?? exampleCtx.slug]?.[
|
|
773
|
+
exampleRef.key
|
|
774
|
+
];
|
|
775
|
+
output.push({
|
|
776
|
+
type: 'link',
|
|
777
|
+
url: href,
|
|
778
|
+
children: [
|
|
779
|
+
{
|
|
780
|
+
type: 'text',
|
|
781
|
+
value: blockRefText('Example', number, title, exampleCtx.show),
|
|
782
|
+
},
|
|
783
|
+
],
|
|
784
|
+
data: {
|
|
785
|
+
hProperties: {
|
|
786
|
+
className: ['example-ref'],
|
|
787
|
+
'data-ex-ref': exampleRef.key,
|
|
788
|
+
...(exampleRef.lesson ? { 'data-ex-lesson': exampleRef.lesson } : {}),
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
});
|
|
792
|
+
references.push({
|
|
793
|
+
key: exampleRef.key,
|
|
794
|
+
kind: 'example',
|
|
795
|
+
...(exampleRef.lesson ? { lesson: exampleRef.lesson } : {}),
|
|
796
|
+
});
|
|
797
|
+
cursor = match.index + match[0].length;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
|
|
646
801
|
const floatRef = tableRef ?? figureRef ?? diagramRef;
|
|
647
802
|
if (floatRef) {
|
|
648
803
|
const kind = tableRef ? 'table' : figureRef ? 'figure' : 'diagram';
|
|
@@ -810,34 +965,67 @@ function inspectExplainCalls(value, resolve, file, node, references) {
|
|
|
810
965
|
* Every number on a page comes from two places that MUST agree: the page's own
|
|
811
966
|
* counters (`numbering`) and the manifest tables a cross-page `[[slug#eq-key]]`
|
|
812
967
|
* is resolved against (`equations` / `tables` / `figures` / `diagrams` /
|
|
813
|
-
* `checks`).
|
|
968
|
+
* `checks` / `examples`).
|
|
814
969
|
* Deriving every input from the same manifest in one step stops a course config from
|
|
815
970
|
* supplying one without the other — the omission that produced page-local
|
|
816
971
|
* `(2.2)` and cross-page `(4.5)` for the same equation.
|
|
817
972
|
*
|
|
818
973
|
* `numbering` maps a lesson slug to
|
|
819
|
-
* `{ part, equations, tables, figures, diagrams, checks
|
|
820
|
-
*
|
|
821
|
-
*
|
|
974
|
+
* `{ part, chapter, prefix, equations, tables, figures, diagrams, checks,
|
|
975
|
+
* examples }`:
|
|
976
|
+
* the part and chapter ordinals, the float-number prefix (`‹part›.‹chapter›`,
|
|
977
|
+
* or the part alone under `numbering.floats: "part"`), and, per numbered kind,
|
|
978
|
+
* how many precede the lesson in that scope. `sections` maps a lesson slug to
|
|
979
|
+
* its top-level headings (`manifest.lessons[].headings`), and
|
|
980
|
+
* `sectionNumbering` is the course's `{ show, depth }`. `checkTitles` and
|
|
981
|
+
* `exampleTitles` map a lesson slug to its checks' and example sets' titles,
|
|
982
|
+
* which a reference prints when the course hides section numbers.
|
|
822
983
|
*/
|
|
823
984
|
export function numberingFromManifest(manifest) {
|
|
824
985
|
if (!manifest || typeof manifest !== 'object') return {};
|
|
986
|
+
const titlesOf = (entriesOf) =>
|
|
987
|
+
Object.fromEntries(
|
|
988
|
+
(manifest.lessons ?? []).map((lesson) => [
|
|
989
|
+
lesson.slug,
|
|
990
|
+
Object.fromEntries(
|
|
991
|
+
(entriesOf(lesson) ?? [])
|
|
992
|
+
.filter((entry) => entry.title)
|
|
993
|
+
.map((entry) => [entry.key, entry.title]),
|
|
994
|
+
),
|
|
995
|
+
]),
|
|
996
|
+
);
|
|
825
997
|
return {
|
|
826
998
|
equations: manifest.equations?.numbersBySlug ?? {},
|
|
827
999
|
tables: manifest.tables?.numbersBySlug ?? {},
|
|
828
1000
|
figures: manifest.figures?.numbersBySlug ?? {},
|
|
829
1001
|
diagrams: manifest.diagrams?.numbersBySlug ?? {},
|
|
830
1002
|
checks: manifest.checks?.numbersBySlug ?? {},
|
|
1003
|
+
examples: manifest.examples?.numbersBySlug ?? {},
|
|
1004
|
+
checkTitles: titlesOf((lesson) => lesson.knowledgeChecks),
|
|
1005
|
+
exampleTitles: titlesOf((lesson) =>
|
|
1006
|
+
lesson.examples?.map(({ key, short }) => ({ key, title: short })),
|
|
1007
|
+
),
|
|
1008
|
+
// Only a lesson that carries `headings` (schema v20 on) — a hand-built
|
|
1009
|
+
// manifest without them leaves the page's headings as Astro would.
|
|
1010
|
+
sections: Object.fromEntries(
|
|
1011
|
+
(manifest.lessons ?? [])
|
|
1012
|
+
.filter((lesson) => Array.isArray(lesson.headings))
|
|
1013
|
+
.map((lesson) => [lesson.slug, lesson.headings]),
|
|
1014
|
+
),
|
|
1015
|
+
sectionNumbering: manifest.numbering?.sections ?? { show: true, depth: 4 },
|
|
831
1016
|
numbering: Object.fromEntries(
|
|
832
1017
|
(manifest.lessons ?? []).map((lesson) => [
|
|
833
1018
|
lesson.slug,
|
|
834
1019
|
{
|
|
835
1020
|
part: lesson.part,
|
|
1021
|
+
chapter: lesson.chapter,
|
|
1022
|
+
prefix: lesson.numbering?.prefix ?? String(lesson.part || ''),
|
|
836
1023
|
equations: lesson.numbering?.equations ?? 0,
|
|
837
1024
|
tables: lesson.numbering?.tables ?? 0,
|
|
838
1025
|
figures: lesson.numbering?.figures ?? 0,
|
|
839
1026
|
diagrams: lesson.numbering?.diagrams ?? 0,
|
|
840
1027
|
checks: lesson.numbering?.checks ?? 0,
|
|
1028
|
+
examples: lesson.numbering?.examples ?? 0,
|
|
841
1029
|
},
|
|
842
1030
|
]),
|
|
843
1031
|
),
|
|
@@ -876,20 +1064,77 @@ export function createRemarkNotationOptions(manifest, overrides = {}) {
|
|
|
876
1064
|
|
|
877
1065
|
/**
|
|
878
1066
|
* `{ prefix, offset }` for one kind of float on one page, from whichever
|
|
879
|
-
* numbering option the caller supplied. `numbering` is the current form
|
|
880
|
-
* `
|
|
881
|
-
* no offset) and is still read
|
|
1067
|
+
* numbering option the caller supplied. `numbering` is the current form (its
|
|
1068
|
+
* `prefix` when given, else the part alone); `chapters` is the superseded
|
|
1069
|
+
* per-lesson form (prefix = the lesson's ordinal, no offset) and is still read
|
|
1070
|
+
* so an unmigrated config keeps working.
|
|
882
1071
|
*/
|
|
883
1072
|
function pageNumbering(options, slug, kind) {
|
|
884
1073
|
if (!slug) return undefined;
|
|
885
1074
|
const placement = options.numbering?.[slug];
|
|
886
1075
|
if (placement?.part) {
|
|
887
|
-
return {
|
|
1076
|
+
return {
|
|
1077
|
+
prefix: placement.prefix || placement.part,
|
|
1078
|
+
offset: placement[kind] ?? 0,
|
|
1079
|
+
};
|
|
888
1080
|
}
|
|
889
1081
|
const chapter = options.chapters?.[slug];
|
|
890
1082
|
return chapter ? { prefix: chapter, offset: 0 } : undefined;
|
|
891
1083
|
}
|
|
892
1084
|
|
|
1085
|
+
/**
|
|
1086
|
+
* Give a lesson's top-level headings the anchors and outline numbers the
|
|
1087
|
+
* manifest decided (`sections.mjs`): an `id` on every one, and
|
|
1088
|
+
* `data-sec-number` on a numbered one when the course shows section numbers
|
|
1089
|
+
* (`global.css` prints it). The page must see the headings the manifest saw —
|
|
1090
|
+
* same count, same levels, so the same numbers — or a `[[sec-…]]` from another
|
|
1091
|
+
* page would print a number this page does not show; any difference fails the
|
|
1092
|
+
* build, the section counterpart of `assertManifestNumberParity`.
|
|
1093
|
+
*/
|
|
1094
|
+
function stampSectionHeadings({ tree, file, slug, published, placement, settings }) {
|
|
1095
|
+
const onPage = collectSectionHeadings(tree, {
|
|
1096
|
+
...(placement?.part && placement?.chapter
|
|
1097
|
+
? { prefix: `${placement.part}.${placement.chapter}` }
|
|
1098
|
+
: {}),
|
|
1099
|
+
depth: settings.depth,
|
|
1100
|
+
});
|
|
1101
|
+
const outline = (headings) =>
|
|
1102
|
+
headings.length === 0
|
|
1103
|
+
? '(none)'
|
|
1104
|
+
: headings
|
|
1105
|
+
.map((heading) =>
|
|
1106
|
+
`${'#'.repeat(heading.level)}${heading.number ? ` ${heading.number}` : ''}`,
|
|
1107
|
+
)
|
|
1108
|
+
.join(', ');
|
|
1109
|
+
const agree =
|
|
1110
|
+
onPage.length === published.length &&
|
|
1111
|
+
onPage.every(
|
|
1112
|
+
(heading, index) =>
|
|
1113
|
+
heading.level === published[index].level &&
|
|
1114
|
+
heading.number === published[index].number,
|
|
1115
|
+
);
|
|
1116
|
+
if (!agree) {
|
|
1117
|
+
fail(
|
|
1118
|
+
file,
|
|
1119
|
+
`Stale Explico manifest for ${slug}: this page's top-level headings are ` +
|
|
1120
|
+
`${outline(onPage)}, but the manifest lists ${outline(published)}. ` +
|
|
1121
|
+
'Recompile the manifest from the current content. If it is current, a ' +
|
|
1122
|
+
'heading is written in a form the manifest does not read — an ' +
|
|
1123
|
+
'underlined (setext) heading, or one inside a list or a blockquote; ' +
|
|
1124
|
+
'write it as `## Heading` on a line of its own.',
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
onPage.forEach(({ node }, index) => {
|
|
1128
|
+
const { slug: id, number } = published[index];
|
|
1129
|
+
node.data = node.data ?? {};
|
|
1130
|
+
node.data.hProperties = {
|
|
1131
|
+
...node.data.hProperties,
|
|
1132
|
+
id,
|
|
1133
|
+
...(settings.show && number ? { 'data-sec-number': number } : {}),
|
|
1134
|
+
};
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
|
|
893
1138
|
/**
|
|
894
1139
|
* Fail when the number this page just computed for a keyed float disagrees
|
|
895
1140
|
* with the number the manifest publishes for the same key (WERN — a reference
|
|
@@ -950,9 +1195,10 @@ function assertManifestNumberParity({
|
|
|
950
1195
|
* missing/wrong map for *keyed* items (it walks `pageLabels`, which unkeyed
|
|
951
1196
|
* equations/floats never enter — see `keyed-jsx-floats.mjs`), so a lesson
|
|
952
1197
|
* whose only floats are unkeyed can silently fall back to `##`-section
|
|
953
|
-
* numbering with no diagnostic at all. `Diagram`
|
|
954
|
-
* every caller: `Diagram.astro`
|
|
955
|
-
*
|
|
1198
|
+
* numbering with no diagnostic at all. `Diagram` and `CompactExamples` are
|
|
1199
|
+
* deliberately excluded by every caller: `Diagram.astro` and
|
|
1200
|
+
* `CompactExamples.astro` already tolerate an unplaced one rendering without
|
|
1201
|
+
* a number, by design.
|
|
956
1202
|
*/
|
|
957
1203
|
function assertStrictNumbering({ file, slug, noun, count, placement, crossPage }) {
|
|
958
1204
|
if (!slug || count === 0) return;
|
|
@@ -992,7 +1238,12 @@ function assertStrictNumbering({ file, slug, noun, count, placement, crossPage }
|
|
|
992
1238
|
* figures?: Record<string, Record<string, string>>,
|
|
993
1239
|
* diagrams?: Record<string, Record<string, string>>,
|
|
994
1240
|
* checks?: Record<string, Record<string, string>>,
|
|
995
|
-
*
|
|
1241
|
+
* examples?: Record<string, Record<string, string>>,
|
|
1242
|
+
* checkTitles?: Record<string, Record<string, string>>,
|
|
1243
|
+
* exampleTitles?: Record<string, Record<string, string>>,
|
|
1244
|
+
* sections?: Record<string, readonly { slug: string, text: string, level: number, number?: string }[]>,
|
|
1245
|
+
* sectionNumbering?: { show: boolean, depth: number },
|
|
1246
|
+
* numbering?: Record<string, { part: number, chapter?: number, prefix?: string, equations: number, tables: number, figures: number, diagrams: number, checks: number, examples?: number }>,
|
|
996
1247
|
* chapters?: Record<string, number>,
|
|
997
1248
|
* }} [options]
|
|
998
1249
|
*
|
|
@@ -1001,16 +1252,23 @@ function assertStrictNumbering({ file, slug, noun, count, placement, crossPage }
|
|
|
1001
1252
|
* They stay available individually for hand-built trees, and an explicitly
|
|
1002
1253
|
* supplied one still wins over the manifest.
|
|
1003
1254
|
*
|
|
1004
|
-
* `numbering` maps a lesson slug to its **part**
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1008
|
-
*
|
|
1255
|
+
* `numbering` maps a lesson slug to its **part** and **chapter**, its float
|
|
1256
|
+
* `prefix` (`‹part›.‹chapter›`, or the part alone under `numbering.floats:
|
|
1257
|
+
* "part"`) and, per kind of float, how many precede the lesson in that scope.
|
|
1258
|
+
* Every display equation / `<Table>` / `<Figure>` / `<Diagram>` /
|
|
1259
|
+
* `<CompactExamples>` on the page is then numbered `‹prefix›.‹offset + n›`,
|
|
1260
|
+
* with `##` and `###` headings ignored;
|
|
1261
|
+
* with no entry the legacy `##`-section scheme applies. It must agree with
|
|
1009
1262
|
* `manifest.lessons[]`, which is what feeds `equations`/`tables`/`figures`/`diagrams`
|
|
1010
1263
|
* above — supplying the tables without `numbering` numbers the same equation
|
|
1011
1264
|
* twice, and `assertManifestNumberParity` turns that into a build error rather
|
|
1012
1265
|
* than a page whose references disagree with their targets.
|
|
1013
1266
|
*
|
|
1267
|
+
* `sections` maps a lesson slug to its top-level headings, and
|
|
1268
|
+
* `sectionNumbering` is the course's `{ show, depth }`: a page's own headings
|
|
1269
|
+
* take the anchors and outline numbers listed there (`stampSectionHeadings`),
|
|
1270
|
+
* and `[[sec-…]]` prints `§ 1.1.2` from the same list.
|
|
1271
|
+
*
|
|
1014
1272
|
* `chapters` is the superseded per-lesson form (slug → the lesson's course
|
|
1015
1273
|
* ordinal, no running offset). Still read when `numbering` has no entry, so a
|
|
1016
1274
|
* config written against the old option keeps building.
|
|
@@ -1029,6 +1287,15 @@ export default function remarkNotation(options = {}) {
|
|
|
1029
1287
|
// The manifest supplies every numbering input at once; an explicit option
|
|
1030
1288
|
// still overrides it, so existing per-field wiring keeps working.
|
|
1031
1289
|
const numbering = { ...numberingFromManifest(options.manifest), ...options };
|
|
1290
|
+
// Every lesson's headings by anchor, built once: `[[lesson#sec-…]]` reads
|
|
1291
|
+
// another page's, `[[sec-…]]` the page's own.
|
|
1292
|
+
const sectionSettings = { show: true, depth: 4, ...numbering.sectionNumbering };
|
|
1293
|
+
const headingsBySlug = Object.fromEntries(
|
|
1294
|
+
Object.entries(numbering.sections ?? {}).map(([slug, headings]) => [
|
|
1295
|
+
slug,
|
|
1296
|
+
Object.fromEntries(headings.map((heading) => [heading.slug, heading])),
|
|
1297
|
+
]),
|
|
1298
|
+
);
|
|
1032
1299
|
|
|
1033
1300
|
return function transform(tree, file) {
|
|
1034
1301
|
const suppliedDefinitions =
|
|
@@ -1040,6 +1307,7 @@ export default function remarkNotation(options = {}) {
|
|
|
1040
1307
|
(definition) => ({ key: definition.key, notation: definition.notation }),
|
|
1041
1308
|
);
|
|
1042
1309
|
const localDefinitions = localDefinitionsFromFile(file);
|
|
1310
|
+
const suspendedOperators = suspendedOperatorsFromFile(file);
|
|
1043
1311
|
const lessonId = lessonIdFrom(file);
|
|
1044
1312
|
const lessonSlug = lessonId ? lessonId.replace(/\./g, '/') : undefined;
|
|
1045
1313
|
// `{ prefix, offset }` per kind of float — see the `numbering` option.
|
|
@@ -1136,8 +1404,9 @@ export default function remarkNotation(options = {}) {
|
|
|
1136
1404
|
noun: tagName,
|
|
1137
1405
|
syntax: (key) => `<${tagName} id="${key}">`,
|
|
1138
1406
|
});
|
|
1139
|
-
// Diagram
|
|
1140
|
-
|
|
1407
|
+
// Diagram and CompactExamples are deliberately excluded — see
|
|
1408
|
+
// `assertStrictNumbering`'s doc.
|
|
1409
|
+
if (strict && tagName !== 'Diagram' && tagName !== 'CompactExamples') {
|
|
1141
1410
|
assertStrictNumbering({
|
|
1142
1411
|
file,
|
|
1143
1412
|
slug: lessonSlug,
|
|
@@ -1171,12 +1440,52 @@ export default function remarkNotation(options = {}) {
|
|
|
1171
1440
|
numbering.diagrams,
|
|
1172
1441
|
'diagrams',
|
|
1173
1442
|
);
|
|
1443
|
+
// A worked-example set is numbered like a float (docs/plans/
|
|
1444
|
+
// titles-and-containers.md Part 6), but its number shows — and a
|
|
1445
|
+
// reference prints it — by the section rules (`show`).
|
|
1446
|
+
const { ctx: exampleFloatCtx, byNode: exampleByNode } = makeFloatCtx(
|
|
1447
|
+
'CompactExamples',
|
|
1448
|
+
numbering.examples,
|
|
1449
|
+
'examples',
|
|
1450
|
+
);
|
|
1451
|
+
const exampleCtx = {
|
|
1452
|
+
...exampleFloatCtx,
|
|
1453
|
+
slug: lessonSlug,
|
|
1454
|
+
show: sectionSettings.show,
|
|
1455
|
+
titles: numbering.exampleTitles ?? {},
|
|
1456
|
+
};
|
|
1174
1457
|
const checkCtx = {
|
|
1175
1458
|
base: options.base ?? '',
|
|
1176
1459
|
pageLabels: new Map(
|
|
1177
1460
|
Object.entries(numbering.checks?.[lessonSlug] ?? {}),
|
|
1178
1461
|
),
|
|
1179
1462
|
crossPage: numbering.checks ?? {},
|
|
1463
|
+
slug: lessonSlug,
|
|
1464
|
+
show: sectionSettings.show,
|
|
1465
|
+
titles: numbering.checkTitles ?? {},
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
// Outline numbering: the lesson's top-level headings take the manifest's
|
|
1469
|
+
// anchors and numbers before anything else reads them, and `[[sec-…]]`
|
|
1470
|
+
// resolves against that same table.
|
|
1471
|
+
const publishedHeadings = lessonSlug
|
|
1472
|
+
? numbering.sections?.[lessonSlug]
|
|
1473
|
+
: undefined;
|
|
1474
|
+
if (lessonId && publishedHeadings) {
|
|
1475
|
+
stampSectionHeadings({
|
|
1476
|
+
tree,
|
|
1477
|
+
file,
|
|
1478
|
+
slug: lessonSlug,
|
|
1479
|
+
published: publishedHeadings,
|
|
1480
|
+
placement: numbering.numbering?.[lessonSlug],
|
|
1481
|
+
settings: sectionSettings,
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
const sectionCtx = {
|
|
1485
|
+
base: options.base ?? '',
|
|
1486
|
+
show: sectionSettings.show,
|
|
1487
|
+
pageHeadings: new Map(Object.entries(headingsBySlug[lessonSlug] ?? {})),
|
|
1488
|
+
crossPage: headingsBySlug,
|
|
1180
1489
|
};
|
|
1181
1490
|
|
|
1182
1491
|
const resolve = (key, kind, node) => {
|
|
@@ -1220,14 +1529,39 @@ export default function remarkNotation(options = {}) {
|
|
|
1220
1529
|
|
|
1221
1530
|
EQ_LABEL_PATTERN.lastIndex = 0;
|
|
1222
1531
|
const labelMatch = EQ_LABEL_PATTERN.exec(value);
|
|
1223
|
-
|
|
1532
|
+
const captions = [...value.matchAll(EQ_CAPTION_PATTERN)];
|
|
1533
|
+
if (labelMatch && node.type === 'inlineMath') {
|
|
1534
|
+
fail(
|
|
1535
|
+
file,
|
|
1536
|
+
`\\label{eq:${labelMatch[1]}} is only valid in a $$…$$ display equation, not inline $…$.`,
|
|
1537
|
+
node,
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
if (captions.length > 0) {
|
|
1224
1541
|
if (node.type === 'inlineMath') {
|
|
1225
1542
|
fail(
|
|
1226
1543
|
file,
|
|
1227
|
-
`\\
|
|
1544
|
+
`\\caption{…} is only valid in a $$…$$ display equation, not inline $…$.`,
|
|
1545
|
+
node,
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
if (captions.length > 1) {
|
|
1549
|
+
fail(
|
|
1550
|
+
file,
|
|
1551
|
+
'A display equation takes one \\caption{…}; this one has ' +
|
|
1552
|
+
`${captions.length}.`,
|
|
1228
1553
|
node,
|
|
1229
1554
|
);
|
|
1230
1555
|
}
|
|
1556
|
+
if (!captions[0][1].trim()) {
|
|
1557
|
+
fail(
|
|
1558
|
+
file,
|
|
1559
|
+
'\\caption{} is empty: give the equation a short caption, or drop it.',
|
|
1560
|
+
node,
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
if (labelMatch || captions.length > 0) {
|
|
1231
1565
|
value = stripEquationLabels(value);
|
|
1232
1566
|
replaceMathSource(node, value);
|
|
1233
1567
|
}
|
|
@@ -1245,7 +1579,10 @@ export default function remarkNotation(options = {}) {
|
|
|
1245
1579
|
|
|
1246
1580
|
let result;
|
|
1247
1581
|
try {
|
|
1248
|
-
|
|
1582
|
+
// Suspension (rule N22) is lesson-body scope only.
|
|
1583
|
+
result = resolveMathGlyphs(value, bindingScope, {
|
|
1584
|
+
suspended: lessonId ? suspendedOperators : [],
|
|
1585
|
+
});
|
|
1249
1586
|
} catch (error) {
|
|
1250
1587
|
const message =
|
|
1251
1588
|
error instanceof GlyphResolutionError
|
|
@@ -1259,8 +1596,14 @@ export default function remarkNotation(options = {}) {
|
|
|
1259
1596
|
if (result.unresolved.length > 0) {
|
|
1260
1597
|
const unresolved = result.unresolved
|
|
1261
1598
|
.map(
|
|
1262
|
-
({ token, start }) =>
|
|
1263
|
-
`${tokenLocation(file, node, value, start)}: "${token}"
|
|
1599
|
+
({ token, start, suspended, font }) =>
|
|
1600
|
+
`${tokenLocation(file, node, value, start)}: "${token}"${
|
|
1601
|
+
suspended
|
|
1602
|
+
? ` (${suspended} is suspended on this page, so this form needs its own entry)`
|
|
1603
|
+
: font
|
|
1604
|
+
? ' (a different symbol from the plain letter; it needs its own entry)'
|
|
1605
|
+
: ''
|
|
1606
|
+
}`,
|
|
1264
1607
|
)
|
|
1265
1608
|
.join(', ');
|
|
1266
1609
|
fail(
|
|
@@ -1301,6 +1644,8 @@ export default function remarkNotation(options = {}) {
|
|
|
1301
1644
|
figureCtx,
|
|
1302
1645
|
diagramCtx,
|
|
1303
1646
|
checkCtx,
|
|
1647
|
+
sectionCtx,
|
|
1648
|
+
exampleCtx,
|
|
1304
1649
|
);
|
|
1305
1650
|
if (replacement) {
|
|
1306
1651
|
node.children.splice(index, 1, ...replacement);
|
|
@@ -1321,6 +1666,8 @@ export default function remarkNotation(options = {}) {
|
|
|
1321
1666
|
injectFloatNumberAttributes(child, figureByNode.get(child), 'fig');
|
|
1322
1667
|
} else if (child?.type === 'mdxJsxFlowElement' && diagramByNode.has(child)) {
|
|
1323
1668
|
injectFloatNumberAttributes(child, diagramByNode.get(child), 'dia');
|
|
1669
|
+
} else if (child?.type === 'mdxJsxFlowElement' && exampleByNode.has(child)) {
|
|
1670
|
+
injectFloatNumberAttributes(child, exampleByNode.get(child), 'ex');
|
|
1324
1671
|
}
|
|
1325
1672
|
}
|
|
1326
1673
|
};
|
|
@@ -1329,10 +1676,13 @@ export default function remarkNotation(options = {}) {
|
|
|
1329
1676
|
// validation — the manifest builder runs the same check via
|
|
1330
1677
|
// `validateRawFloats`/`scanRawFloats` (raw-floats.mjs); this catches
|
|
1331
1678
|
// content added since the manifest was last compiled.
|
|
1679
|
+
// A course that turned rule S3 (tables) or S4 (diagrams) off in its
|
|
1680
|
+
// settings has `manifest.rules.disabled` say so; honour it here too.
|
|
1681
|
+
const disabledRules = new Set(options.manifest?.rules?.disabled ?? []);
|
|
1332
1682
|
if (lessonId) {
|
|
1333
1683
|
const rawFloats = collectRawFloats(tree);
|
|
1334
1684
|
const plural = (count, noun) => (count === 1 ? noun : `${noun}s`);
|
|
1335
|
-
if (rawFloats.markdownTableNodes.length > 0) {
|
|
1685
|
+
if (rawFloats.markdownTableNodes.length > 0 && !disabledRules.has('S3')) {
|
|
1336
1686
|
warn(
|
|
1337
1687
|
file,
|
|
1338
1688
|
`${lessonSlug} has ${rawFloats.markdownTableNodes.length} raw ` +
|
|
@@ -1343,7 +1693,7 @@ export default function remarkNotation(options = {}) {
|
|
|
1343
1693
|
rawFloats.markdownTableNodes[0],
|
|
1344
1694
|
);
|
|
1345
1695
|
}
|
|
1346
|
-
if (rawFloats.htmlTableNodes.length > 0) {
|
|
1696
|
+
if (rawFloats.htmlTableNodes.length > 0 && !disabledRules.has('S3')) {
|
|
1347
1697
|
warn(
|
|
1348
1698
|
file,
|
|
1349
1699
|
`${lessonSlug} has ${rawFloats.htmlTableNodes.length} raw HTML ` +
|
|
@@ -1354,7 +1704,7 @@ export default function remarkNotation(options = {}) {
|
|
|
1354
1704
|
rawFloats.htmlTableNodes[0],
|
|
1355
1705
|
);
|
|
1356
1706
|
}
|
|
1357
|
-
if (rawFloats.unkeyedDiagramNodes.length > 0) {
|
|
1707
|
+
if (rawFloats.unkeyedDiagramNodes.length > 0 && !disabledRules.has('S4')) {
|
|
1358
1708
|
warn(
|
|
1359
1709
|
file,
|
|
1360
1710
|
`${lessonSlug} has ${rawFloats.unkeyedDiagramNodes.length} ` +
|
|
@@ -1364,14 +1714,14 @@ export default function remarkNotation(options = {}) {
|
|
|
1364
1714
|
rawFloats.unkeyedDiagramNodes[0],
|
|
1365
1715
|
);
|
|
1366
1716
|
}
|
|
1367
|
-
if (rawFloats.nestedDiagramNodes.length > 0) {
|
|
1717
|
+
if (rawFloats.nestedDiagramNodes.length > 0 && !disabledRules.has('S4')) {
|
|
1368
1718
|
warn(
|
|
1369
1719
|
file,
|
|
1370
1720
|
`${lessonSlug} has ${rawFloats.nestedDiagramNodes.length} ` +
|
|
1371
1721
|
`<Diagram> ${plural(rawFloats.nestedDiagramNodes.length, 'component')} ` +
|
|
1372
1722
|
`nested inside another component; it is invisible to diagram ` +
|
|
1373
1723
|
`numbering. Move it to the top level (or inside ` +
|
|
1374
|
-
`<Memo>/<ShortNote>), or wrap it in <Figure> to give it identity.`,
|
|
1724
|
+
`<Memo>/<ShortNote>/<Fold>), or wrap it in <Figure> to give it identity.`,
|
|
1375
1725
|
rawFloats.nestedDiagramNodes[0],
|
|
1376
1726
|
);
|
|
1377
1727
|
}
|
|
@@ -1401,5 +1751,6 @@ export default function remarkNotation(options = {}) {
|
|
|
1401
1751
|
file.data.tableLabels = floatLabelData(tableByNode);
|
|
1402
1752
|
file.data.figureLabels = floatLabelData(figureByNode);
|
|
1403
1753
|
file.data.diagramLabels = floatLabelData(diagramByNode);
|
|
1754
|
+
file.data.exampleLabels = floatLabelData(exampleByNode);
|
|
1404
1755
|
};
|
|
1405
1756
|
}
|