1import ReactMarkdown from "react-markdown";
2import rehypeAutolinkHeadings from "rehype-autolink-headings";
3import rehypeExternalLinks from "rehype-external-links";
4import rehypeRaw from "rehype-raw";
5import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
6import rehypeSlug from "rehype-slug";
7import remarkEmoji from "remark-emoji";
8import remarkGfm from "remark-gfm";
9
10import { cn } from "@/lib/utils";
11
12// Sanitization schema: start from the safe default and allow a small set of
13// presentational/structural HTML tags commonly found in READMEs.
14// Script, style, iframe, object, embed and event-handler attributes are
15// blocked by the default schema and remain blocked.
16// rehype-autolink-headings injects <a> with aria-hidden and class, so we
17// allow those attributes on anchors.
18const sanitizeSchema = {
19 ...defaultSchema,
20 tagNames: [...(defaultSchema.tagNames ?? []), "details", "summary", "picture", "source"],
21 attributes: {
22 ...defaultSchema.attributes,
23 a: [...(defaultSchema.attributes?.a ?? []), "aria-hidden", "class"],
24 "*": [...(defaultSchema.attributes?.["*"] ?? []), "id"],
25 },
26};
27
28interface MarkdownProps {
29 content: string;
30 className?: string;
31}
32
33// Renders a Markdown string with GitHub-flavoured extensions (tables, task
34// lists, strikethrough). Used in Timeline comments and NewBugPage preview.
35export function Markdown({ content, className }: MarkdownProps) {
36 return (
37 <div
38 className={cn(
39 "prose prose-sm dark:prose-invert max-w-none",
40 "prose-pre:bg-muted prose-pre:text-foreground",
41 "prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:rounded-sm prose-code:text-sm prose-code:before:content-none prose-code:after:content-none",
42 "prose-img:inline prose-img:my-0",
43 className,
44 )}
45 >
46 <ReactMarkdown
47 remarkPlugins={[remarkGfm, remarkEmoji]}
48 rehypePlugins={[
49 rehypeRaw,
50 [rehypeSanitize, sanitizeSchema],
51 rehypeSlug,
52 [rehypeAutolinkHeadings, { behavior: "append" }],
53 [rehypeExternalLinks, { target: "_blank", rel: ["noopener", "noreferrer"] }],
54 ]}
55 >
56 {content}
57 </ReactMarkdown>
58 </div>
59 );
60}