geekswg

毕少侠

毕少侠也在江湖
github

給你的 Markdown 挑挑刺

會寫 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
AutoCorrectAutoCorrect EditorAutoCorrect
  • 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#

  1. If one is good, two must be better [markdownlint-cli2 is a new kind of command-line interface for markdownlint]

  2. markdownlint-rule-search-replace 用於搜索和替換模式的自定義 markdownlint 規則

  3. 如果有進一步興趣,請閱讀知乎討論「中英文混排時中文與英文之間是否要有空格?」,W3C 標準草案《中文排版需求》§3.2.2,以及收聽《字談字暢》播客 第 14 期

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。