Skip to main content

mdlint/lint/rules/
md025.rs

1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use pulldown_cmark::{Event, HeadingLevel, Tag};
5use serde_json::Value;
6
7pub struct MD025;
8
9impl Rule for MD025 {
10    fn name(&self) -> &str {
11        "MD025"
12    }
13
14    fn description(&self) -> &str {
15        "Multiple top-level headings in the same document"
16    }
17
18    fn tags(&self) -> &[&str] {
19        &["headings", "headers"]
20    }
21
22    fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
23        let mut violations = Vec::new();
24        let mut first_h1_line: Option<usize> = None;
25
26        for (event, range) in parser.parse_with_offsets() {
27            if let Event::Start(Tag::Heading {
28                level: HeadingLevel::H1,
29                ..
30            }) = event
31            {
32                let line = parser.offset_to_line(range.start);
33
34                if let Some(first_line) = first_h1_line {
35                    violations.push(Violation {
36                        line,
37                        column: Some(1),
38                        rule: self.name().to_string(),
39                        message: format!(
40                            "Multiple top-level headings (first h1 at line {})",
41                            first_line
42                        ),
43                        fix: None,
44                    });
45                } else {
46                    first_h1_line = Some(line);
47                }
48            }
49        }
50
51        violations
52    }
53
54    fn fixable(&self) -> bool {
55        false
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn test_single_h1() {
65        let content = "# Title\n## Section\n### Subsection";
66        let parser = MarkdownParser::new(content);
67        let rule = MD025;
68        let violations = rule.check(&parser, None);
69
70        assert_eq!(violations.len(), 0);
71    }
72
73    #[test]
74    fn test_multiple_h1() {
75        let content = "# First Title\n## Section\n# Second Title";
76        let parser = MarkdownParser::new(content);
77        let rule = MD025;
78        let violations = rule.check(&parser, None);
79
80        assert_eq!(violations.len(), 1);
81        assert_eq!(violations[0].line, 3);
82    }
83
84    #[test]
85    fn test_three_h1() {
86        let content = "# First\n# Second\n# Third";
87        let parser = MarkdownParser::new(content);
88        let rule = MD025;
89        let violations = rule.check(&parser, None);
90
91        assert_eq!(violations.len(), 2); // Second and third are violations
92    }
93
94    #[test]
95    fn test_no_h1() {
96        let content = "## Section\n### Subsection";
97        let parser = MarkdownParser::new(content);
98        let rule = MD025;
99        let violations = rule.check(&parser, None);
100
101        assert_eq!(violations.len(), 0);
102    }
103}