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) -> &'static str {
10 "MD007"
11 }
12
13 fn description(&self) -> &'static str {
14 "Unordered list indentation"
15 }
16
17 fn tags(&self) -> &[&str] {
18 &["bullet", "ul", "indentation"]
19 }
20
21 #[allow(clippy::cast_possible_truncation)] fn check(&self, parser: &MarkdownParser, config: Option<&Value>) -> Vec<Violation> {
23 let indent_size = config
24 .and_then(|c| c.get("indent"))
25 .and_then(serde_json::Value::as_u64)
26 .unwrap_or(2) as usize;
27
28 let mut violations = Vec::new();
29 let mut list_depth = 0;
30 let mut prev_indent = 0;
31 let code_block_lines = parser.get_code_block_line_numbers();
32
33 for (line_num, line) in parser.lines().iter().enumerate() {
34 let line_number = line_num + 1;
35
36 if code_block_lines.contains(&line_number) {
37 continue;
38 }
39
40 let trimmed = line.trim_start();
41
42 let is_ul_item =
44 trimmed.starts_with("* ") || trimmed.starts_with("+ ") || trimmed.starts_with("- ");
45
46 if !is_ul_item {
47 if !line.trim().is_empty() && !trimmed.starts_with(" ") {
48 list_depth = 0;
50 prev_indent = 0;
51 }
52 continue;
53 }
54
55 let indent = line.len() - trimmed.len();
57
58 if indent > prev_indent {
60 list_depth += 1;
62 } else if indent < prev_indent {
63 list_depth = indent / indent_size;
65 }
66
67 let expected_indent = list_depth * indent_size;
68
69 if indent != expected_indent {
70 violations.push(Violation {
71 line: line_number,
72 column: Some(1),
73 rule: self.name().to_owned(),
74 message: format!(
75 "Unordered list indentation should be {expected_indent} spaces (found {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}