use crate::util::{join_subpath, make_path_absolute, make_path_relative_to}; use log::trace; use std::cmp::Ordering; use std::ffi::OsStr; use std::fmt; use std::path::{Path, PathBuf}; use std::time::SystemTime; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Name { is_wikitext: bool, src: PathBuf, dest: PathBuf, page: PathBuf, page_name: String, mtime: SystemTime, } impl Name { fn new( is_wikitext: bool, src: PathBuf, dest: PathBuf, page: PathBuf, mtime: SystemTime, ) -> Self { trace!( "Name::new: is_wikitext={} src={} dest={} page={}", is_wikitext, src.display(), dest.display(), page.display() ); let page_name = page .file_name() .unwrap_or_else(|| OsStr::new("/")) .to_string_lossy() .to_string(); Self { is_wikitext, src, dest, page, page_name, mtime, } } pub fn is_wikitext_page(&self) -> bool { self.is_wikitext } pub fn source_path(&self) -> &Path { &self.src } pub fn destination_path(&self) -> &Path { &self.dest } pub fn page_path(&self) -> &Path { &self.page } pub fn page_name(&self) -> &str { &self.page_name } } impl Ord for Name { fn cmp(&self, other: &Self) -> Ordering { self.src.cmp(&other.src).reverse() } } impl PartialOrd for Name { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl PartialEq for &Name { fn eq(&self, other: &Name) -> bool { self.src == other.src } } impl fmt::Display for Name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { write!(f, "{}", self.src.display()) } } pub struct NameBuilder { srcdir: PathBuf, destdir: PathBuf, } impl NameBuilder { pub fn new(srcdir: &Path, destdir: &Path) -> Self { Self { srcdir: srcdir.into(), destdir: destdir.into(), } } pub fn srcdir(&self) -> &Path { &self.srcdir } pub fn destdir(&self) -> &Path { &self.destdir } pub fn page(&self, path: &Path, mtime: SystemTime) -> Name { assert!(path.starts_with(&self.srcdir)); let src = path.into(); let relative = make_path_relative_to(&self.srcdir, path); let (dest, page) = if path.ends_with("index.mdwn") { let dest = join_subpath(&self.destdir, &relative.with_extension("html")); let dirname = relative .parent() .unwrap_or_else(|| panic!("expected parent directory for {}", relative.display())); let page = make_path_absolute(dirname); (dest, page) } else { let dirname = relative .parent() .unwrap_or_else(|| panic!("expected a parent for {}", relative.display())); let basename = relative .file_stem() .unwrap_or_else(|| panic!("expected stem for {}", relative.display())); let page_relative = dirname.join(basename); let dest = join_subpath(&self.destdir, &page_relative.join("index.html")); let page = make_path_absolute(&page_relative); (dest, page) }; Name::new(true, src, dest, page, mtime) } pub fn file(&self, path: &Path, mtime: SystemTime) -> Name { assert!(path.starts_with(&self.srcdir)); let src = path.into(); let relative = make_path_relative_to(&self.srcdir, path); let page = make_path_absolute(&relative); let dest = join_subpath(&self.destdir, &relative); Name::new(false, src, dest, page, mtime) } } #[derive(Default, Debug)] pub struct Names { names: Vec, } impl Names { pub fn insert(&mut self, name: Name) { self.names.push(name); } pub fn iter(&self) -> impl Iterator { self.names.iter() } pub fn source_paths(&self) -> impl Iterator { self.names.iter().map(|name| &name.src) } pub fn pages(&self) -> impl Iterator { self.names.iter().filter(|name| name.is_wikitext_page()) } pub fn files(&self) -> impl Iterator { self.names.iter().filter(|name| !name.is_wikitext_page()) } pub fn get_source_path(&self, path: &Path) -> Option<&Name> { self.names.iter().find(|name| name.src == path) } pub fn get_page_path(&self, path: &Path) -> Option<&Name> { self.names.iter().find(|name| name.page == path) } } #[cfg(test)] mod test { use super::{Name, NameBuilder, Names}; use std::{path::Path, time::UNIX_EPOCH}; fn builder() -> NameBuilder { NameBuilder::new(Path::new("/src"), Path::new("/dest")) } #[test] fn builds_page_name() { let name = builder().page(Path::new("/src/foo/bar.mdwn"), UNIX_EPOCH); assert_eq!(name.source_path(), Path::new("/src/foo/bar.mdwn")); assert_eq!( name.destination_path(), Path::new("/dest/foo/bar/index.html") ); assert_eq!(name.page_path(), Path::new("/foo/bar")); assert_eq!(name.page_name(), "bar"); } #[test] fn builds_page_name_for_index_mdwn() { let name = builder().page(Path::new("/src/foo/index.mdwn"), UNIX_EPOCH); assert_eq!(name.source_path(), Path::new("/src/foo/index.mdwn")); assert_eq!(name.destination_path(), Path::new("/dest/foo/index.html")); assert_eq!(name.page_path(), Path::new("/foo")); assert_eq!(name.page_name(), "foo"); } #[test] fn builds_file_name() { let name = builder().file(Path::new("/src/foo/bar.jpg"), UNIX_EPOCH); assert_eq!(name.source_path(), Path::new("/src/foo/bar.jpg")); assert_eq!(name.destination_path(), Path::new("/dest/foo/bar.jpg")); assert_eq!(name.page_path(), Path::new("/foo/bar.jpg")); assert_eq!(name.page_name(), "bar.jpg"); } #[test] fn names_is_empty_by_default() { let names = Names::default(); assert!(names.names.is_empty()); } #[test] fn names_remembers_inserted() { let mut names = Names::default(); let name = builder().page(Path::new("/src/foo/bar.mdwn"), UNIX_EPOCH); names.insert(name.clone()); assert_eq!( names.get_source_path(Path::new("/src/foo/bar.mdwn")), Some(&name) ); assert_eq!(names.get_page_path(Path::new("/foo/bar")), Some(&name)); } #[test] fn names_remembers_inserted_pages_and_files() { let mut names = Names::default(); let page = builder().page(Path::new("/src/foo/bar.mdwn"), UNIX_EPOCH); let file = builder().file(Path::new("/src/foo/bar.jpg"), UNIX_EPOCH); names.insert(page.clone()); names.insert(file.clone()); let pages: Vec<&Name> = names.pages().collect(); let files: Vec<&Name> = names.files().collect(); assert_eq!(pages, vec![page],); assert_eq!(files, vec![file],); } }