
Markdown 编辑器:基于 Gravity UI 构建的 WYSIWYG 和标记编辑器
Markdown 编辑器:基于 Gravity UI 构建的 WYSIWYG 和标记编辑器

TL;DR
- 支持同时在 WYSIWYG 模式与 markdown 标记模式下工作(带预览与分屏)。
- 开箱即用支持大量区块(blocks)。
- 可扩展功能——WYSIWYG 模式下有扩展系统(extension system)。
- 为 React 应用场景而设计。
- 使用来自 Gravity UI 的主题化与组件。
- 完全基于开源技术构建(ProseMirror、CodeMirror、markdown‑it、Diplodoc、Gravity UI)。
- 符合 CommonMark 标准,支持标准 markdown 语言以及 Yandex Flavored Markdown (YFM)。
你好!我叫谢尔盖·马赫纳特金,在 Yandex Cloud 的 User Experience 部门担任开发者。去年我们曾撰文介绍过我们的设计系统与组件库 Gravity UI。从那之后,该系统多次更新并新增了许多功能。今天我想介绍一个新工具——Markdown Editor,它能显著简化文档编写与维护的流程。
我们将聊聊该用户界面的诞生背景、架构特性,以及集成与自定义扩展开发的技术细节,最后再解释为什么这一切都以开源形式提供。
顺便说一句,你可以在这里试用该工具:
为什么我们需要自己的 Markdown Editor
为了方便存储与结构化企业信息, 我们开发了 Wiki 平台,用来 创建知识库。除了知识库之外,我们也在推进诸如 Docs as Code 这样的文档实践:文档与代码在文件存储中 并排共存(.md 文件)。由此诞生了平台 Diplodoс。
Wiki 与 Diplodoc 的共同点在于,两者都使用一种 markdown 方言—— Yandex Flavored Markdown (YFM)。该方言也被用于 Nebius、Bitrix、DoubleCloud、Mappable、Meteum。
随着时间推移,我们发现有两类用户对写作与编辑的过程 有着不同的理解。一类用户希望在编辑文本时立即看到最终结果, 就像 MS Word、Confluence 或 Notion 那样。另一类用户只信任标记语言, 更倾向于用 markdown 来排版页面。我们没有找到能同时支持 WYSIWYG/markdown 双模式的知名库。例如 Notion 只有 WYSIWYG, 而代码编辑器通常只有 markdown 与预览模式。
因此我们开发了一个 markdown 编辑器,它能够同时在两种模式下工作: 可视化(WYSIWYG)与标记(markdown)。在第一种模式中,工具栏的图标 帮助用户为文本添加格式;在第二种模式中,用户可以手动编辑 markdown 代码。此外,我们的方案会将文档保存为 md 文件, 不管创建时使用的是哪种模式。
下面是可视化编辑器的样子:可以通过按钮对文本进行格式化:

而下面是标记模式:格式化元素通过特殊符号来表示:

Gravity UI 中 Markdown Editor 的能力
编辑器符合 CommonMark 标准,支持标准 markdown 与 YFM。我们还加入了把语法扩展到其他 markdown 方言的能力,例如 GitHub Flavored Markdown。同时,编辑器允许从标记模式切换到 WYSIWYG 模式,而文档将以 md 标记或扩展 md(例如使用 YFM 时)形式存储。
扩展
编辑器内置了很多扩展与配置。例如 Mermaid 图表与 HTML 区块:


我们努力让编辑器的内核易于扩展。开发者可以创建自己的扩展或附加功能,用来:
- 添加新实体——区块(block)或文本修饰符(mark);
- 进一步配置 markdown 解析器;
- 添加 actions,以便从外部与编辑器交互;
- 丰富界面能力,例如输入斜杠时显示可用命令菜单;
- 修改现有行为,例如插入图片与文件,并把它们上传到存储。
下面是我们为 Wiki 开发的一些扩展示例:
- 协作编辑模式;

- draw.io 图表区块;

- YandexGPT 插件;


- includes(包含);

- 章节结构;

- 用于创建便捷网格的分区;

- 带预览的 markdown 模式;

- 以及更多。

标记可以自动转换。如果你更喜欢不使用鼠标,在可视化编辑器模式中 也提供了特殊符号,允许直接在文本中应用标记。 例如,** 会在 WYSIWYG 模式下将文本切换为粗体。 借助这些符号,可以对文本进行格式化,创建行内代码与代码块。
也可以输入 / 来唤起扩展菜单。

预设
编辑器允许为每个项目单独配置工具栏,但也提供了一些现成的配置——预设(presets)。
不使用预设的编辑器:

CommonMark 预设 支持标准 markdown 元素:粗体、斜体、标题、列表、链接、引用、代码块。

在 默认预设 中,还会出现删除线文本,以及一个单元格只能包含纯文本的表格。 该预设对应标准 markdown-it。

