explico 0.8.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1239 -1
- package/README.md +60 -15
- package/dist/compiler/astro.d.ts +6 -1
- package/dist/compiler/cli.d.ts +46 -2
- package/dist/compiler/cli.js +70 -4
- package/dist/compiler/collections.d.ts +35 -2
- package/dist/compiler/collections.js +68 -5
- package/dist/compiler/course-config.d.ts +3 -194
- package/dist/compiler/course-config.js +78 -5
- package/dist/compiler/exports/standalone-html.d.ts +6 -1
- package/dist/compiler/knowledge-version.js +1 -1
- package/dist/compiler/lesson-derivation.d.ts +27 -10
- package/dist/compiler/lesson-derivation.js +23 -8
- package/dist/compiler/logging.d.ts +4 -1
- package/dist/compiler/logging.js +4 -1
- package/dist/compiler/manifest.d.ts +204 -31
- package/dist/compiler/manifest.js +270 -55
- package/dist/compiler/resolution-report.d.ts +6 -1
- package/dist/course-config-Dsnri9Tk.d.ts +464 -0
- package/dist/reference/base-accents.d.mts +26 -0
- package/dist/reference/base-accents.mjs +31 -0
- package/dist/reference/base-roman-labels.d.mts +2 -2
- package/dist/reference/base-roman-labels.mjs +2 -2
- package/dist/reference/consistency.d.ts +14 -2
- package/dist/reference/consistency.js +108 -4
- package/dist/reference/delimiter-size.d.ts +3 -2
- package/dist/reference/diagrams-validate.d.ts +7 -2
- package/dist/reference/diagrams-validate.js +9 -1
- package/dist/reference/equations-validate.d.ts +8 -5
- package/dist/reference/equations-validate.js +1 -0
- package/dist/reference/equations.d.mts +60 -31
- package/dist/reference/equations.mjs +83 -43
- package/dist/reference/examples-validate.d.ts +31 -0
- package/dist/reference/examples-validate.js +48 -0
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/figures-validate.js +4 -1
- package/dist/reference/folds-validate.d.ts +33 -0
- package/dist/reference/folds-validate.js +32 -0
- package/dist/reference/gate-abbreviations.d.ts +3 -1
- package/dist/reference/gate-abbreviations.js +1 -0
- package/dist/reference/gate-course-rules.d.ts +32 -0
- package/dist/reference/gate-course-rules.js +85 -0
- package/dist/reference/gate-heading-length.d.ts +5 -2
- package/dist/reference/gate-heading-length.js +45 -21
- package/dist/reference/gate-heading-structure.d.ts +41 -0
- package/dist/reference/gate-heading-structure.js +113 -0
- package/dist/reference/gate-math-punctuation.d.ts +71 -0
- package/dist/reference/gate-math-punctuation.js +251 -0
- package/dist/reference/gate-math.d.ts +22 -6
- package/dist/reference/gate-math.js +42 -8
- package/dist/reference/gate-part-pages.d.ts +50 -0
- package/dist/reference/gate-part-pages.js +118 -0
- package/dist/reference/gate-prose-notation.d.ts +21 -11
- package/dist/reference/gate-prose-notation.js +25 -7
- package/dist/reference/katex-delimiters.mjs +20 -7
- package/dist/reference/keyed-float-validate.d.ts +27 -10
- package/dist/reference/keyed-float-validate.js +15 -1
- package/dist/reference/keyed-jsx-floats.d.mts +11 -5
- package/dist/reference/keyed-jsx-floats.mjs +73 -20
- package/dist/reference/knowledge-checks-validate.d.ts +1 -1
- package/dist/reference/knowledge-checks.d.mts +3 -1
- package/dist/reference/knowledge-checks.mjs +9 -4
- package/dist/reference/lab-math-scope.d.ts +1 -0
- package/dist/reference/lab-math-slot.d.ts +34 -0
- package/dist/reference/lab-math-slot.js +6 -0
- package/dist/reference/math-glyphs.d.mts +80 -4
- package/dist/reference/math-glyphs.mjs +572 -37
- package/dist/reference/page-notation.d.ts +6 -1
- package/dist/reference/raw-floats-validate.js +1 -1
- package/dist/reference/raw-floats.mjs +1 -1
- package/dist/reference/rehype-heading-anchors.d.mts +14 -0
- package/dist/reference/rehype-heading-anchors.mjs +116 -17
- package/dist/reference/rehype-table-header-labels.d.mts +6 -1
- package/dist/reference/rehype-table-header-labels.mjs +21 -3
- package/dist/reference/remark-asides.d.mts +3 -0
- package/dist/reference/remark-asides.mjs +180 -0
- package/dist/reference/remark-memo.d.mts +3 -1
- package/dist/reference/remark-memo.mjs +42 -11
- package/dist/reference/remark-notation.d.mts +60 -10
- package/dist/reference/remark-notation.mjs +387 -36
- package/dist/reference/render-lab-math.d.ts +4 -26
- package/dist/reference/render-lab-math.js +4 -4
- package/dist/reference/sections-validate.d.ts +27 -0
- package/dist/reference/sections-validate.js +41 -0
- package/dist/reference/sections.d.mts +97 -0
- package/dist/reference/sections.mjs +306 -0
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +17 -1
- package/dist/reference/unit-words.d.ts +37 -0
- package/dist/reference/unit-words.js +170 -0
- package/dist/rules/catalog.d.ts +176 -0
- package/dist/rules/catalog.js +983 -0
- package/dist/rules/course-rules.d.ts +115 -0
- package/dist/rules/course-rules.js +292 -0
- package/dist/rules/course-rules.schema.json +138 -0
- package/dist/rules/in-effect.d.ts +53 -0
- package/dist/rules/in-effect.js +142 -0
- package/dist/rules/rules.schema.json +1072 -0
- package/dist/rules/settings.d.ts +3 -0
- package/dist/rules/settings.js +290 -0
- package/dist/rules/waivers.d.ts +53 -0
- package/dist/rules/waivers.js +132 -0
- package/dist/session/PreferenceStore.d.ts +5 -0
- package/dist/session/PreferenceStore.js +9 -0
- package/docs/VERSIONING.md +2 -2
- package/docs/authoring/README.md +232 -40
- package/docs/authoring/agents.md +55 -9
- package/docs/authoring/components.md +178 -22
- package/docs/authoring/migrating.md +1096 -2
- package/docs/authoring/notation.md +576 -322
- package/docs/authoring/prose.md +551 -0
- package/docs/authoring/retired.md +35 -0
- package/docs/authoring/structure-rules.md +422 -40
- package/docs/authoring/structure.md +445 -62
- package/docs/authoring/versioning.md +20 -28
- package/docs/authoring/visualizations.md +142 -20
- package/package.json +2 -2
- package/src/compiler/cli.ts +116 -6
- package/src/compiler/collections.ts +111 -7
- package/src/compiler/course-config.ts +273 -34
- package/src/compiler/knowledge-version.ts +1 -1
- package/src/compiler/lesson-derivation.ts +54 -15
- package/src/compiler/logging.ts +8 -2
- package/src/compiler/manifest.ts +573 -104
- package/src/components/BlockHeading.astro +113 -0
- package/src/components/CourseContents.astro +221 -0
- package/src/components/CurriculumMap.astro +5 -6
- package/src/components/FloatIndex.astro +200 -0
- package/src/components/HoverPanel.astro +431 -79
- package/src/components/PartBridge.astro +27 -0
- package/src/components/PartOverview.astro +401 -0
- package/src/components/PartScope.astro +27 -0
- package/src/components/abbreviation/AbbreviationLayer.astro +49 -51
- package/src/components/assessments/AssessmentRunner.tsx +8 -5
- package/src/components/assessments/AssessmentSet.astro +23 -26
- package/src/components/block-heading.ts +146 -0
- package/src/components/card-arrow.ts +243 -0
- package/src/components/card-drag.ts +631 -0
- package/src/components/card-jump.ts +45 -0
- package/src/components/card-manager.ts +1589 -0
- package/src/components/card-memory.ts +177 -0
- package/src/components/card-placement.ts +504 -0
- package/src/components/card-side.ts +336 -0
- package/src/components/card-travel.ts +197 -0
- package/src/components/citation/CitationLayer.astro +77 -76
- package/src/components/diagram/Diagram.astro +161 -316
- package/src/components/disclosure/course-disclosure.ts +24 -0
- package/src/components/disclosure/hover-preview.ts +122 -42
- package/src/components/disclosure/persisted-disclosure.ts +3 -3
- package/src/components/disclosure/print-as-shown.ts +252 -0
- package/src/components/examples/CompactExample.astro +3 -1
- package/src/components/examples/CompactExamples.astro +101 -94
- package/src/components/figure/Figure.astro +174 -274
- package/src/components/figure/FloatCaption.astro +87 -0
- package/src/components/figure/figure-state.ts +58 -0
- package/src/components/figure/figure-tools.ts +87 -0
- package/src/components/float-name.ts +101 -0
- package/src/components/fold/Fold.astro +125 -0
- package/src/components/fold/fold-id.ts +40 -0
- package/src/components/memo/Memo.astro +162 -231
- package/src/components/memo/ShortNote.astro +6 -6
- package/src/components/notation/NotationFormula.astro +5 -1
- package/src/components/notation/NotationGlossary.astro +3 -4
- package/src/components/notation/NotationLayer.astro +169 -162
- package/src/components/notation/NotationMath.astro +10 -1
- package/src/components/reference/FloatRefLayer.astro +485 -423
- package/src/components/starlight/AboutPanel.astro +5 -4
- package/src/components/starlight/CollapsibleSections.astro +207 -93
- package/src/components/starlight/DiagramEnhancer.astro +15 -138
- package/src/components/starlight/HeadingAnchorEnhancer.astro +193 -1
- package/src/components/starlight/KeyedFloatEnhancer.astro +100 -9
- package/src/components/starlight/LayoutHeader.astro +298 -19
- package/src/components/starlight/LayoutPageSidebar.astro +99 -12
- package/src/components/starlight/LayoutPageTitle.astro +120 -0
- package/src/components/starlight/LayoutSidebar.astro +229 -11
- package/src/components/starlight/LessonExportActions.astro +53 -72
- package/src/components/starlight/LessonFooter.astro +26 -4
- package/src/components/starlight/section-parts.ts +15 -2
- package/src/components/starlight/section-summary.ts +181 -0
- package/src/components/table/Table.astro +95 -76
- package/src/components/table/TableOverflowExpander.astro +65 -28
- package/src/content-config.ts +40 -4
- package/src/reference/base-accents.mjs +31 -0
- package/src/reference/base-roman-labels.mjs +2 -2
- package/src/reference/consistency.ts +176 -5
- package/src/reference/delimiter-size.ts +3 -2
- package/src/reference/diagrams-validate.ts +17 -1
- package/src/reference/equations-validate.ts +10 -5
- package/src/reference/equations.mjs +83 -43
- package/src/reference/examples-validate.ts +97 -0
- package/src/reference/figures-validate.ts +6 -1
- package/src/reference/folds-validate.ts +65 -0
- package/src/reference/gate-abbreviations.ts +1 -0
- package/src/reference/gate-course-rules.ts +134 -0
- package/src/reference/gate-heading-length.ts +66 -35
- package/src/reference/gate-heading-structure.ts +193 -0
- package/src/reference/gate-math-punctuation.ts +381 -0
- package/src/reference/gate-math.ts +69 -10
- package/src/reference/gate-part-pages.ts +217 -0
- package/src/reference/gate-prose-notation.ts +59 -12
- package/src/reference/katex-delimiters.mjs +20 -7
- package/src/reference/keyed-float-validate.ts +45 -11
- package/src/reference/keyed-jsx-floats.mjs +73 -20
- package/src/reference/knowledge-checks-validate.ts +1 -1
- package/src/reference/knowledge-checks.mjs +9 -4
- package/src/reference/lab-math-slot.ts +36 -0
- package/src/reference/math-glyphs.mjs +572 -37
- package/src/reference/raw-floats-validate.ts +1 -1
- package/src/reference/raw-floats.mjs +1 -1
- package/src/reference/rehype-heading-anchors.mjs +116 -17
- package/src/reference/rehype-table-header-labels.mjs +21 -3
- package/src/reference/remark-asides.mjs +180 -0
- package/src/reference/remark-memo.mjs +42 -11
- package/src/reference/remark-notation.mjs +387 -36
- package/src/reference/render-lab-math.ts +16 -31
- package/src/reference/sections-validate.ts +81 -0
- package/src/reference/sections.mjs +306 -0
- package/src/reference/tables-validate.ts +2 -1
- package/src/reference/types.ts +17 -0
- package/src/reference/unit-words.ts +233 -0
- package/src/rules/catalog.ts +1298 -0
- package/src/rules/course-rules.ts +456 -0
- package/src/rules/in-effect.ts +235 -0
- package/src/rules/settings.ts +448 -0
- package/src/rules/waivers.ts +249 -0
- package/src/session/PreferenceStore.ts +15 -0
- package/src/styles/global.css +1471 -163
- package/src/styles/surfaces.css +182 -0
- package/src/components/hover-panel.ts +0 -339
- package/src/components/popup-stack.ts +0 -345
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { equationLinkFor, mathSpans } from "./gate-math.js";
|
|
2
|
+
import { lessonLine } from "./gate-prose-notation.js";
|
|
3
|
+
const MATH_END_ABBREVIATIONS = [
|
|
4
|
+
"a.e.",
|
|
5
|
+
"a.s.",
|
|
6
|
+
"c.c.",
|
|
7
|
+
"const.",
|
|
8
|
+
"h.c.",
|
|
9
|
+
"i.i.d."
|
|
10
|
+
];
|
|
11
|
+
const LETTERS = /[A-Za-z]+/y;
|
|
12
|
+
function tokenize(latex) {
|
|
13
|
+
const tokens = [];
|
|
14
|
+
const stack = [];
|
|
15
|
+
let i = 0;
|
|
16
|
+
while (i < latex.length) {
|
|
17
|
+
const ch = latex[i];
|
|
18
|
+
const parent = stack.length === 0 ? -1 : stack[stack.length - 1];
|
|
19
|
+
if (ch === "%") {
|
|
20
|
+
const newline = latex.indexOf("\n", i);
|
|
21
|
+
i = newline === -1 ? latex.length : newline;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (ch === "\\") {
|
|
25
|
+
LETTERS.lastIndex = i + 1;
|
|
26
|
+
const letters = LETTERS.exec(latex);
|
|
27
|
+
const name = letters ? letters[0] : latex.charAt(i + 1);
|
|
28
|
+
tokens.push({ kind: "command", text: name, start: i, match: -1, parent });
|
|
29
|
+
i += 1 + Math.max(name.length, 1);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (ch === "{") {
|
|
33
|
+
tokens.push({ kind: "open", text: ch, start: i, match: -1, parent });
|
|
34
|
+
stack.push(tokens.length - 1);
|
|
35
|
+
} else if (ch === "}") {
|
|
36
|
+
const open = stack.pop();
|
|
37
|
+
const index = tokens.length;
|
|
38
|
+
tokens.push({
|
|
39
|
+
kind: "close",
|
|
40
|
+
text: ch,
|
|
41
|
+
start: i,
|
|
42
|
+
match: open ?? -1,
|
|
43
|
+
parent: open === void 0 ? -1 : tokens[open].parent
|
|
44
|
+
});
|
|
45
|
+
if (open !== void 0) tokens[open].match = index;
|
|
46
|
+
} else {
|
|
47
|
+
tokens.push({ kind: /\s/.test(ch) ? "space" : "char", text: ch, start: i, match: -1, parent });
|
|
48
|
+
}
|
|
49
|
+
i += 1;
|
|
50
|
+
}
|
|
51
|
+
return tokens;
|
|
52
|
+
}
|
|
53
|
+
const MARKS = /* @__PURE__ */ new Set([".", ",", ";", ":"]);
|
|
54
|
+
const INVISIBLE = /* @__PURE__ */ new Set([
|
|
55
|
+
",",
|
|
56
|
+
";",
|
|
57
|
+
":",
|
|
58
|
+
"!",
|
|
59
|
+
" ",
|
|
60
|
+
"\\",
|
|
61
|
+
"quad",
|
|
62
|
+
"qquad",
|
|
63
|
+
"enspace",
|
|
64
|
+
"thinspace",
|
|
65
|
+
"medspace",
|
|
66
|
+
"thickspace",
|
|
67
|
+
"negthinspace",
|
|
68
|
+
"negmedspace",
|
|
69
|
+
"negthickspace",
|
|
70
|
+
"nonumber",
|
|
71
|
+
"notag",
|
|
72
|
+
"displaystyle",
|
|
73
|
+
"textstyle"
|
|
74
|
+
]);
|
|
75
|
+
const SKIPPED_GROUP = /* @__PURE__ */ new Set(["label", "caption", "tag", "begin", "end", "hspace", "vspace", "color"]);
|
|
76
|
+
const UPRIGHT = /* @__PURE__ */ new Set(["text", "textrm", "textup", "textnormal", "mathrm", "mbox", "operatorname"]);
|
|
77
|
+
const SIZER = /^(?:left|right|middle|[bB]igg?[lrm]?)$/;
|
|
78
|
+
function groupOwner(tokens, open, from) {
|
|
79
|
+
let k = open - 1;
|
|
80
|
+
while (k >= from && tokens[k].kind === "space") k -= 1;
|
|
81
|
+
if (k >= from && tokens[k].kind === "char" && tokens[k].text === "*") k -= 1;
|
|
82
|
+
return k >= from && tokens[k].kind === "command" ? k : void 0;
|
|
83
|
+
}
|
|
84
|
+
function tail(tokens, from, to) {
|
|
85
|
+
let j = to - 1;
|
|
86
|
+
while (j >= from) {
|
|
87
|
+
const token = tokens[j];
|
|
88
|
+
if (token.kind === "space") {
|
|
89
|
+
j -= 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (token.kind === "command" && INVISIBLE.has(token.text)) {
|
|
93
|
+
j -= 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (token.kind === "char" && token.text === ".") {
|
|
97
|
+
const before = tokens[j - 1];
|
|
98
|
+
if (j - 1 >= from && before?.kind === "command" && SIZER.test(before.text)) {
|
|
99
|
+
j -= 2;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (token.kind === "char" && token.text === "]") {
|
|
104
|
+
let open = j - 1;
|
|
105
|
+
while (open >= from && !(tokens[open].kind === "char" && tokens[open].text === "[")) open -= 1;
|
|
106
|
+
const breakToken = tokens[open - 1];
|
|
107
|
+
if (open - 1 >= from && breakToken?.kind === "command" && breakToken.text === "\\") {
|
|
108
|
+
j = open - 2;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
return "clean";
|
|
112
|
+
}
|
|
113
|
+
if (token.kind === "close") {
|
|
114
|
+
if (token.match < from) return "clean";
|
|
115
|
+
const owner = groupOwner(tokens, token.match, from);
|
|
116
|
+
if (owner !== void 0 && SKIPPED_GROUP.has(tokens[owner].text)) {
|
|
117
|
+
j = owner - 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const inner = tail(tokens, token.match + 1, j);
|
|
121
|
+
if (inner !== "empty") return inner;
|
|
122
|
+
j = token.match - 1;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (token.kind === "char" && MARKS.has(token.text)) return { mark: j };
|
|
126
|
+
return "clean";
|
|
127
|
+
}
|
|
128
|
+
return "empty";
|
|
129
|
+
}
|
|
130
|
+
function classify(tokens, j, rowEnd, allowed) {
|
|
131
|
+
const token = tokens[j];
|
|
132
|
+
const base = { index: token.start, mark: token.text, rowEnd };
|
|
133
|
+
if (token.text !== ".") return { ...base, kind: "sentence" };
|
|
134
|
+
const isChar = (k, pattern) => tokens[k]?.kind === "char" && pattern.test(tokens[k].text);
|
|
135
|
+
if (isChar(j - 1, /^\.$/)) return { ...base, kind: "ellipsis" };
|
|
136
|
+
if (isChar(j - 1, /^\d$/)) {
|
|
137
|
+
let k = j - 1;
|
|
138
|
+
while (isChar(k - 1, /^\d$/)) k -= 1;
|
|
139
|
+
if (!isChar(k - 1, /^[_^]$/)) return { ...base, kind: "decimal" };
|
|
140
|
+
}
|
|
141
|
+
const owner = token.parent >= 0 ? groupOwner(tokens, token.parent, 0) : void 0;
|
|
142
|
+
if (owner !== void 0 && UPRIGHT.has(tokens[owner].text)) {
|
|
143
|
+
let k = j;
|
|
144
|
+
while (isChar(k - 1, /^[A-Za-z.]$/)) k -= 1;
|
|
145
|
+
const word = tokens.slice(k, j + 1).map((t) => t.text).join("");
|
|
146
|
+
if (/[A-Za-z]/.test(word)) {
|
|
147
|
+
if (allowed.has(word)) return void 0;
|
|
148
|
+
return { ...base, kind: "abbreviation", word };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { ...base, kind: "sentence" };
|
|
152
|
+
}
|
|
153
|
+
function allowedAbbreviations(options) {
|
|
154
|
+
const deny = new Set(options.deny ?? []);
|
|
155
|
+
return /* @__PURE__ */ new Set([
|
|
156
|
+
...MATH_END_ABBREVIATIONS.filter((word) => !deny.has(word)),
|
|
157
|
+
...options.allow ?? []
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
function findings(latex, allowed) {
|
|
161
|
+
const tokens = tokenize(latex);
|
|
162
|
+
const byMark = /* @__PURE__ */ new Map();
|
|
163
|
+
const record = (result, rowEnd) => {
|
|
164
|
+
if (typeof result !== "object" || byMark.has(result.mark)) return;
|
|
165
|
+
const finding = classify(tokens, result.mark, rowEnd, allowed);
|
|
166
|
+
if (finding) byMark.set(result.mark, finding);
|
|
167
|
+
};
|
|
168
|
+
tokens.forEach((token, k) => {
|
|
169
|
+
if (token.kind === "command" && token.text === "\\") record(tail(tokens, token.parent + 1, k), true);
|
|
170
|
+
});
|
|
171
|
+
record(tail(tokens, 0, tokens.length), false);
|
|
172
|
+
return [...byMark.values()].sort((left, right) => left.index - right.index);
|
|
173
|
+
}
|
|
174
|
+
function trailingPunctuation(latex, options = {}) {
|
|
175
|
+
return findings(latex, allowedAbbreviations(options));
|
|
176
|
+
}
|
|
177
|
+
const SEE = "See docs/authoring/notation.md#n12.";
|
|
178
|
+
function messageFor(finding, snippet) {
|
|
179
|
+
const where = finding.rowEnd ? "ends a row of the math (before `\\\\`)" : "ends the math";
|
|
180
|
+
switch (finding.kind) {
|
|
181
|
+
case "decimal":
|
|
182
|
+
return `"${snippet}" ${where} with a trailing decimal point, which cannot be told apart from the sentence's full stop. Drop the point, or write the digits that state the precision (\`5.0\`); keep a full stop outside the math. ${SEE}`;
|
|
183
|
+
case "ellipsis":
|
|
184
|
+
return `"${snippet}" ${where} with a typed "...". Write \`\\ldots\`, and keep a full stop outside the math. ${SEE}`;
|
|
185
|
+
case "abbreviation":
|
|
186
|
+
return `"${snippet}" ${where} with "${finding.word}". If that abbreviation is notation (like \`\\text{h.c.}\`), add it to \`rules.N12.allow\`; if the "." is the sentence's full stop, move it after the closing \`$\`. ${SEE}`;
|
|
187
|
+
case "sentence":
|
|
188
|
+
return finding.rowEnd ? `"${snippet}" ${where} with "${finding.mark}" \u2014 the sentence's own punctuation. Drop it; the row break already separates the lines. ${SEE}` : `"${snippet}" ${where} with "${finding.mark}" \u2014 the sentence's own punctuation. Move it after the closing \`$\` (inline math) or drop it (display math). ${SEE}`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function snippetOf(latex, index) {
|
|
192
|
+
const from = Math.max(0, index - 24);
|
|
193
|
+
const text = latex.slice(from, index + 1).replace(/\s+/g, " ").trimStart();
|
|
194
|
+
return from > 0 ? `\u2026${text}` : text;
|
|
195
|
+
}
|
|
196
|
+
function gateMathPunctuation(lessons, options = {}) {
|
|
197
|
+
const allowed = allowedAbbreviations(options);
|
|
198
|
+
const diagnostics = [];
|
|
199
|
+
for (const lesson of lessons) {
|
|
200
|
+
for (const span of mathSpans(lesson.body)) {
|
|
201
|
+
const equation = equationLinkFor(lesson.lessonId, span.latex);
|
|
202
|
+
for (const finding of findings(span.latex, allowed)) {
|
|
203
|
+
const at = span.offset + finding.index;
|
|
204
|
+
const snippet = snippetOf(span.latex, finding.index);
|
|
205
|
+
diagnostics.push({
|
|
206
|
+
context: "body",
|
|
207
|
+
code: "math-trailing-punctuation",
|
|
208
|
+
severity: "error",
|
|
209
|
+
file: lesson.source.file,
|
|
210
|
+
line: lessonLine(lesson, at),
|
|
211
|
+
column: at - lesson.body.lastIndexOf("\n", at - 1),
|
|
212
|
+
token: snippet,
|
|
213
|
+
...equation === void 0 ? {} : { equation },
|
|
214
|
+
message: messageFor(finding, snippet)
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return diagnostics;
|
|
220
|
+
}
|
|
221
|
+
function gateMathPunctuationInAssessments(assessments, options = {}) {
|
|
222
|
+
const allowed = allowedAbbreviations(options);
|
|
223
|
+
const diagnostics = [];
|
|
224
|
+
for (const assessment of assessments) {
|
|
225
|
+
for (const item of assessment.items ?? []) {
|
|
226
|
+
for (const field of [item.prompt, item.explanation]) {
|
|
227
|
+
if (typeof field !== "string") continue;
|
|
228
|
+
for (const span of mathSpans(field)) {
|
|
229
|
+
for (const finding of findings(span.latex, allowed)) {
|
|
230
|
+
const snippet = snippetOf(span.latex, finding.index);
|
|
231
|
+
diagnostics.push({
|
|
232
|
+
context: "assessment",
|
|
233
|
+
code: "math-trailing-punctuation",
|
|
234
|
+
severity: "error",
|
|
235
|
+
file: `${assessment.id} (${item.id ?? "?"})`,
|
|
236
|
+
token: snippet,
|
|
237
|
+
message: messageFor(finding, snippet)
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return diagnostics;
|
|
245
|
+
}
|
|
246
|
+
export {
|
|
247
|
+
MATH_END_ABBREVIATIONS,
|
|
248
|
+
gateMathPunctuation,
|
|
249
|
+
gateMathPunctuationInAssessments,
|
|
250
|
+
trailingPunctuation
|
|
251
|
+
};
|
|
@@ -15,7 +15,7 @@ interface EquationLink {
|
|
|
15
15
|
readonly href: string;
|
|
16
16
|
}
|
|
17
17
|
interface MathGateDiagnostic {
|
|
18
|
-
readonly context: 'body' | 'formula' | 'assessment' | 'prose' | 'abbreviation' | 'structure';
|
|
18
|
+
readonly context: 'body' | 'formula' | 'assessment' | 'prose' | 'abbreviation' | 'structure' | 'course';
|
|
19
19
|
/**
|
|
20
20
|
* Body / component-slot math blocks the build (the render path needs every
|
|
21
21
|
* glyph resolved for the KaTeX trust callback). `formula` joined it at D15:
|
|
@@ -34,13 +34,18 @@ interface MathGateDiagnostic {
|
|
|
34
34
|
* `gate-abbreviations.ts`) is the same "ships straight as an error" story
|
|
35
35
|
* for a different shape of plain-text token — an unexplained acronym or
|
|
36
36
|
* dotted abbreviation, rather than a mis-typed quantity/unit. `structure`
|
|
37
|
-
* (S1, `gate-heading-length.ts`) is unrelated to
|
|
38
|
-
* 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
|
|
39
40
|
* diagnostic shape and the one `manifest.diagnostics.math` array rather
|
|
40
|
-
* 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.
|
|
41
45
|
*/
|
|
42
46
|
readonly severity: 'error' | 'warning';
|
|
43
|
-
|
|
47
|
+
/** Owned by exactly one rule or invariant in `src/rules/catalog.ts`. */
|
|
48
|
+
readonly code: string;
|
|
44
49
|
readonly file: string;
|
|
45
50
|
readonly line?: number;
|
|
46
51
|
readonly column?: number;
|
|
@@ -62,6 +67,15 @@ interface LessonAssessmentLink {
|
|
|
62
67
|
readonly lessonId: string;
|
|
63
68
|
readonly assessmentIds: readonly string[];
|
|
64
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[];
|
|
65
79
|
type ScopeEntry = {
|
|
66
80
|
key: string;
|
|
67
81
|
notation: string;
|
|
@@ -80,6 +94,8 @@ declare function pageScope(lesson: NotationLessonInput, sharedByKey: ReadonlyMap
|
|
|
80
94
|
* reset before use — a stale one would skip the first label of the next span.
|
|
81
95
|
*/
|
|
82
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[];
|
|
83
99
|
/**
|
|
84
100
|
* Resolve every rendered-math context against the page glyph table. Returns one
|
|
85
101
|
* diagnostic per unresolved token; an empty array means the gate passed.
|
|
@@ -90,4 +106,4 @@ declare function gateContentMath(input: {
|
|
|
90
106
|
readonly lessonAssessments: readonly LessonAssessmentLink[];
|
|
91
107
|
}): MathGateDiagnostic[];
|
|
92
108
|
|
|
93
|
-
export { type EquationLink, type MathGateDiagnostic, type ScopeEntry, equationLinkFor, gateContentMath, pageScope };
|
|
109
|
+
export { type EquationLink, type MathGateDiagnostic, type MathSpan, type ScopeEntry, equationLinkFor, gateContentMath, mathSpans, pageScope, suspendedOperators };
|
|
@@ -12,7 +12,9 @@ const SEVERITY = {
|
|
|
12
12
|
assessment: "error",
|
|
13
13
|
prose: "error",
|
|
14
14
|
abbreviation: "error",
|
|
15
|
-
structure: "error"
|
|
15
|
+
structure: "error",
|
|
16
|
+
// Never read: `gateCourseRules` sets the severity the course chose.
|
|
17
|
+
course: "error"
|
|
16
18
|
};
|
|
17
19
|
const TERM_KEY = /\[\[\s*([a-z0-9]+(?:[.-][a-z0-9]+)*)\s*\]\]/g;
|
|
18
20
|
const EXPLAIN_KEY = /\\explain\s*\{([a-z0-9]+(?:[.-][a-z0-9]+)*)\}/g;
|
|
@@ -38,7 +40,8 @@ function mathSpans(body) {
|
|
|
38
40
|
spans.push({
|
|
39
41
|
latex,
|
|
40
42
|
line: lineOf(body, latexOffset),
|
|
41
|
-
column: latexOffset - lastNewline
|
|
43
|
+
column: latexOffset - lastNewline,
|
|
44
|
+
offset: latexOffset
|
|
42
45
|
});
|
|
43
46
|
}
|
|
44
47
|
}
|
|
@@ -71,7 +74,10 @@ function equationLinkFor(lessonId, latex) {
|
|
|
71
74
|
const slug = lessonId.replace(/\./g, "/");
|
|
72
75
|
return { key, href: `/${slug}/#${equationAnchorId({ key })}` };
|
|
73
76
|
}
|
|
74
|
-
function
|
|
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 = []) {
|
|
75
81
|
const at = equation === void 0 ? {} : { equation };
|
|
76
82
|
for (const diagnostic of lintDelimiterSizes(latex)) {
|
|
77
83
|
const locatedLine = line === void 0 ? void 0 : line + diagnostic.line - 1;
|
|
@@ -89,10 +95,15 @@ function resolveInto(latex, scope, context, file, line, column, fix, out, equati
|
|
|
89
95
|
}
|
|
90
96
|
let result;
|
|
91
97
|
try {
|
|
92
|
-
result = resolveMathGlyphs(stripEquationLabels(latex), [...scope]
|
|
98
|
+
result = resolveMathGlyphs(stripEquationLabels(latex), [...scope], {
|
|
99
|
+
suspended
|
|
100
|
+
});
|
|
93
101
|
} catch (error) {
|
|
94
102
|
out.push({
|
|
95
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",
|
|
96
107
|
severity: SEVERITY[context],
|
|
97
108
|
file,
|
|
98
109
|
line,
|
|
@@ -100,15 +111,31 @@ function resolveInto(latex, scope, context, file, line, column, fix, out, equati
|
|
|
100
111
|
});
|
|
101
112
|
return;
|
|
102
113
|
}
|
|
103
|
-
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}}.` : "";
|
|
104
130
|
out.push({
|
|
105
131
|
context,
|
|
132
|
+
code: "unresolved-glyph",
|
|
106
133
|
severity: SEVERITY[context],
|
|
107
134
|
file,
|
|
108
135
|
line,
|
|
109
136
|
token,
|
|
110
137
|
...at,
|
|
111
|
-
message: `Unresolved notation ${JSON.stringify(token)} in ${context} math
|
|
138
|
+
message: `Unresolved notation ${JSON.stringify(token)} in ${context} math.${fontHint} ${fix}`
|
|
112
139
|
});
|
|
113
140
|
}
|
|
114
141
|
}
|
|
@@ -132,6 +159,9 @@ function reportUnknownScopeKeys(definition, cardKeys, file, out) {
|
|
|
132
159
|
)) {
|
|
133
160
|
out.push({
|
|
134
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",
|
|
135
165
|
severity: SEVERITY.formula,
|
|
136
166
|
file,
|
|
137
167
|
token: key,
|
|
@@ -172,6 +202,7 @@ function gateContentMath(input) {
|
|
|
172
202
|
for (const lesson of notationInput.lessons) {
|
|
173
203
|
const scope = pageScope(lesson, sharedByKey);
|
|
174
204
|
const file = lesson.source.file;
|
|
205
|
+
const suspended = suspendedOperators(lesson);
|
|
175
206
|
const bodyOffset = (lesson.bodyLine ?? 1) - 1;
|
|
176
207
|
for (const span of mathSpans(lesson.body)) {
|
|
177
208
|
resolveInto(
|
|
@@ -183,7 +214,8 @@ function gateContentMath(input) {
|
|
|
183
214
|
span.column,
|
|
184
215
|
BODY_FIX,
|
|
185
216
|
diagnostics,
|
|
186
|
-
equationLinkFor(lesson.lessonId, span.latex)
|
|
217
|
+
equationLinkFor(lesson.lessonId, span.latex),
|
|
218
|
+
suspended
|
|
187
219
|
);
|
|
188
220
|
}
|
|
189
221
|
for (const definition of lesson.localDefinitions) {
|
|
@@ -263,5 +295,7 @@ function gateContentMath(input) {
|
|
|
263
295
|
export {
|
|
264
296
|
equationLinkFor,
|
|
265
297
|
gateContentMath,
|
|
266
|
-
|
|
298
|
+
mathSpans,
|
|
299
|
+
pageScope,
|
|
300
|
+
suspendedOperators
|
|
267
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,12 +32,13 @@ 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;
|
|
42
44
|
/**
|
|
@@ -50,12 +52,20 @@ declare function lessonLine(lesson: {
|
|
|
50
52
|
readonly body: string;
|
|
51
53
|
readonly bodyLine?: number;
|
|
52
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
|
+
}
|
|
53
60
|
/**
|
|
54
61
|
* Scan every lesson body for plain-text math notation outside `$…$`. Returns
|
|
55
|
-
* one `MathGateDiagnostic` (context `'prose'`, always an error
|
|
56
|
-
*
|
|
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.
|
|
57
67
|
*/
|
|
58
|
-
declare function gateProseNotation(lessons: readonly NotationLessonInput[]): MathGateDiagnostic[];
|
|
68
|
+
declare function gateProseNotation(lessons: readonly NotationLessonInput[], options?: ProseNotationOptions): MathGateDiagnostic[];
|
|
59
69
|
/**
|
|
60
70
|
* Scan every assessment `prompt` / `explanation` for the same plain-text
|
|
61
71
|
* notation N9 forbids in a lesson body — `AssessmentRunner` renders both
|
|
@@ -66,6 +76,6 @@ declare function gateProseNotation(lessons: readonly NotationLessonInput[]): Mat
|
|
|
66
76
|
* same `"<assessmentId> (<itemId>)"` shape `gate-math.ts` already uses for
|
|
67
77
|
* this context.
|
|
68
78
|
*/
|
|
69
|
-
declare function gateProseNotationInAssessments(assessments: readonly AssessmentLike[]): MathGateDiagnostic[];
|
|
79
|
+
declare function gateProseNotationInAssessments(assessments: readonly AssessmentLike[], options?: ProseNotationOptions): MathGateDiagnostic[];
|
|
70
80
|
|
|
71
|
-
export { gateProseNotation, gateProseNotationInAssessments, lessonLine, lineOf, proseOnly };
|
|
81
|
+
export { type ProseNotationOptions, gateProseNotation, gateProseNotationInAssessments, lessonLine, lineOf, proseOnly };
|