wowlab_docgen_cli/infra/tree/
mod.rs1use wowlab_common::markdown;
4use wowlab_fs::path::Path;
5
6use crate::WorkspaceIndex;
7
8mod doc;
9
10use doc::extract_file_doc;
11
12const MAX_DEPTH: usize = 5;
13
14#[must_use]
16pub fn build(
17 workspace: &WorkspaceIndex,
18 dir: &Path,
19 paths: &[String],
20 max_depth: Option<u32>,
21) -> String {
22 let depth_limit = max_depth.map_or(MAX_DEPTH, |depth| {
23 usize::try_from(depth).unwrap_or(usize::MAX)
24 });
25 let mut lines = Vec::new();
26
27 let owned: Vec<String>;
28 let effective: &[String] = if paths.is_empty() {
29 owned = discover_source_dirs(workspace, dir);
30
31 &owned
32 } else {
33 paths
34 };
35
36 for path in effective {
37 let target = dir.join(path);
38
39 if workspace.is_dir(&target) {
40 lines.push(format!("{path}/"));
41 collect_entries(workspace, &target, &mut lines, 1, depth_limit);
42 }
43 }
44
45 if lines.is_empty() {
46 return String::new();
47 }
48
49 markdown::code_block("", lines.join("\n"))
50}
51
52fn is_dir_empty(workspace: &WorkspaceIndex, dir: &Path) -> bool {
53 !workspace.has_files_below(dir)
54}
55
56fn discover_source_dirs(workspace: &WorkspaceIndex, dir: &Path) -> Vec<String> {
57 let mut names: Vec<String> = workspace
58 .child_dirs(dir)
59 .filter_map(|path| path.file_name()?.to_str().map(str::to_owned))
60 .collect();
61
62 names.sort();
63
64 names
65}
66
67fn collect_entries(
69 workspace: &WorkspaceIndex,
70 root: &Path,
71 lines: &mut Vec<String>,
72 depth: usize,
73 max_depth: usize,
74) {
75 if depth > max_depth {
76 return;
77 }
78
79 let mut dirs = Vec::new();
80 let mut files = Vec::new();
81
82 for path in workspace.child_dirs(root) {
83 let Some(os_name) = path.file_name() else {
84 continue;
85 };
86 let name = os_name.to_string_lossy();
87
88 dirs.push((name.into_owned(), path.to_path_buf()));
89 }
90
91 for path in workspace.files_in(root) {
92 let Some(os_name) = path.file_name() else {
93 continue;
94 };
95
96 files.push((os_name.to_string_lossy().into_owned(), path.to_path_buf()));
97 }
98
99 dirs.sort_by(|a, b| a.0.cmp(&b.0));
100 files.sort_by(|a, b| a.0.cmp(&b.0));
101
102 let indent = " ".repeat(depth);
103
104 for (name, path) in &dirs {
105 if is_dir_empty(workspace, path) {
106 continue;
107 }
108
109 let mut child_lines = Vec::new();
110
111 collect_entries(workspace, path, &mut child_lines, depth + 1, max_depth);
112
113 let desc = extract_file_doc(workspace, &path.join("mod.rs"))
114 .or_else(|| extract_file_doc(workspace, &path.join("lib.rs")));
115
116 match desc {
117 Some(d) => lines.push(format!("{indent}{name}/ — {d}")),
118 None => lines.push(format!("{indent}{name}/")),
119 }
120
121 lines.extend(child_lines);
122 }
123
124 for (name, path) in &files {
125 let is_index = name == "mod.rs" || name == "lib.rs";
126
127 if is_index {
128 lines.push(format!("{indent}{name}"));
129 } else {
130 match extract_file_doc(workspace, path) {
131 Some(d) => lines.push(format!("{indent}{name} — {d}")),
132 None => lines.push(format!("{indent}{name}")),
133 }
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests;