context_creator/formatters/
markdown.rs

1//! Markdown formatter for context generation
2
3use super::{DigestData, DigestFormatter};
4use crate::core::context_builder::{
5    format_import_names, format_imported_by_names, format_path_with_metadata, generate_file_tree,
6    generate_statistics, get_language_hint, path_to_anchor,
7};
8use crate::core::walker::FileInfo;
9use crate::utils::git::{format_git_context_to_markdown, get_file_git_context_with_depth};
10use anyhow::Result;
11use std::path::Path;
12
13/// Formatter that outputs standard Markdown format
14pub struct MarkdownFormatter {
15    buffer: String,
16}
17
18impl MarkdownFormatter {
19    /// Create a new MarkdownFormatter
20    pub fn new() -> Self {
21        Self {
22            buffer: String::with_capacity(1024 * 1024), // 1MB initial capacity
23        }
24    }
25}
26
27impl Default for MarkdownFormatter {
28    fn default() -> Self {
29        Self::new()
30    }
31}
32
33impl DigestFormatter for MarkdownFormatter {
34    fn render_header(&mut self, data: &DigestData) -> Result<()> {
35        if !data.options.doc_header_template.is_empty() {
36            let header = data
37                .options
38                .doc_header_template
39                .replace("{directory}", data.base_directory);
40            self.buffer.push_str(&header);
41            self.buffer.push_str("\n\n");
42        }
43        Ok(())
44    }
45
46    fn render_statistics(&mut self, data: &DigestData) -> Result<()> {
47        if data.options.include_stats {
48            let stats = generate_statistics(data.files);
49            self.buffer.push_str(&stats);
50            self.buffer.push_str("\n\n");
51        }
52        Ok(())
53    }
54
55    fn render_file_tree(&mut self, data: &DigestData) -> Result<()> {
56        if data.options.include_tree {
57            let tree = generate_file_tree(data.files, data.options);
58            self.buffer.push_str("## File Structure\n\n");
59            self.buffer.push_str("```\n");
60            self.buffer.push_str(&tree);
61            self.buffer.push_str("```\n\n");
62        }
63        Ok(())
64    }
65
66    fn render_toc(&mut self, data: &DigestData) -> Result<()> {
67        if data.options.include_toc {
68            self.buffer.push_str("## Table of Contents\n\n");
69            for file in data.files {
70                let anchor = path_to_anchor(&file.relative_path);
71                self.buffer.push_str(&format!(
72                    "- [{path}](#{anchor})\n",
73                    path = file.relative_path.display(),
74                    anchor = anchor
75                ));
76            }
77            self.buffer.push('\n');
78        }
79        Ok(())
80    }
81
82    fn render_file_details(&mut self, file: &FileInfo, data: &DigestData) -> Result<()> {
83        // Add file header
84        let path_with_metadata = format_path_with_metadata(file, data.options);
85        let header = data
86            .options
87            .file_header_template
88            .replace("{path}", &path_with_metadata);
89        self.buffer.push_str(&header);
90        self.buffer.push('\n');
91
92        // Add git context if enabled
93        if data.options.git_context {
94            // Find the repository root from the file path
95            let repo_root = file.path.parent().unwrap_or(Path::new("."));
96            if let Some(git_context) = get_file_git_context_with_depth(
97                repo_root,
98                &file.path,
99                data.options.git_context_depth,
100            ) {
101                self.buffer
102                    .push_str(&format_git_context_to_markdown(&git_context));
103            }
104        }
105
106        self.buffer.push('\n');
107
108        // Add semantic information
109        add_markdown_semantic_info(&mut self.buffer, file);
110
111        // Add file content
112        if let Ok(content) = data.cache.get_or_load(&file.path) {
113            let language = get_language_hint(&file.file_type);
114            self.buffer.push_str(&format!("```{language}\n"));
115            self.buffer.push_str(&content);
116            if !content.ends_with('\n') {
117                self.buffer.push('\n');
118            }
119            self.buffer.push_str("```\n\n");
120        }
121
122        Ok(())
123    }
124
125    fn finalize(self: Box<Self>) -> String {
126        self.buffer
127    }
128
129    fn format_name(&self) -> &'static str {
130        "markdown"
131    }
132}
133
134fn add_markdown_semantic_info(output: &mut String, file: &FileInfo) {
135    if !file.imports.is_empty() {
136        output.push_str("Imports: ");
137        let names = format_import_names(&file.imports);
138        output.push_str(&format!("{}\n\n", names.join(", ")));
139    }
140
141    if !file.imported_by.is_empty() {
142        output.push_str("Imported by: ");
143        let names = format_imported_by_names(&file.imported_by);
144        output.push_str(&format!("{}\n\n", names.join(", ")));
145    }
146
147    if !file.function_calls.is_empty() {
148        output.push_str("Function calls: ");
149        let names = format_function_call_names(&file.function_calls);
150        output.push_str(&format!("{}\n\n", names.join(", ")));
151    }
152
153    if !file.type_references.is_empty() {
154        output.push_str("Type references: ");
155        let names = format_type_reference_names(&file.type_references);
156        output.push_str(&format!("{}\n\n", names.join(", ")));
157    }
158}
159
160fn format_function_call_names(
161    calls: &[crate::core::semantic::analyzer::FunctionCall],
162) -> Vec<String> {
163    calls
164        .iter()
165        .map(|fc| {
166            if let Some(module) = &fc.module {
167                format!("{}.{}", module, fc.name)
168            } else {
169                fc.name.clone()
170            }
171        })
172        .collect()
173}
174
175fn format_type_reference_names(
176    refs: &[crate::core::semantic::analyzer::TypeReference],
177) -> Vec<String> {
178    refs.iter()
179        .map(|tr| {
180            if let Some(module) = &tr.module {
181                if module.ends_with(&format!("::{}", tr.name)) {
182                    module.clone()
183                } else {
184                    format!("{}.{}", module, tr.name)
185                }
186            } else {
187                tr.name.clone()
188            }
189        })
190        .collect()
191}