import { useSpaLinkClick } from '@app/lib' import DOMPurify from 'dompurify' import { marked } from 'marked' import { useMemo } from 'react' marked.use({ gfm: true, breaks: true }) /** * Markdown 渲染器。 * * 模型输出的是 Markdown(总结、评审结论、release 正文),必须按 Markdown * 渲染而不是当纯文本显示。模型输出属于不可信内容,因此渲染结果一律经 * DOMPurify 清洗:脚本、事件属性与 `javascript:` 链接都会被剥离。 */ export function Markdown({ children, className = '', compact = false, inline = false, }: { children: string className?: string /** 行内发现等小字号场景。 */ compact?: boolean /** 只要行内结果(标题、单行字段):无 `
` 包裹,渲染成 ``。 */
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 (