Slate v2 Inline 家族迁移第一切片:Mentions 的运行时接缝设计与浏览器验证(plate 项目解析)
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
本文基于 docs/plans/2026-04-06-slate-v2-inline-family-migration-tranche.md 展开。该计划面向 Slate v2 引擎仓库(原仓库路径
/Users/zbeyens/git/slate-v2),记录了 inline 元素家族向 v2 原生运行时迁移的第一个"诚实切片"——Mentions(@提及)。读完后你将掌握:为什么 inline 家族迁移要以 mentions 为起点、v2 运行时唯一允许新增的键盘事件接缝(runtime seam)如何设计、验收与浏览器验证如何闭环,以及当前 plate 仓库中对应实现的落点。
Slate v2 的引擎重构有一条明确纪律:内核保持无聊(keep the core boring),运行时接缝显式化,应用自有的 inline 行为属于应用代码,除非一个运行时接缝被证明是必要的。Mentions 切片就是这条纪律的第一个实战样本:它不复活旧版withMentions(withReact(...))的插件覆写架构,而是用最小的onKeyDown转发接缝,在 v2 原生运行时上完整交付"渲染 mention 徽章 + @query 建议 + Enter 插入"三个核心行为,并为后续 links / paste-html 切片划清了边界。
切片背景:inline 家族迁移的整体约束
在 v2 重构中,inline 元素(mentions、links 等)与 void、markableVoid 一起构成了"inline 家族"。旧版 Slate 通过withMentions(withReact(...))这类高阶插件覆写链把行为塞进引擎,v2 明确拒绝把这种覆写架构拖回内核。
本切片(Stage 1)的交付物被严格限定为:
- 一个构建在当前 snapshot/runtime 栈之上的 v2 原生 mentions 表面(surface);
- 一个支撑该表面所必需的、最小化的运行时接缝;
- 覆盖"渲染 mention 徽章 / 建议弹出 / Enter 插入"的浏览器证明(browser proof);
- 把本切片归类为"首个 inline 家族迁移切片"的路由图与文档同步。
也就是说,这一轮只做 mentions,不碰完整的旧版 inline 家族。
非目标(Non-Goals):明确"本轮不做什么"
计划文档用四个 Non-Goals 划出边界,防止切片膨胀:
- 不尝试复刻旧版
withMentions(withReact(...)); - 不为
isInline/isVoid/markableVoid引入通用插件覆写接缝; - 不在同一切片内做 link 包裹或 paste-html 迁移;
- 不为了"让 mentions 显得原生"而新增引擎操作(op)家族。
从当前 plate 仓库的实现看,这一边界体现在 BaseMentionPlugin.ts 中:BaseMentionPlugin通过createTSlatePlugin声明node: { isElement: true, isInline: true, isMarkableVoid: true, isVoid: true },以 v2 原生插件声明的形式表达 inline/void 语义,而非旧版withMentions式的行为注入;BaseMentionInputPlugin则单独声明isInline + isVoid的输入态节点。这正是"把节点语义声明化、把行为保留在运行时接缝之外"的落地形态。
设计规则:Engine v2 的三条铁律
计划文档将设计规则归纳为三点,这也是判断"某个能力是否应该进引擎"的判据:
- keep the core boring:内核只承载通用、可预测的机制,不为单个应用特性开洞;
- keep runtime seams explicit:如果确实需要在运行时加接缝,必须显式命名、显式暴露、显式文档化;
- treat app-owned inline behavior as app code unless a runtime seam is clearly justified:应用自有的 inline 行为默认归应用侧,除非运行时接缝有清晰的正当性。
唯一被论证批准的运行时接缝:Editable / EditableBlocks 上的 onKeyDown 转发
本切片唯一获批的运行时接缝是Editable/EditableBlocks上的键盘事件转发。
理由很直白:一个真实的 mentions 表面需要显式的键盘所有权(监听@触发、方向键选择、Enter 确认),而当时的 v2 运行时并未暴露键盘事件入口。没有这个接缝,应用就无法"诚实"地接管键盘,只能退回覆写架构。
切片落地时该接缝以最小形态合入(minimalonKeyDownruntime forwarding onEditable/EditableBlocks),只做转发、不做策略:具体按键如何响应仍由应用侧组合,引擎只保证事件能显式到达应用层。这与后续 links-paste-html 切片 的onPaste转发遵循同一模式——每个 inline 表面各带一个明确、最小、文档化的运行时接缝,而不是一个万能的事件覆写钩子。
验收标准(Acceptance):四条可核验的条件
- 一个新的 v2 mentions 示例存在并运行于
/examples/mentions; - 该示例证明三项行为:初始渲染的 mention 徽章、
@query触发的建议、Enter 插入; - 示例使用当前 v2 运行时栈,而非旧版 Slate 插件覆写;
- 本次新增的运行时接缝保持最小化且有文档说明;
- 相关文档与路线图明确陈述:mentions 是首个 inline 家族迁移切片,links / paste-html 有意后置。
计划文档记录的落地位(原仓库路径)为:
site/examples/ts/mentions.tsx:v2 原生 mentions 示例;playwright/integration/examples/mentions.test.ts:专用浏览器证明;- 跨仓库替换矩阵(replacement matrix)为旧版与现行 mentions 增加对比行。
这些路径指向独立的 slate-v2 引擎仓库,不在当前 plate 仓库内;当前 plate 仓库中与 mentions 表面直接对应的实现位于 packages/mention 与 packages/combobox。浏览器证明的示例清单在 decoration-roadmap.md 中也有交叉引用(mentions 示例与 persistent-annotation-anchors、hovering-toolbar 一起被列为已有 package/browser 证明的表面)。
验证命令(Verification):切片如何自证
计划文档给出的验证清单:
yarn workspace slate-react run test:slate-react 工作区测试;yarn tsc:examples:示例的 TypeScript 类型检查;- 针对新 mentions 示例的定向 Playwright 测试;
yarn test:replacement:compat:local:当替换矩阵扩张时运行的兼容性测试;- 对受影响的 slate-v2 / plate-2 文档执行格式检查。
这些命令面向原 slate-v2 引擎仓库的脚本约定;当前 plate 仓库采用 bun/pnpm 工作区(见 package.json 与 pnpm-workspace.yaml),运行验证时应以各自仓库的脚本为准。
落地结果(Outcome):合入内容与后续边界
切片按"mentions 优先"完成,合入项包括:
Editable/EditableBlocks上的最小onKeyDown运行时转发;- v2 原生 mentions 示例(原仓库
site/examples/ts/mentions.tsx); - 专用浏览器证明(原仓库
playwright/integration/examples/mentions.test.ts); - 为旧版与现行 mentions 加宽了跨仓库替换矩阵行;
- 路线图/文档同步,将 mentions 归类为首个 inline 家族迁移切片。
替换矩阵的语义可参考 replacement-family-ledger.md 的状态分级:Preserved(保留)、Redefined(经更窄的现行接缝重定义)、Comparison-only(仅对比可见)、Intentionally Later(有意后置)。mentions 属于"Redefined"一类:家族行为保留,但通过更窄、更干净的现行接缝承载,而不是对旧版行为的逐字节复刻。
明确后置的工作项:
- links;
- paste-html;
- 任何通用插件覆写架构。
当前 plate 仓库中的 mentions 实现佐证
虽然迁移切片落在独立的 slate-v2 引擎仓库,但 plate 仓库的 mentions 插件与之一脉相承,可作为理解"v2 原生 mentions 表面"的活样本:
1. 节点声明与触发器配置(BaseMentionPlugin.ts)
export const BaseMentionInputPlugin = createSlatePlugin({ key: KEYS.mentionInput, node: { isElement: true, isInline: true, isVoid: true }, }); export const BaseMentionPlugin = createTSlatePlugin<MentionConfig>({ key: KEYS.mention, node: { isElement: true, isInline: true, isMarkableVoid: true, isVoid: true, }, options: { trigger: '@', triggerPreviousCharPattern: /^\s?$/, createComboboxInput: (trigger) => ({ children: [{ text: '' }], trigger, type: KEYS.mentionInput, }), }, plugins: [BaseMentionInputPlugin], })关键参数:trigger默认'@';triggerPreviousCharPattern为^\s?$,即触发器前一个字符必须为空或空白,避免在单词中间误触发;createComboboxInput负责把输入态节点(mentionInput)注入文档,该输入态节点同时是 inline 与 void。
2. 选中建议后的插入行为(getMentionOnSelectItem.ts)
tf.insert.mention({ key: item.key, search, value: item.text }); // 将选区移动到元素之后 editor.tf.move({ unit: 'offset' }); const pathAbove = editor.api.block()?.[1]; const isBlockEnd = editor.selection && pathAbove && editor.api.isEnd(editor.selection.anchor, pathAbove); if (isBlockEnd && insertSpaceAfterMention) { editor.tf.insertText(' '); }这里可以看到与"Enter 插入"直接相关的运行语义:插入 mention 节点 → 光标移到元素之后 → 若位于块末尾且开启insertSpaceAfterMention选项,则自动补一个空格。选项类型insertSpaceAfterMention?: boolean定义在 MentionConfig 中。
3. 建议列表的触发机制由 packages/combobox 的withTriggerCombobox提供(BaseMentionPlugin通过.overrideEditor(withTriggerCombobox as any)接入),其行为由 withTriggerCombobox.spec.tsx 覆盖。combobox 相关的触发、选择、键盘导航逻辑正是"建议弹出 + Enter 确认"这套交互在 plate 中的实现载体。
4. 序列化/反序列化闭环:mentions 的 Markdown 往返由 packages/markdown 承担,包括remarkMention插件(remarkMention.ts)与serializeMention.spec.ts测试,保证 v2 原生 mention 节点在导出/导入时语义不丢失。
小结:一个"诚实切片"的可复用范式
回顾整个计划,mentions 切片之所以被称为"第一个诚实的 inline 家族迁移切片",在于它同时守住了三条底线:
- 不改内核:没有为 mentions 引入新的操作家族或通用覆写机制,只批准了一个有明确正当性的
onKeyDown转发接缝; - 不拖旧债:旧版
withMentions覆写链被明确列入 Non-Goals,示例必须跑在当前 v2 运行时栈上; - 边界清晰:验收、验证、文档三线闭环,且 links / paste-html 被显式标注为后续切片(其计划见 slate-v2-links-paste-html-tranche)。
这套"单表面 + 最小接缝 + 浏览器证明 + 路线图同步"的迁移范式,为 inline 家族后续所有切片(links、paste-html、乃至更丰富的 HTML 格式化策略)提供了可复制的执行模板。
【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考