- 开发工具
- 格式化
- CLI
【免费下载链接】prettier
Prettier is an opinionated code formatter.
在 Markdown 文档中,
<!-- prettier-ignore -->注释用于跳过下一段内容的格式化,但当它出现在引文块(blockquote)、嵌套代码围栏(fence)、列表项等复杂结构中时,其行为会变得微妙。本文以 Prettier 仓库中tests/format/markdown/blockquote/ignore-code.md测试用例为骨架,结合src/language-markdown下的源码实现与快照测试结果,系统解析 prettier-ignore 在 Markdown 引文块中的生效边界、跨语言代码块的忽略机制以及proseWrap三种取值下的行为差异,帮助读者准确预判并掌控 "被引用的代码块" 的格式化结果。
一、用例背景:为什么需要专门测试 "引文块中的 ignore"
Markdown 引文块(>前缀的行)是文档中频繁出现的结构,常被用来承载示例代码、引用段落或嵌套说明。当其中混入代码围栏和prettier-ignore注释时,格式化器的行为涉及两层解析:
- 外层 Markdown 解析:引文块中的每一行都带有
>前缀,代码围栏的起始符、结束符与内部内容都必须正确剥离前缀后再解析; - 内层代码语言解析:被围栏包裹的代码(如 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 引文块中使用忽略机制的实用规则:
- 区分"代码围栏内部"与"代码围栏外部":围栏内的
<!-- prettier-ignore -->只是文本,不产生任何忽略效果;若想保护围栏内的 Markdown 示例,应保证外层围栏语言标记为md(或普通文本围栏),此时内容天然原样输出; - 保护引文块中的列表/段落:将
<!-- prettier-ignore -->作为引文块中的独立一行,置于待保护块级节点(段落、列表)之前,即可在proseWrap: "always"下阻止自动折行,三种 proseWrap 模式均生效; - 保护引文块中的 JS/其他语言代码:必须使用该语言的注释语法(如
// prettier-ignore),并注意其只保护紧随的下一语句;多行代码建议使用// prettier-ignore-start/// prettier-ignore-end成对包裹; - 善用 4 反引号围栏:当需要在引文块内展示"包含代码围栏的 Markdown 示例"时,使用
```级别的围栏避免结束符冲突,测试已覆盖"引文块-围栏-引文块-围栏"的四层嵌套场景; - 回归验证:修改涉及 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.
相关推荐
Prettier 韩文(Hangul)Markdown 格式化解析:`splitCjkText/korean.md` 测试用例深度解读
Prettier 韩文(Hangul)Markdown 格式化解析: splitCjkText/korean.md 测试用例深度解读 本文聚焦 Prettier
开发工具格式化CLIPrettier 处理 Markdown 链接中的 HTML 字符引用:entity.md 测试用例深度解析
Prettier 处理 Markdown 链接中的 HTML 字符引用:entity.md 测试用例深度解析 本文围绕 Prettier 仓库中 tests/f
开发工具格式化CLIPrettier Markdown 折行(proseWrap)深度解析:从 break/wrap.md 测试用例到源码实现
Prettier Markdown 折行(proseWrap)深度解析:从 break/wrap.md 测试用例到源码实现 导读 本文以 Prettier 仓库
开发工具格式化CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考