mdlint/lint/rules/
md025.rs1use 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); }
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}