brookmd 0.27.0 → 0.29.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/dist/dom.js CHANGED
@@ -1,9 +1,19 @@
1
1
  import { highlightDeferred } from "./hi-defer.js";
2
2
  import { createInc, incHighlight, incSeed } from "./hi-inc.js";
3
3
  import { morph } from "./morph.js";
4
+ import { newIncCode, paintIncCode, spliceHtml, spliceKeep } from "./splice.js";
4
5
  import { blockProps, extractLang } from "./block-props.js";
5
6
  import { decorateSegments } from "./decorate.js";
6
7
  import { safeUrl } from "./url-safety.js";
8
+ let keyedAttempts = 0;
9
+ let keyedHits = 0;
10
+ function __keyedStats() {
11
+ return { attempts: keyedAttempts, hits: keyedHits };
12
+ }
13
+ function __resetKeyedStats() {
14
+ keyedAttempts = 0;
15
+ keyedHits = 0;
16
+ }
7
17
  const INTRINSIC_PX = {
8
18
  Paragraph: 80,
9
19
  Heading: 44,
@@ -32,6 +42,7 @@ function mountBrookMarkdown(client, container, options = {}) {
32
42
  const streamingHighlight = options.streamingHighlight !== false;
33
43
  const batch = options.batch !== false && typeof requestAnimationFrame === "function";
34
44
  const morphOpenBlocks = options.morphOpenBlocks === true;
45
+ const fullRebuild = options.__fullRebuild === true;
35
46
  const root = document.createElement("div");
36
47
  root.className = options.className ? `brook-md ${options.className}` : "brook-md";
37
48
  if (options.id) root.id = options.id;
@@ -70,6 +81,7 @@ function mountBrookMarkdown(client, container, options = {}) {
70
81
  id: b.id,
71
82
  node: void 0,
72
83
  html: b.html,
84
+ block: b,
73
85
  open: b.open,
74
86
  speculative: b.speculative,
75
87
  kind: b.kind.type,
@@ -87,6 +99,13 @@ function mountBrookMarkdown(client, container, options = {}) {
87
99
  continue;
88
100
  }
89
101
  const t0 = onRenderMetrics && hasPerf ? performance.now() : 0;
102
+ if (existing.codeInc && existing.inc && b.open && existing.open && b.kind.type === "CodeBlock" && existing.kind === "CodeBlock" && syncIncCode(existing, b)) {
103
+ if (onRenderMetrics) noteRender(existing, b, t0);
104
+ existing.html = b.html;
105
+ existing.block = b;
106
+ existing.speculative = b.speculative;
107
+ continue;
108
+ }
90
109
  if (existing.table && b.open && b.kind.type === "Table") {
91
110
  const data = tableData(b);
92
111
  if (data) {
@@ -96,15 +115,38 @@ function mountBrookMarkdown(client, container, options = {}) {
96
115
  existing.node.classList.toggle("brook-speculative", b.speculative);
97
116
  }
98
117
  existing.html = b.html;
118
+ existing.block = b;
99
119
  existing.open = b.open;
100
120
  existing.speculative = b.speculative;
101
121
  continue;
102
122
  }
103
123
  }
124
+ if (!fullRebuild && existing.list && b.open && b.kind.type === "List" && existing.kind === "List") {
125
+ const ld = b.kind.data;
126
+ if (ld && syncKeyedList(existing.list, ld)) {
127
+ if (onRenderMetrics) noteRender(existing, b, t0);
128
+ existing.node.className = genericClassName(b);
129
+ existing.html = b.html;
130
+ existing.block = b;
131
+ existing.open = b.open;
132
+ existing.speculative = b.speculative;
133
+ continue;
134
+ }
135
+ }
136
+ if (!fullRebuild && existing.container && b.open && existing.kind === b.kind.type && (b.kind.type === "Blockquote" || b.kind.type === "Alert") && syncKeyedContainer(existing.container, b)) {
137
+ if (onRenderMetrics) noteRender(existing, b, t0);
138
+ existing.node.className = genericClassName(b);
139
+ existing.html = b.html;
140
+ existing.block = b;
141
+ existing.open = b.open;
142
+ existing.speculative = b.speculative;
143
+ continue;
144
+ }
104
145
  if (morphOpenBlocks && b.open && existing.open && existing.generic && existing.kind === b.kind.type && usesGenericPath(b)) {
105
146
  morph(existing.node, sanitize ? sanitize(b.html) : b.html);
106
147
  if (onRenderMetrics) noteRender(existing, b, t0);
107
148
  existing.html = b.html;
149
+ existing.block = b;
108
150
  existing.speculative = b.speculative;
109
151
  existing.node.className = genericClassName(b);
110
152
  existing.generic = !sanitize;
@@ -114,9 +156,35 @@ function mountBrookMarkdown(client, container, options = {}) {
114
156
  existing.node.insertAdjacentHTML("beforeend", b.html.slice(existing.html.length));
115
157
  if (onRenderMetrics) noteRender(existing, b, t0);
116
158
  existing.html = b.html;
159
+ existing.block = b;
117
160
  continue;
118
161
  }
162
+ if (!fullRebuild && !sanitize && !hasInlineTransforms && existing.generic && b.open && existing.open && existing.kind === b.kind.type && usesGenericPath(b)) {
163
+ const keep = spliceKeep(existing.block, b);
164
+ if (keep !== void 0 && spliceHtml(existing.node, existing.html, b.html, keep)) {
165
+ if (onRenderMetrics) noteRender(existing, b, t0);
166
+ existing.html = b.html;
167
+ existing.block = b;
168
+ existing.speculative = b.speculative;
169
+ existing.node.className = genericClassName(b);
170
+ continue;
171
+ }
172
+ }
173
+ if (!fullRebuild && existing.plainCode && b.open && existing.open && b.kind.type === "CodeBlock" && existing.kind === "CodeBlock") {
174
+ const keep = spliceKeep(existing.block, b);
175
+ if (keep !== void 0 && spliceHtml(existing.plainCode, existing.html, b.html, keep)) {
176
+ if (onRenderMetrics) noteRender(existing, b, t0);
177
+ existing.html = b.html;
178
+ existing.block = b;
179
+ existing.speculative = b.speculative;
180
+ continue;
181
+ }
182
+ }
119
183
  existing.table = void 0;
184
+ existing.list = void 0;
185
+ existing.container = void 0;
186
+ existing.codeInc = void 0;
187
+ existing.plainCode = void 0;
120
188
  if (existing.inc && b.kind.type !== "CodeBlock") existing.inc = void 0;
121
189
  if (existing.highlight) {
122
190
  existing.highlight.cancel();
@@ -127,6 +195,7 @@ function mountBrookMarkdown(client, container, options = {}) {
127
195
  existing.node = node;
128
196
  if (onRenderMetrics) noteRender(existing, b, t0);
129
197
  existing.html = b.html;
198
+ existing.block = b;
130
199
  existing.open = b.open;
131
200
  existing.speculative = b.speculative;
132
201
  existing.kind = b.kind.type;
@@ -137,6 +206,8 @@ function mountBrookMarkdown(client, container, options = {}) {
137
206
  if (!seen.has(id)) {
138
207
  if (mb.highlight) mb.highlight.cancel();
139
208
  mb.inc = void 0;
209
+ mb.codeInc = void 0;
210
+ mb.plainCode = void 0;
140
211
  mb.node.remove();
141
212
  mounted.delete(id);
142
213
  }
@@ -206,13 +277,13 @@ function mountBrookMarkdown(client, container, options = {}) {
206
277
  if (data) return buildKeyedTable(b, data, mb);
207
278
  }
208
279
  if (b.open && kind === "List" && !hasInlineTransforms) {
209
- const keyed = renderKeyedList(b);
280
+ const keyed = renderKeyedList(b, mb);
210
281
  if (keyed) return keyed;
211
282
  }
212
283
  const node = document.createElement("div");
213
284
  node.className = genericClassName(b);
214
285
  if (b.open && !sanitize && !hasInlineTransforms && (kind === "Blockquote" || kind === "Alert")) {
215
- const wrapper = renderKeyedContainer(b);
286
+ const wrapper = renderKeyedContainer(b, mb);
216
287
  if (wrapper) {
217
288
  node.appendChild(wrapper);
218
289
  return node;
@@ -224,47 +295,122 @@ function mountBrookMarkdown(client, container, options = {}) {
224
295
  lastRenderGeneric = !sanitize && !hasInlineTransforms;
225
296
  return node;
226
297
  }
227
- function renderKeyedList(b) {
298
+ function renderKeyedList(b, mb) {
228
299
  const ld = b.kind.data;
229
300
  const items = ld?.items;
230
301
  if (!Array.isArray(items) || items.length === 0) return null;
231
302
  const node = document.createElement("div");
232
- node.className = "brook-block brook-block-list" + (b.open ? " brook-open" : "") + (b.speculative ? " brook-speculative" : "");
233
- const list = document.createElement(ld?.ordered ? "ol" : "ul");
234
- if (ld?.ordered && ld.start !== void 0 && ld.start !== 1) {
303
+ node.className = genericClassName(b);
304
+ const ordered = !!ld?.ordered;
305
+ const list = document.createElement(ordered ? "ol" : "ul");
306
+ if (ordered && ld.start !== void 0 && ld.start !== 1) {
235
307
  list.setAttribute("start", String(ld.start));
236
308
  }
237
- for (const it of items) {
309
+ const rendered = new Array(items.length);
310
+ for (let i = 0; i < items.length; i++) {
238
311
  const li = document.createElement("li");
239
- li.innerHTML = sanitize ? sanitize(it.html) : it.html;
312
+ li.innerHTML = sanitize ? sanitize(items[i].html) : items[i].html;
240
313
  list.appendChild(li);
314
+ rendered[i] = items[i].html;
241
315
  }
242
316
  node.appendChild(list);
317
+ mb.list = { list, ordered, start: ld?.start, items: rendered };
243
318
  return node;
244
319
  }
245
- function renderKeyedContainer(b) {
320
+ function syncKeyedList(km, ld) {
321
+ keyedAttempts++;
322
+ const items = ld.items;
323
+ if (!Array.isArray(items) || items.length === 0) return false;
324
+ if (!!ld.ordered !== km.ordered) return false;
325
+ const cur = km.items;
326
+ const kids = km.list.children;
327
+ if (kids.length !== cur.length) return false;
328
+ if (ld.start !== km.start) {
329
+ if (km.ordered && ld.start !== void 0 && ld.start !== 1) {
330
+ km.list.setAttribute("start", String(ld.start));
331
+ } else {
332
+ km.list.removeAttribute("start");
333
+ }
334
+ km.start = ld.start;
335
+ }
336
+ const n = items.length;
337
+ while (cur.length > n) {
338
+ km.list.removeChild(km.list.lastElementChild);
339
+ cur.pop();
340
+ }
341
+ for (let i = 0; i < cur.length; i++) {
342
+ if (cur[i] === items[i].html) continue;
343
+ kids[i].innerHTML = sanitize ? sanitize(items[i].html) : items[i].html;
344
+ cur[i] = items[i].html;
345
+ }
346
+ for (let i = cur.length; i < n; i++) {
347
+ const li = document.createElement("li");
348
+ li.innerHTML = sanitize ? sanitize(items[i].html) : items[i].html;
349
+ km.list.appendChild(li);
350
+ cur.push(items[i].html);
351
+ }
352
+ keyedHits++;
353
+ return true;
354
+ }
355
+ function renderKeyedContainer(b, mb) {
246
356
  const nested = b.kind.data?.nested;
247
357
  if (!Array.isArray(nested)) return null;
248
358
  const tagName = b.kind.type === "Alert" ? "div" : "blockquote";
249
359
  const wrapper = document.createElement(tagName);
250
360
  applyOpenTagAttrs(wrapper, b.html);
361
+ let offset = 0;
362
+ let title = "";
251
363
  if (b.kind.type === "Alert") {
252
- const title = alertTitleHtml(b.html);
364
+ title = alertTitleHtml(b.html);
253
365
  if (title) {
254
366
  const t = document.createElement("div");
255
367
  t.innerHTML = title;
256
368
  const titleNode = t.firstElementChild;
257
- if (titleNode) wrapper.appendChild(titleNode);
369
+ if (titleNode) {
370
+ wrapper.appendChild(titleNode);
371
+ offset = 1;
372
+ }
258
373
  }
259
374
  }
375
+ const rendered = new Array(nested.length);
260
376
  for (let i = 0; i < nested.length; i++) {
261
- const child = document.createElement("div");
262
- child.innerHTML = nested[i].html;
263
- const inner = child.firstElementChild;
264
- wrapper.appendChild(inner ?? child);
377
+ wrapper.appendChild(nestedChild(nested[i].html));
378
+ rendered[i] = nested[i].html;
265
379
  }
380
+ mb.container = { wrapper, offset, openTag: openTagOf(b.html), title, nested: rendered };
266
381
  return wrapper;
267
382
  }
383
+ function nestedChild(html) {
384
+ const child = document.createElement("div");
385
+ child.innerHTML = html;
386
+ return child.firstElementChild ?? child;
387
+ }
388
+ function syncKeyedContainer(kc, b) {
389
+ keyedAttempts++;
390
+ const nested = b.kind.data?.nested;
391
+ if (!Array.isArray(nested)) return false;
392
+ if (openTagOf(b.html) !== kc.openTag) return false;
393
+ if (b.kind.type === "Alert" && alertTitleHtml(b.html) !== kc.title) return false;
394
+ const cur = kc.nested;
395
+ const kids = kc.wrapper.children;
396
+ if (kids.length !== kc.offset + cur.length) return false;
397
+ const n = nested.length;
398
+ while (cur.length > n) {
399
+ kc.wrapper.removeChild(kc.wrapper.lastElementChild);
400
+ cur.pop();
401
+ }
402
+ for (let i = 0; i < cur.length; i++) {
403
+ if (cur[i] === nested[i].html) continue;
404
+ kc.wrapper.replaceChild(nestedChild(nested[i].html), kids[kc.offset + i]);
405
+ cur[i] = nested[i].html;
406
+ }
407
+ for (let i = cur.length; i < n; i++) {
408
+ kc.wrapper.appendChild(nestedChild(nested[i].html));
409
+ cur.push(nested[i].html);
410
+ }
411
+ keyedHits++;
412
+ return true;
413
+ }
268
414
  function buildKeyedTable(b, data, mb) {
269
415
  const node = document.createElement("div");
270
416
  node.className = "brook-block brook-block-table brook-open" + (b.speculative ? " brook-speculative" : "");
@@ -352,6 +498,8 @@ function mountBrookMarkdown(client, container, options = {}) {
352
498
  }
353
499
  function renderCodeBlock(b, mb) {
354
500
  const lang = extractLang(b.html) || "text";
501
+ mb.codeInc = void 0;
502
+ mb.plainCode = void 0;
355
503
  const text = b.open ? "" : codeText(b);
356
504
  let openMarkup = null;
357
505
  if (b.open && streamingHighlight) {
@@ -386,7 +534,9 @@ function mountBrookMarkdown(client, container, options = {}) {
386
534
  const body = document.createElement("div");
387
535
  body.className = "brook-code-body";
388
536
  if (highlighted !== null) {
389
- body.appendChild(highlightedPre(lang, highlighted));
537
+ body.appendChild(
538
+ openMarkup !== null && mb.inc !== void 0 && !fullRebuild ? incPre(lang, mb, mb.inc, openMarkup) : highlightedPre(lang, highlighted)
539
+ );
390
540
  } else {
391
541
  const div = document.createElement("div");
392
542
  div.tabIndex = 0;
@@ -394,12 +544,14 @@ function mountBrookMarkdown(client, container, options = {}) {
394
544
  div.setAttribute("aria-label", `${lang} code`);
395
545
  div.innerHTML = b.html;
396
546
  body.appendChild(div);
547
+ if (b.open) mb.plainCode = div;
397
548
  if (run !== null && run.rest !== null) {
398
549
  mb.highlight = run;
399
550
  run.rest.then((markup) => {
400
551
  if (markup === null || dead) return;
401
552
  if (mb.highlight !== run || mb.node !== block) return;
402
553
  mb.highlight = void 0;
554
+ mb.plainCode = void 0;
403
555
  body.replaceChild(highlightedPre(lang, markup), div);
404
556
  });
405
557
  }
@@ -407,6 +559,25 @@ function mountBrookMarkdown(client, container, options = {}) {
407
559
  block.appendChild(body);
408
560
  return block;
409
561
  }
562
+ function syncIncCode(mb, b) {
563
+ const ic = mb.codeInc;
564
+ if ((extractLang(b.html) || "text") !== ic.lang) return false;
565
+ const markup = incHighlight(mb.inc, codeText(b));
566
+ if (markup === null) return false;
567
+ return paintIncCode(ic, mb.inc, markup);
568
+ }
569
+ function incPre(lang, mb, st, markup) {
570
+ const pre = document.createElement("pre");
571
+ pre.tabIndex = 0;
572
+ pre.setAttribute("role", "region");
573
+ pre.setAttribute("aria-label", `${lang} code`);
574
+ const code = document.createElement("code");
575
+ const ic = newIncCode(code, lang, st);
576
+ if (paintIncCode(ic, st, markup)) mb.codeInc = ic;
577
+ else code.innerHTML = markup;
578
+ pre.appendChild(code);
579
+ return pre;
580
+ }
410
581
  function highlightedPre(lang, markup) {
411
582
  const pre = document.createElement("pre");
412
583
  pre.tabIndex = 0;
@@ -515,6 +686,8 @@ function mountBrookMarkdown(client, container, options = {}) {
515
686
  mb.highlight = void 0;
516
687
  }
517
688
  mb.inc = void 0;
689
+ mb.codeInc = void 0;
690
+ mb.plainCode = void 0;
518
691
  }
519
692
  root.remove();
520
693
  },
@@ -608,9 +781,12 @@ function decodeCodeText(html) {
608
781
  return m[1].replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&");
609
782
  }
610
783
  const CONTAINER_ATTR_RE = /([a-zA-Z][a-zA-Z0-9-]*)="([^"]*)"/g;
611
- function applyOpenTagAttrs(el, html) {
784
+ function openTagOf(html) {
612
785
  const gt = html.indexOf(">");
613
- const open = gt < 0 ? html : html.slice(0, gt);
786
+ return gt < 0 ? html : html.slice(0, gt);
787
+ }
788
+ function applyOpenTagAttrs(el, html) {
789
+ const open = openTagOf(html);
614
790
  let m;
615
791
  CONTAINER_ATTR_RE.lastIndex = 0;
616
792
  while (m = CONTAINER_ATTR_RE.exec(open)) {
@@ -669,6 +845,8 @@ function applyUrlTransformDom(root, urlTransform) {
669
845
  });
670
846
  }
671
847
  export {
848
+ __keyedStats,
849
+ __resetKeyedStats,
672
850
  mountBrookMarkdown,
673
851
  tailOpenBlockId
674
852
  };
package/dist/element.js CHANGED
@@ -20,7 +20,8 @@ const CONFIG_ATTRS = [
20
20
  "soft-breaks",
21
21
  "a11y",
22
22
  "unsafe-html",
23
- "block-html"
23
+ "block-html",
24
+ "retain-committed-html"
24
25
  ];
25
26
  function defineBrookMarkdown(tag = "brook-markdown") {
26
27
  if (typeof customElements === "undefined") return;
@@ -162,6 +163,7 @@ function defineBrookMarkdown(tag = "brook-markdown") {
162
163
  set("a11y", "a11y");
163
164
  set("unsafe-html", "unsafeHtml");
164
165
  set("block-html", "blockHtml");
166
+ set("retain-committed-html", "retainCommittedHtml");
165
167
  const tags = this.getAttribute("component-tags");
166
168
  if (tags !== null) {
167
169
  const list = tags.split(/[\s,]+/).filter(Boolean);
package/dist/hi-inc.d.ts CHANGED
@@ -53,6 +53,31 @@ export interface IncState {
53
53
  c: number;
54
54
  /** Markup for `[0, c)`. Always a byte-prefix of the block's final markup. */
55
55
  frozenHtml: string;
56
+ /**
57
+ * Bumped whenever `frozenHtml` is TRUNCATED or cleared — the one checkpoint of
58
+ * rewind {@link adopt} performs, or a restart. It never moves while the prefix
59
+ * merely GROWS.
60
+ *
61
+ * A renderer that mirrors the frozen prefix into the DOM append-only reads it
62
+ * as the "may I splice?" token: same rev ⇒ the prefix only grew, so appending
63
+ * `frozenHtml.slice(alreadyWritten)` is exact; a changed rev means the prefix
64
+ * was rewritten underneath and the mirror must be re-seeded. Length alone is
65
+ * NOT enough — one call can rewind to `c0` and then re-freeze past the old
66
+ * length, which looks like a plain append but is not one.
67
+ */
68
+ frozenRev: number;
69
+ /**
70
+ * The length `frozenHtml` was TRUNCATED to at the last {@link frozenRev} bump:
71
+ * `frozenLen0` for {@link adopt}'s one-checkpoint rewind, `0` for a restart.
72
+ *
73
+ * The rev alone says "rewritten"; this says *from where*, which is what lets a
74
+ * DOM mirror rewind to a boundary it already holds instead of re-seeding the
75
+ * whole run. It is load-bearing that this is reported rather than inferred:
76
+ * `adopt` frequently rewinds and then re-freezes PAST the old length within
77
+ * the same call, so the observable `frozenHtml.length` can come back unchanged
78
+ * while its bytes have moved.
79
+ */
80
+ frozenCut: number;
56
81
  /** The checkpoint BEFORE `c`, and the `frozenHtml` length that went with it —
57
82
  * the one step of rewind a tail revision needs (see {@link adopt}). */
58
83
  c0: number;
package/dist/hi-inc.js CHANGED
@@ -72,6 +72,8 @@ function createInc(lang) {
72
72
  lang: key,
73
73
  c: 0,
74
74
  frozenHtml: "",
75
+ frozenRev: 0,
76
+ frozenCut: 0,
75
77
  c0: 0,
76
78
  frozenLen0: 0,
77
79
  opener: null,
@@ -97,6 +99,8 @@ function adopt(st, d) {
97
99
  }
98
100
  if (st.c0 > 0 && d >= st.c0 + GAP) {
99
101
  st.frozenHtml = st.frozenHtml.slice(0, st.frozenLen0);
102
+ st.frozenRev++;
103
+ st.frozenCut = st.frozenLen0;
100
104
  st.c = st.c0;
101
105
  st.c0 = 0;
102
106
  st.frozenLen0 = 0;
@@ -117,6 +121,8 @@ function dropTail(st) {
117
121
  function reset(st) {
118
122
  st.c = 0;
119
123
  st.frozenHtml = "";
124
+ st.frozenRev++;
125
+ st.frozenCut = 0;
120
126
  st.c0 = 0;
121
127
  st.frozenLen0 = 0;
122
128
  st.opener = null;
@@ -19,6 +19,8 @@ type HNode = {
19
19
  */
20
20
  export declare function parseStyle(css: string): Record<string, string>;
21
21
  export declare function getParseCount(): number;
22
+ /** @internal Test-only. Characters of markup handed to the tokenizer so far. */
23
+ export declare function getParseChars(): number;
22
24
  export declare function resetParseCount(): void;
23
25
  export declare function parseTrustedHtml(html: string): HNode[];
24
26
  /**
@@ -131,14 +131,20 @@ function parseOpenTag(html, start) {
131
131
  return { tag, attrs, selfClose: false, next: i };
132
132
  }
133
133
  let parseCount = 0;
134
+ let parseChars = 0;
134
135
  function getParseCount() {
135
136
  return parseCount;
136
137
  }
138
+ function getParseChars() {
139
+ return parseChars;
140
+ }
137
141
  function resetParseCount() {
138
142
  parseCount = 0;
143
+ parseChars = 0;
139
144
  }
140
145
  function parseTrustedHtml(html) {
141
146
  parseCount++;
147
+ parseChars += html.length;
142
148
  const root = [];
143
149
  const stack = [];
144
150
  let i = 0;
@@ -357,6 +363,7 @@ function wrapLink(text, attrs) {
357
363
  }
358
364
  export {
359
365
  decodeEntities,
366
+ getParseChars,
360
367
  getParseCount,
361
368
  htmlToReact,
362
369
  parseStyle,
package/dist/index.d.ts CHANGED
@@ -15,7 +15,8 @@
15
15
  * // ... wherever your tokens land: client.append(deltaText);
16
16
  * client.finalize();
17
17
  */
18
- export { BrookClient, BrookPool, getDefaultPool } from "./client.js";
18
+ export { BrookClient, BrookPool, getDefaultPool, sourceFingerprint } from "./client.js";
19
+ export type { PersistableSnapshot } from "./client.js";
19
20
  export { BrookMarkdown, useBrookStream, useBrookMarkdownString } from "./react.js";
20
21
  export { highlight, supportedLangs } from "./hi.js";
21
22
  export { htmlToReact, parseTrustedHtml, safeUrl, wrapLink } from "./html-to-react.js";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { BrookClient, BrookPool, getDefaultPool } from "./client.js";
1
+ import { BrookClient, BrookPool, getDefaultPool, sourceFingerprint } from "./client.js";
2
2
  import { BrookMarkdown, useBrookStream, useBrookMarkdownString } from "./react.js";
3
3
  import { highlight, supportedLangs } from "./hi.js";
4
4
  import { htmlToReact, parseTrustedHtml, safeUrl, wrapLink } from "./html-to-react.js";
@@ -11,6 +11,7 @@ export {
11
11
  htmlToReact,
12
12
  parseTrustedHtml,
13
13
  safeUrl,
14
+ sourceFingerprint,
14
15
  supportedLangs,
15
16
  useBrookMarkdownString,
16
17
  useBrookStream,
@@ -0,0 +1,32 @@
1
+ import { type MutableRefObject } from "react";
2
+ import type { Block } from "./types-core.js";
3
+ /**
4
+ * Let a layout effect own an element's children so an OPEN block's patch is
5
+ * applied incrementally (see splice.ts) instead of re-setting the whole
6
+ * `dangerouslySetInnerHTML` every time it grows.
7
+ *
8
+ * ## How React and the effect share the node
9
+ *
10
+ * The returned string is the html captured on the FIRST render and never
11
+ * changes again. Render it as the node's `__html` and React writes the element
12
+ * exactly once, at mount — its own `lastHtml !== nextHtml` check then keeps it
13
+ * from touching the children on any later commit, and the effect owns them from
14
+ * there.
15
+ *
16
+ * That is what makes this safe under concurrent rendering: a render that is
17
+ * thrown away commits nothing and runs no effect, and the effect's own
18
+ * bookkeeping makes a repeat run (StrictMode's double-invoke) a no-op. It also
19
+ * leaves SSR and hydration byte-identical, because the first markup React
20
+ * produces is still the block's full html.
21
+ *
22
+ * The caller hands the node BACK to React by rendering a different element
23
+ * (a closed block's plain `<div dangerouslySetInnerHTML>`), which remounts the
24
+ * subtree and re-renders the settled html in one pass.
25
+ *
26
+ * @param hostRef ref attached to the element whose children are managed
27
+ * @param block the block's CURRENT version (identity matters — `spliceKeep`
28
+ * is keyed on it)
29
+ * @param enabled false to stay entirely out of the way
30
+ * @returns the html to render as `__html`, or `null` when not managing
31
+ */
32
+ export declare function useHtmlSplice(hostRef: MutableRefObject<HTMLElement | null>, block: Block | undefined, enabled: boolean): string | null;
@@ -0,0 +1,33 @@
1
+ import { useEffect, useLayoutEffect, useRef } from "react";
2
+ import { spliceHtml, spliceKeep } from "./splice.js";
3
+ const useIsoLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect;
4
+ function useHtmlSplice(hostRef, block, enabled) {
5
+ const seed = useRef(block);
6
+ const applied = useRef(null);
7
+ useIsoLayoutEffect(() => {
8
+ const base = seed.current;
9
+ if (!enabled || block === void 0 || base === void 0) {
10
+ applied.current = null;
11
+ return;
12
+ }
13
+ const node = hostRef.current;
14
+ if (node === null) return;
15
+ let prev = applied.current;
16
+ if (prev === null || prev.node !== node) prev = { node, block: base };
17
+ if (prev.block === block) {
18
+ applied.current = prev;
19
+ return;
20
+ }
21
+ const keep = spliceKeep(prev.block, block);
22
+ if (keep !== void 0 && spliceHtml(node, prev.block.html, block.html, keep)) {
23
+ applied.current = { node, block };
24
+ return;
25
+ }
26
+ node.innerHTML = block.html;
27
+ applied.current = { node, block };
28
+ });
29
+ return enabled && seed.current !== void 0 ? seed.current.html : null;
30
+ }
31
+ export {
32
+ useHtmlSplice
33
+ };
package/dist/react.d.ts CHANGED
@@ -144,6 +144,15 @@ interface BrookMarkdownProps {
144
144
  * the block — an override bypasses the built-in highlighter entirely.
145
145
  */
146
146
  streamingHighlight?: boolean;
147
+ /**
148
+ * @internal TEST-ONLY. Turn off the incremental apply paths (the open code
149
+ * block's frozen/tail mirror and the open generic block's delta splice) so
150
+ * every patch re-renders the whole block through React, exactly as it did
151
+ * before they existed. The DOM-parity fuzz renders one tree with it on and one
152
+ * with it off and asserts their markup matches after every commit. Not part of
153
+ * the supported API.
154
+ */
155
+ __fullRebuild?: boolean;
147
156
  /** Appended to the root's `className` (the `brook-md` class is always present). */
148
157
  className?: string;
149
158
  /** Set on the root element. */
@@ -281,6 +290,8 @@ interface BlockViewProps {
281
290
  sanitize?: (html: string) => string;
282
291
  childMemo?: boolean;
283
292
  streamingHighlight?: boolean;
293
+ /** @internal TEST-ONLY — see BrookMarkdownProps.__fullRebuild. */
294
+ __fullRebuild?: boolean;
284
295
  onRenderMetrics?: RenderMetricsHook;
285
296
  decorators?: Decorator[];
286
297
  urlTransform?: UrlTransform;