markdownlint MD043 required-headings 规则实战:用 `*`、`+`、`?` 通配符强制 Markdown 文档标题结构

原创2026-10-15 10:20:222,295 阅读
文章标签:开发工具Lint静态分析代码质量

markdownlint MD043 required-headings 规则实战:用 *、+、? 通配符强制 Markdown 文档标题结构

本文以 markdownlint 仓库中的测试夹具 test/required-headings-zero-or-more.md 为线索,系统讲解 MD043(required-headings)规则如何通过 headings 数组与 *(zero or more)、+(one or more)、?(exactly one unspecified)三种特殊值,强制一批 Markdown 文档遵循统一的标题结构。读完本文,你将掌握该规则的完整配置语法、三种通配符的精确语义、大小写匹配策略、错误报告行为,并能读懂其背后的 lib/md043.mjs 实现原理与 test/required-headings-*.md 系列测试用例。

一、规则速览:MD043 / required-headings

MD043 是 markdownlint 内置的标题结构规则,别名为 required-headings,标签为 headings,属于 micromark 解析器体系。官方规则文档见 doc/md043.md,规则声明见 lib/md043.mjs。

当文件中的标题序列与配置的期望序列不一致时触发。它常用于为一批结构相似的文档(如项目文档、教程、产品手册)强制统一骨架:

  • 固定章节必须存在、顺序固定;
  • 允许的部分(如示例、附录)通过通配符放行;
  • 通过 match_case 决定标题文本是否严格区分大小写。

参数说明

参数 类型与默认值 含义
headings string[],默认 [] 期望的标题序列;元素既可以是精确标题(如 "# One"),也可以是三个特殊值 "*"、"+"、"?"
match_case boolean,默认 false 是否要求文档标题与 headings 配置严格大小写一致;默认不区分大小写

规则的 Rationale(设计动机)在 doc/md043.md 中明确说明:项目希望在一组相似内容上强制执行一致的文档结构。

二、核心通配符 *(zero or more):关联测试文档精读

本文的主体是测试夹具 test/required-headings-zero-or-more.md,它完整演示了 "*" 的配置方法与匹配语义。原文如下:

# One

## Two

### THREE

## four

## Five

### SiX

#### 7

<!-- markdownlint-configure-file {
  "required-headings": {
    "headings": [
      "# One",
      "*",
      "### Three",
      "*",
      "### Six",
      "*",
      "#### 7"
    ]
  }
} -->

这个文件没有携带任何 {MD043} 错误标记,意味着在默认(大小写不敏感)配置下它完全通过检查。逐条对照文档标题 # One、## Two、### THREE、## four、## Five、### SiX、#### 7 与配置序列,匹配过程如下:

期望值 实际标题 匹配结果
# One # One 精确匹配
* ## Two 通配符吞掉 1 个标题
### Three ### THREE 大小写不敏感匹配(Three ≈ THREE)
* ## four、## Five 通配符连续吞掉 2 个标题
### Six ### SiX 大小写不敏感匹配(Six ≈ SiX)
* (无剩余标题) 通配符匹配零个标题
#### 7 #### 7 精确匹配

这里的 * 语义是 "零个或多个未指定的标题":它既可以像 ## Two 一样匹配一个标题,也可以像 ## four、## Five 一样连续匹配多个,还可以在文件末尾(### SiX 之后没有标题)匹配零个。这正是 zero-or-more 文件名的由来。

为什么 ### Three 能匹配 ### THREE?

注意文档中实际标题是 ### THREE 和 ### SiX,而配置写的是 ### Three 与 ### Six,大小写并不一致,却没有报错。因为 match_case 默认是 false,规则在比较前会先把双方转为小写。这一行为在 doc/md043.md 中有明确说明,也是 test/required-headings-zero-or-more.md 想验证的核心点之一。

三、通配符 +(one or more):至少一个标题

与 * 相对的另一个特殊值是 "+",表示 "一个或多个未指定的标题"。官方文档的示例(doc/md043.md)同时用到了两者:

[
  "# Heading",
  "## Item",
  "*",
  "## Foot",
  "*"
]

对应允许的结构是:

# Heading
## Item
### Detail (optional)
## Foot
### Notes (optional)

* 出现在中间时表示"这之间可以有零个或多个其他标题",从而让 ### Detail、### Notes 成为可选章节。

+ 与 * 的差异可以通过一对对照测试直观看出:

test/required-headings-one-or-more.md 还展示了一个更复杂的 + 用法:

{
  "headings": [
    "# One",
    "+",
    "### Three",
    "+",
    "### Six",
    "+",
    "#### 7"
  ]
}

文档标题为 # One、## Two、### THREE、## four、## Five、### SiX、#### 7:每个 + 区间内都至少有一个标题(分别是 ## Two、## four/## Five、以及 ### SiX 与 #### 7 之间没有,注意这里 + 之后紧跟 #### 7,而 #### 7 是固定标题,两者不冲突),因此不满足"中间必须有额外标题"时才报错。若把 + 换成 *(即 test/required-headings-zero-or-more.md),则中间的标题数量可以为零,语义更宽松。

