给你的 Markdown 挑挑刺——语法检查器入门与进阶

2022-03-25

Note: This article was published as a member-exclusive post for SSPAI Prime, which is made freely available as a preview and republished here. Please consider subscribing to SSPAI Prime if you find content like this helpful. Thanks for your support.

This article is also another installment in my (informal and implied) series of “nitpicking workflows for writing.” The previous posts in this bucket were concerning misuses and abuses of punctuation marks (2018), hyperlinks (2020), and formatting (2021). I humbly offer these as my way of conducting public service.

引言

Markdown 标记语言 是如今互联网上写作的主流格式,被各类发布平台和创作工具广泛支持。哪怕不熟悉这一语言的用户,也很可能在潜移默化中对 #(标题)、**(加粗)、-(列表)这些 Markdown 风格的标注方式建立起认知。

然而,会写 Markdown 的人很多,但写得好 Markdown 的人却很少。这一方面是 Markdown 生态系统自身的问题:语法变种和实现方式 五花八门,互不兼容甚至相互矛盾。

但另一方面,也鲜有人愿意花时间去仔细阅读 Markdown 的技术规范;大多数人都只是读了一两篇「速成」,就自我批准出师了,对于一些细节问题并未关注;如果在写作中遇到,也是凭想象和直觉随意判断。

由此,就产生了大量语法天马行空、版面张牙舞爪,让读者和排版软件都困惑不已的 Markdown 文件。

当然,每个人精力有限,强求所有 Markdown 用户都去研读标准是不切实际的。那么,有没有什么工具能充当「秘书」,检查文件中的 Markdown 语法和风格,并且提出解决方案、自动修复问题呢?

有编程经验的读者看到这里,或许已经联想到写代码时常用的一种辅助工具——语法检查器(linter)。没听说过也没关系:这个词来自于「lint」,本意是指「绒毛」——是的,就是毛衣上、口袋缝里那些讨厌的毛球。这些毛球既影响美观,又容易堵塞洗衣机;引申用来形容代码里的错误语法、混乱版式等问题,可谓形象。

注: Linter 一词没有完全对应的中文翻译,实践中译为「校验器」或「检查器」的都有,但都无法充分表达 linter 既检查语法问题、又检查版式(风格)等「审美」问题的综合性,也常与另一种只检查是否合乎技术标准的工具 validator 混淆;本文姑且用「检查器」来称呼。)

这跟 Markdown 有什么关系呢?实际上,Markdown 属于一种「标记语言」(markup language),虽然与 Python、JavaScript 这类「编程语言」服务于截然不同的目的,但同样都是通过各种记号(token)来标记文件、表明各部分的不同作用(例如 Python 中的定义、循环控制;Markdown 中的被强调文本、引用段落)。文本编辑器在处理时,也是通过识别这些记号进行语法分析(lexical analysis),进而提供关键词高亮、提取大纲、渲染预览等语法功能。

换言之,在编辑器眼中,Markdown 只不过是另一种「语言」罢了。既然如此,如果能用 ESLint 检查 JavaScript,用 PyLint 检查 Python,当然也可以造出一个 Markdown 语法检查器,专门给 Markdown 文件「挑刺」。

下面,本文就将依次介绍两种 Markdown 格式检查器:一种通用于各种 Markdown 文档,主要用来检查和修复文件中 Markdown 格式标记本身的规范性;另一种则专用于中文 Markdown 文档,用来满足一种历史悠久的「强迫症」——中英文之间要加空格。

结构上,本文会考虑不同层次的需求:如果你是 Markdown 初学者,不妨先学习这些检查器的规则文档,了解 Markdown 使用中的常见错误,然后尝试使用所介绍的工具和插件,优化自己的文档。如果你是高级用户,则不妨尝试后续段落介绍的配置选项和命令行工具,定制出反映自己偏好和思考的自动化流程。

让 Markdown「干净又卫生」——markdownlint

