use crate::cli::ContentFormat;
use crate::config::Config;
use crate::content::FileEntry;
use crate::error::PctxError;
use crate::output::tree;
use std::path::PathBuf;
pub fn format_output(entries: &[FileEntry], config: &Config) -> Result<String, PctxError> {
match config.output_format {
ContentFormat::Markdown => format_markdown(entries, config),
ContentFormat::Xml => format_xml(entries, config),
ContentFormat::Plain => format_plain(entries, config),
}
}
fn fence_for_content(content: &str) -> String {
let mut fence = "```".to_string();
while content.contains(&fence) {
fence.push('`');
}
fence
}
fn format_markdown(entries: &[FileEntry], config: &Config) -> Result<String, PctxError> {
let mut output = String::new();
if config.show_tree {
let paths: Vec<_> = entries
.iter()
.map(|e| PathBuf::from(&e.relative_path))
.collect();
let tree_struct = tree::build_tree(&paths);
output.push_str("## File Tree\n\n```\n");
output.push_str(&tree::tree_to_string(&tree_struct));
output.push_str("```\n\n");
}
for entry in entries {
let display_path = entry.display_path(config.absolute_paths);
output.push_str(&format!("`{}`:\n", display_path));
let lang = extension_to_language(&entry.extension);
let fence = fence_for_content(&entry.content);
output.push_str(&format!("{}{}\n", fence, lang));
output.push_str(&entry.content);
if !entry.content.ends_with('\n') {
output.push('\n');
}
output.push_str(&fence);
output.push_str("\n\n");
}
Ok(output)
}
fn format_xml(entries: &[FileEntry], config: &Config) -> Result<String, PctxError> {
let mut output = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<context>\n");
if config.show_tree {
let paths: Vec<_> = entries
.iter()
.map(|e| PathBuf::from(&e.relative_path))
.collect();
let tree_struct = tree::build_tree(&paths);
let tree_str = tree::tree_to_string(&tree_struct);
output.push_str(" <tree><![CDATA[\n");
output.push_str(&escape_cdata_content(&tree_str));
output.push_str("]]></tree>\n");
}
for entry in entries {
let display_path = entry.display_path(config.absolute_paths);
let escaped_path = escape_xml_attr(&display_path);
output.push_str(&format!(
" <file path=\"{}\" language=\"{}\">\n",
escaped_path,
extension_to_language(&entry.extension)
));
output.push_str("<![CDATA[\n");
output.push_str(&escape_cdata_content(&entry.content));
if !entry.content.ends_with('\n') {
output.push('\n');
}
output.push_str("]]>\n </file>\n");
}
output.push_str("</context>\n");
Ok(output)
}
fn format_plain(entries: &[FileEntry], config: &Config) -> Result<String, PctxError> {
let mut output = String::new();
if config.show_tree {
let paths: Vec<_> = entries
.iter()
.map(|e| PathBuf::from(&e.relative_path))
.collect();
let tree_struct = tree::build_tree(&paths);
output.push_str("=== File Tree ===\n");
output.push_str(&tree::tree_to_string(&tree_struct));
output.push('\n');
}
for entry in entries {
let display_path = entry.display_path(config.absolute_paths);
output.push_str(&format!("=== {} ===\n", display_path));
output.push_str(&entry.content);
if !entry.content.ends_with('\n') {
output.push('\n');
}
output.push('\n');
}
Ok(output)
}
fn escape_xml_attr(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn escape_cdata_content(content: &str) -> String {
content.replace("]]>", "]]]]><![CDATA[>")
}
fn extension_to_language(ext: &str) -> &'static str {
match ext.to_lowercase().as_str() {
"rs" => "rust",
"go" => "go",
"py" | "pyi" => "python",
"js" | "mjs" | "cjs" => "javascript",
"ts" | "mts" | "cts" => "typescript",
"jsx" => "jsx",
"tsx" => "tsx",
"java" => "java",
"kt" | "kts" => "kotlin",
"scala" => "scala",
"groovy" | "gradle" => "groovy",
"c" | "h" => "c",
"cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => "cpp",
"cs" => "csharp",
"fs" | "fsx" => "fsharp",
"rb" | "rake" | "gemspec" => "ruby",
"php" | "phtml" => "php",
"sh" | "bash" => "bash",
"zsh" => "zsh",
"fish" => "fish",
"ps1" | "psm1" => "powershell",
"bat" | "cmd" => "batch",
"html" | "htm" => "html",
"css" => "css",
"scss" => "scss",
"sass" => "sass",
"less" => "less",
"json" | "jsonc" => "json",
"yaml" | "yml" => "yaml",
"toml" => "toml",
"xml" | "xsl" | "xslt" => "xml",
"csv" => "csv",
"md" | "markdown" => "markdown",
"rst" => "rst",
"tex" => "latex",
"sql" => "sql",
"graphql" | "gql" => "graphql",
"dockerfile" => "dockerfile",
"makefile" | "mk" => "makefile",
"tf" | "tfvars" => "hcl",
"vue" => "vue",
"svelte" => "svelte",
"hs" | "lhs" => "haskell",
"elm" => "elm",
"ml" | "mli" => "ocaml",
"clj" | "cljs" | "cljc" => "clojure",
"ex" | "exs" => "elixir",
"erl" | "hrl" => "erlang",
"swift" => "swift",
"m" | "mm" => "objective-c",
"r" => "r",
"jl" => "julia",
"lua" => "lua",
"vim" => "vim",
"el" | "lisp" => "lisp",
"dart" => "dart",
"zig" => "zig",
"nim" => "nim",
"proto" => "protobuf",
"thrift" => "thrift",
"txt" => "text",
_ => "",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_cdata_content() {
assert_eq!(escape_cdata_content("normal text"), "normal text");
assert_eq!(
escape_cdata_content("text with ]]> in it"),
"text with ]]]]><![CDATA[> in it"
);
assert_eq!(
escape_cdata_content("multiple ]]> and ]]> here"),
"multiple ]]]]><![CDATA[> and ]]]]><![CDATA[> here"
);
}
#[test]
fn test_escape_xml_attr() {
assert_eq!(escape_xml_attr("normal"), "normal");
assert_eq!(escape_xml_attr("<test>"), "<test>");
assert_eq!(escape_xml_attr("a&b"), "a&b");
assert_eq!(escape_xml_attr("\"quoted\""), ""quoted"");
}
#[test]
fn test_extension_to_language() {
assert_eq!(extension_to_language("rs"), "rust");
assert_eq!(extension_to_language("RS"), "rust"); assert_eq!(extension_to_language("py"), "python");
assert_eq!(extension_to_language("unknown_ext"), "");
assert_eq!(extension_to_language(""), "");
}
}