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.

326 lines
11KB

  1. use std::collections::HashMap;
  2. use slotmap::Key;
  3. use slug::slugify;
  4. use tera::{Context, Tera};
  5. use config::{Config, Taxonomy as TaxonomyConfig};
  6. use errors::{Result, ResultExt};
  7. use utils::templates::render_template;
  8. use content::SerializingPage;
  9. use library::Library;
  10. use sorting::sort_pages_by_date;
  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 { name: name.to_string(), permalink, slug, pages }
  62. }
  63. }
  64. #[derive(Debug, Clone, PartialEq, Serialize)]
  65. pub struct SerializedTaxonomy<'a> {
  66. kind: &'a TaxonomyConfig,
  67. items: Vec<SerializedTaxonomyItem<'a>>,
  68. }
  69. impl<'a> SerializedTaxonomy<'a> {
  70. pub fn from_taxonomy(taxonomy: &'a Taxonomy, library: &'a Library) -> Self {
  71. let items: Vec<SerializedTaxonomyItem> =
  72. taxonomy.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect();
  73. SerializedTaxonomy { kind: &taxonomy.kind, items }
  74. }
  75. }
  76. /// All different taxonomies we have and their content
  77. #[derive(Debug, Clone, PartialEq)]
  78. pub struct Taxonomy {
  79. pub kind: TaxonomyConfig,
  80. // this vec is sorted by the count of item
  81. pub items: Vec<TaxonomyItem>,
  82. }
  83. impl Taxonomy {
  84. fn new(
  85. kind: TaxonomyConfig,
  86. config: &Config,
  87. items: HashMap<String, Vec<Key>>,
  88. library: &Library,
  89. ) -> Taxonomy {
  90. let mut sorted_items = vec![];
  91. for (name, pages) in items {
  92. sorted_items.push(TaxonomyItem::new(&name, &kind.name, config, pages, library));
  93. }
  94. sorted_items.sort_by(|a, b| a.name.cmp(&b.name));
  95. Taxonomy { kind, items: sorted_items }
  96. }
  97. pub fn len(&self) -> usize {
  98. self.items.len()
  99. }
  100. pub fn is_empty(&self) -> bool {
  101. self.len() == 0
  102. }
  103. pub fn render_term(
  104. &self,
  105. item: &TaxonomyItem,
  106. tera: &Tera,
  107. config: &Config,
  108. library: &Library,
  109. ) -> Result<String> {
  110. let mut context = Context::new();
  111. context.insert("config", config);
  112. context.insert("term", &SerializedTaxonomyItem::from_item(item, library));
  113. context.insert("taxonomy", &self.kind);
  114. context.insert(
  115. "current_url",
  116. &config.make_permalink(&format!("{}/{}", self.kind.name, item.slug)),
  117. );
  118. context.insert("current_path", &format!("/{}/{}", self.kind.name, item.slug));
  119. render_template(&format!("{}/single.html", self.kind.name), tera, &context, &config.theme)
  120. .chain_err(|| format!("Failed to render single term {} page.", self.kind.name))
  121. }
  122. pub fn render_all_terms(
  123. &self,
  124. tera: &Tera,
  125. config: &Config,
  126. library: &Library,
  127. ) -> Result<String> {
  128. let mut context = Context::new();
  129. context.insert("config", config);
  130. let terms: Vec<SerializedTaxonomyItem> =
  131. self.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect();
  132. context.insert("terms", &terms);
  133. context.insert("taxonomy", &self.kind);
  134. context.insert("current_url", &config.make_permalink(&self.kind.name));
  135. context.insert("current_path", &self.kind.name);
  136. render_template(&format!("{}/list.html", self.kind.name), tera, &context, &config.theme)
  137. .chain_err(|| format!("Failed to render a list of {} page.", self.kind.name))
  138. }
  139. pub fn to_serialized<'a>(&'a self, library: &'a Library) -> SerializedTaxonomy<'a> {
  140. SerializedTaxonomy::from_taxonomy(self, library)
  141. }
  142. }
  143. pub fn find_taxonomies(config: &Config, library: &Library) -> Result<Vec<Taxonomy>> {
  144. let taxonomies_def = {
  145. let mut m = HashMap::new();
  146. for t in &config.taxonomies {
  147. m.insert(t.name.clone(), t);
  148. }
  149. m
  150. };
  151. let mut all_taxonomies = HashMap::new();
  152. for (key, page) in library.pages() {
  153. // Draft are not part of taxonomies
  154. if page.is_draft() {
  155. continue;
  156. }
  157. for (name, val) in &page.meta.taxonomies {
  158. if taxonomies_def.contains_key(name) {
  159. all_taxonomies.entry(name).or_insert_with(HashMap::new);
  160. for v in val {
  161. all_taxonomies
  162. .get_mut(name)
  163. .unwrap()
  164. .entry(v.to_string())
  165. .or_insert_with(|| vec![])
  166. .push(key);
  167. }
  168. } else {
  169. bail!(
  170. "Page `{}` has taxonomy `{}` which is not defined in config.toml",
  171. page.file.path.display(),
  172. name
  173. );
  174. }
  175. }
  176. }
  177. let mut taxonomies = vec![];
  178. for (name, taxo) in all_taxonomies {
  179. taxonomies.push(Taxonomy::new(taxonomies_def[name].clone(), config, taxo, library));
  180. }
  181. Ok(taxonomies)
  182. }
  183. #[cfg(test)]
  184. mod tests {
  185. use super::*;
  186. use std::collections::HashMap;
  187. use config::{Config, Taxonomy as TaxonomyConfig};
  188. use content::Page;
  189. use library::Library;
  190. #[test]
  191. fn can_make_taxonomies() {
  192. let mut config = Config::default();
  193. let mut library = Library::new(2, 0);
  194. config.taxonomies = vec![
  195. TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() },
  196. TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() },
  197. TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() },
  198. ];
  199. let mut page1 = Page::default();
  200. let mut taxo_page1 = HashMap::new();
  201. taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
  202. taxo_page1.insert("categories".to_string(), vec!["Programming tutorials".to_string()]);
  203. page1.meta.taxonomies = taxo_page1;
  204. library.insert_page(page1);
  205. let mut page2 = Page::default();
  206. let mut taxo_page2 = HashMap::new();
  207. taxo_page2.insert("tags".to_string(), vec!["rust".to_string(), "js".to_string()]);
  208. taxo_page2.insert("categories".to_string(), vec!["Other".to_string()]);
  209. page2.meta.taxonomies = taxo_page2;
  210. library.insert_page(page2);
  211. let mut page3 = Page::default();
  212. let mut taxo_page3 = HashMap::new();
  213. taxo_page3.insert("tags".to_string(), vec!["js".to_string()]);
  214. taxo_page3.insert("authors".to_string(), vec!["Vincent Prouillet".to_string()]);
  215. page3.meta.taxonomies = taxo_page3;
  216. library.insert_page(page3);
  217. let taxonomies = find_taxonomies(&config, &library).unwrap();
  218. let (tags, categories, authors) = {
  219. let mut t = None;
  220. let mut c = None;
  221. let mut a = None;
  222. for x in taxonomies {
  223. match x.kind.name.as_ref() {
  224. "tags" => t = Some(x),
  225. "categories" => c = Some(x),
  226. "authors" => a = Some(x),
  227. _ => unreachable!(),
  228. }
  229. }
  230. (t.unwrap(), c.unwrap(), a.unwrap())
  231. };
  232. assert_eq!(tags.items.len(), 3);
  233. assert_eq!(categories.items.len(), 2);
  234. assert_eq!(authors.items.len(), 1);
  235. assert_eq!(tags.items[0].name, "db");
  236. assert_eq!(tags.items[0].slug, "db");
  237. assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/db/");
  238. assert_eq!(tags.items[0].pages.len(), 1);
  239. assert_eq!(tags.items[1].name, "js");
  240. assert_eq!(tags.items[1].slug, "js");
  241. assert_eq!(tags.items[1].permalink, "http://a-website.com/tags/js/");
  242. assert_eq!(tags.items[1].pages.len(), 2);
  243. assert_eq!(tags.items[2].name, "rust");
  244. assert_eq!(tags.items[2].slug, "rust");
  245. assert_eq!(tags.items[2].permalink, "http://a-website.com/tags/rust/");
  246. assert_eq!(tags.items[2].pages.len(), 2);
  247. assert_eq!(categories.items[0].name, "Other");
  248. assert_eq!(categories.items[0].slug, "other");
  249. assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/other/");
  250. assert_eq!(categories.items[0].pages.len(), 1);
  251. assert_eq!(categories.items[1].name, "Programming tutorials");
  252. assert_eq!(categories.items[1].slug, "programming-tutorials");
  253. assert_eq!(
  254. categories.items[1].permalink,
  255. "http://a-website.com/categories/programming-tutorials/"
  256. );
  257. assert_eq!(categories.items[1].pages.len(), 1);
  258. }
  259. #[test]
  260. fn errors_on_unknown_taxonomy() {
  261. let mut config = Config::default();
  262. let mut library = Library::new(2, 0);
  263. config.taxonomies =
  264. vec![TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() }];
  265. let mut page1 = Page::default();
  266. let mut taxo_page1 = HashMap::new();
  267. taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
  268. page1.meta.taxonomies = taxo_page1;
  269. library.insert_page(page1);
  270. let taxonomies = find_taxonomies(&config, &library);
  271. assert!(taxonomies.is_err());
  272. let err = taxonomies.unwrap_err();
  273. // no path as this is created by Default
  274. assert_eq!(
  275. err.description(),
  276. "Page `` has taxonomy `tags` which is not defined in config.toml"
  277. );
  278. }
  279. }