satgo1546’s ocean

Any sufficiently primitive magic is indistinguishable from technology.

如果有得选,别选Markdown

今天我要来好好地喷一下Markdown。让我们来看看为什么不管从哪个角度看,Markdown都是错误的选择。

语法怪异

只有开发人员痴迷于纯文本和DSL,而普通用户长久受到所见即所得编辑器的熏陶,明晰地认识到通过特殊语法实现文字效果是一种时代的倒退,即使这种语法看上去人畜无害。因为需要付出努力记忆,因为在遇到问题时无助,因为符号一点也不直观。

快问快答:请用Markdown打出下列文稿片段。

反斜杠\用于转义符号,使其不被视作Markdown特殊语法。被`环绕的文字通常视作代码,但在`前加上\,写成\`,就能原样输出反引号“`”;而要原样输出反斜杠“\”,就要写成\\。如果要打出包含单个`的代码,需要用`` ` ``这种特殊的转义方法,而不是`\``,后者被识别为包含单个\的代码和一个孤立的反引号,因为\在代码中不是转义符号。被多个连续的`(例如`````)环绕的文字也被视作代码,这样就能在代码中包含少量`。代码内两侧皆有的空格(各一)会被剥除,以便表示以`开头或结尾的代码。例如,在AutoHotkey脚本语言中,`被用作转义字符,字符串中的`n表示换行符。除此之外,额外插入的空格会被保留,例如换行符和空格`n 。如果代码中只有空格,可以直接输入。例如,在Markdown中,一行文字以 结尾,表示插入硬换行<br>,这里的代码用` `输入。但是,代码同时以空格开头和结尾的场合,例如 x ,需要多插入两个空格来补偿被剥除的两个空格。由于Discourse Markdown实现问题,在输入大于两个纯空格代码(如本应通过` `打出的 )时,也必须追加两个空格,写成` `,这不符合CommonMark规范。

这是一幅20×20像素的怒猫的图片::pouting_cat:,这幅图片的原始尺寸是72×72。注意:表示尺寸的乘号应该写成“×”,但因为乘号较难打出,不少用户会用“*”代替,写成像20*20、72*72这样,在Markdown中会导致星号丢失,其间文字被视作强调。与之类似,形如“<x>”和“<x y>”的文字也要特别注意,没有转义的话,会被识别成HTML标记,最终消失。

Markdown拥趸可能已经汗流浃背了。对HTML和Word用户而言,这不过是一段稍长一点的技术说明,和一般的网页内容没什么两样;对Markdown而言,每一个富文本标记都是一场灾难。就像外壳脚本,语法看似简洁又合理,实际却是一场彻头彻尾的灾难。

段落开头空两格

CommonMark描述Markdown为“一种用于编写结构化文档的纯文本格式,取材于电子邮件和新闻组的格式惯例”。

中文自古以来就是纯文本,【标题】和《书名》通过标点即可表达,所以即使是纯文本邮件,也未见额外添加格式记号的需求。Markdown中的斜体格式记号在中文没有使用场景,而粗体则存在巨大缺陷:本文中的所有粗体字(包括这句话)都无法用**__语法实现。注意加粗的部分包含标点。这是CommonMark规范定义的预期行为,是无空格语言特有的问题,四年前的工单至今尚未解决。

倒是中文的对齐方式很有讲究。小学就学过:全文标题要居中,段落开头空两格。结果开头的空格在Markdown中被翻译为代码块,而真正的居中和空两格则很难实现。我见了太多没有经验的用户为段落缩成一行感到困惑。缩进 = 代码块的设计不仅困扰中文用户,也同样困扰着开发人员。因为程序自动插入的Markdown文本容易误带缩进,Eleventy默认禁用了缩进代码块

中国人爱写诗,换行尤为重要。Markdown既没有提供非代码块的<pre>,又没有提供直观的换行方式,必须在每行行末追加 (两个空格,很容易丢失)或\,或者回到HTML的<br>。不专门查阅资料,都不知道怎么换行。

兼容的HTML:不可能的任务

Markdown支持嵌入HTML。理论上,100%兼容任意HTML片段是不可能完成的目标,因为HTML和Markdown的解析都绝不会报错,所以没有兼容的余地。而在实际使用中,也确实有不少问题,导致外部粘贴进来的HTML代码无法正确原样输出。HTML的语法十分复杂,简单的启发式识别算法遇到复杂的块结构,输出就会错乱。

CommonMark只考虑了直接以<pre开头块需要原样输出,但没有考虑嵌套的<pre>,导致空行被识别为HTML块的结束、段落的开始,最终输出标记嵌套错乱的HTML。

这个问题的工单更加久远。

行间HTML块会原样输出,但行内HTML元素中的符号却会被Markdown识别。<code>`</code><code>`</code>会被转换为嵌套的<code>标记,于是不得不把`写成\`&grave;。Markdown总会在意想不到之处搞事,最终把文档源码演化成比纯粹写HTML还丑陋的样子。工单

正则表达式堆砌的坟场

尽情地尝试任何转换工具吧,它们也无能为力。我尝试过的工具中,没有一个能从本文的HTML转换出正确的Markdown。

程序很难正确识别人类觉得自然的语言,程序也很难正确从数据源生成符合语法的内容。由于人类觉得自然的语言总是有着许多语义模糊的边界场景,要正确使用就需要仔细对照,小心处理。多数Markdown编辑器提供的行内代码按钮只会在选中文本两侧加上`,在代码中含有反引号时就会错乱,而没有做错任何事的用户只能对着错乱的渲染结果倍感疑惑。

