news 2026/10/16 6:14:13

markdownlint MD023 规则实战:从源码与测试用例解析“标题必须顶格书写”

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
markdownlint MD023 规则实战:从源码与测试用例解析“标题必须顶格书写”
  • 开发工具
  • 代码质量
  • Lint
  • 静态分析

【免费下载链接】markdownlint

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

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

导读

MD023(别名heading-start-left)是 markdownlint 中用于强制ATX/Setext 标题必须从行首开始(无前导空格缩进)的规则,属于headings、spaces标签组。本文以仓库测试文件 test/headings_with_spaces_at_the_beginning.md 为切入点,逐行拆解该规则命中与豁免的边界场景(普通段落、代码块注释、列表内标题、行内配置指令),并结合规则源码 lib/md023.mjs 与相邻规则 lib/md022.mjs 的底层实现、测试快照 test/markdownlint-test-scenarios.mjs.snapshot 中的输出与自动修复结果,讲清规则触发条件、修复原理及与 MD022 的协作关系。读完本文,你将能准确预判“带缩进的标题”何时会被 MD023 拦截、如何利用fixInfo自动修复,以及在列表等嵌套结构中如何规避误报。

一、规则背景:MD023 检查什么

依据官方规则文档 doc/md023.md(同内容收录于 doc/Rules.md):

Headings must start at the beginning of the line(标题必须从行首开始)。

该规则在以下场景触发:

Some text # Indented heading

修正方式是将标题顶格书写:

Some text # Heading

例外场景:块引用(block quote)会使整行看起来“有缩进”,但这是合法的:

> # Heading in Block Quote

规则信息一览:

项目值
规则名MD023
别名heading-start-left
标签headings、spaces
是否可自动修复是(部分违规可由工具修复)
修复类型删除标题前的缩进字符(editColumn+deleteCount)

设计动机:标题如果带有前导空格缩进,Markdown 解析器不会把它当作标题,而会渲染为普通文本(Rationale 原文即指出此点),这会造成文档结构与视觉预期的偏差。

二、从规则源码看检测原理

规则实现在 lib/md023.mjs,其核心逻辑基于 micromark 解析器产生的 token 流:

const headings = filterByTypesCached([ "atxHeading", "linePrefix", "setextHeading" ]);

