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}