初版的Markdown.pl就是一堆正则表达式,这也不能怪作者。在CommonMark规范制定期间,发现原先有许多远处记号影响含义的例子,导致解析很慢。经过对语法调整和妥协后,才有了一遍解析就可完成渲染的CommonMark。

因为语法实际复杂,解析必须使用专门的库,然而语法看起来欺骗性地简单,库生态亦欺骗性地繁荣。不需要专门设计编辑工具,只需要处理和存储纯文本,引用库来渲染,就能有富文本的效果,如此捷径催促着Markdown的蔓延。但这更多表明的是程序员逃避而非面对富文本,是一种让人看上去没有偷懒的捷径,掩盖问题带来的是更多的问题。

验证渲染正确性和数据的迁移比单纯的解析和渲染更加困难。GitHub接轨CommonMark时,花了一番功夫清洗原有数据,这样的投入绝非每个选择Markdown的网站都能承担。

不富的富文本和不是Markdown的Markdown

Markdown是各种标记语言中功能最弱的语言。原味的功能少得可怜,即使算上已成为事实上的标准的GitHub风味,也远远比不上同行。图片尺寸、定义列表、锚点、上下标、合并单元格、目录、脚注、文字高亮……常用但欠缺的功能要多少有多少。Markdown的解决方案,要么退回HTML,要么扩展语法。

前文已经提到在Markdown里写HTML本来就有问题。若如此多的功能都不支持,还不如直接写HTML算了!HTML本身就不怎么方便写,Markdown还瞎掺和。如果一开始写了Markdown语法,后来想要修改,加入一些不支持的元素,就必须推翻重写为HTML。这时的Markdown不仅没有简化输入,还增添了麻烦,在编辑流程中插入了巨大的割裂感。所以大部分开发者和用户都选择扩展语法,催生了数不尽的方言。

每个库都扩展了常用功能的语法,各自为营,各不相同,千奇百怪,看似共通,实不兼容,不兼容之处又相当微妙。据我所知,指定图片大小的扩展语法至少有五种:

Flavor语法
原味<img src="pouting_cat.png" width="20" height="20">
Discourse![alt|20x20](pouting_cat.png)
MultiMarkdown![alt](pouting_cat.png width="20" height="20")
PHP Markdown Extra![alt](pouting_cat.png){width=20 height=20}
Pandoc![alt](pouting_cat.png){width="20" height="20"}
Kramdown![alt](pouting_cat.png){: width="20" height="20"}

许多标记语言都支持定义列表,只有Markdown用户在问:什么是定义列表?定义列表的用途可能比想象中更多。在MDN上随机访问一个页面,有相当的概率其中就有定义列表,如标记的属性、属性的取值、函数的参数、对象的方法。MDN在从HTML迁移到Markdown时必须面对的问题便是如何处理这种Markdown不支持的格式。尽管许多库都支持一种扩展语法以生成定义列表,出于编辑工具兼容性的考虑,他们选择了基于已有的项目符号列表语法自己搓一个扩展。