如上所述,编辑器也支持 YFM,因此可以与 Diplodoc 很好地集成。 在 YFM 预设 中会出现额外元素:增强表格、插入文件与图片、复选框、cut、tabs、等宽字体。

完整预设 包含更多元素。

架构
编辑器在 WYSIWYG 模式下的核心基于知名库 ProseMirror; 在标记模式下使用 CodeMirror。 ProseMirror 支持带格式的编辑,而 CodeMirror 适用于需要处理 未标记文本的场景。
我们选择这些库,是因为它们由同一位作者开发,在架构与实现方式上保持一致, 有庞大社区支持,被许多编辑器采用,并且针对文本处理做了良好优化。 例如:用于对文档应用变更的事务(transaction)系统、用于 view 的装饰(decoration)、 DOM 虚拟化,以及对多种编程语言语法的支持。
集成
我们的编辑器可以很容易地以 React hook 的形式接入:
import React from 'react';
import {useMarkdownEditor, MarkdownEditorView} from '@gravity-ui/markdown-editor';
import {toaster} from '@gravity-ui/uikit/toaster-singleton-react-18';
function Editor({onSubmit}) {
const editor = useMarkdownEditor({allowHTML: false});
React.useEffect(() => {
function submitHandler() {
// Serialize current content to markdown markup
const value = editor.getValue();
onSubmit(value);
}
editor.on('submit', submitHandler);
return () => {
editor.off('submit', submitHandler);
};
}, [onSubmit]);
return <MarkdownEditorView stickyToolbar autofocus toaster={toaster} editor={editor} />;
}
我们使用 GravityUI 的 uikit 组件库。 这能保证整个界面保持一致,并符合统一的样式规范。 使用这些组件也能为用户提供高一致性与可识别性,从而让编辑体验更顺畅。
我们提供了详细 说明文档 介绍如何在 React 应用中接入编辑器,以及如何接入各种扩展:例如 YandexGPT、 Mermaid 或 LaTeX。
自定义扩展的集成
编辑器已经集成了一些额外扩展。但如果这还不够,开发者可以向编辑器的 WYSIWYG 模式添加自己的扩展。扩展能带来什么,我们在上文已讨论过。
如果你想添加新的区块或文本修饰符,首先需要通过 configureMd 方法配置内部的 markdown-it 实例。然后用 addNode 或 addMark 方法把新实体注册进去:传入实体名称,以及一个回调函数,该回调返回一个包含三个必填字段的对象:
import insPlugin from 'markdown-it-ins';
export const underlineMarkName = 'ins';
export const UnderlineSpecs: ExtensionAuto = (builder) => {
builder
.configureMd((md) => md.use(insPlugin))
.addMark(underlineMarkName, () => ({
spec: {
parseDOM: [{tag: 'ins'}, {tag: 'u'}],
toDOM() {
return ['ins'];
},
},
toMd: {open: '++', close: '++', mixable: true, expelEnclosingWhitespace: true},
fromMd: {tokenSpec: {name: underlineMarkName, type: 'mark'}},
}));
};
spec— ProseMirror 的规范(specification);fromMd— 将 markdown 标记解析为 ProseMirror 内部表示的配置;toMd— 将该实体序列化为 markdown 标记的配置。
例如,下方是下划线文本扩展的配置。它还可以通过 addAction 方法添加 action 来进一步扩展:
import {toggleMark} from 'prosemirror-commands';
const undAction = 'underline';
builder
.addAction(undAction, ({schema}) => ({
isActive: (state) => Boolean(isMarkActive(state, markType)),
isEnable: toggleMark(underlineType(schema)),
run: toggleMark(underlineType(schema)),
})
)
该 action 可以在代码中这样调用:
// editor —— 调用 useMarkdownEditor 得到的编辑器实例
editor.actions.underline.run(),
在文档中可以查看创建新扩展的完整指南。
我们持续拓展编辑器的使用边界:目前正在开发一个用于 VS Code 的插件,让你可以直接在 VS Code 中以便捷的 WYSIWYG 模式编辑 md 文件。我们还计划加入完整的移动端模式,这将使用户只需一部手机也能在我们的编辑器中工作。
我们的编辑器并非一蹴而就:它是经验与知识长期积累的结果。我们自豪的是,编辑器完全建立在开源产品之上,其中包括可靠且久经验证的 ProseMirror、CodeMirror、markdown-it,以及我们自己的开发成果——Diplodoc 与 Gravity UI。
你也可以随时为编辑器的发展做出贡献:提交 Pull Request,或者帮助解决 Issues 中列出的现有问题。你的支持与新视角将帮助我们把编辑器做得更好。如果你觉得这个项目有用,也欢迎在我们的 GitHub 仓库 点个 Star,这对我们很重要 :)

谢尔盖·马赫纳特金
界面开发者