brookmd 0.24.0 → 0.25.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 +67 -0
- package/README.md +71 -3
- package/dist/client.d.ts +15 -0
- package/dist/client.js +68 -3
- package/dist/dom.js +4 -1
- package/dist/html-to-react.js +29 -1
- package/dist/react.d.ts +44 -11
- package/dist/react.js +105 -18
- package/dist/server-react.js +2 -1
- package/dist/types-react.d.ts +32 -4
- package/dist/warn.d.ts +9 -0
- package/dist/warn.js +19 -0
- package/dist/wasm/brook_md_core_bg.wasm +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,73 @@ Notable changes to brookmd (formerly `flux-md`). Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); this project aims to follow
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.25.0 — 2026-07-26
|
|
8
|
+
|
|
9
|
+
Fixes a crash class where a `components` override could be invoked with the
|
|
10
|
+
wrong prop shape and take the entire document down with it. If you pass
|
|
11
|
+
`components` and read `props.block` in any of them, upgrade.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **A `components` override no longer receives two incompatible prop shapes
|
|
16
|
+
without warning.** The map is consulted by two dispatchers: the block-kind
|
|
17
|
+
dispatcher (which supplies `BlockComponentProps`, including `block`) and the
|
|
18
|
+
element-name walker that powers `a`/`code`/`table` overrides (which supplies
|
|
19
|
+
attributes + `children` and **no `block`**). The same key reaches both — an
|
|
20
|
+
`inlineComponentTags` chip, or a `componentTags` tag nested inside a list item
|
|
21
|
+
or blockquote — so an override reading `props.block.…` threw
|
|
22
|
+
`can't access property "kind", block is undefined`, intermittently, depending
|
|
23
|
+
on where the model put the tag. Three defenses now apply: block-kind keys are
|
|
24
|
+
typed to `BlockComponentProps` (a mismatched override is a compile error), a
|
|
25
|
+
raw element whose name collides with a block-kind key is never dispatched to
|
|
26
|
+
that override, and every block renders inside its own error boundary.
|
|
27
|
+
- **A throwing override costs one block, not the document.** React unmounts the
|
|
28
|
+
whole tree on an uncaught render error, so a single bad override blanked the
|
|
29
|
+
page. Each block now has its own boundary; the failed block is skipped and
|
|
30
|
+
retried as soon as its HTML changes, so a streaming-tail failure heals itself
|
|
31
|
+
when the block settles.
|
|
32
|
+
- **The parser no longer emits a component tag it will retract.** A component
|
|
33
|
+
open tag was recognized as soon as it looked whole-line, but during streaming
|
|
34
|
+
end-of-buffer is not end-of-line — so `> <Thinking>x</Thinking>` rendered a
|
|
35
|
+
raw `<Thinking>` element for one tick before settling to escaped text. That
|
|
36
|
+
transient raw element is what reached overrides with the wrong props. A
|
|
37
|
+
component tag now opens a block only once its line is known to be complete.
|
|
38
|
+
- **Recovery no longer loses the parser config.** The worker keeps config per
|
|
39
|
+
stream id, so the one-shot recovery re-feed landed on a worker that had never
|
|
40
|
+
seen it while `configSent` stayed latched — the healed parser was silently
|
|
41
|
+
rebuilt with library defaults, dropping `componentTags`, `blockData`,
|
|
42
|
+
`gfmMath` and the whole `kind.data` structured channel for the rest of the
|
|
43
|
+
session.
|
|
44
|
+
- **A terminal worker failure no longer corrupts the document.** The store kept
|
|
45
|
+
the dead generation's blocks while the fresh parser renumbered from zero, so
|
|
46
|
+
the next append merged two generations under colliding ids (duplicate React
|
|
47
|
+
keys, silently overwritten blocks, a shrinking document). The generation now
|
|
48
|
+
restarts cleanly, with a one-time warning. Transient failures still heal
|
|
49
|
+
invisibly through recovery, unchanged.
|
|
50
|
+
- **Per-block error containment is free for committed blocks.** The boundary
|
|
51
|
+
lives inside the per-block memo, so a settled document re-renders no
|
|
52
|
+
boundaries when the streaming tail patches — a new React-side complexity gate
|
|
53
|
+
(`test/boundary-linearity.test.tsx`) pins this, counting work rather than
|
|
54
|
+
timing it, mirroring the Rust `scaling` gate.
|
|
55
|
+
- `applyPatch` and the stale-view merge now drop a malformed entry rather than
|
|
56
|
+
publishing a hole into the snapshot, and both renderers skip a block with no
|
|
57
|
+
`kind` instead of dereferencing it.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- **`onBlockError`** on `<BrookMarkdown>` — fires when a block's render throws,
|
|
62
|
+
with `{ blockId, kind, componentKeys, html }`. Without it the same detail goes
|
|
63
|
+
to `console.error`.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- `Components` is now a mapped type: block-kind keys (`CodeBlock`, `Table`,
|
|
68
|
+
`Alert`, …) are typed to `BlockComponentProps`; every other key stays
|
|
69
|
+
permissive. Type-only, but it will surface existing mismatches at compile time.
|
|
70
|
+
- Requires `brookmd-core` 0.24.0 (the streaming component-tag fix above).
|
|
71
|
+
- `onBlockError` is identity-sensitive like `components` / `onRenderMetrics` —
|
|
72
|
+
hoist or memoize it, or every block re-renders on every patch.
|
|
73
|
+
|
|
7
74
|
## 0.24.0 — 2026-07-24
|
|
8
75
|
|
|
9
76
|
### Added
|
package/README.md
CHANGED
|
@@ -766,6 +766,33 @@ block. The component receives [`BlockComponentProps`](#types): `{ block, html,
|
|
|
766
766
|
open, speculative }`, plus `text`/`language` for code/math blocks (the alert
|
|
767
767
|
type is at `block.kind.data.kind`).
|
|
768
768
|
|
|
769
|
+
> **One map, two prop contracts — the single biggest footgun.** The keys above
|
|
770
|
+
> are looked up by TWO dispatchers. The block-kind dispatcher passes
|
|
771
|
+
> `BlockComponentProps` (with `block`); the element dispatcher, which is what
|
|
772
|
+
> makes `a` / `code` / `table` overrides work, passes **the element's attributes
|
|
773
|
+
> and `children` only — no `block`**. The same name can hit both: an
|
|
774
|
+
> `inlineComponentTags` chip, or a `componentTags` tag that lands inside a list
|
|
775
|
+
> item or blockquote (where it is a real *nested* Component block, rendered as an
|
|
776
|
+
> element inside its container's HTML), takes the element path. So an override
|
|
777
|
+
> that reads `props.block.…` throws `can't access property "kind", block is
|
|
778
|
+
> undefined` for those occurrences — intermittently, because it depends on where
|
|
779
|
+
> the model happened to put the tag.
|
|
780
|
+
>
|
|
781
|
+
> Write any override for a name that can appear in both positions defensively:
|
|
782
|
+
>
|
|
783
|
+
> ```tsx
|
|
784
|
+
> const Thinking = ({ block, children }) =>
|
|
785
|
+
> block ? <Panel data={block.kind.data}>{children}</Panel> : <span>{children}</span>;
|
|
786
|
+
> ```
|
|
787
|
+
>
|
|
788
|
+
> Three things make this survivable rather than fatal: block-kind keys are typed
|
|
789
|
+
> to `BlockComponentProps`, so a mismatched override is a **compile** error; a raw
|
|
790
|
+
> element whose name collides with a block-kind key (`<Table>`, `<Alert>`… — only
|
|
791
|
+
> reachable with raw-HTML passthrough on) is never dispatched to that override;
|
|
792
|
+
> and every block renders inside its own **error boundary**, so a throwing
|
|
793
|
+
> override costs that one block instead of unmounting the document. Wire
|
|
794
|
+
> [`onBlockError`](#onblockerror) to see them.
|
|
795
|
+
|
|
769
796
|
Rules worth knowing:
|
|
770
797
|
|
|
771
798
|
- **There is no `node` prop / no hast tree.** Introspect via `className` /
|
|
@@ -928,9 +955,23 @@ tags only). It works everywhere inline content does — **including table cells*
|
|
|
928
955
|
Tag names match **case-sensitively** and dispatch verbatim to `components[tag]`
|
|
929
956
|
(`<tik>`→`components.tik`, `<Cite>`→`components.Cite`). The
|
|
930
957
|
two lists are independent: list a tag under `componentTags` for blocks,
|
|
931
|
-
`inlineComponentTags` for inline, or both for both.
|
|
932
|
-
|
|
933
|
-
|
|
958
|
+
`inlineComponentTags` for inline, or both for both.
|
|
959
|
+
|
|
960
|
+
Where an allowlisted tag actually lands:
|
|
961
|
+
|
|
962
|
+
| Position | Result |
|
|
963
|
+
| --- | --- |
|
|
964
|
+
| Own line, top level | block `Component` — override gets `BlockComponentProps` |
|
|
965
|
+
| Own line inside a list item / blockquote | real **nested** Component block, emitted as an element inside the container's HTML — the override is dispatched by **element name**, so it gets attributes + `children` and **no `block`** |
|
|
966
|
+
| Mid-paragraph, listed in `inlineComponentTags` | inline element — attributes + `children`, no `block` |
|
|
967
|
+
| Mid-paragraph, NOT listed in `inlineComponentTags` | escaped text |
|
|
968
|
+
| Inside a table cell | escaped text (cells are inline-only) |
|
|
969
|
+
|
|
970
|
+
An allowlisted tag in a position that is not supported degrades **inertly**
|
|
971
|
+
(escaped) — it never consumes surrounding content. But note rows 2 and 3: those
|
|
972
|
+
DO render, through the element path, which is why an override that reads
|
|
973
|
+
`props.block` must guard for its absence (see [the two prop
|
|
974
|
+
contracts](#custom-components--overrides)).
|
|
934
975
|
|
|
935
976
|
> **Link-bridge alternative.** Before `inlineComponentTags`, the way to get an
|
|
936
977
|
> inline custom element was the link bridge: emit `[$AAPL](tik://AAPL)` and
|
|
@@ -1269,6 +1310,33 @@ re-snapping during streaming, so treat smooth following there as best-effort.
|
|
|
1269
1310
|
> is the *shared* worker's heap — clients on the same worker report the same
|
|
1270
1311
|
> value. Aggregate with `Math.max`, not a sum.
|
|
1271
1312
|
|
|
1313
|
+
<a id="onblockerror"></a>
|
|
1314
|
+
|
|
1315
|
+
### When a block fails to render — `onBlockError`
|
|
1316
|
+
|
|
1317
|
+
Every block renders inside its own error boundary. React's default response to
|
|
1318
|
+
an uncaught render error is to unmount the **entire** tree, so before this a
|
|
1319
|
+
single throwing `components` override blanked the whole document. Now the
|
|
1320
|
+
failure costs that one block: the rest of the stream keeps rendering, and the
|
|
1321
|
+
block is retried as soon as its HTML changes (so a streaming-tail failure heals
|
|
1322
|
+
itself when the block settles).
|
|
1323
|
+
|
|
1324
|
+
```tsx
|
|
1325
|
+
<BrookMarkdown
|
|
1326
|
+
client={client}
|
|
1327
|
+
components={components}
|
|
1328
|
+
onBlockError={(err, { blockId, kind, componentKeys, html }) => {
|
|
1329
|
+
reportToSentry(err, { blockId, kind, componentKeys, html });
|
|
1330
|
+
}}
|
|
1331
|
+
/>
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
`componentKeys` is the override map's keys — the shortlist of suspects — and
|
|
1335
|
+
`html` is the first 200 chars of the block that failed. Without the hook the same
|
|
1336
|
+
detail goes to `console.error`. The overwhelmingly common cause is an override
|
|
1337
|
+
reading `props.block.…` on the element path, where there is no `block`; see [the
|
|
1338
|
+
two prop contracts](#custom-components--overrides).
|
|
1339
|
+
|
|
1272
1340
|
## Architecture
|
|
1273
1341
|
|
|
1274
1342
|
```
|
package/dist/client.d.ts
CHANGED
|
@@ -156,6 +156,9 @@ export declare class BrookClient {
|
|
|
156
156
|
private streamId;
|
|
157
157
|
private config?;
|
|
158
158
|
private configSent;
|
|
159
|
+
/** Set when ensureAcquired rebound this stream onto a fresh worker+parser
|
|
160
|
+
* after a fatal failure; consumed by the next content-bearing op. */
|
|
161
|
+
private pendingRebind;
|
|
159
162
|
private listeners;
|
|
160
163
|
private store;
|
|
161
164
|
private onError?;
|
|
@@ -176,6 +179,9 @@ export declare class BrookClient {
|
|
|
176
179
|
private staleTrimmed;
|
|
177
180
|
private idNamespace;
|
|
178
181
|
private mergeCache;
|
|
182
|
+
/** Set by mergeStale when it had to compact a hole out of the view, so
|
|
183
|
+
* getSnapshot skips caching a view whose indices no longer track `base`. */
|
|
184
|
+
private mergeDropped;
|
|
179
185
|
private appendedBytes;
|
|
180
186
|
private patchCount;
|
|
181
187
|
private totalParseMicros;
|
|
@@ -245,6 +251,15 @@ export declare class BrookClient {
|
|
|
245
251
|
* multiplexing (pick() is unchanged and remains the only path to create()).
|
|
246
252
|
*/
|
|
247
253
|
private ensureAcquired;
|
|
254
|
+
/**
|
|
255
|
+
* A worker rebind left the store holding a dead generation's blocks while the
|
|
256
|
+
* fresh parser restarts ids at 0. Called by the ops that actually feed the
|
|
257
|
+
* parser, before they do. Recovery's re-feed handles this itself (it re-parses
|
|
258
|
+
* the whole document over a preserved view) and clears the flag; this is the
|
|
259
|
+
* path where recovery is off or already spent, where the honest outcome is a
|
|
260
|
+
* clean restart rather than two generations interleaved under colliding ids.
|
|
261
|
+
*/
|
|
262
|
+
private settleRebind;
|
|
248
263
|
get ready(): boolean;
|
|
249
264
|
/**
|
|
250
265
|
* The fatal error that killed this client's worker, or `null` if healthy.
|
package/dist/client.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { warnOnce } from "./warn.js";
|
|
1
2
|
import { createWorker } from "./asset-urls.js";
|
|
2
3
|
function emptyBlockStore() {
|
|
3
4
|
return { committed: /* @__PURE__ */ new Map(), committedOrder: [], active: [], snapshot: [] };
|
|
@@ -24,12 +25,27 @@ function applyPatch(store, patch) {
|
|
|
24
25
|
}
|
|
25
26
|
store.active = active;
|
|
26
27
|
const next = new Array(store.committedOrder.length + store.active.length);
|
|
28
|
+
let w = 0;
|
|
27
29
|
for (let i = 0; i < store.committedOrder.length; i++) {
|
|
28
|
-
|
|
30
|
+
const b = store.committed.get(store.committedOrder[i]);
|
|
31
|
+
if (b === void 0) {
|
|
32
|
+
warnOnce(
|
|
33
|
+
"orphan-committed",
|
|
34
|
+
`brookmd: committed block ${store.committedOrder[i]} is missing from the store and was dropped. This is a bug in brookmd \u2014 please report it.`
|
|
35
|
+
);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
next[w++] = b;
|
|
29
39
|
}
|
|
30
40
|
for (let i = 0; i < store.active.length; i++) {
|
|
31
|
-
|
|
41
|
+
const b = store.active[i];
|
|
42
|
+
if (b === void 0) {
|
|
43
|
+
warnOnce("orphan-active", `brookmd: active block at index ${i} is undefined and was dropped.`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
next[w++] = b;
|
|
32
47
|
}
|
|
48
|
+
if (w !== next.length) next.length = w;
|
|
33
49
|
store.snapshot = next;
|
|
34
50
|
}
|
|
35
51
|
class BrookPool {
|
|
@@ -273,6 +289,9 @@ class BrookClient {
|
|
|
273
289
|
streamId = 0;
|
|
274
290
|
config;
|
|
275
291
|
configSent = false;
|
|
292
|
+
/** Set when ensureAcquired rebound this stream onto a fresh worker+parser
|
|
293
|
+
* after a fatal failure; consumed by the next content-bearing op. */
|
|
294
|
+
pendingRebind = false;
|
|
276
295
|
listeners = /* @__PURE__ */ new Set();
|
|
277
296
|
store = emptyBlockStore();
|
|
278
297
|
onError;
|
|
@@ -359,6 +378,9 @@ class BrookClient {
|
|
|
359
378
|
// reads between notifies must return the SAME reference — the
|
|
360
379
|
// useSyncExternalStore cached-snapshot contract.
|
|
361
380
|
mergeCache = null;
|
|
381
|
+
/** Set by mergeStale when it had to compact a hole out of the view, so
|
|
382
|
+
* getSnapshot skips caching a view whose indices no longer track `base`. */
|
|
383
|
+
mergeDropped = false;
|
|
362
384
|
// Perf
|
|
363
385
|
appendedBytes = 0;
|
|
364
386
|
patchCount = 0;
|
|
@@ -430,12 +452,36 @@ class BrookClient {
|
|
|
430
452
|
*/
|
|
431
453
|
ensureAcquired() {
|
|
432
454
|
if (this.pw && !this.pw.failed) return this.pw;
|
|
455
|
+
const rebinding = this.pw !== null;
|
|
433
456
|
this.pw = null;
|
|
434
457
|
const { streamId, pw } = this.pool.acquire((msg) => this.onMessage(msg));
|
|
435
458
|
this.streamId = streamId;
|
|
436
459
|
this.pw = pw;
|
|
460
|
+
if (rebinding) {
|
|
461
|
+
this.configSent = false;
|
|
462
|
+
this.pendingRebind = true;
|
|
463
|
+
}
|
|
437
464
|
return pw;
|
|
438
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* A worker rebind left the store holding a dead generation's blocks while the
|
|
468
|
+
* fresh parser restarts ids at 0. Called by the ops that actually feed the
|
|
469
|
+
* parser, before they do. Recovery's re-feed handles this itself (it re-parses
|
|
470
|
+
* the whole document over a preserved view) and clears the flag; this is the
|
|
471
|
+
* path where recovery is off or already spent, where the honest outcome is a
|
|
472
|
+
* clean restart rather than two generations interleaved under colliding ids.
|
|
473
|
+
*/
|
|
474
|
+
settleRebind() {
|
|
475
|
+
if (!this.pendingRebind) return;
|
|
476
|
+
if (this.failedError === null) return;
|
|
477
|
+
this.pendingRebind = false;
|
|
478
|
+
if (this.store.snapshot.length === 0 && !this.staleSnapshot) return;
|
|
479
|
+
warnOnce(
|
|
480
|
+
"rebind-reset",
|
|
481
|
+
"brookmd: the parser was rebuilt on a new worker after a fatal failure, so the document restarted. Blocks rendered before the failure were dropped because the fresh parser renumbers from zero. Enable `recovery` (the default) to re-feed and heal invisibly instead."
|
|
482
|
+
);
|
|
483
|
+
this.resetParser();
|
|
484
|
+
}
|
|
439
485
|
get ready() {
|
|
440
486
|
return this.pw?.ready ?? false;
|
|
441
487
|
}
|
|
@@ -466,6 +512,7 @@ class BrookClient {
|
|
|
466
512
|
}
|
|
467
513
|
append(chunk) {
|
|
468
514
|
const pw = this.ensureAcquired();
|
|
515
|
+
this.settleRebind();
|
|
469
516
|
if (this.firstAppendMs === 0) this.firstAppendMs = performance.now();
|
|
470
517
|
if (this.recovery) {
|
|
471
518
|
this.recoveryBuffer += chunk;
|
|
@@ -477,6 +524,7 @@ class BrookClient {
|
|
|
477
524
|
}
|
|
478
525
|
finalize() {
|
|
479
526
|
const pw = this.ensureAcquired();
|
|
527
|
+
this.settleRebind();
|
|
480
528
|
this.finalizePending = true;
|
|
481
529
|
this.contentDone = true;
|
|
482
530
|
this.pool.send(pw, { type: "finalize", streamId: this.streamId, config: this.firstConfig(), epoch: this.epoch });
|
|
@@ -599,6 +647,7 @@ class BrookClient {
|
|
|
599
647
|
this.mergeCache = null;
|
|
600
648
|
this.failedError = null;
|
|
601
649
|
this.recoveryAttempted = false;
|
|
650
|
+
this.pendingRebind = false;
|
|
602
651
|
this.resetParser();
|
|
603
652
|
if (hadContent) this.emit(true);
|
|
604
653
|
}
|
|
@@ -700,7 +749,12 @@ class BrookClient {
|
|
|
700
749
|
const cache = this.mergeCache;
|
|
701
750
|
if (cache && cache.base === base && cache.trimmed === this.staleTrimmed) return cache.view;
|
|
702
751
|
const view = this.mergeStale(base, cache && cache.trimmed === this.staleTrimmed ? cache : null);
|
|
703
|
-
this.
|
|
752
|
+
if (this.mergeDropped) {
|
|
753
|
+
this.mergeDropped = false;
|
|
754
|
+
this.mergeCache = null;
|
|
755
|
+
} else {
|
|
756
|
+
this.mergeCache = { base, trimmed: this.staleTrimmed, view };
|
|
757
|
+
}
|
|
704
758
|
return view;
|
|
705
759
|
};
|
|
706
760
|
/**
|
|
@@ -731,6 +785,7 @@ class BrookClient {
|
|
|
731
785
|
const stale = this.staleSnapshot;
|
|
732
786
|
const len = this.staleTrimmed ? base.length : Math.max(base.length, stale.length);
|
|
733
787
|
const view = new Array(len);
|
|
788
|
+
let dropped = false;
|
|
734
789
|
for (let i = 0; i < len; i++) {
|
|
735
790
|
const nb = i < base.length ? base[i] : void 0;
|
|
736
791
|
if (nb !== void 0 && prev !== null && i < prev.base.length && prev.base[i] === nb) {
|
|
@@ -739,6 +794,10 @@ class BrookClient {
|
|
|
739
794
|
}
|
|
740
795
|
const ob = i < stale.length ? stale[i] : void 0;
|
|
741
796
|
if (!nb) {
|
|
797
|
+
if (ob === void 0) {
|
|
798
|
+
dropped = true;
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
742
801
|
view[i] = ob;
|
|
743
802
|
continue;
|
|
744
803
|
}
|
|
@@ -756,6 +815,11 @@ class BrookClient {
|
|
|
756
815
|
}
|
|
757
816
|
view[i] = { ...nb, id: this.idNamespace + nb.id };
|
|
758
817
|
}
|
|
818
|
+
if (dropped) {
|
|
819
|
+
warnOnce("merge-hole", "brookmd: the stale-merge produced an empty position \u2014 compacting.");
|
|
820
|
+
this.mergeDropped = true;
|
|
821
|
+
return view.filter((b) => b !== void 0);
|
|
822
|
+
}
|
|
759
823
|
return view;
|
|
760
824
|
}
|
|
761
825
|
/**
|
|
@@ -919,6 +983,7 @@ class BrookClient {
|
|
|
919
983
|
const displayed = this.getSnapshot();
|
|
920
984
|
if (displayed.length > 0) this.softReset(displayed);
|
|
921
985
|
else this.resetParser();
|
|
986
|
+
this.pendingRebind = false;
|
|
922
987
|
this.append(doc);
|
|
923
988
|
if (done) this.finalize();
|
|
924
989
|
}
|
package/dist/dom.js
CHANGED
|
@@ -55,9 +55,11 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
55
55
|
const snapshot = client.getSnapshot();
|
|
56
56
|
const nextOrder = new Array(snapshot.length);
|
|
57
57
|
const seen = /* @__PURE__ */ new Set();
|
|
58
|
+
let w = 0;
|
|
58
59
|
for (let i = 0; i < snapshot.length; i++) {
|
|
59
60
|
const b = snapshot[i];
|
|
60
|
-
|
|
61
|
+
if (b == null || b.kind == null) continue;
|
|
62
|
+
nextOrder[w++] = b.id;
|
|
61
63
|
seen.add(b.id);
|
|
62
64
|
const existing = mounted.get(b.id);
|
|
63
65
|
if (!existing) {
|
|
@@ -131,6 +133,7 @@ function mountBrookMarkdown(client, container, options = {}) {
|
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
135
|
}
|
|
136
|
+
if (w !== nextOrder.length) nextOrder.length = w;
|
|
134
137
|
order = nextOrder;
|
|
135
138
|
reconcileChildren();
|
|
136
139
|
}
|
package/dist/html-to-react.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { createElement, Fragment } from "react";
|
|
2
2
|
import { decorateSegments } from "./decorate.js";
|
|
3
3
|
import { decodeEntities, safeUrl } from "./url-safety.js";
|
|
4
|
+
import { warnOnce } from "./warn.js";
|
|
5
|
+
const BLOCK_KIND_KEYS = /* @__PURE__ */ new Set([
|
|
6
|
+
"Paragraph",
|
|
7
|
+
"Heading",
|
|
8
|
+
"CodeBlock",
|
|
9
|
+
"MathBlock",
|
|
10
|
+
"Mermaid",
|
|
11
|
+
"List",
|
|
12
|
+
"Blockquote",
|
|
13
|
+
"Alert",
|
|
14
|
+
"Table",
|
|
15
|
+
"Rule",
|
|
16
|
+
"Html",
|
|
17
|
+
"Component"
|
|
18
|
+
]);
|
|
4
19
|
const VOID = /* @__PURE__ */ new Set([
|
|
5
20
|
"area",
|
|
6
21
|
"base",
|
|
@@ -220,6 +235,19 @@ function pushDecoratedText(out, text, decorators, ancestors, keyBase) {
|
|
|
220
235
|
out.push(createElement(Fragment, { key: keyBase + ":" + i }, replacement));
|
|
221
236
|
}
|
|
222
237
|
}
|
|
238
|
+
function resolveTagType(tag, components) {
|
|
239
|
+
const c = tag.charCodeAt(0);
|
|
240
|
+
if (c >= 65 && c <= 90 && BLOCK_KIND_KEYS.has(tag)) {
|
|
241
|
+
if (components[tag] !== void 0) {
|
|
242
|
+
warnOnce(
|
|
243
|
+
"kind-key-as-tag:" + tag,
|
|
244
|
+
`brookmd: raw <${tag}> in the rendered HTML collides with the block-kind override key "${tag}". A block-kind override receives \`BlockComponentProps\` (with \`block\`), which an inline element cannot supply, so the raw tag is rendered as a plain element instead. Rename the raw tag or the override key.`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
return tag;
|
|
248
|
+
}
|
|
249
|
+
return components[tag] ?? tag;
|
|
250
|
+
}
|
|
223
251
|
function nodesToReact(nodes, components, keyPrefix, ctx, ancestors) {
|
|
224
252
|
const out = [];
|
|
225
253
|
for (let idx = 0; idx < nodes.length; idx++) {
|
|
@@ -233,7 +261,7 @@ function nodesToReact(nodes, components, keyPrefix, ctx, ancestors) {
|
|
|
233
261
|
continue;
|
|
234
262
|
}
|
|
235
263
|
const key = keyPrefix + idx;
|
|
236
|
-
const type =
|
|
264
|
+
const type = resolveTagType(n.tag, components);
|
|
237
265
|
const props = attrsToProps(n.tag, n.attrs, key, ctx?.urlTransform);
|
|
238
266
|
if (VOID.has(n.tag.toLowerCase())) {
|
|
239
267
|
out.push(createElement(type, props));
|
package/dist/react.d.ts
CHANGED
|
@@ -165,6 +165,41 @@ interface BrookMarkdownProps {
|
|
|
165
165
|
* concurrent deferral of the visible tail.
|
|
166
166
|
*/
|
|
167
167
|
deferTail?: boolean;
|
|
168
|
+
/**
|
|
169
|
+
* Called when a single block's render THROWS — almost always from inside a
|
|
170
|
+
* `components` override, not from brookmd itself.
|
|
171
|
+
*
|
|
172
|
+
* Every block is wrapped in its own error boundary, so a throwing override
|
|
173
|
+
* costs you that one block instead of unmounting the whole document (React's
|
|
174
|
+
* default for an uncaught render error is to unmount the entire tree — a blank
|
|
175
|
+
* page). The failed block renders nothing; the rest of the stream keeps going,
|
|
176
|
+
* and later patches retry it.
|
|
177
|
+
*
|
|
178
|
+
* Without this hook the failure still goes to `console.error` with the block
|
|
179
|
+
* id, kind, override keys, and an HTML excerpt. Wire it to your error reporter
|
|
180
|
+
* to get the same detail in production.
|
|
181
|
+
*
|
|
182
|
+
* **HOIST / memoize this** (same trap as `components` / `onRenderMetrics`): a
|
|
183
|
+
* fresh closure each render busts every block's memo and re-renders the whole
|
|
184
|
+
* document on every patch.
|
|
185
|
+
*
|
|
186
|
+
* The most common cause by far: an override registered for a *block-kind* or
|
|
187
|
+
* *component-tag* key reading `props.block.…`, invoked on the tag path (the
|
|
188
|
+
* same tag nested inside another block, or used inline), where `block` is not
|
|
189
|
+
* supplied. Guard with `if (!block) return <>{children}</>`.
|
|
190
|
+
*/
|
|
191
|
+
onBlockError?: (error: Error, info: BlockErrorInfo) => void;
|
|
192
|
+
}
|
|
193
|
+
/** Context handed to {@link BrookMarkdownProps.onBlockError}. */
|
|
194
|
+
export interface BlockErrorInfo {
|
|
195
|
+
/** The block's stable parser-assigned id (also its React key). */
|
|
196
|
+
blockId: number;
|
|
197
|
+
/** The block kind that failed, e.g. `"Component"` / `"Table"`. */
|
|
198
|
+
kind: string;
|
|
199
|
+
/** The override keys in play — the shortlist of suspects. */
|
|
200
|
+
componentKeys: string[];
|
|
201
|
+
/** First 200 chars of the block's rendered HTML, to identify the content. */
|
|
202
|
+
html: string;
|
|
168
203
|
}
|
|
169
204
|
export declare function __resetUnstableWarnings(): void;
|
|
170
205
|
/**
|
|
@@ -223,16 +258,7 @@ export declare function useBrookMarkdownString(content: string, options?: {
|
|
|
223
258
|
declare function BrookMarkdownImpl(props: BrookMarkdownProps): import("react/jsx-runtime").JSX.Element;
|
|
224
259
|
export declare const BrookMarkdown: import("react").MemoExoticComponent<typeof BrookMarkdownImpl>;
|
|
225
260
|
export declare function blockKindProps(block: Block, components?: Components): BlockComponentProps;
|
|
226
|
-
|
|
227
|
-
block: Block;
|
|
228
|
-
components?: Components;
|
|
229
|
-
virtualize?: boolean;
|
|
230
|
-
sanitize?: (html: string) => string;
|
|
231
|
-
childMemo?: boolean;
|
|
232
|
-
onRenderMetrics?: RenderMetricsHook;
|
|
233
|
-
decorators?: Decorator[];
|
|
234
|
-
urlTransform?: UrlTransform;
|
|
235
|
-
}, next: {
|
|
261
|
+
interface BlockViewProps {
|
|
236
262
|
block: Block;
|
|
237
263
|
components?: Components;
|
|
238
264
|
virtualize?: boolean;
|
|
@@ -241,5 +267,12 @@ export declare function blocksEqual(prev: {
|
|
|
241
267
|
onRenderMetrics?: RenderMetricsHook;
|
|
242
268
|
decorators?: Decorator[];
|
|
243
269
|
urlTransform?: UrlTransform;
|
|
244
|
-
|
|
270
|
+
/** Diagnostic passthrough for the boundary; derived from `components`, so its
|
|
271
|
+
* identity is stable whenever `components` is. */
|
|
272
|
+
componentKeys?: string[];
|
|
273
|
+
onBlockError?: (error: Error, info: BlockErrorInfo) => void;
|
|
274
|
+
}
|
|
275
|
+
export declare function blocksEqual(prev: BlockViewProps, next: BlockViewProps): boolean;
|
|
276
|
+
export declare function __getBoundaryRenders(): number;
|
|
277
|
+
export declare function __resetBoundaryRenders(): void;
|
|
245
278
|
export {};
|
package/dist/react.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import {
|
|
3
|
+
Component,
|
|
3
4
|
createElement,
|
|
4
5
|
memo,
|
|
5
6
|
useEffect,
|
|
@@ -14,7 +15,16 @@ import { CodeBlock } from "./renderers/CodeBlock.js";
|
|
|
14
15
|
import { MathBlock } from "./renderers/Math.js";
|
|
15
16
|
import { Mermaid } from "./renderers/Mermaid.js";
|
|
16
17
|
import { htmlToReact } from "./html-to-react.js";
|
|
18
|
+
import { warnOnce } from "./warn.js";
|
|
17
19
|
const NO_DEFER_BLOCKS = [];
|
|
20
|
+
const EMPTY_KEYS = [];
|
|
21
|
+
function skipBadBlock(index) {
|
|
22
|
+
warnOnce(
|
|
23
|
+
"bad-block",
|
|
24
|
+
`brookmd: snapshot position ${index} has no block kind and was skipped. This indicates a corrupted block store \u2014 please report it.`
|
|
25
|
+
);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
18
28
|
const warnedUnstable = /* @__PURE__ */ new Set();
|
|
19
29
|
function useUnstablePropWarning(name, value) {
|
|
20
30
|
const ref = useRef(value);
|
|
@@ -49,7 +59,8 @@ function BrookMarkdownFromClient({
|
|
|
49
59
|
onRenderMetrics,
|
|
50
60
|
deferTail,
|
|
51
61
|
decorators,
|
|
52
|
-
urlTransform
|
|
62
|
+
urlTransform,
|
|
63
|
+
onBlockError
|
|
53
64
|
}) {
|
|
54
65
|
const blocks = useSyncExternalStore(client.subscribe, client.getSnapshot, client.getSnapshot);
|
|
55
66
|
useUnstablePropWarning("decorators", decorators);
|
|
@@ -61,6 +72,7 @@ function BrookMarkdownFromClient({
|
|
|
61
72
|
() => components && Object.keys(components).length > 0 ? components : void 0,
|
|
62
73
|
[components]
|
|
63
74
|
);
|
|
75
|
+
const componentKeys = useMemo(() => comps ? Object.keys(comps) : EMPTY_KEYS, [comps]);
|
|
64
76
|
const onMetrics = useMemo(
|
|
65
77
|
() => onRenderMetrics ? (id2, m) => {
|
|
66
78
|
client.__noteRender();
|
|
@@ -78,20 +90,29 @@ function BrookMarkdownFromClient({
|
|
|
78
90
|
"aria-live": ariaLive,
|
|
79
91
|
"aria-atomic": ariaAtomic,
|
|
80
92
|
children: [
|
|
81
|
-
rendered.map(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
rendered.map(
|
|
94
|
+
(b, i) => (
|
|
95
|
+
// The guard runs BEFORE `key={b.id}` is evaluated: a malformed entry
|
|
96
|
+
// must not throw here (it would take the whole document down), and the
|
|
97
|
+
// store's density invariant is not something a renderer should bet on.
|
|
98
|
+
b == null || b.kind == null ? skipBadBlock(i) : /* @__PURE__ */ jsx(
|
|
99
|
+
BlockView,
|
|
100
|
+
{
|
|
101
|
+
block: b,
|
|
102
|
+
components: comps,
|
|
103
|
+
virtualize,
|
|
104
|
+
sanitize,
|
|
105
|
+
childMemo,
|
|
106
|
+
onRenderMetrics: onMetrics,
|
|
107
|
+
decorators,
|
|
108
|
+
urlTransform,
|
|
109
|
+
componentKeys,
|
|
110
|
+
onBlockError
|
|
111
|
+
},
|
|
112
|
+
b.id
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
),
|
|
95
116
|
stickToBottom && /* @__PURE__ */ jsx("div", { "aria-hidden": "true", style: { scrollSnapAlign: "end" }, className: "brook-bottom-anchor" })
|
|
96
117
|
]
|
|
97
118
|
}
|
|
@@ -466,7 +487,8 @@ function renderBlockContent({
|
|
|
466
487
|
decorators,
|
|
467
488
|
urlTransform
|
|
468
489
|
}) {
|
|
469
|
-
const kind = block
|
|
490
|
+
const kind = block?.kind?.type;
|
|
491
|
+
if (kind === void 0) return null;
|
|
470
492
|
const hasInlineTransforms = !!decorators || !!urlTransform;
|
|
471
493
|
if (components) {
|
|
472
494
|
if (kind === "Component") {
|
|
@@ -545,14 +567,79 @@ function renderBlockContent({
|
|
|
545
567
|
);
|
|
546
568
|
}
|
|
547
569
|
function blocksEqual(prev, next) {
|
|
570
|
+
if (prev.block == null || next.block == null) return prev.block === next.block;
|
|
548
571
|
return prev.block.id === next.block.id && prev.block.html === next.block.html && prev.block.open === next.block.open && prev.block.speculative === next.block.speculative && prev.components === next.components && prev.virtualize === next.virtualize && prev.sanitize === next.sanitize && prev.childMemo === next.childMemo && prev.onRenderMetrics === next.onRenderMetrics && // Identity compare: an unstable decorators/urlTransform (fresh each render)
|
|
549
572
|
// busts the memo so every committed block re-decorates — the O(n²) footgun
|
|
550
573
|
// the dev warning calls out. A hoisted/memoized value keeps the memo holding.
|
|
551
|
-
prev.decorators === next.decorators && prev.urlTransform === next.urlTransform
|
|
574
|
+
prev.decorators === next.decorators && prev.urlTransform === next.urlTransform && // Same identity rule as onRenderMetrics: an inline `onBlockError={() => …}`
|
|
575
|
+
// is a fresh closure per render and would re-render every block on every
|
|
576
|
+
// patch. Hoist or memoize it (documented alongside the other hooks).
|
|
577
|
+
prev.onBlockError === next.onBlockError && prev.componentKeys === next.componentKeys;
|
|
578
|
+
}
|
|
579
|
+
let boundaryRenders = 0;
|
|
580
|
+
function __getBoundaryRenders() {
|
|
581
|
+
return boundaryRenders;
|
|
582
|
+
}
|
|
583
|
+
function __resetBoundaryRenders() {
|
|
584
|
+
boundaryRenders = 0;
|
|
585
|
+
}
|
|
586
|
+
function BlockViewOuter(props) {
|
|
587
|
+
const { block, componentKeys, onBlockError } = props;
|
|
588
|
+
return /* @__PURE__ */ jsx(
|
|
589
|
+
BlockBoundary,
|
|
590
|
+
{
|
|
591
|
+
blockId: block.id,
|
|
592
|
+
kind: block.kind.type,
|
|
593
|
+
html: block.html,
|
|
594
|
+
componentKeys: componentKeys ?? EMPTY_KEYS,
|
|
595
|
+
onBlockError,
|
|
596
|
+
children: /* @__PURE__ */ jsx(BlockViewImpl, { ...props })
|
|
597
|
+
}
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
const BlockView = memo(BlockViewOuter, blocksEqual);
|
|
601
|
+
class BlockBoundary extends Component {
|
|
602
|
+
state = { caught: false, failedHtml: null };
|
|
603
|
+
static getDerivedStateFromError() {
|
|
604
|
+
return { caught: true };
|
|
605
|
+
}
|
|
606
|
+
/** Retry once the block's HTML moves on. A streaming-tail failure — a
|
|
607
|
+
* speculatively-closed tag, a prop that is only transiently absent — then
|
|
608
|
+
* heals itself when the block settles, instead of leaving a hole for the rest
|
|
609
|
+
* of the session. */
|
|
610
|
+
static getDerivedStateFromProps(props, state) {
|
|
611
|
+
if (state.caught && state.failedHtml !== null && state.failedHtml !== props.html) {
|
|
612
|
+
return { caught: false, failedHtml: null };
|
|
613
|
+
}
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
componentDidCatch(error) {
|
|
617
|
+
const info = {
|
|
618
|
+
blockId: this.props.blockId,
|
|
619
|
+
kind: this.props.kind,
|
|
620
|
+
componentKeys: this.props.componentKeys,
|
|
621
|
+
html: this.props.html.slice(0, 200)
|
|
622
|
+
};
|
|
623
|
+
this.setState({ failedHtml: this.props.html });
|
|
624
|
+
if (this.props.onBlockError) {
|
|
625
|
+
this.props.onBlockError(error, info);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
console.error(
|
|
629
|
+
`brookmd: block ${info.blockId} (${info.kind}) failed to render and was skipped. This is almost always a \`components\` override throwing. If the override is registered for a block-kind or component-tag key, note that the SAME key is also dispatched for a matching element nested inside a block's HTML \u2014 that call gets attributes + children only, with no \`block\` prop. Guard with \`if (!block) return <>{children}</>\`.`,
|
|
630
|
+
{ componentKeys: info.componentKeys, html: info.html },
|
|
631
|
+
error
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
render() {
|
|
635
|
+
boundaryRenders++;
|
|
636
|
+
return this.state.caught ? null : this.props.children;
|
|
637
|
+
}
|
|
552
638
|
}
|
|
553
|
-
const BlockView = memo(BlockViewImpl, blocksEqual);
|
|
554
639
|
export {
|
|
555
640
|
BrookMarkdown,
|
|
641
|
+
__getBoundaryRenders,
|
|
642
|
+
__resetBoundaryRenders,
|
|
556
643
|
__resetUnstableWarnings,
|
|
557
644
|
blockKindProps,
|
|
558
645
|
blocksEqual,
|
package/dist/server-react.js
CHANGED
|
@@ -3,7 +3,8 @@ import { htmlToReact } from "./html-to-react.js";
|
|
|
3
3
|
import { blockKindProps } from "./react.js";
|
|
4
4
|
import { parseToBlocks } from "./server.js";
|
|
5
5
|
function renderStaticBlock(block, components) {
|
|
6
|
-
const kind = block
|
|
6
|
+
const kind = block?.kind?.type;
|
|
7
|
+
if (kind === void 0) return null;
|
|
7
8
|
if (components) {
|
|
8
9
|
if (kind === "Component") {
|
|
9
10
|
const tag = block.kind.data?.tag;
|
package/dist/types-react.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ComponentType } from "react";
|
|
2
|
+
import type { BlockComponentProps, BlockKindTag } from "./types-core.js";
|
|
2
3
|
/**
|
|
3
4
|
* Override map for {@link BrookMarkdown}. Keys are either lowercase HTML tag
|
|
4
5
|
* names (`table`, `a`, `code`, `h1`… — react-markdown style, applied inside a
|
|
@@ -6,8 +7,35 @@ import type { ComponentType } from "react";
|
|
|
6
7
|
* `CodeBlock`, `Table` — replace the whole block renderer). Values are a React
|
|
7
8
|
* component or an HTML tag string.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* ## Two prop contracts — read this before writing an override
|
|
11
|
+
*
|
|
12
|
+
* The same map is consulted by two dispatchers, and they pass different props:
|
|
13
|
+
*
|
|
14
|
+
* - **Block contract.** A block-kind key, or a `componentTags` tag matched at
|
|
15
|
+
* block level, receives {@link BlockComponentProps} — `block`, `html`, `open`,
|
|
16
|
+
* `speculative` (plus `tag`/`attrs`/`children` for component tags).
|
|
17
|
+
* - **Tag contract.** The SAME key is also matched by *element name* while
|
|
18
|
+
* converting a block's HTML to React — which is how `a`/`code`/`table`
|
|
19
|
+
* overrides work, and also how an `inlineComponentTags` chip, or a component
|
|
20
|
+
* tag nested inside a list item / blockquote, is rendered. That call passes
|
|
21
|
+
* the element's attributes and `children` only: **there is no `block` prop.**
|
|
22
|
+
*
|
|
23
|
+
* So a component registered for a tag that can appear in both positions must not
|
|
24
|
+
* assume `block` exists:
|
|
25
|
+
*
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const Thinking = ({ block, children }: any) =>
|
|
28
|
+
* block ? <Panel data={block.kind.data}>{children}</Panel> : <span>{children}</span>;
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* Block-kind keys are typed to {@link BlockComponentProps} below so the mismatch
|
|
32
|
+
* is a compile error rather than a runtime `undefined` deref; brookmd also
|
|
33
|
+
* refuses to dispatch a raw element whose name collides with a block-kind key,
|
|
34
|
+
* and wraps every block in an error boundary so a throwing override costs one
|
|
35
|
+
* block instead of the document.
|
|
12
36
|
*/
|
|
13
|
-
export type Components =
|
|
37
|
+
export type Components = {
|
|
38
|
+
[K in BlockKindTag]?: ComponentType<BlockComponentProps> | string;
|
|
39
|
+
} & {
|
|
40
|
+
[tag: string]: ComponentType<any> | string | undefined;
|
|
41
|
+
};
|
package/dist/warn.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** True unless the bundler/runtime says NODE_ENV === "production". Read off
|
|
2
|
+
* `globalThis` so there is no @types/node dependency; bundlers inline
|
|
3
|
+
* `process.env.NODE_ENV`, and absence is treated as dev (same rule as the
|
|
4
|
+
* unstable-prop tripwire in react.tsx). */
|
|
5
|
+
export declare function isDev(): boolean;
|
|
6
|
+
/** Warn once per `id` (dev only). Returns true if it actually warned. */
|
|
7
|
+
export declare function warnOnce(id: string, message: string): boolean;
|
|
8
|
+
/** Test-only: clear the latch so a test can assert a warning fires. */
|
|
9
|
+
export declare function __resetWarnOnce(): void;
|
package/dist/warn.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const warned = /* @__PURE__ */ new Set();
|
|
2
|
+
function isDev() {
|
|
3
|
+
const env = globalThis.process?.env;
|
|
4
|
+
return !env || env.NODE_ENV !== "production";
|
|
5
|
+
}
|
|
6
|
+
function warnOnce(id, message) {
|
|
7
|
+
if (!isDev() || warned.has(id)) return false;
|
|
8
|
+
warned.add(id);
|
|
9
|
+
console.warn(message);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
function __resetWarnOnce() {
|
|
13
|
+
warned.clear();
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
__resetWarnOnce,
|
|
17
|
+
isDev,
|
|
18
|
+
warnOnce
|
|
19
|
+
};
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brookmd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Zero-dep streaming markdown for the browser. Rust→WASM core, Web Worker per stream, incremental parse with speculative closure.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": ["./dist/worker.js", "./dist/styles.css"],
|