四、通配符 ?(exactly one unspecified):恰好一个可变标题

第三个特殊值是 "?",表示 "恰好一个未指定的标题"。典型场景是项目名这类单点可变的标题。官方文档示例(doc/md043.md):

# Project Name
## Description
## Examples

配置为:

[
  "?",
  "## Description",
  "## Examples"
]

这里 ? 允许 # Project Name 这部分恰好出现一个任意内容的标题。相关测试覆盖了各种位置与边界:

注意错误标记写法 {MD043:+1} 表示"文件末尾之后偏移 1 行"——当问题出现在文件结尾处时,markdownlint 用相对偏移来定位,这一点在下一节结合源码展开。

五、match_case:严格大小写匹配

默认情况下比较是大小写不敏感的,这在第一节的 THREE/Six 例子中已经体现。若需要严格区分大小写,设置 match_case: true,lib/md043.mjs 读取该配置,lib/md043.mjs 根据开关决定比较前是否调用 toLowerCase()。

一对对照测试最能说明问题:

从源码结构看,handleCase 函数(lib/md043.mjs)是大小写策略的唯一入口:matchCase 为真时原样返回,为假时统一转小写,这意味着 match_case: true 下 # One、## two 这类大小写差异都会被视为不同标题。

六、源码实现:错误报告行为与匹配状态机

