mdbook_lint_core/rules/standard/
md012.rs1use crate::error::Result;
6use crate::rule::{Rule, RuleCategory, RuleMetadata};
7use crate::{
8 Document,
9 violation::{Severity, Violation},
10};
11
12pub struct MD012 {
14 maximum: usize,
16}
17
18impl MD012 {
19 pub fn new() -> Self {
21 Self { maximum: 1 }
22 }
23
24 #[allow(dead_code)]
26 pub fn with_maximum(maximum: usize) -> Self {
27 Self { maximum }
28 }
29}
30
31impl Default for MD012 {
32 fn default() -> Self {
33 Self::new()
34 }
35}
36
37impl Rule for MD012 {
38 fn id(&self) -> &'static str {
39 "MD012"
40 }
41
42 fn name(&self) -> &'static str {
43 "no-multiple-blanks"
44 }
45
46 fn description(&self) -> &'static str {
47 "Multiple consecutive blank lines are not allowed"
48 }
49
50 fn metadata(&self) -> RuleMetadata {
51 RuleMetadata::stable(RuleCategory::Formatting).introduced_in("markdownlint v0.1.0")
52 }
53
54 fn check_with_ast<'a>(
55 &self,
56 document: &Document,
57 _ast: Option<&'a comrak::nodes::AstNode<'a>>,
58 ) -> Result<Vec<Violation>> {
59 let mut violations = Vec::new();
60 let mut consecutive_blank_lines = 0;
61 let mut blank_sequence_start = 0;
62
63 for (line_number, line) in document.lines.iter().enumerate() {
64 let line_num = line_number + 1; if line.trim().is_empty() {
67 if consecutive_blank_lines == 0 {
68 blank_sequence_start = line_num;
69 }
70 consecutive_blank_lines += 1;
71 } else {
72 if consecutive_blank_lines > self.maximum {
74 violations.push(self.create_violation(
75 format!(
76 "Multiple consecutive blank lines ({} found, {} allowed)",
77 consecutive_blank_lines, self.maximum
78 ),
79 blank_sequence_start + self.maximum, 1,
81 Severity::Warning,
82 ));
83 }
84 consecutive_blank_lines = 0;
85 }
86 }
87
88 if consecutive_blank_lines > self.maximum {
90 violations.push(self.create_violation(
91 format!(
92 "Multiple consecutive blank lines at end of file ({} found, {} allowed)",
93 consecutive_blank_lines, self.maximum
94 ),
95 blank_sequence_start + self.maximum,
96 1,
97 Severity::Warning,
98 ));
99 }
100
101 Ok(violations)
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use crate::rule::Rule;
109 use std::path::PathBuf;
110
111 fn create_test_document(content: &str) -> Document {
112 Document::new(content.to_string(), PathBuf::from("test.md")).unwrap()
113 }
114
115 #[test]
116 fn test_md012_no_consecutive_blank_lines() {
117 let content = "# Heading\n\nParagraph one.\n\nParagraph two.";
118 let document = create_test_document(content);
119 let rule = MD012::new();
120 let violations = rule.check(&document).unwrap();
121
122 assert_eq!(violations.len(), 0);
123 }
124
125 #[test]
126 fn test_md012_two_consecutive_blank_lines() {
127 let content = "# Heading\n\n\nParagraph.";
128 let document = create_test_document(content);
129 let rule = MD012::new();
130 let violations = rule.check(&document).unwrap();
131
132 assert_eq!(violations.len(), 1);
133 assert_eq!(violations[0].rule_id, "MD012");
134 assert_eq!(violations[0].line, 3); assert!(violations[0].message.contains("2 found, 1 allowed"));
136 }
137
138 #[test]
139 fn test_md012_three_consecutive_blank_lines() {
140 let content = "# Heading\n\n\n\nParagraph.";
141 let document = create_test_document(content);
142 let rule = MD012::new();
143 let violations = rule.check(&document).unwrap();
144
145 assert_eq!(violations.len(), 1);
146 assert_eq!(violations[0].line, 3); assert!(violations[0].message.contains("3 found, 1 allowed"));
148 }
149
150 #[test]
151 fn test_md012_multiple_violations() {
152 let content = "# Heading\n\n\nParagraph.\n\n\n\nAnother paragraph.";
153 let document = create_test_document(content);
154 let rule = MD012::new();
155 let violations = rule.check(&document).unwrap();
156
157 assert_eq!(violations.len(), 2);
158 assert_eq!(violations[0].line, 3);
159 assert_eq!(violations[1].line, 6);
160 }
161
162 #[test]
163 fn test_md012_custom_maximum() {
164 let content = "# Heading\n\n\nParagraph.";
165 let document = create_test_document(content);
166 let rule = MD012::with_maximum(2);
167 let violations = rule.check(&document).unwrap();
168
169 assert_eq!(violations.len(), 0);
171 }
172
173 #[test]
174 fn test_md012_custom_maximum_violation() {
175 let content = "# Heading\n\n\n\nParagraph.";
176 let document = create_test_document(content);
177 let rule = MD012::with_maximum(2);
178 let violations = rule.check(&document).unwrap();
179
180 assert_eq!(violations.len(), 1);
181 assert!(violations[0].message.contains("3 found, 2 allowed"));
182 }
183
184 #[test]
185 fn test_md012_blank_lines_at_end() {
186 let content = "# Heading\n\nParagraph.\n\n\n";
187 let document = create_test_document(content);
188 let rule = MD012::new();
189 let violations = rule.check(&document).unwrap();
190
191 assert_eq!(violations.len(), 1);
192 assert!(violations[0].message.contains("at end of file"));
193 }
194
195 #[test]
196 fn test_md012_zero_maximum() {
197 let content = "# Heading\n\nParagraph.";
198 let document = create_test_document(content);
199 let rule = MD012::with_maximum(0);
200 let violations = rule.check(&document).unwrap();
201
202 assert_eq!(violations.len(), 1);
203 assert!(violations[0].message.contains("1 found, 0 allowed"));
204 }
205
206 #[test]
207 fn test_md012_only_blank_lines() {
208 let content = "\n\n\n";
209 let document = create_test_document(content);
210 let rule = MD012::new();
211 let violations = rule.check(&document).unwrap();
212
213 assert_eq!(violations.len(), 1);
214 assert!(violations[0].message.contains("at end of file"));
215 }
216}