Wordgard Release 0.1
Wordgard Release 0.1
I am happy to announce that my latest project, which I’ve been talking about for years, is now out with a first release. The project is called Wordgard. It is a new iteration of a ProseMirror-style rich text editor system, integrating the things I’ve learned since stabilizing ProseMirror, nine years ago. The architecture also takes a lot of inspiration from the version 6 redesign of the CodeMirror text editor.
我很高兴地宣布,我谈论了多年的最新项目终于发布了第一个版本。该项目名为 Wordgard。它是 ProseMirror 风格富文本编辑器系统的一次新迭代,整合了我自九年前稳定 ProseMirror 以来所学到的经验。其架构也从 CodeMirror 文本编辑器的第 6 版重构中汲取了大量灵感。
Wordgard is (once again) a JavaScript library that uses the browser DOM to display its editor interface. It is licensed under an MIT license. The code is available on my Forgejo server. It’s a little concerning how I keep implementing new editors over and over (by my count this is the 6th non-trivial one). But doing this somehow hasn’t lost its charm yet. It still feels like the designs get better every iteration. I don’t expect I’ll find editor-architecture nirvana before I retire, but I’m sure I’m getting closer to it.
Wordgard(再次)是一个使用浏览器 DOM 来显示编辑器界面的 JavaScript 库。它采用 MIT 许可证。代码可在我的 Forgejo 服务器上获取。我不断地重复实现新的编辑器(据我统计,这是第 6 个非简单的项目),这确实有点令人担忧。但不知何故,这样做并没有失去它的魅力。我仍然觉得设计在每一次迭代中都在变得更好。我不指望在退休前能找到编辑器架构的“涅槃”,但我确信我正越来越接近它。
Motivation / 动机
I’m still proud of ProseMirror, and ProseMirror isn’t going anywhere—it will continue to be maintained. But there are parts of its design that make me wince every time I have to work with them, because at this point I know that I should have done them differently. Instead of trying to change ProseMirror to incorporate these new insights, I have chosen to create a completely new system with a new name. A ProseMirror 2.0 with an incompatible interface would amount to the same but make it ambiguous what people mean when referring to ProseMirror. Trying to graft stuff on in a backwards-compatible way as an 1.x version would produce a compromised win32-style mess. I’m not all that fond of the ProseMirror pun anymore either (it’s CodeMirror but for prose, get it?) So: green field full rewrite! You’ll find a lot of ideas from ProseMirror in Wordgard, but the programming interface is built from scratch, without concern for compatibility. Let’s look at the parts of ProseMirror that I think I improved on.
我仍然为 ProseMirror 感到自豪,而且 ProseMirror 不会消失——它将继续得到维护。但其设计中有些部分让我每次使用时都感到难受,因为现在我知道我本应该以不同的方式处理它们。与其试图修改 ProseMirror 来融入这些新见解,我选择创建一个全新的系统并命名。一个接口不兼容的 ProseMirror 2.0 效果一样,但会让人们在提到 ProseMirror 时产生歧义。试图以 1.x 版本的方式进行向后兼容的修补,只会产生一个妥协的、类似 Win32 的混乱产物。我也不再那么喜欢 ProseMirror 这个双关语了(它是 CodeMirror 但用于散文,明白吗?)。所以:从零开始完全重写!你会在 Wordgard 中发现许多来自 ProseMirror 的想法,但编程接口是完全从头构建的,无需考虑兼容性。让我们看看我认为在 ProseMirror 基础上有所改进的部分。
Stop Doing Steps / 停止使用“步骤” (Steps)
“Make sure you compensate for the document shift caused by the first step when adding a second.” “To figure out what range of the document was replaced, you have to iterate through the sequence of steps in both directions, mapping positions in the new document forward and positions in the old document backward.” “Yes, I’ll have one replace-around-step please.” — statements dreamed up by the utterly deranged.
“添加第二个步骤时,确保补偿由第一个步骤引起的文档偏移。”“要弄清楚文档的哪个范围被替换了,你必须在两个方向上遍历步骤序列,将新文档中的位置向前映射,将旧文档中的位置向后映射。”“是的,请给我来一个 replace-around-step。”——这些简直是疯子想出来的陈述。
ProseMirror change representation was designed by a person who was very much occupied with the problem of preserving semantic meaning for changes even if the changes were transformed, but who also didn’t have a lot of experience with change formats. Steps break down changes into atomic parts that each do a single clear thing. A given editor update might involve any number of them, each defined to act on the document produced by the one before it. They serve their purpose, but they are seriously awkward to work with.
ProseMirror 的变更表示法是由一个非常专注于“即使在变更被转换后也要保留其语义含义”这一问题的人设计的,但那个人当时在变更格式方面并没有太多经验。Steps 将变更分解为原子部分,每个部分只做一件清晰的事情。给定的编辑器更新可能涉及任意数量的步骤,每个步骤都定义为作用于前一个步骤产生的文档。它们实现了目的,但使用起来非常笨拙。
Wordgard uses a much simpler but arguably more powerful system based on my experience with CodeMirror’s change representation, which derives from the old “delta” format from ShareJS. In CodeMirror, a change is a sequence of sections, each of which either preserves a part of the old document, or replaces it with a piece of new content. So in a document of length 10, inserting an L at 4 is represented [keep 4] [replace 0 with “L”] [keep 6], and deleting the first two characters would be [replace 2 with ""] [keep 8].
Wordgard 使用了一个更简单但可以说更强大的系统,该系统基于我在 CodeMirror 变更表示法方面的经验,而该表示法源自 ShareJS 的旧“delta”格式。在 CodeMirror 中,变更是一系列片段,每个片段要么保留旧文档的一部分,要么用一段新内容替换它。因此,在长度为 10 的文档中,在位置 4 插入一个 L 表示为 [keep 4] [replace 0 with “L”] [keep 6],而删除前两个字符则为 [replace 2 with ""] [keep 8]。
Wordgard extends this with modification sections, which preserve the structure of a section, but add or remove marks to it (which are things like emphasis, link style, or image alt text). Making the word from 3 to 6 bold would be represented as [keep 3] [update 3 +bold] [keep 4]. Of course, unlike CodeMirror’s plain text, rich text content isn’t just a flat string. Because Wordgard uses a token-counting indexing system for document positions (the same system ProseMirror uses) the change format can address the document as a flat sequence of tokens (node open and close tokens, and leaf tokens), into which it splices new sequences of tokens.
Wordgard 通过“修改片段”扩展了这一点,它保留了片段的结构,但会向其添加或删除标记(如强调、链接样式或图像 alt 文本)。将第 3 到第 6 个字符的单词加粗表示为 [keep 3] [update 3 +bold] [keep 4]。当然,与 CodeMirror 的纯文本不同,富文本内容不仅仅是一个扁平的字符串。由于 Wordgard 使用标记计数索引系统来定位文档位置(与 ProseMirror 使用的系统相同),变更格式可以将文档视为扁平的标记序列(节点开始和结束标记,以及叶子标记),并在此中拼接新的标记序列。
These types of changes can easily be combined, so that a single transaction always has a single change associated with it, which is easy to inspect and reason about. They also support a limited form of operational transformation, making it possible to merge a bunch of changes that are all described in terms of the start document. That gives us an ergonomic way of describing transactions with multiple changes and makes it possible to implement collaborative editing and an undo history that supports undoing some changes but not others.
这些类型的变更可以轻松组合,因此单个事务始终关联单个变更,这易于检查和推理。它们还支持有限形式的操作转换(Operational Transformation),使得合并所有基于起始文档描述的多个变更成为可能。这为我们提供了一种描述多变更事务的便捷方式,并使得实现协作编辑以及支持撤销部分变更(而非全部)的撤销历史记录成为可能。