首页
/
工具
/
Wordgard 0.1 发布
Wordgard 0.1 发布
摘要
Wordgard 0.1 已发布,这是一个由 Marijn Haverbeke 开发的开源 JavaScript 富文本编辑器库,灵感来自 ProseMirror 和 CodeMirror。
<p><a href="https://lobste.rs/s/hejdhj/wordgard_release_0_1">评论</a></p>
查看缓存全文
缓存时间:
2026/07/02 10:10
# Wordgard 0.1 发布
来源:https://marijnhaverbeke.nl/blog/wordgard-0.1.html
很高兴宣布,我多年来一直在谈论的最新项目现已发布第一个版本。
该项目名为 **Wordgard** (https://wordgard.net/)。它是基于 ProseMirror (https://prosemirror.net/) 风格的富文本编辑器系统的新迭代,融合了自九年前稳定 ProseMirror 以来我学到的经验。其架构也很大程度上借鉴了 CodeMirror (https://codemirror.net/) 文本编辑器的 v6 重新设计。
Wordgard(再次)是一个使用浏览器 DOM 显示其编辑器界面的 JavaScript 库。它采用 MIT 许可证授权。代码可在我的 Forgejo 服务器 (https://code.haverbeke.berlin/wordgard/) 上获取。
我反复实现新编辑器的频率有点令人担忧(据我统计,这是第六个非平凡的实现)。但不知何故,这么做至今仍未失去魅力。每次迭代都感觉设计变得更好。我不指望在退休前找到编辑器架构的终极答案,但我确信正在越来越接近它。
## 动机
我仍然为 ProseMirror 感到自豪,它也不会消失——将继续得到维护。但每次处理其某些设计部分时,我都会感到畏缩,因为此刻我知道当初应该用不同的方式来做。
我没有试图改变 ProseMirror 来融入这些新见解,而是选择创建一个全新的系统,并赋予一个新名称。一个不兼容接口的 ProseMirror 2.0 本质上是相同的,但会让人们提及 ProseMirror 时产生歧义。试图以向后兼容的方式将其嫁接为 1.x 版本,则会制造出一个妥协的、Win32 式的混乱局面。我也不再那么喜欢 *ProseMirror* 这个双关语了(它是 CodeMirror,但用于散文,明白吗?)。所以:从零开始全面重写!你会在 Wordgard 中找到很多来自 ProseMirror 的想法,但编程接口是从头构建的,不考虑兼容性。
让我们看看我认为改进的 ProseMirror 的几个部分。
### 不再使用 Step
“添加第二个 step 时,确保补偿第一个 step 引起的文档偏移。” “要找出文档中被替换的范围,你必须双向遍历 step 序列,将位置映射到新文档中,并将位置映射回旧文档中。” “是的,我想要一个 replace-around-step。”—— 完全疯癫的人才会想出来的鬼话。
ProseMirror 的变更表示是由一个非常专注于保留变更语义含义(即使变更被转换)的人设计的,但此人当时对变更格式的经验也不多。Step 将变更分解为原子部分,每个部分执行一个清晰的操作。一个给定的编辑器更新可能涉及任意数量的 step,每个 step 都定义在前一个 step 产生的文档上操作。它们有自己的用途,但使用起来非常别扭。
Wordgard 基于我对 CodeMirror 变更表示的经验,使用了更简单但可以说更强大的系统,该系统源自 ShareJS 的旧“delta”格式。在 CodeMirror 中,一个变更是一系列区段,每个区段要么保留旧文档的一部分,要么用新内容替换它。因此,在一个长度为 10 的文档中,在位置 4 插入 'L' 表示为 `[keep 4] [replace 0 with "L"] [keep 6]`,删除前两个字符则表示为 `[replace 2 with ""] [keep 8]`。
Wordgard 通过修改区段扩展了这一点,这些区段保留区段的结构,但为其添加或移除标记(例如强调、链接样式或图片的 alt 文本)。将位置 3 到 6 的单词加粗表示为 `[keep 3] [update 3 +bold] [keep 4]`。
当然,与 CodeMirror 的纯文本不同,富文本内容不仅仅是一个扁平的字符串。因为 Wordgard 使用基于 token 计数的索引系统来表示文档位置(与 ProseMirror 使用的系统相同),变更格式可以将文档视为一个扁平的 token 序列(节点打开和关闭 token,以及叶子 token),并将新的 token 序列拼接进去。
这些类型的变更可以轻松组合,因此一个事务总是关联一个单一的变更,易于检查和推理。它们还支持有限形式的操作转换,使得可以合并基于起始文档描述的一批变更。这为我们提供了一种描述包含多个变更的事务的人机工程学方式,并使得实现协同编辑和支持撤销部分变更(而非全部)的撤销历史成为可能。
但文档实际上并非一个扁平的 token 序列。只有当这些 token 组合成一棵结构良好的树时才有意义。例如,如果你删除一个节点的关闭 token,token 就不再平衡,你就会创建一个无法应用的变更。因此,创建变更集的代码必须能够处理纯文本版本中不需要的一些事情——检查并修正变更以确保它们产生有效的文档结构。
这个问题在操作转换的处理中也会出现。如果你重新解释一个 step 使其能在另一个 step 之后应用,这种转换也必须保持不使文档无效。如果你需要应用转换后的 step A(在 B 之后)产生与应用转换后的 step B(在 A 之后)相同的文档(通常需要这样做,才能使得这种机制有用),情况就变得更加微妙。Wordgard 的变更模型所做的是,在转换变更时,推导出一个修正变更,以纠正组合 step 的结果,使得它对 A-over-B 产生的修正与对 B-over-A 产生的修正相同(通过小心控制修正生成代码的输入)。如果不进行修正,由于转换算法提供的保证,这两个序列都会产生相同的可能无效的文档。通过将两者与相同的修正组合,它们都会产生相同的有效文档。大多数变更实际上不需要修正,但这种方法确保那些需要的变更仍然能够收敛。
### 模式组合
因为 ProseMirror 文档模式以相当直接的方式指定节点之间的关系,设置它们通常需要手动完成。节点和标记类型仅存在于给定的模式内——你可以共享它们的定义对象或部分定义,但不同模式之间没有可用的节点标识。
Wordgard 采用不同的方法,使得节点和标记类型成为独立的事物,可以成为不同文档模式的一部分。这使得这些对象可以作为类型化的、支持自动补全的句柄来使用,代表节点或标记类型,并使组合模式更容易,只需将需要的元素组合在一起即可。
仍然存在一些情况需要直接更改节点或标记之间的某些关系。为此,模式可以覆盖现有元素上的这些关系。节点或标记的定义指定其默认内容或目标类型,但想要以不同方式使用该元素的模式可以更改这些。
这使得内置基本节点可以做更多事情,从而使得直接为这些节点提供编辑支持扩展和系统集成(例如菜单按钮)更加可行。ProseMirror 一直受到模式过于通用的问题困扰,以至于很难提供可重用的功能——代码要么特定于某个模式,要么根本不理解任何模式元素的含义。这在 Wordgard 中要好得多。
ProseMirror 中的另一个模式组合问题是节点属性的定义方式。诸如文本对齐或 alt 文本之类的内容直接在目标节点中定义为节点属性,从而将它们与节点类型紧密耦合。这使得向模式添加诸如对齐或文本方向之类的功能变得很繁琐,因为你需要将该属性添加到每个文本块节点。
将标记泛化,使其可以用于此类目的,使得以模块化方式添加此类功能变得容易得多。节点类型本身甚至不需要知道哪些标记正在针对它们。
### 内容约束
使用正则表达式指定给定父节点允许的内容是 ProseMirror 的标志性特性。Wordgard 不再支持这一点。节点的内容描述只能约束它支持的子节点类型,而不能约束它们的顺序。
有几个不同的原因。最大的原因是:为 ProseMirror 编写通用的文档操作代码太难了。如果代码不是为特定模式编写的,它几乎无法假设哪些转换是有效的,并且需要根据内容约束检查它做的每一件事。这变得如此微妙且繁重,以至于我自己也经常搞错。如果设计这个系统的人都不能使用它,这不是一个好兆头。
除此之外,我确信,用这类约束硬锁定文档形状通常会损害用户体验。文档是通过一系列微小的编辑操作来编辑的,这些操作最终形成预期的形状。如果你的编辑器不允许用户将文档置于临时异常形状的中间步骤,这往往会让他们感到沮丧。
确实存在 ProseMirror 的内容约束效果非常好的情况。但这通常涉及大量精心的设计和脚本编写,以确保编辑体验良好。
Wordgard 采用更宽松的系统,鼓励对文档形状采取不那么僵硬的方法。对于那些确实需要指定超出模式规则提供的不变量情况,它提供了一个称为“修正”(correction)的抽象层,这是一种以编程方式修复你不希望允许的文档形状的方法。这些方法的优点是它们是程序,因此可以比内容表达式强制执行更智能、更上下文感知地修正文档。它们也适用于即使是 ProseMirror 的约束也无法表达的事情,例如确保表格是矩形的。
### 扩展系统
与我们在 CodeMirror 6 中最终所做的相比,ProseMirror 的扩展系统相当粗糙。你有可以影响系统的插件,并且你可以通过更改它们的顺序来在一定程度上影响插件优先级。但是,因为每个插件都会做许多不同的事情,你很容易陷入一种情况:你需要一个插件在某个钩子中具有低优先级,但在另一个钩子中具有高优先级。
基于**面**(facets)(https://marijnhaverbeke.nl/blog/facets.html) 的 CodeMirror 系统使扩展变得更加细粒度,并允许每个扩展值设置自己的优先级类别。Facet 是类型化的扩展点,可以由任何代码定义,而不仅仅是库本身。因此编辑器扩展可以定义自己的扩展点,这出奇地有用。
Wordgard 几乎完全复制了 CodeMirror 的这套系统,包括其状态更新和重新配置机制。
在这个系统中,配置不是插件数组,而是一棵扩展树,每个扩展可以做从定义事件处理器到配置编辑器属性再到添加新的编辑器状态的任何事情。一个特定的功能实现通常由一组协同工作以产生所需行为的扩展组成。
大多数情况下,你可以直接将这样的扩展包放入配置中,无需过多思考,它们就能很好地协同工作,因为扩展所依赖的基本原语已经被定义成可以干净地组合。
### 对浏览器的依赖
人们在 ProseMirror 中遇到的许多问题都与它将对选择的处理委托给浏览器原生实现的方式有关。这种方法并非不合理。正确处理双向文本以及可能以奇怪方式样式化的内容中的光标移动很困难。想法是让浏览器来做,观察它的行为,然后更新我们自己的选择模型来跟随它。
不幸的是,浏览器的表现并不如预期。它们会拒绝将光标移动到某些类型的内容上,有时根本不绘制光标,其他时候绘制在错误的位置,并且如果你用奇怪的方式操作,鼠标选择拖拽手势会出错。因此,Wordgard 迎难而上,几乎自己处理所有基于指针和键盘的选择。
这涉及到处理双向文本(这对其他目的也很有用),形成某种内容布局的模型,并自己绘制光标。
唯一不幸无法做到这一点的是触摸选择。你可以做得不错,但这似乎会不可逆地破坏原生上下文菜单,而在手机和平板系统上没有它会很困难。因此触摸选择是原生的。幸运的是,它往往比键盘选择的奇怪错误行为要少。
在过去 9 年中,浏览器在对编辑事件(特别是 `beforeinput`)的一致支持方面取得了很大进展。实际的世界范围测试将必须证明这是否真的可行,但到目前为止,Wordgard 似乎可以做到不依赖 ProseMirror 所依赖的技巧——即监视文档 DOM 的变化并解析更改的内容来构建实际的文档变更。Wordgard 只是处理 `beforeinput` 事件来处理除组合文本输入之外的所有事情。这避免了一整类混乱的变通方法。
## 状态
Wordgard 比我之前的项目在宣布时更进一步——其核心界面几乎支持我想要的所有功能,我已经编写了一堆扩展来确认我的设计是实用的。文档仍然有些粗糙,但像参考手册之类的东西是完整且可用的。你可以从 npm 仓库安装 `wordgard`,并阅读如何使用它的**网站** (https://wordgard.net/)。
话虽如此,根据我的经验,很多问题只有在人们开始使用系统进行实际工作时才会暴露出来。我自己还有一些东西想添加到系统中,我希望现在它公开了,其他人也会开始研究它。尽管这不是我构建的第一个编辑器,但我确信进一步的见解将要求我重新思考公共接口的部分内容。我将发布的第一个版本定为 0.1,并且会在一段时间内(可能至少一年)停留在 0.x 版本上,以收集反馈,修复错误,并帮助我理清系统中粗糙的角落。
我以 MIT 许可证发布代码,就像我之前项目那样。我曾认真考虑使用更严格的授权风格,以便对自己的工作有更多控制权,但我意识到我主要关心的是这个项目能被广泛使用。我的工作模式一直是富足模式——为全世界提供足够的价值,即使只有一小部分价值回流到我这里,也足以谋生。我不想把自己的事业变成制造人为稀缺、扮演守门人或强制执行复杂许可证的样子。遗憾的是,宽松许可证将允许我不太喜欢的公司使用该软件(通常甚至不付钱给我),但这是套餐的一部分。
说到我不喜欢的公司——无论许可证如何,我很清楚,"AI"秃鹫会吸走……
相似文章
Hacker News Top
Wordgard 是一个新的开源JavaScript富文本编辑器库,来自Marijn Haverbeke,旨在提供强大的编程接口,用于构建定制化的内容编辑器。
Lobsters Hottest
Gram 2.0.0 是一款面向开发者的代码编辑器,现已发布,带来了更新的默认设置、改进的语言服务器管理、平滑滚动以及 Markdown 预览中的 Mermaid 图表支持。
X AI KOLs Timeline
marka.md is a cross-platform Markdown editor specialized for AI context management, built with Tauri, React, and TypeScript. It features live preview, Vim mode, themes, and a context tray to bundle notes for AI chats like Claude, ChatGPT, and Gemini.
Reddit r/LocalLLaMA
作者介绍了 TextWeb,这是一个开源工具,它将网页渲染为 Markdown 格式供 LLM 处理,而非使用昂贵的大视觉模型,该工具支持命令行界面 (CLI) 和 MCP 服务器。
Lobsters Hottest
Aaron Swartz 宣布发布 Markdown——他与 John Gruber 共同开发的轻量级文本转 HTML 工具,以及配套的 html2text 转换器。