【鲜花】关于 Markdown 的一些想法

· · 个人记录

为了设法拯救 Markdown-palletes 的一堆神秘 bug,最近阅读了由 Github 指定的 Markdown 语法规范(GitHub Flavored Markdown Spec),以及被广泛使用的 Markdown 编译器 Markdown-it 源码的实现。感慨颇深。

这篇文章不是用来介绍 Markdown 语法的,只是用来吐槽。

Markdown 是什么?一个轻量级的文本标记语言。轻量级,代表了这东西被设计出来只是用来解决一些简单的排版工作(例如加粗、斜体、列表啥的);轻量级,也代表了它的语法规则可以很随意。

Markdown 支持的标记种类很少,大体上是两类:块级(block)的标记,以及行内(inline)的标记。块级标记,主要是有序/无序列表、引用块、代码块、表格(这个是 GFM 支持的拓展),或许可以算上一至六级标题以及水平线;行内标记,主要是粗体、斜体、删除线、图片、链接、行内代码、索引(link reference,也是 GFM 所支持的拓展)。

这么简单的功能支持,可以产生怎样的火花(答辩)呢?

先从 Markdown 的理念开始说起。Markdown 的一大特点是源码本身就可读,大多数语法的设计就是源自于各种排版工具出现之前人类的习惯用法。比如像这个:

Solution
=============

Here's a simple implementation for A+B problem:

~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp
#include <iostream>
using namespace std;
int main(){
    int a, b;
    cin >> a >> b;
    cout << a + b << endl;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~

What does the code do:

* include the `IO stream`.
* use the **standard namespace**.
* read the two numbers, plus them, and then output.

- - - - - - - - - - - - - -

Some reference:

> https://www.luogu.com.cn/problem/P1001

我想,大多数读者是从洛谷开始接触到 Markdown 的编写的,所以对于某些语法会有些刻板印象。比如说,认为代码块只能是三个反引号、无序列表项只能是短横线、水平线只能是三个短横线。其实不是这样,Markdown 的语法非常、非常灵活,很多纯粹自然形成的应用于纯文本环境(比如说,C++ 注释)的看上去更美观的排版方式都可以在 Markdown 下正确识别并进行渲染。

另外一点,Markdown 本身对已经知道并使用 Markdown 语言的用户也开放了一点便利。比如说,引用块不需要每行都打上右尖括号;对于有序/无序列表项的判定也没那么严格。

此外,像这样的语法:

This is a quite long sentence, so I cannot write
it in just one line.

会被放在同一个自然段(paragraph)里,中间用一个空格隔开。

放在中文环境下,自动添加一个空格的行为似乎很古怪。但是放在西文环境下就比较容易理解了:很多拼音文字使用单词作为最小的意义载体,每两个单词之间都使用空格隔开(句子之间,考虑为了排版美观而在句点后面添加的空格,也算是用空格隔开)。在很多没有使用自动换行的编辑器下,将一个很长的自然段放在同一行是难以阅读和编辑的,因此会经常在其中添加换行,所以产生这样的 Markdown 语法并不奇怪。

不过这种语法在中文环境下就显得怪异了,毕竟通常情况下纯汉字组成的自然段是不会有空格夹杂其中的。

说了以上两点,大致介绍了 Markdown 的自由度以及为用户考虑的特点。这个大概也是 Markdown 迅速风靡并形成大量方言的原因。

但是放在 Markdown 编译器的角度来看,这样的自由度就变成了一个灾难。这里简单截取一下 GFM 涉及到的几个例子:

例 1

1. one

2. two
3. three

列表项的间距应该怎么处理?


1.  one
    - a

    - b
2.  two

这样的列表呢?

例 2


 8. item 1
 9. item 2
10. item 2a

有序列表的序号可以右对齐吗?

例 3


* a
* * * * *
* b

这样的列表,应该被翻译成【一个有三项的无序列表,第二项是一个水平线】,还是【两个分别有一项的无序列表,中间用水平线隔开】?

这里仅举了三例,且只和列表有关。有兴趣的读者可以自己阅读 GFM,里面提供了六百多个示例,有一些示例符合人类正常理解,另外一些就很值得深刻思考。

可以发现,过于自由的设计理念,以及规范的缺失,导致了在特定情形下对源码的解释变得扑朔迷离。

事情到了这里,可以算是搞砸了。从一开始我们就强调,Markdown 支持的功能极其简单,但是能胜任大多数的排版工作。代码块、列表,其实都是高度结构化的功能,很容易描写出他们所对应的树形结构(事实上 HTML 内这些块状结构也就通过标签得到了很好的描述)。然而,高度贴近 plain txt 的 Markdown 句法导致这些结构在处理上变得困难重重。

我们有很多现成的描述结构的方案:

然而,在 Markdown 中,有一部分块状语法是使用标记进行描述(比如代码块),有一部分是基于缩进(比如列表);对于基于缩进的列表,不统一的缩进大小更是个灾难:有序列表里上一项的长度决定了后一项应该怎么样对齐,实际缩进又可以比“预期”的缩进多上一格,我们无法硬性规定 Tab 的长度就是 2 或者就是 4。

由于 Markdown 支持将一段话拆成若干行分开来写,然后有时候又允许你少写一些符号,这使得事态向更复杂的方向发展。

结果就是各家编译器表现得都有点小区别,大家都是根据自己的理解写的。即使是 Markdown 创始人写的 Markdown.pl 脚本,在某些奇异搞笑的源码上表现得也很糟糕。

结果来看,即使是 CommonMark 或者 GFM 这种规范,为了应对潜在的边界条件,不得不在规范里指出了五六百条情况。

从解决标准问题的角度上来看,我们完全可以把 Markdown 变得更加“严格”,约定用户必须得按照某种更规范的写法书写。或者移除一些奇异搞笑的语法(比如 Setext 风格的一级、二级标题)。这样子可以显著减少 corner case 的数量,大大减小了编译器编写的难度,然而这种办法毫无意外与 Markdown 的精神相抵触。

事实上,很多矛盾的原因是早期编辑器功能不完善,或者很多人只能在纯文本阅读器上阅读你写的内容。这些问题摆在今天都算不上问题:自动换行功能可以使得即使将一整段内容写在一行也很容易阅读,自动补全功能可以大大降低引用块、列表的缩进编写难度,即使是将大段文字从别处复制过来,使用编辑器的快捷键也很容易将其缩进调整正确。HTML 与 Markdown 编译器早已普及,基本不存在阅读障碍。

但是作为旧互联网时代向现代互联网转变过程当中诞生的 Markdown,承载了太多期望,被迫肩负上沉重的时代包袱。