rumdl_lib/output/formatters/
text.rs1use crate::output::OutputFormatter;
4use crate::rule::LintWarning;
5use colored::*;
6
7pub struct TextFormatter {
9 use_colors: bool,
10}
11
12impl Default for TextFormatter {
13 fn default() -> Self {
14 Self { use_colors: true }
15 }
16}
17
18impl TextFormatter {
19 pub fn new() -> Self {
20 Self::default()
21 }
22}
23
24impl TextFormatter {
25 pub fn without_colors() -> Self {
26 Self { use_colors: false }
27 }
28}
29
30impl OutputFormatter for TextFormatter {
31 fn format_warnings(&self, warnings: &[LintWarning], file_path: &str) -> String {
32 let mut output = String::new();
33
34 for warning in warnings {
35 let rule_name = warning.rule_name.as_deref().unwrap_or("unknown");
36
37 let fix_indicator = if warning.fix.is_some() { " [*]" } else { "" };
39
40 let line = format!(
42 "{}:{}:{}: {} {}{}",
43 if self.use_colors {
44 file_path.blue().underline().to_string()
45 } else {
46 file_path.to_string()
47 },
48 if self.use_colors {
49 warning.line.to_string().cyan().to_string()
50 } else {
51 warning.line.to_string()
52 },
53 if self.use_colors {
54 warning.column.to_string().cyan().to_string()
55 } else {
56 warning.column.to_string()
57 },
58 if self.use_colors {
59 format!("[{rule_name:5}]").yellow().to_string()
60 } else {
61 format!("[{rule_name:5}]")
62 },
63 warning.message,
64 if self.use_colors {
65 fix_indicator.green().to_string()
66 } else {
67 fix_indicator.to_string()
68 }
69 );
70
71 output.push_str(&line);
72 output.push('\n');
73 }
74
75 if output.ends_with('\n') {
77 output.pop();
78 }
79
80 output
81 }
82
83 fn use_colors(&self) -> bool {
84 self.use_colors
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use crate::rule::{Fix, Severity};
92
93 #[test]
94 fn test_text_formatter_default() {
95 let formatter = TextFormatter::default();
96 assert!(formatter.use_colors());
97 }
98
99 #[test]
100 fn test_text_formatter_new() {
101 let formatter = TextFormatter::new();
102 assert!(formatter.use_colors());
103 }
104
105 #[test]
106 fn test_text_formatter_without_colors() {
107 let formatter = TextFormatter::without_colors();
108 assert!(!formatter.use_colors());
109 }
110
111 #[test]
112 fn test_format_warnings_empty() {
113 let formatter = TextFormatter::without_colors();
114 let warnings = vec![];
115 let output = formatter.format_warnings(&warnings, "test.md");
116 assert_eq!(output, "");
117 }
118
119 #[test]
120 fn test_format_single_warning_no_colors() {
121 let formatter = TextFormatter::without_colors();
122 let warnings = vec![LintWarning {
123 line: 10,
124 column: 5,
125 end_line: 10,
126 end_column: 15,
127 rule_name: Some("MD001".to_string()),
128 message: "Heading levels should only increment by one level at a time".to_string(),
129 severity: Severity::Warning,
130 fix: None,
131 }];
132
133 let output = formatter.format_warnings(&warnings, "README.md");
134 assert_eq!(
135 output,
136 "README.md:10:5: [MD001] Heading levels should only increment by one level at a time"
137 );
138 }
139
140 #[test]
141 fn test_format_warning_with_fix_no_colors() {
142 let formatter = TextFormatter::without_colors();
143 let warnings = vec![LintWarning {
144 line: 15,
145 column: 1,
146 end_line: 15,
147 end_column: 10,
148 rule_name: Some("MD022".to_string()),
149 message: "Headings should be surrounded by blank lines".to_string(),
150 severity: Severity::Warning,
151 fix: Some(Fix {
152 range: 100..110,
153 replacement: "\n# Heading\n".to_string(),
154 }),
155 }];
156
157 let output = formatter.format_warnings(&warnings, "doc.md");
158 assert_eq!(
159 output,
160 "doc.md:15:1: [MD022] Headings should be surrounded by blank lines [*]"
161 );
162 }
163
164 #[test]
165 fn test_format_multiple_warnings_no_colors() {
166 let formatter = TextFormatter::without_colors();
167 let warnings = vec![
168 LintWarning {
169 line: 5,
170 column: 1,
171 end_line: 5,
172 end_column: 10,
173 rule_name: Some("MD001".to_string()),
174 message: "First warning".to_string(),
175 severity: Severity::Warning,
176 fix: None,
177 },
178 LintWarning {
179 line: 10,
180 column: 3,
181 end_line: 10,
182 end_column: 20,
183 rule_name: Some("MD013".to_string()),
184 message: "Second warning".to_string(),
185 severity: Severity::Error,
186 fix: Some(Fix {
187 range: 50..60,
188 replacement: "fixed".to_string(),
189 }),
190 },
191 ];
192
193 let output = formatter.format_warnings(&warnings, "test.md");
194 let expected = "test.md:5:1: [MD001] First warning\ntest.md:10:3: [MD013] Second warning [*]";
195 assert_eq!(output, expected);
196 }
197
198 #[test]
199 fn test_format_warning_unknown_rule() {
200 let formatter = TextFormatter::without_colors();
201 let warnings = vec![LintWarning {
202 line: 1,
203 column: 1,
204 end_line: 1,
205 end_column: 5,
206 rule_name: None,
207 message: "Unknown rule warning".to_string(),
208 severity: Severity::Warning,
209 fix: None,
210 }];
211
212 let output = formatter.format_warnings(&warnings, "file.md");
213 assert_eq!(output, "file.md:1:1: [unknown] Unknown rule warning");
214 }
215
216 #[test]
217 fn test_format_warnings_with_colors() {
218 let formatter = TextFormatter::new(); let warnings = vec![LintWarning {
222 line: 1,
223 column: 1,
224 end_line: 1,
225 end_column: 5,
226 rule_name: Some("MD001".to_string()),
227 message: "Test warning".to_string(),
228 severity: Severity::Warning,
229 fix: Some(Fix {
230 range: 0..5,
231 replacement: "fixed".to_string(),
232 }),
233 }];
234
235 let output = formatter.format_warnings(&warnings, "test.md");
236
237 assert!(formatter.use_colors());
239
240 assert!(output.contains("test.md")); assert!(output.contains("MD001")); assert!(output.contains("Test warning")); assert!(output.contains("[*]")); }
250
251 #[test]
252 fn test_rule_name_padding() {
253 let formatter = TextFormatter::without_colors();
254
255 let warnings = vec![LintWarning {
257 line: 1,
258 column: 1,
259 end_line: 1,
260 end_column: 5,
261 rule_name: Some("MD1".to_string()),
262 message: "Test".to_string(),
263 severity: Severity::Warning,
264 fix: None,
265 }];
266
267 let output = formatter.format_warnings(&warnings, "test.md");
268 assert!(output.contains("[MD1 ]")); }
270
271 #[test]
272 fn test_edge_cases() {
273 let formatter = TextFormatter::without_colors();
274
275 let warnings = vec![LintWarning {
277 line: 99999,
278 column: 12345,
279 end_line: 100000,
280 end_column: 12350,
281 rule_name: Some("MD999".to_string()),
282 message: "Edge case warning".to_string(),
283 severity: Severity::Error,
284 fix: None,
285 }];
286
287 let output = formatter.format_warnings(&warnings, "large.md");
288 assert_eq!(output, "large.md:99999:12345: [MD999] Edge case warning");
289 }
290
291 #[test]
292 fn test_special_characters_in_message() {
293 let formatter = TextFormatter::without_colors();
294 let warnings = vec![LintWarning {
295 line: 1,
296 column: 1,
297 end_line: 1,
298 end_column: 5,
299 rule_name: Some("MD001".to_string()),
300 message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
301 severity: Severity::Warning,
302 fix: None,
303 }];
304
305 let output = formatter.format_warnings(&warnings, "test.md");
306 assert!(output.contains("Warning with \"quotes\" and 'apostrophes' and \n newline"));
307 }
308
309 #[test]
310 fn test_special_characters_in_file_path() {
311 let formatter = TextFormatter::without_colors();
312 let warnings = vec![LintWarning {
313 line: 1,
314 column: 1,
315 end_line: 1,
316 end_column: 5,
317 rule_name: Some("MD001".to_string()),
318 message: "Test".to_string(),
319 severity: Severity::Warning,
320 fix: None,
321 }];
322
323 let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
324 assert!(output.starts_with("path/with spaces/and-dashes.md:1:1:"));
325 }
326
327 #[test]
328 fn test_use_colors_trait_method() {
329 let formatter_with_colors = TextFormatter::new();
330 assert!(formatter_with_colors.use_colors());
331
332 let formatter_without_colors = TextFormatter::without_colors();
333 assert!(!formatter_without_colors.use_colors());
334 }
335}