flux-md 0.5.5 → 0.7.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 +143 -0
- package/README.md +261 -21
- package/package.json +21 -5
- package/src/block-props.ts +96 -0
- package/src/client.ts +111 -11
- package/src/dom.ts +430 -0
- package/src/element.ts +381 -0
- package/src/renderers/CodeBlock.tsx +62 -5
- package/src/solid.tsx +70 -0
- package/src/svelte.ts +55 -0
- package/src/types-core.ts +147 -0
- package/src/types-react.ts +14 -0
- package/src/types.ts +7 -150
- package/src/vue.ts +100 -0
- package/src/wasm/flux_md_core.d.ts +7 -0
- package/src/wasm/flux_md_core.js +9 -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/wasm/package.json +1 -1
- package/src/worker.ts +11 -2
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export type BlockKindTag =
|
|
2
|
+
| "Paragraph"
|
|
3
|
+
| "Heading"
|
|
4
|
+
| "CodeBlock"
|
|
5
|
+
| "MathBlock"
|
|
6
|
+
| "Mermaid"
|
|
7
|
+
| "List"
|
|
8
|
+
| "Blockquote"
|
|
9
|
+
| "Alert"
|
|
10
|
+
| "Table"
|
|
11
|
+
| "Rule"
|
|
12
|
+
| "Html"
|
|
13
|
+
| "Component";
|
|
14
|
+
|
|
15
|
+
export interface BlockKind {
|
|
16
|
+
type: BlockKindTag;
|
|
17
|
+
data?: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Block {
|
|
21
|
+
id: number;
|
|
22
|
+
kind: BlockKind;
|
|
23
|
+
start: number;
|
|
24
|
+
end: number;
|
|
25
|
+
html: string;
|
|
26
|
+
open: boolean;
|
|
27
|
+
speculative: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface Patch {
|
|
31
|
+
newly_committed: Block[];
|
|
32
|
+
active: Block[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Props passed to a block-kind override (e.g. `components.CodeBlock`). */
|
|
36
|
+
export interface BlockComponentProps {
|
|
37
|
+
/** The full parsed block, including `kind` (with `kind.data`) and offsets. */
|
|
38
|
+
block: Block;
|
|
39
|
+
/** Rendered, XSS-safe HTML for this block. */
|
|
40
|
+
html: string;
|
|
41
|
+
/** True while the block is still streaming (its HTML may still change). */
|
|
42
|
+
open: boolean;
|
|
43
|
+
/** True if the block was closed speculatively and may yet be revised. */
|
|
44
|
+
speculative: boolean;
|
|
45
|
+
/** Decoded source text — present for `CodeBlock` / `MathBlock`. */
|
|
46
|
+
text?: string;
|
|
47
|
+
/** Info-string language — present for `CodeBlock` (from `kind.data.lang`). */
|
|
48
|
+
language?: string;
|
|
49
|
+
/** Component tag name — present for `Component` blocks (from `kind.data.tag`). */
|
|
50
|
+
tag?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Sanitized attributes — present for `Component` blocks. The name-form depends
|
|
53
|
+
* on the consumer: the JSX renderer maps `class`→`className`/`for`→`htmlFor`
|
|
54
|
+
* so `{...attrs}` spreads cleanly onto an element; the DOM renderer keeps the
|
|
55
|
+
* literal HTML names (`class`/`for`) because it applies them via
|
|
56
|
+
* `setAttribute`. For `Component` blocks, `html` is the **inner**
|
|
57
|
+
* rendered-markdown HTML (not the `<tag>…</tag>` wrapper), so an override can
|
|
58
|
+
* wrap it itself.
|
|
59
|
+
*/
|
|
60
|
+
attrs?: Record<string, string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Per-stream parser configuration. Omitted fields use the library defaults
|
|
65
|
+
* (autolinks + alerts on, raw HTML escaped, footnotes off) — so the default
|
|
66
|
+
* `new FluxClient()` behaves exactly as before. Config is applied when the
|
|
67
|
+
* stream's parser is created and is **immutable** for that stream's lifetime
|
|
68
|
+
* (a `reset()` keeps it; use a new client for different flags).
|
|
69
|
+
*/
|
|
70
|
+
export interface ParserConfig {
|
|
71
|
+
/** GFM extended autolinks (bare www./http(s)://ftp:// + emails). Default true. */
|
|
72
|
+
gfmAutolinks?: boolean;
|
|
73
|
+
/** GitHub alerts (`> [!NOTE]` → callouts). Default true. */
|
|
74
|
+
gfmAlerts?: boolean;
|
|
75
|
+
/** GFM footnotes (`[^1]` + `[^1]:` → footnote section). Default false. */
|
|
76
|
+
gfmFootnotes?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Math: `$…$` / `\(…\)` inline and `$$…$$` / `\[…\]` display. Default false
|
|
79
|
+
* (so `$` in prose / currency stays literal). Emits KaTeX-ready markup
|
|
80
|
+
* (`<span class="math math-inline">` / `<div class="math math-display">`)
|
|
81
|
+
* carrying the LaTeX — bring your own KaTeX pass (flux-md stays zero-dep).
|
|
82
|
+
*/
|
|
83
|
+
gfmMath?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Emit `dir="auto"` on block-level text elements (`p`, `h1`–`h6`,
|
|
86
|
+
* `blockquote`, `ul`/`ol`/`li`, `table`) so the browser detects each block's
|
|
87
|
+
* direction independently — correct for documents mixing English with
|
|
88
|
+
* Arabic/Hebrew. Default false; code blocks always stay LTR. Recommended for
|
|
89
|
+
* apps that render RTL or mixed-direction content.
|
|
90
|
+
*/
|
|
91
|
+
dirAuto?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Opt-in accessibility markup that deviates from strict GFM byte-output:
|
|
94
|
+
* wraps a task-list checkbox + its text in a `<label>` (programmatic
|
|
95
|
+
* association for screen readers) and adds `scope="col"` to table header
|
|
96
|
+
* cells. Default false (so CommonMark/GFM conformance output is unchanged).
|
|
97
|
+
*/
|
|
98
|
+
a11y?: boolean;
|
|
99
|
+
/** Pass raw HTML through unescaped. Default false. **Never enable for untrusted input.** */
|
|
100
|
+
unsafeHtml?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Opt-in allowlist of custom component tag names (e.g. `["Thinking",
|
|
103
|
+
* "Callout"]`). A `<Tag>…</Tag>` whose name is listed renders as a component
|
|
104
|
+
* whose inner content is parsed as **markdown** — safely, without `unsafeHtml`
|
|
105
|
+
* (the tag is allowlisted and its attributes are sanitized: event handlers
|
|
106
|
+
* dropped, dangerous URL schemes neutralized). The block is dispatched by the
|
|
107
|
+
* renderer via `components[tag]` (or `components.Component`). Empty/omitted =
|
|
108
|
+
* off. Names match case-sensitively.
|
|
109
|
+
*/
|
|
110
|
+
componentTags?: string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Each message carries a `streamId` so one worker can multiplex many parsers
|
|
114
|
+
// (the worker pool). `ready` is the exception — it's worker-level (WASM loaded),
|
|
115
|
+
// not stream-level. The first message for a stream may carry `config`, applied
|
|
116
|
+
// when that stream's parser is created.
|
|
117
|
+
export type ToWorker =
|
|
118
|
+
| { type: "append"; streamId: number; chunk: string; config?: ParserConfig }
|
|
119
|
+
| { type: "finalize"; streamId: number; config?: ParserConfig }
|
|
120
|
+
| { type: "reset"; streamId: number }
|
|
121
|
+
| { type: "dispose"; streamId: number };
|
|
122
|
+
|
|
123
|
+
export type FromWorker =
|
|
124
|
+
| { type: "ready" }
|
|
125
|
+
| {
|
|
126
|
+
type: "patch";
|
|
127
|
+
streamId: number;
|
|
128
|
+
patch: Patch;
|
|
129
|
+
appendedBytes: number;
|
|
130
|
+
parseMicros: number;
|
|
131
|
+
retainedBytes: number;
|
|
132
|
+
wasmMemoryBytes: number;
|
|
133
|
+
}
|
|
134
|
+
// `fatal` marks a worker-level failure (WASM init) that dooms every stream on
|
|
135
|
+
// the worker — not a single parse error. It carries no meaningful streamId.
|
|
136
|
+
| { type: "error"; streamId: number; message: string; fatal?: boolean };
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Minimal structural interface satisfied by the DOM `Worker`. Injectable so the
|
|
140
|
+
* pool's routing/lifecycle logic can be unit-tested with a fake worker — no
|
|
141
|
+
* real Worker or WASM required.
|
|
142
|
+
*/
|
|
143
|
+
export interface WorkerLike {
|
|
144
|
+
postMessage(msg: ToWorker): void;
|
|
145
|
+
addEventListener(type: "message", listener: (ev: { data: FromWorker }) => void): void;
|
|
146
|
+
terminate(): void;
|
|
147
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentType } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Override map for {@link FluxMarkdown}. Keys are either lowercase HTML tag
|
|
5
|
+
* names (`table`, `a`, `code`, `h1`… — react-markdown style, applied inside a
|
|
6
|
+
* block's HTML) or capitalized block-kind names (`BlockKindTag`, e.g.
|
|
7
|
+
* `CodeBlock`, `Table` — replace the whole block renderer). Values are a React
|
|
8
|
+
* component or an HTML tag string.
|
|
9
|
+
*
|
|
10
|
+
* Tag-level components receive the element's parsed attributes (with
|
|
11
|
+
* `class`→`className`, `style` as an object) plus `children`. Block-kind
|
|
12
|
+
* components receive `BlockComponentProps`. There is no `node` prop.
|
|
13
|
+
*/
|
|
14
|
+
export type Components = Record<string, ComponentType<any> | string>;
|
package/src/types.ts
CHANGED
|
@@ -1,150 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
| "Blockquote"
|
|
9
|
-
| "Alert"
|
|
10
|
-
| "Table"
|
|
11
|
-
| "Rule"
|
|
12
|
-
| "Html"
|
|
13
|
-
| "Component";
|
|
14
|
-
|
|
15
|
-
export interface BlockKind {
|
|
16
|
-
type: BlockKindTag;
|
|
17
|
-
data?: unknown;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface Block {
|
|
21
|
-
id: number;
|
|
22
|
-
kind: BlockKind;
|
|
23
|
-
start: number;
|
|
24
|
-
end: number;
|
|
25
|
-
html: string;
|
|
26
|
-
open: boolean;
|
|
27
|
-
speculative: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface Patch {
|
|
31
|
-
newly_committed: Block[];
|
|
32
|
-
active: Block[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
import type { ComponentType } from "react";
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Override map for {@link FluxMarkdown}. Keys are either lowercase HTML tag
|
|
39
|
-
* names (`table`, `a`, `code`, `h1`… — react-markdown style, applied inside a
|
|
40
|
-
* block's HTML) or capitalized block-kind names ({@link BlockKindTag}, e.g.
|
|
41
|
-
* `CodeBlock`, `Table` — replace the whole block renderer). Values are a React
|
|
42
|
-
* component or an HTML tag string.
|
|
43
|
-
*
|
|
44
|
-
* Tag-level components receive the element's parsed attributes (with
|
|
45
|
-
* `class`→`className`, `style` as an object) plus `children`. Block-kind
|
|
46
|
-
* components receive {@link BlockComponentProps}. There is no `node` prop.
|
|
47
|
-
*/
|
|
48
|
-
export type Components = Record<string, ComponentType<any> | string>;
|
|
49
|
-
|
|
50
|
-
/** Props passed to a block-kind override (e.g. `components.CodeBlock`). */
|
|
51
|
-
export interface BlockComponentProps {
|
|
52
|
-
/** The full parsed block, including `kind` (with `kind.data`) and offsets. */
|
|
53
|
-
block: Block;
|
|
54
|
-
/** Rendered, XSS-safe HTML for this block. */
|
|
55
|
-
html: string;
|
|
56
|
-
/** True while the block is still streaming (its HTML may still change). */
|
|
57
|
-
open: boolean;
|
|
58
|
-
/** True if the block was closed speculatively and may yet be revised. */
|
|
59
|
-
speculative: boolean;
|
|
60
|
-
/** Decoded source text — present for `CodeBlock` / `MathBlock`. */
|
|
61
|
-
text?: string;
|
|
62
|
-
/** Info-string language — present for `CodeBlock` (from `kind.data.lang`). */
|
|
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>;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Per-stream parser configuration. Omitted fields use the library defaults
|
|
77
|
-
* (autolinks + alerts on, raw HTML escaped, footnotes off) — so the default
|
|
78
|
-
* `new FluxClient()` behaves exactly as before. Config is applied when the
|
|
79
|
-
* stream's parser is created and is **immutable** for that stream's lifetime
|
|
80
|
-
* (a `reset()` keeps it; use a new client for different flags).
|
|
81
|
-
*/
|
|
82
|
-
export interface ParserConfig {
|
|
83
|
-
/** GFM extended autolinks (bare www./http(s)://ftp:// + emails). Default true. */
|
|
84
|
-
gfmAutolinks?: boolean;
|
|
85
|
-
/** GitHub alerts (`> [!NOTE]` → callouts). Default true. */
|
|
86
|
-
gfmAlerts?: boolean;
|
|
87
|
-
/** GFM footnotes (`[^1]` + `[^1]:` → footnote section). Default false. */
|
|
88
|
-
gfmFootnotes?: boolean;
|
|
89
|
-
/**
|
|
90
|
-
* Math: `$…$` / `\(…\)` inline and `$$…$$` / `\[…\]` display. Default false
|
|
91
|
-
* (so `$` in prose / currency stays literal). Emits KaTeX-ready markup
|
|
92
|
-
* (`<span class="math math-inline">` / `<div class="math math-display">`)
|
|
93
|
-
* carrying the LaTeX — bring your own KaTeX pass (flux-md stays zero-dep).
|
|
94
|
-
*/
|
|
95
|
-
gfmMath?: boolean;
|
|
96
|
-
/**
|
|
97
|
-
* Emit `dir="auto"` on block-level text elements (`p`, `h1`–`h6`,
|
|
98
|
-
* `blockquote`, `ul`/`ol`/`li`, `table`) so the browser detects each block's
|
|
99
|
-
* direction independently — correct for documents mixing English with
|
|
100
|
-
* Arabic/Hebrew. Default false; code blocks always stay LTR. Recommended for
|
|
101
|
-
* apps that render RTL or mixed-direction content.
|
|
102
|
-
*/
|
|
103
|
-
dirAuto?: boolean;
|
|
104
|
-
/** Pass raw HTML through unescaped. Default false. **Never enable for untrusted input.** */
|
|
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[];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Each message carries a `streamId` so one worker can multiplex many parsers
|
|
119
|
-
// (the worker pool). `ready` is the exception — it's worker-level (WASM loaded),
|
|
120
|
-
// not stream-level. The first message for a stream may carry `config`, applied
|
|
121
|
-
// when that stream's parser is created.
|
|
122
|
-
export type ToWorker =
|
|
123
|
-
| { type: "append"; streamId: number; chunk: string; config?: ParserConfig }
|
|
124
|
-
| { type: "finalize"; streamId: number; config?: ParserConfig }
|
|
125
|
-
| { type: "reset"; streamId: number }
|
|
126
|
-
| { type: "dispose"; streamId: number };
|
|
127
|
-
|
|
128
|
-
export type FromWorker =
|
|
129
|
-
| { type: "ready" }
|
|
130
|
-
| {
|
|
131
|
-
type: "patch";
|
|
132
|
-
streamId: number;
|
|
133
|
-
patch: Patch;
|
|
134
|
-
appendedBytes: number;
|
|
135
|
-
parseMicros: number;
|
|
136
|
-
retainedBytes: number;
|
|
137
|
-
wasmMemoryBytes: number;
|
|
138
|
-
}
|
|
139
|
-
| { type: "error"; streamId: number; message: string };
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Minimal structural interface satisfied by the DOM `Worker`. Injectable so the
|
|
143
|
-
* pool's routing/lifecycle logic can be unit-tested with a fake worker — no
|
|
144
|
-
* real Worker or WASM required.
|
|
145
|
-
*/
|
|
146
|
-
export interface WorkerLike {
|
|
147
|
-
postMessage(msg: ToWorker): void;
|
|
148
|
-
addEventListener(type: "message", listener: (ev: { data: FromWorker }) => void): void;
|
|
149
|
-
terminate(): void;
|
|
150
|
-
}
|
|
1
|
+
// Public type surface, split so framework-neutral consumers (`flux-md/client`,
|
|
2
|
+
// `flux-md/dom`) typecheck without resolving react: the neutral types live in
|
|
3
|
+
// ./types-core, the lone React-coupled `Components` type in ./types-react.
|
|
4
|
+
// Re-exported here so `flux-md/types`, index.ts, and every existing import see
|
|
5
|
+
// the identical surface as before.
|
|
6
|
+
export * from "./types-core";
|
|
7
|
+
export * from "./types-react";
|
package/src/vue.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { defineComponent, h, onMounted, onUnmounted, ref, watch } from "vue";
|
|
2
|
+
import type { PropType, Ref } from "vue";
|
|
3
|
+
import type { FluxClient } from "./client";
|
|
4
|
+
import { mountFluxMarkdown, type DomComponents, type MountHandle, type MountOptions } from "./dom";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Vue 3 bindings for {@link mountFluxMarkdown}. Thin lifecycle glue: mount the
|
|
8
|
+
* framework-neutral DOM renderer on `onMounted`, tear it down on `onUnmounted`.
|
|
9
|
+
*
|
|
10
|
+
* The renderer owns all subscribe/diffing; this layer never re-implements it
|
|
11
|
+
* and — per the renderer's contract — never calls `client.destroy()` (the
|
|
12
|
+
* caller owns the worker/stream). Shipped as plain `.ts` (no SFC compiler in
|
|
13
|
+
* the pipeline) via `defineComponent` + `h()`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Everything `mountFluxMarkdown` accepts, plus the client to subscribe to. */
|
|
17
|
+
export type UseFluxMarkdownOptions = { client: FluxClient } & MountOptions;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Composable that mounts the renderer into a container ref. Returns
|
|
21
|
+
* `{ container }` — bind it as the `ref` of the element you want filled.
|
|
22
|
+
*
|
|
23
|
+
* `getOpts` must read its fields lazily (e.g. `() => ({ client: props.client,
|
|
24
|
+
* ... })`) so the watcher sees live prop identities. We watch the five
|
|
25
|
+
* identities individually — `[client, components, sanitize, virtualize,
|
|
26
|
+
* stickToBottom]` — rather than a freshly-composed object, which would change
|
|
27
|
+
* identity every call and remount on every patch. On any of those changing we
|
|
28
|
+
* destroy and remount; `batch`/`highlightCode` still flow through to the mount
|
|
29
|
+
* but are intentionally not remount triggers.
|
|
30
|
+
*/
|
|
31
|
+
export function useFluxMarkdown(getOpts: () => UseFluxMarkdownOptions): {
|
|
32
|
+
container: Ref<HTMLElement | null>;
|
|
33
|
+
} {
|
|
34
|
+
const container = ref<HTMLElement | null>(null);
|
|
35
|
+
let handle: MountHandle | null = null;
|
|
36
|
+
|
|
37
|
+
function mount(): void {
|
|
38
|
+
if (!container.value) return;
|
|
39
|
+
const { client, ...mountOptions } = getOpts();
|
|
40
|
+
handle = mountFluxMarkdown(client, container.value, mountOptions);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function teardown(): void {
|
|
44
|
+
// handle.destroy() is the ONLY teardown — it unsubscribes and removes the
|
|
45
|
+
// renderer root. The caller owns client.destroy(); we never call it.
|
|
46
|
+
handle?.destroy();
|
|
47
|
+
handle = null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
onMounted(mount);
|
|
51
|
+
|
|
52
|
+
watch(
|
|
53
|
+
[
|
|
54
|
+
() => getOpts().client,
|
|
55
|
+
() => getOpts().components,
|
|
56
|
+
() => getOpts().sanitize,
|
|
57
|
+
() => getOpts().virtualize,
|
|
58
|
+
() => getOpts().stickToBottom,
|
|
59
|
+
],
|
|
60
|
+
() => {
|
|
61
|
+
// Only after the initial onMounted has run does `handle` exist; before
|
|
62
|
+
// that the watcher firing (it won't, being lazy) would no-op anyway.
|
|
63
|
+
teardown();
|
|
64
|
+
mount();
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Vue auto-stops this watcher when the owning component unmounts, so a manual
|
|
69
|
+
// stop is unnecessary; we only need to drop the renderer.
|
|
70
|
+
onUnmounted(teardown);
|
|
71
|
+
|
|
72
|
+
return { container };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Component wrapper around {@link useFluxMarkdown}. Renders a single `<div>`
|
|
77
|
+
* whose ref is the mount container.
|
|
78
|
+
*/
|
|
79
|
+
export const FluxMarkdown = defineComponent({
|
|
80
|
+
name: "FluxMarkdown",
|
|
81
|
+
props: {
|
|
82
|
+
client: { type: Object as PropType<FluxClient>, required: true },
|
|
83
|
+
components: { type: Object as PropType<DomComponents>, default: undefined },
|
|
84
|
+
sanitize: { type: Function as PropType<(html: string) => string>, default: undefined },
|
|
85
|
+
virtualize: { type: Boolean, default: undefined },
|
|
86
|
+
stickToBottom: { type: Boolean, default: undefined },
|
|
87
|
+
},
|
|
88
|
+
setup(props) {
|
|
89
|
+
// Read props inside the getter so the watch tracks their live identities;
|
|
90
|
+
// destructuring here would snapshot them and the watcher would never fire.
|
|
91
|
+
const { container } = useFluxMarkdown(() => ({
|
|
92
|
+
client: props.client,
|
|
93
|
+
components: props.components,
|
|
94
|
+
sanitize: props.sanitize,
|
|
95
|
+
virtualize: props.virtualize,
|
|
96
|
+
stickToBottom: props.stickToBottom,
|
|
97
|
+
}));
|
|
98
|
+
return () => h("div", { ref: container });
|
|
99
|
+
},
|
|
100
|
+
});
|
|
@@ -14,6 +14,12 @@ export class FluxParser {
|
|
|
14
14
|
* memory cost against alternatives.
|
|
15
15
|
*/
|
|
16
16
|
retainedBytes(): number;
|
|
17
|
+
/**
|
|
18
|
+
* Opt-in accessibility markup that deviates from strict GFM byte-output:
|
|
19
|
+
* `<label>`-wrap a task-list checkbox with its text, and add `scope="col"`
|
|
20
|
+
* to table header cells. Off by default (conformance output unchanged).
|
|
21
|
+
*/
|
|
22
|
+
setA11y(on: boolean): void;
|
|
17
23
|
/**
|
|
18
24
|
* Set the opt-in component-tag allowlist (e.g. `["Thinking", "Callout"]`).
|
|
19
25
|
* A `<Tag>…</Tag>` whose name is listed renders as a component whose inner
|
|
@@ -67,6 +73,7 @@ export interface InitOutput {
|
|
|
67
73
|
readonly fluxparser_finalize: (a: number, b: number) => void;
|
|
68
74
|
readonly fluxparser_new: () => number;
|
|
69
75
|
readonly fluxparser_retainedBytes: (a: number) => number;
|
|
76
|
+
readonly fluxparser_setA11y: (a: number, b: number) => void;
|
|
70
77
|
readonly fluxparser_setComponentTags: (a: number, b: number, c: number) => void;
|
|
71
78
|
readonly fluxparser_setDirAuto: (a: number, b: number) => void;
|
|
72
79
|
readonly fluxparser_setGfmAlerts: (a: number, b: number) => void;
|
package/src/wasm/flux_md_core.js
CHANGED
|
@@ -73,6 +73,15 @@ export class FluxParser {
|
|
|
73
73
|
const ret = wasm.fluxparser_retainedBytes(this.__wbg_ptr);
|
|
74
74
|
return ret >>> 0;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Opt-in accessibility markup that deviates from strict GFM byte-output:
|
|
78
|
+
* `<label>`-wrap a task-list checkbox with its text, and add `scope="col"`
|
|
79
|
+
* to table header cells. Off by default (conformance output unchanged).
|
|
80
|
+
* @param {boolean} on
|
|
81
|
+
*/
|
|
82
|
+
setA11y(on) {
|
|
83
|
+
wasm.fluxparser_setA11y(this.__wbg_ptr, on);
|
|
84
|
+
}
|
|
76
85
|
/**
|
|
77
86
|
* Set the opt-in component-tag allowlist (e.g. `["Thinking", "Callout"]`).
|
|
78
87
|
* A `<Tag>…</Tag>` whose name is listed renders as a component whose inner
|
|
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_setA11y: (a: number, b: number) => void;
|
|
10
11
|
export const fluxparser_setComponentTags: (a: number, b: number, c: number) => void;
|
|
11
12
|
export const fluxparser_setDirAuto: (a: number, b: number) => void;
|
|
12
13
|
export const fluxparser_setGfmAlerts: (a: number, b: number) => void;
|
package/src/wasm/package.json
CHANGED
package/src/worker.ts
CHANGED
|
@@ -27,8 +27,16 @@ async function setup() {
|
|
|
27
27
|
// init() returns the wasm-bindgen instance; capture its `.memory` export so
|
|
28
28
|
// we can report WASM-side memory usage on every patch. No parser yet — they
|
|
29
29
|
// are created per stream, on demand.
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
try {
|
|
31
|
+
wasmExports = await init({ module_or_path: wasmUrl });
|
|
32
|
+
post({ type: "ready" });
|
|
33
|
+
} catch (e: unknown) {
|
|
34
|
+
// WASM failed to load/instantiate: this worker can never parse anything.
|
|
35
|
+
// Report it so the pool rejects whenReady() (rather than hanging forever)
|
|
36
|
+
// and clients' onError fires. streamId is irrelevant for a worker-level
|
|
37
|
+
// failure — the pool routes a fatal error to every stream it hosts.
|
|
38
|
+
post({ type: "error", streamId: -1, message: e instanceof Error ? e.message : String(e), fatal: true });
|
|
39
|
+
}
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
function getOrCreate(streamId: number): FluxParser {
|
|
@@ -44,6 +52,7 @@ function getOrCreate(streamId: number): FluxParser {
|
|
|
44
52
|
p.setGfmFootnotes(c?.gfmFootnotes ?? false);
|
|
45
53
|
p.setGfmMath(c?.gfmMath ?? false);
|
|
46
54
|
p.setDirAuto(c?.dirAuto ?? false);
|
|
55
|
+
p.setA11y(c?.a11y ?? false);
|
|
47
56
|
p.setUnsafeHtml(c?.unsafeHtml ?? false);
|
|
48
57
|
p.setComponentTags(c?.componentTags ?? []);
|
|
49
58
|
parsers.set(streamId, p);
|