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.

160 lines
4.8KB

  1. #[macro_use]
  2. extern crate serde_derive;
  3. extern crate tera;
  4. extern crate slug;
  5. extern crate errors;
  6. extern crate config;
  7. extern crate content;
  8. extern crate front_matter;
  9. extern crate utils;
  10. use std::collections::HashMap;
  11. use slug::slugify;
  12. use tera::{Context, Tera};
  13. use config::Config;
  14. use errors::{Result, ResultExt};
  15. use content::{Page, sort_pages};
  16. use front_matter::SortBy;
  17. use utils::templates::render_template;
  18. #[derive(Debug, Copy, Clone, PartialEq)]
  19. pub enum TaxonomyKind {
  20. Tags,
  21. Categories,
  22. }
  23. /// A tag or category
  24. #[derive(Debug, Clone, Serialize, PartialEq)]
  25. pub struct TaxonomyItem {
  26. pub name: String,
  27. pub slug: String,
  28. pub pages: Vec<Page>,
  29. }
  30. impl TaxonomyItem {
  31. pub fn new(name: &str, pages: Vec<Page>) -> TaxonomyItem {
  32. // We shouldn't have any pages without dates there
  33. let (sorted_pages, _) = sort_pages(pages, SortBy::Date);
  34. TaxonomyItem {
  35. name: name.to_string(),
  36. slug: slugify(name),
  37. pages: sorted_pages,
  38. }
  39. }
  40. }
  41. /// All the tags or categories
  42. #[derive(Debug, Clone, PartialEq)]
  43. pub struct Taxonomy {
  44. pub kind: TaxonomyKind,
  45. // this vec is sorted by the count of item
  46. pub items: Vec<TaxonomyItem>,
  47. }
  48. impl Taxonomy {
  49. // TODO: take a Vec<&'a Page> if it makes a difference in terms of perf for actual sites
  50. pub fn find_tags_and_categories(all_pages: &[Page]) -> (Taxonomy, Taxonomy) {
  51. let mut tags = HashMap::new();
  52. let mut categories = HashMap::new();
  53. // Find all the tags/categories first
  54. for page in all_pages {
  55. // Don't consider pages without pages for tags/categories as that's the only thing
  56. // we can sort pages with across sections
  57. // If anyone sees that comment and wonder wtf, please open an issue as I can't think of
  58. // usecases other than blog posts for built-in taxonomies
  59. if page.meta.date.is_none() {
  60. continue;
  61. }
  62. if let Some(ref category) = page.meta.category {
  63. categories
  64. .entry(category.to_string())
  65. .or_insert_with(|| vec![])
  66. .push(page.clone());
  67. }
  68. if let Some(ref t) = page.meta.tags {
  69. for tag in t {
  70. tags
  71. .entry(tag.to_string())
  72. .or_insert_with(|| vec![])
  73. .push(page.clone());
  74. }
  75. }
  76. }
  77. // Then make TaxonomyItem out of them, after sorting it
  78. let tags_taxonomy = Taxonomy::new(TaxonomyKind::Tags, tags);
  79. let categories_taxonomy = Taxonomy::new(TaxonomyKind::Categories, categories);
  80. (tags_taxonomy, categories_taxonomy)
  81. }
  82. fn new(kind: TaxonomyKind, items: HashMap<String, Vec<Page>>) -> Taxonomy {
  83. let mut sorted_items = vec![];
  84. for (name, pages) in &items {
  85. sorted_items.push(
  86. TaxonomyItem::new(name, pages.clone())
  87. );
  88. }
  89. sorted_items.sort_by(|a, b| b.pages.len().cmp(&a.pages.len()));
  90. Taxonomy {
  91. kind,
  92. items: sorted_items,
  93. }
  94. }
  95. pub fn len(&self) -> usize {
  96. self.items.len()
  97. }
  98. pub fn is_empty(&self) -> bool {
  99. self.len() == 0
  100. }
  101. pub fn get_single_item_name(&self) -> String {
  102. match self.kind {
  103. TaxonomyKind::Tags => "tag".to_string(),
  104. TaxonomyKind::Categories => "category".to_string(),
  105. }
  106. }
  107. pub fn get_list_name(&self) -> String {
  108. match self.kind {
  109. TaxonomyKind::Tags => "tags".to_string(),
  110. TaxonomyKind::Categories => "categories".to_string(),
  111. }
  112. }
  113. pub fn render_single_item(&self, item: &TaxonomyItem, tera: &Tera, config: &Config) -> Result<String> {
  114. let name = self.get_single_item_name();
  115. let mut context = Context::new();
  116. context.add("config", config);
  117. context.add(&name, item);
  118. context.add("current_url", &config.make_permalink(&format!("{}/{}", name, item.slug)));
  119. context.add("current_path", &format!("/{}/{}", name, item.slug));
  120. render_template(&format!("{}.html", name), tera, &context, config.theme.clone())
  121. .chain_err(|| format!("Failed to render {} page.", name))
  122. }
  123. pub fn render_list(&self, tera: &Tera, config: &Config) -> Result<String> {
  124. let name = self.get_list_name();
  125. let mut context = Context::new();
  126. context.add("config", config);
  127. context.add(&name, &self.items);
  128. context.add("current_url", &config.make_permalink(&name));
  129. context.add("current_path", &name);
  130. render_template(&format!("{}.html", name), tera, &context, config.theme.clone())
  131. .chain_err(|| format!("Failed to render {} page.", name))
  132. }
  133. }