原版Markdown只支持基于缩进的代码块,上文提到这很不好用。前CommonMark时期,代码块的语法也分裂为```~~~两种,直到CommonMark统一局面,才有了今天随处可用的代码块。

被迫进行的随意扩展带来的是实现的分裂,进而导致各家软件间文稿数据不可交换。CommonMark紧盯最小公约数不放,虽然统一了基本语法,但并没有试图统一常用扩展。各家Markdown都称自己为Markdown,常用功能的语法却各不相同。编辑器中看到的Markdown渲染结果根本不作数,因为用于网站展示的管线采用的是别的库。

良莠不齐的扩展

然而这些扩展也只能涵盖常用文档部件。不同类型的文档需要不同类型的文档部件。译文和原文对应的双语文章需要双栏对照的文本段落,帮助开发者迁移的指南需要双栏对照的代码块;指导按键顺序的说明手册需要穿插特殊符号字体,以两个视角叙述的小说则需要为不同章节使用不同的正文字体;数学笔记需要公式、流程图和坐标轴,历史笔记需要地图、关系网和时间线;wiki需要基于文档标题的站内链接,教科书需要标出定理和习题。

得益于其一看就懂(存疑)的语法,Markdown已经出现在了所有它该出现和不该出现的地方:博客、笔记、说明书、书籍、信件、幻灯片……似乎有一种不知从何而来的倾向,只要是有字的地方,加上Markdown就会成为卖点。可是,每一种文档类型都有着微妙的差异,无脑选择Markdown势必产生需求缺口。

GitHub追加了删除线、高亮块;Reddit追加了剧透模糊;Discourse追加了引用帖子;就连CommonMark规范自身,都采用了扩展的语法来标记双栏对照代码块。Markdown笔记软件往往为各领域的用户添加了几十种扩展语法。想要什么功能,就添加,但名义上,仍然称呼其为Markdown。结果,世界上没有两个Markdown实现是相同的。

原本的Markdown精神在扩展的压力之下消散了。这些扩展有的融合进了现有语法,有的创造了新的语法,有的破坏了兼容性,有的扩展间甚至有冲突。插入公式的首选方案是LaTeX,插入绘图的方案有Mermaid和PlantUML等,它们无一例外需要用户学习另一种与Markdown毫不相关的语法,增加文档渲染的复杂度,还可能令渲染产生错误。这些错误可能并非文档作者的疏忽,平台的一次升级就可能破坏原本正常渲染的组件。与Markdown宽容的渲染策略不同,这些组件遇到错误就只会显示干巴巴的错误信息,看不见文字,读者也无法猜测作者的原意。

LaTeX和Typst等出版工具提供用户自定义部件的能力,Web上可以采用Vue等UI组件框架。Markdown?每有个新组件,就要写个解析器插件。即使每个Markdown库都有一大堆插件,用户的需求还是得不到满足。直至最近MDX把React搬了进来,才开始向统一的扩展框架迈进。

不结构化的结构化文档

Markdown所谓的“结构化”就是有六级标题。拜托,六级标题根本不是什么新鲜事。由于可用格式标记过少,标题以外的有用的结构无法表达,用户不得不滥用可用格式创造效果。

原版Markdown太过古早,就连HTML今天支持的结构化能力,Markdown也不具备。LaTeX自古支持的<figure>,没有对应物。对<small><mark><b>等语义化标记没有支持,用户便认为Markdown是个表达样式而非结构的语言。我见过不少用户,竟然用行内代码``代替高亮文字<mark>,仅仅是因为在许多平台上,行内代码有特殊的背景色!

HTML中的六种斜体<i><em><var><dfn><cite><address>,Markdown只有一种。因为没有附加语义,工具也只好将其简单处理为某种斜体标记。可是,这六种标记在中文中的表现形式完全不同:

HTML英文简体中文
<i>斜体(多种)
<em>斜体着重号
<var>斜体斜体
<dfn>斜体黑体
<cite>斜体书名号
<address>斜体无样式

与HTML深度绑定

Markdown从最初就没有想过要输出到HTML以外的格式。富文本和结构化能力弱、支持嵌入HTML、全面面向Web的库生态,注定了输出到其他格式将会是灾难,灾难程度就跟LaTeX转换到HTML一样。

网页是一种呈现效果相当糟糕的媒介,极难保证各端渲染效果一致,高级排版特性可说是没有,只适合屏幕阅读而不适应纸张。以之为目标,意味着事后转换也不可挽回了。

脱离Web平台后,HTML不再是万能钥匙,表现只能依靠Markdown原生的结构。数不尽的平台和工具挣扎着帮助Markdown与Web解耦,Markdown弱小的表达力却是个怎么也挥之不去的问题。语法扩展成了唯一选项,随之丢失的便是数据共通性,文稿从写下的那刻起就锁死平台出不去了,最初选择Markdown可有可无的那点优势终归荡然无存。

永远裂开的图片

我初学HTML时也曾疑惑:插入图片时要怎么填地址?我按最合理的想法,填入了file:// URL,然后上传。打开网页,看起来没有问题。当然,其他人访问时,看到的只剩裂图。

发送Markdown附件时,很可能忘记连同资源一起发送。对于压缩包内的Markdown文件,预览工具找不到图片,图片就看不到了,取而代之的是裂开的图。

为了编写,一些用户开始使用图床存放图片,这样就只需要管理Markdown文件。那么,代价是什么?还没几年,图床挂了,图片全裂。

插入图片带来文件管理上的麻烦,令人逃避插入图片这种本不应困难的操作。或许我们可以向奇怪的HTML实践学习,把所有图片都Base64编码进源码里。

前路漫漫

Markdown原始的定位在浪涛之下淡化,时至今日,已不管做什么都做不好。它既不便于用户编写,又不便于程序识别;既不安全,又不高效;既缺乏表现力,又缺乏扩展能力;既不够统一以用于数据交换,又不够独立以备文件归档。它的文化根基来自英文互联网,没有一条语法符合中文习惯;它的设计根基源于一个简单的小工具,无法为多样的用例作准备;它的技术根基在Web平台,丝毫没有可移植性。

所以,如果没有人强迫你用Markdown,请停止无脑选择它,三思而后行。

想要方便用户编写,就老老实实部署所见即所得编辑器。想要方便程序识别,就用JSON而非HTML描述文章结构。想要安全高效得兼,就回退到纯文本。想要Web平台上的表达能力,就用HTML和组件框架。想要印刷品的表达能力,就用TeX或Typst。念念不忘Markdown,就溯回它的本源,把它作为HTML的缩写来用,就像Emmet那样,缩写只存在于输入期间。坚持使用Markdown,就要做好接受难以表达、处理、传输、归档、迁移、出版的准备。

编写本文时的参考资料

通过搜索“markdown is bad”找到。