Biome Markdown 格式化器对 blockquote(引用块)边界的处理:从 notext-end 测试用例看源码实现
【免费下载链接】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
[!NOTE] 导读 本文以 Biome 仓库中的 Markdown 格式化测试用例
crates/biome_markdown_formatter/tests/specs/prettier/markdown/blockquote/notext-end.md为切入点,剖析 Biome 的 Markdown 格式化器在遇到「以空引用行结尾」或「引用块内嵌套引用」时如何规范化>前缀与空行。读完本文,你将掌握该测试用例的输入/输出差异、QuoteBoundaryTrim边界裁剪算法、MdQuotePrefix前缀重建逻辑,以及这些行为与proseWrap配置项的联动关系。
1. 测试用例背景:它测的是什么
在 Biome 的 Markdown 格式化测试体系中,blockquote/目录存放着一组专门针对引用块(blockquote)的回归与兼容性用例,包含simple.md、nested.md、code.md、list.md、paragraph.md、interrupt-others.md、issue-14228.md以及本文主角notext-end.md等。命名中的notext-end意为「结尾处没有文本」——即引用块最后一行是仅含>标记的空引用行,而非实际内容行。
该文件同时存在三个变体,构成完整的「输入—预期—实测」闭环:
| 文件 | 作用 |
|---|---|
notext-end.md | 原始输入(未格式化) |
notext-end.md.prettier-snap | Prettier 的格式化输出,用于兼容性对比 |
notext-end.md.snap | Biome 的快照,记录输入、与 Prettier 的 diff、Biome 实际输出 |
快照头部信息表明它由crates/biome_formatter_test/src/snapshot_builder.rs生成,info字段标注markdown/blockquote/notext-end.md,说明该用例会同时跑 Prettier 对比和 Biome 自身输出两条验证链路。
1.1 输入内容拆解
原始输入由五组引用块组成,覆盖了五类典型场景:
> [!NOTE] > `DOOM` > _b_ >> `A` >> `B` > *a* >> # foo >> `a` > `b` > This is a quote with an italic _across multuple lines > which should just work_. So make sure there is no > if we set > proseWrap to `never` > This is a quote with a link [across multuple lines > which should just work](). So make sure there is no > if we set > proseWrap to `never`- 第一组:GitHub 风格的
[!NOTE]提示块(Alert 语法)+ 内联代码; - 第二组:外层引用内嵌双层引用(
>与>>混排),内容为行内代码A、B; - 第三组:外层引用内嵌「标题 + 行内代码比较表达式」(
>a>b``),且引用块之后紧接两个连续空行; - 第四组:跨行斜体(italic),文本在换行处跨过引用前缀;
- 第五组:跨行链接(link),同样在换行处跨过引用前缀。
其中第四、五组的注释明确提示:当proseWrap设为never时,换行处不应被插入>前缀——这正是本用例要守护的行为边界。
2. Biome 输出与 Prettier 的差异:快照中的 diff 在说什么
Biome 对notext-end.md的实际输出为:
> [!NOTE] > `DOOM` > _b_ > > `A` > > `B` > _a_ > > # foo > > `a` > `b` > This is a quote with an italic _across multuple lines > which should just work_. So make sure there is no > if we set > proseWrap to `never` > This is a quote with a link [across multuple lines > which should just work](). So make sure there is no > if we set > proseWrap to `never`对照notext-end.md.snap中记录的「Prettier differences」diff,可以精确还原 Biome 与 Prettier 的两处分歧:
- 嵌套引用之间的空引用行被移除:Prettier 在
> _b_与> >A之间保留了一行 `>`(仅一个引用标记的空行),Biome 则将其删除,直接让 `> _b_` 与下一层引用 `> > `A相邻; - 嵌套引用内的标题前空行被移除:Prettier 在
> > # foo之前保留了> >空引用行,Biome 同样删除,并将> >a>b`` 与标题直接衔接; - 引用块之间的空行数量被归一化:第三组引用块与第四组之间原本有两个空行,Prettier 输出中空行被压缩,Biome 则保留了两个空行(即输入中的空行结构),并在 diff 中体现为新增的
+空行。
这三处差异共同指向 Biome 的一个明确设计取向:默认(非 Preserve 模式)下对引用边界(仅含>前缀、不含任何内容的行)做「瘦身」处理,但保留引用块之间真实的空行分隔。
3. 源码实现:边界裁剪从何而来
行为背后的核心逻辑位于crates/biome_markdown_formatter/src/markdown/lists/block_list.rs,其中定义了三种边界裁剪模式:
pub(crate) enum QuoteBoundaryTrim { /// Preserve quote-only boundary lines. #[default] None, /// Remove quote-only lines before blockquote content. Leading, /// Remove quote-only lines before and after blockquote content. LeadingAndTrailing, }选择哪种模式由 FormatMdQuote 在格式化每个MdQuote节点时决定:
let quote_boundary_trim = if node.syntax().next_sibling().is_none() { QuoteBoundaryTrim::LeadingAndTrailing } else { QuoteBoundaryTrim::Leading };即:若该引用块没有后续兄弟节点(是最后一个块),则同时裁剪首尾边界;否则只裁剪头部边界。这解释了为什么输入中位于文件中间、后面还有其他引用块的组别只处理前置空引用行,而不会误删其后的空行。
3.1 头部边界扫描算法
quote_boundary_trim_start展示了实现细节的微妙之处:纯引用行的 CST 形状并不统一——引用块自身前缀对应的首个空行表现为内容列表里的MdNewline,而后续的纯引用行则表现为MdQuotePrefix+MdNewline成对出现。算法因此只精确匹配这两种形态,一旦遇到「前缀后跟真实内容」就立即停止:
// The first empty line of a blockquote is represented by the quote node's // own prefix plus a leading newline in the content list. if iter.peek().is_some_and(|(_, block)| block.is_newline()) { iter.next(); start = 1; } // Additional quote-only leading lines are represented as // MdQuotePrefix + MdNewline pairs. while let Some((prefix_index, AnyMdBlock::MdQuotePrefix(_))) = iter.next() { if iter.peek().is_some_and(|(_, block)| block.is_newline()) { iter.next(); start = prefix_index + 2; } else { break; } }3.2 尾部边界扫描算法
quote_boundary_trim_end采用反向扫描:只把「末尾的MdQuotePrefix」或「MdQuotePrefix+MdNewline」识别为尾部纯引用行。注释特别强调:单独的MdNewline不足以判定为边界,它只有在前一个条目是MdQuotePrefix时才属于纯引用行——避免误删真实内容行的换行。
被判定为边界的条目,在FormatMdBlockList和QuoteBlockList中通过format_removed_quote_boundary输出为空,从而在最终渲染中消失(见 shared.rs 的对应辅助函数)。
4. 前缀重建:MdQuotePrefix的「补空格」行为
当引用内容被重新排版后,Biome 需要逐行重建>前缀。这一逻辑在 quote_prefix.rs:
if let Some(post_marker_space_token) = post_marker_space_token { write!(f, [post_marker_space_token.format()])?; } else { let marker = marker_token?; let next_has_text = marker .next_token() .is_some_and(|t| t.text().starts_with(|c: char| !c.is_whitespace())); if next_has_text { write!(f, [space()])?; } }- 若 CST 中保留了
>后的空格 token,原样输出; - 若没有该 token,则检查紧随其后的 token 是否以非空白字符开头——是则补一个空格(保证
> text的规范形态),否则不加(保持>空行形态)。
此外该文件还支持should_remove选项:当引用块以空行开头(starts_with_blank_line)且裁剪范围非空时,FormatMdQuote会让整个前缀以format_removed输出,从而把开头的空引用行整体抹掉(quote.rs)。
5. 与proseWrap的联动:跨行内容为什么不被插入>
输入第四、五组的注释点明了关键约束:跨行斜体与跨行链接在proseWrap: never下不应在续行补>。这与FormatMdQuote的分支结构对应:
let prose_wrap = f.options().prose_wrap(); if prose_wrap == ProseWrap::Preserve && should_format_quote_structurally(node)? { return Quote::new(node.clone()).fmt(f); }proseWrap: preserve且引用内含「结构性续行」(跨行的段落文本等)时,走Quote::new(...)的专门实现(quote.rs),其中QuoteParagraph会针对跨行段落判断should_format_quote_continuation_after_newline,并配合QuoteLinePrefix按需重建行前缀;- 其余情况(含
never/always)走通用路径,用align("> ", &content)对齐内容——该模式只会按块级结构输出前缀,不会在段落内部的软换行处硬塞>。
ProseWrap枚举定义在 context.rs,取值preserve/always/never,并通过with_prose_wrap注入格式化上下文。因此本用例实际上覆盖了两种渲染策略下的边界稳定性:既验证默认对齐模式对纯引用行的裁剪,也验证 preserve 模式下跨行文本的前缀重建不会破坏语义。
6. 测试如何运行与验证
blockquote/用例属于 Prettier 兼容性测试套件。在crates/biome_markdown_formatter/tests/spec_tests.rs中,tests_macros::gen_tests!会扫描tests/specs/markdown/**/*.md生成测试函数;而prettier/子目录的用例由 prettier_tests.rs 驱动,逻辑上与crates/biome_formatter_test的快照构建器协作:读取输入 → 分别调用 Biome 与 Prettier 格式化 → 生成.snap快照记录 diff → 断言 Biome 输出与 Prettier 输出的一致性策略。
因此notext-end.md的价值是双重的:
- 对格式化器开发者:它是边界行为的回归守卫,任何对
QuoteBoundaryTrim、MdQuotePrefix重建逻辑的改动都会在此用例的快照 diff 中原形毕露; - 对使用者:它直观展示了 Biome 在 blockquote 嵌套、空引用行、跨行行内元素等复杂组合下的输出约定——嵌套引用的空行会被收敛,引用块间的空行保留,跨行内容不补前缀。
7. 小结
通过notext-end.md这一个用例,可以串起 Biome Markdown 格式化器关于引用块的三条核心规则:
- 边界瘦身:默认模式下,仅含
>的纯引用行在块首被裁剪(最后一块还会裁剪块尾),由QuoteBoundaryTrim与quote_boundary_trim_range实现; - 前缀重建:
>后的空格按「后随 token 是否为文本」动态决定,保证输出规范且稳定(quote_prefix.rs); - proseWrap 联动:
preserve模式下跨行段落走专门的结构化路径(quote.rs),never/always下走对齐路径,跨行处均不额外插入>。
如需进一步研究,可对比同目录下的 nested.md(嵌套引用)、code.md(代码块边界)与 issue-14228.md(历史回归问题),它们共同刻画了 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),仅供参考