explico 0.6.0 → 0.8.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 +350 -1
- package/README.md +16 -2
- package/dist/compiler/astro.d.ts +13 -2
- package/dist/compiler/astro.js +39 -0
- package/dist/compiler/cli.d.ts +30 -4
- package/dist/compiler/cli.js +79 -10
- package/dist/compiler/collections.js +16 -3
- package/dist/compiler/exports/standalone-html.js +13 -0
- package/dist/compiler/logging.d.ts +119 -0
- package/dist/compiler/logging.js +108 -0
- package/dist/compiler/manifest.d.ts +7 -3
- package/dist/compiler/manifest.js +1 -1
- package/dist/reference/base-abbreviations.mjs +8 -2
- package/dist/reference/consistency.d.ts +8 -10
- package/dist/reference/consistency.js +3 -47
- package/dist/reference/delimiter-size.d.ts +48 -0
- package/dist/reference/delimiter-size.js +153 -0
- package/dist/reference/diagrams-validate.d.ts +1 -1
- package/dist/reference/equations-validate.d.ts +2 -2
- package/dist/reference/equations-validate.js +1 -1
- package/dist/reference/equations.d.mts +9 -3
- package/dist/reference/equations.mjs +7 -1
- package/dist/reference/figures-validate.d.ts +1 -1
- package/dist/reference/gate-abbreviations.js +2 -2
- package/dist/reference/gate-heading-length.js +2 -2
- package/dist/reference/gate-math.d.ts +25 -1
- package/dist/reference/gate-math.js +50 -9
- package/dist/reference/gate-prose-notation.d.ts +12 -1
- package/dist/reference/gate-prose-notation.js +5 -1
- package/dist/reference/katex-delimiters.d.mts +15 -0
- package/dist/reference/katex-delimiters.mjs +125 -0
- package/dist/reference/katex-options.mjs +4 -0
- package/dist/reference/keyed-float-validate.d.ts +2 -2
- package/dist/reference/keyed-float-validate.js +1 -1
- package/dist/reference/math-glyphs.mjs +16 -2
- package/dist/reference/references.js +2 -1
- package/dist/reference/registry.js +1 -1
- package/dist/reference/rehype-base-links.d.mts +23 -0
- package/dist/reference/rehype-base-links.mjs +40 -0
- package/dist/reference/remark-abbreviations.mjs +10 -12
- package/dist/reference/remark-citation.mjs +0 -0
- package/dist/reference/render-lab-math.d.ts +18 -1
- package/dist/reference/render-lab-math.js +11 -4
- package/dist/reference/tables-validate.d.ts +1 -1
- package/dist/reference/types.d.ts +8 -1
- package/docs/VERSIONING.md +2 -2
- package/docs/authoring/README.md +12 -2
- package/docs/authoring/agents.md +7 -3
- package/docs/authoring/migrating.md +514 -0
- package/docs/authoring/notation.md +157 -7
- package/docs/authoring/prose.md +64 -0
- package/docs/katex-bracket-scaling.md +191 -0
- package/package.json +2 -1
- package/src/compiler/astro.ts +89 -0
- package/src/compiler/cli.ts +167 -12
- package/src/compiler/collections.ts +33 -3
- package/src/compiler/exports/standalone-html.ts +15 -0
- package/src/compiler/logging.ts +259 -0
- package/src/compiler/manifest.ts +7 -3
- package/src/components/memo/Memo.astro +2 -5
- package/src/components/memo/ShortNote.astro +2 -5
- package/src/components/notation/NotationFormula.astro +199 -10
- package/src/components/notation/NotationGlossary.astro +100 -58
- package/src/components/notation/NotationLayer.astro +83 -81
- package/src/components/notation/render-notation-math.ts +111 -10
- package/src/components/scroll-animation.ts +160 -0
- package/src/components/starlight/AboutPanel.astro +17 -2
- package/src/components/starlight/AnchorScrollEnhancer.astro +102 -0
- package/src/components/starlight/HeadingAnchorEnhancer.astro +2 -4
- package/src/components/starlight/KeyedFloatEnhancer.astro +17 -16
- package/src/components/starlight/LessonFooter.astro +2 -0
- package/src/reference/base-abbreviations.mjs +8 -2
- package/src/reference/consistency.ts +10 -66
- package/src/reference/delimiter-size.ts +279 -0
- package/src/reference/diagrams-validate.ts +1 -1
- package/src/reference/equations-validate.ts +3 -3
- package/src/reference/equations.mjs +7 -1
- package/src/reference/figures-validate.ts +1 -1
- package/src/reference/gate-abbreviations.ts +2 -2
- package/src/reference/gate-heading-length.ts +2 -2
- package/src/reference/gate-math.ts +91 -8
- package/src/reference/gate-prose-notation.ts +15 -1
- package/src/reference/katex-delimiters.mjs +125 -0
- package/src/reference/katex-options.mjs +4 -0
- package/src/reference/keyed-float-validate.ts +3 -3
- package/src/reference/math-glyphs.mjs +16 -2
- package/src/reference/references.ts +2 -1
- package/src/reference/registry.ts +1 -1
- package/src/reference/rehype-base-links.mjs +40 -0
- package/src/reference/remark-abbreviations.mjs +10 -12
- package/src/reference/remark-citation.mjs +0 -0
- package/src/reference/render-lab-math.ts +25 -2
- package/src/reference/tables-validate.ts +1 -1
- package/src/reference/types.ts +8 -1
- package/src/styles/global.css +119 -14
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule N17: a nested scalable delimiter is sized by the engine, never by hand.
|
|
3
|
+
*
|
|
4
|
+
* The engine installs a KaTeX whose `\delimitershortfall` is `-1pt`
|
|
5
|
+
* (`katex-delimiters.mjs`), so a `\left`/`\right` pair is always
|
|
6
|
+
* strictly taller than what it wraps. Nesting depth therefore shows up on its
|
|
7
|
+
* own, and a hand-picked `\bigl`…`\Biggr` inside a nest only replaces a size
|
|
8
|
+
* that tracks the content with a fixed glyph that does not.
|
|
9
|
+
*
|
|
10
|
+
* This deliberately parses only sizing syntax. It does not decide whether a
|
|
11
|
+
* parenthesis, bracket, or brace is semantic, and it never recommends changing
|
|
12
|
+
* one delimiter shape into another. N16's alternating-shape preference remains
|
|
13
|
+
* a review-only authoring concern.
|
|
14
|
+
*/
|
|
15
|
+
declare const DELIMITER_SIZE_DIAGNOSTIC_CODE: "delimiter-manual-size";
|
|
16
|
+
type DelimiterSizeName = 'automatic' | 'big' | 'Big' | 'bigg' | 'Bigg';
|
|
17
|
+
interface DelimiterSizeSuggestion {
|
|
18
|
+
readonly open: string;
|
|
19
|
+
readonly close: string;
|
|
20
|
+
readonly latex: string;
|
|
21
|
+
}
|
|
22
|
+
interface DelimiterSizeDiagnostic {
|
|
23
|
+
readonly code: typeof DELIMITER_SIZE_DIAGNOSTIC_CODE;
|
|
24
|
+
readonly severity: 'warning';
|
|
25
|
+
readonly offset: number;
|
|
26
|
+
readonly line: number;
|
|
27
|
+
readonly column: number;
|
|
28
|
+
/** The hand-picked size that should become `\left`/`\right`. */
|
|
29
|
+
readonly size: Exclude<DelimiterSizeName, 'automatic'>;
|
|
30
|
+
readonly suggestion: DelimiterSizeSuggestion;
|
|
31
|
+
readonly message: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Warn when a pair that takes part in a nest of scalable delimiters is sized
|
|
35
|
+
* by hand instead of by `\left`/`\right`.
|
|
36
|
+
*
|
|
37
|
+
* A pair takes part in a nest when it contains, or sits inside, another pair
|
|
38
|
+
* this linter tracks. A lone hand-sized pair around otherwise bare content —
|
|
39
|
+
* `\bigl(x(t),\ y(t)\bigr)` for a tuple — is a deliberate typographic
|
|
40
|
+
* choice at a single level, has no hierarchy to get wrong, and is left alone.
|
|
41
|
+
*
|
|
42
|
+
* Only `()`, `[]`, and `\{\}` are parsed. Bare delimiters are intentionally
|
|
43
|
+
* ignored: N15 owns that separate error, while ordinary semantic notation
|
|
44
|
+
* such as `f(x)` and `\mathbb{E}[X]` must not trigger this size-only linter.
|
|
45
|
+
*/
|
|
46
|
+
declare function lintDelimiterSizes(latex: string): DelimiterSizeDiagnostic[];
|
|
47
|
+
|
|
48
|
+
export { DELIMITER_SIZE_DIAGNOSTIC_CODE, type DelimiterSizeDiagnostic, type DelimiterSizeName, type DelimiterSizeSuggestion, lintDelimiterSizes };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const DELIMITER_SIZE_DIAGNOSTIC_CODE = "delimiter-manual-size";
|
|
2
|
+
const SIZE_SPECS = [
|
|
3
|
+
{
|
|
4
|
+
name: "automatic",
|
|
5
|
+
rank: 1,
|
|
6
|
+
openCommand: "\\left",
|
|
7
|
+
closeCommand: "\\right",
|
|
8
|
+
kind: "automatic"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
name: "big",
|
|
12
|
+
rank: 1,
|
|
13
|
+
openCommand: "\\bigl",
|
|
14
|
+
closeCommand: "\\bigr",
|
|
15
|
+
kind: "manual"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "Big",
|
|
19
|
+
rank: 2,
|
|
20
|
+
openCommand: "\\Bigl",
|
|
21
|
+
closeCommand: "\\Bigr",
|
|
22
|
+
kind: "manual"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: "bigg",
|
|
26
|
+
rank: 3,
|
|
27
|
+
openCommand: "\\biggl",
|
|
28
|
+
closeCommand: "\\biggr",
|
|
29
|
+
kind: "manual"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: "Bigg",
|
|
33
|
+
rank: 4,
|
|
34
|
+
openCommand: "\\Biggl",
|
|
35
|
+
closeCommand: "\\Biggr",
|
|
36
|
+
kind: "manual"
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
const COMMAND_SPECS = SIZE_SPECS.flatMap((spec) => [
|
|
40
|
+
{ ...spec, command: spec.openCommand, tokenKind: "open" },
|
|
41
|
+
{ ...spec, command: spec.closeCommand, tokenKind: "close" }
|
|
42
|
+
]).sort((left, right) => right.command.length - left.command.length);
|
|
43
|
+
function delimiterAt(latex, offset, kind) {
|
|
44
|
+
const candidates = kind === "open" ? ["\\{", "(", "["] : ["\\}", ")", "]"];
|
|
45
|
+
for (const delimiter of candidates) {
|
|
46
|
+
if (latex.startsWith(delimiter, offset)) {
|
|
47
|
+
return { delimiter, end: offset + delimiter.length };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
function tokenAt(latex, offset) {
|
|
53
|
+
if (latex[offset] !== "\\") return void 0;
|
|
54
|
+
for (const spec of COMMAND_SPECS) {
|
|
55
|
+
if (!latex.startsWith(spec.command, offset)) continue;
|
|
56
|
+
const afterCommand = offset + spec.command.length;
|
|
57
|
+
if (/[A-Za-z]/.test(latex[afterCommand] ?? "")) continue;
|
|
58
|
+
let delimiterOffset = afterCommand;
|
|
59
|
+
while (/\s/.test(latex[delimiterOffset] ?? "")) delimiterOffset += 1;
|
|
60
|
+
const delimiter = delimiterAt(latex, delimiterOffset, spec.tokenKind);
|
|
61
|
+
if (!delimiter) continue;
|
|
62
|
+
return {
|
|
63
|
+
kind: spec.tokenKind,
|
|
64
|
+
pairKind: spec.kind,
|
|
65
|
+
rank: spec.rank,
|
|
66
|
+
size: spec.name,
|
|
67
|
+
delimiter: delimiter.delimiter,
|
|
68
|
+
offset,
|
|
69
|
+
end: delimiter.end
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
74
|
+
function pairMatches(open, close) {
|
|
75
|
+
return open.kind === "open" && close.kind === "close" && open.pairKind === close.pairKind && open.rank === close.rank;
|
|
76
|
+
}
|
|
77
|
+
function isEscaped(latex, offset) {
|
|
78
|
+
let slashes = 0;
|
|
79
|
+
for (let index = offset - 1; index >= 0 && latex[index] === "\\"; index -= 1) {
|
|
80
|
+
slashes += 1;
|
|
81
|
+
}
|
|
82
|
+
return slashes % 2 === 1;
|
|
83
|
+
}
|
|
84
|
+
function parseDelimiterPairs(latex) {
|
|
85
|
+
const roots = [];
|
|
86
|
+
const stack = [];
|
|
87
|
+
for (let offset = 0; offset < latex.length; offset += 1) {
|
|
88
|
+
if (latex[offset] === "%" && !isEscaped(latex, offset)) {
|
|
89
|
+
const newline = latex.indexOf("\n", offset + 1);
|
|
90
|
+
if (newline === -1) break;
|
|
91
|
+
offset = newline;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const token = isEscaped(latex, offset) ? void 0 : tokenAt(latex, offset);
|
|
95
|
+
if (!token) continue;
|
|
96
|
+
offset = token.end - 1;
|
|
97
|
+
if (token.kind === "open") {
|
|
98
|
+
const pair2 = { open: token, children: [] };
|
|
99
|
+
const parent = stack[stack.length - 1];
|
|
100
|
+
if (parent) parent.children.push(pair2);
|
|
101
|
+
else roots.push(pair2);
|
|
102
|
+
stack.push(pair2);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const pair = stack[stack.length - 1];
|
|
106
|
+
if (!pair || !pairMatches(pair.open, token)) return void 0;
|
|
107
|
+
pair.close = token;
|
|
108
|
+
stack.pop();
|
|
109
|
+
}
|
|
110
|
+
return stack.length === 0 ? roots : void 0;
|
|
111
|
+
}
|
|
112
|
+
function lineAndColumn(source, offset) {
|
|
113
|
+
const before = source.slice(0, offset);
|
|
114
|
+
const lastNewline = before.lastIndexOf("\n");
|
|
115
|
+
return {
|
|
116
|
+
line: before.split("\n").length,
|
|
117
|
+
column: offset - lastNewline
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const SPEC_BY_SIZE = new Map(SIZE_SPECS.map((spec) => [spec.name, spec]));
|
|
121
|
+
function suggestionFor(pair) {
|
|
122
|
+
const open = `\\left${pair.open.delimiter}`;
|
|
123
|
+
const close = `\\right${pair.close?.delimiter ?? pair.open.delimiter}`;
|
|
124
|
+
return { open, close, latex: `${open} ... ${close}` };
|
|
125
|
+
}
|
|
126
|
+
function lintDelimiterSizes(latex) {
|
|
127
|
+
const roots = parseDelimiterPairs(latex);
|
|
128
|
+
if (!roots) return [];
|
|
129
|
+
const diagnostics = [];
|
|
130
|
+
const visit = (pair, nested) => {
|
|
131
|
+
const inNest = nested || pair.children.length > 0;
|
|
132
|
+
if (inNest && pair.open.pairKind === "manual") {
|
|
133
|
+
const location = lineAndColumn(latex, pair.open.offset);
|
|
134
|
+
const suggestion = suggestionFor(pair);
|
|
135
|
+
diagnostics.push({
|
|
136
|
+
code: DELIMITER_SIZE_DIAGNOSTIC_CODE,
|
|
137
|
+
severity: "warning",
|
|
138
|
+
offset: pair.open.offset,
|
|
139
|
+
...location,
|
|
140
|
+
size: pair.open.size,
|
|
141
|
+
suggestion,
|
|
142
|
+
message: `A nested delimiter pair is sized by hand with ${SPEC_BY_SIZE.get(pair.open.size)?.openCommand ?? pair.open.size}${pair.open.delimiter}. Use ${suggestion.latex} and let the engine size the nest; preserve the original opening and closing delimiter shapes (rule N17).`
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
for (const child of pair.children) visit(child, true);
|
|
146
|
+
};
|
|
147
|
+
for (const root of roots) visit(root, false);
|
|
148
|
+
return diagnostics.sort((left, right) => left.offset - right.offset);
|
|
149
|
+
}
|
|
150
|
+
export {
|
|
151
|
+
DELIMITER_SIZE_DIAGNOSTIC_CODE,
|
|
152
|
+
lintDelimiterSizes
|
|
153
|
+
};
|
|
@@ -5,7 +5,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
|
|
|
5
5
|
type DiagramDiagnosticCode = 'dia-invalid-key' | 'dia-duplicate-key' | 'dia-ref-resolves' | 'dia-key-unused';
|
|
6
6
|
interface DiagramDiagnostic {
|
|
7
7
|
readonly code: DiagramDiagnosticCode;
|
|
8
|
-
readonly severity: 'error' | 'warning';
|
|
8
|
+
readonly severity: 'error' | 'warning' | 'info';
|
|
9
9
|
readonly message: string;
|
|
10
10
|
readonly file?: string;
|
|
11
11
|
readonly lessonId?: string;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* - `eq-duplicate-key` (Tier 1, error): an `eq:` key is unique per lesson.
|
|
5
5
|
* - `eq-ref-resolves` (Tier 1, error): every `[[eq-key]]` / `[[slug#eq-key]]`
|
|
6
6
|
* points at a display equation that carries `\label{eq:key}`.
|
|
7
|
-
* - `eq-key-unused` (
|
|
7
|
+
* - `eq-key-unused` (info): a labelled equation is never referenced.
|
|
8
8
|
*
|
|
9
9
|
* The remark pass enforces the same rules on the render path; this gives the
|
|
10
10
|
* fast `pnpm validate:content` the same coverage and feeds the manifest.
|
|
@@ -26,7 +26,7 @@ interface EquationRefRecord {
|
|
|
26
26
|
type EquationDiagnosticCode = 'eq-duplicate-key' | 'eq-ref-resolves' | 'eq-key-unused';
|
|
27
27
|
interface EquationDiagnostic {
|
|
28
28
|
readonly code: EquationDiagnosticCode;
|
|
29
|
-
readonly severity: 'error' | 'warning';
|
|
29
|
+
readonly severity: 'error' | 'warning' | 'info';
|
|
30
30
|
readonly message: string;
|
|
31
31
|
readonly file?: string;
|
|
32
32
|
readonly lessonId?: string;
|
|
@@ -73,7 +73,7 @@ function validateEquations(lessons) {
|
|
|
73
73
|
if (referenced.has(`${label.slug}#${label.key}`)) continue;
|
|
74
74
|
diagnostics.push({
|
|
75
75
|
code: "eq-key-unused",
|
|
76
|
-
severity: "
|
|
76
|
+
severity: "info",
|
|
77
77
|
message: `${label.lessonId} labels eq:${label.key} (${label.number}) but nothing references it with [[eq-${label.key}]]`,
|
|
78
78
|
file: label.slug,
|
|
79
79
|
lessonId: label.lessonId,
|
|
@@ -35,10 +35,16 @@ export function displayEquationNumber({ prefix, offset, section, indexInSection,
|
|
|
35
35
|
indexInSection: any;
|
|
36
36
|
indexInLesson: any;
|
|
37
37
|
}): string;
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* A display equation's anchor id: `eq-<key>` when it carries a
|
|
40
|
+
* `\label{eq:…}`, else the positional form, e.g. `2.4` → `eq-2-4`.
|
|
41
|
+
*
|
|
42
|
+
* @param {{ key?: string, number?: string | number }} equation - `number` is
|
|
43
|
+
* read only when there is no `key`, so a keyed caller may omit it.
|
|
44
|
+
*/
|
|
39
45
|
export function equationAnchorId({ key, number }: {
|
|
40
|
-
key
|
|
41
|
-
number
|
|
46
|
+
key?: string;
|
|
47
|
+
number?: string | number;
|
|
42
48
|
}): string;
|
|
43
49
|
export function stripFencedCode(markdown: any): string;
|
|
44
50
|
/**
|
|
@@ -106,7 +106,13 @@ export function displayEquationNumber({
|
|
|
106
106
|
: `${prefix}.${offset + indexInLesson}`;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* A display equation's anchor id: `eq-<key>` when it carries a
|
|
111
|
+
* `\label{eq:…}`, else the positional form, e.g. `2.4` → `eq-2-4`.
|
|
112
|
+
*
|
|
113
|
+
* @param {{ key?: string, number?: string | number }} equation - `number` is
|
|
114
|
+
* read only when there is no `key`, so a keyed caller may omit it.
|
|
115
|
+
*/
|
|
110
116
|
export function equationAnchorId({ key, number }) {
|
|
111
117
|
return key ? `eq-${key}` : `eq-${String(number).replace(/\./g, '-')}`;
|
|
112
118
|
}
|
|
@@ -5,7 +5,7 @@ import { KeyedFloatLabelRecord, KeyedFloatLessonInput, KeyedFloatValidation } fr
|
|
|
5
5
|
type FigureDiagnosticCode = 'fig-invalid-key' | 'fig-duplicate-key' | 'fig-ref-resolves' | 'fig-key-unused';
|
|
6
6
|
interface FigureDiagnostic {
|
|
7
7
|
readonly code: FigureDiagnosticCode;
|
|
8
|
-
readonly severity: 'error' | 'warning';
|
|
8
|
+
readonly severity: 'error' | 'warning' | 'info';
|
|
9
9
|
readonly message: string;
|
|
10
10
|
readonly file?: string;
|
|
11
11
|
readonly lessonId?: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { collectOperators, collectRomanLabels } from "./math-glyphs.mjs";
|
|
2
2
|
import { isBaseAbbreviation } from "./base-abbreviations.mjs";
|
|
3
3
|
import { pageScope } from "./gate-math.js";
|
|
4
|
-
import {
|
|
4
|
+
import { lessonLine, proseOnly } from "./gate-prose-notation.js";
|
|
5
5
|
const CAPS_RUN = /\b([A-Z]{2,})(?:'s|s)?\b/g;
|
|
6
6
|
const DOTTED_ABBR = /\b(?:[A-Za-z]\.){2,}/g;
|
|
7
7
|
function findAbbreviationLikeTokens(text) {
|
|
@@ -51,7 +51,7 @@ function gateAbbreviations(input) {
|
|
|
51
51
|
context: "abbreviation",
|
|
52
52
|
severity: "error",
|
|
53
53
|
file: lesson.source.file,
|
|
54
|
-
line:
|
|
54
|
+
line: lessonLine(lesson, index),
|
|
55
55
|
token,
|
|
56
56
|
message: `Unexplained abbreviation ${JSON.stringify(token)} outside math. ${FIX}`
|
|
57
57
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stripFencedCode } from "./equations.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { lessonLine } from "./gate-prose-notation.js";
|
|
3
3
|
const HEADING_LENGTH_LIMIT = 65;
|
|
4
4
|
const HEADING_LINE = /^[ \t]{0,3}(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/gm;
|
|
5
5
|
const TITLE_ATTRIBUTE_BY_TAG = {
|
|
@@ -65,7 +65,7 @@ function gateHeadingLength(lessons) {
|
|
|
65
65
|
context: "structure",
|
|
66
66
|
severity: "error",
|
|
67
67
|
file: lesson.source.file,
|
|
68
|
-
line:
|
|
68
|
+
line: lessonLine(lesson, finding.index),
|
|
69
69
|
token: finding.token,
|
|
70
70
|
message: finding.message
|
|
71
71
|
});
|
|
@@ -1,5 +1,19 @@
|
|
|
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
18
|
readonly context: 'body' | 'formula' | 'assessment' | 'prose' | 'abbreviation' | 'structure';
|
|
5
19
|
/**
|
|
@@ -26,9 +40,13 @@ interface MathGateDiagnostic {
|
|
|
26
40
|
* than adding a second array the CLI has to know about.
|
|
27
41
|
*/
|
|
28
42
|
readonly severity: 'error' | 'warning';
|
|
43
|
+
readonly code?: string;
|
|
29
44
|
readonly file: string;
|
|
30
45
|
readonly line?: number;
|
|
46
|
+
readonly column?: number;
|
|
31
47
|
readonly token?: string;
|
|
48
|
+
/** Present when the offending math carries a `\label{eq:…}`. */
|
|
49
|
+
readonly equation?: EquationLink;
|
|
32
50
|
readonly message: string;
|
|
33
51
|
}
|
|
34
52
|
interface AssessmentItemLike {
|
|
@@ -56,6 +74,12 @@ type ScopeEntry = {
|
|
|
56
74
|
* hand and risk drifting from what the completeness gate actually checks.
|
|
57
75
|
*/
|
|
58
76
|
declare function pageScope(lesson: NotationLessonInput, sharedByKey: ReadonlyMap<string, ScopeEntry>): ScopeEntry[];
|
|
77
|
+
/**
|
|
78
|
+
* The `\label{eq:…}` a math source carries, as a key plus the anchor the
|
|
79
|
+
* rendered page gives it. `EQ_LABEL_PATTERN` is global, so its `lastIndex` is
|
|
80
|
+
* reset before use — a stale one would skip the first label of the next span.
|
|
81
|
+
*/
|
|
82
|
+
declare function equationLinkFor(lessonId: string, latex: string): EquationLink | undefined;
|
|
59
83
|
/**
|
|
60
84
|
* Resolve every rendered-math context against the page glyph table. Returns one
|
|
61
85
|
* diagnostic per unresolved token; an empty array means the gate passed.
|
|
@@ -66,4 +90,4 @@ declare function gateContentMath(input: {
|
|
|
66
90
|
readonly lessonAssessments: readonly LessonAssessmentLink[];
|
|
67
91
|
}): MathGateDiagnostic[];
|
|
68
92
|
|
|
69
|
-
export { type MathGateDiagnostic, type ScopeEntry, gateContentMath, pageScope };
|
|
93
|
+
export { type EquationLink, type MathGateDiagnostic, type ScopeEntry, equationLinkFor, gateContentMath, pageScope };
|
|
@@ -1,5 +1,10 @@
|
|
|
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";
|
|
7
|
+
import { lintDelimiterSizes } from "./delimiter-size.js";
|
|
3
8
|
import { formulaGlyphScope, unknownFormulaScopeKeys } from "./gloss.js";
|
|
4
9
|
const SEVERITY = {
|
|
5
10
|
body: "error",
|
|
@@ -26,10 +31,14 @@ function mathSpans(body) {
|
|
|
26
31
|
pattern.lastIndex = 0;
|
|
27
32
|
let match;
|
|
28
33
|
while (match = pattern.exec(searchable)) {
|
|
34
|
+
const latex = match[1] ?? "";
|
|
35
|
+
const latexOffset = match.index + match[0].indexOf(latex);
|
|
36
|
+
const before = body.slice(0, latexOffset);
|
|
37
|
+
const lastNewline = before.lastIndexOf("\n");
|
|
29
38
|
spans.push({
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
latex,
|
|
40
|
+
line: lineOf(body, latexOffset),
|
|
41
|
+
column: latexOffset - lastNewline
|
|
33
42
|
});
|
|
34
43
|
}
|
|
35
44
|
}
|
|
@@ -53,10 +62,34 @@ function pageScope(lesson, sharedByKey) {
|
|
|
53
62
|
}
|
|
54
63
|
return scope;
|
|
55
64
|
}
|
|
56
|
-
function
|
|
65
|
+
function equationLinkFor(lessonId, latex) {
|
|
66
|
+
EQ_LABEL_PATTERN.lastIndex = 0;
|
|
67
|
+
const match = EQ_LABEL_PATTERN.exec(latex);
|
|
68
|
+
EQ_LABEL_PATTERN.lastIndex = 0;
|
|
69
|
+
if (!match) return void 0;
|
|
70
|
+
const key = match[1];
|
|
71
|
+
const slug = lessonId.replace(/\./g, "/");
|
|
72
|
+
return { key, href: `/${slug}/#${equationAnchorId({ key })}` };
|
|
73
|
+
}
|
|
74
|
+
function resolveInto(latex, scope, context, file, line, column, fix, out, equation) {
|
|
75
|
+
const at = equation === void 0 ? {} : { equation };
|
|
76
|
+
for (const diagnostic of lintDelimiterSizes(latex)) {
|
|
77
|
+
const locatedLine = line === void 0 ? void 0 : line + diagnostic.line - 1;
|
|
78
|
+
const locatedColumn = column === void 0 ? void 0 : diagnostic.line === 1 ? column + diagnostic.column - 1 : diagnostic.column;
|
|
79
|
+
out.push({
|
|
80
|
+
context,
|
|
81
|
+
code: diagnostic.code,
|
|
82
|
+
severity: "warning",
|
|
83
|
+
file,
|
|
84
|
+
...locatedLine === void 0 ? {} : { line: locatedLine },
|
|
85
|
+
...locatedColumn === void 0 ? {} : { column: locatedColumn },
|
|
86
|
+
...at,
|
|
87
|
+
message: diagnostic.message
|
|
88
|
+
});
|
|
89
|
+
}
|
|
57
90
|
let result;
|
|
58
91
|
try {
|
|
59
|
-
result = resolveMathGlyphs(latex, [...scope]);
|
|
92
|
+
result = resolveMathGlyphs(stripEquationLabels(latex), [...scope]);
|
|
60
93
|
} catch (error) {
|
|
61
94
|
out.push({
|
|
62
95
|
context,
|
|
@@ -74,6 +107,7 @@ function resolveInto(latex, scope, context, file, line, fix, out) {
|
|
|
74
107
|
file,
|
|
75
108
|
line,
|
|
76
109
|
token,
|
|
110
|
+
...at,
|
|
77
111
|
message: `Unresolved notation ${JSON.stringify(token)} in ${context} math. ${fix}`
|
|
78
112
|
});
|
|
79
113
|
}
|
|
@@ -138,15 +172,18 @@ function gateContentMath(input) {
|
|
|
138
172
|
for (const lesson of notationInput.lessons) {
|
|
139
173
|
const scope = pageScope(lesson, sharedByKey);
|
|
140
174
|
const file = lesson.source.file;
|
|
175
|
+
const bodyOffset = (lesson.bodyLine ?? 1) - 1;
|
|
141
176
|
for (const span of mathSpans(lesson.body)) {
|
|
142
177
|
resolveInto(
|
|
143
178
|
span.latex,
|
|
144
179
|
scope,
|
|
145
180
|
"body",
|
|
146
181
|
file,
|
|
147
|
-
span.line,
|
|
182
|
+
span.line + bodyOffset,
|
|
183
|
+
span.column,
|
|
148
184
|
BODY_FIX,
|
|
149
|
-
diagnostics
|
|
185
|
+
diagnostics,
|
|
186
|
+
equationLinkFor(lesson.lessonId, span.latex)
|
|
150
187
|
);
|
|
151
188
|
}
|
|
152
189
|
for (const definition of lesson.localDefinitions) {
|
|
@@ -163,6 +200,7 @@ function gateContentMath(input) {
|
|
|
163
200
|
"formula",
|
|
164
201
|
file,
|
|
165
202
|
void 0,
|
|
203
|
+
void 0,
|
|
166
204
|
FORMULA_FIX,
|
|
167
205
|
diagnostics
|
|
168
206
|
);
|
|
@@ -183,6 +221,7 @@ function gateContentMath(input) {
|
|
|
183
221
|
"formula",
|
|
184
222
|
definition.source.file,
|
|
185
223
|
void 0,
|
|
224
|
+
void 0,
|
|
186
225
|
SHARED_FORMULA_FIX,
|
|
187
226
|
diagnostics
|
|
188
227
|
);
|
|
@@ -209,7 +248,8 @@ function gateContentMath(input) {
|
|
|
209
248
|
scope,
|
|
210
249
|
"assessment",
|
|
211
250
|
`${assessmentId} (${item.id ?? "?"})`,
|
|
212
|
-
|
|
251
|
+
span.line,
|
|
252
|
+
span.column,
|
|
213
253
|
ASSESSMENT_FIX,
|
|
214
254
|
diagnostics
|
|
215
255
|
);
|
|
@@ -221,6 +261,7 @@ function gateContentMath(input) {
|
|
|
221
261
|
return diagnostics;
|
|
222
262
|
}
|
|
223
263
|
export {
|
|
264
|
+
equationLinkFor,
|
|
224
265
|
gateContentMath,
|
|
225
266
|
pageScope
|
|
226
267
|
};
|
|
@@ -39,6 +39,17 @@ interface AssessmentLike {
|
|
|
39
39
|
declare function proseOnly(body: string): string;
|
|
40
40
|
/** Exported for the same reason as {@link proseOnly}. */
|
|
41
41
|
declare function lineOf(text: string, index: number): number;
|
|
42
|
+
/**
|
|
43
|
+
* {@link lineOf} corrected to the file the lesson came from. A gate scans
|
|
44
|
+
* `lesson.body`, which starts *after* the frontmatter, so a raw body line is
|
|
45
|
+
* short by the frontmatter's height and points at the wrong source line —
|
|
46
|
+
* silently, because the number is still plausible. Every gate that reports a
|
|
47
|
+
* `line` against `lesson.source.file` must go through this.
|
|
48
|
+
*/
|
|
49
|
+
declare function lessonLine(lesson: {
|
|
50
|
+
readonly body: string;
|
|
51
|
+
readonly bodyLine?: number;
|
|
52
|
+
}, index: number): number;
|
|
42
53
|
/**
|
|
43
54
|
* Scan every lesson body for plain-text math notation outside `$…$`. Returns
|
|
44
55
|
* one `MathGateDiagnostic` (context `'prose'`, always an error — N9 has no
|
|
@@ -57,4 +68,4 @@ declare function gateProseNotation(lessons: readonly NotationLessonInput[]): Mat
|
|
|
57
68
|
*/
|
|
58
69
|
declare function gateProseNotationInAssessments(assessments: readonly AssessmentLike[]): MathGateDiagnostic[];
|
|
59
70
|
|
|
60
|
-
export { gateProseNotation, gateProseNotationInAssessments, lineOf, proseOnly };
|
|
71
|
+
export { gateProseNotation, gateProseNotationInAssessments, lessonLine, lineOf, proseOnly };
|
|
@@ -12,6 +12,9 @@ function proseOnly(body) {
|
|
|
12
12
|
function lineOf(text, index) {
|
|
13
13
|
return text.slice(0, index).split("\n").length;
|
|
14
14
|
}
|
|
15
|
+
function lessonLine(lesson, index) {
|
|
16
|
+
return lineOf(lesson.body, index) + (lesson.bodyLine ?? 1) - 1;
|
|
17
|
+
}
|
|
15
18
|
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
19
|
const NUMERAL_UNIT = new RegExp(
|
|
17
20
|
String.raw`(?<![\w.])-?\d+(?:\.\d+)?\s?(?:${UNIT})(?![A-Za-z])`,
|
|
@@ -89,7 +92,7 @@ function gateProseNotation(lessons) {
|
|
|
89
92
|
context: "prose",
|
|
90
93
|
severity: "error",
|
|
91
94
|
file: lesson.source.file,
|
|
92
|
-
line:
|
|
95
|
+
line: lessonLine(lesson, finding.index),
|
|
93
96
|
token: finding.token,
|
|
94
97
|
message: finding.message
|
|
95
98
|
});
|
|
@@ -120,6 +123,7 @@ function gateProseNotationInAssessments(assessments) {
|
|
|
120
123
|
export {
|
|
121
124
|
gateProseNotation,
|
|
122
125
|
gateProseNotationInAssessments,
|
|
126
|
+
lessonLine,
|
|
123
127
|
lineOf,
|
|
124
128
|
proseOnly
|
|
125
129
|
};
|
|
@@ -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';
|