Markdown.tsx

 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}