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