use std::path::{Path, PathBuf};
use rayon::prelude::*;
#[derive(Debug, Clone)]
pub struct WalkEntry {
pub components: Vec<String>,
pub abs: PathBuf,
pub display: String,
}
impl WalkEntry {
pub fn rel(&self) -> String {
self.components.join("/")
}
}
pub fn find_markdown_files(directory: &str, recursive: bool) -> Vec<WalkEntry> {
let root = Path::new(directory);
let mut found: Vec<(Vec<String>, PathBuf)> = if recursive {
collect_root_parallel(root)
} else {
let mut found = Vec::new();
collect(root, &mut Vec::new(), false, &mut found);
found
};
found.sort_by(|a, b| a.0.cmp(&b.0));
let prefix = normalize_root(directory);
found
.into_iter()
.map(|(components, abs)| {
let display = join_display(&prefix, &components);
WalkEntry {
components,
abs,
display,
}
})
.collect()
}
fn collect_root_parallel(root: &Path) -> Vec<(Vec<String>, PathBuf)> {
let entries = match std::fs::read_dir(root) {
Ok(entries) => entries,
Err(_) => return Vec::new(),
};
let roots: Vec<(String, PathBuf, bool, bool)> = entries
.flatten()
.filter_map(|entry| {
let name = entry.file_name().into_string().ok()?;
if name.starts_with('.') {
return None;
}
let file_type = entry.file_type().ok();
Some((
name,
entry.path(),
file_type.as_ref().is_some_and(|kind| kind.is_dir()),
file_type.as_ref().is_some_and(|kind| kind.is_symlink()),
))
})
.collect();
roots
.into_par_iter()
.map(|(name, path, is_dir, is_symlink)| {
let mut local = Vec::new();
if name.ends_with(".md") {
local.push((vec![name.clone()], path.clone()));
}
if is_dir && !is_symlink {
let mut rel = vec![name];
collect(&path, &mut rel, true, &mut local);
}
local
})
.flatten()
.collect()
}
fn collect(
dir: &Path,
rel: &mut Vec<String>,
recursive: bool,
out: &mut Vec<(Vec<String>, PathBuf)>,
) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let name = match entry.file_name().into_string() {
Ok(n) => n,
Err(_) => continue, };
if name.starts_with('.') {
continue; }
let ft = entry.file_type();
let is_symlink = ft.as_ref().map(|t| t.is_symlink()).unwrap_or(false);
let is_dir = ft.as_ref().map(|t| t.is_dir()).unwrap_or(false);
if name.ends_with(".md") {
rel.push(name.clone());
out.push((rel.clone(), entry.path()));
rel.pop();
}
if recursive && is_dir && !is_symlink {
rel.push(name);
collect(&entry.path(), rel, recursive, out);
rel.pop();
}
}
}
fn join_display(prefix: &str, components: &[String]) -> String {
if prefix.is_empty() || prefix == "." {
components.join("/")
} else if prefix == "/" {
format!("/{}", components.join("/"))
} else if prefix.ends_with('/') {
format!("{}{}", prefix, components.join("/"))
} else {
format!("{}/{}", prefix, components.join("/"))
}
}
pub fn normalize_root(directory: &str) -> String {
let leading = directory.chars().take_while(|&c| c == '/').count();
let root_prefix = match leading {
0 => "",
2 => "//",
_ => "/",
};
let parts: Vec<&str> = directory
.split('/')
.filter(|p| !p.is_empty() && *p != ".")
.collect();
if root_prefix.is_empty() {
if parts.is_empty() {
".".to_string()
} else {
parts.join("/")
}
} else if root_prefix == "//" {
format!("//{}", parts.join("/"))
} else {
format!("/{}", parts.join("/"))
}
}
pub fn py_join(root: &str, components: &[&str]) -> String {
let owned: Vec<String> = components.iter().map(|c| (*c).to_string()).collect();
join_display(&normalize_root(root), &owned)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WalkTarget {
File(PathBuf),
Directory(PathBuf),
Missing(PathBuf),
}
pub fn dispatch(path: &str) -> WalkTarget {
let p = PathBuf::from(path);
if p.is_file() {
WalkTarget::File(p)
} else if p.is_dir() {
WalkTarget::Directory(p)
} else {
WalkTarget::Missing(p)
}
}
pub fn is_directory(path: &str) -> bool {
Path::new(path).is_dir()
}