1use chrono::Utc;
2use ignore::DirEntry;
3use log::{error, info, warn};
4use std::fs;
5use std::io::{self, Write};
6use std::path::Path;
7
8use crate::tree::{write_tree_to_file, FileTree};
9
10#[allow(clippy::too_many_arguments)]
12pub fn generate_markdown(
13 output_path: &str,
14 input_dir: &str,
15 filters: &[String],
16 ignores: &[String],
17 file_tree: &FileTree,
18 files: &[DirEntry],
19 base_path: &Path,
20 line_numbers: bool,
21) -> io::Result<()> {
22 let mut output = fs::File::create(output_path)?;
23
24 writeln!(output, "# Directory Structure Report\n")?;
26
27 if !filters.is_empty() {
28 writeln!(
29 output,
30 "This document contains files from the `{}` directory with extensions: {}",
31 input_dir,
32 filters.join(", ")
33 )?;
34 } else {
35 writeln!(
36 output,
37 "This document contains all files from the `{}` directory, optimized for LLM consumption.",
38 input_dir
39 )?;
40 }
41
42 if !ignores.is_empty() {
43 writeln!(output, "Custom ignored patterns: {}", ignores.join(", "))?;
44 }
45
46 writeln!(
47 output,
48 "Processed at: {}",
49 Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
50 )?;
51 writeln!(output)?;
52
53 writeln!(output, "## File Tree Structure\n")?;
55 write_tree_to_file(&mut output, file_tree, 0)?;
56
57 for entry in files {
59 process_file(base_path, entry.path(), &mut output, line_numbers)?;
60 }
61
62 Ok(())
63}
64
65fn process_file(
67 base_path: &Path,
68 file_path: &Path,
69 output: &mut fs::File,
70 line_numbers: bool,
71) -> io::Result<()> {
72 let relative_path = file_path.strip_prefix(base_path).unwrap_or(file_path);
73 info!("Processing file: {}", relative_path.display());
74
75 let metadata = match fs::metadata(file_path) {
76 Ok(meta) => meta,
77 Err(e) => {
78 error!(
79 "Failed to get metadata for {}: {}",
80 relative_path.display(),
81 e
82 );
83 return Ok(());
84 }
85 };
86
87 let modified_time = metadata
88 .modified()
89 .ok()
90 .map(|time| {
91 let system_time: chrono::DateTime<Utc> = time.into();
92 system_time.format("%Y-%m-%d %H:%M:%S UTC").to_string()
93 })
94 .unwrap_or_else(|| "Unknown".to_string());
95
96 writeln!(output)?;
98 writeln!(output, "## File: `{}`", relative_path.display())?;
99 writeln!(output)?;
100 writeln!(output, "- Size: {} bytes", metadata.len())?;
101 writeln!(output, "- Modified: {}", modified_time)?;
102 writeln!(output)?;
103
104 let extension = file_path
106 .extension()
107 .and_then(|s| s.to_str())
108 .unwrap_or("text");
109 let language = match extension {
110 "rs" => "rust",
111 "js" => "javascript",
112 "ts" => "typescript",
113 "jsx" => "jsx",
114 "tsx" => "tsx",
115 "json" => "json",
116 "toml" => "toml",
117 "md" => "markdown",
118 "yaml" | "yml" => "yaml",
119 "html" => "html",
120 "css" => "css",
121 "py" => "python",
122 "java" => "java",
123 "cpp" => "cpp",
124 "c" => "c",
125 "h" => "c",
126 "hpp" => "cpp",
127 "sql" => "sql",
128 "sh" => "bash",
129 "xml" => "xml",
130 "lock" => "toml",
131 _ => extension,
132 };
133
134 match fs::read_to_string(file_path) {
135 Ok(content) => {
136 writeln!(output, "```{}", language)?;
137 if line_numbers {
138 for (i, line) in content.lines().enumerate() {
139 writeln!(output, "{:>4} | {}", i + 1, line)?;
140 }
141 } else {
142 writeln!(output, "{}", content)?;
143 }
144 writeln!(output, "```")?;
145 }
146 Err(e) => {
147 warn!(
148 "Could not read file {}: {}. Skipping content.",
149 relative_path.display(),
150 e
151 );
152 writeln!(output, "```text")?;
153 writeln!(
154 output,
155 "<Could not read file content (e.g., binary file or permission error)>"
156 )?;
157 writeln!(output, "```")?;
158 }
159 }
160 Ok(())
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use std::fs;
167 use tempfile::tempdir;
168
169 #[test]
170 fn test_code_block_formatting() {
171 let dir = tempdir().unwrap();
172 let base_path = dir.path();
173 let file_path = base_path.join("test.rs");
174 let output_path = base_path.join("output.md");
175
176 fs::write(
178 &file_path,
179 "fn main() {\n println!(\"Hello, world!\");\n}",
180 )
181 .unwrap();
182
183 let mut output = fs::File::create(&output_path).unwrap();
185
186 process_file(base_path, &file_path, &mut output, false).unwrap();
188
189 let content = fs::read_to_string(&output_path).unwrap();
191
192 assert!(content.contains("```rust"));
194 assert!(content.contains("```") && content.matches("```").count() >= 2);
195 }
196
197 #[test]
198 fn test_markdown_file_formatting() {
199 let dir = tempdir().unwrap();
200 let base_path = dir.path();
201 let file_path = base_path.join("README.md");
202 let output_path = base_path.join("output.md");
203
204 fs::write(&file_path, "# Test\n\nThis is a test markdown file.").unwrap();
206
207 let mut output = fs::File::create(&output_path).unwrap();
209
210 process_file(base_path, &file_path, &mut output, false).unwrap();
212
213 let content = fs::read_to_string(&output_path).unwrap();
215
216 println!("Generated content:\n{}", content);
218
219 assert!(
221 content.contains("```markdown"),
222 "Content should contain '```markdown' but was: {}",
223 content
224 );
225 let code_block_markers = content.matches("```").count();
227 assert!(
228 code_block_markers >= 2,
229 "Expected at least 2 code block markers, found {}",
230 code_block_markers
231 );
232 }
233}