为什么需要 Markdown 规范 CommonMark 规范开篇就提到了这个问题,并列举了十多个例子来说明制定一个 Markdown 规范的必要性。 由于没有明确的规范,Markdown 解析渲染会有很大差异。因此用户经常会发现在一个系统(例如 GitHub)上渲染正常的文档在另一个系统上渲染不正常。更糟糕的是由于 ..

CommonMark 规范要点解读

为什么需要 Markdown 规范

CommonMark 规范开篇就提到了这个问题,并列举了十多个例子来说明制定一个 Markdown 规范的必要性。

由于没有明确的规范,Markdown 解析渲染会有很大差异。因此用户经常会发现在一个系统(例如 GitHub)上渲染正常的文档在另一个系统上渲染不正常。更糟糕的是由于 Markdown 中不存在“语法错误”,所以无法立即发现这类问题。

在 Markdown 处理上“模糊的正确”是不可取的。所以 CommonMark 规范的目的就是消除二义性,制定统一明确的 Markdown 解析渲染规则。

作者阵容

CommonMark 规范的主创 John MacFarlanejgm)是加州大学伯克利分校的哲学教授,他在文本标记语言领域有一个很出名的项目 Pandoc(用于在各种文本标记语言之间互转格式)。他用多种编程语言实现过 Markdown 引擎,在 Markdown 处理方面他可以称得上行家中的行家。

该规范的其他参与者包括:

从作者阵容我们可以看出,该规范算是众望所归了,因为这几大社区都需要一个标准化的 Markdown。

除了强大的作者阵容外,最重要的是规范的严谨度我相信不会有任何问题,600+ 测试用例也尽量将各种情况都列举验证了,整体的权威性毋庸置疑。

介绍完大致背景,下面我们进入技术细节,对规范中定义的要点进行解读。

元素分类

为了方便解析,将内容元素分为块级元素和行级元素,其中块级元素又分为两类:

行级元素包括:内联代码(`code`)、强调、加粗、链接、图片、某些 HTML 标签、文本等。

优先级

具体场景细节可在规范中搜索关键字 precedence

段落分段规则

某些情况下不需要空行即可“打断”当前内容,形成新的段落或者其他块级元素。

每种情况的细节可以到规范中搜索关键字 interrupt

段落延续文本

段落延续文本(Paragraph continuation text)即段落开始后不被分段规则打断的部分,该部分也算作当前段落。

最简单的例子是以 \n 分隔的两行文本:

foo
bar

渲染的 HTML 结果应该是:

<p>foo
bar</p>

在其他元素中的例子:

> foo
bar

渲染的 HTML 结果应该是:

<blockquote>
<p>foo
bar</p>
</blockquote>

强调和加粗

分隔符序列(delimiter run):

左侧分隔符序列(left-flanking delimiter run):

  1. 是一个分隔符序列
  2. 后面不能跟空白
  3. 后面不能跟标点;或者后面跟标点并且前面是空白或者标点

右侧分隔符序列(right-flanking delimiter run):

  1. 是一个分隔符序列
  2. 前面不能是空白
  3. 前面不能是标点;或者前面是标点并且后面是空白或者标点

解析策略参考

规范附录部分介绍了一种解析策略,总的来说分为两个阶段:

  1. 将输入文本断行,顺序解析每一行并生成块级节点。文本作为块级节点的内容,暂时不进行解析。链接引用定义在这个阶段也会被解析构造放到一个 Map 中
  2. 解析每个块级节点的内容生成行级节点。如果有引用定义的话使用阶段 1 中的 Map 进行解析

举个例子,对于给定的 Markdown 文本:

## 简介

一款 *Markdown* 引擎。

## 特性

* 实现 _GFM_
* 非常快

生成 Markdown 语法树为:

