Skip to content

Commit 3e7d25d

Browse files
cursoragentseatedro
andcommitted
Add XML output format for improved LLM compatibility
Co-authored-by: rohit <[email protected]>
1 parent ae396d7 commit 3e7d25d

File tree

5 files changed

+159
-24
lines changed

5 files changed

+159
-24
lines changed

src/analyzer.rs

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,15 @@ pub fn process_directory(args: &Cli) -> Result<()> {
4141
fs::write(pdf_path, pdf_data)?;
4242
println!("PDF output written to: {}", pdf_path.display());
4343
} else {
44+
// Determine project name for XML output
45+
let project_name = if args.xml {
46+
Some(determine_project_name(&args.paths))
47+
} else {
48+
None
49+
};
50+
4451
// Handle output (print/copy/save)
45-
let output = generate_output(&entries, output_format)?;
52+
let output = generate_output(&entries, output_format, args.xml, project_name)?;
4653
handle_output(output, args)?;
4754
}
4855

@@ -54,6 +61,33 @@ pub fn process_directory(args: &Cli) -> Result<()> {
5461
Ok(())
5562
}
5663

64+
fn determine_project_name(paths: &[String]) -> String {
65+
if let Some(first_path) = paths.first() {
66+
let path = std::path::Path::new(first_path);
67+
68+
// If it's a directory, use its name
69+
if path.is_dir() {
70+
if let Some(name) = path.file_name() {
71+
return name.to_string_lossy().to_string();
72+
}
73+
}
74+
75+
// If it's a file, use the parent directory name
76+
if path.is_file() {
77+
if let Some(parent) = path.parent() {
78+
if let Some(name) = parent.file_name() {
79+
return name.to_string_lossy().to_string();
80+
}
81+
}
82+
}
83+
84+
// Fallback to just the path itself
85+
first_path.clone()
86+
} else {
87+
"project".to_string()
88+
}
89+
}
90+
5791
pub fn process_entries(args: &Cli) -> Result<Vec<FileEntry>> {
5892
let max_size = args.max_size.expect("max_size should be set from config");
5993
let max_depth = args.max_depth.expect("max_depth should be set from config");
@@ -361,6 +395,7 @@ mod tests {
361395
pdf: None,
362396
traverse_links: false,
363397
link_depth: None,
398+
xml: false,
364399
}
365400
}
366401

src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ pub struct Cli {
114114
/// Maximum depth to traverse sublinks (default: 1)
115115
#[arg(long)]
116116
pub link_depth: Option<usize>,
117+
118+
/// Output in XML format for better LLM compatibility
119+
#[arg(short = 'x', long)]
120+
pub xml: bool,
117121
}
118122

