Plate 节点模型与亲和性(Node Model Affinity)规范:从文档修复到运行时落地的完整实践
2026/9/15 11:15:43 网站建设 项目流程

Plate 节点模型与亲和性(Node Model & Affinity)规范:从文档修复到运行时落地的完整实践

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

导读

本文基于 Plate 仓库中 editor-behavior 文档体系的一次专项规范整理(2026-04-04-node-model-affinity-spec-pass.md),系统讲解 Plate 如何把"节点原子性(atomicity)、voidness 与亲和性(affinity)"从模糊的文字描述升级为每类功能家族必须显式声明的规范字段。你将掌握:Plate 的 7 类节点模型与 4 类亲和性分类体系、文档与运行时源码交叉审计的方法、以及一次真实的"规范先说、实现后追"案例(footnote 引用从非 void 文本变为真正的 inline void atom)。文章同时结合 markdown-standards.md、markdown-editing-spec.md、editor-protocol-matrix.md 与packages/*下的插件源码,给出可在当前仓库中逐条验证的实践路径。


一、这次 Spec Pass 要解决什么问题

1.1 背景:文档在"手挥"编辑器模型

在本次专项之前,Plate 的 editor-behavior 文档已经在谈论"atoms(原子)"和 affinity,但没有建立强制约束:每个功能家族都必须声明一个显式的模型字段。结果就是规范文档在真正关键的地方"手挥"(hand-waving)——用自然语言描述节点的原子性,却不落到可测试、可交叉验证的模型声明上。

1.2 Goal 原文

本次 pass 的目标(Goal)非常明确:

Make node atomicity, voidness, and affinity explicit across the editor-behavior docs so the spec stops hand-waving over the actual editor model.

即:让节点原子性、voidness 与亲和性在全部 editor-behavior 文档中显式化,使规范不再回避真实编辑器模型。

1.3 六阶段执行计划

原文档把工作拆成六个阶段(Phase),全部标记为已完成:

阶段内容交付物
1复查当前规范在 footnote 引用、inline atoms 与 affinity 策略上的缺口缺口清单
2交叉检查当前各包表面真实的isVoidisInline、mark-affinity 声明运行时表面审计
3更新 standards 文档,要求显式的节点模型与 affinity 声明markdown-standards.md
4更新 readable spec,补充节点模型分类与修正后的家族法则markdown-editing-spec.md
5更新 parity/protocol 文档,使每个功能家族都声明自己的模型与 affinity 类markdown-parity-matrix.md、editor-protocol-matrix.md
6验证文档一致性,记录新暴露的过时声明一致性核查结果

这套"先审计、后改文档、再验证"的顺序本身就是值得复用的方法:规范改动不是拍脑袋,而是先对源码做地毯式扫描,再反向约束文档。


二、核心成果一:节点模型(Node Model)七分类

2.1 规范定义

standards 文档(markdown-standards.md)与 readable spec(markdown-editing-spec.md)中,节点模型被统一为以下 7 类:

模型类含义典型实体
block non-void可编辑的块级/容器内容段落、标题、块引用、列表项、代码块、表格、脚注定义
block void atom富文本模式下内部无光标的原子块表面TOC、分隔线(thematic break)、媒体嵌入、图片、绘图、块级公式
inline non-void span可编辑的内联内容,如链接链接、autolink 字面量
inline void atom无富文本正文的原子内联表面mention、date、脚注引用、内联公式
leaf mark由 leaf 携带的文本标记,而非独立内联元素加粗、斜体、删除线、高亮、上/下标、样式类 mark
text token保留语法的文本行为,如解析后的硬换行硬换行、emoji 短代码解析后的文本
overlay / no node不拥有文档节点的编辑器 chrome生成的 TOC 条目、Yjs 远程光标、讨论锚点

2.2 四条强制规则

规范同步立下了四条不允许绕过的规则:

  1. 不要从 UI chrome 推断原子性——界面上看起来像一个整体,不代表节点是原子的;
  2. 不要从 DOM 的contentEditable={false}推断 voidness——渲染层的小技巧不能替代编辑器节点契约;
  3. 必须使用编辑器节点契约(editor node contract),而非渲染 DOM 技巧
  4. 如果一个特性是非 void 且参与内联输入,规范必须声明它使用哪一类 affinity;而 inline void atoms 不依赖 link/mark affinity,它们作为原子自己拥有导航与边界删除行为。

这套规则直接堵死了"实现与文档各说各话"的漏洞,也为此后所有功能家族建立了统一的语言。


三、核心成果二:Affinity(亲和性)四分类

3.1 分类定义

当内联输入可能跨越某个边界时,规范要求声明亲和性类:

Affinity 类行为适用对象
directional从已格式化一侧输入会扩展该格式;从纯文本一侧输入则保持在格式之外加粗/斜体等软 mark、链接 span
hard边界输入保持在格式之外,不扩展格式化 span内联代码、kbd 等偏源码的内联节点
outward元数据范围偏向避免意外增长评论(comment)、建议(suggestion)等协作元数据 mark
none / n-a不拥有内联亲和性块级节点、void atoms、text tokens、overlays

3.2 为什么需要 affinity

readable spec 中有一句非常关键的话(markdown-editing-spec.md):

Affinity belongs here because cursor behavior changes the meaning of later typing and deletion.

光标停留在边界时的行为会改变后续输入与删除的语义。典型规则见EDIT-AFF-MARK-001**bold|**text输入x后得到**boldx**text,即 directional)、EDIT-AFF-LINK-001(链接侧进入扩展链接,纯文本侧进入则留在外面)、EDIT-AFF-HARD-001`code|`text输入后得到`code`xtext,即 hard)。

3.3 源码中的 affinity 实现

在 packages/core/src/lib/plugins/affinity/AffinityPlugin.ts 中,affinity 被实现为一个独立的 core 插件:

  • affinity值为'backward' | 'forward',用于描述删除操作后光标应该贴向哪一侧;
  • 删除跨过 mark 边界时,若删除的是右侧字符则 affinity 为forward,删除的是左侧 mark 字符则为backward
  • 插件通过rules.selection?.affinity读取每个节点类型声明的亲和性(AffinityPlugin.ts)。

isNodeAffinity查询(packages/core/src/lib/plugins/affinity/queries/isNodeAffinity.ts)把规范中的三类值'directional' | 'hard' | 'outward'与插件规则直接对接,形成了"文档分类 ↔ 源码类型"的一一映射。


四、运行时表面审计:每类功能家族的模型归属

本次 pass 最重要的发现之一是:运行时表面是混合的(Runtime surfaces are mixed)。原文档 Findings 明确列出了当时的真实状态:

  • linksinline non-void span+directionalaffinity;
  • mention / date / inline equationinline void atom
  • TOC、thematic break、media embed、file/audio/video、image、drawing、block equationblock void atom
  • 许多格式化 marks 已经在插件规则中暴露 selection affinity(directionalhardoutward)。

protocol matrix 中有一张"实体模型映射表"(Entity Model Map),是这次审计沉淀下来的权威清单(editor-protocol-matrix.md),摘录关键行:

家族实体节点模型亲和性/边界策略
markdown-native段落 / 标题 / 块引用 / 列表项block non-voidn/a
markdown-native链接inline non-void spandirectional
markdown-native图片block void media atomn/a
markdown-native软 mark / 硬 markleaf markdirectional/hard
markdown-native代码块block non-void ownern/a
markdown-native分隔线block void atomn/a
markdown-extension内联公式 / 块级公式inline void atom / block void atomn/a
markdown-extensionautolink 字面量inline non-void link spandirectional
markdown-extension脚注引用 / 脚注定义inline void atom / block non-void containern/a
block-editor-nativemention / dateinline void atomn/a
block-editor-nativecallout / toggle / 列block non-void containern/a
block-editor-nativeTOC / 媒体嵌入 / code drawing / excalidrawblock void atomn/a
collaborationcomment / suggestionleaf metadata markoutward
collaborationdiscussion / Yjs 光标overlay / no noden/a

这张表的意义在于:任何新增功能家族都必须在这张表中占一行,否则协议层不承认它"有模型"。

4.1 源码交叉验证

用插件源码验证上述表格:

  • 链接是inline non-void span:在 packages/link/src/lib/BaseLinkPlugin.ts 中只声明了isInline: true,没有isVoid——这正是"内联但可编辑"的节点契约;
  • mention 是inline void atom:在 packages/mention/src/lib/BaseMentionPlugin.ts 中声明为node: { isElement: true, isInline: true, isVoid: true },三者齐备;
  • 脚注引用修复后同样变为isInline: true, isVoid: true(见下一节)。

这印证了 standards 文档的规则:voidness 由编辑器节点契约决定,而不是由渲染 DOM 决定


五、案例研究:footnote 引用的"规范谎言"与运行时修复

5.1 发现的问题

原文档 Notes 部分记录了一个非常典型的问题:

The spec drifted into a real lie on footnotes:footnoteReferencewas treated like an atom in prose while the runtime node was still non-void.

即:文档散文把footnoteReference当作 atom 来描述,但运行时节点仍然是非 void 的。规范与实现出现了真实的分叉。这次 pass 最初只是"纯文档工作"(docs-only),却在审计中暴露了这个真实的运行时 mismatch,从而触发了后续的执行修复(2026-04-04-footnote-inline-void-fix.md)。

5.2 修复目标与症状

修复文档明确列出了要消灭的浏览器症状:

  • 引用后的 Backspace 会编辑可见的标识符[^1]中的数字被当作可编辑文本);
  • 回链(backlink)导航触发的是通用编辑 chrome,而不是干净的导航

5.3 修复结果

修复完成后:

  • footnoteReference成为带空子节点哨兵(empty child sentinel)的 inline void atom——在 packages/footnote/src/lib/BaseFootnoteReferencePlugin.ts 中可以看到最终声明:node: { isElement: true, isInline: true, isVoid: true },且render: { as: 'sup' }渲染为上标;
  • 回链聚焦落在引用旁最近的稳定兄弟文本点(nearest stable sibling text point),而不是节点范围选择;
  • 浏览器验证(/docs/footnote页面)显示:回链跳转落在[1]后的.上、屏幕上只有一个 toolbar、一次 Backspace 删除整个引用而不是单个数字。

配套的测试也同步固化:在 packages/footnote/src/lib/BaseFootnotePlugins.spec.ts 中同时断言了isInline: true, isVoid: true的模型声明以及非 void 场景的反例。

5.4 这一案例的方法论价值

这是"规范驱动实现"(spec-driven implementation)的教科书案例:

  1. 规范先立规矩(footnote 引用 = inline void atom);
  2. 审计发现运行时不符合(仍是非 void 文本);
  3. 用红色测试锁定期望行为;
  4. 实现修复并把模型声明改到与规范一致;
  5. 浏览器级验证收尾。

它同时说明:文档审计不是文案工作,它能反向暴露真实的产品 bug。


六、文档体系中的落地与关联

6.1 规范的三层结构

本次 pass 更新了三个层面的文档,它们分工不同:

文档角色关键内容
markdown-standards.md方法论与权威模型参考池(Typora/Obsidian/Notion/Google Docs/GitHub/Milkdown)、权威顺序、节点模型与 affinity 要求、偏差政策、Spec ID 方案
markdown-editing-spec.md规范性家族法则(readable law)全局不变量、节点模型与 affinity 类、所有权顺序、每个家族的EDIT-*规则
editor-protocol-matrix.md穷尽式场景矩阵Row Schema、实体模型映射表、按家族的协议行、状态机(seeded/specified/tested/partial/deferred)
markdown-parity-matrix.md发布门槛按家族的语法支持与 round-trip 状态

protocol matrix 的 "Practical Use" 一节给出了工作流闭环:

先在 protocol matrix 中加穷尽场景行 → 在 markdown-editing-spec.md 中锁定行为 → 在 markdown-parity-matrix.md 中跟踪家族级充分性。

6.2 Spec ID 与测试映射

standards 文档还确立了稳定的 Spec ID 方案:EDIT(编辑行为)、PARITY(解析/序列化/往返)、STREAM(流式 markdown)、DEV(有意偏差)。目标是从文档直接驱动 TDD——每个锁定的规则都应映射到测试、所属包与行为 profile。协议矩阵中大量行已经标注了tested状态及对应的证据文件(如AffinityPlugin.spec.tsxwithBreakRules.spec.tsxwithTable.spec.tsx等)。


七、实践建议与延伸阅读

7.1 如果你想为 Plate 新增一个功能家族

按本次 pass 沉淀的流程走:

  1. 在 editor-protocol-matrix.md 的实体模型映射表中为新实体占一行,声明节点模型与 affinity 类;
  2. 在 markdown-editing-spec.md 中为该家族补充EDIT-*规则与 canonical 示例;
  3. 在插件源码的node配置中如实声明isElement/isInline/isVoid(参考 BaseMentionPlugin.ts 或 BaseFootnoteReferencePlugin.ts);
  4. 为边界行为编写 red tests(reference 现有AffinityPlugin.spec.tsx等测试);
  5. 在 markdown-parity-matrix.md 中更新家族级覆盖状态。

7.2 三条可复用的核心经验

  • 文档必须显式声明模型:任何功能家族如果没有声明节点模型与 affinity 类,就视为"未规范";
  • 以编辑器节点契约为准,不以渲染 DOM 为准contentEditable={false}不是 void 的证明;
  • 文档审计是 bug 探测器:规范与实现不一致的地方,往往是真实产品缺陷(footnote 引用就是证据)。

如需继续深入,建议顺序阅读:markdown-standards.md(方法论)→ markdown-editing-spec.md(家族法则)→ editor-protocol-matrix.md(场景矩阵)→ 2026-04-04-footnote-inline-void-fix.md(修复案例),并结合packages/core/src/lib/plugins/affinity/下的实现逐条对照。

【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询