mdbook_lint_core/rules/standard/
md012.rs

1//! MD012: Multiple consecutive blank lines
2//!
3//! This rule checks for multiple consecutive blank lines in the document.
4
5use crate::error::Result;
6use crate::rule::{Rule, RuleCategory, RuleMetadata};
7use crate::{
8    Document,
9    violation::{Severity, Violation},
10};
11
12/// Rule to check for multiple consecutive blank lines
13pub struct MD012 {
14    /// Maximum number of consecutive blank lines allowed
15    maximum: usize,
16}
17
18impl MD012 {
19    /// Create a new MD012 rule with default settings (max 1 blank line)
20    pub fn new() -> Self {
21        Self { maximum: 1 }
22    }
23
24    /// Create a new MD012 rule with custom maximum consecutive blank lines
25    #[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; // Convert to 1-based line numbers
65
66            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                // Non-blank line encountered, check if we had too many blank lines
73                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, // Report at the first violating line
80                        1,
81                        Severity::Warning,
82                    ));
83                }
84                consecutive_blank_lines = 0;
85            }
86        }
87
88        // Check if the document ends with too many blank lines
89        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); // The second blank line
135        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); // First violating line
147        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        // Should allow 2 consecutive blank lines
170        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}