Skip to main content

mdlint/lint/rules/
md027.rs

1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::{Fix, Violation};
4use serde_json::Value;
5
6pub struct MD027;
7
8impl Rule for MD027 {
9    fn name(&self) -> &'static str {
10        "MD027"
11    }
12
13    fn description(&self) -> &'static str {
14        "Multiple spaces after blockquote symbol"
15    }
16
17    fn tags(&self) -> &[&str] {
18        &["blockquote", "whitespace", "indentation"]
19    }
20
21    fn check(&self, parser: &MarkdownParser, _config: Option<&Value>) -> Vec<Violation> {
22        let mut violations = Vec::new();
23
24        for (line_num, line) in parser.lines().iter().enumerate() {
25            let line_number = line_num + 1;
26            let trimmed = line.trim_start();
27
28            // Check if line starts with blockquote marker
29            if let Some(after_gt) = trimmed.strip_prefix('>') {
30                // Count spaces after the >
31                let space_count = after_gt.chars().take_while(|&c| c == ' ').count();
32
33                if space_count > 1 {
34                    // Replace multiple spaces with single space
35                    let leading_spaces = &line[..line.len() - trimmed.len()];
36                    let content = after_gt[space_count..].trim_start();
37                    let replacement = if content.is_empty() {
38                        format!("{leading_spaces}>")
39                    } else {
40                        format!("{leading_spaces}> {content}")
41                    };
42
43                    violations.push(Violation {
44                        line: line_number,
45                        column: Some(line.len() - trimmed.len() + 2),
46                        rule: self.name().to_owned(),
47                        message: format!(
48                            "Multiple spaces after blockquote symbol ({space_count} spaces)"
49                        ),
50                        fix: Some(Fix {
51                            line_start: line_number,
52                            line_end: line_number,
53                            column_start: None,
54                            column_end: None,
55                            replacement,
56                            description: "Replace multiple spaces with single space".to_owned(),
57                        }),
58                    });
59                }
60            }
61        }
62
63        violations
64    }
65
66    fn fixable(&self) -> bool {
67        true
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::fix::Fixer;
75
76    fn apply_fixes(content: &str, violations: &[Violation]) -> String {
77        let fixes: Vec<_> = violations.iter().filter_map(|v| v.fix.clone()).collect();
78        Fixer::new()
79            .apply_fixes_to_content(content, &fixes)
80            .unwrap()
81    }
82
83    #[test]
84    fn test_correct_blockquote() {
85        let content = "> Quote line 1\n> Quote line 2";
86        let parser = MarkdownParser::new(content);
87        let rule = MD027;
88        let violations = rule.check(&parser, None);
89
90        assert_eq!(violations.len(), 0);
91    }
92
93    #[test]
94    fn test_multiple_spaces() {
95        let content = ">  Quote with 2 spaces\n> Correct quote";
96        let parser = MarkdownParser::new(content);
97        let rule = MD027;
98        let violations = rule.check(&parser, None);
99
100        assert_eq!(violations.len(), 1);
101        assert_eq!(violations[0].line, 1);
102    }
103
104    #[test]
105    fn test_many_spaces() {
106        let content = ">     Quote with 5 spaces";
107        let parser = MarkdownParser::new(content);
108        let rule = MD027;
109        let violations = rule.check(&parser, None);
110
111        assert_eq!(violations.len(), 1);
112        assert!(violations[0].message.contains("5 spaces"));
113    }
114
115    #[test]
116    fn test_no_space() {
117        let content = ">Quote without space";
118        let parser = MarkdownParser::new(content);
119        let rule = MD027;
120        let violations = rule.check(&parser, None);
121
122        assert_eq!(violations.len(), 0); // No space is allowed
123    }
124
125    #[test]
126    fn test_fix_collapses_blockquote_spaces() {
127        let content = ">  Too many spaces\n> Correct line\n";
128        let parser = MarkdownParser::new(content);
129        let rule = MD027;
130        let violations = rule.check(&parser, None);
131        assert_eq!(violations.len(), 1);
132        let fixed = apply_fixes(content, &violations);
133        assert_eq!(fixed, "> Too many spaces\n> Correct line\n");
134    }
135}