explico 0.11.1 → 0.13.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 +1292 -1
- package/README.md +91 -13
- package/dist/compiler/astro.d.ts +11 -4
- package/dist/compiler/astro.js +48 -15
- package/dist/compiler/cli.d.ts +27 -12
- package/dist/compiler/cli.js +96 -25
- package/dist/compiler/collections.d.ts +33 -2
- package/dist/compiler/collections.js +91 -19
- package/dist/compiler/content-schema.d.ts +31 -0
- package/dist/compiler/content-schema.js +82 -0
- package/dist/compiler/content-schemas.d.ts +422 -0
- package/dist/compiler/content-schemas.js +229 -0
- package/dist/compiler/course-config.d.ts +1 -1
- package/dist/compiler/course-config.js +30 -2
- package/dist/compiler/exports/combined-html.d.ts +97 -0
- package/dist/compiler/exports/combined-html.js +492 -0
- package/dist/compiler/exports/download-index.d.ts +9 -0
- package/dist/compiler/exports/download-index.js +33 -0
- package/dist/compiler/exports/export-chapters.d.ts +98 -0
- package/dist/compiler/exports/export-chapters.js +164 -0
- package/dist/compiler/exports/export-pages.d.ts +48 -0
- package/dist/compiler/exports/export-pages.js +22 -0
- package/dist/compiler/exports/html-tree.d.ts +22 -0
- package/dist/compiler/exports/html-tree.js +47 -0
- package/dist/compiler/exports/latex/capture.d.ts +106 -0
- package/dist/compiler/exports/latex/capture.js +435 -0
- package/dist/compiler/exports/latex/compile.d.ts +73 -0
- package/dist/compiler/exports/latex/compile.js +115 -0
- package/dist/compiler/exports/latex/document.d.ts +102 -0
- package/dist/compiler/exports/latex/document.js +349 -0
- package/dist/compiler/exports/latex/explico.cls +415 -0
- package/dist/compiler/exports/latex/numbering.d.ts +18 -0
- package/dist/compiler/exports/latex/numbering.js +57 -0
- package/dist/compiler/exports/latex/serialize.d.ts +112 -0
- package/dist/compiler/exports/latex/serialize.js +606 -0
- package/dist/compiler/exports/latex-export.d.ts +44 -0
- package/dist/compiler/exports/latex-export.js +169 -0
- package/dist/compiler/exports/markdown/serialize.d.ts +51 -0
- package/dist/compiler/exports/markdown/serialize.js +527 -0
- package/dist/compiler/exports/markdown-export.d.ts +39 -0
- package/dist/compiler/exports/markdown-export.js +240 -0
- package/dist/compiler/exports/standalone-html.d.ts +15 -5
- package/dist/compiler/exports/standalone-html.js +23 -51
- package/dist/compiler/exports/static-tree.d.ts +253 -0
- package/dist/compiler/exports/static-tree.js +625 -0
- package/dist/compiler/exports/zip.d.ts +16 -0
- package/dist/compiler/exports/zip.js +72 -0
- package/dist/compiler/lesson-derivation.d.ts +8 -2
- package/dist/compiler/lesson-derivation.js +6 -7
- package/dist/compiler/manifest.d.ts +115 -8
- package/dist/compiler/manifest.js +221 -39
- package/dist/compiler/render-diagnostics.d.ts +65 -0
- package/dist/compiler/render-diagnostics.js +56 -0
- package/dist/compiler/resolution-report.d.ts +3 -1
- package/dist/{course-config-Dsnri9Tk.d.ts → course-config-rRtME5UV.d.ts} +56 -13
- package/dist/curriculum/validation.d.ts +9 -3
- package/dist/curriculum/validation.js +35 -17
- package/dist/reference/base-abbreviations.d.mts +2 -2
- package/dist/reference/base-abbreviations.mjs +2 -2
- package/dist/reference/base-operators.d.mts +8 -2
- package/dist/reference/base-operators.mjs +8 -10
- package/dist/reference/base-roman-labels.d.mts +4 -3
- package/dist/reference/base-roman-labels.mjs +4 -3
- package/dist/reference/citation-format.d.mts +35 -0
- package/dist/reference/citation-format.mjs +74 -0
- package/dist/reference/consistency.d.ts +5 -9
- package/dist/reference/consistency.js +34 -38
- package/dist/reference/curriculum-alignment.js +3 -24
- package/dist/reference/derived-entries.d.ts +45 -0
- package/dist/reference/derived-entries.js +50 -0
- package/dist/reference/derived-notation.d.mts +125 -0
- package/dist/reference/derived-notation.mjs +121 -0
- package/dist/reference/diagram-fit.d.ts +45 -0
- package/dist/reference/diagram-fit.js +54 -0
- package/dist/reference/diagram-layout.d.ts +59 -10
- package/dist/reference/diagram-layout.js +85 -24
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +1 -1
- package/dist/reference/equations-validate.js +26 -1
- package/dist/reference/equations.d.mts +27 -0
- package/dist/reference/equations.mjs +120 -22
- package/dist/reference/examples-validate.d.ts +1 -1
- package/dist/reference/figure-fit.d.ts +70 -0
- package/dist/reference/figure-fit.js +60 -0
- package/dist/reference/figure-pictures.d.ts +27 -0
- package/dist/reference/figure-pictures.js +49 -0
- package/dist/reference/figure-symbols.d.ts +32 -0
- package/dist/reference/figure-symbols.js +55 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.d.ts +1 -1
- package/dist/reference/gate-abbreviations.js +1 -1
- package/dist/reference/gate-course-rules.d.ts +4 -3
- package/dist/reference/gate-course-rules.js +9 -4
- package/dist/reference/gate-heading-length.d.ts +2 -0
- package/dist/reference/gate-heading-length.js +7 -5
- package/dist/reference/gate-heading-structure.js +26 -0
- package/dist/reference/gate-indices.d.ts +35 -0
- package/dist/reference/gate-indices.js +299 -0
- package/dist/reference/gate-math.d.ts +32 -2
- package/dist/reference/gate-math.js +68 -12
- package/dist/reference/gate-mdx-expression.d.ts +17 -0
- package/dist/reference/gate-mdx-expression.js +29 -0
- package/dist/reference/gate-non-chapter-pages.d.ts +39 -0
- package/dist/reference/gate-non-chapter-pages.js +156 -0
- package/dist/reference/gate-part-pages.d.ts +4 -2
- package/dist/reference/gate-part-pages.js +2 -1
- package/dist/reference/gate-prop-text.d.ts +28 -0
- package/dist/reference/gate-prop-text.js +106 -0
- package/dist/reference/gate-prose-notation.d.ts +3 -1
- package/dist/reference/gate-prose-notation.js +4 -2
- package/dist/reference/gate-retired-keys.d.ts +35 -0
- package/dist/reference/gate-retired-keys.js +58 -0
- package/dist/reference/gloss.d.ts +2 -0
- package/dist/reference/katex-measure.d.ts +17 -0
- package/dist/reference/katex-measure.js +223 -0
- package/dist/reference/keyed-float-validate.js +12 -0
- package/dist/reference/keyed-jsx-floats.d.mts +2 -2
- package/dist/reference/keyed-jsx-floats.mjs +11 -14
- package/dist/reference/lab-katex-validate.d.ts +43 -0
- package/dist/reference/lab-katex-validate.js +96 -0
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-scope.js +13 -2
- package/dist/reference/lint-ignore.js +1 -1
- package/dist/reference/math-glyphs.d.mts +94 -2
- package/dist/reference/math-glyphs.mjs +851 -46
- package/dist/reference/mixed-content.d.ts +8 -3
- package/dist/reference/mixed-content.js +4 -2
- package/dist/reference/page-notation.d.ts +39 -7
- package/dist/reference/page-notation.js +62 -10
- package/dist/reference/raw-floats.mjs +37 -13
- package/dist/reference/references.d.ts +11 -1
- package/dist/reference/references.js +6 -0
- package/dist/reference/registry.js +115 -11
- package/dist/reference/rehype-fail-katex-errors.d.mts +9 -0
- package/dist/reference/rehype-fail-katex-errors.mjs +19 -0
- package/dist/reference/rehype-heading-anchors.mjs +11 -1
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/remark-notation.mjs +275 -23
- package/dist/reference/render-lab-math.d.ts +13 -2
- package/dist/reference/render-lab-math.js +18 -6
- package/dist/reference/sections-validate.d.ts +28 -3
- package/dist/reference/sections-validate.js +31 -4
- package/dist/reference/sections.d.mts +24 -6
- package/dist/reference/sections.mjs +92 -17
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/tables-validate.js +24 -1
- package/dist/reference/text-measure.d.ts +10 -0
- package/dist/reference/text-measure.js +41 -0
- package/dist/reference/text-widths.d.ts +10 -0
- package/dist/reference/text-widths.js +1238 -0
- package/dist/reference/types.d.ts +64 -8
- package/dist/rules/catalog.js +223 -32
- package/dist/rules/in-effect.d.ts +1 -1
- package/dist/rules/rules.schema.json +100 -27
- package/dist/rules/settings.d.ts +1 -1
- package/dist/rules/settings.js +7 -0
- package/dist/session/PreferenceStore.d.ts +18 -1
- package/dist/session/PreferenceStore.js +29 -1
- package/docs/authoring/README.md +33 -21
- package/docs/authoring/agents.md +38 -7
- package/docs/authoring/components.md +115 -10
- package/docs/authoring/migrating.md +878 -0
- package/docs/authoring/notation.md +285 -30
- package/docs/authoring/retired.md +1 -0
- package/docs/authoring/structure-rules.md +84 -45
- package/docs/authoring/structure.md +67 -2
- package/docs/authoring/visualizations.md +167 -25
- package/package.json +12 -7
- package/src/compiler/astro.ts +72 -21
- package/src/compiler/cli.ts +143 -35
- package/src/compiler/collections.ts +158 -27
- package/src/compiler/content-schema.ts +108 -0
- package/src/compiler/content-schemas.ts +391 -0
- package/src/compiler/course-config.ts +86 -15
- package/src/compiler/exports/combined-html.ts +669 -0
- package/src/compiler/exports/download-index.ts +36 -0
- package/src/compiler/exports/export-chapters.ts +233 -0
- package/src/compiler/exports/export-pages.ts +44 -0
- package/src/compiler/exports/html-tree.ts +62 -0
- package/src/compiler/exports/latex/capture.ts +651 -0
- package/src/compiler/exports/latex/compile.ts +168 -0
- package/src/compiler/exports/latex/document.ts +479 -0
- package/src/compiler/exports/latex/explico.cls +415 -0
- package/src/compiler/exports/latex/numbering.ts +74 -0
- package/src/compiler/exports/latex/serialize.ts +787 -0
- package/src/compiler/exports/latex/texlive-packages.txt +76 -0
- package/src/compiler/exports/latex-export.ts +255 -0
- package/src/compiler/exports/markdown/serialize.ts +632 -0
- package/src/compiler/exports/markdown-export.ts +329 -0
- package/src/compiler/exports/standalone-html.ts +39 -70
- package/src/compiler/exports/static-tree.ts +944 -0
- package/src/compiler/exports/zip.ts +88 -0
- package/src/compiler/lesson-derivation.ts +13 -8
- package/src/compiler/manifest.ts +387 -40
- package/src/compiler/render-diagnostics.ts +111 -0
- package/src/components/CourseContents.astro +6 -0
- package/src/components/HoverPanel.astro +204 -6
- package/src/components/PartOverview.astro +18 -9
- package/src/components/abbreviation/AbbreviationGlossary.astro +6 -2
- package/src/components/abbreviation/AbbreviationLayer.astro +21 -12
- package/src/components/assessments/AssessmentRunner.tsx +7 -1
- package/src/components/assessments/AssessmentSet.astro +14 -1
- package/src/components/block-heading.ts +38 -0
- package/src/components/card-arrow.ts +18 -4
- package/src/components/card-carry.ts +406 -0
- package/src/components/card-drag.ts +1 -30
- package/src/components/card-fetch.ts +87 -0
- package/src/components/card-manager.ts +1222 -38
- package/src/components/card-memory.ts +5 -1
- package/src/components/card-placement.ts +211 -13
- package/src/components/card-side.ts +281 -31
- package/src/components/citation/CitationLayer.astro +45 -40
- package/src/components/diagram/Diagram.astro +123 -44
- package/src/components/disclosure/print-as-shown.ts +42 -6
- package/src/components/figure/Figure.astro +17 -1
- package/src/components/figure/FloatCaption.astro +4 -17
- package/src/components/figure/figure-state.ts +26 -17
- package/src/components/float-toolbar.ts +274 -0
- package/src/components/memo/Memo.astro +21 -10
- package/src/components/memo/ShortNote.astro +4 -0
- package/src/components/notation/NotationGlossary.astro +22 -17
- package/src/components/notation/NotationLayer.astro +300 -79
- package/src/components/notation/NotationMath.astro +8 -1
- package/src/components/notation/render-notation-math.ts +51 -5
- package/src/components/readable-text.ts +25 -0
- package/src/components/reference/FloatRefLayer.astro +162 -89
- package/src/components/starlight/HeadingAnchorEnhancer.astro +13 -8
- package/src/components/starlight/KeyedFloatEnhancer.astro +28 -9
- package/src/components/starlight/LayoutPageSidebar.astro +17 -3
- package/src/components/starlight/LayoutSocialIcons.astro +79 -29
- package/src/components/starlight/LessonExportActions.astro +384 -310
- package/src/components/starlight/LessonFooter.astro +66 -8
- package/src/components/starlight/PrintController.astro +28 -0
- package/src/components/starlight/TextSizeControl.astro +137 -0
- package/src/components/starlight/print-controller.ts +380 -0
- package/src/components/starlight/section-summary.ts +13 -5
- package/src/components/table/Table.astro +14 -22
- package/src/components/table/table-truncation.ts +15 -0
- package/src/content-config.ts +20 -312
- package/src/curriculum/validation.ts +72 -24
- package/src/reference/base-abbreviations.mjs +2 -2
- package/src/reference/base-operators.mjs +8 -10
- package/src/reference/base-roman-labels.mjs +4 -3
- package/src/reference/citation-format.mjs +74 -0
- package/src/reference/consistency.ts +47 -61
- package/src/reference/curriculum-alignment.ts +4 -41
- package/src/reference/derived-entries.ts +68 -0
- package/src/reference/derived-notation.mjs +121 -0
- package/src/reference/diagram-fit.ts +104 -0
- package/src/reference/diagram-layout.ts +182 -51
- package/src/reference/diagrams-validate.ts +2 -1
- package/src/reference/equations-validate.ts +41 -2
- package/src/reference/equations.mjs +120 -22
- package/src/reference/examples-validate.ts +1 -0
- package/src/reference/figure-fit.ts +139 -0
- package/src/reference/figure-pictures.ts +96 -0
- package/src/reference/figure-symbols.ts +117 -0
- package/src/reference/figures-validate.ts +2 -1
- package/src/reference/gate-abbreviations.ts +1 -1
- package/src/reference/gate-course-rules.ts +22 -7
- package/src/reference/gate-heading-length.ts +25 -7
- package/src/reference/gate-heading-structure.ts +40 -1
- package/src/reference/gate-indices.ts +470 -0
- package/src/reference/gate-math.ts +102 -13
- package/src/reference/gate-mdx-expression.ts +45 -0
- package/src/reference/gate-non-chapter-pages.ts +241 -0
- package/src/reference/gate-part-pages.ts +1 -1
- package/src/reference/gate-prop-text.ts +145 -0
- package/src/reference/gate-prose-notation.ts +19 -5
- package/src/reference/gate-retired-keys.ts +101 -0
- package/src/reference/gloss.ts +2 -0
- package/src/reference/katex-measure.ts +384 -0
- package/src/reference/keyed-float-validate.ts +22 -0
- package/src/reference/keyed-jsx-floats.mjs +11 -14
- package/src/reference/lab-katex-validate.ts +156 -0
- package/src/reference/lab-math-scope.ts +15 -1
- package/src/reference/lint-ignore.ts +4 -1
- package/src/reference/math-glyphs.mjs +851 -46
- package/src/reference/mixed-content.ts +11 -2
- package/src/reference/page-notation.ts +114 -14
- package/src/reference/raw-floats.mjs +37 -13
- package/src/reference/references.ts +14 -0
- package/src/reference/registry.ts +170 -9
- package/src/reference/rehype-fail-katex-errors.mjs +19 -0
- package/src/reference/rehype-heading-anchors.mjs +11 -1
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/remark-notation.mjs +275 -23
- package/src/reference/render-lab-math.ts +40 -5
- package/src/reference/sections-validate.ts +67 -3
- package/src/reference/sections.mjs +92 -17
- package/src/reference/tables-validate.ts +40 -2
- package/src/reference/text-measure.ts +63 -0
- package/src/reference/text-widths.ts +133 -0
- package/src/reference/types.ts +75 -8
- package/src/rules/catalog.ts +241 -31
- package/src/rules/settings.ts +10 -0
- package/src/session/PreferenceStore.ts +53 -0
- package/src/styles/global.css +322 -83
|
@@ -69,6 +69,11 @@ const HEADING_TAG = /^h([1-6])$/;
|
|
|
69
69
|
const LINK_ICON_PATH =
|
|
70
70
|
'm12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 1 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 0 0-1.42-1.42m8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 1 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 0 0 0 1.42 1 1 0 0 0 1.42 0l3.88-3.89a4.49 4.49 0 0 0 0-6.33M8.83 15.17a1 1 0 0 0 .71.29 1 1 0 0 0 .71-.29l4.92-4.92a1 1 0 1 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42';
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* A heading's text as a reader hears it. KaTeX writes each formula twice —
|
|
74
|
+
* MathML, whose `<annotation>` holds the LaTeX, and the `aria-hidden` glyphs —
|
|
75
|
+
* so a formula is read once, from its MathML, without the LaTeX.
|
|
76
|
+
*/
|
|
72
77
|
function headingText(node) {
|
|
73
78
|
let text = '';
|
|
74
79
|
const walk = (current) => {
|
|
@@ -77,10 +82,15 @@ function headingText(node) {
|
|
|
77
82
|
text += String(current.value ?? '');
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
85
|
+
if (current.type === 'element') {
|
|
86
|
+
const classes = current.properties?.className;
|
|
87
|
+
if (current.tagName === 'annotation') return;
|
|
88
|
+
if (Array.isArray(classes) && classes.includes('katex-html')) return;
|
|
89
|
+
}
|
|
80
90
|
if (Array.isArray(current.children)) current.children.forEach(walk);
|
|
81
91
|
};
|
|
82
92
|
walk(node);
|
|
83
|
-
return text.replace(/\s+/g, ' ').trim();
|
|
93
|
+
return text.replace(/[\u200b\u2061-\u2064]/g, '').replace(/\s+/g, ' ').trim();
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
// A heading's marker, e.g. `## Some title [collapsed]`, stripped from the
|
|
Binary file
|
|
@@ -35,9 +35,12 @@ export const NOTATION_KEY_PATTERN = /^[a-z0-9]+(?:[.-][a-z0-9]+)*$/;
|
|
|
35
35
|
|
|
36
36
|
import {
|
|
37
37
|
GlyphResolutionError,
|
|
38
|
+
baseSymbolLatex,
|
|
39
|
+
derivedNotationLatex,
|
|
38
40
|
resolveMathGlyphs,
|
|
39
41
|
stripNotationMarkers,
|
|
40
42
|
} from './math-glyphs.mjs';
|
|
43
|
+
import { composeDerivedEntry } from './derived-notation.mjs';
|
|
41
44
|
import {
|
|
42
45
|
EQ_CAPTION_PATTERN,
|
|
43
46
|
EQ_LABEL_PATTERN,
|
|
@@ -46,6 +49,7 @@ import {
|
|
|
46
49
|
equationAnchorId,
|
|
47
50
|
parseEquationRef,
|
|
48
51
|
stripEquationLabels,
|
|
52
|
+
TRANSPARENT_COMPONENTS,
|
|
49
53
|
} from './equations.mjs';
|
|
50
54
|
import {
|
|
51
55
|
collectKeyedJsxFloats,
|
|
@@ -184,6 +188,25 @@ function lessonIdFrom(file) {
|
|
|
184
188
|
return typeof lessonId === 'string' ? lessonId : undefined;
|
|
185
189
|
}
|
|
186
190
|
|
|
191
|
+
/** True for any page under `content/docs/`; a lesson is one of them. */
|
|
192
|
+
function isDocsPageFile(file) {
|
|
193
|
+
return [file?.path, file?.history?.[0], file?.data?.file].some(
|
|
194
|
+
(value) => typeof value === 'string' && LESSON_DOC_PATH.test(value),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The blocks a chapter numbers (`makeFloatCtx` below, and the knowledge
|
|
200
|
+
* check). Rule S10 keeps each off a page that is not a chapter.
|
|
201
|
+
*/
|
|
202
|
+
const NUMBERED_BLOCK_NAMES = new Set([
|
|
203
|
+
'Table',
|
|
204
|
+
'Figure',
|
|
205
|
+
'Diagram',
|
|
206
|
+
'CompactExamples',
|
|
207
|
+
'AssessmentSet',
|
|
208
|
+
]);
|
|
209
|
+
|
|
187
210
|
const ANY_TERM_KEY = /\[\[\s*([^\][]+?)\s*\]\]/g;
|
|
188
211
|
const ANY_EXPLAIN_KEY = /\\explain\s*\{([^{}]+)\}/g;
|
|
189
212
|
|
|
@@ -214,6 +237,26 @@ function collectPageSharedKeys(tree) {
|
|
|
214
237
|
return keys;
|
|
215
238
|
}
|
|
216
239
|
|
|
240
|
+
/**
|
|
241
|
+
* A derived card (rule N23) as the remark pass records it: the frontmatter of
|
|
242
|
+
* its base and index entries composed (`derived-notation.mjs`). Only an
|
|
243
|
+
* authored `label` is known here, so the title is composed when both entries
|
|
244
|
+
* author one, which is what a table header's label needs.
|
|
245
|
+
*/
|
|
246
|
+
function derivedResolution(key, base, index) {
|
|
247
|
+
const entry = composeDerivedEntry({
|
|
248
|
+
key,
|
|
249
|
+
notation: derivedNotationLatex(base.notation, index.notation),
|
|
250
|
+
baseLatex: baseSymbolLatex(base.notation),
|
|
251
|
+
base: { ...base, label: base.label ?? '' },
|
|
252
|
+
index: { ...index, label: index.label ?? '', summary: index.meaning },
|
|
253
|
+
});
|
|
254
|
+
const labelled =
|
|
255
|
+
typeof base.label === 'string' && typeof index.label === 'string';
|
|
256
|
+
const { label, ...rest } = entry;
|
|
257
|
+
return { ...rest, ...(labelled ? { label } : {}), scope: 'derived' };
|
|
258
|
+
}
|
|
259
|
+
|
|
217
260
|
function pageGlyphScope(localDefinitions, sharedDefinitions, pageSharedKeys) {
|
|
218
261
|
return [
|
|
219
262
|
...localDefinitions.values(),
|
|
@@ -226,6 +269,8 @@ function pageGlyphScope(localDefinitions, sharedDefinitions, pageSharedKeys) {
|
|
|
226
269
|
].map((definition) => ({
|
|
227
270
|
key: definition.key,
|
|
228
271
|
notation: definition.notation,
|
|
272
|
+
// Rule N23: an entry whose index is a fixed label is no family.
|
|
273
|
+
...(definition.fixedIndex === true ? { fixedIndex: true } : {}),
|
|
229
274
|
}));
|
|
230
275
|
}
|
|
231
276
|
|
|
@@ -385,8 +430,9 @@ function inlineEquationNode(mathNode) {
|
|
|
385
430
|
* in a lesson is numbered; a keyed one anchors at `#eq-<key>`, an unkeyed one
|
|
386
431
|
* at its positional `#eq-<section>-<index>`. The visible number is a focusable
|
|
387
432
|
* `<a>` sibling — never authored, never inside KaTeX — so it survives print and
|
|
388
|
-
* screen readers name it "Equation 2.4". A "Copy LaTeX" button sits
|
|
389
|
-
* (`.keyed-
|
|
433
|
+
* screen readers name it "Equation 2.4". A "Copy LaTeX" button sits on the
|
|
434
|
+
* equation's bottom border, under the number (`.keyed-equation__copy`, styled
|
|
435
|
+
* in `global.css` and placed by `float-toolbar.ts`; titled "Copy LaTeX —
|
|
390
436
|
* ‹short›" when the equation has a `\caption{}`) — the equation's own
|
|
391
437
|
* copy affordance, distinct from the notation popup card's (`NotationLayer.astro`),
|
|
392
438
|
* which only exists for a symbol with a registry entry, not a bare display
|
|
@@ -431,19 +477,21 @@ function equationNode(mathNode, { key, number, short }) {
|
|
|
431
477
|
},
|
|
432
478
|
},
|
|
433
479
|
},
|
|
434
|
-
el(
|
|
435
|
-
'button',
|
|
436
|
-
{
|
|
437
|
-
type: 'button',
|
|
438
|
-
className: ['keyed-equation__copy'],
|
|
439
|
-
'data-eq-copy': '',
|
|
440
|
-
'data-latex': latex,
|
|
441
|
-
'aria-label': `Copy LaTeX for Equation ${number}`,
|
|
442
|
-
title: short ? `Copy LaTeX — ${short}` : 'Copy LaTeX',
|
|
443
|
-
},
|
|
444
|
-
[copyIconNode()],
|
|
445
|
-
),
|
|
446
480
|
]),
|
|
481
|
+
// On the block's border, not beside the number: the number's toolbar
|
|
482
|
+
// (`float-toolbar.ts`) takes the room around the number.
|
|
483
|
+
el(
|
|
484
|
+
'button',
|
|
485
|
+
{
|
|
486
|
+
type: 'button',
|
|
487
|
+
className: ['keyed-equation__copy'],
|
|
488
|
+
'data-eq-copy': '',
|
|
489
|
+
'data-latex': latex,
|
|
490
|
+
'aria-label': `Copy LaTeX for Equation ${number}`,
|
|
491
|
+
title: short ? `Copy LaTeX — ${short}` : 'Copy LaTeX',
|
|
492
|
+
},
|
|
493
|
+
[copyIconNode()],
|
|
494
|
+
),
|
|
447
495
|
],
|
|
448
496
|
};
|
|
449
497
|
}
|
|
@@ -467,6 +515,137 @@ function injectFloatNumberAttributes(node, float, prefix) {
|
|
|
467
515
|
});
|
|
468
516
|
}
|
|
469
517
|
|
|
518
|
+
/**
|
|
519
|
+
* The URL path Starlight serves a docs page at: `motion/index.mdx` is
|
|
520
|
+
* `/motion/`, `motion/speed.mdx` is `/motion/speed/`, `index.mdx` is `/`.
|
|
521
|
+
*/
|
|
522
|
+
function pageUrlFrom(file) {
|
|
523
|
+
for (const value of [file?.path, file?.history?.[0], file?.data?.file]) {
|
|
524
|
+
if (typeof value !== 'string') continue;
|
|
525
|
+
const match = value.match(LESSON_DOC_PATH);
|
|
526
|
+
if (!match) continue;
|
|
527
|
+
const route = match[1].replace(/\\/g, '/').replace(/(?:^|\/)index$/, '');
|
|
528
|
+
return route ? `/${route}/` : '/';
|
|
529
|
+
}
|
|
530
|
+
return undefined;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* The chapter slug a link's `href` names — `motion/speed` for
|
|
535
|
+
* `/motion/speed/#sec-x`, for `../speed/` from a sibling page, and for
|
|
536
|
+
* `/base/motion/speed/` under the course's `base` — or `undefined` for a
|
|
537
|
+
* same-page anchor, another site, or a scheme such as `mailto:`.
|
|
538
|
+
*/
|
|
539
|
+
function linkedSlug(href, pageUrl, base) {
|
|
540
|
+
if (typeof href !== 'string' || href === '' || href.startsWith('#')) return undefined;
|
|
541
|
+
if (href.startsWith('//') || /^[a-z][a-z0-9+.-]*:/i.test(href)) return undefined;
|
|
542
|
+
let url;
|
|
543
|
+
try {
|
|
544
|
+
url = new URL(href, `https://course.invalid${pageUrl ?? '/'}`);
|
|
545
|
+
} catch {
|
|
546
|
+
return undefined;
|
|
547
|
+
}
|
|
548
|
+
let path;
|
|
549
|
+
try {
|
|
550
|
+
path = decodeURIComponent(url.pathname);
|
|
551
|
+
} catch {
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
const prefix = base && base !== '/' ? base.replace(/\/+$/, '') : '';
|
|
555
|
+
if (prefix && (path === prefix || path.startsWith(`${prefix}/`))) {
|
|
556
|
+
path = path.slice(prefix.length);
|
|
557
|
+
}
|
|
558
|
+
return path.replace(/^\/+|\/+$/g, '');
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* A link the author wrote to another chapter shows that chapter's full title
|
|
563
|
+
* as its native tooltip (`title`): prose names a chapter any way it likes —
|
|
564
|
+
* "the previous chapter", "Chapter 1.2", "this derivation" — and a reader can
|
|
565
|
+
* see where the link goes before following it. A Markdown link and a JSX
|
|
566
|
+
* `<a href>` both get it; a link with a title of its own, or one to the page
|
|
567
|
+
* it is on, keeps what it has.
|
|
568
|
+
*/
|
|
569
|
+
function titleChapterLinks(tree, file, titleBySlug, base) {
|
|
570
|
+
if (titleBySlug.size === 0) return;
|
|
571
|
+
const pageUrl = pageUrlFrom(file);
|
|
572
|
+
const here = pageUrl?.replace(/^\/+|\/+$/g, '');
|
|
573
|
+
const titleOf = (href) => {
|
|
574
|
+
const slug = linkedSlug(href, pageUrl, base);
|
|
575
|
+
return slug !== undefined && slug !== here ? titleBySlug.get(slug) : undefined;
|
|
576
|
+
};
|
|
577
|
+
const walk = (node) => {
|
|
578
|
+
if (!node || typeof node !== 'object') return;
|
|
579
|
+
if (node.type === 'link' && !node.title) {
|
|
580
|
+
const title = titleOf(node.url);
|
|
581
|
+
if (title) node.title = title;
|
|
582
|
+
} else if (
|
|
583
|
+
(node.type === 'mdxJsxTextElement' || node.type === 'mdxJsxFlowElement') &&
|
|
584
|
+
node.name === 'a'
|
|
585
|
+
) {
|
|
586
|
+
const attributes = node.attributes ?? [];
|
|
587
|
+
const named = (name) =>
|
|
588
|
+
attributes.find((attribute) => attribute?.type === 'mdxJsxAttribute' && attribute.name === name);
|
|
589
|
+
const title = named('title') ? undefined : titleOf(named('href')?.value);
|
|
590
|
+
if (title) {
|
|
591
|
+
node.attributes = [...attributes, { type: 'mdxJsxAttribute', name: 'title', value: title }];
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (Array.isArray(node.children)) node.children.forEach(walk);
|
|
595
|
+
};
|
|
596
|
+
walk(tree);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* The innermost component around `target` that numbering does not see
|
|
601
|
+
* through — the reason a float in it has no number — or `undefined`.
|
|
602
|
+
*/
|
|
603
|
+
function blockingComponent(tree, target) {
|
|
604
|
+
const search = (node, blocking) => {
|
|
605
|
+
if (node === target) return blocking;
|
|
606
|
+
if (!node || typeof node !== 'object' || !Array.isArray(node.children)) return undefined;
|
|
607
|
+
const here =
|
|
608
|
+
node.type === 'mdxJsxFlowElement' &&
|
|
609
|
+
node.name &&
|
|
610
|
+
!TRANSPARENT_COMPONENTS.includes(node.name)
|
|
611
|
+
? node.name
|
|
612
|
+
: node.type === 'containerDirective'
|
|
613
|
+
? `:::${node.name ?? ''}`
|
|
614
|
+
: blocking;
|
|
615
|
+
for (const child of node.children) {
|
|
616
|
+
const found = search(child, here);
|
|
617
|
+
if (found !== undefined) return found;
|
|
618
|
+
}
|
|
619
|
+
return undefined;
|
|
620
|
+
};
|
|
621
|
+
return search(tree, undefined);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Why a `<Table>`/`<Figure>` on a lesson page got no number: it sits in a
|
|
626
|
+
* component other than the `TRANSPARENT_COMPONENTS`, or in a `:::note`
|
|
627
|
+
* aside, where a float is illustrative, as an equation there is
|
|
628
|
+
* (`keyed-jsx-floats.mjs`).
|
|
629
|
+
*/
|
|
630
|
+
function unnumberedFloatMessage(tagName, component) {
|
|
631
|
+
const where = !component
|
|
632
|
+
? 'another component'
|
|
633
|
+
: component.startsWith(':::')
|
|
634
|
+
? `a ${component} aside`
|
|
635
|
+
: `<${component}>`;
|
|
636
|
+
const noun = tagName === 'Table' ? 'table' : 'figure';
|
|
637
|
+
const instead =
|
|
638
|
+
tagName === 'Table'
|
|
639
|
+
? 'write it as a plain Markdown table (rule S3 does not flag one there)'
|
|
640
|
+
: 'show its content without <Figure>';
|
|
641
|
+
return (
|
|
642
|
+
`<${tagName}> inside ${where} gets no number: a lesson numbers a ${noun} ` +
|
|
643
|
+
`at its top level or in a <Memo>, <ShortNote> or <Fold>. A ${noun} in a ` +
|
|
644
|
+
`worked example or another component is illustrative, like an equation ` +
|
|
645
|
+
`there: ${instead}, or move the <${tagName}> out of ${where}.`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
470
649
|
/** Resolve a `[[eq-key]]` / `[[slug#eq-key]]` reference to `{ number, href }`. */
|
|
471
650
|
function resolveEquationRef(eqRef, eqCtx, file, node) {
|
|
472
651
|
if (eqRef.lesson) {
|
|
@@ -1161,6 +1340,16 @@ function assertManifestNumberParity({
|
|
|
1161
1340
|
|
|
1162
1341
|
for (const [key, number] of pageLabels) {
|
|
1163
1342
|
const expected = published[key];
|
|
1343
|
+
if (expected === undefined && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(key)) {
|
|
1344
|
+
// The manifest left it out on purpose: `validate:content` reports the
|
|
1345
|
+
// id (`<prefix>-invalid-key`), and recompiling changes nothing.
|
|
1346
|
+
fail(
|
|
1347
|
+
file,
|
|
1348
|
+
`${slug}: the id in ${syntax(key)} is not a valid key — lowercase ` +
|
|
1349
|
+
`letters, digits and hyphens only — so the manifest leaves it unnumbered. ` +
|
|
1350
|
+
`Fix the id.`,
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1164
1353
|
if (expected === undefined) {
|
|
1165
1354
|
fail(
|
|
1166
1355
|
file,
|
|
@@ -1287,6 +1476,9 @@ export default function remarkNotation(options = {}) {
|
|
|
1287
1476
|
// The manifest supplies every numbering input at once; an explicit option
|
|
1288
1477
|
// still overrides it, so existing per-field wiring keeps working.
|
|
1289
1478
|
const numbering = { ...numberingFromManifest(options.manifest), ...options };
|
|
1479
|
+
const chapterTitleBySlug = new Map(
|
|
1480
|
+
(options.manifest?.lessons ?? []).map((lesson) => [lesson.slug, lesson.title]),
|
|
1481
|
+
);
|
|
1290
1482
|
// Every lesson's headings by anchor, built once: `[[lesson#sec-…]]` reads
|
|
1291
1483
|
// another page's, `[[sec-…]]` the page's own.
|
|
1292
1484
|
const sectionSettings = { show: true, depth: 4, ...numbering.sectionNumbering };
|
|
@@ -1310,6 +1502,9 @@ export default function remarkNotation(options = {}) {
|
|
|
1310
1502
|
const suspendedOperators = suspendedOperatorsFromFile(file);
|
|
1311
1503
|
const lessonId = lessonIdFrom(file);
|
|
1312
1504
|
const lessonSlug = lessonId ? lessonId.replace(/\./g, '/') : undefined;
|
|
1505
|
+
// The landing page, a part's overview page (rule S10): its math binds
|
|
1506
|
+
// like a chapter's, but it numbers nothing.
|
|
1507
|
+
const nonChapterPage = !lessonId && isDocsPageFile(file);
|
|
1313
1508
|
// `{ prefix, offset }` per kind of float — see the `numbering` option.
|
|
1314
1509
|
// Missing ⇒ the legacy `##`-section numbering.
|
|
1315
1510
|
const equationNumbering = pageNumbering(numbering, lessonSlug, 'equations');
|
|
@@ -1329,6 +1524,13 @@ export default function remarkNotation(options = {}) {
|
|
|
1329
1524
|
const equationLabels = collectEquationLabels(tree, equationNumbering);
|
|
1330
1525
|
const pageLabels = new Map();
|
|
1331
1526
|
for (const label of equationLabels) {
|
|
1527
|
+
if (nonChapterPage) {
|
|
1528
|
+
fail(
|
|
1529
|
+
file,
|
|
1530
|
+
`\\label{eq:${label.key}} on a page that is not a chapter (rule S10): only a chapter numbers an equation. Drop the label to show the equation unnumbered, or link the chapter's own with [[<chapter>#eq-<key>]].`,
|
|
1531
|
+
label.node,
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1332
1534
|
if (pageLabels.has(label.key)) {
|
|
1333
1535
|
fail(
|
|
1334
1536
|
file,
|
|
@@ -1568,20 +1770,28 @@ export default function remarkNotation(options = {}) {
|
|
|
1568
1770
|
|
|
1569
1771
|
inspectExplainCalls(value, resolve, file, node, references);
|
|
1570
1772
|
|
|
1571
|
-
const bindingScope =
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1773
|
+
const bindingScope =
|
|
1774
|
+
lessonId || nonChapterPage
|
|
1775
|
+
? mathScope
|
|
1776
|
+
: registryFile
|
|
1777
|
+
? registryMathDefinitions
|
|
1778
|
+
: undefined;
|
|
1576
1779
|
|
|
1577
1780
|
if (bindingScope) {
|
|
1578
|
-
const context = lessonId
|
|
1781
|
+
const context = lessonId
|
|
1782
|
+
? 'lesson math'
|
|
1783
|
+
: nonChapterPage
|
|
1784
|
+
? 'page math'
|
|
1785
|
+
: 'notation definition math';
|
|
1579
1786
|
|
|
1580
1787
|
let result;
|
|
1581
1788
|
try {
|
|
1582
1789
|
// Suspension (rule N22) is lesson-body scope only.
|
|
1583
1790
|
result = resolveMathGlyphs(value, bindingScope, {
|
|
1584
1791
|
suspended: lessonId ? suspendedOperators : [],
|
|
1792
|
+
// Derived cards (rule N23) are a page's own, built by the
|
|
1793
|
+
// manifest for its body math — not the registry page's.
|
|
1794
|
+
derive: Boolean(lessonId || nonChapterPage),
|
|
1585
1795
|
});
|
|
1586
1796
|
} catch (error) {
|
|
1587
1797
|
const message =
|
|
@@ -1596,28 +1806,46 @@ export default function remarkNotation(options = {}) {
|
|
|
1596
1806
|
if (result.unresolved.length > 0) {
|
|
1597
1807
|
const unresolved = result.unresolved
|
|
1598
1808
|
.map(
|
|
1599
|
-
({ token, start, suspended, font }) =>
|
|
1809
|
+
({ token, start, suspended, font, lenders }) =>
|
|
1600
1810
|
`${tokenLocation(file, node, value, start)}: "${token}"${
|
|
1601
1811
|
suspended
|
|
1602
1812
|
? ` (${suspended} is suspended on this page, so this form needs its own entry)`
|
|
1603
1813
|
: font
|
|
1604
1814
|
? ' (a different symbol from the plain letter; it needs its own entry)'
|
|
1605
|
-
:
|
|
1815
|
+
: lenders
|
|
1816
|
+
? ` (${lenders.join(', ')} explains only its own instances with a subscript, never a bare "${token}", rule N4: give it its own entry, or \\explain{${lenders[0]}}{${token}} if it is that entry's shorthand)`
|
|
1817
|
+
: ''
|
|
1606
1818
|
}`,
|
|
1607
1819
|
)
|
|
1608
1820
|
.join(', ');
|
|
1609
1821
|
fail(
|
|
1610
1822
|
file,
|
|
1611
|
-
|
|
1823
|
+
nonChapterPage
|
|
1824
|
+
? `Unresolved notation in ${context}: ${unresolved}. Introduce the symbol with [[key]], or wrap it in \\explain{key}{latex}: a page that is not a chapter recalls shared notation and defines none (rule S10).`
|
|
1825
|
+
: `Unresolved notation in ${context}: ${unresolved}. Introduce the symbol with [[key]], define it in notation.local, or wrap it in \\explain{key}{latex}.`,
|
|
1612
1826
|
node,
|
|
1613
1827
|
);
|
|
1614
1828
|
}
|
|
1615
1829
|
|
|
1616
1830
|
for (const binding of result.bindings) {
|
|
1617
1831
|
if (binding.level === 'explicit') continue;
|
|
1832
|
+
if (binding.match === 'derived') continue;
|
|
1618
1833
|
resolve(binding.key, 'math', node);
|
|
1619
1834
|
references.push({ key: binding.key, kind: 'math' });
|
|
1620
1835
|
}
|
|
1836
|
+
// A derived card (rule N23) uses its base and its index entry; it
|
|
1837
|
+
// is recorded composed, so a table header reads its own label.
|
|
1838
|
+
for (const derived of result.derived) {
|
|
1839
|
+
const base = resolve(derived.base, 'math', node);
|
|
1840
|
+
const index = resolve(derived.index, 'math', node);
|
|
1841
|
+
references.push(
|
|
1842
|
+
{ key: derived.base, kind: 'math' },
|
|
1843
|
+
{ key: derived.index, kind: 'math' },
|
|
1844
|
+
);
|
|
1845
|
+
if (base && index) {
|
|
1846
|
+
resolved.set(derived.key, derivedResolution(derived.key, base, index));
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1621
1849
|
replaceMathSource(node, result.latex);
|
|
1622
1850
|
}
|
|
1623
1851
|
return;
|
|
@@ -1668,6 +1896,27 @@ export default function remarkNotation(options = {}) {
|
|
|
1668
1896
|
injectFloatNumberAttributes(child, diagramByNode.get(child), 'dia');
|
|
1669
1897
|
} else if (child?.type === 'mdxJsxFlowElement' && exampleByNode.has(child)) {
|
|
1670
1898
|
injectFloatNumberAttributes(child, exampleByNode.get(child), 'ex');
|
|
1899
|
+
} else if (
|
|
1900
|
+
nonChapterPage &&
|
|
1901
|
+
child?.type === 'mdxJsxFlowElement' &&
|
|
1902
|
+
NUMBERED_BLOCK_NAMES.has(child.name)
|
|
1903
|
+
) {
|
|
1904
|
+
// `validate:content` reports this first (gate-non-chapter-pages.ts);
|
|
1905
|
+
// this is the same message for a build that skipped it.
|
|
1906
|
+
fail(
|
|
1907
|
+
file,
|
|
1908
|
+
`<${child.name}> on a page that is not a chapter (rule S10): only a chapter numbers it. Move it into the chapter it belongs to, and link it from here: the link shows it in its card.`,
|
|
1909
|
+
child,
|
|
1910
|
+
);
|
|
1911
|
+
} else if (
|
|
1912
|
+
lessonId &&
|
|
1913
|
+
child?.type === 'mdxJsxFlowElement' &&
|
|
1914
|
+
(child.name === 'Table' || child.name === 'Figure')
|
|
1915
|
+
) {
|
|
1916
|
+
// Numbered everywhere a lesson numbers one, so this one is nested
|
|
1917
|
+
// in a component that is not transparent. Say so here, at its
|
|
1918
|
+
// line, before the component fails on its missing number.
|
|
1919
|
+
fail(file, unnumberedFloatMessage(child.name, blockingComponent(tree, child)), child);
|
|
1671
1920
|
}
|
|
1672
1921
|
}
|
|
1673
1922
|
};
|
|
@@ -1727,6 +1976,9 @@ export default function remarkNotation(options = {}) {
|
|
|
1727
1976
|
}
|
|
1728
1977
|
}
|
|
1729
1978
|
|
|
1979
|
+
// Before `visit`, which writes the `[[…]]` reference links: each of
|
|
1980
|
+
// those opens a card of its own.
|
|
1981
|
+
titleChapterLinks(tree, file, chapterTitleBySlug, options.base);
|
|
1730
1982
|
visit(tree);
|
|
1731
1983
|
|
|
1732
1984
|
file.data.notationReferences = references;
|
|
@@ -2,6 +2,9 @@ import katex from './katex-delimiters.mjs';
|
|
|
2
2
|
import { resolveMathGlyphs, stripNotationMarkers } from './math-glyphs.mjs';
|
|
3
3
|
import { createNotationKatexOptions } from './katex-options.mjs';
|
|
4
4
|
import { slotPlaceholder, type RenderedLabMath } from './lab-math-slot';
|
|
5
|
+
import { pageOfScope, registerPageDerivedNotation } from './page-notation';
|
|
6
|
+
import { indexFindings } from './gate-indices';
|
|
7
|
+
import type { DerivedUse, IndexedSymbol } from './types';
|
|
5
8
|
|
|
6
9
|
// Re-exported so server code keeps one import. An island imports these from
|
|
7
10
|
// `lab-math-slot` instead: this module is server-only.
|
|
@@ -10,12 +13,26 @@ export { slotPlaceholder, type RenderedLabMath };
|
|
|
10
13
|
export interface LabMathScopeDefinition {
|
|
11
14
|
readonly key: string;
|
|
12
15
|
readonly notation: string;
|
|
16
|
+
/** Rule N23: the entry's index is a fixed label, so it is no family. */
|
|
17
|
+
readonly fixedIndex?: true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RenderLabMathOptions {
|
|
21
|
+
/**
|
|
22
|
+
* `false` renders the template as inline math — a `.katex` with no
|
|
23
|
+
* `.katex-display` — for a formula that sits in a line: a legend entry,
|
|
24
|
+
* an axis title, a control label. It never scrolls (`global.css`
|
|
25
|
+
* `.lab-math`). Default `true`, a display equation.
|
|
26
|
+
*/
|
|
27
|
+
readonly display?: boolean;
|
|
13
28
|
}
|
|
14
29
|
|
|
15
30
|
interface BindResult {
|
|
16
31
|
latex: string;
|
|
17
32
|
bindings: ReadonlyArray<{ key: string }>;
|
|
18
33
|
unresolved: ReadonlyArray<{ token: string; start: number; font?: string }>;
|
|
34
|
+
derived: readonly DerivedUse[];
|
|
35
|
+
indices: readonly IndexedSymbol[];
|
|
19
36
|
}
|
|
20
37
|
|
|
21
38
|
// `\slot{name}{fallback}` — `name` is a lab-slot id, `fallback` may contain one
|
|
@@ -36,6 +53,7 @@ const sentinelFor = (index: number): string => `909909${index}.909909`;
|
|
|
36
53
|
export function renderLabMath(
|
|
37
54
|
template: string,
|
|
38
55
|
scope: readonly LabMathScopeDefinition[],
|
|
56
|
+
{ display = true }: RenderLabMathOptions = {},
|
|
39
57
|
): RenderedLabMath {
|
|
40
58
|
const slots: Array<{ name: string; fallback: string }> = [];
|
|
41
59
|
const forBinding = template.replace(
|
|
@@ -47,9 +65,14 @@ export function renderLabMath(
|
|
|
47
65
|
},
|
|
48
66
|
);
|
|
49
67
|
|
|
68
|
+
// A table `labMathScope` built knows its page: its indexed symbols take
|
|
69
|
+
// derived cards there (rule N23), registered so the page ships them.
|
|
70
|
+
const page = pageOfScope(scope);
|
|
50
71
|
let result: BindResult;
|
|
51
72
|
try {
|
|
52
|
-
result = resolveMathGlyphs(forBinding, [...scope]
|
|
73
|
+
result = resolveMathGlyphs(forBinding, [...scope], {
|
|
74
|
+
derive: page !== undefined,
|
|
75
|
+
}) as BindResult;
|
|
53
76
|
} catch (error) {
|
|
54
77
|
throw new Error(
|
|
55
78
|
`Lab math failed to compile: ${
|
|
@@ -76,6 +99,17 @@ export function renderLabMath(
|
|
|
76
99
|
);
|
|
77
100
|
}
|
|
78
101
|
|
|
102
|
+
if (page?.checkIndices) {
|
|
103
|
+
const findings = indexFindings(result.indices);
|
|
104
|
+
if (findings.length > 0) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Lab math has an index no card explains (rule N23, ${page.context}): ` +
|
|
107
|
+
`${findings.join('; ')}.\n template: ${template.trim()}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (page) registerPageDerivedNotation(page.locals, result.derived, page.context);
|
|
112
|
+
|
|
79
113
|
let bound = result.latex;
|
|
80
114
|
let copyBound = result.latex;
|
|
81
115
|
slots.forEach(({ name, fallback }, index) => {
|
|
@@ -93,10 +127,11 @@ export function renderLabMath(
|
|
|
93
127
|
copyBound = copyBound.replace(sentinel, slotPlaceholder(name));
|
|
94
128
|
});
|
|
95
129
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
130
|
+
// The site search indexes a formula's glyphs, not its MathML copy and the
|
|
131
|
+
// LaTeX annotation in it (`rehype-fail-katex-errors.mjs`).
|
|
132
|
+
const html = katex
|
|
133
|
+
.renderToString(bound, createNotationKatexOptions({ displayMode: display }))
|
|
134
|
+
.replaceAll('<span class="katex-mathml">', '<span class="katex-mathml" data-pagefind-ignore="">');
|
|
100
135
|
|
|
101
136
|
return {
|
|
102
137
|
html,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import type { MathGateDiagnostic } from './gate-math';
|
|
12
12
|
import { lessonLine } from './gate-prose-notation';
|
|
13
|
-
import { parseSectionRef } from './sections.mjs';
|
|
13
|
+
import { parseSectionRef, scanOutlineBlocks } from './sections.mjs';
|
|
14
14
|
|
|
15
15
|
export interface SectionRefLessonInput {
|
|
16
16
|
/** Lesson slug, e.g. `motion/position-and-coordinates`. */
|
|
@@ -36,10 +36,19 @@ function withoutCode(text: string): string {
|
|
|
36
36
|
function unresolved(
|
|
37
37
|
ref: { readonly lesson?: string; readonly key: string },
|
|
38
38
|
lessonExists: boolean,
|
|
39
|
+
pages: ReadonlySet<string>,
|
|
39
40
|
): string {
|
|
40
41
|
const written = ref.lesson
|
|
41
42
|
? `[[${ref.lesson}#sec-${ref.key}]]`
|
|
42
43
|
: `[[sec-${ref.key}]]`;
|
|
44
|
+
const page = ref.lesson?.replace(/\/index$/, '');
|
|
45
|
+
if (page !== undefined && !lessonExists && pages.has(page)) {
|
|
46
|
+
return (
|
|
47
|
+
`${written} names ${page}, a page that is not a chapter: a section reference ` +
|
|
48
|
+
`reaches a chapter's top-level headings only (rule S10). Link the heading ` +
|
|
49
|
+
`with a Markdown link instead, [its words](/${page}/#${ref.key}).`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
43
52
|
if (ref.lesson && !lessonExists) {
|
|
44
53
|
return `${written} names lesson ${ref.lesson}, which does not exist.`;
|
|
45
54
|
}
|
|
@@ -52,10 +61,17 @@ function unresolved(
|
|
|
52
61
|
);
|
|
53
62
|
}
|
|
54
63
|
|
|
55
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* One `sec-ref-resolves` error per `[[sec-…]]` that names no heading.
|
|
66
|
+
* `pageSlugs` are the pages that are not chapters (`motion/index`), which a
|
|
67
|
+
* section reference cannot reach; the message says so instead of calling the
|
|
68
|
+
* page missing.
|
|
69
|
+
*/
|
|
56
70
|
export function validateSectionRefs(
|
|
57
71
|
lessons: readonly SectionRefLessonInput[],
|
|
72
|
+
pageSlugs: readonly string[] = [],
|
|
58
73
|
): MathGateDiagnostic[] {
|
|
74
|
+
const pages = new Set(pageSlugs.map((slug) => slug.replace(/\/index$/, '')));
|
|
59
75
|
const headingsByLesson = new Map(
|
|
60
76
|
lessons.map((lesson) => [lesson.slug, new Set(lesson.headingSlugs)]),
|
|
61
77
|
);
|
|
@@ -73,9 +89,57 @@ export function validateSectionRefs(
|
|
|
73
89
|
file: lesson.file,
|
|
74
90
|
line: lessonLine(lesson, match.index ?? 0),
|
|
75
91
|
token: match[0],
|
|
76
|
-
message: unresolved(ref, headings !== undefined),
|
|
92
|
+
message: unresolved(ref, headings !== undefined, pages),
|
|
77
93
|
});
|
|
78
94
|
}
|
|
79
95
|
}
|
|
80
96
|
return diagnostics;
|
|
81
97
|
}
|
|
98
|
+
|
|
99
|
+
export interface HeadingAnchorLessonInput {
|
|
100
|
+
readonly file: string;
|
|
101
|
+
readonly body: string;
|
|
102
|
+
readonly bodyLine?: number;
|
|
103
|
+
/** The lesson's outline headings in order (`manifest.lessons[].headings`). */
|
|
104
|
+
readonly headings: readonly { readonly slug: string; readonly text: string }[];
|
|
105
|
+
/** Every anchor the engine writes on the page, with what it belongs to. */
|
|
106
|
+
readonly anchors: ReadonlyMap<string, string>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One `heading-anchor-collision` error per heading whose anchor the engine
|
|
111
|
+
* already gives something else on the page — `## References` on a page that
|
|
112
|
+
* cites (the generated References section is `#references`), or `## Tab x`
|
|
113
|
+
* beside `<Table id="x">` (`#tab-x`). Two elements with one id send every link
|
|
114
|
+
* to the first, and the LaTeX export defines the label twice.
|
|
115
|
+
*/
|
|
116
|
+
export function validateHeadingAnchors(
|
|
117
|
+
lessons: readonly HeadingAnchorLessonInput[],
|
|
118
|
+
): MathGateDiagnostic[] {
|
|
119
|
+
const diagnostics: MathGateDiagnostic[] = [];
|
|
120
|
+
for (const lesson of lessons) {
|
|
121
|
+
const offsets = scanOutlineBlocks(lesson.body)
|
|
122
|
+
.filter((block) => block.kind === 'heading')
|
|
123
|
+
.map((block) => block.index);
|
|
124
|
+
lesson.headings.forEach((heading, index) => {
|
|
125
|
+
const owner = lesson.anchors.get(heading.slug);
|
|
126
|
+
if (owner === undefined) return;
|
|
127
|
+
const advice =
|
|
128
|
+
heading.slug === 'references'
|
|
129
|
+
? 'Rename the heading, or drop it: the References section is generated from the page’s citations.'
|
|
130
|
+
: `Rename the heading, or give ${owner} another id.`;
|
|
131
|
+
diagnostics.push({
|
|
132
|
+
context: 'structure',
|
|
133
|
+
code: 'heading-anchor-collision',
|
|
134
|
+
severity: 'error',
|
|
135
|
+
file: lesson.file,
|
|
136
|
+
line: lessonLine(lesson, offsets[index] ?? 0),
|
|
137
|
+
token: heading.text,
|
|
138
|
+
message:
|
|
139
|
+
`The heading "${heading.text}" gets the anchor #${heading.slug}, which ${owner} ` +
|
|
140
|
+
`already has on this page, so a link to either lands on the first. ${advice}`,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return diagnostics;
|
|
145
|
+
}
|