context_creator/formatters/
markdown.rs1use 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
13pub struct MarkdownFormatter {
15 buffer: String,
16}
17
18impl MarkdownFormatter {
19 pub fn new() -> Self {
21 Self {
22 buffer: String::with_capacity(1024 * 1024), }
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 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 if data.options.git_context {
94 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_markdown_semantic_info(&mut self.buffer, file);
110
111 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}