安装

npm i @rtdui/md-editor

tip: 记得安装@rtdui/hooks@rtdui/core配对依赖, 如果还未安装的话.

介绍

@rtdui/md-editor 包导出了两个组件: MdEditor 和 MdViewer.

  • MdEditor 组件用于书写markdown文本, 并带有实时预览功能.
  • MdViewer 组件用于渲染markdown, 例如渲染从后台加载的markdown文本供阅读.

MdEditor

功能:

  • 响应式布局
    小屏幕使用tab标签布局, 大屏幕使用分区布局
  • 在分区布局下, 用户可拖拉分隔线调整分区大小
  • 插件系统
    除了Markdown标准语法, 其它功能可通过插件方式扩展.
    已内置了常用的插件: 回车换行, GFM扩展语法, 围栏代码块语法高亮, Katex数学公式, Mermaid图表, 自动生成目录索引, Gemoji表情
    也支持自定义插件, 兼容unified生态系统中的 remarkrehype 生态
  • 本地化支持
    预置了英文(默认)和简体中文, 其它区域语言可通过locale属性设置.
  • 默认安全
    默认会自动进行消毒处理防止跨站脚本攻击(XSS), 无需额外的清理步骤。
  • 明/暗主题适配
  • 自定义图片上传处理
  • 使用 Typescript 编写, 具有完备的类型系统

MdEditor的使用

import { MdEditor, UploadResult } from "@rtdui/md-editor";
// 需要额外导入外部依赖包中的样式.
import "allotment/dist/style.css";
import "katex/dist/katex.min.css";

const handleImageUpload = async (files: File[]) => {
  const formdata = new FormData();
  files.forEach((d) => formdata.append("upload", d));
  const res = await fetch("<your_upload_server>", {
    method: "POST",
    body: formdata,
  });
  const result = await res.json();
  return result as UploadResult[];
};

const md = `# 目录

# 语法示例
...
`;

export default function MdEditorDemo() {
  const [value, setValue] = useState(md);
  return (
    <MdEditor
      handleImageUpload={handleImageUpload}
      value={value}
      onChange={setValue}
    />
  );
}
在新窗口打开

MdViewer

MdViewer应该和MdEditor使用一致的插件集, 这样才能保证编辑时的预览和阅读时保持一致.

import { MdViewer } from "@rtdui/md-editor";
// 需要额外导入外部依赖包中的样式.
import "katex/dist/katex.min.css";

const md = `# 目录

# 语法示例
...
`;

export default function MdEditorDemo() {
  return <MdViewer value={md} />;
}
在新窗口打开

本地化

@rtdui/md-editor 包已附带了英文和简体中文

默认使用英文, 使用英文无需额外的操作.

使用内置的简体中文:

import { MdEditor, MdViewer, zhLocale } from "@rtdui/md-editor";

<MdEditor locale={zhLocale} />
<MdViewer locale={zhLocale} />

使用自定义的本地化:

import { MdEditor, MdViewer, type Locale } from "@rtdui/md-editor";

const myLocale: Locale = {
  // ...
}

<MdEditor locale={myLocale} />
<MdViewer locale={myLocale} />

自定义插件

Plugin接口类型:

interface Plugin {
  /**
   * 使用Remark生态系统中的扩展
   *
   * https://github.com/remarkjs/remark/blob/main/doc/plugins.md
   */
  remark?: (p: Processor) => Processor;
  /**
   * 使用Rehype生态系统中的扩展
   *
   * https://github.com/rehypejs/rehype/blob/main/doc/plugins.md
   */
  rehype?: (p: Processor) => Processor;
  /**
   * 用于工具栏中的图标及处理
   */
  toolbar?: ToolbarItem[];

  /**
   * 编辑器预览和查看器中的副作用函数. 当不能在rehype扩展中处理时的自定义渲染.
   */
  viewerEffect?(ctx: ViewerContext): void | (() => void);
}

插件使用unified系统中的Remark生态和Rehype生态, 示意图如下:

每个红框表示的就是插件钩子生效的位置.

tips: 应该优先使用rehype生态中的扩展插件, 因为使用rehype生态中的扩展插件可以支持SSR, 只有当rehype生态扩展无法满足时, 才使用viewerEffect作为最后的选择, 使用viewerEffect只支持CSR(客户端渲染).

实现支持Katex数学公式支持的例子:

tip: 组件已集成了该功能, 此处仅作为例子

编写插件

import { wrapText, appendBlock, type Plugin } from "@rtdui/md-editor";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import { IconSum } from "@tabler/icons-react";

export default function mathPlugin(): Plugin {
  return {
    remark: (processor) => processor.use(remarkMath),
    rehype: (processor) => processor.use(rehypeKatex),
    toolbar: [
      {
        type: "multiple",
        icon: <IconSum size={iconSize} stroke={iconStroke} />,
        title: locale.math,
        actions: [
          {
            title: locale.inline,
            icon: "$",
            cheatsheet: `$${locale.inlineText}$`,
            click: (e, { editor }) => {
              wrapText(editor, "$");
            },
          },
          {
            title: locale.block,
            icon: "$$",
            cheatsheet: `$$↵${locale.blockText}↵$$`,
            click: (e, { editor }) => {
              appendBlock(editor, "\\TeX", {
                prefix: "$$\n",
                suffix: "\n$$",
              });
            },
          },
        ],
      },
    ],
  };
}

如果不使用rehype生态扩展, 则可以使用viewerEffect:

import type { Plugin } from "@rtdui/md-editor";
import remarkMath from "remark-math";
- import rehypeKatex from "rehype-katex";

export default function mathPlugin(): Plugin {
  return {
    remark: (processor) => processor.use(remarkMath),
-   rehype: (processor) => processor.use(rehypeKatex),
+   viewerEffect({ markdownBody }) {
+     const renderMath = async (selector: string, displayMode: boolean) => {
+       const katex = await import('katex').then((m) => m.default)
+
+       const els = markdownBody.querySelectorAll<HTMLElement>(selector)
+       els.forEach((el) => {
+         katex.render(el.innerText, el, { displayMode })
+       })
+     }
+
+     renderMath('.math.math-inline', false)
+     renderMath('.math.math-display', true)
+   },
  };
}

使用插件

import { MdEditor, MdViewer } from "@rtdui/md-editor";
import mathPlugin from "./mathPlugin";

export default function EditorDemo() {
  return <MdEditor plugins={[mathPlugin()]} />;
}

// editor 和 viewer 应同时应用插件
export default function ViewerDemo() {
  return <MdViewer plugins={[mathPlugin()]} />;
}