mdlint/lint/rules/
md055.rs1use crate::lint::rule::Rule;
2use crate::markdown::MarkdownParser;
3use crate::types::Violation;
4use serde_json::Value;
5
6pub struct MD055;
7
8impl Rule for MD055 {
9 fn name(&self) -> &str {
10 "MD055"
11 }
12
13 fn description(&self) -> &str {
14 "Table pipe style"
15 }
16
17 fn tags(&self) -> &[&str] {
18 &["table"]
19 }
20
21 fn check(&self, parser: &MarkdownParser, config: Option<&Value>) -> Vec<Violation> {
22 let style = config
23 .and_then(|c| c.get("style"))
24 .and_then(|v| v.as_str())
25 .unwrap_or("leading_and_trailing");
26
27 let mut violations = Vec::new();
28 let mut first_style: Option<&str> = None;
29 let code_block_lines = parser.get_code_block_line_numbers();
30
31 for (line_num, line) in parser.lines().iter().enumerate() {
32 let line_number = line_num + 1;
33
34 if code_block_lines.contains(&line_number) {
35 continue;
36 }
37
38 if !line.contains('|') {
40 continue;
41 }
42
43 let trimmed = line.trim();
44
45 let has_leading = trimmed.starts_with('|');
47 let has_trailing = trimmed.ends_with('|');
48
49 let current_style = match (has_leading, has_trailing) {
50 (true, true) => "leading_and_trailing",
51 (true, false) => "leading_only",
52 (false, true) => "trailing_only",
53 (false, false) => "no_leading_or_trailing",
54 };
55
56 if style == "consistent" {
57 if let Some(first) = first_style {
58 if current_style != first {
59 let (first_leading, first_trailing) = match first {
61 "leading_and_trailing" => (true, true),
62 "leading_only" => (true, false),
63 "trailing_only" => (false, true),
64 "no_leading_or_trailing" => (false, false),
65 _ => (false, false),
66 };
67
68 if has_leading != first_leading {
70 violations.push(Violation {
71 line: line_number,
72 column: Some(1),
73 rule: self.name().to_string(),
74 message: format!(
75 "Table pipe style should be consistent: expected {}, found {}",
76 if first_leading {
77 "leading pipe"
78 } else {
79 "no leading pipe"
80 },
81 if has_leading {
82 "leading pipe"
83 } else {
84 "no leading pipe"
85 }
86 ),
87 fix: None,
88 });
89 }
90
91 if has_trailing != first_trailing {
93 violations.push(Violation {
94 line: line_number,
95 column: Some(1),
96 rule: self.name().to_string(),
97 message: format!(
98 "Table pipe style should be consistent: expected {}, found {}",
99 if first_trailing {
100 "trailing pipe"
101 } else {
102 "no trailing pipe"
103 },
104 if has_trailing {
105 "trailing pipe"
106 } else {
107 "no trailing pipe"
108 }
109 ),
110 fix: None,
111 });
112 }
113 }
114 } else {
115 first_style = Some(current_style);
116 }
117 } else if style == "leading_and_trailing" && current_style != "leading_and_trailing" {
118 if !has_leading {
120 violations.push(Violation {
121 line: line_number,
122 column: Some(1),
123 rule: self.name().to_string(),
124 message: "Table should have leading pipe".to_string(),
125 fix: None,
126 });
127 }
128 if !has_trailing {
129 violations.push(Violation {
130 line: line_number,
131 column: Some(1),
132 rule: self.name().to_string(),
133 message: "Table should have trailing pipe".to_string(),
134 fix: None,
135 });
136 }
137 } else if style == "no_leading_or_trailing" && (has_leading || has_trailing) {
138 if has_leading {
140 violations.push(Violation {
141 line: line_number,
142 column: Some(1),
143 rule: self.name().to_string(),
144 message: "Table should not have leading pipe".to_string(),
145 fix: None,
146 });
147 }
148 if has_trailing {
149 violations.push(Violation {
150 line: line_number,
151 column: Some(1),
152 rule: self.name().to_string(),
153 message: "Table should not have trailing pipe".to_string(),
154 fix: None,
155 });
156 }
157 }
158 }
159
160 violations
161 }
162
163 fn fixable(&self) -> bool {
164 false
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_consistent_with_pipes() {
174 let content = "| Col1 | Col2 |\n|------|------|\n| A | B |";
175 let parser = MarkdownParser::new(content);
176 let rule = MD055;
177 let violations = rule.check(&parser, None);
178
179 assert_eq!(violations.len(), 0);
180 }
181
182 #[test]
183 fn test_consistent_without_pipes() {
184 let content = "Col1 | Col2\n-----|-----\nA | B";
185 let parser = MarkdownParser::new(content);
186 let rule = MD055;
187 let config = serde_json::json!({ "style": "consistent" });
188 let violations = rule.check(&parser, Some(&config));
189
190 assert_eq!(violations.len(), 0);
191 }
192
193 #[test]
194 fn test_inconsistent_pipes() {
195 let content = "| Col1 | Col2 |\n|------|------|\nA | B";
196 let parser = MarkdownParser::new(content);
197 let rule = MD055;
198 let violations = rule.check(&parser, None);
199
200 assert_eq!(violations.len(), 2);
202 }
203
204 #[test]
205 fn test_enforced_leading_and_trailing() {
206 let content = "Col1 | Col2\n-----|-----\nA | B";
207 let parser = MarkdownParser::new(content);
208 let rule = MD055;
209 let config = serde_json::json!({ "style": "leading_and_trailing" });
210 let violations = rule.check(&parser, Some(&config));
211
212 assert_eq!(violations.len(), 6);
214 }
215
216 #[test]
217 fn test_simple_table() {
218 let content = "| Header |\n| ------ |\n| Cell |";
219 let parser = MarkdownParser::new(content);
220 let rule = MD055;
221 let violations = rule.check(&parser, None);
222
223 assert_eq!(violations.len(), 0);
224 }
225}