两个解析器的已知差异
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 / h1…h6 / quote / ul / ol / li / code / hr / html / def / table…)之后逐字比对。
652 例中有 2 例不一致,占 0.31%。 GFM 那几样(表格、任务列表、删除线、 自动链接)另有 8 个手写用例,全部一致。
只比块级骨架,不比行内元素,也不比文本内容 —— 理由写在那个测试文件的开头。
清单
两条都是Lezer 判定与 CommonMark 规范不符,remark 正确。也就是说: 导出的结果是对的,编辑器里看到的是错的。
#171 · <textarea> 没被当成 HTML 块
<textarea>
*foo*
_bar_
</textarea>| 判定 | |
|---|---|
| CommonMark 规范期望 | 整段原样输出,*foo* 不是强调 |
| remark | html(一整块)✅ |
| Lezer | html / 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 · 空列表项后面的缩进段落被并了进去
-
foo| 判定 | |
|---|---|
| CommonMark 规范期望 | <ul><li></li></ul><p>foo</p> —— 空列表项,段落在列表外面 |
| remark | ul / li + p(平级)✅ |
| Lezer | ul / li / p(嵌在里面)❌ |
为什么:CommonMark 规定「列表项开头至多允许一个空行」—— 以空行开头的列表项不能再包含更多内容,所以 foo 另起一段。 Lezer 按缩进把它接进了列表项。
用户会看到什么:编辑器里 foo 缩在列表项内(继承列表的缩进与样式), 导出后它跑到了列表外面。
为什么暂不修:同样是上游的块级解析细节,而触发它需要「一个完全空的列表项 紧跟一个缩进段落」—— 这在真实文档里几乎只会是笔误。
加一条 / 删一条
回归用例就是那个测试里的 KNOWN_DIVERGENCES 表。它两个方向都卡:
- 出现表外的新差异 → 测试失败(防止分歧悄悄增加);
- 表内的某条不再有差异 → 测试也失败(上游修好了就该把它从表里删掉)。
第二条是这张清单不腐烂的唯一保证。只卡「不许变多」的允许清单,几年后会变成一份 谁也不敢动的化石:里面一半的条目早已不成立,但没人知道是哪一半。
所以流程是:
- 改动导致新分歧 → 要么修,要么在这里写清楚谁对、用户会看到什么、为什么暂不修, 并把编号加进
KNOWN_DIVERGENCES; - 升级
@lezer/markdown或 remark 之后测试报「已不再有差异」→ 把对应条目从表和 这份文档里删掉。那是好消息,别把它当成测试坏了。