- 开发工具
- 格式化
- CLI
【免费下载链接】prettier
Prettier is an opinionated code formatter.
Markdown 的引用式链接(reference links)将链接目标集中定义在文档底部,但标签中夹杂多余空格、大小写混写时,格式化器极易破坏引用匹配或语义。本文以 Prettier 仓库中的格式测试 case-and-space.md 为核心,结合其快照输出与 Markdown 打印器源码,系统讲解 Prettier 如何规范化引用链接标签(label)中的空白、保留标签大小写与图片替代文本,并说明proseWrap等选项对引用定义格式的影响。读完你将掌握 Prettier 处理linkReference、imageReference、definition、footnoteReference、footnoteDefinition五类引用节点的完整规则与源码级依据。
一、测试场景定位:case-and-space 测的是什么
case-and-space.md位于 Prettier 的格式测试目录 tests/format/markdown/linkReference/ 下,文件内容刻意构造了标签内包含大量多余空格、且字母大小写混写的 Markdown 引用语法。文件名 "case-and-space" 直译即"大小写与空格",它验证的是 Prettier 在格式化引用式链接时两个相互制衡的目标:
- 折叠空白:标签内
[ See ... ]这种多余空格应被规范化为单个空格; - 保留大小写:
AsyncGeneratorFunction、PascalCase这类大小写敏感标签不能被改写,否则会破坏与文末定义(definition)的匹配关系。
该目录下的配套用例共同构成对引用语法格式化行为的完整回归测试集,除本文件外还包括:
- collapsed.md:折叠式引用(
[label][]); - full.md:完整引用(
[text][label]); - shortcut.md:快捷引用(
[label]); - definition.md:引用定义;
- cjk.md:中日韩字符标签;
- wrap.md、title.md:换行与标题场景。
每个用例目录下的 format.test.js 均只有一行:
runFormatTest(import.meta, ["markdown"]);它通过runFormatTest对目录内所有.md文件执行 markdown 解析器格式化,并将输入/输出与snapshots/format.test.js.snap 中的快照比对,任何输出变化都会导致测试失败。在仓库根目录通过项目的 Jest 配置(见 jest.config.js)运行该测试即可复现文中所有输入输出。
二、case-and-space.md 完整输入与格式化输出对照
原文档 case-and-space.md 按五类节点组织,每类均包含"带多余空格"与"带附加说明文字"两个变体。结合快照 format.test.js.snap,Prettier(markdown 解析器,默认printWidth: 80)的实际转换如下。
2.1linkReference:标签空白被折叠
输入:
[ See `AsyncGeneratorFunction` ][ See `AsyncGeneratorFunction` ] [ See `AsyncGeneratorFunction` label ][ See `AsyncGeneratorFunction` ] [ See `AsyncGeneratorFunction` ][]输出:
[ See `AsyncGeneratorFunction` ][ See `AsyncGeneratorFunction` ] [ See `AsyncGeneratorFunction` label ][ See `AsyncGeneratorFunction` ] [ See `AsyncGeneratorFunction` ][]可见链接文本(第一对方括号内)与引用标签(第二对方括号内)两侧及内部连续的多个空格,均被折叠为单个空格;AsyncGeneratorFunction的大小写被原样保留。
2.2imageReference:标签折叠,alt 文本保留原始空白
输入:
![ See `AsyncGeneratorFunction` ][ See `AsyncGeneratorFunction` ] ![ See `AsyncGeneratorFunction` alt ][ See `AsyncGeneratorFunction` ] ![ See `AsyncGeneratorFunction` ][]输出:
![ See `AsyncGeneratorFunction` ][ See `AsyncGeneratorFunction` ] ![ See `AsyncGeneratorFunction` alt ][ See `AsyncGeneratorFunction` ] ![ See `AsyncGeneratorFunction` ][]这是本用例中最值得注意的行为差异:引用标签部分(第二对方括号)的空格照常折叠为单个空格,但图片替代文本部分(第一对方括号内)在完整引用(full)场景下保持输入时的原始空白不变——前两行的 alt 仍是See ...。只有第三行折叠式引用([][])的 alt 才被折叠为See ...。
2.3definition:定义标签同样折叠空白
输入:
[ See `AsyncGeneratorFunction` ]: ./index.html输出:
[ See `AsyncGeneratorFunction` ]: ./index.html引用定义(reference definition)的标签经折叠后为SeeAsyncGeneratorFunction``,与正文中的引用标签(同样折叠后)保持一致的规范化形式,从而保证 CommonMark 的标签匹配(label matching)规则仍然成立。
2.4footnoteReference与footnoteDefinition:原样保留
输入:
[^See`AsyncGeneratorFunction`] [^See`AsyncGeneratorFunction`]: ./index.html输出:与输入逐字相同。
脚注引用与脚注定义不受空白折叠规则影响,原样输出。
三、源码级机制解析
上述行为并非偶然,而是 Markdown 打印器多个处理阶段协作的结果。下面沿 Prettier 的处理管线逐层拆解。
3.1 阶段一:AST 按摩时预先折叠标签空白
在 src/language-markdown/massage-ast/index.js 中,Prettier 对三类节点统一处理:
if ( original.type === "definition" || original.type === "linkReference" || original.type === "imageReference" ) { cloned.label = collapseWhiteSpace(original.label); } // Maybe we should fix this if ( original.type === "imageReference" && original.referenceType === "collapsed" ) { cloned.alt = collapseWhiteSpace(original.alt); }collapseWhiteSpace来自collapse-white-space工具库,将标签内的连续空白(含空格、换行、制表符)收敛为单个空格。注意这里的两个细节:
- 只折叠
label:linkReference、imageReference、definition 的标签在 AST 阶段就被规范化; - 折叠式 imageReference 才折叠
alt:源码注释 "Maybe we should fix this" 表明,仅当图片引用是折叠式(![...][])时才折叠 alt 文本——这正是 2.2 节中第三行 alt 被折叠、前两行 alt 保留空白的原因。
3.2 阶段二:打印 linkReference 时保留大小写并转义方括号
核心实现在 src/language-markdown/print/mdast.js 与同文件 printLinkReference:
case "linkReference": return [ "[", printChildren(path, options, print), "]", node.referenceType === "full" ? printLinkReference(node) : node.referenceType === "collapsed" ? "[]" : "", ];function printLinkReference(node, options) { // `remark-parse` lowercase the `label` as `identifier`, we don't want do that const label = collapseWhiteSpace(node.label); if (options?.parser === "mdx") { return `[${label}]`; } const name = label.replaceAll(/[\\[\]]/g, (s) => `\\${s}`); return `[${name}]`; }三点关键信息:
- 打印时再次
collapseWhiteSpace,与 AST 按摩阶段双重保障,确保标签空白必然规范化; - 明确拒绝小写化:注释指出底层解析器
remark-parse会把标签小写化为identifier,而 Prettier 刻意不这样做——AsyncGeneratorFunction的大小写必须保留,这是 2.1 节输出的直接依据; - 转义方括号:标签中的
[、]、\会被转义(\前缀),避免破坏引用语法; - MDX 差异:MDX 解析器下标签直接输出,不执行转义分支。
3.3 阶段三:imageReference 依赖预处理恢复原始 alt 文本
打印端在 case "imageReference" 中调用printImageAlt(mdast.js):
function printImageAlt(node, options) { if (options.parser !== "mdx" && node.originalAltText) { return node.originalAltText; } ... }originalAltText来自预处理阶段 markOriginalImageAndLinkAlt:
// remark 11 removes nested links so we need to recover the original alt text function markOriginalImageAndLinkAlt(ast, options) { const { originalText } = options; return mapAst(ast, (node) => { if (node.type === "image" || node.type === "imageReference") { node.originalAltText = getBracketContent( originalText, node.position.start.offset, node.position.end.offset, ); return node; } ...辅助函数 getBracketContent 从原始输入文本中按偏移量提取第一对方括号之间的内容(支持转义与嵌套括号计数)。也就是说:alt 文本并非来自解析后的 AST 字段,而是从原文逐字符恢复,因此原始空白得以 1:1 保留。这解释了 2.2 节中完整引用图片 alt 空格不变的现象——Prettier 对图片 alt 采取"原样照搬"策略,避免 alt 内容因规范化而失真。
3.4 阶段四:脚注节点原样输出
case "footnoteReference" 调用 printFootnoteReference:
function printFootnoteReference(node) { return `[^${node.label}]`; }脚注标签不经过空白折叠,直接以[^label]形式输出;而 case "footnoteDefinition" 则会根据内容形态决定排版:若脚注定义仅含一个段落,且proseWrap为never,或为preserve且段落本身单行,则内联输出;否则以 4 空格缩进的对齐形式输出。
3.5 补充规则:折叠式引用保持原样
shouldRemainTheSameContent 表明:当节点是折叠式或快捷式引用(非 full 的 linkReference,以及所有 imageReference)时,其内容保持原样、不参与常规的文本重排:
function shouldRemainTheSameContent(path) { const node = path.findAncestor( (node) => node.type === "linkReference" || node.type === "imageReference", ); return ( node && (node.type !== "linkReference" || node.referenceType !== "full") ); }四、空白折叠之外的两种特殊引用场景
目录中另两个用例文件补充了"大小写与空格"主题之外的重要边界场景。
4.1 纯数字标签(issue-3835)
issue-3835.md 内容为:
[1][Test Text](http://example.com)快照显示其输出保持原样。以数字开头的标签需警惕被解析为有序列表项的干扰,Prettier 在此场景下不做任何改动,保证格式化的幂等性与安全性。
4.2 大小写敏感标签与各类定义(issue-7118)
issue-7118.md 覆盖了更多真实文档中常见的引用定义:
- see[Link to Foo][master-LinkToFoo] [master-LinkToFoo]: http://foo.com Bla bla [PascalCase][] bla. [PascalCase]: ./PascalCase.md ## [Unreleased] … [Unreleased]: https://github.com/username/project/compare/v1.0.0...HEAD快照输出显示:master-LinkToFoo、PascalCase、Unreleased等连字符与大小写混写的标签全部保持原样;唯一的变化是标题## [Unreleased]与正文…之间补出了一个空行——这是 Markdown 文档结构中标题与正文间空行规范化的体现,与引用标签本身无关。该用例验证了 Prettier 在标签规范化上的克制:只折叠空白、绝不改写标签字符,从而保证与 docs/options.md 中"Prettier 不会改变 Markdown 语义"的整体承诺一致。
五、proseWrap选项与定义排版的联动
引用定义的输出形态受全局选项proseWrap影响。在 case "definition" 中:
const lineOrSpace = options.proseWrap === "always" ? line : " "; return group([ printLinkReference(node), ":", indent([ lineOrSpace, options.parser !== "mdx" && node.url === "" ? "<>" : printUrl(node.url, true), node.title === null ? "" : [lineOrSpace, printTitle(node.title, options, false)], ]), ]);proseWrap: "always"时,:后的分隔符是可换行的line(超出printWidth即换行缩进);- 其余取值(
never/preserve)下固定为单个空格。
该选项在 src/common/common-options.evaluate.js 中定义,默认值为"preserve",可选值:
| 取值 | 行为 |
|---|---|
always | 散文超出打印宽度时换行 |
never | 不换行,散文块单行输出 |
preserve | 保持原有换行不变(默认) |
CLI 与 API 对应写法见 docs/options.md:CLI 为--prose-wrap <always|never|preserve>,API 为proseWrap: "<always|never|preserve>"。例如希望引用定义始终单行紧凑排列,可配置:
prettier --prose-wrap never doc.md// prettier 配置 { proseWrap: "never", }六、实用要点总结
基于 case-and-space 用例及其快照、源码实现,可将 Prettier 对 Markdown 引用语法的格式化规则归纳为四条可依赖的结论:
- 标签空白一律折叠:
linkReference、imageReference、definition的标签(label)中的连续空格在 AST 按摩阶段(massage-ast/index.js)与打印阶段(mdast.js)双重折叠为单空格; - 标签大小写永不改写:
printLinkReference明确绕过解析器的小写化,AsyncGeneratorFunction、PascalCase、master-LinkToFoo等标签原样保留(见 issue-7118.md 输出); - 图片 alt 文本原样保留:非 MDX 解析器下,完整引用图片的 alt 从原始文本恢复(preprocess.js),多余空格不做折叠;仅折叠式图片引用的 alt 例外;
- 脚注引用与定义基本不动:
[^label]不折叠空白,定义排版仅受proseWrap与内容形态影响。
对使用者而言,这意味着:只要标签字符(含大小写)一致,即使空格书写随意,格式化后标签都能稳定匹配文末定义;图片 alt 中的刻意排版(如代码块内空白)不会被破坏。若需调整引用定义换行行为,通过proseWrap选项即可控制。上述规则均有 case-and-space.md 等测试用例及其快照作为可复现的验证依据。
- 开发工具
- 格式化
- CLI
【免费下载链接】prettier
Prettier is an opinionated code formatter.
相关推荐
Prettier Markdown 引用式链接(linkReference)规范化:从 issue-3835 看大小写、空白与标签匹配的格式化规则
Prettier Markdown 引用式链接(linkReference)规范化:从 issue 3835 看大小写、空白与标签匹配的格式化规则 导读 本文基
开发工具格式化CLIPrettier YAML 格式化保留块标量行尾空白:`|-` 与 `>-` 的尾部空格处理机制解析
Prettier YAML 格式化保留块标量行尾空白: | 与 的尾部空格处理机制解析 Prettier 的 YAML 格式化器(基于 eemeli/yaml
开发工具格式化CLIPrettier 格式化 Markdown Wiki 链接:内部多余空格为何被完整保留
Prettier 格式化 Markdown Wiki 链接:内部多余空格为何被完整保留 Prettier 对 Markdown 的格式化遵循"语法结构不变、纯文
开发工具格式化CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考