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

发布时间:2026/9/19 23:06:18
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 代码块内嵌的 CSSimport规则——包括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.jsprintCodeFences将格式化后的内容以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))); }groupindentline的组合意味着若整条查询能放进一行则保持单行放不下时在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),仅供参考