- 开发工具
- 代码质量
- Lint
- 静态分析
【免费下载链接】markdownlint
A Node.js style checker and lint tool for Markdown/CommonMark files.
导读
本文以 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 行)分为三步:
- 用
lint({ "files": [ file ] })对文件执行完整检查; - 用正则
/\{(MD\d+)(?::([-+]?)(\d+))?\}/g解析文件中内联的{MD032}这类标记,把它们声明为预期违例(可带行号偏移,如{MD031:-1}表示"上一行触发 MD031"); - 断言实际 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)):
| 规则 | 别名 | 检查对象 | 默认参数 | 核心判定 |
|---|---|---|---|---|
MD004 | ul-style | 无序列表符号 | style: consistent(可选asterisk/dash/plus/sublist) | 全文符号风格一致 |
MD007 | ul-indent | 无序列表缩进 | indent: 2、start_indent: 2、start_indented: false | 每层缩进 = 层级 × indent,仅作用于父级全为无序的链 |
MD029 | ol-prefix | 有序列表编号 | style: one_or_ordered(可选one/ordered/zero) | 前缀从 1 或 0 开始,或按 1/2/3 递增 |
MD032 | blanks-around-lists | 所有顶层列表 | 无参数 | 列表前后必须有空行(文档首尾除外) |
值得注意的边界细节:
- MD007 不检查"缩进过多":只要实际缩进等于
nesting × indent即通过,4 空格/6 空格嵌套不报错(Excessive nesting组验证); - MD007 忽略含有序父级的链:混合类型嵌套完全交给缩进对齐直觉,规则主动让路(
Correct nesting, different types组验证); - MD032 只盯顶层列表:子列表间的空行不归它管,由
MD031(围栏代码块)、MD028(引用块空行)等规则各司其职; - 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编写自己的列表场景时,遵循三步:
- 在
test/下新建.md文件,正文直接使用实际 Markdown 内容; - 在预期违例行的行内追加
{MD###}标记,需要声明"上一行/下一行"时使用{MD###:-1}/{MD###:+1},需要精确指定行号时使用{MD###:行号}(语法定义见 test/markdownlint-test-scenarios.mjs 第 40~63 行); - 运行上述测试命令,测试框架会自动比对实际结果并生成/校验快照。
若以编程方式直接调用,可参考 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.
相关推荐
markdownlint 列表解析边界场景详解:段落中的数字为何触发 MD029/MD030/MD032/MD004
markdownlint 列表解析边界场景详解:段落中的数字为何触发 MD029/MD030/MD032/MD004 markdownlint 是 Node.j
开发工具代码质量Lint静态分析Wand-Enhancer 本地补丁实操:WeMod 客户端离线增强怎么跑起来
Wand Enhancer 本地补丁实操:WeMod 客户端离线增强怎么跑起来 Wand Enhancer 是一个开源工具,在本地改写 WeMod(Wand)客
开发工具代码质量Lint静态分析markdownlint MD032/blanks-around-lists 规则详解:列表前后必须留空行
markdownlint MD032/blanks around lists 规则详解:列表前后必须留空行 导读 本文深入解析 markdownlint 的 M
开发工具代码质量Lint静态分析
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考