@kushagradhawan/kookie-blocks 0.1.9 → 0.1.11
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/dist/cjs/components/code/CodeBlock.js +1 -1
- package/dist/cjs/components/code/CodeBlock.js.map +3 -3
- package/dist/cjs/components/index.d.ts +1 -0
- package/dist/cjs/components/index.d.ts.map +1 -1
- package/dist/cjs/components/index.js +1 -1
- package/dist/cjs/components/index.js.map +2 -2
- package/dist/cjs/components/markdown/StreamingMarkdown.d.ts +78 -0
- package/dist/cjs/components/markdown/StreamingMarkdown.d.ts.map +1 -0
- package/dist/cjs/components/markdown/StreamingMarkdown.js +2 -0
- package/dist/cjs/components/markdown/StreamingMarkdown.js.map +7 -0
- package/dist/cjs/components/markdown/createMarkdownComponents.d.ts +27 -0
- package/dist/cjs/components/markdown/createMarkdownComponents.d.ts.map +1 -0
- package/dist/cjs/components/markdown/createMarkdownComponents.js +3 -0
- package/dist/cjs/components/markdown/createMarkdownComponents.js.map +7 -0
- package/dist/cjs/components/markdown/index.d.ts +6 -0
- package/dist/cjs/components/markdown/index.d.ts.map +1 -0
- package/dist/cjs/components/markdown/index.js +2 -0
- package/dist/cjs/components/markdown/index.js.map +7 -0
- package/dist/cjs/components/markdown/types.d.ts +32 -0
- package/dist/cjs/components/markdown/types.d.ts.map +1 -0
- package/dist/cjs/components/markdown/types.js +2 -0
- package/dist/cjs/components/markdown/types.js.map +7 -0
- package/dist/cjs/components/markdown/utils/markdownStreaming.d.ts +32 -0
- package/dist/cjs/components/markdown/utils/markdownStreaming.d.ts.map +1 -0
- package/dist/cjs/components/markdown/utils/markdownStreaming.js +5 -0
- package/dist/cjs/components/markdown/utils/markdownStreaming.js.map +7 -0
- package/dist/esm/components/code/CodeBlock.js +1 -1
- package/dist/esm/components/code/CodeBlock.js.map +3 -3
- package/dist/esm/components/index.d.ts +1 -0
- package/dist/esm/components/index.d.ts.map +1 -1
- package/dist/esm/components/index.js +1 -1
- package/dist/esm/components/index.js.map +2 -2
- package/dist/esm/components/markdown/StreamingMarkdown.d.ts +78 -0
- package/dist/esm/components/markdown/StreamingMarkdown.d.ts.map +1 -0
- package/dist/esm/components/markdown/StreamingMarkdown.js +2 -0
- package/dist/esm/components/markdown/StreamingMarkdown.js.map +7 -0
- package/dist/esm/components/markdown/createMarkdownComponents.d.ts +27 -0
- package/dist/esm/components/markdown/createMarkdownComponents.d.ts.map +1 -0
- package/dist/esm/components/markdown/createMarkdownComponents.js +3 -0
- package/dist/esm/components/markdown/createMarkdownComponents.js.map +7 -0
- package/dist/esm/components/markdown/index.d.ts +6 -0
- package/dist/esm/components/markdown/index.d.ts.map +1 -0
- package/dist/esm/components/markdown/index.js +2 -0
- package/dist/esm/components/markdown/index.js.map +7 -0
- package/dist/esm/components/markdown/types.d.ts +32 -0
- package/dist/esm/components/markdown/types.d.ts.map +1 -0
- package/dist/esm/components/markdown/types.js +1 -0
- package/dist/esm/components/markdown/types.js.map +7 -0
- package/dist/esm/components/markdown/utils/markdownStreaming.d.ts +32 -0
- package/dist/esm/components/markdown/utils/markdownStreaming.d.ts.map +1 -0
- package/dist/esm/components/markdown/utils/markdownStreaming.js +5 -0
- package/dist/esm/components/markdown/utils/markdownStreaming.js.map +7 -0
- package/package.json +10 -1
- package/src/components/code/CodeBlock.tsx +6 -6
- package/src/components/index.ts +1 -0
- package/src/components/markdown/StreamingMarkdown.tsx +183 -0
- package/src/components/markdown/createMarkdownComponents.tsx +206 -0
- package/src/components/markdown/index.ts +8 -0
- package/src/components/markdown/types.ts +31 -0
- package/src/components/markdown/utils/markdownStreaming.ts +297 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { memo, useMemo, type ReactNode } from "react";
|
|
4
|
+
import ReactMarkdown, { type Components } from "react-markdown";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
import rehypeRaw from "rehype-raw";
|
|
7
|
+
import hardenReactMarkdown from "harden-react-markdown";
|
|
8
|
+
import { Box, Flex } from "@kushagradhawan/kookie-ui";
|
|
9
|
+
import { createMarkdownComponents } from "./createMarkdownComponents";
|
|
10
|
+
import { completeUnterminatedMarkdown, parseMarkdownIntoBlocks } from "./utils/markdownStreaming";
|
|
11
|
+
import type { MarkdownComponentOptions } from "./types";
|
|
12
|
+
|
|
13
|
+
const HardenedMarkdown = hardenReactMarkdown(ReactMarkdown);
|
|
14
|
+
|
|
15
|
+
const LINK_PREFIXES = ["https://", "http://", "/"];
|
|
16
|
+
const IMAGE_PREFIXES = ["https://", "http://", "/", "data:"];
|
|
17
|
+
const ALLOWED_PROTOCOLS = ["mailto:", "tel:", "data:", "http:", "https:"];
|
|
18
|
+
const DEFAULT_APP_ORIGIN = typeof window !== "undefined" && window.location?.origin ? window.location.origin : "https://app.kookie.ai";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Options for StreamingMarkdown component
|
|
22
|
+
*/
|
|
23
|
+
export type StreamingMarkdownOptions = MarkdownComponentOptions & {
|
|
24
|
+
/**
|
|
25
|
+
* Security origin for link/image validation
|
|
26
|
+
* @default window.location.origin or "https://app.kookie.ai"
|
|
27
|
+
*/
|
|
28
|
+
defaultOrigin?: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether to enable block-level memoization for performance
|
|
32
|
+
* Recommended for streaming scenarios where content updates frequently
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
enableBlockMemoization?: boolean;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Custom parser for splitting content into blocks
|
|
39
|
+
* If not provided, content will be rendered as a single block
|
|
40
|
+
* For optimal streaming performance, use marked.lexer with GFM enabled
|
|
41
|
+
*/
|
|
42
|
+
blockParser?: (content: string) => Array<{ raw?: string }>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Override default component mappings
|
|
46
|
+
*/
|
|
47
|
+
components?: Partial<Components>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
type StreamingMarkdownProps = {
|
|
51
|
+
/**
|
|
52
|
+
* Markdown content to render (supports streaming/incomplete markdown)
|
|
53
|
+
*/
|
|
54
|
+
content: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Unique identifier for this markdown instance (used for keys)
|
|
58
|
+
*/
|
|
59
|
+
id: string;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Optional configuration
|
|
63
|
+
*/
|
|
64
|
+
options?: StreamingMarkdownOptions;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type MarkdownBlockProps = {
|
|
68
|
+
content: string;
|
|
69
|
+
defaultOrigin: string;
|
|
70
|
+
components: Components;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolves the default origin for security validation
|
|
75
|
+
*/
|
|
76
|
+
function resolveDefaultOrigin(customOrigin?: string): string {
|
|
77
|
+
if (customOrigin) {
|
|
78
|
+
return customOrigin;
|
|
79
|
+
}
|
|
80
|
+
return DEFAULT_APP_ORIGIN;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Memoized markdown block component for efficient streaming rendering
|
|
85
|
+
*/
|
|
86
|
+
const MarkdownBlock = memo(
|
|
87
|
+
({ content, defaultOrigin, components }: MarkdownBlockProps) => {
|
|
88
|
+
return (
|
|
89
|
+
<Box width="100%">
|
|
90
|
+
<HardenedMarkdown
|
|
91
|
+
defaultOrigin={defaultOrigin}
|
|
92
|
+
allowedLinkPrefixes={LINK_PREFIXES}
|
|
93
|
+
allowedImagePrefixes={IMAGE_PREFIXES}
|
|
94
|
+
allowedProtocols={ALLOWED_PROTOCOLS}
|
|
95
|
+
allowDataImages
|
|
96
|
+
components={components}
|
|
97
|
+
remarkPlugins={[remarkGfm]}
|
|
98
|
+
rehypePlugins={[rehypeRaw]}
|
|
99
|
+
>
|
|
100
|
+
{content}
|
|
101
|
+
</HardenedMarkdown>
|
|
102
|
+
</Box>
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
(previous, next) => previous.content === next.content && previous.defaultOrigin === next.defaultOrigin && previous.components === next.components
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
MarkdownBlock.displayName = "MarkdownBlock";
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* StreamingMarkdown - A drop-in markdown renderer designed for AI streaming.
|
|
112
|
+
*
|
|
113
|
+
* Features:
|
|
114
|
+
* - Unterminated block parsing (handles incomplete markdown during streaming)
|
|
115
|
+
* - Block-level memoization for performance
|
|
116
|
+
* - Security hardening (validates links/images)
|
|
117
|
+
* - GitHub Flavored Markdown support
|
|
118
|
+
* - KookieUI component integration
|
|
119
|
+
* - Code syntax highlighting via CodeBlock
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```tsx
|
|
123
|
+
* import { StreamingMarkdown } from '@kushagradhawan/kookie-blocks';
|
|
124
|
+
* import { marked } from 'marked';
|
|
125
|
+
*
|
|
126
|
+
* function ChatMessage({ message }) {
|
|
127
|
+
* return (
|
|
128
|
+
* <StreamingMarkdown
|
|
129
|
+
* content={message.content}
|
|
130
|
+
* id={message.id}
|
|
131
|
+
* options={{
|
|
132
|
+
* blockParser: (content) => marked.lexer(content, { gfm: true }),
|
|
133
|
+
* enableBlockMemoization: true,
|
|
134
|
+
* }}
|
|
135
|
+
* />
|
|
136
|
+
* );
|
|
137
|
+
* }
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export function StreamingMarkdown({ content, id, options = {} }: StreamingMarkdownProps) {
|
|
141
|
+
const { defaultOrigin: customOrigin, enableBlockMemoization = true, blockParser, components: customComponents = {}, ...componentOptions } = options;
|
|
142
|
+
|
|
143
|
+
// Resolve security origin
|
|
144
|
+
const defaultOrigin = useMemo(() => resolveDefaultOrigin(customOrigin), [customOrigin]);
|
|
145
|
+
|
|
146
|
+
// Create component mappings with custom overrides
|
|
147
|
+
const markdownComponents = useMemo(() => {
|
|
148
|
+
const baseComponents = createMarkdownComponents(componentOptions);
|
|
149
|
+
return {
|
|
150
|
+
...baseComponents,
|
|
151
|
+
...customComponents,
|
|
152
|
+
};
|
|
153
|
+
}, [componentOptions, customComponents]);
|
|
154
|
+
|
|
155
|
+
// Parse content into blocks for memoization (if enabled and parser provided)
|
|
156
|
+
const blocks = useMemo(() => {
|
|
157
|
+
if (!enableBlockMemoization || !blockParser) {
|
|
158
|
+
// No block splitting - just complete unterminated markdown
|
|
159
|
+
const completed = completeUnterminatedMarkdown(content);
|
|
160
|
+
return completed.trim() ? [completed] : [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return parseMarkdownIntoBlocks(content, blockParser);
|
|
164
|
+
}, [content, enableBlockMemoization, blockParser]);
|
|
165
|
+
|
|
166
|
+
if (!blocks.length) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Single block - no need for wrapper
|
|
171
|
+
if (blocks.length === 1) {
|
|
172
|
+
return <MarkdownBlock content={blocks[0]} defaultOrigin={defaultOrigin} components={markdownComponents} />;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Multiple blocks - render with flex wrapper
|
|
176
|
+
return (
|
|
177
|
+
<Flex direction="column" gap="2" width="100%">
|
|
178
|
+
{blocks.map((block, index) => (
|
|
179
|
+
<MarkdownBlock key={`${id}-block-${index}`} content={block} defaultOrigin={defaultOrigin} components={markdownComponents} />
|
|
180
|
+
))}
|
|
181
|
+
</Flex>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import React, { type ReactNode } from "react";
|
|
2
|
+
import type { Components } from "react-markdown";
|
|
3
|
+
import { Blockquote, Box, Code, Flex, Heading, Text, Table } from "@kushagradhawan/kookie-ui";
|
|
4
|
+
import { CodeBlock } from "../code";
|
|
5
|
+
import type { MarkdownComponentOptions, MarkdownChildrenProps } from "./types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extracts language from className (e.g., "language-typescript" -> "typescript")
|
|
9
|
+
*/
|
|
10
|
+
function extractLanguage(className?: string): string {
|
|
11
|
+
if (!className) {
|
|
12
|
+
return "text";
|
|
13
|
+
}
|
|
14
|
+
const match = className.match(/language-([\w-]+)/i);
|
|
15
|
+
return match?.[1] ?? "text";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Extracts code string from ReactNode children
|
|
20
|
+
*/
|
|
21
|
+
function extractCode(children?: ReactNode): string {
|
|
22
|
+
let code = "";
|
|
23
|
+
if (!children) {
|
|
24
|
+
return code;
|
|
25
|
+
}
|
|
26
|
+
if (typeof children === "string") {
|
|
27
|
+
code = children;
|
|
28
|
+
} else if (Array.isArray(children)) {
|
|
29
|
+
code = children.map((child) => (typeof child === "string" ? child : "")).join("");
|
|
30
|
+
}
|
|
31
|
+
// Trim trailing newlines but preserve internal whitespace
|
|
32
|
+
return code.replace(/^\n+|\n+$/g, "");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Creates markdown component mappings that work with both react-markdown and MDX.
|
|
37
|
+
* Uses KookieUI components for consistent styling across all projects.
|
|
38
|
+
*
|
|
39
|
+
* @param options - Optional configuration for component behavior
|
|
40
|
+
* @returns Component mappings for markdown/MDX renderers
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* // In react-markdown
|
|
45
|
+
* <ReactMarkdown components={createMarkdownComponents()}>
|
|
46
|
+
* {content}
|
|
47
|
+
* </ReactMarkdown>
|
|
48
|
+
*
|
|
49
|
+
* // In MDX
|
|
50
|
+
* export function useMDXComponents(components: MDXComponents) {
|
|
51
|
+
* return {
|
|
52
|
+
* ...createMarkdownComponents(),
|
|
53
|
+
* ...components,
|
|
54
|
+
* };
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function createMarkdownComponents(options: MarkdownComponentOptions = {}): Components {
|
|
59
|
+
const { codeBlockCollapsible = false, imageComponent, inlineCodeHighContrast = true } = options;
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
// Headings with consistent visual hierarchy
|
|
63
|
+
h1: ({ children }: MarkdownChildrenProps) => (
|
|
64
|
+
<Heading size="8" weight="medium" as="h1" style={{ marginTop: "1rem", marginBottom: "0.5rem" }}>
|
|
65
|
+
{children}
|
|
66
|
+
</Heading>
|
|
67
|
+
),
|
|
68
|
+
h2: ({ children }: MarkdownChildrenProps) => (
|
|
69
|
+
<Heading weight="medium" size="5" as="h2" style={{ marginTop: "0.875rem", marginBottom: "0.5rem" }}>
|
|
70
|
+
{children}
|
|
71
|
+
</Heading>
|
|
72
|
+
),
|
|
73
|
+
h3: ({ children }: MarkdownChildrenProps) => (
|
|
74
|
+
<Heading weight="medium" size="4" as="h3" style={{ marginTop: "0.75rem", marginBottom: "0.5rem" }}>
|
|
75
|
+
{children}
|
|
76
|
+
</Heading>
|
|
77
|
+
),
|
|
78
|
+
h4: ({ children }: MarkdownChildrenProps) => (
|
|
79
|
+
<Heading weight="medium" size="3" as="h4" style={{ marginTop: "0.625rem", marginBottom: "0.5rem" }}>
|
|
80
|
+
{children}
|
|
81
|
+
</Heading>
|
|
82
|
+
),
|
|
83
|
+
h5: ({ children }: MarkdownChildrenProps) => (
|
|
84
|
+
<Heading weight="medium" size="3" as="h5" style={{ marginTop: "0.5rem", marginBottom: "0.5rem" }}>
|
|
85
|
+
{children}
|
|
86
|
+
</Heading>
|
|
87
|
+
),
|
|
88
|
+
h6: ({ children }: MarkdownChildrenProps) => (
|
|
89
|
+
<Heading weight="medium" size="3" as="h6" style={{ marginTop: "0.5rem", marginBottom: "0.5rem" }}>
|
|
90
|
+
{children}
|
|
91
|
+
</Heading>
|
|
92
|
+
),
|
|
93
|
+
|
|
94
|
+
// Paragraph text
|
|
95
|
+
p: ({ children }: MarkdownChildrenProps) => (
|
|
96
|
+
<Text size="3" as="p" style={{ lineHeight: "1.6" }}>
|
|
97
|
+
{children}
|
|
98
|
+
</Text>
|
|
99
|
+
),
|
|
100
|
+
|
|
101
|
+
// Code - inline vs block
|
|
102
|
+
code: ({ className, children, inline }: { className?: string; children?: ReactNode; inline?: boolean }) => {
|
|
103
|
+
const code = extractCode(children);
|
|
104
|
+
|
|
105
|
+
// Block code: has className (language) OR is not marked as inline
|
|
106
|
+
// Inline code: explicitly marked as inline=true, or no className and short single-line content
|
|
107
|
+
const isInlineCode = inline === true || (inline === undefined && !className && !code.includes("\n") && code.length < 100);
|
|
108
|
+
|
|
109
|
+
if (isInlineCode) {
|
|
110
|
+
return (
|
|
111
|
+
<Code highContrast={inlineCodeHighContrast} size="3">
|
|
112
|
+
{code}
|
|
113
|
+
</Code>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<Box my="2" style={{ minWidth: 0 }}>
|
|
119
|
+
<CodeBlock code={code} language={extractLanguage(className)} collapsible={codeBlockCollapsible} />
|
|
120
|
+
</Box>
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
// Lists
|
|
125
|
+
ul: ({ children }: MarkdownChildrenProps) => (
|
|
126
|
+
<ul style={{ marginTop: "0.5rem", marginBottom: "0.5rem", lineHeight: "1.6", paddingLeft: "1.5rem", listStyleType: "disc" }}>{children}</ul>
|
|
127
|
+
),
|
|
128
|
+
ol: ({ children }: MarkdownChildrenProps) => (
|
|
129
|
+
<ol style={{ marginTop: "0.5rem", marginBottom: "0.5rem", lineHeight: "1.6", paddingLeft: "1.5rem", listStyleType: "decimal" }}>{children}</ol>
|
|
130
|
+
),
|
|
131
|
+
li: ({ children }: MarkdownChildrenProps) => <li style={{ marginBottom: "0.25rem", lineHeight: "1.6" }}>{children}</li>,
|
|
132
|
+
|
|
133
|
+
// Blockquote
|
|
134
|
+
blockquote: ({ children }: MarkdownChildrenProps) => <Blockquote>{children}</Blockquote>,
|
|
135
|
+
|
|
136
|
+
// Links
|
|
137
|
+
a: ({ href, children }: { href?: string; children?: ReactNode }) => (
|
|
138
|
+
<a href={href} style={{ color: "var(--accent-9)", textDecoration: "underline" }}>
|
|
139
|
+
{children}
|
|
140
|
+
</a>
|
|
141
|
+
),
|
|
142
|
+
|
|
143
|
+
// Text styling
|
|
144
|
+
strong: ({ children }: MarkdownChildrenProps) => (
|
|
145
|
+
<Text weight="medium" style={{ lineHeight: "1.6" }}>
|
|
146
|
+
{children}
|
|
147
|
+
</Text>
|
|
148
|
+
),
|
|
149
|
+
em: ({ children }: MarkdownChildrenProps) => <Text style={{ lineHeight: "1.6", fontStyle: "italic" }}>{children}</Text>,
|
|
150
|
+
|
|
151
|
+
// Horizontal rule
|
|
152
|
+
hr: () => (
|
|
153
|
+
<hr
|
|
154
|
+
style={{
|
|
155
|
+
color: "var(--gray-6)",
|
|
156
|
+
marginTop: "0.5rem",
|
|
157
|
+
marginBottom: "0.5rem",
|
|
158
|
+
height: "1px",
|
|
159
|
+
width: "100%",
|
|
160
|
+
opacity: 0.5,
|
|
161
|
+
}}
|
|
162
|
+
/>
|
|
163
|
+
),
|
|
164
|
+
|
|
165
|
+
// Pre wrapper (pass through to let code handle it)
|
|
166
|
+
pre: ({ children }: MarkdownChildrenProps) => <>{children}</>,
|
|
167
|
+
|
|
168
|
+
// Tables using KookieUI
|
|
169
|
+
table: ({ children }: MarkdownChildrenProps) => (
|
|
170
|
+
<Box my="2" style={{ overflowX: "auto" }}>
|
|
171
|
+
<Table.Root size="2" variant="ghost">
|
|
172
|
+
{children}
|
|
173
|
+
</Table.Root>
|
|
174
|
+
</Box>
|
|
175
|
+
),
|
|
176
|
+
thead: ({ children }: MarkdownChildrenProps) => <Table.Header>{children}</Table.Header>,
|
|
177
|
+
tbody: ({ children }: MarkdownChildrenProps) => <Table.Body>{children}</Table.Body>,
|
|
178
|
+
tr: ({ children }: MarkdownChildrenProps) => <Table.Row>{children}</Table.Row>,
|
|
179
|
+
th: ({ children }: MarkdownChildrenProps) => <Table.ColumnHeaderCell>{children}</Table.ColumnHeaderCell>,
|
|
180
|
+
td: ({ children }: MarkdownChildrenProps) => <Table.Cell>{children}</Table.Cell>,
|
|
181
|
+
|
|
182
|
+
// HTML elements for raw HTML support
|
|
183
|
+
sub: ({ children }: MarkdownChildrenProps) => <sub>{children}</sub>,
|
|
184
|
+
sup: ({ children }: MarkdownChildrenProps) => <sup>{children}</sup>,
|
|
185
|
+
br: () => <br />,
|
|
186
|
+
|
|
187
|
+
// Images - use custom component if provided
|
|
188
|
+
img: imageComponent
|
|
189
|
+
? (props: React.ImgHTMLAttributes<HTMLImageElement>) => {
|
|
190
|
+
const { src, alt, width, height } = props;
|
|
191
|
+
if (!src || typeof src !== "string") return null;
|
|
192
|
+
return imageComponent({
|
|
193
|
+
src,
|
|
194
|
+
alt: alt ?? "Image",
|
|
195
|
+
width: width ? String(width) : undefined,
|
|
196
|
+
height: height ? String(height) : undefined,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
: undefined,
|
|
200
|
+
|
|
201
|
+
// Details/Summary for expandable sections
|
|
202
|
+
details: ({ children }: MarkdownChildrenProps) => <details style={{ padding: "0.5rem 0" }}>{children}</details>,
|
|
203
|
+
summary: ({ children }: MarkdownChildrenProps) => <summary style={{ cursor: "pointer", fontWeight: 500 }}>{children}</summary>,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { StreamingMarkdown } from "./StreamingMarkdown";
|
|
2
|
+
export type { StreamingMarkdownOptions } from "./StreamingMarkdown";
|
|
3
|
+
|
|
4
|
+
export { createMarkdownComponents } from "./createMarkdownComponents";
|
|
5
|
+
export type { MarkdownComponentOptions, MarkdownChildrenProps } from "./types";
|
|
6
|
+
|
|
7
|
+
export { completeUnterminatedMarkdown, parseMarkdownIntoBlocks } from "./utils/markdownStreaming";
|
|
8
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for customizing markdown component behavior
|
|
5
|
+
*/
|
|
6
|
+
export type MarkdownComponentOptions = {
|
|
7
|
+
/**
|
|
8
|
+
* Whether code blocks should be collapsible
|
|
9
|
+
* @default false
|
|
10
|
+
*/
|
|
11
|
+
codeBlockCollapsible?: boolean;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Custom image component
|
|
15
|
+
*/
|
|
16
|
+
imageComponent?: (props: { src?: string; alt?: string; width?: string; height?: string }) => ReactNode;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether to use high contrast for inline code
|
|
20
|
+
* @default true
|
|
21
|
+
*/
|
|
22
|
+
inlineCodeHighContrast?: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Common props for markdown child components
|
|
27
|
+
*/
|
|
28
|
+
export type MarkdownChildrenProps = {
|
|
29
|
+
children?: ReactNode;
|
|
30
|
+
};
|
|
31
|
+
|