use crate::error::SiteError; use crate::name::{Name, NameBuilder, Names}; use crate::page::{MarkdownPage, UnprocessedPage, WikitextPage}; use crate::parser::WikitextParser; use crate::token::TokenPatterns; use crate::util::make_relative_link; use log::{debug, info, trace}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use walkdir::WalkDir; pub struct Site { builder: NameBuilder, wikitext_pages: Vec, unprocessed_pages: Vec, markdown_pages: Vec, files: Names, patterns: TokenPatterns, name_queue: Vec, page_queue: 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 { builder: NameBuilder::new(srcdir.as_ref(), destdir.as_ref()), wikitext_pages: vec![], unprocessed_pages: vec![], markdown_pages: vec![], files: Names::default(), patterns: TokenPatterns::default(), name_queue: vec![], page_queue: PageSet::default(), } } pub fn scan(&mut self) -> Result<(), SiteError> { for name in self.all_files()? { trace!("scan: name={}", name); if name.is_wikitext_page() { trace!("scan: it's a page"); self.name_queue.push(name); } else { trace!("scan: it's a non-page file"); let filename = name.source_path(); if filename.is_file() || filename.is_symlink() { self.add_other_file(name); } } } Ok(()) } fn add_wikitextpage(&mut self, page: WikitextPage) { info!("add wikitext page {}", page.meta().path().display()); self.page_queue.insert(&page); self.wikitext_pages.push(page); } fn add_other_file(&mut self, name: Name) { info!("add other file {}", name); self.files.insert(name); } pub fn process(&mut self) -> Result<(), SiteError> { trace!("processing queues"); loop { if !self.process_name()? && !self.process_wikipage()? && !self.process_unrocessed_page()? { trace!("processing queues done"); break; } } Ok(()) } fn process_name(&mut self) -> Result { if let Some(name) = self.name_queue.pop() { debug!("loading wikitext page {}", name.source_path().display()); let page = WikitextPage::read(&name)?; self.files.insert(name); self.add_wikitextpage(page); Ok(true) } else { trace!("name_queue was empty"); Ok(false) } } 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 { trace!("wikitext_ages was empty"); Ok(false) } } 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 { trace!("unprocessed_ages was empty"); Ok(false) } } pub fn markdown_pages(&self) -> &[MarkdownPage] { &self.markdown_pages } pub fn files_only(&self) -> impl Iterator { self.files.files() } pub fn pages_and_files(&self) -> impl Iterator { self.files.iter().chain(self.name_queue.iter()) } fn all_files(&self) -> Result, SiteError> { let mut names = vec![]; let root = self.builder.srcdir(); trace!("all_files: root={}", root.display()); for e in WalkDir::new(root) { let e = e.map_err(|err| SiteError::WalkDir(root.to_path_buf(), err))?; let path = e.path(); trace!("all_files: path={}", path.display()); if Self::is_excluded(path) { debug!("exclude {}", path.display()); } else { debug!("include {}", path.display()); if Self::is_markdown(path) { trace!("it's markdown"); names.push(self.builder.page(path)); } else if path.is_file() { trace!("it's not markdown"); names.push(self.builder.file(path)); } else { trace!("it's not a file"); } } } Ok(names) } 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.page_queue.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.page_queue.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.page_queue.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.files() { if filename == existing.page_path() { 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::{NameBuilder, Site, SiteError, WikitextPage}; use crate::page::MetaBuilder; use std::{ path::{Path, PathBuf}, time::SystemTime, }; fn site() -> Site { Site::new("/src", "/dest") } fn builder() -> NameBuilder { NameBuilder::new(Path::new("/src"), Path::new("/dest")) } fn page(path: &str) -> WikitextPage { let name = builder().page(Path::new(path)); let mtime = SystemTime::now(); let meta = MetaBuilder::default().name(name).mtime(mtime).build(); WikitextPage::new(meta, "".into()) } #[test] fn has_no_pages_initially() { assert_eq!(site().markdown_pages().to_vec(), vec![]); } #[test] fn absolute_link_resolves_to_link_relative_root_of_site() { let mut site = site(); site.add_wikitextpage(page("/src/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(); match site.resolve("/src/foo/bar", "yo") { Err(SiteError::PageMissing(page, target)) => { assert_eq!(target, PathBuf::from("yo")); assert_eq!(page, PathBuf::from("/src/foo/bar")); } _ => panic!("unexpected success"), } } #[test] fn link_to_sibling_resolves_to_it() { let mut site = site(); site.add_wikitextpage(page("/src/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(); site.add_wikitextpage(page("/src/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(); site.add_wikitextpage(page("/src/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(); site.add_wikitextpage(page("/src/foo/bar/yo")); site.add_wikitextpage(page("/src/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(); 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(); site.add_wikitextpage(page("/src/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(); let name = builder().file(Path::new("/src/foo/bar.jpg")); site.add_other_file(name); site.process().unwrap(); assert_eq!( site.resolve("/foo/bar", "bar.jpg").unwrap(), Path::new("bar.jpg") ); } }