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

TL;DR

你好!我叫谢尔盖·马赫纳特金,在 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 文件, 不管创建时使用的是哪种模式。

下面是可视化编辑器的样子:可以通过按钮对文本进行格式化:

Full screen image

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

Full screen image

Gravity UI 中 Markdown Editor 的能力

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

扩展

编辑器内置了很多扩展与配置。例如 Mermaid 图表与 HTML 区块:

Full screen image
Full screen image

我们努力让编辑器的内核易于扩展。开发者可以创建自己的扩展或附加功能,用来:

  • 添加新实体——区块(block)或文本修饰符(mark);
  • 进一步配置 markdown 解析器;
  • 添加 actions,以便从外部与编辑器交互;
  • 丰富界面能力,例如输入斜杠时显示可用命令菜单;
  • 修改现有行为,例如插入图片与文件,并把它们上传到存储。

下面是我们为 Wiki 开发的一些扩展示例:

  • 协作编辑模式;
Full screen image
Full screen image
Full screen image
Full screen image
  • includes(包含);
Full screen image
  • 章节结构;
Full screen image
  • 用于创建便捷网格的分区;
Full screen image
  • 带预览的 markdown 模式;
Full screen image
  • 以及更多。
Full screen image

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

也可以输入 / 来唤起扩展菜单。

Full screen image

预设

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

不使用预设的编辑器:

Full screen image

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

Full screen image

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

Full screen image

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

Full screen image

完整预设 包含更多元素。

Full screen image

架构

编辑器在 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 应用中接入编辑器,以及如何接入各种扩展:例如 YandexGPTMermaidLaTeX

自定义扩展的集成

编辑器已经集成了一些额外扩展。但如果这还不够,开发者可以向编辑器的 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,这对我们很重要 :)

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

Sign in to save this post