1use anyhow::Result;
2use ignore::WalkBuilder;
3use std::path::Path;
4
5const FILE_TYPES: &[(&str, &str)] = &[
7 ("rs", "rust"),
9 ("ts", "typescript"),
11 ("tsx", "tsx"),
12 ("js", "javascript"),
13 ("jsx", "jsx"),
14 ("mjs", "javascript"),
15 ("cjs", "javascript"),
16 ("py", "python"),
18 ("go", "go"),
20 ("java", "java"),
22 ("kt", "kotlin"),
23 ("kts", "kotlin"),
24 ("scala", "scala"),
25 ("c", "c"),
27 ("h", "c"),
28 ("cpp", "cpp"),
29 ("cc", "cpp"),
30 ("cxx", "cpp"),
31 ("hpp", "cpp"),
32 ("hxx", "cpp"),
33 ("cs", "csharp"),
35 ("rb", "ruby"),
37 ("php", "php"),
39 ("swift", "swift"),
41 ("sh", "bash"),
43 ("bash", "bash"),
44 ("zsh", "zsh"),
45 ("html", "html"),
47 ("htm", "html"),
48 ("css", "css"),
49 ("scss", "scss"),
50 ("sass", "sass"),
51 ("less", "less"),
52 ("vue", "vue"),
53 ("svelte", "svelte"),
54 ("json", "json"),
56 ("yaml", "yaml"),
57 ("yml", "yaml"),
58 ("toml", "toml"),
59 ("xml", "xml"),
60 ("sql", "sql"),
62 ("graphql", "graphql"),
64 ("gql", "graphql"),
65 ("proto", "protobuf"),
67 ("md", "markdown"),
69 ("mdx", "mdx"),
70 ("ex", "elixir"),
72 ("exs", "elixir"),
73 ("erl", "erlang"),
74 ("hs", "haskell"),
76 ("lua", "lua"),
78 ("zig", "zig"),
80 ("nim", "nim"),
82 ("ml", "ocaml"),
84 ("mli", "ocaml"),
85 ("fs", "fsharp"),
87 ("fsi", "fsharp"),
88 ("fsx", "fsharp"),
89 ("dart", "dart"),
91 ("r", "r"),
93 ("R", "r"),
94 ("jl", "julia"),
96 ("clj", "clojure"),
98 ("cljs", "clojure"),
99 ("cljc", "clojure"),
100];
101
102pub 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
111pub fn is_target_file(path: &Path) -> bool {
113 get_language(path).is_some()
114}
115
116pub 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
138pub fn walk_files(path: &Path) -> impl Iterator<Item = std::path::PathBuf> {
140 WalkBuilder::new(path)
141 .hidden(true) .git_ignore(true) .git_global(true) .git_exclude(true) .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 ("test.rs", true),
165 ("test.ts", true),
166 ("test.js", true),
167 ("test.py", true),
168 ("test.go", true),
169 ("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 ("test.txt", false),
192 ("test", false),
193 ("test.RS", false), ];
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 std::fs::create_dir(base_path.join(".git"))?;
269
270 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 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 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 assert!(!files.iter().any(|p| p.ends_with("debug.log")));
295
296 Ok(())
297 }
298}