use slugify::slugify;
use std::{
ffi::OsStr,
path::{Path, PathBuf, StripPrefixError},
};
#[derive(Debug, Clone)]
pub(crate) struct FilePath<'a, P: AsRef<Path>> {
pub input: PathBuf,
pub root_dir: PathBuf,
pub output: &'a P,
pub slug_word_limit: usize,
}
impl<'a, P: AsRef<Path> + 'a> FilePath<'a, P> {
pub fn new(input: &'a P, output: &'a P) -> Self {
Self {
input: Self::make_recursive(input),
root_dir: Self::strip_wildcards(Self::strip_dot(&input)),
output,
slug_word_limit: Default::default(),
}
}
fn make_recursive(path: &'a P) -> PathBuf {
path.as_ref().join("**/*")
}
fn has_no_wildcard<S: AsRef<str>>(path: &S) -> bool {
!path.as_ref().contains("*")
}
fn strip_dot(path: &P) -> &Path {
path.as_ref().strip_prefix("./").unwrap_or(path.as_ref())
}
fn strip_wildcards<P2: AsRef<Path> + ?Sized>(path: &'a P2) -> PathBuf {
path.as_ref()
.ancestors()
.map(Path::to_str)
.flatten()
.find(Self::has_no_wildcard)
.map_or_else(|| PathBuf::new(), PathBuf::from)
}
pub fn as_slug<P2: AsRef<Path> + ?Sized>(&self, path: &P2) -> Option<PathBuf> {
let path = path.as_ref();
let ext = path.extension();
let file_name: Option<PathBuf> = path
.with_extension("")
.file_name()
.or_else(|| Some(OsStr::new("")))
.and_then(OsStr::to_str)
.map(|name| slugify!(name, separator = "-"))
.map(|f| {
f.split_terminator('-')
.take(self.slug_word_limit)
.collect::<Vec<&str>>()
.join("-")
})
.map(PathBuf::from)
.map(|f| f.with_extension(ext.unwrap_or_default()));
match (path.parent(), file_name) {
(Some(parent), Some(name)) => Some(parent.join(name)),
(None, Some(name)) => Some(PathBuf::from(name)),
(Some(parent), None) => Some(parent.to_path_buf()),
_ => None,
}
}
pub fn to_output<P2: AsRef<Path>>(&self, value: &'a P2) -> Option<PathBuf> {
let value = value.as_ref();
let path = value.strip_prefix(&self.root_dir).unwrap_or_else(|_| value);
self.as_slug(path)
.map(|path| self.output.as_ref().join(path))
}
pub fn strip_root<P2: AsRef<Path>>(&self, value: &'a P2) -> Result<&Path, StripPrefixError> {
value.as_ref().strip_prefix(&self.root_dir)
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::FilePath;
#[test]
fn relative_paths() {
let relative = FilePath::new(&"./in", &"./out");
let bare = FilePath::new(&"in", &"out");
let path = "in/nested/deeply/test.md" ;
let expected = Path::new("nested/deeply/test.md");
assert_eq!(expected, relative.strip_root(&path).unwrap());
assert_eq!(expected, bare.strip_root(&path).unwrap());
}
}