@@ -12,6 +12,8 @@ accessible everywhere | |||||
- Add `total_pages` to paginator | - Add `total_pages` to paginator | ||||
- Do not prepend URL prefix to links that start with a scheme | - 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 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) | ## 0.9.0 (2019-09-28) | ||||
@@ -63,6 +63,18 @@ impl Error { | |||||
pub fn chain(value: impl ToString, source: impl Into<Box<dyn StdError>>) -> Self { | pub fn chain(value: impl ToString, source: impl Into<Box<dyn StdError>>) -> Self { | ||||
Self { kind: ErrorKind::Msg(value.to_string()), source: Some(source.into()) } | 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<String>)>) -> 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 { | impl From<&str> for Error { | ||||
@@ -263,7 +263,10 @@ mod tests { | |||||
&Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.md"), | &Path::new("/home/vincent/code/site/content/posts/tutorials/python/index.md"), | ||||
&PathBuf::new(), | &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 | /// Regression test for https://github.com/getzola/zola/issues/854 | ||||
@@ -277,6 +280,9 @@ mod tests { | |||||
); | ); | ||||
let res = file.find_language(&config); | let res = file.find_language(&config); | ||||
assert!(res.is_ok()); | 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") | |||||
); | |||||
} | } | ||||
} | } |
@@ -121,6 +121,7 @@ impl Section { | |||||
} else { | } else { | ||||
section.path = format!("{}/", path); | section.path = format!("{}/", path); | ||||
} | } | ||||
section.components = section | section.components = section | ||||
.path | .path | ||||
.split('/') | .split('/') | ||||
@@ -131,7 +132,7 @@ impl Section { | |||||
Ok(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<P: AsRef<Path>>( | pub fn from_file<P: AsRef<Path>>( | ||||
path: P, | path: P, | ||||
config: &Config, | config: &Config, | ||||
@@ -9,6 +9,19 @@ use config::Config; | |||||
use content::{Page, Section}; | use content::{Page, Section}; | ||||
use sorting::{find_siblings, sort_pages_by_date, sort_pages_by_weight}; | 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 | /// Houses everything about pages and sections | ||||
/// Think of it as a database where each page and section has an id (Key here) | /// 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 | /// that can be used to find the actual value | ||||
@@ -398,4 +411,128 @@ impl Library { | |||||
pub fn contains_page<P: AsRef<Path>>(&self, path: P) -> bool { | pub fn contains_page<P: AsRef<Path>>(&self, path: P) -> bool { | ||||
self.paths_to_pages.contains_key(path.as_ref()) | 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<String>)> { | |||||
let mut paths: HashMap<&str, HashSet<DefaultKey>> = 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<String> = 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)); | |||||
} | |||||
} | } |
@@ -253,6 +253,14 @@ impl Site { | |||||
self.add_page(p, false)?; | 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` | // taxonomy Tera fns are loaded in `register_early_global_fns` | ||||
// so we do need to populate it first. | // so we do need to populate it first. | ||||
self.populate_taxonomies()?; | self.populate_taxonomies()?; | ||||
@@ -465,6 +473,7 @@ impl Site { | |||||
index_path.file_name().unwrap().to_string_lossy().to_string(); | index_path.file_name().unwrap().to_string_lossy().to_string(); | ||||
if let Some(ref l) = lang { | if let Some(ref l) = lang { | ||||
index_section.file.name = format!("_index.{}", l); | index_section.file.name = format!("_index.{}", l); | ||||
index_section.path = format!("{}/", l); | |||||
index_section.permalink = self.config.make_permalink(l); | index_section.permalink = self.config.make_permalink(l); | ||||
let filename = format!("_index.{}.md", l); | let filename = format!("_index.{}.md", l); | ||||
index_section.file.path = self.content_path.join(&filename); | index_section.file.path = self.content_path.join(&filename); | ||||