news 2026/10/15 20:59:58

markdownlint 规则 MD021 深度解析:closed atx 标题内多余的井号间距

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
markdownlint 规则 MD021 深度解析:closed atx 标题内多余的井号间距
  • 开发工具
  • 代码质量
  • Lint
  • 静态分析

【免费下载链接】markdownlint

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

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

本文围绕 markdownlint 仓库中的 MD021 规则文档 展开,系统讲解该规则如何识别并修复「闭合式 ATX 标题(closed atx style heading)」中标题文本与井号之间的多余空格,并结合 规则实现源码、micromark 解析辅助函数 与测试快照,说明其判定逻辑、双向扫描机制与自动修复能力。读完本文,你将能准确理解 MD021 的触发边界、与 MD018/MD019/MD020 的规则家族关系,并能在实际项目中正确配置、排除或利用其自动修复能力。

规则速览

MD021 是 markdownlint 中一条针对标题书写的风格检查规则,其完整元信息定义于 doc/md021.md,同时汇总在 doc/Rules.md 与 README.md 的规则清单中:

  • 规则编号:MD021
  • 别名(Alias):no-multiple-space-closed-atx
  • 标签(Tags):atx_closed、headings、spaces
  • 可自动修复:部分违规可通过工具自动修复
  • 规则描述:闭合式 ATX 标题中,井号(#)与标题文本之间存在多个空格

这里的「closed atx style heading」指同时具备开头井号与结尾闭合井号的 ATX 标题,例如# Heading 1 #、## Heading 2 ##。它与仅以开头井号标记的普通 ATX 标题(如# Heading 1)以及 Setext 标题(用===/---下划线标记)是三种不同的标题风格,具体区分逻辑见下文源码分析。

触发条件:什么时候会报 MD021

按照 doc/md021.md 的定义,当闭合式 ATX 标题中任一侧(开头井号之后、或结尾闭合井号之前)出现多于一个空格时,该规则即被触发:

# Heading 1 # ## Heading 2 ##

上面两行中,#与Heading 1之间、Heading 1与结尾#之间均插入了两个空格,这两行都会产生 MD021 违规。

需要注意的要点:

  1. 两侧独立判定:规则会同时检查开头与结尾两侧,只要任意一侧有多余空格就会报告;
  2. 两侧同时违规时报告多次:当标题两侧同时包含多个空格时,同一行会报告两条独立错误(分别对应开头侧与结尾侧);
  3. 单个空格是唯一合法形式:检查基准是「恰好一个空格」,零个空格或两个及以上空格都不符合要求。

零空格与多个空格分别由不同规则覆盖:缺少空格对应 MD020 规则(no-missing-space-closed-atx),多余空格对应本文的 MD021。

正确写法与修复方式

将标题文本与井号之间统一为单个空格即可消除违规:

# Heading 1 # ## Heading 2 ##

在 markdownlint 中,MD021 属于「Fixable: Some violations can be fixed by tooling」的规则。这意味着通过官方提供的applyFix/applyFixes工具函数,可以自动删除多余的空格(详见下文「自动修复机制」一节),无需手工逐个清理。

边界情况:多空格数量的差异

从规则实现看,MD021 对多余空格的数量不做上限区分——只要空白 token 长度大于 1 就会触发,2 个空格、3 个空格乃至更多均属违规。这一行为可以从测试用例 test/atx_closed_heading_spacing.md 中得到印证:

## Heading 4 {MD021} ## ← 开头侧 2 个空格 ## Heading 5 {MD021} ## ← 结尾侧 2 个空格 ## Heading 6 {MD021} ## ← 两侧各 2 个空格,报告两条错误 ## Heading 7 {MD021} ## ← 两侧各 3 个空格,报告两条错误

而同一测试文件中的这些写法不会触发 MD021:

## Heading 8\# ← 结尾井号被转义,不是闭合井号 ## Heading 9 \# ## Heading 10 \#

可以看到,只有真正的闭合井号(未转义、与开头井号成对)之前的多余空格才会被检查;带反斜杠转义的\#属于标题文本的一部分,不受 MD021 约束。

源码实现:双向扫描的空白校验

MD021 的实现位于 lib/md019-md021.mjs,它与 MD019(普通 ATX 标题开头的多余空格)共用同一个校验函数validateHeadingSpaces,区别仅在于扫描方向与过滤条件:

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) ) { // ... 计算列号与长度,报告错误并附加 fixInfo } }

关键逻辑可以拆解为三步:

  1. 定位井号序列:在 heading token 的 children 中,沿delta方向(+1表示从开头向结尾、-1表示从结尾向开头)找到类型为atxHeadingSequence的节点,即该侧的井号串;
  2. 检查相邻空白:取井号序列紧邻的相邻 token,若其类型为whitespace且文本长度大于 1,判定为违规;
  3. 计算错误位置:以空白 token 的startColumn/endColumn计算出range(列号与长度),并通过addErrorContext上报。

MD021 的规则函数对每个闭合式 ATX 标题调用两次validateHeadingSpaces,一次delta = 1检查开头侧、一次delta = -1检查结尾侧,这正对应了文档中「either side」的说明:

function MD021(params, onError) { const atxClosedHeadings = filterByTypesCached([ "atxHeading" ]) .filter((heading) => getHeadingStyle(heading) === "atx_closed"); for (const atxClosedHeading of atxClosedHeadings) { validateHeadingSpaces(onError, atxClosedHeading, 1); validateHeadingSpaces(onError, atxClosedHeading, -1); } }

如何判定「closed atx」:getHeadingStyle

MD021 只作用于闭合式 ATX 标题,风格的判定复用 helpers/micromark-helpers.cjs 中的getHeadingStyle函数:

function getHeadingStyle(heading) { if (heading.type === "setextHeading") { return "setext"; } const atxHeadingSequenceLength = heading.children.filter( (child) => child.type === "atxHeadingSequence" ).length; if (atxHeadingSequenceLength === 1) { return "atx"; } return "atx_closed"; }

判定规则很直观:Setext 标题返回setext;其余 ATX 类标题中,atxHeadingSequence节点(井号串)的数量为 1 的是普通 ATX 标题,数量为 2(开头 + 结尾各一组)的才是atx_closed。MD019 与 MD021 正是通过这一函数分流:前者过滤出"atx"风格、只做单侧(开头)扫描,后者过滤出"atx_closed"风格、做双侧扫描。

自动修复机制

MD021 属于可修复规则,validateHeadingSpaces在报告错误时携带了fixInfo:

addErrorContext( onError, startLine, text.trim(), delta > 0, // 标记错误位于开头侧 delta < 0, // 标记错误位于结尾侧 [ column, length ], { "editColumn": column, "deleteCount": length } );

editColumn指向多余空白的起始列,deleteCount为需删除的空白长度。从 test/markdownlint-test-scenarios.mjs.snapshot 中atx_closed_heading_spacing.md的期望结果可以看到具体的修复参数:

  • 第 7 行## Heading 4 {MD021} ##:errorRange: [4, 1],fixInfo: { "editColumn": 4, "deleteCount": 1 };
  • 第 9 行## Heading 5 {MD021} ##:errorRange: [22, 1],fixInfo: { "editColumn": 22, "deleteCount": 1 };
  • 第 11 行## Heading 6 {MD021} ##:同时报告两条错误,errorRange分别为[4, 1]与[23, 1];
  • 第 13 行## Heading 7 {MD021} ##:同样两条错误,errorRange分别为[4, 2]与[24, 2](3 个空格需删除 2 个)。

在同一快照的fixed输出中,上述标题全部被规整为## Heading 4 {MD021} ##这类单空格形式。这说明自动修复是增量删除:保留第一个空格,仅删除多余部分,而不是整行重写。

实际应用时,可参照 README.md 的「Fixing」章节,用applyFixes把 lint 结果直接写回内容:

import { applyFixes } from "markdownlint"; import { lint as lintSync } from "markdownlint/sync"; const results = lintSync({ "strings": { "content": original } }); const fixed = applyFixes(original, results.content);

与其他相关规则的边界

MD021 并非孤立存在,它与 MD018、MD019、MD020 共同构成「ATX 标题与井号间距」的规则家族,容易混淆,这里给出对照:

规则别名标题风格检查位置违规形态
MD018no-missing-space-atx普通 ATX开头井号后缺少空格(#Heading)
MD019no-multiple-space-atx普通 ATX开头井号后多个空格(# Heading)
MD020no-missing-space-closed-atx闭合式 ATX两侧缺少空格(#Heading 1#)
MD021no-multiple-space-closed-atx闭合式 ATX两侧多个空格(# Heading 1 #)

其中 MD019 与 MD021 共享同一实现文件 lib/md019-md021.mjs(该文件同时导出两条规则,在 lib/rules.mjs 中解构注册);MD018 与 MD020 则分别在 lib/md018.mjs、lib/md020.mjs 独立实现。从仓库的模块组织方式可以看出,作者按「间距方向」分组共享逻辑:md019-md021.mjs处理「多个空格」,md020.mjs处理「缺少空格」。

测试验证与回归保障

仓库为 MD021 准备了多层测试验证:

  • 专项场景文件test/atx_closed_heading_spacing.md:集中构造了开头侧多空格、结尾侧多空格、两侧同时多空格、以及转义井号等边界用例,文件头部通过<!-- markdownlint-configure-file { "first-line-heading": false, "heading-style": false } -->内联配置排除了无关规则的干扰;
  • 快照断言test/markdownlint-test-scenarios.mjs.snapshot:由markdownlint-test-scenarios.mjs生成,精确记录了每条错误的lineNumber、errorRange与fixInfo,并对修复后的全文做逐字符比对,防止修复逻辑回归;
  • 综合用例test/MD011-MD021.md 与 test/atx-heading-spacing-trailing-spaces.md:将 MD021 与 MD018/MD019/MD020 混排在同一文件中,验证各规则互不误报;
  • 全规则回归test/break-all-the-rules.md 配合 test/markdownlint-test.mjs 中的styleAll测试,使用 style/all.json 配置跑全量规则,确认 MD021 在其第 31 行正常触发、无遗漏。

配置与使用建议

MD021 默认在 markdownlint 的完整配置中启用(它在 lib/rules.mjs 的rules数组中注册,运行时由库统一加载)。可按需进行以下操作:

  • 按编号或别名启用/禁用:在.markdownlint.json等配置文件中将"MD021": false或"no-multiple-space-closed-atx": false即可关闭;置为true或"error"恢复;
  • 行内临时控制:借助 markdownlint 的行内指令,例如<!-- markdownlint-disable MD021 -->禁用、<!-- markdownlint-enable MD021 -->恢复,或使用<!-- markdownlint-disable-next-line MD021 -->仅豁免下一行(测试文件中的markdownlint-configure-file注释即为行内配置的实例);
  • 自动修复流程:在 CI 或提交钩子中接入lintSync+applyFixes(见上文示例),让多余空格问题在保存或提交阶段自动消失,无需人工介入。

实践上的核心建议是:保持标题两侧与井号之间恰好一个空格。这一约定不仅让源码更整洁,也让标题文本在复制、搜索、以及嵌入其他工具链时行为可预期。由于多余的空白不影响渲染(这正是 doc/md021.md 中 Rationale 的表述:Extra space has no purpose and does not affect the rendering of content),MD021 本质上是一条纯风格规范,是否启用完全取决于团队约定;若你的文档风格允许对齐式的多空格标题,直接关闭该规则即可,不会产生功能影响。

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

【免费下载链接】markdownlint

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

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

相关推荐

上一篇:Lemmy:基于 ActivityPub 的 Rust 去中心化讨论平台——架构、特性与部署指南
下一篇:终极自动化指南:无需代码轻松实现电脑操作自动化

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

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

SAP库存调拨MB1B移动类型详解:从311/313到在途库存与转储订单

简介&#xff1a;一份系统讲解SAP MM模块库存调拨与发货流程的图文资料&#xff0c;面向SAP顾问、ERP实施人员及供应链相关从业者&#xff0c;帮助理解MB1B事务代码在库存地间、工厂间及状态转变场景中的实际用法。资料以单个Word文档形式提供&#xff0c;压缩包内共1个docx文件…

作者头像 李华
网站建设 2026/10/15 20:58:30

LeetCode 437路径总和III:前缀和与哈希表优化解法

1. 题目背景与核心问题LeetCode 437题"路径总和 III"是二叉树类问题中的经典题目&#xff0c;主要考察对树结构的遍历和路径统计能力。题目要求&#xff1a;给定一个二叉树的根节点和一个目标和&#xff0c;找出路径和等于给定值的路径数量。这里的路径不需要从根节点…

作者头像 李华
网站建设 2026/10/15 20:57:37

GPU环境搭建指南:Anaconda、CUDA、TF2.0与Keras兼容配置

简介&#xff1a;一套面向深度学习入门者的环境搭建教程PPT&#xff0c;聚焦 Anaconda、CUDA、cuDNN、TensorFlow-gpu 与 Keras 的版本匹配和完整安装链路。PPT 从 Anaconda 下载安装讲起&#xff0c;逐步演示 NVIDIA 显卡驱动查看、CUDA 10.0 与 cuDNN 的下载及环境变量配置&a…

作者头像 李华
网站建设 2026/10/15 20:56:58

VST3插件开发实战:从两段式架构到process()避坑指南

简介&#xff1a;Steinberg官方发布的VST 3插件SDK压缩包&#xff0c;面向桌面与移动端音频开发者&#xff0c;用于在Windows、macOS、Linux和iOS平台构建音频效果器、合成器及处理工具。资源共7个文件&#xff0c;整体约405KB&#xff0c;包含txt说明、pdf协议与指南、md说明文…

作者头像 李华
网站建设 2026/10/15 20:53:54

PlanQA: A Benchmark for Spatial Reasoning in LLMs using Structured Representations

文章主要内容总结 背景与目的:现有大型语言模型(LLMs)在结构化推理方面表现出较强能力,但在物理环境中的空间推理能力仍待探索。在建筑设计、辅助规划等实际应用中,空间理解依赖JSON等结构化格式(包含物体位置、尺寸、方向等信息),而非图像或自然语言。因此,本文提出P…

作者头像 李华
网站建设 2026/10/15 20:52:56

FIR滤波器设计全攻略:从线性相位到窗函数与工程落地

1. 从一段真实经历说起&#xff1a;为什么 FIR 设计没那么简单我接过不少数字信号处理相关的任务&#xff0c;设计 FIR 滤波器算是其中出现频率最高的需求之一。很多人一开始觉得这事简单&#xff0c;MATLAB 里一行fir1或者 Python 里一句signal.firwin就完事了。但实际做起来&…

作者头像 李华