use ignore::{WalkBuilder, WalkState};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
#[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 markdown_paths = walk_markdown_files(root);
build_tree(root, markdown_paths)
}
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 walk_markdown_files(root: &Path) -> Vec<PathBuf> {
let paths: Mutex<Vec<PathBuf>> = Mutex::new(Vec::new());
WalkBuilder::new(root)
.hidden(false)
.build_parallel()
.run(|| {
let paths = &paths;
Box::new(move |result| {
let Ok(entry) = result else {
return WalkState::Continue;
};
if entry.file_type().is_some_and(|ft| ft.is_file())
&& entry
.path()
.extension()
.is_some_and(|ext| ext == "md" || ext == "markdown")
{
paths.lock().unwrap().push(entry.path().to_path_buf());
}
WalkState::Continue
})
});
paths.into_inner().unwrap_or_default()
}
fn build_tree(root: &Path, paths: Vec<PathBuf>) -> Vec<FileEntry> {
let mut root_entry = FileEntry {
path: root.to_path_buf(),
name: String::new(),
is_dir: true,
children: Vec::new(),
};
for path in paths {
let Ok(rel) = path.strip_prefix(root) else {
continue;
};
insert_path(&mut root_entry, root, rel);
}
sort_entries(&mut root_entry.children);
root_entry.children
}
fn insert_path(parent: &mut FileEntry, root: &Path, rel: &Path) {
let mut current = parent;
let mut abs = root.to_path_buf();
let components: Vec<_> = rel.components().collect();
let last = components.len().saturating_sub(1);
for (idx, comp) in components.iter().enumerate() {
let name = comp.as_os_str().to_string_lossy().to_string();
abs.push(&name);
let is_dir = idx < last;
let existing = current.children.iter().position(|child| child.name == name);
let child_idx = match existing {
Some(i) => i,
None => {
current.children.push(FileEntry {
path: abs.clone(),
name,
is_dir,
children: Vec::new(),
});
current.children.len() - 1
}
};
current = &mut current.children[child_idx];
}
}
fn sort_entries(entries: &mut Vec<FileEntry>) {
entries.sort_by(|a, b| b.is_dir.cmp(&a.is_dir).then(a.name.cmp(&b.name)));
for entry in entries {
if entry.is_dir {
sort_entries(&mut entry.children);
}
}
}