
Plate 项目 Slate v2 op-family 第一切片实战以 insert_node / remove_node 补齐核心操作族 API【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本文基于仓库文档 2026-04-07-slate-v2-op-family-first-slice.md 展开。该文档记录了一次典型的最小诚实切片smallest honest slice执行当slate包的公开文档已经暗示了insert_node/remove_node操作与Transforms.insertNodes/Transforms.removeNodes变换而实际包内只实现了四个操作与四个变换辅助函数时如何用一次窄范围的切片补齐缺口。本文以该计划为骨架结合当前仓库packages/slate中的类型定义、变换实现与测试用例还原从缺口确认、失败测试、实现到验证的完整闭环并给出下一个切片path-basedset_node/Transforms.setNodes的前瞻。一、背景op-family 缺口从何而来在 Slate 的架构里Operation是编辑器改变内部状态时使用的底层指令。把一切变更统一表示为操作是 Slate 编辑器实现 history撤销/重做、协同编辑等功能的前提——这一点在 operation.ts 的类型注释中写得很明确Operationobjects define the low-level instructions that Slate editors use to apply changes to their internal state. Representing all changes as operations is what allows Slate editors to easily implement history, collaboration, and other features.计划文档Finding 一节指出的核心问题是文档与实现之间存在错位。当时的slate文档已经暗示了insert_node/remove_node两个操作以及Transforms.insertNodes(...)/Transforms.removeNodes(...)两个变换辅助函数的存在但包内实际只实现了四个操作和四个变换辅助函数即公开 API 承诺了一部分能力而底层操作族op-family尚未跟上。这种文档先行、实现滞后的缺口在大型编辑器重构中非常典型先有 API 面surface的契约再逐步用操作族实现填充。从当前仓库看该切片已经落地——operation.ts 中定义了完整的InsertNodeOperationoperation.ts 定义了完整的RemoveNodeOperation两者都已被纳入 NodeOperation 联合类型。二、目标与范围坚持最小诚实切片原则计划文档的 Scope 一节给出了三个明确的边界从最小的、已被文档暗示的 op-family / API 错位开始不贪多、不铺开优先实现insert_node/remove_node及配套的Transforms包装前提是核心core能够干净地支撑它们保持切片窄小明确排除以下内容不做虚假的、泛化的NodeOptions兼容no fake broad NodeOptions parity不做选择语义繁重的变换no selection-heavy transform semantics不扩展更宽的节点族no broader node families yet。这条最小诚实切片原则的价值在于每次只交付一个可独立验证、可回归、不会引入大规模副作用的增量而不是一次性重写整个操作层。它同时为后续切片留下了清晰的接缝seam。三、五阶段执行流程从缺口确认到验证闭环计划文档以勾选清单形式记录了五个执行阶段全部完成。这套流程本身就是一个可复用的 op-family 切片方法论阶段内容状态1确认确切的缺口与当前代码接缝Confirm the exact mismatch and current code seams已完成2编写聚焦的失败测试Write focused failing tests已完成3实现最小诚实的核心 / API 切片Implement the smallest honest core/API slice已完成4同步包与公开文档Sync package/public docs已完成5验证受影响的包与文档Verify the touched package/docs已完成每个阶段的产物在当前仓库中都能找到对应证据阶段 2 的失败测试沉淀为 insertNodes.spec.tsx 与 removeNodes.spec.tsx阶段 3 的核心实现对应 operation.ts 的类型层与 insertNodes.ts、removeNodes.ts 的变换层阶段 4 的文档同步指向仓库的 roadmap 真相来源 master-roadmap.md计划文档开头的引用即为该文件。四、核心实现一Operation 类型层切片首先补齐的是操作类型的静态定义。当前仓库中 operation.ts 已经完整定义了本次切片涉及的类型export type InsertNodeOperationN extends Descendant Descendant { [key: string]: unknown; node: N; path: Path; type: insert_node; }; export type RemoveNodeOperationN extends Descendant Descendant { [key: string]: unknown; node: N; path: Path; type: remove_node; }; export type NodeOperationN extends Descendant Descendant | InsertNodeOperationN | MergeNodeOperationN | MoveNodeOperation | RemoveNodeOperationN | SetNodeOperationN | SplitNodeOperationN;几个值得注意的类型设计细节两个操作都携带node被插入/被移除的节点快照与path操作发生的路径并带有[key: string]: unknown索引签名以保持向前兼容Operation顶层联合类型由NodeOperation | SelectionOperation | TextOperation组成operation.ts本次切片完全落在NodeOperation分支内OperationApi提供了isNodeOperation、isOperation、isOperationList等运行时判别方法operation.ts这些判别函数是 history、协同等上层模块对操作进行分派的基础。从源码结构看当前仓库的NodeOperation已包含六种节点操作insert、merge、move、remove、set、split说明该切片完成后后续的set_node/merge_node/move_node/split_node等操作族成员也已在类型层面逐步就位——这印证了计划文档Follow-on一节所述路线图的方向。五、核心实现二Transforms 变换层操作类型之外切片还需要提供面向使用者的变换 API。当前仓库中 insertNodes.ts 是对slate基础insertNodes的包装其实现要点如下export const insertNodes N extends ElementOrTextOfE, E extends Editor Editor( editor: E, nodes: N | N[], { nextBlock, removeEmpty, ...options }: InsertNodesOptionsValueOfE {} ) { options getQueryOptions(editor, options); editor.tf.withoutNormalizing(() { if (removeEmpty) { const blockEntry editor.api.above({ at: options.at }); // ...查询匹配后若为空块则先 editor.tf.removeNodes({ at: blockEntry[1] }) } if (nextBlock) { // ...将插入位置改为当前块的后一个兄弟路径 PathApi.next(blockEntry[1]) } insertNodesBase(editor as any, nodes, options as any); }); };对应地removeNodes.ts 处理了children与previousEmptyBlock两个扩展选项previousEmptyBlock: true时先定位目标位置前一个兄弟块若为空则删除它children: true时用NodeApi.children(editor, options.at, { reverse: true })以逆序遍历子节点并逐个删除——逆序是为了避免删除过程中路径偏移导致下标错乱这一点在 removeNodes.spec.tsx 的嵌套列表用例中有直接验证默认情况则委托给基础removeNodesBase。两条变换都统一包裹在editor.tf.withoutNormalizing(...)中保证批量操作期间不触发中间态的归一化normalization。5.1 选项类型与默认值变换层的能力边界由 editor-transforms.ts 中的选项类型刻画InsertNodesOptionsVeditor-transforms.ts选项类型说明batchDirtyboolean是否批量标记脏状态hangingboolean是否允许悬空范围nextBlockboolean在当前块之后插入节点若removeEmpty已导致当前块被移除则不生效removeEmptyQueryNodeOptions \| boolean插入前若当前块为空则移除默认仅针对段落allow: [p]可传入QueryNodeOptions定制selectboolean若为 true插入后选中插入的节点继承—QueryOptions、QueryMode、QueryVoids位置/匹配/void 查询约束RemoveNodesOptionsVeditor-transforms.ts选项类型说明childrenboolean为 true 时删除指定位置节点的全部子节点event{ type: mergeNodes }事件标注供deleteBackward等内部路径使用hangingboolean是否允许悬空范围previousEmptyBlockboolean为 true 时若前一个块为空则先删除它继承—QueryOptions、QueryMode、QueryVoids5.2 变换的入口包装值得注意editor.tf.insertNodes之上还有一层薄封装 insertNode.ts它直接把单节点委托给变换层export const insertNode N extends DescendantOfE, E extends Editor Editor( editor: E, node: N, options?: InsertNodesOptionsValueOfE ) editor.tf.insertNodes(node, options);这形成了editor.api.insertNode→editor.tf.insertNodes→ 基础slate实现的调用链从源码结构看这是该仓库为 Editor API 与 Transforms API 双入口保持一致而设计的模式。六、测试驱动spec 如何锁定行为计划文档明确要求编写聚焦的失败测试。两个变换的测试文件展示了不同但互补的验证风格insertNodes.spec.tsx 使用createEditor构建输入覆盖四条关键行为路径removeEmpty: true时先移除空段落再插入第 4-24 行removeEmpty传QueryNodeOptions如{ allow: [blockquote] }时按过滤器决定是否移除空块第 26-47 行nextBlock: true时插入到当前块之后第 49-70 行无选择、无显式目标时追加到文档末尾显式at优先于当前选择第 72-111 行。removeNodes.spec.tsx 使用platejs/test-utils的jsxt语法构造编辑器状态分三个describe组覆盖previousEmptyBlock为 true/false 时对前驱空块的处理指定路径的节点删除与未指定路径时的幂等行为children: true时对嵌套列表的逆序递归删除含路径无子节点时保持不变的边界用例。此外history 层也有对insert_node/remove_node操作语义的直接依赖在 with-history.spec.tsx 中手动向editor.history.undos压入一段由remove_nodeinsert_node组成的操作序列随后调用editor.undo()验证文档内容与 selection 均能按预期回滚。这说明本次切片不只是类型补全而是真正参与了 history 的撤销/重做回路。七、验证命令如何确认切片健康计划文档的 Verification 一节列出了完整的验证矩阵可直接在当前仓库中执行# 1. 运行 snapshot-contract 测试需要 babel register 作为 loader yarn mocha --require ./config/babel/register.cjs ./packages/slate/test/snapshot-contract.ts # 2. 运行整个 mocha 测试套件 yarn test:mocha # 3. 运行 slate-react 工作区测试验证下游 React 绑定未回归 yarn workspace slate-react run test # 4. 对改动的 packages/slate 文件做定向 tsc 诊断期望 0 错误 # 5. 对改动的 packages/slate 文件运行 eslint yarn exec eslint # 6. 对改动的 slate-v2 文件运行 prettier 检查 yarn prettier --check # 7. 对改动的 plate-2 文档运行 prettier 检查 pnpm exec prettier --check7.1 两个诚实的工程信号计划文档的 Notes 一节记录了仓库级检查的真实状况值得每一位在大型 monorepo 中做定向改动的人借鉴仓库级yarn lint:typescript仍会命中该切片之外的过期 workspace*-v2tsconfig 引用因此全仓类型检查在此阶段不能作为该切片的有效信号仓库级yarn lint:eslint仍会报告数千条与本切片无关的既有问题所以对改动文件的定向 eslint 才是诚实honest的回归信号。换言之当全仓检查被历史遗留噪音污染时收敛到改动文件的定向检查 聚焦测试是更可靠的验证策略——这也是本计划将验证范围精确限定在改动的 packages/slate 文件的原因。八、已知约束与后续路线计划文档明确记录了本次切片不做的事这些约束既是验收边界也是下一步的路线图不做选择语义繁重的变换本切片只保证基于路径path-based的插入/删除语义不承诺复杂的选区扩散、悬空范围折叠等行为不做泛化的 NodeOptions 兼容避免为了看起来完整而伪造与上游 Slate 完全一致的选项面Follow-on 明确指向下一个切片路径化set_node/Transforms.setNodes(...)。从当前仓库看setNodes.ts 与其 spec 已经存在说明这条路线已在后续切片中持续推进。九、关联文件速查角色路径本文依据的计划文档2026-04-07-slate-v2-op-family-first-slice.md路线图真相来源master-roadmap.mdOperation 类型定义operation.tsTransform 选项类型editor-transforms.tsinsertNodes 变换实现insertNodes.tsremoveNodes 变换实现removeNodes.tsinsertNode 入口包装insertNode.tsinsertNodes 测试insertNodes.spec.tsxremoveNodes 测试removeNodes.spec.tsxhistory 层操作依赖验证with-history.spec.tsx十、小结这份计划文档示范了一种可复制的操作族补齐方法论先以文档契约为基准确认缺口用失败测试锁定最小行为实现时严格收窄范围、拒绝伪兼容最后用定向测试与定向 lint 对抗 monorepo 的历史噪音。对读者而言insert_node/remove_node及其Transforms包装不仅是两个 API更是一条从契约到实现再到验证的完整切片范式可直接套用于set_node、merge_node、move_node、split_node等后续操作族成员。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考