use crate::error::SiteError; use crate::page::{MarkdownPage, UnprocessedPage, WikitextPage}; use crate::parser::WikitextParser; use crate::token::TokenPatterns; use crate::util::{join_subpath, make_path_absolute, make_path_relative_to, make_relative_link}; use log::{debug, info, trace}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use walkdir::WalkDir; pub struct Site { wikitext_pages: Vec, unprocessed_pages: Vec, markdown_pages: Vec, files: Vec, included_files: Vec, patterns: TokenPatterns, srcdir: PathBuf, destdir: PathBuf, pages: PageSet, } impl Site { const EXCLUDE_SUBSTRINGS: &'static [&'static str] = &[".git"]; const EXCLUDE_ENDS: &'static [&'static str] = &[".git", "~", "#"]; pub fn new

(srcdir: P, destdir: P) -> Self where P: AsRef, { Self { wikitext_pages: vec![], unprocessed_pages: vec![], markdown_pages: vec![], files: vec![], included_files: vec![], patterns: TokenPatterns::default(), srcdir: srcdir.as_ref().into(), destdir: destdir.as_ref().into(), pages: PageSet::default(), } } pub fn scan(&mut self) -> Result<(), SiteError> { for filename in Self::all_files(&self.srcdir)? { self.included_files .push(make_path_relative_to(&self.srcdir, &filename)); if Self::is_markdown(&filename) { let page = WikitextPage::read(&self.srcdir, &filename)?; self.add_wikitextpage(page); } else if filename.is_file() || filename.is_symlink() { self.add_other_file(filename); } } Ok(()) } pub fn add_wikitextpage(&mut self, page: WikitextPage) { info!("add wikitext page {}", page.meta().path().display()); self.pages.insert(&page); self.wikitext_pages.push(page); } pub fn add_other_file(&mut self, filename: PathBuf) { info!("add other file {}", filename.display()); let filename = make_path_relative_to(&self.srcdir, &filename); let filename = make_path_absolute(&filename); self.files.push(filename); } pub fn process(&mut self) -> Result<(), SiteError> { loop { if !self.process_wikipage()? && !self.process_unrocessed_page()? { break; } } Ok(()) } pub fn process_wikipage(&mut self) -> Result { if let Some(page) = self.wikitext_pages.pop() { debug!("processing wikitext page {}", page.meta().path().display()); let mut parser = WikitextParser::new(page.wikitext(), &self.patterns); let page = UnprocessedPage::new(page.meta().clone(), &mut parser)?; self.unprocessed_pages.push(page); Ok(true) } else { Ok(false) } } pub fn process_unrocessed_page(&mut self) -> Result { if let Some(page) = self.unprocessed_pages.pop() { debug!( "processing unprocessed page {}", page.meta().path().display() ); let page = page.process(self)?; self.markdown_pages.push(page); Ok(true) } else { Ok(false) } } pub fn markdown_pages(&self) -> &[MarkdownPage] { &self.markdown_pages } pub fn files(&self) -> &[PathBuf] { &self.files } pub fn included_files(&self) -> &[PathBuf] { &self.included_files } pub fn input_filename(&self, filename: &Path) -> Result { Ok(join_subpath(&self.srcdir, filename)) } pub fn output_filename(&self, filename: &Path) -> Result { Ok(join_subpath(&self.destdir, filename)) } fn all_files(root: &Path) -> Result, SiteError> { let mut files = vec![]; for e in WalkDir::new(root) { let e = e.map_err(|err| SiteError::WalkDir(root.to_path_buf(), err))?; let path = e.path(); if Self::is_excluded(path) { debug!("exclude {}", path.display()); } else { debug!("include {}", path.display()); files.push(path.to_path_buf()); } } Ok(files) } fn is_excluded(path: &Path) -> bool { let path = path.to_string_lossy(); for pat in Self::EXCLUDE_ENDS { if path.ends_with(pat) { return true; } } for pat in Self::EXCLUDE_SUBSTRINGS { if path.contains(pat) { return true; } } false } fn is_markdown(path: &Path) -> bool { if let Some(ext) = path.extension() { ext == "mdwn" } else { false } } pub fn resolve>(&self, page: P, target: P) -> Result { let page = page.as_ref(); let target = target.as_ref(); let resolved = self.resolve_helper(page, target)?; assert!(page.is_absolute()); assert!(resolved.is_absolute()); let resolved = make_relative_link(page, &resolved); trace!( "resolve: page={}, target={} -> {}", page.display(), target.display(), resolved.display() ); Ok(resolved) } fn resolve_helper(&self, page: &Path, target: &Path) -> Result { trace!("page={} target={}", page.display(), target.display()); // Is target absolute? if target.starts_with("/") { if let Some(path) = self.pages.get(target) { trace!("absolute target exists"); return Ok(path.into()); } else { trace!("absolute target does not exist"); return Err(SiteError::PageMissing(page.into(), target.into())); } } // Does a sub-page exist? let path = page.join(target); trace!("checking for subpage {}", path.display()); if let Some(path) = self.pages.get(&path) { trace!("subpage exists: {}", path.display()); return Ok(path.into()); } // Does a sibling page or file exist? if let Some(parent) = page.parent() { let path = parent.join(target); trace!( "checking for sibling in {}: {}", parent.display(), path.display() ); if let Some(path) = self.pages.get(path.as_path()) { trace!("sibling page exists: {}", path.display()); return Ok(path.into()); } trace!("consider files: {:?}", self.files); if self.file_exists(&path) { trace!("sibling file exists: {}", path.display()); return Ok(path); } } // Target doesn't exist. trace!("target does not exist: {}", target.display()); Err(SiteError::PageMissing(page.into(), target.into())) } fn file_exists(&self, filename: &Path) -> bool { for existing in self.files.iter() { if filename == existing { return true; } } false } } #[derive(Default)] struct PageSet { map: HashMap, } impl PageSet { fn insert(&mut self, page: &WikitextPage) { let path = page.meta().path(); let key = Self::normalize(path); self.map.insert(key, path.into()); } fn get(&self, path: &Path) -> Option<&Path> { self.map.get(&Self::normalize(path)).map(|x| x.as_ref()) } fn normalize(path: &Path) -> String { path.to_str().expect("path is UTF8").to_lowercase() } } #[cfg(test)] mod test { use super::{Site, SiteError, WikitextPage}; use crate::page::MetaBuilder; use std::{ path::{Path, PathBuf}, time::SystemTime, }; fn page(path: &str) -> WikitextPage { let mtime = SystemTime::now(); let meta = MetaBuilder::default() .path(PathBuf::from(path)) .mtime(mtime) .build(); WikitextPage::new(meta, "".into()) } #[test] fn has_no_pages_initially() { let site = Site::new(".", "."); assert_eq!(site.markdown_pages().to_vec(), vec![]); } #[test] fn absolute_link_resolves_to_link_relative_root_of_site() { let mut site = Site::new(".", "."); site.add_wikitextpage(page("/yo/yoyo")); assert_eq!( site.resolve("/foo/bar", "/yo/yoyo").unwrap(), Path::new("../yo/yoyo") ); } #[test] fn link_to_missing_is_an_error() { let site = Site::new(".", "."); match site.resolve("/foo/bar", "yo") { Err(SiteError::PageMissing(page, target)) => { assert_eq!(target, PathBuf::from("yo")); assert_eq!(page, PathBuf::from("/foo/bar")); } _ => panic!("unexpected success"), } } #[test] fn link_to_sibling_resolves_to_it() { let mut site = Site::new(".", "."); site.add_wikitextpage(page("/foo/yo")); site.process().unwrap(); assert_eq!(site.resolve("/foo/bar", "yo").unwrap(), Path::new("yo")); } #[test] fn link_using_other_casing_is_resolved() { let mut site = Site::new(".", "."); site.add_wikitextpage(page("/foo/yo")); site.process().unwrap(); assert_eq!(site.resolve("/foo/bar", "YO").unwrap(), Path::new("yo")); } #[test] fn link_to_sublpage_resolves_to_it() { let mut site = Site::new(".", "."); site.add_wikitextpage(page("/foo/bar/yo")); site.process().unwrap(); assert_eq!(site.resolve("/foo/bar", "yo").unwrap(), Path::new("bar/yo")); } #[test] fn link_to_sublpage_resolves_to_it_and_not_sibling() { let mut site = Site::new(".", "."); site.add_wikitextpage(page("/foo/bar/yo")); site.add_wikitextpage(page("/foo/yo")); site.process().unwrap(); assert_eq!(site.resolve("/foo/bar", "yo").unwrap(), Path::new("bar/yo")); } #[test] fn link_to_unrelated_subpage_is_an_error() { let mut site = Site::new(".", "."); site.process().unwrap(); match site.resolve("/foo/bar", "yo/yoyo") { Err(SiteError::PageMissing(page, target)) => { assert_eq!(target, PathBuf::from("yo/yoyo")); assert_eq!(page, PathBuf::from("/foo/bar")); } _ => panic!("unexpected success"), } } #[test] fn link_to_subsubpage_resolves_to_it() { let mut site = Site::new(".", "."); site.add_wikitextpage(page("/foo/bar/yo/yoyo")); site.process().unwrap(); assert_eq!( site.resolve("/foo/bar", "yo/yoyo").unwrap(), Path::new("bar/yo/yoyo") ); } #[test] fn link_to_sibling_file_resolves_to_it() { let mut site = Site::new("/src", "/dest"); site.add_other_file(PathBuf::from("/src/foo/bar.jpg")); site.process().unwrap(); assert_eq!( site.resolve("/foo/bar", "bar.jpg").unwrap(), Path::new("bar.jpg") ); } }