Biome 的 useTopLevelHeading 规则:强制 Markdown 文档以一级标题开头(nursery 组)
【免费下载链接】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
导读
useTopLevelHeading是 Biome 为 Markdown 提供的一条 lint 规则(当前位于nursery组,版本门槛 2.5.8),它要求每个 Markdown 文档的第一个块级元素必须是一级标题(h1),无论是 ATX 语法(# Heading)还是 setext 语法(Heading后跟===)。本文以仓库中的官方测试用例为骨架,从规则行为、判定逻辑、测试矩阵到配置启用方式做一次完整拆解,读完你将能精确理解该规则在哪些场景触发诊断、哪些场景被放行,并掌握在biome.json中启用它的正确姿势。
一、从一条测试用例说起:文档第一块是段落会怎样
仓库中用于验证"文档以普通段落开头"这一非法场景的测试输入位于 paragraph.md,全文如下:
<!-- should generate diagnostics --> Some text # Top-level heading这个文件的第一个非空块是普通段落Some text,虽然后面出现了# Top-level heading,但因为它不在文档开头,useTopLevelHeading依然会报错。对应的快照 paragraph.md.snap 记录了完整诊断输出:
paragraph.md:2:1 lint/nursery/useTopLevelHeading i Missing top-level heading. 1 │ <!-- should generate diagnostics --> > 2 │ Some text │ ^^^^^^^^^ > 3 │ 4 │ # Top-level heading i The document should start with a top-level heading (h1) so readers and tools can identify its title. Add a # Heading (or a level-1 setext heading) at the start of the document.诊断定位在2:1(段落的起始位置),错误消息为Missing top-level heading.,并附带修复建议:在文档开头添加# Heading或一级 setext 标题。
二、规则声明与出处:对齐 markdownlint 的 md041
规则的完整定义位于 use_top_level_heading.rs,其声明信息如下:
pub UseTopLevelHeading { version: "2.5.8", name: "useTopLevelHeading", language: "md", recommended: false, sources: &[RuleSource::MarkdownLint("md041", "first-line-heading").same()], }几个关键点:
recommended: false:该规则默认不随推荐组启用,需要用户显式配置。language: "md":规则只作用于 Markdown 文件。sources:规则语义对齐 markdownlint 的md041 / first-line-heading规则,采用"同一语义"(.same())而非"等效"标注,便于用户从其他工具迁移时理解行为一致性。nursery组:规则尚未稳定,接口与行为在未来可能调整。从 rules.rs 可以看到nursery作为独立配置组存在于 linter 配置结构中。
三、判定逻辑源码级拆解:什么算"合格的第一块"
规则的核心逻辑在run方法中,查询类型为Ast<MdRoot>,即整个文档的语法树根节点。算法分三步:
1. 找到第一个"不可忽略"的块
let first_block = root .value() .iter() .find(|block| !is_ignorable_leading_block(block))?;is_ignorable_leading_block会跳过三类前置内容:
- HTML 注释块(含段落形式的
<!-- ... -->注释,见is_html_comment_block); - 换行块(
block.is_newline()); - 延续缩进块(
block.is_continuation_indent())。
这就是为什么测试输入paragraph.md中第一行<!-- should generate diagnostics -->被忽略,真正的判定对象是后面的段落。
2. 按首块类型分类处理
match first_block { AnyMdBlock::AnyMdLeafBlock(AnyMdLeafBlock::MdHeader(header)) => { if header.level() == 1 { None } else { Some(header.range()) } } AnyMdBlock::AnyMdLeafBlock(AnyMdLeafBlock::MdSetextHeader(header)) => { if header.is_level_1() { None } else { Some(header.range()) } } AnyMdBlock::AnyMdLeafBlock( AnyMdLeafBlock::MdThematicBreakBlock(_) | AnyMdLeafBlock::MdHtmlBlock(_), ) => None, _ => Some(first_block.range()), }分四种情况:
| 首块类型 | 判定结果 |
|---|---|
ATX 标题MdHeader且 level == 1 | 通过,不报错 |
| ATX 标题但 level > 1 | 报错(定位到该标题) |
setext 标题MdSetextHeader且为一级 | 通过,不报错 |
| setext 标题但非一级 | 报错 |
主题分隔线MdThematicBreakBlock | 放行 |
HTML 块MdHtmlBlock | 放行 |
| 其他任何块(段落、列表、引用等) | 报错(定位到该块) |
3. 生成诊断
诊断统一为Missing top-level heading.,并附一条 note 解释原因与修法(添加# Heading或一级 setext 标题)。
需要特别强调的是:HTML 块和主题分隔线被明确放行。规则文档注释给出的理由是:一些项目(尤其是 README)会用 HTML 标记来书写标题,因此 HTML 块开头的文档不报错;同时规则文档还给出"以 YAML front matter 开头"的合法示例。这两类放行都体现在测试套件中。
四、测试矩阵:valid 与 invalid 全量对照
仓库通过一组规格化测试覆盖规则的全部行为,测试位于 useTopLevelHeading 测试目录,分为valid/(不应产生诊断)与invalid/(应产生诊断)两类:
valid:以下文档均不应触发诊断
| 测试文件 | 文档开头形态 | 放行原因 |
|---|---|---|
| heading-1.md | # Top-level heading | 首块即 ATX 一级标题 |
| setext-heading-1.md | Top-level heading+=== | 首块即 setext 一级标题 |
| html.md | <div>HTML content</div>后跟## Second level heading | 首块是 HTML 块,被放行 |
| yaml.md | ---front matter 后跟## Second level heading | front matter 可视为文件前置信息,放行 |
其中 yaml.md 的内容是:
--- path: "/post" date: "2012-06-21T10:14:00.000+02:00" title: "First level heading" --- ## Second level heading这说明文档从 YAML front matter 开始是允许的——这是博客、文档站等场景的常见写法。
invalid:以下文档均应触发诊断
| 测试文件 | 文档开头形态 | 触发原因 |
|---|---|---|
| paragraph.md | 普通段落Some text后跟# Top-level heading | 首块是段落 |
| heading-2.md | ## Second level heading | 首块是二级 ATX 标题 |
| setext-heading-2.md | Second level heading+---- | 首块是二级 setext 标题 |
这些用例覆盖了"非一级标题(ATX 与 setext 两种写法)"以及"非标题块(段落)"两类非法形态,与规则源码中的match分支一一对应。所有测试均由 spec_tests.rs 驱动执行,快照文件与输入文件成对出现,用于锁定诊断输出。
五、选项与配置:如何在项目中启用
规则接受空的选项类型UseTopLevelHeadingOptions(见 use_top_level_heading.rs 选项定义),即当前不提供任何可调参数,行为是固定的。要启用它,需要显式配置nursery组。在项目的biome.json中:
{ "linter": { "enabled": true, "rules": { "nursery": { "useTopLevelHeading": "warn" } } } }nursery组的规则均可通过SeverityOrGroup配置为"error"/"warn"/"info"/"off"(或按规则粒度单独覆盖),具体解析逻辑见 rules.rs。由于该规则recommended: false,即使开启了推荐规则集也不会自动生效,必须如上显式声明。
启用后执行:
biome lint或针对单个文件:
biome lint README.md即可看到形如快照中的Missing top-level heading.诊断。
六、为什么需要这条规则:文档结构与可读性收益
从规则自带的 note 可以看出设计动机:"文档应以一级标题(h1)开头,以便读者与工具识别其标题。"具体收益包括:
- 文档导航与目录生成:渲染器(GitHub、文档站)依据首个 h1 生成页面标题,缺失时标题会退化为文件名或为空;
- 工具链一致性:与 markdownlint
md041对齐后,从 ESLint + markdownlint 迁移到 Biome 的团队可保持相同规范; - 强制内容结构:以标题开头的文档强制作者先给出主题,避免"无题文档"。
同时,规则对 README 场景做了务实让步——HTML 块(如<div>包裹的标题)、主题分隔线、YAML front matter 均不触发诊断,避免误伤常见工程实践。
七、注意事项与稳定性说明
- nursery 稳定性:规则仍处于
nursery阶段,未来可能调整判定边界或默认行为,升级 Biome 后应关注 CHANGELOG 中对该规则的变更记录; - HTML 放行是整体性的:只要首块是 HTML 块,无论内容是否真的是标题,都不会报错(从 html.md 可见);
- 注释放行:开头的 HTML 注释(文件级 preamble 注释)会被跳过,不会因注释在前而误报,这在 paragraph.md 与 heading-1.md 中均有体现;
- 空文档不报错:若文档没有任何块,
find返回None,run直接返回None(不产生诊断)。
延伸阅读
- 规则完整实现:use_top_level_heading.rs
- 全部测试用例:useTopLevelHeading 测试目录
- 诊断快照示例:paragraph.md.snap
- 测试驱动入口:spec_tests.rs
- linter 配置结构(nursery 组解析):rules.rs
【免费下载链接】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),仅供参考