Biome Markdown 格式化器围栏代码块缩进规范化解析:以 0-indent-js 测试用例为入口
2026/9/21 18:28:53 网站建设 项目流程

Biome Markdown 格式化器围栏代码块缩进规范化解析:以 0-indent-js 测试用例为入口

【免费下载链接】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

本文以 0-indent-js.md 这一 Prettier 兼容性测试用例为线索,深入剖析 Biome Markdown 格式化器(biome_markdown_formatter)在处理嵌套列表内js围栏代码块时的"根缩进(root indent)"规范化行为。读完本文,你将理解代码块内容为何不会被重新排版、开围栏缩进如何逐行剥离、围栏长度如何按 CommonMark 规则自动归一化,以及如何在本仓库中运行同类用例、通过biome.json配置 Markdown 格式化能力。

一、测试用例本体:一个"输入即输出"的缩进边界场景

1.1 原始输入文件

该用例位于 0-indent-js.md,完整内容如下(行号仅为阅读辅助):

1 | - 1 2 | - 2 3 | - 3 4 | ```js 5 | md` 6 | # this is the root indent 7 | 8 | # this is the root indent 9 | 10 | # this is the root indent 11 | ` 12 | 13 | something` 14 | asd 15 | 16 | asd 17 | 18 | asd 19 | ` 20 | ```

这是一个三层嵌套的无序列表:- 1(缩进 0)、- 2(缩进 2)、- 3(缩进 4),在第三层列表项内部放置了一个缩进 6 个空格的 ```js 围栏代码块。代码块内容是一段 JavaScript 模板字符串字面量:

  • 第一段为md`开头的模板字符串,内含三行重复的# this is the root indent
  • 第二段为something`开头的模板字符串,内含多行asd文本。

从用例命名0-indent-js可以推断其意图:验证缩进为 0(即相对根位置)的 js 代码块内容在格式化后保持不变。这一用例源自 Prettier 官方测试套件,Biome 将其纳入自己的兼容性测试目录,用于对齐 Prettier 的格式化行为。

1.2 预期输出快照:格式化结果与输入完全一致

配套的预期快照 0-indent-js.md.prettier-snap 与输入逐字节一致:列表层级、6 空格缩进、代码块内所有行(包括空行)均原样保留。

这一"输入即输出"的结果揭示了 Biome Markdown 格式化器对围栏代码块的两条核心语义:

  1. 代码块内容是 verbatim(逐字保留)的:无论内容里的 JS 写得多么"乱"(如空行、重复文本),格式化器都不会对其做任何词法/语法级重排;
  2. 代码块内容行的缩进只做"以开围栏为基准"的规范化:每行行首最多剥离与开围栏缩进等长的空格,剥离后剩余部分原样输出,本例中内容行与围栏同处 6 空格缩进,因此结果不变。

二、测试如何被执行:Prettier 兼容性测试基建

2.1 prettier_tests.rs 的宏驱动

该用例位于tests/specs/prettier/markdown/目录下,与普通快照测试目录tests/specs/markdown/分开管理。执行入口是 prettier_tests.rs:

tests_macros::gen_tests! {"tests/specs/prettier/markdown/**/*.{md}", crate::test_snapshot, ""}

gen_tests!宏会为prettier/markdown目录下的每一个.md文件生成一个测试函数,全部调用test_snapshot。该函数的关键配置在 prettier_tests.rs 中:

fn test_snapshot(input: &'static str, _: &str, _: &str, _: &str) { countme::enable(true); let root_path = Utf8Path::new(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/specs/prettier/" )); let test_file = PrettierTestFile::new(input, root_path); let options = MdFormatOptions::default() .with_indent_style(IndentStyle::Space) .with_indent_width(IndentWidth::default()); let language = language::MarkdownTestFormatLanguage::gfm(); let snapshot = PrettierSnapshot::new(test_file, language, MdFormatLanguage::new(options)); snapshot.test() }

几个值得注意的细节:

  • 测试使用空格缩进(IndentStyle::Space与默认缩进宽度(Biome 默认 2 空格),这与0-indent-js.md中每层列表缩进 2 空格的编排吻合;
  • 语言实例通过 language.rs 中的MarkdownTestFormatLanguage::gfm()构造,解析时走parse_markdown_with_cache(..., MarkdownParserOptions::default().with_gfm(true)),即启用 GitHub Flavored Markdown 扩展
  • PrettierSnapshot将格式化结果与同目录下的.prettier-snap文件对比——这正是本用例没有普通.snap文件、只带一个.prettier-snap的原因:它属于"与 Prettier 对齐"的兼容性测试,而非 Biome 独立行为的快照测试。

2.2 与 spec_tests.rs 的分工

对照 spec_tests.rs 可以看到,Biome 自己的快照测试走的是另一条宏路径:

tests_macros::gen_tests! { "tests/specs/markdown/**/*.md", crate::spec_test::run, "" }

它只覆盖tests/specs/markdown/目录,并且在 spec_test.rs 中显式构造配置(MarkdownFormatterConfiguration { enabled: Some(true.into()), .. })来驱动格式化。因此可以推断:tests/specs/prettier/markdown/是专为 Prettier 兼容性对照而设的测试域,0-indent-js.md正是这一对照体系中的一员。

三、源码级原理:围栏代码块的两大格式化机制

理解了测试如何运行后,我们进入实现层。围栏代码块的格式化逻辑分散在两个文件中:外层结构处理在fenced_code_block.rs,内容行的逐行输出在code_content.rs

3.1 开围栏缩进(opening fence indent)的计算与剥离

code_content.rs 负责将MdCodeContent的字面量文本按行输出,其核心是一个"按行扫描 + 有条件剥离行首空格"的循环:

let mut line_start = match bytes { [b'\r', b'\n', ..] => 2, [b'\r' | b'\n', ..] => 1, _ => 0, }; while line_start < bytes.len() { let mut content_start = line_start; let max_content_start = line_start + self.opening_fence_indent; while content_start < bytes.len() && content_start < max_content_start && bytes[content_start] == b' ' { content_start += 1; } // ... 找到行尾并输出 content_start..line_end 的切片 }

这段逻辑的含义非常明确:每一行的行首,最多剥离opening_fence_indent个空格max_content_start = line_start + opening_fence_indent),一旦遇到非空格字符或达到上限立即停止。也就是说:

  • 内容行与围栏对齐的缩进会被"吸收"掉,输出时统一从列表实际内容位置开始;
  • 超过围栏缩进的额外空格不会被误删——这保证了模板字符串、缩进敏感的代码(如本例中的md\`` 与something`` 字面量)在剥离基准缩进后保持相对结构不变。