119123
impl Cli {

src/output.rs

Lines changed: 117 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,37 +15,89 @@ pub struct FileEntry {
1515
pub size: u64,
1616
}
1717

18-
pub fn generate_output(entries: &[FileEntry], format: OutputFormat) -> Result<String> {
18+
pub fn generate_output(entries: &[FileEntry], format: OutputFormat, xml_format: bool, project_name: Option<String>) -> Result<String> {
1919
let mut output = String::new();
2020

21+
if xml_format {
22+
let project_name = project_name.unwrap_or_else(|| "project".to_string());
23+
output.push_str(&format!("<context name=\"{}\">\n", xml_escape(&project_name)));
24+
}
25+
2126
match format {
2227
OutputFormat::Tree => {
23-
output.push_str("Directory Structure:\n");
28+
if xml_format {
29+
output.push_str("<tree>\n");
30+
} else {
31+
output.push_str("Directory Structure:\n");
32+
}
2433
output.push_str(&generate_tree(entries)?);
34+
if xml_format {
35+
output.push_str("</tree>\n");
36+
}
2537
}
2638
OutputFormat::Files => {
27-
output.push_str("File Contents:\n");
28-
output.push_str(&generate_files(entries)?);
39+
if xml_format {
40+
output.push_str("<files>\n");
41+
} else {
42+
output.push_str("File Contents:\n");
43+
}
44+
output.push_str(&generate_files(entries, xml_format)?);
45+
if xml_format {
46+
output.push_str("</files>\n");
47+
}
2948
}
3049
OutputFormat::Both => {
31-
output.push_str("Directory Structure:\n");
50+
if xml_format {
51+
output.push_str("<tree>\n");
52+
} else {
53+
output.push_str("Directory Structure:\n");
54+
}
3255
output.push_str(&generate_tree(entries)?);
33-
output.push_str("\nFile Contents:\n");
34-
output.push_str(&generate_files(entries)?);
56+
if xml_format {
57+
output.push_str("</tree>\n\n<files>\n");
58+
} else {
59+
output.push_str("\nFile Contents:\n");
60+
}
61+
output.push_str(&generate_files(entries, xml_format)?);
62+
if xml_format {
63+
output.push_str("</files>\n");
64+
}
3565
}
3666
}
3767

3868
// Add summary
39-
output.push_str("\nSummary:\n");
40-
output.push_str(&format!("Total files: {}\n", entries.len()));
41-
output.push_str(&format!(
42-
"Total size: {} bytes\n",
43-
entries.iter().map(|e| e.size).sum::<u64>()
44-
));
69+
if xml_format {
70+
output.push_str("<summary>\n");
71+
output.push_str(&format!("Total files: {}\n", entries.len()));
72+
output.push_str(&format!(
73+
"Total size: {} bytes\n",
74+
entries.iter().map(|e| e.size).sum::<u64>()
75+
));
76+
output.push_str("</summary>\n");
77+
} else {
78+
output.push_str("\nSummary:\n");
79+
output.push_str(&format!("Total files: {}\n", entries.len()));
80+
output.push_str(&format!(
81+
"Total size: {} bytes\n",
82+
entries.iter().map(|e| e.size).sum::<u64>()
83+
));
84+
}
85+
86+
if xml_format {
87+
output.push_str("</context>");
88+
}
4589

4690
Ok(output)
4791
}
4892

93+
fn xml_escape(text: &str) -> String {
94+
text.replace('&', "&amp;")
95+
.replace('<', "&lt;")
96+
.replace('>', "&gt;")
97+
.replace('"', "&quot;")
98+
.replace('\'', "&apos;")
99+
}
100+
49101
pub fn display_token_counts(token_counter: TokenCounter, entries: &[FileEntry]) -> Result<()> {
50102
let token_count = token_counter.count_files(entries)?;
51103

@@ -117,15 +169,24 @@ fn generate_tree(entries: &[FileEntry]) -> Result<String> {
117169
Ok(output)
118170
}
119171

120-
fn generate_files(entries: &[FileEntry]) -> Result<String> {
172+
fn generate_files(entries: &[FileEntry], xml_format: bool) -> Result<String> {
121173
let mut output = String::new();
122174

123175
for entry in entries {
124-
output.push_str(&format!("\nFile: {}\n", entry.path.display()));
125-
output.push_str(&"=".repeat(48));
126-
output.push('\n');
127-
output.push_str(&entry.content);
128-
output.push('\n');
176+
if xml_format {
177+
output.push_str(&format!("<file path=\"{}\">\n", xml_escape(entry.path.display().to_string().as_str())));
178+
output.push_str(&"=".repeat(48));
179+
output.push('\n');
180+
output.push_str(&entry.content);
181+
output.push('\n');
182+
output.push_str("</file>\n");
183+
} else {
184+
output.push_str(&format!("\nFile: {}\n", entry.path.display()));
185+
output.push_str(&"=".repeat(48));
186+
output.push('\n');
187+
output.push_str(&entry.content);
188+
output.push('\n');
189+
}
129190
}
130191

131192
Ok(output)
@@ -280,7 +341,7 @@ mod tests {
280341
#[test]
281342
fn test_files_output() {
282343
let entries = create_test_entries();
283-
let files = generate_files(&entries).unwrap();
344+
let files = generate_files(&entries, false).unwrap();
284345
let expected = format!(
285346
"\nFile: {}\n{}\n{}\n\nFile: {}\n{}\n{}\n",
286347
"src/main.rs",
@@ -298,23 +359,55 @@ mod tests {
298359
let entries = create_test_entries();
299360

300361
// Test tree format
301-
let tree_output = generate_output(&entries, OutputFormat::Tree).unwrap();
362+
let tree_output = generate_output(&entries, OutputFormat::Tree, false, None).unwrap();
302363
assert!(tree_output.contains("Directory Structure:"));
303364
assert!(tree_output.contains("src/"));
304365
assert!(tree_output.contains("main.rs"));
305366

306367
// Test files format
307-
let files_output = generate_output(&entries, OutputFormat::Files).unwrap();
368+
let files_output = generate_output(&entries, OutputFormat::Files, false, None).unwrap();
308369
assert!(files_output.contains("File Contents:"));
309370
assert!(files_output.contains("fn main()"));
310371
assert!(files_output.contains("pub fn helper()"));
311372

312373
// Test both format
313-
let both_output = generate_output(&entries, OutputFormat::Both).unwrap();
374+
let both_output = generate_output(&entries, OutputFormat::Both, false, None).unwrap();
314375
assert!(both_output.contains("Directory Structure:"));
315376
assert!(both_output.contains("File Contents:"));
316377
}
317378

379+
#[test]
380+
fn test_xml_output() {
381+
let entries = create_test_entries();
382+
383+
// Test XML tree format
384+
let xml_tree_output = generate_output(&entries, OutputFormat::Tree, true, Some("test_project".to_string())).unwrap();
385+
assert!(xml_tree_output.contains("<context name=\"test_project\">"));
386+
assert!(xml_tree_output.contains("<tree>"));
387+
assert!(xml_tree_output.contains("</tree>"));
388+
assert!(xml_tree_output.contains("<summary>"));
389+
assert!(xml_tree_output.contains("</summary>"));
390+
assert!(xml_tree_output.contains("</context>"));
391+
392+
// Test XML files format
393+
let xml_files_output = generate_output(&entries, OutputFormat::Files, true, Some("test_project".to_string())).unwrap();
394+
assert!(xml_files_output.contains("<context name=\"test_project\">"));
395+
assert!(xml_files_output.contains("<files>"));
396+
assert!(xml_files_output.contains("<file path=\"src/main.rs\">"));
397+
assert!(xml_files_output.contains("</file>"));
398+
assert!(xml_files_output.contains("</files>"));
399+
assert!(xml_files_output.contains("</context>"));
400+
401+
// Test XML both format
402+
let xml_both_output = generate_output(&entries, OutputFormat::Both, true, Some("test_project".to_string())).unwrap();
403+
assert!(xml_both_output.contains("<context name=\"test_project\">"));
404+
assert!(xml_both_output.contains("<tree>"));
405+
assert!(xml_both_output.contains("</tree>"));
406+
assert!(xml_both_output.contains("<files>"));
407+
assert!(xml_both_output.contains("</files>"));
408+
assert!(xml_both_output.contains("</context>"));
409+
}
410+
318411
#[test]
319412
fn test_handle_output() {
320413
use tempfile::tempdir;
@@ -345,6 +438,7 @@ mod tests {
345438
traverse_links: false,
346439
link_depth: None,
347440
config_path: false,
441+
xml: false,
348442
};
349443

350444
handle_output(content.clone(), &args).unwrap();

test_project/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub fn helper() { println!("Helper function"); }

test_project/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fn main() { println!("Hello, world!"); }

0 commit comments

Comments
 (0)