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.

496 lines
17KB

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