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) -> &'static str {
11        "MD025"
12    }
13
14    fn description(&self) -> &'static 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_owned(),
39                        message: format!(
40                            "Multiple top-level headings (first h1 at line {first_line})"
41                        ),
42                        fix: None,
43                    });
44                } else {
45                    first_h1_line = Some(line);
46                }
47            }
48        }
49
50        violations
51    }
52
53    fn fixable(&self) -> bool {
54        false
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn test_single_h1() {
64        let content = "# Title\n## Section\n### Subsection";
65        let parser = MarkdownParser::new(content);
66        let rule = MD025;
67        let violations = rule.check(&parser, None);
68
69        assert_eq!(violations.len(), 0);
70    }
71
72    #[test]
73    fn test_multiple_h1() {
74        let content = "# First Title\n## Section\n# Second Title";
75        let parser = MarkdownParser::new(content);
76        let rule = MD025;
77        let violations = rule.check(&parser, None);
78
79        assert_eq!(violations.len(), 1);
80        assert_eq!(violations[0].line, 3);
81    }
82
83    #[test]
84    fn test_three_h1() {
85        let content = "# First\n# Second\n# Third";
86        let parser = MarkdownParser::new(content);
87        let rule = MD025;
88        let violations = rule.check(&parser, None);
89
90        assert_eq!(violations.len(), 2); // Second and third are violations
91    }
92
93    #[test]
94    fn test_no_h1() {
95        let content = "## Section\n### Subsection";
96        let parser = MarkdownParser::new(content);
97        let rule = MD025;
98        let violations = rule.check(&parser, None);
99
100        assert_eq!(violations.len(), 0);
101    }
102}