explico 0.7.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 +171 -1
- package/dist/compiler/cli.d.ts +8 -1
- package/dist/compiler/cli.js +10 -2
- package/dist/compiler/collections.js +16 -3
- package/dist/compiler/logging.d.ts +11 -1
- package/dist/compiler/logging.js +2 -1
- package/dist/compiler/manifest.d.ts +1 -1
- package/dist/compiler/manifest.js +1 -1
- package/dist/reference/delimiter-size.d.ts +18 -9
- package/dist/reference/delimiter-size.js +15 -39
- package/dist/reference/equations.d.mts +9 -3
- package/dist/reference/equations.mjs +7 -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 +23 -1
- package/dist/reference/gate-math.js +23 -4
- 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/math-glyphs.mjs +16 -2
- package/dist/reference/render-lab-math.js +1 -1
- package/dist/reference/types.d.ts +7 -0
- package/docs/authoring/README.md +1 -1
- package/docs/authoring/migrating.md +228 -0
- package/docs/authoring/notation.md +80 -62
- package/docs/katex-bracket-scaling.md +191 -0
- package/package.json +2 -1
- package/src/compiler/cli.ts +23 -0
- package/src/compiler/collections.ts +33 -3
- package/src/compiler/logging.ts +14 -3
- package/src/compiler/manifest.ts +1 -1
- package/src/components/notation/NotationGlossary.astro +21 -10
- package/src/components/notation/render-notation-math.ts +111 -10
- package/src/components/starlight/AboutPanel.astro +17 -2
- package/src/reference/delimiter-size.ts +37 -59
- package/src/reference/equations.mjs +7 -1
- package/src/reference/gate-abbreviations.ts +2 -2
- package/src/reference/gate-heading-length.ts +2 -2
- package/src/reference/gate-math.ts +50 -2
- 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/math-glyphs.mjs +16 -2
- package/src/reference/render-lab-math.ts +1 -1
- package/src/reference/types.ts +7 -0
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { resolveMathGlyphs, GlyphResolutionError } from "./math-glyphs.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
EQ_LABEL_PATTERN,
|
|
4
|
+
equationAnchorId,
|
|
5
|
+
stripEquationLabels
|
|
6
|
+
} from "./equations.mjs";
|
|
3
7
|
import { lintDelimiterSizes } from "./delimiter-size.js";
|
|
4
8
|
import { formulaGlyphScope, unknownFormulaScopeKeys } from "./gloss.js";
|
|
5
9
|
const SEVERITY = {
|
|
@@ -58,7 +62,17 @@ function pageScope(lesson, sharedByKey) {
|
|
|
58
62
|
}
|
|
59
63
|
return scope;
|
|
60
64
|
}
|
|
61
|
-
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 };
|
|
62
76
|
for (const diagnostic of lintDelimiterSizes(latex)) {
|
|
63
77
|
const locatedLine = line === void 0 ? void 0 : line + diagnostic.line - 1;
|
|
64
78
|
const locatedColumn = column === void 0 ? void 0 : diagnostic.line === 1 ? column + diagnostic.column - 1 : diagnostic.column;
|
|
@@ -69,6 +83,7 @@ function resolveInto(latex, scope, context, file, line, column, fix, out) {
|
|
|
69
83
|
file,
|
|
70
84
|
...locatedLine === void 0 ? {} : { line: locatedLine },
|
|
71
85
|
...locatedColumn === void 0 ? {} : { column: locatedColumn },
|
|
86
|
+
...at,
|
|
72
87
|
message: diagnostic.message
|
|
73
88
|
});
|
|
74
89
|
}
|
|
@@ -92,6 +107,7 @@ function resolveInto(latex, scope, context, file, line, column, fix, out) {
|
|
|
92
107
|
file,
|
|
93
108
|
line,
|
|
94
109
|
token,
|
|
110
|
+
...at,
|
|
95
111
|
message: `Unresolved notation ${JSON.stringify(token)} in ${context} math. ${fix}`
|
|
96
112
|
});
|
|
97
113
|
}
|
|
@@ -156,16 +172,18 @@ function gateContentMath(input) {
|
|
|
156
172
|
for (const lesson of notationInput.lessons) {
|
|
157
173
|
const scope = pageScope(lesson, sharedByKey);
|
|
158
174
|
const file = lesson.source.file;
|
|
175
|
+
const bodyOffset = (lesson.bodyLine ?? 1) - 1;
|
|
159
176
|
for (const span of mathSpans(lesson.body)) {
|
|
160
177
|
resolveInto(
|
|
161
178
|
span.latex,
|
|
162
179
|
scope,
|
|
163
180
|
"body",
|
|
164
181
|
file,
|
|
165
|
-
span.line,
|
|
182
|
+
span.line + bodyOffset,
|
|
166
183
|
span.column,
|
|
167
184
|
BODY_FIX,
|
|
168
|
-
diagnostics
|
|
185
|
+
diagnostics,
|
|
186
|
+
equationLinkFor(lesson.lessonId, span.latex)
|
|
169
187
|
);
|
|
170
188
|
}
|
|
171
189
|
for (const definition of lesson.localDefinitions) {
|
|
@@ -243,6 +261,7 @@ function gateContentMath(input) {
|
|
|
243
261
|
return diagnostics;
|
|
244
262
|
}
|
|
245
263
|
export {
|
|
264
|
+
equationLinkFor,
|
|
246
265
|
gateContentMath,
|
|
247
266
|
pageScope
|
|
248
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';
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule N17's engine half: make `\left`/`\right` grow strictly outward.
|
|
3
|
+
*
|
|
4
|
+
* Importing this module for its side effect installs a KaTeX whose
|
|
5
|
+
* `\delimitershortfall` is `-1pt` instead of the stock `5pt`, so a scalable
|
|
6
|
+
* delimiter pair is always *strictly* taller than what it wraps and nesting
|
|
7
|
+
* depth shows up on its own. See `docs/katex-bracket-scaling.md` for the
|
|
8
|
+
* derivation and `docs/authoring/notation.md#n17` for the authoring rule.
|
|
9
|
+
*
|
|
10
|
+
* ## Why this is not a `patches/` file
|
|
11
|
+
*
|
|
12
|
+
* `delimiterExtend` is a function-local `const` inside `makeLeftRightDelim`.
|
|
13
|
+
* Nothing KaTeX exports reaches it — `__defineMacro`, `__defineFunction`,
|
|
14
|
+
* `__defineSymbol`, `__setFontMetrics` and the settings schema all sit above
|
|
15
|
+
* the layout engine — so the constant can only be changed in the module's own
|
|
16
|
+
* source. A `pnpm patch` does that, but a patch is project-local: it cannot
|
|
17
|
+
* travel inside the published `explico` artifact, so every downstream course
|
|
18
|
+
* would have to reapply it or silently render flat nests.
|
|
19
|
+
*
|
|
20
|
+
* So the engine does it in code instead. It reads KaTeX's own ESM bundle,
|
|
21
|
+
* rewrites that one constant, imports the result as a module, and moves the
|
|
22
|
+
* rendering entry points onto the shared `katex` object every consumer in the
|
|
23
|
+
* process already imported. KaTeX's default export is a plain mutable object
|
|
24
|
+
* and `rehype-katex` reads `katex.renderToString` at call time, so the swap
|
|
25
|
+
* reaches lesson-body math without forking the plugin.
|
|
26
|
+
*
|
|
27
|
+
* ## Why this is safe to rely on
|
|
28
|
+
*
|
|
29
|
+
* The assumptions above (a matching constant, a mutable export, a bundle with
|
|
30
|
+
* no imports of its own) are all checked here, and `assertDelimiterGrowth()`
|
|
31
|
+
* renders a known nest afterwards and throws when the sizes do not actually
|
|
32
|
+
* increase. A course therefore either renders with engine-sized delimiters or
|
|
33
|
+
* fails its build saying so — it cannot quietly get the stock behaviour.
|
|
34
|
+
*/
|
|
35
|
+
import { readFileSync } from 'node:fs';
|
|
36
|
+
import { createRequire } from 'node:module';
|
|
37
|
+
import katex from 'katex';
|
|
38
|
+
|
|
39
|
+
/** TeX's `\delimitershortfall`, in points, as KaTeX hard-codes it. */
|
|
40
|
+
const STOCK_SHORTFALL = '5.0';
|
|
41
|
+
/** The negative shortfall that forces strict outward growth. */
|
|
42
|
+
const GROWN_SHORTFALL = '-1.0';
|
|
43
|
+
|
|
44
|
+
const needle = (points) =>
|
|
45
|
+
`var delimiterExtend = ${points} / options.fontMetrics().ptPerEm;`;
|
|
46
|
+
|
|
47
|
+
/** The entry points that build a delimiter and must come from the rewrite. */
|
|
48
|
+
const RENDER_EXPORTS = [
|
|
49
|
+
'render',
|
|
50
|
+
'renderToString',
|
|
51
|
+
'__renderToDomTree',
|
|
52
|
+
'__renderToHTMLTree',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Exported for the test that pins both failure messages — a KaTeX upgrade that
|
|
57
|
+
* moves the constant, and a leftover `patches/` file, are the two ways this
|
|
58
|
+
* stops being able to install itself.
|
|
59
|
+
*/
|
|
60
|
+
export function rewriteBundle(source) {
|
|
61
|
+
const stock = needle(STOCK_SHORTFALL);
|
|
62
|
+
if (!source.includes(stock)) {
|
|
63
|
+
// Either the constant moved or someone already rewrote it. Both mean the
|
|
64
|
+
// assumption this module rests on no longer holds for this KaTeX build.
|
|
65
|
+
const alreadyGrown = source.includes(needle(GROWN_SHORTFALL));
|
|
66
|
+
throw new Error(
|
|
67
|
+
alreadyGrown
|
|
68
|
+
? `explico: katex ${katex.version} already carries the -1pt delimiter ` +
|
|
69
|
+
'shortfall (a patches/ file?). Remove that patch — the engine ' +
|
|
70
|
+
'installs it in code (src/reference/katex-delimiters.mjs).'
|
|
71
|
+
: `explico: cannot install the N17 delimiter sizing into katex ` +
|
|
72
|
+
`${katex.version}: the \\delimitershortfall constant ` +
|
|
73
|
+
`(${stock}) is not in its ESM bundle. Re-derive the one-line ` +
|
|
74
|
+
'change against the new bundle — see docs/katex-bracket-scaling.md.',
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return source.replace(stock, needle(GROWN_SHORTFALL));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function loadGrownKatex() {
|
|
81
|
+
const require = createRequire(import.meta.url);
|
|
82
|
+
const source = readFileSync(require.resolve('katex/dist/katex.mjs'), 'utf8');
|
|
83
|
+
// The bundle is self-contained, so it can be re-imported from a data URL
|
|
84
|
+
// with no base to resolve relative specifiers against.
|
|
85
|
+
const rewritten = rewriteBundle(source);
|
|
86
|
+
const module = await import(
|
|
87
|
+
`data:text/javascript;base64,${Buffer.from(rewritten).toString('base64')}`
|
|
88
|
+
);
|
|
89
|
+
return module.default;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Render a known nest and confirm the outer pair really is larger than the
|
|
94
|
+
* inner one. Cheap, and it turns every way this could go wrong — an upstream
|
|
95
|
+
* refactor, a frozen export, an import that beat us to it — into one loud
|
|
96
|
+
* failure at the point of installation.
|
|
97
|
+
*/
|
|
98
|
+
export function assertDelimiterGrowth(instance = katex) {
|
|
99
|
+
const html = instance.renderToString(
|
|
100
|
+
String.raw`\left[1+\left(\frac{a}{b}\right)\right]`,
|
|
101
|
+
{ displayMode: true, output: 'html' },
|
|
102
|
+
);
|
|
103
|
+
const sizes = [...html.matchAll(/delimsizing size(\d)/g)].map((m) =>
|
|
104
|
+
Number(m[1]),
|
|
105
|
+
);
|
|
106
|
+
// Two pairs, so four glyphs: outer, inner, inner, outer.
|
|
107
|
+
const grew = sizes.length === 4 && sizes[0] > sizes[1];
|
|
108
|
+
if (!grew) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
'explico: the N17 delimiter sizing did not take effect — a nested ' +
|
|
111
|
+
`\\left…\\right pair rendered as [${sizes.join(', ')}] instead of ` +
|
|
112
|
+
'growing outward. Something rendered math before ' +
|
|
113
|
+
"'explico/reference/katex-delimiters.mjs' was imported, or katex " +
|
|
114
|
+
'changed shape. See docs/katex-bracket-scaling.md.',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const grown = await loadGrownKatex();
|
|
120
|
+
for (const name of RENDER_EXPORTS) {
|
|
121
|
+
if (typeof grown[name] === 'function') katex[name] = grown[name];
|
|
122
|
+
}
|
|
123
|
+
assertDelimiterGrowth();
|
|
124
|
+
|
|
125
|
+
export default katex;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Side-effect import: installs rule N17's delimiter sizing onto the shared
|
|
2
|
+
// `katex` module before anything renders. A course wires this file into its
|
|
3
|
+
// `astro.config.mjs`, so `rehype-katex` inherits it without a patch.
|
|
4
|
+
import './katex-delimiters.mjs';
|
|
1
5
|
import { NOTATION_KEY_PATTERN } from './remark-notation.mjs';
|
|
2
6
|
|
|
3
7
|
export const EXPLAIN_MACRO = String.raw`\htmlData{notation-key=#1}{#2}`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import katex from 'katex';
|
|
1
|
+
import katex from './katex-delimiters.mjs';
|
|
2
2
|
|
|
3
3
|
import { BASE_LIBRARY } from './base-library.mjs';
|
|
4
4
|
import { PARENTHESIZED_FUNCTIONS } from './base-operators.mjs';
|
|
@@ -386,6 +386,19 @@ function trimTrailingWhitespace(latex, start, end) {
|
|
|
386
386
|
return trimmed;
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
+
/**
|
|
390
|
+
* KaTeX gives a node it produced by expanding a macro a `loc` from the lexer
|
|
391
|
+
* over the *macro body*, not over the source being scanned: `\vdots` expands
|
|
392
|
+
* to `{\varvdots\rule{0pt}{15pt}}`, and its `\varvdots` atom arrives with
|
|
393
|
+
* `loc.start = 1`, `loc.end = 10` — offsets into that 27-character body.
|
|
394
|
+
* Those numbers are valid indices into any longer source, so they read as a
|
|
395
|
+
* real span pointing at unrelated text. Every `loc` therefore has to prove it
|
|
396
|
+
* belongs to the string this pass parsed before its offsets are believed.
|
|
397
|
+
*/
|
|
398
|
+
function isAuthoredLocation(loc, latex) {
|
|
399
|
+
return loc?.lexer?.input === latex;
|
|
400
|
+
}
|
|
401
|
+
|
|
389
402
|
function nodeSpan(node, latex) {
|
|
390
403
|
let start = Number.POSITIVE_INFINITY;
|
|
391
404
|
let end = Number.NEGATIVE_INFINITY;
|
|
@@ -397,7 +410,8 @@ function nodeSpan(node, latex) {
|
|
|
397
410
|
if (
|
|
398
411
|
value.loc &&
|
|
399
412
|
Number.isInteger(value.loc.start) &&
|
|
400
|
-
Number.isInteger(value.loc.end)
|
|
413
|
+
Number.isInteger(value.loc.end) &&
|
|
414
|
+
isAuthoredLocation(value.loc, latex)
|
|
401
415
|
) {
|
|
402
416
|
start = Math.min(start, value.loc.start);
|
|
403
417
|
end = Math.max(end, value.loc.end);
|
|
@@ -78,6 +78,13 @@ interface NotationLessonInput {
|
|
|
78
78
|
readonly references: readonly NotationReferenceInput[];
|
|
79
79
|
/** Original lesson body, retained for lossless loader adapters. */
|
|
80
80
|
readonly body: string;
|
|
81
|
+
/**
|
|
82
|
+
* 1-based line in `source.file` where `body` starts. Gates scan the body, so
|
|
83
|
+
* every finding's index is body-relative; this is what turns one into a real
|
|
84
|
+
* location in the file. Optional so a hand-built input (a test, an adapter)
|
|
85
|
+
* stays valid — it then behaves as 1, the pre-0.7.1 behavior.
|
|
86
|
+
*/
|
|
87
|
+
readonly bodyLine?: number;
|
|
81
88
|
readonly source: SourceSpan;
|
|
82
89
|
}
|
|
83
90
|
interface NotationRegistryInput {
|
package/docs/authoring/README.md
CHANGED
|
@@ -106,7 +106,7 @@ file table.
|
|
|
106
106
|
| [N14](notation.md#n14) | An abbreviation-shaped word in prose is universal, or explained | Build-enforced (`gate-abbreviations.ts`) |
|
|
107
107
|
| [N15](notation.md#n15) | A nested `(`, `[`, `{`, or `\|` must use scalable sizing | Build-enforced (`resolveMathGlyphs`) |
|
|
108
108
|
| [N16](notation.md#n16) | AI-generated stylistic grouping alternates delimiter types | Review-only |
|
|
109
|
-
| [N17](notation.md#n17) |
|
|
109
|
+
| [N17](notation.md#n17) | A nested delimiter is sized by the engine, never by hand | Tier-3 warning (`delimiter-manual-size`) |
|
|
110
110
|
| [V1](visualizations.md#v1) | Graphs and flowcharts use the `Diagram` component | Review-only |
|
|
111
111
|
| [V2](visualizations.md#v2) | Static charts: SVG → D3 → Observable Plot, chosen per figure | Review-only |
|
|
112
112
|
| [V3](visualizations.md#v3) | Interactive figures: island + `renderLabMath` seam + text alt | Review-only |
|
|
@@ -33,6 +33,234 @@ required config block throws with the section of this file to read.
|
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
|
+
## 0.7.0 → 0.8.0
|
|
37
|
+
|
|
38
|
+
### 1. Rule N17 is inverted: delimiters are sized by the engine (`\left`/`\right` everywhere)
|
|
39
|
+
|
|
40
|
+
The engine now installs its own KaTeX delimiter sizing, so a `\left … \right`
|
|
41
|
+
pair is always strictly taller than its content and a nest shows its depth
|
|
42
|
+
without help. Hand-picking `\bigl` → `\Bigl` → `\biggl` → `\Biggl` outward —
|
|
43
|
+
which rule N17 previously *required* — is now the thing the linter warns about.
|
|
44
|
+
|
|
45
|
+
**There is no install step.** `astro.config.mjs` already imports
|
|
46
|
+
`explico/reference/katex-options.mjs`, and that module now imports
|
|
47
|
+
`explico/reference/katex-delimiters.mjs` for its side effect. Confirm the import
|
|
48
|
+
is still there; a course that inlined its own KaTeX options object instead must
|
|
49
|
+
add, as the **first** import in `astro.config.mjs`:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import 'explico/reference/katex-delimiters.mjs';
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Order matters: the installer moves KaTeX's render entry points onto the shared
|
|
56
|
+
`katex` object, so anything that renders math before the import would keep stock
|
|
57
|
+
spacing. It refuses to fail quietly — it renders a known nest at install time and
|
|
58
|
+
throws if the sizes did not grow.
|
|
59
|
+
|
|
60
|
+
Two course-side conditions make the build throw, both with an explicit message:
|
|
61
|
+
|
|
62
|
+
- **A `patches/katex@*.patch` file that already applies a negative
|
|
63
|
+
`\delimitershortfall`.** Delete the patch and its `pnpm.patchedDependencies`
|
|
64
|
+
entry; the engine does this in code now, and a patch is project-local anyway
|
|
65
|
+
so it never reached a consumer.
|
|
66
|
+
- **A `katex` whose bundle no longer holds the constant where the engine looks
|
|
67
|
+
for it.** The rewrite targets one line in KaTeX's ESM bundle, so keep `katex`
|
|
68
|
+
on the declared peer version (`0.16.47`). An upgrade that moves or reformats
|
|
69
|
+
that line fails the build asking you to re-derive the change against
|
|
70
|
+
[`../katex-bracket-scaling.md`](../katex-bracket-scaling.md), which is
|
|
71
|
+
shipped in the package. The failure is loud and immediate, not a silent
|
|
72
|
+
return to stock spacing.
|
|
73
|
+
|
|
74
|
+
### 2. Diagnostic renamed: `delimiter-size-hierarchy` → `delimiter-manual-size`
|
|
75
|
+
|
|
76
|
+
The old code string no longer exists. Update every place that names it:
|
|
77
|
+
|
|
78
|
+
- `lint-ignore.yml` — a stale line is inert and silently stops suppressing.
|
|
79
|
+
- Course scripts, tests, and type narrowings over the diagnostic union;
|
|
80
|
+
`astro check` flags a comparison against a non-member.
|
|
81
|
+
|
|
82
|
+
Its payload changed shape too. A consumer reading the fields, rather than just
|
|
83
|
+
the `code` and `message`, needs this edit:
|
|
84
|
+
|
|
85
|
+
```diff
|
|
86
|
+
-// outerSize / nestedSize described the mismatch between two levels
|
|
87
|
+
-const found = diagnostic.outerSize;
|
|
88
|
+
-const suggested = diagnostic.suggestion?.size;
|
|
89
|
+
+// `size` is the hand-picked size found; `suggestion` is always present and is
|
|
90
|
+
+// always a \left/\right pair, so it no longer carries a `size` of its own.
|
|
91
|
+
+const found = diagnostic.size;
|
|
92
|
+
+const suggested = diagnostic.suggestion.latex;
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`DelimiterSizeSuggestion` keeps `open`, `close`, and `latex`, and drops `size`.
|
|
96
|
+
`DelimiterSizeDiagnostic` drops `outerSize` and `nestedSize`, gains a required
|
|
97
|
+
`size`, and `suggestion` is no longer optional.
|
|
98
|
+
|
|
99
|
+
### 3. Content: hand-sized nests now warn
|
|
100
|
+
|
|
101
|
+
Math that was N17-compliant under 0.7.0 reports one `delimiter-manual-size`
|
|
102
|
+
finding per hand-sized level. This is a Tier-3 warning — it does not fail a
|
|
103
|
+
build — so it can be worked through at your own pace, but the findings are real
|
|
104
|
+
and the repair improves the rendering.
|
|
105
|
+
|
|
106
|
+
Find them with:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
pnpm content warnings --json
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The repair is mechanical: replace the hand-picked pair with `\left`/`\right`,
|
|
113
|
+
keeping the delimiter shapes.
|
|
114
|
+
|
|
115
|
+
```diff
|
|
116
|
+
-\Biggl[a+\biggl(b+\Bigl[c+\left(d+e\right)\Bigr]\biggr)\Biggr]
|
|
117
|
+
+\left[a+\left(b+\left[c+\left(d+e\right)\right]\right)\right]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
A **lone** hand-sized pair that is not part of a nest — `\bigl(x,\ y\bigr)`
|
|
121
|
+
around a tuple, for instance — is a single-level typographic choice with no
|
|
122
|
+
hierarchy to get wrong, and is deliberately left alone. Do not convert those.
|
|
123
|
+
|
|
124
|
+
Expect a small visual change everywhere a nest is rendered: each level is now
|
|
125
|
+
strictly larger than the one inside it, so outer delimiters grow by roughly one
|
|
126
|
+
size step. A course holding screenshot or bounding-box baselines over math
|
|
127
|
+
should re-record them.
|
|
128
|
+
|
|
129
|
+
### 4. Manifest schema is now v16: a math diagnostic can name its equation
|
|
130
|
+
|
|
131
|
+
`MANIFEST_SCHEMA_VERSION` is 16 (v15 → v16). The bump is **additive** — nothing
|
|
132
|
+
existing changed shape — so the only required edit is to a client that asserts
|
|
133
|
+
the number by exact equality, which now expects 16.
|
|
134
|
+
|
|
135
|
+
What was added: a finding the math gate reports from a `$$…$$` block carrying a
|
|
136
|
+
`\label{eq:…}` gains an optional `equation` field, `{ key, href }`. `href` is
|
|
137
|
+
the rendered page's anchor for that equation
|
|
138
|
+
(`/l1-ar/ar1-basics/#eq-ar1-variance`). `CliDiagnostic` carries the same
|
|
139
|
+
optional field, so a course's own `scripts/content.ts` picks it up with no
|
|
140
|
+
change.
|
|
141
|
+
|
|
142
|
+
`diagnosticLine()` now appends it, so console output, the run log, and
|
|
143
|
+
`pnpm content diagnostics` read:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
[code] file:line:column: message (eq:<key> -> <href>)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
A course script that parses that line with an anchored regex must tolerate the
|
|
150
|
+
optional trailing ` (eq:… -> …)`. One that just prints the string needs nothing.
|
|
151
|
+
|
|
152
|
+
### 5. Diagnostic line numbers are corrected — re-record anything that stored them
|
|
153
|
+
|
|
154
|
+
Every gate scans the lesson body, which Astro hands over with the frontmatter
|
|
155
|
+
already stripped, but findings are reported against the lesson *file* — so every
|
|
156
|
+
`file:line:column` the engine has emitted for a lesson was short by the height
|
|
157
|
+
of that lesson's frontmatter. `gate-math` (every `delimiter-manual-size`
|
|
158
|
+
column), `gate-prose-notation` (rule N9), `gate-abbreviations` (rule N14), and
|
|
159
|
+
`gate-heading-length` (S1) are all corrected at once.
|
|
160
|
+
|
|
161
|
+
Nothing to edit in content, but line numbers move, so:
|
|
162
|
+
|
|
163
|
+
- Re-check any suppression, snapshot, or problem matcher keyed to a line number.
|
|
164
|
+
Line numbers were wrong before; they now point at the real line.
|
|
165
|
+
- `pnpm validate:content` (twice) regenerates `build/resolution/*` with the
|
|
166
|
+
corrected positions. Commit the result.
|
|
167
|
+
|
|
168
|
+
### 6. Malformed `\explain` / `\slot` keys now fail the build
|
|
169
|
+
|
|
170
|
+
Previously a key that did not match the notation key pattern —
|
|
171
|
+
`\explain{Not A Key}{\Omega}` — was not a KaTeX parse error: KaTeX rendered the
|
|
172
|
+
command name in its error color and carried on, so the page shipped red
|
|
173
|
+
`\htmlData` text with a green build. `renderNotationMath` now checks both marker
|
|
174
|
+
macros before KaTeX runs and throws, naming the offending key.
|
|
175
|
+
|
|
176
|
+
**This can newly fail a build that passed on 0.7.0.** If it does, the page was
|
|
177
|
+
already broken for readers. Fix the key to match the notation key pattern
|
|
178
|
+
(lower-case, hyphen-separated) and make sure it resolves to a real entry.
|
|
179
|
+
`renderNotationFormula` still degrades to inert-but-correct math rather than
|
|
180
|
+
costing a reader the equation.
|
|
181
|
+
|
|
182
|
+
### 7. Glossary entry heading markup: the anchor is now a sibling of the `<h2>`
|
|
183
|
+
|
|
184
|
+
The per-entry anchor link moved out of the `<h2>` and into a new
|
|
185
|
+
`.glossary-heading` flex row wrapping both — the same shape
|
|
186
|
+
`rehype-heading-anchors` gives a lesson heading. This restores the heading's
|
|
187
|
+
accessible name (a screen reader was announcing "Velocity Link to Velocity").
|
|
188
|
+
|
|
189
|
+
Nothing visual changed, but update course-owned CSS and end-to-end tests that
|
|
190
|
+
assumed the nesting:
|
|
191
|
+
|
|
192
|
+
```diff
|
|
193
|
+
-h2:hover .glossary-anchor,
|
|
194
|
+
-h2:focus-within .glossary-anchor {
|
|
195
|
+
+.glossary-heading:hover .glossary-anchor,
|
|
196
|
+
+.glossary-heading:focus-within .glossary-anchor {
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
A test locating the anchor as a descendant of the heading must target
|
|
200
|
+
`.glossary-heading .glossary-anchor` instead. A check for the heading *by
|
|
201
|
+
accessible name* now matches the term alone, which is the point of the fix — if
|
|
202
|
+
such a test was written against the broken name ("Velocity Link to Velocity"),
|
|
203
|
+
it needs the term.
|
|
204
|
+
|
|
205
|
+
### 8. The About panel publishes its versions as data attributes
|
|
206
|
+
|
|
207
|
+
Additive; no course edit is required. `details[data-about-panel]` now carries
|
|
208
|
+
`data-knowledge-version`, `data-knowledge-date` (omitted when the content
|
|
209
|
+
changelog has no date), and `data-engine-version`, and the rendered version line
|
|
210
|
+
is marked `data-knowledge-line`.
|
|
211
|
+
|
|
212
|
+
This exists so a build check does not have to match prose. A course asserting
|
|
213
|
+
the panel's contents by text is pinned to one version string and turns every
|
|
214
|
+
routine bump into a test edit — this repo's own `about.spec.ts` was asserting
|
|
215
|
+
`0.4.0` against a playground long since past it. Prefer:
|
|
216
|
+
|
|
217
|
+
```diff
|
|
218
|
+
-await expect(panel).toContainText('0.6.0');
|
|
219
|
+
+await expect(panel).toHaveAttribute('data-knowledge-version', knowledgeVersion);
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
reading the expected value from `content/course.config.ts` rather than hard-coding
|
|
223
|
+
it. A bare version substring is not scoped to the panel either — the engine
|
|
224
|
+
version printed one row below can satisfy it.
|
|
225
|
+
|
|
226
|
+
### 9. Fixes that need no action
|
|
227
|
+
|
|
228
|
+
These remove build failures; they never require a content edit. Formulas you
|
|
229
|
+
previously worked around may now be written directly.
|
|
230
|
+
|
|
231
|
+
- **A notation `formula` can use a manual delimiter size, an accent, a box, or
|
|
232
|
+
a color.** `render-notation-math.ts`'s attribute allowlist is now KaTeX's
|
|
233
|
+
whole inert presentation surface, so `\Bigl`/`\biggl`/`\Biggl`,
|
|
234
|
+
`\xrightarrow`, `\boxed`, and `\colorbox` no longer throw
|
|
235
|
+
`Unexpected KaTeX property`. `alt`, `href`, `src`, and `title` are explicitly
|
|
236
|
+
denied; no attribute that can carry a URL is allowed through.
|
|
237
|
+
- **`\vec` and every other accent renders.** The style sanitizer no longer
|
|
238
|
+
requires a trailing `;` on the last CSS declaration. `\mathbf` remains the
|
|
239
|
+
house style for a vector; `\vec` simply works now.
|
|
240
|
+
- **A macro in a formula no longer invents a source span.** `\vdots`,
|
|
241
|
+
`\varSigma`, `\argmax`, `\bmod`, `\R`/`\N`/`\Z` and 79 of KaTeX's 282 macros
|
|
242
|
+
produce nodes whose `loc` indexes the macro body, which the glyph resolver
|
|
243
|
+
read as a span over unrelated text — failing with `Unresolved notation ""`,
|
|
244
|
+
or worse, silently splitting a formula mid-command. Fixed. An existing
|
|
245
|
+
explicit `\explain{key}{\varSigma}` annotation still works and is still the
|
|
246
|
+
way to bind a macro-expanded glyph, since it has no authored span for
|
|
247
|
+
canonical matching to see.
|
|
248
|
+
- **`equationAnchorId({ key })` type-checks without a `number`.** Its JSDoc
|
|
249
|
+
required a property the function ignores when a `key` is given.
|
|
250
|
+
|
|
251
|
+
### 10. Verify
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
pnpm add -D explico@0.8.0
|
|
255
|
+
pnpm validate:content # twice — the first run regenerates build/resolution/*
|
|
256
|
+
pnpm content check --changed
|
|
257
|
+
pnpm content warnings # expect new delimiter-manual-size findings (item 3)
|
|
258
|
+
astro check
|
|
259
|
+
astro build
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Commit the regenerated `build/resolution/*` (items 4 and 5 both change it).
|
|
263
|
+
|
|
36
264
|
## 0.6.0 → 0.7.0
|
|
37
265
|
|
|
38
266
|
### 1. Manifest schema is now v15: diagnostic severity has three levels
|