Skip to main content

mdlint/lint/rules/
md028.rs

1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use serde_json::Value;
5
6pub struct MD028;
7
8impl Rule for MD028 {
9    fn name(&self) -> &'static str {
10        "MD028"
11    }
12
13    fn description(&self) -> &'static str {
14        "Blank line inside blockquote"
15    }
16
17    fn tags(&self) -> &[&str] {
18        &["blockquote", "whitespace"]
19    }
20
21    fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
22        let mut violations = Vec::new();
23        let lines = parser.lines();
24        let mut in_blockquote = false;
25
26        for (line_num, line) in lines.iter().enumerate() {
27            let line_number = line_num + 1;
28            let trimmed = line.trim_start();
29
30            let is_blockquote_line = trimmed.starts_with('>');
31            let is_blank = line.trim().is_empty();
32
33            if is_blockquote_line {
34                in_blockquote = true;
35            } else if is_blank && in_blockquote {
36                // Look ahead to find if blockquote continues (skip multiple blank lines)
37                let mut found_continuation = false;
38                for future_line in lines.iter().skip(line_num + 1) {
39                    let trimmed_start = future_line.trim_start();
40                    if trimmed_start.starts_with('>') {
41                        found_continuation = true;
42                        break;
43                    } else if !future_line.trim().is_empty() {
44                        // Non-blank, non-blockquote line means blockquote ended
45                        break;
46                    }
47                }
48
49                if found_continuation {
50                    violations.push(Violation {
51                        line: line_number,
52                        column: Some(1),
53                        rule: self.name().to_owned(),
54                        message: "Blank line inside blockquote".to_owned(),
55                        fix: None,
56                    });
57                    // After reporting violation, don't check subsequent blank lines
58                    in_blockquote = false;
59                } else {
60                    in_blockquote = false;
61                }
62            } else if !is_blank {
63                // Non-blockquote, non-blank line ends the blockquote
64                in_blockquote = false;
65            }
66        }
67
68        violations
69    }
70
71    fn fixable(&self) -> bool {
72        false
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn test_continuous_blockquote() {
82        let content = "> Line 1\n> Line 2\n> Line 3";
83        let parser = MarkdownParser::new(content);
84        let rule = MD028;
85        let violations = rule.check(&parser, None);
86
87        assert_eq!(violations.len(), 0);
88    }
89
90    #[test]
91    fn test_blank_inside_blockquote() {
92        let content = "> Line 1\n\n> Line 2";
93        let parser = MarkdownParser::new(content);
94        let rule = MD028;
95        let violations = rule.check(&parser, None);
96
97        assert_eq!(violations.len(), 1);
98        assert_eq!(violations[0].line, 2);
99    }
100
101    #[test]
102    fn test_blank_ends_blockquote() {
103        let content = "> Line 1\n\nNormal text";
104        let parser = MarkdownParser::new(content);
105        let rule = MD028;
106        let violations = rule.check(&parser, None);
107
108        assert_eq!(violations.len(), 0); // Blank line ends the blockquote
109    }
110
111    #[test]
112    fn test_multiple_blank_lines() {
113        let content = "> Line 1\n\n\n> Line 2";
114        let parser = MarkdownParser::new(content);
115        let rule = MD028;
116        let violations = rule.check(&parser, None);
117
118        assert_eq!(violations.len(), 1); // First blank line is the violation
119    }
120}