Skip to main content

argyph_pack/render/
markdown.rs

1use camino::Utf8PathBuf;
2
3/// Render a set of packed files in the human-readable markdown format.
4///
5/// Each file produces a level-2 heading with path and metadata, followed
6/// by a fenced code block using the detected language from the file
7/// extension.
8pub fn render_markdown(files: &[(Utf8PathBuf, &str, bool, usize)], repo_name: &str) -> String {
9    let mut out = String::new();
10    out.push_str(&format!("# Repository: {repo_name}\n\n"));
11
12    for (path, content, truncated, token_count) in files {
13        let lang = lang_from_ext(path);
14        out.push_str(&format!(
15            "## File: {path} (tokens: {token_count}, truncated: {truncated})\n\n"
16        ));
17        out.push_str(&format!("```{lang}\n{content}\n```\n\n"));
18    }
19
20    out
21}
22
23/// Map a file extension to a markdown code-fence language identifier.
24fn lang_from_ext(path: &camino::Utf8Path) -> &str {
25    match path.extension().unwrap_or("") {
26        "rs" => "rust",
27        "ts" => "typescript",
28        "tsx" => "tsx",
29        "js" => "javascript",
30        "jsx" => "jsx",
31        "py" | "pyi" | "pyx" => "python",
32        "md" | "mdx" => "markdown",
33        "toml" => "toml",
34        "yaml" | "yml" => "yaml",
35        "json" => "json",
36        "html" | "htm" => "html",
37        "css" => "css",
38        "scss" | "sass" => "scss",
39        "sql" => "sql",
40        "sh" | "bash" | "zsh" => "bash",
41        "c" | "h" => "c",
42        "cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp",
43        "go" => "go",
44        "java" => "java",
45        "kt" | "kts" => "kotlin",
46        "swift" => "swift",
47        "rb" => "ruby",
48        "php" => "php",
49        "cs" => "csharp",
50        "fs" | "fsx" => "fsharp",
51        "scala" | "sc" => "scala",
52        "xml" | "svg" => "xml",
53        "r" => "r",
54        "lua" => "lua",
55        "zig" => "zig",
56        "ex" | "exs" => "elixir",
57        "erl" | "hrl" => "erlang",
58        "hs" => "haskell",
59        "ml" | "mli" => "ocaml",
60        "dockerfile" => "dockerfile",
61        "makefile" | "mk" => "makefile",
62        "cmake" => "cmake",
63        "proto" => "protobuf",
64        "graphql" | "gql" => "graphql",
65        "vue" => "vue",
66        "svelte" => "svelte",
67        "tf" | "tfvars" => "terraform",
68        _ => "",
69    }
70}
71
72#[cfg(test)]
73#[allow(clippy::unwrap_used)]
74mod tests {
75    use super::*;
76    use camino::Utf8PathBuf;
77
78    fn p(s: &str) -> Utf8PathBuf {
79        Utf8PathBuf::from(s)
80    }
81
82    #[test]
83    fn empty_file_list_produces_only_header() {
84        let result = render_markdown(&[], "my-repo");
85        assert_eq!(result.trim(), "# Repository: my-repo");
86    }
87
88    #[test]
89    fn single_file_has_heading_and_code_block() {
90        let files = [(p("src/main.rs"), "fn main() {}", false, 4)];
91        let result = render_markdown(&files, "test");
92        assert!(result.contains("## File: src/main.rs (tokens: 4, truncated: false)"));
93        assert!(result.contains("```rust"));
94        assert!(result.contains("fn main() {}"));
95        assert!(result.contains("```"));
96    }
97
98    #[test]
99    fn truncated_flag_is_written() {
100        let files = [(p("lib.ts"), "export const x = 1;", true, 3)];
101        let result = render_markdown(&files, "repo");
102        assert!(result.contains("truncated: true"));
103    }
104
105    #[test]
106    fn multiple_files_in_order() {
107        let files = [(p("a.rs"), "// a", false, 1), (p("b.py"), "# b", false, 1)];
108        let result = render_markdown(&files, "repo");
109        let a_pos = result.find("a.rs").unwrap();
110        let b_pos = result.find("b.py").unwrap();
111        assert!(a_pos < b_pos);
112    }
113
114    #[test]
115    fn lang_detection_common_extensions() {
116        assert_eq!(lang_from_ext(camino::Utf8Path::new("foo.rs")), "rust");
117        assert_eq!(lang_from_ext(camino::Utf8Path::new("foo.ts")), "typescript");
118        assert_eq!(lang_from_ext(camino::Utf8Path::new("foo.py")), "python");
119        assert_eq!(lang_from_ext(camino::Utf8Path::new("foo.md")), "markdown");
120        assert_eq!(lang_from_ext(camino::Utf8Path::new("foo.toml")), "toml");
121        assert_eq!(lang_from_ext(camino::Utf8Path::new("foo.json")), "json");
122    }
123
124    #[test]
125    fn unknown_extension_returns_empty() {
126        assert_eq!(lang_from_ext(camino::Utf8Path::new("foo.unknown_ext")), "");
127        assert_eq!(lang_from_ext(camino::Utf8Path::new("no_extension")), "");
128    }
129}