context_builder/
markdown.rs

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/// Generates the final markdown file.
11#[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    // --- Header --- //
25    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    // --- File Tree --- //
54    writeln!(output, "## File Tree Structure\n")?;
55    write_tree_to_file(&mut output, file_tree, 0)?;
56
57    // --- File Contents --- //
58    for entry in files {
59        process_file(base_path, entry.path(), &mut output, line_numbers)?;
60    }
61
62    Ok(())
63}
64
65/// Processes a single file and writes its content to the output.
66fn 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    // --- File Header --- //
97    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    // --- File Content --- //
105    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        // Create a test Rust file
177        fs::write(
178            &file_path,
179            "fn main() {\n    println!(\"Hello, world!\");\n}",
180        )
181        .unwrap();
182
183        // Create output file
184        let mut output = fs::File::create(&output_path).unwrap();
185
186        // Process the file
187        process_file(base_path, &file_path, &mut output, false).unwrap();
188
189        // Read the output
190        let content = fs::read_to_string(&output_path).unwrap();
191
192        // Check that code blocks are properly formatted
193        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        // Create a test markdown file
205        fs::write(&file_path, "# Test\n\nThis is a test markdown file.").unwrap();
206
207        // Create output file
208        let mut output = fs::File::create(&output_path).unwrap();
209
210        // Process the file
211        process_file(base_path, &file_path, &mut output, false).unwrap();
212
213        // Read the output
214        let content = fs::read_to_string(&output_path).unwrap();
215
216        // Debug print the content
217        println!("Generated content:\n{}", content);
218
219        // Check that markdown files use the correct language identifier
220        assert!(
221            content.contains("```markdown"),
222            "Content should contain '```markdown' but was: {}",
223            content
224        );
225        // Count the number of code block markers
226        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}