markdownlint MD009 规则实战:用 list_item_empty_lines 放行列表项内空行的尾随空格

发布时间:2026/10/16 6:00:13
markdownlint MD009 规则实战:用 list_item_empty_lines 放行列表项内空行的尾随空格
开发工具代码质量Lint静态分析【免费下载链接】markdownlintA Node.js style checker and lint tool for Markdown/CommonMark files.项目地址https://gitcode.com/gh_mirrors/ma/markdownlint点击查看免费下载本文以 markdownlint 仓库中的规则测试夹具 test/trailing-spaces-in-lists-allowed.md 为主线完整讲解MD009/no-trailing-spaces规则在列表场景下的行为边界当开启list_item_empty_lines: true时哪些尾随空格被豁免、哪些仍会被报告并深入 lib/md009.mjs 源码剖析其 token 级判定逻辑。读完本文你将掌握 MD009 全部四个参数的语义与组合用法学会阅读仓库中{MD009:行号}测试标记机制并能在自己的项目中通过配置文件或markdownlint-configure-file内联注释精确控制列表空行检查策略。一、背景MD009/no-trailing-spaces 到底检查什么MD009别名no-trailing-spaces标签whitespace是 markdownlint 中检查“行尾多余空白”的核心规则。根据官方规则文档 doc/md009.md 的说明该规则会对任何以意外空白结尾的行触发。修复方式是删除行尾的空格。其设计理由Rationale非常明确除了用于制造换行hard break行尾空白没有任何用途也不会影响内容的渲染却会造成 diff 噪声、复制粘贴错位和编辑器的“幽灵字符”。因此绝大多数项目都会要求消除行尾空格但 Markdown 语法中恰恰有一个合法例外——行尾两个空格表示强制换行br这是br_spaces参数存在的意义。从源码角度看规则实现位于 lib/md009.mjs它基于 micromark 解析器parser: micromark产出的 token 流判断每一行是否处于代码块、列表项等特殊上下文再决定是否报告。这个“上下文豁免”机制正是本文要展开的核心。二、MD009 四个参数速览依据 doc/md009.md 的参数表MD009 共支持四个配置参数参数类型默认值作用br_spacesinteger2允许作为硬换行的尾随空格数量。必须设为 2才生效设为1时等价于0即完全不允许尾随空格code_blocksbooleanfalse是否检查缩进/围栏代码块内的尾随空格。默认关闭因为部分编程语言要求行尾保留空格list_item_empty_linesbooleanfalse是否放行“列表项内部空行”上的尾随空格部分解析器要求此类空行带缩进。即便strict: true也生效strictbooleanfalse是否把“恰好等于允许数量但没有实际产生硬换行”的尾随空格也视为违规即多余的行内断行规则说明中特别强调两点br_spaces的取值陷阱默认值为2即允许 2 个空格表示硬换行但把br_spaces设为1并不会得到“允许 1 个空格”而是与0行为一致——完全禁止。只有 2的取值才会真正开放豁免额度。strict的语义默认情况下即便行尾恰好用了允许数量的空格如段落末尾的 2 个空格且并未形成硬换行规则也不会触发strict: true会把这些“不必要的断行”一并报出。源码中这一分支对应lib/md009.mjs中strict (!paragraphLineNumbers.has(lineNumber) || codeInlineLineNumbers.has(lineNumber))的判定。MD009属于可自动修复Fixable规则报错时会附带fixInfoeditColumndeleteCountapplyFixes可直接删除违规的空格。三、核心夹具解析trailing-spaces-in-lists-allowed.md仓库的测试目录下存放了大量.md场景夹具test/trailing-spaces-in-lists-allowed.md 就是专门验证list_item_empty_lines: true行为的样例。文件末尾通过内联注释注入了本次测试的配置!-- markdownlint-configure-file { no-multiple-blanks: false, no-trailing-spaces: { list_item_empty_lines: true } } --其中no-multiple-blanks: false用于关闭MD012连续空行检查以便隔离观察MD009的行为no-trailing-spaces.list_item_empty_lines: true则是本次测试的主题。3.1 文件的真实内容含不可见空格该夹具的“正文”里藏着大量不可见尾随空格3、5、6 个不等下面用␣表示一个空格、$表示行尾完整还原 72 行内容# Heading$ $ 1. text$ text$ 1. text$ $ text$ 1. text$ ␣␣␣$ text$ 1. text$ text$ $ 1. text$ text$ ␣␣␣$ 1. text$ ␣␣␣$ {MD009:16}$ {MD009:18}$ $ 1. text$ text$ 1. text$ $ text$ 1. text$ ␣␣␣␣␣␣$ text$ 1. text$ text$ $ 1. text$ text$ ␣␣␣␣␣$ 1. text$ ␣␣␣␣␣$ 1. text$ - text$ $ text$ - text$ ␣␣␣␣␣$ text$ - text$ text$ $ - text$ text$ ␣␣␣␣␣$ {MD009:35}$ {MD009:37}$ {MD009:50}$ $ 1. text$ text$ ␣␣␣$ ␣␣␣$ 1. text$ ␣␣␣$ ␣␣␣$ {MD009:57}$ {MD009:58}$ {MD009:60}$ {MD009:61}$ $ !-- markdownlint-configure-file { no-multiple-blanks: false, no-trailing-spaces: { list_item_empty_lines: true } } --3.2 九处违规的逐行解读文件中用{MD009:16}这类行尾标记声明“预期在第 16 行报告一条 MD009”。去掉这些标记行和空行后真实违规分布如下行号尾随空格数所在上下文为何仍被报告163有序列表项第 14-16 行内容之后的空行空行位于列表项末尾该列表项最后一行内容之后183列表项第 17-18 行之后的空行同上属于列表项末尾的“尾部缩进”355嵌套列表项第 33-35 行末尾空行嵌套列表项末尾空行375嵌套列表项第 36-37 行末尾空行同上505无序列表项第 48-50 行末尾空行同上573列表项第 55-58 行末尾第一个空行末尾空行不受豁免583同一列表项末尾第二个空行同上no-multiple-blanks已被关闭故只观察 MD009603列表项第 59-61 行末尾空行同上613同一列表项末尾第二个空行同上与之形成对照的是被豁免的空行第 9 行3 空格位于列表项内容行 81. text与行 10text之间第 28 行6 空格位于嵌套列表项内容行 26-27 与行 29-30之间第 43 行5 空格位于无序列表项内容行 42 与行 44之间。这三处空行虽然带着尾随空格但因为它们“夹在”列表项内容中间属于列表项的缩进延续在list_item_empty_lines: true下全部被放行。结论一list_item_empty_lines只豁免“列表项内部内容之间的空行缩进”不豁免“列表项末尾的空行”。末尾空行即使缩进再正确也仍然报错——这与文档中“一些解析器要求列表项内空行带缩进但通常没有必要”的说明完全吻合。四、对照组默认行为下全部 8 处违规test/trailing-spaces-in-lists-default.md 是与上面内容几乎相同的场景但没有注入任何配置即list_item_empty_lines取默认值false其标记如下{MD009:9} {MD009:16} {MD009:18} {MD009:29} {MD009:36} {MD009:38} {MD009:44} {MD009:51}对照两张夹具的尾随空格行号带空格的行默认版默认行为同位置行允许版list_item_empty_lines: true行为9报错9豁免列表项内容之间16报错16仍报错列表项末尾18报错18仍报错列表项末尾29报错28豁免嵌套列表项内容之间36报错35仍报错嵌套列表项末尾38报错37仍报错嵌套列表项末尾44报错43豁免列表项内容之间51报错50仍报错列表项末尾两张夹具因第 22 行附近是否存在空行而整体相差一行因此同位置行号略有偏移。这一对比把规则开关的效果量化得清清楚楚默认开启 MD009 的项目里所有带尾随空格的空行都会触发告警开启list_item_empty_lines后恰好只有“内容之间的缩进空行”被放行边界处理与官方文档 doc/md009.md 中的示例一致。五、与 strict 叠加allowed-strict 变体test/trailing-spaces-in-lists-allowed-strict.md 与允许版夹具正文完全相同仅在内联配置中额外加入strict: true!-- markdownlint-configure-file { no-multiple-blanks: false, no-trailing-spaces: { list_item_empty_lines: true, strict: true } } --其标记与允许版完全一致{MD009:16}、{MD009:18}、{MD009:35}、{MD009:37}、{MD009:50}、{MD009:57}、{MD009:58}、{MD009:60}、{MD009:61}九处违规一一对应没有新增。原因在于strict的职责是揪出“恰好等于br_spaces数量默认 2 个却没有形成硬换行”的尾随空格而本夹具中所有尾随空格数量都是 3、5、6无一命中expected trailingSpaces分支同时被豁免的列表项内部空行在list_item_empty_lines: true下不受strict影响源码注释明确写道“even whenstrictistrue”。这验证了两个开关可以安全叠加既允许内容之间的缩进空行又不放过任何多余的行内断行。六、源码级原理md009.mjs 如何判定“列表项内空行”规则实现位于 lib/md009.mjs其判定流程分为三步。6.1 参数解析与代码块排除let brSpaces params.config.br_spaces; brSpaces Number((brSpaces undefined) ? 2 : brSpaces); const codeBlocks params.config.code_blocks; const includeCode (codeBlocks undefined) ? false : !!codeBlocks; const listItemEmptyLines !!params.config.list_item_empty_lines; const strict !!params.config.strict;随后当includeCode为false时规则会把codeFenced围栏代码块内部行与codeIndented缩进代码块全部行的行号收集进codeBlockLineNumbers集合这些行的尾随空格一律豁免——对应code_blocks: false的默认行为lib/md009.mjs。6.2 核心列表项豁免集合的构建这是本文的关键逻辑lib/md009.mjsconst listItemLineNumbers new Set(); if (listItemEmptyLines) { for (const listBlock of filterByTypesCached([ listOrdered, listUnordered ])) { addRangeToSet(listItemLineNumbers, listBlock.startLine, listBlock.endLine); let trailingIndent true; for (let i listBlock.children.length - 1; i 0; i--) { const child listBlock.children[i]; switch (child.type) { case content: trailingIndent false; break; case listItemIndent: if (trailingIndent) { listItemLineNumbers.delete(child.startLine); } break; case listItemPrefix: trailingIndent true; break; default: break; } } } }理解这段代码的关键是**“从后往前”遍历列表块的子 token**初始trailingIndent true表示当前处于“列表项末尾”状态每遇到一个listItemPrefix列表项标记如1.、-意味着进入一个新的列表项重置trailingIndent true遇到content真实内容说明内容之后还有内容trailingIndent false遇到listItemIndent列表项的缩进空行 token时如果trailingIndent仍为true即该空行位于某个列表项内容之后、再无内容就从豁免集合中删除该行号——这一行因此重新落入 MD009 的检查范围。反向遍历天然实现了“内容之后的空行不算豁免内容之间的空行保留豁免”的语义与第三、四节观察到的行为完全一致第 9、28、43 行内容之间被豁免第 16、18、35、37、50、57、58、60、61 行内容之后仍报错。6.3 主检测循环与报错信息最后的主循环lib/md009.mjs逐行统计尾随空格数量const expected (brSpaces 2) ? 0 : brSpaces; for (let lineIndex 0; lineIndex params.lines.length; lineIndex) { const line params.lines[lineIndex]; const lineNumber lineIndex 1; const trailingSpaces line.length - line.trimEnd().length; if ( trailingSpaces !codeBlockLineNumbers.has(lineNumber) !listItemLineNumbers.has(lineNumber) ( (expected ! trailingSpaces) || (strict (!paragraphLineNumbers.has(lineNumber) || codeInlineLineNumbers.has(lineNumber))) ) ) { ... addError(onError, lineNumber, Expected: (expected 0 ? : 0 or ) expected ; Actual: trailingSpaces, undefined, [ column, trailingSpaces ], { editColumn: column, deleteCount: trailingSpaces }); } }要点只有当expected ! trailingSpaces数量不符或strict下出现“多余断行”时才报错快照中的Expected: 0 or 2; Actual: 3正是br_spaces2、实际 3 个空格的产物报错信息附带errorRange与fixInfoeditColumndeleteCount因此applyFixes可以精准删除违规空格——这也是该规则标记为 Fixable 的实现基础当expected 0时消息变为Expected: ; Actual: N“0 or”前缀被省略。七、测试如何自动校验标记语法与快照断言这类夹具由 test/markdownlint-test-scenarios.mjs 自动驱动它枚举./test下所有*.md文件对每个文件执行lint({ files: [ file ] })然后做三件事test/markdownlint-test-scenarios.mjs解析标记用正则/\{(MD\d)(?::([-]?)(\d))?\}/g读取形如{MD009:16}的声明。MD009是规则名冒号后的数字表示绝对行号也支持{MD009:3}这类相对偏移写法/-前缀。标记通常集中写在文件末尾但通过绝对行号指向正文中的真实违规位置。断言比对把 lint 得到的实际违规行号集合与标记声明的期望行号集合做deepEqual严格比对任何多报、漏报都会让测试失败同时校验“带fixInfo的规则必须位于可修复规则名单”中。修复闭环验证对原内容执行applyFixes(content, errors)生成修复结果与errors一起写入快照 test/markdownlint-test-scenarios.mjs.snapshot随后对修复结果二次 lint断言“不存在仍带fixInfo的错误”确保自动修复一次到位。以允许版夹具的快照为例9 处错误的errorDetail均为Expected: 0 or 2; Actual: 3/6/5errorRange起始列 1、长度等于空格数fixed输出中第 16、18、35、37、50、57、58、60、61 行的尾随空格被删除行变成真空行而第 9、28、43 行豁免区的缩进空格原样保留——修复器精准遵守了配置边界。默认版快照则显示 8 处错误全部被修复。想手动复现这套验证可以直接运行# 运行全部场景测试含本夹具 npm test # 单独查看某个夹具的 token 结构与 lint 结果 node test/harness.mjs test/trailing-spaces-in-lists-allowed.mdtest/harness.mjs 会先打印该文件的 micromark token 树可以看到listOrdered、listItemPrefix、listItemIndent、content等节点正好对应 6.2 节的反向遍历逻辑再输出lint()的完整结果对象。八、在自己的项目中应用该配置8.1 配置文件方式在项目的.markdownlint.json或package.json的markdownlint.config字段仓库支持 JSON、YAML、TOML 等多格式配置解析中写入{ MD009: { br_spaces: 2, code_blocks: false, list_item_empty_lines: true, strict: false } }这样整个仓库的 MD009 都会放行列表项内容之间的缩进空行适用于需要兼容“列表项内空行必须带缩进”的解析器如某些渲染引擎的文档项目。8.2 文件级内联配置如果只想对单个文件生效可以在该 Markdown 文件任意位置放置markdownlint-configure-file注释README.md!-- markdownlint-configure-file { no-multiple-blanks: false, no-trailing-spaces: { list_item_empty_lines: true } } --相关语法约定与本文夹具完全同源该注释对整个文件生效与注释所处位置无关同一文件可写多个按从上到下顺序依次应用默认按 JSON 解析如需其他格式可借助options.configParsers扩展这也是仓库自身测试夹具 test/trailing-spaces-in-lists-allowed.md 使用的注入方式内联解析逻辑位于 lib/markdownlint.mjs 的configureFile/handleInlineConfig中。8.3 修复与落地MD009 可由工具自动修复。用 Node.js API 时import { applyFixes, lint } from markdownlint/promise; const results await lint({ files: [ docs/example.md ] }); const fixed applyFixes(await readFile(docs/example.md, utf8), results[docs/example.md]);applyFixes会依据每条错误的fixInfo删除违规空格被list_item_empty_lines豁免的空行缩进不会被动到。九、注意事项与最佳实践边界牢记list_item_empty_lines: true只豁免列表项内容之间的空行缩进列表项末尾空行上的尾随空格依旧报错。若你的解析器只要求“空行缩进”而不区分位置可考虑同时配合no-multiple-blanks: false观察整体效果。br_spaces不要设 11 与 0 等价都会彻底禁止尾随空格、连硬换行的 2 空格也被误杀需要放行硬换行就保持默认2或更大值。strict与list_item_empty_lines正交两者可安全叠加见第五节验证前者管“多余断行”后者管“列表空行缩进”互不干扰。代码块默认豁免code_blocks默认false即缩进/围栏代码块内的尾随空格默认不检查如需强制清除才显式置true。阅读测试夹具是理解规则最快的路径仓库中每个规则都配套大量test/*.md场景与快照 test/markdownlint-test-scenarios.mjs.snapshot{MD009:行号}标记本身就是可执行的断言文档配合 test/markdownlint-test-scenarios.mjs 的解析逻辑即可快速验证自己对规则行为的理解是否正确。赞分享开发工具代码质量Lint静态分析【免费下载链接】markdownlintA Node.js style checker and lint tool for Markdown/CommonMark files.项目地址https://gitcode.com/gh_mirrors/ma/markdownlint点击查看免费下载相关推荐markdownlint MD009 规则进阶用 code_blocks: true 检查代码块内的行尾空白markdownlint MD009 规则进阶用 code_blocks: true 检查代码块内的行尾空白 本文以 markdownlint 仓库中的场景测开发工具代码质量Lint静态分析markdownlint 硬换行Hard Line Break检测实战从 test/hard-line-breaks.md 读懂 MD009 尾随空格规则markdownlint 硬换行Hard Line Break检测实战从 test/hard line breaks.md 读懂 MD009 尾随空格规则开发工具代码质量Lint静态分析markdownlint MD009 尾随空格规则代码块与严格模式的完整配置实战markdownlint MD009 尾随空格规则代码块与严格模式的完整配置实战 导读 本文以 markdownlint 仓库中的 code block tr开发工具代码质量Lint静态分析上一篇3种实用方法在Mac上永久备份微信聊天记录的专业指南下一篇如何将Windows电脑变成免费无线热点VirtualRouter完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考