@plannotator/ui 0.33.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.
- package/HANDOFF.md +41 -8
- package/README.md +3 -3
- package/components/MermaidBlock.tsx +16 -0
- package/components/html-viewer/HtmlViewer.tsx +17 -4
- package/package.json +1 -1
- package/utils/math.ts +30 -6
- package/utils/mermaid-math-slot.ts +56 -0
package/HANDOFF.md
CHANGED
|
@@ -92,7 +92,7 @@ Pass any subset of these to `configurePlannotatorUI({ ... })`. Anything omitted
|
|
|
92
92
|
| `aiTransport` | `AITransport` | The "Ask AI" chat session/query/abort/permission | `POST /api/ai/{session,query,abort,permission}` |
|
|
93
93
|
| `serverSync` | `ServerSyncFn` | Push a settings change back to the server | No-op-ish (Plannotator's local sync) |
|
|
94
94
|
| `loadSettingsFromBackend` | `boolean` | After install, re-hydrate settings from your `storageBackend` | off |
|
|
95
|
-
| `mathRendererLoader` | `() => Promise<MathRenderer>` | How KaTeX is loaded when no renderer is registered before the first math node renders (see "Lazy renderers and eager entries"). Once registered, the package default is never called, not even as a fallback after a rejected load; a default load already in flight at registration still fills the slot (pre-existing, see `setMathRendererLoader`), so register before the first math render | `utils/math-default-loader`'s `import('katex')`, JS only; CSS stays yours |
|
|
95
|
+
| `mathRendererLoader` | `() => Promise<MathRenderer>` | How KaTeX is loaded when no renderer is registered before the first math node renders (see "Lazy renderers and eager entries"). Once registered, the package default is never called, not even as a fallback after a rejected load, and `resetMathRenderer()` keeps the registration (0.34.0); a default load already in flight at registration still fills the slot (pre-existing, see `setMathRendererLoader`), so register before the first math render | `utils/math-default-loader`'s `import('katex')`, JS only; CSS stays yours |
|
|
96
96
|
| `identityGenerator` | `() => string` | The synchronous generator behind the default "tater" display name when no `identityProvider` is installed | A built-in 16 x 16 word pool of the same `adjective-noun-tater` shape; Plannotator registers the full dictionary via `utils/identity-tater` |
|
|
97
97
|
|
|
98
98
|
### Interface details worth knowing
|
|
@@ -212,7 +212,8 @@ We deliberately did **not** restructure the exports map in this PR (move-don't-r
|
|
|
212
212
|
| `shortcuts` (`useHtmlAnnotateShortcuts`, `defineShortcutScope`, the scope registry) | The declarative keyboard-shortcut engine and the per-surface scopes, including the HTML annotate scope (Mod+Shift+A). Pure: React plus `utils/platform`; no backend. |
|
|
213
213
|
| `utils/inputMethod` (`getInputMethod`, `saveInputMethod`, `refreshInputMethodStamp`) | The per-surface pinpoint/drag input-method preference with its TTL. Persists through the `storageBackend` seam; no backend of its own. |
|
|
214
214
|
| `utils/codeHighlight` / `utils/codeBlockMark` / `utils/syntaxTheme` | The Shiki-based fence highlighter, swap-surviving annotation marks, and palette→Shiki theme mapping. Replaces all `.hljs` styling. *(Blessed in 0.29.0.)* |
|
|
215
|
-
| `utils/math` (`loadMathRenderer`, `getMathRenderer`, `getMathRendererSource`, `setMathRenderer`) and `utils/math-eager` | The math renderer slot and its eager KaTeX registration. Import `utils/math-eager` for synchronous typesetting on the first commit; call `loadMathRenderer()` to pre-warm the lazy path. See "Lazy renderers and eager entries". |
|
|
215
|
+
| `utils/math` (`loadMathRenderer`, `getMathRenderer`, `getMathRendererSource`, `setMathRenderer`, `setMathRendererLoader`, `getMathRendererLoader`, `resetMathRenderer`) and `utils/math-eager` | The math renderer slot and its eager KaTeX registration. Import `utils/math-eager` for synchronous typesetting on the first commit; call `loadMathRenderer()` to pre-warm the lazy path. `resetMathRenderer()` empties the slot and keeps the registered loader; `setMathRendererLoader(null)` drops it. See "Lazy renderers and eager entries". |
|
|
216
|
+
| `utils/mermaid-math-slot` | Alias target only: what a host redirects Mermaid's own `katex` import to, so `$$` labels in diagrams typeset through the math slot and the host build carries one KaTeX chunk. Never import it yourself. See "Lazy renderers and eager entries", item 2. |
|
|
216
217
|
| `utils/identity-tater` | Side-effect entry that registers the full username dictionary into the identity generator slot. Import it only if you rely on the default tater names and want the full dictionary; a host with `identityProvider` should not. |
|
|
217
218
|
| `utils/mermaid` (`loadMermaidRuntime`, `getMermaidRuntime`, `getMermaidRuntimeSource`, `setMermaidRuntime`, `MERMAID_CONFIG`) and `utils/mermaid-eager` | The Mermaid runtime slot and its eager registration. Import `utils/mermaid-eager` to keep Mermaid in your entry chunk as Plannotator does; omit it for the lazy path with retry. See "Lazy renderers and eager entries". |
|
|
218
219
|
|
|
@@ -478,13 +479,38 @@ Four modules that used to ride every document read for a host that bundles by ro
|
|
|
478
479
|
export function loadDefaultMathRenderer(): Promise<never> { return Promise.reject(new Error('default math loader aliased out')); }
|
|
479
480
|
```
|
|
480
481
|
|
|
481
|
-
With the alias the same consumer build emits zero chunks carrying the KaTeX body and the entry's only `import()` in that area is the host's own loader chunk. Do not alias without registering a loader: math would then render as TeX text forever. Plannotator's entries import `math-eager`, so the slot is filled before the first render and this branch is never reached there; the single-file builds inline the default through `inlineDynamicImports` as before (`tests/entry-assets.test.ts` pins the split: `utils/math` has no `import('katex')` site, `utils/math-default-loader` has the only one).
|
|
482
|
+
With the alias the same consumer build emits zero chunks carrying the KaTeX body out of the package and the entry's only `import()` in that area is the host's own loader chunk. Do not alias without registering a loader: math would then render as TeX text forever. Plannotator's entries import `math-eager`, so the slot is filled before the first render and this branch is never reached there; the single-file builds inline the default through `inlineDynamicImports` as before (`tests/entry-assets.test.ts` pins the split: `utils/math` has no `import('katex')` site, `utils/math-default-loader` has the only one).
|
|
483
|
+
|
|
484
|
+
**Mermaid's own KaTeX, and the last shared chunk (0.34.0, from 0.33.0 adoption feedback).** The alias above is not the whole story once a page can render a Mermaid diagram. The Mermaid runtime (11.15.0 in this checkout) typesets `$$...$$` labels through its own `import("katex")`, inside `renderKatexUnsanitized`, and it offers nothing to turn that off: `legacyMathML` / `forceLegacyMathML` only choose the output mode, the guard around the import is the `@mermaid-js/tiny` build marker, and there is no hook to hand it a renderer. The import only *runs* for a label that matches Mermaid's `$$` test, but it is *emitted* regardless, so a host that registered a loader and aliased the default still built a `katex-*.js` chunk, and because that chunk then had two dynamic importers (the host's loader module and the Mermaid runtime) Rollup kept it separate from the host's loader chunk: a math document fetched two files, the 57-byte loader chunk plus the shared 261 KB KaTeX chunk. Measured on the scratch Vite 6 consumer of this checkout (loader registered, default aliased, a document with inline math, a display block and a flowchart with a `$$` label): one chunk carried the KaTeX body before, `katex-*.js`, imported by `mermaid.core-*.js` and by the host's loader chunk; 367 JS chunks in all.
|
|
485
|
+
|
|
486
|
+
The fix is a bundler-facing redirect to a package module, `utils/mermaid-math-slot`, whose default export has the one method Mermaid calls (`renderToString`) and delegates to whatever fills the math slot, with Mermaid's own options (`throwOnError: true`, `displayMode: true`, the MathML `output` mode) passed through untouched, so a KaTeX renderer produces exactly the markup Mermaid produced from its direct import. Redirect the `katex` specifier for importers inside the `mermaid` package ONLY; a plain `resolve.alias` on `katex` would also rewrite your own loader's import and break math everywhere:
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
// vite.config.ts of a host that registers mathRendererLoader (beside the math-default-loader alias)
|
|
490
|
+
import { fileURLToPath } from 'node:url';
|
|
491
|
+
const configFile = fileURLToPath(import.meta.url);
|
|
492
|
+
const mermaidKatexToSlot: Plugin = {
|
|
493
|
+
name: 'mermaid-katex-to-plannotator-slot',
|
|
494
|
+
enforce: 'pre',
|
|
495
|
+
resolveId(source, importer) {
|
|
496
|
+
if (source !== 'katex' || !importer || !/[\\/]node_modules[\\/]mermaid[\\/]/.test(importer)) return null;
|
|
497
|
+
return this.resolve('@plannotator/ui/utils/mermaid-math-slot', configFile, { skipSelf: true });
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
// plugins: [mermaidKatexToSlot, react(), ...]
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
Two details of that snippet are layout-proofing. The importer test is `node_modules/mermaid/` anywhere in the path, not a pattern for one install layout: a hoisted install puts the runtime at `node_modules/mermaid/`, Bun's isolated layout at `node_modules/.bun/mermaid@11.15.0/node_modules/mermaid/`, and pnpm's at `node_modules/.pnpm/mermaid@11.15.0/node_modules/mermaid/`; every one of them ends in that segment, and the trailing separator keeps `mermaid-something` packages out. The slot module is resolved from the host's own config file (`configFile`), not from the Mermaid importer: resolving from the importer walks up from Mermaid's location, which finds `@plannotator/ui` on hoisted and Bun-isolated installs but not under pnpm's strict `node_modules`, where the package is only visible from the host root. Resolving from the config file is the same lookup the host's own imports use; passing an absolute path to the file (`path.resolve(...)` of the installed `utils/mermaid-math-slot.ts`) works too.
|
|
504
|
+
|
|
505
|
+
With the redirect the same consumer build emits one chunk carrying the KaTeX body, the host's own loader chunk (`host-katex-*.js`, 261 KB, reached only by the entry's `import()`), `mermaid.core-*.js` has no KaTeX import left, and the chunk count drops to 366: one KaTeX chunk, owned by the host, one file fetched. The slot must be filled by the time Mermaid asks, so `MermaidBlock` awaits `loadMathRenderer()` before rendering a diagram whose source carries a `$$` label (`hasMermaidMath`, Mermaid's own regex); on a filled slot that resolves at once, on the lazy path it runs your loader, and if that load fails the label throws a message naming the cause (`MERMAID_MATH_SLOT_EMPTY_MESSAGE`) which the block's error panel shows with the source. Do not import the module yourself; it exists to be resolved to. Plannotator does not redirect: its Mermaid keeps its direct KaTeX, inlined by the single-file builds with everything else, and the pre-render wait is a resolved promise there because `math-eager` filled the slot at startup. No test in the repo renders a real Mermaid diagram with a math label (Mermaid does not render under happy-dom), and nothing in Plannotator's own documents exercises `$$` labels; the bridge is pinned by `utils/mermaid-math-slot.test.ts` (delegation with Mermaid's exact options, KaTeX parity, the empty-slot error, the label regex) and the pre-render warm by the "Mermaid math labels warm the math slot" cases in `components/DiagramBlock.lazyRetry.test.tsx`.
|
|
506
|
+
|
|
507
|
+
**`resetMathRenderer()` keeps the loader (0.34.0).** Through 0.33.0 the reset hook also nulled the registered loader, so a host test harness that reset the slot between cases silently fell back to the package default `import('katex')` on the next render. Reset now empties the renderer and its source, forgets a load in flight (its late result no longer fills the slot; the next `loadMathRenderer()` invokes the registered loader afresh) and leaves the loader registered. `setMathRendererLoader(null)` is the explicit way back to the package default, and `getMathRendererLoader()` reads the registration; `configurePlannotatorUI` cannot unregister a loader (a `null` or absent `mathRendererLoader` is a no-op there), so only a direct `setMathRendererLoader(null)` does. `setMathRendererLoader` itself is unchanged: a load already in flight at registration still fills the slot, because the component that started it is waiting on that result. The other reset-style helpers were reviewed and are consistent with their names: `resetIdentityProvider` and `resetIdentityGenerator` reset exactly the thing they name (the provider, the generator), and Mermaid's `__setMermaidRuntimeLoaderForTests` is a stand-in by name. Pinned in `utils/math.test.ts`.
|
|
482
508
|
|
|
483
509
|
3. **Identity: a generator slot, filled eagerly by Plannotator.** `utils/generateIdentity` no longer imports `unique-username-generator`. It holds a synchronous generator slot (`setIdentityGenerator`, `getIdentityGenerator`) with a built-in fallback that produces the same `adjective-noun-tater` shape from a 16 x 16 pool. `utils/identity-tater` registers the full dictionary as a side effect and is what Plannotator's entries import. A host with `identityProvider` never calls the generator and, with the static import gone, no longer ships the word lists; delete any dictionary shim. A host that wants the full dictionary without its own provider imports `@plannotator/ui/utils/identity-tater`, or passes its own `identityGenerator` to `configurePlannotatorUI`. The slot is synchronous on purpose: `configStore` persists the first generated name to the identity cookie during the first render-time settings read, so a name that arrived later would be a visible identity change.
|
|
484
510
|
|
|
485
|
-
4. **Scope, as of 0.
|
|
511
|
+
4. **Scope, as of 0.34.0.** 0.32.0 shipped items 1 to 3 and deliberately left two things out of the design record's list: the raw-HTML bridge script as a separately served asset, and a lazy table popout. 0.33.0 ships the first (see "HTML viewer bridge as an asset" below) and, from adoption feedback, the `utils/math-default-loader` split in item 2. 0.34.0 adds the Mermaid KaTeX redirect and the `resetMathRenderer` fix in item 2, both from 0.33.0 adoption feedback. The lazy table popout is still not shipped and stays tracked in the design record for a follow-up.
|
|
486
512
|
|
|
487
|
-
Pinned by `utils/math.test.ts`, `components/MathBlock.firstPaint.test.tsx`, `utils/generateIdentity.test.ts`, `components/MermaidBlock.test.ts`, and the eager-entry and built-HTML marker guards in `tests/entry-assets.test.ts`.
|
|
513
|
+
Pinned by `utils/math.test.ts`, `utils/mermaid-math-slot.test.ts`, `components/MathBlock.firstPaint.test.tsx`, `utils/generateIdentity.test.ts`, `components/MermaidBlock.test.ts`, `components/DiagramBlock.lazyRetry.test.tsx`, and the eager-entry and built-HTML marker guards in `tests/entry-assets.test.ts`.
|
|
488
514
|
|
|
489
515
|
---
|
|
490
516
|
|
|
@@ -519,16 +545,23 @@ With the prop set, `buildSrcdocInjection` emits `<script src="…"></script>` in
|
|
|
519
545
|
|
|
520
546
|
**Protocol version.** `BRIDGE_PROTOCOL_VERSION` (exported from `components/html-viewer/bridge-script` and re-exported from `components/html-viewer`) is embedded in the bridge text and stamped on its `ready` message as `protocolVersion`. Note for the design record's "current state": `BRIDGE_SCRIPT` now carries its first `${}` interpolation (that constant, evaluated at module load); it remains a plain string export with no per-session values, so the CLI, Pi and the live proxy consume it exactly as before. The parent (`checkBridgeProtocolVersion`, `HtmlViewer`'s ready branch) compares it: on the inline path and in live sessions the two sides come from one bundle and always match; on the URL path a cached asset from a previous package version answers with an older stamp, or none, and the viewer logs one console warning naming both versions, shows a dismissible error banner over the top of the frame (`[data-bridge-error="version-mismatch"]`, `role="alert"`, a `[data-bridge-error-dismiss]` button; the page stays visible) and calls `onBridgeUnavailable` once. The ready is still honored (an older bridge answers every message shape it knows), so this is a loud diagnostic, not a refusal. Bump the constant whenever a bridge message shape changes in a way an older bridge or parent would misread; a bump forces a warning against any not-yet-redeployed asset, which is the point.
|
|
521
547
|
|
|
548
|
+
**Who renders the strip (`bridgeErrorDisplay`; 0.34.0, from 0.33.0 adoption feedback).** The package owns the failure strip by default: `bridgeErrorDisplay="banner"` renders the `[data-bridge-error]` element for both the mismatch and the timeout states exactly as 0.33.0 did, so Plannotator and every existing host are unchanged. A host that renders its own notice from `onBridgeUnavailable` passes `bridgeErrorDisplay="none"`: no strip and no dismiss button are rendered for either state, while `onBridgeUnavailable` fires exactly as before and a version mismatch still logs its one console warning. Through 0.33.0 the strip could not be suppressed, so such a host showed two banners. The prop is meaningless on the inline path, which never shows a strip. Pinned by the two `bridgeErrorDisplay` cases in `components/html-viewer/HtmlViewer.bridgeAsset.test.tsx`.
|
|
549
|
+
|
|
522
550
|
**Ready timeout.** On the URL path only, `bridgeReadyTimeoutMs` (default 5000) is armed once per document load (URL or srcdoc change), read through a ref, so changing the prop after the bridge is ready never re-arms it; with no `ready` in time the surface shows `[data-bridge-error="timeout"]` naming the URL and the wait (not dismissible: the surface is dead), and `onBridgeUnavailable({ kind: 'timeout', url, timeoutMs })` fires. A late `ready` clears it. The inline path arms no timer and can never show a banner.
|
|
523
551
|
|
|
524
552
|
**Dropping the literal from the host chunk (optional).** The URL path alone leaves the inline string in the chunk unused, because `srcdoc.ts` imports it statically (the default must stay synchronous). To remove it, alias the package's `./bridge-script` resolution to the generated lite module in your bundler; with Vite:
|
|
525
553
|
|
|
526
554
|
```ts
|
|
527
555
|
resolve: {
|
|
528
|
-
alias: [{
|
|
556
|
+
alias: [{
|
|
557
|
+
find: /^\.\/bridge-script$/,
|
|
558
|
+
replacement: "@plannotator/ui/components/html-viewer/bridge-script.lite",
|
|
559
|
+
}],
|
|
529
560
|
}
|
|
530
561
|
```
|
|
531
562
|
|
|
563
|
+
The `find` is anchored on purpose (0.34.0; 0.33.0 documented `/\/bridge-script$/`). Every import of the module inside the package is the relative sibling form, `./bridge-script` (`srcdoc.ts`, `useHtmlAnnotation.ts`, `index.ts`), so `/^\.\/bridge-script$/` matches exactly those. The unanchored form matched any specifier ENDING in `/bridge-script`, which is also the shape of another package's entry point (`some-dependency/bridge-script`) or of a deeper import in your own tree (`../vendor/bridge-script`), and would have silently swapped those for the lite module too. If your own source has a sibling module named `bridge-script`, add an importer check (a `customResolver` on the alias entry, or a `resolveId` plugin that tests `importer` for `@plannotator/ui/components/html-viewer/`) rather than widening the pattern.
|
|
564
|
+
|
|
532
565
|
Under that alias an `HtmlViewer` rendered WITHOUT `bridgeScriptUrl` throws at render (`buildBridgeScriptTag` refuses to emit an empty inline script), so the misconfiguration cannot ship as a silently dead surface. Measured on the proof harness (PR #1398's description): the viewer chunk shrinks by the size of the literal, 557 kB to 371 kB (168 kB to 118 kB gzip).
|
|
533
566
|
|
|
534
567
|
**Live app annotation is unaffected.** `packages/shared/live-proxy-bridge-inline.test.ts` pins at source level that both proxy transports and both runtimes' composers still ship the inline bridge from the proxy route and never reference `bridgeScriptUrl` or the generated files.
|
|
@@ -641,8 +674,8 @@ Additive only, but required: `@plannotator/ui` 0.32.0 imports the new `@plannota
|
|
|
641
674
|
|
|
642
675
|
## Publishing & versioning
|
|
643
676
|
|
|
644
|
-
- The current pair is `@plannotator/ui` `0.
|
|
645
|
-
- Recent pairs, for the consumer's install matrix: ui 0.31.0 on core 0.24.0 (lockstep), ui 0.32.0 on core 0.25.0 (lockstep, `html-anchor`), ui 0.33.0 on core 0.25.0 (ui only, core unchanged).
|
|
677
|
+
- The current pair is `@plannotator/ui` `0.34.0` on `@plannotator/core` `0.25.0`. **No lockstep again**: nothing under `packages/core` changed since the 0.32.0 pair, so core is not republished and ui 0.34.0 pins the already published core 0.25.0 (only the ui tarball is built and published). 0.34.0 carries the 0.33.0 adoption feedback: the Mermaid KaTeX redirect target `utils/mermaid-math-slot`, `HtmlViewer` `bridgeErrorDisplay`, the anchored bridge-script alias, and `resetMathRenderer` keeping the registered loader (with `setMathRendererLoader(null)` and `getMathRendererLoader`). 0.33.0 carried the bridge-script asset (`bridge-script.asset.js`, `bridge-script.lite`, both generated by `prepack`), the `utils/math-default-loader` split, and `HANDOFF.md` inside the tarball so the README's section references resolve for a consumer.
|
|
678
|
+
- Recent pairs, for the consumer's install matrix: ui 0.31.0 on core 0.24.0 (lockstep), ui 0.32.0 on core 0.25.0 (lockstep, `html-anchor`), ui 0.33.0 and ui 0.34.0 on core 0.25.0 (ui only, core unchanged).
|
|
646
679
|
- When a pair IS lockstep, **publish `core` first**: ui 0.32.0 imports the new `@plannotator/core/html-anchor` subpath, which no earlier published core (0.24.0 and before) has, just as ui 0.29.0 needed core 0.23.0 for `@plannotator/core/annotatable`. The ui→core dependency resolves exactly at pack time, from the lockfile: after a version bump, run `bun install` so `bun.lock` carries the new workspace versions, or `bun pm pack` will still stamp the previous core version into ui's tarball (the 0.31.0 lesson).
|
|
647
680
|
- The HTML annotation seams also changed the guides.show viewer **stylesheet** (five utility rules from `HtmlSurfaceControls`; the viewer JS is unchanged), so `packages/core/guide-viewer-manifest.ts` now pins a CSS hash that exists on guides.show only after the deploy workflow has published this build's `/v1/` assets. A guide exported from this build before that deploy would pin a stylesheet the host does not serve yet: **deploy guides.show before any release that ships this manifest.**
|
|
648
681
|
- They depend on each other via `workspace:*`. At publish time that must resolve to the **exact** version in the tarball, so publish with a tool that does that resolution (the repo's existing flow uses `bun pm pack` to build the tarball, then `npm publish *.tgz --access public`). Publish **`core` first, then `ui`**.
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ Building your own tooltip and removing the built-in double-click reset are host-
|
|
|
54
54
|
|
|
55
55
|
The Mermaid runtime, the Graphviz engine, KaTeX and the username dictionary are off the static import graph of `Viewer`, so a host that bundles by route does not download them for a plain markdown read. Graphviz needs nothing from you (the block imports the engine inside its render effect and shows the source fence until the SVG lands, as it always did). Mermaid, KaTeX and the dictionary sit behind synchronous slots:
|
|
56
56
|
|
|
57
|
-
- **Math.** Without registration, a math node renders its TeX as text in the same wrapper (same `data-math-tex` / `data-math-display` / `aria-label` / class names), loads KaTeX via `import('katex')`, and re-renders typeset. To keep math typeset on the very first commit, as Plannotator does, add one line to your entry: `import "@plannotator/ui/utils/math-eager";`. To put KaTeX and its stylesheet on one lazy chunk instead, pass `mathRendererLoader`. The stylesheet remains your job either way (see "Consuming it", step 3). The default `import('katex')` is the only runtime mention of `katex` in the package and lives in `utils/math-default-loader` (0.33.0), called only while no loader is registered; a registered loader is never backfilled by it, though a default load already in flight at registration still fills the slot (pre-existing), so register the loader before the first math render. Chunk emission is static, so a bundler still emits that chunk (never requested) unless you alias the module away; see HANDOFF.md "Lazy renderers and eager entries" for the two-line alias.
|
|
57
|
+
- **Math.** Without registration, a math node renders its TeX as text in the same wrapper (same `data-math-tex` / `data-math-display` / `aria-label` / class names), loads KaTeX via `import('katex')`, and re-renders typeset. To keep math typeset on the very first commit, as Plannotator does, add one line to your entry: `import "@plannotator/ui/utils/math-eager";`. To put KaTeX and its stylesheet on one lazy chunk instead, pass `mathRendererLoader`. The stylesheet remains your job either way (see "Consuming it", step 3). The default `import('katex')` is the only runtime mention of `katex` in the package and lives in `utils/math-default-loader` (0.33.0), called only while no loader is registered; a registered loader is never backfilled by it, though a default load already in flight at registration still fills the slot (pre-existing), so register the loader before the first math render. Chunk emission is static, so a bundler still emits that chunk (never requested) unless you alias the module away; see HANDOFF.md "Lazy renderers and eager entries" for the two-line alias. The Mermaid runtime has its own `import("katex")` for `$$` labels, which leaves a second, shared KaTeX chunk in a host build even with the alias; since 0.34.0 a host redirects that one import (for importers inside the `mermaid` package only) to `@plannotator/ui/utils/mermaid-math-slot`, which typesets the labels through your registered renderer, so one KaTeX chunk remains and it is yours. Recipe and measurement in HANDOFF.md, same section. `resetMathRenderer()` empties the slot only and keeps a registered loader (0.34.0); `setMathRendererLoader(null)` is the explicit way back to the package default.
|
|
58
58
|
- **Mermaid.** Without registration, the first diagram on a page fetches the runtime through `import('mermaid')`; a failed import is dropped from the memo, re-attempted once after a short delay, and the error panel (with the source) offers Retry, which issues another fresh attempt. Plannotator keeps Mermaid eager by policy so it can never fail separately from the app: `import "@plannotator/ui/utils/mermaid-eager";` in your entry does the same for your bundle. Honest limit of any in-page retry: a browser records a failed module fetch in its module map for the page lifetime, so a fresh `import()` of the same chunk URL rejects without a request; the retry recovers failures after the fetch (engine instantiation, initialize) and hosts that version chunk URLs. A host that needs recovery from a failed first fetch uses versioned chunk URLs or a `vite:preloadError` reload at app level.
|
|
59
59
|
- **Identity.** With an `identityProvider` the generator is never called and the word lists stay out of your bundle. Without one, default names come from a small built-in pool of the same `adjective-noun-tater` shape; `import "@plannotator/ui/utils/identity-tater";` registers the full dictionary, or pass your own `identityGenerator`.
|
|
60
60
|
|
|
@@ -122,7 +122,7 @@ import bridgeScriptUrl from "@plannotator/ui/components/html-viewer/bridge-scrip
|
|
|
122
122
|
<HtmlViewer rawHtml={html} bridgeScriptUrl={bridgeScriptUrl} … />
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
The srcdoc then carries one classic `<script src>` in the exact place the inline script sat (at the end of `<head>`, before the body), the browser caches the asset across documents, and the bridge's `ready` message carries `BRIDGE_PROTOCOL_VERSION`, which the viewer checks: a stale cached asset (no stamp, or another version) logs one console warning naming both versions and shows a dismissible error banner in the surface (`onBridgeUnavailable` fires too); no `ready` within `bridgeReadyTimeoutMs` (default 5000) shows a timeout banner. The URL is resolved against your document's base (`document.baseURI`) before it is written into the srcdoc, never against the framed page, so a page's own `<base href>` cannot redirect it. Plannotator passes nothing and stays inline; none of this runs on the inline path. **CSP:** the package sets no CSP `<meta>` in the srcdoc document, and the frame is an opaque origin so the classic script needs no CORS (no `crossorigin` is set), but a CSP delivered as a header on your page is inherited by the frame: allow `script-src` for the asset's origin. Because the frame is an opaque origin, an asset served with `Cross-Origin-Resource-Policy: same-origin` (common alongside COEP) is blocked; serve it with a CORP that admits cross-origin loads, or without CORP. To also drop the inline literal from your viewer chunk, alias the package's `./bridge-script`
|
|
125
|
+
The srcdoc then carries one classic `<script src>` in the exact place the inline script sat (at the end of `<head>`, before the body), the browser caches the asset across documents, and the bridge's `ready` message carries `BRIDGE_PROTOCOL_VERSION`, which the viewer checks: a stale cached asset (no stamp, or another version) logs one console warning naming both versions and shows a dismissible error banner in the surface (`onBridgeUnavailable` fires too); no `ready` within `bridgeReadyTimeoutMs` (default 5000) shows a timeout banner. The package owns that banner by default (`bridgeErrorDisplay="banner"`); a host that renders its own notice from `onBridgeUnavailable` passes `bridgeErrorDisplay="none"` (0.34.0) and no strip is rendered, while the callback and the console warning are unchanged. The URL is resolved against your document's base (`document.baseURI`) before it is written into the srcdoc, never against the framed page, so a page's own `<base href>` cannot redirect it. Plannotator passes nothing and stays inline; none of this runs on the inline path. **CSP:** the package sets no CSP `<meta>` in the srcdoc document, and the frame is an opaque origin so the classic script needs no CORS (no `crossorigin` is set), but a CSP delivered as a header on your page is inherited by the frame: allow `script-src` for the asset's origin. Because the frame is an opaque origin, an asset served with `Cross-Origin-Resource-Policy: same-origin` (common alongside COEP) is blocked; serve it with a CORP that admits cross-origin loads, or without CORP. To also drop the inline literal from your viewer chunk, alias the package's relative `./bridge-script` import (match `/^\.\/bridge-script$/`, never a bare `/\/bridge-script$/`, which would also catch another package's `bridge-script` entry) to the generated `bridge-script.lite` module (see HANDOFF.md § "HTML viewer bridge as an asset").
|
|
126
126
|
|
|
127
127
|
#### Also blessed in 0.32.0: `shortcuts` and `utils/inputMethod`
|
|
128
128
|
|
|
@@ -164,7 +164,7 @@ npm install @plannotator/ui @plannotator/core
|
|
|
164
164
|
- `@plannotator/core` — pure utils + types, zero deps, browser-safe (CI enforces no `node:` imports). Published.
|
|
165
165
|
- `@plannotator/ui` — React components/hooks + theme + `configure()`. Depends on `@plannotator/core` (exact-version lockstep). Published.
|
|
166
166
|
- `@plannotator/shared`, `@plannotator/ai` — stay private to the monorepo; `shared` re-exports `core`'s modules via shims so Plannotator's internals are untouched.
|
|
167
|
-
- Versioned together (currently `@plannotator/ui` 0.
|
|
167
|
+
- Versioned together (currently `@plannotator/ui` 0.34.0 on `@plannotator/core` 0.25.0). `core` is bumped only when something under `packages/core` changed, so `ui` can advance alone: 0.33.0 and 0.34.0 are such releases, published on the already available core 0.25.0. When both change, publish `core` then `ui`: build each tarball with **`bun pm pack`** (resolves `workspace:*` to the exact version at pack time, from `bun.lock`, so run `bun install` after a bump), then **`npm publish *.tgz --provenance --access public`**, the repo's existing flow (`--provenance` needs CI OIDC; local publishes drop it, see HANDOFF.md "Publishing & versioning").
|
|
168
168
|
|
|
169
169
|
## The one rule
|
|
170
170
|
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
loadMermaidRuntime,
|
|
10
10
|
__setMermaidRuntimeLoaderForTests,
|
|
11
11
|
} from '../utils/mermaid';
|
|
12
|
+
import { loadMathRenderer } from '../utils/math';
|
|
13
|
+
import { hasMermaidMath } from '../utils/mermaid-math-slot';
|
|
12
14
|
|
|
13
15
|
// Re-exported: the config pin test and the lazy-retry test import them from here.
|
|
14
16
|
export { MERMAID_CONFIG, __setMermaidRuntimeLoaderForTests };
|
|
@@ -208,6 +210,20 @@ const MermaidBlockImpl: React.FC<{ block: Block }> = ({ block }) => {
|
|
|
208
210
|
return;
|
|
209
211
|
}
|
|
210
212
|
try {
|
|
213
|
+
// A `$$` label makes Mermaid render KaTeX. On a host that redirects
|
|
214
|
+
// Mermaid's `katex` import to `utils/mermaid-math-slot` the label is
|
|
215
|
+
// typeset through the math slot, which must be filled by then: warm
|
|
216
|
+
// it with the registered loader first. A filled slot (Plannotator's
|
|
217
|
+
// eager entry) resolves at once; a load failure is left to the
|
|
218
|
+
// render, whose error panel names it with the source.
|
|
219
|
+
if (hasMermaidMath(block.content)) {
|
|
220
|
+
try {
|
|
221
|
+
await loadMathRenderer();
|
|
222
|
+
} catch {
|
|
223
|
+
// Reported by the render below.
|
|
224
|
+
}
|
|
225
|
+
if (cancelled) return;
|
|
226
|
+
}
|
|
211
227
|
const id = `mermaid-${block.id}`;
|
|
212
228
|
const { svg: renderedSvg } = await mermaid.render(id, block.content);
|
|
213
229
|
if (!cancelled) {
|
|
@@ -281,9 +281,19 @@ export interface HtmlViewerProps {
|
|
|
281
281
|
bridgeReadyTimeoutMs?: number;
|
|
282
282
|
/** The bridge could not be established on the `bridgeScriptUrl` path (no
|
|
283
283
|
* ready within the timeout, or a protocol version mismatch). The surface
|
|
284
|
-
* shows its own banner as well
|
|
285
|
-
* retry affordance). Never called
|
|
284
|
+
* shows its own banner as well unless `bridgeErrorDisplay` is `'none'`;
|
|
285
|
+
* this lets the host react (telemetry, a retry affordance). Never called
|
|
286
|
+
* on the inline path. */
|
|
286
287
|
onBridgeUnavailable?: (info: BridgeUnavailableInfo) => void;
|
|
288
|
+
/**
|
|
289
|
+
* Who renders the bridge-failure strip on the `bridgeScriptUrl` path.
|
|
290
|
+
* `'banner'` (default): the package renders its `[data-bridge-error]`
|
|
291
|
+
* strip over the frame, as in 0.33.0. `'none'`: no strip is rendered and
|
|
292
|
+
* the host owns the display through `onBridgeUnavailable`, which fires
|
|
293
|
+
* exactly as before (and a version mismatch still logs its one console
|
|
294
|
+
* warning). Meaningless on the inline path, which never shows a strip.
|
|
295
|
+
*/
|
|
296
|
+
bridgeErrorDisplay?: "banner" | "none";
|
|
287
297
|
}
|
|
288
298
|
|
|
289
299
|
/**
|
|
@@ -329,6 +339,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
329
339
|
bridgeScriptUrl,
|
|
330
340
|
bridgeReadyTimeoutMs = DEFAULT_BRIDGE_READY_TIMEOUT_MS,
|
|
331
341
|
onBridgeUnavailable,
|
|
342
|
+
bridgeErrorDisplay = "banner",
|
|
332
343
|
},
|
|
333
344
|
ref,
|
|
334
345
|
) => {
|
|
@@ -1061,8 +1072,10 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
|
|
|
1061
1072
|
{/* bridgeScriptUrl path only: the bridge did not come up (no
|
|
1062
1073
|
ready within the timeout, or a stale asset's version). Floated
|
|
1063
1074
|
over the top of the iframe so it never changes the layout the
|
|
1064
|
-
page renders in; the page itself stays visible.
|
|
1065
|
-
|
|
1075
|
+
page renders in; the page itself stays visible. A host that
|
|
1076
|
+
renders its own notice from onBridgeUnavailable passes
|
|
1077
|
+
bridgeErrorDisplay="none" and no strip is rendered at all. */}
|
|
1078
|
+
{bridgeError && !bridgeErrorDismissed && bridgeErrorDisplay !== "none" && (
|
|
1066
1079
|
<div
|
|
1067
1080
|
role="alert"
|
|
1068
1081
|
data-print-hide
|
package/package.json
CHANGED
package/utils/math.ts
CHANGED
|
@@ -48,6 +48,12 @@ let rendererSource: MathRendererSource | null = null;
|
|
|
48
48
|
*/
|
|
49
49
|
let loader: MathRendererLoader | null = null;
|
|
50
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;
|
|
51
57
|
const listeners = new Set<() => void>();
|
|
52
58
|
|
|
53
59
|
function notify(): void {
|
|
@@ -84,14 +90,22 @@ export function subscribeMathRenderer(listener: () => void): () => void {
|
|
|
84
90
|
* Swap the loader `loadMathRenderer()` uses. Host seam
|
|
85
91
|
* (`configurePlannotatorUI({ mathRendererLoader })`): a host may return a
|
|
86
92
|
* module that imports katex AND its stylesheet in one chunk. A load already in
|
|
87
|
-
* flight keeps going
|
|
88
|
-
*
|
|
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.
|
|
89
98
|
*/
|
|
90
|
-
export function setMathRendererLoader(next: MathRendererLoader): void {
|
|
99
|
+
export function setMathRendererLoader(next: MathRendererLoader | null): void {
|
|
91
100
|
loader = next;
|
|
92
101
|
pending = null;
|
|
93
102
|
}
|
|
94
103
|
|
|
104
|
+
/** The registered host loader, or `null` while the package default applies. */
|
|
105
|
+
export function getMathRendererLoader(): MathRendererLoader | null {
|
|
106
|
+
return loader;
|
|
107
|
+
}
|
|
108
|
+
|
|
95
109
|
/**
|
|
96
110
|
* Load and register the renderer. Idempotent: a filled slot resolves at once,
|
|
97
111
|
* a load in flight is shared, and a rejected load is dropped so the next call
|
|
@@ -100,9 +114,12 @@ export function setMathRendererLoader(next: MathRendererLoader): void {
|
|
|
100
114
|
export function loadMathRenderer(): Promise<MathRenderer> {
|
|
101
115
|
if (renderer) return Promise.resolve(renderer);
|
|
102
116
|
if (!pending) {
|
|
117
|
+
const epoch = resetEpoch;
|
|
103
118
|
const attempt = (loader ? loader() : loadDefaultMathRenderer()).then(
|
|
104
119
|
(loaded) => {
|
|
105
|
-
|
|
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');
|
|
106
123
|
return loaded;
|
|
107
124
|
},
|
|
108
125
|
(err: unknown) => {
|
|
@@ -115,12 +132,19 @@ export function loadMathRenderer(): Promise<MathRenderer> {
|
|
|
115
132
|
return pending;
|
|
116
133
|
}
|
|
117
134
|
|
|
118
|
-
/**
|
|
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
|
+
*/
|
|
119
143
|
export function resetMathRenderer(): void {
|
|
120
144
|
renderer = null;
|
|
121
145
|
rendererSource = null;
|
|
122
|
-
loader = null;
|
|
123
146
|
pending = null;
|
|
147
|
+
resetEpoch += 1;
|
|
124
148
|
notify();
|
|
125
149
|
}
|
|
126
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;
|