- 开发工具
- Lint
- 格式化
- 静态分析
- 代码质量
- 前端
【免费下载链接】biome
A toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP.
本篇文章围绕 Biome 的 Markdown lint 规则useSingleTopLevelHeading展开,以规则测试套件中的 deeper_levels.md 用例为切入点,讲清该规则的核心语义、判定逻辑、level配置项以及完整的测试矩阵。读完本文,你将掌握如何在项目中配置该规则,理解其"何时报错、何时保持沉默"的边界条件,并能读懂对应测试用例与快照文件。
一、规则背景:为什么一个 Markdown 文档只能有一个顶层标题
useSingleTopLevelHeading是 Biome 内置的 Markdown lint 规则(language: "md"),其声明位于 use_single_top_level_heading.rs,规则要求:
一个 Markdown 文档应只有一个顶层标题(top-level heading),它充当文档的标题(title);默认情况下是
h1,后续标题应使用更低层级(h2、h3等)。
该规则受 markdownlint 的MD025 single-title规则启发(sources: &[RuleSource::MarkdownLint("md025", "single-title").inspired()]),目前归属于nursery分组,即尚未稳定、未来可能调整的规则,recommended: true。
规则的诊断信息明确说明了约束理由:
- 单一顶层标题充当文档标题,多余的顶层标题会破坏文档大纲(document outlines)、目录(tables of contents),以及转成 HTML 后的文档结构;
- 修复建议是"将该标题降级为更低层级,或将其所在章节移入独立文档"。
二、deeper_levels 测试用例:只有深层级标题时,规则保持沉默
本篇文章的主角是测试输入文件 deeper_levels.md,其内容如下:
<!-- should not generate diagnostics --> ### Heading 3 #### Heading 4 ### Heading 3 again文件的头注释明确了它的断言意图:不应当产生任何诊断(should not generate diagnostics)。
这个用例验证的是一个容易被忽略的行为边界:当文档完全不存在顶层标题(既没有h1,也没有通过level配置指定的层级),只有更深层级的标题(h3、h4)时,规则不会报告任何问题。
为什么?结合规则的实现逻辑可以理解:
- 规则只关心"顶层标题的数量"——即标题层级等于配置
level的标题; deeper_levels.md中的### Heading 3和#### Heading 4层级分别为 3 和 4,都不等于默认的顶层层级 1;- 因此没有候选标题参与判定,文档自然不会被报告。
对应的快照文件 deeper_levels.md.snap 也印证了这一点——快照中只有# Input部分,没有任何 Diagnostics 段落,说明规则对该输入保持了静默。
三、规则实现原理:源码视角的判定流程
规则的核心实现在use_single_top_level_heading.rs中,其查询类型为Ast<AnyMdHeader>,即每次匹配到一个 Markdown 标题节点时触发。判定流程可分为四步:
1. 层级过滤
let level = ctx.options().level() as usize; if header.level() != level { return None; }规则只对层级等于配置值的标题感兴趣,其余层级直接忽略。默认level为 1。
2. front matter 提前短路
if root.frontmatter().is_some() { return None; }当文档带有 front matter(仅在markdown.parser.frontmatter开启时才会被解析)时,规则直接保持沉默。原因是 front matter 可能已经承载了文档标题(如title: ...),正文中的标题不一定构成重复,为避免误报宁可放过。这一点在 yaml_front_matter.md 测试中得到验证:带 front matter 的文档即使包含两个#标题也不报错。
3. 必须是文档的直接子节点
if header.syntax().parent().as_ref() != Some(block_list.syntax()) { return None; }标题必须直接挂在文档的块列表中(direct child of the document)。嵌套在 blockquote 或列表项里的标题永远不计为文档标题,也永远不会被报告为多余的顶层标题。测试用例 nested_headings.md 专门覆盖了这一点:
<!-- should not generate diagnostics --> > # Title - # Another top-level heading尽管这里出现了两个#标题,但都被嵌套在引用和列表中,因此不产生诊断。
4. 确认首个顶层标题是文档标题
let title = header .syntax() .siblings(Direction::Prev) .skip(1) .filter_map(AnyMdHeader::cast) .filter(|header| header.level() == level) .last()?; if !is_document_title(&title) { return None; }规则会向前查找同层级的最近一个标题,并通过辅助函数is_document_title判断它是否真的是文档标题:
fn is_document_title(header: &AnyMdHeader) -> bool { header .syntax() .siblings(Direction::Prev) .skip(1) .all(|sibling| match sibling.kind() { MD_NEWLINE => true, MD_HTML_BLOCK => { MdHtmlBlock::cast(sibling).is_some_and(|block| block.is_html_comment()) } _ => false, }) }一个标题要成为"文档标题",其前面只能有空白行和 HTML 注释。如果标题之前存在段落、其他层级的标题等"非空白"内容,则它不算文档标题,规则保持沉默。测试用例 no_title_at_start.md 验证了这一行为:
<!-- should not generate diagnostics --> Some intro paragraph precedes the first top-level heading. # One # Two文档先有一段引言段落,再出现两个#标题,由于第一个#标题前有段落,不满足"文档标题"条件,因此不报错。
只有确认存在一个"真正的文档标题"之后,后续的每个同级顶层标题才会被报告为多余标题(Some(title.range())),诊断信息会同时指向违规标题("This document has more than one top-level heading.")和原始标题("The other top-level heading is here.")。
四、报错场景:invalid 用例与快照对照
与deeper_levels.md相对的是 invalid.md:
<!-- should generate diagnostics --> # One # Two # Three文档以# One开头(前面只有 HTML 注释和空行,满足is_document_title),因此# Two和# Three都会被报告。对应快照 invalid.md.snap 展示了完整的诊断输出:
- 第 5 行
# Two和 第 7 行# Three各产生一条lint/nursery/useSingleTopLevelHeading诊断; - 诊断主信息为 "This document has more than one top-level heading.";
- 附加 detail 指向文档标题
# One("The other top-level heading is here."); - 附带两条 note:一是说明顶层标题对文档大纲、目录和 HTML 结构的意义;二是给出修复建议"降级标题或拆分文档"。
同样会产生诊断的还有 setext 风格标题测试 setext.md:
<!-- should generate diagnostics --> Title ===== Another Title =============两个用=====下划线构成的 setext 标题(h1)同样构成"多个顶层标题",规则对 ATX(#形式)和 setext(下划线形式)标题一视同仁。
混合场景 mixed_headings.md 则验证了 ATX 与 setext 混合时也会触发:
<!-- should generate diagnostics --> # Title Another Title =============五、level 选项:改变"顶层"的判定层级
规则的选项定义在biome_rule_optionscrate 的use_single_top_level_heading模块中,核心选项只有一个:level。
/// Use the `level` option to change which heading level is treated as the top-level one. /// This is useful when an external tool (a static site generator, for example) already /// injects an `h1` for the page title, so the Markdown source is expected to start at `h2`. /// The value must be between `1` and `6`. /// /// Default: `1`.- 取值范围:1 到 6;
- 默认值:1。
适用场景:当外部工具(如静态站点生成器)已经为页面注入了h1作为页面标题时,Markdown 源码约定从h2开始,此时应将level配置为 2。
测试目录中的 level_two.options.json 给出了完整的配置示例:
{ "$schema": "../../../../../../packages/@biomejs/biome/configuration_schema.json", "linter": { "rules": { "nursery": { "useSingleTopLevelHeading": { "level": "error", "options": { "level": 2 } } } } } }配套测试 level_two.md 验证了level=2时的行为:
<!-- should generate diagnostics --> ## Section A # An h1, ignored under level=2 Section B --------- Content More content在level=2下,## Section A是顶层标题;# An h1与Section B下的 setext 标题(---------对应h2)中,第二个h2会被报告,而h1因为层级不等于 2 被忽略(注意测试注释 "An h1, ignored under level=2")。这正是"更深的标题层级不参与判定"这一核心语义在自定义层级下的再次体现,与deeper_levels.md验证的是同一条规则。
六、完整测试矩阵一览
useSingleTopLevelHeading测试目录(crates/biome_markdown_analyze/tests/specs/nursery/useSingleTopLevelHeading)中的用例覆盖了规则的全部行为边界:
| 测试文件 | 是否产生诊断 | 验证点 |
|---|---|---|
deeper_levels.md | 否 | 文档只有 h3/h4,无顶层标题时不报错 |
valid.md | 否 | 单个#标题 + 更低层级标题,符合规范 |
nested_headings.md | 否 | 引用/列表中的标题不计入顶层标题 |
no_title_at_start.md | 否 | 首个顶层标题前有段落,不算文档标题 |
yaml_front_matter.md | 否 | 存在 front matter 时规则静默 |
empty_list_before_title.md | 否 | 标题前的空列表不影响判定 |
empty_quote_before_title.md | 否 | 标题前的空引用不影响判定 |
html_comment_before_title.md | 否 | 标题前的 HTML 注释被允许 |
invalid.md | 是 | 多个 ATXh1全部报告 |
setext.md | 是 | 多个 setexth1同样报告 |
mixed_headings.md | 是 | ATX 与 setext 混合时报告 |
level_two.md(level=2) | 是 | 自定义层级下报告多余h2 |
invalid_level.md(level=2) | 是 | 自定义层级场景的另一变体 |
每个用例都配套.snap快照文件,由 spec_tests.rs 驱动执行。快照中# Input段落展示输入内容;若产生诊断,则# Diagnostics段落展示完整输出,否则只有 Input 段落(如deeper_levels.md.snap所示)。
七、在项目中配置与运行
配置规则
在biome.json中启用该规则并调整层级:
{ "linter": { "rules": { "nursery": { "useSingleTopLevelHeading": "error" } } } }指定level选项:
{ "linter": { "rules": { "nursery": { "useSingleTopLevelHeading": { "level": "error", "options": { "level": 2 } } } } } }需要注意:规则位于nursery分组,启用时需显式声明(recommended: true意味着在推荐配置中开启,但 nursery 规则的语义可能随版本演进发生变化)。
本地运行测试
如需在本地验证规则行为,可运行 Biome 的 Markdown 分析测试:
cargo test -p biome_markdown_analyze测试会读取tests/specs/nursery/useSingleTopLevelHeading/目录下的用例,并将实际输出与.snap快照比对,从而验证deeper_levels.md这类"不应产生诊断"的用例确实保持静默。
八、小结
deeper_levels.md虽然只是一份简短的测试输入文件,但它精准锁定了useSingleTopLevelHeading规则的一条核心边界:规则只统计顶层标题的数量,文档里只有更深层级标题(h3、h4)时不属于"多个顶层标题",不产生任何诊断。结合 use_single_top_level_heading.rs 的实现源码与整个测试目录,可以完整还原该规则的判定链条——层级过滤、front matter 短路、直接子节点校验、文档标题确认——并据此在实际项目中正确配置level选项、避免误报,让 Markdown 文档保持单一标题的清晰大纲结构。
- 开发工具
- Lint
- 格式化
- 静态分析
- 代码质量
- 前端
【免费下载链接】biome
A toolchain for web projects, aimed to provide functionalities to maintain them. Biome offers formatter and linter, usable via CLI and LSP.
相关推荐
WebPlotDigitizer 图表数据提取完整指南:5大痛点场景实测与配置详解
WebPlotDigitizer 图表数据提取完整指南:5大痛点场景实测与配置详解 论文里的曲线图没有原始数据、老文献里的手绘图只能靠眼睛读数、报告的柱状图想复
开发工具Lint格式化静态分析代码质量前端ComfyUI工作流进阶指南:从模块化思维到创作效率提升
ComfyUI工作流进阶指南:从模块化思维到创作效率提升 如果你已经熟悉ComfyUI的基础操作,却常常在复杂工作流中迷失方向,或者花费大量时间重复配置相同节点
开发工具Lint格式化静态分析代码质量前端ThinkPHP验证场景终极指南:快速掌握多场景验证规则配置技巧
ThinkPHP验证场景终极指南:快速掌握多场景验证规则配置技巧 ThinkPHP作为一款十年匠心的高性能PHP框架,其强大的验证功能可以帮助开发者轻松处理各种
后端Web框架
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考