1use 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#[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
30pub 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 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 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 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 fn generate_markdown(&self, functions: &[FunctionInfo], _stats: &TreeStats) -> String {
92 let mut output = String::new();
93
94 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 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 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 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 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 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 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 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 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 output.push_str(&format!(
281 "π **Location**: `{}:{}-{}`\n\n",
282 func.file, func.line_start, func.line_end
283 ));
284
285 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 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 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 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 let functions = self.extract_functions(nodes);
336
337 let markdown = self.generate_markdown(&functions, stats);
339
340 writer.write_all(markdown.as_bytes())?;
342 Ok(())
343 }
344}
345
346fn 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
388fn 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
426fn is_valid_function_name(name: &str) -> bool {
428 !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}