news 2026/9/19 23:06:17

Prettier 格式化 Markdown 代码块内的 CSS @import 规则:从 mdn-import 测试用例到源码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Prettier 格式化 Markdown 代码块内的 CSS @import 规则:从 mdn-import 测试用例到源码实现

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)));

三条语句覆盖了三种典型难度:

  1. 简单supports()条件supports(display: grid)前混入多个空格;
  2. supports()内嵌notand逻辑supports(not (display: grid) and (display: flex)),内部括号前后留白严重;
  3. 多条件组合 + 跨行书写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 }); } // ... }

工作流程可以归纳为以下步骤:

  1. 识别语言:围栏代码块的lang(此处为css)被读取;缩进式代码块(isIndented)或不带语言标记的代码块不进入内嵌格式化流程;
  2. 推断解析器:通过inferParser将语言名映射到实际解析器(css对应 postcss 解析器);
  3. 递归格式化:调用textToDoc把代码块内容当作独立文档交给 CSS 解析器/打印机处理,得到格式化后的 Doc;
  4. 重算围栏长度printCodeFences根据内容中最大连续反引号数决定围栏长度(详见下文);
  5. 拼回外层文档:以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-atrulecss-importname会被统一为小写;value-unknown节点末尾的分号会被剥离,再由打印机按需补回。printer-postcss.js中对@import还有专门处理(isImportUnknownValueEndsWithSemiColon),避免重复输出分号。
  • URL 文本规整media-url节点打印时会移除url(后与)前的多余空白(见printer-postcss.jsmedia-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 多语言协作能力的绝佳样本:它以@importsupports()条件语法为载体,串联起 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 23:02:05

Claude破解30年难题与果蝇全脑上传:AI科研协作者时代来临

1. 从一条日报说起:为什么"Claude破解30年难题"和"果蝇全脑上传"值得单独拎出来聊3月10日这条AI日报里塞了两件事,一件是Claude在某个悬置了三十年的科学问题上给出了突破性结果,另一件是果蝇全脑被完整上传。乍一看像是…

作者头像 李华
网站建设 2026/9/19 23:00:34

告别命令行混乱:BrewUI让Homebrew依赖管理一目了然

1. 为什么我最终放弃纯命令行,开始用 BrewUI 管 Homebrew事情得从一次把开发环境搞崩的经历说起。当时我正在同时维护三个项目,一个基于 PHP 8.1,一个基于 Node 18,还有一个跑着老版本的 Python 3.9。Homebrew 作为 macOS 上最核心…

作者头像 李华
网站建设 2026/9/19 22:57:47

PhysX 5源码尽调:从架构演进到Omniverse集成的物理引擎深度解析

1. 项目概述与源码尽调目标1.1 为什么在这个时间点做PhysX源码尽调先说点背景。PhysX从2008年被NVIDIA收购算起,在物理引擎这个圈子里已经跑了十五年以上。游戏开发者对它不陌生,Unity、Unreal都在用,但大部分人是把它当黑盒用——调几个参数…

作者头像 李华
网站建设 2026/9/19 22:55:51

欧姆龙PLC四层电梯控制方案:梯形图分块与调试实战

简介:欧姆龙PLC四层电梯控制系统设计资料,是一份面向自动化、电气工程及计算机科学方向学习者的完整课程设计方案,适合PLC入门者、职校/高校学生及参加自动化实训的读者参考。资料以电梯垂直运输设备为对象,先概述电梯的定义、用途…

作者头像 李华