ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WangEditor粘贴Word公式乱码解决:OMML转MathML与MathJax渲染完整方案

WangEditor粘贴Word公式乱码解决:OMML转MathML与MathJax渲染完整方案 先问大家一个实操中特别常见、又特别“玄学”的问题在 WangEditor 里粘贴一段从 Word 复制过来的公式文字和表格都还能看唯独公式部分要么显示成m:oMath这种鬼标签要么排版直接乱成一锅粥行内公式挤出一大片空白段落的字体、行距也跟着一起崩。我最早遇到这个问题时第一反应是“这编辑器不行换个吧”后来排查了一圈才发现问题根本不在编辑器而在 Word 塞给浏览器的“HTML”根本不是正经网页用的 HTML。这篇文章把我踩过的坑、试过的方案以及最终落地的代码一起捋一遍。适合所有用 WangEditor 做后台富文本、每天要处理 Word 粘贴内容的同学。我会从问题根源开始拆再给出完整的“粘贴拦截 样式清洗 公式转换 MathJax 渲染”一体化方案最后附上我实际排查时踩到的高频坑。1. 为什么粘公式进来会崩先把根源拆明白1.1 剪贴板里不只有文本还有一整套 Word 专属 HTML当你从 Word 里复制内容剪贴板并不是只存了一份“纯文本”Windows/macOS 会同时塞进来好几种格式纯文本、HTML、图片、RTF 等。浏览器在粘贴事件里默认读取其中的 HTML 片段然后以text/html的形式暴露给你。问题就在于Word 生成的 HTML 是一套非常“自我”的格式顶部带着一堆xmlns:o、xmlns:m、xmlns:v命名空间声明标签里全是MsoNormal、MsoToc1这种 Word 内部类名style 属性里塞满了mso-fareast-font-family、mso-pagination、mso-style-parent这些浏览器根本不认识的专有属性甚至还有!--[if !supportLists]--这类条件注释。把这套东西原样丢给 WangEditor它会直接作为内联 HTML 插入可编辑区域。浏览器遇到m:oMath、v:shape这种未知标签时既不知道该怎么排版也不知道该应用什么样式于是公式区域的文本被拆散成普通字符内联样式又和编辑器自身的 CSS 互相打架最终呈现出来的就是“样式错乱”四个字。1.2 公式在剪贴板里到底长什么样在深入解决之前得先搞清楚公式的“存在形态”。我从实际粘贴内容里见过的公式主要有三种OMML 格式Word 2007 之后默认生成的就是 OMMLOffice Math Markup Language也就是m:oMath和m:oMathPara这一套数学标记。它和网页的 HTML 完全不是一回事浏览器自然不认。MathML 格式少数情况下Word 或第三方插件会输出math标签这个其实是网页标准浏览器虽然原生不支持渲染但 MathJax 可以处理。图片格式老版本 Word、MathType OLE 对象或者用户手动“以图片形式复制”的公式粘贴过来是v:imagedata或普通img这种相对省事但无法再编辑放大还发虚。以最常见的 OMML 为例一段“xy”公式在剪贴板里的简化结构大概长这样m:oMath xmlns:mhttp://schemas.openxmlformats.org/officeDocument/2006/math m:r m:tx/m:t /m:r m:r m:t/m:t /m:r m:r m:ty/m:t /m:r /m:oMath把它直接插入 WangEditor浏览器看到的是m:oMath、m:r、m:t这一堆“生面孔”只能按未知内联元素处理把里面的文本“x y”勉强显示出来但字体、间距、基线全不对遇到分式、根号、上下标就更是灾难现场。1.3 样式错乱的本质不是编辑器 bug而是上下文缺失很多人遇到这个问题第一个念头是换编辑器我一开始也这样想后来才意识到这不是哪家编辑器能靠“默认行为”解决的因为 Word 粘贴进来的是一个“自包含文档片段”它假设自己是独立 HTML 文档的一部分有自己的命名空间、自己的全局设置、自己的样式表上下文。一旦被塞进网页编辑器这些假设全部失效。所以说到底要做的事情只有一件把 Word 的私有格式翻译成网页能理解的内容。翻译分两层一层是“视觉样式”的清洗另一层是“数学公式”的语义转换。只做任何一层都不够后面我会详细说。2. 方案选型纯清洗和公式转换怎么取舍2.1 路线 A只做样式清洗如果你的业务场景里“公式”很少出现或者粘贴进来的公式本来就是图片那可以只做一个轻量级的 HTML 清洗把 Word 的mso-*样式、命名空间、条件注释、o:p空段落等垃圾全部去掉只保留常规的段落、文字、表格结构。这套方案的优点是简单十几行正则或者一个 DOM 遍历就能搞定不改动任何核心逻辑。缺点是公式如果是 OMML清洗之后虽然排版不再乱但公式本身依然无法正常渲染可能显示成一行奇怪的普通字符如果公式是图片那倒是没什么问题图片本身就能显示。所以路线 A 只适合“公式以图片为主”或“公式压根不重要”的项目真正遇到带公式的 Word 文档光清洗门面是不够的。2.2 路线 B公式识别与转换路线 B 的核心思路是粘贴的时候把 OMML 内容识别出来转成浏览器生态能渲染的格式再走正常插入流程。这里有一个技术选型问题OMML 转成什么转成LaTeX适合后端处理比如用 Pandoc或者自己做 OMML → MathML → LaTeX 的链路。前端直接渲染 LaTeX 也很方便KaTeX、MathJax 都支持。转成MathML在浏览器端可以直接用 XSLT 转换微软官方提供了现成的OMML2MML.XSL样式表能把 OMML 转成 MathML。MathJax 原生支持 MathML 输入渲染效果和 Word 里的公式已经很接近了。我最终选择的是 OMML → MathML → MathJax 这条纯前端链路原因有二保真度高OMML2MML.XSL 是微软官方维护的转换器对分式、根号、上下标、矩阵、分段函数这些复杂结构的支持比我自己写解析器靠谱得多。不用后端参与所有转换都在粘贴事件里即时完成用户无感知部署也不增加复杂度。缺点也比较明显需要把那个 XSLT 文件放到静态资源里加载还要引入 MathJax 依赖整体体量不小。2.3 我的建议清洗 转换一起做顺序不能反实际做下来只清洗不转换公式是死的只转换不清洗公式是活了但周围的文字排版还是乱。真正的解法是两步合并成一条流水线拦截粘贴事件拿到text/html用 DOM 解析器把 HTML 变成可操作的 DOM 树在清洗之前先把m:oMath/m:oMathPara提取出来逐个转成 MathML清洗剩余 HTML 里的 Word 垃圾样式和标签把转换好的 MathML 放回原来的位置用 WangEditor 的插入 API 把最终片段插入编辑器调用 MathJax 渲染。这里的关键是第 3 步必须在前如果先做样式清洗可能会破坏 OMML 节点的结构导致后续转换失败。我一开始就是先清洗再转换结果公式节点被正则误伤转换出来一堆残缺 MathML排查了好久才意识到是顺序问题。3. 实操落地从拦截粘贴到公式渲染全流程3.1 在 customPaste 里接管粘贴事件WangEditor v5 提供了customPaste配置项允许开发者完全接管粘贴逻辑。下面的代码是基础框架import { createEditor } from wangeditor/editor const editor createEditor({ selector: #editor-container, html: pbr/p, config: { placeholder: 请输入内容..., customPaste: async (editor, event) { const html event.clipboardData?.getData(text/html) if (!html) { // 没有 HTML 就走默认逻辑比如从 Excel 粘贴纯文本 return false } // 接管本次粘贴 event.preventDefault() const cleanHtml await processWordHtml(html) editor.dangerouslyInsertHtml(cleanHtml) // 返回 true 表示已经处理完毕阻止编辑器默认行为 return true } } })注意customPaste的返回值语义返回true代表“我自己处理了你别管了”返回false或undefined代表“我不拦截按编辑器默认逻辑走”。实际操作中只要是带text/html的粘贴我都会走统一处理流程避免还有遗漏的 Word 内容从缝里溜进去。不同版本的 WangEditor API 略有差异v4 里可能是editor.config.customPaste如果你用的版本没有dangerouslyInsertHtml也可以用editor.insertHtml使用前查一下当前版本文档就行。3.2 清洗 Word 专有样式写一个 sanitize 函数粘贴 HTML 清洗是整套方案的地基。我的做法是先用DOMParser把字符串解析成 DOM再对 DOM 做遍历和处理这样比正则可靠得多正则在处理嵌套标签时非常容易误伤。function sanitizeWordHtml(html) { const doc new DOMParser().parseFromString(html, text/html) // 1. 删除条件注释节点 const comments [...doc.querySelectorAll(*)].filter(node { return node.nodeType 8 || /^\!\[if/i.test(node.nodeName || ) }) comments.forEach(node node.remove()) // 2. 删除命名空间相关属性 doc.querySelectorAll(*).forEach(el { for (const attr of [...el.attributes]) { if (attr.name.startsWith(xmlns)) { el.removeAttribute(attr.name) } if (attr.name.startsWith(mso-)) { el.removeAttribute(attr.name) } } }) // 3. 样式白名单 const styleWhitelist [ text-align, vertical-align, font-weight, font-style, text-decoration, background-color ] doc.querySelectorAll(*).forEach(el { if (!el.style) return const style el.style const keepList styleWhitelist .map(key ${key}:${style.getPropertyValue(key)}) .filter(item item.split(:)[1] item.split(:)[1] ! ) .join(;) if (keepList) { el.setAttribute(style, keepList) } else { el.removeAttribute(style) } }) // 4. 清理 Word 常见垃圾标签但要保留内部文本 doc.querySelectorAll(o\\:p, v\\:shape, v\\:imagedata, st1\\:personname).forEach(el { el.replaceWith(...el.childNodes) }) // 5. 清理空的 span doc.querySelectorAll(span).forEach(el { if (!el.textContent.trim() el.children.length 0) { el.remove() } }) return doc.body.innerHTML }这里有几个我特意强调的细节点style 不能全删。Word 里的上下标经常写成span stylevertical-align:sub加粗可能直接写在font-weight:bold上一刀切全删会导致最后渲染出来上下标变平排、加粗丢失。白名单里我特意留了vertical-align和font-weight。o:p标签要删掉但里面的文本要留下。Word 用它表示段落内的隐形空格直接remove()会丢失内容正确做法是replaceWith(...el.childNodes)。重复删除命名空间属性很重要如果不清理浏览器解析时虽然不会报错但会留下大量无用属性不干净。3.3 OMML 转 MathML用 XSLT 这个隐藏神器清洗只是第一层真正决定公式能不能还原的是 OMML 转换这一步。这里用到的核心工具是微软官方提供的OMML2MML.XSL样式表。这个文件通常可以在 Office 安装目录里找到比如C:\Program Files\Microsoft Office\root\Office16\网上也有很多镜像版本你可以直接下载放到项目静态资源目录。转换思路是把 OMML 节点序列化成 XML 字符串然后交给浏览器内置的XSLTProcessor套用OMML2MML.XSL转换成 MathML。参考实现如下async function initXsl() { if (window.__omml2mmlXslt) return window.__omml2mmlXslt const res await fetch(/assets/OMML2MML.XSL) const text await res.text() window.__omml2mmlXslt new DOMParser().parseFromString(text, application/xml) return window.__omml2mmlXslt } function ommlToMathML(ommlNode, xsltDoc) { const serializer new XMLSerializer() let ommlStr serializer.serializeToString(ommlNode) // 防止命名空间缺失 if (!ommlStr.includes(xmlns:m)) { ommlStr ommlStr.replace( m:oMath, m:oMath xmlns:mhttp://schemas.openxmlformats.org/officeDocument/2006/math ) } const xmlParser new DOMParser() const ommlDoc xmlParser.parseFromString(ommlStr, application/xml) const processor new XSLTProcessor() processor.importStylesheet(xsltDoc) const resultDoc processor.transformToDocument(ommlDoc) const mathmlStr new XMLSerializer().serializeToString(resultDoc) return mathmlStr.replace(/\?xml[^]*\?/, ) }然后在整个处理流程里把 OMML 节点找出来逐个替换成转好的 MathMLasync function processWordHtml(html) { const doc new DOMParser().parseFromString(html, text/html) const xsltDoc await initXsl() // 注意先转公式再做样式清洗顺序不能反 const ommlNodes doc.querySelectorAll(m\\:oMath, m\\:oMathPara) ommlNodes.forEach(ommlNode { const mathmlStr ommlToMathML(ommlNode, xsltDoc) // 用 MathML 片段替换原来的 OMML 节点 const container doc.createElement(div) container.innerHTML mathmlStr const mathmlElement container.firstElementChild ommlNode.replaceWith(mathmlElement) }) return sanitizeWordHtml(doc.body.innerHTML) }这里有一个我在实际项目中才真正踩明白的坑不能用ommlNode.outerHTML直接作为 XML 字符串。outerHTML在 HTML 文档里序列化时命名空间信息可能会丢失导致 XSLTProcessor 转换后输出空结果。所以一定要用XMLSerializer来序列化然后显式检查并补上xmlns:m命名空间声明。3.4 插入 MathML 并让 MathJax 渲染MathML 是标准网页内容但浏览器不会主动把它“画”成漂亮的公式所以还需要 MathJax 接手渲染。MathJax v3 的配置方式很简单引入带 MathML 输入的版本即可script window.MathJax { loader: { load: [input/mathml, output/chtml] }, options: { enableMenu: false } } /script script async srchttps://cdn.jsdelivr.net/npm/mathjax3/es5/tex-mml-chtml.js/script如果你不想单独配 loader直接加载tex-mml-chtml.js这个组合包也可以它已经包含 MathML 输入和 HTML 输出。插入和渲染的调用顺序要注意先用dangerouslyInsertHtml把清洗后的 HTML 插入编辑器再触发 MathJax 重新扫描渲染editor.dangerouslyInsertHtml(cleanHtml) // 等待 MathJax 渲染当前文档里的 MathML if (window.MathJax?.typesetPromise) { await MathJax.typesetPromise() }这里我遇到过一个问题dangerouslyInsertHtml是异步渲染的如果立刻调用MathJax.typesetPromise()新插入的节点可能还没进 DOMMathJax 扫不到它。稳妥的做法是稍微等一下或者用setTimeout推迟调用setTimeout(async () { if (window.MathJax?.typesetPromise) { await MathJax.typesetPromise() } }, 0)实际操作中setTimeout 0通常就够了因为dangerouslyInsertHtml内部也是通过requestAnimationFrame或微任务插入 DOM 的。3.5 图片公式兜底老 Word 公式的保底方案不是所有公式都能转成 OMML。老版本 Word 里用 MathType 插入的公式复制出来可能是一张v:imagedata对应的图片用户也可能主动选择“以图片形式复制”。针对这种情况我保留了图片通道作为兜底。处理逻辑比较简单在清洗前先扫描v:imagedata标签把它的src属性提取出来转成普通img或者直接保留原img节点doc.querySelectorAll(v\\:imagedata).forEach(el { const img doc.createElement(img) img.src el.getAttribute(src) || el.getAttribute(data-src) || img.alt formula image el.replaceWith(img) })如果图片是 base64 格式还要考虑体积问题。我习惯的做法是粘贴后立即把 base64 图片提取出来异步上传到对象存储再替换成 CDN 地址避免编辑器里存大量 base64 导致文档体积爆炸。上传时机可以放在粘贴之后不阻塞用户输入。4. 常见问题与排查技巧实录4.1 公式变成“空心方块”或“乱码标签”这是最常见的症状出现原因基本就三种粘贴 HTML 里根本不是 OMML而是图片或其他格式OMML 转换没被调用MathML 没生成浏览器直接渲染了原始标签MathJax 没加载完成或没配置 MathML 输入。排查时先看控制台在processWordHtml入口打印一下原始 HTML确认里面有没有m:oMath或math。如果没有那说明 Word 复制出来的就是图片或别的格式走图片兜底逻辑如果有检查转换函数是否真的执行了再确认最终插入编辑器的 HTML 里是不是math标签而不是m:oMath。4.2 清洗后加粗、居中、上下标丢了这个问题我在刚开始做清洗时踩过一次偷懒直接el.removeAttribute(style)结果原来 Word 里用样式表示的居中、上下标全部失效。后来改成白名单制把text-align、vertical-align、font-weight这几个属性保留下来问题才解决。更好的方式其实是“看菜下饭”你先从一个真实 Word 文档里复制一段内容丰富的内容把粘贴 HTMLconsole.log出来看看字体、上下标、对齐到底是通过标签还是 style 表示的再调整白名单。不同版本的 Word、WPS 输出的格式有差异建议拿你们业务里最常见的 Word 版本做基准测试。4.3 表格里的公式漏转或渲染错位公式经常出现在 Word 表格单元格里m:oMath节点是td的后代。如果你只对doc.querySelector(.content)这个局部范围做查找很可能把表格里的公式漏掉所以查找范围一定要用整个doc不要限定局部容器。还有一种情况是 XSLT 转换之后MathML 片段里带了多余的换行或空白符插到表格 cell 里就把宽度撑爆了。遇到这种情况可以在转换后把 MathML 字符串里的空白符压缩一下或者手动清理mstyle里不必要的属性。4.4 保存后打开的页面公式不渲染如果你把粘贴后的内容保存到了数据库下一次打开页面时HTML 里的math标签不会自动渲染必须重新加载 MathJax 并扫描整页。正确做法是在页面内容渲染完成后再调用一次MathJax.typesetPromise()而不是只在粘贴事件里调用。同时我建议在“查看模式”下把 WangEditor 设为只读避免用户二次编辑时不小心破坏 MathML 结构。WangEditor v5 里可以直接调用editor.disable()切到只读如果需要创建时就只读可以看下你的版本是否支持readOnly: true配置v4 里则是设置editor.config.readonly true。只读场景下也要先等 MathJax 渲染完否则用户看到的还是没排版的 MathML 标签。4.5 粘贴超大文档卡顿后台系统经常有人直接粘贴一整篇 WordHTML 可能达到几十上百 KB甚至有人说测得数据量到“4万条”级别的内容。DOM 遍历和 XSLT 转换都挺吃 CPU处理不好页面会卡死。我的经验是加一个大小门槛如果html.length超过某个阈值比如 500KB就不强行走公式转换先提示用户“内容过大只保留纯文本”或者分块处理。MathJax 渲染也不能全部一次过可以分批typesetPromise避免渲染线程被长时间占用。另一个优化点是缓存 XSLT 文档我上面的window.__omml2mmlXslt就是在做这个事避免每次粘贴都重新 fetch 一份几十 KB 的 XML。5. 我踩坑后留下的几条经验这套方案从上手到稳定运行我自己大概折腾了两三个版本才最终定下来。第一个版本就是简单删 style公式全乱第二个版本加了公式转换但顺序反了先清洗再转公式最后转出来的 MathML 总缺东西第三个版本才彻底理顺。几个个人觉得特别值得分享的经验第一Word HTML 清洗没有银弹。市面上很多text sanitize库主要是为了 XSS 安全设计的对 Word 垃圾样式并不上心别指望一个库通吃所有场景。我的建议是拿你们真实业务里的 Word 文档做样本自己维护一套清洗白名单合身最重要。第二OMML2MML.XSL 这个文件建议放到自己的静态资源里别用第三方在线地址。这个文件比较大在线加载既不稳定也拖慢速度放自己 CDN 上线前测试一下就完全可控。第三保存时别只存渲染后的 HTML。我现在的做法是同时存一份纯文本版方便做搜索和摘要如果服务端还想做公式的进一步处理比如转 LaTeX 文档导出可以在后端加一步 OMML → LaTeX 的转换。这样前端展示、后端导出、内容检索各用各的格式互不干扰。第四一定要拿多个 Word 版本做测试。Word 2016、Word 2019、WPS 生成的 OMML 结构大同小异但m:r、m:oMathPara的组合方式可能略有区别XSLT 转换偶尔会漏掉个别子节点。测试用例里至少包含行内公式、块级公式、带上下标的分式、矩阵、多行公式这几种典型结构。最后再说一个小技巧如果你实在不想引入 MathJax只想要公式不崩可以检测到 OMML 之后提取公式的纯文本部分插入比如把“xy”作为普通文本显示。至少不会再出现乱码标签代价是公式长什么样就完全看不出来了。对有公式排版要求的产品我还是推荐走完 MathJax 这条完整链路效果真的完全不一样。
返回列表