正如主流编程语言都有不止一种检查器,Markdown 的检查器也有很多竞品,但其中使用者最多、维护最为活跃的要数 markdownlint;这也是 WordPressMDN 等很多知名开源项目的选择。

markdownlint 有两个版本,分别是 Mark Harrison 基于 Ruby 的 原版 和 David Anson 基于 Node.js 的 移植版。由于比较方便部署(例如下文介绍的网页版和 VSCode 插件),Node.js 版在人气和活跃程度上后来居上,本文也优先介绍该版本。

截至本文写作时,markdownlint(Node.js 版)总共有 50 条规则,编号从 MD001 到 MD050,其中有两条已被废弃;Ruby 版则有 39 条规则。两个版本的规则编号是兼容的,相同编号的规则内容也相同。

这些规则各有分工,有的规范 Markdown 中的空格与留白,有的规范链接的使用,有的则规范标题、列表等具体样式(可阅读文档了解完整的 分类标签)。

具体到内容上,有的规则显得很像「废话」,但确实容易在匆忙中忽略,例如:

  • MD001:标题层级一次只应增加一个级别(例如,一级标题后面不能直接来一个三级标题);
  • MD025:一个文档中只应有一个一级标题(对于一些内容管理系统,也包括 YAML frontmatter 中的 title 属性);
  • MD029:数字列表的前缀应为递增的数字(或全部写成 1)。

有的并不影响含义和最终渲染结果,只是为了文件整洁,满足「强迫症」,例如:

  • MD005:同一级别的列表,开头缩进的空格数量应保持一致;
  • MD012:不应连续出现一个以上的空行;
  • MD037:强调标记(*_)内侧不应紧邻空格。

还有的则见仁见智,更多是审美和「哲学」上的取舍,例如:

  • MD026:标题段落不应以标点符号结尾(理由是「标题不是完整的句子」);
  • MD036:不应为整行文字加粗或斜体(理由是「这种情况请设为标题」);
  • MD045:图片应有题注(理由是「方便视觉不便者和读屏器」)。

篇幅所限,这里不一一列举。markdownlint 的 规则文档 非常完整,对于每条规则都给出了描述、实例和说理,本文强烈建议花些时间逐条阅读。这既有助于自己写出更规范的 Markdown 文件,也可以在理解和思考的基础上做出取舍,为后文介绍的自定义配置做准备。

网页版

对于初识 markdownlint 的用户来说,最直观、最方便的体验方式,就是使用原作者提供的 网页版演示工具

打开该工具后,点击右上角的「Browse…」打开任意一个 Markdown 格式文件(你可以下载本文 演示用的文件),或者手动将 Markdown 内容粘贴到左上角的文本框中。

注: 这个工具在加载后就只在本地运行,文本内容不会被上传,因此可以放心使用。)

这时,右下角的文本框就会以

<行数> - <规则名称> <规则内容> [ 出错原因 ]

的格式逐条列举输入文件存在的问题。

例如,在上图的例子中,第一条问题点:

9 - MD004 / ul-style Unordered list style [Expected: asterisk; Actual: dash]

是指文件第 9 行违反了 MD004(无序列表样式)。

根据这一规则,同一个 Markdown 文件中的无序列表标记应当保持一致,不能混用。这里,文件中第一个无序列表项(第 8 行)是用星号开头的;按照 MD004,后续的列表项也应当都用星号开头(Expected: asterisk),而第 9 行却使用了横线开头(Actual: dash),因此检查器报错。

对此,只需要点击这条错误信息末尾的「Fix」字样,markdownlint 就会将开头的横线改为星号,从而满足 MD004 的要求。

用同样的方法审阅其他错误信息,视情况决定是否接受即可。

在 VSCode 中使用

安装和基本使用

网页版工具虽然方便,但也只适合演示和临时使用。如果要将 markdownlint 融入写作流程,还需要更为方便的调用方式。一种理想的选择,就是将其安装为编辑器扩展,边写文章、边检查错误。

本文以目前流行的 Visual Studio Code(VSCode)为例演示。

