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