Prettier Markdown 脚注定义格式化全解析:长段落折行、proseWrap 策略与源码实现
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
本文以 Prettier 仓库中的脚注定义格式化测试夹具
tests/format/markdown/footnoteDefinition/long.md为核心,深入剖析 Prettier 如何处理 Markdown 脚注定义(footnote definition)中的超长段落。读完本文,你将掌握proseWrap三种取值对脚注折行的实际影响、脚注内容固定 4 空格缩进的底层原理,以及如何通过源码与快照测试验证这些格式化行为。
一、脚注定义语法与测试夹具定位
Markdown 脚注(footnote)由两部分组成:正文中的脚注引用[^label]与文末的脚注定义[^label]: content。Prettier 在格式化 Markdown 时,会对脚注定义进行专门的排版处理。仓库中与之对应的测试目录是 tests/format/markdown/footnoteDefinition,包含四个输入夹具与配套快照:
| 文件 | 覆盖场景 |
|---|---|
| long.md | 超长段落脚注定义(含续行) |
| simple.md | 最短的单行脚注定义 |
| multiline.md | 包含代码块、引用块的脚注定义 |
| sibling.md | 大量相邻脚注定义与多行引用内容 |
这些夹具由 format.test.js 驱动,分别以proseWrap: "always"、proseWrap: "never"、proseWrap: "preserve"和tabWidth: 3四组选项运行格式化,输出结果全部记录在 format.test.js.snap 快照中。该测试通过仓库统一的格式测试基架(见 format-test-setup.js)执行。
二、long.md 场景还原:三种 proseWrap 模式的行为对比
long.md的输入极其简短,仅三行,但覆盖了两个典型形态:
[^hello]: this is a long long long long long long long long long long long long long paragraph. [^world]: this is a long long long long long long long long long long long long long paragraph. this is a long long long long long long long long long long long long long paragraph.其中[^hello]是单行定义;[^world]的第三行是前一行段落的续行(Markdown 语法中,同一段落可以跨多行源码),因此[^world]是一个跨两行的段落。这两种形态恰好触发了 Prettier 不同的格式化分支。
1.proseWrap: "always":一律块级排版并按 80 列折行
快照中long.md - {"proseWrap":"always"}的输出为:
[^hello]: this is a long long long long long long long long long long long long long paragraph. [^world]: this is a long long long long long long long long long long long long long paragraph. this is a long long long long long long long long long long long long long paragraph.关键行为:
- 标签与内容分行:两个脚注的
[^label]:都单独成行,正文缩进 4 个空格。 - 按 printWidth(默认 80)折行:正文在超过 80 列时自动换行,续行同样缩进 4 个空格。
- 续行并入同一段落:
[^world]的续行与首行合并为同一个段落后统一重排,不再保留源码的换行位置。
2.proseWrap: "never":全部内联为单行
[^hello]: this is a long long long long long long long long long long long long long paragraph. [^world]: this is a long long long long long long long long long long long long long paragraph. this is a long long long long long long long long long long long long long paragraph.此时无论内容多长都不折行:定义内容紧跟[^label]:后内联输出,[^world]的续行也被合并为一行,完全忽略 printWidth 限制。
3.proseWrap: "preserve":保留源码的换行形态
[^hello]: this is a long long long long long long long long long long long long long paragraph. [^world]: this is a long long long long long long long long long long long long long paragraph. this is a long long long long long long long long long long long long long paragraph.[^hello]在源码中是单行,输出保持内联单行;[^world]在源码中跨两行,输出转为块级形态:标签单独一行、正文按源码原有的行边界逐行保留,仅统一缩进 4 个空格,不按 80 列重新折行。
行为对照表
| proseWrap | 单行定义 | 跨行定义 | 是否按 80 列折行 | 标签位置 |
|---|---|---|---|---|
always | 块级(标签换行) | 块级,段落合并重排 | 是 | 独立一行 |
never | 内联单行 | 内联单行,段落合并 | 否 | 与内容同行 |
preserve | 内联单行 | 块级,保留原行边界 | 否 | 视源码形态而定 |
三、源码级原理:footnoteDefinition 的打印逻辑
上述行为并非黑盒魔法,其实现位于 src/language-markdown/print/mdast.js 的footnoteDefinition分支:
case "footnoteDefinition": { const shouldInlineFootnote = node.children.length === 1 && node.children[0].type === "paragraph" && (options.proseWrap === "never" || (options.proseWrap === "preserve" && node.children[0].position.start.line === node.children[0].position.end.line)); return [ printFootnoteReference(node), ": ", shouldInlineFootnote ? printChildren(path, options, print) : group([ align( " ".repeat(4), printChildren(path, options, print, { processor: ({ isFirst }) => isFirst ? group([softline, print()]) : print(), }), ), ]), ]; }逐一拆解:
1. 内联判定的三重条件(shouldInlineFootnote)
脚注定义只有在同时满足以下条件时才以内联形态输出:
- 定义体只有一个子节点,且类型是
paragraph(即纯段落,不含代码块、引用块等); proseWrap === "never",或proseWrap === "preserve"且该段落在源码中起始行号与结束行号相同(单行)。
这解释了long.md中的现象:always模式下永远走块级分支;never模式下只要体是单段落就内联;preserve模式下单行定义内联、跨行定义转为块级。
2. 固定 4 空格缩进(align(" ".repeat(4), ...))
块级分支通过align将子节点整体缩进 4 个空格——这是硬编码的,不随tabWidth变化。这正是快照中tabWidth: 3的用例输出与preserve完全一致、缩进仍为 4 空格的原因。
3. 首行软换行(group([softline, ...]))
块级分支对第一个子节点前置softline,并包在group中:若整体宽度放得下则softline折叠为空格(标签与内容同行,如multiline.md中较短的脚注),放不下则断行(标签单独成行,如long.md中的always输出)。
4. 段落折行引擎(fill)
块级正文的按宽度折行由 src/language-markdown/print/sentence.js 中的printSentence实现——它遍历段落的单词与空白节点,用fill(parts)组装文档,fill在超出 printWidth 时自动插入换行。因此always模式下的脚注正文与普通段落享受同一套折行机制。
四、相邻定义与多行内容的格式化细节
sibling.md与multiline.md补充了更复杂的场景:
相邻定义强制用空行分隔:无论哪种proseWrap模式,连续出现的多个脚注定义在输出中都会被空行隔开,避免定义块粘连(见 sibling.md 的快照输出)。
多行引用块:[^a]: > 123\与续行> 456这类多行 blockquote 在块级分支下,引用内容整体缩进 4 空格:
[^a]: > 123\ > 456含代码块的脚注体:当定义体不是单一段落(例如段落加代码块)时,shouldInlineFootnote恒为false,即使proseWrap: "never"也会走块级分支。此时短段落仍可与标签同行(group 不断行),而长段落会迫使 group 断行、标签独立成行,代码块内容按 Markdown 语法保持 4 空格缩进。完整输入输出可对照 multiline.md 与对应快照。
五、proseWrap 选项:定义、默认值与配置方式
proseWrap是影响脚注排版的核心开关,在 src/common/common-options.evaluate.js 中定义:
| 属性 | 值 |
|---|---|
| 类型 | choice(枚举) |
| 默认值 | "preserve" |
| 可选值 | "always"(超宽即折行)、"never"(不折行)、"preserve"(保持原样) |
它是 Prettier 的通用选项(Markdown 语言插件在 src/language-markdown/options.js 中直接复用),可通过配置文件或 CLI 指定:
// prettier.config.js export default { proseWrap: "always", // 或 "never" / "preserve" };prettier --prose-wrap always README.md官方选项说明可进一步查阅 docs/options.md。需要留意:proseWrap只影响段落类内容(含脚注定义正文)的折行,不影响代码块、表格等非散文内容。
六、如何复现与验证
若想在本地复现long.md的全部格式化行为,可直接对夹具运行 Prettier:
npx prettier --parser markdown --prose-wrap always tests/format/markdown/footnoteDefinition/long.md npx prettier --parser markdown --prose-wrap never tests/format/markdown/footnoteDefinition/long.md npx prettier --parser markdown --prose-wrap preserve tests/format/markdown/footnoteDefinition/long.md npx prettier --parser markdown --tab-width 3 tests/format/markdown/footnoteDefinition/long.md四组命令的输出应与 format.test.js.snap 中long.md的四个用例一一对应。该夹具由 format.test.js 通过runFormatTest执行,属于仓库统一的格式测试体系,任何对 mdast.js 中脚注打印逻辑的改动,都需要让这组快照保持通过,从而保证了脚注格式化行为的长期稳定。
总结
从三行测试输入到完整的格式化语义,long.md这一个夹具便覆盖了 Prettier Markdown 脚注定义格式化的核心规则:proseWrap决定内联还是块级、是否折行;纯段落且满足行数条件才可内联;块级内容固定缩进 4 空格且不随tabWidth变化;折行复用fill段落引擎。理解 mdast.js 中shouldInlineFootnote的判定与align/softline/group的组合方式,你就能准确预测任意脚注定义在 Prettier 下的输出形态。
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考