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.
Files changed (47) hide show
  1. package/CHANGELOG.md +171 -1
  2. package/dist/compiler/cli.d.ts +8 -1
  3. package/dist/compiler/cli.js +10 -2
  4. package/dist/compiler/collections.js +16 -3
  5. package/dist/compiler/logging.d.ts +11 -1
  6. package/dist/compiler/logging.js +2 -1
  7. package/dist/compiler/manifest.d.ts +1 -1
  8. package/dist/compiler/manifest.js +1 -1
  9. package/dist/reference/delimiter-size.d.ts +18 -9
  10. package/dist/reference/delimiter-size.js +15 -39
  11. package/dist/reference/equations.d.mts +9 -3
  12. package/dist/reference/equations.mjs +7 -1
  13. package/dist/reference/gate-abbreviations.js +2 -2
  14. package/dist/reference/gate-heading-length.js +2 -2
  15. package/dist/reference/gate-math.d.ts +23 -1
  16. package/dist/reference/gate-math.js +23 -4
  17. package/dist/reference/gate-prose-notation.d.ts +12 -1
  18. package/dist/reference/gate-prose-notation.js +5 -1
  19. package/dist/reference/katex-delimiters.d.mts +15 -0
  20. package/dist/reference/katex-delimiters.mjs +125 -0
  21. package/dist/reference/katex-options.mjs +4 -0
  22. package/dist/reference/math-glyphs.mjs +16 -2
  23. package/dist/reference/render-lab-math.js +1 -1
  24. package/dist/reference/types.d.ts +7 -0
  25. package/docs/authoring/README.md +1 -1
  26. package/docs/authoring/migrating.md +228 -0
  27. package/docs/authoring/notation.md +80 -62
  28. package/docs/katex-bracket-scaling.md +191 -0
  29. package/package.json +2 -1
  30. package/src/compiler/cli.ts +23 -0
  31. package/src/compiler/collections.ts +33 -3
  32. package/src/compiler/logging.ts +14 -3
  33. package/src/compiler/manifest.ts +1 -1
  34. package/src/components/notation/NotationGlossary.astro +21 -10
  35. package/src/components/notation/render-notation-math.ts +111 -10
  36. package/src/components/starlight/AboutPanel.astro +17 -2
  37. package/src/reference/delimiter-size.ts +37 -59
  38. package/src/reference/equations.mjs +7 -1
  39. package/src/reference/gate-abbreviations.ts +2 -2
  40. package/src/reference/gate-heading-length.ts +2 -2
  41. package/src/reference/gate-math.ts +50 -2
  42. package/src/reference/gate-prose-notation.ts +15 -1
  43. package/src/reference/katex-delimiters.mjs +125 -0
  44. package/src/reference/katex-options.mjs +4 -0
  45. package/src/reference/math-glyphs.mjs +16 -2
  46. package/src/reference/render-lab-math.ts +1 -1
  47. package/src/reference/types.ts +7 -0
@@ -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);
@@ -1,4 +1,4 @@
1
- import katex from 'katex';
1
+ import katex from './katex-delimiters.mjs';
2
2
  import { resolveMathGlyphs, stripNotationMarkers } from './math-glyphs.mjs';
3
3
  import { createNotationKatexOptions } from './katex-options.mjs';
4
4
 
@@ -88,6 +88,13 @@ export interface NotationLessonInput {
88
88
  readonly references: readonly NotationReferenceInput[];
89
89
  /** Original lesson body, retained for lossless loader adapters. */
90
90
  readonly body: string;
91
+ /**
92
+ * 1-based line in `source.file` where `body` starts. Gates scan the body, so
93
+ * every finding's index is body-relative; this is what turns one into a real
94
+ * location in the file. Optional so a hand-built input (a test, an adapter)
95
+ * stays valid — it then behaves as 1, the pre-0.7.1 behavior.
96
+ */
97
+ readonly bodyLine?: number;
91
98
  readonly source: SourceSpan;
92
99
  }
93
100