import type { Components } from "react-markdown"; import type { ReactElement } from "react"; import { Fragment, jsx, jsxs } from "react/jsx-runtime"; import { toJsxRuntime } from "hast-util-to-jsx-runtime"; import { unified } from "unified"; import remarkParse from "remark-parse"; import remarkGfm from "remark-gfm"; import remarkRehype from "remark-rehype"; import rehypePrettyCode, { type Options as PrettyCodeOptions } from "rehype-pretty-code"; import type { Root } from "hast"; import type { Root as MdastRoot } from "mdast"; import { visit } from "unist-util-visit"; import { mergeMarkdownComponents } from "./mergeComponents"; import { getPrettyCodeHighlighter, resolveShikiLang, SHIKI_THEMES, } from "./shikiHighlighter"; /** * 将代码围栏语言归一到白名单(未知 → plaintext), * 避免 rehype-pretty-code 对未装载语言 loadLanguage 失败。 */ function remarkNormalizeCodeLang() { return (tree: MdastRoot) => { visit(tree, "code", (node) => { node.lang = resolveShikiLang(node.lang); }); }; } export const prettyCodeOptions: PrettyCodeOptions = { theme: { light: SHIKI_THEMES.light, dark: SHIKI_THEMES.dark, }, keepBackground: false, defaultLang: "plaintext", // HighlighterCore 与 pretty-code 期望的 Highlighter 在运行时兼容 getHighlighter: getPrettyCodeHighlighter as unknown as PrettyCodeOptions["getHighlighter"], // 空行在 display:grid 下会塌缩成 0 高,补一个空格撑住行高 onVisitLine(element) { if (element.children.length === 0) { element.children = [{ type: "text", value: " " }]; } }, }; /** 服务端异步管线:GFM + Shiki 双主题高亮 → React 树 */ export async function renderMarkdownToReact( content: string, overrides?: Components ): Promise { const processor = unified() .use(remarkParse) .use(remarkGfm) .use(remarkNormalizeCodeLang) .use(remarkRehype) .use(rehypePrettyCode, { ...prettyCodeOptions }); const mdast = processor.parse(content || ""); const hast = (await processor.run(mdast)) as Root; return toJsxRuntime(hast, { Fragment, jsx, jsxs, components: mergeMarkdownComponents(overrides) as Record< string, React.ElementType >, }) as ReactElement; }