codelens-core 0.0.3

Core library for codelens - high performance code statistics tool
Documentation
//! Markdown output format.

use std::io::Write;

use crate::analyzer::stats::AnalysisResult;
use crate::error::Result;

use super::format::{OutputFormat, OutputOptions};

/// Markdown output formatter.
pub struct MarkdownOutput;

impl MarkdownOutput {
    /// Create a new Markdown output formatter.
    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)?;

        // Summary
        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)?;

        // Language breakdown
        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();

        // Only Rust should appear (it has more code lines)
        assert!(md_str.contains("| Rust |"));
        // Count occurrences of language rows (excluding header)
        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"));
    }
}