1use std::io;
2use std::path::{Path, PathBuf};
3
4use walkdir::{DirEntry, WalkDir};
5
6use crate::model::RawDoc;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct AssetFile {
12 pub rel_path: String,
14 pub src_path: PathBuf,
16}
17
18const PRUNED_DIR_NAMES: &[&str] = &["node_modules", "target", "vendor", ".git"];
25
26fn is_kept(entry: &DirEntry) -> bool {
30 if entry.depth() == 0 {
31 return true;
32 }
33 let name = entry.file_name().to_string_lossy();
34 if entry.file_type().is_dir() {
35 if name.starts_with('.') || PRUNED_DIR_NAMES.contains(&name.as_ref()) {
37 return false;
38 }
39 }
40 true
41}
42
43pub fn discover_docs(root: &Path) -> std::io::Result<Vec<RawDoc>> {
49 let mut docs = Vec::new();
50 let walker = WalkDir::new(root)
54 .sort_by_file_name()
55 .into_iter()
56 .filter_entry(is_kept);
57 for entry in walker {
58 let entry = entry.map_err(io::Error::from)?;
61 let path = entry.path();
62 if path.extension().and_then(|e| e.to_str()) != Some("md") {
63 continue;
64 }
65 let rel = path
66 .strip_prefix(root)
67 .unwrap_or(path)
68 .to_string_lossy()
69 .replace('\\', "/");
70 let raw = std::fs::read_to_string(path)?;
71 docs.push(RawDoc { rel_path: rel, raw });
72 }
73 Ok(docs)
74}
75
76pub fn discover_assets(root: &Path) -> std::io::Result<Vec<AssetFile>> {
84 let mut assets = Vec::new();
85 let walker = WalkDir::new(root)
86 .sort_by_file_name()
87 .into_iter()
88 .filter_entry(is_kept);
89 for entry in walker {
90 let entry = entry.map_err(io::Error::from)?;
91 if !entry.file_type().is_file() {
92 continue;
93 }
94 let path = entry.path();
95 if path.extension().and_then(|e| e.to_str()) == Some("md") {
97 continue;
98 }
99 if path
101 .file_name()
102 .and_then(|n| n.to_str())
103 .map(|n| n.starts_with('.'))
104 .unwrap_or(false)
105 {
106 continue;
107 }
108 let rel = path
109 .strip_prefix(root)
110 .unwrap_or(path)
111 .to_string_lossy()
112 .replace('\\', "/");
113 assets.push(AssetFile {
114 rel_path: rel,
115 src_path: path.to_path_buf(),
116 });
117 }
118 Ok(assets)
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn skips_non_md_and_returns_slash_separated_rel_paths() {
127 let dir = std::env::temp_dir().join(format!("docgen_discover_{}", std::process::id()));
128 let _ = std::fs::remove_dir_all(&dir);
129 std::fs::create_dir_all(dir.join("guide")).unwrap();
130 std::fs::write(dir.join("index.md"), "# Home\n").unwrap();
131 std::fs::write(dir.join("notes.txt"), "ignore me\n").unwrap();
132 std::fs::write(dir.join("guide/intro.md"), "# Intro\n").unwrap();
133
134 let docs = discover_docs(&dir).unwrap();
135 let rels: Vec<&str> = docs.iter().map(|d| d.rel_path.as_str()).collect();
136
137 assert_eq!(rels, vec!["guide/intro.md", "index.md"]);
139
140 let _ = std::fs::remove_dir_all(&dir);
141 }
142
143 #[test]
144 fn prunes_hidden_and_vendor_dirs() {
145 let dir = std::env::temp_dir().join(format!("docgen_prune_{}", std::process::id()));
146 let _ = std::fs::remove_dir_all(&dir);
147 std::fs::create_dir_all(dir.join(".obsidian/plugins")).unwrap();
148 std::fs::create_dir_all(dir.join("node_modules/pkg")).unwrap();
149 std::fs::create_dir_all(dir.join("guide")).unwrap();
150 std::fs::write(dir.join("index.md"), "# Home\n").unwrap();
151 std::fs::write(dir.join("guide/intro.md"), "# Intro\n").unwrap();
152 std::fs::write(dir.join(".obsidian/plugins/conf.md"), "# junk\n").unwrap();
154 std::fs::write(dir.join("node_modules/pkg/README.md"), "# dep\n").unwrap();
155
156 let docs = discover_docs(&dir).unwrap();
157 let rels: Vec<&str> = docs.iter().map(|d| d.rel_path.as_str()).collect();
158 assert_eq!(rels, vec!["guide/intro.md", "index.md"]);
159
160 let _ = std::fs::remove_dir_all(&dir);
161 }
162
163 #[test]
164 fn discover_assets_collects_non_md_preserving_tree() {
165 let dir = std::env::temp_dir().join(format!("docgen_assets_{}", std::process::id()));
166 let _ = std::fs::remove_dir_all(&dir);
167 std::fs::create_dir_all(dir.join("system/attachments")).unwrap();
168 std::fs::create_dir_all(dir.join(".obsidian")).unwrap();
169 std::fs::write(dir.join("system/index.md"), "# Sys\n").unwrap();
170 std::fs::write(dir.join("system/attachments/image.png"), b"\x89PNG").unwrap();
171 std::fs::write(dir.join("logo.svg"), "<svg/>").unwrap();
172 std::fs::write(dir.join(".DS_Store"), b"junk").unwrap();
174 std::fs::write(dir.join(".obsidian/workspace.json"), "{}").unwrap();
175
176 let assets = discover_assets(&dir).unwrap();
177 let rels: Vec<&str> = assets.iter().map(|a| a.rel_path.as_str()).collect();
178 assert_eq!(rels, vec!["logo.svg", "system/attachments/image.png"]);
179
180 let _ = std::fs::remove_dir_all(&dir);
181 }
182}