Skip to content

两个解析器的已知差异

ADR-0003 选了双解析器:编辑期用 @lezer/markdown(求增量解析与位置精确), 导出期用 remark/mdast(求语义完整与生态)。那份 ADR 同时写明了这个选择唯一真正的 风险,以及必须为它付的代价:

不一致的具体后果:编辑器把某段显示成引用块(Lezer 判定),导出的 HTML 里 却是普通段落(remark 判定)。用户会认为是 bug,而且很难自己搞清楚。

缓解措施 3 · 已知差异显式登记:确实无法消除的差异写进 docs/known-divergences.md,每条都要有回归用例,并在文档里对用户可见地说明。

这份文件就是那张清单。

它是怎么来的

packages/export/test/parser-consistency.test.ts 拿 CommonMark 官方的 652 个用例 喂给两个解析器,把各自的语法树规范化成同一套块级骨架doc / p / h1h6 / quote / ul / ol / li / code / hr / html / def / table…)之后逐字比对。

652 例中有 2 例不一致,占 0.31%。 GFM 那几样(表格、任务列表、删除线、 自动链接)另有 8 个手写用例,全部一致。

只比块级骨架,不比行内元素,也不比文本内容 —— 理由写在那个测试文件的开头。

清单

两条都是Lezer 判定与 CommonMark 规范不符,remark 正确。也就是说: 导出的结果是对的,编辑器里看到的是错的。

#171 · <textarea> 没被当成 HTML 块

markdown
<textarea>

*foo*

_bar_

</textarea>
判定
CommonMark 规范期望整段原样输出,*foo* 不是强调
remarkhtml(一整块)✅
Lezerhtml / p / p / html

为什么:CommonMark 的「type 1」HTML 块以 <script><pre><style><textarea> 开头,一直延续到对应的闭合标签为止,中间的空行不终止它。 textarea 是 CommonMark 0.30 才加进这个清单的,而 @lezer/markdown 的实现还停在 之前的三个标签上。

用户会看到什么:文档里写了一个 <textarea> 块时,编辑器把里面的 *foo* 渲染成斜体,而导出的 HTML 里它是字面的 *foo*所见与所得不一致, 正是 ADR 预言的那种。

为什么暂不修:修法是给 Lezer 的 HTML 块解析器打补丁或换用上游修复版。 这是一个上游 bug,影响面极小(<textarea> 出现在 Markdown 文档里本就罕见), 而自己打补丁意味着要维护一份分叉的块级解析逻辑 —— 那个代价远大于收益。 盯着上游即可。

#280 · 空列表项后面的缩进段落被并了进去

markdown
-

  foo
判定
CommonMark 规范期望<ul><li></li></ul><p>foo</p> —— 空列表项,段落在列表外面
remarkul / li + p(平级)✅
Lezerul / li / p(嵌在里面)❌

为什么:CommonMark 规定「列表项开头至多允许一个空行」—— 以空行开头的列表项不能再包含更多内容,所以 foo 另起一段。 Lezer 按缩进把它接进了列表项。

用户会看到什么:编辑器里 foo 缩在列表项内(继承列表的缩进与样式), 导出后它跑到了列表外面。

为什么暂不修:同样是上游的块级解析细节,而触发它需要「一个完全空的列表项 紧跟一个缩进段落」—— 这在真实文档里几乎只会是笔误。

加一条 / 删一条

回归用例就是那个测试里的 KNOWN_DIVERGENCES 表。它两个方向都卡

  • 出现表外的新差异 → 测试失败(防止分歧悄悄增加);
  • 表内的某条不再有差异 → 测试也失败(上游修好了就该把它从表里删掉)。

第二条是这张清单不腐烂的唯一保证。只卡「不许变多」的允许清单,几年后会变成一份 谁也不敢动的化石:里面一半的条目早已不成立,但没人知道是哪一半。

所以流程是:

  1. 改动导致新分歧 → 要么修,要么在这里写清楚谁对、用户会看到什么、为什么暂不修, 并把编号加进 KNOWN_DIVERGENCES
  2. 升级 @lezer/markdown 或 remark 之后测试报「已不再有差异」→ 把对应条目从表和 这份文档里删掉。那是好消息,别把它当成测试坏了。

MIT 许可发布。与 Typora 无关联,是一个独立实现。