
深入 Slate v2 占位符与 IME 合成输入的浏览器级验证从 FEFF 零宽字符到渲染器所有权边界【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate导读本文围绕 plate 仓库中docs/plans/2026-04-04-slate-v2-placeholder-ime.md这份支撑型执行计划完整梳理 Slate v2含slate-react-v2、slate-dom-v2、slate-browser在真实 Chromium 浏览器中验证「空块占位符 输入法IME合成输入」的完整过程。读者将掌握IME 合成期composition与提交期的正确提交策略、FEFFBOM 零宽字符占位符是否可被移除的判断依据、React 渲染器所有权边界如何决定 IME 崩溃与否以及从零宽矩阵到混合内联、嵌套引用、复杂列表等碎片语义逐级被浏览器证明的演进路径。背景为什么占位符与 IME 需要真实浏览器证明Slate v2 是 plate 项目中对编辑器内核与渲染层的一次架构级推进其工作目录散落在slate-v2与slate-browser两条线上。占位符placeholder与 IME 看似是两个小功能但它们共同命中编辑器最脆弱的一段链路空块的占位符路径需要在 DOM 中渲染「零宽内容」否则空段落会塌陷、光标无处可放IME中文、日文等输入法合成输入期间浏览器临时持有原生文本区域并可能在提交前多次重写它两者叠加时任何「把 DOM 临时状态当作编辑器真值」的实现都会污染编辑器模型。该计划在开篇就明确了工作前提与纪律原文档「Context」节继续推进slate-browser与slate-v2当前的状态下一个活接缝live seam是真实slate-v2界面上的「零宽 / 渲染器输入策略」路径全程采用 TDD先建真实浏览器证明面proof surface让 Playwright IME 测试「因正确的原因」保持红再修复最小的运行时接缝把它变绿。这条「先红后绿、每次只修最小接缝」的纪律贯穿整个计划也解释了为什么文档里 80 个阶段全部标记为 Complete——每一行进度都对应一条已收敛的证明链而不是一次性大重构。IME 合成输入的正确提交边界compositionend失败模式把合成步骤逐条写进文档状态计划对应的第一份真实浏览器证明是slate-v2-placeholder-ime.test.ts目标是slate-v2-placeholder.tsx示例面。该示例最初挂载了一个最小slate-v2slate-react-v2编辑器占位符路径包含FEFF 零宽 span、br /、以及原生beforeinput/input对账层。首轮红测试暴露出的问题非常微妙Chromium 的 IME 步骤被一条一条当作文档文本提交最终 DOM 变成所有合成步骤的拼接。即输入「すし」时模型里可能出现的是「す」「すし」这样逐阶段叠加的文本而不是最终提交的「すし」。计划文档在「Findings」中给出了根因定位路径最初在每次input时都从 DOM 对账编辑器——看起来合理但把瞬态合成文本当成了已提交状态桥接bridge归一化加强并不能修复它桥不再是主要说谎者等待「友好的」insertTextinput 事件也是不可靠的——一旦瞬态合成 input 停止触发重渲染Chromium 在干净路径上以compositionend收尾之后并不会再来一个可提交的input事件。修复规则把合成与提交当作两个阶段对应解决方案文档 2026-04-04-slate-v2-placeholder-ime-proofs-must-commit-on-compositionend.md 记录了最终规则当event.isComposing为 true 时忽略该input事件忽略insertCompositionText/deleteCompositionText这类瞬态合成事件在compositionend时从 DOM 对账编辑器状态对非 IME 的普通文本输入仍走正常input对账。这样处理后DOM 内保留合成期的活文本编辑器只在合成真正结束时提交一次。最终浏览器证明落点正是「すし」与选区0.0:2|0.0:2。为什么这是对的合成期文本不是稳定的文档真值。合成期间浏览器持有临时原生区域且可能多次重写如果证明面每一步都把瞬态 DOM 拷回 Slate就摧毁了它自己正要测量的合成生命周期。在 Chromium 这条路径上compositionend是第一个可信点——最终提交文本已经在 DOM 里可以无重复地翻译回编辑器模型。这条规则在 plate 主仓库的编辑器 API 中同样能找到回音slate包把isComposing作为编辑器实例方法公开其实现位于 packages/slate/src/internal/dom-editor/isComposing.ts直接委托给slate-dom的DOMEditor.isComposing并在 packages/slate/src/create-editor.ts 通过bindFirst(isComposing, editor)绑定到编辑器。这意味着「合成中」是一个可以从业务代码直接查询的一等状态而非隐藏在事件处理里的内部细节。可复用的结论是如果一个 IME 敏感的占位符证明面在每次合成 input 上都做对账它测试的是自己糟糕的输入策略而不是编辑器的接缝本身。无 FEFF 的换行占位符React 渲染器所有权边界崩溃不是桥的问题是渲染器所有权的问题证明完 FEFF 支撑的占位符路径后下一个诚实的证明切片直接抛出了策略问题同一个占位符路径在 Chromium IME 下能否不依赖 FEFF 存活第一次 no-FEFF 红测试比「选区不对」更尖锐React 直接以NotFoundError: Failed to execute removeChild on Node崩溃可编辑界面整个消失。计划文档明确指出——这不是桥接债务而是渲染器所有权债务renderer ownership debt。为什么浏览器在 IME 期间会改写空换行占位符子树。如果 React 恰好拥有那个会被浏览器重写的br /子节点React 之后对账时就会拿自己过期的子节点簿记去匹配一个已被浏览器改过的子树removeChild崩溃由此而来。最小修复所有权止步于零宽包裹 span对应解决方案文档 2026-04-04-slate-v2-no-feff-line-break-placeholders-need-dom-owned-br-interiors.md 记录的分界是FEFF 路径React 渲染\uFEFF加br /no-FEFF 路径React 渲染零宽包裹 span但内部br /通过dangerouslySetInnerHTML注入作为 DOM 自有内部节点而不是 React 子 fiber。边界确立后no-FEFF 的 Chromium 证明通过占位符形态保持{ hasBr: true, hasFEFF: false, kind: n }IME 提交「すし」最终选区落在0.0:2|0.0:2。由此得到的关键认知升级FEFF 并非空换行占位符路径在 Chromium 下天然必需但渲染器绝不能让 React 拥有被 IME 改写的br /内部节点。React 所有权停在包裹边界后浏览器可以自由改写内部React 也能在提交文本到达时干净地替换整个空分支。共享渲染接缝ZeroWidthString 与零宽矩阵证明收敛后行为不能再停留在示例 markup 里必须沉淀为共享渲染接缝。计划把当前拆分固化进slate-react-v2的ZeroWidthString组件换行占位符默认无 FEFFbr /内部 DOM 自有非换行零宽占位符仍保留 FEFF。浏览器矩阵slate-v2-zero-width-matrix.test.ts钉住四种零宽形态形态默认策略Chromium 证明状态换行路径line-break无 FEFF DOM 自有br /已证明空块换行路径无 FEFF已证明内联边缘inline-edge起点/终点FEFF 支撑已证明类 void 零宽路径FEFF 支撑已证明需镜像真实 void spacer 结构两条重要的「假阴性」教训也记录在案内联边缘首轮红不是渲染策略 bug而是测试设置粗糙——仅 root click 无法保证选区落在目标零宽 leaf 上改为在合成前语义化设置 Slate 选区后FEFF 支撑的内联边缘路径在 Chromium 通过类 void 首轮红是证明面不足导致的假阴性——它把 void 外观与 spacer 折叠进同一个包裹镜像真实 legacy void 接缝data-slate-voidtrue元素 不可编辑内容包裹 独立绝对定位 spacer leaf后FEFF 支撑的类 void 路径同样通过。因此当前诚实的拆分是换行路径的 no-FEFF 已在 Chromium 证明内联边缘与类 void 路径的 FEFF 行为已证明而非换行的 no-FEFF 路径仍未证明因此保持保守。从零宽到完整渲染栈TextString、SlatePlaceholder、EditableText、Editable零宽矩阵证明后重复出现的渲染/输入接缝逐步被收编为slate-react-v2的共享原语证明面不再手搓 DOMTextString共享 DOM 文本边界data-slate-stringspan 不再由各证明面手写并带 DOM 文本修复行为有包级测试SlateElement/SlateSpacer/SlateText/SlateLeaf最小渲染节点形态原语SlatePlaceholder占位符覆盖层的 attr 与样式由包统一拥有EditableText组合「叶子文本 / 零宽 / 占位符」的分支逻辑支持从 projection 切片拆分多 leaf并以path{[...]}绑定文本与runtimeId零长度切片作为 mark 占位符渲染EditableElement/VoidElement组合普通元素与 void 包裹 spacer 的元素层Editable根循环——挂载、选区同步、焦点初始化、DOM 提交、剪贴板桥接ClipboardBridge全部收编EditableBlocks第一个「诚实的」公开编辑器面向 v2 表面。对应每个原语计划都落地了聚焦的包级测试集中在packages/slate-react-v2/test/runtime.tsx与解决方案文档例如2026-04-04-v2-editable-text-primitives-should-compose-leaf-text-zero-width-and-placeholder.md等。这条收编路径的实质是从「示例冒充渲染器」走向「示例依赖最小真实渲染栈」。从零宽到文档级碎片混合内联、嵌套容器与复杂列表混合内联Editable必须保留后代形态首个「诚实的公开表面」之后的极限是混合内联编辑顶层块同时有直接文本子节点与内联元素子节点。首个红不在剪贴板而在编辑器真值——Editable仍以「把整个根拍平回单个文本块」的方式对账 DOM 编辑。这对单文本节点的证明面够用但对真实混合内联表面是错的输入或粘贴可能保留可见文本却摧毁选区路径仍指向的节点结构。持久修复是让Editable接受snapshotFromDom(...)并让EditableTextBlocks在只替换文本 leaf 内容的同时保留当前后代形态。Chromium 证明随后覆盖跨内联边界的语义化选区、内联子文本节点内输入、混合内联选区的复制/粘贴往返。嵌套与包装单元复用已证明的几何模型计划随后把碎片语义逐级推宽但反复强调一个模式——不是发明新的变换族而是复用已证明的几何模型多块混合内联当每个触及的块仍匹配已证明形态时让碎片提取跨多个顶层块插入侧保持窄——多块混合内联碎片可粘贴进折叠或同块混合内联目标范围但不假装解决任意嵌套块树嵌套引用quote 段落检测任意深度的简单文本块兄弟容器嵌套时把选区提取为包裹碎片粘贴时插回同一容器形态显式at重定基通过「剥掉容器前缀 → 复用顶层文本块重定基逻辑 → 前缀贴回」实现更丰富内联几何把块几何从「直接子节点或一跳」推广为「块内递归文本 leaf 条目leaf 路径 文本长度」其余变换栈块相对偏移映射、碎片提取/插入、显式at选区与 range-ref 重定基、嵌套引用前缀剥离全部继续工作列表包装单元list-item碎片在目标是嵌套bulleted-list时作为兄弟单元处理而非拆成段落子节点同一接缝随后推广到ordered-list、check-list、兼容异构兄弟单元以及「引用 → list-item → 内层引用 → 更丰富内联段落」这样的更深包装栈。每条推广都在三层同时证明slate-v2的核心契约测试clipboard-contract.ts、range-ref-contract.ts、slate-dom-v2的剪贴板边界测试clipboard-boundary.ts、以及 Playwright Chromium 浏览器证明。浏览器证明还沉淀了一条真值真实剪贴板粘贴后折叠选区落在最深插入后代而不是第一个显眼的插入叶子。诚实的剩余极限计划没有声称解决一切而是不断把剩余极限收窄任意嵌套树 range-ref 重定基仍需要更丰富的插入元数据但同块顶层混合内联 refs 已不再被阻塞嵌套容器中「非简单单文本子节点」或「嵌套混合内联容器」的子块仍需要更广的变换无法归约为「递归文本 leaf 列表 包装前缀」的块后代几何仍超出当前模型。验证命令与测试拓扑计划在「Progress」中沉淀了一套可复现的验证命令三层依次收紧# 核心契约slate-v2bun 运行 bun test packages/slate-v2/test/clipboard-contract.ts bun test packages/slate-v2/test/range-ref-contract.ts # DOM 剪贴板边界slate-dom-v2 yarn workspace slate-dom-v2 test clipboard-boundary # 渲染包 yarn workspace slate-react-v2 test # 类型与静态检查 yarn lint:typescript # 浏览器证明面本地服务 Playwright Chromium bash ./scripts/run-slate-browser-local.sh 3100 /examples/slate-v2-placeholder \ yarn build:slate-browser:playwright yarn exec playwright test playwright/integration/examples/slate-v2-placeholder-ime.test.ts --projectchromium --workers1 # 合并后的本地 IME 通道 yarn test:slate-browser:ime:local # 合并后的本地 e2e 通道 yarn test:slate-browser:e2e:local三条值得注意的工程细节Playwright 构建通道必须同时重建slate-v2与slate-react-v2等 v2 包否则站点示例会消费陈旧的 dist 输出——计划中一次「浏览器假阴性」正是由此而来而非运行时逻辑问题slate-browser的嵌套路径选区辅助函数需要把 handle key 显式序列化进 page-eval 快照并等待 handle 水合后再回退到纯 DOM 选区路径避免过早回退导致的假 null 选区真实 Chromium IME 证明依赖yarn exec playwright install chromium安装浏览器二进制。结论占位符 IME 证明沉淀的可复用资产这条证明链最终交付的不仅是「占位符在 Chromium IME 下可用」而是一组可复用的技术结论提交边界IME 合成期文本不是文档真值compositionend才是 Chromium 路径上可信的提交点对每次合成 input 做对账等于测试自己的坏输入策略详见 2026-04-04-slate-v2-placeholder-ime-proofs-must-commit-on-compositionend.md渲染器所有权FEFF 不是换行占位符在 Chromium 下的必需品但 React 不能拥有被 IME 改写的br /内部遇到removeChild崩溃先查 React 是否拥有了错误的 DOM 节点详见 2026-04-04-slate-v2-no-feff-line-break-placeholders-need-dom-owned-br-interiors.md共享接缝策略拆分必须沉淀进slate-react-v2的共享原语ZeroWidthString、TextString、SlatePlaceholder、EditableText、EditableElement、VoidElement、Editable、EditableBlocks而非散落在示例 markup证明纪律任何「浏览器专属」行为FEFF 需求、void spacer 结构、粘贴后选区落点都应以真实 Chromium 证明为准jsdom 合成剧场不可作为 IME 证明对应方案文档见 2026-04-03-jsdom-contenteditable-composition-is-not-a-trustworthy-ime-proof.md渐进推广嵌套容器、列表包装单元、异构兄弟单元等更宽形态优先复用已证明的「递归文本 leaf 包装前缀剥离」几何模型而不是每次发明新变换族。对希望深入验证的读者当前仓库中的相关实现与测试入口包括slate包的编辑器创建与isComposing绑定、isComposing的 DOM 委托实现、slate-react-v2的组件与运行时测试、以及计划对应的完整进度台账 2026-04-04-slate-v2-placeholder-ime.md 与更上层的 slate-v2 总览。【免费下载链接】plateRich-text editor with AI and shadcn/ui项目地址: https://gitcode.com/GitHub_Trending/pl/plate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考