會寫 Markdown 的人很多,但寫得好 Markdown 的人卻很少。是否有什麼工具能充當「秘書」,檢查文件中的 Markdown 語法和風格,並且提出解決方案、自動修復問題,甚至自動補齊中英文之間的「盤古之白」呢?本文介紹的 Markdown 語法檢查器就能做到。
引言#
會寫 Markdown 的人很多,但寫得好 Markdown 的人卻很少。這一方面是 Markdown 生態系統自身的問題:語法變種和實現方式 五花八門,互不兼容甚至相互矛盾。
另一方面,也鮮有人願意花時間去仔細閱讀 Markdown 的技術規範;大多數人都只是讀了一兩篇「速成」,就自我批准出師了,對於一些細節問題並未關注;如果在寫作中遇到,也是憑想像和直覺隨意判斷。
由此,就產生了大量語法天馬行空、版面張牙舞爪,讓讀者和排版軟件都困惑不已的 Markdown 文件。
既然 JavaScript 有 ESLint,Python 有 PyLint,是不是 Markdown 也有 markdownlint 呢?答案是肯定的!
示例#
本博客源碼已引入 markdownlint 規範,可下載本博客源碼查看配置。
{{< link href="http://github.com/Lruihao/hugo-blog" content="Lruihao/hugo-blog" card=true >}}
引入 markdownlint#
markdownlint 是一個 Markdown 語法檢查工具,它可以檢查 Markdown 文件中的語法錯誤,以及一些不規範的寫法,讓 Markdown 幹淨又衛生。
markdownlint 有兩個版本,分別是 Mark Harrison 基於 Ruby 的 原版 和 David Anson 基於 Node.js 的 移植版。Node.js 版在人氣和活躍程度上後來居上,本文也以 Node.js 版為例。
markdownlint 可以在多個場景下使用,包括:
本文主要的目的是介紹 markdownlint-cli2 的使用,因為它可以在項目中集成,方便團隊協作。
markdownlint cli 歷史#
根據 David 的博客1,在大約 2015 年左右 Igor Shubovych 和他探討了開發 CLI 工具的想法,當時,David 還沒做好準備,所以 Igor 獨自開發了 markdownlint-cli 這個 CLI 工具。
經過兩年的發展,越來越多的人開始使用 markdownlint-cli,於是 David 開始給 markdownlint-cli 項目貢獻代碼,添加新功能,並在之後三年裡成為了主要的維護人員。直到 2020 年,David 覺得在別人的項目中,很難改變一些事情(可能涉及向後兼容性的问题),因此他重新建立了一個名叫 markdownlint-cli2 的項目,在 markdownlint-cli 的基礎上進行了改進,使其具有更快的執行速度、更靈活的配置和更少的依賴等優點。
目前,這兩個工具仍然隨著 markdownlint 的更新而更新。如果已經在使用 markdownlint-cli 的舊項目,可以繼續使用它,以避免出現未知的問題。而對於新引入的項目,可以考慮使用更強大的 markdownlint-cli2。
安裝 markdownlint-cli2#
npm install markdownlint-cli2 --save-dev
配置快捷命令:
{
"scripts": {
"lint:md": "markdownlint-cli2 \"content/**/*.md\"",
"fix:md": "npm run lint:md -- --fix"
}
}
安裝 markdownlint-rule-search-replace
插件2:
npm install markdownlint-rule-search-replace --save-dev
在項目根目錄下創建 .markdownlint.jsonc
文件,配置規則:
// This file defines our configuration for Markdownlint. See
// https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md
// for more details on each rule.
{
"default": true,
"ul-style": {
"style": "dash"
},
"ul-indent": {
"indent": 2
},
"no-hard-tabs": {
"spaces_per_tab": 2
},
"line-length": false,
"no-duplicate-header": {
"allow_different_nesting": true
},
"single-title": {
"front_matter_title": "^\\s*title\\s*[:=]"
},
"no-trailing-punctuation": {
"punctuation": ".,;:"
},
// Consecutive Notes/Callouts currently don't conform with this rule
"no-blanks-blockquote": false,
// Force ordered numbering to catch accidental list ending from indenting
"ol-prefix": {
"style": "ordered"
},
"no-inline-html": {
"allowed_elements": [
"br",
"code",
"details",
"div",
"img",
"kbd",
"p",
"pre",
"sub",
"summary",
"sup",
"table",
"tbody",
"td",
"tfoot",
"th",
"thead",
"tr",
"ul",
"ol",
"var",
"ruby",
"rp",
"rt",
"i"
]
},
"no-bare-urls": false,
// Produces too many false positives
"fenced-code-language": false,
"code-block-style": {
"style": "fenced"
},
"no-space-in-code": false,
"emphasis-style": {
"style": "underscore"
},
"strong-style": {
"style": "asterisk"
},
// https://github.com/OnkarRuikar/markdownlint-rule-search-replace
"search-replace": {
"rules": [
{
"name": "nbsp",
"message": "Don't use no-break spaces",
"searchPattern": "/ /g",
"replace": " ",
"searchScope": "all"
},
{
// zh-cn/zh-tw prefers double em-dash instead
"name": "em-dash",
"message": "Don't use '--'. Use em-dash (—) instead",
"search": " -- ",
"replace": " — ",
"searchScope": "text"
},
{
"name": "trailing-spaces",
"message": "Avoid trailing spaces",
"searchPattern": "/ +$/gm",
"replace": "",
"searchScope": "all"
},
{
"name": "double-spaces",
"message": "Avoid double spaces",
"searchPattern": "/([^\\s>]) ([^\\s|])/g",
"replace": "$1 $2",
"searchScope": "text"
},
{
"name": "stuck-definition",
"message": "Character is stuck to definition description marker",
"searchPattern": "/- :(\\w)/g",
"replace": "- : $1",
"searchScope": "text"
},
{
"name": "localhost-links",
"message": "Don't use localhost for links",
"searchPattern": "/\\]\\(https?:\\/\\/localhost:\\d+\\//g",
"replace": "](/",
"searchScope": "text"
},
// zh-cn prefers rules
{
"name": "double-em-dash",
"message": "Don't use '--'. Use double em-dash (——) instead",
"search": " -- ",
"replace": "——",
"searchScope": "text"
},
{
"name": "force-pronoun",
"message": "Consider using '你' instead of '您'",
"searchPattern": "/您/g",
"searchScope": "text"
}
]
}
}
在項目根目錄下再創建 .markdownlint-cli2.jsonc
文件,配置規則:
{
"config": {
"extends": "./.markdownlint.jsonc"
},
"customRules": ["markdownlint-rule-search-replace"],
"ignores": [
"node_modules",
".git",
".github",
"**/conflicting/**",
"**/orphaned/**"
]
}
安裝 lint-staged#
npm install lint-staged --save-dev
配置 .lintstagedrc.json
:
{
"content/**/*.md": "markdownlint-cli2 --fix"
}
安裝 husky#
npx husky-init && npm install
配置 .husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
這樣每次提交代碼時,就會自動檢查並修復 content 目錄下的所有 markdown 文件中的語法錯誤。
引入 AutoCorrect#
盤古之白#
在很多中文社區,中英文之間要手動加空格,俗稱「盤古之白」,都是不成文的風格要求。這項要求是否合理、又該如何滿足,是很有價值的話題,但超出了本文的討論範圍3。
這裡,只簡單概括通說:中英文之間加入空隙,是為了實現視覺上的區隔,更加美觀和易讀。理想情況下,這種「空隙」應當由排版引擎自動加入,寬度宜為 1/4 個全角空格(em)。但由於數字排版環境複雜多變,在大多數時候(包括最常見的網頁環境)不能指望排版引擎有這種能力,因此只能退而求其次,手動插入一個半角空格(因其寬度通常接近於 1/4 em),達到類似效果。
如果想要在中英文之間手動加空格,有什麼自動檢查和補全的方法嗎?
答案是當然有,而且選擇也不止一個。
pangu.js#
其中,最著名的可能是 pangu.js 項目。如果你用過一個叫做「為什麼你們就是不能加個空格呢?」的瀏覽器插件,那你也就用過 pangu.js —— 它正是出自同一位作者之手、以 pangu.js 為底層支撐的。Hugo FixIt 主題也內置了 pangu.js 以自動優化博客文章內容中西混排。
AutoCorrect#
另一個選擇是 AutoCorrect。與主要關注文本內容的 pangu.js 相比,AutoCorrect 出生於 Ruby 語言的中文社區,因此從一開始就考慮到了編程代碼中的中英混排場景(可以參見該項目的 測試文件),通用性更強。
pangu.js 和 AutoCorrect 的對比:
項目 | 在線版 | VSCode 擴展 | 命令行工具 |
---|---|---|---|
pangu.js | ❌ | ❌ | ✅ |
AutoCorrect | AutoCorrect Editor | AutoCorrect | ✅ |
- pangu.js 沒有官方 VSCode 插件,使用較多的是 xlthu 開發的 Pangu-Markdown 第三方移植版
- pangu.js 的命令行工具受限於 Node.js,需要通過 npm 安裝:
npm i pangu
- AutoCorrect 的命令行工具則可獨立安裝,同時也有 Rust、Node.js 等更多語言版本
我在博客、VSCode、瀏覽器插件中都使用了 pangu.js,長期以來,就會發現很多問題,它的便捷同時也帶來了 “暴力”,處理規則不可控,這一直讓我很頭疼,所以本文嘗試使用 AutoCorrect 替代 pangu.js。事實上,AutoCorrect 的效果確實更好。
Use AutoCorrect in NPM#
安裝 autocorrect-node
:
npm install autocorrect-node --save-dev
修改快捷命令:
{
"scripts": {
"fix:md": "autocorrect content --fix && markdownlint-cli2 \"content/**/*.md\" --fix",
"lint:md": "autocorrect content --lint && markdownlint-cli2 \"content/**/*.md\""
}
}
修改 .lintstagedrc.json
:
{
"content/**/*.md": [
"autocorrect --fix",
"markdownlint-cli2 --fix"
]
}
新增 .autocorrectignore
:
# AutoCorrect Link ignore rules.
# https://github.com/huacnlee/autocorrect
#
# Like `.gitignore`, this file to tell AutoCorrect which files need to check, some need to ignore.
node_modules/
build/
public/
resources/
執行 npx autocorrect init
拉取默認 .autocorrectrc
配置,然後添加一條規則:
textRules:
# sorted by `LC_ALL=C sort` command
一二三,四五六.七八九: 0
總結#
本文主要介紹了 markdownlint-cli2 和 AutoCorrect 兩個工具,前者用於檢查 Markdown 語法和風格,後者用於自動補齊中英文之間的「盤古之白」。這兩個工具都可以在項目中集成,方便統一規範、團隊協作。
Footnotes#
-
If one is good, two must be better [markdownlint-cli2 is a new kind of command-line interface for markdownlint] ↩
-
markdownlint-rule-search-replace 用於搜索和替換模式的自定義 markdownlint 規則 ↩
-
如果有進一步興趣,請閱讀知乎討論「中英文混排時中文與英文之間是否要有空格?」,W3C 標準草案《中文排版需求》§3.2.2,以及收聽《字談字暢》播客 第 14 期。 ↩