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 拼写纠错 + 报错不白屏,外加一次教科书级的"用户说没修好"复核。

一、问题描述:一个环境名,三连击

现象按时间线展开:

  1. 块级公式用 \begin{multiline}...\end{multiline} 写多行连等式,渲染红字报错;\begin{multline}(amsmath 标准拼写,少一个 i)同样报错;
  2. 首版修复上线(v1000052)后,用户复测反馈"依旧存在此问题";
  3. 用户顺带要求:\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 个依赖点实测后才动手升级风险不在新版本,在自家集成点的耦合面

八、三条心得

  1. 容错的目标是"用户意图优先",红线是"不掩盖真错误":别名表管语义、编辑距离管笔误、阈值之外原样报错——三层各司其职,比一刀切"都容错"或"都报错"体验都好;
  2. "没修好"是取证问题,不是修复问题:这次若直接信了"依旧存在"去改代码,会在正确的修复上打补丁。版本在跑(URL ?v=)+ 代码在包里(unzip 特征串)+ 现象可复现(CDP),三证齐了再下结论;
  3. 报错也是产品:红字 + 支持范围提示 + 不白屏,错误路径的体验值得和成功路径一样认真设计。

如果你也在试 AI 开发鸿蒙,或者想看后续,关注专栏,所有踩坑都会持续更新。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