- 开发工具
- 格式化
- CLI
【免费下载链接】prettier
Prettier is an opinionated code formatter.
Prettier 在格式化 Markdown 文档时,会识别并完整保留文件头部的 YAML/TOML Front-Matter(front matter),其中包含中文汉字、日文假名、emoji 表情等 Unicode 字符的内容也会被原样保留、绝不改动。本文以仓库中的 unicode.md 测试用例为切入点,结合 Prettier 的 front-matter 解析、嵌入格式化、打印与测试体系源码,完整讲解 Front-Matter 的识别规则、Unicode 内容的保留原理、内嵌 YAML/TOML 的格式化行为,以及相关边界情况,帮助读者理解 Prettier 处理 Markdown 元信息块的全链路实现。
一、从测试用例出发:Unicode Front-Matter 的输入与输出
仓库中的 tests/format/markdown/front-matter/unicode.md 是一个针对 Markdown 格式化器的最小化测试样例,全文如下:
--- title: "ABC 漢字 🇯🇵" --- ## Retrospective该文件归属于tests/format/markdown/front-matter/目录,由 format.test.js 通过runFormatTest(import.meta, ["markdown"])驱动,即以markdown解析器对文件执行格式化快照测试。对应的快照snapshots/format.test.js.snap 中unicode.md format 1一节的输入与输出完全一致:
- 输入:
title: "ABC 漢字 🇯🇵" - 输出:
title: "ABC 漢字 🇯🇵"
也就是说,title字段中的 ASCII 字母(ABC)、中日韩统一表意文字(漢字)以及由区域指示符组成的国旗 emoji(🇯🇵)在格式化前后没有任何变化。该测试用例直接验证了 Prettier 的一条核心保证:Front-Matter 元信息块的原始文本会被无条件保留,包括其中的任意 Unicode 字符。这为使用中文、日文等多语言标题,或使用 emoji 装饰站点标题的 Markdown 站点(如 Hugo、Jekyll、Astro、VitePress 等静态站点生成器)提供了确定性的格式化行为。
二、Front-Matter 的识别与解析原理
Prettier 对 Front-Matter 的识别并不依赖 Markdown 解析器(micromark/mdast),而是在主流程中通过独立的解析模块先行完成。核心实现在 src/main/front-matter/parse.js,getFrontMatter(text)函数(parse.js#L31-L101)按以下规则识别:
- 起始分隔符判断(parse.js#L32-L36):只接受
---或+++作为文档开头的三分隔符;文件首个字符不是二者之一时直接返回undefined,该文档视为普通 Markdown。 - 语言推断(parse.js#L52-L53):起始分隔符后同行若显式书写了语言名则采用之(如
---toml);未书写时,---默认推断为yaml,+++默认推断为toml。 - 结束分隔符匹配(parse.js#L47-L67):在后续文本中查找
\n---(或\n+++)作为结束标记;对于yaml且未显式指定语言的情况,还兼容 pandoc 等 Markdown 处理器使用...作为结束分隔符的写法(源码注释明确说明:“In some markdown processors such as pandoc,...can be used as the end delimiter for YAML front-matter.”)。 - 合法性校验(parse.js#L71-L74):结束分隔符之后的下一个字符必须是空白或换行,避免误判后续
---开头的 Markdown 分隔线等场景。
解析成功后会产出一个结构化的FrontMatter节点,包含language、explicitLanguage、value(分隔符之间的正文)、startDelimiter、endDelimiter、raw(完整的原始文本块)以及基于 1-based 行号与 0-based 列号的start/end位置信息,并以Symbol.for("PRETTIER_IS_FRONT_MATTER")打上类型标记(见 src/main/front-matter/constants.js)。
对外暴露的parseFrontMatter(text)(parse.js#L103-L117)返回{ frontMatter, content }两部分:content是一个惰性求值属性,将raw中的换行替换为空格后与剩余文本拼接,用于后续交给 Markdown 解析器处理而不受元信息块影响(借助 src/utilities/replace-non-line-breaks-with-space.js)。
值得注意的是,上述解析过程完全是基于原始文本的逐字符扫描,不涉及任何 Unicode 归一化、字符集转换或宽度计算,因此无论元信息块内是 ASCII、CJK 还是代理对组成的 emoji,都会被当作普通文本字节原样切分——这正是 Unicode 内容能够无损保留的第一层保障。
三、Unicode 内容原样保留:打印与内嵌的双重路径
Front-Matter 的打印存在两条路径,二者都以“不改动原始文本”为设计目标:
3.1 兜底打印:直接输出原始文本
src/main/front-matter/print.js 的printFrontMatter极其简单:
function printFrontMatter({ node }) { return node.raw; }它直接将解析阶段保存的raw原样返回。也就是说,只要 Front-Matter 进入打印阶段,Prettier 输出的就是它看到的原始字节序列,title: "ABC 漢字 🇯🇵"这样的行不会被重写、不会被重新排版,也不会受printWidth、tabWidth等选项影响。这与 src/language-markdown/print/mdast.js#L357 中case "frontMatter": // Handled in core的注释相互印证——Markdown 自身的打印器不处理 frontMatter 节点,而是把控制权交给核心层。
3.2 内嵌路径:YAML/TOML 内容按对应语言格式化
不过,print.js只是兜底。实际多数场景下 Front-Matter 会走另一条更精细的路径——src/main/front-matter/embed.js:
- embed.js#L5 定义
SUPPORTED_EMBED_LANGUAGES = new Set(["yaml", "toml"]),只有这两种语言会被内嵌处理; isEmbedFrontMatter(embed.js#L7-L8)判断节点是否带 Front-Matter 标记且语言属于上述集合;printEmbedFrontMatter(embed.js#L10-L41)将node.value抽取出来后,通过textToDoc(value, { parser })交给 YAML/TOML 解析器重新格式化,最后用markAsRoot组装为“起始分隔符 + 语言名 + 格式化后的内容 + 结束分隔符”的完整文档。若value为空(如empty.md的空 Front-Matter),则直接保留空内容。
回到unicode.md用例:其 Front-Matter 是---开头的 YAML,值title: "ABC 漢字 🇯🇵"本身就是合法的单行 YAML 标量,YAML 格式化器不会对其做任何改动,因此两条路径殊途同归,输出与输入完全一致。这一行为也从快照 unicode.md format 1 中得到确认。
3.3 AST 清理:内嵌节点丢弃冗余信息
src/main/front-matter/clean.js 在 AST 清洗阶段(massageAstNode)会删除可内嵌 Front-Matter 的end、raw、value字段,仅保留与格式化结果相关的结构信息,用于--debug-print-ast输出及 AST 比较等调试场景,不影响打印行为。
四、Front-Matter 与 Markdown 主流程的整合
Front-Matter 不是游离于 Markdown 之外的旁路功能,而是深度接入了解析、打印、pragma 与忽略机制。
4.1 解析阶段:拼接为 AST 根节点
src/language-markdown/parse/parse-markdown.js 的parseMarkdown(parse-markdown.js#L39-L61)先调用parseFrontMatter(text)拆出元信息块,再对content执行fromMarkdown得到 mdast 树,最后将frontMatter节点(type: "frontMatter")unshift到根节点children的最前面,并把start/end位置转换为 1-based 的 mdastposition格式。这样 Front-Matter 就成为了 Markdown 语法树中的第一个正式节点,可以与其他节点统一走位置计算、忽略区间识别等通用逻辑。此外 src/language-markdown/parse/unified-plugins/front-matter.js 在 MDX 解析路径中也以 micromark 插件的形式注册了相同的parseFrontMattertokenizer。
4.2 打印阶段:核心层的装饰器包装
Markdown 打印机在 src/language-markdown/printers.js 中通过features.experimental_frontMatterSupport声明了三项能力:
experimental_frontMatterSupport: { massageAstNode: true, embed: true, print: true, }核心层 src/main/parser-and-printer.js 依据这些特性对原打印机进行装饰(Proxy 包装):
massageAstNode(parser-and-printer.js#L110-L117):调用cleanFrontMatter清理 AST;embed(parser-and-printer.js#L119-L142):当节点命中isEmbedFrontMatter时,用printEmbedFrontMatter替代原 embed 逻辑,实现 YAML/TOML 内嵌格式化;print(parser-and-printer.js#L144-L155):当path.node是 Front-Matter 节点时直接走printFrontMatter。
这种“特性声明 + Proxy 装饰”的设计让 front-matter 支持成为可插拔的通用能力,理论上任何语言解析器只要声明features.experimental_frontMatterSupport即可获得同等行为(src/language-js/embed 等模块也有类似的内嵌思路)。
4.3 pragma 与忽略:Front-Matter 不影响文档指令识别
src/language-markdown/pragma.js 表明,判断 Markdown 是否含@format/@prettierpragma 或忽略注释时,会先剥离 Front-Matter 再在正文首部进行正则匹配(hasPragma、hasIgnorePragma);而insertPragma在插入<!-- @format -->时,会将其置于 Front-Matter 原始块之后(${frontMatter.raw}\n\n${pragma}\n\n...)。这意味着即使文档带元信息块,--insert-pragma、--require-pragma等 CLI 选项依然可以正常工作,且插入的 pragma 绝不会污染 Front-Matter 区域。
五、相关测试矩阵:空块、自定义语言与 Unicode
tests/format/markdown/front-matter/目录下的其余用例从不同角度覆盖了 Front-Matter 边界,可与unicode.md对照阅读:
- empty.md 与 empty-2.md:空 Front-Matter(
---紧接---)被保留;empty-2.md额外验证了文档中部独立的---分隔线不会被误判为 Front-Matter 结束符,从而与正文的 Markdown 水平线语义共存。 - custom-parser.md:使用
---mycustomparser这种自定义语言名,快照显示其内部内容(含故意的不规范缩进)被完整原样保留——因为mycustomparser不在SUPPORTED_EMBED_LANGUAGES内,isEmbedFrontMatter返回 false,直接走printFrontMatter的raw兜底输出。 - unicode.md:验证 Unicode 内容在合法 YAML 结构下保持字节级稳定。
四个用例共同证明:Front-Matter 的保留策略是“无条件的”,无论内容是否可被 YAML/TOML 解析、是否包含多字节字符,Prettier 都不会在元信息区域引入任何格式噪声。
六、使用建议与边界说明
基于上述实现,使用 Prettier 格式化带 Front-Matter 的 Markdown 时可以参考以下结论:
- Unicode 标题放心写:
title: "ABC 漢字 🇯🇵"这类含 CJK、emoji 的元信息会被原样保留,不会因printWidth被折行或转义;快照测试即是对此行为的回归保障。 - YAML 内容会被重新格式化:Front-Matter 内部实际上是交给 YAML/TOML 解析器处理的,因此多行 YAML 的缩进、引号风格等会遵循 YAML 格式化规则(受
tabWidth等选项影响),而单行标量如title通常保持原状。 - TOML 使用
+++分隔:默认---对应 YAML、+++对应 TOML,也可以在起始分隔符后显式声明语言(如---toml)。 - 自定义语言的元信息块原样输出:若使用框架自定义的 Front-Matter 方言(如
---mycustomparser),Prettier 不会对其内容做任何格式化,仅原样保留。 - pandoc 兼容:YAML Front-Matter 以
...结束时同样可以被识别。 - 注意版本适用前提:上述行为由
features.experimental_frontMatterSupport(massageAstNode/embed/print)驱动,特性命名含experimental前缀,说明其属于内部实现细节,建议以当前仓库源码(src/main/front-matter/)及快照测试作为行为基准,而非依赖文档层面的口头承诺。
七、总结
从unicode.md这一个 5 行的测试用例出发,可以窥见 Prettier 对 Markdown Front-Matter 的完整设计:独立于 micromark 的文本级识别器(parse.js)负责切分与语言推断;raw字段与printFrontMatter(print.js)保证了 Unicode 与自定义方言的字节级原样保留;embed.js则让标准 YAML/TOML 元信息获得二次格式化能力;最后通过 parser-and-printer.js 的装饰器机制与 Markdown 主流程无缝整合。理解这一链路后,无论是排查“Front-Matter 被改动”的疑惑,还是为自定义语言扩展元信息支持,都能快速定位到正确的源码位置,并以 front-matter 目录下的测试 作为可执行的行为规范。
- 开发工具
- 格式化
- CLI
【免费下载链接】prettier
Prettier is an opinionated code formatter.
相关推荐
Prettier require-pragma 实战:Markdown Front-Matter 文件中的 `@prettier` 标记识别与格式化机制
Prettier require pragma 实战:Markdown Front Matter 文件中的 @prettier 标记识别与格式化机制 导读 本文
开发工具格式化CLIPrettier 对 Markdown 空 front-matter 的识别与保留:从 empty-2.md 测试用例看前置元数据的解析与打印原理
Prettier 对 Markdown 空 front matter 的识别与保留:从 empty 2.md 测试用例看前置元数据的解析与打印原理 本文以 Pr
开发工具格式化CLI深入解析 Prettier 的 insert-pragma:Markdown 与 YAML Front-matter 中的 @format 标记插入机制
深入解析 Prettier 的 insert pragma:Markdown 与 YAML Front matter 中的 @format 标记插入机制 Pre
开发工具格式化CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考