1. 项目背景与需求拆解:XHEDITOR在涉密办公场景的定位
做军工配套办公系统的人应该都有同感:涉密内网环境下的 Web 办公系统,能选的富文本编辑器其实非常有限。国外那几家主流编辑器,哪怕功能再强,也天然存在合规疑虑——代码闭源、更新依赖国外社区、还时不时要请求外部资源。所以当项目里需要一个既能在完全离线环境跑、又能按涉密要求做代码审计和裁剪的编辑器时,选来选去,XHEDITOR(小红帽编辑器)成了最顺手的底座。它是国内作者开源的轻量级 HTML 编辑器,文件体积小、依赖少、源码结构清晰,更重要的是所有功能都在浏览器本地完成,不需要往任何外部服务器发请求,这在内网隔离环境下是硬指标。
项目的标题里有个关键词——“Word 公式安全导入”。这句话拆开看,包含三层需求:第一,用户已经用 Word 排好含有公式的材料,希望直接搬到网页端继续编辑,而不是重新敲一遍;第二,公式不是普通文字,Word 里的公式在浏览器中默认是无法解析的,必须走一条转换链路;第三,“安全”二字在内网语境下格外沉重——公式数据不能外传、不经过任何外部在线识别服务、品牌不能有授权风险、HTML 内容不能被注入恶意代码。也就是说,这不仅是一个格式转换问题,更是一个“链路自给自足 + 内容清洗过滤”的工程问题。
我这边落地的场景是这样的:单位内部的信息发布和文档协同系统使用 XHEDITOR 作为正文编辑器,用户提交的原始文档大量来自 Word,尤其是技术文档、试验报告、方案评审材料,公式几乎是标配。过去这些文档进系统,最粗暴的办法是整篇转 PDF 或者截图,公式是保住了,但文字不可编辑、不可检索、不可复用,后续维护成本极高。这个项目本质上要解决的,就是把 Word 公式这条“单向死胡同”,变成一条“可编辑、可渲染、可复用的双车道”。
下文我尽量把思路、选型、踩坑过程都摊开讲。这中间没有银弹,每一步都是基于内网约束妥协出来的方案,但对同样受困于“Word 公式 + 网页编辑器”组合的人,应该有直接的参考价值。
1.1 涉密内网环境下编辑器选型的真实约束
很多人在互联网项目里做惯了,习惯于“先选编辑器再说”——TinyMCE 功能全、Quill 好看、wangEditor 中文友好。但在军工涉密场景下,选型逻辑完全倒过来了。第一优先级不是功能丰富度,而是可控性和合规性。所谓可控,是指源码必须拿得到、读得懂、改得动,一旦出了安全问题,不能等着开源社区发补丁,必须在本地研发团队手里消化;第二是内网环境中软件获取困难,商业编辑器的授权文件、第三方插件的联网验证机制很可能在隔离网络里直接失效;第三,编辑器必须彻底剥离“外联”能力,不能自带 CDN 引用、字体加载、远程图片上传回调这类功能,哪怕只有一个地方会发出外网请求,都会被安全审查一票否决。
XHEDITOR 在这几个维度上得分都不错。它采用 MIT 许可,源码是国内开发者维护,对 HTML 解析和过滤逻辑是内嵌的,没有依赖 Node 服务端或者云端 API。编辑器核心就两个文件:xheditor.js和xheditor_lang.js,再加上几个皮肤文件,部署起来极其干净。它的 HTML 清洗是“白名单策略”——只保留预先定义的标签和属性,其余一律过滤,这比很多编辑器“黑名单策略”的安全底子要好。更关键的是,它支持源码模式和可视化模式互切,这为后面的公式导入提供了入口:我们可以在源码模式下直接注入 LaTeX 代码块,再让 MathJax 在可视化模式下完成渲染。
1.2 真正要解决的三个核心问题
围绕“Word 公式安全导入”,这个项目其实是在同时解决三个问题,任何一个环节没闭环,整套流程就断掉。
第一个是“格式翻译”。Word 公式是以对象形式嵌入文档的,最常见的是 MathType/OLE 对象,也可能是 Word 自2010 版以后的 OMML(Office Math Markup Language)原生公式。浏览器不认识这些格式。必须有一套离线方案把它们翻译成网页能理解的语言。业内公认的中间语言是 LaTeX——它既不是 Word 私有格式,也不是浏览器原生格式,但 MathJax/KaTeX 这类渲染库只需寥寥几行配置,就能在网页里把它变成跟 Word 里几乎一致的排版效果。
第二个是“通道清洗”。从 Word 复制粘贴到浏览器编辑器的内容,天然携带大量垃圾元素:<span>标签套<!-- -->注释、VML 矢量标记、OLE 加载控件、<xml>命名空间定义。更危险的是,一些 Word 宏或者嵌入对象可能带有客户端执行能力。如果只做格式不转换而直接放行,轻则编辑页面乱掉,重则给内部系统开一个 XSS 的洞。所以安全导入必须做成一个“过滤管道”,HTML 进来,先过白名单、洗掉一切可执行属性,再落进编辑器。
第三个是“使用体验”。公式导入之后不允许变成一张死图片,它得能跟随文字排版、能参与二次编辑、能保持编号和交叉引用。这意味着在链路设计之初就要想好公式的“持久化形态”——存成什么格式、以什么标记包围、渲染脚本怎么挂载。我们最终确定的是:源数据存 LaTeX 源码 + 渲染层用 MathJax 按需加载,这个方案既保存了编辑活性,也保证了预览效果。
这三个问题,后面每一节都会对着实操展开。
2. 方案设计:Word 公式为什么不能直接进浏览器
别看“把 Word 公式导入网页编辑器”这句话说起来轻松,真正写代码的人第一次遇到这需求,大概率会在浏览器控制台里反复看空白页。想明白“为什么”之前,先别急着“怎么做”。
2.1 Word 公式的三种形态与技术本质
在拆解方案前,我先把 Word 公式的“底盘”讲清楚。不同时代的 Word、不同插件装配习惯,会让同一篇文章里的公式呈现完全不一样的底层结构:
第一种形态是老牌插件 MathType 创建的 OLE 对象。MathType 是安装在 Word 里的 ActiveX/OLE 组件,公式作为内嵌对象存在。你在 Word 里双击公式,会弹出 MathType 的独立编辑窗口,而公式本身在 docx 文件里是一段以\x01开头、\x0B结尾的 OLE 二进制包装。这种对象浏览器端无法解析、无法直接读取文字内容,常见的做法是在 Word 端借助 MathType 自身的“转换为 LaTeX”功能导出公式文本。
第二种形态是 AxMath、Mathtype 高版本等国产插件创建的域对象。AxMath 的公式本质上是域代码,Word 按 F9 切换视图时,能看见类似{ EQ \f(...) }或者自定义域的原始结构。它比 OLE 略“透”一点,但同样无法被网页直接渲染。
第三种形态是 Word 原生公式(OMML)。从 Word 2007 起,微软引入了 Office Math Markup Language,公式以<m:oMath>标签存放在 docx 包内,Word 2019/365 里用Alt+Shift+=输入的就是这类公式。OMML 是 XML 格式,理论上浏览器其实能解析——如果用 Office Online 的渲染服务。但内网环境没有 Office 在线服务,所以还是得转。
这还没算第四种“伪公式”——直接从 PDF 截图或者拍照贴进 Word 的公式图片。严格说它不算公式,只是图形,不在本项目的转换范围内。
理解了这几种形态,你就能明白整个问题的本质:浏览器只认 HTML/CSS/JS,而 Word 公式是私有二进制或专有 XML,两者之间必须有一个“翻译官”。这个翻译官就是 LaTeX 吗?不一定,但它目前是最划算的。
2.2 为什么“LaTeX + MathJax”是最优中间态
技术圈聊公式交换格式,绕不开两个:LaTeX 和 MathML。前者是 TeX 系统里人类可直接编写的标记语言,以\frac{1}{2}、\sum_{i=1}^{n}这种反斜杠命令为代表;后者是 W3C 制定的数学 XML 标准,结构严谨但冗长到令人窒息——一个分式就要写近十层标签。
选 LaTeX 不选 MathML,我基于三条理由:
一是人可读性。内网环境里,公式从 Word 转过来之后,总要有人检查、校对、修正。LaTeX 的源码干不干净一眼就能看出来,MathML 那种<mrow><mfrac><mrow><msub>的嵌套地狱,人工审阅几乎不可能。
二是工具链的成熟度。MathType 和 AxMath 的“复制为 LaTeX”功能已经做得非常完善;Pandoc 可以将 docx 里的 OMML 公式自动转成 LaTeX 片段;甚至可以自己写解析器处理 OMML 到 LaTeX 的映射。反过来,把 MathML 作为目标的转换工具少得多,中文资料更是匮乏。
三是前端渲染的生态。MathJax 是老牌渲染引擎,KaTeX 主打高性能,两者都以 LaTeX 为主体语法。MathJax 对 LaTeX 的兼容性尤其好,在文档流中支持\(...\)和$$...$$两种定界方式,而且可以手动控制渲染时机,这正好契合 XHEDITOR 这种“动态内容注入”的场景。MathML 在 MathJax 里也能渲染,但那是“备选项”,不是主路径。
所以项目定的中间格式就是 LaTeX 文本,渲染层用 MathJax 2.7.8(后面细说为什么没用 3.x),编辑器内用一对定界符把 LaTeX 源码包起来,存储和传输都以源码形式走。
提示:这里说的 LaTeX 不是指“用 LaTeX 写整篇论文”,而是特指数学公式片段,即 document body 里的公式命令。脱离论文框架的单条公式片段,转换成本低、复用效率高。
2.3 “安全导入”的双重含义
提到安全,很多人第一反应是防止 XSS 攻击。但在这个军工项目里,“安全”二字是双层的。
第一层是数据链路安全。涉密环境下,公式内容本身就是敏感信息,哪怕只是一个简单的参数关系式,也不能通过任何在线公式识别 API、OCR 云服务、公共转换网页进行处理。整个转换过程必须发生在内网内的三台设备之间:用户的办公电脑(Word 端)、应用服务器(转换清洗端)、用户的浏览器(渲染端)。所有工具要么是内网已部署的,要么是开源可离线运行的。任何形式的“调一下公网接口”都直接违反保密纪律。
第二层是内容注入安全。Word 文档转成 HTML 时,其中可能夹杂可执行脚本、事件属性、iframe 引用、对象嵌套。即便来源是内部员工,也不能排除终端中毒后文档被污染的可能性。所以在 XHEDITOR 接收 HTML 的前面,必须设一道强校验的清洗层,只放行我们定义好的标签和属性,把一切可执行度高、与公式渲染无关的元素全部剥离。
我在项目初期踩过一个相关的坑:从 Word“另存为网页”得到的 HTML,如果直接倒进编辑器,页面里会出现大量v:group、v:shape、<!--[if gte mso 9]>...<![endif]-->这类 VML/条件注释。看起来无害,但里面埋着 OLE 对象加载逻辑,在某些浏览器版本中就是任意命令执行的突破口。所以清洗层不能只过滤script这种一眼可见的标签,而是要把 Word 生成命名空间、IE 条件注释、ActiveX 引用的所有元素都纳入黑名单,只保留纯文本和图片。
内网项目的安全,很多时候是“自己不给自己挖坑”。公式转换链里任何一环引用了不可控的外部资源,就是给整个系统留了口子。
3. 实操落地:XHEDITOR 中公式安全导入的完整流程
思路定下来之后,剩下的就是工程实现。这部分比较长,我按“单条公式导入”“批量文档导入”“MathJax 集成”“安全过滤配置”四个小节逐一展开,每一步都给可直接抄的配置和代码。
3.1 单条公式导入:MathType/AxMath 转换路径
日常最常见的场景是:用户手里已经写好了 Word 文档,里面的公式数量不算多,需要时不时补几个。针对这种“点状操作”,最合理的流程是走 Word 插件端,把公式转成 LaTeX,再人工粘贴进 XHEDITOR。这个路径不需要开发专门的导入工具,部署上最轻。
具体操作步骤:
在 Word 中双击公式,进入 MathType 编辑窗口。点击 MathType 菜单栏的“预置(Preferences)” → “剪切和复制预置( Cut and Copy Preferences)”,将“转换格式”设为“LaTeX 2.09 或之后的版本”,确定保存。这一步很关键,默认的复制格式是 MathML,粘贴到网页里就是一堆
<math>标签,MathJax 虽能识别但比较别扭,而且带命名空间的 MathML 在清洗环节容易被拦掉。在 MathType 编辑窗口里,选中公式全部内容(
Ctrl+A),再Ctrl+C复制。此时剪贴板里同时有 LaTeX 文本和其他格式。切到 XHEDITOR,不要直接粘贴到可视化模式,而是先点工具栏的“源码”按钮,进入源码编辑模式。在源码模式里,选择你要插入公式的位置,粘贴内容。粘贴进去的 LaTeX 可能是
\frac{a}{b}这种不带定界符的裸代码,也可能带\[...\]或$$...$$。为了保证 MathJax 一定能识别,我们统一约定:行内公式套\(...\),独立公式套$$...$$。所以在源码模式里贴完,手动在首尾补上定界符。保存或预览,MathJax 会自动把
$$...$$块渲染成排版后的公式。如果编辑器内没看到效果,刷新预览页或者触发一次手动渲染(渲染接口见 3.3 节)。
AxMath 的逻辑类似:在 Word 里选中公式,用它的“复制 LaTeX”功能(AxMath 面板上有对应按钮),然后走同样的粘贴路径。值得一提的是,AxMath 生成的 LaTeX 在个别命令上和 MathType 有差异,比如绝对值符号的写法、矩阵环境的&分隔符,这类细节建议在 XHEDITOR 源码模式下肉眼核对一遍再保存。
这条路最大的优点是零开发量、零工具安装。缺点是效率低:十几二十条公式还能忍,上百条公式靠人工粘贴,手会断,而且出错率高。所以批量场景必须走自动化。
3.2 批量文档导入:Word 转 HTML 加本地 LaTeX 化
批量场景是这种系统的重头戏——积压的历史 Word 文档要一次性倒进知识库,里面的公式几十上百个,不可能让人坐在那儿一条条复制。批量的链路我设计成三段式:“Word 单机转码 → 本地清洗脚本 → XHEDITOR 批量入库”。
第一段,用 Word 自身的“另存为网页(过滤格式)”功能,将 docx 转成 HTML 文件。这一步会在本机完成,需要一台安装了 Word 2019/365 的办公电脑,配合一个带宏的 Word 模板来批量导出(VBA 循环遍历词本目录,用SaveAs2指定wdFormatFilteredHTML)。导出的 HTML 里,Word 原生公式会以<m:oMath>OMML 元素存在,MathType 公式则变成图片加上 VML 包装,这两种都需要后续处理。
第二段,跑本地清洗和转换脚本。脚本要做两件事,而且必须分开做:
一是把 OMML 元素转成 LaTeX。我试过几种方式,最稳的是用 Pandoc:pandoc input.docx -t html --mathjax可以直接把 docx 里的公式以 (...) 形式输出到 HTML。如果不想引入 Pandoc,也可以写 XSLT 脚本把 OMML 映射成 LaTeX,但工程量不小,要处理的节点很多,光是分数、根号、上下标、求和、积分、矩阵这几个大类就够写几百条模板。实际项目里我最终选用了 Pandoc,离线安装后作为内网标准工具之一,性能和准确性都经得起几十页文档的考验。
二是把转后 HTML 里的“危险内容”全部剥掉。这一步不仅仅是等编辑器端的清洗,而是要提前做第一遍过滤,缩短脏数据进入数据库的时间线。过滤规则白名单只保留:p, br, div, span, table, thead, tbody, tr, td, th, h1~h6, ul, ol, li, a, img, strong, em, sub, sup, blockquote这几个结构性标签,其余全部丢弃。属性只保留href, src, alt, title, width, height, colspan, rowspan,事件类属性(onclick/onload/onerror等)一律不放进清单里。
第三段,把清洗后的 HTML 分段导入系统。导入时的关键操作是用 XHEDITOR 的setContent(html)方法写入内容,而不是走前端粘贴。这样可以绕过浏览器剪贴板的格式噪音,也便于在服务端对内容做二次落库审查。如果量特别大,可以分批导入,每批 20~30 篇,避免一次事务过大导致系统卡顿。
注意:Pandoc 转出来的 HTML 里公式是用 MathJax 定界符包的,比如
\(x^2 + y^2 = z^2\)。这些定界符在清洗脚本里必须保留,不能因为“非标准 HTML 标签”被过滤掉。所以在写白名单时,文本节点内容不参与标签过滤,只针对标签本身做白名单校验。
3.3 XHEDITOR 与 MathJax 的集成细节
公式转换成 LaTeX 且放进编辑器内容区之后,还差一步“渲染”。这一步由 MathJax 完成。很多人卡在这里,因为 XHEDITOR 是动态渲染富文本,MathJax 默认只在页面加载时扫描一次 DOM,后由 XHEDITOR 写入的内容它根本不会碰。不处理这个问题,公式永远显示为源码文本。
我在页面里加载 MathJax 2.7.8,配置如下:
<script type="text/x-mathjax-config"> MathJax.Hub.Config({ tex2jax: { inlineMath: [ ['\\(','\\)'] ], displayMath: [ ['$$','$$'] ] }, skipStartupTypeset: true }); </script> <script src="/lib/mathjax/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>这里的关键是skipStartupTypeset: true——让 MathJax 启动时不自动渲染全文,避免与页面初始化时序冲突,把渲染时机交给我们手动控制。
渲染触发点在 XHEDITOR 的两个事件上挂:
var editor = $('#content').xheditor({ ... onRender: function() { typesetMath(); }, afterSetContent: function() { typesetMath(); }, afterUp: function() { typesetMath(); } }); function typesetMath() { if (window.MathJax && MathJax.Hub) { MathJax.Hub.Queue(["Typeset", MathJax.Hub]); } }afterSetContent对应我们通过脚本向编辑器注入内容的场景,afterUp对应源码模式切换回可视化模式,onRender则是页面初始化。三处都挂上,公式才能保证“该亮的时候一定亮”。
为什么用 2.7.8 而不是 3.x?MathJax 3.x 走的是模块化 ES6 架构,配置方式完全不同,倒是性能更好。但 2.x 的MathJax.Hub.Queue(["Typeset", MathJax.Hub])这种手动队列模式与动态富文本编辑器配合最顺,而且 2.x 的中文社区资料多、踩坑记录全。内网项目的原则是“稳定优先、文档可查”,所以最终锁定 2.7.8。如果你上来就装 3.x,记得把渲染接口换成MathJax.typesetPromise(),实质思路一样,代码不通用。
3.4 安全过滤规则配置实战
XHEDITOR 自带一套 HTML 过滤机制,但默认配置是针对公开网站设计的,对涉密内网来说还是过于宽松。需要自己定制一份“内网白名单”,加在xheditor.js的配置里。
基础过滤配置这样写:
$('#content').xheditor({ tools: '...', onPrePaste: function(html) { return safeClean(html); } }); function safeClean(html) { // 移除所有不在白名单的标签 var allowedTags = ['p','br','div','span','table','tr','td','th', 'tbody','thead','ul','ol','li','a','img', 'strong','em','sub','sup','blockquote','h1','h2','h3']; // 用 DOMParser 解析 var doc = new DOMParser().parseFromString('<div id="wrap">' + html + '</div>', 'text/html'); var root = doc.getElementById('wrap'); function clean(node) { if (node.nodeType === Node.ELEMENT_NODE) { var tag = node.tagName.toLowerCase(); if (allowedTags.indexOf(tag) === -1) { // 保留子节点,拆掉元素本身 while (node.firstChild) { node.parentNode.insertBefore(node.firstChild, node); } node.remove(); return; } // 属性白名单 var allowedAttrs = ['href','src','alt','title','width','height','colspan','rowspan']; Array.from(node.attributes).forEach(function(attr) { var name = attr.name.toLowerCase(); if (allowedAttrs.indexOf(name) === -1 || /^on/i.test(name)) { node.removeAttribute(attr.name); } }); } Array.from(node.childNodes).forEach(clean); } Array.from(root.childNodes).forEach(clean); return root.innerHTML; }这个safeClean函数用在onPrePaste回调里,所有粘贴进入编辑器的内容都会先过一遍这道闸门。它的设计理念是“宁可丢格式,不可留风险”。
再补一条服务端防线:入库时用 Java/Python 后端再调用一次同样的清洗逻辑(复用同一份白名单规则,前端后端各持一份,保持同步)。前端过滤防的是普通用户有意无意粘贴的坏内容,后端过滤防的是有人绕过前端直接 POST 数据。两道防线缺谁都不踏实。
还有一个小细节容易被忽略:公式里的 LaTeX 源码中常见\{、\}、_、^这些合法但敏感的字符,在 XHEDITOR 的源代码模式里会正常显示,但如果切到可视化模式,XHEDITOR 的轻量级 HTML tidy 器可能会对<、>做实体转义,导致 LaTeX 源码变形(比如把\frac{a}{b}的{}转成了{})。处理办法是在afterUp回调里,先对内容里的公式定界符做保护(临时替换为占位符),再切换编辑器模式。这个细节如果不在测试阶段重点盯,上线后一定会遇到“公式莫名其妙变成乱码”的工单。
4. 常见问题与排查实录
做这类系统的经验往往集中在“踩坑”上。我把项目上线前后遇到的高频问题整理成一份实录,每个问题都附上当时的排查路径和最终解法。
4.1 公式粘贴后乱码,或 LaTeX 源码被 HTML 实体化
现象:从 MathType 复制公式,切到源码模式粘贴,显示是一堆\frac{1}{2},但是切回可视化模式再切回来,源码里多了&、{之类的实体编码。
原因:XHEDITOR 在模式切换时会对内容做 HTML 规范化,它默认把<、>、&、引号转为实体。LaTeX 代码里恰好充满这些字符,于是“失真”。
解法:不要直接通过可视化模式切换,而是把 LaTeX 代码在源码模式里插入后,先保存到后台再重新加载渲染。如果一定要在编辑器内即时渲染,可以先把 LaTeX 放进一个textarea或pre标签里包裹,让 XHEDITOR 不对包裹内的字符做转义,再配合 MathJax 渲染。另外,我写过一个临时替换函数,在切模式前把\(、\)、$$替换成【M】、【/M】这类占位符,切完再替换回来,实测能规避绝大多数实体化问题。这个土办法看起来笨,但在内网项目里异常好用。
4.2 MathJax 渲染不出来,公式显示为源码
现象:内容已经写入,库里也能看到$$...$$,但页面只显示文本,没有公式图形。
排查路径:打开浏览器控制台看有没有 JS 报错;确认 MathJax 脚本有没有加载;确认skipStartupTypeset配置后是否有代码主动触发渲染;最后确认定界符与配置是否一致——最常见的是库里存的是$...$(单美元)而配置只认\\(...\\),两边对不上,自然静默不渲染。
解法:统一定界符。项目里所有导入链路都约定\(...\)和$$...$$,不再允许单美元定界,因为单美元在正文里误伤率极高,一个美元符号就可能导致整段异常。同时在typesetMath函数里加一行日志,渲染队列触发时打印“Typeset called”,方便线上排查是不是回调没挂上。
4.3 公式编号与交叉引用丢失
现象:Word 文档里的公式是有编号的,比如(1-1)、(2-3),正文里写着“见式(1-1)”。导入后,编号没了,交叉引用变成了死文本。
原因:Word 公式的编号本质是 SEQ 域,不是公式内容的一部分。把公式转成 LaTeX 时,编号域并不会被 Pandoc/MathType 自动带出来,它停留在原文档的上下文里。
解法:分两步走。第一步,在 Word 端用宏把 SEQ 域“煮熟”——选中所有公式编号域,Ctrl+F9展开,然后Ctrl+Shift+F9取消域链接,把编号变成静态文本。第二步,批量导入后写一个后处理脚本,检测独立公式行($$...$$),如果它前面一行或后面一行有(数字-数字)样式的文本,就把这个编号嵌入公式环境,形如:
$$ E_k = \frac{1}{2}mv^2 \qquad (2-1) $$这样编号进入公式源文本,后续维护时人工调整也会容易很多。
4.4 Word 另存网页带来的“毒标签”与样式污染
现象:导入 XHEDITOR 后,整个编辑区域字体大小、行距变得混乱,甚至有的段落在可视化模式不显示,但源码模式里明明有内容。
原因:Word 另存为网页时,会产生大量mso-*内联样式,比如mso-bidi-font-size:11.0pt、mso-fareast-language:ZH-CN之类的命名空间属性。XHEDITOR 的默认过滤并不会逐条清理这些属性,它们残留在span上,就会污染整个页面的展示。
解法:在清洗脚本的白名单属性里,明确禁止mso-前缀过线。上面那段safeClean的allowedAttrs里加入一个前缀过滤条件:
if (name.indexOf('mso-') === 0) { node.removeAttribute(attr.name); return; }同时把style属性从白名单中移除——公式渲染靠 MathJax 自己算样式,不需要 Word 注入的内联样式,移掉之后排版反而更干净。
4.5 表格内公式显示差一行、对齐异常
现象:公式在普通段落里显示正常,但放进 XHEDITOR 的表格单元格后,公式的基线与文字不对齐,出现明显的上下偏移。
原因:Word 转出的表格td高度不一致,且 MathJax 在表格内渲染时,默认的行高与中文文字行高有冲突。这个问题的根子在 CSS 而没有。
解法:给 XHEDITOR 的内容区补充一段统一样式:
.xheditor-content mjx-container { vertical-align: middle; margin: 0 4px; } .xheditor-content td mjx-container { display: inline-block; line-height: 1.2; }必要时也可以给td设置固定行高,并将垂直对齐方式设为middle。这类问题在测试环境基本不会暴露,数据集一多就会冒头,建议上线前就内置该样式。
5. 项目收尾后的几点体会
流程跑通之后,回头再看这个项目,有几个认知层面的收获值得留个记录。
第一,内网环境逼出来的方案未必绕远路。为了不调用在线服务,我把公式转换链路全部压到本地:Word 端转换、Pandoc 批量处理、后端清洗、前端 MathJax 渲染。最初觉得没有“云公式识别”这种大杀器,转换精度可能不行,实际跑下来,Pandoc 对 OMML 的转换准确率超过预期,MathType 手动路径更是完全保真。而这条路换来的最大好处是,整个过程没有任何外部依赖,系统运行近一年,“外联风险”在审计记录里始终保持为零。
第二,过滤规则的“白名单”理念比“黑名单”省心太多。早期我也试着放过 Word 生成的绝大多数标签,只把script和iframe拉黑,结果发现 Word 的格式垃圾根本归不完。后来下定决心,凡是不在业务需要的清单里的一律拆标签保文本,这之后恶意注入和样式污染的问题几乎归零。过滤策略这种设计上的取舍,会实际决定你后续要维护多少补充规则。
第三,单条公式和批量公式的场景要分离。强行用批量方案处理零星公式,会引入过度工程;强行用人工方式处理大批量文档,则会消耗大量工时。项目上线后我给运维人员写了一份流程指南:关键词只含两三条的文档,直接走 MathType 复制 LaTeX 通道;十页以上的报告,挂到批量转换服务器上跑一跑。这套“场景分流”策略让不同岗位的人都能找到效率最大化的路径。
最后分享一个实用小技巧:XHEDITOR 的源码模式里,可以把 LaTeX 定界符包进<span contenteditable="false">防止误删,同时在可视化模式里用 MathJax 渲染。这样既保留了公式整体的原子性,又能让非技术用户像编辑图片一样选中、拖拽公式位置,不会因为鼠标误点到源码标签而产生难以理解的编辑事故。我们在一个测试版本里对比过有原子保护和无原子保护的编辑体验,前者让行政、质量、技术各岗位的用户都能无培训上手,后者则经常有人反馈“我不知道为什么公式就变色了”。公式原子化之后,才算真正做到了让 Word 公式在网页端“进得来、留得住、改得动”。