如开头所铺垫,作为一款代码编辑器,VSCode 是将 Markdown 作为一种「语言」提供支持的。此外,VSCode 也具有任何成熟代码编辑器必备的**「快速修复」(Quick Fix)**功能:检测和提示代码中的问题,并提供修改建议和「一键修复」的快捷方式。

而 markdownlint 插件的作用,正是将 Quick Fix 的支持范围延伸到 Markdown,让 VSCode「学会」这门语言边边拐拐的细节。

markdownlint 插件用起来非常简单。首先前往 VSCode 市场页面,点击「Install」按钮,跳转到 VSCode 完成安装。

还是以上文提供的测试文件为例,安装插件后再打开这个文件,就会看到界面上多出了很多波浪线,每处波浪线都对应一处发现的错误:

markdownlint 的错误检查是实时的;标记和建议会随着输入和编辑而不断刷新。

对于这些错误,可以进行的操作包括:

  • 将鼠标移动到画线位置,将会弹出一个浮窗,其中说明了该处存在错误的原因——与上文在网页版中看到的提示完全一致。
  • 点击「快速修复」(或按 Command-. 快捷键),就可以调用 markdownlint 引擎自动改正这条错误。
  • Shift-Command-M 快捷键弹出「问题」面板,查看所有问题的列表,点击其中的条目可以跳转定位。
  • Shift-Command-P 快捷键弹出命令面板,输入「format」,选择「格式化文档」,修复文件中的所有错误(初次运行可能需要先选择 markdownlint 作为默认引擎)。

自定义配置

如上文所说,markdownlint 的规则并不是什么「神谕」或「铁律」,至多只是该项目作者基于较多经验和观察的「一家之言」,不可能适合每个人的口味、与每个使用场景兼容。因此,根据自己的偏好和需求予以调整是非常必要的。

对此,好消息是,markdownlint 提供了充分的调整空间,每一条规则都可以随意开关,其中很多还提供了细粒度的专用选项。坏消息是,作者似乎默认用户都是谙熟纯文本配置的高手,没有像大多数插件那样提供 UI 选项,所有设置都只能通过手写配置文件调整;下文将尽量用简单易理解的方式介绍其方法。

首先,按 Shift-Command-P 打开命令面板,输入「settings」,选择「首选项:打开设置(JSON)」,打开 VSCode 的配置文件。

然后,在这个文件中加入一个名为 markdownlint 的对象(位置任意,但可以与其他插件的配置写在一起方便管理)。形如:

{
  // 其他插件设置
  "markdownlint" : {
    "config" : {
      "<规则编号>" : { <配置内容> },
      // 更多配置
    },
    "focusMode" : true
  },
  // 其他插件设置
}

其中,config 子对象即用来添加针对各个规则的配置。不同规则有不同的设置方法,具体需要查阅规则文档来确定:

  • 有的规则只有简单的「开」或「关」两种状态。

    例如,MD001 要求标题一次增加一级,文档 也没有为其列举更多参数(parameter)。如果你不喜欢这个规则,那么可以通过添加 { "MD001" : false } 将其直接关闭。

  • 有的规则提供更细致的选项。

    例如,MD007 要求无序列表开头缩进的空格数量保持一致,并且默认为每 2 个空格为一级。但实践中很多人习惯以 4 个空格为单位来缩进。根据 文档,该规则提供一个名为 indent 的整数型参数,默认值为 2(number; default 2)。据此,在配置中添加 { "MD007" : { "indent" : 4 } } 即可将缩紧步进改为 4。

    又如,MD024 禁止文中出现两个内容相同的标题。根据 文档,该规则有一个名为 siblings_only 的布尔型参数。据此,如果添加({ "MD024" : { "siblings_only" : true } }),那么这条规则只会检查紧邻的标题是否重复。或者,也可以彻底禁用这条规则({ "MD024" : false })。