同时源码注释还指出一个细节:MdCodeContent的字面量以"结束开围栏行的换行"开头,因此循环起始会先跳过开头的\r\n\n;开围栏行 info-string 后的空白会被解析器作为该 token 的前导 trivia 挂载,格式化时特意排除(format_replaced),避免多打印出一行多余的空行。

3.2 opening_fence_indent 的来源与围栏长度归一化

opening_fence_indent由外层 fenced_code_block.rs 计算:

let opening_fence_indent = indent .iter() .map(|token| token.md_indent_char_token().map(|token| token.text().len())) .sum::<Result<usize, _>>()?;

它累加语法树中indent节点(列表项/引用块等容器的缩进 token)的字符长度。对0-indent-js.md而言,三层列表累积的缩进恰为 6 个空格,与围栏自身的 6 空格对齐,于是内容行 6 空格缩进被完整剥离后再原样写出——最终结果与输入一致。

同一个文件中还实现了围栏长度的归一化(fenced_code_block.rs):

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

辅助函数longest_fence_char_sequence(fenced_code_block.rs)遍历MdCodeContent/MdTextual节点,统计内容中连续反引号的最长长度。最终围栏长度取max(最长连续反引号 + 1, 3)——这正对应 CommonMark 规范 §4.5 的要求:闭围栏必须严格长于内容中任何相同字符序列。闭围栏(r_fence)同样会被替换为normalized_fence

3.3 列表 / 引用块上下文的分支处理

fenced_code_block.rs 根据上下文走三条输出路径:

  1. 内容含引用前缀(>)时:整段内容连同>前缀一起dedent_to_root后原样打印(keep_fences_in_italics: true);
  2. MdCodeContent、纯文本行时:按TextPrintMode::Clean/Fill(取决于是否在列表内)走行内项列表格式化;
  3. 存在MdCodeContent(即本例场景)时:对MdCodeContent项单独调用.with_options(FormatMdCodeContentOptions { opening_fence_indent }),其余行内项原样输出——即"内容按行剥离基准缩进"的路径。

此外,开围栏/闭围栏前的缩进 token 在非引用场景下会被显式移除(format_removed),因为列表本身的缩进已由外层列表规则负责;只有在引用块内才保留原缩进 token。

四、同类用例家族:从 sibling 文件看格式化边界

0-indent-js.md并非孤例,tests/specs/prettier/markdown/code/目录下还有一批针对代码块行为的对照用例,共同勾勒出 Biome Markdown 代码块格式化的完整边界:

