flux-md 0.16.0 → 0.16.2
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/package.json +1 -1
- package/src/dom.ts +13 -2
- package/src/element.ts +7 -1
- package/src/html-to-react.ts +14 -6
- package/src/react.tsx +11 -5
- package/src/server.tsx +7 -1
- package/src/wasm/flux_md_core_bg.wasm +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flux-md",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
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": ["./src/worker.ts", "./src/styles.css"],
|
package/src/dom.ts
CHANGED
|
@@ -455,7 +455,7 @@ export function mountFluxMarkdown(
|
|
|
455
455
|
function renderKeyedList(b: Block): HTMLElement | null {
|
|
456
456
|
const ld = b.kind.data as ListData | undefined;
|
|
457
457
|
const items = ld?.items;
|
|
458
|
-
if (!items || items.length === 0) return null;
|
|
458
|
+
if (!Array.isArray(items) || items.length === 0) return null;
|
|
459
459
|
const node = document.createElement("div");
|
|
460
460
|
node.className =
|
|
461
461
|
"flux-block flux-block-list" +
|
|
@@ -803,7 +803,18 @@ export function mountFluxMarkdown(
|
|
|
803
803
|
function tableData(b: Block): TableData | undefined {
|
|
804
804
|
if (b.kind.type !== "Table") return undefined;
|
|
805
805
|
const data = b.kind.data as TableData | undefined;
|
|
806
|
-
|
|
806
|
+
// Validate the shapes the keyed path indexes (rows/aligns/headers) so a
|
|
807
|
+
// drifted/malformed blockData wire falls back to the full-HTML path instead
|
|
808
|
+
// of crashing makeRow/makeCell on `aligns[j]`/`headers.map` of undefined.
|
|
809
|
+
if (
|
|
810
|
+
!data ||
|
|
811
|
+
!Array.isArray(data.rows) ||
|
|
812
|
+
!Array.isArray(data.aligns) ||
|
|
813
|
+
!Array.isArray(data.headers)
|
|
814
|
+
) {
|
|
815
|
+
return undefined;
|
|
816
|
+
}
|
|
817
|
+
return data;
|
|
807
818
|
}
|
|
808
819
|
|
|
809
820
|
// HTML void elements: they self-terminate, so they never push element depth.
|
package/src/element.ts
CHANGED
|
@@ -385,7 +385,13 @@ export function defineFluxMarkdown(tag = "flux-markdown"): void {
|
|
|
385
385
|
// A supersede/disconnect aborts the fetch — intentional, not an error.
|
|
386
386
|
if (abort.signal.aborted || !current()) return;
|
|
387
387
|
// eslint-disable-next-line no-console
|
|
388
|
-
|
|
388
|
+
// Log only the message — `src` can carry a tokenized URL and `err` can be
|
|
389
|
+
// a large structured body; both would otherwise be shipped verbatim by any
|
|
390
|
+
// console-forwarding monitoring. (src is still on the element for debugging.)
|
|
391
|
+
console.error(
|
|
392
|
+
"<flux-markdown>: failed to stream src",
|
|
393
|
+
err instanceof Error ? err.message : String(err),
|
|
394
|
+
);
|
|
389
395
|
}
|
|
390
396
|
}
|
|
391
397
|
}
|
package/src/html-to-react.ts
CHANGED
|
@@ -149,11 +149,19 @@ function safeStyle(style: Record<string, string>): Record<string, string> {
|
|
|
149
149
|
return out;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// Single-char classifiers for parseOpenTag, hoisted to module scope so the
|
|
153
|
+
// scan loops don't re-evaluate a fresh regex literal per character on every
|
|
154
|
+
// block render (parseTrustedHtml runs this for every open streaming block).
|
|
155
|
+
const TAG_CHAR = /[a-zA-Z0-9-]/;
|
|
156
|
+
const WS_CHAR = /\s/;
|
|
157
|
+
const ATTR_NAME_END = /[\s=>/]/;
|
|
158
|
+
const UNQUOTED_VALUE_END = /[\s>]/;
|
|
159
|
+
|
|
152
160
|
/** Parse one opening tag starting at `start` (the `<`). */
|
|
153
161
|
function parseOpenTag(html: string, start: number) {
|
|
154
162
|
let i = start + 1;
|
|
155
163
|
let j = i;
|
|
156
|
-
while (j < html.length &&
|
|
164
|
+
while (j < html.length && TAG_CHAR.test(html[j])) j++;
|
|
157
165
|
// Preserve the tag's ORIGINAL case so an inline custom-component element (e.g.
|
|
158
166
|
// `<Cite>`) dispatches to `components.Cite`. Standard elements the core emits
|
|
159
167
|
// are already lowercase; the semantic checks below (VOID, `input`, close-tag
|
|
@@ -163,18 +171,18 @@ function parseOpenTag(html: string, start: number) {
|
|
|
163
171
|
const attrs: Record<string, string | true> = {};
|
|
164
172
|
while (i < html.length) {
|
|
165
173
|
const loopStart = i;
|
|
166
|
-
while (i < html.length &&
|
|
174
|
+
while (i < html.length && WS_CHAR.test(html[i])) i++;
|
|
167
175
|
if (html[i] === ">") return { tag, attrs, selfClose: false, next: i + 1 };
|
|
168
176
|
if (html[i] === "/" && html[i + 1] === ">") return { tag, attrs, selfClose: true, next: i + 2 };
|
|
169
177
|
if (i >= html.length) break;
|
|
170
178
|
let k = i;
|
|
171
|
-
while (k < html.length &&
|
|
179
|
+
while (k < html.length && !ATTR_NAME_END.test(html[k])) k++;
|
|
172
180
|
const name = html.slice(i, k);
|
|
173
181
|
i = k;
|
|
174
|
-
while (i < html.length &&
|
|
182
|
+
while (i < html.length && WS_CHAR.test(html[i])) i++;
|
|
175
183
|
if (html[i] === "=") {
|
|
176
184
|
i++;
|
|
177
|
-
while (i < html.length &&
|
|
185
|
+
while (i < html.length && WS_CHAR.test(html[i])) i++;
|
|
178
186
|
let value = "";
|
|
179
187
|
const q = html[i];
|
|
180
188
|
if (q === '"' || q === "'") {
|
|
@@ -184,7 +192,7 @@ function parseOpenTag(html: string, start: number) {
|
|
|
184
192
|
i = e === -1 ? html.length : e + 1;
|
|
185
193
|
} else {
|
|
186
194
|
let v = i;
|
|
187
|
-
while (v < html.length &&
|
|
195
|
+
while (v < html.length && !UNQUOTED_VALUE_END.test(html[v])) v++;
|
|
188
196
|
value = html.slice(i, v);
|
|
189
197
|
i = v;
|
|
190
198
|
}
|
package/src/react.tsx
CHANGED
|
@@ -185,8 +185,13 @@ function FluxMarkdownFromClient({
|
|
|
185
185
|
const rendered = deferTail ? deferred : blocks;
|
|
186
186
|
const isDeferring = deferTail ? rendered !== blocks : false;
|
|
187
187
|
// Normalize "no overrides" to a stable `undefined` so memo comparisons and
|
|
188
|
-
// the fast path don't churn on an empty object identity.
|
|
189
|
-
|
|
188
|
+
// the fast path don't churn on an empty object identity. Memoized on
|
|
189
|
+
// [components] so the Object.keys scan runs only when the prop changes, not
|
|
190
|
+
// on every tail patch (identity is unchanged, so blocksEqual still holds).
|
|
191
|
+
const comps = useMemo(
|
|
192
|
+
() => (components && Object.keys(components).length > 0 ? components : undefined),
|
|
193
|
+
[components],
|
|
194
|
+
);
|
|
190
195
|
// Wrap the user hook so each fire also advances the client's aggregate
|
|
191
196
|
// renderCount. Memoized on (client, hook) so its identity stays stable across
|
|
192
197
|
// tail patches — a fresh closure would bust every block's memo. When no hook
|
|
@@ -329,9 +334,10 @@ export function useFluxMarkdownString(
|
|
|
329
334
|
|
|
330
335
|
// Reconcile the parser to the controlled string. setContent diffs internally,
|
|
331
336
|
// so this stays correct whether `content` grows by a token or is swapped wholesale.
|
|
337
|
+
const streaming = options?.streaming;
|
|
332
338
|
useEffect(() => {
|
|
333
|
-
client.setContent(content, { done:
|
|
334
|
-
}, [client, content,
|
|
339
|
+
client.setContent(content, { done: streaming === false });
|
|
340
|
+
}, [client, content, streaming]);
|
|
335
341
|
|
|
336
342
|
return client;
|
|
337
343
|
}
|
|
@@ -931,7 +937,7 @@ function renderBlockContent({
|
|
|
931
937
|
const items = ld?.items;
|
|
932
938
|
const tagOverride =
|
|
933
939
|
!!components && (!!components.ul || !!components.ol || !!components.li);
|
|
934
|
-
if (items && items.length > 0 && !tagOverride) {
|
|
940
|
+
if (Array.isArray(items) && items.length > 0 && !tagOverride) {
|
|
935
941
|
return (
|
|
936
942
|
<KeyedList
|
|
937
943
|
className={className}
|
package/src/server.tsx
CHANGED
|
@@ -68,7 +68,13 @@ export function initFlux(opts?: { wasm?: BufferSource | WebAssembly.Module }): P
|
|
|
68
68
|
await initWasmAsync({ module_or_path: wasmUrl });
|
|
69
69
|
ready = true;
|
|
70
70
|
}
|
|
71
|
-
})()
|
|
71
|
+
})().catch((err) => {
|
|
72
|
+
// Drop the cached rejected promise so a transient failure (e.g. a flaky
|
|
73
|
+
// .wasm fetch on the web path) can be retried by the next initFlux()
|
|
74
|
+
// instead of poisoning every subsequent call until a process restart.
|
|
75
|
+
initPromise = null;
|
|
76
|
+
throw err;
|
|
77
|
+
});
|
|
72
78
|
}
|
|
73
79
|
return initPromise;
|
|
74
80
|
}
|
|
Binary file
|