你在博客里写了这样一段 Markdown:一个列表里嵌套了代码块,推送到 GitHub 预览时格式完美,但用 Hugo 生成的页面上代码块却脱离了列表。换了一个解析器,列表项之间的段落间距又变了。为什么会这样?因为 Markdown 最初只有一份非正式的说明文档,各解析器对边界情况的理解不同,导致同一个文件在不同平台上渲染结果各异。CommonMark 和 GFM(GitHub Flavored Markdown)就是为了解决这个问题而诞生的。理解规范细节,才能写出在所有平台上都表现一致的 Markdown 文档。
一、为什么需要规范
1.1 原始 Markdown 的问题
John Gruber 在 2004 年发布的原始 Markdown 说明只有不到 30 个例子,很多边界情况没有定义:
| 问题 | 原始规范 | 实际影响 |
|---|---|---|
| 嵌套列表缩进 | 未明确定义 | 各解析器缩进要求不同 |
| 列表内的代码块 | 未定义 | 有的解析器允许,有的不认 |
| 段落与列表的交互 | 未定义 | 空行后列表是否中断? |
| 强调的边界规则 | 模糊 | a**"foo"**b 是否加粗? |
| HTML 块的结束 | 未定义 | 何时算 HTML 块结束? |
| 硬换行规则 | 不一致 | 行末两空格 vs <br> |
1.2 规范演进
2004 Markdown (John Gruber) |2012 CommonMark 开始(Jeff Atwood 等) |2014 CommonMark 0.1 发布 |2017 GFM 规范发布(基于 CommonMark) |2019 CommonMark 0.29 |2022 GFM 正式纳入 GitHub 文档二、CommonMark 规范核心
2.1 块级与行内结构
CommonMark 将 Markdown 结构分为两层:
块级结构(Block Structure):确定文档的整体骨架
> 引用块> > 嵌套引用
- 列表项 1- 列表项 2
缩进代码块行内结构(Inline Structure):块级元素内部的格式
**加粗**、*斜体*、`代码`、[链接](url)解析过程:先解析块级结构,再解析行内结构。这决定了很多边界情况的处理方式。
2.2 强调规则
CommonMark 对强调(emphasis)有精确的规则,处理了原始 Markdown 中模糊的情况。
规则一:星号与下划线的区别
*foo* -> 强调_foo_ -> 强调
foo*bar* -> 强调foo_bar_ -> 不强调(下划线两侧都是字母数字)
*foo bar* -> 强调_foo bar_ -> 强调规则二:奇数规则
*foo* -> <em>foo</em>**foo** -> <strong>foo</strong>***foo*** -> <em><strong>foo</strong></em>****foo**** -> <strong><strong>foo</strong></strong>规则三:开闭定界符的判定
左侧开定界符:
- 前面是空白、标点,或行首
- 后面不是空白、标点
右侧闭定界符:
- 前面不是空白、标点
- 后面是空白、标点,或行末
a *"foo"* b -> 强调(开闭定界符都满足)a_"foo"_b -> 不强调(_ 两侧都有字母数字)2.3 列表规则
列表的延续
- 项目一
第二段(缩进与列表内容对齐)
- 项目二列表的宽松与紧凑
# 紧凑列表(列表项之间无空行)- foo- bar- baz
# 宽松列表(列表项之间有空行)- foo
- bar
- baz宽松列表中每个列表项被 <p> 包裹,紧凑列表则不会。
列表的开始条件
# 这样会中断列表- 项目一
新的段落
- 项目二(新列表,不是上面列表的延续)列表中断条件:列表后面跟一个空行,空行后是非列表内容,再空行后才开始新列表。
2.4 代码块规则
缩进代码块
缩进 4 个空格即为代码块,但前提是它不在列表中(或缩进足够多):
缩进代码块
- 列表项
列表内的代码块(需要 8 空格缩进)围栏代码块
```代码块```
```pythondef hello(): print("hi")```围栏代码块的关闭条件:遇到与开围栏同类型且长度大于等于开围栏的行。
2.5 HTML 块规则
<div>Markdown 语法在这里**不被解析**</div>HTML 块开始条件(7 种):
<script>,<pre>,<style>开始标签- 注释
<!-- ... --> <? ... ?><! ... >(非注释)<![CDATA[ ... ]]>- 块级 HTML 元素开始/结束标签
- 不可在行内出现的开标签
HTML 块结束条件取决于开始类型:
- 类型 1:遇到对应的关闭标签
- 类型 2-5:遇到对应的结束标记
- 类型 6-7:遇到空行
2.6 链接规则
行内链接
[link](/uri "title")[link](/uri)[link]()[link](<>)引用链接
[link][foo]
[foo]: /url "title"快捷引用
[foo]
[foo]: /url "title"链接目标解析规则
[link](/url\(paren\)) -> 目标: /url(paren)[link](/url\40space) -> 目标: /url space三、GFM 扩展
3.1 表格
GFM 在 CommonMark 基础上增加了表格语法:
| 列一 | 列二 || ---- | ---- || 内容 | 内容 |解析规则:
- 必须有分隔行(
| --- |) - 列数由分隔行决定
- 单元格用
|分隔,首尾|可选 - 对齐:
:---左,:---:中,---:右
3.2 任务列表
- [x] 已完成- [ ] 未完成规则:
- 必须在列表项内部
[x]和[ ]中不能有多余空格- 只有无序列表支持
3.3 删除线
~~删除线~~规则:
- 必须成对出现
~~内不能有换行
3.4 自动链接
https://example.comwww.example.comuser@example.comGFM 自动识别这些模式并转为链接,CommonMark 不会。
3.5 围栏代码块的语言标识
```pythoncodeGFM 规定语言标识只取第一个单词,且转为小写:
```markdown```python 3.11# 语言标识为 "python"(忽略 "3.11")## 四、CommonMark 与 GFM 差异
### 4.1 完整对照
| 特性 | CommonMark | GFM | 说明 || ------------ | ---------- | ------------ | ---------------------- || 表格 | 不支持 | 支持 | GFM 扩展 || 任务列表 | 不支持 | 支持 | GFM 扩展 || 删除线 | 不支持 | 支持 | GFM 扩展 || 自动链接 | 不支持 | 支持 | GFM 扩展 || 强调规则 | 精确定义 | 相同 | 两者一致 || 列表规则 | 精确定义 | 相同 | 两者一致 || HTML 块 | 7 种类型 | 相同 | 两者一致 || 围栏代码块 | 支持 | 支持+语言标识 | GFM 定义了语言标识处理 |
### 4.2 解析器兼容性
不同解析器对规范的实现程度:
| 解析器 | CommonMark 合规 | GFM 合规 | 备注 || ------------- | --------------- | -------- | ---------------- || cmark | 官方参考实现 | 完全 | C 语言 || commonmark.js | 完全 | 完全 | JavaScript || goldmark | 完全 | 部分 | Go 语言 || pulldown-cmark | 大部分 | 部分 | Rust 语言 || markdown-it | 大部分 | 部分 | 需插件 || remark | 大部分 | 部分 | Unified 生态 |
## 五、规范中的边界情况
### 5.1 嵌套强调
```markdown*foo **bar** baz*CommonMark 规定:外层强调没有关闭前,内层可以开新的强调。结果是 <em>foo <strong>bar</strong> baz</em>。
*foo *bar* baz*这里第二个 * 和第三个 * 先配对,结果是 <em>foo </em>bar<em> baz</em>。这是因为开定界符的优先匹配规则。
5.2 列表与代码块
- 项目一
缩进代码块(8 空格 = 列表缩进 4 + 代码缩进 4)在 CommonMark 中,列表内的缩进代码块需要额外缩进 4 空格(列表标记占的宽度)。
5.3 空白行与段落
段落一
<!-- 空行 -->
段落二一个空行就足以分隔段落。多个空行和单个空行效果相同。
5.4 转义
\*不是强调\*\#不是标题\[不是链接](url)可转义的字符(CommonMark 明确列出):
\ ` * _ { } [ ] ( ) # + - . ! | ~ >其他字符前的 \ 不会被转义,\ 原样保留。
5.5 围栏代码块的嵌套
内层围栏外层用更多反引号(4 个),内层用更少(3 个),可以正确嵌套。
六、编写兼容性良好的 Markdown
6.1 避免歧义写法
# 差:依赖解析器的边界行为1. 项目一2. 项目二 - 嵌套(2 空格缩进,有的解析器不认)
# 好:明确缩进1. 项目一2. 项目二 - 嵌套(4 空格缩进,所有解析器都认)6.2 围栏代码块优于缩进代码块
# 差:缩进代码块 code here
# 好:围栏代码块code here
围栏代码块意图更明确,且支持语言标识。
6.3 空行规则
# 块级元素前后都要有空行
## 标题
段落
- 列表项
> 引用6.4 避免原始 HTML
# 差<table><tr><td>cell</td></tr></table>
# 好| 列 || --- || cell |除非必要,尽量用 Markdown 语法而非 HTML。
6.5 链接写法
# 差:URL 中有特殊字符[link](/url with spaces)
# 好:用尖括号或编码[link](</url%20with%20spaces>)[link](/url%20with%20spaces)七、解析器实现要点
7.1 两阶段解析
CommonMark 解析分两个阶段:
- 块级解析:将输入文本解析为块级元素的树结构
- 行内解析:对每个块级元素的文本内容解析行内格式
输入文本 → 块级解析 → 块级树 → 行内解析 → 最终 AST → 渲染7.2 链接引用解析
[foo][bar]
[bar]: /url "title"链接引用定义可以在文档的任何位置,不一定要在引用之前。解析器需要在整个文档扫描完成后才能确定链接目标。
7.3 列表延续条件
- 项目一 第二行(延续,缩进对齐)
第三段(延续,空行后缩进对齐)
- 项目二延续条件:后续行的缩进与列表项内容对齐(至少与列表标记后的第一个空格对齐)。
参考资料
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






