前置知识: Markdown

CommonMark 规范

5 min中级

CommonMark规范详解:标准化Markdown的定义、解析规则与一致性测试。

Layer 2 专业层标注:本篇讲 CommonMark 规范的解析器原理,属于高级话题。零基础学习者可完全跳过,直接学 Layer 0/1 的语法文档即可。

1. CommonMark 概述

1.1 为什么需要 CommonMark

原始 Markdown 由 John Gruber 于 2004 年创建,但规范描述模糊,导致各平台实现差异巨大。CommonMark 项目始于 2014 年,目标是创建无歧义的 Markdown 标准规范;GFM 规范即在其基础上扩展而来。当前规范版本为 0.31.2(2024-01-28 发布),版本号长期处于 0.x,意味着规范仍在小步修订而非”未完成”。

问题示例
嵌套列表解析不一致不同解析器对缩进列表的处理不同
链接定义优先级行内链接 vs 引用链接的优先级不明确
HTML 块边界HTML 块何时结束的定义不统一
强调规则*foo*bar*baz* 的解析结果不一致

1.2 CommonMark 的目标

  • 无歧义:每个输入都有唯一确定的输出
  • 可测试:规范自带 600 多个带标准答案的官方示例(spec.txt 内嵌测试用例),任何实现都可逐条对照验证
  • 向后兼容:尽量兼容原始 Markdown
  • 可扩展:允许定义扩展规范(如 GFM)

2. 规范核心规则

2.1 块级与行内结构

CommonMark 将文档解析分为两个阶段:

输入文本
    ↓
第一阶段:识别块级结构
  (段落、标题、列表、代码块、引用等)
    ↓
第二阶段:在段落文本中识别行内结构
  (强调、链接、代码、图片等)

2.2 空白处理规则

规则说明示例
行尾空格两个以上触发硬换行空格空格↵ → <br>
制表符展开到下一个 4 列倍数(非简单等于 4 空格)\t 依所在列展开
连续空行多个空行等同一个段落间只需一个空行
缩进4 空格触发代码块 code → <pre><code>

2.3 列表解析规则

CommonMark 对列表的解析有严格定义:

- 项目一
  - 子项目(2空格缩进)

1. 有序列表
   1. 子列表(3空格缩进,与文本对齐)

关键规则:

  • 无序列表项的子内容需缩进到列表标记后的第一个非空字符位置
  • 有序列表项的子内容需缩进到列表标记(含点号和空格)之后的位置
  • 列表连续性由空行和缩进共同决定

2.4 强调规则

CommonMark 使用左右分界(flanking)规则(依据定界符前后字符判定其能否开启/关闭强调)解析强调:

*foo*        → <em>foo</em>
**foo**      → <strong>foo</strong>
***foo***    → <strong><em>foo</em></strong>
foo*bar*baz  → foo<em>bar</em>baz(星号支持词内强调)
snake_case   → 原样输出(下划线不支持词内强调,保护标识符)
5*3*2        → 5<em>3</em>2(星号夹在数字间同样触发强调)
* not em *   → 原样输出(定界符内侧有空格,不构成定界)

规则要点:

  • * 和 _ 都可以表示强调,唯一重要差异是 _ 不允许词内强调(保护 snake_case 标识符);
  • 定界符必须紧贴内容,内侧有空格即失效;
  • 嵌套强调时遵循”最短优先”等匹配规则,外层不能先于内层关闭。

完整的左右_flanking 判定较绕,日常写作记住两条即可:用 * 别用 _、定界符紧贴内容。

3. 与原始 Markdown 的差异

3.1 关键差异

方面原始 Markdown(Markdown.pl)CommonMark
无空格标题#foo 也是标题(宽容)# 后必须有空格才是标题
列表续行规则模糊,实现各异严格的内容列缩进规则
HTML 块仅笼统说明精确定义 7 种 HTML 块类型及边界
链接引用标签匹配行为未严格定义标签匹配不区分大小写、空白归一化
硬换行行尾两个空格行尾两个空格或反斜杠 \

3.2 已知不兼容

一个经典例子是行首 # 无空格的写法——原始实现把它当标题,CommonMark 明确它只是普通文本:

#无空格
<!-- Markdown.pl:渲染为 <h1>无空格</h1> -->
<!-- CommonMark:渲染为普通段落文本 "#无空格" -->

类似”老实现宽容、新规范收紧”的点还有多处(如缩进列表、集合式引用定义)。迁移老文档时最常见的翻车点就在这类边缘写法上——修正为规范写法即可(# 无空格 加空格)。

4. 解析算法

4.1 两阶段解析

第一阶段:块级解析

1. 逐行读取输入
2. 将行分类(ATX 标题、Setext 标题、代码围栏、引用、列表项等)
3. 构建块级文档树
4. 将内容行附加到对应的块级元素

第二阶段:行内解析

1. 遍历段落和标题的文本内容
2. 识别行内元素(代码跨度、强调、链接等)
3. 构建行内元素树

4.3 优先级

行内解析的处理顺序(先处理者”抢走”内容,其余定界符退化为普通文本):

  1. 代码跨度(`code`)— 最高优先级,内部不解析任何内容
  2. 自动链接(<url>)与原始 HTML 标签
  3. 链接和图片([文本](地址))— 优先于强调,因此 [**文本**](url) 中的加粗属于链接文字
  4. 强调(* / _)
  5. 剩余字符按普通文本输出

这条顺序解释了一个常见现象:*text* 中被反引号包住的星号永远只是字面字符——代码跨度先把它”圈”走了。

5. 一致性测试

5.1 测试套件

CommonMark 提供了完整的测试套件,每个测试用例包含:

# 示例测试用例
---
title: 'ATX headers'
section: 'ATX headings'
example: 32
markdown: |
  ## foo
    bar
  baz
html: |
  <h2>foo</h2>
  <pre><code>bar
  </code></pre>
  <p>baz</p>
---

5.2 验证工具

# 使用 cmark 参考实现
echo '# Hello' | cmark
# <h1>Hello</h1>

# 运行规范测试
python3 spec_tests.py --program cmark

# 使用 CommonMark.js
npm install commonmark
node -e "var commonmark = require('commonmark'); \
  var reader = new commonmark.Parser(); \
  var writer = commonmark.HtmlRenderer(); \
  var doc = reader.parse('# Hello'); \
  console.log(writer.render(doc));"

6. 实现与生态

6.1 主要实现

实现语言说明
cmarkC参考实现,性能最优
commonmark.jsJavaScriptJavaScript 参考实现
commonmark-hsHaskell类型安全的实现
comrakRust高性能 Rust 实现
goldmarkGoGo 生态主流 Markdown 解析器

6.2 扩展规范

CommonMark 定义了扩展机制,GFM(GitHub Flavored Markdown)是最著名的扩展:

  • 表格:| col1 | col2 |
  • 任务列表:- [ ] todo
  • 删除线:~~strikethrough~~
  • 自动链接:https://example.com
  • 代码围栏语言:```python