前端

Odoo 编辑器粘贴为什么不是“把 clipboard HTML 塞进 contenteditable”:ClipboardPlugin、白名单清洗与结构重写讲透

很多人做富文本编辑器时,最容易低估的就是粘贴:浏览器能拿到 HTML,插进去不就行了?但 Odoo 的 html_editor 在 clipboard_plugin.js 和 utils/clipboard.js 里做了远比“插入一段 HTML”更复杂的工作:识别 Odoo 自有 MIME、清洗外部内容、修正表格与样式、把 inline 根节点包成 base container,还要和 history、selection、split、sanitize 协同。本文结合官方源码,讲清 Odoo 为什么把粘贴当成编辑器内核问题而不是小功能。

前端
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

只要做过富文本编辑器,你迟早会发现:

粘贴,看起来像小功能,实际上经常是编辑器最脏、最容易失控的一条链路。

因为用户粘进来的内容几乎什么都有:

  • 浏览器复制的普通文本;
  • 别的网站的 HTML;
  • Word / Excel 输出的脏结构;
  • 图片文件;
  • 同一个 Odoo 编辑器里复制出来的专有内容;
  • 还可能带着一堆类名、行内样式、伪节点和奇怪换行。

如果你只把粘贴理解成:

  • clipboardData
  • 拿到 text/html
  • insertHTML()

那基本等于把内容污染、结构错乱和撤销异常主动请进来。

Odoo 在 addons/html_editor/static/src/core/clipboard_plugin.jsutils/clipboard.js 里的处理非常说明问题:它把粘贴视为编辑器内核级输入管线,不是普通事件回调。

一、ClipboardPlugin 不是单点处理器,而是挂在多个核心依赖之间

ClipboardPlugin 的依赖里直接写着:

  • baseContainer
  • dom
  • selection
  • sanitize
  • history
  • split
  • delete
  • lineBreak

这已经告诉你了:

粘贴在 Odoo 里不是“拿数据然后塞进去”,而是会牵动选择区、结构清洗、历史记录、块拆分、删除逻辑和基础容器规则的一整条管线。

如果一个功能要同时依赖这么多核心插件,就说明它已经不是外围小功能,而是编辑器稳定性的中心环节。

二、复制时 Odoo 就已经在为“可控粘贴”做准备

很多人只看 onPaste,其实 onCopysetSelectionTransferData() 同样关键。

Odoo 在复制时不只写:

  • text/plain
  • text/html

它还会在合适场景下写入专有 MIME:

  • application/vnd.odoo.odoo-editor

而且 fillHtmlTransferData() 还会给相对路径图片补上 origin。

这意味着 Odoo 复制链路的目标不是“让浏览器剪贴板里有点东西”,而是:

让同类编辑器之间复制时,能识别出这是一段已经按 Odoo 规则整理过的内容。

这会直接降低跨实例复制时的信息损失。

三、onPaste 的顺序说明 Odoo 对粘贴来源有明确优先级

onPaste() 里很关键的一段是:

  • preventDefault()
  • history.stageSelection()
  • 触发 before_paste_handlers
  • 然后按顺序尝试:
  • handlePasteUnsupportedHtml
  • handlePasteOdooEditorHtml
  • handlePasteHtml
  • handlePasteText
  • 最后 after_paste_handlershistory.addStep()

这个顺序特别重要。

它说明 Odoo 不是“拿到什么插什么”,而是先判断:

  1. 当前选区是否支持 HTML;
  2. 有没有 Odoo 自有 MIME;
  3. 有没有外部 HTML / 图片;
  4. 最后才退回纯文本。

这是一条分层降级链路。

也就是说,粘贴并不是一个 API,而是一组判断后的导流策略。

四、为什么 Odoo 优先识别自有 MIME

handlePasteOdooEditorHtml() 会读 application/vnd.odoo.odoo-editor

如果能命中,Odoo 就会:

  • parseHTML
  • sanitize
  • 再插入 fragment

这一步的价值非常大。

因为同一个编辑器体系里复制出来的内容,通常已经更接近系统认可的结构。相比从第三方网站来的 HTML,自有 MIME 更可能保留:

  • 合法容器结构;
  • 可接受类名;
  • 编辑器内部语义。

所以 Odoo 会优先走这条“高可信来源”路径。

五、外部 HTML 不会直接入库,而要经过 prepareClipboardData() 重写

真正最值得反复读的是 prepareClipboardData()

