use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct FileEntry {
pub path: PathBuf,
pub name: String,
pub is_dir: bool,
pub children: Vec<FileEntry>,
}
impl FileEntry {
pub fn discover(root: &Path) -> Vec<FileEntry> {
let mut entries = Vec::new();
collect_entries(root, &mut entries);
entries.sort_by(|a, b| {
b.is_dir.cmp(&a.is_dir).then(a.name.cmp(&b.name))
});
entries
}
pub fn flat_paths(entries: &[FileEntry]) -> Vec<PathBuf> {
let mut paths = Vec::new();
collect_flat_paths(entries, &mut paths);
paths
}
}
fn collect_flat_paths(entries: &[FileEntry], out: &mut Vec<PathBuf>) {
for entry in entries {
if !entry.is_dir {
out.push(entry.path.clone());
}
collect_flat_paths(&entry.children, out);
}
}
fn collect_entries(dir: &Path, entries: &mut Vec<FileEntry>) {
let walker = ignore::WalkBuilder::new(dir)
.max_depth(Some(1))
.hidden(false)
.sort_by_file_name(|a, b| a.cmp(b))
.build();
for result in walker {
let Ok(entry) = result else { continue };
let path = entry.path().to_path_buf();
if path == dir {
continue;
}
let name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if entry.file_type().is_some_and(|ft| ft.is_dir()) {
let mut children = Vec::new();
collect_entries(&path, &mut children);
children.sort_by(|a, b| b.is_dir.cmp(&a.is_dir).then(a.name.cmp(&b.name)));
if has_markdown_files(&children) {
entries.push(FileEntry {
path,
name,
is_dir: true,
children,
});
}
} else if path
.extension()
.is_some_and(|ext| ext == "md" || ext == "markdown")
{
entries.push(FileEntry {
path,
name,
is_dir: false,
children: Vec::new(),
});
}
}
}
fn has_markdown_files(entries: &[FileEntry]) -> bool {
entries.iter().any(|e| {
if e.is_dir {
has_markdown_files(&e.children)
} else {
true
}
})
}