用例输入要点预期行为
indent.md5 空格缩进的缩进式代码块 + 列表内围栏代码块 + 有序列表缩进代码块保留;1.后补一个空格变为1.(列表标记间距 1→2)
backtick.md10 个反引号的开/闭围栏包住 3 反引号内层代码块外层围栏被归一化为 4 个反引号(max(3+1, 3) = 4
simple.md无语言标注的```代码块内容原样保留
lang.md```js 信息字符串语言标注与内容均不变
format.md```js 内包含明显"乱排"的 JS(多空格、乱换行)内容完全不被重排——印证 verbatim 语义
leading-trailing-newlines.md代码块首尾多个空行空行原样保留
mdn-auth-api.md / mdn-import.mdMDN 文档中真实摘录的javascript /css 大段代码逐字保留(含内部乱缩进)
additional-space.md有序列表项内嵌代码块、段落间多余空行空行/围栏间距规范化,编号自动重排,代码块内容保留
ts-trailing-comma.md / tsx-trailing-comma.mdts /typescript / ```tsx 内<T,>尖括号尾逗号写法内容不触碰,原样输出

从这些用例可以归纳出 Biome Markdown 格式化器对围栏代码块的一贯策略:"围栏与容器负责排版,内容负责保留"——容器(列表、引用、段落间距、围栏长度)由格式化器统一治理,而代码块内部文本永远作为不透明字面量输出。

五、配置与运行:让 Markdown 格式化器跑起来

5.1 配置项总览

Biome 的 Markdown 格式化器目前是实验性特性,默认关闭。在 markdown.rs 中可见其默认值定义:

pub type MarkdownFormatterEnabled = Bool<false>; // Keep it disabled by default while experimental.

MarkdownFormatterConfiguration支持以下字段(见 markdown.rs):

字段说明默认值
enabled是否对 Markdown 及其"超语言"文件启用格式化false(实验性阶段默认关闭)
indentStyle缩进风格(tab/space继承全局,测试中固定为space
indentWidth缩进宽度2
lineWidth行最大宽度80
trailingNewline文件末尾是否保留换行(关闭可能引发其他工具问题,官方强烈不建议)true
lineEnding行尾符(lf/crlf/autoauto在 Windows 用 CRLF,其余平台用 LF)auto
proseWrap段落换行策略(preserve/always/never),手工换行(行尾两空格或反斜杠)始终保留preserve

对应的MarkdownParserConfiguration还提供两个解析级开关(markdown.rs):

  • frontmatter:是否解析文件开头的 frontmatter,默认falseMarkdownParseFrontmatter = Bool<false>);
  • gfm:是否启用 GitHub Flavored Markdown 扩展,默认trueMarkdownParseGfm = Bool<true>)。

5.2 一个可用的 biome.json 示例

{ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "markdown": { "parser": { "frontmatter": true, "gfm": true }, "formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2, "lineWidth": 80, "lineEnding": "lf", "trailingNewline": true, "proseWrap": "preserve" } } }

formatter.enabled: true的前提下,biome format/biome check即会对.md文件按上述规则格式化。

5.3 本地复现本用例

本仓库为纯 Rust 项目(当前环境未安装 cargo,以下命令按仓库结构给出执行方式)。要运行本文讨论的 Prettier 兼容性用例,可在仓库根目录执行:

cargo test -p biome_markdown_formatter --test prettier_tests

若只关注本用例,可用测试名过滤(宏生成测试名基于文件路径,下划线分隔):

cargo test -p biome_markdown_formatter --test prettier_tests 0_indent_js

断言失败时,PrettierSnapshot会输出格式化结果与.prettier-snap的差异,便于对照检查缩进规范化是否符合预期。

六、小结

0-indent-js.md虽然只是一个十几行的测试夹具,但它精确锁定了 Biome Markdown 格式化器的一项关键行为契约:嵌套列表内围栏代码块的内容行,只按开围栏缩进做基准剥离,不做任何内容重排。配合 fenced_code_block.rs 的围栏长度归一化(CommonMark §4.5)与 code_content.rs 的逐行剥离算法,以及prettier/markdown/code目录下十余个 sibling 用例,我们可以完整还原 Biome 在"容器排版、内容 verbatim"这一设计原则下的具体实现。如果你正在为项目接入 Biome 的 Markdown 格式化,理解这条边界能帮你预判哪些代码块会被改动(围栏长度、容器缩进、列表间距),哪些永远不会被动(块内所有文本)。

【免费下载链接】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),仅供参考

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

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

立即咨询