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
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule N23 — an index is part of the symbol. A subscript on a symbol is
|
|
3
|
+
* explained by the indexed symbol's own entry (`v_0`), by a family entry
|
|
4
|
+
* whose index runs (`x_k` explains `x_1`), or by an index entry for the
|
|
5
|
+
* subscript (`{}_0`, which makes the derived card of `\mathbf{v}_0`); a card
|
|
6
|
+
* on the base alone does not explain it. Arithmetic on an index letter that
|
|
7
|
+
* has its own card is explained (`X_{t-1}`), and a subscript on an operator or
|
|
8
|
+
* a bracket is not an index (`\sum_{i=1}`). A running index — a lowercase
|
|
9
|
+
* letter the page does not declare fixed — states the set it runs over, on
|
|
10
|
+
* its card (`range`) or after a `\quad` on the first display line that uses
|
|
11
|
+
* it.
|
|
12
|
+
*
|
|
13
|
+
* The same pass collects the derived cards each page's body uses, which the
|
|
14
|
+
* manifest materializes (`derived-entries.ts`) whether or not the rule is on:
|
|
15
|
+
* the render path writes their keys either way.
|
|
16
|
+
*
|
|
17
|
+
* Lesson bodies, notation formulas and assessments are checked here; a lab's
|
|
18
|
+
* and a diagram's math, which only exists as they render, is checked there
|
|
19
|
+
* (`render-lab-math.ts`, `Diagram.astro`, through {@link indexFindings}). An
|
|
20
|
+
* indexed symbol takes a derived card wherever page math renders — a body,
|
|
21
|
+
* an assessment, a lab, a diagram — and never in a notation formula, which
|
|
22
|
+
* resolves against its own card's table. A span the resolver rejects is the
|
|
23
|
+
* completeness gate's to report. A body finding is located, so `explico-allow
|
|
24
|
+
* N23` waives it at one place.
|
|
25
|
+
*/
|
|
26
|
+
import { stripEquationLabels } from './equations.mjs';
|
|
27
|
+
import { resolveMathGlyphs, stripNotationMarkers } from './math-glyphs.mjs';
|
|
28
|
+
import {
|
|
29
|
+
equationLinkFor,
|
|
30
|
+
mathSpans,
|
|
31
|
+
pageScope,
|
|
32
|
+
suspendedOperators,
|
|
33
|
+
type AssessmentLike,
|
|
34
|
+
type LessonAssessmentLink,
|
|
35
|
+
type MathGateDiagnostic,
|
|
36
|
+
type ScopeEntry,
|
|
37
|
+
} from './gate-math';
|
|
38
|
+
import { formulaGlyphScope, type GlyphScopeEntry, type ResolvedGloss } from './gloss';
|
|
39
|
+
import { indexOfEntry } from './math-glyphs.mjs';
|
|
40
|
+
import type { DerivedUse, IndexedSymbol, NotationLessonInput } from './types';
|
|
41
|
+
|
|
42
|
+
export type { DerivedUse } from './types';
|
|
43
|
+
|
|
44
|
+
/** What the check needs of an entry besides its glyph-table row. */
|
|
45
|
+
export interface IndexEntryFacts extends ScopeEntry {
|
|
46
|
+
readonly range?: string;
|
|
47
|
+
readonly formula?: string;
|
|
48
|
+
readonly glosses?: readonly ResolvedGloss[];
|
|
49
|
+
readonly source?: { readonly file: string };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Where a math source sits: what the message calls it, and its fix. */
|
|
53
|
+
type Place = 'body' | 'formula' | 'assessment';
|
|
54
|
+
|
|
55
|
+
/** How to explain an index where the source sits. */
|
|
56
|
+
const INDEX_FIX: Record<Place, (token: string, subscript: string) => string> = {
|
|
57
|
+
body: (token, subscript) =>
|
|
58
|
+
`Give the indexed symbol its own entry (latex: ${token}), a family entry whose index ` +
|
|
59
|
+
`runs (latex: x_k), or an index entry for the subscript (latex: "{}_${subscript}", ` +
|
|
60
|
+
`label and meaning), introduced with [[key]] or in notation.local`,
|
|
61
|
+
assessment: (token, subscript) =>
|
|
62
|
+
`Give the indexed symbol its own entry (latex: ${token}), a family entry whose index ` +
|
|
63
|
+
`runs (latex: x_k), or an index entry for the subscript (latex: "{}_${subscript}"), ` +
|
|
64
|
+
`introduced in the lesson's prose with [[key]] or in its notation.local`,
|
|
65
|
+
formula: (token, subscript) =>
|
|
66
|
+
`In a formula, wrap the symbol in \\explain{key}{${token}} with an entry for the indexed ` +
|
|
67
|
+
`symbol, declare it under glosses: (latex: ${token}), or wrap the subscript in an index ` +
|
|
68
|
+
`entry's key (\\explain{key}{${subscript}})`,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
interface Occurrence {
|
|
72
|
+
readonly letter: string;
|
|
73
|
+
readonly line: number;
|
|
74
|
+
readonly display: boolean;
|
|
75
|
+
/** The math line (between `\\` breaks) the letter is an index on. */
|
|
76
|
+
readonly mathLine: string;
|
|
77
|
+
readonly token: string;
|
|
78
|
+
readonly ownKey: string | null;
|
|
79
|
+
readonly coverKey: string | null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const DISPLAY_OPEN = /\$\$\s*$/;
|
|
83
|
+
const RANGE_RELATION =
|
|
84
|
+
/(?:=|<|>|\\(?:in|le|leq|ge|geq|lt|gt|dots|ldots|cdots|to|subseteq|subset)(?![A-Za-z]))/;
|
|
85
|
+
|
|
86
|
+
/** The text after each `\quad` on a math line — never `\qquad` (rule N18's reason slot). */
|
|
87
|
+
function quadClauses(mathLine: string): string[] {
|
|
88
|
+
return mathLine
|
|
89
|
+
.split(/\\quad(?![A-Za-z])/)
|
|
90
|
+
.slice(1)
|
|
91
|
+
.map((clause) => stripNotationMarkers(clause));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Whether a clause states the set `letter` runs over: `\quad i = 1, \dots, n`. */
|
|
95
|
+
function statesRange(clause: string, letter: string): boolean {
|
|
96
|
+
const named = new RegExp(`(?<![A-Za-z\\\\])${letter}(?![A-Za-z])`);
|
|
97
|
+
return named.test(clause) && RANGE_RELATION.test(clause);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The `\\`-separated line of `latex` that holds `offset`. */
|
|
101
|
+
function mathLineAt(latex: string, offset: number): string {
|
|
102
|
+
const before = latex.slice(0, offset).split(/\\\\/);
|
|
103
|
+
const after = latex.slice(offset).split(/\\\\/);
|
|
104
|
+
return `${before[before.length - 1] ?? ''}${after[0] ?? ''}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function message(
|
|
108
|
+
lessonId: string,
|
|
109
|
+
code: 'index-explained' | 'index-swallowed' | 'index-range' | 'derived-key-taken',
|
|
110
|
+
details: Record<string, string>,
|
|
111
|
+
place: Place = 'body',
|
|
112
|
+
): string {
|
|
113
|
+
switch (code) {
|
|
114
|
+
case 'index-explained': {
|
|
115
|
+
const index = details.index ?? '';
|
|
116
|
+
const subscript = index.length === 1 ? index : `{${index}}`;
|
|
117
|
+
const what =
|
|
118
|
+
details.base === 'false'
|
|
119
|
+
? `has no card, and neither does its base (${details.parts})`
|
|
120
|
+
: `has a subscript no card explains (${details.parts})`;
|
|
121
|
+
return (
|
|
122
|
+
`${lessonId}: ${JSON.stringify(details.token)} ${what}. ` +
|
|
123
|
+
`${INDEX_FIX[place](details.token ?? '', subscript)}; a letter in a subscript is an ` +
|
|
124
|
+
`index, so a base-library i, e or d there needs a card (rule N23).`
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
case 'index-swallowed':
|
|
128
|
+
return (
|
|
129
|
+
`${lessonId}: \\explain{${details.key}}{${details.token}} wraps an indexed symbol in ` +
|
|
130
|
+
`${details.key}, whose latex (${details.notation}) has no index, so its card cannot ` +
|
|
131
|
+
`explain ${JSON.stringify(details.index)}. Drop the wrap and give the subscript an index ` +
|
|
132
|
+
`entry (latex: "{}_${details.index}"), or wrap it in an entry for the indexed symbol (rule N23).`
|
|
133
|
+
);
|
|
134
|
+
case 'index-range':
|
|
135
|
+
return (
|
|
136
|
+
`${lessonId}: the running index ${details.letter} (first in ${JSON.stringify(details.token)}) ` +
|
|
137
|
+
`has no stated set. Give ${details.where} a range (range: "1, \\\\dots, n"), state it after a ` +
|
|
138
|
+
`\\quad on the first display line that uses it (\\quad ${details.letter} = 1, \\dots, n), or ` +
|
|
139
|
+
`mark the card fixedIndex: true if ${details.letter} is a label there, not a running index (rule N23).`
|
|
140
|
+
);
|
|
141
|
+
case 'derived-key-taken':
|
|
142
|
+
return (
|
|
143
|
+
`${lessonId}: ${JSON.stringify(details.token)} composes the derived card ${details.key}, ` +
|
|
144
|
+
`but an authored entry has that key. Rename the entry: a key of the form ` +
|
|
145
|
+
`<symbol>.sub.<index> is the build's (rule N23).`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Run rule N23 over every lesson (and non-chapter page) body, every notation
|
|
152
|
+
* formula and every assessment, and collect the derived cards each body
|
|
153
|
+
* uses. `entries` holds every shared entry by key; a lesson's own come from
|
|
154
|
+
* its `localDefinitions`. The index-set check (`index-range`) reads lesson
|
|
155
|
+
* bodies only: a letter's set is stated on its card or on the page.
|
|
156
|
+
*/
|
|
157
|
+
export function gateIndices(
|
|
158
|
+
lessons: readonly NotationLessonInput[],
|
|
159
|
+
entries: ReadonlyMap<string, IndexEntryFacts>,
|
|
160
|
+
extras: {
|
|
161
|
+
readonly assessments?: readonly AssessmentLike[];
|
|
162
|
+
readonly lessonAssessments?: readonly LessonAssessmentLink[];
|
|
163
|
+
} = {},
|
|
164
|
+
): {
|
|
165
|
+
diagnostics: MathGateDiagnostic[];
|
|
166
|
+
derivedByLesson: Map<string, DerivedUse[]>;
|
|
167
|
+
} {
|
|
168
|
+
const diagnostics: MathGateDiagnostic[] = [];
|
|
169
|
+
const derivedByLesson = new Map<string, DerivedUse[]>();
|
|
170
|
+
|
|
171
|
+
for (const lesson of lessons) {
|
|
172
|
+
const file = lesson.source.file;
|
|
173
|
+
const bodyOffset = (lesson.bodyLine ?? 1) - 1;
|
|
174
|
+
const scope = pageScope(lesson, entries);
|
|
175
|
+
const local = new Map<string, IndexEntryFacts>(
|
|
176
|
+
lesson.localDefinitions.map((definition) => [definition.key, definition]),
|
|
177
|
+
);
|
|
178
|
+
const factsOf = (key: string | null): IndexEntryFacts | undefined =>
|
|
179
|
+
key === null ? undefined : (local.get(key) ?? entries.get(key));
|
|
180
|
+
const suspended = suspendedOperators(lesson);
|
|
181
|
+
const derived = new Map<string, DerivedUse>();
|
|
182
|
+
const reported = new Set<string>();
|
|
183
|
+
const occurrences: Occurrence[] = [];
|
|
184
|
+
|
|
185
|
+
const spans = mathSpans(lesson.body).sort((left, right) => left.offset - right.offset);
|
|
186
|
+
for (const span of spans) {
|
|
187
|
+
const latex = stripEquationLabels(span.latex);
|
|
188
|
+
let result;
|
|
189
|
+
try {
|
|
190
|
+
result = resolveMathGlyphs(latex, scope, { suspended, derive: true });
|
|
191
|
+
} catch {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const line = span.line + bodyOffset;
|
|
195
|
+
const equation = equationLinkFor(lesson.lessonId, span.latex);
|
|
196
|
+
const at = {
|
|
197
|
+
context: 'body' as const,
|
|
198
|
+
severity: 'error' as const,
|
|
199
|
+
file,
|
|
200
|
+
line,
|
|
201
|
+
...(equation === undefined ? {} : { equation }),
|
|
202
|
+
};
|
|
203
|
+
const display = DISPLAY_OPEN.test(lesson.body.slice(0, span.offset));
|
|
204
|
+
|
|
205
|
+
for (const use of result.derived) {
|
|
206
|
+
if (!derived.has(use.key)) derived.set(use.key, use);
|
|
207
|
+
if (factsOf(use.key) !== undefined && !reported.has(`taken:${use.key}`)) {
|
|
208
|
+
reported.add(`taken:${use.key}`);
|
|
209
|
+
diagnostics.push({
|
|
210
|
+
...at,
|
|
211
|
+
code: 'derived-key-taken',
|
|
212
|
+
token: use.key,
|
|
213
|
+
message: message(lesson.lessonId, 'derived-key-taken', {
|
|
214
|
+
key: use.key,
|
|
215
|
+
token: use.key,
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const item of result.indices) {
|
|
222
|
+
pushFindings(item, at, lesson.lessonId, 'body', factsOf, reported, diagnostics);
|
|
223
|
+
for (const running of item.running) {
|
|
224
|
+
// A letter with no card at all is `index-explained`'s, or N4's.
|
|
225
|
+
if (running.key === null && running.cover === null) continue;
|
|
226
|
+
occurrences.push({
|
|
227
|
+
letter: running.letter,
|
|
228
|
+
line,
|
|
229
|
+
display,
|
|
230
|
+
mathLine: mathLineAt(latex, running.start),
|
|
231
|
+
token: item.token,
|
|
232
|
+
ownKey: running.key,
|
|
233
|
+
coverKey: running.cover,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// `index-range`: once per running letter per page.
|
|
240
|
+
const byLetter = new Map<string, Occurrence[]>();
|
|
241
|
+
for (const occurrence of occurrences) {
|
|
242
|
+
const list = byLetter.get(occurrence.letter) ?? [];
|
|
243
|
+
list.push(occurrence);
|
|
244
|
+
byLetter.set(occurrence.letter, list);
|
|
245
|
+
}
|
|
246
|
+
for (const [letter, list] of byLetter) {
|
|
247
|
+
const declared = list.some((occurrence) =>
|
|
248
|
+
[occurrence.ownKey, occurrence.coverKey].some((key) => {
|
|
249
|
+
const facts = factsOf(key);
|
|
250
|
+
return facts?.range !== undefined || facts?.fixedIndex === true;
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
if (declared) continue;
|
|
254
|
+
const firstDisplay = list.find((occurrence) => occurrence.display);
|
|
255
|
+
if (
|
|
256
|
+
firstDisplay &&
|
|
257
|
+
quadClauses(firstDisplay.mathLine).some((clause) => statesRange(clause, letter))
|
|
258
|
+
) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const [first] = list;
|
|
262
|
+
if (!first) continue;
|
|
263
|
+
const card = first.ownKey ?? first.coverKey;
|
|
264
|
+
diagnostics.push({
|
|
265
|
+
context: 'body',
|
|
266
|
+
severity: 'error',
|
|
267
|
+
code: 'index-range',
|
|
268
|
+
file,
|
|
269
|
+
line: first.line,
|
|
270
|
+
token: letter,
|
|
271
|
+
message: message(lesson.lessonId, 'index-range', {
|
|
272
|
+
letter,
|
|
273
|
+
token: first.token,
|
|
274
|
+
where: card === null ? `the card of ${letter}` : `the card ${card}`,
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (derived.size > 0) derivedByLesson.set(lesson.lessonId, [...derived.values()]);
|
|
280
|
+
|
|
281
|
+
// A notation.local formula, against the table the formula gate uses.
|
|
282
|
+
for (const definition of lesson.localDefinitions) {
|
|
283
|
+
if (!definition.formula || indexOfEntry(definition.notation) !== undefined) continue;
|
|
284
|
+
checkFormula(
|
|
285
|
+
definition.formula,
|
|
286
|
+
dedupeRows([...scope, ...definition.glosses]),
|
|
287
|
+
`${lesson.lessonId}: notation.local ${definition.key} formula`,
|
|
288
|
+
file,
|
|
289
|
+
factsOf,
|
|
290
|
+
reported,
|
|
291
|
+
diagnostics,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// A shared entry's formula, against its own table (D15). Each formula is
|
|
297
|
+
// its own place: a form two formulas leave unexplained is reported in both.
|
|
298
|
+
const cards = new Map<string, GlyphScopeEntry>(entries);
|
|
299
|
+
for (const entry of entries.values()) {
|
|
300
|
+
if (!entry.formula || indexOfEntry(entry.notation) !== undefined) continue;
|
|
301
|
+
checkFormula(
|
|
302
|
+
entry.formula,
|
|
303
|
+
formulaGlyphScope({
|
|
304
|
+
key: entry.key,
|
|
305
|
+
notation: entry.notation,
|
|
306
|
+
formula: entry.formula,
|
|
307
|
+
glosses: entry.glosses ?? [],
|
|
308
|
+
cardsByKey: cards,
|
|
309
|
+
}),
|
|
310
|
+
`notation ${entry.key} formula`,
|
|
311
|
+
entry.source?.file ?? `content/notation/${entry.key}.md`,
|
|
312
|
+
(key) => (key === null ? undefined : entries.get(key)),
|
|
313
|
+
new Set<string>(),
|
|
314
|
+
diagnostics,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Assessment math renders on its lesson's page, with that page's table.
|
|
319
|
+
const lessonById = new Map(lessons.map((lesson) => [lesson.lessonId, lesson]));
|
|
320
|
+
const assessmentById = new Map(
|
|
321
|
+
(extras.assessments ?? []).map((assessment) => [assessment.id, assessment]),
|
|
322
|
+
);
|
|
323
|
+
for (const link of extras.lessonAssessments ?? []) {
|
|
324
|
+
const lesson = lessonById.get(link.lessonId);
|
|
325
|
+
if (!lesson) continue;
|
|
326
|
+
const scope = pageScope(lesson, entries);
|
|
327
|
+
const local = new Map<string, IndexEntryFacts>(
|
|
328
|
+
lesson.localDefinitions.map((definition) => [definition.key, definition]),
|
|
329
|
+
);
|
|
330
|
+
const factsOf = (key: string | null): IndexEntryFacts | undefined =>
|
|
331
|
+
key === null ? undefined : (local.get(key) ?? entries.get(key));
|
|
332
|
+
for (const assessmentId of link.assessmentIds) {
|
|
333
|
+
const assessment = assessmentById.get(assessmentId);
|
|
334
|
+
const reported = new Set<string>();
|
|
335
|
+
for (const item of assessment?.items ?? []) {
|
|
336
|
+
for (const field of [
|
|
337
|
+
item.prompt,
|
|
338
|
+
item.explanation,
|
|
339
|
+
...(item.options ?? []).map((option) => option.label),
|
|
340
|
+
]) {
|
|
341
|
+
if (typeof field !== 'string') continue;
|
|
342
|
+
for (const span of mathSpans(field)) {
|
|
343
|
+
let result;
|
|
344
|
+
try {
|
|
345
|
+
result = resolveMathGlyphs(span.latex, scope, { derive: true });
|
|
346
|
+
} catch {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const at = {
|
|
350
|
+
context: 'assessment' as const,
|
|
351
|
+
severity: 'error' as const,
|
|
352
|
+
file: `${assessmentId} (${item.id ?? '?'})`,
|
|
353
|
+
line: span.line,
|
|
354
|
+
};
|
|
355
|
+
for (const indexed of result.indices) {
|
|
356
|
+
pushFindings(
|
|
357
|
+
indexed,
|
|
358
|
+
at,
|
|
359
|
+
`${link.lessonId}: assessment ${assessmentId}`,
|
|
360
|
+
'assessment',
|
|
361
|
+
factsOf,
|
|
362
|
+
reported,
|
|
363
|
+
diagnostics,
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { diagnostics, derivedByLesson };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** `index-explained` or `index-swallowed` for one indexed symbol, once per form. */
|
|
376
|
+
function pushFindings(
|
|
377
|
+
item: IndexedSymbol,
|
|
378
|
+
at: Omit<MathGateDiagnostic, 'code' | 'message'>,
|
|
379
|
+
owner: string,
|
|
380
|
+
place: Place,
|
|
381
|
+
factsOf: (key: string | null) => IndexEntryFacts | undefined,
|
|
382
|
+
reported: Set<string>,
|
|
383
|
+
diagnostics: MathGateDiagnostic[],
|
|
384
|
+
): void {
|
|
385
|
+
const token = item.token.replace(/\s+/g, '');
|
|
386
|
+
if (item.how === 'swallowed' && !reported.has(`swallowed:${token}`)) {
|
|
387
|
+
reported.add(`swallowed:${token}`);
|
|
388
|
+
diagnostics.push({
|
|
389
|
+
...at,
|
|
390
|
+
code: 'index-swallowed',
|
|
391
|
+
token: item.token,
|
|
392
|
+
message: message(owner, 'index-swallowed', {
|
|
393
|
+
key: item.key ?? '',
|
|
394
|
+
token: item.token,
|
|
395
|
+
notation: factsOf(item.key)?.notation ?? '',
|
|
396
|
+
index: item.index,
|
|
397
|
+
}),
|
|
398
|
+
});
|
|
399
|
+
} else if (item.how === 'composed' && !item.explained && !reported.has(`explained:${token}`)) {
|
|
400
|
+
reported.add(`explained:${token}`);
|
|
401
|
+
diagnostics.push({
|
|
402
|
+
...at,
|
|
403
|
+
code: 'index-explained',
|
|
404
|
+
token: item.token,
|
|
405
|
+
message: message(
|
|
406
|
+
owner,
|
|
407
|
+
'index-explained',
|
|
408
|
+
{
|
|
409
|
+
token: item.token,
|
|
410
|
+
index: item.index.replace(/^\{([\s\S]*)\}$/, '$1'),
|
|
411
|
+
parts: item.unexplained.map((part) => JSON.stringify(part)).join(', '),
|
|
412
|
+
base: String(item.baseExplained),
|
|
413
|
+
},
|
|
414
|
+
place,
|
|
415
|
+
),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Rule N23 in one notation formula: derived cards do not compose there. */
|
|
421
|
+
function checkFormula(
|
|
422
|
+
formula: string,
|
|
423
|
+
scope: readonly ScopeEntry[],
|
|
424
|
+
owner: string,
|
|
425
|
+
file: string,
|
|
426
|
+
factsOf: (key: string | null) => IndexEntryFacts | undefined,
|
|
427
|
+
reported: Set<string>,
|
|
428
|
+
diagnostics: MathGateDiagnostic[],
|
|
429
|
+
): void {
|
|
430
|
+
let result;
|
|
431
|
+
try {
|
|
432
|
+
result = resolveMathGlyphs(formula, [...scope]);
|
|
433
|
+
} catch {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const at = { context: 'formula' as const, severity: 'error' as const, file };
|
|
437
|
+
for (const item of result.indices) {
|
|
438
|
+
pushFindings(item, at, owner, 'formula', factsOf, reported, diagnostics);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** A glyph table with each key once, the first row winning. */
|
|
443
|
+
function dedupeRows(rows: readonly ScopeEntry[]): ScopeEntry[] {
|
|
444
|
+
const seen = new Set<string>();
|
|
445
|
+
return rows.filter((row) => !seen.has(row.key) && Boolean(seen.add(row.key)));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Rule N23's findings in one math source rendered outside a lesson body — a
|
|
450
|
+
* lab's or a diagram's, which fail the build as they render — as one line
|
|
451
|
+
* each: the form, what explains none of it, and the fix.
|
|
452
|
+
*/
|
|
453
|
+
export function indexFindings(indices: readonly IndexedSymbol[]): string[] {
|
|
454
|
+
return indices.flatMap((item) => {
|
|
455
|
+
if (item.how === 'swallowed') {
|
|
456
|
+
return [
|
|
457
|
+
`${item.token}: \\explain{${item.key ?? ''}}{…} names an entry whose latex has no ` +
|
|
458
|
+
`index, so its card cannot explain ${JSON.stringify(item.index)}; give the subscript an ` +
|
|
459
|
+
`index entry ({}_${item.index.length === 1 ? item.index : `{${item.index}}`}) and drop the wrap, or name an entry for the indexed symbol`,
|
|
460
|
+
];
|
|
461
|
+
}
|
|
462
|
+
if (item.how !== 'composed' || item.explained) return [];
|
|
463
|
+
return [
|
|
464
|
+
`${item.token}: no card explains ${item.unexplained
|
|
465
|
+
.map((part) => JSON.stringify(part))
|
|
466
|
+
.join(', ')}; give the indexed symbol its own entry, a family entry (x_k), or an ` +
|
|
467
|
+
`index entry for the subscript ({}_0) in the page's notation`,
|
|
468
|
+
];
|
|
469
|
+
});
|
|
470
|
+
}
|
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
NotationRegistryInput,
|
|
25
25
|
SharedNotationDefinitionInput,
|
|
26
26
|
} from './types';
|
|
27
|
+
import { blankPropStrings } from './gate-prop-text';
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* The equation a finding sits in, when its math carries a `\label{eq:…}`.
|
|
@@ -99,16 +100,18 @@ const SEVERITY: Record<MathGateDiagnostic['context'], 'error' | 'warning'> = {
|
|
|
99
100
|
course: 'error',
|
|
100
101
|
};
|
|
101
102
|
|
|
102
|
-
interface AssessmentItemLike {
|
|
103
|
+
export interface AssessmentItemLike {
|
|
103
104
|
readonly id?: string;
|
|
104
105
|
readonly prompt?: string;
|
|
105
106
|
readonly explanation?: string;
|
|
107
|
+
/** A choice item's options; a label's math renders like the prompt's. */
|
|
108
|
+
readonly options?: readonly { readonly label?: string }[];
|
|
106
109
|
}
|
|
107
|
-
interface AssessmentLike {
|
|
110
|
+
export interface AssessmentLike {
|
|
108
111
|
readonly id: string;
|
|
109
112
|
readonly items?: readonly AssessmentItemLike[];
|
|
110
113
|
}
|
|
111
|
-
interface LessonAssessmentLink {
|
|
114
|
+
export interface LessonAssessmentLink {
|
|
112
115
|
readonly lessonId: string;
|
|
113
116
|
readonly assessmentIds: readonly string[];
|
|
114
117
|
}
|
|
@@ -136,9 +139,13 @@ export interface MathSpan {
|
|
|
136
139
|
readonly offset: number;
|
|
137
140
|
}
|
|
138
141
|
|
|
139
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* Every `$$…$$` then `$…$` span in `body`, outside code and quoted JSX props
|
|
144
|
+
* (plain text that never reaches KaTeX; invariant `prop-text`), with its
|
|
145
|
+
* position.
|
|
146
|
+
*/
|
|
140
147
|
export function mathSpans(body: string): MathSpan[] {
|
|
141
|
-
const searchable = stripCode(body);
|
|
148
|
+
const searchable = stripCode(blankPropStrings(body));
|
|
142
149
|
const spans: MathSpan[] = [];
|
|
143
150
|
for (const pattern of [DISPLAY_MATH, INLINE_MATH]) {
|
|
144
151
|
pattern.lastIndex = 0;
|
|
@@ -159,7 +166,24 @@ export function mathSpans(body: string): MathSpan[] {
|
|
|
159
166
|
return spans;
|
|
160
167
|
}
|
|
161
168
|
|
|
162
|
-
|
|
169
|
+
/**
|
|
170
|
+
* One row of a page's glyph table. `fixedIndex` (rule N23) tells the
|
|
171
|
+
* resolver that an entry's index is a fixed label, so it is no family.
|
|
172
|
+
*/
|
|
173
|
+
export type ScopeEntry = { key: string; notation: string; fixedIndex?: true };
|
|
174
|
+
|
|
175
|
+
/** A definition as a glyph-table row, `fixedIndex` kept. */
|
|
176
|
+
export function scopeEntry(definition: {
|
|
177
|
+
readonly key: string;
|
|
178
|
+
readonly notation: string;
|
|
179
|
+
readonly fixedIndex?: true;
|
|
180
|
+
}): ScopeEntry {
|
|
181
|
+
return {
|
|
182
|
+
key: definition.key,
|
|
183
|
+
notation: definition.notation,
|
|
184
|
+
...(definition.fixedIndex ? { fixedIndex: true as const } : {}),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
163
187
|
|
|
164
188
|
/**
|
|
165
189
|
* A lesson's flat glyph table: its own `notation.local` entries plus every
|
|
@@ -172,10 +196,7 @@ export function pageScope(
|
|
|
172
196
|
lesson: NotationLessonInput,
|
|
173
197
|
sharedByKey: ReadonlyMap<string, ScopeEntry>,
|
|
174
198
|
): ScopeEntry[] {
|
|
175
|
-
const local = lesson.localDefinitions.map(
|
|
176
|
-
key: definition.key,
|
|
177
|
-
notation: definition.notation,
|
|
178
|
-
}));
|
|
199
|
+
const local = lesson.localDefinitions.map(scopeEntry);
|
|
179
200
|
const localKeys = new Set(local.map((entry) => entry.key));
|
|
180
201
|
|
|
181
202
|
const referenced = new Set<string>();
|
|
@@ -187,7 +208,7 @@ export function pageScope(
|
|
|
187
208
|
for (const key of referenced) {
|
|
188
209
|
if (localKeys.has(key)) continue;
|
|
189
210
|
const shared = sharedByKey.get(key);
|
|
190
|
-
if (shared) scope.push(
|
|
211
|
+
if (shared) scope.push(scopeEntry(shared));
|
|
191
212
|
}
|
|
192
213
|
return scope;
|
|
193
214
|
}
|
|
@@ -227,6 +248,7 @@ function resolveInto(
|
|
|
227
248
|
out: MathGateDiagnostic[],
|
|
228
249
|
equation?: EquationLink,
|
|
229
250
|
suspended: readonly string[] = [],
|
|
251
|
+
derive = false,
|
|
230
252
|
): void {
|
|
231
253
|
const at = equation === undefined ? {} : { equation };
|
|
232
254
|
for (const diagnostic of lintDelimiterSizes(latex)) {
|
|
@@ -255,6 +277,7 @@ function resolveInto(
|
|
|
255
277
|
// D7: `\label{eq:…}` is an identity marker, not math KaTeX parses.
|
|
256
278
|
result = resolveMathGlyphs(stripEquationLabels(latex), [...scope], {
|
|
257
279
|
suspended,
|
|
280
|
+
derive,
|
|
258
281
|
});
|
|
259
282
|
} catch (error) {
|
|
260
283
|
out.push({
|
|
@@ -297,6 +320,11 @@ function resolveInto(
|
|
|
297
320
|
'font' in item && typeof item.font === 'string'
|
|
298
321
|
? ` ${token} is a different symbol from the plain letter: give it its own entry (latex: ${token}) or wrap it in \\explain{key}{${token}}.`
|
|
299
322
|
: '';
|
|
323
|
+
// Rule N23: an indexed entry with this base explains no bare letter.
|
|
324
|
+
const lenderHint =
|
|
325
|
+
'lenders' in item && Array.isArray(item.lenders) && item.lenders.length > 0
|
|
326
|
+
? ` ${item.lenders.join(', ')} ${item.lenders.length === 1 ? 'has' : 'have'} ${token} with a subscript, and an indexed entry explains its own instances only, never a bare ${token} (rule N4). If ${token} is a different object, give it its own entry (latex: ${token}); if it is shorthand for that entry, wrap it in \\explain{${item.lenders[0]}}{${token}}.`
|
|
327
|
+
: '';
|
|
300
328
|
out.push({
|
|
301
329
|
context,
|
|
302
330
|
code: 'unresolved-glyph',
|
|
@@ -305,11 +333,45 @@ function resolveInto(
|
|
|
305
333
|
line,
|
|
306
334
|
token,
|
|
307
335
|
...at,
|
|
308
|
-
message: `Unresolved notation ${JSON.stringify(token)} in ${context} math.${fontHint} ${fix}`,
|
|
336
|
+
message: `Unresolved notation ${JSON.stringify(token)} in ${context} math.${fontHint}${lenderHint} ${fix}`,
|
|
309
337
|
});
|
|
310
338
|
}
|
|
311
339
|
}
|
|
312
340
|
|
|
341
|
+
/**
|
|
342
|
+
* The `$…$` pairs remark-math reads as math that hold an amount of money
|
|
343
|
+
* instead: `$5 to $6`, `$5–$10`, `$5, and $x`. remark-math pairs any two
|
|
344
|
+
* single `$` on a line, left to right; `mathSpans` follows Pandoc (no space
|
|
345
|
+
* before a closing `$`, no digit after it) and skips them, so such a pair was
|
|
346
|
+
* neither checked as math nor shown as money — the page printed "5 to 6", or
|
|
347
|
+
* the build failed on the letters of "and". A pair is money when its content
|
|
348
|
+
* starts with a digit and either ends in a space or runs straight into a
|
|
349
|
+
* digit after the closing `$`.
|
|
350
|
+
*/
|
|
351
|
+
export function currencyDollars(body: string): MathSpan[] {
|
|
352
|
+
const searchable = stripCode(blankPropStrings(body)).replace(DISPLAY_MATH, (block) =>
|
|
353
|
+
block.replace(/[^\n]/g, ' '),
|
|
354
|
+
);
|
|
355
|
+
const spans: MathSpan[] = [];
|
|
356
|
+
let lineStart = 0;
|
|
357
|
+
searchable.split('\n').forEach((text, index) => {
|
|
358
|
+
const dollars: number[] = [];
|
|
359
|
+
for (let at = 0; at < text.length; at += 1) {
|
|
360
|
+
if (text[at] !== '$' || text[at - 1] === '\\' || text[at - 1] === '$' || text[at + 1] === '$') continue;
|
|
361
|
+
dollars.push(at);
|
|
362
|
+
}
|
|
363
|
+
for (let pair = 0; pair + 1 < dollars.length; pair += 2) {
|
|
364
|
+
const open = dollars[pair]!;
|
|
365
|
+
const close = dollars[pair + 1]!;
|
|
366
|
+
const latex = text.slice(open + 1, close);
|
|
367
|
+
if (!/^\d/.test(latex) || !(/\s$/.test(latex) || /^\d/.test(text.slice(close + 1)))) continue;
|
|
368
|
+
spans.push({ latex, line: index + 1, column: open + 2, offset: lineStart + open + 1 });
|
|
369
|
+
}
|
|
370
|
+
lineStart += text.length + 1;
|
|
371
|
+
});
|
|
372
|
+
return spans;
|
|
373
|
+
}
|
|
374
|
+
|
|
313
375
|
const BODY_FIX =
|
|
314
376
|
'Introduce the symbol with [[key]], define it in notation.local, or wrap it in \\explain{key}{latex}.';
|
|
315
377
|
function dedupeScope(scope: readonly GlyphScopeEntry[]): GlyphScopeEntry[] {
|
|
@@ -424,9 +486,28 @@ export function gateContentMath(input: {
|
|
|
424
486
|
diagnostics,
|
|
425
487
|
equationLinkFor(lesson.lessonId, span.latex),
|
|
426
488
|
suspended,
|
|
489
|
+
// Derived cards (rule N23) are lesson-body math, like the render path.
|
|
490
|
+
true,
|
|
427
491
|
);
|
|
428
492
|
}
|
|
429
493
|
|
|
494
|
+
for (const span of currencyDollars(lesson.body)) {
|
|
495
|
+
const shown = span.latex.replace(/\s+$/, '');
|
|
496
|
+
diagnostics.push({
|
|
497
|
+
context: 'body',
|
|
498
|
+
code: 'currency-dollar',
|
|
499
|
+
severity: SEVERITY.body,
|
|
500
|
+
file,
|
|
501
|
+
line: span.line + bodyOffset,
|
|
502
|
+
column: span.column,
|
|
503
|
+
token: `$${span.latex}$`,
|
|
504
|
+
message:
|
|
505
|
+
`"$${span.latex}$" reads as math: remark-math pairs any two dollars on a line, so the ` +
|
|
506
|
+
`page shows "${shown}" without them. Write a currency dollar as \\$ (\\$5 to \\$6), ` +
|
|
507
|
+
'or the amount in words.',
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
430
511
|
for (const definition of lesson.localDefinitions) {
|
|
431
512
|
if (!definition.formula) continue;
|
|
432
513
|
// `scope` already carries every page-local entry, including this one, so
|
|
@@ -491,7 +572,11 @@ export function gateContentMath(input: {
|
|
|
491
572
|
const assessment = assessmentById.get(assessmentId);
|
|
492
573
|
if (!assessment?.items) continue;
|
|
493
574
|
for (const item of assessment.items) {
|
|
494
|
-
for (const field of [
|
|
575
|
+
for (const field of [
|
|
576
|
+
item.prompt,
|
|
577
|
+
item.explanation,
|
|
578
|
+
...(item.options ?? []).map((option) => option.label),
|
|
579
|
+
]) {
|
|
495
580
|
if (typeof field !== 'string') continue;
|
|
496
581
|
for (const span of mathSpans(field)) {
|
|
497
582
|
resolveInto(
|
|
@@ -503,6 +588,10 @@ export function gateContentMath(input: {
|
|
|
503
588
|
span.column,
|
|
504
589
|
ASSESSMENT_FIX,
|
|
505
590
|
diagnostics,
|
|
591
|
+
undefined,
|
|
592
|
+
[],
|
|
593
|
+
// Page math, rendered with derived cards (rule N23).
|
|
594
|
+
true,
|
|
506
595
|
);
|
|
507
596
|
}
|
|
508
597
|
}
|