Skip to main content

st/formatters/
function_markdown.rs

1//! Function Markdown Formatter - Visual function documentation as you work!
2//!
3//! This formatter creates a beautiful markdown visualization of functions
4//! in your codebase, perfect for real-time documentation while coding.
5//!
6//! Trisha says: "It's like having a living blueprint of your code!" πŸ“
7
8use crate::formatters::Formatter;
9use crate::scanner::{FileNode, TreeStats};
10use anyhow::Result;
11use std::collections::{HashMap, HashSet};
12use std::io::Write;
13use std::path::Path;
14
15/// Function information extracted from code
16#[derive(Debug, Clone)]
17struct FunctionInfo {
18    name: String,
19    file: String,
20    line_start: usize,
21    line_end: usize,
22    signature: String,
23    doc_comment: Option<String>,
24    calls: Vec<String>,
25    visibility: String,
26    complexity: usize,
27    language: String,
28}
29
30/// Function markdown formatter
31pub struct FunctionMarkdownFormatter {
32    no_emoji: bool,
33    show_private: bool,
34    show_complexity: bool,
35    show_call_graph: bool,
36    group_by_file: bool,
37}
38
39impl FunctionMarkdownFormatter {
40    pub fn new(show_private: bool, show_complexity: bool, show_call_graph: bool) -> Self {
41        Self {
42            no_emoji: false,
43            show_private,
44            show_complexity,
45            show_call_graph,
46            group_by_file: true,
47        }
48    }
49
50    /// Extract functions from code files
51    fn extract_functions(&self, nodes: &[FileNode]) -> Vec<FunctionInfo> {
52        let mut functions = Vec::new();
53
54        for node in nodes {
55            if node.is_dir || node.permission_denied {
56                continue;
57            }
58
59            // Check if it's a code file
60            let ext = node.path.extension().and_then(|e| e.to_str()).unwrap_or("");
61
62            if !is_code_extension(ext) {
63                continue;
64            }
65
66            // For now, create placeholder functions based on file content
67            // In a real implementation, we would use tree-sitter or similar
68            if let Ok(content) = std::fs::read_to_string(&node.path) {
69                let file_functions = extract_functions_from_content(&content, ext);
70                for (idx, func_name) in file_functions.into_iter().enumerate() {
71                    functions.push(FunctionInfo {
72                        name: func_name.clone(),
73                        file: node.path.to_string_lossy().to_string(),
74                        line_start: idx * 10 + 1,
75                        line_end: idx * 10 + 10,
76                        signature: format!("{}(...)", func_name),
77                        doc_comment: None,
78                        calls: Vec::new(),
79                        visibility: "public".to_string(),
80                        complexity: 5,
81                        language: ext.to_string(),
82                    });
83                }
84            }
85        }
86
87        functions
88    }
89
90    /// Generate the markdown output
91    fn generate_markdown(&self, functions: &[FunctionInfo], _stats: &TreeStats) -> String {
92        let mut output = String::new();
93
94        // Header
95        let doc_title = if self.no_emoji {
96            "# Function Documentation\n\n"
97        } else {
98            "# πŸ“š Function Documentation\n\n"
99        };
100        output.push_str(doc_title);
101        output.push_str(&format!(
102            "*Generated by Smart Tree - {} functions found*\n\n",
103            functions.len()
104        ));
105
106        // Summary stats
107        let summary_title = if self.no_emoji {
108            "## Summary\n\n"
109        } else {
110            "## πŸ“Š Summary\n\n"
111        };
112        output.push_str(summary_title);
113        output.push_str(&format!("- **Total Functions**: {}\n", functions.len()));
114        output.push_str(&format!(
115            "- **Public Functions**: {}\n",
116            functions
117                .iter()
118                .filter(|f| f.visibility == "public")
119                .count()
120        ));
121        output.push_str(&format!(
122            "- **Private Functions**: {}\n",
123            functions
124                .iter()
125                .filter(|f| f.visibility != "public")
126                .count()
127        ));
128
129        // Language breakdown
130        let mut lang_counts: HashMap<String, usize> = HashMap::new();
131        for func in functions {
132            *lang_counts.entry(func.language.clone()).or_insert(0) += 1;
133        }
134
135        let lang_title = if self.no_emoji {
136            "\n### Languages\n"
137        } else {
138            "\n### πŸ—£οΈ Languages\n"
139        };
140        output.push_str(lang_title);
141        for (lang, count) in lang_counts.iter() {
142            if self.no_emoji {
143                output.push_str(&format!("- {}: {} functions\n", lang, count));
144            } else {
145                let emoji = get_language_emoji(lang);
146                output.push_str(&format!("- {} {}: {} functions\n", emoji, lang, count));
147            }
148        }
149
150        // Table of contents
151        let toc_title = if self.no_emoji {
152            "\n## Table of Contents\n\n"
153        } else {
154            "\n## πŸ“‘ Table of Contents\n\n"
155        };
156        output.push_str(toc_title);
157        if self.group_by_file {
158            let mut files: HashSet<String> = HashSet::new();
159            for func in functions {
160                files.insert(func.file.clone());
161            }
162
163            let mut sorted_files: Vec<String> = files.into_iter().collect();
164            sorted_files.sort();
165
166            for file in &sorted_files {
167                let file_funcs: Vec<&FunctionInfo> =
168                    functions.iter().filter(|f| &f.file == file).collect();
169
170                output.push_str(&format!(
171                    "- [{}](#{})\n",
172                    file,
173                    file.replace(['/', '.'], "-").to_lowercase()
174                ));
175
176                for func in &file_funcs {
177                    if !self.show_private && func.visibility != "public" {
178                        continue;
179                    }
180                    output.push_str(&format!(
181                        "  - [{}()](#{})\n",
182                        func.name,
183                        format!("{}-{}", file, func.name)
184                            .replace('/', "-")
185                            .replace('.', "-")
186                            .to_lowercase()
187                    ));
188                }
189            }
190        }
191
192        // Function details
193        let functions_title = if self.no_emoji {
194            "\n## Functions\n\n"
195        } else {
196            "\n## πŸ”§ Functions\n\n"
197        };
198        output.push_str(functions_title);
199
200        if self.group_by_file {
201            let mut files: HashSet<String> = HashSet::new();
202            for func in functions {
203                files.insert(func.file.clone());
204            }
205
206            let mut sorted_files: Vec<String> = files.into_iter().collect();
207            sorted_files.sort();
208
209            for file in sorted_files {
210                let file_funcs: Vec<&FunctionInfo> =
211                    functions.iter().filter(|f| f.file == file).collect();
212
213                if file_funcs.is_empty() {
214                    continue;
215                }
216
217                output.push_str(&format!("### πŸ“„ {}\n\n", file));
218
219                for func in file_funcs {
220                    if !self.show_private && func.visibility != "public" {
221                        continue;
222                    }
223
224                    self.format_function(&mut output, func);
225                }
226            }
227        } else {
228            // Sort functions by name
229            let mut sorted_funcs = functions.to_vec();
230            sorted_funcs.sort_by(|a, b| a.name.cmp(&b.name));
231
232            for func in &sorted_funcs {
233                if !self.show_private && func.visibility != "public" {
234                    continue;
235                }
236
237                self.format_function(&mut output, func);
238            }
239        }
240
241        // Call graph
242        if self.show_call_graph {
243            output.push_str("\n## πŸ•ΈοΈ Call Graph\n\n");
244            output.push_str("```mermaid\ngraph TD\n");
245
246            for func in functions {
247                for call in &func.calls {
248                    output.push_str(&format!(
249                        "    {}[{}] --> {}[{}]\n",
250                        func.name, func.name, call, call
251                    ));
252                }
253            }
254
255            output.push_str("```\n");
256        }
257
258        // Footer
259        output.push_str("\n---\n");
260        output.push_str("*Generated by Smart Tree Function Markdown Formatter*\n");
261        output.push_str("*\"It's like having a living blueprint of your code!\" - Trisha* πŸ“\n");
262
263        output
264    }
265
266    /// Format a single function
267    fn format_function(&self, output: &mut String, func: &FunctionInfo) {
268        let visibility_emoji = if func.visibility == "public" {
269            "πŸ”“"
270        } else {
271            "πŸ”’"
272        };
273
274        output.push_str(&format!(
275            "#### {} {} `{}`\n\n",
276            visibility_emoji, func.name, func.visibility
277        ));
278
279        // Location
280        output.push_str(&format!(
281            "πŸ“ **Location**: `{}:{}-{}`\n\n",
282            func.file, func.line_start, func.line_end
283        ));
284
285        // Signature
286        output.push_str("**Signature**:\n```");
287        output.push_str(&func.language);
288        output.push('\n');
289        output.push_str(&func.signature);
290        output.push_str("\n```\n\n");
291
292        // Documentation
293        if let Some(doc) = &func.doc_comment {
294            output.push_str("**Documentation**:\n");
295            output.push_str(doc);
296            output.push_str("\n\n");
297        }
298
299        // Complexity
300        if self.show_complexity {
301            let complexity_emoji = match func.complexity {
302                0..=5 => "🟒",
303                6..=10 => "🟑",
304                11..=20 => "🟠",
305                _ => "πŸ”΄",
306            };
307            output.push_str(&format!(
308                "**Complexity**: {} {}\n\n",
309                complexity_emoji, func.complexity
310            ));
311        }
312
313        // Calls
314        if !func.calls.is_empty() {
315            output.push_str("**Calls**:\n");
316            for call in &func.calls {
317                output.push_str(&format!("- `{}`\n", call));
318            }
319            output.push('\n');
320        }
321
322        output.push_str("---\n\n");
323    }
324}
325
326impl Formatter for FunctionMarkdownFormatter {
327    fn format(
328        &self,
329        writer: &mut dyn Write,
330        nodes: &[FileNode],
331        stats: &TreeStats,
332        _root_path: &Path,
333    ) -> Result<()> {
334        // Extract functions from all code files
335        let functions = self.extract_functions(nodes);
336
337        // Generate markdown
338        let markdown = self.generate_markdown(&functions, stats);
339
340        // Write output
341        writer.write_all(markdown.as_bytes())?;
342        Ok(())
343    }
344}
345
346// Helper functions
347
348fn is_code_extension(ext: &str) -> bool {
349    matches!(
350        ext,
351        "rs" | "py"
352            | "js"
353            | "ts"
354            | "jsx"
355            | "tsx"
356            | "java"
357            | "cpp"
358            | "c"
359            | "h"
360            | "hpp"
361            | "go"
362            | "rb"
363            | "php"
364            | "swift"
365            | "kt"
366            | "scala"
367            | "r"
368            | "jl"
369            | "cs"
370            | "vb"
371            | "lua"
372            | "pl"
373            | "sh"
374            | "bash"
375            | "zsh"
376            | "ps1"
377            | "dart"
378            | "elm"
379            | "ex"
380            | "exs"
381            | "clj"
382            | "cljs"
383            | "ml"
384            | "mli"
385    )
386}
387
388/// Simple function extraction using regex patterns
389fn extract_functions_from_content(content: &str, ext: &str) -> Vec<String> {
390    let mut functions = Vec::new();
391
392    let patterns = match ext {
393        "rs" => vec![r"fn\s+(\w+)\s*[<(]", r"pub\s+fn\s+(\w+)\s*[<(]"],
394        "py" => vec![r"def\s+(\w+)\s*\(", r"async\s+def\s+(\w+)\s*\("],
395        "js" | "ts" | "jsx" | "tsx" => vec![
396            r"function\s+(\w+)\s*\(",
397            r"const\s+(\w+)\s*=\s*\(",
398            r"(\w+)\s*:\s*function\s*\(",
399            r"(\w+)\s*\(.*\)\s*\{",
400        ],
401        "java" => vec![
402            r"(?:public|private|protected)\s+\w+\s+(\w+)\s*\(",
403            r"\b(\w+)\s*\(.*\)\s*\{",
404        ],
405        "go" => vec![r"func\s+(\w+)\s*\(", r"func\s+\(.*\)\s+(\w+)\s*\("],
406        "cpp" | "c" | "hpp" | "h" => vec![r"\b(\w+)\s*\(.*\)\s*\{", r"\b(\w+)\s*\(.*\);$"],
407        _ => vec![],
408    };
409
410    for pattern in patterns {
411        if let Ok(re) = regex::Regex::new(pattern) {
412            for cap in re.captures_iter(content) {
413                if let Some(name) = cap.get(1) {
414                    let func_name = name.as_str().to_string();
415                    if !functions.contains(&func_name) && is_valid_function_name(&func_name) {
416                        functions.push(func_name);
417                    }
418                }
419            }
420        }
421    }
422
423    functions
424}
425
426/// Check if a name is likely a function name
427fn is_valid_function_name(name: &str) -> bool {
428    // Filter out common false positives
429    !matches!(
430        name,
431        "if" | "for" | "while" | "switch" | "catch" | "return" | "import" | "export"
432    ) && name.len() > 1
433        && !name.chars().all(|c| c.is_uppercase())
434}
435
436fn get_language_emoji(lang: &str) -> &'static str {
437    match lang {
438        "rs" => "πŸ¦€",
439        "py" => "🐍",
440        "js" | "ts" | "jsx" | "tsx" => "πŸ“œ",
441        "java" => "β˜•",
442        "go" => "🐹",
443        "rb" => "πŸ’Ž",
444        "php" => "🐘",
445        "swift" => "πŸ¦‰",
446        "cpp" | "c" | "h" | "hpp" => "βš™οΈ",
447        _ => "πŸ“„",
448    }
449}