@assistant-ui/react-streamdown 0.0.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.
Files changed (49) hide show
  1. package/README.md +163 -0
  2. package/dist/adapters/PreOverride.d.ts +27 -0
  3. package/dist/adapters/PreOverride.d.ts.map +1 -0
  4. package/dist/adapters/PreOverride.js +31 -0
  5. package/dist/adapters/PreOverride.js.map +1 -0
  6. package/dist/adapters/code-adapter.d.ts +22 -0
  7. package/dist/adapters/code-adapter.d.ts.map +1 -0
  8. package/dist/adapters/code-adapter.js +75 -0
  9. package/dist/adapters/code-adapter.js.map +1 -0
  10. package/dist/adapters/components-adapter.d.ts +18 -0
  11. package/dist/adapters/components-adapter.d.ts.map +1 -0
  12. package/dist/adapters/components-adapter.js +34 -0
  13. package/dist/adapters/components-adapter.js.map +1 -0
  14. package/dist/defaults.d.ts +18 -0
  15. package/dist/defaults.d.ts.map +1 -0
  16. package/dist/defaults.js +37 -0
  17. package/dist/defaults.js.map +1 -0
  18. package/dist/index.d.ts +9 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +7 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/memoization.d.ts +10 -0
  23. package/dist/memoization.d.ts.map +1 -0
  24. package/dist/memoization.js +30 -0
  25. package/dist/memoization.js.map +1 -0
  26. package/dist/primitives/StreamdownText.d.ts +60 -0
  27. package/dist/primitives/StreamdownText.d.ts.map +1 -0
  28. package/dist/primitives/StreamdownText.js +124 -0
  29. package/dist/primitives/StreamdownText.js.map +1 -0
  30. package/dist/types.d.ts +356 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/types.js +2 -0
  33. package/dist/types.js.map +1 -0
  34. package/package.json +93 -0
  35. package/src/__tests__/PreOverride.test.tsx +132 -0
  36. package/src/__tests__/code-adapter.integration.test.tsx +325 -0
  37. package/src/__tests__/code-adapter.test.tsx +46 -0
  38. package/src/__tests__/components-adapter.test.tsx +152 -0
  39. package/src/__tests__/defaults.test.ts +96 -0
  40. package/src/__tests__/index.test.ts +40 -0
  41. package/src/__tests__/memoization.test.ts +71 -0
  42. package/src/adapters/PreOverride.tsx +52 -0
  43. package/src/adapters/code-adapter.tsx +148 -0
  44. package/src/adapters/components-adapter.tsx +51 -0
  45. package/src/defaults.ts +46 -0
  46. package/src/index.ts +45 -0
  47. package/src/memoization.ts +38 -0
  48. package/src/primitives/StreamdownText.tsx +201 -0
  49. package/src/types.ts +416 -0
