新增: - BrandCropModal 品牌图裁剪弹窗 + cropImage 工具 - Markdown 行内短代码(inlineShortcodes + remark 插件) - MdDetails 折叠块、MdVideo 视频块组件 - directiveBlocks/inlineShortcodes 测试 其它: - 聊天后端(handler/service chat)调整 - Markdown 渲染链路、编辑器、全局样式与品牌相关面板更新
76 lines
2.3 KiB
TypeScript
76 lines
2.3 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, { 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 { remarkInlineShortcodes } from "./remarkInlineShortcodes";
|
||
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<ReactElement> {
|
||
const processor = unified()
|
||
.use(remarkParse)
|
||
.use(remarkGfm)
|
||
.use(remarkInlineShortcodes)
|
||
.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;
|
||
}
|