use std::collections::BTreeMap;
use std::fmt;
use super::virtual_fs::VirtualFile;
pub fn write_markdown<W: fmt::Write>(files: &[VirtualFile], sink: &mut W) -> fmt::Result {
for (i, file) in files.iter().enumerate() {
if i > 0 {
writeln!(sink)?;
}
let lang = file.extension().unwrap_or("");
let fence = "`".repeat(min_fence_len(file.content()));
writeln!(sink, "## `{}`", file.normalized_path())?;
writeln!(sink)?;
writeln!(sink, "{fence}{lang}")?;
write!(sink, "{}", file.content())?;
if !file.content().ends_with('\n') {
writeln!(sink)?;
}
writeln!(sink, "{fence}")?;
}
Ok(())
}
pub fn min_fence_len(content: &str) -> usize {
let mut max_run: usize = 0;
let mut current_run: usize = 0;
for &b in content.as_bytes() {
if b == b'`' {
current_run += 1;
if current_run > max_run {
max_run = current_run;
}
} else {
current_run = 0;
}
}
if max_run >= 3 { max_run + 1 } else { 3 }
}
pub fn write_tree<W: fmt::Write>(
files: &[VirtualFile],
root_label: &str,
sink: &mut W,
) -> fmt::Result {
let root = build_tree(files);
writeln!(sink, "{root_label}")?;
let mut prefix_buf = String::new();
render_children(sink, &root.children, &mut prefix_buf)
}
#[derive(Debug, Default)]
struct TreeNode {
children: BTreeMap<String, Self>,
}
fn build_tree(files: &[VirtualFile]) -> TreeNode {
let mut root = TreeNode::default();
for file in files {
insert_path(&mut root, file.normalized_path());
}
root
}
fn insert_path(node: &mut TreeNode, path: &str) {
if let Some((dir, rest)) = path.split_once('/') {
if dir.is_empty() {
insert_path(node, rest);
} else {
let child = node.children.entry(dir.to_owned()).or_default();
insert_path(child, rest);
}
} else if !path.is_empty() {
let _ = node.children.entry(path.to_owned()).or_default();
}
}
fn render_children<W: fmt::Write>(
sink: &mut W,
children: &BTreeMap<String, TreeNode>,
prefix_buf: &mut String,
) -> fmt::Result {
let count = children.len();
for (i, (name, node)) in children.iter().enumerate() {
let is_last = i + 1 == count;
let connector = if is_last { "└── " } else { "├── " };
let suffix = if node.children.is_empty() { "" } else { "/" };
writeln!(sink, "{prefix_buf}{connector}{name}{suffix}")?;
if !node.children.is_empty() {
let segment = if is_last { " " } else { "│ " };
let prev_len = prefix_buf.len();
prefix_buf.push_str(segment);
render_children(sink, &node.children, prefix_buf)?;
prefix_buf.truncate(prev_len);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn file(path: &str, content: &str) -> VirtualFile {
VirtualFile::new_unchecked(path, content)
}
#[test]
fn min_fence_len_no_backticks() {
assert_eq!(min_fence_len("hello world"), 3);
}
#[test]
fn min_fence_len_short_backticks() {
assert_eq!(min_fence_len("some `inline` code"), 3);
assert_eq!(min_fence_len("``double``"), 3);
}
#[test]
fn min_fence_len_triple_backticks() {
assert_eq!(min_fence_len("```\ncode\n```"), 4);
}
#[test]
fn min_fence_len_many_backticks() {
assert_eq!(min_fence_len("``````"), 7);
}
#[test]
fn markdown_single_file() {
let mut out = String::new();
write_markdown(&[file("src/main.rs", "fn main() {}\n")], &mut out).unwrap();
assert_eq!(out, "## `src/main.rs`\n\n```rs\nfn main() {}\n```\n");
}
#[test]
fn markdown_adds_trailing_newline_to_content() {
let mut out = String::new();
write_markdown(&[file("f.txt", "no newline")], &mut out).unwrap();
assert!(out.contains("no newline\n```"));
}
#[test]
fn markdown_variable_fence_for_backtick_content() {
let content = "before\n```\ninner\n```\nafter\n";
let mut out = String::new();
write_markdown(&[file("tricky.md", content)], &mut out).unwrap();
assert!(
out.contains("````"),
"fence should be at least 4 backticks when content contains ```"
);
assert!(out.contains(content));
}
#[test]
fn markdown_long_backtick_runs() {
let content = "some ```````` long run\n";
let mut out = String::new();
write_markdown(&[file("f.txt", content)], &mut out).unwrap();
assert!(
out.contains("`````````"),
"fence should be at least 9 backticks: {out}"
);
}
#[test]
fn markdown_empty_file_list() {
let mut out = String::new();
write_markdown(&[], &mut out).unwrap();
assert!(out.is_empty());
}
#[test]
fn tree_basic_structure() {
let files = &[
file("Cargo.toml", ""),
file("src/lib.rs", ""),
file("src/main.rs", ""),
];
let mut out = String::new();
write_tree(files, "project", &mut out).unwrap();
assert!(out.starts_with("project\n"));
assert!(out.contains("Cargo.toml"));
assert!(out.contains("src/"));
assert!(out.contains("main.rs"));
assert!(out.contains("lib.rs"));
}
#[test]
fn tree_empty_input() {
let mut out = String::new();
write_tree(&[], "root", &mut out).unwrap();
assert_eq!(out, "root\n");
}
#[test]
fn tree_component_that_is_both_file_and_directory() {
let files = &[file("src", "file content"), file("src/main.rs", "")];
let mut out = String::new();
write_tree(files, ".", &mut out).unwrap();
assert!(
out.contains("src/"),
"node with children must show trailing slash: {out}"
);
assert!(out.contains("main.rs"));
}
#[test]
fn tree_trailing_slash_path_does_not_create_ghost_node() {
let files = &[file("src/", "")];
let mut out = String::new();
write_tree(files, ".", &mut out).unwrap();
assert!(!out.contains("└── \n"), "ghost empty-name node: {out}");
}
}