mdlint/lint/rules/
md012.rs1use 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 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 if consecutive_blank > maximum {
70 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); }
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); }
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}