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.

312 lines
11KB

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