mdlint/lint/rules/
md005.rs1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use serde_json::Value;
5use std::collections::HashMap;
6
7pub struct MD005;
8
9impl Rule for MD005 {
10 fn name(&self) -> &str {
11 "MD005"
12 }
13
14 fn description(&self) -> &str {
15 "Inconsistent indentation for list items at the same level"
16 }
17
18 fn tags(&self) -> &[&str] {
19 &["bullet", "ul", "indentation"]
20 }
21
22 fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
23 let mut violations = Vec::new();
24 let mut level_indents: HashMap<usize, usize> = HashMap::new();
25 let mut prev_indent = 0;
26 let mut current_level = 0;
27
28 for (line_num, line) in parser.lines().iter().enumerate() {
29 let line_number = line_num + 1;
30
31 let is_list_item = line.trim_start().starts_with("* ")
33 || line.trim_start().starts_with("+ ")
34 || line.trim_start().starts_with("- ")
35 || line
36 .trim_start()
37 .chars()
38 .next()
39 .map(|c| c.is_ascii_digit())
40 .unwrap_or(false);
41
42 if !is_list_item {
43 continue;
44 }
45
46 let indent = line.len() - line.trim_start().len();
48
49 if indent >= prev_indent + 2 {
52 current_level += 1;
53 } else if indent < prev_indent {
54 current_level = level_indents
56 .iter()
57 .filter(|(_, i)| **i == indent)
58 .map(|(l, _)| *l)
59 .next()
60 .unwrap_or(0);
61 }
62 if let Some(&expected_indent) = level_indents.get(¤t_level) {
66 if indent != expected_indent {
67 violations.push(Violation {
68 line: line_number,
69 column: Some(1),
70 rule: self.name().to_string(),
71 message: format!(
72 "List item indentation mismatch: expected {} spaces, found {}",
73 expected_indent, indent
74 ),
75 fix: None,
76 });
77 }
78 } else {
79 level_indents.insert(current_level, indent);
81 }
82
83 prev_indent = indent;
84 }
85
86 violations
87 }
88
89 fn fixable(&self) -> bool {
90 false
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 #[test]
99 fn test_consistent_indentation() {
100 let content = "* Item 1\n* Item 2\n * Nested 1\n * Nested 2\n* Item 3";
101 let parser = MarkdownParser::new(content);
102 let rule = MD005;
103 let violations = rule.check(&parser, None);
104
105 assert_eq!(violations.len(), 0);
106 }
107
108 #[test]
109 fn test_inconsistent_indentation() {
110 let content = "* Item 1\n * Item 2 - wrong indent\n* Item 3";
111 let parser = MarkdownParser::new(content);
112 let rule = MD005;
113 let violations = rule.check(&parser, None);
114
115 assert!(!violations.is_empty());
116 }
117
118 #[test]
119 fn test_ordered_list() {
120 let content = "1. Item 1\n2. Item 2\n3. Item 3";
121 let parser = MarkdownParser::new(content);
122 let rule = MD005;
123 let violations = rule.check(&parser, None);
124
125 assert_eq!(violations.len(), 0);
126 }
127}