ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

markdownlint MD043 required-headings 规则实战:用 `*` / `+` / `?` 通配符约束文档标题结构

markdownlint MD043 required-headings 规则实战:用 `*` / `+` / `?` 通配符约束文档标题结构
  • 开发工具
  • 代码质量
  • Lint
  • 静态分析

【免费下载链接】markdownlint

A Node.js style checker and lint tool for Markdown/CommonMark files.

项目地址:https://gitcode.com/gh_mirrors/ma/markdownlint
点击查看免费下载

markdownlint 的MD043 required-headings(必需标题结构)规则允许你用一份"预期标题清单"强制约束一组 Markdown 文件的结构,并借助*(零个或多个未指定标题)、+(一个或多个未指定标题)、?(恰好一个未指定标题)三个特殊值表达灵活的需求。本文以仓库中针对"无标题文档 +*通配符"场景的测试夹具 test/required-headings-none-zero-or-more.md 为引子,结合 规则官方文档、规则源码实现 与 Schema 定义,系统讲解该规则的全部配置参数、匹配算法与可落地的工程实践,读完即可在团队文档体系中实施"文档模板强制校验"。

一、规则概览:MD043 是什么

在 markdownlint 的规则清单中(见 README.md),MD043 的完整信息如下:

  • 规则编号:MD043
  • 别名:required-headings
  • 描述:Required heading structure(必需标题结构)
  • 标签:headings(标题类)
  • 解析器:micromark(规则源码中"parser": "micromark")

该规则在文件中的标题序列与配置中给出的headings数组不一致时触发告警,适用于"一组内容相近的文件必须保持统一文档结构"的场景——例如项目模板、发布说明、迁移指南、产品文档章节骨架。

规则官方文档位于 doc/md043.md,其中定义了仅有的两个参数:

参数类型默认值说明
headingsstring[][]期望的标题列表,元素为# Text形式的 ATX 标题字符串
match_casebooleanfalse匹配标题时是否要求大小写完全一致

在 schema/markdownlint-config-schema.json 中,required-headings规则还可以整体被设为布尔值(true/false)或"error"/"warning"严重级别字符串;当以对象形式配置时,除上述两个参数外还支持enabled与severity字段。

二、匹配的"预期标题"如何书写

headings数组的每个元素都是形如# Text、## Item这样的字符串——规则采用 ATX 标题风格书写,仅是为了简化表达。官方文档明确说明:文件实际可以使用任何受支持的标题风格(ATX、ATX 闭合式## Text ##、Setext 下划线式均可),规则在内部会先归一化再比较。

每个元素只允许两类取值,这一点由 Schema 中items的pattern约束体现(见 schema/markdownlint-config-schema.json):

