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";89interface MarkdownViewerProps {10 content: string;11}1213// Utility function to convert heading text to URL-friendly ID14function generateHeadingId(text: string): string {15 return text16 .toLowerCase()17 .replace(/[^\w\s-]/g, "") // Remove special characters except spaces and hyphens18 .replace(/\s+/g, "-") // Replace spaces with hyphens19 .replace(/-+/g, "-") // Replace multiple hyphens with single hyphen20 .trim();21}2223export async function MarkdownViewer({ content }: MarkdownViewerProps) {24 if (typeof content !== "string") {25 throw new Error(26 `MarkdownViewer expects string content, got ${typeof content}`27 );28 }2930 return (31 <MarkdownAsync32 remarkPlugins={[remarkGfm]}33 rehypePlugins={[34 rehypeRaw,35 [36 rehypeSanitize,37 // Sanitization schema that allows images while maintaining security38 {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";89interface MarkdownViewerProps {10 content: string;11}1213// Utility function to convert heading text to URL-friendly ID14function generateHeadingId(text: string): string {15 return text16 .toLowerCase()17 .replace(/[^\w\s-]/g, "") // Remove special characters except spaces and hyphens18 .replace(/\s+/g, "-") // Replace spaces with hyphens19 .replace(/-+/g, "-") // Replace multiple hyphens with single hyphen20 .trim();21}2223export async function MarkdownViewer({ content }: MarkdownViewerProps) {24 if (typeof content !== "string") {25 throw new Error(26 `MarkdownViewer expects string content, got ${typeof content}`27 );28 }2930 return (31 <MarkdownAsync32 remarkPlugins={[remarkGfm]}33 rehypePlugins={[34 rehypeRaw,35 [36 rehypeSanitize,37 // Sanitization schema that allows images while maintaining security38 {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}