☰
markdownlint 列表嵌套规则解析:从 `test/lists-with-nesting.md` 场景看 MD004 / MD007 / MD029 / MD032 的判定逻辑
2026/10/16 2:53:28 网站建设 项目流程
  • 开发工具
  • 代码质量
  • Lint
  • 静态分析

【免费下载链接】markdownlint

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

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

导读

本文以 markdownlint 仓库中的场景测试文件test/lists-with-nesting.md为切入点,逐行剖析无序列表与有序列表在嵌套时触发MD032(列表周围空行)、MD007(无序列表缩进)、MD004(无序列表符号风格)、MD029(有序列表编号前缀)四类规则违例的完整判定过程。读完本文,你将掌握 markdownlint 列表嵌套的"正确缩进量"标准、测试夹具中{MD###}标记的语义与自动断言机制,以及如何对照规则源码解释一次 lint 报告中的每一条错误。

一、文档定位:一个"自带答案"的列表嵌套测试场景

test/lists-with-nesting.md不是普通文档,而是一个场景化测试夹具(test fixture)。它把无序/有序列表的嵌套情况按四组组织:

  • Excessive nesting(过度缩进)
  • Insufficient nesting(缩进不足)
  • Correct nesting, same type(同类型列表的正确嵌套)
  • Correct nesting, different types(不同类型列表的正确嵌套)

该文件与test/目录下其他.md文件一样,由test/markdownlint-test-scenarios.mjs统一驱动。测试逻辑(见 test/markdownlint-test-scenarios.mjs 第 19~106 行)分为三步:

  1. 用lint({ "files": [ file ] })对文件执行完整检查;
  2. 用正则/\{(MD\d+)(?::([-+]?)(\d+))?\}/g解析文件中内联的{MD032}这类标记,把它们声明为预期违例(可带行号偏移,如{MD031:-1}表示"上一行触发 MD031");
  3. 断言实际 lint 结果与标记完全一致(多一条、少一条都算失败),并比对修复后的快照(test/markdownlint-test-scenarios.mjs.snapshot)。

因此,文档中每一处{MD032}、{MD007}等标记都是经过完整 lint 验证的官方预期结论,直接可作为学习列表嵌套规则的权威答案。

二、过度缩进(Excessive nesting):Markdown 语法合法 ≠ lint 通过

- one 1. two 1. three - four 1. one - two - three 1. four

第一组中,无序列表项- one下挂了缩进 3 个空格的1. two、1. three;第二组中,有序列表项1. one下挂了缩进 4 个空格的- two、- three。

这一组完全没有{MD###}标记,含义是:这些写法虽然缩进量"偏大"(常见规范要求 2 空格),但MD007默认只要求子列表缩进为配置值(默认 2 空格)的整数倍。结合 lib/md007.mjs 第 21~23 行与第 57~58 行可以看到核心计算:

const indent = Number(params.config.indent || 2); const expectedIndent = baseIndent + (startIndented ? startIndent : 0) + (nesting * indent);

expectedIndent等于「嵌套层级 × 配置缩进」;MD007只对每个无序列表项前缀的实际列位置与期望位置的差值报错,并不校验"是否恰好等于最小值"。所以- one下缩进 3 空格、1. one下缩进 4 空格都属于"合法但偏多"的嵌套,测试断言为 0 违例。

提示:若想让这种写法也被报出,需配置MD007: { "indent": 4 }配合"每级必须恰好 4 空格"的编辑规范;从源码看,规则只比较实际缩进与期望缩进是否相等,配置为 4 时 3 空格子项即会被报为Expected: 4; Actual: 3。

三、缩进不足(Insufficient nesting):同一行可能同时违反多条规则

- one {MD032} 1. two {MD032} - three {MD032} 1. one {MD032} - two {MD007} {MD032} 1. three {MD032}

这是全文违例密度最高的一段,需要逐行拆解:

  • {MD032}× 6:MD032(blanks-around-lists)要求每个顶层列表前后都必须有空行(文档首尾除外)。这里段落之间只有 1 个空行分隔列表,第 17 行- one与第 18 行1. two、第 22 行- two等行彼此直接相邻,被判定为"列表间缺少空行"。查看 lib/md032.mjs 第 24~47 行:规则只遍历顶层列表(filterByPredicate找listOrdered/listUnordered),检查其startLine的上一行与"视觉末尾"的下一行是否为空白行,每个缺失处生成一条addErrorContext。
  • {MD007}× 1(第 22 行):- two仅缩进 1 空格,低于默认 2 空格,MD007报Expected: 2; Actual: 1。注意它与{MD032}同时出现——markdownlint 各规则彼此独立,同一行可携带多条违例,快照中第 22 行因此有 4 条 error(2 条 MD032 + 1 条 MD007,且另有上下文重复计次)。
  • 第 18 行1. two未标{MD007}:因为它是有序列表项,而MD007只针对无序列表(源码第 8~9 行的unorderedListTypes仅含blockQuotePrefix、listItemPrefix、listUnordered,且嵌套计算遇到listOrdered祖先会置为-1跳过,见 lib/md007.mjs 第 40~47 行)。

对照快照(test/markdownlint-test-scenarios.mjs.snapshot 第 32030~32160 行),本段共产生 11 条 error,全部对应上述标记,验证了"一处文本、多条规则并发判定"的行为。

四、同类型列表的正确嵌套:层级递进且对齐

- one - two - three - four - five - six - seven 1. one 1. two 1. three 1. four 1. five 1. six 1. seven

本组展示了零违例的两类写法:

  • 无序列表:每层缩进恰好 2 空格(- two在 2 列、- six在 6 列、- seven在 8 列),且全程统一使用-符号,同时满足MD007(缩进=2×层级)与MD004(风格一致);
  • 有序列表:1. one→ 缩进 3 空格1. two(4 列)→ 缩进 6 空格1. five(7 列)→ 缩进 9 空格1. seven(10 列)。每级恰好"列表标记位数(2 列)+ 1 空格 + 层级",且编号全部从1重新开始,符合MD029默认one_or_ordered风格中"子列表可重置为 1"的规则。

为什么有序子列表缩进 3 空格不算 MD007 违例?关键在 lib/md007.mjs 第 8~9 行:MD007的 token 集合只包含listUnordered类型;第 43~45 行更明确——当向上查找父级时若遇到listOrdered,nesting被置为-1,即该子列表的缩进完全不参与 MD007 判定(有序列表参与的是MD030的标记后空格规则)。文档注释也直接写明:"本规则仅在父列表全部为无序列表时才作用于子列表,否则有序列表的额外缩进会干扰规则判断"(见 doc/md007.md)。

五、不同类型列表的正确嵌套:类型切换的缩进规律

- one 1. two 1. three - four 1. one - two - three 1. four

无序列表项内嵌有序列表(缩进 2 空格、子项在 3 列),或有序列表项内嵌无序列表(缩进 2 空格、子项在 3 列),均无标记。这说明 markdownlint 对"类型不同的父子列表"不要求 4 空格对齐,2 空格即可成立——与 CommonMark 的"列表项内容从标记后 1 空格处开始"规则一致。此处1.后接- two缩进 2 空格(3 列),正好让子列表内容起点与父项文本two的起点(第 3 列)对齐。

六、深层混合嵌套:交替类型直至 8 层

- one 1. two - three 1. four - five 1. six - seven 1. eight 1. one - two 1. three - four 1. five - six 1. seven - eight

两组分别以-与1.开头,交替嵌套 8 层,全程零违例。规律是:当嵌套类型切换时,子项从父项标记后 1 空格处开始缩进(即1.子项缩进 2 空格、-子项在交替后同样按 2 空格递进);而MD007只对"纯无序链"生效,类型一旦掺入有序列表即跳过,因此这种紧凑的混合缩进完全合法。

七、同层内的"标记堆叠":* 1. Item组合陷阱

* 1. Item {MD004} {MD007} * 2. Item {MD004} {MD007} {MD029} * 3. Item {MD004} {MD007} {MD029}

最后三行是全文最"隐蔽"的用例:* 1. Item中*与1.之间用空格分隔,会被 CommonMark 解析为无序列表项内嵌有序列表,而非"无序列表 + 文本"。由此同时触发:

  • MD004(Expected: dash; Actual: asterisk):全文其余无序列表使用-,按照consistent默认风格,*与-不一致。源码见 lib/md004.mjs 第 50~52 行——consistent风格以第一个遇到的无序列表符号为准。修复建议是将其替换为-(快照中fixInfo.insertText即为"-")。
  • MD007(Expected: 0; Actual: 1):第 97 行*前有 1 个空格缩进,作为顶层无序列表项其期望缩进为 0。
  • MD029(仅第 98、99 行):* 2. Item与* 3. Item的子有序列表编号递增但未从 1 开始。结合 lib/md029.mjs 第 40~50 行:规则取每个listOrdered的前两个前缀值判断是否"递增模式"——2、3不满足"第二个等于 1 且第一个为 0",于是按ordered风格要求从1/2/3开始,第 98 行报Expected: 1; Actual: 2,第 99 行报Expected: 1; Actual: 3。而第 97 行的子列表只有一个1.,长度不足 2 不会触发递增判断,故无 MD029。

该段 9 行内堆叠 8 条 error(快照第 31837~32027 行逐条可查),是所有列表相关规则在"符号 + 缩进 + 编号 + 空行"四维度同时作用的最佳演示。

八、把标记解读为"诊断说明书":四个规则的行为边界

综合整个文件,可提炼出四规则的判定边界(均以默认配置为准,规则参数见 doc/md004.md、doc/md007.md、doc/md029.md、[doc/md032.md)):

规则别名检查对象默认参数核心判定
MD004ul-style无序列表符号style: consistent(可选asterisk/dash/plus/sublist)全文符号风格一致
MD007ul-indent无序列表缩进indent: 2、start_indent: 2、start_indented: false每层缩进 = 层级 × indent,仅作用于父级全为无序的链
MD029ol-prefix有序列表编号style: one_or_ordered(可选one/ordered/zero)前缀从 1 或 0 开始,或按 1/2/3 递增
MD032blanks-around-lists所有顶层列表无参数列表前后必须有空行(文档首尾除外)

值得注意的边界细节:

  1. MD007 不检查"缩进过多":只要实际缩进等于nesting × indent即通过,4 空格/6 空格嵌套不报错(Excessive nesting组验证);
  2. MD007 忽略含有序父级的链:混合类型嵌套完全交给缩进对齐直觉,规则主动让路(Correct nesting, different types组验证);
  3. MD032 只盯顶层列表:子列表间的空行不归它管,由MD031(围栏代码块)、MD028(引用块空行)等规则各司其职;
  4. MD029 的递增判断基于"每张有序列表":子列表编号重置为 1 不违规,但2/3/4这类"中间断档"会被要求改回1(* 2. Item组验证)。

九、如何复现与扩展:运行测试与编写自己的场景

复现本文全部结论,在仓库根目录执行:

node --test test/markdownlint-test-scenarios.mjs

该命令会对test/*.md全部场景执行断言,lists-with-nesting.md正是其中之一;如需单独查看原始 lint 输出(含fixInfo修复建议),可参照 test/harness.mjs 的用法:

node test/harness.mjs test/lists-with-nesting.md

编写自己的列表场景时,遵循三步:

  1. 在test/下新建.md文件,正文直接使用实际 Markdown 内容;
  2. 在预期违例行的行内追加{MD###}标记,需要声明"上一行/下一行"时使用{MD###:-1}/{MD###:+1},需要精确指定行号时使用{MD###:行号}(语法定义见 test/markdownlint-test-scenarios.mjs 第 40~63 行);
  3. 运行上述测试命令,测试框架会自动比对实际结果并生成/校验快照。

若以编程方式直接调用,可参考 README.md 的Usage一节,使用 Promise 风格一次性检查多个文件:

import { lint as lintPromise } from "markdownlint/promise"; const results = await lintPromise({ "files": [ "test/lists-with-nesting.md" ] }); console.dir(results, { "colors": true, "depth": null });

返回对象按文件名分组,每条 error 包含lineNumber、ruleNames(如[ "MD007", "ul-indent" ])、errorDetail(如Expected: 0; Actual: 1)与fixInfo(可自动修复的编辑指令),格式详见 README.md 第 966~1003 行的示例。

十、总结

test/lists-with-nesting.md用 99 行文本完整覆盖了 markdownlint 对列表嵌套的四类核心判定:缩进量级(MD007,默认每层 2 空格)、符号风格(MD004,默认全文一致)、编号规则(MD029,默认 1/1/1 或 1/2/3)、空行隔离(MD032,顶层列表前后留白)。更重要的是,它以{MD###}标记形式把"正确答案"直接嵌入文档,配合 test/markdownlint-test-scenarios.mjs 的自动断言机制,形成了一份可运行、可回归的规则行为说明书——无论是排查自己文档中的列表报错,还是理解各规则在"类型混合、层级深嵌套"下的边界,这份场景文件都是最直观的参考起点。

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

【免费下载链接】markdownlint

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

项目地址:https://gitcode.com/gh_mirrors/ma/markdownlint
点击查看免费下载
上一篇:Logto 云片(Yunpian)短信连接器接入实战:从 API Key 申请到验证码登录全流程
下一篇:douyin-downloader 抖音批量下载指南:3 步搭好无水印增量下载

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询