Skip to main content

mdlint/lint/rules/
md005.rs

1use 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            // Check if this is a list item
32            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            // Calculate indentation
47            let indent = line.len() - line.trim_start().len();
48
49            // Determine list level based on indentation
50            // Only consider it a new level if indented by at least 2 spaces more
51            if indent >= prev_indent + 2 {
52                current_level += 1;
53            } else if indent < prev_indent {
54                // Find the level for this indentation
55                current_level = level_indents
56                    .iter()
57                    .filter(|(_, i)| **i == indent)
58                    .map(|(l, _)| *l)
59                    .next()
60                    .unwrap_or(0);
61            }
62            // If indent is between prev_indent and prev_indent + 2, stay at same level
63
64            // Check if this level has a recorded indentation
65            if let Some(&expected_indent) = level_indents.get(&current_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                // Record this level's indentation
80                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}