深入 Prettier 的 Markdown 内嵌 CSS 格式化:以 mdn-background 测试集为例
2026/9/19 19:36:01 网站建设 项目流程

深入 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.snapmdn-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 在此场景下执行的五项关键规范化:

  1. 围栏外层空行被收敛:输入在```css之前有三个空行,输出直接以围栏开头;围栏内紧跟```css的空行也被移除,代码块从第一行真实内容开始。
  2. 渐变的后续图层独立成行center center / 400px 200px no-repeat不再与linear-gradient(...)的闭括号挤在同一行,而是换行后使用与闭括号一致的续行缩进。
  3. 超长空格被折叠:原输入中闭括号后) center center之间的十几个空格被压缩为标准换行缩进,多图层之间以,结尾逐行展开,可读性与 diff 友好性显著提升。
  4. url(big-star.png) center no-repeat图层独立成行:不再与上一图层共用一行,整个background值呈现「一个图层一行」的稳定结构。
  5. 值内容保持语义不变:角度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 解析器)。命中后返回一个异步函数,其内部调用textToDocnode.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.mdbackground-imagelinear-gradienturl()的混合、极端参差缩进
mdn-background-3.mdlinear-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-gradientcircle at不同圆心)堆叠,末层带beige颜色
mdn-background-7.md多个repeating-linear-gradient堆叠,含大量rgb(...)颜色停靠点
mdn-background-8.md格纹(plaid)效果:四个repeating-linear-gradient,含两种颜色停靠点写法(分写与0 50px区间缩写)
mdn-background-9.mdconic-gradientturn角度单位 + 图层位置/尺寸/重复(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的处理逻辑如出一辙。

六、如何本地复现与验证

如果你想亲手验证上述行为,仓库是只读的,但完全可以本地运行:

  1. 安装依赖后,用 Jest 执行该目录的格式测试入口:yarn jest tests/format/markdown/code/format.test.js(具体命令以 jest.config.js 与本地脚本为准)。
  2. 测试框架(runFormatTest)会以parsers: ["markdown"]proseWrap: "always"printWidth: 80的固定配置处理mdn-background-*.md,并与快照比对。
  3. 修改测试夹具后,Jest 支持-u更新快照来对比新旧输出差异,这正适合观察复杂 CSS 在不同输入下的规范化结果。

proseWrapprintWidth的语义可参考 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询