mdlint/lint/rules/
md007.rs1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use serde_json::Value;
5
6pub struct MD007;
7
8impl Rule for MD007 {
9 fn name(&self) -> &str {
10 "MD007"
11 }
12
13 fn description(&self) -> &str {
14 "Unordered list indentation"
15 }
16
17 fn tags(&self) -> &[&str] {
18 &["bullet", "ul", "indentation"]
19 }
20
21 fn check(&self, parser: &MarkdownParser, config: Option<&Value>) -> Vec<Violation> {
22 let indent_size = config
23 .and_then(|c| c.get("indent"))
24 .and_then(|v| v.as_u64())
25 .unwrap_or(2) as usize;
26
27 let mut violations = Vec::new();
28 let mut list_depth = 0;
29 let mut prev_indent = 0;
30 let code_block_lines = parser.get_code_block_line_numbers();
31
32 for (line_num, line) in parser.lines().iter().enumerate() {
33 let line_number = line_num + 1;
34
35 if code_block_lines.contains(&line_number) {
36 continue;
37 }
38
39 let trimmed = line.trim_start();
40
41 let is_ul_item =
43 trimmed.starts_with("* ") || trimmed.starts_with("+ ") || trimmed.starts_with("- ");
44
45 if !is_ul_item {
46 if !line.trim().is_empty() && !trimmed.starts_with(" ") {
47 list_depth = 0;
49 prev_indent = 0;
50 }
51 continue;
52 }
53
54 let indent = line.len() - trimmed.len();
56
57 if indent > prev_indent {
59 list_depth += 1;
61 } else if indent < prev_indent {
62 list_depth = indent / indent_size;
64 }
65
66 let expected_indent = list_depth * indent_size;
67
68 if indent != expected_indent {
69 violations.push(Violation {
70 line: line_number,
71 column: Some(1),
72 rule: self.name().to_string(),
73 message: format!(
74 "Unordered list indentation should be {} spaces (found {})",
75 expected_indent, indent
76 ),
77 fix: None,
78 });
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_correct_indentation() {
98 let content = "* Item 1\n * Nested 1\n * Double nested\n * Nested 2\n* Item 2";
99 let parser = MarkdownParser::new(content);
100 let rule = MD007;
101 let violations = rule.check(&parser, None);
102
103 assert_eq!(violations.len(), 0);
104 }
105
106 #[test]
107 fn test_incorrect_indentation() {
108 let content = "* Item 1\n * Nested wrong - 3 spaces instead of 2";
109 let parser = MarkdownParser::new(content);
110 let rule = MD007;
111 let violations = rule.check(&parser, None);
112
113 assert!(!violations.is_empty());
114 }
115
116 #[test]
117 fn test_custom_indent_size() {
118 let content = "* Item 1\n * Nested with 4 spaces";
119 let parser = MarkdownParser::new(content);
120 let rule = MD007;
121 let config = serde_json::json!({ "indent": 4 });
122 let violations = rule.check(&parser, Some(&config));
123
124 assert_eq!(violations.len(), 0);
125 }
126
127 #[test]
128 fn test_multiple_levels() {
129 let content = "* Level 1\n * Level 2\n * Level 3\n * Level 4";
130 let parser = MarkdownParser::new(content);
131 let rule = MD007;
132 let violations = rule.check(&parser, None);
133
134 assert_eq!(violations.len(), 0);
135 }
136}