brookmd 0.23.2 → 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 +104 -0
- package/README.md +83 -3
- package/dist/client.d.ts +99 -2
- package/dist/client.js +287 -26
- package/dist/dom.js +4 -1
- package/dist/html-to-react.js +29 -1
- package/dist/react.d.ts +54 -12
- package/dist/react.js +129 -20
- package/dist/server-react.js +2 -1
- package/dist/types-core.d.ts +12 -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/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,29 +90,48 @@ 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
|
}
|
|
98
119
|
);
|
|
99
120
|
}
|
|
100
121
|
function useBrookStream(stream, options) {
|
|
101
|
-
const [client] = useState(() => new BrookClient({ config: options?.config, coalesce: true }));
|
|
102
122
|
const onErrorRef = useRef(options?.onError);
|
|
103
123
|
onErrorRef.current = options?.onError;
|
|
124
|
+
const [client] = useState(
|
|
125
|
+
() => new BrookClient({
|
|
126
|
+
config: options?.config,
|
|
127
|
+
coalesce: true,
|
|
128
|
+
onError: (err) => {
|
|
129
|
+
const e = Object.assign(new Error(err.message), { fatal: err.fatal });
|
|
130
|
+
if (onErrorRef.current) onErrorRef.current(e);
|
|
131
|
+
else console.error(e);
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
);
|
|
104
135
|
const prevStream = useRef(void 0);
|
|
105
136
|
useEffect(() => {
|
|
106
137
|
client.reattach();
|
|
@@ -123,7 +154,19 @@ function useBrookStream(stream, options) {
|
|
|
123
154
|
return client;
|
|
124
155
|
}
|
|
125
156
|
function useBrookMarkdownString(content, options) {
|
|
126
|
-
const
|
|
157
|
+
const onErrorRef = useRef(options?.onError);
|
|
158
|
+
onErrorRef.current = options?.onError;
|
|
159
|
+
const [client] = useState(
|
|
160
|
+
() => new BrookClient({
|
|
161
|
+
config: options?.config,
|
|
162
|
+
coalesce: true,
|
|
163
|
+
onError: (err) => {
|
|
164
|
+
const e = Object.assign(new Error(err.message), { fatal: err.fatal });
|
|
165
|
+
if (onErrorRef.current) onErrorRef.current(e);
|
|
166
|
+
else console.error(e);
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
);
|
|
127
170
|
useEffect(() => {
|
|
128
171
|
client.reattach();
|
|
129
172
|
return () => client.destroy();
|
|
@@ -444,7 +487,8 @@ function renderBlockContent({
|
|
|
444
487
|
decorators,
|
|
445
488
|
urlTransform
|
|
446
489
|
}) {
|
|
447
|
-
const kind = block
|
|
490
|
+
const kind = block?.kind?.type;
|
|
491
|
+
if (kind === void 0) return null;
|
|
448
492
|
const hasInlineTransforms = !!decorators || !!urlTransform;
|
|
449
493
|
if (components) {
|
|
450
494
|
if (kind === "Component") {
|
|
@@ -523,14 +567,79 @@ function renderBlockContent({
|
|
|
523
567
|
);
|
|
524
568
|
}
|
|
525
569
|
function blocksEqual(prev, next) {
|
|
570
|
+
if (prev.block == null || next.block == null) return prev.block === next.block;
|
|
526
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)
|
|
527
572
|
// busts the memo so every committed block re-decorates — the O(n²) footgun
|
|
528
573
|
// the dev warning calls out. A hoisted/memoized value keeps the memo holding.
|
|
529
|
-
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
|
+
}
|
|
530
638
|
}
|
|
531
|
-
const BlockView = memo(BlockViewImpl, blocksEqual);
|
|
532
639
|
export {
|
|
533
640
|
BrookMarkdown,
|
|
641
|
+
__getBoundaryRenders,
|
|
642
|
+
__resetBoundaryRenders,
|
|
534
643
|
__resetUnstableWarnings,
|
|
535
644
|
blockKindProps,
|
|
536
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-core.d.ts
CHANGED
|
@@ -450,8 +450,19 @@ export type FromWorker = {
|
|
|
450
450
|
*/
|
|
451
451
|
export interface WorkerLike {
|
|
452
452
|
postMessage(msg: ToWorker): void;
|
|
453
|
-
|
|
453
|
+
/**
|
|
454
|
+
* Structural superset of DOM `Worker.addEventListener` for the three channels
|
|
455
|
+
* the pool listens on: `message` (patches / ready / in-band errors — read via
|
|
456
|
+
* `ev.data`) plus the out-of-band failure channels `error` (a script that
|
|
457
|
+
* 404s or throws at load — `ev.message`) and `messageerror` (an
|
|
458
|
+
* undeserializable posted message). One widened signature keeps the unit-test
|
|
459
|
+
* fakes that declare only `"message"` compiling — method parameters are
|
|
460
|
+
* checked bivariantly — while letting the pool attach all three without a
|
|
461
|
+
* structural cast.
|
|
462
|
+
*/
|
|
463
|
+
addEventListener(type: "message" | "error" | "messageerror", listener: (ev: {
|
|
454
464
|
data: FromWorker;
|
|
465
|
+
message?: string;
|
|
455
466
|
}) => void): void;
|
|
456
467
|
terminate(): void;
|
|
457
468
|
}
|
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"],
|