mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1502 字
4 分钟
Markdown 规范详解
2021-02-01

你在博客里写了这样一段 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 空格缩进)

围栏代码块

```
代码块
```
```python
def hello():
print("hi")
```

围栏代码块的关闭条件:遇到与开围栏同类型且长度大于等于开围栏的行。

2.5 HTML 块规则#

<div>
Markdown 语法在这里**不被解析**
</div>

HTML 块开始条件(7 种):

  1. <script>, <pre>, <style> 开始标签
  2. 注释 <!-- ... -->
  3. <? ... ?>
  4. <! ... >(非注释)
  5. <![CDATA[ ... ]]>
  6. 块级 HTML 元素开始/结束标签
  7. 不可在行内出现的开标签

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.com
www.example.com
user@example.com

GFM 自动识别这些模式并转为链接,CommonMark 不会。

3.5 围栏代码块的语言标识#

```python
code
GFM 规定语言标识只取第一个单词,且转为小写:
```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 解析分两个阶段:

  1. 块级解析:将输入文本解析为块级元素的树结构
  2. 行内解析:对每个块级元素的文本内容解析行内格式
输入文本 → 块级解析 → 块级树 → 行内解析 → 最终 AST → 渲染

7.2 链接引用解析#

[foo][bar]
[bar]: /url "title"

链接引用定义可以在文档的任何位置,不一定要在引用之前。解析器需要在整个文档扫描完成后才能确定链接目标。

7.3 列表延续条件#

- 项目一
第二行(延续,缩进对齐)
第三段(延续,空行后缩进对齐)
- 项目二

延续条件:后续行的缩进与列表项内容对齐(至少与列表标记后的第一个空格对齐)。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Markdown 规范详解
https://blog.souloss.com/posts/language/language-markdown-specification/
作者
Souloss
发布于
2021-02-01
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时