它至少做了这些事情:

  • parseHTML 把字符串转 fragment;
  • sanitize.sanitize(fragment) 先过一遍安全清洗;
  • 给表格补 table table-bordered o_table
  • 某些场景去掉图片;
  • 对 Excel 粘贴内容提取 style 规则并写回单元格;
  • 遍历子节点执行 cleanForPaste()
  • 把根层 inline 内容包成 base container;
  • <br> 驱动的伪段落切分成独立块。

这已经不是“清洗 HTML”,而是把外部内容重新编译成 Odoo 编辑器能接受的文档结构

六、cleanForPaste() 的重点不是删脏标签,而是重建结构边界

很多编辑器讲白名单时,大家只想到 XSS 或危险标签清除。

但 Odoo 的 cleanForPaste() 更进一步。它处理的不只是安全,还有结构可编辑性。

例如:

  • 黑名单节点直接移除或拆开;
  • 非法节点会被 unwrap;
  • DIV 在满足条件时会被替换成合适的 base container;
  • THEAD 可能被改成 TBODY
  • TD 会补入基础容器;
  • FONT 标签的历史属性会映射成现代 style;
  • 图片形式的 checklist 会被还原成 Odoo 的 checklist 语义类。

这说明 Odoo 想要的不是“HTML 大致长得对”,而是:

粘贴后的内容必须重新回到编辑器自己的结构法则里。

这一步如果不做,后面的列表转换、标题切换、选区定位、历史记录都会越来越脆。

七、白名单不只是标签白名单,还有类名、属性和可样式标签白名单

源码里专门维护了:

  • CLIPBOARD_WHITELISTS.nodes
  • CLIPBOARD_WHITELISTS.classes
  • CLIPBOARD_WHITELISTS.attributes
  • styledTags

这体现了 Odoo 很务实的一点:

不是所有 style 都一刀切,也不是所有 class 都无差别保留。

它会尽量保留对编辑器生态有意义的内容,例如:

  • img-fluid
  • table
  • o_table
  • checklist 类名
  • 部分彩色文本类
  • 合法链接和图片属性

但超出边界的 class / attribute 会被剥掉。

这套策略本质上是在平衡两件事:

  • 别把外部内容洗得什么都不剩;
  • 也别把第三方页面的整套样式污染带进来。

八、为什么粘贴还要和 historyselectionsplit 协同

这点特别能看出 Odoo 的编辑器思维。

history 协同

粘贴前 stageSelection(),结束后 addStep(),说明粘贴必须是可撤销的正式历史步骤。

selection 协同

粘贴、拖放、拆分块时都要基于当前可编辑选区,不然内容会插错地方。

split / lineBreak 协同

纯文本粘贴不是简单插字符串。多行文本需要决定:

  • PRE 里保留;
  • 还是切成多个块;
  • 遇到不可拆块怎么办;
  • 遇到 base container 怎么选正确节点类型。

这说明 Odoo 粘贴处理的目标不是“内容进去了”,而是“内容以编辑器可继续操作的形式进去”。

九、Excel、图片、链接这些特殊分支说明粘贴从来不是单一路径

源码里一些特殊逻辑非常有代表性:

  • 如果是 Excel,解析 style 并补回 td
  • 如果 HTML 和图片文件同时存在,表格优先于截图图片;
  • 如果文本只是一个链接,可能走更适合链接的逻辑;
  • 如果在 <a> 内粘 HTML,会退成文本避免结构污染。

这都在说明一件事:

粘贴不是“格式越丰富越好”,而是“当前上下文下,什么结果最稳定可编辑”。

十、二开里最容易踩的坑

误区 1:自己在 paste 事件里直接插 innerHTML

短期能看到内容,长期一定会在历史、选区、块结构、样式污染上出问题。

误区 2:只做 sanitize,不做结构重写

安全了,但编辑体验仍会坏,因为编辑器内部结构语义没恢复。

误区 3:忽略复制时写专有 MIME

这样同编辑器之间复制会丢掉本来可以保留的上下文质量。

误区 4:把纯文本换行当作简单 <br>

在富文本编辑器里,换行到底对应行内换行还是新块,差别非常大。

十一、结论

Odoo 编辑器里的粘贴,真正难的从来不是“拿到剪贴板内容”。

真正难的是:

  • 识别内容来源;
  • 给自有内容走更高可信路径;
  • 对外部 HTML 执行安全与结构双重清洗;
  • 让插入结果回到 base container / selection / history 能理解的世界;
  • 最后仍然保留尽可能合理的视觉信息。

所以更准确的理解应该是:

ClipboardPlugin 不是把剪贴板内容塞进编辑器,而是在把外部内容重新翻译成 Odoo 可持续编辑的内部文档。

理解了这一点,你再做编辑器粘贴二开,就不会把它当成一个小事件,而会把它当成真正的内容入口管线来设计。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。