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) -> &'static str {
11 "MD005"
12 }
13
14 fn description(&self) -> &'static 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 .is_some_and(|c| c.is_ascii_digit());
40
41 if !is_list_item {
42 continue;
43 }
44
45 let indent = line.len() - line.trim_start().len();
47
48 if indent >= prev_indent + 2 {
51 current_level += 1;
52 } else if indent < prev_indent {
53 current_level = level_indents
55 .iter()
56 .filter(|(_, i)| **i == indent)
57 .map(|(l, _)| *l)
58 .next()
59 .unwrap_or(0);
60 }
61 if let Some(&expected_indent) = level_indents.get(¤t_level) {
65 if indent != expected_indent {
66 violations.push(Violation {
67 line: line_number,
68 column: Some(1),
69 rule: self.name().to_owned(),
70 message: format!(
71 "List item indentation mismatch: expected {expected_indent} spaces, found {indent}"
72 ),
73 fix: None,
74 });
75 }
76 } else {
77 level_indents.insert(current_level, indent);
79 }
80
81 prev_indent = indent;
82 }
83
84 violations
85 }
86
87 fn fixable(&self) -> bool {
88 false
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn test_consistent_indentation() {
98 let content = "* Item 1\n* Item 2\n * Nested 1\n * Nested 2\n* Item 3";
99 let parser = MarkdownParser::new(content);
100 let rule = MD005;
101 let violations = rule.check(&parser, None);
102
103 assert_eq!(violations.len(), 0);
104 }
105
106 #[test]
107 fn test_inconsistent_indentation() {
108 let content = "* Item 1\n * Item 2 - wrong indent\n* Item 3";
109 let parser = MarkdownParser::new(content);
110 let rule = MD005;
111 let violations = rule.check(&parser, None);
112
113 assert!(!violations.is_empty());
114 }
115
116 #[test]
117 fn test_ordered_list() {
118 let content = "1. Item 1\n2. Item 2\n3. Item 3";
119 let parser = MarkdownParser::new(content);
120 let rule = MD005;
121 let violations = rule.check(&parser, None);
122
123 assert_eq!(violations.len(), 0);
124 }
125}