brookmd 0.22.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 +1229 -0
- package/LICENSE +21 -0
- package/README.md +1265 -0
- package/dist/block-props.d.ts +18 -0
- package/dist/block-props.js +75 -0
- package/dist/client.d.ts +370 -0
- package/dist/client.js +754 -0
- package/dist/decorate.d.ts +24 -0
- package/dist/decorate.js +71 -0
- package/dist/dom.d.ts +130 -0
- package/dist/dom.js +627 -0
- package/dist/element.d.ts +20 -0
- package/dist/element.js +288 -0
- package/dist/hi.d.ts +12 -0
- package/dist/hi.js +215 -0
- package/dist/html-to-react.d.ts +61 -0
- package/dist/html-to-react.js +338 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +18 -0
- package/dist/morph.d.ts +28 -0
- package/dist/morph.js +166 -0
- package/dist/react.d.ts +236 -0
- package/dist/react.js +539 -0
- package/dist/renderers/CodeBlock.d.ts +7 -0
- package/dist/renderers/CodeBlock.js +75 -0
- package/dist/renderers/Math.d.ts +14 -0
- package/dist/renderers/Math.js +15 -0
- package/dist/renderers/Mermaid.d.ts +13 -0
- package/dist/renderers/Mermaid.js +15 -0
- package/dist/server-react.d.ts +32 -0
- package/dist/server-react.js +48 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.js +82 -0
- package/dist/solid.d.ts +104 -0
- package/dist/solid.js +54 -0
- package/dist/styles.css +188 -0
- package/dist/svelte.d.ts +80 -0
- package/dist/svelte.js +59 -0
- package/dist/types-core.d.ts +436 -0
- package/dist/types-core.js +0 -0
- package/dist/types-react.d.ts +13 -0
- package/dist/types-react.js +0 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -0
- package/dist/url-safety.d.ts +12 -0
- package/dist/url-safety.js +45 -0
- package/dist/vue.d.ts +94 -0
- package/dist/vue.js +79 -0
- package/dist/wasm/LICENSE +21 -0
- package/dist/wasm/README.md +71 -0
- package/dist/wasm/brook_md_core.d.ts +166 -0
- package/dist/wasm/brook_md_core.js +512 -0
- package/dist/wasm/brook_md_core_bg.wasm +0 -0
- package/dist/wasm/brook_md_core_bg.wasm.d.ts +26 -0
- package/dist/worker-core.d.ts +65 -0
- package/dist/worker-core.js +155 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +49 -0
- package/package.json +87 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
class WorkerCore {
|
|
2
|
+
constructor(deps) {
|
|
3
|
+
this.deps = deps;
|
|
4
|
+
}
|
|
5
|
+
deps;
|
|
6
|
+
// One parser per stream id; WASM is loaded once and shared by all of them.
|
|
7
|
+
parsers = /* @__PURE__ */ new Map();
|
|
8
|
+
config = /* @__PURE__ */ new Map();
|
|
9
|
+
pending = /* @__PURE__ */ new Map();
|
|
10
|
+
totalAppended = /* @__PURE__ */ new Map();
|
|
11
|
+
finalizePending = /* @__PURE__ */ new Set();
|
|
12
|
+
// Per-stream generation, echoed on emitted patches so the client can drop a
|
|
13
|
+
// patch produced before a reset (see FromWorker.epoch). A microtask flush
|
|
14
|
+
// drains `pending` between messages, so the epoch at emit time is always the
|
|
15
|
+
// epoch the buffered input was appended under.
|
|
16
|
+
epochs = /* @__PURE__ */ new Map();
|
|
17
|
+
flushScheduled = false;
|
|
18
|
+
ready = false;
|
|
19
|
+
/** Handle one message from the main thread (append/finalize/reset/dispose). */
|
|
20
|
+
handle(msg) {
|
|
21
|
+
const id = msg.streamId;
|
|
22
|
+
if ((msg.type === "append" || msg.type === "finalize") && msg.config) {
|
|
23
|
+
this.config.set(id, msg.config);
|
|
24
|
+
}
|
|
25
|
+
if (msg.type !== "dispose" && msg.epoch !== void 0) {
|
|
26
|
+
this.epochs.set(id, msg.epoch);
|
|
27
|
+
}
|
|
28
|
+
switch (msg.type) {
|
|
29
|
+
case "append":
|
|
30
|
+
this.pending.set(id, (this.pending.get(id) ?? "") + msg.chunk);
|
|
31
|
+
this.scheduleFlush();
|
|
32
|
+
break;
|
|
33
|
+
case "finalize":
|
|
34
|
+
if (!this.ready) this.finalizePending.add(id);
|
|
35
|
+
else this.doFinalize(id);
|
|
36
|
+
break;
|
|
37
|
+
case "reset":
|
|
38
|
+
this.safeFree(id);
|
|
39
|
+
this.parsers.delete(id);
|
|
40
|
+
this.pending.delete(id);
|
|
41
|
+
this.finalizePending.delete(id);
|
|
42
|
+
this.totalAppended.delete(id);
|
|
43
|
+
break;
|
|
44
|
+
case "dispose":
|
|
45
|
+
this.dispose(id);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Called once WASM init resolves: open the gate and drain what was buffered. */
|
|
50
|
+
markReady() {
|
|
51
|
+
this.ready = true;
|
|
52
|
+
this.deps.post({ type: "ready" });
|
|
53
|
+
if (this.pending.size > 0) this.flush();
|
|
54
|
+
if (this.finalizePending.size > 0) {
|
|
55
|
+
for (const id of this.finalizePending) this.doFinalize(id);
|
|
56
|
+
this.finalizePending.clear();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
getOrCreate(streamId) {
|
|
60
|
+
let p = this.parsers.get(streamId);
|
|
61
|
+
if (!p) {
|
|
62
|
+
p = this.deps.makeParser(this.config.get(streamId));
|
|
63
|
+
this.parsers.set(streamId, p);
|
|
64
|
+
}
|
|
65
|
+
return p;
|
|
66
|
+
}
|
|
67
|
+
dispose(streamId) {
|
|
68
|
+
this.safeFree(streamId);
|
|
69
|
+
this.parsers.delete(streamId);
|
|
70
|
+
this.config.delete(streamId);
|
|
71
|
+
this.pending.delete(streamId);
|
|
72
|
+
this.finalizePending.delete(streamId);
|
|
73
|
+
this.totalAppended.delete(streamId);
|
|
74
|
+
this.epochs.delete(streamId);
|
|
75
|
+
}
|
|
76
|
+
/** free() a stream's parser, swallowing a throw from a poisoned WASM instance
|
|
77
|
+
* so teardown can never escape the message loop. */
|
|
78
|
+
safeFree(streamId) {
|
|
79
|
+
try {
|
|
80
|
+
this.parsers.get(streamId)?.free();
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
emitPatch(streamId, patch, parser, parseMicros, final) {
|
|
85
|
+
this.deps.post({
|
|
86
|
+
type: "patch",
|
|
87
|
+
streamId,
|
|
88
|
+
patch,
|
|
89
|
+
appendedBytes: this.totalAppended.get(streamId) ?? 0,
|
|
90
|
+
parseMicros,
|
|
91
|
+
retainedBytes: parser.retainedBytes(),
|
|
92
|
+
wasmMemoryBytes: this.deps.memBytes(),
|
|
93
|
+
final,
|
|
94
|
+
epoch: this.epochs.get(streamId)
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
scheduleFlush() {
|
|
98
|
+
if (this.flushScheduled || !this.ready) return;
|
|
99
|
+
this.flushScheduled = true;
|
|
100
|
+
this.deps.schedule(() => this.flush());
|
|
101
|
+
}
|
|
102
|
+
flush() {
|
|
103
|
+
this.flushScheduled = false;
|
|
104
|
+
if (!this.ready || this.pending.size === 0) return;
|
|
105
|
+
for (const [streamId, chunk] of this.pending) {
|
|
106
|
+
this.pending.delete(streamId);
|
|
107
|
+
if (chunk.length === 0) continue;
|
|
108
|
+
const t0 = performance.now();
|
|
109
|
+
try {
|
|
110
|
+
const parser = this.getOrCreate(streamId);
|
|
111
|
+
const patch = parser.append(chunk);
|
|
112
|
+
const dt = (performance.now() - t0) * 1e3;
|
|
113
|
+
this.totalAppended.set(streamId, (this.totalAppended.get(streamId) ?? 0) + chunk.length);
|
|
114
|
+
this.emitPatch(streamId, patch, parser, dt, false);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
this.postParseError(streamId, e);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Drain a stream's buffered input (if any), then finalize its parser. Shared by
|
|
121
|
+
// the `finalize` message path and markReady()'s post-ready drain.
|
|
122
|
+
doFinalize(streamId) {
|
|
123
|
+
const buffered = this.pending.get(streamId);
|
|
124
|
+
this.pending.delete(streamId);
|
|
125
|
+
try {
|
|
126
|
+
const parser = this.getOrCreate(streamId);
|
|
127
|
+
if (buffered && buffered.length > 0) {
|
|
128
|
+
const drained = parser.append(buffered);
|
|
129
|
+
this.totalAppended.set(streamId, (this.totalAppended.get(streamId) ?? 0) + buffered.length);
|
|
130
|
+
this.emitPatch(streamId, drained, parser, 0, false);
|
|
131
|
+
}
|
|
132
|
+
const patch = parser.finalize();
|
|
133
|
+
this.emitPatch(streamId, patch, parser, 0, true);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
this.postParseError(streamId, e);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// A WASM trap (stack overflow / unreachable / OOM) throws a
|
|
139
|
+
// WebAssembly.RuntimeError and POISONS the singleton instance shared by every
|
|
140
|
+
// stream on this worker — so it is escalated to `fatal`, which the pool treats
|
|
141
|
+
// as a worker-level failure (eviction + terminate). A plain Error (e.g. a
|
|
142
|
+
// parser-construction failure) stays a recoverable per-stream error.
|
|
143
|
+
postParseError(streamId, e) {
|
|
144
|
+
const fatal = typeof WebAssembly !== "undefined" && typeof WebAssembly.RuntimeError === "function" && e instanceof WebAssembly.RuntimeError;
|
|
145
|
+
this.deps.post({
|
|
146
|
+
type: "error",
|
|
147
|
+
streamId,
|
|
148
|
+
message: e instanceof Error ? e.message : String(e),
|
|
149
|
+
fatal
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export {
|
|
154
|
+
WorkerCore
|
|
155
|
+
};
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import init, { BrookParser } from "./wasm/brook_md_core.js";
|
|
2
|
+
import { WorkerCore } from "./worker-core.js";
|
|
3
|
+
const wasmUrl = new URL("./wasm/brook_md_core_bg.wasm", import.meta.url);
|
|
4
|
+
const ctx = self;
|
|
5
|
+
let wasmExports = null;
|
|
6
|
+
const core = new WorkerCore({
|
|
7
|
+
// Create + configure a parser for a stream. Omitted config fields fall back to
|
|
8
|
+
// the library defaults — autolinks + alerts on (LLM output is full of bare
|
|
9
|
+
// URLs and `> [!NOTE]` blocks), raw HTML escaped, footnotes/math off.
|
|
10
|
+
makeParser(c) {
|
|
11
|
+
const p = new BrookParser();
|
|
12
|
+
p.setGfmAutolinks(c?.gfmAutolinks ?? true);
|
|
13
|
+
p.setGfmAlerts(c?.gfmAlerts ?? true);
|
|
14
|
+
p.setGfmTagfilter(c?.gfmTagfilter ?? false);
|
|
15
|
+
p.setGfmFootnotes(c?.gfmFootnotes ?? false);
|
|
16
|
+
p.setGfmMath(c?.gfmMath ?? false);
|
|
17
|
+
p.setDirAuto(c?.dirAuto ?? false);
|
|
18
|
+
p.setA11y(c?.a11y ?? false);
|
|
19
|
+
p.setUnsafeHtml(c?.unsafeHtml ?? false);
|
|
20
|
+
p.setComponentTags(c?.componentTags ?? []);
|
|
21
|
+
p.setInlineComponentTags(c?.inlineComponentTags ?? []);
|
|
22
|
+
p.setHtmlSanitize(
|
|
23
|
+
c?.htmlAllowlist !== void 0 || c?.dropHtmlTags !== void 0,
|
|
24
|
+
c?.htmlAllowlist ?? [],
|
|
25
|
+
c?.dropHtmlTags ?? []
|
|
26
|
+
);
|
|
27
|
+
p.setBlockData(c?.blockData ?? false);
|
|
28
|
+
return p;
|
|
29
|
+
},
|
|
30
|
+
post: (msg) => ctx.postMessage(msg),
|
|
31
|
+
memBytes: () => {
|
|
32
|
+
try {
|
|
33
|
+
return wasmExports?.memory?.buffer?.byteLength ?? 0;
|
|
34
|
+
} catch {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
schedule: (fn) => queueMicrotask(fn)
|
|
39
|
+
});
|
|
40
|
+
ctx.addEventListener("message", (ev) => core.handle(ev.data));
|
|
41
|
+
async function setup() {
|
|
42
|
+
try {
|
|
43
|
+
wasmExports = await init({ module_or_path: wasmUrl });
|
|
44
|
+
core.markReady();
|
|
45
|
+
} catch (e) {
|
|
46
|
+
ctx.postMessage({ type: "error", streamId: -1, message: e instanceof Error ? e.message : String(e), fatal: true });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
setup();
|
package/package.json
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "brookmd",
|
|
3
|
+
"version": "0.22.0",
|
|
4
|
+
"description": "Zero-dep streaming markdown for the browser. Rust→WASM core, Web Worker per stream, incremental parse with speculative closure.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": ["./dist/worker.js", "./dist/styles.css"],
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
11
|
+
"./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" },
|
|
12
|
+
"./react": { "types": "./dist/react.d.ts", "import": "./dist/react.js" },
|
|
13
|
+
"./server": { "types": "./dist/server.d.ts", "import": "./dist/server.js" },
|
|
14
|
+
"./server/react": { "types": "./dist/server-react.d.ts", "import": "./dist/server-react.js" },
|
|
15
|
+
"./dom": { "types": "./dist/dom.d.ts", "import": "./dist/dom.js" },
|
|
16
|
+
"./element": { "types": "./dist/element.d.ts", "import": "./dist/element.js" },
|
|
17
|
+
"./vue": { "types": "./dist/vue.d.ts", "import": "./dist/vue.js" },
|
|
18
|
+
"./svelte": { "types": "./dist/svelte.d.ts", "import": "./dist/svelte.js" },
|
|
19
|
+
"./solid": { "types": "./dist/solid.d.ts", "import": "./dist/solid.js" },
|
|
20
|
+
"./highlight": { "types": "./dist/hi.d.ts", "import": "./dist/hi.js" },
|
|
21
|
+
"./html-to-react": { "types": "./dist/html-to-react.d.ts", "import": "./dist/html-to-react.js" },
|
|
22
|
+
"./block-props": { "types": "./dist/block-props.d.ts", "import": "./dist/block-props.js" },
|
|
23
|
+
"./worker-core": { "types": "./dist/worker-core.d.ts", "import": "./dist/worker-core.js" },
|
|
24
|
+
"./types": { "types": "./dist/types.d.ts", "import": "./dist/types.js" },
|
|
25
|
+
"./styles.css": "./dist/styles.css",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"CHANGELOG.md"
|
|
32
|
+
],
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": ">=18",
|
|
35
|
+
"vue": ">=3",
|
|
36
|
+
"svelte": ">=4",
|
|
37
|
+
"solid-js": "^1.8.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependenciesMeta": {
|
|
40
|
+
"react": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
43
|
+
"vue": {
|
|
44
|
+
"optional": true
|
|
45
|
+
},
|
|
46
|
+
"svelte": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"solid-js": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/bun": "^1.3.14",
|
|
55
|
+
"@types/react": "^18.3.12",
|
|
56
|
+
"@types/react-dom": "^18.3.1",
|
|
57
|
+
"esbuild": "^0.28.1",
|
|
58
|
+
"happy-dom": "^15.11.6",
|
|
59
|
+
"react": "^18.3.1",
|
|
60
|
+
"react-dom": "^18.3.1",
|
|
61
|
+
"solid-js": "^1.8.0",
|
|
62
|
+
"svelte": "^4.2.0",
|
|
63
|
+
"typescript": "^5.6.3",
|
|
64
|
+
"vue": "^3.4.0"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "node scripts/build.mjs",
|
|
68
|
+
"test": "bun test",
|
|
69
|
+
"test:ssr-cold": "bun test/ssr-cold.mjs",
|
|
70
|
+
"test:consumer-smoke": "bash test/consumer-smoke/run.sh",
|
|
71
|
+
"typecheck:test": "tsc --noEmit -p tsconfig.test.json",
|
|
72
|
+
"prepublishOnly": "cd ../.. && bun run build:wasm && cd packages/brookmd && node scripts/build.mjs"
|
|
73
|
+
},
|
|
74
|
+
"keywords": ["markdown", "streaming", "wasm", "rust", "react", "vue", "svelte", "solid", "web-component", "custom-element", "incremental", "llm", "ai", "math", "katex", "latex", "bidi", "rtl"],
|
|
75
|
+
"license": "MIT",
|
|
76
|
+
"publishConfig": {
|
|
77
|
+
"access": "public"
|
|
78
|
+
},
|
|
79
|
+
"repository": {
|
|
80
|
+
"type": "git",
|
|
81
|
+
"url": "git+https://github.com/siinghd/brookmd.git"
|
|
82
|
+
},
|
|
83
|
+
"homepage": "https://md.hsingh.app",
|
|
84
|
+
"bugs": {
|
|
85
|
+
"url": "https://github.com/siinghd/brookmd/issues"
|
|
86
|
+
}
|
|
87
|
+
}
|