flux-md 0.5.1 → 0.6.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.
@@ -0,0 +1,138 @@
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
+ /** Pass raw HTML through unescaped. Default false. **Never enable for untrusted input.** */
93
+ unsafeHtml?: boolean;
94
+ /**
95
+ * Opt-in allowlist of custom component tag names (e.g. `["Thinking",
96
+ * "Callout"]`). A `<Tag>…</Tag>` whose name is listed renders as a component
97
+ * whose inner content is parsed as **markdown** — safely, without `unsafeHtml`
98
+ * (the tag is allowlisted and its attributes are sanitized: event handlers
99
+ * dropped, dangerous URL schemes neutralized). The block is dispatched by the
100
+ * renderer via `components[tag]` (or `components.Component`). Empty/omitted =
101
+ * off. Names match case-sensitively.
102
+ */
103
+ componentTags?: string[];
104
+ }
105
+
106
+ // Each message carries a `streamId` so one worker can multiplex many parsers
107
+ // (the worker pool). `ready` is the exception — it's worker-level (WASM loaded),
108
+ // not stream-level. The first message for a stream may carry `config`, applied
109
+ // when that stream's parser is created.
110
+ export type ToWorker =
111
+ | { type: "append"; streamId: number; chunk: string; config?: ParserConfig }
112
+ | { type: "finalize"; streamId: number; config?: ParserConfig }
113
+ | { type: "reset"; streamId: number }
114
+ | { type: "dispose"; streamId: number };
115
+
116
+ export type FromWorker =
117
+ | { type: "ready" }
118
+ | {
119
+ type: "patch";
120
+ streamId: number;
121
+ patch: Patch;
122
+ appendedBytes: number;
123
+ parseMicros: number;
124
+ retainedBytes: number;
125
+ wasmMemoryBytes: number;
126
+ }
127
+ | { type: "error"; streamId: number; message: string };
128
+
129
+ /**
130
+ * Minimal structural interface satisfied by the DOM `Worker`. Injectable so the
131
+ * pool's routing/lifecycle logic can be unit-tested with a fake worker — no
132
+ * real Worker or WASM required.
133
+ */
134
+ export interface WorkerLike {
135
+ postMessage(msg: ToWorker): void;
136
+ addEventListener(type: "message", listener: (ev: { data: FromWorker }) => void): void;
137
+ terminate(): void;
138
+ }
@@ -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
- 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
- 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
+ });
Binary file