
Slate v2 Op-family 第二十六切片跨相邻顶级块边界的非空 Range 删除实现解析【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate本文基于 plate 仓库中 docs/plans/2026-04-07-slate-v2-op-family-twenty-sixth-slice.md 这一支持性计划文档结合 packages/slate 的deleteText实现与测试讲解 Slate v2 中如何兑现跨相邻受支持顶级块边界的显式非空Range删除这一操作族切片。导读本文聚焦 plate 仓库中 Slate v2 操作族Op-family第 26 个切片跨相邻顶级块边界的非空Range删除。这是editor.delete()/deleteText能力矩阵中从块内删除走向跨块删除的关键一步——用户选中两个相邻段落各一部分文本并按下删除键时编辑器需要删除选中内容并将两个块合并为一个。读完本文你将掌握该切片的边界范围支持什么、明确不做什举、同类型/同属性块合并要求以及底层deleteText在 packages/slate/src/internal/transforms/deleteText.ts 中的实现链路与对应测试证据。切片背景Op-family 与 master-roadmap 的定位该文档是一份支持性计划Supporting plan其队列与路线图的权威来源是 docs/slate-v2/master-roadmap.md。在 master-roadmap 的 Tranche 3slate读/更新事务核心与公开 API 重置中操作族切片按序推进以merged contract corpus合并契约语料库为方法论以 legacy 测试/文档为默认事实保留无冲突的 v2 意图行为并对保留行允许重写。从切片编号序列可以还原出这条递进路线第一切片insert_node/remove_node操作类型与Transforms.insertNodes/Transforms.removeNodes见 docs/plans/2026-04-07-slate-v2-op-family-first-slice.md第二十四切片Editor.before(...)/Editor.after(...)跨受支持顶级块并让Transforms.move(...)继承该位置缝隙见 docs/plans/2026-04-07-slate-v2-op-family-twenty-fourth-slice.md第二十五切片折叠collapseddelete(...)跨相邻顶级块边界见 docs/plans/2026-04-07-slate-v2-op-family-twenty-fifth-slice.md第二十六切片本文在折叠删除的基础上把能力扩展到显式非空Range的跨相邻块删除。第二十四切片解决了位置步进器能否跨越块边界before/after/move第二十五切片解决了折叠选区下 Backspace/Delete 跨块依赖distance、reverse第二十六切片则解决展开选区非空 Range跨块删除并合并——三者共同构成跨块操作的基础设施。Goal本切片要兑现的能力文档给出的目标非常精确Cash out explicit non-emptyRangedeletion across adjacent supported top-level block boundaries.即兑现跨相邻受支持顶级块边界的显式非空Range删除。所谓兑现cash out意味着此前该行为可能只在折叠删除或文档语义中隐含本切片要让它成为真实、可测试、有测试背书的能力。在 packages/slate/src/internal/transforms/deleteText.spec.tsx 中恰好存在一个直接对应此目标的测试用例it(merges blocks when deleting an expanded cross-block selection, () { const editor: any createEditor( ( editor hp wo anchor / rd /hp hp an focus / other /hp /editor ) as any ); editor.delete(); const output ( editor hp wo cursor / other /hp /editor ) as any; expect(editor.children).toEqual(output.children); expect(editor.selection).toEqual(output.selection); });该用例使用jsxt构建两个相邻hp段落块选区从第一个段落的wo之后anchor /延伸到第二个段落的an之后focus /。调用editor.delete()后期望结果是两个块合并为一个hp文本为woother中间的rd与an被删除光标落在合并后的位置。这就是显式非空 Range 跨相邻顶级块删除并合并的最小行为契约。Scope精确的边界控制本切片刻意把范围收得很窄避免一次做太多支持的范围显式非空Range调用方显式传入一个anchor ! focus的选区或直接依赖当前非空选区当前非空选区省略at时与折叠删除切片一致当at被省略时以editor.selection作为删除目标相邻受支持顶级块跨越选区起点与终点必须落在两个相邻的顶级块中且这些块必须是受支持的类型即可合并的同类型块同类型/同属性块合并要求合并后的结果块保持相同的type与属性props否则不满足合并条件。明确排除的范围更宽泛的多块 Range 删除本切片不做跨越三个及以上块的大范围删除一次删除只涉及一对相邻块边界。从实现角度看这一点体现在 deleteText.ts 的isAcrossBlocks判定逻辑中const startBlock editor.api.above({ at: start, voids, match: (n) ElementApi.isElement(n) editor.api.isBlock(n), }); const endBlock editor.api.above({ at: end, voids, match: (n) ElementApi.isElement(n) editor.api.isBlock(n), }); const isAcrossBlocks startBlock endBlock !PathApi.equals(startBlock[1], endBlock[1]);isAcrossBlocks仅在起点所在块与终点所在块路径不同时为真——即恰好跨越一对相邻块。块必须是元素且被editor.api.isBlock判定为块级节点这呼应了受支持的顶级块这一范围约束。实现链路deleteText 的跨块删除管线本切片的能力最终落到deleteText的既有管线中。以下结合 packages/slate/src/internal/transforms/deleteText.ts 的源码逐步拆解。1. 目标定位at resolutionlet at: any getAt(editor, options?.at) ?? editor.selection;getAt优先解析显式传入的at否则回退到editor.selection。这正是文档中支持显式非空Range 当前非空选区省略at两个入口的实现位置。若两者皆无直接返回no-op。随后如果at是折叠的 Range会被降级为 Point 处理isCollapsed分支走单点删除路径非空 Range则继续走下面的多节点删除管线——本切片关心的正是这条路径。2. 范围挂起与清理hanging / unhangif (!hanging) { const [, end] RangeApi.edges(at); const endOfDoc editor.api.end([])!; if (!PointApi.equals(end, endOfDoc)) { at editor.api.unhangRange(at, { voids }); } }非空 Range 若其焦点超出文档末尾hanging range会先通过unhangRange收紧到有效边界避免删除越界。3. 跨越判定与不可编辑节点处理管线先分别取start与end所在块上文isAcrossBlocks判定再检查起点/终点是否落在 inline void 或只读元素内若命中则将起点向前editor.api.before、终点向后editor.api.after挪出该节点但仅在挪动后仍停留在同一块的祖先路径内时才接受if (startNonEditable) { const before editor.api.before(start); if (before startBlock PathApi.isAncestor(startBlock[1], before.path)) { start before; } }这一逻辑保证选区边缘恰好卡在图片、mention 等 inline void 或只读内联元素上时删除边界会被推到元素外侧而不会破坏块结构。4. 收集完全落入 Range 的节点并移除for (const entry of editor.api.nodes({ at, voids })) { const [node, path] entry; ... if ( (!voids ElementApi.isElement(node) editor.api.isElementReadOnly(node)) || (!PathApi.isCommon(path, start.path) !PathApi.isCommon(path, end.path)) ) { matches.push(entry); lastPath path; } }凡是既不包含起点、也不包含终点的路径即完全被选区覆盖的中间节点/块都被收集进matches随后通过 pathRef路径引用逆序removeNodes保证在路径索引不断变化时也能安全删除。5. 首尾文本裁剪起点一侧若起点与终点不在同一文本节点则删除起点文本节点中从start.offset到末尾的文本remove_text操作终点一侧删除终点文本节点中从0跨块时到end.offset的文本。if (!isSingleText !startNonEditable) { const point startRef.current!; const [node] editor.api.leaf(point)!; const { offset } start; const text node.text.slice(offset); if (text.length 0) { editor.tf.apply({ offset, path, text, type: remove_text }); removedText text; } }6. 跨块合并本切片的灵魂if (!isSingleText isAcrossBlocks endRef.current startRef.current) { editor.tf.mergeNodes({ at: endRef.current, hanging: true, reverse: !reverse, voids, }); }当确认是**跨块isAcrossBlocks**且起点/终点引用仍然有效时以终点位置为锚调用mergeNodes将后一块合并进前一块reverse: !reverse确保合并方向与删除方向一致。合并发生在remove_text之后因此最终呈现的效果是中间内容被删除两个同类型段落粘合为一个段落——与测试中的woother期望完全吻合。7. 选区复位与特殊脚本处理删除完成后若调用方未显式传at则把光标移动到删除后的边界点if (options?.at null point) { editor.tf.select(point); }另外针对泰语等组合字符脚本向后按字符删除时会保留剩余码点并重新插入THAI_SCRIPT_REGEX分支避免把整个字形簇grapheme cluster一次性删掉。测试 deleteText.spec.tsx 中พี่ 光标 delete({ distance: 2, reverse: true, unit: character })得到พ即验证此行为。测试矩阵跨块删除的行为护栏packages/slate/src/internal/transforms/deleteText.spec.tsx 中与本切片主题相关的测试形成一个完整的行为护栏测试用例输入期望结果覆盖点deletes one character forward from a collapsed text selectionwocursor /rdwocursor /d折叠选区基础删除第二十五切片能力removes the node at a path location两个hpat: [1]仅剩第一个hpPath 形式删除merges blocks when deleting an expanded cross-block selectionwoanchor /rd/anfocus /other单块wocursor /other本切片核心非空 Range 跨块删除合并deletes an inline void when moving forward from before itcursor /himg 文本仅剩光标与空文本inline void 边界处理removes an inline void when deleting from a point inside it光标在himg内仅剩after起点在 void 内nudges backward around a read-only inline before deleting it空文本 hmentioncursorreverse: true空文本 光标只读内联回退挪动re-inserts remaining Thai code points after backward character deletionพี่ 光标พ 光标泰语码点保留no-ops when deleting forward from the end of the documentwordcursor /不变文档末尾 no-op其中merges blocks when deleting an expanded cross-block selection直接对应本切片文档目标是判断该切片是否兑现的验收用例其余用例则验证跨块删除管线在 void、只读内联、脚本与边界上的鲁棒性。验证方式切片阶段的执行闭环文档的 Phases 全部标记为完成[x]构成该切片的标准执行闭环Confirm确认相邻跨块 Range 删除语义与现有 helper 覆盖情况——对应确认deleteText管线中isAcrossBlocks、mergeNodes、pathRef/pointRef 等既有缝隙seamWrite focused failing tests先写聚焦的失败测试即上文跨块合并用例此前的实现无法通过它Implement实现或确认最小的诚实跨块 Range 删除切片——即让既有deleteText管线中的跨块分支真实可用Sync package/public docs同步slate包与公开文档使文档描述的能力与实现一致Verify验证被触及的包与文档按 master-roadmap 的关闭标准通常为bun test ./packages/slate/test、bunx turbo build --filter./packages/slate、bunx turbo typecheck --filter./packages/slate等。值得注意的纪律是本切片只处理一对相邻块的边界多块大范围 Range 删除被明确留到后续切片。这种最小诚实切片smallest honest slice策略贯穿整个 Op-family 系列——每个切片只推进一个可独立验证的能力避免在单个 PR 中混入过多语义变更从而保证测试与文档始终与实现同步。延伸阅读操作族切片起点docs/plans/2026-04-07-slate-v2-op-family-first-slice.md跨块位置步进before/after/movedocs/plans/2026-04-07-slate-v2-op-family-twenty-fourth-slice.md折叠删除跨块 docs/plans/2026-04-07-slate-v2-op-family-twenty-fifth-slice.md整体路线图与 Tranche 划分docs/slate-v2/master-roadmap.md核心实现packages/slate/src/internal/transforms/deleteText.ts行为护栏测试packages/slate/src/internal/transforms/deleteText.spec.tsx【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考