Skip to main content

mdlint/lint/rules/
md012.rs

1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::{Fix, Violation};
4use serde_json::Value;
5
6pub struct MD012;
7
8impl Rule for MD012 {
9    fn name(&self) -> &str {
10        "MD012"
11    }
12
13    fn description(&self) -> &str {
14        "Multiple consecutive blank lines"
15    }
16
17    fn tags(&self) -> &[&str] {
18        &["whitespace", "blank_lines"]
19    }
20
21    fn check(&self, parser: &MarkdownParser, config: Option<&Value>) -> Vec<Violation> {
22        let maximum = config
23            .and_then(|c| c.get("maximum"))
24            .and_then(|v| v.as_u64())
25            .unwrap_or(1) as usize;
26
27        let mut violations = Vec::new();
28        let mut consecutive_blank = 0;
29        let mut blank_start_line = 0;
30
31        for (line_num, line) in parser.lines().iter().enumerate() {
32            let line_number = line_num + 1;
33
34            if line.trim().is_empty() {
35                if consecutive_blank == 0 {
36                    blank_start_line = line_number;
37                }
38                consecutive_blank += 1;
39            } else {
40                if consecutive_blank > maximum {
41                    // Report a violation for each excess blank line
42                    for i in maximum..consecutive_blank {
43                        violations.push(Violation {
44                            line: blank_start_line + i,
45                            column: Some(1),
46                            rule: self.name().to_string(),
47                            message: format!(
48                                "{} [Expected: {}; Actual: {}]",
49                                self.description(),
50                                1,
51                                consecutive_blank
52                            ),
53                            fix: Some(Fix {
54                                line_start: blank_start_line + i,
55                                line_end: blank_start_line + i,
56                                column_start: None,
57                                column_end: None,
58                                replacement: String::new(),
59                                description: "Remove excess blank line".to_string(),
60                            }),
61                        });
62                    }
63                }
64                consecutive_blank = 0;
65            }
66        }
67
68        // Check if file ends with too many blank lines
69        if consecutive_blank > maximum {
70            // Report a violation for each excess blank line
71            for i in maximum..consecutive_blank {
72                violations.push(Violation {
73                    line: blank_start_line + i,
74                    column: Some(1),
75                    rule: self.name().to_string(),
76                    message: format!("Expected: {}; Actual: {}", 1, consecutive_blank),
77                    fix: Some(Fix {
78                        line_start: blank_start_line + i,
79                        line_end: blank_start_line + i,
80                        column_start: None,
81                        column_end: None,
82                        replacement: String::new(),
83                        description: "Remove excess blank line".to_string(),
84                    }),
85                });
86            }
87        }
88
89        violations
90    }
91
92    fn fixable(&self) -> bool {
93        true
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::fix::Fixer;
101
102    fn apply_fixes(content: &str, violations: &[Violation]) -> String {
103        let fixes: Vec<_> = violations.iter().filter_map(|v| v.fix.clone()).collect();
104        Fixer::new()
105            .apply_fixes_to_content(content, &fixes)
106            .unwrap()
107    }
108
109    #[test]
110    fn test_no_consecutive_blanks() {
111        let content = "Line 1\n\nLine 2\n\nLine 3";
112        let parser = MarkdownParser::new(content);
113        let rule = MD012;
114        let violations = rule.check(&parser, None);
115
116        assert_eq!(violations.len(), 0);
117    }
118
119    #[test]
120    fn test_multiple_consecutive_blanks() {
121        let content = "Line 1\n\n\nLine 2";
122        let parser = MarkdownParser::new(content);
123        let rule = MD012;
124        let violations = rule.check(&parser, None);
125
126        assert_eq!(violations.len(), 1);
127        assert_eq!(violations[0].line, 3); // Third line is the excess blank
128    }
129
130    #[test]
131    fn test_custom_maximum() {
132        let content = "Line 1\n\n\nLine 2";
133        let parser = MarkdownParser::new(content);
134        let rule = MD012;
135        let config = serde_json::json!({ "maximum": 2 });
136        let violations = rule.check(&parser, Some(&config));
137
138        assert_eq!(violations.len(), 0); // 2 blank lines allowed
139    }
140
141    #[test]
142    fn test_trailing_blank_lines() {
143        let content = "Line 1\n\n\n";
144        let parser = MarkdownParser::new(content);
145        let rule = MD012;
146        let violations = rule.check(&parser, None);
147
148        assert_eq!(violations.len(), 1);
149    }
150
151    #[test]
152    fn test_fix_removes_excess_blank_line() {
153        let content = "Line 1\n\n\nLine 2\n";
154        let parser = MarkdownParser::new(content);
155        let rule = MD012;
156        let violations = rule.check(&parser, None);
157        assert_eq!(violations.len(), 1);
158        let fixed = apply_fixes(content, &violations);
159        assert_eq!(fixed, "Line 1\n\nLine 2\n");
160    }
161}