markdownlint 的 MD046(code-block-style):用代码块风格一致性检查打造规范文档
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)
);
}
}
};
实现要点可以拆成四步:
- 声明规则元信息:
names数组同时注册MD046与别名code-block-style;description、tags与文档一致;parser: "micromark"声明该规则基于 micromark 解析器输出运行。 - 读取配置:
params.config.style || "consistent"负责读取style参数,缺省时回退到默认值consistent。 - 筛选代码块 token:
filterByTypesCached(<a href="https://link.gitcode.com/i/a76344fb657b7ec05302bf52643c26d1" target="_blank"> "codeFenced", "codeIndented" ])从 micromark 的 token 流中一次性取出所有围栏式与缩进式代码块。该函数来自 [lib/cache.mjs 的缓存层——同名参数组合的筛选结果会被缓存,避免同一文档内多条规则重复解析 token 流造成性能浪费。 - 风格归一与比较:辅助函数
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),允许的三种写法是:
- 布尔值:
"code-block-style": true(启用)或false(停用); - 字符串严重级别:
"code-block-style": "error"或"warning"; - 对象形式,其中
additionalProperties: false(不允许出现未声明的键),可配置属性为:enabled:boolean,是否启用,默认true;severity:"error"/"warning",默认"error";style:"consistent"/"fenced"/"indented",默认"consistent"。
这意味着配置文件里写错键名(如 stlye)会被 Schema 校验直接拒绝,能有效避免手滑造成的配置失效。
测试机制:{MD046:10} 标记如何被验证
想确认规则行为是否正确,可以看测试驱动脚本 test/markdownlint-test-scenarios.mjs 的工作流程:
- 读取
./test下所有.md文件原文,并用markdownlint/promise的lint执行默认配置检查; - 用正则
/\{(MD\d+)(?::([-+]?)(\d+))\}/g解析文档内的{MD046:10}这类标记::后带+/-前缀的数字表示相对行偏移,否则是绝对行号; - 把标记推导出的“期望违规位置”与 lint 实际结果
deepEqual对比,多余的或缺失的报错都会导致断言失败(错误信息为 "Too few or too many issues found."); - 对结果执行
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 与快照验证,整个链路证明了该规则“文档、代码、测试”三方一致的工程严谨性。掌握它的配置与行为,你就能让团队文档中的代码块风格始终如一。