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) -> &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); }
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}