news 2026/10/16 1:45:11

markdownlint 空标题(无内容标题)与 MD019 规则:从测试用例到源码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
markdownlint 空标题(无内容标题)与 MD019 规则:从测试用例到源码实现
  • 开发工具
  • 代码质量
  • Lint
  • 静态分析

【免费下载链接】markdownlint

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

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

导读

本文以 markdownlint 仓库中的场景测试文件 test/headings-without-content.md 为主线,深入讲解"只有井号、没有任何正文内容"的 ATX 空标题在 CommonMark 与 markdownlint 中的行为差异:Markdown 解析器允许空标题存在,而 markdownlint 的MD019(no-multiple-space-atx)规则会精准检查井号与标题文本之间是否使用了超过一个空格。读完本文,你将掌握空标题的合法语法边界、如何用内联配置隔离单条规则进行测试、MD019 的判定与自动修复原理,以及如何阅读本仓库的快照测试(snapshot)输出。

一、场景测试文件全景

markdownlint的test/目录下存放了大量"场景测试" Markdown 文件,每个文件本身就是一份自带预期结果的迷你测试。test/headings-without-content.md是其中专门验证"空标题"场景的用例,全文共 22 行:

# Headings Without Content <!-- markdownlint-disable single-title heading-style --> <!-- markdownlint-disable no-duplicate-heading no-trailing-spaces --> # # # # ## ## ## ## {MD019:10} {MD019:12} {MD019:18} {MD019:20}

