use std::io::Write;
use crate::analyzer::stats::AnalysisResult;
use crate::error::Result;
use super::format::{OutputFormat, OutputOptions};
pub struct MarkdownOutput;
impl MarkdownOutput {
pub fn new() -> Self {
Self
}
}
impl Default for MarkdownOutput {
fn default() -> Self {
Self::new()
}
}
impl OutputFormat for MarkdownOutput {
fn name(&self) -> &'static str {
"markdown"
}
fn extension(&self) -> &'static str {
"md"
}
fn write(
&self,
result: &AnalysisResult,
options: &OutputOptions,
writer: &mut dyn Write,
) -> Result<()> {
let summary = &result.summary;
writeln!(writer, "# Code Statistics Report")?;
writeln!(writer)?;
writeln!(writer, "## Summary")?;
writeln!(writer)?;
writeln!(writer, "| Metric | Value |")?;
writeln!(writer, "|--------|-------|")?;
writeln!(writer, "| Total Files | {} |", summary.total_files)?;
writeln!(writer, "| Code Lines | {} |", summary.lines.code)?;
writeln!(writer, "| Comment Lines | {} |", summary.lines.comment)?;
writeln!(writer, "| Blank Lines | {} |", summary.lines.blank)?;
writeln!(writer, "| Total Lines | {} |", summary.lines.total)?;
writeln!(writer, "| Languages | {} |", summary.by_language.len())?;
writeln!(writer)?;
if !options.summary_only && !summary.by_language.is_empty() {
writeln!(writer, "## By Language")?;
writeln!(writer)?;
writeln!(
writer,
"| Language | Files | Code | Comment | Blank | Total |"
)?;
writeln!(
writer,
"|----------|-------|------|---------|-------|-------|"
)?;
let mut langs: Vec<_> = summary.by_language.iter().collect();
if let Some(n) = options.top_n {
langs.truncate(n);
}
for (name, stats) in langs {
writeln!(
writer,
"| {} | {} | {} | {} | {} | {} |",
name,
stats.files,
stats.lines.code,
stats.lines.comment,
stats.lines.blank,
stats.lines.total
)?;
}
writeln!(writer)?;
}
writeln!(writer, "---")?;
writeln!(
writer,
"*Generated by codelens in {:.2}s*",
result.elapsed.as_secs_f64()
)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analyzer::stats::{FileStats, LineStats, Summary};
use std::path::PathBuf;
use std::time::Duration;
fn make_test_result() -> AnalysisResult {
let files = vec![
FileStats {
path: PathBuf::from("main.rs"),
language: "Rust".to_string(),
lines: LineStats {
total: 100,
code: 80,
comment: 10,
blank: 10,
},
size: 2000,
complexity: Default::default(),
},
FileStats {
path: PathBuf::from("test.py"),
language: "Python".to_string(),
lines: LineStats {
total: 50,
code: 40,
comment: 5,
blank: 5,
},
size: 1000,
complexity: Default::default(),
},
];
AnalysisResult {
summary: Summary::from_file_stats(&files),
files,
elapsed: Duration::from_secs(1),
scanned_files: 2,
skipped_files: 0,
}
}
#[test]
fn test_markdown_output_name() {
let output = MarkdownOutput::new();
assert_eq!(output.name(), "markdown");
assert_eq!(output.extension(), "md");
}
#[test]
fn test_markdown_output_title() {
let output = MarkdownOutput;
let result = make_test_result();
let options = OutputOptions::default();
let mut buffer = Vec::new();
output.write(&result, &options, &mut buffer).unwrap();
let md_str = String::from_utf8(buffer).unwrap();
assert!(md_str.starts_with("# Code Statistics Report"));
}
#[test]
fn test_markdown_output_summary_table() {
let output = MarkdownOutput;
let result = make_test_result();
let options = OutputOptions::default();
let mut buffer = Vec::new();
output.write(&result, &options, &mut buffer).unwrap();
let md_str = String::from_utf8(buffer).unwrap();
assert!(md_str.contains("## Summary"));
assert!(md_str.contains("| Total Files | 2 |"));
assert!(md_str.contains("| Code Lines | 120 |"));
}
#[test]
fn test_markdown_output_language_breakdown() {
let output = MarkdownOutput;
let result = make_test_result();
let options = OutputOptions {
summary_only: false,
..Default::default()
};
let mut buffer = Vec::new();
output.write(&result, &options, &mut buffer).unwrap();
let md_str = String::from_utf8(buffer).unwrap();
assert!(md_str.contains("## By Language"));
assert!(md_str.contains("| Rust |"));
assert!(md_str.contains("| Python |"));
}
#[test]
fn test_markdown_output_summary_only() {
let output = MarkdownOutput;
let result = make_test_result();
let options = OutputOptions {
summary_only: true,
..Default::default()
};
let mut buffer = Vec::new();
output.write(&result, &options, &mut buffer).unwrap();
let md_str = String::from_utf8(buffer).unwrap();
assert!(md_str.contains("## Summary"));
assert!(!md_str.contains("## By Language"));
}
#[test]
fn test_markdown_output_top_n() {
let output = MarkdownOutput;
let result = make_test_result();
let options = OutputOptions {
top_n: Some(1),
..Default::default()
};
let mut buffer = Vec::new();
output.write(&result, &options, &mut buffer).unwrap();
let md_str = String::from_utf8(buffer).unwrap();
assert!(md_str.contains("| Rust |"));
let rust_count = md_str.matches("| Rust |").count();
let python_count = md_str.matches("| Python |").count();
assert_eq!(rust_count, 1);
assert_eq!(python_count, 0);
}
#[test]
fn test_markdown_output_footer() {
let output = MarkdownOutput;
let result = make_test_result();
let options = OutputOptions::default();
let mut buffer = Vec::new();
output.write(&result, &options, &mut buffer).unwrap();
let md_str = String::from_utf8(buffer).unwrap();
assert!(md_str.contains("---"));
assert!(md_str.contains("*Generated by codelens"));
}
}