codelens_core/output/
markdown.rs1use std::io::Write;
4
5use crate::analyzer::stats::AnalysisResult;
6use crate::error::Result;
7
8use super::format::{OutputFormat, OutputOptions};
9
10pub struct MarkdownOutput;
12
13impl MarkdownOutput {
14 pub fn new() -> Self {
16 Self
17 }
18}
19
20impl Default for MarkdownOutput {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26impl OutputFormat for MarkdownOutput {
27 fn name(&self) -> &'static str {
28 "markdown"
29 }
30
31 fn extension(&self) -> &'static str {
32 "md"
33 }
34
35 fn write(
36 &self,
37 result: &AnalysisResult,
38 options: &OutputOptions,
39 writer: &mut dyn Write,
40 ) -> Result<()> {
41 let summary = &result.summary;
42
43 writeln!(writer, "# Code Statistics Report")?;
44 writeln!(writer)?;
45
46 writeln!(writer, "## Summary")?;
48 writeln!(writer)?;
49 writeln!(writer, "| Metric | Value |")?;
50 writeln!(writer, "|--------|-------|")?;
51 writeln!(writer, "| Total Files | {} |", summary.total_files)?;
52 writeln!(writer, "| Code Lines | {} |", summary.lines.code)?;
53 writeln!(writer, "| Comment Lines | {} |", summary.lines.comment)?;
54 writeln!(writer, "| Blank Lines | {} |", summary.lines.blank)?;
55 writeln!(writer, "| Total Lines | {} |", summary.lines.total)?;
56 writeln!(writer, "| Languages | {} |", summary.by_language.len())?;
57 writeln!(writer)?;
58
59 if !options.summary_only && !summary.by_language.is_empty() {
61 writeln!(writer, "## By Language")?;
62 writeln!(writer)?;
63 writeln!(
64 writer,
65 "| Language | Files | Code | Comment | Blank | Total |"
66 )?;
67 writeln!(
68 writer,
69 "|----------|-------|------|---------|-------|-------|"
70 )?;
71
72 let mut langs: Vec<_> = summary.by_language.iter().collect();
73 if let Some(n) = options.top_n {
74 langs.truncate(n);
75 }
76
77 for (name, stats) in langs {
78 writeln!(
79 writer,
80 "| {} | {} | {} | {} | {} | {} |",
81 name,
82 stats.files,
83 stats.lines.code,
84 stats.lines.comment,
85 stats.lines.blank,
86 stats.lines.total
87 )?;
88 }
89 writeln!(writer)?;
90 }
91
92 writeln!(writer, "---")?;
93 writeln!(
94 writer,
95 "*Generated by codelens in {:.2}s*",
96 result.elapsed.as_secs_f64()
97 )?;
98
99 Ok(())
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::analyzer::stats::{FileStats, LineStats, Summary};
107 use std::path::PathBuf;
108 use std::time::Duration;
109
110 fn make_test_result() -> AnalysisResult {
111 let files = vec![
112 FileStats {
113 path: PathBuf::from("main.rs"),
114 language: "Rust".to_string(),
115 lines: LineStats {
116 total: 100,
117 code: 80,
118 comment: 10,
119 blank: 10,
120 },
121 size: 2000,
122 complexity: Default::default(),
123 },
124 FileStats {
125 path: PathBuf::from("test.py"),
126 language: "Python".to_string(),
127 lines: LineStats {
128 total: 50,
129 code: 40,
130 comment: 5,
131 blank: 5,
132 },
133 size: 1000,
134 complexity: Default::default(),
135 },
136 ];
137 AnalysisResult {
138 summary: Summary::from_file_stats(&files),
139 files,
140 elapsed: Duration::from_secs(1),
141 scanned_files: 2,
142 skipped_files: 0,
143 }
144 }
145
146 #[test]
147 fn test_markdown_output_name() {
148 let output = MarkdownOutput::new();
149 assert_eq!(output.name(), "markdown");
150 assert_eq!(output.extension(), "md");
151 }
152
153 #[test]
154 fn test_markdown_output_title() {
155 let output = MarkdownOutput;
156 let result = make_test_result();
157 let options = OutputOptions::default();
158
159 let mut buffer = Vec::new();
160 output.write(&result, &options, &mut buffer).unwrap();
161
162 let md_str = String::from_utf8(buffer).unwrap();
163 assert!(md_str.starts_with("# Code Statistics Report"));
164 }
165
166 #[test]
167 fn test_markdown_output_summary_table() {
168 let output = MarkdownOutput;
169 let result = make_test_result();
170 let options = OutputOptions::default();
171
172 let mut buffer = Vec::new();
173 output.write(&result, &options, &mut buffer).unwrap();
174
175 let md_str = String::from_utf8(buffer).unwrap();
176
177 assert!(md_str.contains("## Summary"));
178 assert!(md_str.contains("| Total Files | 2 |"));
179 assert!(md_str.contains("| Code Lines | 120 |"));
180 }
181
182 #[test]
183 fn test_markdown_output_language_breakdown() {
184 let output = MarkdownOutput;
185 let result = make_test_result();
186 let options = OutputOptions {
187 summary_only: false,
188 ..Default::default()
189 };
190
191 let mut buffer = Vec::new();
192 output.write(&result, &options, &mut buffer).unwrap();
193
194 let md_str = String::from_utf8(buffer).unwrap();
195
196 assert!(md_str.contains("## By Language"));
197 assert!(md_str.contains("| Rust |"));
198 assert!(md_str.contains("| Python |"));
199 }
200
201 #[test]
202 fn test_markdown_output_summary_only() {
203 let output = MarkdownOutput;
204 let result = make_test_result();
205 let options = OutputOptions {
206 summary_only: true,
207 ..Default::default()
208 };
209
210 let mut buffer = Vec::new();
211 output.write(&result, &options, &mut buffer).unwrap();
212
213 let md_str = String::from_utf8(buffer).unwrap();
214
215 assert!(md_str.contains("## Summary"));
216 assert!(!md_str.contains("## By Language"));
217 }
218
219 #[test]
220 fn test_markdown_output_top_n() {
221 let output = MarkdownOutput;
222 let result = make_test_result();
223 let options = OutputOptions {
224 top_n: Some(1),
225 ..Default::default()
226 };
227
228 let mut buffer = Vec::new();
229 output.write(&result, &options, &mut buffer).unwrap();
230
231 let md_str = String::from_utf8(buffer).unwrap();
232
233 assert!(md_str.contains("| Rust |"));
235 let rust_count = md_str.matches("| Rust |").count();
237 let python_count = md_str.matches("| Python |").count();
238 assert_eq!(rust_count, 1);
239 assert_eq!(python_count, 0);
240 }
241
242 #[test]
243 fn test_markdown_output_footer() {
244 let output = MarkdownOutput;
245 let result = make_test_result();
246 let options = OutputOptions::default();
247
248 let mut buffer = Vec::new();
249 output.write(&result, &options, &mut buffer).unwrap();
250
251 let md_str = String::from_utf8(buffer).unwrap();
252
253 assert!(md_str.contains("---"));
254 assert!(md_str.contains("*Generated by codelens"));
255 }
256}