再注意到,上面的例子中,与 config 对象平行,还有一个 focusMode。这是我认为非常必要的一条配置:写作过程中,一行内容在完成输入之前,不符合 Markdown 语法是很正常的。 按照默认设置,这些还没写完的内容都会被当作「有问题」,打上波浪线。这显然是多此一举,也会造成视觉干扰。

focusMode 设为 true 后,插件就不会检查光标所在的当前行,回避了上述问题。此外,也可以将该键的值填为具体的数字,从而将忽略范围扩展到当前行上下的相应行数,进一步减少干扰。

最后,该插件还可以设置 仅在存盘时检查根据路径忽略特定文件 等,限于篇幅在此不赘,有兴趣的读者可以进一步阅读 文档

在命令行中使用

安装与基本使用

尽管 VSCode 插件已经可以满足大部分人的需求,但专业用户可能还希望有一个命令行界面,以实现批量化、自动化的处理。

markdownlint 确实提供了命令行版本。但比较幽默的是,它不仅有命令行工具,而且有两个命令行工具,第一个叫做 markdownlint-cli,第二个……当然就叫做 markdownlint-cli2。这两者的 开发背景和区别 过于琐碎,就本文目的,读者只需知道 markdownlint-cli2 相对较快,并且和 VSCode 插件版共用一套配置语法,结合使用比较方便;本文也将以其为例演示。

下面的操作假定读者熟悉终端的基本知识和使用方法,并且已经 安装好 Node.js

首先,通过 npm 安装 markdownlint-cli2

npm i -g markdownlint-cli2

markdownlint-cli2 的使用方式主要有两种:

markdownlint-cli2 [文件路径]     // 检查文件并报告问题
markdownlint-cli2-fix [文件路径] // 检查并直接修复文件

其中,文件路径可以是一个或多个实际的文件名,也可以是通配符。例如:

markdownlint-cli2 "**/*.md"

是指检查当前目录以及任意层级子目录中,所有以 md 为后缀名的文件。

自定义配置

markdownlint-cli2 与 VSCode 插件使用完全相同的配置语法。因此,在根据自己习惯配置好 VSCode 插件后,只要将其中 config 对象的内容复制出来,保存为一个名为 .markdownlint.json 的文件(也可以用一些其他的文件名和格式,详见 文档),与待检查的 Markdown 文件放在同一文件夹(如果要检查多个文件,则放在其共同的最上级文件夹),markdownlint-cli2 就会自动读取并执行。

在下图例子中,由于通过配置文件改变了部分规则,下方的终端的输出也少了相应条目:

值得一提的是,如果某个文件夹中存放了上述配置文件,而其配置内容与 VSCode 插件的设置不同,那么在编辑这个文件夹内的 Markdown 文件时,VSCode 也会优先使用这个外部配置文件的设置。这特别适合为不同项目微调配置。

最后,如果你有固定的设置想要搭配 markdownlint-cli2 使用,又不想每次都创建一个配置文件,也可以用:

markdownlint-cli2-config [配置文件路径] [Markdown 文件路径]

的语法来手动指定配置文件路径。

保持中英文之间的「盘古之白」

在很多中文社区,中英文之间要手动加空格——俗称「盘古之白」——都是不成文的风格要求。这项要求是否合理、又该如何满足,是很有价值的话题,但超出了本文的讨论范围。

这里,只简单概括通说:中英文之间加入空隙,是为了实现视觉上的区隔,更加美观和易读。理想情况下,这种「空隙」应当由排版引擎自动加入,宽度宜为 1/4 个全角空格(em)。但由于数字排版环境复杂多变,在大多数时候(包括最常见的网页环境)不能指望排版引擎有这种能力,因此只能退而求其次,手动插入一个半角空格(因其宽度通常接近于 1/4 em),达到类似效果。

注: 如果有进一步兴趣,请阅读知乎讨论「中英文混排时中文与英文之间是否要有空格?」,W3C 标准草案《中文排版需求》§3.2.2,以及收听《字谈字畅》播客 第 14 期。)

