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) -> &str {
10        "MD027"
11    }
12
13    fn description(&self) -> &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_string(),
47                        message: format!(
48                            "Multiple spaces after blockquote symbol ({} spaces)",
49                            space_count
50                        ),
51                        fix: Some(Fix {
52                            line_start: line_number,
53                            line_end: line_number,
54                            column_start: None,
55                            column_end: None,
56                            replacement,
57                            description: "Replace multiple spaces with single space".to_string(),
58                        }),
59                    });
60                }
61            }
62        }
63
64        violations
65    }
66
67    fn fixable(&self) -> bool {
68        true
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::fix::Fixer;
76
77    fn apply_fixes(content: &str, violations: &[Violation]) -> String {
78        let fixes: Vec<_> = violations.iter().filter_map(|v| v.fix.clone()).collect();
79        Fixer::new()
80            .apply_fixes_to_content(content, &fixes)
81            .unwrap()
82    }
83
84    #[test]
85    fn test_correct_blockquote() {
86        let content = "> Quote line 1\n> Quote line 2";
87        let parser = MarkdownParser::new(content);
88        let rule = MD027;
89        let violations = rule.check(&parser, None);
90
91        assert_eq!(violations.len(), 0);
92    }
93
94    #[test]
95    fn test_multiple_spaces() {
96        let content = ">  Quote with 2 spaces\n> Correct quote";
97        let parser = MarkdownParser::new(content);
98        let rule = MD027;
99        let violations = rule.check(&parser, None);
100
101        assert_eq!(violations.len(), 1);
102        assert_eq!(violations[0].line, 1);
103    }
104
105    #[test]
106    fn test_many_spaces() {
107        let content = ">     Quote with 5 spaces";
108        let parser = MarkdownParser::new(content);
109        let rule = MD027;
110        let violations = rule.check(&parser, None);
111
112        assert_eq!(violations.len(), 1);
113        assert!(violations[0].message.contains("5 spaces"));
114    }
115
116    #[test]
117    fn test_no_space() {
118        let content = ">Quote without space";
119        let parser = MarkdownParser::new(content);
120        let rule = MD027;
121        let violations = rule.check(&parser, None);
122
123        assert_eq!(violations.len(), 0); // No space is allowed
124    }
125
126    #[test]
127    fn test_fix_collapses_blockquote_spaces() {
128        let content = ">  Too many spaces\n> Correct line\n";
129        let parser = MarkdownParser::new(content);
130        let rule = MD027;
131        let violations = rule.check(&parser, None);
132        assert_eq!(violations.len(), 1);
133        let fixed = apply_fixes(content, &violations);
134        assert_eq!(fixed, "> Too many spaces\n> Correct line\n");
135    }
136}