markdownlint 的 MD046(code-block-style):用代码块风格一致性检查打造规范文档

原创2026-10-14 19:58:10641 阅读
文章标签:开发工具Lint静态分析代码质量

markdownlint 的 MD046(code-block-style):用代码块风格一致性检查打造规范文档

导读

MD046 是 markdownlint 内置规则之一,用于强制同一篇 Markdown 文档中的代码块采用一致的书写风格(缩进式 indented 或围栏式 fenced),避免缩进代码块与反引号/波浪号围栏混用导致的排版混乱。本文以仓库测试用例 test/code_block_consistency.md 为起点,结合官方规则文档 doc/md046.md 与规则源码 lib/md046.mjs,完整讲解该规则的触发条件、三种 style 参数语义、JSON Schema 约束与配置写法,并通过快照测试验证其真实报错行为。读完本文,你将能在自己的 markdownlint 配置中精准控制代码块风格检查,并理解其底层 micromark 解析实现。

规则总览:MD046 检查什么

根据规则文档 doc/md046.md,MD046 的基本信息如下:

  • 规则编号:MD046
  • 别名(Aliases):code-block-style
  • 标签(Tags):code
  • 参数(Parameters):style(string,默认值 consistent,可选值 consistent / fenced / indented)

规则说明明确指出:

This rule is triggered when unwanted or different code block styles are used in the same document.

即:当同一文档中使用了“不符合预期”或“风格彼此不同”的代码块时,该规则报告违规。文档给出的修复思路也很直接——统一使用一种风格(要么全部缩进,要么全部围栏),因为一致的格式化能让文档更易理解(Rationale: Consistent formatting makes it easier to understand a document)。同一内容也收录在汇总文档 doc/Rules.md 的 MD046 小节中。

从一个测试用例讲起:code_block_consistency.md 全解析

仓库测试目录下的 test/code_block_consistency.md 是一个极简但信息量完整的“违约样例”,它模拟了**默认配置(style: consistent)**下两种代码块混用的场景:

# code_block_consistency

This is text.

    This is a
    code block.

And here is more text

```text
and here is a different {MD046:10}
code block

逐行拆解这个文件:

- 第 1 行是标题 `# code_block_consistency`;
- 第 5~6 行是**缩进式代码块**:每行以 4 个空格开头(CommonMark 的缩进代码块规则),内容为 `This is a` 与 `code block.`;
- 第 10~12 行是**围栏式代码块**:以三个反引号 `` ``` `` 开合,语言标注为 `text`;
- 第 11 行行内嵌入了特殊标记 `{MD046:10}`,这是 markdownlint 测试体系使用的“预期报错标记”(marker)语法:`{MD数字}` 表示期望该规则报错,`{MD046:10}` 表示期望 MD046 在**第 10 行**报错(`:` 后的数字可以带 `+`/`-` 前缀表示相对偏移,见下文测试机制)。

由于默认配置 `style` 为 `consistent`(与第一个代码块保持一致),规则取**文档中第一个代码块**(第 5 行的缩进式)作为基准风格 `indented`,随后在第 10 行发现围栏式代码块与之不一致,于是报告违规。

### 快照测试验证的精确报错输出

该用例由 <a href="https://link.gitcode.com/i/f928283b2db5bd0ab4725b0343a30bfc" target="_blank">test/markdownlint-test-scenarios.mjs</a> 驱动(它会扫描 `./test` 下所有 `.md` 文件并用默认配置执行 `lint`),其期望结果记录在快照文件 <a href="https://link.gitcode.com/i/07d6a0c1f569249ef6e24946ee1fae76" target="_blank">test/markdownlint-test-scenarios.mjs.snapshot</a> 中(`code_block_consistency.md 1` 条目),字段完整地还原了真实报错:

```json
{
  "lineNumber": 10,
  "ruleNames": [ "MD046", "code-block-style" ],
  "ruleDescription": "Code block style",
  "errorDetail": "Expected: indented; Actual: fenced",
  "errorContext": null,
  "errorRange": null,
  "fixInfo": null,
  "severity": "error"
}