从文件结构可以拆出三个层次:

  1. 标题行:第 1 行# Headings Without Content是文件自身的标题(注意它本身也是一个合法的#一级标题);
  2. 禁用注释:第 3、4 行通过markdownlint-disable内联指令临时关闭 4 条规则,把测试范围收敛到MD019一条规则上;
  3. 被测内容:第 6~20 行是 8 个没有任何标题文本的 ATX 空标题(4 个#、4 个##),最后一行是测试标记(marker)。

需要特别指出的是,"空标题"本身是完全合法的 Markdown。CommonMark 规范允许#或##之后不跟任何文本,micromark 解析器会把它们正常识别为atxHeading类型的标题 token——它们可以正常参与标题层级、锚点生成等后续处理。正因为语法合法,markdownlint才对"空标题里的空格数量"这一纯格式问题单独立项为MD019。

二、内联禁用注释:如何把测试聚焦到 MD019

文件第 3、4 行使用了 markdownlint 的内联配置指令,这两行注释是理解整个测试的关键:

<!-- markdownlint-disable single-title heading-style --> <!-- markdownlint-disable no-duplicate-heading no-trailing-spaces -->

markdownlint-disable表示从当前行开始到文件末尾(或遇到markdownlint-enable之前)关闭指定规则。这里一共关闭了 4 条规则,关闭原因都可以在对应文档与源码中找到依据:

规则别名文档为什么要关闭
MD025single-title/single-h1doc/md025.md文档中出现了 5 个空的一级标题(#),会触发"同一文档存在多个顶层标题"
MD003heading-styledoc/md003.md混合出现#与##等不同级别、且与首标题风格比较,会触发"标题风格不一致"
MD024no-duplicate-headingdoc/md024.md8 个空标题的文本内容全部相同(都是空字符串),会触发"标题内容重复"
MD009no-trailing-spacesdoc/md009.md第 8、10、12、16、18、20 行在井号后存在行尾空格,会触发"行尾多余空格"

从源码实现看,MD025在 lib/md025.mjs 中通过filterByTypesCached(["atxHeading", "setextHeading"])收集所有顶层标题并报出"除第一个之外"的重复标题;MD024在 lib/md024.mjs 中会记录每个标题的文本(这里全部是空文本),第二次出现相同文本即报错。因此如果不禁用这 4 条规则,测试输出会被这些"噪音"污染,无法单独验证 MD019 对空格数量的判定——这正是该文件两行禁用注释的用意:把一条场景测试裁剪到只覆盖一条规则,使断言结果精确可控。

三、MD019 规则:井号后多重空格的判定

3.1 规则定义

MD019的完整规则名是no-multiple-space-atx,官方文档 doc/md019.md 定义如下:

  • 规则名:MD019/no-multiple-space-atx
  • 标签:atx、headings、spaces
  • 可修复:部分违规可由工具自动修复

触发条件:ATX 风格标题中,井号(#)与标题文本之间使用了超过一个空格。例如:

# Heading 1 ## Heading 2

正确写法是井号后只保留一个空格:

# Heading 1 ## Heading 2

规则设计原理在文档中写得很直白:额外的空格没有任何用途,也不会影响渲染结果,属于纯粹的多余格式。

3.2 空标题场景下的特殊表现

回到空标题场景:当#后面没有任何文本、只有若干空格时,这些空格恰好构成了atxHeadingSequence(井号 token)之后唯一的子 token。于是MD019的判定逻辑就转化为——检查空标题里井号后到底跟了几个空格:

# → 井号后 0 个空格,MD019 不触发 # → 井号后 1 个空格,MD019 不触发(但 MD009 行尾空格会管它) # → 井号后 2 个空格,MD019 触发(第 10 行) # → 井号后 3 个空格,MD019 触发(第 12 行) ## → 2 个井号后 0 个空格,不触发 ## → 2 个井号后 2 个空格,MD019 触发(第 18 行) ## → 2 个井号后 3 个空格,MD019 触发(第 20 行)

这正是测试文件第 22 行标记所声明的预期:{MD019:10} {MD019:12} {MD019:18} {MD019:20},即只有第 10、12、18、20 行会报MD019。注意第 8、16 行各带 1 个空格的行不报 MD019(因为 1 个空格是合法分隔),它们的存在是为了证明"单空格边界不会误报"。

四、源码级实现:token 层面的扫描

MD019与MD021(闭式 ATX 标题的井号内侧空格)共用同一套核心逻辑,实现在 lib/md019-md021.mjs 中。

4.1 空标题如何变成 token 流

仓库使用 micromark 作为 Markdown 解析器(见 package.json 中的micromark依赖与 lib/micromark-parse.mjs)。#空标题会被解析为结构如下的 token 树:

atxHeading ├── atxHeadingSequence (#,文本 "#") └── whitespace (空格序列,文本 " ")

当井号后没有任何内容时,whitespace就是标题里唯一的"正文占位";MD019扫描的正是这个whitespacetoken 的文本长度。

4.2 核心判定函数 validateHeadingSpaces

lib/md019-md021.mjs 中的validateHeadingSpaces(onError, heading, delta)是判定的核心:

function validateHeadingSpaces(onError, heading, delta) { const { children, startLine, text } = heading; let index = (delta > 0) ? 0 : (children.length - 1); while ( children[index] && (children[index].type !== "atxHeadingSequence") ) { index += delta; } const headingSequence = children[index]; const whitespace = children[index + delta]; if ( (headingSequence?.type === "atxHeadingSequence") && (whitespace?.type === "whitespace") && (whitespace.text.length > 1) ) { const column = whitespace.startColumn + 1; const length = whitespace.endColumn - column; addErrorContext( onError, startLine, text.trim(), delta > 0, delta < 0, [ column, length ], { "editColumn": column, "deleteCount": length } ); } }

关键点逐条拆解:

  1. 定位井号 token:沿delta(+1 从前向后、-1 从后向前)在标题的children中找到atxHeadingSequence,它紧邻的下一个子 token 就是whitespace;
  2. 判定条件:whitespace.text.length > 1,即空格数大于 1 才报错——1 个空格是合法分隔,0 个空格(#后直接换行)也合法;
  3. 误差范围:column = whitespace.startColumn + 1从第二个空格开始计算,length是剩余空格数量。例如#(井号 + 2 空格)报错范围为[3, 1],只标记第 2 个空格;#(3 空格)报错范围为[3, 2];
  4. 修复信息:fixInfo中editColumn指向第一个"多余空格"所在列,deleteCount是要删除的空格个数——修复策略是删除多余空格、保留井号后那 1 个合法空格。

MD019 的规则函数(lib/md019-md021.mjs)先按filterByTypesCached(["atxHeading"])收集所有 ATX 标题,再借助 helpers/micromark-helpers.cjs 中的getHeadingStyle过滤出纯atx风格(排除atx_closed闭式标题),最后对每个标题调用validateHeadingSpaces(onError, atxHeading, 1)从前向后扫描。

五、测试驱动的验证闭环

5.1 场景测试机制

markdownlint的场景测试由 test/markdownlint-test-scenarios.mjs 驱动。该文件会扫描test/目录下所有.md文件,对每个文件执行两条并行操作(第 22-27 行):

  1. 读取 Markdown 原文;
  2. 调用lint({ "files": [file] })执行真实 lint。

随后(第 40-64 行)用正则/\{(MD\d+)(?::([-+]?)(\d+))?\}/g解析原文中的{MD019:10}这类标记,把"期望在第几行报哪条规则"解析成expected字典;再与 lint 实际输出比较,多报或漏报都会导致断言失败。文件最后一行{MD019:10} {MD019:12} {MD019:18} {MD019:20}因此扮演了"测试预期清单"的角色。

5.2 快照:修复后的输出

测试还会调用applyFixes生成"修复后的内容"并写入快照,结果保存在 test/markdownlint-test-scenarios.mjs.snapshot。针对本文件的快照包含 4 条MD019错误,每条都携带精确的定位与修复信息:

行号errorContexterrorRangefixInfo
10(#)#[3, 1]editColumn: 3, deleteCount: 1
12(#)#[3, 2]editColumn: 3, deleteCount: 2
18(##)##[4, 1]editColumn: 4, deleteCount: 1
20(##)##[4, 2]editColumn: 4, deleteCount: 2

快照中的fixed字段展示了自动修复的最终结果:所有空标题被统一规整为"井号 + 1 个空格"(如#、##)。这与上一节源码中的fixInfo完全对应——只删多余空格、保留合法单空格。由于第 4 行已禁用MD009(行尾空格),修复后残留的那个单空格不会被二次报错;同时 test/markdownlint-test-scenarios.mjs 还会对修复结果再次 lint,确保所有可修复错误都被彻底修复。

5.3 对照实验:headings_good.md

test/目录下的 test/headings_good.md 是本场景的"对照组":

# Heading 1 ## Heading 2 ## Heading 3

它对应的快照(test/markdownlint-test-scenarios.mjs.snapshot)中errors为空数组,fixed与原文完全一致。这个对照说明:只要标题有内容且井号后只有 1 个空格,就不会触发 MD019 或任何其它标题相关规则——从正反两面印证了 MD019 判定条件的唯一变量就是"井号后的空格数量"。

六、实战:在自己的文档中规避与利用空标题

6.1 空标题的正确姿势

根据MD019的语义,写空标题时应遵守两条约束:

  • 井号后最多 1 个空格:#或#都不会触发 MD019;
  • 不要使用 2 个及以上的空格:#、##都会报MD019,并会被自动修复工具折叠为单空格形式。

如果空标题是刻意为之(例如用于占位、作为模板骨架),建议:

#

即只保留一个空格,既满足 MD019,又让渲染结果保持为标题。同时需要注意,空标题会继续触发 MD025、MD024、MD003、MD009 等其它规则(这正是测试文件要用两行禁用注释隔离它们的原因),所以实践中更稳妥的做法是为标题补齐实际文本,或按需在文件头部使用markdownlint-disable明确声明意图。

6.2 亲手复现与验证

如果你希望在本仓库中复现上述全部行为,可以:

# 运行包含本场景在内的全部场景测试 npm run test # 仅运行场景测试 node --test test/markdownlint-test-scenarios.mjs # 重新生成快照(更新测试预期) npm run update-snapshots

其中npm run test在 package.json 中定义,运行环境要求 Node.js>=22(见 package.json 的engines字段)。修改test/headings-without-content.md后运行场景测试,即可观察断言与快照如何随文件变化,从而深入理解 markdownlint 的"测试文件即文档"设计哲学。

结语

test/headings-without-content.md虽然只有 22 行,却完整覆盖了 markdownlint 规则开发与测试的一条典型链路:合法的空标题语法 → 用内联禁用注释隔离目标规则 → micromark token 级扫描 → 精确到列的空格计数 → 可自动修复的 fixInfo → 快照回归验证。它同时告诉我们一个容易被忽略的事实:Markdown 语法合法不等于 lint 通过,MD019这类"格式化规则"正是负责在语法与规范之间补齐最后一道约束的。理解了这个测试用例,也就理解了 markdownlint 全部 60+ 条规则背后"解析器 + 规则 + 测试 + 快照"的统一工作模式,后续阅读 lib/md019-md021.mjs 或 doc/Rules.md 中的其它标题类规则(MD001、MD003、MD024、MD025)时即可举一反三。

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

【免费下载链接】markdownlint

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

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

相关推荐

上一篇:Midday数据库索引优化:为嵌入相似度搜索专门设计的索引方案
下一篇:终结系统幽灵:System Informer高效清除僵尸进程全指南

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 1:43:18

【Unity开发】C#线程Thread类详细解析

&#x1f43e; 个人主页 &#x1f43e; 阿松爱睡觉&#xff0c;横竖醒不来&#x1f3c5;你可以不屠龙&#xff0c;但不能不磨剑&#x1f5e1;文章目录一、Thread二、线程创建与启动三、线程生命周期控制四、线程锁&#x1f512;&#xff08;一&#xff09;lock&#xff08;二&a…

作者头像 李华
网站建设 2026/10/16 1:42:20

OpenStack多区域部署实战:基于kolla-ansible的架构设计与避坑指南

1. 多区域部署到底在解决什么问题第一次接触多区域这个概念&#xff0c;是在一个跨机房容灾的项目里。当时业务侧提的需求很直接&#xff1a;单机房挂掉之后&#xff0c;核心业务要在可接受的时间内恢复&#xff0c;而且日常状态下两个机房的资源都要能用起来&#xff0c;不能白…

作者头像 李华
网站建设 2026/10/16 1:39:19

MXSPyCOM源码解析:MaxScript与Python进程间通信实战

简介&#xff1a;MXSPyCOM是一套面向3ds Max脚本开发者的开源工具源码包&#xff0c;重点解决内置编辑器功能有限、编写MaxScript与Python脚本效率低的问题。它通过COM通信机制打通3ds Max与外部编辑器&#xff0c;让用户可在VS Code、Sublime Text、PyCharm等工具中编写并实时…

作者头像 李华
网站建设 2026/10/16 1:38:26

lolPing 桌面应用怎么测:测试与 CI 实践完整清单

【免费下载链接】lolPing League of Legends ping wheel for your Windows desktop. Hold Alt, drag, and ping anywhere, with the original animations and sounds. 项目地址&#xff1a; https://gitcode.com/gh_mirrors/lo/lolPing 点击查看 免费下载 lolPing 是一个给 Wi…

作者头像 李华
网站建设 2026/10/16 1:38:21

如何下载安装 Chatlog Alpha:macOS 与 Windows 全平台部署清单

后端CLIMCP 服务即时通讯RAG人工智能AI 技能 【免费下载链接】chatlog_alpha 原 [chatlog]项目&#xff08;一个微信数据库解密读取及提供mcp服务、http服务的开源软件&#xff09;&#xff0c;现已支持通过微信clawbot接口推送消息&#xff0c;可以实时转发全部或指定消息到cl…

作者头像 李华