use crate::core::stats::aggregation::AggregatedStats;
use crate::utils::errors::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FormattingOptions {
pub format: OutputFormat,
pub show_percentages: bool,
pub show_ratios: bool,
pub decimal_places: usize,
pub use_emojis: bool,
pub color_output: bool,
pub compact_mode: bool,
pub sort_by: SortBy,
pub sort_descending: bool,
pub max_items: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum OutputFormat {
Text,
Json,
Csv,
Html,
Markdown,
Table,
Summary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum SortBy {
Name,
Files,
Lines,
Code,
Comments,
Docs,
Size,
Complexity,
Time,
Quality,
}
impl Default for FormattingOptions {
fn default() -> Self {
Self {
format: OutputFormat::Text,
show_percentages: true,
show_ratios: false,
decimal_places: 1,
use_emojis: true,
color_output: true,
compact_mode: false,
sort_by: SortBy::Lines,
sort_descending: true,
max_items: None,
}
}
}
pub struct StatFormatter {
size_units: Vec<&'static str>,
}
impl StatFormatter {
pub fn new() -> Self {
Self {
size_units: vec!["B", "KB", "MB", "GB", "TB"],
}
}
pub fn format_stats(
&self,
stats: &AggregatedStats,
options: &FormattingOptions,
) -> Result<String> {
match options.format {
OutputFormat::Text => self.format_text(stats, options),
OutputFormat::Json => self.format_json(stats, options),
OutputFormat::Csv => self.format_csv(stats, options),
OutputFormat::Html => self.format_html(stats, options),
OutputFormat::Markdown => self.format_markdown(stats, options),
OutputFormat::Table => self.format_table(stats, options),
OutputFormat::Summary => self.format_summary(stats, options),
}
}
fn format_text(&self, stats: &AggregatedStats, options: &FormattingOptions) -> Result<String> {
let mut output = String::new();
if options.use_emojis {
output.push_str("📊 CODE STATISTICS\n");
} else {
output.push_str("CODE STATISTICS\n");
}
output.push_str(&"═".repeat(50));
output.push('\n');
output.push_str(&format!(
"Total Files: {}\n",
self.format_number(stats.basic.total_files)
));
output.push_str(&format!(
"Total Lines: {}\n",
self.format_number(stats.basic.total_lines)
));
output.push_str(&format!(
"Code Lines: {}",
self.format_number(stats.basic.code_lines)
));
if options.show_percentages {
let code_pct = (stats.basic.code_lines as f64 / stats.basic.total_lines as f64) * 100.0;
output.push_str(&format!(" ({:.1}%)", code_pct));
}
output.push('\n');
output.push_str(&format!(
"Comment Lines: {}",
self.format_number(stats.basic.comment_lines)
));
if options.show_percentages {
let comment_pct =
(stats.basic.comment_lines as f64 / stats.basic.total_lines as f64) * 100.0;
output.push_str(&format!(" ({:.1}%)", comment_pct));
}
output.push('\n');
output.push_str(&format!(
"Documentation Lines: {}",
self.format_number(stats.basic.doc_lines)
));
if options.show_percentages {
let doc_pct = (stats.basic.doc_lines as f64 / stats.basic.total_lines as f64) * 100.0;
output.push_str(&format!(" ({:.1}%)", doc_pct));
}
output.push('\n');
output.push_str(&format!(
"Blank Lines: {}",
self.format_number(stats.basic.blank_lines)
));
if options.show_percentages {
let blank_pct =
(stats.basic.blank_lines as f64 / stats.basic.total_lines as f64) * 100.0;
output.push_str(&format!(" ({:.1}%)", blank_pct));
}
output.push('\n');
output.push_str(&format!(
"Total Size: {}\n",
self.format_size(stats.basic.total_size)
));
if stats.complexity.function_count > 0 {
output.push('\n');
output.push_str(&format!(
"Functions: {}\n",
self.format_number(stats.complexity.function_count)
));
output.push_str(&format!(
"Avg Complexity: {:.1}\n",
stats.complexity.cyclomatic_complexity
));
output.push_str(&format!(
"Max Nesting: {}\n",
stats.complexity.max_nesting_depth
));
}
if !options.compact_mode {
output.push('\n');
if options.use_emojis {
output.push_str("🏆 QUALITY SCORES\n");
} else {
output.push_str("QUALITY SCORES\n");
}
output.push_str(&"─".repeat(30));
output.push('\n');
output.push_str(&format!(
"Overall Quality: {:.1}/100\n",
stats.ratios.quality_metrics.overall_quality_score
));
output.push_str(&format!(
"Documentation: {:.1}/100\n",
stats.ratios.quality_metrics.documentation_score
));
output.push_str(&format!(
"Maintainability: {:.1}/100\n",
stats.ratios.quality_metrics.maintainability_score
));
output.push_str(&format!(
"Readability: {:.1}/100\n",
stats.ratios.quality_metrics.readability_score
));
}
Ok(output)
}
fn format_json(&self, stats: &AggregatedStats, _options: &FormattingOptions) -> Result<String> {
let json = serde_json::to_string_pretty(stats)?;
Ok(json)
}
fn format_csv(&self, stats: &AggregatedStats, options: &FormattingOptions) -> Result<String> {
let mut output = String::new();
output.push_str(
"Extension,Files,Lines,Code,Comments,Docs,Blank,Size,Functions,Complexity,Time\n",
);
let mut extensions: Vec<_> = stats.basic.stats_by_extension.iter().collect();
self.sort_extensions(&mut extensions, options);
for (ext, ext_stats) in extensions {
let complexity = stats
.complexity
.complexity_by_extension
.get(ext)
.map(|c| c.cyclomatic_complexity)
.unwrap_or(0.0);
output.push_str(&format!(
"{},{},{},{},{},{},{},{},{:.1},{}\n",
ext,
ext_stats.file_count,
ext_stats.total_lines,
ext_stats.code_lines,
ext_stats.comment_lines,
ext_stats.doc_lines,
ext_stats.blank_lines,
ext_stats.total_size,
stats
.complexity
.complexity_by_extension
.get(ext)
.map(|c| c.function_count)
.unwrap_or(0),
complexity
));
}
Ok(output)
}
fn format_html(&self, stats: &AggregatedStats, options: &FormattingOptions) -> Result<String> {
let mut html = String::new();
html.push_str("<!DOCTYPE html>\n");
html.push_str("<html>\n<head>\n");
html.push_str("<title>Code Statistics</title>\n");
html.push_str("<style>\n");
html.push_str(self.get_html_styles());
html.push_str("</style>\n");
html.push_str("</head>\n<body>\n");
html.push_str("<h1>📊 Code Statistics</h1>\n");
html.push_str("<table class='summary-table'>\n");
html.push_str("<tr><th>Metric</th><th>Value</th></tr>\n");
html.push_str(&format!(
"<tr><td>Total Files</td><td>{}</td></tr>\n",
self.format_number(stats.basic.total_files)
));
html.push_str(&format!(
"<tr><td>Total Lines</td><td>{}</td></tr>\n",
self.format_number(stats.basic.total_lines)
));
html.push_str(&format!(
"<tr><td>Code Lines</td><td>{}</td></tr>\n",
self.format_number(stats.basic.code_lines)
));
html.push_str(&format!(
"<tr><td>Functions</td><td>{}</td></tr>\n",
self.format_number(stats.complexity.function_count)
));
html.push_str(&format!(
"<tr><td>Avg Complexity</td><td>{:.1}</td></tr>\n",
stats.complexity.cyclomatic_complexity
));
html.push_str(&format!(
"<tr><td>Total Size</td><td>{}</td></tr>\n",
self.format_size(stats.basic.total_size)
));
html.push_str("</table>\n");
html.push_str("<h2>📋 File Type Breakdown</h2>\n");
html.push_str("<table class='breakdown-table'>\n");
html.push_str("<tr><th>Extension</th><th>Files</th><th>Lines</th><th>Code</th><th>Comments</th><th>Docs</th><th>Size</th></tr>\n");
let mut extensions: Vec<_> = stats.basic.stats_by_extension.iter().collect();
self.sort_extensions(&mut extensions, options);
for (ext, ext_stats) in extensions {
html.push_str(&format!(
"<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>\n",
ext,
self.format_number(ext_stats.file_count),
self.format_number(ext_stats.total_lines),
self.format_number(ext_stats.code_lines),
self.format_number(ext_stats.comment_lines),
self.format_number(ext_stats.doc_lines),
self.format_size(ext_stats.total_size)
));
}
html.push_str("</table>\n");
html.push_str("</body>\n</html>\n");
Ok(html)
}
fn format_markdown(
&self,
stats: &AggregatedStats,
options: &FormattingOptions,
) -> Result<String> {
let mut md = String::new();
md.push_str("# 📊 Code Statistics\n\n");
md.push_str("## Summary\n\n");
md.push_str("| Metric | Value |\n");
md.push_str("|--------|-------|\n");
md.push_str(&format!(
"| Total Files | {} |\n",
self.format_number(stats.basic.total_files)
));
md.push_str(&format!(
"| Total Lines | {} |\n",
self.format_number(stats.basic.total_lines)
));
md.push_str(&format!(
"| Code Lines | {} |\n",
self.format_number(stats.basic.code_lines)
));
md.push_str(&format!(
"| Functions | {} |\n",
self.format_number(stats.complexity.function_count)
));
md.push_str(&format!(
"| Avg Complexity | {:.1} |\n",
stats.complexity.cyclomatic_complexity
));
md.push_str(&format!(
"| Total Size | {} |\n",
self.format_size(stats.basic.total_size)
));
md.push('\n');
md.push_str("## 📋 File Type Breakdown\n\n");
md.push_str("| Extension | Files | Lines | Code | Comments | Docs | Size |\n");
md.push_str("|-----------|-------|-------|------|----------|------|------|\n");
let mut extensions: Vec<_> = stats.basic.stats_by_extension.iter().collect();
self.sort_extensions(&mut extensions, options);
for (ext, ext_stats) in extensions {
md.push_str(&format!(
"| {} | {} | {} | {} | {} | {} | {} |\n",
ext,
self.format_number(ext_stats.file_count),
self.format_number(ext_stats.total_lines),
self.format_number(ext_stats.code_lines),
self.format_number(ext_stats.comment_lines),
self.format_number(ext_stats.doc_lines),
self.format_size(ext_stats.total_size)
));
}
Ok(md)
}
fn format_table(&self, stats: &AggregatedStats, options: &FormattingOptions) -> Result<String> {
let mut output = String::new();
let header = format!(
"{:<12} {:>8} {:>10} {:>10} {:>12} {:>10} {:>12}",
"Extension", "Files", "Lines", "Code", "Comments", "Docs", "Size"
);
output.push_str(&header);
output.push('\n');
output.push_str(&"─".repeat(header.len()));
output.push('\n');
let mut extensions: Vec<_> = stats.basic.stats_by_extension.iter().collect();
self.sort_extensions(&mut extensions, options);
if let Some(max) = options.max_items {
extensions.truncate(max);
}
for (ext, ext_stats) in extensions {
output.push_str(&format!(
"{:<12} {:>8} {:>10} {:>10} {:>12} {:>10} {:>12}\n",
ext,
self.format_number(ext_stats.file_count),
self.format_number(ext_stats.total_lines),
self.format_number(ext_stats.code_lines),
self.format_number(ext_stats.comment_lines),
self.format_number(ext_stats.doc_lines),
self.format_size(ext_stats.total_size)
));
}
Ok(output)
}
fn format_summary(
&self,
stats: &AggregatedStats,
options: &FormattingOptions,
) -> Result<String> {
let mut output = String::new();
if options.use_emojis {
output.push_str("📊 ");
}
output.push_str(&format!(
"{} files, {} lines ({} code), {} functions, {:.1} avg complexity, {}",
self.format_number(stats.basic.total_files),
self.format_number(stats.basic.total_lines),
self.format_number(stats.basic.code_lines),
self.format_number(stats.complexity.function_count),
stats.complexity.cyclomatic_complexity,
self.format_size(stats.basic.total_size)
));
Ok(output)
}
pub fn format_number(&self, num: usize) -> String {
let num_str = num.to_string();
let mut result = String::new();
let chars: Vec<char> = num_str.chars().collect();
for (i, ch) in chars.iter().enumerate() {
if i > 0 && (chars.len() - i).is_multiple_of(3) {
result.push(',');
}
result.push(*ch);
}
result
}
pub fn format_size(&self, size: u64) -> String {
let mut size = size as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < self.size_units.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
if unit_index == 0 {
format!("{} {}", size as u64, self.size_units[unit_index])
} else {
format!("{:.1} {}", size, self.size_units[unit_index])
}
}
pub fn format_percentage(&self, ratio: f64, decimal_places: usize) -> String {
format!("{:.prec$}%", ratio * 100.0, prec = decimal_places)
}
pub fn get_extension_emoji(&self, ext: &str) -> &'static str {
match ext {
"rs" => "🦀",
"py" => "🐍",
"js" | "jsx" => "📜",
"ts" | "tsx" => "📘",
"html" => "🌐",
"css" | "scss" | "sass" => "🎨",
"json" => "📋",
"xml" => "📄",
"yaml" | "yml" => "⚙️",
"toml" => "🔧",
"md" => "📝",
"txt" => "📄",
"java" => "☕",
"c" | "cpp" | "cc" | "cxx" => "⚡",
"h" | "hpp" => "📎",
"go" => "🐹",
"php" => "🐘",
"rb" => "💎",
"swift" => "🍎",
"kt" => "🎯",
"scala" => "🎭",
"sh" | "bash" | "zsh" => "🐚",
_ => "📄",
}
}
fn sort_extensions(
&self,
extensions: &mut Vec<(&String, &crate::core::stats::basic::ExtensionStats)>,
options: &FormattingOptions,
) {
extensions.sort_by(|a, b| {
let comparison = match options.sort_by {
SortBy::Name => a.0.cmp(b.0),
SortBy::Files => a.1.file_count.cmp(&b.1.file_count),
SortBy::Lines => a.1.total_lines.cmp(&b.1.total_lines),
SortBy::Code => a.1.code_lines.cmp(&b.1.code_lines),
SortBy::Comments => a.1.comment_lines.cmp(&b.1.comment_lines),
SortBy::Docs => a.1.doc_lines.cmp(&b.1.doc_lines),
SortBy::Size => a.1.total_size.cmp(&b.1.total_size),
_ => a.1.total_lines.cmp(&b.1.total_lines), };
if options.sort_descending {
comparison.reverse()
} else {
comparison
}
});
}
fn get_html_styles(&self) -> &'static str {
r#"
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 20px; }
h1, h2 { color: #2c3e50; }
table { border-collapse: collapse; width: 100%; margin: 20px 0; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; font-weight: bold; }
.summary-table { max-width: 400px; }
.breakdown-table th:not(:first-child), .breakdown-table td:not(:first-child) { text-align: right; }
tr:nth-child(even) { background-color: #f9f9f9; }
"#
}
pub fn create_preset_options(preset: &str) -> FormattingOptions {
match preset {
"compact" => FormattingOptions {
compact_mode: true,
show_percentages: false,
use_emojis: false,
max_items: Some(10),
..Default::default()
},
"detailed" => FormattingOptions {
show_percentages: true,
show_ratios: true,
decimal_places: 2,
compact_mode: false,
..Default::default()
},
"json" => FormattingOptions {
format: OutputFormat::Json,
..Default::default()
},
"csv" => FormattingOptions {
format: OutputFormat::Csv,
use_emojis: false,
color_output: false,
..Default::default()
},
"html" => FormattingOptions {
format: OutputFormat::Html,
show_percentages: true,
..Default::default()
},
"markdown" => FormattingOptions {
format: OutputFormat::Markdown,
use_emojis: true,
..Default::default()
},
_ => FormattingOptions::default(),
}
}
}
impl Default for StatFormatter {
fn default() -> Self {
Self::new()
}
}