理解规则行为最好的方式是读实现。lib/md043.mjs 的 MD043 函数是一个线性扫描的状态机:

  1. 前置校验:lib/md043.mjs 中 headings 不是数组时直接返回,避免无谓开销;
  2. 标题枚举:lib/md043.mjs 通过 filterByTypesCached(<a href="https://link.gitcode.com/i/bf5b05dc79976d9080af86fef282ec95" target="_blank">"atxHeading", "setextHeading"])(见 [lib/cache.mjs)遍历所有 ATX 与 Setext 标题 token,因此文档中标题的书写风格不受限制——配置里虽然统一用 "## Text" 的 ATX 写法,但文档可以混用任何受支持的标题样式(doc/md043.md);
  3. 实际值构造:lib/md043.mjs 用 "#".repeat(headingLevel) 按标题级别重建 actual,标题文本取自 helpers/micromark-helpers.cjs 的 getHeadingText(拼接文本子 token 并把换行替换为空格),级别取自 getHeadingLevel(helpers/micromark-helpers.cjs);
  4. 通配符处理:
    • expected === "*"(lib/md043.mjs):先取下一个期望值,若当前标题不等于它则把游标 i 回退一位,让 * 继续吞标题;若等于则停止通配;
    • expected === "+"(lib/md043.mjs):标记 matchAny = true,允许当前标题进入通配区间;
    • expected === "?"(lib/md043.mjs):"允许当前,匹配下一个",即恰好消费一个任意标题;
  5. 精确匹配:lib/md043.mjs 按 handleCase 比较,相等则清除 matchAny;不相等且处于通配区间则回退游标继续尝试;否则通过 addErrorDetailIf 报出 Expected: ...; Actual: ... 的详细错误并置 hasError = true(每个文件只报第一处问题)。

行号报告规则

规则只报告一次错误,位置分两种情况(doc/md043.md):

  • 若在标题序列中发现问题,报告第一个有问题的标题所在行;
  • 若问题是标题数量不足/多余(循环结束后 extraHeadings 判定,见 lib/md043.mjs),则报告文件最后一行。

后者在测试中大量出现:如 test/required-headings-missing-last.md 期望 # One、## Two、### Three 但文档只有两级 Setext 标题,在文件末尾行报 {MD043};test/required-headings-missing-last-zero-or-more.md 和 test/required-headings-missing-middle-zero-or-more.md 都是配置 * 区间后的固定标题(#### FOO)在文档中缺失,在末尾报 {MD043:30}。而 {MD043:+1} 形式的偏移标记出现在 test/required-headings-question-extra.md、test/required-headings-question-missing.md、test/required-headings-none-one-or-more.md 中,对应问题判定位置在文件结尾之后。

边界判定细节

结尾处的 extraHeadings 判定(lib/md043.mjs)还有两个细节值得注意:

七、测试用例矩阵:required-headings 家族

仓库在 test 目录下为 MD043 维护了一组非常完整的测试夹具,文件名即语义标签。下表按场景归类(无错误标记的文件表示应通过检查):

场景 测试文件 关注点
* 匹配零个或多个 required-headings-zero-or-more.md 中间/末尾通配 + 大小写不敏感
+ 匹配一个或多个 required-headings-one-or-more.md 每个区间至少一个标题
全通配 required-headings-all-optional.md / required-headings-all-optional-at-least-one.md ["*"] 与 ["+"] 的差异
无标题文档 required-headings-none.md / required-headings-none-zero-or-more.md / required-headings-none-one-or-more.md 空数组、*、+ 三种情况
? 恰好一个 required-headings-question-first.md / required-headings-question-middle.md / required-headings-question-last.md 出现在开头/中间/末尾
? 边界失败 required-headings-question-extra.md / required-headings-question-missing.md 无标题可匹配 / 目标标题缺失
缺失固定标题 required-headings-missing-first.md / required-headings-missing-middle.md / required-headings-missing-last.md 开头/中间/末尾缺失
* + 缺失固定标题 required-headings-missing-middle-zero-or-more.md / required-headings-missing-last-zero-or-more.md 通配后固定标题缺失的报错
* 在各位置 required-headings-optional-first.md / required-headings-optional-middle.md / required-headings-optional-last.md / required-headings-optional-redundant.md 开头/中间/末尾/连续两个 *
大小写 required-headings-match-case.md / required-headings-wrong-match-case.md match_case: true 的通过与失败
全匹配基线 required-headings-all-present.md 无通配符的精确匹配

这些夹具配合 test/markdownlint-test-scenarios.mjs 等测试脚本(其快照见 test/markdownlint-test-scenarios.mjs.snapshot)运行,确保规则行为长期稳定;后续修改实现时必须保持这些断言不变。

八、如何配置与运行验证

方式一:项目配置文件

在 markdownlint 的配置文件中按规则别名或编号启用,例如:

{
  "required-headings": {
    "headings": [
      "# 项目名",
      "## 安装",
      "*",
      "## 使用方法",
      "*",
      "## 许可证"
    ],
    "match_case": false
  }
}

这份配置要求每个文档固定包含一级标题、安装、使用方法、许可证 四个章节,其中 使用方法 前后的 * 允许插入任意数量的可选小节,同时标题大小写保持宽松。相关参数含义与规则完整说明见 doc/md043.md。

方式二:文件内联配置(本文档采用的写法)

本文主题测试文件使用的是 markdownlint-configure-file HTML 注释,该注释必须以 <!-- 开头、内容为 JSON、以 --> 结束:

<!-- markdownlint-configure-file {
  "required-headings": {
    "headings": [
      "# One",
      "*",
      "### Three",
      "*",
      "### Six",
      "*",
      "#### 7"
    ]
  }
} -->

注释可以出现在文档任意位置,只对当前文件生效(相关解析逻辑见 helpers/helpers.cjs 中 inlineCommentStartRe 对 markdownlint-configure-file 指令的识别;单行变体示例见 test/inline-configure-file-single-line.md)。在测试夹具里用内联配置可以精确控制每个用例的规则参数,无需维护独立配置文件。

方式三:运行验证

安装后可通过 markdownlint 的 Promise API 对单个文件执行检查,仓库自带的测试驱动脚本 test/harness.mjs 演示了标准用法:

node test/harness.mjs test/required-headings-zero-or-more.md

脚本内部调用 markdownlint/promise 的 lint({ "files": <a href="https://link.gitcode.com/i/13b82fb5f76c4c03edd2da06d3b97e1d" target="_blank">file] }) 并打印结果对象([test/harness.mjs)。对本文的夹具运行后应得到空结果;若把 ### Six 误写成 ### SiX 且开启 match_case: true,则会在对应标题行报出 Expected: ### Six; Actual: ### SiX 的详细错误。

九、实战建议与边界注意

综合规则文档(doc/md043.md)、实现(lib/md043.mjs)与测试族,给出如下实践建议:

  1. 固定骨架 + 通配放行是 MD043 最典型的使用方式:把必有的章节写成精确标题,把可选的中间章节用 * 包起来。要区分"至少一节"与"可有可无",分别选 + 与 *;
  2. ? 专用于单点可变:如项目名、版本号所在的标题,? 恰好消费一个任意标题,比 * 更严格,可防止"多出"额外标题被静默接受;
  3. 大小写策略全局生效:match_case 是对整个 headings 序列生效的开关,无法针对单个标题单独配置;默认不区分大小写可以容忍 THREE/Three 这类差异,但也会让"必须大写"的约束失效,需要根据团队规范权衡;
  4. 注意报错定位:标题序列中间出错时报告在具体标题行,而数量不符时报告在文件末尾(可能表现为 {MD043:+1} 的偏移形式),排错时需要区分两类问题;
  5. 配置写法与实际风格解耦:headings 统一使用 ATX 风格("## Text")书写,但文档实际可以使用 Setext 或闭合 ATX 等任意受支持样式(test/required-headings-missing-last.md 中 Setext 标题同样参与匹配),不要因此误判;
  6. 空配置即全面禁用检查:headings: [] 时任何标题都会报错(test/required-headings-none.md),如需临时关闭该规则应配置 "required-headings": false 而不是空数组。

MD043 是 markdownlint 中少有的"结构性"规则——它不关心单个标题写得规不规范,而是把关整篇文档的章节骨架。配合 *、+、? 三种通配符与 match_case,足以应对从"固定章节模板"到"仅约束关键章节"的各种文档治理需求,是构建大规模 Markdown 文档库一致性基线的有力工具。

登录后查看全文
markdownlint