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::new(100..110, "\n# Heading\n".to_string())),
152 }];
153
154 let output = formatter.format_warnings(&warnings, "doc.md");
155 assert_eq!(
156 output,
157 "doc.md:15:1: [MD022] Headings should be surrounded by blank lines [*]"
158 );
159 }
160
161 #[test]
162 fn test_format_multiple_warnings_no_colors() {
163 let formatter = TextFormatter::without_colors();
164 let warnings = vec![
165 LintWarning {
166 line: 5,
167 column: 1,
168 end_line: 5,
169 end_column: 10,
170 rule_name: Some("MD001".to_string()),
171 message: "First warning".to_string(),
172 severity: Severity::Warning,
173 fix: None,
174 },
175 LintWarning {
176 line: 10,
177 column: 3,
178 end_line: 10,
179 end_column: 20,
180 rule_name: Some("MD013".to_string()),
181 message: "Second warning".to_string(),
182 severity: Severity::Error,
183 fix: Some(Fix::new(50..60, "fixed".to_string())),
184 },
185 ];
186
187 let output = formatter.format_warnings(&warnings, "test.md");
188 let expected = "test.md:5:1: [MD001] First warning\ntest.md:10:3: [MD013] Second warning [*]";
189 assert_eq!(output, expected);
190 }
191
192 #[test]
193 fn test_format_warning_unknown_rule() {
194 let formatter = TextFormatter::without_colors();
195 let warnings = vec![LintWarning {
196 line: 1,
197 column: 1,
198 end_line: 1,
199 end_column: 5,
200 rule_name: None,
201 message: "Unknown rule warning".to_string(),
202 severity: Severity::Warning,
203 fix: None,
204 }];
205
206 let output = formatter.format_warnings(&warnings, "file.md");
207 assert_eq!(output, "file.md:1:1: [unknown] Unknown rule warning");
208 }
209
210 #[test]
211 fn test_format_warnings_with_colors() {
212 let formatter = TextFormatter::new(); let warnings = vec![LintWarning {
216 line: 1,
217 column: 1,
218 end_line: 1,
219 end_column: 5,
220 rule_name: Some("MD001".to_string()),
221 message: "Test warning".to_string(),
222 severity: Severity::Warning,
223 fix: Some(Fix::new(0..5, "fixed".to_string())),
224 }];
225
226 let output = formatter.format_warnings(&warnings, "test.md");
227
228 assert!(formatter.use_colors());
230
231 assert!(output.contains("test.md")); assert!(output.contains("MD001")); assert!(output.contains("Test warning")); assert!(output.contains("[*]")); }
241
242 #[test]
243 fn test_rule_name_padding() {
244 let formatter = TextFormatter::without_colors();
245
246 let warnings = vec![LintWarning {
248 line: 1,
249 column: 1,
250 end_line: 1,
251 end_column: 5,
252 rule_name: Some("MD1".to_string()),
253 message: "Test".to_string(),
254 severity: Severity::Warning,
255 fix: None,
256 }];
257
258 let output = formatter.format_warnings(&warnings, "test.md");
259 assert!(output.contains("[MD1 ]")); }
261
262 #[test]
263 fn test_edge_cases() {
264 let formatter = TextFormatter::without_colors();
265
266 let warnings = vec![LintWarning {
268 line: 99999,
269 column: 12345,
270 end_line: 100000,
271 end_column: 12350,
272 rule_name: Some("MD999".to_string()),
273 message: "Edge case warning".to_string(),
274 severity: Severity::Error,
275 fix: None,
276 }];
277
278 let output = formatter.format_warnings(&warnings, "large.md");
279 assert_eq!(output, "large.md:99999:12345: [MD999] Edge case warning");
280 }
281
282 #[test]
283 fn test_special_characters_in_message() {
284 let formatter = TextFormatter::without_colors();
285 let warnings = vec![LintWarning {
286 line: 1,
287 column: 1,
288 end_line: 1,
289 end_column: 5,
290 rule_name: Some("MD001".to_string()),
291 message: "Warning with \"quotes\" and 'apostrophes' and \n newline".to_string(),
292 severity: Severity::Warning,
293 fix: None,
294 }];
295
296 let output = formatter.format_warnings(&warnings, "test.md");
297 assert!(output.contains("Warning with \"quotes\" and 'apostrophes' and \n newline"));
298 }
299
300 #[test]
301 fn test_special_characters_in_file_path() {
302 let formatter = TextFormatter::without_colors();
303 let warnings = vec![LintWarning {
304 line: 1,
305 column: 1,
306 end_line: 1,
307 end_column: 5,
308 rule_name: Some("MD001".to_string()),
309 message: "Test".to_string(),
310 severity: Severity::Warning,
311 fix: None,
312 }];
313
314 let output = formatter.format_warnings(&warnings, "path/with spaces/and-dashes.md");
315 assert!(output.starts_with("path/with spaces/and-dashes.md:1:1:"));
316 }
317
318 #[test]
319 fn test_use_colors_trait_method() {
320 let formatter_with_colors = TextFormatter::new();
321 assert!(formatter_with_colors.use_colors());
322
323 let formatter_without_colors = TextFormatter::without_colors();
324 assert!(!formatter_without_colors.use_colors());
325 }
326}