mdlint/lint/rules/
md009.rs1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::{Fix, Violation};
4use serde_json::Value;
5
6pub struct MD009;
7
8impl Rule for MD009 {
9 fn name(&self) -> &str {
10 "MD009"
11 }
12
13 fn description(&self) -> &str {
14 "Trailing spaces"
15 }
16
17 fn tags(&self) -> &[&str] {
18 &["whitespace"]
19 }
20
21 fn check(&self, parser: &MarkdownParser, config: Option<&Value>) -> Vec<Violation> {
22 let br_spaces = config
23 .and_then(|c| c.get("br_spaces"))
24 .and_then(|v| v.as_u64())
25 .unwrap_or(2) as usize;
26
27 let strict = config
28 .and_then(|c| c.get("strict"))
29 .and_then(|v| v.as_bool())
30 .unwrap_or(false);
31
32 let mut violations = Vec::new();
33
34 for (line_num, line) in parser.lines().iter().enumerate() {
35 let trimmed = line.trim_end();
36 let trailing_spaces = line.len() - trimmed.len();
37
38 if trailing_spaces > 0 {
39 if !strict && trailing_spaces == br_spaces {
41 continue;
42 }
43
44 violations.push(Violation {
45 line: line_num + 1,
46 column: Some(trimmed.len() + 1),
47 rule: self.name().to_string(),
48 message: format!("Trailing spaces ({} spaces)", trailing_spaces),
49 fix: Some(Fix {
50 line_start: line_num + 1,
51 line_end: line_num + 1,
52 column_start: Some(trimmed.len() + 1),
53 column_end: Some(line.len()),
54 replacement: String::new(),
55 description: "Remove trailing spaces".to_string(),
56 }),
57 });
58 }
59 }
60
61 violations
62 }
63
64 fn fixable(&self) -> bool {
65 true
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72 use crate::fix::Fixer;
73
74 fn apply_fixes(content: &str, violations: &[Violation]) -> String {
75 let fixes: Vec<_> = violations.iter().filter_map(|v| v.fix.clone()).collect();
76 Fixer::new()
77 .apply_fixes_to_content(content, &fixes)
78 .unwrap()
79 }
80
81 #[test]
82 fn test_no_trailing_spaces() {
83 let content = "Line 1\nLine 2\nLine 3";
84 let parser = MarkdownParser::new(content);
85 let rule = MD009;
86 let violations = rule.check(&parser, None);
87
88 assert_eq!(violations.len(), 0);
89 }
90
91 #[test]
92 fn test_trailing_spaces() {
93 let content = "Line 1 \nLine 2\nLine 3 ";
94 let parser = MarkdownParser::new(content);
95 let rule = MD009;
96 let violations = rule.check(&parser, None);
97
98 assert_eq!(violations.len(), 1); assert_eq!(violations[0].line, 3);
100 assert_eq!(violations[0].column, Some(7));
101 }
102
103 #[test]
104 fn test_strict_mode() {
105 let content = "Line 1 \nLine 2";
106 let parser = MarkdownParser::new(content);
107 let rule = MD009;
108 let config = serde_json::json!({ "strict": true });
109 let violations = rule.check(&parser, Some(&config));
110
111 assert_eq!(violations.len(), 1);
112 assert_eq!(violations[0].line, 1);
113 }
114
115 #[test]
116 fn test_custom_br_spaces() {
117 let content = "Line 1 \nLine 2";
118 let parser = MarkdownParser::new(content);
119 let rule = MD009;
120 let config = serde_json::json!({ "br_spaces": 3 });
121 let violations = rule.check(&parser, Some(&config));
122
123 assert_eq!(violations.len(), 0); }
125
126 #[test]
127 fn test_fix_removes_trailing_spaces() {
128 let content = "Line 1 \nLine 2\n";
129 let parser = MarkdownParser::new(content);
130 let rule = MD009;
131 let violations = rule.check(&parser, None);
132 let fixed = apply_fixes(content, &violations);
133 assert_eq!(fixed, "Line 1\nLine 2\n");
134 }
135}