拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Word公式接入CKEditor并发布到公众号的完整实现方案

1. 政务场景里为什么公式导入是个绕不开的坑接手这个项目之前我一直觉得“从Word复制内容到网页编辑器”是个已经解决得很好的基础功能。直到真去处理税务、统计、教育口子的稿件时才发现普通文字和图片粘贴是一回事公式粘贴完全是另一回事。政务平台的编辑人员每天要处理大量Word文稿这些文稿里有三类内容逃不开公式一是业务计算类比如退抵税额、补贴金额、绩效评分公式二是统计解读类比如标准差、增长率、加权平均公式三是教育科研类比如考试大纲里的函数表达式、数列求和公式。以前的做法基本是截图后贴图公式区域和正文的排版经常对不齐到了微信公众号上更是要反复调整运维成本极高。我这边接到的项目是某区级政务内容发布平台的后台改版。后台原先用UEditor编辑人员把带公式的Word内容粘贴进来公式要么变成乱码要么直接丢失推送到微信公众号后更是惨不忍睹。后来我们换成CKEditor并且打通了“Word公式进编辑器、编辑器公式进公众号”这条链路。整个项目的核心就一件事让公式像普通文字一样能编辑、能保存、能发布全程不丢不烂。这次改造有几个特殊约束。首先是内网环境不能依赖公网CDN公式渲染库必须本地化部署。其次是发布流程政务文章有审批制度不能全自动发公众号只能走半自动流程。最后是内容长期可维护公式不能转成图片就完事还必须保留可编辑的源码。这几个约束决定了整个技术方案的走向。1.1 政务内容的公式不只是数学符号先看几张实际的公式清单你就明白为什么不能一刀切处理了。从业务类型看政务平台里的公式可以分成三类内容类型典型场景公式样子业务计算退抵税、补贴、绩效退税额 进项税额 - 销项税额统计解读数据发布、指数计算标准差、增长率、加权平均教育科研考试大纲、教案分享二次方程、函数、数列求和这三类内容有一个共同特征公式往往是行内嵌在正文里的不是独立展示的图表。比如“根据公式\bar{x} \frac{1}{n}\sum x_i计算平均值”公式前面有正文、后面也有正文排版上必须保持行内对齐。截图法在这种场景下很难看因为截图图片的基线很难和文字对齐稍微一拖动就歪。还有一层麻烦是政务文档往往来源多样。有的从Word 2016新建有的从旧版WPS转换过来还有的干脆是扫描件文字识别后再编辑。公式的底层结构千差万别有的规范无比有的缺胳膊少腿。开发阶段用规范文档测试一切正常到了验收阶段一用真实文档就各种翻车。1.2 从Word到公众号公式要走四步转换当我把“从Word复制公式到CKEditor再发布公众号”拆解成技术链路时发现这条链路比想象中长。很多人以为这只是一次复制粘贴实际上公式在中间要经历四种形态的转换。第一步Word内部格式。Word里的公式以OMMLOffice Math Markup Language存储这是一种微软定义的数学标记语言。当你从Word复制内容时OMML结构会混在剪贴板的HTML片段里一起出来。第二步网页可识别格式。网页端不认识OMML需要转换成MathML或LaTeXMathJax才能渲染。这一步是技术核心也是坑最多的地方。第三步编辑器内可视格式。编辑人员看到的是渲染后的公式而不是源码。CKEditor配合MathJax能做到这一点但体验上的细节需要打磨。第四步公众号发布格式。微信公众号编辑器不识别MathJax也不支持前端脚本公式必须预渲染成图片再跟随正文一起粘贴进去。这四步任意一步断掉公式就会在某个环节消失。项目初期我们试过用现成的Word粘贴插件编辑区里公式显示正常存储也没问题结果推到公众号预览后公式区域全是空白。后来排查下来就是卡在了发布这一步。1.3 三条技术路线的选型对比动手前我们把方案摊在桌面上比了一轮最终结论还挺明确的。方案A是直接用CKEditor配套的MathType插件Wiris它公式编辑体验很好也支持从Word粘贴公式但有两个问题一是商业授权费用不低政务项目采购流程复杂二是它的OMML转换依赖Wiris服务器在线支持内网环境直接卡死。方案B是CKEditor自带的mathjax插件加自研转换逻辑编辑器负责交互MathJax负责渲染OMML到LaTeX的转换由自己实现或引开源库。这个方案工作量最大但可控性最强不依赖外部服务内网也能跑。方案C是完全放弃公式编辑统一转图片。技术上最简单但后续内容一旦需要修改编辑人员得重新进Word改公式再截图长期维护成本非常高政务平台往往要用上十年这个账划不来。综合对比后选了方案B。核心思路是公式在数据库里以LaTeX源码形式保存编辑器里以MathJax渲染形式呈现发布到公众号时再临时转为图片。这样既保证编辑体验也保证长期可维护性。2. 公式的“三张皮”OMML、MathML与LaTeX很多开发者卡住的第一个点其实是分不清OMML、MathML和LaTeX三者到底是什么关系。这里我用最直白的方式讲清楚。2.1 Word贴出来的OMML结构长什么样OMML的完整名称是Office Math Markup Language它是Word文档内部使用的数学标记格式。当你在Word里用公式编辑器敲了一个公式复制它再粘贴到网页编辑区时浏览器剪贴板的HTML片段会包含类似这样的结构m:oMath m:r m:tEmc/m:t /m:r m:sSup m:e m:rm:t/m:t/m:r /m:e m:sup m:rm:t2/m:t/m:r /m:sup /m:sSup /m:oMathm:前缀的节点都在xmlns:mhttp://schemas.openxmlformats.org/officeDocument/2006/math命名空间下。OMML的特点是和Word底层文档结构强绑定比如分数用m:f上标用m:sSup根号用m:rad。这些结构网页端根本不认识如果编辑器没有处理逻辑浏览器会直接把它当普通XML标签显示于是你就在编辑区里看到一堆“m:oMath”的字样。2.2 MathML和LaTeX各自的优劣势MathML是W3C定义的数学标记语言标准结构化强、语义化强但标签非常冗长。写一个分数用MathML表达可能要十几个标签。LaTeX则是文本标记语言简洁直观一行\frac{1}{2}就表示二分之一不管是存储还是阅读都比MathML高效得多。政务CMS里我强烈建议数据库统一存LaTeX。原因有三一是占用空间小几万个公式也就是几兆的事二是纯文本形式方便在后台SQL里检索、批量替换三是LaTeX生态成熟无论对接公众号、导出PDF、还是生成静态页面都有现成的转换工具。MathML可以用作中间交换格式但没必要作为主存储格式。2.3 我们最终采用的转换链路经过多轮技术验证最终确定的链路是OMML从粘贴HTML中提取→ 前端解析转换为MathML → 服务端接口将MathML转换为LaTeX → CKEditor内MathJax渲染为什么后面又加了“前端先将OMML转MathML”这一步主要是为了减少服务端压力。OMML转MathML是纯文本解析放前端做很轻盈MathML转LaTeX涉及大量规则判断和语义分析放服务端更稳定也方便后续扩充功能比如批量转换历史文档。这里要特别强调一个原则转换工具必须做异常兜底。有些老版本Word生成的OMML并不规范转换器解析失败的情况很常见。失败时一定要在编辑器里保留原始OMML内容或者弹出明确提示绝不能静默丢弃。否则用户辛辛苦苦排好的版面发布时才发现公式丢了就真的很难追责了。3. CKEditor公式导入的完整实现步骤下面进入实操部分。这个项目后台基于PHP前端用的是CKEditor 4公式插件用官方mathjax插件。选CKEditor 4不是因为它比5先进而是政务项目里大量老系统已经在用CKEditor 4插件体系成熟迁移成本低。下面所有配置和代码都以CKEditor 4为例。3.1 编辑器配置与MathJax本地化部署CKEditor 4的mathjax插件官方给的配置通常是直接引公网CDN的MathJax在政务内网完全不可用。我们的做法是把MathJax 2.7.9完整安装包放到内部静态资源服务器配置时指向本地地址。CKEDITOR.replace(content, { extraPlugins: mathjax, mathJaxLib: /static/mathjax/MathJax.js?configTeX-AMS-MML_HTMLorMML, removePlugins: sourcearea, height: 480, disableNativeSpellChecker: true, forcePasteAsPlainText: false });mathJaxLib这个参数决定了公式预览时的MathJax加载地址必须改成内网可访问的URL。TeX-AMS-MML_HTMLorMML这个config让MathJax同时支持LaTeX和MathML两种输入方式粘贴进来的内容不管是哪种格式都能渲染。disableNativeSpellChecker和forcePasteAsPlainText这两个参数容易被忽略。前者是为了防止浏览器在公式区域做拼写检查干扰后者是为了确保粘贴时不会把HTML强行转成纯文本否则OMML结构在进入编辑器前就被剥掉了。3.2 粘贴拦截与OMML提取公式导入的核心逻辑在paste事件里。我们的做法是在粘贴内容正式进入编辑器之前截获它识别OMML结构转换为LaTeX再放行插入。editor.on(paste, function(evt) { var html evt.data.dataValue; if (html.indexOf(m:oMath) -1) { return; // 没公式走默认逻辑 } evt.cancel(); // 先取消默认插入 var omathList []; var regex /m:oMath[\s\S]*?\/m:oMath/g; var match; while ((match regex.exec(html)) ! null) { omathList.push({ raw: match[0], index: match.index }); } // 这里实际会用Promise.all收集所有异步转换 var convertTasks omathList.map(function(item) { // 前端 OMML - MathML var mathml convertOmmlToMathml(item.raw); // 服务端 MathML - LaTeX return fetch(/api/mathml2latex, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ mathml: mathml }) }).then(function(res) { return res.json(); }).then(function(data) { if (data.latex) { var latexSpan span classmath-tex\\( data.latex.replace(/\\/g, \\\\) \\)/span;
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门