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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flux-md",
3
- "version": "0.16.0",
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
- return data && Array.isArray(data.rows) ? data : undefined;
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
- console.error("<flux-markdown>: failed to stream src", src, err);
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
  }
@@ -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 && /[a-zA-Z0-9-]/.test(html[j])) j++;
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 && /\s/.test(html[i])) i++;
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 && !/[\s=>/]/.test(html[k])) k++;
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 && /\s/.test(html[i])) i++;
182
+ while (i < html.length && WS_CHAR.test(html[i])) i++;
175
183
  if (html[i] === "=") {
176
184
  i++;
177
- while (i < html.length && /\s/.test(html[i])) i++;
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 && !/[\s>]/.test(html[v])) v++;
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
- const comps = components && Object.keys(components).length > 0 ? components : undefined;
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: options?.streaming === false });
334
- }, [client, content, options?.streaming]);
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