深入 Prettier 的 Markdown 内嵌 CSS 格式化:以 mdn-background 测试集为例
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
本篇技术指南以 Prettier 仓库中的格式测试夹具 mdn-background-1.md 为切入点,系统讲解 Prettier 如何对 Markdown 围栏代码块内的 CSS 进行二次格式化。你将理解从 Markdown 解析、内嵌语言识别(embed)到 CSS 语法树重建的完整调用链,掌握复杂background简写(多图层渐变、位置/尺寸、颜色层)被规范化重排的底层原理,并学会如何本地运行与验证同类格式测试。
一、这个测试夹具在测什么
mdn-background-1.md是一份极简但极有针对性的测试输入,全文只有一个 CSS 围栏代码块:
.box { background: linear-gradient( 105deg, rgb(255 255 255 / 20%) 39%, rgb(51 56 57 / 100%) 96% ) center center / 400px 200px no-repeat, url(big-star.png) center no-repeat, rebeccapurple; }它浓缩了真实开发中最难人工维护的几种 CSS 写法:
- 多图层
background简写:一个属性值里同时出现linear-gradient(...)、url(...)和纯颜色rebeccapurple三个图层; - 图层内同时携带位置、尺寸、重复方式:
center center / 400px 200px no-repeat是「位置 / 尺寸 + repeat」的复合写法; - 现代空格分隔颜色语法:
rgb(255 255 255 / 20%)这类 CSS Color 4 写法; - 大量人工对齐留下的不规则空格:闭括号与下一个图层之间堆叠了超长空格,
url(...)图层挤在行尾,缩进参差。
该文件本身不包含任何测试断言逻辑,真正的断言在目录级测试入口 format.test.js 中,它只有一行:
runFormatTest(import.meta, ["markdown"], { proseWrap: "always" });它指示 Prettier 以markdown解析器、proseWrap: "always"选项格式化本目录下所有*.md文件,并将实际输出与快照snapshots/format.test.js.snap 中记录的期望结果逐字符比对。
二、逐层拆解:输入到输出发生了什么
快照(__snapshots__/format.test.js.snap中mdn-background-1.md一节)记录了该输入在printWidth: 80(默认值)下的期望输出:
.box { background: linear-gradient( 105deg, rgb(255 255 255 / 20%) 39%, rgb(51 56 57 / 100%) 96% ) center center / 400px 200px no-repeat, url(big-star.png) center no-repeat, rebeccapurple; }与输入对比,可以提炼出 Prettier 在此场景下执行的五项关键规范化:
- 围栏外层空行被收敛:输入在
```css之前有三个空行,输出直接以围栏开头;围栏内紧跟```css的空行也被移除,代码块从第一行真实内容开始。 - 渐变的后续图层独立成行:
center center / 400px 200px no-repeat不再与linear-gradient(...)的闭括号挤在同一行,而是换行后使用与闭括号一致的续行缩进。 - 超长空格被折叠:原输入中闭括号后
) center center之间的十几个空格被压缩为标准换行缩进,多图层之间以,结尾逐行展开,可读性与 diff 友好性显著提升。 url(big-star.png) center no-repeat图层独立成行:不再与上一图层共用一行,整个background值呈现「一个图层一行」的稳定结构。- 值内容保持语义不变:角度
105deg、空格分隔的rgb(...)写法、尺寸400px 200px、最终颜色层rebeccapurple全部原样保留,格式化只调整结构与空白,不重写任何有效值。
注意一个细节:linear-gradient(...)内部参数(105deg、两个rgb(...))的缩进在输出中保持为 8 空格,与输入一致。这说明 CSS 打印器对这类已经多行展开的函数调用采用「保持既有缩进层级」的策略,只负责规整函数外部的换行与对齐。
三、底层机制:Markdown 代码块如何被二次格式化
看到这里你可能会问:Markdown 解析器怎么会懂得 CSS?答案在 Prettier 的「内嵌语言格式化(embed)」机制中。
3.1 embed 钩子:识别语言并委托格式化
src/language-markdown/embed.js 在遇到type: "code"的 AST 节点时,会先读取围栏标记的语言(node.lang)。只要不是缩进代码块且语言非空,就通过 infer-parser.js 的inferParser把语言名映射到具体解析器(例如css→ postcss 解析器)。命中后返回一个异步函数,其内部调用textToDoc将node.value交给 CSS 解析器重新解析并打印成文档(doc)结构。
3.2 围栏样式计算:printCodeFences
格式化完成后的 doc 会交给 src/language-markdown/print/code.js 中的printCodeFences输出。它根据格式化后内容中最长的连续反引号串长度动态决定围栏长度:
styleUnit.repeat(Math.max(3, getMaxContinuousCount(value, styleUnit) + 1));即围栏至少 3 个反引号;若代码内容中出现了n个连续反引号,则使用n+1个反引号闭合,避免围栏与内容冲突。最终输出由「围栏 + 语言名 + 元信息 + 换行 + 格式化后的代码 + 换行 + 围栏」拼接而成(markAsRoot确保内嵌 doc 独立成根)。同一个文件中还处理了缩进式代码块:用 4 空格前缀对齐其内容(align)。
3.3 未识别语言时的回退
如果语言名无法映射到任何已注册解析器(如自定义未知标记),inferParser返回空,embed直接返回,代码块内容原样保留、不做任何改动。这正是「mdn-background-1.md必须写css语言标记才能触发 CSS 格式化」的原因。
四、CSS 侧的打印细节:渐变、图层与数值
内嵌格式化最终落在 CSS 打印器上,相关实现横跨多个文件,值得逐个对应:
- 值解析:src/language-css/parse/parse-value.js 负责把
background这类复合值拆解成可遍历的值节点树,多图层之间的逗号分隔结构由此而来。 - 逗号分隔值分组:src/language-css/print/comma-separated-value-group.js 负责决定「何时一个图层占一行」。从快照看,当图层本身无法在
printWidth: 80内放下时,各图层会拆分为独立行;而能容纳的短图层(如输出中的radial-gradient(...))仍可能内联。 - 数字与单位规整:src/language-css/print/misc.js 中的
adjustNumbers/printCssNumber会去除数值多余的小数尾零(.0),并借助 css-units.evaluate.js 中的 CSS 单位表统一单位大小写。在mdn-background-1.md这个用例里没有触发改写,但同一测试族(见下节)中大量rgb(... / 50%)、0.25turn等写法都经由这条路径保持原样或规范化。 - 入口解析器:src/language-css/parser-postcss.js 基于 postcss 生态构建 AST,是 Markdown 内嵌 CSS 与独立
.css文件共用的解析入口。
从整体架构看,「Markdown 文件里写 CSS 代码块」与「直接格式化 .css 文件」最终共享同一套 CSS 打印器,这就是为什么上面五项规范化行为和你用 Prettier 直接格式化 CSS 时看到的效果完全一致。
五、mdn-background 测试族:一个系列,九种渐变场景
mdn-background-1.md并非孤例,它与另外 8 个同前缀夹具构成完整的测试族,共同覆盖 CSS 背景/渐变语法的各个角落:
| 夹具文件 | 覆盖场景 |
|---|---|
| mdn-background-1.md | 多图层background简写:linear-gradient+url()+ 纯色,含位置/尺寸/重复复合值 |
| mdn-background-2.md | background-image中linear-gradient与url()的混合、极端参差缩进 |
| mdn-background-3.md | linear-gradient+radial-gradient双层叠加,兼有声明缩进错乱与width未缩进 |
| mdn-background-4.md | 三个图层(两个url+ 渐变)的background-image,以及多值background-repeat/background-position |
| mdn-background-5.md | 三层linear-gradient堆叠(RGB 三原色渐变) |
| mdn-background-6.md | 三层radial-gradient(circle at不同圆心)堆叠,末层带beige颜色 |
| mdn-background-7.md | 多个repeating-linear-gradient堆叠,含大量rgb(...)颜色停靠点 |
| mdn-background-8.md | 格纹(plaid)效果:四个repeating-linear-gradient,含两种颜色停靠点写法(分写与0 50px区间缩写) |
| mdn-background-9.md | conic-gradient的turn角度单位 + 图层位置/尺寸/重复(top left / 25% 25% repeat) |
快照文件 format.test.js.snap 中对上述每个夹具都记录了一段「options → input → output」三段式结果。例如mdn-background-9.md的期望输出把conic-gradient(...) top left / 25% 25% repeat中混乱的空白规整为「渐变闭括号独立缩进 +top left / 25% 25% repeat换行缩进」的稳定形态,与mdn-background-1.md的处理逻辑如出一辙。
六、如何本地复现与验证
如果你想亲手验证上述行为,仓库是只读的,但完全可以本地运行:
- 安装依赖后,用 Jest 执行该目录的格式测试入口:
yarn jest tests/format/markdown/code/format.test.js(具体命令以 jest.config.js 与本地脚本为准)。 - 测试框架(
runFormatTest)会以parsers: ["markdown"]、proseWrap: "always"、printWidth: 80的固定配置处理mdn-background-*.md,并与快照比对。 - 修改测试夹具后,Jest 支持
-u更新快照来对比新旧输出差异,这正适合观察复杂 CSS 在不同输入下的规范化结果。
proseWrap与printWidth的语义可参考 docs/options.md;对本用例而言,printWidth: 80是决定「渐变色函数是否折叠、图层是否独立成行」的标尺,而proseWrap: "always"影响的是 Markdown 正文换行,围栏内的 CSS 不受其直接约束。
七、小结
通过mdn-background-1.md这一个夹具,我们完整看到了一条端到端链路:Markdown 围栏 → embed.js 的语言识别与委托 → infer-parser.js 的解析器推断 → print/code.js 的围栏重建 → CSS 打印器对复合background值的逐层重排。这解释了 Prettier 的一个核心设计:语言无关的 Markdown 解析与语言相关的代码格式化通过 embed 机制优雅解耦,无论代码块里是 CSS、JavaScript 还是 TypeScript,Markdown 侧只负责「把内容交出去」,语言侧只负责「把内容排好再送回来」。理解这一机制后,你不仅能预测复杂 CSS 在代码块中的格式化结果,也能更好地理解 Prettier 多语言插件体系的工作方式。
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考