use crate::lint::rule::Rule;
use crate::markdown::MarkdownParser;
use crate::types::Violation;
use serde_json::Value;
use std::collections::HashMap;
pub struct MD005;
impl Rule for MD005 {
fn name(&self) -> &str {
"MD005"
}
fn description(&self) -> &str {
"Inconsistent indentation for list items at the same level"
}
fn tags(&self) -> &[&str] {
&["bullet", "ul", "indentation"]
}
fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
let mut violations = Vec::new();
let mut level_indents: HashMap<usize, usize> = HashMap::new();
let mut prev_indent = 0;
let mut current_level = 0;
for (line_num, line) in parser.lines().iter().enumerate() {
let line_number = line_num + 1;
let is_list_item = line.trim_start().starts_with("* ")
|| line.trim_start().starts_with("+ ")
|| line.trim_start().starts_with("- ")
|| line
.trim_start()
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false);
if !is_list_item {
continue;
}
let indent = line.len() - line.trim_start().len();
if indent >= prev_indent + 2 {
current_level += 1;
} else if indent < prev_indent {
current_level = level_indents
.iter()
.filter(|(_, i)| **i == indent)
.map(|(l, _)| *l)
.next()
.unwrap_or(0);
}
if let Some(&expected_indent) = level_indents.get(¤t_level) {
if indent != expected_indent {
violations.push(Violation {
line: line_number,
column: Some(1),
rule: self.name().to_string(),
message: format!(
"List item indentation mismatch: expected {} spaces, found {}",
expected_indent, indent
),
fix: None,
});
}
} else {
level_indents.insert(current_level, indent);
}
prev_indent = indent;
}
violations
}
fn fixable(&self) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_consistent_indentation() {
let content = "* Item 1\n* Item 2\n * Nested 1\n * Nested 2\n* Item 3";
let parser = MarkdownParser::new(content);
let rule = MD005;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
#[test]
fn test_inconsistent_indentation() {
let content = "* Item 1\n * Item 2 - wrong indent\n* Item 3";
let parser = MarkdownParser::new(content);
let rule = MD005;
let violations = rule.check(&parser, None);
assert!(!violations.is_empty());
}
#[test]
fn test_ordered_list() {
let content = "1. Item 1\n2. Item 2\n3. Item 3";
let parser = MarkdownParser::new(content);
let rule = MD005;
let violations = rule.check(&parser, None);
assert_eq!(violations.len(), 0);
}
}