Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/pull_request_markdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ jobs:
#crates/biome_markdown_parser/tests
#crates/biome_markdown_parser/benches/fixtures
#crates/biome_markdown_formatter/tests
#crates/biome_markdown_analyze/tests
Comment thread
ematipico marked this conversation as resolved.
#crates/biome_js_formatter/report*.md
4 changes: 4 additions & 0 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

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.

3 changes: 2 additions & 1 deletion crates/biome_diagnostics_categories/src/categories.rs

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this about?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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
Comment thread
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

```
Loading
Loading