我 fork 了 MathLive,补了一点化学公式编辑能力

16 阅读6分钟

最近把一个基于 MathLive 的公式编辑能力补了一下,最后单独发了一个包:

GitHub: github.com/LatoAndroid…

npm: www.npmjs.com/package/mat…

image.png 它不是 MathLive 官方包,是基于 arnog/mathlive 的一个 fork。名字叫 mathlive-chemistry,主要处理两件事:

  1. 让一部分常见的 \ce{...} 化学方程式更像普通公式一样可编辑;
  2. 补一个很小范围的 \chemfig{...} 渲染能力,用来显示简单有机结构式。

这次大部分编码是用 AI coding 协作完成的。不是那种“写一句 prompt 然后直接信了”的方式,更多是我定需求边界、看现有代码、挑问题、反复测显示效果,AI 负责改代码和跑验证。这个过程挺适合这种 fork:改动要小,不能把原项目架构推翻,还得一直盯着兼容性。

先说结论:这个东西不是专业化学编辑器,也不是完整 chemfig/mhchem 实现。它解决的是比较日常的公式编辑问题。

为什么要 fork

MathLive 本身已经很好用了。数学公式输入、渲染、选择、序列化、虚拟键盘这些能力都比较完整。

但化学公式有点尴尬。

MathLive 已经支持一部分 mhchem 语法,比如:

\ce{2H2 + O2 -> 2H2O}
\ce{CaCO3 ->[高温] CaO + CO2 ^}
\ce{SO4^2- + Ba^2+ -> BaSO4 v}

不过这里的 \ce 更偏显示。源码里对应的是 ChemAtom,大致流程是:

\ce{...}
  -> balanced-string 读入参数
  -> mhchem parser 解析
  -> texify 成普通 TeX
  -> parseLatex(tex)
  -> MathLive 按普通公式渲染

这个设计挺稳的。问题是它原来更像一个整体对象,保留原始 \ce{...} 文本用于序列化,编辑体验没有普通公式自然。

比如用户点到公式末尾删除,期望删除最后一个字符或一小段内容,而不是整个 \ce 都像一个不可拆对象一样被处理。这个在教学、题目录入、公式二次编辑里会比较别扭。

第一块改动:ChemAtom 只对简单内容开放编辑

我没有把所有 \ce 都强行拆开,这样风险太高。mhchem 的语法不少,里面还可以混 TeX,真要完整支持,很容易把内容改坏。

现在的处理是保守一点:

  • 只有 \ce 会走可编辑增强;
  • \pu 继续保持整体对象;
  • 参数太长不处理;
  • 参数里出现 \$& 这类复杂 TeX 痕迹不处理;
  • mhchem parser 的输出类型必须在一个白名单里。

代码上就是加了一层类似这样的判断:

function isEditableChemFormula(command, arg, parsed) {
  if (command !== '\\ce') return false;
  if (!arg || arg.length > 512) return false;
  if (/[\\$&]/.test(arg)) return false;
  return isEditableChemOutput(parsed);
}

白名单里放的是比较基础的输出类型,比如 rmtextbondarrowoperatorspace、上下标、沉淀/气体符号等。

这样做的好处是边界明确:常见化学方程式可以编辑,复杂内容还是原来的整体对象,不会为了“支持更多”把序列化弄得不可控。

可编辑状态下,ChemAtomcaptureSelection 会关掉,让内部 body 能参与 MathLive 的选择和删除。序列化时不直接吐旧字符串,而是把当前 body 序列化,再做一层化学公式归一化,最后重新包回:

\ce{...}

举个测试里的例子:

\ce{2H2 + O2 -> 2H2O}

内部改成:

3\mathrm{H}_{2}+\mathrm{O}_{2}\longrightarrow2\mathrm{H}_{2}\mathrm{O}

最后序列化回来是:

\ce{3H2+O2 -> 2H2O}

这里我更关心“能稳定保存和再次编辑”,不是追求完全还原用户原始空格。

第二块改动:补一个很小的 chemfig 子集

MathLive 原版不支持 \chemfig,会按未知命令处理。

但很多常见内容其实很简单:

\chemfig{CH_3-CH_2-OH}
\chemfig{CH_2=CH_2}
\chemfig{HC#CH}
\chemfig{CH_3-C(=O)-OH}
\chemfig{*6(-=-=-=)}
\chemfig{*6(-=-(-CH_3)-=-)}
\chemfig{*6(-=-(-NO_2)-=-)}
\chemfig{*6(-=-(-COOH)-=-)}

我没有接完整 chemfig parser。完整 chemfig 是另一件事,语法、布局、键角、分支、环、嵌套都能展开很大。为了几个常见结构直接引入一套重东西,感觉也不划算。

所以现在只做了两个小 parser:

  • parseLinear():处理线性结构,比如 CH_3-CH_2-OHCH_2=CH_2HC#CH
  • parseRing():处理 *6(...) 六元环,以及少量简单取代基。

解析出来不是变成 MathLive 的一堆普通 Atom,而是走一个 ChemfigAtom,渲染时生成 SVG,再塞回 MathLive 的 Box 体系:

\chemfig{...}
  -> parseChemfig()
  -> ChemfigStructure
  -> SVG
  -> Box(width / height / depth)
  -> MathLive inline 渲染

这个选择有点土,但是直接。因为有机结构式的布局不像普通 TeX 字符,硬拆成一堆 atom 反而会把问题搞复杂。

另外,chemfig 现在仍然按整体对象编辑。也就是说它可以显示、选中、删除、序列化,但不是完整的可视化结构编辑器。这个边界我觉得目前还算合理。

为什么不直接接专业化学编辑器

专业化学编辑器当然能做得更完整,比如结构绘图、键角、立体化学、反应机理箭头之类。

但代价也明显:

  • 交互体系会和 MathLive 分裂;
  • 序列化格式可能不再是 LaTeX;
  • 体积、依赖和维护成本会上去;
  • 公式里混排数学和化学会更麻烦。

我这个 fork 的目标很窄:已经在用 MathLive 的项目,如果只是想让常见化学公式别那么难用,可以先有一个轻量方案。

安装

npm install mathlive-chemistry

用法基本沿用 MathLive:

import 'mathlive-chemistry';

或者:

import { MathfieldElement } from 'mathlive-chemistry';

局限

这个包的局限要提前说清楚:

  • 不是官方 MathLive;
  • 不是完整 mhchem
  • 不是完整 chemfig
  • \chemfig{...} 现在主要是渲染和序列化保留,不是可视化结构编辑;
  • 不支持复杂立体化学、楔形键、虚线键、反应机理箭头、电子转移箭头;
  • 复杂 \ce\pu 会继续保持整体对象行为。

如果你要做大学/科研级化学结构编辑,这个包不适合,应该接专业工具。

当前测试过的一些例子

\ce{2H2 + O2 -> 2H2O}
\ce{CaCO3 ->[高温] CaO + CO2 ^}
\ce{SO4^2- + Ba^2+ -> BaSO4 v}

\chemfig{CH_3-CH_2-OH}
\chemfig{CH_2=CH_2}
\chemfig{HC#CH}
\chemfig{CH_3-C(=O)-OH}
\chemfig{*6(-=-=-=)}
\chemfig{*6(-=-(-CH_3)-=-)}
\chemfig{*6(-=-(-NO_2)-=-)}
\chemfig{*6(-=-(-COOH)-=-)}

项目里也补了一些序列化测试,主要是防止改完之后 \ce / \chemfig 保存出来变形。

和原项目的关系

原项目:

github.com/arnog/mathl…

本 fork:

github.com/LatoAndroid…

原作者是 Arno Gourdol 和 MathLive contributors,License 是 MIT。这个 fork 也保留 MIT。

我后面可能还会继续补一点常见 chemfig 子集,或者把基线、间距这些细节再磨一下。这个地方其实很烦,看起来差一两个像素不算 bug,但放到公式里就挺明显。

如果你刚好也在做公式编辑、题目录入、教学产品,或者在 MathLive 里碰到化学公式编辑问题,可以试一下。真实公式样例也欢迎丢 issue,单靠自己脑补测试用例很容易漏。