Prettier 引文块(Blockquote)中 prettier-ignore 的行为解析:测试用例与源码原理深度解读
2026/9/19 21:50:20 网站建设 项目流程
  • 开发工具
  • 格式化
  • CLI

【免费下载链接】prettier

Prettier is an opinionated code formatter.

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

在 Markdown 文档中,<!-- prettier-ignore -->注释用于跳过下一段内容的格式化,但当它出现在引文块(blockquote)嵌套代码围栏(fence)列表项等复杂结构中时,其行为会变得微妙。本文以 Prettier 仓库中tests/format/markdown/blockquote/ignore-code.md测试用例为骨架,结合src/language-markdown下的源码实现与快照测试结果,系统解析 prettier-ignore 在 Markdown 引文块中的生效边界、跨语言代码块的忽略机制以及proseWrap三种取值下的行为差异,帮助读者准确预判并掌控 "被引用的代码块" 的格式化结果。

一、用例背景:为什么需要专门测试 "引文块中的 ignore"

Markdown 引文块(>前缀的行)是文档中频繁出现的结构,常被用来承载示例代码、引用段落或嵌套说明。当其中混入代码围栏和prettier-ignore注释时,格式化器的行为涉及两层解析:

  1. 外层 Markdown 解析:引文块中的每一行都带有>前缀,代码围栏的起始符、结束符与内部内容都必须正确剥离前缀后再解析;
  2. 内层代码语言解析:被围栏包裹的代码(如 JS)是否跳过格式化,取决于<!-- prettier-ignore -->是否被正确识别。

tests/format/markdown/blockquote/ignore-code.md(下称 "该用例文件")正是为此设计的回归测试输入:它通过runFormatTest在三种proseWrap取值下分别运行格式化,并产出快照tests/format/markdown/blockquote/__snapshots__/format.test.js.snap。用例文件本身仅包含 7 个输入片段,却覆盖了"引文块内嵌套代码围栏""列表项内的引文块""引文块包裹引文块""无引文前缀的顶层代码围栏""直接出现在引文块段落中的 ignore 注释"等多种组合,是理解 Prettier Markdown 格式化边界的最佳实验样本。

二、测试驱动方式:如何运行与验证该用例

该用例由tests/format/markdown/blockquote/format.test.js驱动,其核心只有三行:

runFormatTest(import.meta, ["markdown"], { proseWrap: "always" }); runFormatTest(import.meta, ["markdown"], { proseWrap: "preserve" }); runFormatTest(import.meta, ["markdown"], { proseWrap: "never" });

这意味着同一个输入文件会分别在proseWrap: "always""preserve""never"三种配置下各格式化一次,并与快照文件中的期望输出比对。通过输入与输出的差异,即可精确锁定 ignore 注释在不同场景下的生效范围。运行方式为:

yarn jest tests/format/markdown/blockquote

若需单独验证快照更新,可使用-u更新快照。目录下的兄弟用例文件(如 code.md)用于对照"引文块内普通 JSON 代码围栏"的格式化行为,从而反衬 ignore 用例的特殊性。

三、七个测试片段逐段精读:输入与输出的差异分析

以下将用例文件中的 7 个片段逐一拆解,结合快照中的输出(format.test.js.snap)说明每种结构下的实际行为。

片段 1:引文块内的"四反引号 + 三反引号"嵌套围栏(JS 代码保持原样)

输入:

> ````md > <!-- prettier-ignore --> > ```js > ugly ( code ) ; > ``` > ````

这是一个"引文块中的 4 反引号围栏(语言标记md)内再嵌套 3 反引号 JS 围栏"的结构。4 反引号用于让外层代码块内安全地展示内层 3 反引号围栏。输出与输入完全一致ugly ( code ) ;中的多余空格和分号前空格均被保留。

这里出现了 Prettier 的一个关键行为:外层md围栏中的内容本质是"Markdown 代码",而代码块内部的<!-- prettier-ignore -->只是普通文本,并不会触发忽略机制。真正让 JS 保持原样的是Prettier 不会格式化代码块中的内容——Markdown 中的代码围栏内容默认按原文输出。因此,即使没有 ignore 注释,ugly ( code ) ;也不会被改变。该片段验证的是"引文块 + 双层围栏"不会导致代码内容被意外改写。

片段 2:引文块内嵌md代码围栏(长段落保持原样)

输入:

> ```md > <!-- prettier-ignore --> > - This is a long long > long long long long > long long paragraph. > ```

输出同样与输入一致。这里的md围栏内放置了一个长列表段落,若不忽略,proseWrap: "always"下会被按 80 列自动换行。但由于整个内容处于代码围栏中,Prettier 视其为不可格式化的代码块,原样输出。该片段与片段 1 共同确认:代码围栏是 prettier-ignore 之外的"天然免疫区",忽略注释在此更多是语义上的"保险"。

片段 3:列表项内的引文块 + 代码围栏

输入:

> - test > ```md > <!-- prettier-ignore --> > - This is a long long > long long long long > long long paragraph. > ```

