KaTeX 公式渲染容错实战:环境语义映射与拼写自动纠错(鸿蒙编辑器 LaTeX 支持)
文章目录
KaTeX 公式渲染容错实战:环境语义映射与拼写自动纠错(鸿蒙编辑器 LaTeX 支持)
摘要:本文记录 MarkPin 公式渲染中一次典型的"用户报障→取证复核→分层容错"实战。针对
\begin{multiline}等环境名报错,作者先通过本地实证与 CDP 三点取证,证明渲染管线无缺陷、用户"依旧存在"实为 bmatix 预期失败或回显误判;随后落地环境语义映射 + Levenshtein 拼写纠错 + 报错不白屏三层容错方案,重写仅发生在渲染输入、源码零改动,并顺带完成 KaTeX 0.16.47→0.18.5 升级评估。全文含真实源码节选、单测与装机验证数据,以及"用户意图优先、不掩盖真错误"的容错心得。
用户在公式块里写 \begin{multiline},渲染出一行红字:KaTeX parse error: No such environment: multiline。更麻烦的还在后面:修完上线,用户说"依旧存在",还追加了新需求——\begin{bmatix}(bmatrix 少个 r)也得能渲染。这篇讲 MarkPin 公式渲染的容错设计:环境语义映射 + Levenshtein 拼写纠错 + 报错不白屏,外加一次教科书级的"用户说没修好"复核。
一、问题描述:一个环境名,三连击
现象按时间线展开:
- 块级公式用
\begin{multiline}...\end{multiline}写多行连等式,渲染红字报错;\begin{multline}(amsmath 标准拼写,少一个 i)同样报错; - 首版修复上线(v1000052)后,用户复测反馈"依旧存在此问题";
- 用户顺带要求:
\begin{bmatix}这种拼写错误也一并容错。
第 2 点最值得展开——代码明明已经修了,为什么用户还看到红字?
二、分析方法:先证明"管线无缺陷",再证明"用户看到的是什么"
第一步,本地 node 实证:KaTeX 0.16.47 对 multiline/multline 确实都抛「No such environment」;对照 \begin{gathered} 渲染成功。再核实错误传播链路(mdParser mathBlockRule → math_block token → renderMath 调 katex.renderToString(throwOnError: true) 抛错 → catch 分支按规格返回红字提示)——渲染管线本身无缺陷,是环境支持度缺失。还要确认没有捷径:KaTeX 无运行时环境注册 API(defineEnvironment 是内部接口)、strict 选项只影响警告不影响硬错误——唯一现实路径是渲染前环境名重写。
第二步,“依旧存在"复核(这步是全案的关键)。装机 v1000053 用 CDP 三点取证:解包 HAP 确认 index.html 含新代码特征串;确认模拟器加载 URL 带 ?v=1000053(新版本确实在跑);CDP 实测 multiline 在浏览器与模拟器双端都渲染正常。结论:用户复测踩中的是 bmatix(不在首版白名单,红字属于预期失败),或者预览模式光标落在公式块内时活动行回显源码(这是设计行为,光标在公式行就显示 LaTeX 原文)被误判为"没修复”。复盘价值:用户说"没修好",先别改代码——先证明设备上跑的到底是哪个版本、用户看到的到底是哪个现象。
三、解决代码(一):环境语义映射 + 拼写容错
核心是两张表 + 一个编辑距离函数(真实源码节选):
// editor-build/src/render/mathRender.ts(真实代码,节选)
const ENV_ALIASES: Record<string, EnvAlias> = {
multiline: { target: 'gathered' }, // 常见拼写
multline: { target: 'gathered' }, // amsmath 标准拼写
// 旧式对齐环境(LaTeX l2tabu 淘汰语法):语义映射 + 内容变换
// 特征 x &=& y 三列写法 → aligned 的 &= 等价形式(实测不变换时列间距异常)
eqnarray: { target: 'aligned', transform: (content: string): string => content.replace(/&=&/g, '&=') }
};
const SUPPORTED_ENVS: string[] = [ // 拼写容错候选集(星号变体去星号覆盖)
'align', 'alignat', 'aligned', 'alignedat', 'array',
'Bmatrix', 'bmatrix', 'cases', 'dcases', 'dmatrix',
'equation', 'gather', 'gathered', 'matrix', 'pmatrix',
'smallmatrix', 'subarray', 'vmatrix', 'Vmatrix'
];
/** Levenshtein 编辑距离(环境名长度很小,全量 DP 无性能顾虑)。 */
function editDistance(a: string, b: string): number { /* 标准 DP,略 */ }
/** 拼写容错:在 SUPPORTED_ENVS 中找编辑距离 ≤2 的最近候选;
尾部 `*` 先剥离;距离 >2 视为非拼写错误(如自定义宏名),返回 null。 */
function suggestSupportedEnv(unknown: string): string | null {
const target = unknown.endsWith('*') ? unknown.slice(0, -1) : unknown;
let best: string | null = null;
let bestDist = 3; // 阈值:>2 不匹配
for (const env of SUPPORTED_ENVS) {
const d = editDistance(target, env);
if (d < bestDist) { bestDist = d; best = env; }
}
return best; // bmatix→bmatrix、alignd→aligned
}
两个设计决策值得说:
- 语义别名表优先于拼写容错。multiline 距任何支持环境的编辑距离都 >2,靠容错永远命中不了——必须显式映射。这也天然形成分层:已知语义缺口走表,未知拼写错误走距离,都 miss 的保留原始错误;
- 距离 ≤2 是克制的阈值。
myenv这种自定义环境名离最近候选可能恰好 ≤2,但用户写它多半是有意为之——距离 >2 一律保留原始错误,不掩盖真错误是容错的红线。
四、解决代码(二):错误恢复链——重写重试,源码不动
兜底逻辑挂在 renderMath 的 catch 分支,重写只发生在渲染输入上,sourceLatex 保持原始源码:
// editor-build/src/render/mathRender.ts renderMath(真实代码,节选)
} catch (e: unknown) {
const errorMsg = e instanceof Error ? e.message : String(e);
const env = unknownEnvFromError(errorMsg); // 提取 "No such environment: X" 的 X
if (env !== null) {
let alias: EnvAlias | null = null;
if (env in ENV_ALIASES) {
alias = ENV_ALIASES[env]; // ① 语义别名优先
} else {
const suggested = suggestSupportedEnv(env); // ② 拼写容错(≤2)
if (suggested !== null) { alias = { target: suggested }; }
}
if (alias !== null) {
const aliased = aliasEnvironment(latex, env, alias); // 重写 begin/end(含 * 变体)
if (aliased !== latex) {
try {
const html = katex.renderToString(aliased, { displayMode, throwOnError: true, output: 'html' });
return { html, sourceLatex: latex, displayMode }; // 缓存身份以原文为键
} catch { /* 重写后仍失败:走下方原始错误路径 */ }
}
}
}
// 报错不白屏:红字错误 + 环境类错误附支持范围提示(普通语法错误不加噪音)
const hint = env !== null ? ENV_SUPPORT_HINT : '';
return { html: '<span class="mk-math-error">' + escapeHtml(errorMsg) + hint + '</span>',
error: errorMsg, sourceLatex: latex, displayMode };
}
注意两个不动点:文档源码零改动(用户的 LaTeX 原文一个字节不动,重写只发生在渲染管线内部,sourceLatex 返回原文保证公式缓存身份不变);全链路自动覆盖(行内/块级/双栏镜像/导出 HTML 都走这一个 renderMath,一处兜底全线生效)。
五、引擎升级:0.16.47 → 0.18.5 的四点实测
容错上线后顺势评估了 KaTeX 升级(含 0.18.2 的 prototype pollution 安全修复)。升级前对 4 个集成依赖点逐一实测:错误消息格式一致(别名/容错正则继续有效);核心 CSS 类保留(仅编号类 tag→katex-tag,项目样式无旧类依赖);字体文件名一致(内联流程自适应);编号 counter 机制保留。全量回归 107/107 + 装机验证通过。
六、验证与效果
单测 18→22 用例:用户原始示例、标准拼写、* 变体、行内公式、嵌套 cases、距离 >2 保留错误、别名后仍失败保留原始错误、bmatix/bmatrx/alignd/gatherd 容错、与 bmatrix 渲染 HTML 逐字节一致、eqnarray 内容变换一致、环境错误附 hint、普通语法错误无 hint。装机验证:CDP 实测 multiline→gathered 渲染、bmatix→bmatrix 渲染、myenv 红字保留 + hint 可见,截屏存档。用户模拟器验收通过(multiline 与 bmatix 两点均确认)。versionCode 1000051→1000054 四轮递增。
七、能力边界表
| 事项 | AI 表现 | 我的结论 |
|---|---|---|
| 管线诊断 | 快速证明"渲染管线无缺陷,是支持度缺失" | 先分清缺陷与能力边界,修复方向才不会跑偏 |
| "依旧存在"复核 | CDP 三点取证还原真相(bmatix 预期失败/回显误判) | 用户报"没修好",先证明设备在跑哪个版本、看到哪个现象 |
| 容错方案分层 | 别名表+编辑距离+阈值>2保留错误,一次成型 | 语义缺口走表、拼写走距离、真错误不掩盖——分层容错是通用范式 |
| 重写不影响源码 | sourceLatex 保持原文,缓存身份稳定 | 渲染层容错的原则:可以改"怎么画",不能改"画的是什么" |
| 引擎升级评估 | 4 个依赖点实测后才动手 | 升级风险不在新版本,在自家集成点的耦合面 |
八、三条心得
- 容错的目标是"用户意图优先",红线是"不掩盖真错误":别名表管语义、编辑距离管笔误、阈值之外原样报错——三层各司其职,比一刀切"都容错"或"都报错"体验都好;
- "没修好"是取证问题,不是修复问题:这次若直接信了"依旧存在"去改代码,会在正确的修复上打补丁。版本在跑(URL ?v=)+ 代码在包里(unzip 特征串)+ 现象可复现(CDP),三证齐了再下结论;
- 报错也是产品:红字 + 支持范围提示 + 不白屏,错误路径的体验值得和成功路径一样认真设计。
如果你也在试 AI 开发鸿蒙,或者想看后续,关注专栏,所有踩坑都会持续更新。
更多推荐


所有评论(0)