flux-md 0.16.2 → 0.17.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/README.md +70 -39
- package/dist/block-props.d.ts +18 -0
- package/dist/block-props.js +75 -0
- package/dist/client.d.ts +310 -0
- package/{src/client.ts → dist/client.js} +123 -319
- package/dist/dom.d.ts +110 -0
- package/dist/dom.js +576 -0
- package/dist/element.d.ts +20 -0
- package/dist/element.js +287 -0
- package/dist/hi.d.ts +12 -0
- package/{src/hi.ts → dist/hi.js} +42 -74
- package/dist/html-to-react.d.ts +40 -0
- package/dist/html-to-react.js +344 -0
- package/{src/index.ts → dist/index.d.ts} +5 -25
- package/dist/index.js +16 -0
- package/dist/morph.d.ts +28 -0
- package/dist/morph.js +166 -0
- package/dist/react.d.ts +204 -0
- package/dist/react.js +490 -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.d.ts +61 -0
- package/dist/server.js +126 -0
- package/{src/solid.tsx → dist/solid.d.ts} +19 -95
- package/dist/solid.js +54 -0
- package/dist/svelte.d.ts +80 -0
- package/dist/svelte.js +59 -0
- package/dist/types-core.d.ts +377 -0
- package/dist/types-core.js +0 -0
- package/{src/types-react.ts → dist/types-react.d.ts} +0 -1
- package/dist/types-react.js +0 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +2 -0
- package/dist/vue.d.ts +94 -0
- package/dist/vue.js +79 -0
- package/{src → dist}/wasm/flux_md_core.d.ts +18 -6
- package/{src → dist}/wasm/flux_md_core.js +50 -55
- package/dist/wasm/flux_md_core_bg.wasm +0 -0
- package/{src → dist}/wasm/flux_md_core_bg.wasm.d.ts +1 -0
- package/dist/worker-core.d.ts +60 -0
- package/dist/worker-core.js +121 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +48 -0
- package/package.json +22 -18
- package/src/block-props.ts +0 -141
- package/src/dom.ts +0 -946
- package/src/element.ts +0 -400
- package/src/html-to-react.ts +0 -455
- package/src/morph.ts +0 -253
- package/src/react.tsx +0 -1020
- package/src/renderers/CodeBlock.tsx +0 -116
- package/src/renderers/Math.tsx +0 -28
- package/src/renderers/Mermaid.tsx +0 -27
- package/src/server.tsx +0 -221
- package/src/svelte.ts +0 -179
- package/src/types-core.ts +0 -398
- package/src/types.ts +0 -7
- package/src/vue.ts +0 -184
- package/src/wasm/flux_md_core_bg.wasm +0 -0
- package/src/worker-core.ts +0 -174
- package/src/worker.ts +0 -72
- /package/{src → dist}/styles.css +0 -0
|
@@ -13,31 +13,48 @@ export class FluxParser {
|
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* All blocks currently parsed (committed + active), in document order — the
|
|
16
|
-
* whole rendered document as a
|
|
17
|
-
* server-side render primitive: feed the full
|
|
18
|
-
* `finalize`, then read `allBlocks()` (no worker,
|
|
19
|
-
*
|
|
16
|
+
* whole rendered document as a **JSON string** of a `Block[]` (parse with
|
|
17
|
+
* `JSON.parse`). The one-shot / server-side render primitive: feed the full
|
|
18
|
+
* markdown via `append`, call `finalize`, then read `allBlocks()` (no worker,
|
|
19
|
+
* no patch accumulation).
|
|
20
|
+
* @returns {string}
|
|
20
21
|
*/
|
|
21
22
|
allBlocks() {
|
|
23
|
+
let deferred2_0;
|
|
24
|
+
let deferred2_1;
|
|
22
25
|
try {
|
|
23
26
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
24
27
|
wasm.fluxparser_allBlocks(retptr, this.__wbg_ptr);
|
|
25
28
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
26
29
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
27
30
|
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
|
|
32
|
+
var ptr1 = r0;
|
|
33
|
+
var len1 = r1;
|
|
34
|
+
if (r3) {
|
|
35
|
+
ptr1 = 0; len1 = 0;
|
|
36
|
+
throw takeObject(r2);
|
|
30
37
|
}
|
|
31
|
-
|
|
38
|
+
deferred2_0 = ptr1;
|
|
39
|
+
deferred2_1 = len1;
|
|
40
|
+
return getStringFromWasm0(ptr1, len1);
|
|
32
41
|
} finally {
|
|
33
42
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
43
|
+
wasm.__wbindgen_export3(deferred2_0, deferred2_1, 1);
|
|
34
44
|
}
|
|
35
45
|
}
|
|
36
46
|
/**
|
|
47
|
+
* Returns the patch as a **JSON string** (parsed with `JSON.parse` on the
|
|
48
|
+
* main thread), not a live JS object. This is deliberate: serializing once to
|
|
49
|
+
* a string in Rust avoids serde-wasm-bindgen's per-node boundary calls, and a
|
|
50
|
+
* string is far cheaper than an object graph to `structuredClone` across the
|
|
51
|
+
* worker→main `postMessage` (the worker forwards the string verbatim).
|
|
37
52
|
* @param {string} chunk
|
|
38
|
-
* @returns {
|
|
53
|
+
* @returns {string}
|
|
39
54
|
*/
|
|
40
55
|
append(chunk) {
|
|
56
|
+
let deferred3_0;
|
|
57
|
+
let deferred3_1;
|
|
41
58
|
try {
|
|
42
59
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
43
60
|
const ptr0 = passStringToWasm0(chunk, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
@@ -46,12 +63,19 @@ export class FluxParser {
|
|
|
46
63
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
47
64
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
48
65
|
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
|
|
67
|
+
var ptr2 = r0;
|
|
68
|
+
var len2 = r1;
|
|
69
|
+
if (r3) {
|
|
70
|
+
ptr2 = 0; len2 = 0;
|
|
71
|
+
throw takeObject(r2);
|
|
51
72
|
}
|
|
52
|
-
|
|
73
|
+
deferred3_0 = ptr2;
|
|
74
|
+
deferred3_1 = len2;
|
|
75
|
+
return getStringFromWasm0(ptr2, len2);
|
|
53
76
|
} finally {
|
|
54
77
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
78
|
+
wasm.__wbindgen_export3(deferred3_0, deferred3_1, 1);
|
|
55
79
|
}
|
|
56
80
|
}
|
|
57
81
|
/**
|
|
@@ -62,21 +86,31 @@ export class FluxParser {
|
|
|
62
86
|
return ret >>> 0;
|
|
63
87
|
}
|
|
64
88
|
/**
|
|
65
|
-
*
|
|
89
|
+
* JSON-string patch — see [`FluxParser::append`].
|
|
90
|
+
* @returns {string}
|
|
66
91
|
*/
|
|
67
92
|
finalize() {
|
|
93
|
+
let deferred2_0;
|
|
94
|
+
let deferred2_1;
|
|
68
95
|
try {
|
|
69
96
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
70
97
|
wasm.fluxparser_finalize(retptr, this.__wbg_ptr);
|
|
71
98
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
72
99
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
73
100
|
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
|
|
74
|
-
|
|
75
|
-
|
|
101
|
+
var r3 = getDataViewMemory0().getInt32(retptr + 4 * 3, true);
|
|
102
|
+
var ptr1 = r0;
|
|
103
|
+
var len1 = r1;
|
|
104
|
+
if (r3) {
|
|
105
|
+
ptr1 = 0; len1 = 0;
|
|
106
|
+
throw takeObject(r2);
|
|
76
107
|
}
|
|
77
|
-
|
|
108
|
+
deferred2_0 = ptr1;
|
|
109
|
+
deferred2_1 = len1;
|
|
110
|
+
return getStringFromWasm0(ptr1, len1);
|
|
78
111
|
} finally {
|
|
79
112
|
wasm.__wbindgen_add_to_stack_pointer(16);
|
|
113
|
+
wasm.__wbindgen_export3(deferred2_0, deferred2_1, 1);
|
|
80
114
|
}
|
|
81
115
|
}
|
|
82
116
|
constructor() {
|
|
@@ -215,17 +249,6 @@ if (Symbol.dispose) FluxParser.prototype[Symbol.dispose] = FluxParser.prototype.
|
|
|
215
249
|
function __wbg_get_imports() {
|
|
216
250
|
const import0 = {
|
|
217
251
|
__proto__: null,
|
|
218
|
-
__wbg_Error_ef53bc310eb298a0: function(arg0, arg1) {
|
|
219
|
-
const ret = Error(getStringFromWasm0(arg0, arg1));
|
|
220
|
-
return addHeapObject(ret);
|
|
221
|
-
},
|
|
222
|
-
__wbg_String_8564e559799eccda: function(arg0, arg1) {
|
|
223
|
-
const ret = String(getObject(arg1));
|
|
224
|
-
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
225
|
-
const len1 = WASM_VECTOR_LEN;
|
|
226
|
-
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
227
|
-
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
228
|
-
},
|
|
229
252
|
__wbg___wbindgen_string_get_72bdf95d3ae505b1: function(arg0, arg1) {
|
|
230
253
|
const obj = getObject(arg1);
|
|
231
254
|
const ret = typeof(obj) === 'string' ? obj : undefined;
|
|
@@ -237,39 +260,11 @@ function __wbg_get_imports() {
|
|
|
237
260
|
__wbg___wbindgen_throw_1506f2235d1bdba0: function(arg0, arg1) {
|
|
238
261
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
239
262
|
},
|
|
240
|
-
|
|
241
|
-
const ret = new Object();
|
|
242
|
-
return addHeapObject(ret);
|
|
243
|
-
},
|
|
244
|
-
__wbg_new_d90091b82fdf5b91: function() {
|
|
245
|
-
const ret = new Array();
|
|
246
|
-
return addHeapObject(ret);
|
|
247
|
-
},
|
|
248
|
-
__wbg_set_6be42768c690e380: function(arg0, arg1, arg2) {
|
|
249
|
-
getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
|
|
250
|
-
},
|
|
251
|
-
__wbg_set_dca99999bba88a9a: function(arg0, arg1, arg2) {
|
|
252
|
-
getObject(arg0)[arg1 >>> 0] = takeObject(arg2);
|
|
253
|
-
},
|
|
254
|
-
__wbindgen_cast_0000000000000001: function(arg0) {
|
|
255
|
-
// Cast intrinsic for `F64 -> Externref`.
|
|
256
|
-
const ret = arg0;
|
|
257
|
-
return addHeapObject(ret);
|
|
258
|
-
},
|
|
259
|
-
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
|
263
|
+
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
|
260
264
|
// Cast intrinsic for `Ref(String) -> Externref`.
|
|
261
265
|
const ret = getStringFromWasm0(arg0, arg1);
|
|
262
266
|
return addHeapObject(ret);
|
|
263
267
|
},
|
|
264
|
-
__wbindgen_cast_0000000000000003: function(arg0) {
|
|
265
|
-
// Cast intrinsic for `U64 -> Externref`.
|
|
266
|
-
const ret = BigInt.asUintN(64, arg0);
|
|
267
|
-
return addHeapObject(ret);
|
|
268
|
-
},
|
|
269
|
-
__wbindgen_object_clone_ref: function(arg0) {
|
|
270
|
-
const ret = getObject(arg0);
|
|
271
|
-
return addHeapObject(ret);
|
|
272
|
-
},
|
|
273
268
|
__wbindgen_object_drop_ref: function(arg0) {
|
|
274
269
|
takeObject(arg0);
|
|
275
270
|
},
|
|
Binary file
|
|
@@ -22,3 +22,4 @@ export const fluxparser_setUnsafeHtml: (a: number, b: number) => void;
|
|
|
22
22
|
export const __wbindgen_export: (a: number, b: number) => number;
|
|
23
23
|
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
24
24
|
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
|
|
25
|
+
export const __wbindgen_export3: (a: number, b: number, c: number) => void;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { FromWorker, ParserConfig, ToWorker } from "./types-core.js";
|
|
2
|
+
/** The slice of `FluxParser` the worker drives — narrowed to an interface so the
|
|
3
|
+
* message/readiness state machine is unit-testable with a fake parser, no WASM.
|
|
4
|
+
* (Same testability move as {@link FluxPool} taking an injected worker factory.)
|
|
5
|
+
*
|
|
6
|
+
* `append`/`finalize` return the patch as a **JSON string**, not an object: the
|
|
7
|
+
* worker forwards it verbatim to the main thread (a string structuredClones far
|
|
8
|
+
* cheaper than an object graph) where it is `JSON.parse`d exactly once. */
|
|
9
|
+
export interface ParserLike {
|
|
10
|
+
append(chunk: string): string;
|
|
11
|
+
finalize(): string;
|
|
12
|
+
free(): void;
|
|
13
|
+
retainedBytes(): number;
|
|
14
|
+
}
|
|
15
|
+
/** Dependencies injected into {@link WorkerCore}, isolating it from the worker
|
|
16
|
+
* globals (`self`, `queueMicrotask`) and the WASM module so it can be tested. */
|
|
17
|
+
export interface WorkerCoreDeps {
|
|
18
|
+
/** Create + configure a parser for a stream (prod: `new FluxParser()` + setX). */
|
|
19
|
+
makeParser(config: ParserConfig | undefined): ParserLike;
|
|
20
|
+
/** Post a message to the main thread (prod: `self.postMessage`). */
|
|
21
|
+
post(msg: FromWorker): void;
|
|
22
|
+
/** Current WASM heap size in bytes, reported on each patch (0 if unknown). */
|
|
23
|
+
memBytes(): number;
|
|
24
|
+
/** Defer a flush to a future microtask (prod: `queueMicrotask`). */
|
|
25
|
+
schedule(fn: () => void): void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The worker's message reducer + WASM-readiness gate, extracted from the worker
|
|
29
|
+
* shell so its trickiest invariant is testable without a real Worker or WASM.
|
|
30
|
+
*
|
|
31
|
+
* **The invariant:** WASM `init()` is async, and the client does NOT wait for
|
|
32
|
+
* readiness before appending — so chunks can arrive first. A parser must never
|
|
33
|
+
* be constructed before init resolves (`new FluxParser()` against an
|
|
34
|
+
* uninitialized module throws `fluxparser_new of undefined` and silently drops
|
|
35
|
+
* that chunk). So while `ready` is false, appends only accumulate in `pending`
|
|
36
|
+
* (scheduleFlush is a no-op) and `finalize` is deferred; {@link markReady}
|
|
37
|
+
* drains both — appends first (creating each parser + applying buffered text),
|
|
38
|
+
* then any deferred finalize — once init has completed.
|
|
39
|
+
*/
|
|
40
|
+
export declare class WorkerCore {
|
|
41
|
+
private deps;
|
|
42
|
+
private parsers;
|
|
43
|
+
private config;
|
|
44
|
+
private pending;
|
|
45
|
+
private totalAppended;
|
|
46
|
+
private finalizePending;
|
|
47
|
+
private flushScheduled;
|
|
48
|
+
private ready;
|
|
49
|
+
constructor(deps: WorkerCoreDeps);
|
|
50
|
+
/** Handle one message from the main thread (append/finalize/reset/dispose). */
|
|
51
|
+
handle(msg: ToWorker): void;
|
|
52
|
+
/** Called once WASM init resolves: open the gate and drain what was buffered. */
|
|
53
|
+
markReady(): void;
|
|
54
|
+
private getOrCreate;
|
|
55
|
+
private dispose;
|
|
56
|
+
private emitPatch;
|
|
57
|
+
private scheduleFlush;
|
|
58
|
+
private flush;
|
|
59
|
+
private doFinalize;
|
|
60
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
flushScheduled = false;
|
|
13
|
+
ready = false;
|
|
14
|
+
/** Handle one message from the main thread (append/finalize/reset/dispose). */
|
|
15
|
+
handle(msg) {
|
|
16
|
+
const id = msg.streamId;
|
|
17
|
+
if ((msg.type === "append" || msg.type === "finalize") && msg.config) {
|
|
18
|
+
this.config.set(id, msg.config);
|
|
19
|
+
}
|
|
20
|
+
switch (msg.type) {
|
|
21
|
+
case "append":
|
|
22
|
+
this.pending.set(id, (this.pending.get(id) ?? "") + msg.chunk);
|
|
23
|
+
this.scheduleFlush();
|
|
24
|
+
break;
|
|
25
|
+
case "finalize":
|
|
26
|
+
if (!this.ready) this.finalizePending.add(id);
|
|
27
|
+
else this.doFinalize(id);
|
|
28
|
+
break;
|
|
29
|
+
case "reset":
|
|
30
|
+
this.parsers.get(id)?.free();
|
|
31
|
+
this.parsers.delete(id);
|
|
32
|
+
this.pending.delete(id);
|
|
33
|
+
this.finalizePending.delete(id);
|
|
34
|
+
this.totalAppended.delete(id);
|
|
35
|
+
break;
|
|
36
|
+
case "dispose":
|
|
37
|
+
this.dispose(id);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Called once WASM init resolves: open the gate and drain what was buffered. */
|
|
42
|
+
markReady() {
|
|
43
|
+
this.ready = true;
|
|
44
|
+
this.deps.post({ type: "ready" });
|
|
45
|
+
if (this.pending.size > 0) this.flush();
|
|
46
|
+
if (this.finalizePending.size > 0) {
|
|
47
|
+
for (const id of this.finalizePending) this.doFinalize(id);
|
|
48
|
+
this.finalizePending.clear();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
getOrCreate(streamId) {
|
|
52
|
+
let p = this.parsers.get(streamId);
|
|
53
|
+
if (!p) {
|
|
54
|
+
p = this.deps.makeParser(this.config.get(streamId));
|
|
55
|
+
this.parsers.set(streamId, p);
|
|
56
|
+
}
|
|
57
|
+
return p;
|
|
58
|
+
}
|
|
59
|
+
dispose(streamId) {
|
|
60
|
+
this.parsers.get(streamId)?.free();
|
|
61
|
+
this.parsers.delete(streamId);
|
|
62
|
+
this.config.delete(streamId);
|
|
63
|
+
this.pending.delete(streamId);
|
|
64
|
+
this.finalizePending.delete(streamId);
|
|
65
|
+
this.totalAppended.delete(streamId);
|
|
66
|
+
}
|
|
67
|
+
emitPatch(streamId, patch, parser, parseMicros) {
|
|
68
|
+
this.deps.post({
|
|
69
|
+
type: "patch",
|
|
70
|
+
streamId,
|
|
71
|
+
patch,
|
|
72
|
+
appendedBytes: this.totalAppended.get(streamId) ?? 0,
|
|
73
|
+
parseMicros,
|
|
74
|
+
retainedBytes: parser.retainedBytes(),
|
|
75
|
+
wasmMemoryBytes: this.deps.memBytes()
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
scheduleFlush() {
|
|
79
|
+
if (this.flushScheduled || !this.ready) return;
|
|
80
|
+
this.flushScheduled = true;
|
|
81
|
+
this.deps.schedule(() => this.flush());
|
|
82
|
+
}
|
|
83
|
+
flush() {
|
|
84
|
+
this.flushScheduled = false;
|
|
85
|
+
if (!this.ready || this.pending.size === 0) return;
|
|
86
|
+
for (const [streamId, chunk] of this.pending) {
|
|
87
|
+
this.pending.delete(streamId);
|
|
88
|
+
if (chunk.length === 0) continue;
|
|
89
|
+
const t0 = performance.now();
|
|
90
|
+
try {
|
|
91
|
+
const parser = this.getOrCreate(streamId);
|
|
92
|
+
const patch = parser.append(chunk);
|
|
93
|
+
const dt = (performance.now() - t0) * 1e3;
|
|
94
|
+
this.totalAppended.set(streamId, (this.totalAppended.get(streamId) ?? 0) + chunk.length);
|
|
95
|
+
this.emitPatch(streamId, patch, parser, dt);
|
|
96
|
+
} catch (e) {
|
|
97
|
+
this.deps.post({ type: "error", streamId, message: e instanceof Error ? e.message : String(e) });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Drain a stream's buffered input (if any), then finalize its parser. Shared by
|
|
102
|
+
// the `finalize` message path and markReady()'s post-ready drain.
|
|
103
|
+
doFinalize(streamId) {
|
|
104
|
+
const buffered = this.pending.get(streamId);
|
|
105
|
+
this.pending.delete(streamId);
|
|
106
|
+
try {
|
|
107
|
+
const parser = this.getOrCreate(streamId);
|
|
108
|
+
if (buffered && buffered.length > 0) {
|
|
109
|
+
parser.append(buffered);
|
|
110
|
+
this.totalAppended.set(streamId, (this.totalAppended.get(streamId) ?? 0) + buffered.length);
|
|
111
|
+
}
|
|
112
|
+
const patch = parser.finalize();
|
|
113
|
+
this.emitPatch(streamId, patch, parser, 0);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
this.deps.post({ type: "error", streamId, message: e instanceof Error ? e.message : String(e) });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
WorkerCore
|
|
121
|
+
};
|
package/dist/worker.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/worker.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import init, { FluxParser } from "./wasm/flux_md_core.js";
|
|
2
|
+
import { WorkerCore } from "./worker-core.js";
|
|
3
|
+
const wasmUrl = new URL("./wasm/flux_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 FluxParser();
|
|
12
|
+
p.setGfmAutolinks(c?.gfmAutolinks ?? true);
|
|
13
|
+
p.setGfmAlerts(c?.gfmAlerts ?? true);
|
|
14
|
+
p.setGfmFootnotes(c?.gfmFootnotes ?? false);
|
|
15
|
+
p.setGfmMath(c?.gfmMath ?? false);
|
|
16
|
+
p.setDirAuto(c?.dirAuto ?? false);
|
|
17
|
+
p.setA11y(c?.a11y ?? false);
|
|
18
|
+
p.setUnsafeHtml(c?.unsafeHtml ?? false);
|
|
19
|
+
p.setComponentTags(c?.componentTags ?? []);
|
|
20
|
+
p.setInlineComponentTags(c?.inlineComponentTags ?? []);
|
|
21
|
+
p.setHtmlSanitize(
|
|
22
|
+
c?.htmlAllowlist !== void 0 || c?.dropHtmlTags !== void 0,
|
|
23
|
+
c?.htmlAllowlist ?? [],
|
|
24
|
+
c?.dropHtmlTags ?? []
|
|
25
|
+
);
|
|
26
|
+
p.setBlockData(c?.blockData ?? false);
|
|
27
|
+
return p;
|
|
28
|
+
},
|
|
29
|
+
post: (msg) => ctx.postMessage(msg),
|
|
30
|
+
memBytes: () => {
|
|
31
|
+
try {
|
|
32
|
+
return wasmExports?.memory?.buffer?.byteLength ?? 0;
|
|
33
|
+
} catch {
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
schedule: (fn) => queueMicrotask(fn)
|
|
38
|
+
});
|
|
39
|
+
ctx.addEventListener("message", (ev) => core.handle(ev.data));
|
|
40
|
+
async function setup() {
|
|
41
|
+
try {
|
|
42
|
+
wasmExports = await init({ module_or_path: wasmUrl });
|
|
43
|
+
core.markReady();
|
|
44
|
+
} catch (e) {
|
|
45
|
+
ctx.postMessage({ type: "error", streamId: -1, message: e instanceof Error ? e.message : String(e), fatal: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
setup();
|
package/package.json
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flux-md",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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
|
-
"sideEffects": ["./
|
|
7
|
-
"main": "./
|
|
8
|
-
"types": "./
|
|
6
|
+
"sideEffects": ["./dist/worker.js", "./dist/styles.css"],
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
|
-
".": "./
|
|
11
|
-
"./client": "./
|
|
12
|
-
"./react": "./
|
|
13
|
-
"./server": "./
|
|
14
|
-
"./dom": "./
|
|
15
|
-
"./element": "./
|
|
16
|
-
"./vue": "./
|
|
17
|
-
"./svelte": "./
|
|
18
|
-
"./solid": "./
|
|
19
|
-
"./highlight": "./
|
|
20
|
-
"./types": "./
|
|
21
|
-
"./styles.css": "./
|
|
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
|
+
"./dom": { "types": "./dist/dom.d.ts", "import": "./dist/dom.js" },
|
|
15
|
+
"./element": { "types": "./dist/element.d.ts", "import": "./dist/element.js" },
|
|
16
|
+
"./vue": { "types": "./dist/vue.d.ts", "import": "./dist/vue.js" },
|
|
17
|
+
"./svelte": { "types": "./dist/svelte.d.ts", "import": "./dist/svelte.js" },
|
|
18
|
+
"./solid": { "types": "./dist/solid.d.ts", "import": "./dist/solid.js" },
|
|
19
|
+
"./highlight": { "types": "./dist/hi.d.ts", "import": "./dist/hi.js" },
|
|
20
|
+
"./types": { "types": "./dist/types.d.ts", "import": "./dist/types.js" },
|
|
21
|
+
"./styles.css": "./dist/styles.css",
|
|
22
|
+
"./package.json": "./package.json"
|
|
22
23
|
},
|
|
23
24
|
"files": [
|
|
24
|
-
"
|
|
25
|
+
"dist",
|
|
25
26
|
"README.md"
|
|
26
27
|
],
|
|
27
28
|
"peerDependencies": {
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"@types/bun": "^1.3.14",
|
|
49
50
|
"@types/react": "^18.3.12",
|
|
50
51
|
"@types/react-dom": "^18.3.1",
|
|
52
|
+
"esbuild": "^0.28.1",
|
|
51
53
|
"happy-dom": "^15.11.6",
|
|
52
54
|
"react": "^18.3.1",
|
|
53
55
|
"react-dom": "^18.3.1",
|
|
@@ -57,10 +59,12 @@
|
|
|
57
59
|
"vue": "^3.4.0"
|
|
58
60
|
},
|
|
59
61
|
"scripts": {
|
|
62
|
+
"build": "node scripts/build.mjs",
|
|
60
63
|
"test": "bun test",
|
|
61
64
|
"test:ssr-cold": "bun test/ssr-cold.mjs",
|
|
65
|
+
"test:consumer-smoke": "bash test/consumer-smoke/run.sh",
|
|
62
66
|
"typecheck:test": "tsc --noEmit -p tsconfig.test.json",
|
|
63
|
-
"prepublishOnly": "cd ../.. && bun run build:wasm"
|
|
67
|
+
"prepublishOnly": "cd ../.. && bun run build:wasm && cd packages/flux-md && node scripts/build.mjs"
|
|
64
68
|
},
|
|
65
69
|
"keywords": ["markdown", "streaming", "wasm", "rust", "react", "vue", "svelte", "solid", "web-component", "custom-element", "incremental", "llm", "ai", "math", "katex", "latex", "bidi", "rtl"],
|
|
66
70
|
"license": "MIT",
|
package/src/block-props.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Block,
|
|
3
|
-
BlockComponentProps,
|
|
4
|
-
CodeBlockData,
|
|
5
|
-
ContainerData,
|
|
6
|
-
HeadingData,
|
|
7
|
-
ListData,
|
|
8
|
-
MathBlockData,
|
|
9
|
-
TableData,
|
|
10
|
-
} from "./types-core";
|
|
11
|
-
|
|
12
|
-
// Pure helpers duplicated from the JSX renderer / its CodeBlock so the
|
|
13
|
-
// framework-neutral DOM renderer carries no framework dependency. The JSX
|
|
14
|
-
// renderer is held byte-identical, so these are copies — match it exactly.
|
|
15
|
-
|
|
16
|
-
/** Decode the small entity set the core emits (amp last so `&lt;` → `<`).
|
|
17
|
-
* This is the simple ordered chain, not the numeric/named-entity decoder. */
|
|
18
|
-
function decodeEntities(s: string): string {
|
|
19
|
-
return s
|
|
20
|
-
.replace(/</g, "<")
|
|
21
|
-
.replace(/>/g, ">")
|
|
22
|
-
.replace(/"/g, '"')
|
|
23
|
-
.replace(/'/g, "'")
|
|
24
|
-
.replace(/&/g, "&");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Decoded source text inside `<pre><code>…</code></pre>`. */
|
|
28
|
-
function decodeCodeText(html: string): string {
|
|
29
|
-
const m = html.match(/<pre><code[^>]*>([\s\S]*?)<\/code><\/pre>/);
|
|
30
|
-
return m ? decodeEntities(m[1]) : "";
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* The LaTeX source for a MathBlock. Display math (`$$…$$` / `\[…\]`) renders as
|
|
35
|
-
* `<div class="math math-display">…</div>`; a fenced ```math block renders as
|
|
36
|
-
* `<pre><code>…</code></pre>`. Either way the body is the HTML-escaped LaTeX —
|
|
37
|
-
* decode it back so a `components.MathBlock` override gets the raw source.
|
|
38
|
-
*/
|
|
39
|
-
function decodeMathText(html: string): string {
|
|
40
|
-
const d = html.match(/<div class="math math-display">([\s\S]*?)<\/div>/);
|
|
41
|
-
if (d) return decodeEntities(d[1]);
|
|
42
|
-
return decodeCodeText(html);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** Info-string language from a code block's `data-lang="…"`. */
|
|
46
|
-
export function extractLang(html: string): string {
|
|
47
|
-
const m = html.match(/data-lang="([^"]+)"/);
|
|
48
|
-
return m ? m[1] : "";
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Strip the `<tag …>` open and trailing `</tag>` from a component block's HTML,
|
|
52
|
-
* leaving the inner (already-rendered markdown) HTML. Handles open (unclosed)
|
|
53
|
-
* blocks, where there is no close tag yet. */
|
|
54
|
-
function componentInnerHtml(html: string, tag: string): string {
|
|
55
|
-
const gt = html.indexOf(">");
|
|
56
|
-
if (gt < 0) return "";
|
|
57
|
-
let inner = html.slice(gt + 1);
|
|
58
|
-
const close = `</${tag}>`;
|
|
59
|
-
if (inner.endsWith(close)) inner = inner.slice(0, -close.length);
|
|
60
|
-
return inner.replace(/^\n/, "").replace(/\n$/, "");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Convert sanitized HTML attribute pairs into a spreadable object, keeping the
|
|
65
|
-
* HTML-form names (`class`, `for`) verbatim. This is the deliberate divergence
|
|
66
|
-
* from the JSX renderer (which renames to `className`/`htmlFor` for a prop
|
|
67
|
-
* spread): the DOM renderer applies them via `el.setAttribute(name, value)`,
|
|
68
|
-
* which wants the literal HTML names.
|
|
69
|
-
*/
|
|
70
|
-
export function htmlAttrs(pairs: [string, string][]): Record<string, string> {
|
|
71
|
-
const out: Record<string, string> = {};
|
|
72
|
-
for (const [k, v] of pairs) out[k] = v;
|
|
73
|
-
return out;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Build the props a block-kind / component-tag override receives — the same
|
|
78
|
-
* shape the JSX renderer's block-kind props carry, with ONE deliberate
|
|
79
|
-
* divergence: for `Component` blocks `attrs` stay in HTML form (`class`/`for`)
|
|
80
|
-
* because DOM overrides apply them via `setAttribute` (see {@link htmlAttrs}).
|
|
81
|
-
*/
|
|
82
|
-
export function blockProps(block: Block): BlockComponentProps {
|
|
83
|
-
const props: BlockComponentProps = {
|
|
84
|
-
block,
|
|
85
|
-
html: block.html,
|
|
86
|
-
open: block.open,
|
|
87
|
-
speculative: block.speculative,
|
|
88
|
-
};
|
|
89
|
-
const data = block.kind.data as
|
|
90
|
-
| { lang?: string | null; code?: string; latex?: string; start?: number; ordered?: boolean; items?: { html: string }[]; tag?: string; attrs?: [string, string][] }
|
|
91
|
-
| undefined;
|
|
92
|
-
if (block.kind.type === "CodeBlock") {
|
|
93
|
-
// Prefer the structured `code` (present when blockData is on) over the HTML
|
|
94
|
-
// regex — it is the lossless decoded source. Fall back to the regex when off.
|
|
95
|
-
props.text = data?.code ?? decodeCodeText(block.html);
|
|
96
|
-
props.language = data?.lang ?? "";
|
|
97
|
-
// Surface the typed convenience field only when the opt-in `code` is present.
|
|
98
|
-
if (typeof data?.code === "string") {
|
|
99
|
-
props.code = { lang: data.lang ?? null, code: data.code } as CodeBlockData;
|
|
100
|
-
}
|
|
101
|
-
} else if (block.kind.type === "MathBlock") {
|
|
102
|
-
// Prefer the structured `latex` (present when blockData is on) over the regex.
|
|
103
|
-
props.text = data?.latex ?? decodeMathText(block.html);
|
|
104
|
-
if (typeof data?.latex === "string") {
|
|
105
|
-
props.math = { latex: data.latex } as MathBlockData;
|
|
106
|
-
}
|
|
107
|
-
} else if (block.kind.type === "List") {
|
|
108
|
-
// Structured list data is present only when blockData is on (the `start` key
|
|
109
|
-
// rides the opt-in channel); surface it as the typed convenience field,
|
|
110
|
-
// including the opt-in per-item HTML (`items`) for a keyed renderer.
|
|
111
|
-
if (data && typeof data.start === "number") {
|
|
112
|
-
props.list = { ordered: !!data.ordered, start: data.start, items: data.items } as ListData;
|
|
113
|
-
}
|
|
114
|
-
} else if (block.kind.type === "Component") {
|
|
115
|
-
props.tag = data?.tag ?? "";
|
|
116
|
-
props.attrs = htmlAttrs(data?.attrs ?? []);
|
|
117
|
-
// An override replaces the `<tag>` wrapper, so it gets the *inner* HTML
|
|
118
|
-
// (markdown already rendered) rather than the full wrapped block.
|
|
119
|
-
props.html = componentInnerHtml(block.html, props.tag);
|
|
120
|
-
} else if (block.kind.type === "Table") {
|
|
121
|
-
// Structured data is present only when `blockData` is on (else `undefined`).
|
|
122
|
-
// Pure data — identical for the DOM and JSX renderers, no name-form divergence.
|
|
123
|
-
props.table = block.kind.data as TableData | undefined;
|
|
124
|
-
} else if (block.kind.type === "Heading") {
|
|
125
|
-
// When `blockData` is on, `kind.data` is the `{ level, text, id }` object;
|
|
126
|
-
// when off it is the bare level `number`. Surface the rich object only — the
|
|
127
|
-
// `typeof === "object"` guard keeps the off-path naked int out of `heading`.
|
|
128
|
-
if (typeof block.kind.data === "object" && block.kind.data !== null) {
|
|
129
|
-
props.heading = block.kind.data as HeadingData;
|
|
130
|
-
}
|
|
131
|
-
} else if (block.kind.type === "Blockquote" || block.kind.type === "Alert") {
|
|
132
|
-
// When `blockData` is on, a Blockquote's `kind.data` is `{ nested }` and an
|
|
133
|
-
// Alert's is `{ kind, nested }`; off, a Blockquote has no `data` and an Alert
|
|
134
|
-
// only `{ kind }`. Surface the keyed `nested` sub-blocks only when present.
|
|
135
|
-
const cd = block.kind.data as { nested?: { html: string }[] } | undefined;
|
|
136
|
-
if (cd && Array.isArray(cd.nested)) {
|
|
137
|
-
props.container = { nested: cd.nested } as ContainerData;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return props;
|
|
141
|
-
}
|