2f11d69
import { useMemo } from 'react'
import DOMPurify from 'dompurify'
import { marked } from 'marked'
import { useSpaLinkClick } from '@/shared/lib'
marked.use({ gfm: true, breaks: true })
/**
* AI 生成的 Markdown 渲染器。
*
* 模型输出的是 Markdown(总结、评审结论、release 正文),必须按 Markdown
* 渲染而不是当纯文本显示。模型输出属于不可信内容,因此渲染结果一律经
* DOMPurify 清洗:脚本、事件属性与 `javascript:` 链接都会被剥离。
*/
export function Markdown({
children,
className = '',
compact = false,
inline = false,
}: {
children: string
className?: string
/** 行内发现等小字号场景。 */
compact?: boolean
/** 只要行内结果(标题、单行字段):无 `<p>` 包裹,渲染成 `<span>`。 */
inline?: boolean
}) {
const html = useMemo(
() => {
// `async: false` is what makes both calls synchronous; the cast only
// undoes TypeScript's widening of the option literal to `boolean`.
const raw = (
inline ? marked.parseInline(children) : marked.parse(children, { async: false })
) as string
return DOMPurify.sanitize(raw)
},
[children, inline],
)
// 渲染出的链接不允许直接跳转:站内走 react-router,站外开新标签页。
const onClick = useSpaLinkClick()
const Tag = inline ? 'span' : 'div'
return (
<Tag
className={`gitdata-markdown ${compact ? 'gitdata-markdown-compact' : ''} ${className}`}
onClick={onClick}
dangerouslySetInnerHTML={{ __html: html }}
/>
)
}