@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.
- package/README.md +163 -0
- package/dist/adapters/PreOverride.d.ts +27 -0
- package/dist/adapters/PreOverride.d.ts.map +1 -0
- package/dist/adapters/PreOverride.js +31 -0
- package/dist/adapters/PreOverride.js.map +1 -0
- package/dist/adapters/code-adapter.d.ts +22 -0
- package/dist/adapters/code-adapter.d.ts.map +1 -0
- package/dist/adapters/code-adapter.js +75 -0
- package/dist/adapters/code-adapter.js.map +1 -0
- package/dist/adapters/components-adapter.d.ts +18 -0
- package/dist/adapters/components-adapter.d.ts.map +1 -0
- package/dist/adapters/components-adapter.js +34 -0
- package/dist/adapters/components-adapter.js.map +1 -0
- package/dist/defaults.d.ts +18 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +37 -0
- package/dist/defaults.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/memoization.d.ts +10 -0
- package/dist/memoization.d.ts.map +1 -0
- package/dist/memoization.js +30 -0
- package/dist/memoization.js.map +1 -0
- package/dist/primitives/StreamdownText.d.ts +60 -0
- package/dist/primitives/StreamdownText.d.ts.map +1 -0
- package/dist/primitives/StreamdownText.js +124 -0
- package/dist/primitives/StreamdownText.js.map +1 -0
- package/dist/types.d.ts +356 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +93 -0
- package/src/__tests__/PreOverride.test.tsx +132 -0
- package/src/__tests__/code-adapter.integration.test.tsx +325 -0
- package/src/__tests__/code-adapter.test.tsx +46 -0
- package/src/__tests__/components-adapter.test.tsx +152 -0
- package/src/__tests__/defaults.test.ts +96 -0
- package/src/__tests__/index.test.ts +40 -0
- package/src/__tests__/memoization.test.ts +71 -0
- package/src/adapters/PreOverride.tsx +52 -0
- package/src/adapters/code-adapter.tsx +148 -0
- package/src/adapters/components-adapter.tsx +51 -0
- package/src/defaults.ts +46 -0
- package/src/index.ts +45 -0
- package/src/memoization.ts +38 -0
- package/src/primitives/StreamdownText.tsx +201 -0
- 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., ` */
|
|
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 };
|