//! Function Markdown Formatter - Visual function documentation as you work!
//!
//! This formatter creates a beautiful markdown visualization of functions
//! in your codebase, perfect for real-time documentation while coding.
//!
//! Trisha says: "It's like having a living blueprint of your code!" 📐
use crate::formatters::Formatter;
use crate::scanner::{FileNode, TreeStats};
use anyhow::Result;
use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::path::Path;
/// Function information extracted from code
#[derive(Debug, Clone)]
struct FunctionInfo {
name: String,
file: String,
line_start: usize,
line_end: usize,
signature: String,
doc_comment: Option<String>,
calls: Vec<String>,
visibility: String,
complexity: usize,
language: String,
}
/// Function markdown formatter
pub struct FunctionMarkdownFormatter {
no_emoji: bool,
show_private: bool,
show_complexity: bool,
show_call_graph: bool,
group_by_file: bool,
}
impl FunctionMarkdownFormatter {
pub fn new(show_private: bool, show_complexity: bool, show_call_graph: bool) -> Self {
Self {
no_emoji: false,
show_private,
show_complexity,
show_call_graph,
group_by_file: true,
}
}
/// Extract functions from code files
fn extract_functions(&self, nodes: &[FileNode]) -> Vec<FunctionInfo> {
let mut functions = Vec::new();
for node in nodes {
if node.is_dir || node.permission_denied {
continue;
}
// Check if it's a code file
let ext = node.path.extension().and_then(|e| e.to_str()).unwrap_or("");
if !is_code_extension(ext) {
continue;
}
// For now, create placeholder functions based on file content
// In a real implementation, we would use tree-sitter or similar
if let Ok(content) = std::fs::read_to_string(&node.path) {
let file_functions = extract_functions_from_content(&content, ext);
for (idx, func_name) in file_functions.into_iter().enumerate() {
functions.push(FunctionInfo {
name: func_name.clone(),
file: node.path.to_string_lossy().to_string(),
line_start: idx * 10 + 1,
line_end: idx * 10 + 10,
signature: format!("{}(...)", func_name),
doc_comment: None,
calls: Vec::new(),
visibility: "public".to_string(),
complexity: 5,
language: ext.to_string(),
});
}
}
}
functions
}
/// Generate the markdown output
fn generate_markdown(&self, functions: &[FunctionInfo], _stats: &TreeStats) -> String {
let mut output = String::new();
// Header
let doc_title = if self.no_emoji {
"# Function Documentation\n\n"
} else {
"# 📚 Function Documentation\n\n"
};
output.push_str(doc_title);
output.push_str(&format!(
"*Generated by Smart Tree - {} functions found*\n\n",
functions.len()
));
// Summary stats
let summary_title = if self.no_emoji {
"## Summary\n\n"
} else {
"## 📊 Summary\n\n"
};
output.push_str(summary_title);
output.push_str(&format!("- **Total Functions**: {}\n", functions.len()));
output.push_str(&format!(
"- **Public Functions**: {}\n",
functions
.iter()
.filter(|f| f.visibility == "public")
.count()
));
output.push_str(&format!(
"- **Private Functions**: {}\n",
functions
.iter()
.filter(|f| f.visibility != "public")
.count()
));
// Language breakdown
let mut lang_counts: HashMap<String, usize> = HashMap::new();
for func in functions {
*lang_counts.entry(func.language.clone()).or_insert(0) += 1;
}
let lang_title = if self.no_emoji {
"\n### Languages\n"
} else {
"\n### 🗣️ Languages\n"
};
output.push_str(lang_title);
for (lang, count) in lang_counts.iter() {
if self.no_emoji {
output.push_str(&format!("- {}: {} functions\n", lang, count));
} else {
let emoji = get_language_emoji(lang);
output.push_str(&format!("- {} {}: {} functions\n", emoji, lang, count));
}
}
// Table of contents
let toc_title = if self.no_emoji {
"\n## Table of Contents\n\n"
} else {
"\n## 📑 Table of Contents\n\n"
};
output.push_str(toc_title);
if self.group_by_file {
let mut files: HashSet<String> = HashSet::new();
for func in functions {
files.insert(func.file.clone());
}
let mut sorted_files: Vec<String> = files.into_iter().collect();
sorted_files.sort();
for file in &sorted_files {
let file_funcs: Vec<&FunctionInfo> =
functions.iter().filter(|f| &f.file == file).collect();
output.push_str(&format!(
"- [{}](#{})\n",
file,
file.replace(['/', '.'], "-").to_lowercase()
));
for func in &file_funcs {
if !self.show_private && func.visibility != "public" {
continue;
}
output.push_str(&format!(
" - [{}()](#{})\n",
func.name,
format!("{}-{}", file, func.name)
.replace('/', "-")
.replace('.', "-")
.to_lowercase()
));
}
}
}
// Function details
let functions_title = if self.no_emoji {
"\n## Functions\n\n"
} else {
"\n## 🔧 Functions\n\n"
};
output.push_str(functions_title);
if self.group_by_file {
let mut files: HashSet<String> = HashSet::new();
for func in functions {
files.insert(func.file.clone());
}
let mut sorted_files: Vec<String> = files.into_iter().collect();
sorted_files.sort();
for file in sorted_files {
let file_funcs: Vec<&FunctionInfo> =
functions.iter().filter(|f| f.file == file).collect();
if file_funcs.is_empty() {
continue;
}
output.push_str(&format!("### 📄 {}\n\n", file));
for func in file_funcs {
if !self.show_private && func.visibility != "public" {
continue;
}
self.format_function(&mut output, func);
}
}
} else {
// Sort functions by name
let mut sorted_funcs = functions.to_vec();
sorted_funcs.sort_by(|a, b| a.name.cmp(&b.name));
for func in &sorted_funcs {
if !self.show_private && func.visibility != "public" {
continue;
}
self.format_function(&mut output, func);
}
}
// Call graph
if self.show_call_graph {
output.push_str("\n## 🕸️ Call Graph\n\n");
output.push_str("```mermaid\ngraph TD\n");
for func in functions {
for call in &func.calls {
output.push_str(&format!(
" {}[{}] --> {}[{}]\n",
func.name, func.name, call, call
));
}
}
output.push_str("```\n");
}
// Footer
output.push_str("\n---\n");
output.push_str("*Generated by Smart Tree Function Markdown Formatter*\n");
output.push_str("*\"It's like having a living blueprint of your code!\" - Trisha* 📐\n");
output
}
/// Format a single function
fn format_function(&self, output: &mut String, func: &FunctionInfo) {
let visibility_emoji = if func.visibility == "public" {
"🔓"
} else {
"🔒"
};
output.push_str(&format!(
"#### {} {} `{}`\n\n",
visibility_emoji, func.name, func.visibility
));
// Location
output.push_str(&format!(
"📍 **Location**: `{}:{}-{}`\n\n",
func.file, func.line_start, func.line_end
));
// Signature
output.push_str("**Signature**:\n```");
output.push_str(&func.language);
output.push('\n');
output.push_str(&func.signature);
output.push_str("\n```\n\n");
// Documentation
if let Some(doc) = &func.doc_comment {
output.push_str("**Documentation**:\n");
output.push_str(doc);
output.push_str("\n\n");
}
// Complexity
if self.show_complexity {
let complexity_emoji = match func.complexity {
0..=5 => "🟢",
6..=10 => "🟡",
11..=20 => "🟠",
_ => "🔴",
};
output.push_str(&format!(
"**Complexity**: {} {}\n\n",
complexity_emoji, func.complexity
));
}
// Calls
if !func.calls.is_empty() {
output.push_str("**Calls**:\n");
for call in &func.calls {
output.push_str(&format!("- `{}`\n", call));
}
output.push('\n');
}
output.push_str("---\n\n");
}
}
impl Formatter for FunctionMarkdownFormatter {
fn format(
&self,
writer: &mut dyn Write,
nodes: &[FileNode],
stats: &TreeStats,
_root_path: &Path,
) -> Result<()> {
// Extract functions from all code files
let functions = self.extract_functions(nodes);
// Generate markdown
let markdown = self.generate_markdown(&functions, stats);
// Write output
writer.write_all(markdown.as_bytes())?;
Ok(())
}
}
// Helper functions
fn is_code_extension(ext: &str) -> bool {
matches!(
ext,
"rs" | "py"
| "js"
| "ts"
| "jsx"
| "tsx"
| "java"
| "cpp"
| "c"
| "h"
| "hpp"
| "go"
| "rb"
| "php"
| "swift"
| "kt"
| "scala"
| "r"
| "jl"
| "cs"
| "vb"
| "lua"
| "pl"
| "sh"
| "bash"
| "zsh"
| "ps1"
| "dart"
| "elm"
| "ex"
| "exs"
| "clj"
| "cljs"
| "ml"
| "mli"
)
}
/// Simple function extraction using regex patterns
fn extract_functions_from_content(content: &str, ext: &str) -> Vec<String> {
let mut functions = Vec::new();
let patterns = match ext {
"rs" => vec![r"fn\s+(\w+)\s*[<(]", r"pub\s+fn\s+(\w+)\s*[<(]"],
"py" => vec![r"def\s+(\w+)\s*\(", r"async\s+def\s+(\w+)\s*\("],
"js" | "ts" | "jsx" | "tsx" => vec![
r"function\s+(\w+)\s*\(",
r"const\s+(\w+)\s*=\s*\(",
r"(\w+)\s*:\s*function\s*\(",
r"(\w+)\s*\(.*\)\s*\{",
],
"java" => vec![
r"(?:public|private|protected)\s+\w+\s+(\w+)\s*\(",
r"\b(\w+)\s*\(.*\)\s*\{",
],
"go" => vec![r"func\s+(\w+)\s*\(", r"func\s+\(.*\)\s+(\w+)\s*\("],
"cpp" | "c" | "hpp" | "h" => vec![r"\b(\w+)\s*\(.*\)\s*\{", r"\b(\w+)\s*\(.*\);$"],
_ => vec![],
};
for pattern in patterns {
if let Ok(re) = regex::Regex::new(pattern) {
for cap in re.captures_iter(content) {
if let Some(name) = cap.get(1) {
let func_name = name.as_str().to_string();
if !functions.contains(&func_name) && is_valid_function_name(&func_name) {
functions.push(func_name);
}
}
}
}
}
functions
}
/// Check if a name is likely a function name
fn is_valid_function_name(name: &str) -> bool {
// Filter out common false positives
!matches!(
name,
"if" | "for" | "while" | "switch" | "catch" | "return" | "import" | "export"
) && name.len() > 1
&& !name.chars().all(|c| c.is_uppercase())
}
fn get_language_emoji(lang: &str) -> &'static str {
match lang {
"rs" => "🦀",
"py" => "🐍",
"js" | "ts" | "jsx" | "tsx" => "📜",
"java" => "☕",
"go" => "🐹",
"rb" => "💎",
"php" => "🐘",
"swift" => "🦉",
"cpp" | "c" | "h" | "hpp" => "⚙️",
_ => "📄",
}
}