这里是"引文块 → 列表项 → 代码围栏"的三级嵌套。注意输入中列表项内容行与围栏行使用了不同的缩进层级(>后 1 空格与 2 空格),输出原样保留了这种缩进与内部结构。这表明 Prettier 在引文块内解析嵌套列表与代码围栏时,能够准确保留每一层的前缀和缩进,不会因格式化而改变代码围栏内部的空白布局。

片段 4:顶层(无引文块)的 4 反引号包裹的引文块

输入:

````md > ```md > <!-- prettier-ignore --> > - This is a long long > long long long long > long long paragraph. > ```
这与片段 2 互为镜像:片段 2 是"引文块内嵌代码围栏",片段 4 是"代码围栏内嵌引文块"。输出与输入一致,说明 **代码围栏无论位于引文块内还是包裹引文块,其内容均按原文输出**,`<!-- prettier-ignore -->` 在围栏内部只作为文本存在。 ### 片段 5:引文块包裹引文块 + 代码围栏(深层嵌套) **输入:**
> ```md > <!-- prettier-ignore --> > - This is a long long > long long long long > long long paragraph. > ```
这是最深的一层嵌套:外层引文块内是 4 反引号围栏,围栏内再嵌套一层引文块,其内才是 3 反引号代码围栏。每行都需要双重 `>` 前缀。输出与输入完全一致,验证了多层前缀剥离与重建的稳定性。 ### 片段 6:引文块段落中的 `<!-- prettier-ignore -->`(真正触发忽略机制) **输入:**
  • This is a long long long long long long long long paragraph.
