1import { Text, Heading, Link as RadixLink, Table } from "@radix-ui/themes";
2import { MarkdownAsync } from "react-markdown";
3import remarkGfm from "remark-gfm";
4import rehypeRaw from "rehype-raw";
5import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
6import rehypeStarryNight from "rehype-starry-night";
7import "@/styles/MarkdownViewer.css";
8
9interface MarkdownViewerProps {
10 content: string;
11}
12
13// Utility function to convert heading text to URL-friendly ID
14function generateHeadingId(text: string): string {
15 return text
16 .toLowerCase()
17 .replace(/[^\w\s-]/g, "") // Remove special characters except spaces and hyphens
18 .replace(/\s+/g, "-") // Replace spaces with hyphens
19 .replace(/-+/g, "-") // Replace multiple hyphens with single hyphen
20 .trim();
21}
22
23export async function MarkdownViewer({ content }: MarkdownViewerProps) {
24 if (typeof content !== "string") {
25 throw new Error(
26 `MarkdownViewer expects string content, got ${typeof content}`
27 );
28 }
29
30 return (
31 <MarkdownAsync
32 remarkPlugins={[remarkGfm]}
33 rehypePlugins={[
34 rehypeRaw,
35 [
36 rehypeSanitize,
37 // Sanitization schema that allows images while maintaining security
38 {
39 ...defaultSchema,
40 attributes: {
41 ...defaultSchema.attributes,
42 img: ["src", "alt", "title", "width", "height"],
43 },
44 tagNames: [...(defaultSchema.tagNames || []), "img"],
45 },
46 ],
47 // rehypeStarryNight,
48 ]}
49 components={{
50 h1: ({ children }) => (
51 <Heading size="8" mb="4" id={generateHeadingId(String(children))}>
52 {children}
53 </Heading>
54 ),
55 h2: ({ children }) => (
56 <Heading size="7" mb="3" id={generateHeadingId(String(children))}>
57 {children}
58 </Heading>
59 ),
60 h3: ({ children }) => (
61 <Heading size="6" mb="2" id={generateHeadingId(String(children))}>
62 {children}
63 </Heading>
64 ),
65 h4: ({ children }) => (
66 <Heading size="5" mb="2" id={generateHeadingId(String(children))}>
67 {children}
68 </Heading>
69 ),
70 h5: ({ children }) => (
71 <Heading size="4" mb="2" id={generateHeadingId(String(children))}>
72 {children}
73 </Heading>
74 ),
75 h6: ({ children }) => (
76 <Heading size="3" mb="2" id={generateHeadingId(String(children))}>
77 {children}
78 </Heading>
79 ),
80 p: ({ children }) => (
81 <Text as="p" mb="3">
82 {children}
83 </Text>
84 ),
85 a: ({ href, children }) => (
86 <RadixLink href={href}>{children}</RadixLink>
87 ),
88 table: ({ children }) => (
89 <Table.Root mb="3" variant="surface">
90 {children}
91 </Table.Root>
92 ),
93 thead: ({ children }) => <Table.Header>{children}</Table.Header>,
94 tbody: ({ children }) => <Table.Body>{children}</Table.Body>,
95 tr: ({ children }) => <Table.Row>{children}</Table.Row>,
96 th: ({ children }) => (
97 <Table.ColumnHeaderCell>{children}</Table.ColumnHeaderCell>
98 ),
99 td: ({ children }) => <Table.Cell>{children}</Table.Cell>,
100 }}
101 >
102 {content}
103 </MarkdownAsync>
104 );
105}
1import { Text, Heading, Link as RadixLink, Table } from "@radix-ui/themes";
2import { MarkdownAsync } from "react-markdown";
3import remarkGfm from "remark-gfm";
4import rehypeRaw from "rehype-raw";
5import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
6import rehypeStarryNight from "rehype-starry-night";
7import "@/styles/MarkdownViewer.css";
8
9interface MarkdownViewerProps {
10 content: string;
11}
12
13// Utility function to convert heading text to URL-friendly ID
14function generateHeadingId(text: string): string {
15 return text
16 .toLowerCase()
17 .replace(/[^\w\s-]/g, "") // Remove special characters except spaces and hyphens
18 .replace(/\s+/g, "-") // Replace spaces with hyphens
19 .replace(/-+/g, "-") // Replace multiple hyphens with single hyphen
20 .trim();
21}
22
23export async function MarkdownViewer({ content }: MarkdownViewerProps) {
24 if (typeof content !== "string") {
25 throw new Error(
26 `MarkdownViewer expects string content, got ${typeof content}`
27 );
28 }
29
30 return (
31 <MarkdownAsync
32 remarkPlugins={[remarkGfm]}
33 rehypePlugins={[
34 rehypeRaw,
35 [
36 rehypeSanitize,
37 // Sanitization schema that allows images while maintaining security
38 {
39 ...defaultSchema,
40 attributes: {
41 ...defaultSchema.attributes,
42 img: ["src", "alt", "title", "width", "height"],
43 },
44 tagNames: [...(defaultSchema.tagNames || []), "img"],
45 },
46 ],
47 // rehypeStarryNight,
48 ]}
49 components={{
50 h1: ({ children }) => (
51 <Heading size="8" mb="4" id={generateHeadingId(String(children))}>
52 {children}
53 </Heading>
54 ),
55 h2: ({ children }) => (
56 <Heading size="7" mb="3" id={generateHeadingId(String(children))}>
57 {children}
58 </Heading>
59 ),
60 h3: ({ children }) => (
61 <Heading size="6" mb="2" id={generateHeadingId(String(children))}>
62 {children}
63 </Heading>
64 ),
65 h4: ({ children }) => (
66 <Heading size="5" mb="2" id={generateHeadingId(String(children))}>
67 {children}
68 </Heading>
69 ),
70 h5: ({ children }) => (
71 <Heading size="4" mb="2" id={generateHeadingId(String(children))}>
72 {children}
73 </Heading>
74 ),
75 h6: ({ children }) => (
76 <Heading size="3" mb="2" id={generateHeadingId(String(children))}>
77 {children}
78 </Heading>
79 ),
80 p: ({ children }) => (
81 <Text as="p" mb="3">
82 {children}
83 </Text>
84 ),
85 a: ({ href, children }) => (
86 <RadixLink href={href}>{children}</RadixLink>
87 ),
88 table: ({ children }) => (
89 <Table.Root mb="3" variant="surface">
90 {children}
91 </Table.Root>
92 ),
93 thead: ({ children }) => <Table.Header>{children}</Table.Header>,
94 tbody: ({ children }) => <Table.Body>{children}</Table.Body>,
95 tr: ({ children }) => <Table.Row>{children}</Table.Row>,
96 th: ({ children }) => (
97 <Table.ColumnHeaderCell>{children}</Table.ColumnHeaderCell>
98 ),
99 td: ({ children }) => <Table.Cell>{children}</Table.Cell>,
100 }}
101 >
102 {content}
103 </MarkdownAsync>
104 );
105}