避免客户端覆盖 SSR 高亮;空行撑高;收起后 scrollIntoView 到代码块头部。 Co-authored-by: Cursor <cursoragent@cursor.com>
52 lines
1.5 KiB
TypeScript
52 lines
1.5 KiB
TypeScript
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 from "rehype-pretty-code";
|
||
import type { Root } from "hast";
|
||
import { mergeMarkdownComponents } from "./mergeComponents";
|
||
|
||
export const prettyCodeOptions = {
|
||
theme: {
|
||
light: "github-light",
|
||
dark: "github-dark-dimmed",
|
||
},
|
||
keepBackground: false,
|
||
defaultLang: "plaintext",
|
||
// 空行在 display:grid 下会塌缩成 0 高,补一个空格撑住行高
|
||
onVisitLine(element: { children: unknown[] }) {
|
||
if (element.children.length === 0) {
|
||
element.children = [{ type: "text", value: " " }];
|
||
}
|
||
},
|
||
} as const;
|
||
|
||
/** 服务端异步管线:GFM + Shiki 双主题高亮 → React 树 */
|
||
export async function renderMarkdownToReact(
|
||
content: string,
|
||
overrides?: Components
|
||
): Promise<ReactElement> {
|
||
const processor = unified()
|
||
.use(remarkParse)
|
||
.use(remarkGfm)
|
||
.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;
|
||
}
|