几个值得注意的细节:

  • errorDetail 给出了与源码实现一致的 "Expected: indented; Actual: fenced" 文本,即期望缩进式、实际是围栏式;
  • fixInfo 为 null,说明 MD046 目前不是可自动修复(fixable)的规则——测试框架在 test/markdownlint-test-scenarios.mjs 中会校验“可修复规则必须被登记”,而 MD046 不在 lib/constants.mjs 的 fixableRuleNames 列表中,因此不会生成修复建议,只能人工调整代码块风格;
  • severity 为 error,符合 Schema 中该规则的默认严重级别。

三种 style 语义:consistent / fenced / indented

style 参数决定规则以哪种风格为“基准”,语义区分如下(依据 doc/md046.md 的说明):

取值 含义 报错时机
consistent(默认) 全部代码块必须与文档中第一个代码块风格一致 后续出现与首个代码块风格不同的代码块时
fenced 强制全部使用围栏式(``` 或 ~~~) 出现缩进式代码块时
indented 强制全部使用缩进式(4 空格缩进) 出现围栏式代码块时

官方文档 doc/md046.md 中的违约示例(默认配置)正体现了“混用即违规”:文档先是缩进式文本块,随后出现 4 空格缩进的 # Indented code,接着又出现围栏式 ruby 代码块——两种风格并存,因此默认的 consistent 模式必然触发违规。

仓库自身的配置佐证

项目自用的 .markdownlint.json 是一个真实的应用范例——它把 code-block-style 显式配置为 fenced:

{
  "code-block-style": {
    "style": "fenced"
  }
}

即:markdownlint 仓库要求自身所有文档中的代码块统一使用围栏式,这也与同一份配置里 code-fence-style(反引号)、fenced-code-language(语言标注白名单)等规则相互呼应,构成一套完整的代码块规范体系。

源码实现原理:lib/md046.mjs 逐段解读

规则的完整实现位于 lib/md046.mjs,全文不过 30 行,却清晰展示了 markdownlint 规则的标准写法:

import { addErrorDetailIf } from "../helpers/helpers.cjs";
import { filterByTypesCached } from "./cache.mjs";

const tokenTypeToStyle = (tokenType) => (tokenType === "codeFenced") ? "fenced" : "indented";

export default {
  "names": [ "MD046", "code-block-style" ],
  "description": "Code block style",
  "tags": [ "code" ],
  "parser": "micromark",
  "function": function MD046(params, onError) {
    let expectedStyle = String(params.config.style || "consistent");
    for (const token of filterByTypesCached([ "codeFenced", "codeIndented" ])) {
      const { startLine, type } = token;
      if (expectedStyle === "consistent") {
        expectedStyle = tokenTypeToStyle(type);
      }
      addErrorDetailIf(
        onError,
        startLine,
        expectedStyle,
        tokenTypeToStyle(type)
      );
    }
  }
};

实现要点可以拆成四步:

  1. 声明规则元信息:names 数组同时注册 MD046 与别名 code-block-style;description、tags 与文档一致;parser: "micromark" 声明该规则基于 micromark 解析器输出运行。
  2. 读取配置:params.config.style || "consistent" 负责读取 style 参数,缺省时回退到默认值 consistent。
  3. 筛选代码块 token:filterByTypesCached(<a href="https://link.gitcode.com/i/a76344fb657b7ec05302bf52643c26d1" target="_blank"> "codeFenced", "codeIndented" ]) 从 micromark 的 token 流中一次性取出所有围栏式与缩进式代码块。该函数来自 [lib/cache.mjs 的缓存层——同名参数组合的筛选结果会被缓存,避免同一文档内多条规则重复解析 token 流造成性能浪费。
  4. 风格归一与比较:辅助函数 tokenTypeToStyle 把 codeFenced 映射为 fenced,其余(即 codeIndented)映射为 indented。若 expectedStyle 为 consistent,则用遇到的第一个代码块的风格初始化基准;之后每个 token 都通过 helpers/helpers.cjs 中的 addErrorDetailIf 与基准比对——二者不同即在 startLine(代码块起始行)上报 "Expected: X; Actual: Y"。

其中 addErrorDetailIf 的报错文案格式("Expected: " + expected + "; Actual: " + actual)正对应前文快照中 Expected: indented; Actual: fenced 的输出,闭环印证了“文档 → 源码 → 快照”三者的一致性。

配置方式与 JSON Schema 约束

配置文件写法

style 参数可在各类配置文件中设置,仓库自身即使用 .markdownlint.json。同时,markdownlint 支持在文档内用 HTML 注释做局部配置(inline configuration),测试用例中就有两处极佳的示范:

  • test/code_block_fenced.md 在文件末尾通过 <!-- markdownlint-configure-file { "code-block-style": { "style": "fenced" } } --> 将整篇文档的代码块风格锁定为围栏式,于是文中的两处缩进代码块分别在第 5、16 行被标记 {MD046} 报错,而围栏代码块不触发;
  • test/code_block_indented.md 反向操作,通过同样语法把 style 设为 indented,此时只有第 10 行的围栏式代码块被标记 {MD046:10}。

这两个用例与 code_block_consistency.md 形成完整对照,覆盖了 style 三种取值下的全部行为。

Schema 层面的约束

test/markdownlint-config-schema.json 对 MD046(以及别名 code-block-style)定义了严格的 JSON Schema(strict 版本见 test/markdownlint-config-schema-strict.json),允许的三种写法是:

  1. 布尔值:"code-block-style": true(启用)或 false(停用);
  2. 字符串严重级别:"code-block-style": "error" 或 "warning";
  3. 对象形式,其中 additionalProperties: false(不允许出现未声明的键),可配置属性为:
    • enabled:boolean,是否启用,默认 true;
    • severity:"error" / "warning",默认 "error";
    • style:"consistent" / "fenced" / "indented",默认 "consistent"。

这意味着配置文件里写错键名(如 stlye)会被 Schema 校验直接拒绝,能有效避免手滑造成的配置失效。

测试机制:{MD046:10} 标记如何被验证

想确认规则行为是否正确,可以看测试驱动脚本 test/markdownlint-test-scenarios.mjs 的工作流程:

  1. 读取 ./test 下所有 .md 文件原文,并用 markdownlint/promise 的 lint 执行默认配置检查;
  2. 用正则 /\{(MD\d+)(?::([-+]?)(\d+))\}/g 解析文档内的 {MD046:10} 这类标记:: 后带 +/- 前缀的数字表示相对行偏移,否则是绝对行号;
  3. 把标记推导出的“期望违规位置”与 lint 实际结果 deepEqual 对比,多余的或缺失的报错都会导致断言失败(错误信息为 "Too few or too many issues found.");
  4. 对结果执行 applyFixes 生成修复后文本,与快照比对,并确认“带修复信息的错误”都已修复。

因此,在测试文件中看到 {MD046:10} 就等同于“此处必须且只能由 MD046 报一次错”。这种“文档内嵌期望、快照固化结果”的做法让每个测试用例文件既是可读样例又是自动化断言,值得在写 lint 工具时借鉴。

实战建议:如何选择与排查

结合上述分析,给出几条可落地的实践建议:

  • 新文档推荐 fenced:围栏式代码块支持语言标注(如 ```javascript),可配合 fenced-code-language、code-fence-style 等规则细化约束,利于语法高亮与文档可读性;markdownlint 自身 .markdownlint.json 即采用此风格。
  • 旧文档优先 consistent:若存量文档以缩进式为主,保留默认 consistent 可自动以“首个代码块”为基准,无需改动历史内容,仅在混用处报错。
  • 遇到 MD046 报错时的修复路径:因为该规则不可自动修复(fixInfo: null),需人工将违规代码块改写成基准风格——缩进式代码块改为围栏式,或反之;若文档中存在 HTML、表格等需要缩进语义的结构,要特别注意改写后可能触发的其他规则。
  • 善用局部配置:对个别特殊文档(如示例片段较多的教程),可用 <!-- markdownlint-configure-file { "code-block-style": { "style": "indented" } } --> 做局部覆盖,避免为整仓配置让步。

小结

MD046(code-block-style)通过 micromark token 流识别 codeFenced 与 codeIndented 两类代码块,以 consistent / fenced / indented 三种模式统一文档内代码块风格:consistent 跟随首块、fenced 强制围栏、indented 强制缩进。从 test/code_block_consistency.md 测试用例到 lib/md046.mjs 实现,再到 Schema 与快照验证,整个链路证明了该规则“文档、代码、测试”三方一致的工程严谨性。掌握它的配置与行为,你就能让团队文档中的代码块风格始终如一。

登录后查看全文
markdownlint