#[macro_use] extern crate serde_derive; extern crate tera; extern crate slug; #[macro_use] extern crate errors; extern crate config; extern crate content; extern crate front_matter; extern crate utils; use std::collections::HashMap; use slug::slugify; use tera::{Context, Tera}; use config::{Config, Taxonomy as TaxonomyConfig}; use errors::{Result, ResultExt}; use content::{Page, sort_pages}; use front_matter::SortBy; use utils::templates::render_template; /// A tag or category #[derive(Debug, Clone, Serialize, PartialEq)] pub struct TaxonomyItem { pub name: String, pub slug: String, pub permalink: String, pub pages: Vec, } impl TaxonomyItem { pub fn new(name: &str, path: &str, config: &Config, pages: Vec) -> TaxonomyItem { // Taxonomy are almost always used for blogs so we filter by dates // and it's not like we can sort things across sections by anything other // than dates let (mut pages, ignored_pages) = sort_pages(pages, SortBy::Date); let slug = slugify(name); let permalink = { config.make_permalink(&format!("/{}/{}", path, slug)) }; // We still append pages without dates at the end pages.extend(ignored_pages); TaxonomyItem { name: name.to_string(), permalink, slug, pages, } } } /// All the tags or categories #[derive(Debug, Clone, PartialEq, Serialize)] pub struct Taxonomy { pub kind: TaxonomyConfig, // this vec is sorted by the count of item pub items: Vec, } impl Taxonomy { fn new(kind: TaxonomyConfig, config: &Config, items: HashMap>) -> Taxonomy { let mut sorted_items = vec![]; for (name, pages) in items { sorted_items.push( TaxonomyItem::new(&name, &kind.name, config, pages) ); } sorted_items.sort_by(|a, b| a.name.cmp(&b.name)); Taxonomy { kind, items: sorted_items, } } pub fn len(&self) -> usize { self.items.len() } pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn render_term(&self, item: &TaxonomyItem, tera: &Tera, config: &Config) -> Result { let mut context = Context::new(); context.add("config", config); context.add("term", item); context.add("taxonomy", &self.kind); context.add("current_url", &config.make_permalink(&format!("{}/{}", self.kind.name, item.slug))); context.add("current_path", &format!("/{}/{}", self.kind.name, item.slug)); render_template(&format!("{}/single.html", self.kind.name), tera, &context, &config.theme) .chain_err(|| format!("Failed to render single term {} page.", self.kind.name)) } pub fn render_all_terms(&self, tera: &Tera, config: &Config) -> Result { let mut context = Context::new(); context.add("config", config); context.add("terms", &self.items); context.add("taxonomy", &self.kind); context.add("current_url", &config.make_permalink(&self.kind.name)); context.add("current_path", &self.kind.name); render_template(&format!("{}/list.html", self.kind.name), tera, &context, &config.theme) .chain_err(|| format!("Failed to render a list of {} page.", self.kind.name)) } } pub fn find_taxonomies(config: &Config, all_pages: &[Page]) -> Result> { let taxonomies_def = { let mut m = HashMap::new(); for t in &config.taxonomies { m.insert(t.name.clone(), t); } m }; let mut all_taxonomies = HashMap::new(); // Find all the taxonomies first for page in all_pages { for (name, val) in &page.meta.taxonomies { if taxonomies_def.contains_key(name) { all_taxonomies .entry(name) .or_insert_with(|| HashMap::new()); for v in val { all_taxonomies.get_mut(name) .unwrap() .entry(v.to_string()) .or_insert_with(|| vec![]) .push(page.clone()); } } else { bail!("Page `{}` has taxonomy `{}` which is not defined in config.toml", page.file.path.display(), name); } } } let mut taxonomies = vec![]; for (name, taxo) in all_taxonomies { taxonomies.push(Taxonomy::new(taxonomies_def[name].clone(), config, taxo)); } Ok(taxonomies) } #[cfg(test)] mod tests { use super::*; use std::collections::HashMap; use config::{Config, Taxonomy}; use content::Page; #[test] fn can_make_taxonomies() { let mut config = Config::default(); config.taxonomies = vec![ Taxonomy {name: "categories".to_string(), ..Taxonomy::default()}, Taxonomy {name: "tags".to_string(), ..Taxonomy::default()}, Taxonomy {name: "authors".to_string(), ..Taxonomy::default()}, ]; let mut page1 = Page::default(); let mut taxo_page1 = HashMap::new(); taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]); taxo_page1.insert("categories".to_string(), vec!["Programming tutorials".to_string()]); page1.meta.taxonomies = taxo_page1; let mut page2 = Page::default(); let mut taxo_page2 = HashMap::new(); taxo_page2.insert("tags".to_string(), vec!["rust".to_string(), "js".to_string()]); taxo_page2.insert("categories".to_string(), vec!["Other".to_string()]); page2.meta.taxonomies = taxo_page2; let mut page3 = Page::default(); let mut taxo_page3 = HashMap::new(); taxo_page3.insert("tags".to_string(), vec!["js".to_string()]); taxo_page3.insert("authors".to_string(), vec!["Vincent Prouillet".to_string()]); page3.meta.taxonomies = taxo_page3; let pages = vec![page1, page2, page3]; let taxonomies = find_taxonomies(&config, &pages).unwrap(); let (tags, categories, authors) = { let mut t = None; let mut c = None; let mut a = None; for x in taxonomies { match x.kind.name.as_ref() { "tags" => t = Some(x), "categories" => c = Some(x), "authors" => a = Some(x), _ => unreachable!(), } } (t.unwrap(), c.unwrap(), a.unwrap()) }; assert_eq!(tags.items.len(), 3); assert_eq!(categories.items.len(), 2); assert_eq!(authors.items.len(), 1); assert_eq!(tags.items[0].name, "db"); assert_eq!(tags.items[0].slug, "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].slug, "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].slug, "rust"); assert_eq!(tags.items[2].permalink, "http://a-website.com/tags/rust/"); assert_eq!(tags.items[2].pages.len(), 2); assert_eq!(categories.items[0].name, "Other"); assert_eq!(categories.items[0].slug, "other"); assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/other/"); assert_eq!(categories.items[0].pages.len(), 1); assert_eq!(categories.items[1].name, "Programming tutorials"); assert_eq!(categories.items[1].slug, "programming-tutorials"); assert_eq!(categories.items[1].permalink, "http://a-website.com/categories/programming-tutorials/"); assert_eq!(categories.items[1].pages.len(), 1); } #[test] fn errors_on_unknown_taxonomy() { let mut config = Config::default(); config.taxonomies = vec![ Taxonomy {name: "authors".to_string(), ..Taxonomy::default()}, ]; let mut page1 = Page::default(); let mut taxo_page1 = HashMap::new(); taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]); page1.meta.taxonomies = taxo_page1; let taxonomies = find_taxonomies(&config, &vec![page1]); assert!(taxonomies.is_err()); let err = taxonomies.unwrap_err(); // no path as this is created by Default assert_eq!(err.description(), "Page `` has taxonomy `tags` which is not defined in config.toml"); } }