cr_prep/
lib.rs

1use anyhow::Result;
2use ignore::WalkBuilder;
3use std::path::Path;
4
5/// Mapping of file extension to language name for syntax highlighting
6const FILE_TYPES: &[(&str, &str)] = &[
7    // Rust
8    ("rs", "rust"),
9    // TypeScript/JavaScript
10    ("ts", "typescript"),
11    ("tsx", "tsx"),
12    ("js", "javascript"),
13    ("jsx", "jsx"),
14    ("mjs", "javascript"),
15    ("cjs", "javascript"),
16    // Python
17    ("py", "python"),
18    // Go
19    ("go", "go"),
20    // Java/JVM
21    ("java", "java"),
22    ("kt", "kotlin"),
23    ("kts", "kotlin"),
24    ("scala", "scala"),
25    // C/C++
26    ("c", "c"),
27    ("h", "c"),
28    ("cpp", "cpp"),
29    ("cc", "cpp"),
30    ("cxx", "cpp"),
31    ("hpp", "cpp"),
32    ("hxx", "cpp"),
33    // C#
34    ("cs", "csharp"),
35    // Ruby
36    ("rb", "ruby"),
37    // PHP
38    ("php", "php"),
39    // Swift
40    ("swift", "swift"),
41    // Shell
42    ("sh", "bash"),
43    ("bash", "bash"),
44    ("zsh", "zsh"),
45    // Web
46    ("html", "html"),
47    ("htm", "html"),
48    ("css", "css"),
49    ("scss", "scss"),
50    ("sass", "sass"),
51    ("less", "less"),
52    ("vue", "vue"),
53    ("svelte", "svelte"),
54    // Data/Config
55    ("json", "json"),
56    ("yaml", "yaml"),
57    ("yml", "yaml"),
58    ("toml", "toml"),
59    ("xml", "xml"),
60    // SQL
61    ("sql", "sql"),
62    // GraphQL
63    ("graphql", "graphql"),
64    ("gql", "graphql"),
65    // Protocol Buffers
66    ("proto", "protobuf"),
67    // Markdown
68    ("md", "markdown"),
69    ("mdx", "mdx"),
70    // Elixir/Erlang
71    ("ex", "elixir"),
72    ("exs", "elixir"),
73    ("erl", "erlang"),
74    // Haskell
75    ("hs", "haskell"),
76    // Lua
77    ("lua", "lua"),
78    // Zig
79    ("zig", "zig"),
80    // Nim
81    ("nim", "nim"),
82    // OCaml
83    ("ml", "ocaml"),
84    ("mli", "ocaml"),
85    // F#
86    ("fs", "fsharp"),
87    ("fsi", "fsharp"),
88    ("fsx", "fsharp"),
89    // Dart
90    ("dart", "dart"),
91    // R
92    ("r", "r"),
93    ("R", "r"),
94    // Julia
95    ("jl", "julia"),
96    // Clojure
97    ("clj", "clojure"),
98    ("cljs", "clojure"),
99    ("cljc", "clojure"),
100];
101
102/// Get the language name for syntax highlighting from a file extension
103pub fn get_language(path: &Path) -> Option<&'static str> {
104    let ext = path.extension()?.to_str()?;
105    FILE_TYPES
106        .iter()
107        .find(|(e, _)| *e == ext)
108        .map(|(_, lang)| *lang)
109}
110
111/// Check if a file is a supported target file
112pub fn is_target_file(path: &Path) -> bool {
113    get_language(path).is_some()
114}
115
116/// Process a single file and return formatted output with syntax highlighting
117pub fn process_file(file_path: &Path, base_path: &Path) -> Result<String> {
118    use anyhow::Context;
119    use std::fs;
120
121    let content = fs::read_to_string(file_path)
122        .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
123
124    let relative_path = file_path
125        .strip_prefix(base_path)
126        .with_context(|| format!("Failed to get relative path for: {}", file_path.display()))?;
127
128    let language = get_language(file_path).unwrap_or("");
129
130    Ok(format!(
131        "## {}\n```{}\n{}\n```\n\n",
132        relative_path.display(),
133        language,
134        content.trim_end()
135    ))
136}
137
138/// Iterator over files respecting .gitignore
139pub fn walk_files(path: &Path) -> impl Iterator<Item = std::path::PathBuf> {
140    WalkBuilder::new(path)
141        .hidden(true) // Respect hidden files settings
142        .git_ignore(true) // Respect .gitignore
143        .git_global(true) // Respect global gitignore
144        .git_exclude(true) // Respect .git/info/exclude
145        .build()
146        .filter_map(|e| e.ok())
147        .filter(|e| e.path().is_file())
148        .filter(|e| is_target_file(e.path()))
149        .map(|e| e.path().to_path_buf())
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use std::fs::File;
156    use std::io::Write;
157    use std::path::PathBuf;
158    use tempfile::TempDir;
159
160    #[test]
161    fn test_is_target_file() {
162        let test_cases = vec![
163            // Original supported types
164            ("test.rs", true),
165            ("test.ts", true),
166            ("test.js", true),
167            ("test.py", true),
168            ("test.go", true),
169            // New supported types
170            ("test.tsx", true),
171            ("test.jsx", true),
172            ("test.java", true),
173            ("test.c", true),
174            ("test.cpp", true),
175            ("test.h", true),
176            ("test.rb", true),
177            ("test.php", true),
178            ("test.swift", true),
179            ("test.kt", true),
180            ("test.cs", true),
181            ("test.sh", true),
182            ("test.yaml", true),
183            ("test.yml", true),
184            ("test.toml", true),
185            ("test.json", true),
186            ("test.md", true),
187            ("test.html", true),
188            ("test.css", true),
189            ("test.sql", true),
190            // Unsupported
191            ("test.txt", false),
192            ("test", false),
193            ("test.RS", false), // Case sensitive
194        ];
195
196        for (file_name, expected) in test_cases {
197            let path = PathBuf::from(file_name);
198            assert_eq!(is_target_file(&path), expected, "Testing {}", file_name);
199        }
200    }
201
202    #[test]
203    fn test_get_language() {
204        let test_cases = vec![
205            ("test.rs", Some("rust")),
206            ("test.ts", Some("typescript")),
207            ("test.tsx", Some("tsx")),
208            ("test.js", Some("javascript")),
209            ("test.py", Some("python")),
210            ("test.go", Some("go")),
211            ("test.java", Some("java")),
212            ("test.cpp", Some("cpp")),
213            ("test.txt", None),
214        ];
215
216        for (file_name, expected) in test_cases {
217            let path = PathBuf::from(file_name);
218            assert_eq!(get_language(&path), expected, "Testing {}", file_name);
219        }
220    }
221
222    #[test]
223    fn test_process_file() -> Result<()> {
224        let temp_dir = TempDir::new()?;
225        let base_path = temp_dir.path();
226        let file_path = base_path.join("test.rs");
227
228        let test_content = "fn main() {\n    println!(\"Hello\");\n}";
229        let mut file = File::create(&file_path)?;
230        file.write_all(test_content.as_bytes())?;
231
232        let result = process_file(&file_path, base_path)?;
233        let expected = format!("## test.rs\n```rust\n{}\n```\n\n", test_content);
234
235        assert_eq!(result, expected);
236        Ok(())
237    }
238
239    #[test]
240    fn test_process_file_typescript() -> Result<()> {
241        let temp_dir = TempDir::new()?;
242        let base_path = temp_dir.path();
243        let file_path = base_path.join("app.tsx");
244
245        let test_content = "export const App = () => <div>Hello</div>;";
246        let mut file = File::create(&file_path)?;
247        file.write_all(test_content.as_bytes())?;
248
249        let result = process_file(&file_path, base_path)?;
250        assert!(result.contains("```tsx"));
251        Ok(())
252    }
253
254    #[test]
255    fn test_process_file_not_found() {
256        let base_path = Path::new(".");
257        let file_path = Path::new("nonexistent.rs");
258
259        assert!(process_file(file_path, base_path).is_err());
260    }
261
262    #[test]
263    fn test_walk_files_respects_gitignore() -> Result<()> {
264        let temp_dir = TempDir::new()?;
265        let base_path = temp_dir.path();
266
267        // Initialize a git repository so .gitignore is respected
268        std::fs::create_dir(base_path.join(".git"))?;
269
270        // Create .gitignore
271        let gitignore_path = base_path.join(".gitignore");
272        let mut gitignore = File::create(&gitignore_path)?;
273        writeln!(gitignore, "ignored/")?;
274        writeln!(gitignore, "*.log")?;
275
276        // Create files
277        let included_file = base_path.join("main.rs");
278        File::create(&included_file)?;
279
280        let ignored_dir = base_path.join("ignored");
281        std::fs::create_dir(&ignored_dir)?;
282        let ignored_file = ignored_dir.join("ignored.rs");
283        File::create(&ignored_file)?;
284
285        let log_file = base_path.join("debug.log");
286        File::create(&log_file)?;
287
288        // Test walk_files
289        let files: Vec<_> = walk_files(base_path).collect();
290
291        assert!(files.iter().any(|p| p.ends_with("main.rs")));
292        assert!(!files.iter().any(|p| p.ends_with("ignored.rs")));
293        // Note: *.log is not a supported file type, so it won't be in the list anyway
294        assert!(!files.iter().any(|p| p.ends_with("debug.log")));
295
296        Ok(())
297    }
298}