diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f1d5d4..64fe14c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ accessible everywhere - Add `total_pages` to paginator - Do not prepend URL prefix to links that start with a scheme - Allow skipping anchor checking in `zola check` for some URL prefixes +- Allow skipping prefixes in `zola check` +- Check for path collisions when building the site ## 0.9.0 (2019-09-28) diff --git a/components/errors/src/lib.rs b/components/errors/src/lib.rs index 3d806cb..265d0b8 100755 --- a/components/errors/src/lib.rs +++ b/components/errors/src/lib.rs @@ -63,6 +63,18 @@ impl Error { pub fn chain(value: impl ToString, source: impl Into>) -> Self { Self { kind: ErrorKind::Msg(value.to_string()), source: Some(source.into()) } } + + /// Create an error from a list of path collisions, formatting the output + pub fn from_collisions(collisions: Vec<(&str, Vec)>) -> Self { + let mut msg = String::from("Found path collisions:\n"); + + for (path, filepaths) in collisions { + let row = format!("- `{}` from files {:?}\n", path, filepaths); + msg.push_str(&row); + } + + Self { kind: ErrorKind::Msg(msg), source: None } + } } impl From<&str> for Error { diff --git a/components/library/src/content/file_info.rs b/components/library/src/content/file_info.rs index 4440ff2..8664f95 100644 --- a/components/library/src/content/file_info.rs +++ b/components/library/src/content/file_info.rs @@ -263,7 +263,10 @@ mod tests { &Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.md"), &PathBuf::new(), ); - assert_eq!(file.canonical, Path::new("/home/vincent/code/site/content/posts/tutorials/python/index")); + assert_eq!( + file.canonical, + Path::new("/home/vincent/code/site/content/posts/tutorials/python/index") + ); } /// Regression test for https://github.com/getzola/zola/issues/854 @@ -277,6 +280,9 @@ mod tests { ); let res = file.find_language(&config); assert!(res.is_ok()); - assert_eq!(file.canonical, Path::new("/home/vincent/code/site/content/posts/tutorials/python/index")); + assert_eq!( + file.canonical, + Path::new("/home/vincent/code/site/content/posts/tutorials/python/index") + ); } } diff --git a/components/library/src/content/section.rs b/components/library/src/content/section.rs index 380f5cd..786f24c 100644 --- a/components/library/src/content/section.rs +++ b/components/library/src/content/section.rs @@ -121,6 +121,7 @@ impl Section { } else { section.path = format!("{}/", path); } + section.components = section .path .split('/') @@ -131,7 +132,7 @@ impl Section { Ok(section) } - /// Read and parse a .md file into a Page struct + /// Read and parse a .md file into a Section struct pub fn from_file>( path: P, config: &Config, diff --git a/components/library/src/library.rs b/components/library/src/library.rs index 556390b..37c05c8 100644 --- a/components/library/src/library.rs +++ b/components/library/src/library.rs @@ -9,6 +9,19 @@ use config::Config; use content::{Page, Section}; use sorting::{find_siblings, sort_pages_by_date, sort_pages_by_weight}; +// Like vec! but for HashSet +macro_rules! set { + ( $( $x:expr ),* ) => { + { + let mut s = HashSet::new(); + $( + s.insert($x); + )* + s + } + }; +} + /// Houses everything about pages and sections /// Think of it as a database where each page and section has an id (Key here) /// that can be used to find the actual value @@ -398,4 +411,128 @@ impl Library { pub fn contains_page>(&self, path: P) -> bool { self.paths_to_pages.contains_key(path.as_ref()) } + + /// This will check every section/page paths + the aliases and ensure none of them + /// are colliding. + /// Returns (path colliding, [list of files causing that collision]) + pub fn check_for_path_collisions(&self) -> Vec<(&str, Vec)> { + let mut paths: HashMap<&str, HashSet> = HashMap::new(); + + for (key, page) in &self.pages { + paths + .entry(&page.path) + .and_modify(|s| { + s.insert(key); + }) + .or_insert_with(|| set!(key)); + + for alias in &page.meta.aliases { + paths + .entry(&alias) + .and_modify(|s| { + s.insert(key); + }) + .or_insert_with(|| set!(key)); + } + } + + for (key, section) in &self.sections { + if !section.meta.render { + continue; + } + paths + .entry(§ion.path) + .and_modify(|s| { + s.insert(key); + }) + .or_insert_with(|| set!(key)); + } + + let mut collisions = vec![]; + for (p, keys) in paths { + if keys.len() > 1 { + let file_paths: Vec = keys + .iter() + .map(|k| { + self.pages.get(*k).map(|p| p.file.relative.clone()).unwrap_or_else(|| { + self.sections.get(*k).map(|s| s.file.relative.clone()).unwrap() + }) + }) + .collect(); + + collisions.push((p, file_paths)); + } + } + + collisions + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_find_no_collisions() { + let mut library = Library::new(10, 10, false); + let mut page = Page::default(); + page.path = "hello".to_string(); + let mut page2 = Page::default(); + page2.path = "hello-world".to_string(); + let mut section = Section::default(); + section.path = "blog".to_string(); + library.insert_page(page); + library.insert_page(page2); + library.insert_section(section); + + let collisions = library.check_for_path_collisions(); + assert_eq!(collisions.len(), 0); + } + + #[test] + fn can_find_collisions_between_pages() { + let mut library = Library::new(10, 10, false); + let mut page = Page::default(); + page.path = "hello".to_string(); + page.file.relative = "hello".to_string(); + let mut page2 = Page::default(); + page2.path = "hello".to_string(); + page2.file.relative = "hello-world".to_string(); + let mut section = Section::default(); + section.path = "blog".to_string(); + section.file.relative = "hello-world".to_string(); + library.insert_page(page.clone()); + library.insert_page(page2.clone()); + library.insert_section(section); + + let collisions = library.check_for_path_collisions(); + assert_eq!(collisions.len(), 1); + assert_eq!(collisions[0].0, page.path); + assert!(collisions[0].1.contains(&page.file.relative)); + assert!(collisions[0].1.contains(&page2.file.relative)); + } + + #[test] + fn can_find_collisions_with_an_alias() { + let mut library = Library::new(10, 10, false); + let mut page = Page::default(); + page.path = "hello".to_string(); + page.file.relative = "hello".to_string(); + let mut page2 = Page::default(); + page2.path = "hello-world".to_string(); + page2.file.relative = "hello-world".to_string(); + page2.meta.aliases = vec!["hello".to_string()]; + let mut section = Section::default(); + section.path = "blog".to_string(); + section.file.relative = "hello-world".to_string(); + library.insert_page(page.clone()); + library.insert_page(page2.clone()); + library.insert_section(section); + + let collisions = library.check_for_path_collisions(); + assert_eq!(collisions.len(), 1); + assert_eq!(collisions[0].0, page.path); + assert!(collisions[0].1.contains(&page.file.relative)); + assert!(collisions[0].1.contains(&page2.file.relative)); + } } diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index be71ed4..b5694bf 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -253,6 +253,14 @@ impl Site { self.add_page(p, false)?; } + { + let library = self.library.read().unwrap(); + let collisions = library.check_for_path_collisions(); + if !collisions.is_empty() { + return Err(Error::from_collisions(collisions)); + } + } + // taxonomy Tera fns are loaded in `register_early_global_fns` // so we do need to populate it first. self.populate_taxonomies()?; @@ -465,6 +473,7 @@ impl Site { index_path.file_name().unwrap().to_string_lossy().to_string(); if let Some(ref l) = lang { index_section.file.name = format!("_index.{}", l); + index_section.path = format!("{}/", l); index_section.permalink = self.config.make_permalink(l); let filename = format!("_index.{}.md", l); index_section.file.path = self.content_path.join(&filename);