这是 7 个片段中**唯一真正触发 ignore 机制**的用例:注释不在代码围栏内部,而是直接作为引文块中的一段独立 HTML 注释,紧邻其后的是一段超长的列表段落。快照输出显示: - 在 `proseWrap: "always"` 下,**列表段落保持原样**,没有按 80 列自动换行(对比同目录 [paragraph.md](https://link.gitcode.com/i/772f36bd1e3d36726fa4ff0438756c30) 中普通段落在 `always` 下会被强制折行); - 在 `proseWrap: "never"` 下,输出同样保持原样; - 三份快照中,输入里那个孤立的空引文块行(`>`)在输出中被移除。 这说明 `<!-- prettier-ignore -->` 在引文块内被正确识别为 HTML 注释节点,且其忽略范围覆盖了**紧随其后的下一个块级节点**——这里即那个超长列表段落。这正是 Markdown 语言插件中 ignore 语义的核心。 ### 片段 7:引文块内嵌 JS 代码围栏 + `// prettier-ignore`(跨语言忽略) **输入:**
// prettier-ignore const x = 1, b = 2
**输出对比(以 `always` 快照为例):**
// prettier-ignore const x = 1, b = 2;
这是唯一一个**输出被修改**的片段:`b = 2` 末尾被追加了分号。其原理是:外层围栏语言标记为 `js`,此时围栏内容不再被视为"不可格式化的 Markdown 代码",而是被 **嵌入的 JS 格式化器**接管。`// prettier-ignore` 是 JS 语言层的忽略注释,它只保护紧随其后的声明语句 `const x = 1,`,因此 `b = 2` 仍被 JS 格式化器处理并补上分号,而 `const x = 1,` 保持原样(未被拆成 `const x = 1;`)。 由此可以得出一个重要的实际结论:**在引文块的 JS 围栏中,必须使用 `// prettier-ignore`(语言层注释)而非 `<!-- prettier-ignore -->`(HTML/Markdown 层注释)**,因为前者作用于被嵌入的 JS 代码,后者仅作用于 Markdown 层。 ## 四、三种 proseWrap 模式下的行为汇总 三种模式下输入输出仅有两处差异(详见快照文件),其余结构完全一致: | 片段 | proseWrap: always | proseWrap: preserve | proseWrap: never | | --- | --- | --- | --- | | 片段 1~5(代码围栏内) | 原样输出 | 原样输出 | 原样输出 | | 片段 6(真 ignore) | 列表段落不折行 | 列表段落不折行 | 列表段落不折行 | | 片段 7(JS 围栏) | `b = 2` 补分号 | `b = 2` 补分号 | `b = 2` 补分号 | 由此可见: 1. **`proseWrap` 只影响"可换行的散文/列表文本"**,对代码围栏、ignore 保护的内容均无作用; 2. **引文块中的 ignore 保护在三种模式下都生效**,说明 ignore 机制优先级高于 `proseWrap`; 3. JS 嵌入格式化行为与 `proseWrap` 无关,属于语言层恒定行为。 ## 五、源码级原理:isPrettierIgnore 与 ignore 范围的计算 要理解片段 6、片段 7 的差异,需要深入 `src/language-markdown` 的实现。 ### 5.1 忽略注释的识别规则 [utilities.js](https://link.gitcode.com/i/90110832c06122c6e32ab63383f33a4e) 中的 `isPrettierIgnore(node)` 定义了"什么节点算 ignore 指令": ```js function isPrettierIgnore(node) { let match; if (node.type === "html") { match = node.value.match(/^<!--\s*prettier-ignore(?:-(start|end))?\s*-->$/); } else { let comment; if (node.type === "esComment") { comment = node; } else if ( node.type === "paragraph" && node.children.length === 1 && node.children[0].type === "esComment" ) { comment = node.children[0]; } if (comment) { match = comment.value.match(/^prettier-ignore(?:-(start|end))?$/); } } return match ? match[1] || "next" : false; }

关键点有三:

  • HTML 注释<!-- prettier-ignore --><!-- prettier-ignore-start --><!-- prettier-ignore-end -->)在 Markdown 中对应 AST 节点类型html,通过正则严格匹配(允许注释内有多余空白,如<!-- prettier-ignore -->也合法);
  • 代码注释(如 JS 的// prettier-ignore)对应节点类型esComment,可能是独立节点,也可能被解析为只有一个esComment子节点的paragraph,两种情况都会被识别;
  • 返回值"next"表示"忽略下一个节点","start"/"end"表示范围忽略的起止。

5.2 忽略范围在根节点上的计算

mdast.js 的printRoot会在遍历子节点前统一扫描所有 ignore 指令,构建ignoreRanges数组:遇到start记录起点,遇到匹配的end记录终点;而"next"类指令由 children.js 中的isPrevNodePrettierIgnore在打印兄弟节点时即时判断:

const isPrevNodePrettierIgnore = isPrettierIgnore(previous) === "next";

该判断参与needsBlankLine(是否在节点间插入空行)的计算,从而影响换行布局;同时hasPrettierIgnore(path)(utilities.js)被用于 printers.js 中,作为"上一节点被忽略"的标记传递到具体打印逻辑。

5.3 被忽略内容的原文透传

对于start/end范围,printRoot中的processor会直接使用原始文本切片透传(mdast.js):

return [ printIgnoreComment(children[ignoreRange.start.index]), options.originalText.slice(ignoreRange.start.offset, ignoreRange.end.offset), printIgnoreComment(children[ignoreRange.end.index]), ];

即:打印起始注释 → 原样复制中间文本 → 打印结束注释,中间内容完全不经过格式化器。这从源码层面印证了片段 6 中"长列表段落原样保留"的行为。

5.4 为什么 JS 围栏内必须用// prettier-ignore

Markdown 打印器对代码围栏的处理是:当语言标记(如js)能匹配到内置解析器时,围栏内容会被委托给对应语言的嵌入打印逻辑(embed),而非作为文本原样输出。此时 Markdown 层的<!-- prettier-ignore -->已失去作用,因为围栏内容已进入 JS 打印器的作用域;JS 打印器只认// prettier-ignore等代码注释(见 src/language-js/comments/is-prettier-ignore-comment.js)。因此片段 7 中的// prettier-ignore保护了const x = 1,,却管不到同围栏内的b = 2

5.5 兄弟目录对照:不被 ignore 保护的引文块

同目录 code.md 提供了一个极佳的反例:引文块内的json围栏没有 ignore 注释,快照中该 JSON 内容会被正常格式化。将它与片段 7 对比即可得出:"引文块"本身不会让代码围栏免于格式化——决定格式化与否的是围栏语言标记与 ignore 注释的语言层级,而非是否处于引文块中。

六、实战建议:在引文块中正确使用 prettier-ignore

基于上述分析,可归纳出在 Markdown 引文块中使用忽略机制的实用规则:

  1. 区分"代码围栏内部"与"代码围栏外部":围栏内的<!-- prettier-ignore -->只是文本,不产生任何忽略效果;若想保护围栏内的 Markdown 示例,应保证外层围栏语言标记为md(或普通文本围栏),此时内容天然原样输出;
  2. 保护引文块中的列表/段落:将<!-- prettier-ignore -->作为引文块中的独立一行,置于待保护块级节点(段落、列表)之前,即可在proseWrap: "always"下阻止自动折行,三种 proseWrap 模式均生效;
  3. 保护引文块中的 JS/其他语言代码:必须使用该语言的注释语法(如// prettier-ignore),并注意其只保护紧随的下一语句;多行代码建议使用// prettier-ignore-start/// prettier-ignore-end成对包裹;
  4. 善用 4 反引号围栏:当需要在引文块内展示"包含代码围栏的 Markdown 示例"时,使用```级别的围栏避免结束符冲突,测试已覆盖"引文块-围栏-引文块-围栏"的四层嵌套场景;
  5. 回归验证:修改涉及 Markdown 忽略逻辑时,可运行yarn jest tests/format/markdown/blockquote结合 format.test.js.snap 快速确认各嵌套层级的行为未被破坏。

七、延伸阅读

  • 测试输入文件:ignore-code.md 与驱动文件 format.test.js
  • 期望输出快照:format.test.js.snap(含三种 proseWrap 模式的完整输入/输出对照)
  • Markdown 忽略指令识别:utilities.js、printers.js
  • 忽略范围计算与原文透传:mdast.js、children.js
  • JS 语言层的忽略注释实现:is-prettier-ignore-comment.js
  • 相关测试目录:tests/format/markdown/blockquote
  • 开发工具
  • 格式化
  • CLI

【免费下载链接】prettier

Prettier is an opinionated code formatter.

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

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

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

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

立即咨询