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.

450 lines
16KB

  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, Error};
  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. pub 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, taxonomy: &TaxonomyConfig, 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 = if let Some(ref lang) = taxonomy.lang {
  59. config.make_permalink(&format!("/{}/{}/{}", lang, taxonomy.name, slug))
  60. } else {
  61. config.make_permalink(&format!("/{}/{}", taxonomy.name, slug))
  62. };
  63. // We still append pages without dates at the end
  64. pages.extend(ignored_pages);
  65. TaxonomyItem { name: name.to_string(), permalink, slug, pages }
  66. }
  67. pub fn serialize<'a>(&'a self, library: &'a Library) -> SerializedTaxonomyItem<'a> {
  68. SerializedTaxonomyItem::from_item(self, library)
  69. }
  70. }
  71. #[derive(Debug, Clone, PartialEq, Serialize)]
  72. pub struct SerializedTaxonomy<'a> {
  73. kind: &'a TaxonomyConfig,
  74. items: Vec<SerializedTaxonomyItem<'a>>,
  75. }
  76. impl<'a> SerializedTaxonomy<'a> {
  77. pub fn from_taxonomy(taxonomy: &'a Taxonomy, library: &'a Library) -> Self {
  78. let items: Vec<SerializedTaxonomyItem> =
  79. taxonomy.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect();
  80. SerializedTaxonomy { kind: &taxonomy.kind, items }
  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(
  92. kind: TaxonomyConfig,
  93. config: &Config,
  94. items: HashMap<String, Vec<Key>>,
  95. library: &Library,
  96. ) -> Taxonomy {
  97. let mut sorted_items = vec![];
  98. for (name, pages) in items {
  99. sorted_items.push(TaxonomyItem::new(&name, &kind, config, pages, library));
  100. }
  101. sorted_items.sort_by(|a, b| a.name.cmp(&b.name));
  102. Taxonomy { kind, items: sorted_items }
  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(
  111. &self,
  112. item: &TaxonomyItem,
  113. tera: &Tera,
  114. config: &Config,
  115. library: &Library,
  116. ) -> Result<String> {
  117. let mut context = Context::new();
  118. context.insert("config", config);
  119. context.insert("term", &SerializedTaxonomyItem::from_item(item, library));
  120. context.insert("taxonomy", &self.kind);
  121. context.insert(
  122. "current_url",
  123. &config.make_permalink(&format!("{}/{}", self.kind.name, item.slug)),
  124. );
  125. context.insert("current_path", &format!("/{}/{}", self.kind.name, item.slug));
  126. render_template(&format!("{}/single.html", self.kind.name), tera, &context, &config.theme)
  127. .map_err(|e| Error::chain(format!("Failed to render single term {} page.", self.kind.name), e))
  128. }
  129. pub fn render_all_terms(
  130. &self,
  131. tera: &Tera,
  132. config: &Config,
  133. library: &Library,
  134. ) -> Result<String> {
  135. let mut context = Context::new();
  136. context.insert("config", config);
  137. let terms: Vec<SerializedTaxonomyItem> =
  138. self.items.iter().map(|i| SerializedTaxonomyItem::from_item(i, library)).collect();
  139. context.insert("terms", &terms);
  140. context.insert("taxonomy", &self.kind);
  141. context.insert("current_url", &config.make_permalink(&self.kind.name));
  142. context.insert("current_path", &self.kind.name);
  143. render_template(&format!("{}/list.html", self.kind.name), tera, &context, &config.theme)
  144. .map_err(|e| Error::chain(format!("Failed to render a list of {} page.", self.kind.name), e))
  145. }
  146. pub fn to_serialized<'a>(&'a self, library: &'a Library) -> SerializedTaxonomy<'a> {
  147. SerializedTaxonomy::from_taxonomy(self, library)
  148. }
  149. }
  150. pub fn find_taxonomies(config: &Config, library: &Library) -> Result<Vec<Taxonomy>> {
  151. let taxonomies_def = {
  152. let mut m = HashMap::new();
  153. for t in &config.taxonomies {
  154. m.insert(t.name.clone(), t);
  155. }
  156. m
  157. };
  158. let mut all_taxonomies = HashMap::new();
  159. for (key, page) in library.pages() {
  160. // Draft are not part of taxonomies
  161. if page.is_draft() {
  162. continue;
  163. }
  164. for (name, val) in &page.meta.taxonomies {
  165. if taxonomies_def.contains_key(name) {
  166. if taxonomies_def[name].lang != page.lang {
  167. bail!(
  168. "Page `{}` has taxonomy `{}` which is not available in that language",
  169. page.file.path.display(),
  170. name
  171. );
  172. }
  173. all_taxonomies.entry(name).or_insert_with(HashMap::new);
  174. for v in val {
  175. all_taxonomies
  176. .get_mut(name)
  177. .unwrap()
  178. .entry(v.to_string())
  179. .or_insert_with(|| vec![])
  180. .push(key);
  181. }
  182. } else {
  183. bail!(
  184. "Page `{}` has taxonomy `{}` which is not defined in config.toml",
  185. page.file.path.display(),
  186. name
  187. );
  188. }
  189. }
  190. }
  191. let mut taxonomies = vec![];
  192. for (name, taxo) in all_taxonomies {
  193. taxonomies.push(Taxonomy::new(taxonomies_def[name].clone(), config, taxo, library));
  194. }
  195. Ok(taxonomies)
  196. }
  197. #[cfg(test)]
  198. mod tests {
  199. use super::*;
  200. use std::collections::HashMap;
  201. use config::{Config, Taxonomy as TaxonomyConfig, Language};
  202. use content::Page;
  203. use library::Library;
  204. #[test]
  205. fn can_make_taxonomies() {
  206. let mut config = Config::default();
  207. let mut library = Library::new(2, 0, false);
  208. config.taxonomies = vec![
  209. TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() },
  210. TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() },
  211. TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() },
  212. ];
  213. let mut page1 = Page::default();
  214. let mut taxo_page1 = HashMap::new();
  215. taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
  216. taxo_page1.insert("categories".to_string(), vec!["Programming tutorials".to_string()]);
  217. page1.meta.taxonomies = taxo_page1;
  218. library.insert_page(page1);
  219. let mut page2 = Page::default();
  220. let mut taxo_page2 = HashMap::new();
  221. taxo_page2.insert("tags".to_string(), vec!["rust".to_string(), "js".to_string()]);
  222. taxo_page2.insert("categories".to_string(), vec!["Other".to_string()]);
  223. page2.meta.taxonomies = taxo_page2;
  224. library.insert_page(page2);
  225. let mut page3 = Page::default();
  226. let mut taxo_page3 = HashMap::new();
  227. taxo_page3.insert("tags".to_string(), vec!["js".to_string()]);
  228. taxo_page3.insert("authors".to_string(), vec!["Vincent Prouillet".to_string()]);
  229. page3.meta.taxonomies = taxo_page3;
  230. library.insert_page(page3);
  231. let taxonomies = find_taxonomies(&config, &library).unwrap();
  232. let (tags, categories, authors) = {
  233. let mut t = None;
  234. let mut c = None;
  235. let mut a = None;
  236. for x in taxonomies {
  237. match x.kind.name.as_ref() {
  238. "tags" => t = Some(x),
  239. "categories" => c = Some(x),
  240. "authors" => a = Some(x),
  241. _ => unreachable!(),
  242. }
  243. }
  244. (t.unwrap(), c.unwrap(), a.unwrap())
  245. };
  246. assert_eq!(tags.items.len(), 3);
  247. assert_eq!(categories.items.len(), 2);
  248. assert_eq!(authors.items.len(), 1);
  249. assert_eq!(tags.items[0].name, "db");
  250. assert_eq!(tags.items[0].slug, "db");
  251. assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/db/");
  252. assert_eq!(tags.items[0].pages.len(), 1);
  253. assert_eq!(tags.items[1].name, "js");
  254. assert_eq!(tags.items[1].slug, "js");
  255. assert_eq!(tags.items[1].permalink, "http://a-website.com/tags/js/");
  256. assert_eq!(tags.items[1].pages.len(), 2);
  257. assert_eq!(tags.items[2].name, "rust");
  258. assert_eq!(tags.items[2].slug, "rust");
  259. assert_eq!(tags.items[2].permalink, "http://a-website.com/tags/rust/");
  260. assert_eq!(tags.items[2].pages.len(), 2);
  261. assert_eq!(categories.items[0].name, "Other");
  262. assert_eq!(categories.items[0].slug, "other");
  263. assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/other/");
  264. assert_eq!(categories.items[0].pages.len(), 1);
  265. assert_eq!(categories.items[1].name, "Programming tutorials");
  266. assert_eq!(categories.items[1].slug, "programming-tutorials");
  267. assert_eq!(
  268. categories.items[1].permalink,
  269. "http://a-website.com/categories/programming-tutorials/"
  270. );
  271. assert_eq!(categories.items[1].pages.len(), 1);
  272. }
  273. #[test]
  274. fn errors_on_unknown_taxonomy() {
  275. let mut config = Config::default();
  276. let mut library = Library::new(2, 0, false);
  277. config.taxonomies =
  278. vec![TaxonomyConfig { name: "authors".to_string(), ..TaxonomyConfig::default() }];
  279. let mut page1 = Page::default();
  280. let mut taxo_page1 = HashMap::new();
  281. taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
  282. page1.meta.taxonomies = taxo_page1;
  283. library.insert_page(page1);
  284. let taxonomies = find_taxonomies(&config, &library);
  285. assert!(taxonomies.is_err());
  286. let err = taxonomies.unwrap_err();
  287. // no path as this is created by Default
  288. assert_eq!(
  289. format!("{}", err),
  290. "Page `` has taxonomy `tags` which is not defined in config.toml"
  291. );
  292. }
  293. #[test]
  294. fn can_make_taxonomies_in_multiple_languages() {
  295. let mut config = Config::default();
  296. config.languages.push(Language {rss: false, code: "fr".to_string()});
  297. let mut library = Library::new(2, 0, true);
  298. config.taxonomies = vec![
  299. TaxonomyConfig { name: "categories".to_string(), ..TaxonomyConfig::default() },
  300. TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() },
  301. TaxonomyConfig { name: "auteurs".to_string(), lang: Some("fr".to_string()), ..TaxonomyConfig::default() },
  302. ];
  303. let mut page1 = Page::default();
  304. let mut taxo_page1 = HashMap::new();
  305. taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
  306. taxo_page1.insert("categories".to_string(), vec!["Programming tutorials".to_string()]);
  307. page1.meta.taxonomies = taxo_page1;
  308. library.insert_page(page1);
  309. let mut page2 = Page::default();
  310. let mut taxo_page2 = HashMap::new();
  311. taxo_page2.insert("tags".to_string(), vec!["rust".to_string()]);
  312. taxo_page2.insert("categories".to_string(), vec!["Other".to_string()]);
  313. page2.meta.taxonomies = taxo_page2;
  314. library.insert_page(page2);
  315. let mut page3 = Page::default();
  316. page3.lang = Some("fr".to_string());
  317. let mut taxo_page3 = HashMap::new();
  318. taxo_page3.insert("auteurs".to_string(), vec!["Vincent Prouillet".to_string()]);
  319. page3.meta.taxonomies = taxo_page3;
  320. library.insert_page(page3);
  321. let taxonomies = find_taxonomies(&config, &library).unwrap();
  322. let (tags, categories, authors) = {
  323. let mut t = None;
  324. let mut c = None;
  325. let mut a = None;
  326. for x in taxonomies {
  327. match x.kind.name.as_ref() {
  328. "tags" => t = Some(x),
  329. "categories" => c = Some(x),
  330. "auteurs" => a = Some(x),
  331. _ => unreachable!(),
  332. }
  333. }
  334. (t.unwrap(), c.unwrap(), a.unwrap())
  335. };
  336. assert_eq!(tags.items.len(), 2);
  337. assert_eq!(categories.items.len(), 2);
  338. assert_eq!(authors.items.len(), 1);
  339. assert_eq!(tags.items[0].name, "db");
  340. assert_eq!(tags.items[0].slug, "db");
  341. assert_eq!(tags.items[0].permalink, "http://a-website.com/tags/db/");
  342. assert_eq!(tags.items[0].pages.len(), 1);
  343. assert_eq!(tags.items[1].name, "rust");
  344. assert_eq!(tags.items[1].slug, "rust");
  345. assert_eq!(tags.items[1].permalink, "http://a-website.com/tags/rust/");
  346. assert_eq!(tags.items[1].pages.len(), 2);
  347. assert_eq!(authors.items[0].name, "Vincent Prouillet");
  348. assert_eq!(authors.items[0].slug, "vincent-prouillet");
  349. assert_eq!(authors.items[0].permalink, "http://a-website.com/fr/auteurs/vincent-prouillet/");
  350. assert_eq!(authors.items[0].pages.len(), 1);
  351. assert_eq!(categories.items[0].name, "Other");
  352. assert_eq!(categories.items[0].slug, "other");
  353. assert_eq!(categories.items[0].permalink, "http://a-website.com/categories/other/");
  354. assert_eq!(categories.items[0].pages.len(), 1);
  355. assert_eq!(categories.items[1].name, "Programming tutorials");
  356. assert_eq!(categories.items[1].slug, "programming-tutorials");
  357. assert_eq!(
  358. categories.items[1].permalink,
  359. "http://a-website.com/categories/programming-tutorials/"
  360. );
  361. assert_eq!(categories.items[1].pages.len(), 1);
  362. }
  363. #[test]
  364. fn errors_on_taxonomy_of_different_language() {
  365. let mut config = Config::default();
  366. config.languages.push(Language {rss: false, code: "fr".to_string()});
  367. let mut library = Library::new(2, 0, false);
  368. config.taxonomies =
  369. vec![TaxonomyConfig { name: "tags".to_string(), ..TaxonomyConfig::default() }];
  370. let mut page1 = Page::default();
  371. page1.lang = Some("fr".to_string());
  372. let mut taxo_page1 = HashMap::new();
  373. taxo_page1.insert("tags".to_string(), vec!["rust".to_string(), "db".to_string()]);
  374. page1.meta.taxonomies = taxo_page1;
  375. library.insert_page(page1);
  376. let taxonomies = find_taxonomies(&config, &library);
  377. assert!(taxonomies.is_err());
  378. let err = taxonomies.unwrap_err();
  379. // no path as this is created by Default
  380. assert_eq!(
  381. format!("{}", err),
  382. "Page `` has taxonomy `tags` which is not available in that language"
  383. );
  384. }
  385. }