flux-md 0.3.2 → 0.5.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 +54 -0
- package/README.md +65 -0
- package/package.json +1 -1
- package/src/react.tsx +82 -10
- package/src/types.ts +21 -1
- package/src/wasm/flux_md_core.d.ts +8 -0
- package/src/wasm/flux_md_core.js +34 -0
- package/src/wasm/flux_md_core_bg.wasm +0 -0
- package/src/wasm/flux_md_core_bg.wasm.d.ts +1 -0
- package/src/worker.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,60 @@ Notable changes to flux-md. Format based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/); this project aims to follow
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.5.0 — 2026-05-27
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Streaming GFM tables now render incrementally.** A table no longer waits for
|
|
12
|
+
the whole block to arrive: the header renders the moment the delimiter row
|
|
13
|
+
(`|---|`) streams in, and each body row appends as it arrives. Previously the
|
|
14
|
+
incremental paragraph fast-path kept extending the header line as a paragraph
|
|
15
|
+
and only formed the table on a full reparse, so a streaming table appeared all
|
|
16
|
+
at once. The fast-path now bails (like it does for a setext underline) when a
|
|
17
|
+
delimiter row forms a table with its preceding header. Output is unchanged for
|
|
18
|
+
one-shot parsing; streamed output now matches one-shot at every prefix.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **`<FluxMarkdown sanitize={fn} />`** — an optional HTML sanitizer hook. When
|
|
23
|
+
provided, flux-md runs every block's HTML through it before injecting via
|
|
24
|
+
`innerHTML`, **including the streaming (open/speculative) tail** that the raw
|
|
25
|
+
fast path would otherwise expose. Bring your own sanitizer (e.g.
|
|
26
|
+
`DOMPurify.sanitize`) to render untrusted / LLM HTML with `unsafeHtml` on;
|
|
27
|
+
flux-md stays zero-dep. Built-in code/math renderers (already-escaped content)
|
|
28
|
+
are not run through it, so highlighting and math markup are preserved. Omitting
|
|
29
|
+
the prop is byte-identical and zero-cost.
|
|
30
|
+
|
|
31
|
+
## 0.4.0 — 2026-05-27
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- **`componentTags`** — opt-in custom component tags. List tag names (e.g.
|
|
36
|
+
`componentTags: ['Thinking', 'Callout']`) and a `<Thinking>…</Thinking>` in the
|
|
37
|
+
stream renders as a component whose **inner content is parsed as markdown** —
|
|
38
|
+
safely, **without `unsafeHtml`**: the tag is allowlisted and its attributes are
|
|
39
|
+
sanitized (event handlers dropped, dangerous URL schemes neutralized). The
|
|
40
|
+
container spans blank lines (unlike a raw HTML block) up to its matching close
|
|
41
|
+
tag, supports nesting, and ignores a `</Tag>` inside a code fence. Each renders
|
|
42
|
+
as a `Component` block dispatched on the React side via `components[tag]` (e.g.
|
|
43
|
+
`components.Thinking`) or the generic `components.Component`, receiving `{ tag,
|
|
44
|
+
attrs, … }`. Off unless configured; tag names match case-sensitively.
|
|
45
|
+
|
|
46
|
+
### Performance
|
|
47
|
+
|
|
48
|
+
- Streaming a long open display-math block (`$$…$$` / `\[…\]`) is now O(n)
|
|
49
|
+
instead of O(n²). The incremental fence cache that already covered code fences
|
|
50
|
+
was generalized to math fences: an append only escapes the newly arrived lines
|
|
51
|
+
instead of re-scanning and re-escaping the whole growing body. Measured on a
|
|
52
|
+
200 KB `$$…$$` block at 16-byte chunks: **16,271 ms → ~93 ms** (~174×). Output
|
|
53
|
+
is byte-identical (gated by `tests/math_fence_cache.rs`).
|
|
54
|
+
- A long trailing run of link-reference / footnote definitions now commits
|
|
55
|
+
incrementally instead of being re-scanned on every append. Previously such a
|
|
56
|
+
run produced no renderable blocks, so the committed offset never advanced. A
|
|
57
|
+
document ending in a large reference section streams ~10× faster (235 KB at
|
|
58
|
+
16-byte chunks: **13,799 ms → ~1,380 ms**). Output is byte-identical (gated by
|
|
59
|
+
`tests/ref_defs_streaming.rs`).
|
|
60
|
+
|
|
7
61
|
## 0.3.2 — 2026-05-27
|
|
8
62
|
|
|
9
63
|
### Documentation
|
package/README.md
CHANGED
|
@@ -105,6 +105,7 @@ const client = new FluxClient({
|
|
|
105
105
|
gfmMath: true, // $…$ / \(…\) inline + $$…$$ / \[…\] display math (default false)
|
|
106
106
|
dirAuto: true, // per-block dir="auto" for RTL/bidi text (default false)
|
|
107
107
|
unsafeHtml: false, // pass raw HTML through (default false — keep it false for untrusted input)
|
|
108
|
+
componentTags: ["Thinking", "Callout"], // custom tags with markdown inside (default none)
|
|
108
109
|
},
|
|
109
110
|
});
|
|
110
111
|
```
|
|
@@ -211,6 +212,48 @@ Rules worth knowing:
|
|
|
211
212
|
(so your override wins) when you pass `components.CodeBlock`, `components.pre`,
|
|
212
213
|
or `components.code`.
|
|
213
214
|
|
|
215
|
+
### Component tags
|
|
216
|
+
|
|
217
|
+
LLMs increasingly emit custom component tags like `<Thinking>…</Thinking>`. By
|
|
218
|
+
default these are inert (escaped, or — with `unsafeHtml` — raw HTML whose body
|
|
219
|
+
is *not* markdown). Opt in by allowlisting the tag names:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
const client = new FluxClient({ config: { componentTags: ["Thinking", "Callout"] } });
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Now a listed tag is a **markdown container**: its inner content is parsed as
|
|
226
|
+
markdown, it spans blank lines up to its matching close tag (not split like a
|
|
227
|
+
raw HTML block), it nests, and a `</Tag>` inside a code fence stays content. It's
|
|
228
|
+
**safe without `unsafeHtml`** — the tag is allowlisted and its attributes are
|
|
229
|
+
sanitized (event handlers dropped, dangerous URL schemes → `#`).
|
|
230
|
+
|
|
231
|
+
Each renders as a `Component` block. Override it in React by tag name (or with
|
|
232
|
+
the generic `Component` fallback). The override receives `tag`, the sanitized
|
|
233
|
+
`attrs`, and `html` — the **inner** (already-rendered markdown) HTML, so you can
|
|
234
|
+
wrap it in your own element:
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
<FluxMarkdown
|
|
238
|
+
client={client}
|
|
239
|
+
components={{
|
|
240
|
+
Thinking: ({ html }) => (
|
|
241
|
+
<details className="thinking">
|
|
242
|
+
<summary>Reasoning</summary>
|
|
243
|
+
<div dangerouslySetInnerHTML={{ __html: html }} />
|
|
244
|
+
</details>
|
|
245
|
+
),
|
|
246
|
+
}}
|
|
247
|
+
/>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
With no override, the component renders as `<thinking …>…</thinking>` HTML. The
|
|
251
|
+
override's `html` is the inner content only; `attrs` keys are React-form
|
|
252
|
+
(`class`→`className`, `for`→`htmlFor`) so `{...attrs}` spreads cleanly. While the
|
|
253
|
+
component is still streaming, `html` is the partial inner content and re-renders
|
|
254
|
+
as more arrives. Tag names match case-sensitively; the feature is off unless
|
|
255
|
+
`componentTags` is set.
|
|
256
|
+
|
|
214
257
|
### Types
|
|
215
258
|
|
|
216
259
|
```ts
|
|
@@ -302,6 +345,28 @@ flux-md is XSS-safe by default — its HTML output is meant to be injected via
|
|
|
302
345
|
third-party HTML, these guards are your only line of defense — prefer a
|
|
303
346
|
dedicated HTML sanitizer for genuinely hostile input.
|
|
304
347
|
|
|
348
|
+
### Rendering untrusted / LLM HTML safely
|
|
349
|
+
|
|
350
|
+
If you enable `unsafeHtml` to render HTML from an untrusted source (e.g. an LLM
|
|
351
|
+
that returns raw HTML), **bring a real sanitizer** and pass it via
|
|
352
|
+
`<FluxMarkdown sanitize={…} />`. flux-md applies it to every block's HTML before
|
|
353
|
+
injection — **including the streaming (open) tail**, which the raw-`innerHTML`
|
|
354
|
+
fast path would otherwise expose. flux-md stays zero-dep; you choose the
|
|
355
|
+
sanitizer:
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
import DOMPurify from "dompurify";
|
|
359
|
+
|
|
360
|
+
<FluxMarkdown client={client} sanitize={(html) => DOMPurify.sanitize(html)} />
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The built-in code/math renderers operate on already-escaped content and are not
|
|
364
|
+
run through `sanitize`, so syntax highlighting and math markup are preserved.
|
|
365
|
+
With no `sanitize` prop, rendering is byte-identical and zero-cost. For
|
|
366
|
+
genuinely hostile content where CSS-overlay/clickjacking matters, render inside
|
|
367
|
+
a sandboxed `<iframe>` instead — sanitization stops injection, not every
|
|
368
|
+
visual-overlay trick.
|
|
369
|
+
|
|
305
370
|
## Scaling
|
|
306
371
|
|
|
307
372
|
`FluxClient`s share a **worker pool** (`getDefaultPool()`), so concurrency
|
package/package.json
CHANGED
package/src/react.tsx
CHANGED
|
@@ -65,9 +65,19 @@ interface FluxMarkdownProps {
|
|
|
65
65
|
* up (and re-locks when they scroll back near the bottom). Off by default.
|
|
66
66
|
*/
|
|
67
67
|
stickToBottom?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Optional HTML sanitizer applied to every block's HTML before it is injected
|
|
70
|
+
* via `innerHTML` — **including the streaming (open/speculative) tail**, the
|
|
71
|
+
* path that raw `innerHTML` would otherwise expose. Pass a real sanitizer
|
|
72
|
+
* (e.g. DOMPurify's `sanitize`) when rendering untrusted / LLM HTML with
|
|
73
|
+
* `unsafeHtml` on. flux-md stays zero-dep — you bring the sanitizer. The
|
|
74
|
+
* built-in code/math renderers operate on already-escaped content and are not
|
|
75
|
+
* run through it. When omitted, rendering is byte-identical and zero-cost.
|
|
76
|
+
*/
|
|
77
|
+
sanitize?: (html: string) => string;
|
|
68
78
|
}
|
|
69
79
|
|
|
70
|
-
function FluxMarkdownImpl({ client, components, virtualize, stickToBottom }: FluxMarkdownProps) {
|
|
80
|
+
function FluxMarkdownImpl({ client, components, virtualize, stickToBottom, sanitize }: FluxMarkdownProps) {
|
|
71
81
|
const blocks = useSyncExternalStore(client.subscribe, client.getSnapshot, client.getSnapshot);
|
|
72
82
|
// Normalize "no overrides" to a stable `undefined` so memo comparisons and
|
|
73
83
|
// the fast path don't churn on an empty object identity.
|
|
@@ -75,7 +85,7 @@ function FluxMarkdownImpl({ client, components, virtualize, stickToBottom }: Flu
|
|
|
75
85
|
return (
|
|
76
86
|
<div className="flux-md">
|
|
77
87
|
{blocks.map((b) => (
|
|
78
|
-
<BlockView key={b.id} block={b} components={comps} virtualize={virtualize} />
|
|
88
|
+
<BlockView key={b.id} block={b} components={comps} virtualize={virtualize} sanitize={sanitize} />
|
|
79
89
|
))}
|
|
80
90
|
{stickToBottom && <div aria-hidden="true" style={{ scrollSnapAlign: "end" }} className="flux-bottom-anchor" />}
|
|
81
91
|
</div>
|
|
@@ -117,16 +127,49 @@ function blockKindProps(block: Block): BlockComponentProps {
|
|
|
117
127
|
open: block.open,
|
|
118
128
|
speculative: block.speculative,
|
|
119
129
|
};
|
|
120
|
-
const data = block.kind.data as
|
|
130
|
+
const data = block.kind.data as
|
|
131
|
+
| { lang?: string | null; tag?: string; attrs?: [string, string][] }
|
|
132
|
+
| undefined;
|
|
121
133
|
if (block.kind.type === "CodeBlock") {
|
|
122
134
|
props.text = decodeCodeText(block.html);
|
|
123
135
|
props.language = data?.lang ?? "";
|
|
124
136
|
} else if (block.kind.type === "MathBlock") {
|
|
125
137
|
props.text = decodeMathText(block.html);
|
|
138
|
+
} else if (block.kind.type === "Component") {
|
|
139
|
+
props.tag = data?.tag ?? "";
|
|
140
|
+
// React-form attribute names, so `{...attrs}` spreads cleanly onto an element
|
|
141
|
+
// (HTML `class`/`for` → React `className`/`htmlFor`).
|
|
142
|
+
props.attrs = reactAttrs(data?.attrs ?? []);
|
|
143
|
+
// An override replaces the `<tag>` wrapper, so it gets the *inner* HTML
|
|
144
|
+
// (markdown already rendered) rather than the full wrapped block.
|
|
145
|
+
props.html = componentInnerHtml(block.html, props.tag);
|
|
126
146
|
}
|
|
127
147
|
return props;
|
|
128
148
|
}
|
|
129
149
|
|
|
150
|
+
const REACT_ATTR_NAME: Record<string, string> = { class: "className", for: "htmlFor" };
|
|
151
|
+
|
|
152
|
+
/** Convert sanitized HTML attribute pairs into a React-spreadable object,
|
|
153
|
+
* renaming the two names React requires (`class`→`className`, `for`→`htmlFor`).
|
|
154
|
+
* Other names (including `data-*` / `aria-*`) pass through unchanged. */
|
|
155
|
+
function reactAttrs(pairs: [string, string][]): Record<string, string> {
|
|
156
|
+
const out: Record<string, string> = {};
|
|
157
|
+
for (const [k, v] of pairs) out[REACT_ATTR_NAME[k] ?? k] = v;
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Strip the `<tag …>` open and trailing `</tag>` from a component block's HTML,
|
|
162
|
+
* leaving the inner (already-rendered markdown) HTML. Handles open (unclosed)
|
|
163
|
+
* blocks, where there is no close tag yet. */
|
|
164
|
+
function componentInnerHtml(html: string, tag: string): string {
|
|
165
|
+
const gt = html.indexOf(">");
|
|
166
|
+
if (gt < 0) return "";
|
|
167
|
+
let inner = html.slice(gt + 1);
|
|
168
|
+
const close = `</${tag}>`;
|
|
169
|
+
if (inner.endsWith(close)) inner = inner.slice(0, -close.length);
|
|
170
|
+
return inner.replace(/^\n/, "").replace(/\n$/, "");
|
|
171
|
+
}
|
|
172
|
+
|
|
130
173
|
/** Convert a closed block's HTML to a React tree, memoized on html+components. */
|
|
131
174
|
function SafeHtml({ html, components }: { html: string; components: Components }) {
|
|
132
175
|
return useMemo(() => htmlToReact(html, components), [html, components]) as JSX.Element;
|
|
@@ -138,9 +181,15 @@ function SafeHtml({ html, components }: { html: string; components: Components }
|
|
|
138
181
|
const INTRINSIC_PX: Record<string, number> = {
|
|
139
182
|
Paragraph: 80, Heading: 44, CodeBlock: 300, MathBlock: 140, Mermaid: 220,
|
|
140
183
|
List: 120, Blockquote: 100, Alert: 120, Table: 200, Rule: 24, Html: 80,
|
|
184
|
+
Component: 120,
|
|
141
185
|
};
|
|
142
186
|
|
|
143
|
-
function BlockViewImpl(props: {
|
|
187
|
+
function BlockViewImpl(props: {
|
|
188
|
+
block: Block;
|
|
189
|
+
components?: Components;
|
|
190
|
+
virtualize?: boolean;
|
|
191
|
+
sanitize?: (html: string) => string;
|
|
192
|
+
}) {
|
|
144
193
|
const { block, virtualize } = props;
|
|
145
194
|
const content = renderBlockContent(props);
|
|
146
195
|
// Virtualize only *closed* blocks: the streaming tail (open/speculative) is
|
|
@@ -158,11 +207,28 @@ function BlockViewImpl(props: { block: Block; components?: Components; virtualiz
|
|
|
158
207
|
return content;
|
|
159
208
|
}
|
|
160
209
|
|
|
161
|
-
function renderBlockContent({
|
|
210
|
+
function renderBlockContent({
|
|
211
|
+
block,
|
|
212
|
+
components,
|
|
213
|
+
sanitize,
|
|
214
|
+
}: {
|
|
215
|
+
block: Block;
|
|
216
|
+
components?: Components;
|
|
217
|
+
sanitize?: (html: string) => string;
|
|
218
|
+
}) {
|
|
162
219
|
const kind = block.kind.type;
|
|
163
220
|
|
|
164
|
-
// Block-kind override replaces the entire renderer for this block.
|
|
221
|
+
// Block-kind override replaces the entire renderer for this block. A
|
|
222
|
+
// `Component` block also dispatches on its tag name, so `components.Thinking`
|
|
223
|
+
// (the specific tag) wins over `components.Component` (the generic fallback).
|
|
165
224
|
if (components) {
|
|
225
|
+
if (kind === "Component") {
|
|
226
|
+
const tag = (block.kind.data as { tag?: string } | undefined)?.tag;
|
|
227
|
+
const override = (tag && components[tag]) || components.Component;
|
|
228
|
+
if (override) {
|
|
229
|
+
return createElement(override, blockKindProps(block));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
166
232
|
const blockOverride = components[kind];
|
|
167
233
|
if (blockOverride) {
|
|
168
234
|
return createElement(blockOverride, blockKindProps(block));
|
|
@@ -199,7 +265,12 @@ function renderBlockContent({ block, components }: { block: Block; components?:
|
|
|
199
265
|
);
|
|
200
266
|
}
|
|
201
267
|
|
|
202
|
-
return
|
|
268
|
+
return (
|
|
269
|
+
<div
|
|
270
|
+
className={className}
|
|
271
|
+
dangerouslySetInnerHTML={{ __html: sanitize ? sanitize(block.html) : block.html }}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
203
274
|
}
|
|
204
275
|
|
|
205
276
|
// A block is the same render when its identity, HTML, open-state, and the
|
|
@@ -207,8 +278,8 @@ function renderBlockContent({ block, components }: { block: Block; components?:
|
|
|
207
278
|
// is what stops a committed block from re-rendering (and thus re-parsing) on
|
|
208
279
|
// every streaming patch.
|
|
209
280
|
export function blocksEqual(
|
|
210
|
-
prev: { block: Block; components?: Components; virtualize?: boolean },
|
|
211
|
-
next: { block: Block; components?: Components; virtualize?: boolean },
|
|
281
|
+
prev: { block: Block; components?: Components; virtualize?: boolean; sanitize?: (html: string) => string },
|
|
282
|
+
next: { block: Block; components?: Components; virtualize?: boolean; sanitize?: (html: string) => string },
|
|
212
283
|
): boolean {
|
|
213
284
|
return (
|
|
214
285
|
prev.block.id === next.block.id &&
|
|
@@ -216,7 +287,8 @@ export function blocksEqual(
|
|
|
216
287
|
prev.block.open === next.block.open &&
|
|
217
288
|
prev.block.speculative === next.block.speculative &&
|
|
218
289
|
prev.components === next.components &&
|
|
219
|
-
prev.virtualize === next.virtualize
|
|
290
|
+
prev.virtualize === next.virtualize &&
|
|
291
|
+
prev.sanitize === next.sanitize
|
|
220
292
|
);
|
|
221
293
|
}
|
|
222
294
|
|
package/src/types.ts
CHANGED
|
@@ -9,7 +9,8 @@ export type BlockKindTag =
|
|
|
9
9
|
| "Alert"
|
|
10
10
|
| "Table"
|
|
11
11
|
| "Rule"
|
|
12
|
-
| "Html"
|
|
12
|
+
| "Html"
|
|
13
|
+
| "Component";
|
|
13
14
|
|
|
14
15
|
export interface BlockKind {
|
|
15
16
|
type: BlockKindTag;
|
|
@@ -60,6 +61,15 @@ export interface BlockComponentProps {
|
|
|
60
61
|
text?: string;
|
|
61
62
|
/** Info-string language — present for `CodeBlock` (from `kind.data.lang`). */
|
|
62
63
|
language?: string;
|
|
64
|
+
/** Component tag name — present for `Component` blocks (from `kind.data.tag`). */
|
|
65
|
+
tag?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Sanitized attributes — present for `Component` blocks. Names are React-form
|
|
68
|
+
* (`class`→`className`, `for`→`htmlFor`) so `{...attrs}` spreads cleanly onto
|
|
69
|
+
* an element. For `Component` blocks, `html` is the **inner** rendered-markdown
|
|
70
|
+
* HTML (not the `<tag>…</tag>` wrapper), so an override can wrap it itself.
|
|
71
|
+
*/
|
|
72
|
+
attrs?: Record<string, string>;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
/**
|
|
@@ -93,6 +103,16 @@ export interface ParserConfig {
|
|
|
93
103
|
dirAuto?: boolean;
|
|
94
104
|
/** Pass raw HTML through unescaped. Default false. **Never enable for untrusted input.** */
|
|
95
105
|
unsafeHtml?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Opt-in allowlist of custom component tag names (e.g. `["Thinking",
|
|
108
|
+
* "Callout"]`). A `<Tag>…</Tag>` whose name is listed renders as a component
|
|
109
|
+
* whose inner content is parsed as **markdown** — safely, without `unsafeHtml`
|
|
110
|
+
* (the tag is allowlisted and its attributes are sanitized: event handlers
|
|
111
|
+
* dropped, dangerous URL schemes neutralized). The block is dispatched on the
|
|
112
|
+
* React side via `components[tag]` (or `components.Component`). Empty/omitted =
|
|
113
|
+
* off. Names match case-sensitively.
|
|
114
|
+
*/
|
|
115
|
+
componentTags?: string[];
|
|
96
116
|
}
|
|
97
117
|
|
|
98
118
|
// Each message carries a `streamId` so one worker can multiplex many parsers
|
|
@@ -14,6 +14,13 @@ export class FluxParser {
|
|
|
14
14
|
* memory cost against alternatives.
|
|
15
15
|
*/
|
|
16
16
|
retainedBytes(): number;
|
|
17
|
+
/**
|
|
18
|
+
* Set the opt-in component-tag allowlist (e.g. `["Thinking", "Callout"]`).
|
|
19
|
+
* A `<Tag>…</Tag>` whose name is listed renders as a component whose inner
|
|
20
|
+
* content is markdown — safely, without unsafe HTML (the tag is allowlisted
|
|
21
|
+
* and its attributes are sanitized). Empty by default (feature off).
|
|
22
|
+
*/
|
|
23
|
+
setComponentTags(tags: string[]): void;
|
|
17
24
|
/**
|
|
18
25
|
* Emit `dir="auto"` on block-level text elements so the browser detects
|
|
19
26
|
* each block's direction (LTR/RTL) independently — correct rendering for
|
|
@@ -60,6 +67,7 @@ export interface InitOutput {
|
|
|
60
67
|
readonly fluxparser_finalize: (a: number, b: number) => void;
|
|
61
68
|
readonly fluxparser_new: () => number;
|
|
62
69
|
readonly fluxparser_retainedBytes: (a: number) => number;
|
|
70
|
+
readonly fluxparser_setComponentTags: (a: number, b: number, c: number) => void;
|
|
63
71
|
readonly fluxparser_setDirAuto: (a: number, b: number) => void;
|
|
64
72
|
readonly fluxparser_setGfmAlerts: (a: number, b: number) => void;
|
|
65
73
|
readonly fluxparser_setGfmAutolinks: (a: number, b: number) => void;
|
package/src/wasm/flux_md_core.js
CHANGED
|
@@ -73,6 +73,18 @@ export class FluxParser {
|
|
|
73
73
|
const ret = wasm.fluxparser_retainedBytes(this.__wbg_ptr);
|
|
74
74
|
return ret >>> 0;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Set the opt-in component-tag allowlist (e.g. `["Thinking", "Callout"]`).
|
|
78
|
+
* A `<Tag>…</Tag>` whose name is listed renders as a component whose inner
|
|
79
|
+
* content is markdown — safely, without unsafe HTML (the tag is allowlisted
|
|
80
|
+
* and its attributes are sanitized). Empty by default (feature off).
|
|
81
|
+
* @param {string[]} tags
|
|
82
|
+
*/
|
|
83
|
+
setComponentTags(tags) {
|
|
84
|
+
const ptr0 = passArrayJsValueToWasm0(tags, wasm.__wbindgen_export);
|
|
85
|
+
const len0 = WASM_VECTOR_LEN;
|
|
86
|
+
wasm.fluxparser_setComponentTags(this.__wbg_ptr, ptr0, len0);
|
|
87
|
+
}
|
|
76
88
|
/**
|
|
77
89
|
* Emit `dir="auto"` on block-level text elements so the browser detects
|
|
78
90
|
* each block's direction (LTR/RTL) independently — correct rendering for
|
|
@@ -141,6 +153,14 @@ function __wbg_get_imports() {
|
|
|
141
153
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
142
154
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
143
155
|
},
|
|
156
|
+
__wbg___wbindgen_string_get_72bdf95d3ae505b1: function(arg0, arg1) {
|
|
157
|
+
const obj = getObject(arg1);
|
|
158
|
+
const ret = typeof(obj) === 'string' ? obj : undefined;
|
|
159
|
+
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export, wasm.__wbindgen_export2);
|
|
160
|
+
var len1 = WASM_VECTOR_LEN;
|
|
161
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
162
|
+
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
163
|
+
},
|
|
144
164
|
__wbg___wbindgen_throw_1506f2235d1bdba0: function(arg0, arg1) {
|
|
145
165
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
146
166
|
},
|
|
@@ -233,6 +253,20 @@ heap.push(undefined, null, true, false);
|
|
|
233
253
|
|
|
234
254
|
let heap_next = heap.length;
|
|
235
255
|
|
|
256
|
+
function isLikeNone(x) {
|
|
257
|
+
return x === undefined || x === null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function passArrayJsValueToWasm0(array, malloc) {
|
|
261
|
+
const ptr = malloc(array.length * 4, 4) >>> 0;
|
|
262
|
+
const mem = getDataViewMemory0();
|
|
263
|
+
for (let i = 0; i < array.length; i++) {
|
|
264
|
+
mem.setUint32(ptr + 4 * i, addHeapObject(array[i]), true);
|
|
265
|
+
}
|
|
266
|
+
WASM_VECTOR_LEN = array.length;
|
|
267
|
+
return ptr;
|
|
268
|
+
}
|
|
269
|
+
|
|
236
270
|
function passStringToWasm0(arg, malloc, realloc) {
|
|
237
271
|
if (realloc === undefined) {
|
|
238
272
|
const buf = cachedTextEncoder.encode(arg);
|
|
Binary file
|
|
@@ -7,6 +7,7 @@ export const fluxparser_bufferLen: (a: number) => number;
|
|
|
7
7
|
export const fluxparser_finalize: (a: number, b: number) => void;
|
|
8
8
|
export const fluxparser_new: () => number;
|
|
9
9
|
export const fluxparser_retainedBytes: (a: number) => number;
|
|
10
|
+
export const fluxparser_setComponentTags: (a: number, b: number, c: number) => void;
|
|
10
11
|
export const fluxparser_setDirAuto: (a: number, b: number) => void;
|
|
11
12
|
export const fluxparser_setGfmAlerts: (a: number, b: number) => void;
|
|
12
13
|
export const fluxparser_setGfmAutolinks: (a: number, b: number) => void;
|
package/src/worker.ts
CHANGED
|
@@ -45,6 +45,7 @@ function getOrCreate(streamId: number): FluxParser {
|
|
|
45
45
|
p.setGfmMath(c?.gfmMath ?? false);
|
|
46
46
|
p.setDirAuto(c?.dirAuto ?? false);
|
|
47
47
|
p.setUnsafeHtml(c?.unsafeHtml ?? false);
|
|
48
|
+
p.setComponentTags(c?.componentTags ?? []);
|
|
48
49
|
parsers.set(streamId, p);
|
|
49
50
|
}
|
|
50
51
|
return p;
|