Biome Markdown 格式化器如何保护围栏代码块中的 CSS 内容:以 mdn-font-face-1 测试用例为线索的源码级解析
【免费下载链接】biomeA toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP.项目地址: https://gitcode.com/gh_mirrors/bi/biome
本篇文章以 Biome 仓库中的测试规格文件
crates/biome_markdown_formatter/tests/specs/prettier/markdown/code/mdn-font-face-1.md为切入点,深入解析 Biome Markdown 格式化器(formatter)对围栏代码块(fenced code block)内部代码内容的处理策略,包括围栏长度归一化、代码块缩进保护、CommonMark 兼容规则,以及对应的测试基础设施与配置方法。读完本文,你将理解为什么格式混乱的 CSS 代码块在 Markdown 中会被原样保留,并掌握如何运行、验证与配置 Biome 的 Markdown 格式化能力。
一、这个测试用例在测什么:一段“故意凌乱”的 CSS 代码块
在 Biome 仓库中,crates/biome_markdown_formatter/tests/specs/prettier/markdown/code/mdn-font-face-1.md是一个从 Prettier 官方测试套件中迁移过来的 Markdown 规格测试输入文件,其完整内容如下:
```css @font-face { font-family: "HeydingsControlsRegular"; src: url("fonts/heydings_controls-webfont.eot"); src: url("fonts/heydings_controls-webfont.eot?#iefix") format("embedded-opentype"), url("fonts/heydings_controls-webfont.woff") format("woff"), url("fonts/heydings_controls-webfont.ttf") format("truetype"); font-weight: normal; font-style: normal; }这段输入刻意模拟了开发者从 MDN(Mozilla Developer Network)教程中复制过来的真实代码:CSS 规则内部缩进极其混乱——`src` 属性前有 8 个空格、第二条 `url(...)` 完全没有缩进、`font-weight` 与 `font-style` 的缩进也不一致。与之配套的期望输出文件 `mdn-font-face-1.md.prettier-snap` 表明:**Prettier 与 Biome 对该文件格式化后的结果与输入完全一致,代码块内部的 CSS 内容被逐字保留**。 这正是该测试用例要验证的核心行为:**Markdown 格式化器不应重排围栏代码块内部的代码**。无论内部 CSS 缩进多么混乱,只要它位于反引号围栏内,就属于“字面量内容”(verbatim content),格式化器只负责处理 Markdown 语法层面的结构(围栏本身、外层缩进、空行),而把内部代码原样交给渲染器。 同目录下还有姊妹用例 `mdn-font-face-2.md`,其中包含了 `tech(color-COLVr1)` 这类现代 CSS 字体技术语法,同样验证了含 `tech()`/`format()` 参数的复杂 `@font-face` 声明在围栏内不会被重排。 ## 二、围栏代码块格式化核心:FormatMdFencedCodeBlock 的源码实现 围栏代码块的格式化逻辑位于 [fenced_code_block.rs](https://link.gitcode.com/i/9b3bcfc6417f8b5cd092e422ff98eec1),核心类型是 `FormatMdFencedCodeBlock`,它实现了 `FormatNodeRule<MdFencedCodeBlock>`。在 `fmt_fields` 方法中,代码块被拆解为 `l_fence`(开围栏)、`r_fence`(闭围栏)、`r_fence_indent`(闭围栏缩进)、`content`(内容)、`code_list`(语言标识)、`indent`(开围栏缩进)等字段分别处理。 ### 2.1 围栏长度归一化:CommonMark §4.5 规则 `fenced_code_block.rs` 中最关键的一段逻辑是围栏长度的计算(第 26–33 行): ```rust // Compute the minimum fence length needed (CommonMark §4.5). // The fence must be strictly longer than any same-character sequence // in the content, otherwise the inner sequence would be parsed as a // closing fence. E.g. if the content contains ``` (3 backticks), // the outer fence needs at least 4. let max_inner = longest_fence_char_sequence(node, '`'); let fence_len = (max_inner + 1).max(3); let normalized_fence: String = std::iter::repeat_n('`', fence_len).collect();其原理遵循 CommonMark 规范 §4.5:闭围栏是一行至少与开围栏等长(或更长)的连续反引号序列。如果代码内容中恰好存在与围栏等长的连续反引号,解析器会把内容中的那一段误判为闭围栏。因此 Biome 会先通过辅助函数longest_fence_char_sequence扫描内容中最长的连续反引号序列max_inner,再将围栏长度设为max_inner + 1(且至少 3 个反引号),从而保证围栏永远“严格长于”内容中的任何反引号串。
随后,开围栏l_fence与闭围栏r_fence都会被替换为统一计算出的normalized_fence。也就是说,即使源码中使用了 4 个、5 个甚至更多反引号,只要内容中不需要更长的围栏,输出都会被归一化为恰好够用的长度。这正是“Markdown 语法层格式化”与“代码内容不动”之间边界的一个典型体现。
2.2 围栏缩进的移除与标准化
除了围栏长度,代码块还涉及缩进处理:
- 开围栏前的缩进(
indent字段):用于将代码块嵌入列表等嵌套结构,Biome 会遍历这些缩进 token,通过format_removed将其从输出中移除,再依赖外层(如列表项)的格式化器统一重新生成缩进(第 44–50 行)。 - 闭围栏前的缩进(
r_fence_indent字段):同样被format_removed移除并重新标准化(第 96–102 行),随后写出归一化后的闭围栏(第 104–117 行)。
此外,代码还统计了开围栏的缩进宽度opening_fence_indent(第 39–42 行),供后续内容处理时计算“应被剥离的公共缩进”使用。
2.3 语言标识与内容的分流处理
code_list(如 ```css 中的css)与围栏一并输出。而代码块内容(content)的处理分为两种情况:
- 若内容中不存在
MdCodeContent(即文档级代码块通常以单个字面量节点存储),则走普通内容格式化分支; - 若存在
MdCodeContent节点,则对每个代码内容节点调用FormatMdCodeContentOptions,并传入opening_fence_indent,见 fenced_code_block.rs。
三、代码内容保护:FormatMdCodeContent 如何逐行剥离缩进
代码块内部的逐行处理由 code_content.rs 中的FormatMdCodeContent完成,其行为在源码注释中有明确说明:“Trivia is excluded on both sides”(两侧排除 trivia),即开围栏信息字符串后的空白不会作为多余内容行输出。
关键算法在fmt_fields中(第 36–82 行):
- 跳过行首换行符:
value_token以开围栏行末的换行符开头(\r\n或\n),先从line_start中跳过,避免产生空内容行。 - 剥离公共缩进:对每一行,从行首开始,在不超过
opening_fence_indent(开围栏缩进宽度)的范围内剥离连续空格(第 43–50 行)。这意味着,如果代码块位于列表项中(开围栏有 2 空格缩进),内容行的前 2 个空格会被视为 Markdown 语法缩进而去除,剩余部分才是真正的代码。 - 逐行按原样输出:剥离缩进后的每一行代码,通过
syntax_token_cow_slice(...).with_literal_line_breaks()以字面换行的方式写出(第 27–34 行)。with_literal_line_breaks保证换行符被忠实保留,不会参与自动换行(line wrapping)或重排。
对于 CRLF(\r\n)行尾,代码还做了细致的兼容处理:遇到\r时,若后面紧跟\n则一起作为行尾输出,否则单独生成一个不依赖父级的字面换行(第 62–74 行)。
这就是mdn-font-face-1.md中那些“缩进错乱”的 CSS 行得以原样保留的根本原因:格式化器只剥离与围栏对齐的 Markdown 语法缩进,而 CSS 内部每个属性、每条url(...)之前的空格都是代码内容的组成部分,会被逐字输出。整段@font-face对格式化器而言是“不透明”的文本,从而保证了 MDN 示例这种真实代码在文档中不会被破坏。
四、测试基础设施:该用例如何被自动执行与验证
4.1 从 Prettier 测试套件迁移
这些mdn-*.md文件来自 prepare_tests.js,该脚本以 Prettier 仓库的tests/format目录为输入,遍历其中所有测试文件:
- 将输入文件复制到 Biome 的
tests/specs/prettier/对应目录; - 从 Prettier 的快照中提取“输出”部分,并用 Prettier 自身重新格式化后写入
.prettier-snap文件(第 122–124 行); - 若 Prettier 重格式化前后不一致,还会额外生成
.prettier-snap-original文件用于比对。
也就是说,.md是输入、.prettier-snap是期望输出,二者成对出现,构成一份可对照的格式化测试规格。
4.2 Prettier 兼容性快照测试
prettier_tests.rs 通过宏批量生成测试:
tests_macros::gen_tests! {"tests/specs/prettier/markdown/**/*.{md}", crate::test_snapshot, ""}每个.md输入都会触发test_snapshot:它使用PrettierTestFile读取测试文件,以MdFormatOptions::default()(IndentStyle::Space、缩进宽度 2)和 GFM(GitHub Flavored Markdown)方言构造MarkdownTestFormatLanguage,最终交给PrettierSnapshot::new(...)执行格式化并与.prettier-snap期望输出对比。这保证了 Biome 的 Markdown 输出与 Prettier 保持兼容——这正是mdn-font-face-1.md这类用例存在的意义:任何破坏代码块内容的行为都会导致快照不一致,从而让测试失败。
4.3 通用规格测试与运行方式
除了 Prettier 兼容性测试,Biome 还有一套面向自身语法的规格测试 spec_tests.rs,它扫描tests/specs/markdown/**/*.md下的所有用例,并以启用 Markdown formatter 的配置执行SpecSnapshot测试。两套测试体系共同守护 Markdown 格式化行为。
在仓库根目录下运行以下命令即可执行 Markdown 格式化器的全部测试:
cargo test -p biome_markdown_formatter若要只跑某个特定用例(例如本主题的mdn-font-face-1),可用:
cargo test -p biome_markdown_formatter -- mdn_font_face五、如何在真实项目中启用与配置 Markdown 格式化
需要特别说明的是:在 Biome 当前配置源码 markdown.rs 中,Markdown 格式化器默认处于禁用状态(pub type MarkdownFormatterEnabled = Bool<false>;注释明确指出“Keep it disabled by default while experimental”,即实验功能默认关闭)。因此若要在项目中使用,需在biome.json中显式开启:
{ "formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 80 }, "markdown": { "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineWidth": 80, "proseWrap": "preserve", "lineEnding": "lf", "trailingNewline": true }, "parser": { "frontmatter": false, "gfm": true } } }各配置项的含义与默认值(来自 markdown.rs 的结构体定义):
| 配置项 | 默认值 | 作用 |
|---|---|---|
markdown.formatter.enabled | false(实验性,默认关闭) | 是否启用 Markdown 格式化 |
indentStyle | 跟随全局(测试默认space) | Markdown 文件的缩进风格 |
indentWidth | 2 | 缩进宽度 |
lineWidth | 80 | 单行最大宽度 |
proseWrap | preserve | 段落换行策略:preserve保持原样、always按行宽重排、never合并为单行;手动换行(行尾两个空格或反斜杠)始终保留,见 context.rs 中ProseWrap枚举定义 |
lineEnding | lf | 行尾风格,auto在 Windows 用 CRLF、其他平台用 LF |
trailingNewline | true | 文件末尾是否保留换行符 |
parser.frontmatter | false | 是否解析文件开头的 frontmatter |
parser.gfm | true | 是否启用 GitHub Flavored Markdown 扩展 |
启用后,可对单个文件执行格式化验证本文描述的行为:
biome format crates/biome_markdown_formatter/tests/specs/prettier/markdown/code/mdn-font-face-1.md也可以搭配--write参数直接写入格式化结果,或使用biome check做整体检查。
六、关键要点总结
- 代码块是字面量:无论代码块内部 CSS 的缩进多么混乱,Biome 的 Markdown 格式化器都会将其逐字保留——这正是 code_content.rs 中
with_literal_line_breaks逐行原样输出的结果。 - 格式化边界清晰:格式化器只处理 Markdown 语法层(围栏长度归一化、围栏前后缩进标准化、空行),并遵循 CommonMark §4.5 规则保证围栏严格长于内容中的反引号序列,见 fenced_code_block.rs。
- 兼容性有测试兜底:
mdn-font-face-1.md及其.prettier-snap期望输出由 prepare_tests.js 从 Prettier 套件迁移而来,经 prettier_tests.rs 的快照机制持续验证。 - 默认关闭、需显式开启:Markdown 格式化器在 markdown.rs 中默认禁用,属于实验性功能,需在
biome.json中设置markdown.formatter.enabled: true后才会生效。
对于希望在文档中嵌入 CSS、JavaScript 等代码示例的开发者而言,理解“代码块内容受保护、Markdown 结构被规范化”这一设计,能够帮助你放心地把凌乱的示例代码放进 Markdown——Biome 会替你把围栏整理干净,同时绝不擅自动你的代码。
【免费下载链接】biomeA toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP.项目地址: https://gitcode.com/gh_mirrors/bi/biome
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考