roxy_cli 0.1.2

A command-line static site generator
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());
    }
}