From bad0127c9f29893a6c6a0adb89655122d06d5ef1 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 2 May 2022 16:51:46 +0200 Subject: [PATCH] Build taxonomies as pages are added (#1841) --- components/config/src/config/languages.rs | 1 - components/config/src/config/mod.rs | 20 +- components/config/src/config/taxonomies.rs | 3 + components/content/src/library.rs | 231 ++++++++++++++++-- components/content/src/pagination.rs | 2 +- components/content/src/taxonomies.rs | 223 +---------------- components/site/src/lib.rs | 16 +- components/site/tests/site.rs | 41 +++- .../templates/src/global_fns/content.rs | 5 +- 9 files changed, 292 insertions(+), 250 deletions(-) diff --git a/components/config/src/config/languages.rs b/components/config/src/config/languages.rs index fa1b3bad..f7b33288 100644 --- a/components/config/src/config/languages.rs +++ b/components/config/src/config/languages.rs @@ -26,7 +26,6 @@ pub struct LanguageOptions { pub search: search::Search, /// A toml crate `Table` with String key representing term and value /// another `String` representing its translation. - /// /// Use `get_translation()` method for translating key into different languages. pub translations: HashMap, } diff --git a/components/config/src/config/mod.rs b/components/config/src/config/mod.rs index 81cd36ef..0553d8fd 100644 --- a/components/config/src/config/mod.rs +++ b/components/config/src/config/mod.rs @@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize}; use crate::theme::Theme; use errors::{anyhow, bail, Result}; use utils::fs::read_file; +use utils::slugs::slugify_paths; // We want a default base url for tests static DEFAULT_BASE_URL: &str = "http://a-website.com"; @@ -55,7 +56,6 @@ pub struct Config { pub feed_filename: String, /// If set, files from static/ will be hardlinked instead of copied to the output dir. pub hard_link_static: bool, - pub taxonomies: Vec, /// Whether to compile the `sass` directory and output the css files into the static folder @@ -124,6 +124,7 @@ impl Config { } config.add_default_language(); + config.slugify_taxonomies(); if !config.ignored_content.is_empty() { // Convert the file glob strings into a compiled glob set matcher. We want to do this once, @@ -149,6 +150,7 @@ impl Config { pub fn default_for_test() -> Self { let mut config = Config::default(); config.add_default_language(); + config.slugify_taxonomies(); config } @@ -168,6 +170,14 @@ impl Config { Ok(config) } + pub fn slugify_taxonomies(&mut self) { + for (_, lang_options) in self.languages.iter_mut() { + for tax_def in lang_options.taxonomies.iter_mut() { + tax_def.slug = slugify_paths(&tax_def.name, self.slugify.taxonomies); + } + } + } + /// Makes a url, taking into account that the base url might have a trailing slash pub fn make_permalink(&self, path: &str) -> String { let trailing_bit = @@ -283,6 +293,14 @@ impl Config { } } + pub fn has_taxonomy(&self, name: &str, lang: &str) -> bool { + if let Some(lang_options) = self.languages.get(lang) { + lang_options.taxonomies.iter().any(|t| t.name == name) + } else { + false + } + } + pub fn serialize(&self, lang: &str) -> SerializedConfig { let options = &self.languages[lang]; diff --git a/components/config/src/config/taxonomies.rs b/components/config/src/config/taxonomies.rs index 8932a175..6638471d 100644 --- a/components/config/src/config/taxonomies.rs +++ b/components/config/src/config/taxonomies.rs @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize}; pub struct TaxonomyConfig { /// The name used in the URL, usually the plural pub name: String, + /// The slug according to the config slugification strategy + pub slug: String, /// If this is set, the list of individual taxonomy term page will be paginated /// by this much pub paginate_by: Option, @@ -19,6 +21,7 @@ impl Default for TaxonomyConfig { fn default() -> Self { Self { name: String::new(), + slug: String::new(), paginate_by: None, paginate_path: None, render: true, diff --git a/components/content/src/library.rs b/components/content/src/library.rs index e4464670..bae36477 100644 --- a/components/content/src/library.rs +++ b/components/content/src/library.rs @@ -1,27 +1,40 @@ use std::path::{Path, PathBuf}; use config::Config; -use errors::Result; use libs::ahash::{AHashMap, AHashSet}; use crate::ser::TranslatedContent; use crate::sorting::sort_pages; -use crate::taxonomies::{find_taxonomies, Taxonomy}; +use crate::taxonomies::{Taxonomy, TaxonomyFound}; use crate::{Page, Section, SortBy}; #[derive(Debug, Default)] pub struct Library { pub pages: AHashMap, pub sections: AHashMap, - pub taxonomies: Vec, // aliases -> files, so we can easily check for conflicts pub reverse_aliases: AHashMap>, pub translations: AHashMap>, + // A mapping of {lang -> vec}>>} + taxonomies_def: AHashMap>>>, + // So we don't need to pass the Config when adding a page to know how to slugify and we only + // slugify once + taxo_name_to_slug: AHashMap, } impl Library { - pub fn new() -> Self { - Self::default() + pub fn new(config: &Config) -> Self { + let mut lib = Self::default(); + + for (lang, options) in &config.languages { + let mut taxas = AHashMap::new(); + for tax_def in &options.taxonomies { + taxas.insert(tax_def.slug.clone(), AHashMap::new()); + lib.taxo_name_to_slug.insert(tax_def.name.clone(), tax_def.slug.clone()); + } + lib.taxonomies_def.insert(lang.to_string(), taxas); + } + lib } fn insert_reverse_aliases(&mut self, file_path: &Path, entries: Vec) { @@ -60,6 +73,25 @@ impl Library { let mut entries = vec![page.path.clone()]; entries.extend(page.meta.aliases.to_vec()); self.insert_reverse_aliases(&file_path, entries); + + for (taxa_name, terms) in &page.meta.taxonomies { + for term in terms { + // Safe unwraps as we create all lang/taxa and we validated that they are correct + // before getting there + let taxa_def = self + .taxonomies_def + .get_mut(&page.lang) + .expect("lang not found") + .get_mut(&self.taxo_name_to_slug[taxa_name]) + .expect("taxa not found"); + + if !taxa_def.contains_key(term) { + taxa_def.insert(term.to_string(), Vec::new()); + } + taxa_def.get_mut(term).unwrap().push(page.file.path.clone()); + } + } + self.pages.insert(file_path, page); } @@ -71,10 +103,29 @@ impl Library { self.sections.insert(file_path, section); } - /// Separate from `populate_sections` as it's called _before_ markdown the pages/sections - pub fn populate_taxonomies(&mut self, config: &Config) -> Result<()> { - self.taxonomies = find_taxonomies(config, &self.pages)?; - Ok(()) + /// This is called _before_ rendering the markdown the pages/sections + pub fn find_taxonomies(&self, config: &Config) -> Vec { + let mut taxonomies = Vec::new(); + + for (lang, taxonomies_data) in &self.taxonomies_def { + for (taxa_slug, terms_pages) in taxonomies_data { + let taxo_config = &config.languages[lang] + .taxonomies + .iter() + .find(|t| &t.slug == taxa_slug) + .expect("taxo should exist"); + let mut taxo_found = TaxonomyFound::new(taxa_slug.to_string(), lang, taxo_config); + for (term, page_path) in terms_pages { + taxo_found + .terms + .insert(term, page_path.iter().map(|p| &self.pages[p]).collect()); + } + + taxonomies.push(Taxonomy::new(taxo_found, config)); + } + } + + taxonomies } /// Sort all sections pages according to sorting method given @@ -280,21 +331,19 @@ impl Library { pub fn find_sections_by_path(&self, paths: &[PathBuf]) -> Vec<&Section> { paths.iter().map(|p| &self.sections[p]).collect() } - - pub fn find_taxonomies(&self, config: &Config) -> Result> { - find_taxonomies(config, &self.pages) - } } #[cfg(test)] mod tests { use super::*; use crate::FileInfo; - use config::LanguageOptions; + use config::{LanguageOptions, TaxonomyConfig}; + use std::collections::HashMap; + use utils::slugs::SlugifyStrategy; #[test] fn can_find_collisions_with_paths() { - let mut library = Library::new(); + let mut library = Library::default(); let mut section = Section { path: "hello".to_owned(), ..Default::default() }; section.file.path = PathBuf::from("hello.md"); library.insert_section(section.clone()); @@ -311,7 +360,7 @@ mod tests { #[test] fn can_find_collisions_with_aliases() { - let mut library = Library::new(); + let mut library = Library::default(); let mut section = Section { path: "hello".to_owned(), ..Default::default() }; section.file.path = PathBuf::from("hello.md"); library.insert_section(section.clone()); @@ -378,7 +427,7 @@ mod tests { fn can_populate_sections() { let mut config = Config::default_for_test(); config.languages.insert("fr".to_owned(), LanguageOptions::default()); - let mut library = Library::new(); + let mut library = Library::default(); let sections = vec![ ("content/_index.md", "en", 0, false, SortBy::None), ("content/_index.fr.md", "fr", 0, false, SortBy::None), @@ -514,4 +563,152 @@ mod tests { assert!(translations[0].title.is_some()); assert!(translations[1].title.is_some()); } + + macro_rules! taxonomies { + ($config:expr, [$($page:expr),+]) => {{ + let mut library = Library::new(&$config); + $( + library.insert_page($page); + )+ + library.find_taxonomies(&$config) + }}; + } + + fn create_page_w_taxa(path: &str, lang: &str, taxo: Vec<(&str, Vec<&str>)>) -> Page { + let mut page = Page::default(); + page.file.path = PathBuf::from(path); + page.lang = lang.to_owned(); + let mut taxonomies = HashMap::new(); + for (name, terms) in taxo { + taxonomies.insert(name.to_owned(), terms.iter().map(|t| t.to_string()).collect()); + } + page.meta.taxonomies = taxonomies; + page + } + + #[test] + fn can_make_taxonomies() { + let mut config = Config::default_for_test(); + config.languages.get_mut("en").unwrap().taxonomies = vec![ + TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() }, + TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }, + TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() }, + ]; + config.slugify_taxonomies(); + + let page1 = create_page_w_taxa( + "a.md", + "en", + vec![("tags", vec!["rust", "db"]), ("categories", vec!["tutorials"])], + ); + let page2 = create_page_w_taxa( + "b.md", + "en", + vec![("tags", vec!["rust", "js"]), ("categories", vec!["others"])], + ); + let page3 = create_page_w_taxa( + "c.md", + "en", + vec![("tags", vec!["js"]), ("authors", vec!["Vincent Prouillet"])], + ); + let taxonomies = taxonomies!(config, [page1, page2, page3]); + + let tags = taxonomies.iter().find(|t| t.kind.name == "tags").unwrap(); + assert_eq!(tags.len(), 3); + assert_eq!(tags.items[0].name, "db"); + assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/db/"); + assert_eq!(tags.items[0].pages.len(), 1); + assert_eq!(tags.items[1].name, "js"); + assert_eq!(tags.items[1].permalink, "http://a-website.com/tags/js/"); + assert_eq!(tags.items[1].pages.len(), 2); + assert_eq!(tags.items[2].name, "rust"); + assert_eq!(tags.items[2].permalink, "http://a-website.com/tags/rust/"); + assert_eq!(tags.items[2].pages.len(), 2); + + let categories = taxonomies.iter().find(|t| t.kind.name == "categories").unwrap(); + assert_eq!(categories.items.len(), 2); + assert_eq!(categories.items[0].name, "others"); + assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/others/"); + assert_eq!(categories.items[0].pages.len(), 1); + + let authors = taxonomies.iter().find(|t| t.kind.name == "authors").unwrap(); + assert_eq!(authors.items.len(), 1); + assert_eq!(authors.items[0].permalink, "http://a-website.com/authors/vincent-prouillet/"); + } + + #[test] + fn can_make_multiple_language_taxonomies() { + let mut config = Config::default_for_test(); + config.slugify.taxonomies = SlugifyStrategy::Safe; + config.languages.insert("fr".to_owned(), LanguageOptions::default()); + config.languages.get_mut("en").unwrap().taxonomies = vec![ + TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() }, + TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }, + ]; + config.languages.get_mut("fr").unwrap().taxonomies = vec![ + TaxonomyConfig { name: "catégories".to_string(), ..TaxonomyConfig::default() }, + TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }, + ]; + config.slugify_taxonomies(); + + let page1 = create_page_w_taxa("a.md", "en", vec![("categories", vec!["rust"])]); + let page2 = create_page_w_taxa("b.md", "en", vec![("tags", vec!["rust"])]); + let page3 = create_page_w_taxa("c.md", "fr", vec![("catégories", vec!["rust"])]); + let taxonomies = taxonomies!(config, [page1, page2, page3]); + + let categories = taxonomies.iter().find(|t| t.kind.name == "categories").unwrap(); + assert_eq!(categories.len(), 1); + assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/rust/"); + let tags = taxonomies.iter().find(|t| t.kind.name == "tags" && t.lang == "en").unwrap(); + assert_eq!(tags.len(), 1); + assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/rust/"); + let fr_categories = taxonomies.iter().find(|t| t.kind.name == "catégories").unwrap(); + assert_eq!(fr_categories.len(), 1); + assert_eq!(fr_categories.items[0].permalink, "http://a-website.com/fr/catégories/rust/"); + } + + #[test] + fn taxonomies_with_unic_are_grouped_with_default_slugify_strategy() { + let mut config = Config::default_for_test(); + config.languages.get_mut("en").unwrap().taxonomies = vec![ + TaxonomyConfig { name: "test-taxonomy".to_string(), ..TaxonomyConfig::default() }, + TaxonomyConfig { name: "test taxonomy".to_string(), ..TaxonomyConfig::default() }, + TaxonomyConfig { name: "test-taxonomy ".to_string(), ..TaxonomyConfig::default() }, + TaxonomyConfig { name: "Test-Taxonomy ".to_string(), ..TaxonomyConfig::default() }, + ]; + config.slugify_taxonomies(); + let page1 = create_page_w_taxa("a.md", "en", vec![("test-taxonomy", vec!["Ecole"])]); + let page2 = create_page_w_taxa("b.md", "en", vec![("test taxonomy", vec!["École"])]); + let page3 = create_page_w_taxa("c.md", "en", vec![("test-taxonomy ", vec!["ecole"])]); + let page4 = create_page_w_taxa("d.md", "en", vec![("Test-Taxonomy ", vec!["école"])]); + let taxonomies = taxonomies!(config, [page1, page2, page3, page4]); + assert_eq!(taxonomies.len(), 1); + + let tax = &taxonomies[0]; + // under the default slugify strategy all of the provided terms should be the same + assert_eq!(tax.items.len(), 1); + let term1 = &tax.items[0]; + assert_eq!(term1.name, "Ecole"); + assert_eq!(term1.slug, "ecole"); + assert_eq!(term1.permalink, "http://a-website.com/test-taxonomy/ecole/"); + assert_eq!(term1.pages.len(), 4); + } + + #[test] + fn taxonomies_with_unic_are_not_grouped_with_safe_slugify_strategy() { + let mut config = Config::default_for_test(); + config.slugify.taxonomies = SlugifyStrategy::Safe; + config.languages.get_mut("en").unwrap().taxonomies = + vec![TaxonomyConfig { name: "test".to_string(), ..TaxonomyConfig::default() }]; + config.slugify_taxonomies(); + let page1 = create_page_w_taxa("a.md", "en", vec![("test", vec!["Ecole"])]); + let page2 = create_page_w_taxa("b.md", "en", vec![("test", vec!["École"])]); + let page3 = create_page_w_taxa("c.md", "en", vec![("test", vec!["ecole"])]); + let page4 = create_page_w_taxa("d.md", "en", vec![("test", vec!["école"])]); + let taxonomies = taxonomies!(config, [page1, page2, page3, page4]); + assert_eq!(taxonomies.len(), 1); + let tax = &taxonomies[0]; + // under the safe slugify strategy all terms should be distinct + assert_eq!(tax.items.len(), 4); + } } diff --git a/components/content/src/pagination.rs b/components/content/src/pagination.rs index c779ffda..0df62f33 100644 --- a/components/content/src/pagination.rs +++ b/components/content/src/pagination.rs @@ -285,7 +285,7 @@ mod tests { num_pages: usize, paginate_reversed: bool, ) -> (Section, Library) { - let mut library = Library::new(); + let mut library = Library::default(); for i in 1..=num_pages { let mut page = Page::default(); page.meta.title = Some(i.to_string()); diff --git a/components/content/src/taxonomies.rs b/components/content/src/taxonomies.rs index 682adb0e..24773b0a 100644 --- a/components/content/src/taxonomies.rs +++ b/components/content/src/taxonomies.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use serde::Serialize; use config::{Config, TaxonomyConfig}; -use errors::{bail, Context as ErrorContext, Result}; +use errors::{Context as ErrorContext, Result}; use libs::ahash::AHashMap; use libs::tera::{Context, Tera}; use utils::slugs::slugify_paths; @@ -125,7 +125,7 @@ pub struct Taxonomy { } impl Taxonomy { - fn new(tax_found: TaxonomyFound, config: &Config) -> Self { + pub(crate) fn new(tax_found: TaxonomyFound, config: &Config) -> Self { let mut sorted_items = vec![]; let slug = tax_found.slug; for (name, pages) in tax_found.terms { @@ -231,7 +231,7 @@ impl Taxonomy { /// Only used while building the taxonomies #[derive(Debug, PartialEq)] -struct TaxonomyFound<'a> { +pub(crate) struct TaxonomyFound<'a> { pub lang: &'a str, pub slug: String, pub config: &'a TaxonomyConfig, @@ -243,220 +243,3 @@ impl<'a> TaxonomyFound<'a> { Self { slug, lang, config, terms: AHashMap::new() } } } - -pub fn find_taxonomies(config: &Config, pages: &AHashMap) -> Result> { - let mut taxonomies_def = AHashMap::new(); - let mut taxonomies_slug = AHashMap::new(); - - for (code, options) in &config.languages { - let mut taxo_lang_def = AHashMap::new(); - for t in &options.taxonomies { - let slug = slugify_paths(&t.name, config.slugify.taxonomies); - taxonomies_slug.insert(&t.name, slug.clone()); - taxo_lang_def.insert(slug.clone(), TaxonomyFound::new(slug, code, t)); - } - taxonomies_def.insert(code, taxo_lang_def); - } - - for (_, page) in pages { - for (name, terms) in &page.meta.taxonomies { - let slug = taxonomies_slug.get(name); - let mut exists = slug.is_some(); - if let Some(s) = slug { - if !taxonomies_def[&page.lang].contains_key(s) { - exists = false; - } - } - if !exists { - bail!( - "Page `{}` has taxonomy `{}` which is not defined in config.toml", - page.file.path.display(), - name - ); - } - let slug = slug.unwrap(); - - let taxonomy_found = taxonomies_def.get_mut(&page.lang).unwrap().get_mut(slug).unwrap(); - for term in terms { - taxonomy_found.terms.entry(term).or_insert_with(Vec::new).push(page); - } - } - } - - // And now generates the actual taxonomies - let mut taxonomies = vec![]; - for (_, vals) in taxonomies_def { - for (_, tax_found) in vals { - taxonomies.push(Taxonomy::new(tax_found, config)); - } - } - - Ok(taxonomies) -} - -#[cfg(test)] -mod tests { - use super::*; - use config::LanguageOptions; - use std::collections::HashMap; - use utils::slugs::SlugifyStrategy; - - macro_rules! taxonomies { - ($config:expr, [$($page:expr),+]) => {{ - let mut pages = AHashMap::new(); - $( - pages.insert($page.file.path.clone(), $page.clone()); - )+ - find_taxonomies(&$config, &pages).unwrap() - }}; - } - - fn create_page(path: &str, lang: &str, taxo: Vec<(&str, Vec<&str>)>) -> Page { - let mut page = Page::default(); - page.file.path = PathBuf::from(path); - page.lang = lang.to_owned(); - let mut taxonomies = HashMap::new(); - for (name, terms) in taxo { - taxonomies.insert(name.to_owned(), terms.iter().map(|t| t.to_string()).collect()); - } - page.meta.taxonomies = taxonomies; - page - } - - #[test] - fn errors_on_unknown_taxonomy() { - let config = Config::default_for_test(); - let page1 = create_page("unknown/taxo.md", "en", vec![("tags", vec!["rust", "db"])]); - let mut pages = AHashMap::new(); - pages.insert(page1.file.path.clone(), page1); - let taxonomies = find_taxonomies(&config, &pages); - assert!(taxonomies.is_err()); - let err = taxonomies.unwrap_err(); - assert_eq!( - err.to_string(), - "Page `unknown/taxo.md` has taxonomy `tags` which is not defined in config.toml" - ); - } - - #[test] - fn can_make_taxonomies() { - let mut config = Config::default_for_test(); - config.languages.get_mut("en").unwrap().taxonomies = vec![ - TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() }, - TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }, - TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() }, - ]; - - let page1 = create_page( - "a.md", - "en", - vec![("tags", vec!["rust", "db"]), ("categories", vec!["tutorials"])], - ); - let page2 = create_page( - "b.md", - "en", - vec![("tags", vec!["rust", "js"]), ("categories", vec!["others"])], - ); - let page3 = create_page( - "c.md", - "en", - vec![("tags", vec!["js"]), ("authors", vec!["Vincent Prouillet"])], - ); - let taxonomies = taxonomies!(config, [page1, page2, page3]); - - let tags = taxonomies.iter().find(|t| t.kind.name == "tags").unwrap(); - assert_eq!(tags.len(), 3); - assert_eq!(tags.items[0].name, "db"); - assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/db/"); - assert_eq!(tags.items[0].pages.len(), 1); - assert_eq!(tags.items[1].name, "js"); - assert_eq!(tags.items[1].permalink, "http://a-website.com/tags/js/"); - assert_eq!(tags.items[1].pages.len(), 2); - assert_eq!(tags.items[2].name, "rust"); - assert_eq!(tags.items[2].permalink, "http://a-website.com/tags/rust/"); - assert_eq!(tags.items[2].pages.len(), 2); - - let categories = taxonomies.iter().find(|t| t.kind.name == "categories").unwrap(); - assert_eq!(categories.items.len(), 2); - assert_eq!(categories.items[0].name, "others"); - assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/others/"); - assert_eq!(categories.items[0].pages.len(), 1); - - let authors = taxonomies.iter().find(|t| t.kind.name == "authors").unwrap(); - assert_eq!(authors.items.len(), 1); - assert_eq!(authors.items[0].permalink, "http://a-website.com/authors/vincent-prouillet/"); - } - - #[test] - fn can_make_multiple_language_taxonomies() { - let mut config = Config::default_for_test(); - config.slugify.taxonomies = SlugifyStrategy::Safe; - config.languages.insert("fr".to_owned(), LanguageOptions::default()); - config.languages.get_mut("en").unwrap().taxonomies = vec![ - TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() }, - TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }, - ]; - config.languages.get_mut("fr").unwrap().taxonomies = vec![ - TaxonomyConfig { name: "catégories".to_string(), ..TaxonomyConfig::default() }, - TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }, - ]; - - let page1 = create_page("a.md", "en", vec![("categories", vec!["rust"])]); - let page2 = create_page("b.md", "en", vec![("tags", vec!["rust"])]); - let page3 = create_page("c.md", "fr", vec![("catégories", vec!["rust"])]); - let taxonomies = taxonomies!(config, [page1, page2, page3]); - - let categories = taxonomies.iter().find(|t| t.kind.name == "categories").unwrap(); - assert_eq!(categories.len(), 1); - assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/rust/"); - let tags = taxonomies.iter().find(|t| t.kind.name == "tags" && t.lang == "en").unwrap(); - assert_eq!(tags.len(), 1); - assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/rust/"); - let fr_categories = taxonomies.iter().find(|t| t.kind.name == "catégories").unwrap(); - assert_eq!(fr_categories.len(), 1); - assert_eq!(fr_categories.items[0].permalink, "http://a-website.com/fr/catégories/rust/"); - } - - #[test] - fn taxonomies_with_unic_are_grouped_with_default_slugify_strategy() { - let mut config = Config::default_for_test(); - config.languages.get_mut("en").unwrap().taxonomies = vec![ - TaxonomyConfig { name: "test-taxonomy".to_string(), ..TaxonomyConfig::default() }, - TaxonomyConfig { name: "test taxonomy".to_string(), ..TaxonomyConfig::default() }, - TaxonomyConfig { name: "test-taxonomy ".to_string(), ..TaxonomyConfig::default() }, - TaxonomyConfig { name: "Test-Taxonomy ".to_string(), ..TaxonomyConfig::default() }, - ]; - let page1 = create_page("a.md", "en", vec![("test-taxonomy", vec!["Ecole"])]); - let page2 = create_page("b.md", "en", vec![("test taxonomy", vec!["École"])]); - let page3 = create_page("c.md", "en", vec![("test-taxonomy ", vec!["ecole"])]); - let page4 = create_page("d.md", "en", vec![("Test-Taxonomy ", vec!["école"])]); - let taxonomies = taxonomies!(config, [page1, page2, page3, page4]); - assert_eq!(taxonomies.len(), 1); - - let tax = &taxonomies[0]; - // under the default slugify strategy all of the provided terms should be the same - assert_eq!(tax.items.len(), 1); - let term1 = &tax.items[0]; - assert_eq!(term1.name, "Ecole"); - assert_eq!(term1.slug, "ecole"); - assert_eq!(term1.permalink, "http://a-website.com/test-taxonomy/ecole/"); - assert_eq!(term1.pages.len(), 4); - } - - #[test] - fn taxonomies_with_unic_are_not_grouped_with_safe_slugify_strategy() { - let mut config = Config::default_for_test(); - config.slugify.taxonomies = SlugifyStrategy::Safe; - config.languages.get_mut("en").unwrap().taxonomies = - vec![TaxonomyConfig { name: "test".to_string(), ..TaxonomyConfig::default() }]; - let page1 = create_page("a.md", "en", vec![("test", vec!["Ecole"])]); - let page2 = create_page("b.md", "en", vec![("test", vec!["École"])]); - let page3 = create_page("c.md", "en", vec![("test", vec!["ecole"])]); - let page4 = create_page("d.md", "en", vec![("test", vec!["école"])]); - let taxonomies = taxonomies!(config, [page1, page2, page3, page4]); - assert_eq!(taxonomies.len(), 1); - let tax = &taxonomies[0]; - // under the safe slugify strategy all terms should be distinct - assert_eq!(tax.items.len(), 4); - } -} diff --git a/components/site/src/lib.rs b/components/site/src/lib.rs index fdb04f82..9e8d1d3a 100644 --- a/components/site/src/lib.rs +++ b/components/site/src/lib.rs @@ -99,7 +99,7 @@ impl Site { permalinks: HashMap::new(), include_drafts: false, // We will allocate it properly later on - library: Arc::new(RwLock::new(Library::new())), + library: Arc::new(RwLock::new(Library::default())), build_mode: BuildMode::Disk, shortcode_definitions, }; @@ -164,7 +164,7 @@ impl Site { pub fn load(&mut self) -> Result<()> { let base_path = self.base_path.to_string_lossy().replace('\\', "/"); - self.library = Arc::new(RwLock::new(Library::new())); + self.library = Arc::new(RwLock::new(Library::new(&self.config))); let mut pages_insert_anchors = HashMap::new(); // not the most elegant loop, but this is necessary to use skip_current_dir @@ -397,6 +397,16 @@ impl Site { /// Add a page to the site /// The `render` parameter is used in the serve command with --fast, when rebuilding a page. pub fn add_page(&mut self, mut page: Page, render_md: bool) -> Result<()> { + for (taxa_name, _) in &page.meta.taxonomies { + if !self.config.has_taxonomy(taxa_name, &page.lang) { + bail!( + "Page `{}` has taxonomy `{}` which is not defined in config.toml", + page.file.path.display(), + taxa_name + ); + } + } + self.permalinks.insert(page.file.relative.clone(), page.permalink.clone()); if render_md { let insert_anchor = @@ -490,7 +500,7 @@ impl Site { return Ok(()); } - self.taxonomies = self.library.read().unwrap().find_taxonomies(&self.config)?; + self.taxonomies = self.library.read().unwrap().find_taxonomies(&self.config); Ok(()) } diff --git a/components/site/tests/site.rs b/components/site/tests/site.rs index 169687b4..b2cce71b 100644 --- a/components/site/tests/site.rs +++ b/components/site/tests/site.rs @@ -2,10 +2,12 @@ mod common; use std::collections::HashMap; use std::env; -use std::path::Path; +use std::path::{Path, PathBuf}; use common::{build_site, build_site_with_setup}; use config::TaxonomyConfig; +use content::Page; +use libs::ahash::AHashMap; use site::sitemap; use site::Site; @@ -98,6 +100,21 @@ fn can_parse_site() { assert_eq!(Some(&prog_section.meta.extra), sitemap_entry.extra); } +#[test] +fn errors_on_unknown_taxonomies() { + let (mut site, _, _) = build_site("test_site"); + let mut page = Page::default(); + page.file.path = PathBuf::from("unknown/taxo.md"); + page.meta.taxonomies.insert("wrong".to_string(), vec![]); + let res = site.add_page(page, false); + assert!(res.is_err()); + let err = res.unwrap_err(); + assert_eq!( + err.to_string(), + "Page `unknown/taxo.md` has taxonomy `wrong` which is not defined in config.toml" + ); +} + #[test] fn can_build_site_without_live_reload() { let (_, _tmp_dir, public) = build_site("test_site"); @@ -268,8 +285,11 @@ fn can_build_site_with_taxonomies() { let (site, _tmp_dir, public) = build_site_with_setup("test_site", |mut site| { site.load().unwrap(); { - let mut library = site.library.write().unwrap(); - for (i, (_, page)) in library.pages.iter_mut().enumerate() { + let library = &mut *site.library.write().unwrap(); + let mut pages = vec![]; + + let pages_data = std::mem::replace(&mut library.pages, AHashMap::new()); + for (i, (_, mut page)) in pages_data.into_iter().enumerate() { page.meta.taxonomies = { let mut taxonomies = HashMap::new(); taxonomies.insert( @@ -278,6 +298,10 @@ fn can_build_site_with_taxonomies() { ); taxonomies }; + pages.push(page); + } + for p in pages { + library.insert_page(p); } } site.populate_taxonomies().unwrap(); @@ -543,6 +567,7 @@ fn can_build_site_with_pagination_for_taxonomy() { let (_, _tmp_dir, public) = build_site_with_setup("test_site", |mut site| { site.config.languages.get_mut("en").unwrap().taxonomies.push(TaxonomyConfig { name: "tags".to_string(), + slug: "tags".to_string(), paginate_by: Some(2), paginate_path: None, render: true, @@ -550,9 +575,11 @@ fn can_build_site_with_pagination_for_taxonomy() { }); site.load().unwrap(); { - let mut library = site.library.write().unwrap(); + let library = &mut *site.library.write().unwrap(); + let mut pages = vec![]; - for (i, (_, page)) in library.pages.iter_mut().enumerate() { + let pages_data = std::mem::replace(&mut library.pages, AHashMap::new()); + for (i, (_, mut page)) in pages_data.into_iter().enumerate() { page.meta.taxonomies = { let mut taxonomies = HashMap::new(); taxonomies.insert( @@ -561,6 +588,10 @@ fn can_build_site_with_pagination_for_taxonomy() { ); taxonomies }; + pages.push(page); + } + for p in pages { + library.insert_page(p); } } site.populate_taxonomies().unwrap(); diff --git a/components/templates/src/global_fns/content.rs b/components/templates/src/global_fns/content.rs index 4ab744cc..938100ee 100644 --- a/components/templates/src/global_fns/content.rs +++ b/components/templates/src/global_fns/content.rs @@ -190,12 +190,13 @@ mod tests { #[test] fn can_get_taxonomy() { - let mut config = Config::default(); + let mut config = Config::default_for_test(); config.slugify.taxonomies = SlugifyStrategy::On; let taxo_config = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }; let taxo_config_fr = TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }; - let library = Arc::new(RwLock::new(Library::new())); + config.slugify_taxonomies(); + let library = Arc::new(RwLock::new(Library::new(&config))); let tag = TaxonomyTerm::new("Programming", &config.default_language, "tags", &[], &config); let tag_fr = TaxonomyTerm::new("Programmation", "fr", "tags", &[], &config); let tags = Taxonomy {