跳转至

笔记正文花括号让 MDX 解析失败,后续内容被静默截断

范围与现象

长篇论文笔记(实测 papers/2604.09245/NOTES.md,250 KB / 3,612 行)在编辑器里只显示开头一小段:状态栏 6,746 词(约 11%),外部工具对同一文件统计 58,537 词。磁盘文件完整,App 无报错、无 toast、无 JS 错误日志,重开依旧复现。

两个误导性特征:

  • 可见内容的结尾(列表之后)语法完全正常,逐行检查找不到问题;真正的触发点在 2,700 行之后。
  • 文件从 166 KB 长到 250 KB 的过程中,能看到的比例越来越小——很像"加载/渲染有尺寸上限",实际是截断点固定在文档前部、分母变大。

原因

四层叠加:

  1. 笔记里有一行用 \(...\) 写的行内公式,内部是 LaTeX 花括号:\(\mathcal E^{t+1}\le\max\{\rho_z,\rho_{\text{dual}},\rho_G\}\,\mathcal E^t\)。编辑器只把 $…$ 当公式,这段是普通文本;remarkMdx 把第一个 { 当 JS 表达式起点,acorn 抛出 Could not parse expression with acorn。
  2. 同一篇笔记的表格里有 <br/>,markdownNeedsMdx 为真,必须挂 remarkMdx,因此错误必然发生并进入 Plate 的 MDX 回退路径。
  3. @platejs/markdown 的 markdownToSlateNodesSafely → splitIncompleteMdx 在原始字符串里按 JSX 标签扫描,不区分公式与代码:它把行内公式 $\rho<1$ 里的 <1 当成未闭合标签(数字也是它的合法标签名字符),从那个 < 把文档切成两段。前半段正常解析;< 之后的"不完整"半段只按行内 Markdown 解析且只取第一个块,其后所有段落、列表、标题、公式被静默丢弃。
  4. 于是截断点($\rho<1$)与真正报错的 { 所在行(2,700 行之后)无关;< 的位置固定,文件越长丢掉的比例越大。

#533 的 escapeStrayLessThan 只防住了"< 让 micromark 抛错"这一种 MDX 失败,没有覆盖别的 MDX 解析错误,也没有覆盖 Plate 回退本身会丢内容的问题。

修复

两层防线:

  1. 不再让 MDX 解析失败:prepareMarkdownForDeserialize 增加 escapeStrayMdxBraces——代码围栏、$$ 块、行内代码与行内公式之外的 { / } 改写成 &#123; / &#125; 实体;\{ / \} 连同前面那个转义反斜杠一起改写(保留反斜杠的话 markdown 会把 & 转义掉,页面上会显示成字面量 &#123;)。remark 解析时实体解码回花括号,显示不变;MDX 令牌器再也看不到表达式起点。该预处理是打开文件、外部 / Agent 重载、粘贴、嵌入投影、导出共用的唯一入口,一处修复覆盖全部路径。
  2. 任何 MDX 失败都不许静默丢内容:deserializeMarkdownBody 向 Plate 传 onError;一旦 MDX 仍然解析失败,不再接受"从某个 < 切两段"的恢复结果,而是整篇用 withoutMdx: true 重新反序列化(真正支持的 HTML 标签退化为字面文本,但所有块都保留),并写一条 warn 日志。

顺带修正状态栏统计:原来 NodeApi.string 只走文本叶子,公式节点是 void、TeX 存在 texExpression 里,$…$ / $$…$$ 的源码完全不计数——这篇纯数学笔记在状态栏只有 3.4 万词,与实际 5.9 万词差约 40%,也放大了"截断"的观感。plateDocumentText 现在把公式源码一并计入,状态栏约 5.8 万词,与外部统计基本一致。

回归验证

  • test/markdown-mdx-brace.test.ts:
  • escapeStrayMdxBraces 的实体化、\{ 去反斜杠、代码/公式豁免、无花括号 no-op;
  • 带 <br/> 表格(必须走 MDX)加 \(...\{...\} 的笔记反序列化后,列表、后续标题与段落全部保留;
  • 仍存在 MDX 解析错误时整篇走无 MDX 重解析(断言内容保留且写了 warn 日志)。
  • test/markdown-stats.test.ts:公式源码计入状态栏词数/字符数。
  • test/markdown-deserialize.test.ts:更新未闭合 $$ 用例的预处理期望(围栏被修好后其后的 { 是普通文本,会被实体化)。
  • 真实长文复现:NOTES.md 修复前反序列化 290 块 / 6,746 词;修复后 1,314 块,能完整读到文末「10. 一句话总结」。