1use std::path::Path;
9
10use crate::walker::FileEntry;
11
12pub trait Formatter {
14 fn format_tree(&self, tree: &str) -> String;
16
17 fn format_file(&self, entry: &FileEntry, content: &str) -> String;
19
20 fn wrap(&self, tree_block: Option<&str>, files_block: &str) -> String;
22
23 fn stream_start(&self, tree_block: Option<&str>) -> String;
25
26 fn stream_end(&self) -> String;
28
29 fn separator(&self) -> &'static str;
31}
32
33pub struct XmlFormatter;
35
36impl XmlFormatter {
37 fn escape_xml_text(s: &str) -> String {
40 s.replace('&', "&")
41 .replace('<', "<")
42 .replace('>', ">")
43 }
44
45 fn escape_xml_attr(s: &str) -> String {
48 s.replace('&', "&")
49 .replace('<', "<")
50 .replace('>', ">")
51 .replace('"', """)
52 .replace('\'', "'")
53 }
54}
55
56impl Formatter for XmlFormatter {
57 fn format_tree(&self, tree: &str) -> String {
58 format!(
59 "<project_tree>\n{}</project_tree>",
60 Self::escape_xml_text(tree)
61 )
62 }
63
64 fn format_file(&self, entry: &FileEntry, content: &str) -> String {
65 let filename = entry
66 .relative_path
67 .file_name()
68 .map(|s| s.to_string_lossy())
69 .unwrap_or_default();
70
71 let path = format_path_for_output(&entry.relative_path);
72
73 format!(
74 "<file name=\"{}\" path=\"{}\">\n{}\n</file>",
75 Self::escape_xml_attr(&filename),
76 Self::escape_xml_attr(&path),
77 Self::escape_xml_text(content.trim())
78 )
79 }
80
81 fn wrap(&self, tree_block: Option<&str>, files_block: &str) -> String {
82 match tree_block {
83 Some(tree) => format!(
84 "<context>\n{}\n<project_files>\n{}\n</project_files>\n</context>",
85 tree, files_block
86 ),
87 None => format!(
88 "<context>\n<project_files>\n{}\n</project_files>\n</context>",
89 files_block
90 ),
91 }
92 }
93
94 fn stream_start(&self, tree_block: Option<&str>) -> String {
95 match tree_block {
96 Some(tree) => format!("<context>\n{}\n<project_files>", tree),
97 None => "<context>\n<project_files>".to_string(),
98 }
99 }
100
101 fn stream_end(&self) -> String {
102 "</project_files>\n</context>".to_string()
103 }
104
105 fn separator(&self) -> &'static str {
106 "\n"
107 }
108}
109
110pub struct MarkdownFormatter;
112
113impl Formatter for MarkdownFormatter {
114 fn format_tree(&self, tree: &str) -> String {
115 format!("## Project Tree\n\n```\n{}```", tree)
116 }
117
118 fn format_file(&self, entry: &FileEntry, content: &str) -> String {
119 let path = format_path_for_output(&entry.relative_path);
120 let extension = entry
121 .relative_path
122 .extension()
123 .map(|s| s.to_string_lossy())
124 .unwrap_or_default();
125
126 format!("## {}\n\n```{}\n{}\n```", path, extension, content.trim())
127 }
128
129 fn wrap(&self, tree_block: Option<&str>, files_block: &str) -> String {
130 match tree_block {
131 Some(tree) => format!("# Project Context\n\n{}\n\n{}", tree, files_block),
132 None => format!("# Project Context\n\n{}", files_block),
133 }
134 }
135
136 fn stream_start(&self, tree_block: Option<&str>) -> String {
137 match tree_block {
138 Some(tree) => format!("# Project Context\n\n{}\n", tree),
139 None => "# Project Context\n".to_string(),
140 }
141 }
142
143 fn stream_end(&self) -> String {
144 String::new()
145 }
146
147 fn separator(&self) -> &'static str {
148 "\n\n"
149 }
150}
151
152pub struct PlainFormatter;
154
155impl Formatter for PlainFormatter {
156 fn format_tree(&self, tree: &str) -> String {
157 format!("=== PROJECT TREE ===\n\n{}", tree)
158 }
159
160 fn format_file(&self, entry: &FileEntry, content: &str) -> String {
161 let path = format_path_for_output(&entry.relative_path);
162 format!("=== {} ===\n\n{}\n", path, content.trim())
163 }
164
165 fn wrap(&self, tree_block: Option<&str>, files_block: &str) -> String {
166 match tree_block {
167 Some(tree) => format!("{}\n{}", tree, files_block),
168 None => files_block.to_string(),
169 }
170 }
171
172 fn stream_start(&self, tree_block: Option<&str>) -> String {
173 match tree_block {
174 Some(tree) => format!("{}\n", tree),
175 None => String::new(),
176 }
177 }
178
179 fn stream_end(&self) -> String {
180 String::new()
181 }
182
183 fn separator(&self) -> &'static str {
184 "\n"
185 }
186}
187
188pub struct JsonFormatter;
203
204impl JsonFormatter {
205 fn escape_json_string(s: &str) -> String {
207 serde_json::to_string(s).unwrap_or_else(|_| format!("\"{}\"", s))
208 }
209}
210
211impl Formatter for JsonFormatter {
212 fn format_tree(&self, tree: &str) -> String {
213 Self::escape_json_string(tree)
215 }
216
217 fn format_file(&self, entry: &FileEntry, content: &str) -> String {
218 let filename = entry
219 .relative_path
220 .file_name()
221 .map(|s| s.to_string_lossy().to_string())
222 .unwrap_or_default();
223
224 let path = format_path_for_output(&entry.relative_path);
225
226 format!(
228 r#"{{"name":{},"path":{},"content":{}}}"#,
229 Self::escape_json_string(&filename),
230 Self::escape_json_string(&path),
231 Self::escape_json_string(content.trim())
232 )
233 }
234
235 fn wrap(&self, tree_block: Option<&str>, files_block: &str) -> String {
236 match tree_block {
239 Some(tree) => format!(
240 r#"{{"tree":{},"files":[{}]}}"#,
241 tree, files_block
243 ),
244 None => format!(r#"{{"files":[{}]}}"#, files_block),
245 }
246 }
247
248 fn stream_start(&self, tree_block: Option<&str>) -> String {
249 match tree_block {
252 Some(tree) => format!(r#"{{"type":"tree","content":{}}}"#, tree),
253 None => String::new(),
254 }
255 }
256
257 fn stream_end(&self) -> String {
258 String::new()
260 }
261
262 fn separator(&self) -> &'static str {
263 "\n"
265 }
266}
267
268fn format_path_for_output(path: &Path) -> String {
270 let path_str = path.to_string_lossy().replace('\\', "/");
271 if path_str.starts_with('/') {
272 path_str.to_string()
273 } else {
274 format!("/{}", path_str)
275 }
276}
277
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
280pub enum OutputFormat {
281 #[default]
283 Xml,
284 Markdown,
286 Plain,
288 Json,
290}
291
292impl OutputFormat {}
293
294impl std::str::FromStr for OutputFormat {
295 type Err = ();
296 fn from_str(s: &str) -> Result<Self, Self::Err> {
297 match s.to_lowercase().as_str() {
298 "xml" => Ok(Self::Xml),
299 "markdown" | "md" => Ok(Self::Markdown),
300 "plain" => Ok(Self::Plain),
301 "json" => Ok(Self::Json),
302 _ => Err(()),
303 }
304 }
305}
306
307pub fn get_formatter(format: OutputFormat) -> Box<dyn Formatter> {
309 match format {
310 OutputFormat::Xml => Box::new(XmlFormatter),
311 OutputFormat::Markdown => Box::new(MarkdownFormatter),
312 OutputFormat::Plain => Box::new(PlainFormatter),
313 OutputFormat::Json => Box::new(JsonFormatter),
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use std::path::PathBuf;
321
322 fn make_entry(rel_path: &str) -> FileEntry {
323 FileEntry {
324 absolute_path: PathBuf::from("/project").join(rel_path),
325 relative_path: PathBuf::from(rel_path),
326 size: 100,
327 }
328 }
329
330 #[test]
331 fn test_xml_formatter() {
332 let formatter = XmlFormatter;
333 let entry = make_entry("src/main.rs");
334 let output = formatter.format_file(&entry, "fn main() {}");
335
336 assert!(output.contains("<file name=\"main.rs\""));
337 assert!(output.contains("path=\"/src/main.rs\""));
338 assert!(output.contains("fn main() {}"));
339 }
340
341 #[test]
342 fn test_xml_formatter_escapes_content() {
343 let formatter = XmlFormatter;
344 let entry = make_entry("src/test.rs");
345 let content = r#"if x < 10 && y > 5 { println!("<tag>"); }"#;
347 let output = formatter.format_file(&entry, content);
348
349 assert!(output.contains("x < 10"));
351 assert!(output.contains("&&"));
352 assert!(output.contains("y > 5"));
353 assert!(output.contains("<tag>"));
354 assert!(!output.contains("< 10"));
356 assert!(!output.contains("> 5"));
357 }
358
359 #[test]
360 fn test_xml_formatter_escapes_attributes() {
361 let formatter = XmlFormatter;
362 let entry = make_entry("src/test&file.rs");
364 let output = formatter.format_file(&entry, "content");
365
366 assert!(output.contains("name=\"test&file.rs\""));
368 assert!(output.contains("path=\"/src/test&file.rs\""));
369 }
370
371 #[test]
372 fn test_xml_formatter_escapes_quotes_in_attrs() {
373 let escaped = XmlFormatter::escape_xml_attr(r#"file"name'test"#);
375 assert_eq!(escaped, "file"name'test");
376 }
377
378 #[test]
379 fn test_xml_formatter_escapes_tree() {
380 let formatter = XmlFormatter;
381 let tree = "src/\n <generated>/\n test&file.rs";
382 let output = formatter.format_tree(tree);
383
384 assert!(output.contains("<generated>"));
385 assert!(output.contains("test&file.rs"));
386 }
387
388 #[test]
389 fn test_markdown_formatter() {
390 let formatter = MarkdownFormatter;
391 let entry = make_entry("src/main.rs");
392 let output = formatter.format_file(&entry, "fn main() {}");
393
394 assert!(output.contains("## /src/main.rs"));
395 assert!(output.contains("```rs"));
396 assert!(output.contains("fn main() {}"));
397 }
398
399 #[test]
400 fn test_plain_formatter() {
401 let formatter = PlainFormatter;
402 let entry = make_entry("src/main.rs");
403 let output = formatter.format_file(&entry, "fn main() {}");
404
405 assert!(output.contains("=== /src/main.rs ==="));
406 assert!(output.contains("fn main() {}"));
407 }
408
409 #[test]
410 fn test_json_formatter_file() {
411 let formatter = JsonFormatter;
412 let entry = make_entry("src/main.rs");
413 let output = formatter.format_file(&entry, "fn main() {}");
414
415 let parsed: serde_json::Value = serde_json::from_str(&output).expect("Invalid JSON");
417 assert_eq!(parsed["name"], "main.rs");
418 assert_eq!(parsed["path"], "/src/main.rs");
419 assert_eq!(parsed["content"], "fn main() {}");
420 }
421
422 #[test]
423 fn test_json_formatter_escapes_special_chars() {
424 let formatter = JsonFormatter;
425 let entry = make_entry("src/test.rs");
426 let content = r#"let s = "hello\nworld"; // <test>"#;
428 let output = formatter.format_file(&entry, content);
429
430 let parsed: serde_json::Value = serde_json::from_str(&output).expect("Invalid JSON");
432 assert_eq!(parsed["content"], content);
433 }
434
435 #[test]
436 fn test_json_formatter_wrap_with_tree() {
437 let formatter = JsonFormatter;
438 let entry = make_entry("src/main.rs");
439 let file_block = formatter.format_file(&entry, "fn main() {}");
440 let tree = formatter.format_tree("src/\n main.rs");
441 let output = formatter.wrap(Some(&tree), &file_block);
442
443 let parsed: serde_json::Value = serde_json::from_str(&output).expect("Invalid JSON");
445 assert!(parsed["tree"].is_string());
446 assert!(parsed["files"].is_array());
447 assert_eq!(parsed["files"].as_array().unwrap().len(), 1);
448 }
449
450 #[test]
451 fn test_json_formatter_wrap_without_tree() {
452 let formatter = JsonFormatter;
453 let entry = make_entry("src/main.rs");
454 let file_block = formatter.format_file(&entry, "fn main() {}");
455 let output = formatter.wrap(None, &file_block);
456
457 let parsed: serde_json::Value = serde_json::from_str(&output).expect("Invalid JSON");
459 assert!(parsed.get("tree").is_none());
460 assert!(parsed["files"].is_array());
461 }
462
463 #[test]
464 fn test_json_formatter_multiple_files() {
465 let formatter = JsonFormatter;
466 let entry1 = make_entry("src/main.rs");
467 let entry2 = make_entry("src/lib.rs");
468 let file1 = formatter.format_file(&entry1, "fn main() {}");
469 let file2 = formatter.format_file(&entry2, "pub mod test;");
470 let files_block = format!("{},{}", file1, file2);
472 let output = formatter.wrap(None, &files_block);
473
474 let parsed: serde_json::Value = serde_json::from_str(&output).expect("Invalid JSON");
476 assert_eq!(parsed["files"].as_array().unwrap().len(), 2);
477 assert_eq!(parsed["files"][0]["name"], "main.rs");
478 assert_eq!(parsed["files"][1]["name"], "lib.rs");
479 }
480
481 #[test]
482 fn test_json_formatter_streaming() {
483 let formatter = JsonFormatter;
484 let entry1 = make_entry("src/main.rs");
485 let entry2 = make_entry("src/lib.rs");
486 let file1 = formatter.format_file(&entry1, "fn main() {}");
487 let file2 = formatter.format_file(&entry2, "pub mod test;");
488
489 let separator = formatter.separator();
491 assert_eq!(separator, "\n");
492
493 let parsed1: serde_json::Value = serde_json::from_str(&file1).expect("Invalid JSON");
495 let parsed2: serde_json::Value = serde_json::from_str(&file2).expect("Invalid JSON");
496 assert_eq!(parsed1["name"], "main.rs");
497 assert_eq!(parsed2["name"], "lib.rs");
498 }
499}