explico 0.7.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1409 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +54 -3
- package/dist/compiler/cli.js +80 -6
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +84 -8
- package/dist/compiler/course-config.d.ts +3 -194
- package/dist/compiler/course-config.js +78 -5
- package/dist/compiler/exports/standalone-html.d.ts +6 -1
- package/dist/compiler/knowledge-version.js +1 -1
- package/dist/compiler/lesson-derivation.d.ts +27 -10
- package/dist/compiler/lesson-derivation.js +23 -8
- package/dist/compiler/logging.d.ts +14 -1
- package/dist/compiler/logging.js +6 -2
- package/dist/compiler/manifest.d.ts +204 -31
- package/dist/compiler/manifest.js +270 -55
- package/dist/compiler/resolution-report.d.ts +6 -1
- package/dist/course-config-Dsnri9Tk.d.ts +464 -0
- package/dist/reference/base-accents.d.mts +26 -0
- package/dist/reference/base-accents.mjs +31 -0
- package/dist/reference/base-roman-labels.d.mts +2 -2
- package/dist/reference/base-roman-labels.mjs +2 -2
- package/dist/reference/consistency.d.ts +14 -2
- package/dist/reference/consistency.js +108 -4
- package/dist/reference/delimiter-size.d.ts +21 -11
- package/dist/reference/delimiter-size.js +15 -39
- package/dist/reference/diagrams-validate.d.ts +7 -2
- package/dist/reference/diagrams-validate.js +9 -1
- package/dist/reference/equations-validate.d.ts +8 -5
- package/dist/reference/equations-validate.js +1 -0
- package/dist/reference/equations.d.mts +69 -34
- package/dist/reference/equations.mjs +90 -44
- package/dist/reference/examples-validate.d.ts +31 -0
- package/dist/reference/examples-validate.js +48 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/figures-validate.js +4 -1
- package/dist/reference/folds-validate.d.ts +33 -0
- package/dist/reference/folds-validate.js +32 -0
- package/dist/reference/gate-abbreviations.d.ts +3 -1
- package/dist/reference/gate-abbreviations.js +3 -2
- package/dist/reference/gate-course-rules.d.ts +32 -0
- package/dist/reference/gate-course-rules.js +85 -0
- package/dist/reference/gate-heading-length.d.ts +5 -2
- package/dist/reference/gate-heading-length.js +47 -23
- package/dist/reference/gate-heading-structure.d.ts +41 -0
- package/dist/reference/gate-heading-structure.js +113 -0
- package/dist/reference/gate-math-punctuation.d.ts +71 -0
- package/dist/reference/gate-math-punctuation.js +251 -0
- package/dist/reference/gate-math.d.ts +44 -6
- package/dist/reference/gate-math.js +63 -10
- package/dist/reference/gate-part-pages.d.ts +50 -0
- package/dist/reference/gate-part-pages.js +118 -0
- package/dist/reference/gate-prose-notation.d.ts +32 -11
- package/dist/reference/gate-prose-notation.js +30 -8
- package/dist/reference/katex-delimiters.d.mts +15 -0
- package/dist/reference/katex-delimiters.mjs +138 -0
- package/dist/reference/katex-options.mjs +4 -0
- package/dist/reference/keyed-float-validate.d.ts +27 -10
- package/dist/reference/keyed-float-validate.js +15 -1
- package/dist/reference/keyed-jsx-floats.d.mts +11 -5
- package/dist/reference/keyed-jsx-floats.mjs +73 -20
- package/dist/reference/knowledge-checks-validate.d.ts +1 -1
- package/dist/reference/knowledge-checks.d.mts +3 -1
- package/dist/reference/knowledge-checks.mjs +9 -4
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-slot.d.ts +34 -0
- package/dist/reference/lab-math-slot.js +6 -0
- package/dist/reference/math-glyphs.d.mts +80 -4
- package/dist/reference/math-glyphs.mjs +588 -39
- package/dist/reference/page-notation.d.ts +6 -1
- package/dist/reference/raw-floats-validate.js +1 -1
- package/dist/reference/raw-floats.mjs +1 -1
- package/dist/reference/rehype-heading-anchors.d.mts +14 -0
- package/dist/reference/rehype-heading-anchors.mjs +116 -17
- package/dist/reference/rehype-table-header-labels.d.mts +6 -1
- package/dist/reference/rehype-table-header-labels.mjs +21 -3
- package/dist/reference/remark-asides.d.mts +3 -0
- package/dist/reference/remark-asides.mjs +180 -0
- package/dist/reference/remark-memo.d.mts +3 -1
- package/dist/reference/remark-memo.mjs +42 -11
- package/dist/reference/remark-notation.d.mts +60 -10
- package/dist/reference/remark-notation.mjs +387 -36
- package/dist/reference/render-lab-math.d.ts +4 -26
- package/dist/reference/render-lab-math.js +5 -5
- package/dist/reference/sections-validate.d.ts +27 -0
- package/dist/reference/sections-validate.js +41 -0
- package/dist/reference/sections.d.mts +97 -0
- package/dist/reference/sections.mjs +306 -0
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +24 -1
- package/dist/reference/unit-words.d.ts +37 -0
- package/dist/reference/unit-words.js +170 -0
- package/dist/rules/catalog.d.ts +176 -0
- package/dist/rules/catalog.js +983 -0
- package/dist/rules/course-rules.d.ts +115 -0
- package/dist/rules/course-rules.js +292 -0
- package/dist/rules/course-rules.schema.json +138 -0
- package/dist/rules/in-effect.d.ts +53 -0
- package/dist/rules/in-effect.js +142 -0
- package/dist/rules/rules.schema.json +1072 -0
- package/dist/rules/settings.d.ts +3 -0
- package/dist/rules/settings.js +290 -0
- package/dist/rules/waivers.d.ts +53 -0
- package/dist/rules/waivers.js +132 -0
- package/dist/session/PreferenceStore.d.ts +5 -0
- package/dist/session/PreferenceStore.js +9 -0
- package/docs/VERSIONING.md +2 -2
- package/docs/authoring/README.md +232 -40
- package/docs/authoring/agents.md +55 -9
- package/docs/authoring/components.md +178 -22
- package/docs/authoring/migrating.md +1324 -2
- package/docs/authoring/notation.md +616 -344
- package/docs/authoring/prose.md +551 -0
- package/docs/authoring/retired.md +35 -0
- package/docs/authoring/structure-rules.md +422 -40
- package/docs/authoring/structure.md +445 -62
- package/docs/authoring/versioning.md +20 -28
- package/docs/authoring/visualizations.md +142 -20
- package/docs/katex-bracket-scaling.md +191 -0
- package/package.json +3 -2
- package/src/compiler/cli.ts +139 -6
- package/src/compiler/collections.ts +144 -10
- package/src/compiler/course-config.ts +273 -34
- package/src/compiler/knowledge-version.ts +1 -1
- package/src/compiler/lesson-derivation.ts +54 -15
- package/src/compiler/logging.ts +21 -4
- package/src/compiler/manifest.ts +573 -104
- package/src/components/BlockHeading.astro +113 -0
- package/src/components/CourseContents.astro +221 -0
- package/src/components/CurriculumMap.astro +5 -6
- package/src/components/FloatIndex.astro +200 -0
- package/src/components/HoverPanel.astro +431 -79
- package/src/components/PartBridge.astro +27 -0
- package/src/components/PartOverview.astro +401 -0
- package/src/components/PartScope.astro +27 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
- package/src/components/assessments/AssessmentRunner.tsx +8 -5
- package/src/components/assessments/AssessmentSet.astro +23 -26
- package/src/components/block-heading.ts +146 -0
- package/src/components/card-arrow.ts +243 -0
- package/src/components/card-drag.ts +631 -0
- package/src/components/card-jump.ts +45 -0
- package/src/components/card-manager.ts +1589 -0
- package/src/components/card-memory.ts +177 -0
- package/src/components/card-placement.ts +504 -0
- package/src/components/card-side.ts +336 -0
- package/src/components/card-travel.ts +197 -0
- package/src/components/citation/CitationLayer.astro +77 -76
- package/src/components/diagram/Diagram.astro +161 -316
- package/src/components/disclosure/course-disclosure.ts +24 -0
- package/src/components/disclosure/hover-preview.ts +122 -42
- package/src/components/disclosure/persisted-disclosure.ts +3 -3
- package/src/components/disclosure/print-as-shown.ts +252 -0
- package/src/components/examples/CompactExample.astro +3 -1
- package/src/components/examples/CompactExamples.astro +101 -94
- package/src/components/figure/Figure.astro +174 -274
- package/src/components/figure/FloatCaption.astro +87 -0
- package/src/components/figure/figure-state.ts +58 -0
- package/src/components/figure/figure-tools.ts +87 -0
- package/src/components/float-name.ts +101 -0
- package/src/components/fold/Fold.astro +125 -0
- package/src/components/fold/fold-id.ts +40 -0
- package/src/components/memo/Memo.astro +162 -231
- package/src/components/memo/ShortNote.astro +6 -6
- package/src/components/notation/NotationFormula.astro +5 -1
- package/src/components/notation/NotationGlossary.astro +24 -14
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/notation/render-notation-math.ts +111 -10
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +22 -6
- package/src/components/starlight/CollapsibleSections.astro +207 -93
- package/src/components/starlight/DiagramEnhancer.astro +15 -138
- package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
- package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
- package/src/components/starlight/LayoutHeader.astro +298 -19
- package/src/components/starlight/LayoutPageSidebar.astro +99 -12
- package/src/components/starlight/LayoutPageTitle.astro +120 -0
- package/src/components/starlight/LayoutSidebar.astro +229 -11
- package/src/components/starlight/LessonExportActions.astro +53 -72
- package/src/components/starlight/LessonFooter.astro +26 -4
- package/src/components/starlight/section-parts.ts +15 -2
- package/src/components/starlight/section-summary.ts +181 -0
- package/src/components/table/Table.astro +95 -76
- package/src/components/table/TableOverflowExpander.astro +65 -28
- package/src/content-config.ts +40 -4
- package/src/reference/base-accents.mjs +31 -0
- package/src/reference/base-roman-labels.mjs +2 -2
- package/src/reference/consistency.ts +176 -5
- package/src/reference/delimiter-size.ts +40 -61
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +90 -44
- package/src/reference/examples-validate.ts +97 -0
- package/src/reference/figures-validate.ts +6 -1
- package/src/reference/folds-validate.ts +65 -0
- package/src/reference/gate-abbreviations.ts +3 -2
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +68 -37
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +119 -12
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +74 -13
- package/src/reference/katex-delimiters.mjs +138 -0
- package/src/reference/katex-options.mjs +4 -0
- package/src/reference/keyed-float-validate.ts +45 -11
- package/src/reference/keyed-jsx-floats.mjs +73 -20
- package/src/reference/knowledge-checks-validate.ts +1 -1
- package/src/reference/knowledge-checks.mjs +9 -4
- package/src/reference/lab-math-slot.ts +36 -0
- package/src/reference/math-glyphs.mjs +588 -39
- package/src/reference/raw-floats-validate.ts +1 -1
- package/src/reference/raw-floats.mjs +1 -1
- package/src/reference/rehype-heading-anchors.mjs +116 -17
- package/src/reference/rehype-table-header-labels.mjs +21 -3
- package/src/reference/remark-asides.mjs +180 -0
- package/src/reference/remark-memo.mjs +42 -11
- package/src/reference/remark-notation.mjs +387 -36
- package/src/reference/render-lab-math.ts +17 -32
- package/src/reference/sections-validate.ts +81 -0
- package/src/reference/sections.mjs +306 -0
- package/src/reference/tables-validate.ts +2 -1
- package/src/reference/types.ts +24 -0
- package/src/reference/unit-words.ts +233 -0
- package/src/rules/catalog.ts +1298 -0
- package/src/rules/course-rules.ts +456 -0
- package/src/rules/in-effect.ts +235 -0
- package/src/rules/settings.ts +448 -0
- package/src/rules/waivers.ts +249 -0
- package/src/session/PreferenceStore.ts +15 -0
- package/src/styles/global.css +1471 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- package/src/components/popup-stack.ts +0 -345
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import { NotationRegistryInput, NotationLessonInput } from './types.js';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* The equation a finding sits in, when its math carries a `\label{eq:…}`.
|
|
5
|
+
*
|
|
6
|
+
* A location tells you where to edit; this tells you *what you are looking
|
|
7
|
+
* at*. A size-only N17 finding inside a six-line `aligned` block is otherwise
|
|
8
|
+
* anonymous — the message names delimiters, not the result they belong to —
|
|
9
|
+
* so the key and the rendered page's anchor both travel with the diagnostic.
|
|
10
|
+
*/
|
|
11
|
+
interface EquationLink {
|
|
12
|
+
/** The label's key, without the `eq:` prefix. */
|
|
13
|
+
readonly key: string;
|
|
14
|
+
/** Where the reader sees it, e.g. `/l1-ar/ar1-basics/#eq-ar1-variance`. */
|
|
15
|
+
readonly href: string;
|
|
16
|
+
}
|
|
3
17
|
interface MathGateDiagnostic {
|
|
4
|
-
readonly context: 'body' | 'formula' | 'assessment' | 'prose' | 'abbreviation' | 'structure';
|
|
18
|
+
readonly context: 'body' | 'formula' | 'assessment' | 'prose' | 'abbreviation' | 'structure' | 'course';
|
|
5
19
|
/**
|
|
6
20
|
* Body / component-slot math blocks the build (the render path needs every
|
|
7
21
|
* glyph resolved for the KaTeX trust callback). `formula` joined it at D15:
|
|
@@ -20,17 +34,24 @@ interface MathGateDiagnostic {
|
|
|
20
34
|
* `gate-abbreviations.ts`) is the same "ships straight as an error" story
|
|
21
35
|
* for a different shape of plain-text token — an unexplained acronym or
|
|
22
36
|
* dotted abbreviation, rather than a mis-typed quantity/unit. `structure`
|
|
23
|
-
* (S1, `gate-heading-length.ts`) is unrelated to
|
|
24
|
-
* long a heading or a keyed float's title may run
|
|
37
|
+
* (S1, `gate-heading-length.ts`; S2, `gate-part-pages.ts`) is unrelated to
|
|
38
|
+
* notation — it caps how long a heading or a keyed float's title may run,
|
|
39
|
+
* and requires a part page's bridge and scope paragraphs — but reuses this same
|
|
25
40
|
* diagnostic shape and the one `manifest.diagnostics.math` array rather
|
|
26
|
-
* than adding a second array the CLI has to know about.
|
|
41
|
+
* than adding a second array the CLI has to know about. `course`
|
|
42
|
+
* (`gate-course-rules.ts`) is a rule the *course* wrote in `courseRules`, not
|
|
43
|
+
* one the engine ships; its severity is the course's choice, so it is the one
|
|
44
|
+
* context whose severity is not fixed here.
|
|
27
45
|
*/
|
|
28
46
|
readonly severity: 'error' | 'warning';
|
|
29
|
-
|
|
47
|
+
/** Owned by exactly one rule or invariant in `src/rules/catalog.ts`. */
|
|
48
|
+
readonly code: string;
|
|
30
49
|
readonly file: string;
|
|
31
50
|
readonly line?: number;
|
|
32
51
|
readonly column?: number;
|
|
33
52
|
readonly token?: string;
|
|
53
|
+
/** Present when the offending math carries a `\label{eq:…}`. */
|
|
54
|
+
readonly equation?: EquationLink;
|
|
34
55
|
readonly message: string;
|
|
35
56
|
}
|
|
36
57
|
interface AssessmentItemLike {
|
|
@@ -46,6 +67,15 @@ interface LessonAssessmentLink {
|
|
|
46
67
|
readonly lessonId: string;
|
|
47
68
|
readonly assessmentIds: readonly string[];
|
|
48
69
|
}
|
|
70
|
+
interface MathSpan {
|
|
71
|
+
readonly latex: string;
|
|
72
|
+
readonly line: number;
|
|
73
|
+
readonly column: number;
|
|
74
|
+
/** Index of `latex` in the text it was cut from. */
|
|
75
|
+
readonly offset: number;
|
|
76
|
+
}
|
|
77
|
+
/** Every `$$…$$` then `$…$` span in `body`, outside code, with its position. */
|
|
78
|
+
declare function mathSpans(body: string): MathSpan[];
|
|
49
79
|
type ScopeEntry = {
|
|
50
80
|
key: string;
|
|
51
81
|
notation: string;
|
|
@@ -58,6 +88,14 @@ type ScopeEntry = {
|
|
|
58
88
|
* hand and risk drifting from what the completeness gate actually checks.
|
|
59
89
|
*/
|
|
60
90
|
declare function pageScope(lesson: NotationLessonInput, sharedByKey: ReadonlyMap<string, ScopeEntry>): ScopeEntry[];
|
|
91
|
+
/**
|
|
92
|
+
* The `\label{eq:…}` a math source carries, as a key plus the anchor the
|
|
93
|
+
* rendered page gives it. `EQ_LABEL_PATTERN` is global, so its `lastIndex` is
|
|
94
|
+
* reset before use — a stale one would skip the first label of the next span.
|
|
95
|
+
*/
|
|
96
|
+
declare function equationLinkFor(lessonId: string, latex: string): EquationLink | undefined;
|
|
97
|
+
/** Decorations a lesson suspends (`notation.suspend`, rule N22), by name. */
|
|
98
|
+
declare function suspendedOperators(lesson: NotationLessonInput): string[];
|
|
61
99
|
/**
|
|
62
100
|
* Resolve every rendered-math context against the page glyph table. Returns one
|
|
63
101
|
* diagnostic per unresolved token; an empty array means the gate passed.
|
|
@@ -68,4 +106,4 @@ declare function gateContentMath(input: {
|
|
|
68
106
|
readonly lessonAssessments: readonly LessonAssessmentLink[];
|
|
69
107
|
}): MathGateDiagnostic[];
|
|
70
108
|
|
|
71
|
-
export { type MathGateDiagnostic, type ScopeEntry, gateContentMath, pageScope };
|
|
109
|
+
export { type EquationLink, type MathGateDiagnostic, type MathSpan, type ScopeEntry, equationLinkFor, gateContentMath, mathSpans, pageScope, suspendedOperators };
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { resolveMathGlyphs, GlyphResolutionError } from "./math-glyphs.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
EQ_LABEL_PATTERN,
|
|
4
|
+
equationAnchorId,
|
|
5
|
+
stripEquationLabels
|
|
6
|
+
} from "./equations.mjs";
|
|
3
7
|
import { lintDelimiterSizes } from "./delimiter-size.js";
|
|
4
8
|
import { formulaGlyphScope, unknownFormulaScopeKeys } from "./gloss.js";
|
|
5
9
|
const SEVERITY = {
|
|
@@ -8,7 +12,9 @@ const SEVERITY = {
|
|
|
8
12
|
assessment: "error",
|
|
9
13
|
prose: "error",
|
|
10
14
|
abbreviation: "error",
|
|
11
|
-
structure: "error"
|
|
15
|
+
structure: "error",
|
|
16
|
+
// Never read: `gateCourseRules` sets the severity the course chose.
|
|
17
|
+
course: "error"
|
|
12
18
|
};
|
|
13
19
|
const TERM_KEY = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
|
|
14
20
|
const EXPLAIN_KEY = /\\explain\s*\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}/g;
|
|
@@ -34,7 +40,8 @@ function mathSpans(body) {
|
|
|
34
40
|
spans.push({
|
|
35
41
|
latex,
|
|
36
42
|
line: lineOf(body, latexOffset),
|
|
37
|
-
column: latexOffset - lastNewline
|
|
43
|
+
column: latexOffset - lastNewline,
|
|
44
|
+
offset: latexOffset
|
|
38
45
|
});
|
|
39
46
|
}
|
|
40
47
|
}
|
|
@@ -58,7 +65,20 @@ function pageScope(lesson, sharedByKey) {
|
|
|
58
65
|
}
|
|
59
66
|
return scope;
|
|
60
67
|
}
|
|
61
|
-
function
|
|
68
|
+
function equationLinkFor(lessonId, latex) {
|
|
69
|
+
EQ_LABEL_PATTERN.lastIndex = 0;
|
|
70
|
+
const match = EQ_LABEL_PATTERN.exec(latex);
|
|
71
|
+
EQ_LABEL_PATTERN.lastIndex = 0;
|
|
72
|
+
if (!match) return void 0;
|
|
73
|
+
const key = match[1];
|
|
74
|
+
const slug = lessonId.replace(/\./g, "/");
|
|
75
|
+
return { key, href: `/${slug}/#${equationAnchorId({ key })}` };
|
|
76
|
+
}
|
|
77
|
+
function suspendedOperators(lesson) {
|
|
78
|
+
return (lesson.suspended ?? []).map((item) => item.operator);
|
|
79
|
+
}
|
|
80
|
+
function resolveInto(latex, scope, context, file, line, column, fix, out, equation, suspended = []) {
|
|
81
|
+
const at = equation === void 0 ? {} : { equation };
|
|
62
82
|
for (const diagnostic of lintDelimiterSizes(latex)) {
|
|
63
83
|
const locatedLine = line === void 0 ? void 0 : line + diagnostic.line - 1;
|
|
64
84
|
const locatedColumn = column === void 0 ? void 0 : diagnostic.line === 1 ? column + diagnostic.column - 1 : diagnostic.column;
|
|
@@ -69,15 +89,21 @@ function resolveInto(latex, scope, context, file, line, column, fix, out) {
|
|
|
69
89
|
file,
|
|
70
90
|
...locatedLine === void 0 ? {} : { line: locatedLine },
|
|
71
91
|
...locatedColumn === void 0 ? {} : { column: locatedColumn },
|
|
92
|
+
...at,
|
|
72
93
|
message: diagnostic.message
|
|
73
94
|
});
|
|
74
95
|
}
|
|
75
96
|
let result;
|
|
76
97
|
try {
|
|
77
|
-
result = resolveMathGlyphs(stripEquationLabels(latex), [...scope]
|
|
98
|
+
result = resolveMathGlyphs(stripEquationLabels(latex), [...scope], {
|
|
99
|
+
suspended
|
|
100
|
+
});
|
|
78
101
|
} catch (error) {
|
|
79
102
|
out.push({
|
|
80
103
|
context,
|
|
104
|
+
// The resolver's own code (`unparenthesized-function`, `invalid-latex`,
|
|
105
|
+
// …) is what the rule catalog keys on; anything else is a KaTeX crash.
|
|
106
|
+
code: error instanceof GlyphResolutionError && typeof error.code === "string" ? error.code : "math-compile-error",
|
|
81
107
|
severity: SEVERITY[context],
|
|
82
108
|
file,
|
|
83
109
|
line,
|
|
@@ -85,14 +111,31 @@ function resolveInto(latex, scope, context, file, line, column, fix, out) {
|
|
|
85
111
|
});
|
|
86
112
|
return;
|
|
87
113
|
}
|
|
88
|
-
for (const
|
|
114
|
+
for (const item of result.unresolved) {
|
|
115
|
+
const { token } = item;
|
|
116
|
+
if ("suspended" in item && typeof item.suspended === "string") {
|
|
117
|
+
out.push({
|
|
118
|
+
context,
|
|
119
|
+
code: "suspended-operator-unresolved",
|
|
120
|
+
severity: SEVERITY[context],
|
|
121
|
+
file,
|
|
122
|
+
line,
|
|
123
|
+
token,
|
|
124
|
+
...at,
|
|
125
|
+
message: `On this page ${JSON.stringify(item.suspended)} is suspended (notation.suspend), so ${JSON.stringify(token)} is not ${JSON.stringify(item.suspended)} applied to a symbol: it needs its own notation entry (latex: ${token}) or an \\explain{key}{${token}} wrap.`
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const fontHint = "font" in item && typeof item.font === "string" ? ` ${token} is a different symbol from the plain letter: give it its own entry (latex: ${token}) or wrap it in \\explain{key}{${token}}.` : "";
|
|
89
130
|
out.push({
|
|
90
131
|
context,
|
|
132
|
+
code: "unresolved-glyph",
|
|
91
133
|
severity: SEVERITY[context],
|
|
92
134
|
file,
|
|
93
135
|
line,
|
|
94
136
|
token,
|
|
95
|
-
|
|
137
|
+
...at,
|
|
138
|
+
message: `Unresolved notation ${JSON.stringify(token)} in ${context} math.${fontHint} ${fix}`
|
|
96
139
|
});
|
|
97
140
|
}
|
|
98
141
|
}
|
|
@@ -116,6 +159,9 @@ function reportUnknownScopeKeys(definition, cardKeys, file, out) {
|
|
|
116
159
|
)) {
|
|
117
160
|
out.push({
|
|
118
161
|
context: "formula",
|
|
162
|
+
// Same failure the resolver reports for a body `\explain{key}` that
|
|
163
|
+
// names nothing — one code, one owner (`notation-references`).
|
|
164
|
+
code: "unknown-explicit-key",
|
|
119
165
|
severity: SEVERITY.formula,
|
|
120
166
|
file,
|
|
121
167
|
token: key,
|
|
@@ -156,16 +202,20 @@ function gateContentMath(input) {
|
|
|
156
202
|
for (const lesson of notationInput.lessons) {
|
|
157
203
|
const scope = pageScope(lesson, sharedByKey);
|
|
158
204
|
const file = lesson.source.file;
|
|
205
|
+
const suspended = suspendedOperators(lesson);
|
|
206
|
+
const bodyOffset = (lesson.bodyLine ?? 1) - 1;
|
|
159
207
|
for (const span of mathSpans(lesson.body)) {
|
|
160
208
|
resolveInto(
|
|
161
209
|
span.latex,
|
|
162
210
|
scope,
|
|
163
211
|
"body",
|
|
164
212
|
file,
|
|
165
|
-
span.line,
|
|
213
|
+
span.line + bodyOffset,
|
|
166
214
|
span.column,
|
|
167
215
|
BODY_FIX,
|
|
168
|
-
diagnostics
|
|
216
|
+
diagnostics,
|
|
217
|
+
equationLinkFor(lesson.lessonId, span.latex),
|
|
218
|
+
suspended
|
|
169
219
|
);
|
|
170
220
|
}
|
|
171
221
|
for (const definition of lesson.localDefinitions) {
|
|
@@ -243,6 +293,9 @@ function gateContentMath(input) {
|
|
|
243
293
|
return diagnostics;
|
|
244
294
|
}
|
|
245
295
|
export {
|
|
296
|
+
equationLinkFor,
|
|
246
297
|
gateContentMath,
|
|
247
|
-
|
|
298
|
+
mathSpans,
|
|
299
|
+
pageScope,
|
|
300
|
+
suspendedOperators
|
|
248
301
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { N as NormalizedPartPages } from '../course-config-Dsnri9Tk.js';
|
|
2
|
+
import { MathGateDiagnostic } from './gate-math.js';
|
|
3
|
+
import '../rules/catalog.js';
|
|
4
|
+
import '../rules/course-rules.js';
|
|
5
|
+
import './types.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The part-page gate (rule S2): a part's overview page
|
|
9
|
+
* (`content/docs/<dir>/index.mdx`) states, in two authored paragraphs, how
|
|
10
|
+
* the course got here and where this part goes —
|
|
11
|
+
*
|
|
12
|
+
* - `<PartBridge>` — how the earlier parts lead into this one. Required on
|
|
13
|
+
* every part after the first; optional on the first, which has no earlier
|
|
14
|
+
* part to link from.
|
|
15
|
+
* - `<PartScope>` — what this part itself covers. Required on every part page.
|
|
16
|
+
*
|
|
17
|
+
* Each must appear once, hold exactly one paragraph, and fall inside the
|
|
18
|
+
* course's rule-S2 word range (`rules.S2.*`, see `partPageLimits`); the bridge comes
|
|
19
|
+
* first. What a paragraph *says* is Review-only — the gate checks that the
|
|
20
|
+
* slot is there and sized, not that its prose is about the right thing. See
|
|
21
|
+
* docs/authoring/structure-rules.md#s2.
|
|
22
|
+
*
|
|
23
|
+
* A part without an overview page is not in scope: overview pages stay
|
|
24
|
+
* opt-in, and this gate only shapes the ones a course authors. Ships straight
|
|
25
|
+
* as an error, same as S1.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** One part overview page, as the manifest compiler loads it. */
|
|
29
|
+
interface PartPageInput {
|
|
30
|
+
readonly dir: string;
|
|
31
|
+
/** 1-based position in `courseConfig.parts`. */
|
|
32
|
+
readonly ordinal: number;
|
|
33
|
+
readonly file: string;
|
|
34
|
+
readonly body: string;
|
|
35
|
+
readonly bodyLine: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Words a reader reads in a paragraph of MDX prose. A math span, a `[[key]]`
|
|
39
|
+
* reference, or a link each count by what they render as: one token for the
|
|
40
|
+
* first two, the link text for the third. Citations and markup count nothing.
|
|
41
|
+
*/
|
|
42
|
+
declare function proseWordCount(markdown: string): number;
|
|
43
|
+
/**
|
|
44
|
+
* Check every part overview page for its S2 paragraphs. Returns one
|
|
45
|
+
* `MathGateDiagnostic` (context `'structure'`, code `part-page-paragraph`,
|
|
46
|
+
* always an error) per finding.
|
|
47
|
+
*/
|
|
48
|
+
declare function gatePartPages(pages: readonly PartPageInput[], limits: NormalizedPartPages): MathGateDiagnostic[];
|
|
49
|
+
|
|
50
|
+
export { type PartPageInput, gatePartPages, proseWordCount };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { stripFencedCode } from "./equations.mjs";
|
|
2
|
+
import { lessonLine } from "./gate-prose-notation.js";
|
|
3
|
+
const TAG_BY_SLOT = {
|
|
4
|
+
bridge: "PartBridge",
|
|
5
|
+
scope: "PartScope"
|
|
6
|
+
};
|
|
7
|
+
const SUBJECT_BY_SLOT = {
|
|
8
|
+
bridge: "how the earlier parts lead into this one",
|
|
9
|
+
scope: "what this part covers"
|
|
10
|
+
};
|
|
11
|
+
function scannable(body) {
|
|
12
|
+
return stripFencedCode(body).replace(
|
|
13
|
+
/\{\/\*[\s\S]*?\*\/\}/g,
|
|
14
|
+
(comment) => comment.replace(/[^\n]/g, " ")
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
function occurrencesOf(text, tag) {
|
|
18
|
+
const pattern = new RegExp(
|
|
19
|
+
`<${tag}\\b[^>]*?(?:\\/>|>([\\s\\S]*?)<\\/${tag}\\s*>)`,
|
|
20
|
+
"g"
|
|
21
|
+
);
|
|
22
|
+
return [...text.matchAll(pattern)].map((match) => ({
|
|
23
|
+
index: match.index ?? 0,
|
|
24
|
+
content: match[1]
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
function paragraphsOf(content) {
|
|
28
|
+
return content.trim().split(/\n[ \t]*\n/).filter((block) => block.trim().length > 0);
|
|
29
|
+
}
|
|
30
|
+
function proseWordCount(markdown) {
|
|
31
|
+
return markdown.replace(/\$\$[\s\S]*?\$\$/g, " math ").replace(/\$[^$\n]+\$/g, " math ").replace(/\[@[^\]]*\]/g, " ").replace(/\[\[[^\][]+\]\]/g, " term ").replace(/!?\[([^\]]*)\]\([^)]*\)/g, " $1 ").replace(/<\/?[A-Za-z][^>]*>/g, " ").split(/\s+/).filter((token) => /[\p{L}\p{N}]/u.test(token)).length;
|
|
32
|
+
}
|
|
33
|
+
function slotFindings(page, text, slot, limits) {
|
|
34
|
+
const tag = TAG_BY_SLOT[slot];
|
|
35
|
+
const found = occurrencesOf(text, tag);
|
|
36
|
+
const required = slot === "scope" || page.ordinal > 1;
|
|
37
|
+
const out = [];
|
|
38
|
+
if (found.length === 0) {
|
|
39
|
+
if (required) {
|
|
40
|
+
out.push({
|
|
41
|
+
index: 0,
|
|
42
|
+
token: `<${tag}>`,
|
|
43
|
+
message: `Part ${page.ordinal} ("${page.dir}") has an overview page with no <${tag}> paragraph (rule S2). Add one paragraph on ${SUBJECT_BY_SLOT[slot]}, wrapped in <${tag}>\u2026</${tag}>.`
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
for (const extra of found.slice(1)) {
|
|
49
|
+
out.push({
|
|
50
|
+
index: extra.index,
|
|
51
|
+
token: `<${tag}>`,
|
|
52
|
+
message: `A part overview page carries one <${tag}> (rule S2); this is another. Merge the two into one paragraph.`
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const [first] = found;
|
|
56
|
+
const paragraphs = paragraphsOf(first.content ?? "");
|
|
57
|
+
if (paragraphs.length === 0) {
|
|
58
|
+
out.push({
|
|
59
|
+
index: first.index,
|
|
60
|
+
token: `<${tag}>`,
|
|
61
|
+
message: `<${tag}> is empty (rule S2). Write one paragraph on ${SUBJECT_BY_SLOT[slot]} inside it.`
|
|
62
|
+
});
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
if (paragraphs.length > 1) {
|
|
66
|
+
out.push({
|
|
67
|
+
index: first.index,
|
|
68
|
+
token: `<${tag}>`,
|
|
69
|
+
message: `<${tag}> holds ${paragraphs.length} paragraphs; rule S2 allows one. Join them, or move the rest of the prose outside the slot.`
|
|
70
|
+
});
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
const { minWords, maxWords } = limits[slot];
|
|
74
|
+
const words = proseWordCount(paragraphs[0]);
|
|
75
|
+
if (words < minWords || words > maxWords) {
|
|
76
|
+
out.push({
|
|
77
|
+
index: first.index,
|
|
78
|
+
token: `<${tag}>`,
|
|
79
|
+
message: `<${tag}> is ${words} words; rule S2 wants ${minWords}\u2013${maxWords} (rules.S2.${slot}MinWords / ${slot}MaxWords in the course settings). ` + (words < minWords ? `Say more about ${SUBJECT_BY_SLOT[slot]}.` : "Cut it to the connections a reader needs before the chapter list.")
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function gatePartPages(pages, limits) {
|
|
85
|
+
const diagnostics = [];
|
|
86
|
+
for (const page of pages) {
|
|
87
|
+
const text = scannable(page.body);
|
|
88
|
+
const findings = [
|
|
89
|
+
...slotFindings(page, text, "bridge", limits),
|
|
90
|
+
...slotFindings(page, text, "scope", limits)
|
|
91
|
+
];
|
|
92
|
+
const [bridge] = occurrencesOf(text, TAG_BY_SLOT.bridge);
|
|
93
|
+
const [scope] = occurrencesOf(text, TAG_BY_SLOT.scope);
|
|
94
|
+
if (bridge && scope && bridge.index > scope.index) {
|
|
95
|
+
findings.push({
|
|
96
|
+
index: bridge.index,
|
|
97
|
+
token: "<PartBridge>",
|
|
98
|
+
message: "<PartBridge> comes after <PartScope> (rule S2). Move it first: the reader needs the link from the earlier parts before this part's own scope."
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
for (const finding of findings.sort((a, b) => a.index - b.index)) {
|
|
102
|
+
diagnostics.push({
|
|
103
|
+
context: "structure",
|
|
104
|
+
severity: "error",
|
|
105
|
+
code: "part-page-paragraph",
|
|
106
|
+
file: page.file,
|
|
107
|
+
line: lessonLine(page, finding.index),
|
|
108
|
+
token: finding.token,
|
|
109
|
+
message: finding.message
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return diagnostics;
|
|
114
|
+
}
|
|
115
|
+
export {
|
|
116
|
+
gatePartPages,
|
|
117
|
+
proseWordCount
|
|
118
|
+
};
|
|
@@ -6,7 +6,8 @@ import { NotationLessonInput } from './types.js';
|
|
|
6
6
|
* completeness gate (`gate-math.ts`). Where that gate asks "does every glyph
|
|
7
7
|
* *inside* `$…$` resolve", this one asks the complementary question: "is
|
|
8
8
|
* anything that reads as math sitting *outside* `$…$` in the first place" —
|
|
9
|
-
* a unit abbreviation, a
|
|
9
|
+
* a unit abbreviation, a unit spelled out as a word next to a number or
|
|
10
|
+
* `per` (`unit-words.ts`), a division of two variable-like symbols, an escaped
|
|
10
11
|
* absolute-value bar, a hand-typed sub/superscript. Both gates share one
|
|
11
12
|
* `MathGateDiagnostic` shape and one output array (`manifest.diagnostics.math`)
|
|
12
13
|
* so the CLI needs no separate wiring.
|
|
@@ -31,20 +32,40 @@ interface AssessmentLike {
|
|
|
31
32
|
readonly items?: readonly AssessmentItemLike[];
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* these regexes.
|
|
35
|
+
* `keepMath` leaves `$…$` / `$$…$$` in place (everything else still blanked,
|
|
36
|
+
* offsets unchanged) — for a check that needs to see what a math span sits
|
|
37
|
+
* next to, such as `$x = 3$ meters`.
|
|
38
38
|
*/
|
|
39
|
-
declare function proseOnly(body: string
|
|
39
|
+
declare function proseOnly(body: string, options?: {
|
|
40
|
+
readonly keepMath?: boolean;
|
|
41
|
+
}): string;
|
|
40
42
|
/** Exported for the same reason as {@link proseOnly}. */
|
|
41
43
|
declare function lineOf(text: string, index: number): number;
|
|
44
|
+
/**
|
|
45
|
+
* {@link lineOf} corrected to the file the lesson came from. A gate scans
|
|
46
|
+
* `lesson.body`, which starts *after* the frontmatter, so a raw body line is
|
|
47
|
+
* short by the frontmatter's height and points at the wrong source line —
|
|
48
|
+
* silently, because the number is still plausible. Every gate that reports a
|
|
49
|
+
* `line` against `lesson.source.file` must go through this.
|
|
50
|
+
*/
|
|
51
|
+
declare function lessonLine(lesson: {
|
|
52
|
+
readonly body: string;
|
|
53
|
+
readonly bodyLine?: number;
|
|
54
|
+
}, index: number): number;
|
|
55
|
+
/** Rule N9's settings the gate reads (`rules.N9` in the course settings). */
|
|
56
|
+
interface ProseNotationOptions {
|
|
57
|
+
/** Unit words this course lets prose spell out (`rules.N9.allow`), singular or plural. */
|
|
58
|
+
readonly allowSpelledUnits?: readonly string[];
|
|
59
|
+
}
|
|
42
60
|
/**
|
|
43
61
|
* Scan every lesson body for plain-text math notation outside `$…$`. Returns
|
|
44
|
-
* one `MathGateDiagnostic` (context `'prose'`, always an error
|
|
45
|
-
*
|
|
62
|
+
* one `MathGateDiagnostic` (context `'prose'`, always an error) per finding:
|
|
63
|
+
* code `spelled-unit` for a unit written as a word ("2 meters per second
|
|
64
|
+
* squared", "30 degrees"), `prose-notation` for everything else. A place is
|
|
65
|
+
* excused with a waiver (`{/* explico-allow N9: reason *\/}`), applied by the
|
|
66
|
+
* manifest.
|
|
46
67
|
*/
|
|
47
|
-
declare function gateProseNotation(lessons: readonly NotationLessonInput[]): MathGateDiagnostic[];
|
|
68
|
+
declare function gateProseNotation(lessons: readonly NotationLessonInput[], options?: ProseNotationOptions): MathGateDiagnostic[];
|
|
48
69
|
/**
|
|
49
70
|
* Scan every assessment `prompt` / `explanation` for the same plain-text
|
|
50
71
|
* notation N9 forbids in a lesson body — `AssessmentRunner` renders both
|
|
@@ -55,6 +76,6 @@ declare function gateProseNotation(lessons: readonly NotationLessonInput[]): Mat
|
|
|
55
76
|
* same `"<assessmentId> (<itemId>)"` shape `gate-math.ts` already uses for
|
|
56
77
|
* this context.
|
|
57
78
|
*/
|
|
58
|
-
declare function gateProseNotationInAssessments(assessments: readonly AssessmentLike[]): MathGateDiagnostic[];
|
|
79
|
+
declare function gateProseNotationInAssessments(assessments: readonly AssessmentLike[], options?: ProseNotationOptions): MathGateDiagnostic[];
|
|
59
80
|
|
|
60
|
-
export { gateProseNotation, gateProseNotationInAssessments, lineOf, proseOnly };
|
|
81
|
+
export { type ProseNotationOptions, gateProseNotation, gateProseNotationInAssessments, lessonLine, lineOf, proseOnly };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { findSpelledUnits } from "./unit-words.js";
|
|
1
2
|
const CODE_FENCE = /```[\s\S]*?```/g;
|
|
2
3
|
const INLINE_CODE = /(^|[^`])`[^`\n]*`/g;
|
|
3
4
|
const DISPLAY_MATH = /\$\$([\s\S]+?)\$\$/g;
|
|
@@ -5,13 +6,19 @@ const INLINE_MATH = /(?<![\\$])\$(?!\s)([^$\n]+?)(?<![\\\s])\$(?!\d)/g;
|
|
|
5
6
|
const IMPORT_LINE = /^[ \t]*import\b[^\n]*$/gm;
|
|
6
7
|
const KEY_REFERENCE = /\[\[[^\]\n]*\]\]/g;
|
|
7
8
|
const JSX_TAG = /<\/?[A-Za-z][^<>]*>/g;
|
|
8
|
-
|
|
9
|
+
const MDX_COMMENT = /\{\/\*[\s\S]*?\*\/\}/g;
|
|
10
|
+
function proseOnly(body, options = {}) {
|
|
9
11
|
const blank = (match) => match.replace(/[^\n]/g, " ");
|
|
10
|
-
|
|
12
|
+
const code = body.replace(MDX_COMMENT, blank).replace(CODE_FENCE, blank).replace(INLINE_CODE, (match, lead) => lead + blank(match.slice(lead.length)));
|
|
13
|
+
const math = options.keepMath ? code : code.replace(DISPLAY_MATH, blank).replace(INLINE_MATH, blank);
|
|
14
|
+
return math.replace(IMPORT_LINE, blank).replace(KEY_REFERENCE, blank).replace(JSX_TAG, blank);
|
|
11
15
|
}
|
|
12
16
|
function lineOf(text, index) {
|
|
13
17
|
return text.slice(0, index).split("\n").length;
|
|
14
18
|
}
|
|
19
|
+
function lessonLine(lesson, index) {
|
|
20
|
+
return lineOf(lesson.body, index) + (lesson.bodyLine ?? 1) - 1;
|
|
21
|
+
}
|
|
15
22
|
const UNIT = String.raw`m/s²|m/s|km/h|rad/s|m²/s²|J/kg|m²|s⁻¹|kg|N|J|W|Hz|deg|°|rad|m|s`;
|
|
16
23
|
const NUMERAL_UNIT = new RegExp(
|
|
17
24
|
String.raw`(?<![\w.])-?\d+(?:\.\d+)?\s?(?:${UNIT})(?![A-Za-z])`,
|
|
@@ -37,6 +44,7 @@ const SLASH_IDIOMS = /* @__PURE__ */ new Set([
|
|
|
37
44
|
]);
|
|
38
45
|
const SLASH_TOKENS = /\b([A-Za-z]{1,6})\/([A-Za-z]{1,6})\b/g;
|
|
39
46
|
const FAKE_SCRIPT = /[A-Za-z0-9][_^][A-Za-z0-9{]/g;
|
|
47
|
+
const PROSE_NOTATION_CODE = "prose-notation";
|
|
40
48
|
function findAll(text, pattern, toFinding) {
|
|
41
49
|
const out = [];
|
|
42
50
|
const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
@@ -48,8 +56,17 @@ function findAll(text, pattern, toFinding) {
|
|
|
48
56
|
return out;
|
|
49
57
|
}
|
|
50
58
|
const FIX = "Wrap it in $\u2026$ (or $$\u2026$$) instead \u2014 see docs/authoring/notation.md#n9.";
|
|
51
|
-
function findingsIn(text) {
|
|
59
|
+
function findingsIn(text, withMath, options) {
|
|
60
|
+
const spelled = findSpelledUnits(text, withMath, options.allowSpelledUnits).map(
|
|
61
|
+
(unit) => ({
|
|
62
|
+
index: unit.index,
|
|
63
|
+
token: unit.token,
|
|
64
|
+
code: "spelled-unit",
|
|
65
|
+
message: `Spelled-out unit ${JSON.stringify(unit.token)}: write it as ${unit.suggestion}. A unit that quantifies something is an upright symbol inside $\u2026$, even in flowing prose. Where the sentence defines or names the unit itself, waive the block with {/* explico-allow N9: reason */} \u2014 see docs/authoring/notation.md#n9.`
|
|
66
|
+
})
|
|
67
|
+
);
|
|
52
68
|
return [
|
|
69
|
+
...spelled,
|
|
53
70
|
...findAll(text, /\\\|/g, (m) => ({
|
|
54
71
|
index: m.index,
|
|
55
72
|
token: m[0],
|
|
@@ -80,16 +97,18 @@ function findingsIn(text) {
|
|
|
80
97
|
}).filter(Boolean)
|
|
81
98
|
];
|
|
82
99
|
}
|
|
83
|
-
function gateProseNotation(lessons) {
|
|
100
|
+
function gateProseNotation(lessons, options = {}) {
|
|
84
101
|
const diagnostics = [];
|
|
85
102
|
for (const lesson of lessons) {
|
|
86
103
|
const text = proseOnly(lesson.body);
|
|
87
|
-
|
|
104
|
+
const withMath = proseOnly(lesson.body, { keepMath: true });
|
|
105
|
+
for (const finding of findingsIn(text, withMath, options)) {
|
|
88
106
|
diagnostics.push({
|
|
89
107
|
context: "prose",
|
|
108
|
+
code: finding.code ?? PROSE_NOTATION_CODE,
|
|
90
109
|
severity: "error",
|
|
91
110
|
file: lesson.source.file,
|
|
92
|
-
line:
|
|
111
|
+
line: lessonLine(lesson, finding.index),
|
|
93
112
|
token: finding.token,
|
|
94
113
|
message: finding.message
|
|
95
114
|
});
|
|
@@ -97,15 +116,17 @@ function gateProseNotation(lessons) {
|
|
|
97
116
|
}
|
|
98
117
|
return diagnostics;
|
|
99
118
|
}
|
|
100
|
-
function gateProseNotationInAssessments(assessments) {
|
|
119
|
+
function gateProseNotationInAssessments(assessments, options = {}) {
|
|
101
120
|
const diagnostics = [];
|
|
102
121
|
for (const assessment of assessments) {
|
|
103
122
|
for (const item of assessment.items ?? []) {
|
|
104
123
|
for (const field of [item.prompt, item.explanation]) {
|
|
105
124
|
if (typeof field !== "string") continue;
|
|
106
|
-
|
|
125
|
+
const withMath = proseOnly(field, { keepMath: true });
|
|
126
|
+
for (const finding of findingsIn(proseOnly(field), withMath, options)) {
|
|
107
127
|
diagnostics.push({
|
|
108
128
|
context: "prose",
|
|
129
|
+
code: finding.code ?? PROSE_NOTATION_CODE,
|
|
109
130
|
severity: "error",
|
|
110
131
|
file: `${assessment.id} (${item.id ?? "?"})`,
|
|
111
132
|
token: finding.token,
|
|
@@ -120,6 +141,7 @@ function gateProseNotationInAssessments(assessments) {
|
|
|
120
141
|
export {
|
|
121
142
|
gateProseNotation,
|
|
122
143
|
gateProseNotationInAssessments,
|
|
144
|
+
lessonLine,
|
|
123
145
|
lineOf,
|
|
124
146
|
proseOnly
|
|
125
147
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exported for the test that pins both failure messages — a KaTeX upgrade that
|
|
3
|
+
* moves the constant, and a leftover `patches/` file, are the two ways this
|
|
4
|
+
* stops being able to install itself.
|
|
5
|
+
*/
|
|
6
|
+
export function rewriteBundle(source: any): any;
|
|
7
|
+
/**
|
|
8
|
+
* Render a known nest and confirm the outer pair really is larger than the
|
|
9
|
+
* inner one. Cheap, and it turns every way this could go wrong — an upstream
|
|
10
|
+
* refactor, a frozen export, an import that beat us to it — into one loud
|
|
11
|
+
* failure at the point of installation.
|
|
12
|
+
*/
|
|
13
|
+
export function assertDelimiterGrowth(instance?: typeof katex): void;
|
|
14
|
+
export default katex;
|
|
15
|
+
import katex from 'katex';
|