You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

309 lines
11KB

  1. use std::collections::HashMap;
  2. use slug::slugify;
  3. use tera::{Context, Tera, Value};
  4. use slotmap::{Key};
  5. use config::{Config, Taxonomy as TaxonomyConfig};
  6. use errors::{Result, ResultExt};
  7. use utils::templates::render_template;
  8. use sorting::sort_pages_by_date;
  9. use library::Library;
  10. #[derive(Debug, Clone, PartialEq, Serialize)]
  11. struct SerializedTaxonomyItem<'a> {
  12. name: &'a str,
  13. slug: &'a str,
  14. permalink: &'a str,
  15. pages: Vec<&'a Value>,
  16. }
  17. impl<'a> SerializedTaxonomyItem<'a> {
  18. pub fn from_item(item: &'a TaxonomyItem, library: &'a Library) -> Self {
  19. let mut pages = vec![];
  20. for k in &item.pages {
  21. pages.push(library.get_cached_page_value_by_key(k));
  22. }
  23. SerializedTaxonomyItem {
  24. name: &item.name,
  25. slug: &item.slug,
  26. permalink: &item.permalink,
  27. pages,
  28. }
  29. }
  30. }
  31. /// A taxonomy with all its pages
  32. #[derive(Debug, Clone, PartialEq)]
  33. pub struct TaxonomyItem {
  34. pub name: String,
  35. pub slug: String,
  36. pub permalink: String,
  37. pub pages: Vec<Key>,
  38. }
  39. impl TaxonomyItem {
  40. pub fn new(name: &str, path: &str, config: &Config, keys: Vec<Key>, library: &Library) -> Self {
  41. // Taxonomy are almost always used for blogs so we filter by dates
  42. // and it's not like we can sort things across sections by anything other
  43. // than dates
  44. let data = keys
  45. .iter()
  46. .map(|k| {
  47. if let Some(page) = library.pages().get(*k) {
  48. (k, page.meta.datetime, page.permalink.as_ref())
  49. } else {
  50. unreachable!("Sorting got an unknown page")
  51. }
  52. })
  53. .collect();
  54. let (mut pages, ignored_pages) = sort_pages_by_date(data);
  55. let slug = slugify(name);
  56. let permalink = config.make_permalink(&format!("/{}/{}", path, slug));
  57. // We still append pages without dates at the end
  58. pages.extend(ignored_pages);
  59. TaxonomyItem {
  60. name: name.to_string(),
  61. permalink,
  62. slug,
  63. pages,
  64. }
  65. }
  66. }
  67. #[derive(Debug, Clone, PartialEq, Serialize)]
  68. pub struct SerializedTaxonomy<'a> {
  69. kind: &'a TaxonomyConfig,
  70. items: Vec<SerializedTaxonomyItem<'a>>,
  71. }
  72. impl<'a> SerializedTaxonomy<'a> {
  73. pub fn from_taxonomy(taxonomy: &'a Taxonomy, library: &'a Library) -> Self {
  74. let items: Vec<SerializedTaxonomyItem> = taxonomy.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect();
  75. SerializedTaxonomy {
  76. kind: &taxonomy.kind,
  77. items,
  78. }
  79. }
  80. }
  81. /// All different taxonomies we have and their content
  82. #[derive(Debug, Clone, PartialEq)]
  83. pub struct Taxonomy {
  84. pub kind: TaxonomyConfig,
  85. // this vec is sorted by the count of item
  86. pub items: Vec<TaxonomyItem>,
  87. }
  88. impl Taxonomy {
  89. fn new(kind: TaxonomyConfig, config: &Config, items: HashMap<String, Vec<Key>>, library: &Library) -> Taxonomy {
  90. let mut sorted_items = vec![];
  91. for (name, pages) in items {
  92. sorted_items.push(
  93. TaxonomyItem::new(&name, &kind.name, config, pages, library)
  94. );
  95. }
  96. sorted_items.sort_by(|a, b| a.name.cmp(&b.name));
  97. Taxonomy {
  98. kind,
  99. items: sorted_items,
  100. }
  101. }
  102. pub fn len(&self) -> usize {
  103. self.items.len()
  104. }
  105. pub fn is_empty(&self) -> bool {
  106. self.len() == 0
  107. }
  108. pub fn render_term(&self, item: &TaxonomyItem, tera: &Tera, config: &Config, library: &Library) -> Result<String> {
  109. let mut context = Context::new();
  110. context.insert("config", config);
  111. context.insert("term", &SerializedTaxonomyItem::from_item(item, library));
  112. context.insert("taxonomy", &self.kind);
  113. context.insert("current_url", &config.make_permalink(&format!("{}/{}", self.kind.name, item.slug)));
  114. context.insert("current_path", &format!("/{}/{}", self.kind.name, item.slug));
  115. render_template(&format!("{}/single.html", self.kind.name), tera, &context, &config.theme)
  116. .chain_err(|| format!("Failed to render single term {} page.", self.kind.name))
  117. }
  118. pub fn render_all_terms(&self, tera: &Tera, config: &Config, library: &Library) -> Result<String> {
  119. let mut context = Context::new();
  120. context.insert("config", config);
  121. let terms: Vec<SerializedTaxonomyItem> = self.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect();
  122. context.insert("terms", &terms);
  123. context.insert("taxonomy", &self.kind);
  124. context.insert("current_url", &config.make_permalink(&self.kind.name));
  125. context.insert("current_path", &self.kind.name);
  126. render_template(&format!("{}/list.html", self.kind.name), tera, &context, &config.theme)
  127. .chain_err(|| format!("Failed to render a list of {} page.", self.kind.name))
  128. }
  129. pub fn to_serialized<'a>(&'a self, library: &'a Library) -> SerializedTaxonomy<'a> {
  130. SerializedTaxonomy::from_taxonomy(self, library)
  131. }
  132. }
  133. pub fn find_taxonomies(config: &Config, library: &Library) -> Result<Vec<Taxonomy>> {
  134. let taxonomies_def = {
  135. let mut m = HashMap::new();
  136. for t in &config.taxonomies {
  137. m.insert(t.name.clone(), t);
  138. }
  139. m
  140. };
  141. let mut all_taxonomies = HashMap::new();
  142. for (key, page) in library.pages() {
  143. // Draft are not part of taxonomies
  144. if page.is_draft() {
  145. continue;
  146. }
  147. for (name, val) in &page.meta.taxonomies {
  148. if taxonomies_def.contains_key(name) {
  149. all_taxonomies
  150. .entry(name)
  151. .or_insert_with(HashMap::new);
  152. for v in val {
  153. all_taxonomies.get_mut(name)
  154. .unwrap()
  155. .entry(v.to_string())
  156. .or_insert_with(|| vec![])
  157. .push(key);
  158. }
  159. } else {
  160. bail!("Page `{}` has taxonomy `{}` which is not defined in config.toml", page.file.path.display(), name);
  161. }
  162. }
  163. }
  164. let mut taxonomies = vec![];
  165. for (name, taxo) in all_taxonomies {
  166. taxonomies.push(Taxonomy::new(taxonomies_def[name].clone(), config, taxo, library));
  167. }
  168. Ok(taxonomies)
  169. }
  170. #[cfg(test)]
  171. mod tests {
  172. use super::*;
  173. use std::collections::HashMap;
  174. use config::{Config, Taxonomy as TaxonomyConfig};
  175. use content::Page;
  176. use library::Library;
  177. #[test]
  178. fn can_make_taxonomies() {
  179. let mut config = Config::default();
  180. let mut library = Library::new(2, 0);
  181. config.taxonomies = vec![
  182. TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() },
  183. TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() },
  184. TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() },
  185. ];
  186. let mut page1 = Page::default();
  187. let mut taxo_page1 = HashMap::new();
  188. taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
  189. taxo_page1.insert("categories".to_string(), vec!["Programming tutorials".to_string()]);
  190. page1.meta.taxonomies = taxo_page1;
  191. library.insert_page(page1);
  192. let mut page2 = Page::default();
  193. let mut taxo_page2 = HashMap::new();
  194. taxo_page2.insert("tags".to_string(), vec!["rust".to_string(), "js".to_string()]);
  195. taxo_page2.insert("categories".to_string(), vec!["Other".to_string()]);
  196. page2.meta.taxonomies = taxo_page2;
  197. library.insert_page(page2);
  198. let mut page3 = Page::default();
  199. let mut taxo_page3 = HashMap::new();
  200. taxo_page3.insert("tags".to_string(), vec!["js".to_string()]);
  201. taxo_page3.insert("authors".to_string(), vec!["Vincent Prouillet".to_string()]);
  202. page3.meta.taxonomies = taxo_page3;
  203. library.insert_page(page3);
  204. let taxonomies = find_taxonomies(&config, &library).unwrap();
  205. let (tags, categories, authors) = {
  206. let mut t = None;
  207. let mut c = None;
  208. let mut a = None;
  209. for x in taxonomies {
  210. match x.kind.name.as_ref() {
  211. "tags" => t = Some(x),
  212. "categories" => c = Some(x),
  213. "authors" => a = Some(x),
  214. _ => unreachable!(),
  215. }
  216. }
  217. (t.unwrap(), c.unwrap(), a.unwrap())
  218. };
  219. assert_eq!(tags.items.len(), 3);
  220. assert_eq!(categories.items.len(), 2);
  221. assert_eq!(authors.items.len(), 1);
  222. assert_eq!(tags.items[0].name, "db");
  223. assert_eq!(tags.items[0].slug, "db");
  224. assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/db/");
  225. assert_eq!(tags.items[0].pages.len(), 1);
  226. assert_eq!(tags.items[1].name, "js");
  227. assert_eq!(tags.items[1].slug, "js");
  228. assert_eq!(tags.items[1].permalink, "http://a-website.com/tags/js/");
  229. assert_eq!(tags.items[1].pages.len(), 2);
  230. assert_eq!(tags.items[2].name, "rust");
  231. assert_eq!(tags.items[2].slug, "rust");
  232. assert_eq!(tags.items[2].permalink, "http://a-website.com/tags/rust/");
  233. assert_eq!(tags.items[2].pages.len(), 2);
  234. assert_eq!(categories.items[0].name, "Other");
  235. assert_eq!(categories.items[0].slug, "other");
  236. assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/other/");
  237. assert_eq!(categories.items[0].pages.len(), 1);
  238. assert_eq!(categories.items[1].name, "Programming tutorials");
  239. assert_eq!(categories.items[1].slug, "programming-tutorials");
  240. assert_eq!(categories.items[1].permalink, "http://a-website.com/categories/programming-tutorials/");
  241. assert_eq!(categories.items[1].pages.len(), 1);
  242. }
  243. #[test]
  244. fn errors_on_unknown_taxonomy() {
  245. let mut config = Config::default();
  246. let mut library = Library::new(2, 0);
  247. config.taxonomies = vec![
  248. TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() },
  249. ];
  250. let mut page1 = Page::default();
  251. let mut taxo_page1 = HashMap::new();
  252. taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
  253. page1.meta.taxonomies = taxo_page1;
  254. library.insert_page(page1);
  255. let taxonomies = find_taxonomies(&config, &library);
  256. assert!(taxonomies.is_err());
  257. let err = taxonomies.unwrap_err();
  258. // no path as this is created by Default
  259. assert_eq!(err.description(), "Page `` has taxonomy `tags` which is not defined in config.toml");
  260. }
  261. }