Prettier 格式化 Markdown 代码块内的 CSS @import 规则:从 mdn-import 测试用例到源码实现
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
本篇文章基于 Prettier 仓库中的mdn-import格式测试用例(tests/format/markdown/code/mdn-import.md),深入讲解 Prettier 如何处理 Markdown 代码块内嵌的 CSS@import规则——包括supports()条件与媒体查询的空白规整、超长语句的折行策略,以及"代码块内容交给对应语言解析器重新格式化"的底层实现原理。读完本文,你将掌握 Markdown 中嵌入式代码格式化(Embedded Code Formatting)的完整工作机制,并能复现与验证该测试用例的全部行为。
测试用例全貌:一份"最小却五脏俱全"的输入样本
mdn-import.md本身是一份 Markdown 格式测试的输入文件,文件全文只有一个css围栏代码块,内部包含三条来自 MDN 文档的@import语句。其特点是刻意塞入了大量不规则空白、换行与复杂嵌套条件,用于检验格式化器在"代码块内嵌 CSS"场景下的鲁棒性:
@import url("gridy.css") supports( display: grid) screen and (max-width: 400px); @import url("flexy.css") supports(not (display: grid ) and (display: flex)) screen and (max-width: 400px); @import url( "whatever.css") supports((selector(h2 > p)) and (font-tech(color-COLRv1)));三条语句覆盖了三种典型难度:
- 简单
supports()条件:supports(display: grid)前混入多个空格; supports()内嵌not与and逻辑:supports(not (display: grid) and (display: flex)),内部括号前后留白严重;- 多条件组合 + 跨行书写:
supports((selector(h2 > p)) and (font-tech(color-COLRv1))),且url(...)的括号与字符串被拆到两行。
该文件由同目录下的 format.test.js 驱动,测试配置为:
runFormatTest(import.meta, ["markdown"], { proseWrap: "always" });即使用markdown解析器、proseWrap: "always"选项执行格式化,并将结果与快照文件比对。
格式化输出:快照中的标准答案
测试的预期输出记录在同目录快照snapshots/format.test.js.snap 中(mdn-import.md - {"proseWrap":"always"} format 1条目)。快照同时记录了输入与输出,格式化后的结果如下:
@import url("gridy.css") supports(display: grid) screen and (max-width: 400px); @import url("flexy.css") supports(not (display: grid) and (display: flex)) screen and (max-width: 400px); @import url("whatever.css") supports((selector(h2 > p)) and (font-tech(color-COLRv1)));对照输入逐条解读 Prettier 的决策逻辑:
- 空白规整:
@import后多余空格被压缩为单个空格;supports()内部多余空白(如grid )、flex))前的连续空格)被全部清除,只保留必要的单个空格分隔。这是 CSS 解析器对 AST 重新排版的结果,而非简单的正则替换。 - 条件/媒体查询不折叠:
supports(...)与screen and (max-width: 400px)保持在同一行输出,但整体遵循printWidth(快照头部标注printWidth: 80 (default))约束。 - 折行策略:第二条语句超出 80 列时,在媒体查询的
screen前折行并缩进 2 个空格;第三条语句则因supports(...)内容过长,在url("whatever.css")之后直接换行,且后续条件不再追加缩进。 - 代码围栏保留:外层 Markdown 围栏保持三个反引号,代码块内容整体作为一个"内嵌文档"被重新格式化后原样放回。
关键机制一:Markdown 代码块如何交给 CSS 解析器
Markdown 本身并不理解 CSS 语法。上述格式化行为的关键在于 Prettier 的 embed 机制。在 src/language-markdown/embed.js 中,code节点类型被专门处理:
case "code": { const { isIndented, lang: language } = node; if (isIndented || !language) { return; } let parser; if (language === "angular-ts") { parser = inferParser(options, { language: "typescript" }); } else if (language === "angular-html") { parser = "angular"; } else { parser = inferParser(options, { language }); } // ... }工作流程可以归纳为以下步骤:
- 识别语言:围栏代码块的
lang(此处为css)被读取;缩进式代码块(isIndented)或不带语言标记的代码块不进入内嵌格式化流程; - 推断解析器:通过
inferParser将语言名映射到实际解析器(css对应 postcss 解析器); - 递归格式化:调用
textToDoc把代码块内容当作独立文档交给 CSS 解析器/打印机处理,得到格式化后的 Doc; - 重算围栏长度:
printCodeFences根据内容中最大连续反引号数决定围栏长度(详见下文); - 拼回外层文档:以
markAsRoot将围栏、语言标记与格式化后的代码块组装为 Markdown 最终输出。
值得一提的是,ts/tsx语言还会被强制指定dummy.ts/dummy.tsx文件路径,以解决类型参数尾逗号打印的歧义——这印证了"代码块按对应语言完整格式化"的设计思路。
关键机制二:围栏长度与换行由谁决定
代码块内容的行尾换行与围栏规范化位于 src/language-markdown/print/code.js:
printCodeFences将格式化后的内容以printWidth: Infinity(不折行)渲染为字符串,再用getMaxContinuousCount统计内容中最大连续反引号个数,围栏长度取Math.max(3, count + 1)——即至少 3 个反引号,且绝不会与内容中的反引号串冲突;printFencedCodeBlock使用replaceEndOfLine统一代码块内部行尾,确保换行风格与整体文档一致。
而 CSS 语句内部的折行则完全由 CSS 打印器的 Doc 结构决定。在 src/language-css/printer-postcss.js 中,css-atrule节点的params会被递归打印;媒体查询部分走media-query-list分支:
case "media-query-list": { const parts = []; path.each(({ node }) => { // ... parts.push(print()); }, "nodes"); return group(indent(join(line, parts))); }group+indent+line的组合意味着:若整条查询能放进一行则保持单行;放不下时,在line位置(即查询间空白处)折行并缩进。这正解释了第二条语句在screen and (...)前换行、缩进 2 空格的现象。而第三条语句supports(...)之后没有缩进,是因为该位置对应的是 at-rule 的params尾部处理路径,与media-query-list的缩进逻辑不同——两种折行形态来自同一打印器的不同分支。
关键机制三:解析与归一的底层细节
CSS 语句之所以能被"重新排版"而非"按原样保留",依赖解析层的结构化处理:
- 媒体查询解析:
supports(...)与screen and (max-width: 400px)由 src/language-css/parse/parse-media-query.js 调用postcss-media-query-parser解析为media-query/media-feature/media-value等节点;解析失败时回退为selector-unknown,保证极端输入不会导致格式化崩溃。 - 大小写与分号归一:在 src/language-css/massage-ast/index.js 中,
css-atrule与css-import的name会被统一为小写;value-unknown节点末尾的分号会被剥离,再由打印机按需补回。printer-postcss.js中对@import还有专门处理(isImportUnknownValueEndsWithSemiColon),避免重复输出分号。 - URL 文本规整:
media-url节点打印时会移除url(后与)前的多余空白(见printer-postcss.js中media-url分支的replaceAll逻辑),这正是第三条语句跨行url(...)被折叠为url("whatever.css")的原因。
如何复现与验证
在本地克隆本仓库后,可自行复现该测试:
# 运行单个 markdown 代码块测试目录 yarn jest tests/format/markdown/code -t mdn-import该命令会加载format.test.js,将mdn-import.md作为输入执行格式化,并与快照文件比对;若修改了输入文件导致输出变化,可用-u更新快照后 diff 观察。也可以直接用 CLI 验证内嵌格式化效果:
yarn prettier tests/format/markdown/code/mdn-import.md --parser markdown将输出与快照中的结果对照,即可直观理解"Markdown 外层不变、内嵌 CSS 被完整重排"的行为边界:包括空白压缩、80 列折行、围栏保留,以及第三条语句supports(...)不缩进的细节。
小结
mdn-import.md虽然只是一个 5 行的测试输入文件,却是观察 Prettier 多语言协作能力的绝佳样本:它以@import的supports()条件语法为载体,串联起 src/language-markdown/embed.js 的内嵌解析器推断、src/language-css/print/code.js 的围栏重算、src/language-css/printer-postcss.js 的 at-rule 与媒体查询打印、src/language-css/parse/parse-media-query.js 的媒体查询解析,以及 src/language-css/massage-ast/index.js 的 AST 归一化。理解这条链路,你便掌握了 Markdown 内嵌 CSS(以及扩展至 JS、HTML、GraphQL 等任意受支持语言)格式化的通用原理,也就能准确预判 Prettier 在你自己的文档中会如何处理代码块里的复杂 CSS 语句。
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考