@plannotator/ui 0.32.0 → 0.34.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.
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The default math renderer loader: KaTeX's JS only, fetched lazily.
3
+ *
4
+ * This module is the ONLY place in `@plannotator/ui` that names `katex` at
5
+ * runtime (`./math-eager` names it too, but a host chooses to import that).
6
+ * `./math` calls `loadDefaultMathRenderer` only when no host loader is
7
+ * registered (`setMathRendererLoader` / `configurePlannotatorUI({
8
+ * mathRendererLoader })`), so a host that registers one never runs the
9
+ * `import('katex')` below and never requests the chunk it produces.
10
+ *
11
+ * Keeping the import in its own module is what lets a bundler drop the chunk
12
+ * entirely: chunk emission is static, so a host that registers a loader and
13
+ * wants no KaTeX chunk from the package at all points this module at a stub
14
+ * (see HANDOFF.md "Lazy renderers and eager entries", the alias recipe). The
15
+ * stylesheet is deliberately NOT imported here; CSS loading stays the host's
16
+ * job (HANDOFF.md "Math rendering"), and a host that already serves
17
+ * `katex.min.css` would otherwise load it twice.
18
+ */
19
+
20
+ import type { MathRenderer } from './math';
21
+
22
+ export function loadDefaultMathRenderer(): Promise<MathRenderer> {
23
+ return import('katex').then((m) => m.default);
24
+ }
package/utils/math.ts CHANGED
@@ -11,12 +11,16 @@
11
11
  * call `loadMathRenderer()`, and re-render typeset once it resolves.
12
12
  *
13
13
  * This module deliberately has NO runtime import of `katex`: the only place
14
- * the dependency is named is the default loader's `import('katex')`, which a
15
- * chunking bundler turns into a lazy chunk and Plannotator's single-file
16
- * builds inline (the eager entry keeps it in the entry either way).
14
+ * the dependency is named is `./math-default-loader`'s `import('katex')`,
15
+ * which a chunking bundler turns into a lazy chunk and Plannotator's
16
+ * single-file builds inline (the eager entry keeps it in the entry either
17
+ * way). That default is called only while no host loader is registered, and
18
+ * it lives in its own module so a host that registers a loader can alias it
19
+ * away and drop the chunk (see HANDOFF.md "Lazy renderers and eager entries").
17
20
  */
18
21
 
19
22
  import type { KatexOptions } from 'katex';
23
+ import { loadDefaultMathRenderer } from './math-default-loader';
20
24
 
21
25
  /** The subset of KaTeX's API the renderer needs. `katex` itself satisfies it. */