@@ -0,0 +1,201 @@
1
+ "use client";
2
+
3
+ import { INTERNAL, useMessagePartText } from "@assistant-ui/react";
4
+ import { harden } from "rehype-harden";
5
+ import rehypeRaw from "rehype-raw";
6
+ import rehypeSanitize from "rehype-sanitize";
7
+ import { Streamdown, type StreamdownProps } from "streamdown";
8
+ import { type ComponentRef, forwardRef, useMemo } from "react";
9
+ import { useAdaptedComponents } from "../adapters/components-adapter";
10
+ import { DEFAULT_SHIKI_THEME, mergePlugins } from "../defaults";
11
+ import type { SecurityConfig, StreamdownTextPrimitiveProps } from "../types";
12
+
13
+ const { useSmoothStatus } = INTERNAL;
14
+
15
+ type StreamdownTextPrimitiveElement = ComponentRef<"div">;
16
+
17
+ /**
18
+ * Builds rehypePlugins array with security configuration.
19
+ */
20
+ function buildSecurityRehypePlugins(
21
+ security: SecurityConfig,
22
+ ): NonNullable<StreamdownProps["rehypePlugins"]> {
23
+ return [
24
+ rehypeRaw,
25
+ [rehypeSanitize, {}],
26
+ [
27
+ harden,
28
+ {
29
+ allowedImagePrefixes: security.allowedImagePrefixes ?? ["*"],
30
+ allowedLinkPrefixes: security.allowedLinkPrefixes ?? ["*"],
31
+ allowedProtocols: security.allowedProtocols ?? ["*"],
32
+ allowDataImages: security.allowDataImages ?? true,
33
+ defaultOrigin: security.defaultOrigin,
34
+ blockedLinkClass: security.blockedLinkClass,
35
+ blockedImageClass: security.blockedImageClass,
36
+ },
37
+ ],
38
+ ];
39
+ }
40
+
41
+ /**
42
+ * A primitive component for rendering markdown text using Streamdown.
43
+ *
44
+ * Streamdown is optimized for AI-powered streaming with features like:
45
+ * - Block-based rendering for better streaming performance
46
+ * - Incomplete markdown handling via remend
47
+ * - Built-in syntax highlighting via Shiki
48
+ * - Math, Mermaid, and CJK support via plugins
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * // Basic usage
53
+ * <StreamdownTextPrimitive />
54
+ *
55
+ * // With plugins
56
+ * import { code } from "@streamdown/code";
57
+ * import { math } from "@streamdown/math";
58
+ *
59
+ * <StreamdownTextPrimitive
60
+ * plugins={{ code, math }}
61
+ * shikiTheme={["github-light", "github-dark"]}
62
+ * />
63
+ *
64
+ * // Disable a specific plugin
65
+ * <StreamdownTextPrimitive plugins={{ code: false }} />
66
+ *
67
+ * // Migration from react-markdown (compatibility mode)
68
+ * <StreamdownTextPrimitive
69
+ * components={{
70
+ * SyntaxHighlighter: MySyntaxHighlighter,
71
+ * CodeHeader: MyCodeHeader,
72
+ * }}
73
+ * componentsByLanguage={{
74
+ * mermaid: { SyntaxHighlighter: MermaidRenderer }
75
+ * }}
76
+ * />
77
+ * ```
78
+ */
79
+ export const StreamdownTextPrimitive = forwardRef<
80
+ StreamdownTextPrimitiveElement,
81
+ StreamdownTextPrimitiveProps
82
+ >(
83
+ (
84
+ {
85
+ // assistant-ui compatibility props
86
+ components,
87
+ componentsByLanguage,
88
+ preprocess,
89
+
90
+ // plugin configuration
91
+ plugins: userPlugins,
92
+
93
+ // container props
94
+ containerProps,
95
+ containerClassName,
96
+
97
+ // streamdown native props (explicitly listed for documentation)
98
+ caret,
99
+ controls,
100
+ linkSafety,
101
+ remend,
102
+ mermaid,
103
+ parseIncompleteMarkdown,
104
+ allowedTags,
105
+ remarkRehypeOptions,
106
+ security,
107
+ BlockComponent,
108
+ parseMarkdownIntoBlocksFn,
109
+
110
+ // streamdown props
111
+ mode = "streaming",
112
+ className,
113
+ shikiTheme,
114
+ ...streamdownProps
115
+ },
116
+ ref,
117
+ ) => {
118
+ const { text } = useMessagePartText();
119
+ const status = useSmoothStatus();
120
+
121
+ const processedText = useMemo(
122
+ () => (preprocess ? preprocess(text) : text),
123
+ [text, preprocess],
124
+ );
125
+
126
+ const resolvedPlugins = useMemo(() => {
127
+ const merged = mergePlugins(userPlugins, {});
128
+ return Object.keys(merged).length > 0 ? merged : undefined;
129
+ }, [userPlugins]);
130
+
131
+ const resolvedShikiTheme = useMemo(
132
+ () =>
133
+ shikiTheme ?? (resolvedPlugins?.code ? DEFAULT_SHIKI_THEME : undefined),
134
+ [shikiTheme, resolvedPlugins?.code],
135
+ );
136
+
137
+ const adaptedComponents = useAdaptedComponents({
138
+ components,
139
+ componentsByLanguage,
140
+ });
141
+
142
+ const mergedComponents = useMemo(() => {
143
+ const {
144
+ SyntaxHighlighter: _,
145
+ CodeHeader: __,
146
+ ...userHtmlComponents
147
+ } = components ?? {};
148
+ return { ...userHtmlComponents, ...adaptedComponents };
149
+ }, [components, adaptedComponents]);
150
+
151
+ const containerClass = useMemo(() => {
152
+ const classes = [containerClassName, containerProps?.className]
153
+ .filter(Boolean)
154
+ .join(" ");
155
+ return classes || undefined;
156
+ }, [containerClassName, containerProps?.className]);
157
+
158
+ const rehypePlugins = useMemo(
159
+ () => (security ? buildSecurityRehypePlugins(security) : undefined),
160
+ [security],
161
+ );
162
+
163
+ const optionalProps = {
164
+ ...(className && { className }),
165
+ ...(caret && { caret }),
166
+ ...(controls !== undefined && { controls }),
167
+ ...(linkSafety && { linkSafety }),
168
+ ...(remend && { remend }),
169
+ ...(mermaid && { mermaid }),
170
+ ...(parseIncompleteMarkdown !== undefined && { parseIncompleteMarkdown }),
171
+ ...(allowedTags && { allowedTags }),
172
+ ...(resolvedPlugins && { plugins: resolvedPlugins }),
173
+ ...(resolvedShikiTheme && { shikiTheme: resolvedShikiTheme }),
174
+ ...(remarkRehypeOptions && { remarkRehypeOptions }),
175
+ ...(rehypePlugins && { rehypePlugins }),
176
+ ...(BlockComponent && { BlockComponent }),
177
+ ...(parseMarkdownIntoBlocksFn && { parseMarkdownIntoBlocksFn }),
178
+ };
179
+
180
+ return (
181
+ <div
182
+ ref={ref}
183
+ data-status={status.type}
184
+ {...containerProps}
185
+ className={containerClass}
186
+ >
187
+ <Streamdown
188
+ mode={mode}
189
+ isAnimating={status.type === "running"}
190
+ components={mergedComponents}
191
+ {...optionalProps}
192
+ {...streamdownProps}
193
+ >
194
+ {processedText}
195
+ </Streamdown>
196
+ </div>
197
+ );
198
+ },
199
+ );
200
+
201
+ StreamdownTextPrimitive.displayName = "StreamdownTextPrimitive";
package/src/types.ts ADDED
@@ -0,0 +1,416 @@
1
+ import type { Element } from "hast";
2
+ import type { ComponentPropsWithoutRef, ComponentType, ReactNode } from "react";
3
+ import type { Options as RemarkRehypeOptions } from "remark-rehype";
4
+ import type {
5
+ StreamdownProps,
6
+ MermaidOptions,
7
+ MermaidErrorComponentProps,
8
+ } from "streamdown";
9
+
10
+ /**
11
+ * Caret style for streaming indicator.
12
+ */
13
+ export type CaretStyle = "block" | "circle";
14
+
15
+ /**
16
+ * Controls configuration for interactive elements.
17
+ */
18
+ export type ControlsConfig =
19
+ | boolean
20
+ | {
21
+ table?: boolean;
22
+ code?: boolean;
23
+ mermaid?:
24
+ | boolean
25
+ | {
26
+ download?: boolean;
27
+ copy?: boolean;
28
+ fullscreen?: boolean;
29
+ panZoom?: boolean;
30
+ };
31
+ };
32
+
33
+ /**
34
+ * Props passed to the link safety modal component.
35
+ */
36
+ export type LinkSafetyModalProps = {
37
+ url: string;
38
+ isOpen: boolean;
39
+ onClose: () => void;
40
+ onConfirm: () => void;
41
+ };
42
+
43
+ /**
44
+ * Configuration for link safety confirmation.
45
+ */
46
+ export type LinkSafetyConfig = {
47
+ /** Whether link safety is enabled. */
48
+ enabled: boolean;
49
+ /** Custom function to check if a link is safe. */
50
+ onLinkCheck?: (url: string) => Promise<boolean> | boolean;
51
+ /** Custom modal component for link confirmation. */
52
+ renderModal?: (props: LinkSafetyModalProps) => ReactNode;
53
+ };
54
+
55
+ /**
56
+ * Custom handler for incomplete markdown completion.
57
+ */
58
+ export type RemendHandler = {
59
+ /** Handler name for identification */
60
+ name: string;
61
+ /** Function to transform text */
62
+ handle: (text: string) => string;
63
+ /** Priority for handler execution order (lower runs first, default: 100) */
64
+ priority?: number;
65
+ };
66
+
67
+ /**
68
+ * Configuration for incomplete markdown auto-completion.
69
+ */
70
+ export type RemendConfig = {
71
+ /** Complete links (e.g., `[text](url` → `[text](streamdown:incomplete-link)`) */
72
+ links?: boolean;
73
+ /** Complete images (e.g., `![alt](url` → removed) */
74
+ images?: boolean;
75
+ /** How to handle incomplete links: 'protocol' or 'text-only' */
76
+ linkMode?: "protocol" | "text-only";
77
+ /** Complete bold formatting (e.g., `**text` → `**text**`) */
78
+ bold?: boolean;
79
+ /** Complete italic formatting (e.g., `*text` → `*text*`) */
80
+ italic?: boolean;
81
+ /** Complete bold-italic formatting (e.g., `***text` → `***text***`) */
82
+ boldItalic?: boolean;
83
+ /** Complete inline code formatting (e.g., `` `code `` → `` `code` ``) */
84
+ inlineCode?: boolean;
85
+ /** Complete strikethrough formatting (e.g., `~~text` → `~~text~~`) */
86
+ strikethrough?: boolean;
87
+ /** Complete block KaTeX math (e.g., `$$equation` → `$$equation$$`) */
88
+ katex?: boolean;
89
+ /** Handle incomplete setext headings to prevent misinterpretation */
90
+ setextHeadings?: boolean;
91
+ /** Custom handlers for incomplete markdown completion */
92
+ handlers?: RemendHandler[];
93
+ };
94
+
95
+ /**
96
+ * Props for the SyntaxHighlighter component.
97
+ * Compatible with @assistant-ui/react-markdown API.
98
+ */
99
+ export type SyntaxHighlighterProps = {
100
+ node?: Element | undefined;
101
+ components: {
102
+ Pre: ComponentType<
103
+ ComponentPropsWithoutRef<"pre"> & { node?: Element | undefined }
104
+ >;
105
+ Code: ComponentType<
106
+ ComponentPropsWithoutRef<"code"> & { node?: Element | undefined }
107
+ >;
108
+ };
109
+ language: string;
110
+ code: string;
111
+ };
112
+
113
+ /**
114
+ * Props for the CodeHeader component.
115
+ * Compatible with @assistant-ui/react-markdown API.
116
+ */
117
+ export type CodeHeaderProps = {
118
+ node?: Element | undefined;
119
+ language: string | undefined;
120
+ code: string;
121
+ };
122
+
123
+ /**
124
+ * Language-specific component overrides.
125
+ */
126
+ export type ComponentsByLanguage = Record<
127
+ string,
128
+ {
129
+ CodeHeader?: ComponentType<CodeHeaderProps> | undefined;
130
+ SyntaxHighlighter?: ComponentType<SyntaxHighlighterProps> | undefined;
131
+ }
132
+ >;
133
+
134
+ /**
135
+ * Extended components prop that includes SyntaxHighlighter and CodeHeader.
136
+ */
137
+ export type StreamdownTextComponents = NonNullable<
138
+ StreamdownProps["components"]
139
+ > & {
140
+ SyntaxHighlighter?: ComponentType<SyntaxHighlighterProps> | undefined;
141
+ CodeHeader?: ComponentType<CodeHeaderProps> | undefined;
142
+ };
143
+
144
+ /**
145
+ * Plugin configuration type.
146
+ * Set to `false` to explicitly disable a plugin.
147
+ * Set to a plugin instance to use that plugin.
148
+ *
149
+ * NOTE: Plugins are NOT auto-detected for tree-shaking optimization.
150
+ * You must explicitly import and provide them.
151
+ *
152
+ * @example
153
+ * import { code } from "@streamdown/code";
154
+ * import { math } from "@streamdown/math";
155
+ * <StreamdownTextPrimitive plugins={{ code, math }} />
156
+ */
157
+ export type PluginConfig = {
158
+ /** Code syntax highlighting plugin. Must be explicitly provided. */
159
+ code?: unknown | false | undefined;
160
+ /** Math/LaTeX rendering plugin. Must be explicitly provided. */
161
+ math?: unknown | false | undefined;
162
+ /** CJK text optimization plugin. Must be explicitly provided. */
163
+ cjk?: unknown | false | undefined;
164
+ /** Mermaid diagram plugin. Must be explicitly provided. */
165
+ mermaid?: unknown | false | undefined;
166
+ };
167
+
168
+ /**
169
+ * Resolved plugin configuration (without false values).
170
+ * This is the type passed to streamdown after processing.
171
+ */
172
+ export type ResolvedPluginConfig = NonNullable<StreamdownProps["plugins"]>;
173
+
174
+ /**
175
+ * Allowed HTML tags whitelist.
176
+ * Maps tag names to allowed attribute names.
177
+ */
178
+ export type AllowedTags = Record<string, string[]>;
179
+
180
+ /**
181
+ * Security configuration for URL validation via rehype-harden.
182
+ * Overrides streamdown's permissive defaults (allow-all policy).
183
+ *
184
+ * @example
185
+ * // Restrict to specific domains
186
+ * security={{
187
+ * allowedLinkPrefixes: ["https://example.com", "https://docs.example.com"],
188
+ * allowedImagePrefixes: ["https://cdn.example.com"],
189
+ * allowedProtocols: ["https", "mailto"],
190
+ * }}
191
+ */
192
+ export type SecurityConfig = {
193
+ /** URL prefixes allowed for links. Default: ["*"] (all) */
194
+ allowedLinkPrefixes?: string[];
195
+ /** URL prefixes allowed for images. Default: ["*"] (all) */
196
+ allowedImagePrefixes?: string[];
197
+ /** Allowed protocols (e.g., ["http", "https", "mailto"]). Default: ["*"] */
198
+ allowedProtocols?: string[];
199
+ /** Allow base64 data images. Default: true */
200
+ allowDataImages?: boolean;
201
+ /** Default origin for relative URLs */
202
+ defaultOrigin?: string;
203
+ /** CSS class for blocked links */
204
+ blockedLinkClass?: string;
205
+ /** CSS class for blocked images */
206
+ blockedImageClass?: string;
207
+ };
208
+
209
+ export type { MermaidOptions, MermaidErrorComponentProps, RemarkRehypeOptions };
210
+
211
+ /**
212
+ * Props for the BlockComponent override.
213
+ * Used to customize how individual markdown blocks are rendered.
214
+ *
215
+ * Note: This is a documentation type. The actual BlockComponent prop
216
+ * uses StreamdownProps["BlockComponent"] for type compatibility.
217
+ */
218
+ export type BlockProps = {
219
+ content: string;
220
+ shouldParseIncompleteMarkdown: boolean;
221
+ index: number;
222
+ components?: StreamdownProps["components"];
223
+ rehypePlugins?: StreamdownProps["rehypePlugins"];
224
+ remarkPlugins?: StreamdownProps["remarkPlugins"];
225
+ remarkRehypeOptions?: RemarkRehypeOptions;
226
+ };
227
+
228
+ /**
229
+ * Props for StreamdownTextPrimitive.
230
+ */
231
+ export type StreamdownTextPrimitiveProps = Omit<
232
+ StreamdownProps,
233
+ | "children"
234
+ | "components"
235
+ | "plugins"
236
+ | "caret"
237
+ | "controls"
238
+ | "linkSafety"
239
+ | "remend"
240
+ | "mermaid"
241
+ | "BlockComponent"
242
+ | "parseMarkdownIntoBlocksFn"
243
+ > & {
244
+ /**
245
+ * Custom components for rendering markdown elements.
246
+ * Includes SyntaxHighlighter and CodeHeader for code block customization.
247
+ */
248
+ components?: StreamdownTextComponents | undefined;
249
+
250
+ /**
251
+ * Language-specific component overrides.
252
+ * @example { mermaid: { SyntaxHighlighter: MermaidDiagram } }
253
+ */
254
+ componentsByLanguage?: ComponentsByLanguage | undefined;
255
+
256
+ /**
257
+ * Plugin configuration.
258
+ * Set to `false` to disable a specific plugin when using merged configs.
259
+ *
260
+ * @example
261
+ * // With plugins
262
+ * import { code } from "@streamdown/code";
263
+ * import { math } from "@streamdown/math";
264
+ * plugins={{ code, math }}
265
+ *
266
+ * @example
267
+ * // Disable a plugin in merged config
268
+ * plugins={{ code: false }}
269
+ */
270
+ plugins?: PluginConfig | undefined;
271
+
272
+ /**
273
+ * Function to transform text before markdown processing.
274
+ */
275
+ preprocess?: ((text: string) => string) | undefined;
276
+
277
+ /**
278
+ * Container element props.
279
+ */
280
+ containerProps?:
281
+ | Omit<ComponentPropsWithoutRef<"div">, "children">
282
+ | undefined;
283
+
284
+ /**
285
+ * Additional class name for the container.
286
+ */
287
+ containerClassName?: string | undefined;
288
+
289
+ /**
290
+ * Streaming caret style.
291
+ * - "block": Block cursor (▋)
292
+ * - "circle": Circle cursor (●)
293
+ */
294
+ caret?: CaretStyle | undefined;
295
+
296
+ /**
297
+ * Interactive controls configuration.
298
+ * Set to `true` to enable all controls, `false` to disable all,
299
+ * or provide an object to configure specific controls.
300
+ *
301
+ * @example
302
+ * // Enable all controls
303
+ * controls={true}
304
+ *
305
+ * @example
306
+ * // Configure specific controls
307
+ * controls={{ code: true, table: false, mermaid: { fullscreen: true } }}
308
+ */
309
+ controls?: ControlsConfig | undefined;
310
+
311
+ /**
312
+ * Link safety configuration.
313
+ * Shows a confirmation dialog before opening external links.
314
+ *
315
+ * @example
316
+ * // Disable link safety
317
+ * linkSafety={{ enabled: false }}
318
+ *
319
+ * @example
320
+ * // Custom link check
321
+ * linkSafety={{
322
+ * enabled: true,
323
+ * onLinkCheck: (url) => url.startsWith('https://trusted.com')
324
+ * }}
325
+ */
326
+ linkSafety?: LinkSafetyConfig | undefined;
327
+
328
+ /**
329
+ * Incomplete markdown auto-completion configuration.
330
+ * Controls how streaming markdown with incomplete syntax is handled.
331
+ *
332
+ * @example
333
+ * // Disable link completion
334
+ * remend={{ links: false }}
335
+ *
336
+ * @example
337
+ * // Use text-only mode for incomplete links
338
+ * remend={{ linkMode: "text-only" }}
339
+ */
340
+ remend?: RemendConfig | undefined;
341
+
342
+ /**
343
+ * Mermaid diagram configuration.
344
+ * Allows customization of Mermaid rendering.
345
+ *
346
+ * @example
347
+ * // Custom Mermaid config
348
+ * mermaid={{ config: { theme: 'dark' } }}
349
+ *
350
+ * @example
351
+ * // Custom error component
352
+ * mermaid={{ errorComponent: MyMermaidError }}
353
+ */
354
+ mermaid?: MermaidOptions | undefined;
355
+
356
+ /**
357
+ * Whether to parse incomplete markdown during streaming.
358
+ * When true, incomplete markdown syntax will be processed as-is.
359
+ * When false (default), remend will complete the syntax first.
360
+ */
361
+ parseIncompleteMarkdown?: boolean | undefined;
362
+
363
+ /**
364
+ * Allowed HTML tags whitelist.
365
+ * Maps tag names to their allowed attribute names.
366
+ * Use this to allow specific HTML tags in markdown content.
367
+ *
368
+ * @example
369
+ * allowedTags={{ div: ['class', 'id'], span: ['class'] }}
370
+ */
371
+ allowedTags?: AllowedTags | undefined;
372
+
373
+ /**
374
+ * Options passed to remark-rehype during markdown processing.
375
+ * Allows customization of the remark to rehype conversion.
376
+ *
377
+ * @example
378
+ * remarkRehypeOptions={{ allowDangerousHtml: true }}
379
+ */
380
+ remarkRehypeOptions?: RemarkRehypeOptions | undefined;
381
+
382
+ /**
383
+ * Security configuration for URL/image validation.
384
+ * Overrides streamdown's default (allow-all) policy via rehype-harden.
385
+ *
386
+ * @example
387
+ * // Restrict links to trusted domains only
388
+ * security={{
389
+ * allowedLinkPrefixes: ["https://trusted.com"],
390
+ * allowedImagePrefixes: ["https://cdn.trusted.com"],
391
+ * allowedProtocols: ["https"],
392
+ * }}
393
+ */
394
+ security?: SecurityConfig | undefined;
395
+
396
+ /**
397
+ * Custom component for rendering individual markdown blocks.
398
+ * Use this for advanced block-level customization.
399
+ *
400
+ * @example
401
+ * BlockComponent={({ content, index }) => <div key={index}>{content}</div>}
402
+ */
403
+ BlockComponent?: StreamdownProps["BlockComponent"] | undefined;
404
+
405
+ /**
406
+ * Custom function to parse markdown into blocks.
407
+ * By default, streamdown splits on double newlines.
408
+ * Use this to implement custom block splitting logic.
409
+ *
410
+ * @example
411
+ * parseMarkdownIntoBlocksFn={(md) => md.split(/\n{2,}/)}
412
+ */
413
+ parseMarkdownIntoBlocksFn?: ((markdown: string) => string[]) | undefined;
414
+ };
415
+
416
+ export type { StreamdownProps };