^(\*|\+|\?|#{1,6}\s+\S.*)$

即:要么是三个特殊通配符之一(*、+、?),要么是#到######的 1~6 级标题后跟至少一个空白字符和非空内容。

三、三种通配符的语义(核心能力)

headings数组中除具体标题外的三个特殊值,是 MD043 表达"宽松结构"的关键:

1."*":零个或多个未指定标题

放在两个必须标题之间时,表示"这里可以出现任意数量、任意内容的标题,也可以一个都没有"。官方文档给出的经典示例是"可选的中间章节":

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

配置写成:

{ "headings": [ "# Heading", "## Item", "*", "## Foot", "*" ] }

注意*位置上的"零个"语义:即便## Item之后直接紧跟## Foot,中间没有任何标题,也能通过校验。

2."+":一个或多个未指定标题

与*几乎相同,唯一区别是"至少出现一个"。例如把上面的配置中*换成+,则## Item与## Foot之间必须有至少一个标题,否则报错。

3."?":恰好一个未指定标题

适合"某个必须存在、但内容可变"的标题,典型场景是项目名。官方文档示例:

# Project Name ## Description ## Examples

配置:

{ "headings": [ "?", "## Description", "## Examples" ] }

?要求文件第一个标题是任意内容的一级标题(恰好一个),随后必须依次出现## Description、## Examples。

四、从源码理解匹配算法

规则的完整实现位于 lib/md043.mjs。理解它的状态机逻辑,才能准确预判"什么情况会报错"。

4.1 前置检查与核心循环

const requiredHeadings = params.config.headings; if (!Array.isArray(requiredHeadings)) { // 未配置 headings 数组时直接跳过,避免无谓开销 return; }

随后规则通过filterByTypesCached([ "atxHeading", "setextHeading" ])取出文档中所有标题 token(见 lib/md043.mjs),并维护一个"期望位置指针"i和三个状态位:

  • i:当前消费到headings数组的第几个元素;
  • matchAny:是否处于"已匹配到任意标题"的通配区域;
  • hasError:是否已产生首个错误(一旦报错,后续标题不再检查);
  • anyHeadings:文档中是否至少存在一个标题。

对每个实际标题,规则用getHeadingLevel与getHeadingText构造实际字符串"#".repeat(level) + " " + text,然后与期望值比对。

4.2 通配符分支的实际行为

对照源码 lib/md043.mjs 可以看到:

  • 当期望值是*时,规则预读下一个期望值,如果下一个期望值仍未匹配当前标题,则把matchAny置为true并把指针回退i--,即继续停留在*位置——这解释了为什么*可以匹配任意数量标题;若下一个期望值匹配上了,指针前进,*消费结束。
  • 当期望值是+时,规则直接置matchAny = true并继续消费——+本身不比对内容,只要求后面不再有空缺。
  • 当期望值是?时,注释写明"允许当前标题、继续匹配下一个"(Allow current, match next),即当前标题被任意接受,随后推进指针。
  • 普通标题期望值:大小写归一化(match_case为false时统一小写)后与当前实际标题比对;不匹配时如果正处于matchAny状态则回退指针继续等,否则调用addErrorDetailIf报告"期望值 vs 实际值",并置hasError = true。

4.3 文件末尾的"剩余期望值"检查

循环结束后,还有一段收尾逻辑(lib/md043.mjs):计算extraHeadings = requiredHeadings.length - i,即尚未被匹配消费的期望条目数。只要满足以下条件就会在文件末尾(params.lines.length)上报错误:

  • 未发生错误(!hasError);
  • 剩余期望条目多于 1 个,或恰好剩 1 个且它不是*;
  • 且(文档中存在标题,或期望列表并非全部为*)。

这条收尾逻辑正是通配符语义"零个"的落地点:当headings为["*"]且文档一个标题都没有时,anyHeadings为false且所有期望值都是*,因此不报错;而["+"]时剩余期望值+不是*,因此必然报错(要求至少一个标题)。

4.4 辅助函数如何取标题

实际标题字符串由 helpers/micromark-helpers.cjs 中的两个函数生成:

  • getHeadingLevel(L181-L194):在标题 token 的 children 中查找atxHeadingSequence或setextHeadingLine,#数量即级别(上限 6),-下划线则固定为 2 级;
  • getHeadingText(L221-L227):拼接atxHeadingText/setextHeadingText子 token 的文本,过滤htmlText(HTML 标签不参与比对),并把换行替换为空格。

因此,### Detail (optional)会被规整为### Detail (optional)这样"级别 + 空格 + 纯文本"的形态,闭合式## Two ##的尾随#不会进入文本比较(测试夹具 required-headings-optional-middle.md 验证了这一点)。

五、聚焦关联文档:无标题文档 +*的零个语义

本仓库的测试夹具 test/required-headings-none-zero-or-more.md 完整内容如下:

Text Text Text <!-- markdownlint-configure-file { "first-line-heading": false, "required-headings": { "headings": [ "*" ] } } -->

这是一个"极端但合法"的场景——整个文档没有任何标题,headings配置只有一个*。由于*表示"零个或多个未指定标题",零个是允许的,所以该文件不触发任何 MD043 报错。这一点由对应的测试快照直接证实:在 test/markdownlint-test-scenarios.mjs.snapshot 中,该夹具的"errors": [],fixed结果与原文完全一致(无任何可修复项)。

夹具中的两处细节值得说明:

  1. "first-line-heading": false:文件首行是正文Text而非一级标题,因此必须显式关闭 MD041(first-line-heading)规则,否则会叠加报错。这表明在"无标题文档"模板中,通常需要同时处理"首行标题"与"必需标题"两条规则。
  2. 内联配置注释:<!-- markdownlint-configure-file { ... } -->是 markdownlint 的文件级内联配置语法(参见 README.md),其中的 JSON 对整个文件生效,即使注释位于文件末尾。除该语法外,也可使用markdownlint-disable-file/markdownlint-enable-file注释,或通过外层.markdownlint.json配置。

5.1 与[]和["+"]的对比

把["*"]与另外两种配置并排对比,能更清晰地看出"零个"语义的边界(均有对应测试夹具与快照佐证):

配置无标题文档有标题文档测试夹具快照结论
"headings": []报错(期望[None],实际首个标题不匹配)报错required-headings-none.md第 1 行报Expected: [None]; Actual: # One {MD043}
"headings": ["*"]通过通过(任意标题均可)required-headings-none-zero-or-more.mderrors: []
"headings": ["+"]报错(至少需要一个标题)通过required-headings-none-one-or-more.md无标题文档第 15 行报MD043

其中空数组[]的含义是"文档不允许出现任何标题",与["*"]("允许任意数量标题")语义截然相反——getExpected在越界时返回占位值[None](见 lib/md043.mjs),所以无标题文档能通过["*"],却无法通过[]。

5.2*通配符的常见组合用法

单个["*"]属于"完全放行",实际生产中更多是把它夹在具体标题之间。例如测试夹具 required-headings-zero-or-more.md:

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

它要求:必须出现# One,其后任意数量标题,再出现### Three,其后任意数量标题……依次类推,最终以#### 7结尾。即使中间实际存在## Two、## four、## Five等未被列出的标题也全部通过(快照errors: [])。若某个*位置之后找不到指定的下一标题,则报错位置是文件末尾,错误上下文是"未能匹配到的期望标题"(可对比 required-headings-missing-middle-zero-or-more.md 第 15 行的{MD043:30}标记)。

六、match_case:是否要求大小写一致

默认情况下,match_case为false,规则比对时会将期望值与实际值统一转为小写(源码中handleCase函数,见 lib/md043.mjs)。也就是说,配置"### Three"能匹配文档中的### THREE、### three等任意大小写变体。

当需要严格区分大小写时,设置为true:

{ "required-headings": { "headings": [ "# Title", "## First Heading", "## Second Heading", "*", "## Third Heading" ], "match_case": true } }

测试夹具 required-headings-match-case.md 展示了match_case: true下的合法结构(中间允许一个### Random heading自由标题);而 required-headings-wrong-match-case.md 对应的快照中,## SECOND HEADING因大小写不符而报Expected: ## Second Heading; Actual: ## SECOND HEADING。

七、错误行号与错误信息的输出约定

规则官方文档特别说明:当检测到错误时,MD043 输出第一个问题标题所在的行号;否则输出文件最后一行(末尾)的行号。这个行为在源码中得到印证——循环内首个不匹配即用addErrorDetailIf(onError, heading.startLine, expected, actual)上报并置hasError,循环外再用addErrorContext(onError, params.lines.length, requiredHeadings[i])处理"剩余期望值"的末尾错误。

实际测试文件中用{MD043:15}这样的行号标记来断言期望的报错位置,例如:

  • required-headings-none-one-or-more.md:无标题文档 +["+"],报错行号为配置注释所在行 15;
  • required-headings-missing-middle-zero-or-more.md:### FOO永远匹配不上,报错在文件末尾第 15 行;
  • required-headings-question-missing.md:?匹配掉## Examples后又缺少后续标题,报{MD043:+1}(末尾偏移一行)。

这些行号标记由测试驱动 test/markdownlint-test-scenarios.mjs 中的正则解析(支持{MD043:30}绝对行号与{MD043:+1}相对偏移),再与真实 lint 结果比对,最终生成快照文件 test/markdownlint-test-scenarios.mjs.snapshot。

八、配置文件与内联配置:两种接入方式

8.1 全局配置文件

在.markdownlint.json(或 package.json 的markdownlintConfig字段)中按规则名或别名配置:

{ "required-headings": { "headings": [ "# 项目名", "## 背景", "*", "## 使用说明", "*", "## 常见问题" ], "match_case": false } }

在标准配置中每个规则默认开启(Schema 中"default": true),可直接整体置为false关闭,或设为"warning"降级为警告级别。

8.2 文件级内联配置

本关联文档演示的<!-- markdownlint-configure-file { ... } -->注释可对单个文件覆盖规则配置,JSON 内容作用于整个文件、与注释位置无关,多个此类注释按从上到下的顺序叠加应用(README.md)。这对"模板类文件自带校验规则"的场景非常实用:把配置写进模板文件本身,模板使用者无需感知外层配置。

九、典型实战场景与注意事项

场景一:文档模板强制结构。对发布说明、提案模板、迁移清单等"同构文档",用# 标题+*的组合固定骨架章节,防止协作者漏写或增删关键章节。

场景二:项目名等可变内容。用?表达"这里必须有一个任意内容的标题",例如["?", "## Description", "## Examples"]——即使文档开头的 H1 是项目名,也满足"恰好一个未指定标题"。

场景三:无标题或纯文本文件。若模板允许完全没有标题(如仅含正文的 README 片段),配置["*"]即可放行;同时记得处理first-line-heading(MD041)等首行相关规则,可参考本关联文档夹具中"first-line-heading": false的写法。

注意事项汇总:

  1. headings默认[]表示"不允许任何标题",勿与["*"]混淆;若团队从未配置过该规则,它实际处于"校验空结构"状态,遇到任何标题都会报错——这正是"未配置即严格"的隐藏行为。
  2. *、+、?只在headings数组的独立元素位置上生效,不能嵌入具体标题文本(Schema 的 pattern 已约束元素要么是单个通配符、要么是完整的# 文本)。
  3. 标题文本比对基于去掉 HTML 标签后的纯文本(getHeadingText过滤htmlText子 token),行内强调、代码等标记在规整时如何呈现,可通过 harness.mjs 这类 token 打印工具实地观察。
  4. 大小写默认不敏感;match_case: true会让### THREE与### Three判为不同,适合品牌名、专有名词场景。
  5. 由于"首个错误即停止后续匹配",一份文档若有多个结构问题,只会报告第一个;修复后重新运行 lint 才能暴露下一个问题。

十、结论

MD043 required-headings用一份可含通配符的标题清单,把"文档结构合规"从人工评审变成可自动化的 CI 检查。*(零或多个)、+(一个或多个)、?(恰好一个)三个特殊值让它兼具"刚性骨架"与"弹性内容",而 doc/md043.md 中的参数说明、lib/md043.mjs 的状态机实现,以及 test/required-headings-none-zero-or-more.md 等十余个测试夹具与快照,共同构成了从"语义理解"到"边界行为"的完整证据链。无论是强制团队文档模板、校验项目自述文件结构,还是为自动化文档生成流程设定输出骨架,MD043 都是值得优先落地的标题级质量闸门。

  • 开发工具
  • 代码质量
  • Lint
  • 静态分析

【免费下载链接】markdownlint

A Node.js style checker and lint tool for Markdown/CommonMark files.

项目地址:https://gitcode.com/gh_mirrors/ma/markdownlint
点击查看免费下载

相关推荐

上一篇:Congo主题高级自定义指南:从配色方案到CSS构建
下一篇:告别低效开发:Sunmao-UI组件开发完全指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进