检测条件由三部分组合而成(lib/md023.mjs#L14-L34):

  1. 前一个 token 是linePrefix(行前缀,即行首的空格/缩进);
  2. 后一个 token 不是linePrefix(说明该行确实以标题开始);
  3. 前后两个 token 位于同一行(startLine相同)。

命中后调用addErrorContext记录违规,并携带editColumn(起始列)与deleteCount(删除字符数)作为fixInfo,供自动修复使用。linePrefixtoken 的endColumn - startColumn即为缩进长度。

关于 token 过滤与缓存:filterByTypesCached定义于 lib/cache.mjs,底层委托给 helpers/micromark-helpers.cjs 的filterByTypes,并在规则执行期间按参数做缓存,避免多条规则重复遍历 token 流。

三、测试文件逐行解读:哪些缩进会被判违规

测试文件 test/headings_with_spaces_at_the_beginning.md 全文件仅 25 行,却覆盖了 MD023 的 4 个命中点与 2 类豁免场景。

3.1 ATX 标题带 1 个空格(第 3 行)

# Heading 1 {MD023}

第 1 列是空格,linePrefix长度为 1,随后同一行出现atxHeading→ 触发 MD023。快照中对应错误(test/markdownlint-test-scenarios.mjs.snapshot):

{ "lineNumber": 3, "ruleNames": [ "MD023", "heading-start-left" ], "errorContext": " # Heading 1 {MD023}", "errorRange": [ 1, 1 ], "fixInfo": { "editColumn": 1, "deleteCount": 1 } }

3.2 Setext 标题:两条下划线一行缩进一行不缩进(第 5–6、8–9 行)

Setext 标题由“文本行 + 下划线行”构成,MD023 对下划线行的缩进同样敏感:

Setext style fully indented {MD023} ===================================

文本行与下划线行都带 1 个空格缩进 → 触发。快照中第 5 行错误(test/markdownlint-test-scenarios.mjs.snapshot#L14842-L14861)的fixInfo为{ "editColumn": 1, "deleteCount": 1 }。

对比第 8–9 行:

Setext style title only indented {MD023} =========================================

文本行缩进、下划线行顶格。快照显示第 8 行仍被判定违规(test/markdownlint-test-scenarios.mjs.snapshot#L14862-L14881),因为setextHeading的起始位置取决于文本行是否带前导缩进;而下划线行顶格并不会“抵消”违规。反过来说,如果文本行顶格而下划线行缩进,同样会触发——两条线必须都顶格才算合规。

3.3 列表项内嵌套标题(第 19–20 行):与 MD022 的联动

* This is another case where MD023 shouldn't be triggered # Test {MD022} Valid heading for CommonMark (see section 5.2) # Test {MD022} {MD023} Also valid heading for CommonMark

这是本测试文件中信息量最大的部分,需要结合 CommonMark 规范 5.2 节来理解:

  • 第 19 行:列表项内缩进 2 个空格后的# Test ...。按 CommonMark,标题位于列表项内容内部,是合法嵌套,因此MD023 不触发;但因为其上方/下方缺少空行,触发MD022(标题周围应有空行)。快照显示 MD022 给出两条错误(Above/Below,test/markdownlint-test-scenarios.mjs.snapshot#L14772-L14805),fixInfo均为insertText: "\n"。
  • 第 20 行:缩进 4 个空格(# Test ...)。此时标题不仅缺少空行(MD022),而且缩进已超过列表内容所需的缩进级别,linePrefix与atxHeading出现在同一行且无其他 token 隔断,因此MD023 同时触发。快照中第 20 行的 MD023 错误为(test/markdownlint-test-scenarios.mjs.snapshot#L14882-L14901):
{ "lineNumber": 20, "errorContext": " # Test {MD022} {MD023} Als...", "errorRange": [ 3, 2 ], "fixInfo": { "editColumn": 3, "deleteCount": 2 } }

注意errorRange从第 3 列开始、长度为 2——修复只删除多出的 2 个空格,保留列表项内容缩进所需的 2 个空格,避免破坏列表嵌套结构。这正是 markdownlint 自动修复“最小干预”设计的一个体现。

3.4 豁免场景:代码块中的“井号”注释(第 11–16 行)

* Test situations in which MD023 shouldn't be triggered. ```rb # This shouldn't trigger MD023 as it is a code comment. foo = "And here is some code"
代码栅栏内的 `# ...` 是代码注释,不是 Markdown 标题。micromark 会将其解析为 `codeFenced` 内容而非 `atxHeading`,因此 MD023 的 token 过滤天然将其排除——快照中该区域没有任何错误。这一场景说明:**MD023 的判定完全依赖解析结果(token 类型),而不是简单的正则匹配**,这也是本项目采用 micromark 解析器(`"parser": "micromark"`,见 [lib/md023.mjs#L11](https://link.gitcode.com/i/8b8fd0e261275647a187c029d8a20313#L11))的根本原因。 ### 3.5 行内配置指令:关闭相邻规则的干扰(第 22–25 行) 测试文件末尾通过 HTML 注释形式声明了行内配置: ```markdown <!-- markdownlint-configure-file { "heading-style": false, "first-line-heading": false } -->

该指令关闭了heading-style(MD003,ATX 与 Setext 标题风格一致性)与first-line-heading(MD041,首行应为一级标题),使得测试可以专注验证 MD023/MD022,而不会被文件首行Some text触发 MD041、也不会因混合 ATX/Setext 风格触发 MD003。这种markdownlint-configure-file行内配置是仓库测试用例的常用手法,也适用于实际项目按文件粒度定向开关规则。

四、自动修复:applyFixes 与 fixInfo 的完整链路

修复能力由fixInfo提供,最终通过applyFixes落地。测试框架 test/markdownlint-test-scenarios.mjs 展示了完整验证链路:

  1. 用lint()检测得到errors(其中包含fixInfo);
  2. 调用applyFixes(content, errors)生成修复后文本;
  3. 对修复后文本再次lint(),断言不再残留任何带fixInfo的错误;
  4. 同时校验:凡出现fixInfo的规则名必须登记在 lib/constants.mjs 的fixableRuleNames中(MD022、MD023 均在列)。

快照中fixed字段展示了本测试文件的修复结果(test/markdownlint-test-scenarios.mjs.snapshot#L14903):

  • 第 3、5、8 行的前导空格被删除(deleteCount: 1);
  • 第 20 行删除第 3 列起的 2 个多余空格(editColumn: 3, deleteCount: 2);
  • MD022 在第 19、20 行标题上方插入空行(insertText: "\n")。

注意第 5 行的 Setext 下划线行:修复后下划线仍保留 1 个前导空格(===================================),因为该违规标记在第 5 行文本行上,fixInfo只针对文本行,下划线行并不单独计错——这也解释了快照中第 6 行没有单独错误条目的原因。修复后标题变为普通文本行,MD023 对后续下划线行的缩进不再追究,这是规则“基于 token 同源判定”的又一佐证。

五、与相邻规则的边界划分

MD023 常与以下规则在同一行同时出现,实际使用中需要区分职责:

规则检查内容本测试文件中的体现
MD022blanks-around-headings标题上/下方是否缺少空行第 19、20 行,每个标题报 Above/Below 两条
MD023heading-start-left标题是否带前导空格缩进第 3、5、8、20 行
MD003heading-styleATX 与 Setext 风格是否混用被行内配置关闭以隔离干扰
MD041first-line-heading文档首行是否为一/二级标题被行内配置关闭(文件首行为Some text)

MD022 的实现位于 lib/md022.mjs,其lines_above/lines_below参数支持按标题级别(1–6)分别配置空行数量(整数或整数数组,-1表示任意数量),并通过getBlockQuotePrefixText(helpers/micromark-helpers.cjs)在插入空行时自动带上块引用前缀,保证修复不破坏块引用结构。这与 MD023 的deleteCount修复思路同源:修复必须尊重文档原有的结构上下文。

六、实战建议与验证方式

  1. 顶格是硬性要求:无论 ATX(#)还是 Setext(===/---),标题起始行与下划线行都应从第 1 列开始,除非位于块引用(>)内。
  2. 列表内标题先补空行再谈缩进:列表项内的标题常见问题其实是 MD022(缺空行)而非 MD023;只有当缩进超过列表内容所需缩进量时,MD023 才会介入,此时自动修复只会删除多余的缩进。
  3. 代码块注释不会误报:因为 MD023 基于 micromark 的 token 类型判定,栅栏代码、行内代码中的#内容天然豁免。
  4. 本地复现:可直接运行node test/harness.mjs test/headings_with_spaces_at_the_beginning.md查看lint的完整结果对象(test/harness.mjs),或用--profile观察 1000 次 lint 的耗时;运行node --test test/markdownlint-test-scenarios.mjs可执行全部场景测试并校验快照与自动修复闭环。

结语

MD023 表面上是“去掉标题前的空格”这么简单,但通过 test/headings_with_spaces_at_the_beginning.md 这个 25 行的小样本可以看到:真正的工程复杂度在于区分“结构缩进”(列表、块引用)与“非法缩进”、在自动修复时只删除多余字符,以及与 MD022、MD003、MD041 等规则的职责协同。理解 token 级别的判定方式,你就能在任何 Markdown 文档中准确预测并快速修复标题缩进问题。

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

【免费下载链接】markdownlint

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

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

相关推荐

上一篇:XGP-save-extractor 使用指南:3 分钟提取 Xbox Game Pass 存档
下一篇:抖音无水印批量下载怎么做:新手 5 分钟上手的完整指南

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

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

STM32CubeMX+FatFs:SD卡文件系统完整移植与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 6:12:24

用kubeadm快速搭建Kubernetes集群:从初始化到生产实践

很多朋友问我&#xff0c;到底怎么把一套 kubernetes 集群在最短时间内搭起来&#xff0c;既不是那种只能在笔记本上跑的单机玩具&#xff0c;也不是需要手动维护几十个二进制服务的硬核模式。我的答案一直是同一个&#xff1a;kubeadm。它是 Kubernetes 官方提供的集群初始化工…

作者头像 李华
网站建设 2026/10/16 6:11:48

C#实现语音识别与朗读,打造离线本地语音助手

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 6:11:33

Apollo-11式高可靠系统设计:实时容错与资源约束下的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华