相比之下,本文关心的问题是:如果想要在中英文之间手动加空格,有什么自动检查和补全的方法吗?

答案是当然有,而且选择也不止一个。其中,最著名的可能是 pangu.js 项目。如果你用过一个叫做「为什么你们就是不能加个空格呢?」的浏览器插件,那你也就用过 pangu.js——它正是出自同一位作者之手、以 pangu.js 为底层支撑的。

另一个选择是 AutoCorrect。与主要关注文本内容的 pangu.js 相比,AutoCorrect 出生于 Ruby 语言的中文社区,因此从一开始就考虑到了编程代码中的中英混排场景(可以参见该项目的 测试文件),通用性更强。

不过,两者在实际使用的区别很小,唯一比较明显的差别是 pangu.js 会在链接文本的前后加上空格,而 AutoCorrect 不会;这也只是见仁见智的选择。此外,pangu.js 没有官方的 VSCode 插件,只有几个第三方插件,但已久不更新,命令行工具也依赖于 Node.js,一般用户初次运行起来可能有点繁琐;而 AutoCorrect 则提供了官方 VSCode 插件,命令行版本没有外部依赖,选项也更多。

因此,下文将同时介绍这两个工具,读者按偏好选择其一即可。

网页版

与 markdownlint 类似,AutoCorrect 也提供了一个 在线预览版,既可用来测试该工具的效果,也可用来处理实际文件。

打开该工具后,在左上角的下拉菜单中选择「Markdown」,粘入自己的内容,然后点击下方的「Format」按钮即可看到输出。

pangu.js 没有官方网页版。

在 VSCode 中使用

AutoCorrent 提供了第一方的 VSCode 插件。安装后,建议在「设置」>「扩展」>「AutoCorrect」中按需调整该插件的两个选项:

  • Enable:是否允许实时检查。如果启用,文件里中英文未留空的位置将会被当作「问题」予以提示,并且可以通过 Quick Fix 功能一键添加空格。
  • Format On Save:是否在保存时自动修复中英文之间的空格。

这两个选项都是默认启用的,但我建议考虑关闭。理由也与上面配置 markdownlint 时类似:文档是否「整洁」并没有必要在写作阶段就特意关注,而是修订校对阶段的任务。特别是对于中英文空格这种「小节」,启用实时修复只会让界面充斥着提示问题的波浪线,非常干扰写作。

无论是否启用这些选项,都可以通过命令面板中的「AutoCorrect: Format document」命令来手动为文件添加中英文空格。如果想要更方便,还可以点击该命令右侧的齿轮图标,为其指定一个快捷键,随时满足自己的强迫症。

pangu.js 没有官方 VSCode 插件,但有几个第三方移植版,其中用户较多、效果也不错的是 xlthu 开发的 Pangu-Markdown。安装后,在命令面板选择「Pangu Format」即可检查全文并追加空格。

在命令行中使用

AutoCorrect 提供了为各个平台编译的命令行工具,官方文档提供的安装脚本如下:

curl -sSL https://git.io/JcGER | bash

注: 有义务提醒,curl | sh 风格的安装方式是有 内在风险 的,请务必逐行审阅 curl 下载的 安装脚本内容 再决定要不要运行。)

与 markdownlint 类似,AutoCorrect 也可以选择是只报告问题,还是直接修复文件:

autocorrect [文件路径]       // 检查文件并报告问题
autocorrect --fix [文件路径] // 检查并直接修复文件

更完整的使用方法可以使用 autocorrect --help 查看,或者 阅读在线文档

至于 pangu.js,其命令行工具则需通过 npm 安装:

npm i -g pangu

相比 AutoCorrect,其功能也比较简单,只能将补齐空格后的内容直接显示出来,必须配合 > 重定向才能写入到文件:

pangu -t [文本内容]             // 检查指定文本内容
panfu -f [文件路径]             // 检查指定文件
pangu -f [文件路径] > [写入路径] // 检查指定文件并将结果写入到文件