Markdown.tsx

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