Prettier 如何格式化 Markdown 列表中的代码块:从测试用例到源码实现解析
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
本文以 Prettier 仓库中的 Markdown 格式化测试用例 tests/format/markdown/list/codeblock.md 为切入点,深入讲解 Prettier 在格式化"列表项内嵌套围栏代码块(fenced code block)"时的缩进对齐规则、空行压缩策略与围栏长度调整逻辑,并结合 src/language-markdown/print/code.js 与 src/language-markdown/print/list.js 的源码实现,说明这些行为背后的设计约束。读完本文,你将能准确预测 Prettier 对列表内代码块的格式化结果,并理解其"绝不把内容误判为缩进代码块"的核心设计原则。
一、测试用例定位:列表内代码块格式化的标准样本
在 Prettier 仓库中,Markdown 格式化行为通过"输入文件 + Jest 快照"的方式固化。本文关联的 codeblock.md 就是这样一个标准输入样本,它刻意构造了四组"列表项 + 嵌套代码块":
- 两个有序列表项(
1. ol01、2. ol02),每项后跟一个缩进 4 空格的js围栏代码块,代码块内部含有连续两个空行; - 两个无序列表项(
- ul01、- ul02),结构完全相同。
该样本专门用来回答一个问题:当代码块出现在列表项内部时,Prettier 会把它输出成什么样?答案是快照文件 tests/format/markdown/list/snapshots/format.test.js.snap(第 147–223 行为codeblock.md对应的输入/输出对照)。
驱动该测试的代码只有一行(tests/format/markdown/list/format.test.js):
runFormatTest(import.meta, ["markdown"], { proseWrap: "always" });其含义是:对list目录下全部.md输入文件,分别以markdown解析器(快照中同时标注parsers: ["markdown"])运行格式化,并固定启用proseWrap: "always"选项,其余选项(如printWidth: 80、tabWidth: 2)取默认值。runFormatTest由 tests/config/format-test-setup.js 注入全局。
二、输入与输出逐行对照:四个关键行为
把快照中的输入、输出并列,可以清楚看到 Prettier 对同一输入施加的四类改写:
| 行为 | 输入 | 输出 |
|---|---|---|
| 有序列表代码块缩进 | 4 空格 | 3 空格(与1.前缀右缘对齐) |
| 无序列表代码块缩进 | 4 空格 | 2 空格(与-前缀右缘对齐) |
| 代码块内连续空行 | const a = 1;后接 2 个空行 | 压缩为 1 个空行 |
| 围栏标记与语言 | ```js | 保持不变(3 个反引号 +js) |
输出片段(节选自 format.test.js.snap):
1. ol01 ```js const a = 1; const b = 2;ul01
const a = 1; const b = 2;
三个值得注意的细节: 1. **对齐基准是"列表标记的右缘"**:`1. ` 长度为 3,所以有序列表内的代码块缩进 3 空格;`- ` 长度为 2,所以无序列表内的代码块缩进 2 空格。代码块整体(包括开合围栏)与列表项首行文本对齐。 2. **列表项之间保持一个空行**:`ol01` 的代码块与 `ol02` 之间保留空行,保证两个列表项在视觉上独立。 3. **代码块内部的空行被压缩**:输入中代码块内存在的两个连续空行(这是作者故意放置的"不干净"内容),输出中被规整为单个空行,这是 Prettier 对代码块内空白的统一清理。 ## 三、源码实现(一):围栏长度与空行的生成逻辑 代码块的打印逻辑位于 [src/language-markdown/print/code.js](https://link.gitcode.com/i/67812743a3022b5fadb50d115c8095a3): - `printFencedCodeBlock`(第 26–44 行)负责生成围栏:先取节点内容(`mdx` 解析器下通过 `getFencedCodeBlockValue` 从原始文本还原,普通 `markdown` 直接用 `node.value`),再调用 `printCodeFences` 计算围栏。 - `printCodeFences`(第 10–24 行)决定围栏长度:`styleUnit.repeat(Math.max(3, getMaxContinuousCount(value, styleUnit) + 1))`。即**至少 3 个反引号;若代码内容中本身含更长的连续反引号序列,围栏长度会自动加长**(连续 N 个反引号 → 使用 N+1 个反引号闭合),避免围栏提前终止。同时它把内容中的换行统一替换为 `hardline`,使输出使用 Prettier 规范化后的换行符。 - 代码内容在输出前还会经过 `replaceEndOfLine` 处理,并最终通过 `align` 文档节点统一缩进——空行压缩行为即发生在文档构建阶段(连续 `hardline` 在打印层被折叠为单行,或由解析器清洗空行),这正是快照中"两个空行变一个"的实现基础。 ## 四、源码实现(二):列表内对齐为何是"恰好"而非"随意" 列表打印的核心在 [src/language-markdown/print/list.js](https://link.gitcode.com/i/dbf787607a1bd853601724ca56d0ca9f) 的 `printListItem`(第 95–117 行)。对列表项内的每个子节点,它计算一个对齐量: ```js const alignment = " ".repeat( clamp(options.tabWidth - listPrefix.length, 0, 3), // 4+ will cause indented code block ); return [alignment, align(alignment, print())];这里有两层约束,注释里写得很直白:
- 对齐宽度上限被 clamp 到 3:注释
// 4+ will cause indented code block表明,如果对齐空格达到 4 个及以上,CommonMark 解析器会把后续内容重新解析为缩进代码块(indented code block),彻底改变语义。因此 Prettier 宁可牺牲严格对齐,也绝不冒险超过 3 个空格。这解释了为什么有序列表项(前缀1.长 3)内代码块缩进恰好为 3、无序列表项(前缀-长 2)内缩进恰好为 2——这正是"对齐到标记右缘"与"不超过 3 空格"两个约束相交的结果。 - 对齐量还受
tabWidth影响:options.tabWidth - listPrefix.length意味着在tabWidth: 4且无序列表(前缀 2)时,对齐量会是min(2, 3) = 2;在默认tabWidth: 2时则取 0。换言之,代码块的精确缩进由列表前缀宽度与tabWidth共同决定。
此外,printList中的getPrefix(第 56–90 行)还会在列表被requiredIndent(第 125–143 行)判定需要更深缩进(例如列表后续紧邻缩进代码块)时,通过前后补充空格把前缀撑到足够宽度,但同样被限制在"前后各不超过 3/4 个空格"的安全范围内。
五、选项影响:tabWidth 与 proseWrap
tabWidth:快照目录 tests/format/markdown/list/tab-width/ 下的 indented-code-block.md 与对应快照专门验证了tabWidth: 4时嵌套列表与缩进代码块的缩进变化(嵌套项从 2 空格变为 4 空格,缩进代码块保持原缩进)。这说明tabWidth影响的是嵌套层级与对齐,而缩进代码块内容本身不重排。proseWrap:本次测试固定使用"always"(见 src/language-markdown/options.js 中对proseWrap的声明),它主要影响段落文本换行,对代码块内部不做折行处理——代码块内容始终按原样输出,仅做换行符与空行规整。
六、本地复现与验证
在仓库根目录执行 Jest 即可复现本文全部结论:
yarn jest tests/format/markdown/list也可单独针对该样本运行:
yarn jest tests/format/markdown/list -t codeblock若想观察真实 CLI 行为,用仓库内置的 prettier 格式化该文件:
yarn prettier tests/format/markdown/list/codeblock.md --parser markdown --prose-wrap always输出应与快照中的 output 部分完全一致。修改 codeblock.md 后运行yarn jest tests/format/markdown/list -u可更新快照(注意:仓库为只读研究环境,此处仅说明测试工作流)。
七、延伸阅读:同一目录下的相关样本
tests/format/markdown/list/目录还包含大量与"列表 + 代码块"场景相关的兄弟测试,可作为深入研究的入口:
- followed-by-indented-things.md:列表项后紧跟顶层/嵌套缩进代码块时
requiredIndent的判定; - indent.md:列表缩进与代码块、引用、嵌套列表的混合场景;
- issue-17652.md:代码块与嵌套列表的先后顺序对输出结构的影响;
- tab-width/indented-code-block.md:
tabWidth: 4下的回归样本。
结语
围绕 codeblock.md 这一个测试样本,可以完整还原 Prettier 处理"列表内代码块"的三条主线:对齐到列表标记右缘、代码块内空行与换行规整、以及绝不触发缩进代码块语义的对齐上限(3 空格)。理解 code.js 与 list.js 中的实现细节,能帮助你准确预测任意列表内代码块的格式化结果,也解释了为什么 Prettier 有时"看起来没对齐"——那是它为了保住 Markdown 语义正确性而做出的刻意取舍。
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考