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,31 @@
|
|
|
1
|
+
import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } from './keyed-float-validate.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `validateKeyedFloats` for `<CompactExamples>` — see
|
|
5
|
+
* `keyed-float-validate.ts`. A worked-example set is a numbered kind of its
|
|
6
|
+
* own, like a knowledge check (docs/plans/titles-and-containers.md Part 6,
|
|
7
|
+
* Q11): `Example ‹prefix›.‹n›`, anchored `#ex-‹id›`, cited with `[[ex-‹id›]]`.
|
|
8
|
+
*
|
|
9
|
+
* On top of the shared checks, both of the set's names are required
|
|
10
|
+
* (`ex-missing-prop`): the `id` its anchor and references need, and the
|
|
11
|
+
* `short` title its heading shows after the engine's "Example" badge. There is
|
|
12
|
+
* no default title to fall back on.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
type ExampleDiagnosticCode = 'ex-invalid-key' | 'ex-duplicate-key' | 'ex-ref-resolves' | 'ex-key-unused' | 'ex-removed-prop' | 'ex-missing-prop';
|
|
16
|
+
interface ExampleDiagnostic {
|
|
17
|
+
readonly code: ExampleDiagnosticCode;
|
|
18
|
+
readonly severity: 'error' | 'warning' | 'info';
|
|
19
|
+
readonly message: string;
|
|
20
|
+
readonly file?: string;
|
|
21
|
+
readonly lessonId?: string;
|
|
22
|
+
readonly key?: string;
|
|
23
|
+
}
|
|
24
|
+
type ExampleLabelRecord = KeyedFloatLabelRecord;
|
|
25
|
+
type ExampleLessonInput = KeyedFloatLessonInput;
|
|
26
|
+
interface ExampleValidation extends KeyedFloatValidation {
|
|
27
|
+
readonly diagnostics: readonly ExampleDiagnostic[];
|
|
28
|
+
}
|
|
29
|
+
declare function validateExamples(lessons: readonly ExampleLessonInput[]): ExampleValidation;
|
|
30
|
+
|
|
31
|
+
export { type ExampleDiagnostic, type ExampleDiagnosticCode, type ExampleLabelRecord, type ExampleLessonInput, type ExampleValidation, validateExamples };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
validateKeyedFloats
|
|
3
|
+
} from "./keyed-float-validate.js";
|
|
4
|
+
import { scanKeyedJsxFloats } from "./keyed-jsx-floats.mjs";
|
|
5
|
+
const REQUIRED_PROPS = {
|
|
6
|
+
id: "give it an `id` (lowercase letters, digits and hyphens): the set is anchored `#ex-\u2039id\u203A` and cited with `[[ex-\u2039id\u203A]]`.",
|
|
7
|
+
short: 'give it a `short` title naming the topic, e.g. "Converting between coordinate systems"; the engine draws "Example" and the number before it.'
|
|
8
|
+
};
|
|
9
|
+
function validateExamples(lessons) {
|
|
10
|
+
const shared = validateKeyedFloats(lessons, {
|
|
11
|
+
tagName: "CompactExamples",
|
|
12
|
+
prefix: "ex",
|
|
13
|
+
noun: "worked-example set",
|
|
14
|
+
removedProps: {
|
|
15
|
+
summary: 'rename it `short`: the set\u2019s topical title, shown after the engine\u2019s "Example" badge.'
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const missing = [];
|
|
19
|
+
for (const lesson of lessons) {
|
|
20
|
+
const sets = scanKeyedJsxFloats(lesson.body, {
|
|
21
|
+
tagName: "CompactExamples",
|
|
22
|
+
numbering: lesson.numbering
|
|
23
|
+
});
|
|
24
|
+
for (const set of sets) {
|
|
25
|
+
for (const [prop, fix] of Object.entries(REQUIRED_PROPS)) {
|
|
26
|
+
if (set.props.includes(prop)) continue;
|
|
27
|
+
missing.push({
|
|
28
|
+
code: "ex-missing-prop",
|
|
29
|
+
severity: "error",
|
|
30
|
+
message: `${lesson.lessonId} has a <CompactExamples>${set.key ? ` (id="${set.key}")` : ""} with no \`${prop}\`: ${fix}`,
|
|
31
|
+
file: lesson.file,
|
|
32
|
+
lessonId: lesson.lessonId,
|
|
33
|
+
...set.key ? { key: set.key } : {}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const diagnostics = [
|
|
39
|
+
...shared.diagnostics,
|
|
40
|
+
...missing
|
|
41
|
+
].sort(
|
|
42
|
+
(a, b) => a.code.localeCompare(b.code) || (a.lessonId ?? "").localeCompare(b.lessonId ?? "") || (a.key ?? "").localeCompare(b.key ?? "") || a.message.localeCompare(b.message)
|
|
43
|
+
);
|
|
44
|
+
return { ...shared, diagnostics };
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
validateExamples
|
|
48
|
+
};
|
|
@@ -2,7 +2,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
|
|
|
2
2
|
|
|
3
3
|
/** `validateKeyedFloats` for `<Figure>` — see `keyed-float-validate.ts`. */
|
|
4
4
|
|
|
5
|
-
type FigureDiagnosticCode = 'fig-invalid-key' | 'fig-duplicate-key' | 'fig-ref-resolves' | 'fig-key-unused';
|
|
5
|
+
type FigureDiagnosticCode = 'fig-invalid-key' | 'fig-duplicate-key' | 'fig-ref-resolves' | 'fig-key-unused' | 'fig-removed-prop';
|
|
6
6
|
interface FigureDiagnostic {
|
|
7
7
|
readonly code: FigureDiagnosticCode;
|
|
8
8
|
readonly severity: 'error' | 'warning' | 'info';
|
|
@@ -5,7 +5,10 @@ function validateFigures(lessons) {
|
|
|
5
5
|
return validateKeyedFloats(lessons, {
|
|
6
6
|
tagName: "Figure",
|
|
7
7
|
prefix: "fig",
|
|
8
|
-
noun: "figure"
|
|
8
|
+
noun: "figure",
|
|
9
|
+
removedProps: {
|
|
10
|
+
title: "a figure is named by its `caption` (the long caption, shown below it with its number) and an optional `short` caption; move a short title to `short`."
|
|
11
|
+
}
|
|
9
12
|
});
|
|
10
13
|
}
|
|
11
14
|
export {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { KeyedFloatLessonInput } from './keyed-float-validate.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `<Fold>` validation, run inside `compileManifest()`
|
|
5
|
+
* (docs/plans/figures-and-short-captions.md, decision 11). A fold's bar is
|
|
6
|
+
* its `label` or, without one, the numbers and short captions of the floats
|
|
7
|
+
* it holds (`../components/float-name.ts`), so a fold needs one or the other:
|
|
8
|
+
*
|
|
9
|
+
* - `fold-unnamed` (error): a `<Fold>` with no `label` and no `<Figure>`,
|
|
10
|
+
* `<Table>` or `<Diagram>` that takes a number inside it — its bar would
|
|
11
|
+
* have nothing to show.
|
|
12
|
+
*
|
|
13
|
+
* `Fold.astro` throws on the same fold when it renders; this reports it
|
|
14
|
+
* earlier, at `validate:content`, with the lesson it is in. A float that a
|
|
15
|
+
* wrapper component renders counts when the lesson writes it under a float's
|
|
16
|
+
* tag name (`<Diagram id="…" />` for a wrapper imported as `Diagram`), the
|
|
17
|
+
* way the numbering scan sees it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
type FoldDiagnosticCode = 'fold-unnamed';
|
|
21
|
+
interface FoldDiagnostic {
|
|
22
|
+
readonly code: FoldDiagnosticCode;
|
|
23
|
+
readonly severity: 'error';
|
|
24
|
+
readonly message: string;
|
|
25
|
+
readonly file?: string;
|
|
26
|
+
readonly lessonId?: string;
|
|
27
|
+
}
|
|
28
|
+
interface FoldValidation {
|
|
29
|
+
readonly diagnostics: readonly FoldDiagnostic[];
|
|
30
|
+
}
|
|
31
|
+
declare function validateFolds(lessons: readonly KeyedFloatLessonInput[]): FoldValidation;
|
|
32
|
+
|
|
33
|
+
export { type FoldDiagnostic, type FoldDiagnosticCode, type FoldValidation, validateFolds };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { stripFencedCode } from "./equations.mjs";
|
|
2
|
+
import { scanKeyedJsxFloats } from "./keyed-jsx-floats.mjs";
|
|
3
|
+
const ATTRIBUTES = String.raw`(?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}|[^>"'])*`;
|
|
4
|
+
const FOLD = new RegExp(`<Fold\\b(${ATTRIBUTES})>([\\s\\S]*?)<\\/Fold\\s*>`, "g");
|
|
5
|
+
const NUMBERED_FLOATS = ["Figure", "Table", "Diagram"];
|
|
6
|
+
function validateFolds(lessons) {
|
|
7
|
+
const diagnostics = [];
|
|
8
|
+
for (const lesson of [...lessons].sort(
|
|
9
|
+
(a, b) => a.lessonId.localeCompare(b.lessonId)
|
|
10
|
+
)) {
|
|
11
|
+
for (const [, attributes = "", body = ""] of stripFencedCode(lesson.body).matchAll(
|
|
12
|
+
FOLD
|
|
13
|
+
)) {
|
|
14
|
+
if (/(?:^|\s)label\s*=/.test(attributes)) continue;
|
|
15
|
+
const named = NUMBERED_FLOATS.some(
|
|
16
|
+
(tagName) => scanKeyedJsxFloats(body, { tagName, numbering: void 0 }).length > 0
|
|
17
|
+
);
|
|
18
|
+
if (named) continue;
|
|
19
|
+
diagnostics.push({
|
|
20
|
+
code: "fold-unnamed",
|
|
21
|
+
severity: "error",
|
|
22
|
+
message: `${lesson.lessonId} has a <Fold> with no \`label\` and no numbered <Figure>, <Table> or <Diagram> inside it, so its bar has nothing to name it by: give it a \`label\`, or fold a numbered float with it.`,
|
|
23
|
+
file: lesson.file,
|
|
24
|
+
lessonId: lesson.lessonId
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { diagnostics };
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
validateFolds
|
|
32
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { MathGateDiagnostic } from './gate-math.js';
|
|
2
2
|
import { NotationRegistryInput, AbbreviationDefinitionInput } from './types.js';
|
|
3
|
-
import { CourseAbbreviations } from '../
|
|
3
|
+
import { a as CourseAbbreviations } from '../course-config-Dsnri9Tk.js';
|
|
4
|
+
import '../rules/catalog.js';
|
|
5
|
+
import '../rules/course-rules.js';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Scan every lesson body for an abbreviation-shaped token outside `$…$` that
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { MathGateDiagnostic } from './gate-math.js';
|
|
2
|
+
import { NotationLessonInput } from './types.js';
|
|
3
|
+
import { CourseRule } from '../rules/course-rules.js';
|
|
4
|
+
import '../rules/catalog.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The gate for a course's own rules (`courseRules` in `course.settings.json`,
|
|
8
|
+
* resolved by `src/rules/course-rules.ts`).
|
|
9
|
+
*
|
|
10
|
+
* One loop over every enabled course rule's `forbid` patterns, matched against
|
|
11
|
+
* the same "is this really prose" surface rule N9 uses — `proseOnly` blanks
|
|
12
|
+
* code fences, inline code, `$…$` / `$$…$$`, `[[key]]` references, `import`
|
|
13
|
+
* lines, JSX tags and MDX comments to spaces, so offsets still point at the
|
|
14
|
+
* source. A match becomes a `MathGateDiagnostic` with the rule's own ID as its
|
|
15
|
+
* code and a lesson `file` + `line`, which is what lets `{/* explico-allow X1:
|
|
16
|
+
* reason *\/}` waive one place.
|
|
17
|
+
*
|
|
18
|
+
* Nothing here knows what a rule *means*: the patterns come from the course,
|
|
19
|
+
* already validated (they compile, and none matches the empty string). The
|
|
20
|
+
* three scopes are the three units an author can reason about — a paragraph, a
|
|
21
|
+
* whole lesson's prose, a heading.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Scan every lesson body for what the course's own rules forbid. Only enabled
|
|
26
|
+
* rules with at least one pattern do anything; a guide rule is not this gate's
|
|
27
|
+
* business. Findings carry the rule ID as `code`, so `ownerOf` / `ruleIdOf`
|
|
28
|
+
* name the rule and a waiver can target it.
|
|
29
|
+
*/
|
|
30
|
+
declare function gateCourseRules(lessons: readonly NotationLessonInput[], rules: readonly CourseRule[]): MathGateDiagnostic[];
|
|
31
|
+
|
|
32
|
+
export { gateCourseRules };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { lessonLine, proseOnly } from "./gate-prose-notation.js";
|
|
2
|
+
const HEADING = /^[ \t]{0,3}(#{1,6})[ \t]+(.*)$/;
|
|
3
|
+
function paragraphs(text) {
|
|
4
|
+
const units = [];
|
|
5
|
+
let index = 0;
|
|
6
|
+
let start = -1;
|
|
7
|
+
const lines = text.split("\n");
|
|
8
|
+
lines.forEach((line, i) => {
|
|
9
|
+
const blank = line.trim() === "";
|
|
10
|
+
if (!blank && start === -1) start = index;
|
|
11
|
+
if ((blank || i === lines.length - 1) && start !== -1) {
|
|
12
|
+
const end = blank ? index : index + line.length;
|
|
13
|
+
units.push({ text: text.slice(start, end).replace(/\s+$/, ""), index: start });
|
|
14
|
+
start = -1;
|
|
15
|
+
}
|
|
16
|
+
index += line.length + 1;
|
|
17
|
+
});
|
|
18
|
+
return units.filter((unit) => unit.text !== "");
|
|
19
|
+
}
|
|
20
|
+
function headings(text) {
|
|
21
|
+
const units = [];
|
|
22
|
+
let index = 0;
|
|
23
|
+
for (const line of text.split("\n")) {
|
|
24
|
+
const match = HEADING.exec(line);
|
|
25
|
+
if (match !== null) {
|
|
26
|
+
const body = match[2].replace(/\s+#*\s*$/, "");
|
|
27
|
+
if (body !== "") {
|
|
28
|
+
units.push({ text: body, index: index + line.indexOf(body) });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
index += line.length + 1;
|
|
32
|
+
}
|
|
33
|
+
return units;
|
|
34
|
+
}
|
|
35
|
+
function unitsFor(scope, prose) {
|
|
36
|
+
switch (scope) {
|
|
37
|
+
case "prose":
|
|
38
|
+
return [{ text: prose, index: 0 }];
|
|
39
|
+
case "heading":
|
|
40
|
+
return headings(prose);
|
|
41
|
+
case "paragraph":
|
|
42
|
+
return paragraphs(prose);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function* matches(pattern, unit) {
|
|
46
|
+
const regex = new RegExp(pattern.pattern, `${pattern.flags}g`);
|
|
47
|
+
let match;
|
|
48
|
+
while ((match = regex.exec(unit.text)) !== null) {
|
|
49
|
+
yield [unit.index + match.index, match[0]];
|
|
50
|
+
if (match.index === regex.lastIndex) regex.lastIndex += 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function gateCourseRules(lessons, rules) {
|
|
54
|
+
const active = rules.filter((rule) => rule.enabled && rule.forbid.length > 0);
|
|
55
|
+
if (active.length === 0) return [];
|
|
56
|
+
const diagnostics = [];
|
|
57
|
+
for (const lesson of lessons) {
|
|
58
|
+
const prose = proseOnly(lesson.body);
|
|
59
|
+
const cache = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const rule of active) {
|
|
61
|
+
for (const pattern of rule.forbid) {
|
|
62
|
+
if (!cache.has(pattern.scope)) cache.set(pattern.scope, unitsFor(pattern.scope, prose));
|
|
63
|
+
for (const unit of cache.get(pattern.scope)) {
|
|
64
|
+
for (const [index, token] of matches(pattern, unit)) {
|
|
65
|
+
diagnostics.push({
|
|
66
|
+
context: "course",
|
|
67
|
+
code: rule.id,
|
|
68
|
+
severity: rule.severity,
|
|
69
|
+
file: lesson.source.file,
|
|
70
|
+
line: lessonLine(lesson, index),
|
|
71
|
+
// The matched text, trimmed — unless the pattern deliberately
|
|
72
|
+
// matched whitespace, in which case there is nothing else to show.
|
|
73
|
+
token: token.trim() || token,
|
|
74
|
+
message: `${pattern.message} (course rule ${rule.id})`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return diagnostics;
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
gateCourseRules
|
|
85
|
+
};
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { MathGateDiagnostic } from './gate-math.js';
|
|
2
2
|
import { NotationLessonInput } from './types.js';
|
|
3
3
|
|
|
4
|
+
/** Rule S1's default; a course sets its own with `rules.S1.maxLength`. */
|
|
4
5
|
declare const HEADING_LENGTH_LIMIT = 65;
|
|
5
6
|
/**
|
|
6
|
-
* Scan every lesson body for a heading or a
|
|
7
|
+
* Scan every lesson body for a heading or a short caption running past
|
|
7
8
|
* `HEADING_LENGTH_LIMIT` characters. Returns one `MathGateDiagnostic`
|
|
8
9
|
* (context `'structure'`, always an error) per finding.
|
|
9
10
|
*/
|
|
10
|
-
declare function gateHeadingLength(lessons: readonly NotationLessonInput[]
|
|
11
|
+
declare function gateHeadingLength(lessons: readonly NotationLessonInput[], options?: {
|
|
12
|
+
readonly maxLength?: number;
|
|
13
|
+
}): MathGateDiagnostic[];
|
|
11
14
|
|
|
12
15
|
export { HEADING_LENGTH_LIMIT, gateHeadingLength };
|
|
@@ -1,68 +1,92 @@
|
|
|
1
|
-
import { stripFencedCode } from "./equations.mjs";
|
|
1
|
+
import { EQ_CAPTION_PATTERN, stripFencedCode } from "./equations.mjs";
|
|
2
|
+
import { stripHeadingMarker } from "./rehype-heading-anchors.mjs";
|
|
2
3
|
import { lessonLine } from "./gate-prose-notation.js";
|
|
3
4
|
const HEADING_LENGTH_LIMIT = 65;
|
|
4
5
|
const HEADING_LINE = /^[ \t]{0,3}(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/gm;
|
|
5
|
-
const
|
|
6
|
-
Figure: "
|
|
7
|
-
Diagram: "
|
|
8
|
-
|
|
6
|
+
const SHORT_ATTRIBUTE_BY_TAG = {
|
|
7
|
+
Figure: "short",
|
|
8
|
+
Diagram: "short",
|
|
9
|
+
Table: "short",
|
|
10
|
+
Memo: "short",
|
|
11
|
+
CompactExamples: "short",
|
|
9
12
|
ShortNote: "title"
|
|
10
13
|
};
|
|
14
|
+
const DISPLAY_MATH = /\$\$([\s\S]+?)\$\$/g;
|
|
11
15
|
function readAttribute(attributesSource, name) {
|
|
12
16
|
const pattern = new RegExp(`\\b${name}\\s*=\\s*(["'])([^"']*)\\1`);
|
|
13
17
|
const match = pattern.exec(attributesSource);
|
|
14
18
|
return match ? match[2] : void 0;
|
|
15
19
|
}
|
|
16
|
-
function overLimitMessage(kind, text) {
|
|
17
|
-
return `${kind} ${JSON.stringify(text)} is ${text.length} characters, over the ${
|
|
20
|
+
function overLimitMessage(kind, text, limit) {
|
|
21
|
+
return `${kind} ${JSON.stringify(text)} is ${text.length} characters, over the ${limit}-character limit (rule S1, rules.S1.maxLength in the course settings). Shorten it and move the rest into the paragraph or caption that follows.`;
|
|
18
22
|
}
|
|
19
|
-
function headingFindings(text) {
|
|
23
|
+
function headingFindings(text, limit) {
|
|
20
24
|
const out = [];
|
|
21
25
|
const re = new RegExp(HEADING_LINE.source, HEADING_LINE.flags);
|
|
22
26
|
let match;
|
|
23
27
|
while (match = re.exec(text)) {
|
|
24
28
|
const level = match[1]?.length ?? 1;
|
|
25
|
-
const title = (match[2] ?? "").trim();
|
|
26
|
-
if (title.length >
|
|
29
|
+
const title = stripHeadingMarker(match[2] ?? "").trim();
|
|
30
|
+
if (title.length > limit) {
|
|
27
31
|
out.push({
|
|
28
32
|
index: match.index,
|
|
29
33
|
token: title,
|
|
30
|
-
message: overLimitMessage(`${"#".repeat(level)} heading`, title)
|
|
34
|
+
message: overLimitMessage(`${"#".repeat(level)} heading`, title, limit)
|
|
31
35
|
});
|
|
32
36
|
}
|
|
33
37
|
}
|
|
34
38
|
return out;
|
|
35
39
|
}
|
|
36
|
-
function floatTitleFindings(text) {
|
|
40
|
+
function floatTitleFindings(text, limit) {
|
|
37
41
|
const out = [];
|
|
38
|
-
for (const [tagName, attribute] of Object.entries(
|
|
42
|
+
for (const [tagName, attribute] of Object.entries(SHORT_ATTRIBUTE_BY_TAG)) {
|
|
39
43
|
const opening = new RegExp(`<${tagName}\\b([^>]*)>`, "g");
|
|
40
44
|
let match;
|
|
41
45
|
while (match = opening.exec(text)) {
|
|
42
46
|
const value = readAttribute(match[1] ?? "", attribute)?.trim();
|
|
43
|
-
if (value && value.length >
|
|
47
|
+
if (value && value.length > limit) {
|
|
44
48
|
out.push({
|
|
45
49
|
index: match.index,
|
|
46
50
|
token: value,
|
|
47
|
-
message: overLimitMessage(`<${tagName} ${attribute}>`, value)
|
|
51
|
+
message: overLimitMessage(`<${tagName} ${attribute}>`, value, limit)
|
|
48
52
|
});
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
return out;
|
|
53
57
|
}
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
function equationCaptionFindings(text, limit) {
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const display of text.matchAll(DISPLAY_MATH)) {
|
|
61
|
+
for (const caption of (display[1] ?? "").matchAll(EQ_CAPTION_PATTERN)) {
|
|
62
|
+
const value = (caption[1] ?? "").replace(/\s+/g, " ").trim();
|
|
63
|
+
if (value.length > limit) {
|
|
64
|
+
out.push({
|
|
65
|
+
index: (display.index ?? 0) + 2 + (caption.index ?? 0),
|
|
66
|
+
token: value,
|
|
67
|
+
message: overLimitMessage("Equation \\caption{}", value, limit)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function findingsIn(text, limit) {
|
|
75
|
+
return [
|
|
76
|
+
...headingFindings(text, limit),
|
|
77
|
+
...floatTitleFindings(text, limit),
|
|
78
|
+
...equationCaptionFindings(text, limit)
|
|
79
|
+
].sort((a, b) => a.index - b.index);
|
|
58
80
|
}
|
|
59
|
-
function gateHeadingLength(lessons) {
|
|
81
|
+
function gateHeadingLength(lessons, options = {}) {
|
|
60
82
|
const diagnostics = [];
|
|
83
|
+
const limit = options.maxLength ?? HEADING_LENGTH_LIMIT;
|
|
61
84
|
for (const lesson of lessons) {
|
|
62
85
|
const text = stripFencedCode(lesson.body);
|
|
63
|
-
for (const finding of findingsIn(text)) {
|
|
86
|
+
for (const finding of findingsIn(text, limit)) {
|
|
64
87
|
diagnostics.push({
|
|
65
88
|
context: "structure",
|
|
89
|
+
code: "title-length",
|
|
66
90
|
severity: "error",
|
|
67
91
|
file: lesson.source.file,
|
|
68
92
|
line: lessonLine(lesson, finding.index),
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { MathGateDiagnostic } from './gate-math.js';
|
|
2
|
+
import { NotationLessonInput } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The heading-structure gate: rules S6 and S7, over a lesson's outline — the
|
|
6
|
+
* top-level headings `sections.mjs` numbers, read by the same
|
|
7
|
+
* `scanOutlineBlocks`, so the rules and the section numbers can never
|
|
8
|
+
* disagree about which headings a page has. A heading inside a component
|
|
9
|
+
* (`<Memo>`, `<CompactExample>`, …) belongs to that component and is in
|
|
10
|
+
* neither. See docs/authoring/structure-rules.md#s6 and #s7.
|
|
11
|
+
*
|
|
12
|
+
* - **S6 (`heading-skip`)** — headings descend one level at a time. The page
|
|
13
|
+
* title (`frontmatter.title`) is the lesson's one `#`, so the body starts at
|
|
14
|
+
* `##`, a `#` in the body is an error, and a heading is at most one level
|
|
15
|
+
* deeper than the one before it. Going back up any number of levels is fine.
|
|
16
|
+
* - **S7 (`heading-adjacent`)** — a heading is followed by body content,
|
|
17
|
+
* never directly by another heading, whatever the two levels.
|
|
18
|
+
*
|
|
19
|
+
* Both also see the headings a component draws where its tag stands
|
|
20
|
+
* ({@link TITLED_COMPONENTS}): a knowledge check's and a worked-example set's
|
|
21
|
+
* `<h2>` (`BlockHeading.astro`) read as a `##`, so a heading written directly
|
|
22
|
+
* above either is two headings in a row, and the next heading steps from
|
|
23
|
+
* `##`.
|
|
24
|
+
*
|
|
25
|
+
* Ships straight as an error, same as S1: every playground violation was fixed
|
|
26
|
+
* or waived in the same change.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Scan every lesson body's outline for rule S6 (`heading-skip`) and rule S7
|
|
31
|
+
* (`heading-adjacent`) findings. `levels` / `adjacency` switch each rule's
|
|
32
|
+
* half off, the way `rules.S6.enabled` / `rules.S7.enabled` do. Returns one
|
|
33
|
+
* `MathGateDiagnostic` (context `'structure'`, always an error) per finding,
|
|
34
|
+
* located at the heading that breaks the rule.
|
|
35
|
+
*/
|
|
36
|
+
declare function gateHeadingStructure(lessons: readonly NotationLessonInput[], options?: {
|
|
37
|
+
readonly levels?: boolean;
|
|
38
|
+
readonly adjacency?: boolean;
|
|
39
|
+
}): MathGateDiagnostic[];
|
|
40
|
+
|
|
41
|
+
export { gateHeadingStructure };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { lessonLine } from "./gate-prose-notation.js";
|
|
2
|
+
import { scanOutlineBlocks } from "./sections.mjs";
|
|
3
|
+
const TITLED_COMPONENTS = {
|
|
4
|
+
AssessmentSet: {
|
|
5
|
+
level: 2,
|
|
6
|
+
what: "which renders its own `<h2>`: the \u201CKnowledge check\u201D badge, its number and the check\u2019s title"
|
|
7
|
+
},
|
|
8
|
+
CompactExamples: {
|
|
9
|
+
level: 2,
|
|
10
|
+
what: "which renders its own `<h2>`: the \u201CExample\u201D badge, its number and the set\u2019s `short` title"
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
function blocksOf(body) {
|
|
14
|
+
return scanOutlineBlocks(body).map((block) => {
|
|
15
|
+
if (block.kind === "heading") {
|
|
16
|
+
const hashes = "#".repeat(block.level);
|
|
17
|
+
return {
|
|
18
|
+
kind: "heading",
|
|
19
|
+
level: block.level,
|
|
20
|
+
label: `\`${hashes} ${block.text}\``,
|
|
21
|
+
token: block.text,
|
|
22
|
+
index: block.index
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
if (block.kind === "element") {
|
|
26
|
+
const titled = TITLED_COMPONENTS[block.name];
|
|
27
|
+
if (titled) {
|
|
28
|
+
return {
|
|
29
|
+
kind: "titled",
|
|
30
|
+
name: block.name,
|
|
31
|
+
index: block.index,
|
|
32
|
+
...titled.level === void 0 ? {} : { level: titled.level }
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { kind: "content" };
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function skipFindings(blocks) {
|
|
40
|
+
const out = [];
|
|
41
|
+
let previous = { level: 1, label: "the page title (`#`)" };
|
|
42
|
+
for (const block of blocks) {
|
|
43
|
+
if (block.kind === "content") continue;
|
|
44
|
+
if (block.kind === "titled") {
|
|
45
|
+
if (block.level !== void 0) {
|
|
46
|
+
previous = { level: block.level, label: `the \`<h${block.level}>\` of \`<${block.name} />\`` };
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (block.level === 1) {
|
|
51
|
+
out.push({
|
|
52
|
+
code: "heading-skip",
|
|
53
|
+
index: block.index,
|
|
54
|
+
token: block.token,
|
|
55
|
+
message: `${block.label} is a \`#\` in the lesson body, but the page title (\`frontmatter.title\`) is the lesson\u2019s one \`#\`: the body\u2019s headings start at \`##\` (rule S6). Make it \`##\`, or move the text into the title.`
|
|
56
|
+
});
|
|
57
|
+
} else if (block.level > previous.level + 1) {
|
|
58
|
+
const expected = "#".repeat(previous.level + 1);
|
|
59
|
+
out.push({
|
|
60
|
+
code: "heading-skip",
|
|
61
|
+
index: block.index,
|
|
62
|
+
token: block.token,
|
|
63
|
+
message: `${block.label} follows ${previous.label}, skipping a level: headings descend one level at a time (rule S6). Make it \`${expected}\`, or add the \`${expected}\` section it belongs under.`
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
previous = { level: block.level, label: block.label };
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function adjacentFindings(blocks, lesson) {
|
|
71
|
+
const out = [];
|
|
72
|
+
blocks.forEach((block, position) => {
|
|
73
|
+
if (block.kind !== "heading") return;
|
|
74
|
+
const next = blocks[position + 1];
|
|
75
|
+
if (next === void 0 || next.kind === "content") return;
|
|
76
|
+
const line = lessonLine(lesson, next.index);
|
|
77
|
+
const followedBy = next.kind === "heading" ? `${next.label} (line ${line})` : `\`<${next.name}>\` (line ${line}), ${TITLED_COMPONENTS[next.name].what}`;
|
|
78
|
+
out.push({
|
|
79
|
+
code: "heading-adjacent",
|
|
80
|
+
index: block.index,
|
|
81
|
+
token: block.token,
|
|
82
|
+
message: `${block.label} is followed directly by ${followedBy}: a heading is followed by body content, never by another heading (rule S7). Write what the section is about before it goes on, or drop the heading that says nothing of its own.`
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function gateHeadingStructure(lessons, options = {}) {
|
|
88
|
+
const { levels = true, adjacency = true } = options;
|
|
89
|
+
const diagnostics = [];
|
|
90
|
+
if (!levels && !adjacency) return diagnostics;
|
|
91
|
+
for (const lesson of lessons) {
|
|
92
|
+
const blocks = blocksOf(lesson.body);
|
|
93
|
+
const findings = [
|
|
94
|
+
...levels ? skipFindings(blocks) : [],
|
|
95
|
+
...adjacency ? adjacentFindings(blocks, lesson) : []
|
|
96
|
+
].sort((a, b) => a.index - b.index);
|
|
97
|
+
for (const finding of findings) {
|
|
98
|
+
diagnostics.push({
|
|
99
|
+
context: "structure",
|
|
100
|
+
code: finding.code,
|
|
101
|
+
severity: "error",
|
|
102
|
+
file: lesson.source.file,
|
|
103
|
+
line: lessonLine(lesson, finding.index),
|
|
104
|
+
token: finding.token,
|
|
105
|
+
message: finding.message
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return diagnostics;
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
gateHeadingStructure
|
|
113
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { MathGateDiagnostic } from './gate-math.js';
|
|
2
|
+
import { NotationLessonInput } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The trailing-punctuation gate (rule N12): math never ends in the sentence's
|
|
6
|
+
* own `.`, `,`, `;` or `:`. It runs from the CLI alongside the completeness
|
|
7
|
+
* gate (`gate-math.ts`) over the same spans (`mathSpans`: every `$…$` /
|
|
8
|
+
* `$$…$$` in a lesson body, including table cells and component slots, and in
|
|
9
|
+
* an assessment item's `prompt` / `explanation`), and shares its
|
|
10
|
+
* `MathGateDiagnostic` shape and output array.
|
|
11
|
+
*
|
|
12
|
+
* "Ends" is read the way a reader sees it, not the raw source: trailing
|
|
13
|
+
* spacing (`\,`, `\quad`), `\label{…}` / `\caption{…}` / `\tag{…}`, a row break `\\`, an
|
|
14
|
+
* `\end{…}`, and the invisible delimiter `\right.` are looked through, and a
|
|
15
|
+
* closing `\text{…}` / `\mathrm{…}` is looked into — so `5\ \text{m,}` and
|
|
16
|
+
* `c &= d. \end{aligned}` are both caught. Every row of a multi-line block
|
|
17
|
+
* (the text before each `\\`) is checked the same way.
|
|
18
|
+
*
|
|
19
|
+
* Two shapes are called out by name because they look like notation: a
|
|
20
|
+
* trailing decimal point (`5.`) — a precision marker that cannot be told
|
|
21
|
+
* apart from the full stop, so it is never allowed — and a typed `...`
|
|
22
|
+
* (write `\ldots`). The one exception is a dotted abbreviation set upright
|
|
23
|
+
* that is itself notation (`+ \text{h.c.}`, `X_n \to X\ \text{a.s.}`): the
|
|
24
|
+
* shipped {@link MATH_END_ABBREVIATIONS}, edited per course with
|
|
25
|
+
* `rules.N12.allow` / `deny`.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
interface AssessmentItemLike {
|
|
29
|
+
readonly id?: string;
|
|
30
|
+
readonly prompt?: string;
|
|
31
|
+
readonly explanation?: string;
|
|
32
|
+
}
|
|
33
|
+
interface AssessmentLike {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly items?: readonly AssessmentItemLike[];
|
|
36
|
+
}
|
|
37
|
+
/** Dotted abbreviations that are notation, so they may end math (`\text{h.c.}`). */
|
|
38
|
+
declare const MATH_END_ABBREVIATIONS: readonly string[];
|
|
39
|
+
interface MathPunctuationOptions {
|
|
40
|
+
/** Abbreviations allowed in addition to {@link MATH_END_ABBREVIATIONS}. */
|
|
41
|
+
readonly allow?: readonly string[];
|
|
42
|
+
/** Shipped abbreviations this course does not accept. */
|
|
43
|
+
readonly deny?: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
interface PunctuationFinding {
|
|
46
|
+
/** Index of the mark in the LaTeX. */
|
|
47
|
+
readonly index: number;
|
|
48
|
+
readonly mark: string;
|
|
49
|
+
readonly kind: 'sentence' | 'decimal' | 'ellipsis' | 'abbreviation';
|
|
50
|
+
/** True when the mark ends a row (text before a `\\`), not the whole span. */
|
|
51
|
+
readonly rowEnd: boolean;
|
|
52
|
+
/** The upright dotted word the mark closes (`etc.`), for `abbreviation`. */
|
|
53
|
+
readonly word?: string;
|
|
54
|
+
}
|
|
55
|
+
/** Every N12 finding in one span of LaTeX (the text between the `$` delimiters). */
|
|
56
|
+
declare function trailingPunctuation(latex: string, options?: MathPunctuationOptions): PunctuationFinding[];
|
|
57
|
+
/**
|
|
58
|
+
* Scan every lesson body's math for the sentence's own trailing punctuation.
|
|
59
|
+
* One error per mark, located at the mark itself, so a per-place waiver
|
|
60
|
+
* (`{/* explico-allow N12: reason *\/}`) can cover it.
|
|
61
|
+
*/
|
|
62
|
+
declare function gateMathPunctuation(lessons: readonly NotationLessonInput[], options?: MathPunctuationOptions): MathGateDiagnostic[];
|
|
63
|
+
/**
|
|
64
|
+
* The same check over every assessment item's `prompt` / `explanation`, which
|
|
65
|
+
* render through the same KaTeX pipeline as lesson math. No line number: the
|
|
66
|
+
* item is one JSON string field, so `file` names the item, the same
|
|
67
|
+
* `"<assessmentId> (<itemId>)"` shape `gate-math.ts` uses.
|
|
68
|
+
*/
|
|
69
|
+
declare function gateMathPunctuationInAssessments(assessments: readonly AssessmentLike[], options?: MathPunctuationOptions): MathGateDiagnostic[];
|
|
70
|
+
|
|
71
|
+
export { MATH_END_ABBREVIATIONS, type MathPunctuationOptions, type PunctuationFinding, gateMathPunctuation, gateMathPunctuationInAssessments, trailingPunctuation };
|