forked from vercel/streamdown
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.tsx
More file actions
124 lines (108 loc) · 4.35 KB
/
index.tsx
File metadata and controls
124 lines (108 loc) · 4.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"use client";
import { memo, useId, useMemo } from "react";
import ReactMarkdown, { type Options } from "react-markdown";
import rehypeKatex from "rehype-katex";
import remarkGfm from "remark-gfm";
// import remarkMath from "remark-math";
// import type { BundledTheme } from "shiki";
// import "katex/dist/katex.min.css";
import "@speed-highlight/core/themes/github-dark.css";
import hardenReactMarkdownImport from "harden-react-markdown";
import type { Options as RemarkGfmOptions } from "remark-gfm";
// import type { Options as RemarkMathOptions } from "remark-math";
import { components as defaultComponents } from "./lib/components";
import { parseMarkdownIntoBlocks } from "./lib/parse-blocks";
import { parseIncompleteMarkdown } from "./lib/parse-incomplete-markdown";
import { cn } from "./lib/utils";
type HardenReactMarkdownProps = Options & {
defaultOrigin?: string;
allowedLinkPrefixes?: string[];
allowedImagePrefixes?: string[];
};
// Handle both ESM and CJS imports
const hardenReactMarkdown =
// biome-ignore lint/suspicious/noExplicitAny: "this is needed."
(hardenReactMarkdownImport as unknown as { default: typeof hardenReactMarkdownImport }).default || hardenReactMarkdownImport;
// Create a hardened version of ReactMarkdown
const HardenedMarkdown: ReturnType<typeof hardenReactMarkdown> =
hardenReactMarkdown(ReactMarkdown);
export type StreamdownProps = HardenReactMarkdownProps & {
parseIncompleteMarkdown?: boolean;
className?: string;
};
type BlockProps = HardenReactMarkdownProps & {
content: string;
shouldParseIncompleteMarkdown: boolean;
};
// const remarkMathOptions: RemarkMathOptions = {
// singleDollarTextMath: false,
// };
const remarkGfmOptions: RemarkGfmOptions = {};
const Block = memo(
({ content, shouldParseIncompleteMarkdown, ...props }: BlockProps) => {
const parsedContent = useMemo(
() =>
typeof content === "string" && shouldParseIncompleteMarkdown
? parseIncompleteMarkdown(content.trim())
: content,
[content, shouldParseIncompleteMarkdown]
);
return <HardenedMarkdown {...props}>{parsedContent}</HardenedMarkdown>;
},
(prevProps, nextProps) => prevProps.content === nextProps.content
);
Block.displayName = "Block";
export const Streamdown = memo(
({
children,
allowedImagePrefixes = ["*"],
allowedLinkPrefixes = ["*"],
defaultOrigin,
parseIncompleteMarkdown: shouldParseIncompleteMarkdown = true,
components,
rehypePlugins,
remarkPlugins,
className,
...props
}: StreamdownProps) => {
// Parse the children to remove incomplete markdown tokens if enabled
const generatedId = useId();
const blocks = useMemo(
() =>
parseMarkdownIntoBlocks(typeof children === "string" ? children : ""),
[children]
);
const rehypeKatexPlugin = useMemo(
() => () => rehypeKatex({ errorColor: "var(--color-muted-foreground)" }),
[]
);
return (
<div className={cn("space-y-4", className)} {...props}>
{blocks.map((block, index) => (
<Block
allowedImagePrefixes={allowedImagePrefixes}
allowedLinkPrefixes={allowedLinkPrefixes}
components={{
...defaultComponents,
...components,
}}
content={block}
defaultOrigin={defaultOrigin}
// biome-ignore lint/suspicious/noArrayIndexKey: "required"
key={`${generatedId}-block_${index}`}
rehypePlugins={[rehypeKatexPlugin, ...(rehypePlugins ?? [])]}
remarkPlugins={[
[remarkGfm, remarkGfmOptions],
// [remarkMath, remarkMathOptions],
...(remarkPlugins ?? []),
]}
shouldParseIncompleteMarkdown={shouldParseIncompleteMarkdown}
/>
))}
</div>
);
},
(prevProps, nextProps) =>
prevProps.children === nextProps.children
);
Streamdown.displayName = "Streamdown";