22
26
  export interface MathRenderer {
@@ -25,13 +29,6 @@ export interface MathRenderer {
25
29
 
26
30
  export type MathRendererLoader = () => Promise<MathRenderer>;
27
31
 
28
- /**
29
- * Default loader: KaTeX's JS only. The stylesheet is deliberately NOT imported
30
- * here; CSS loading stays the host's job (see HANDOFF.md "Math rendering"),
31
- * and a host that already serves `katex.min.css` would otherwise load it twice.
32
- */
33
- const defaultMathRendererLoader: MathRendererLoader = () => import('katex').then((m) => m.default);
34
-
35
32
  /**
36
33
  * Who filled the slot: the eager entry (`./math-eager`), the lazy loader, or a
37
34
  * host calling `setMathRenderer` directly. Diagnostic for a host chasing a TeX
@@ -43,8 +40,20 @@ export type MathRendererSource = 'plannotator-math-eager' | 'loader' | 'host';
43
40
 
44
41
  let renderer: MathRenderer | null = null;
45
42
  let rendererSource: MathRendererSource | null = null;
46
- let loader: MathRendererLoader = defaultMathRendererLoader;
43
+ /**
44
+ * The host loader, or `null` while none is registered. `null` is the only
45
+ * state in which `loadMathRenderer()` reaches `loadDefaultMathRenderer` and
46
+ * its `import('katex')`; a registered loader is never backfilled by the
47
+ * default, not even after it rejects.
48
+ */
49
+ let loader: MathRendererLoader | null = null;
47
50
  let pending: Promise<MathRenderer> | null = null;
51
+ /**
52
+ * Bumped by `resetMathRenderer()`. A load in flight across a reset must not
53
+ * fill the slot when it lands: the reset promised an empty slot, and the next
54
+ * `loadMathRenderer()` re-invokes the registered loader instead.
55
+ */
56
+ let resetEpoch = 0;
48
57
  const listeners = new Set<() => void>();
49
58
 
50
59
  function notify(): void {
@@ -81,14 +90,22 @@ export function subscribeMathRenderer(listener: () => void): () => void {
81
90
  * Swap the loader `loadMathRenderer()` uses. Host seam
82
91
  * (`configurePlannotatorUI({ mathRendererLoader })`): a host may return a
83
92
  * module that imports katex AND its stylesheet in one chunk. A load already in
84
- * flight keeps going; the new loader is used from the next `loadMathRenderer()`
85
- * call that finds the slot empty.
93
+ * flight keeps going and still fills the slot when it lands (the component
94
+ * that started it is waiting on that result and would otherwise never
95
+ * typeset); the new loader is used from the next `loadMathRenderer()` call
96
+ * that finds the slot empty. Passing `null` unregisters the host loader and
97
+ * restores the package default.
86
98
  */
87
- export function setMathRendererLoader(next: MathRendererLoader): void {
99
+ export function setMathRendererLoader(next: MathRendererLoader | null): void {
88
100
  loader = next;
89
101
  pending = null;
90
102
  }
91
103
 
104
+ /** The registered host loader, or `null` while the package default applies. */
105
+ export function getMathRendererLoader(): MathRendererLoader | null {
106
+ return loader;
107
+ }
108
+
92
109
  /**
93
110
  * Load and register the renderer. Idempotent: a filled slot resolves at once,
94
111
  * a load in flight is shared, and a rejected load is dropped so the next call
@@ -97,9 +114,12 @@ export function setMathRendererLoader(next: MathRendererLoader): void {
97
114
  export function loadMathRenderer(): Promise<MathRenderer> {
98
115
  if (renderer) return Promise.resolve(renderer);
99
116
  if (!pending) {
100
- const attempt = loader().then(
117
+ const epoch = resetEpoch;
118
+ const attempt = (loader ? loader() : loadDefaultMathRenderer()).then(
101
119
  (loaded) => {
102
- setMathRenderer(loaded, 'loader');
120
+ // A reset since this load started wants the slot empty: hand the
121
+ // result to the caller that awaited it, but do not register it.
122
+ if (epoch === resetEpoch) setMathRenderer(loaded, 'loader');
103
123
  return loaded;
104
124
  },
105
125
  (err: unknown) => {
@@ -112,12 +132,19 @@ export function loadMathRenderer(): Promise<MathRenderer> {
112
132
  return pending;
113
133
  }
114
134
 
115
- /** Test hook: clear the slot, the loader override and any pending load. */
135
+ /**
136
+ * Test hook: empty the slot (renderer and source) and forget any load in
137
+ * flight, so the next `loadMathRenderer()` invokes the loader afresh and a
138
+ * stale in-flight result cannot fill the slot after the reset. The registered
139
+ * loader is KEPT: resetting the renderer is not unregistering the host seam
140
+ * (a host's `configurePlannotatorUI` runs once, before any reset a test issues
141
+ * later). To drop the loader too, call `setMathRendererLoader(null)`.
142
+ */
116
143
  export function resetMathRenderer(): void {
117
144
  renderer = null;
118
145
  rendererSource = null;
119
- loader = defaultMathRendererLoader;
120
146
  pending = null;
147
+ resetEpoch += 1;
121
148
  notify();
122
149
  }
123
150
 
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Mermaid's KaTeX, served from the math renderer slot.
3
+ *
4
+ * Mermaid renders `$$...$$` labels through its own `import("katex")`
5
+ * (`renderKatexUnsanitized` in the runtime; there is no config flag and no
6
+ * hook for it, and chunk emission is static), so a host that registers a
7
+ * `mathRendererLoader` and aliases `./math-default-loader` away still gets a
8
+ * shared `katex-*.js` chunk out of the Mermaid runtime, and a math document
9
+ * fetches two files: the host's loader chunk plus that shared chunk.
10
+ *
11
+ * This module is the alias target that removes it. A host redirects the
12
+ * `katex` specifier, for importers inside the `mermaid` package ONLY, to
13
+ * `@plannotator/ui/utils/mermaid-math-slot` (HANDOFF.md "Lazy renderers and
14
+ * eager entries", item 2). Its default export has the one method Mermaid
15
+ * calls, `renderToString`, and delegates to whatever renderer fills the slot
16
+ * in `./math`: the host's loader result, or the eager KaTeX registration.
17
+ * Mermaid's own options (`throwOnError: true`, `displayMode: true`, the
18
+ * MathML `output` mode) are passed through untouched, so a KaTeX renderer
19
+ * produces exactly the markup Mermaid produced from its direct import.
20
+ *
21
+ * The slot must be filled by the time Mermaid asks: `MermaidBlock` awaits
22
+ * `loadMathRenderer()` before rendering a diagram whose source carries a
23
+ * `$$` label (`hasMermaidMath`), which is a no-op resolve on a filled slot
24
+ * and the host's loader otherwise. An empty slot here means that load
25
+ * failed, and the error below surfaces in the block's error panel with the
26
+ * source, instead of a silently unlabeled node.
27
+ *
28
+ * Nothing imports this module by default: Plannotator never aliases, so its
29
+ * Mermaid keeps its direct KaTeX (inlined by the single-file builds), and
30
+ * this file must never import `katex` itself, or the redirect would re-create
31
+ * the chunk it exists to remove (`tests/entry-assets.test.ts` pins that).
32
+ */
33
+
34
+ import { getMathRenderer, type MathRenderer } from './math';
35
+
36
+ /** Mermaid's own test for a math label (`katexRegex` in the runtime). */
37
+ const MERMAID_MATH_REGEX = /\$\$(.*)\$\$/;
38
+
39
+ /** True when a diagram source carries at least one `$$...$$` label. */
40
+ export function hasMermaidMath(source: string): boolean {
41
+ return MERMAID_MATH_REGEX.test(source);
42
+ }
43
+
44
+ /** Thrown when Mermaid asks for KaTeX while the math slot is still empty. */
45
+ export const MERMAID_MATH_SLOT_EMPTY_MESSAGE =
46
+ 'Math label: no math renderer is registered (the mathRendererLoader did not resolve before the diagram rendered)';
47
+
48
+ const slotRenderer: MathRenderer = {
49
+ renderToString(tex, options) {
50
+ const renderer = getMathRenderer();
51
+ if (!renderer) throw new Error(MERMAID_MATH_SLOT_EMPTY_MESSAGE);
52
+ return renderer.renderToString(tex, options);
53
+ },
54
+ };
55
+
56
+ export default slotRenderer;