- 开发工具
- 代码质量
- Lint
- 静态分析
【免费下载链接】markdownlint
A Node.js style checker and lint tool for Markdown/CommonMark files.
导读
在 Markdown 文档中,标题前后是否留空行不仅影响排版美观,还直接决定kramdown等解析器能否正确识别标题。markdownlint 的 MD022(别名blanks-around-headings)规则默认要求标题上下各留 1 个空行,但通过lines_above与lines_below两个参数,可以按需自定义为空行、任意空行甚至按标题级别分别设值。本文以仓库中的测试用例 test/blanks-around-headings-0-2.md 为骨架,结合 规则官方文档 与 规则源码实现,完整拆解该规则的配置语法、预期行为、自动修复机制与底层实现原理,帮助你掌握在真实项目里精确控制标题空行数量的完整方案。
一、MD022 规则概述:标题必须被空行包围
MD022(别名blanks-around-headings)属于headings与blank_lines标签组,核心约束是:任何样式的标题(ATX 标题#、闭合 ATX 标题# ##、Setext 标题===/---)前后都必须存在至少一个空行。触发场景与修复方式如下(来自 规则文档):
# Heading 1 Some text Some more text ## Heading 2以上文档中# Heading 1与## Heading 2均未满足"前后各有空行"的要求。修复后应为:
# Heading 1 Some text Some more text ## Heading 2该规则是**可自动修复(Fixable)**的,即部分违规可以通过工具自动插入缺失的空行。
1.1 三个配置参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
include_front_matter | boolean | false | 是否将 YAML front matter 内容计入标题上方空行检查 |
lines_above | integer \| integer[] | 1 | 标题上方应存在的空行数量 |
lines_below | integer \| integer[] | 1 | 标题下方应存在的空行数量 |
其中lines_above/lines_below的取值规则为:
0:标题上下方不需要空行;- 正整数 N:至少需要 N 个空行(超出部分忽略,规则只检查"不少于"指定数量);
-1:允许任意数量的空行(包含 0);integer[]数组:按顺序对应标题级别 1~6 分别设值,数组长度可不足 6,未覆盖的级别回落到默认值1(此规则在源码getLinesFunction中以new Array(6).fill(defaultLines)体现,见下文源码分析)。
1.2 与 MD012 的联动
如果lines_above或lines_below被配置为大于 1,则 MD012 / no-multiple-blanks(禁止连续多个空行)也应同时自定义,否则两个规则会互相冲突。此外 MD022 只检查"至少指定数量"的空行,额外的空行不会被报错——这正是测试用例中同时关闭 MD012 的原因。
1.3 规则背后的解析器动机
除美学因素外,kramdown等部分 Markdown 解析器不会解析前面没有空行的标题,而是将其当作普通文本处理。因此该规则本质上是保证标题语义正确性的防御性检查。
二、测试用例本体:blanks-around-headings-0-2.md逐行解析
本文的核心关联文档是 test/blanks-around-headings-0-2.md,它是一份针对 MD022 规则的回归测试样例,专门验证lines_above: 0, lines_below: 2组合下的检查行为。全文如下:
# Blanks Around Headings ## Apple Text ## Banana {MD022} Text ## Cherry Text ## Durian ## Text --- Elderberry {MD022} ------------------ Text ## Fig <!-- markdownlint-configure-file { "heading-style": false, "no-multiple-blanks": false, "blanks-around-headings": { "lines_above": 0, "lines_below": 2 } } -->文件中出现了两个关键标记{MD022},它们是 markdownlint 测试框架约定俗成的预期错误标记(见 测试框架源码 中/\{(MD\d+)(?::([-+]?)(\d+))?\}/g正则解析逻辑):声明该行(或通过MD022:+/-N偏移定位的行)应当被 MD022 报告为违规。该文件还通过内联配置注释<!-- markdownlint-configure-file { ... } -->声明了本次测试的有效配置:
heading-style: false:关闭标题样式一致性规则,避免 ATX / 闭合 ATX / Setext 混用被干扰;no-multiple-blanks: false:关闭 MD012,允许测试文档中出现连续空行,从而让"标题下方要求 2 个空行"的检查不被 MD012 抢先报错;blanks-around-headings: { "lines_above": 0, "lines_below": 2 }:将标题上方空行需求清零、标题下方空行需求提升为 2。
2.1 预期违规结果解读(对照快照)
根据 场景测试快照 中blanks-around-headings-0-2.md的条目,在lines_above: 0, lines_below: 2配置下,该文档应精确报告 2 条错误:
- 第 8 行
## Banana {MD022}:errorDetail为Expected: 2; Actual: 1; Below。即 Banana 标题下方只有 1 个空行,少于要求的 2 个。其fixInfo为在下一行插入\n(补足 1 个空行)。 - 第 21 行
Elderberry {MD022}:errorDetail为Expected: 2; Actual: 0; Below。即 Setext 标题Elderberry / ------------------下方完全没有空行。其fixInfo为在标题结束行下一行插入\n\n(补足 2 个空行)。
这个结果精准印证了lines_below: 2的语义:标题下方(含 Setext 下划线所在行之后)必须至少有 2 个空行,否则报错。与此同时:
lines_above: 0意味着标题上方即使没有空行(如Text直接紧跟## Banana、## Durian ##等)也不算违规;- 第一个标题
# Blanks Around Headings位于文档开头,上方无空行且被允许;最后一个标题## Fig位于文档末尾,下方是配置注释行,也未被报错——符合"标题位于文档开头或结尾时无需空行"的默认豁免。
快照中的fixed字段还展示了自动修复后的完整文档:Apple、Cherry、Durian 标题下方被补足为 2 个空行,Banana 补 1 个、Elderberry 补 2 个,最终文档经 applyFixes 后重新 lint 不应再有任何可修复错误。
三、参数取值的实战对照:仓库中的兄弟测试用例
blanks-around-headings-0-2.md只是该规则测试矩阵中的一员。仓库test/目录下还提供了覆盖各种取值组合的兄弟用例,可对照理解每个参数行为:
| 测试文件 | 配置要点 | 验证语义 |
|---|---|---|
| blanks-around-headings--1-1.md | lines_above: -1, lines_below: 1 | 上方允许任意空行(仅 1 个 Setext 标题因上方无空行被报错) |
| blanks-around-headings-1--1.md | lines_above: 1, lines_below: -1 | 下方允许任意空行(仅 1 个标题因上方缺空行被报错) |
| blanks-around-headings-3-0.md | lines_above: 3, lines_below: 0 | 上方要求 3 个空行、下方清零(报 2 条 Above 违规) |
| blanks-around-headings-arrays.md | lines_above: [ 1, 2, 3, -1 ]、lines_below: [ -1, 2, 1, 3, -1, 4, 10 ] | 按标题级别 1~6 分别设值 |
| blanks-around-headings-one-true.md 等 | include_front_matter: true/false | 标题上方空行检查是否计入 front matter 行 |
其中 blanks-around-headings-arrays.md 对数组语义的覆盖尤为完整:lines_above: [ 1, 2, 3, -1 ]表示 1 级标题上方需 1 个空行、2 级需 2 个、3 级需 3 个、4 级及以上任意空行;lines_below: [ -1, 2, 1, 3, -1, 4, 10 ]中的第 7 项(10)会被忽略,因为数组只截取前 6 项对应标题级别 1~6。
四、源码实现原理:lib/md022.mjs深度拆解
规则的真实行为由 lib/md022.mjs 实现,它基于micromark解析器("parser": "micromark")。核心逻辑可归纳为以下三层:
4.1 配置归一化:getLinesFunction
const defaultLines = 1; const getLinesFunction = (linesParam) => { if (Array.isArray(linesParam)) { const linesArray = new Array(6).fill(defaultLines); for (const [ index, value ] of [ ...linesParam.entries() ].slice(0, 6)) { linesArray[index] = value; } return (heading) => linesArray[getHeadingLevel(heading) - 1]; } const lines = (linesParam === undefined) ? defaultLines : Number(linesParam); return () => lines; };- 传入数组时:初始化 6 个元素、全部填充默认值
1,再按索引覆盖前 6 项;返回的函数根据 getHeadingLevel 得到的级别(1~6)取对应值; - 传入数字(含
undefined和-1)时:undefined回落为默认值1,其余经Number()强转后作为常量返回。
4.2 空行统计与违规判定
主函数遍历filterByTypesCached([ "atxHeading", "setextHeading" ])得到的所有标题 token:
- 上方检查:从
startLine - 2起向上逐行调用isBlankLine,统计实际空行数actualAbove,最多统计linesAbove次; - 下方检查:从
endLine + 1起向下逐行统计actualBelow; - 当
linesAbove / linesBelow >= 0时检查才生效(-1表示跳过检查); - 最终通过 addErrorDetailIf(仅当
expected !== actual时上报)生成Expected: N; Actual: M; Above/Below这样的错误详情——与快照中Expected: 2; Actual: 1; Below的格式完全对应。
值得注意的是isBlankLine(helpers/helpers.cjs)对"空行"的定义很宽:仅含空白字符的行,以及仅含 HTML 注释(<!-- ... -->,允许未闭合)的行都被视为空行。这解释了为何本测试用例文件末尾的<!-- markdownlint-configure-file { ... } -->配置注释行不会触发违规——它被视作空行。
4.3 自动修复与 blockquote 前缀保留
addErrorDetailIf的fixInfo携带两段关键信息:
insertText:由 getBlockQuotePrefixText 生成,若标题位于 blockquote(引用块)内,会在插入的空行前保留>前缀,避免修复破坏引用块结构;- 下方检查的
fixInfo.lineNumber指向endLine + 1,即标题结束行的下一行,确保空行插入位置准确。
快照中 Banana 的修复为insertText: "\n"(差 1 个空行补 1 行),Elderberry 为insertText: "\n\n"(差 2 个补 2 行),与源码逻辑完全吻合。
五、front matter 交互:include_front_matter的边界行为
默认情况下(include_front_matter: false),YAML front matter 被忽略,以下文档不会报告 MD022 违规(来自 规则文档):
--- title: Title --- ## Heading在源码层面,params.frontMatterLines由主库 removeFrontMatter 解析产生:文档开头若匹配 front matter 正则,则将其按行拆分并剥离出正文。MD022 的getLine辅助函数中,仅上方检查在includeFrontMatter为true且索引越界到 front matter 区域时才会回读frontMatterLines;下方检查固定以false调用(见 lib/md022.mjs 中getLine(lines, endLine + i, frontMatterLines, false)),即标题下方空行从不与 front matter 发生关系。
对应测试用例:默认行为可看 blanks-around-headings-one-yaml-false.md;include_front_matter: true后,front matter 与首个标题之间必须出现要求的空行数,blanks-around-headings-zero-yaml-true.md 中的Text {MD022:-2}标记正是断言这种新引入的违规(行号偏移定位到标题行)。
六、如何运行与验证该测试用例
该测试文件由 test/markdownlint-test-scenarios.mjs 驱动:它遍历./test目录下所有.md文件,逐一执行lint({ "files": [ file ] }),用文档中的{MD022}标记与真实报错行号做assert.deepEqual比对,并将错误详情与修复结果写入 快照文件。这种"标记即断言"的测试范式让每个.md文件本身就是可读的规格说明书。
在本地克隆仓库后,可执行以下命令运行全部场景测试:
npm install npm test(运行环境要求 Node.js 支持 ESM 与node:test,仓库根目录的 package.json 中声明了依赖与脚本;也可单独运行node --test test/markdownlint-test-scenarios.mjs聚焦本类用例。)
七、工程实践建议
综合文档、测试与源码,使用 MD022 自定义空行数量时建议遵循以下要点:
- 优先用
0收紧、用-1放宽:0表示"不允许空行"(如追求紧凑排版),-1表示"不检查"(如标题处于 blockquote 密集结构时); - 数组按级别精细化:若 2 级标题与 1 级标题排版风格不同,使用
lines_above: [ 1, 2 ]之类的数组,未列出的级别自动回落默认值1; - 与 MD012 协同配置:一旦某方向要求 ≥2 个空行,务必同时将 MD012 / no-multiple-blanks 的
maximum调大或直接关闭,否则两规则互相冲突(本测试用例即关闭了 MD012); - 善用
{MD022}标记驱动开发:仿照 blanks-around-headings-0-2.md 的写法,在配置变更时用测试标记快速确认预期报错行号与可修复性。
总结
blanks-around-headings-0-2.md虽然只是一个测试文件,却浓缩了 MD022 规则最核心的定制能力:通过lines_above: 0与lines_below: 2分别收紧与放宽标题上下空行要求,并以{MD022}标记与快照双向验证行为。结合 规则文档 的参数说明与 源码实现 的归一化逻辑,你可以准确预测任意lines_above/lines_below组合(数字、-1、按级别数组)下的 lint 结果与自动修复行为,从而在自己的 Markdown 工程中精确落地标题空行规范。
- 开发工具
- 代码质量
- Lint
- 静态分析
【免费下载链接】markdownlint
A Node.js style checker and lint tool for Markdown/CommonMark files.
相关推荐
markdownlint MD022(blanks-around-headings)规则详解:标题前后空行校验与配置实战
markdownlint MD022(blanks around headings)规则详解:标题前后空行校验与配置实战 本文基于 markdownlint 开
开发工具代码质量Lint静态分析markdownlint MD022 规则深度解析:用 blanks-around-headings 精细控制标题四周的空白行
markdownlint MD022 规则深度解析:用 blanks around headings 精细控制标题四周的空白行 导读 本文围绕 markdown
开发工具代码质量Lint静态分析markdownlint 规则 MD022(blanks-around-headings)完全指南:标题空行环绕检查的原理、配置与修复
markdownlint 规则 MD022(blanks around headings)完全指南:标题空行环绕检查的原理、配置与修复 导读 本文基于 mark
开发工具代码质量Lint静态分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考