-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
feat: useTopLevelHeading #11286
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: useTopLevelHeading #11286
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| use biome_analyze::{ | ||
| Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule, | ||
| }; | ||
| use biome_console::markup; | ||
| use biome_markdown_syntax::{AnyMdBlock, AnyMdLeafBlock, MdRoot}; | ||
| use biome_rowan::{AstNode, AstNodeList, TextRange}; | ||
| use biome_rule_options::use_top_level_heading::UseTopLevelHeadingOptions; | ||
|
|
||
| declare_lint_rule! { | ||
| /// Require Markdown documents to start with a top-level heading. | ||
| /// | ||
| /// Enforces the first meaningful block in the document to be an h1 heading, | ||
| /// either ATX (`# Heading`) or setext level 1 (`Heading` followed by `===`). | ||
| /// | ||
| /// Leading HTML comments used as file preamble are ignored when determining | ||
| /// the first meaningful block. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's this about?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the markdown files starts with a comment, like we do in test files, we skip the comment to check what the next block is
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like an implementation detail, doesn't it? Or, if not, reword it? "Meaningful block" is very technical. What's a meaningful block? That's not something I would write to our end users |
||
| /// | ||
| /// The rule does not report when the first meaningful block is an HTML block | ||
| /// or a thematic break block. | ||
| /// | ||
| /// ## Examples | ||
| /// | ||
| /// ### Invalid | ||
| /// | ||
| /// ```md,expect_diagnostic | ||
| /// Some text | ||
| /// | ||
| /// # Heading | ||
| /// ``` | ||
| /// | ||
| /// ### Valid | ||
| /// | ||
| /// ```md | ||
| /// # Heading | ||
| /// | ||
| /// Some text | ||
| /// ``` | ||
| /// | ||
| /// ```md | ||
| /// --- | ||
| /// title: Example | ||
| /// --- | ||
| /// | ||
| /// ## Section | ||
| /// ``` | ||
| /// | ||
| /// ```md | ||
| /// <div>Intro</div> | ||
| /// | ||
| /// ## Section | ||
| /// ``` | ||
| /// | ||
| pub UseTopLevelHeading { | ||
| version: "next", | ||
| name: "useTopLevelHeading", | ||
| language: "md", | ||
| recommended: false, | ||
| sources: &[RuleSource::MarkdownLint("md041", "first-line-heading").same()], | ||
| } | ||
| } | ||
|
|
||
| impl Rule for UseTopLevelHeading { | ||
| type Query = Ast<MdRoot>; | ||
| type State = TextRange; | ||
| type Signals = Option<Self::State>; | ||
| type Options = UseTopLevelHeadingOptions; | ||
|
|
||
| fn run(ctx: &RuleContext<Self>) -> Self::Signals { | ||
| let root = ctx.query(); | ||
| let first_block = root | ||
| .value() | ||
| .iter() | ||
| .find(|block| !is_ignorable_leading_block(block))?; | ||
|
|
||
| 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()), | ||
| } | ||
| } | ||
|
|
||
| fn diagnostic(_ctx: &RuleContext<Self>, state: &Self::State) -> Option<RuleDiagnostic> { | ||
| Some( | ||
| RuleDiagnostic::new( | ||
| rule_category!(), | ||
| *state, | ||
| markup! { | ||
| "Missing top-level heading." | ||
| }, | ||
| ) | ||
| .note(markup! { | ||
| "The first meaningful block should be a top-level heading (h1) so readers and tools can identify the document title. Add a "<Emphasis>"# Heading"</Emphasis>" (or a level-1 setext heading) to the start of the document." | ||
| }), | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| fn is_html_comment_block(block: &AnyMdBlock) -> bool { | ||
| match block { | ||
| AnyMdBlock::AnyMdLeafBlock(AnyMdLeafBlock::MdHtmlBlock(html_block)) => { | ||
| html_block.is_html_comment() | ||
| } | ||
| AnyMdBlock::AnyMdLeafBlock(AnyMdLeafBlock::MdParagraph(paragraph)) => { | ||
| let text = paragraph.syntax().text_trimmed().to_string(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This allocates a string |
||
| text.starts_with("<!--") && text.ends_with("-->") | ||
| } | ||
| _ => false, | ||
| } | ||
| } | ||
|
|
||
| fn is_ignorable_leading_block(block: &AnyMdBlock) -> bool { | ||
| is_html_comment_block(block) || block.is_newline() || block.is_continuation_indent() | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| <!-- should generate diagnostics --> | ||
| ## Second level heading |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| --- | ||
| source: crates/biome_markdown_analyze/tests/spec_tests.rs | ||
| expression: heading-2.md | ||
| --- | ||
| # Input | ||
| ```md | ||
| <!-- should generate diagnostics --> | ||
| ## Second level heading | ||
|
|
||
| ``` | ||
|
|
||
| # Diagnostics | ||
| ``` | ||
| heading-2.md:2:1 lint/nursery/useTopLevelHeading ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| i Missing top-level heading. | ||
|
|
||
| 1 │ <!-- should generate diagnostics --> | ||
| > 2 │ ## Second level heading | ||
| │ ^^^^^^^^^^^^^^^^^^^^^^^ | ||
| 3 │ | ||
|
|
||
| i The first meaningful block should be a top-level heading (h1) so readers and tools can identify the document title. Add a # Heading (or a level-1 setext heading) to the start of the document. | ||
|
|
||
| i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. | ||
|
|
||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| <!-- should generate diagnostics --> | ||
| Some text | ||
|
|
||
| # Top-level heading |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| --- | ||
| source: crates/biome_markdown_analyze/tests/spec_tests.rs | ||
| expression: paragraph.md | ||
| --- | ||
| # Input | ||
| ```md | ||
| <!-- should generate diagnostics --> | ||
| Some text | ||
|
|
||
| # Top-level heading | ||
|
|
||
| ``` | ||
|
|
||
| # Diagnostics | ||
| ``` | ||
| paragraph.md:2:1 lint/nursery/useTopLevelHeading ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| i Missing top-level heading. | ||
|
|
||
| 1 │ <!-- should generate diagnostics --> | ||
| > 2 │ Some text | ||
| │ ^^^^^^^^^ | ||
| > 3 │ | ||
| │ | ||
| 4 │ # Top-level heading | ||
| 5 │ | ||
|
|
||
| i The first meaningful block should be a top-level heading (h1) so readers and tools can identify the document title. Add a # Heading (or a level-1 setext heading) to the start of the document. | ||
|
|
||
| i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. | ||
|
|
||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| <!-- should generate diagnostics --> | ||
| Second level heading | ||
| -------------------- |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| --- | ||
| source: crates/biome_markdown_analyze/tests/spec_tests.rs | ||
| expression: setext-heading-2.md | ||
| --- | ||
| # Input | ||
| ```md | ||
| <!-- should generate diagnostics --> | ||
| Second level heading | ||
| -------------------- | ||
|
|
||
| ``` | ||
|
|
||
| # Diagnostics | ||
| ``` | ||
| setext-heading-2.md:2:1 lint/nursery/useTopLevelHeading ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ | ||
|
|
||
| i Missing top-level heading. | ||
|
|
||
| 1 │ <!-- should generate diagnostics --> | ||
| > 2 │ Second level heading | ||
| │ ^^^^^^^^^^^^^^^^^^^^ | ||
| > 3 │ -------------------- | ||
| │ ^^^^^^^^^^^^^^^^^^^^ | ||
| 4 │ | ||
|
|
||
| i The first meaningful block should be a top-level heading (h1) so readers and tools can identify the document title. Add a # Heading (or a level-1 setext heading) to the start of the document. | ||
|
|
||
| i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information. | ||
|
|
||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| <!-- should not generate diagnostics --> | ||
| # Top-level heading |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| source: crates/biome_markdown_analyze/tests/spec_tests.rs | ||
| expression: heading-1.md | ||
| --- | ||
| # Input | ||
| ```md | ||
| <!-- should not generate diagnostics --> | ||
| # Top-level heading | ||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| <!-- should not generate diagnostics --> | ||
| <div>HTML content</div> | ||
|
|
||
| ## Second level heading |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| --- | ||
| source: crates/biome_markdown_analyze/tests/spec_tests.rs | ||
| expression: html.md | ||
| --- | ||
| # Input | ||
| ```md | ||
| <!-- should not generate diagnostics --> | ||
| <div>HTML content</div> | ||
|
|
||
| ## Second level heading | ||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| <!-- should not generate diagnostics --> | ||
| Top-level heading | ||
| ================= |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| source: crates/biome_markdown_analyze/tests/spec_tests.rs | ||
| expression: setext-heading-1.md | ||
| --- | ||
| # Input | ||
| ```md | ||
| <!-- should not generate diagnostics --> | ||
| Top-level heading | ||
| ================= | ||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| <!-- should not generate diagnostics --> | ||
| --- | ||
| path: "/post" | ||
| date: "2012-06-21T10:14:00.000+02:00" | ||
| title: "First level heading" | ||
| --- | ||
|
|
||
| ## Second level heading | ||
|
Netail marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| --- | ||
| source: crates/biome_markdown_analyze/tests/spec_tests.rs | ||
| expression: yaml.md | ||
| --- | ||
| # Input | ||
| ```md | ||
| <!-- should not generate diagnostics --> | ||
| --- | ||
| path: "/post" | ||
| date: "2012-06-21T10:14:00.000+02:00" | ||
| title: "First level heading" | ||
| --- | ||
|
|
||
| ## Second level heading | ||
|
|
||
| ``` |
Uh oh!
There was an error while loading. Please reload this page.