{
    "title": {
        "text": "Markdown 语法树示例"
    }, 
    "tooltip": {
        "trigger": "item",
        "triggerOn": "mousemove"
    },
    "toolbox": {
        "show": true, 
        "feature": {
            "mark": {
                "show": true
            }, 
            "restore": {
                "show": true
            }, 
            "saveAsImage": {
                "show": true
            }
        }
    }, 
    "calculable": false, 
    "series": [
        {
            "name": "树图", 
            "type": "tree", 
            "symbolSize": 10, 
            "initialTreeDepth": -1, 
            "roam": true,
            "left": 0,
            "right": 0,
            "orient": "vertical", 
            "label": {
                    "position": "top", 
                    "verticalAlign": "middle", 
                    "align": "left", 
                    "fontSize": 12,
                    "offset": [9, 12]
            }, 
            "lineStyle": {
                        "color": "#4285f4", 
                        "shadowBlur": 8, 
                        "shadowOffsetX": 3, 
                        "shadowOffsetY": 5, 
                        "type": "curve"
            },
            "data": [
                {
                    "name": "Document", 
                    "children": [
                        {
                            "name": "Heading\nh2", 
                            "children": [
                                {
                                    "name": "Text\n'简介'"
                                }
                            ]
                        }, 
                        {
                            "name": "Paragraph\np", 
                            "children": [
                                {
                                    "name": "Text\n'一款'"
                                }, 
                                {
                                    "name": "Emph\nem", 
                                    "children": [
                                        {
                                            "name": "Text\n'Markdown'"
                                        }
                                    ]
                                }, 
                                {
                                    "name": "Text\n'引擎'"
                                }
                            ]
                        }, 
                        {
                            "name": "Heading\nh2", 
                            "children": [
                                {
                                    "name": "Text\n'特性'"
                                }
                            ]
                        },
                        {
                            "name": "List\nul", 
                            "children": [
                                {
                                    "name": "Item\nli",
                                    "children": [{"name": "Paragrap\np", "children": [{"name": "Text\n'实现'"},{"name": "Emph\nem", "children": [{"name": "Text\n'GFM'"}]}]}]
                                },
                               {
                                    "name": "Item\nli",
                                    "children": [{"name": "Paragrap\np", "children": [{"name": "Text\n'非常快'"}]}]
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}

下面介绍强调和链接解析处理,这部分比较有技巧。

嵌套强调和链接的解析算法

强调、加粗、链接、图片这四种节点都是行级元素,但这四种元素的解析生成稍微有点麻烦,因为它们有可能存在嵌套。建议结合 CommonM 官方参考实现 JavaScript 版的文件 inlines.js 来看就容易理解了(注意该实现进行了一定优化,比如去掉了 current_position 变量,但总体逻辑没有变)。

解析行级元素时,如果遇到:

时,则以这些符号作为文本内容生成一个文本节点,并在分隔符栈(delimiter stack)中压入一个指向该文本节点的元素。

分隔符栈是一个双向链表,其中每个元素都指向一个文本节点,并附加如下信息:

当我们解析时遇到 ],则进入下面介绍的链接和图片处理过程

当我们解析到输入结束时,则将 stack_bottom 置为 NULL 并进入下面介绍的强调处理过程

链接和图片处理过程

从分隔符栈顶部开始回看寻找开始的 [ 或者 ![ 分隔符元素。

强调处理过程

参数 stack_bottom 设置了分隔符栈的栈底下限。如果其值为 NULL 则我们可以一直遍历到栈底。否则我们应该在访问到 stack_bottom 之前停止。

current_position 指向分隔符栈中高于 stack_bottom 的元素(当 stack_bottom 为 NULL 时指向第一个元素)。

使用 openers_bottom 来跟踪每种分隔符(按类型 *_ 和结束分隔符长度模 3)。初始化值为 stack_bottom

然后我们重复以下步骤,直到用完了潜在的结束分隔符:

由于没有明确的规范,Markdown 解析渲染会有很大差异。因此用户经常会发现在一个系统(例如 GitHub)上渲染正常的文档在另一个系统上渲染不正常。更糟糕的是由于 Markdown 中不存在“语法错误”,所以无法立即发现这类问题。

在 Markdown 处理上“模糊的正确”是不可取的。所以 CommonMark 规范的目的就是消除二义性,制定统一明确的 Markdown 解析渲染规则。

作者阵容

CommonMark 规范的主创 John MacFarlanejgm)是加州大学伯克利分校的哲学教授,他在文本标记语言领域有一个很出名的项目 Pandoc(用于在各种文本标记语言之间互转格式)。他用多种编程语言实现过 Markdown 引擎,在 Markdown 处理方面他可以称得上行家中的行家。

该规范的其他参与者包括:

从作者阵容我们可以看出,该规范算是众望所归了,因为这几大社区都需要一个标准化的 Markdown。

除了强大的作者阵容外,最重要的是规范的严谨度我相信不会有任何问题,600+ 测试用例也尽量将各种情况都列举验证了,整体的权威性毋庸置疑。

介绍完大致背景,下面我们进入技术细节,对规范中定义的要点进行解读。

元素分类

为了方便解析,将内容元素分为块级元素和行级元素,其中块级元素又分为两类:

行级元素包括:内联代码(`code`)、强调、加粗、链接、图片、某些 HTML 标签、文本等。

优先级

具体场景细节可在规范中搜索关键字 precedence

段落分段规则

某些情况下不需要空行即可“打断”当前内容,形成新的段落或者其他块级元素。

每种情况的细节可以到规范中搜索关键字 interrupt

段落延续文本

段落延续文本(Paragraph continuation text)即段落开始后不被分段规则打断的部分,该部分也算作当前段落。

最简单的例子是以 \n 分隔的两行文本:

foo
bar

渲染的 HTML 结果应该是:

<p>foo
bar</p>

在其他元素中的例子:

> foo
bar

渲染的 HTML 结果应该是:

<blockquote>
<p>foo
bar</p>
</blockquote>

强调和加粗

分隔符序列(delimiter run):

左侧分隔符序列(left-flanking delimiter run):

  1. 是一个分隔符序列
  2. 后面不能跟空白
  3. 后面不能跟标点;或者后面跟标点并且前面是空白或者标点

右侧分隔符序列(right-flanking delimiter run):

  1. 是一个分隔符序列
  2. 前面不能是空白
  3. 前面不能是标点;或者前面是标点并且后面是空白或者标点

解析策略参考

规范附录部分介绍了一种解析策略,总的来说分为两个阶段:

  1. 将输入文本断行,顺序解析每一行并生成块级节点。文本作为块级节点的内容,暂时不进行解析。链接引用定义在这个阶段也会被解析构造放到一个 Map 中
  2. 解析每个块级节点的内容生成行级节点。如果有引用定义的话使用阶段 1 中的 Map 进行解析

举个例子,对于给定的 Markdown 文本:

## 简介

一款 *Markdown* 引擎。

## 特性

* 实现 _GFM_
* 非常快

生成 Markdown 语法树为:

{
    "title": {
        "text": "Markdown 语法树示例"
    }, 
    "tooltip": {
        "trigger": "item",
        "triggerOn": "mousemove"
    },
    "toolbox": {
        "show": true, 
        "feature": {
            "mark": {
                "show": true
            }, 
            "restore": {
                "show": true
            }, 
            "saveAsImage": {
                "show": true
            }
        }
    }, 
    "calculable": false, 
    "series": [
        {
            "name": "树图", 
            "type": "tree", 
            "symbolSize": 10, 
            "initialTreeDepth": -1, 
            "roam": true,
            "left": 0,
            "right": 0,
            "orient": "vertical", 
            "label": {
                    "position": "top", 
                    "verticalAlign": "middle", 
                    "align": "left", 
                    "fontSize": 12,
                    "offset": [9, 12]
            }, 
            "lineStyle": {
                        "color": "#4285f4", 
                        "shadowBlur": 8, 
                        "shadowOffsetX": 3, 
                        "shadowOffsetY": 5, 
                        "type": "curve"
            },
            "data": [
                {
                    "name": "Document", 
                    "children": [
                        {
                            "name": "Heading\nh2", 
                            "children": [
                                {
                                    "name": "Text\n'简介'"
                                }
                            ]
                        }, 
                        {
                            "name": "Paragraph\np", 
                            "children": [
                                {
                                    "name": "Text\n'一款'"
                                }, 
                                {
                                    "name": "Emph\nem", 
                                    "children": [
                                        {
                                            "name": "Text\n'Markdown'"
                                        }
                                    ]
                                }, 
                                {
                                    "name": "Text\n'引擎'"
                                }
                            ]
                        }, 
                        {
                            "name": "Heading\nh2", 
                            "children": [
                                {
                                    "name": "Text\n'特性'"
                                }
                            ]
                        },
                        {
                            "name": "List\nul", 
                            "children": [
                                {
                                    "name": "Item\nli",
                                    "children": [{"name": "Paragrap\np", "children": [{"name": "Text\n'实现'"},{"name": "Emph\nem", "children": [{"name": "Text\n'GFM'"}]}]}]
                                },
                               {
                                    "name": "Item\nli",
                                    "children": [{"name": "Paragrap\np", "children": [{"name": "Text\n'非常快'"}]}]
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}

下面介绍强调和链接解析处理,这部分比较有技巧。

嵌套强调和链接的解析算法

强调、加粗、链接、图片这四种节点都是行级元素,但这四种元素的解析生成稍微有点麻烦,因为它们有可能存在嵌套。建议结合 CommonM 官方参考实现 JavaScript 版的文件 inlines.js 来看就容易理解了(注意该实现进行了一定优化,比如去掉了 current_position 变量,但总体逻辑没有变)。

解析行级元素时,如果遇到:

时,则以这些符号作为文本内容生成一个文本节点,并在分隔符栈(delimiter stack)中压入一个指向该文本节点的元素。

分隔符栈是一个双向链表,其中每个元素都指向一个文本节点,并附加如下信息:

当我们解析时遇到 ],则进入下面介绍的链接和图片处理过程

当我们解析到输入结束时,则将 stack_bottom 置为 NULL 并进入下面介绍的强调处理过程

链接和图片处理过程

从分隔符栈顶部开始回看寻找开始的 [ 或者 ![ 分隔符元素。

强调处理过程

参数 stack_bottom 设置了分隔符栈的栈底下限。如果其值为 NULL 则我们可以一直遍历到栈底。否则我们应该在访问到 stack_bottom 之前停止。

current_position 指向分隔符栈中高于 stack_bottom 的元素(当 stack_bottom 为 NULL 时指向第一个元素)。

使用 openers_bottom 来跟踪每种分隔符(按类型 *_ 和结束分隔符长度模 3)。初始化值为 stack_bottom

然后我们重复以下步骤,直到用完了潜在的结束分隔符:

处理完后,我们从分隔符栈中移除了位于 stack_bottom 之上的所有分隔符。

  • CommonMark
    2 引用 • 37 回帖
  • Markdown

    Markdown 是一种可以使用普通文本编辑器编写的标记语言,通过类似 HTML 的标记语法,它可以使普通文本内容具有一定的格式,具体语法请参考 Markdown 教程

    93 引用 • 622 回帖 • 621 关注
1 操作
88250 在 2019-10-29 17:26:09 更新了该帖
6 回帖
请输入回帖内容...
  • cmlanche

    文章中是怎么嵌入的这个 markdown 文档树的?

    1 回复
  • 88250

    Vditor 编辑器支持的,可参考这里 https://hacpai.com/guide/markdown

  • darren

    文档写得非常棒 👍

    不过文档中是不是用的版本比较旧了,试了下最新版本的 vidtor,渲染出来的结构树没有问题

    文档中的 最新版本 Vditor 渲染出来的
    old.jpg echarts.png

    希望能更新一下下

    我不是强迫症……我真的不是强迫症……我就是看着不舒服而已……

    1 回复
  • 88250

    感谢支持。AST 渲染的问题我没看明白,麻烦详细说一下。

    1 回复
  • darren 1

    就是本文中,Markdown 的语法树的图表中,最右边,ul 下面的第一个 li 下面的右侧 em 应该是在 p 下面的,现在文中的 li 下面的 p 和 em 并列了。

    1 回复
  • 88250

    感谢指出 🙏

请输入回帖内容 ...