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.

392 lines
15KB

  1. use std::collections::HashMap;
  2. use serde_derive::Serialize;
  3. use slotmap::DefaultKey;
  4. use tera::{to_value, Context, Tera, Value};
  5. use config::Config;
  6. use errors::{Error, Result};
  7. use utils::templates::render_template;
  8. use crate::content::{Section, SerializingPage, SerializingSection};
  9. use crate::library::Library;
  10. use crate::taxonomies::{Taxonomy, TaxonomyItem};
  11. #[derive(Clone, Debug, PartialEq)]
  12. enum PaginationRoot<'a> {
  13. Section(&'a Section),
  14. Taxonomy(&'a Taxonomy, &'a TaxonomyItem),
  15. }
  16. /// A list of all the pages in the paginator with their index and links
  17. #[derive(Clone, Debug, PartialEq, Serialize)]
  18. pub struct Pager<'a> {
  19. /// The page number in the paginator (1-indexed)
  20. pub index: usize,
  21. /// Permalink to that page
  22. permalink: String,
  23. /// Path to that page
  24. path: String,
  25. /// All pages for the pager
  26. pages: Vec<SerializingPage<'a>>,
  27. }
  28. impl<'a> Pager<'a> {
  29. fn new(
  30. index: usize,
  31. pages: Vec<SerializingPage<'a>>,
  32. permalink: String,
  33. path: String,
  34. ) -> Pager<'a> {
  35. Pager { index, permalink, path, pages }
  36. }
  37. }
  38. #[derive(Clone, Debug, PartialEq)]
  39. pub struct Paginator<'a> {
  40. /// All pages in the section/taxonomy
  41. all_pages: &'a [DefaultKey],
  42. /// Pages split in chunks of `paginate_by`
  43. pub pagers: Vec<Pager<'a>>,
  44. /// How many content pages on a paginated page at max
  45. paginate_by: usize,
  46. /// The thing we are creating the paginator for: section or taxonomy
  47. root: PaginationRoot<'a>,
  48. // Those below can be obtained from the root but it would make the code more complex than needed
  49. pub permalink: String,
  50. path: String,
  51. pub paginate_path: String,
  52. template: String,
  53. /// Whether this is the index section, we need it for the template name
  54. is_index: bool,
  55. }
  56. impl<'a> Paginator<'a> {
  57. /// Create a new paginator from a section
  58. /// It will always at least create one pager (the first) even if there are not enough pages to paginate
  59. pub fn from_section(section: &'a Section, library: &'a Library) -> Paginator<'a> {
  60. let paginate_by = section.meta.paginate_by.unwrap();
  61. let mut paginator = Paginator {
  62. all_pages: &section.pages,
  63. pagers: Vec::with_capacity(section.pages.len() / paginate_by),
  64. paginate_by,
  65. root: PaginationRoot::Section(section),
  66. permalink: section.permalink.clone(),
  67. path: section.path.clone(),
  68. paginate_path: section.meta.paginate_path.clone(),
  69. is_index: section.is_index(),
  70. template: section.get_template_name().to_string(),
  71. };
  72. paginator.fill_pagers(library);
  73. paginator
  74. }
  75. /// Create a new paginator from a taxonomy
  76. /// It will always at least create one pager (the first) even if there are not enough pages to paginate
  77. pub fn from_taxonomy(
  78. taxonomy: &'a Taxonomy,
  79. item: &'a TaxonomyItem,
  80. library: &'a Library,
  81. ) -> Paginator<'a> {
  82. let paginate_by = taxonomy.kind.paginate_by.unwrap();
  83. let mut paginator = Paginator {
  84. all_pages: &item.pages,
  85. pagers: Vec::with_capacity(item.pages.len() / paginate_by),
  86. paginate_by,
  87. root: PaginationRoot::Taxonomy(taxonomy, item),
  88. permalink: item.permalink.clone(),
  89. path: format!("{}/{}", taxonomy.kind.name, item.slug),
  90. paginate_path: taxonomy
  91. .kind
  92. .paginate_path
  93. .clone()
  94. .unwrap_or_else(|| "page".to_string()),
  95. is_index: false,
  96. template: format!("{}/single.html", taxonomy.kind.name),
  97. };
  98. paginator.fill_pagers(library);
  99. paginator
  100. }
  101. fn fill_pagers(&mut self, library: &'a Library) {
  102. // the list of pagers
  103. let mut pages = vec![];
  104. // the pages in the current pagers
  105. let mut current_page = vec![];
  106. for key in self.all_pages {
  107. let page = library.get_page_by_key(*key);
  108. current_page.push(page.to_serialized_basic(library));
  109. if current_page.len() == self.paginate_by {
  110. pages.push(current_page);
  111. current_page = vec![];
  112. }
  113. }
  114. if !current_page.is_empty() {
  115. pages.push(current_page);
  116. }
  117. let mut pagers = vec![];
  118. for (index, page) in pages.into_iter().enumerate() {
  119. // First page has no pagination path
  120. if index == 0 {
  121. pagers.push(Pager::new(1, page, self.permalink.clone(), self.path.clone()));
  122. continue;
  123. }
  124. let page_path = if self.paginate_path.is_empty() {
  125. format!("{}/", index + 1)
  126. } else {
  127. format!("{}/{}/", self.paginate_path, index + 1)
  128. };
  129. let permalink = format!("{}{}", self.permalink, page_path);
  130. let pager_path = if self.is_index {
  131. page_path
  132. } else if self.path.ends_with('/') {
  133. format!("{}{}", self.path, page_path)
  134. } else {
  135. format!("{}/{}", self.path, page_path)
  136. };
  137. pagers.push(Pager::new(index + 1, page, permalink, pager_path));
  138. }
  139. // We always have the index one at least
  140. if pagers.is_empty() {
  141. pagers.push(Pager::new(1, vec![], self.permalink.clone(), self.path.clone()));
  142. }
  143. self.pagers = pagers;
  144. }
  145. pub fn build_paginator_context(&self, current_pager: &Pager) -> HashMap<&str, Value> {
  146. let mut paginator = HashMap::new();
  147. // the pager index is 1-indexed so we want a 0-indexed one for indexing there
  148. let pager_index = current_pager.index - 1;
  149. // Global variables
  150. paginator.insert("paginate_by", to_value(self.paginate_by).unwrap());
  151. paginator.insert("first", to_value(&self.permalink).unwrap());
  152. let last_pager = &self.pagers[self.pagers.len() - 1];
  153. paginator.insert("last", to_value(&last_pager.permalink).unwrap());
  154. // Variables for this specific page
  155. if pager_index > 0 {
  156. let prev_pager = &self.pagers[pager_index - 1];
  157. paginator.insert("previous", to_value(&prev_pager.permalink).unwrap());
  158. } else {
  159. paginator.insert("previous", Value::Null);
  160. }
  161. if pager_index < self.pagers.len() - 1 {
  162. let next_pager = &self.pagers[pager_index + 1];
  163. paginator.insert("next", to_value(&next_pager.permalink).unwrap());
  164. } else {
  165. paginator.insert("next", Value::Null);
  166. }
  167. paginator.insert("number_pagers", to_value(&self.pagers.len()).unwrap());
  168. let base_url = if self.paginate_path.is_empty() {
  169. self.permalink.to_string()
  170. } else {
  171. format!("{}{}/", self.permalink, self.paginate_path)
  172. };
  173. paginator.insert(
  174. "base_url",
  175. to_value(&base_url).unwrap(),
  176. );
  177. paginator.insert("pages", to_value(&current_pager.pages).unwrap());
  178. paginator.insert("current_index", to_value(current_pager.index).unwrap());
  179. paginator.insert("total_pages", to_value(self.all_pages.len()).unwrap());
  180. paginator
  181. }
  182. pub fn render_pager(
  183. &self,
  184. pager: &Pager,
  185. config: &Config,
  186. tera: &Tera,
  187. library: &Library,
  188. ) -> Result<String> {
  189. let mut context = Context::new();
  190. context.insert("config", &config);
  191. match self.root {
  192. PaginationRoot::Section(s) => {
  193. context
  194. .insert("section", &SerializingSection::from_section_basic(s, Some(library)));
  195. context.insert("lang", &s.lang);
  196. }
  197. PaginationRoot::Taxonomy(t, item) => {
  198. context.insert("taxonomy", &t.kind);
  199. context.insert("term", &item.serialize(library));
  200. context.insert("lang", &t.kind.lang);
  201. }
  202. };
  203. context.insert("current_url", &pager.permalink);
  204. context.insert("current_path", &pager.path);
  205. context.insert("paginator", &self.build_paginator_context(pager));
  206. render_template(&self.template, tera, context, &config.theme)
  207. .map_err(|e| Error::chain(format!("Failed to render pager {}", pager.index), e))
  208. }
  209. }
  210. #[cfg(test)]
  211. mod tests {
  212. use std::path::PathBuf;
  213. use tera::to_value;
  214. use crate::content::{Page, Section};
  215. use crate::library::Library;
  216. use crate::taxonomies::{Taxonomy, TaxonomyItem};
  217. use config::Taxonomy as TaxonomyConfig;
  218. use front_matter::SectionFrontMatter;
  219. use super::Paginator;
  220. fn create_section(is_index: bool) -> Section {
  221. let mut f = SectionFrontMatter::default();
  222. f.paginate_by = Some(2);
  223. f.paginate_path = "page".to_string();
  224. let mut s = Section::new("content/_index.md", f, &PathBuf::new());
  225. if !is_index {
  226. s.path = "posts/".to_string();
  227. s.permalink = "https://vincent.is/posts/".to_string();
  228. s.file.components = vec!["posts".to_string()];
  229. } else {
  230. s.permalink = "https://vincent.is/".to_string();
  231. }
  232. s
  233. }
  234. fn create_library(is_index: bool) -> (Section, Library) {
  235. let mut library = Library::new(3, 0, false);
  236. library.insert_page(Page::default());
  237. library.insert_page(Page::default());
  238. library.insert_page(Page::default());
  239. let mut draft = Page::default();
  240. draft.meta.draft = true;
  241. library.insert_page(draft);
  242. let mut section = create_section(is_index);
  243. section.pages = library.pages().keys().collect();
  244. library.insert_section(section.clone());
  245. (section, library)
  246. }
  247. #[test]
  248. fn test_can_create_paginator() {
  249. let (section, library) = create_library(false);
  250. let paginator = Paginator::from_section(&section, &library);
  251. assert_eq!(paginator.pagers.len(), 2);
  252. assert_eq!(paginator.pagers[0].index, 1);
  253. assert_eq!(paginator.pagers[0].pages.len(), 2);
  254. assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/posts/");
  255. assert_eq!(paginator.pagers[0].path, "posts/");
  256. assert_eq!(paginator.pagers[1].index, 2);
  257. assert_eq!(paginator.pagers[1].pages.len(), 2);
  258. assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/posts/page/2/");
  259. assert_eq!(paginator.pagers[1].path, "posts/page/2/");
  260. }
  261. #[test]
  262. fn test_can_create_paginator_for_index() {
  263. let (section, library) = create_library(true);
  264. let paginator = Paginator::from_section(&section, &library);
  265. assert_eq!(paginator.pagers.len(), 2);
  266. assert_eq!(paginator.pagers[0].index, 1);
  267. assert_eq!(paginator.pagers[0].pages.len(), 2);
  268. assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/");
  269. assert_eq!(paginator.pagers[0].path, "");
  270. assert_eq!(paginator.pagers[1].index, 2);
  271. assert_eq!(paginator.pagers[1].pages.len(), 2);
  272. assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/page/2/");
  273. assert_eq!(paginator.pagers[1].path, "page/2/");
  274. }
  275. #[test]
  276. fn test_can_build_paginator_context() {
  277. let (section, library) = create_library(false);
  278. let paginator = Paginator::from_section(&section, &library);
  279. assert_eq!(paginator.pagers.len(), 2);
  280. let context = paginator.build_paginator_context(&paginator.pagers[0]);
  281. assert_eq!(context["paginate_by"], to_value(2).unwrap());
  282. assert_eq!(context["first"], to_value("https://vincent.is/posts/").unwrap());
  283. assert_eq!(context["last"], to_value("https://vincent.is/posts/page/2/").unwrap());
  284. assert_eq!(context["previous"], to_value::<Option<()>>(None).unwrap());
  285. assert_eq!(context["next"], to_value("https://vincent.is/posts/page/2/").unwrap());
  286. assert_eq!(context["current_index"], to_value(1).unwrap());
  287. let context = paginator.build_paginator_context(&paginator.pagers[1]);
  288. assert_eq!(context["paginate_by"], to_value(2).unwrap());
  289. assert_eq!(context["first"], to_value("https://vincent.is/posts/").unwrap());
  290. assert_eq!(context["last"], to_value("https://vincent.is/posts/page/2/").unwrap());
  291. assert_eq!(context["next"], to_value::<Option<()>>(None).unwrap());
  292. assert_eq!(context["previous"], to_value("https://vincent.is/posts/").unwrap());
  293. assert_eq!(context["current_index"], to_value(2).unwrap());
  294. assert_eq!(context["total_pages"], to_value(4).unwrap());
  295. }
  296. #[test]
  297. fn test_can_create_paginator_for_taxonomy() {
  298. let (_, library) = create_library(false);
  299. let taxonomy_def = TaxonomyConfig {
  300. name: "tags".to_string(),
  301. paginate_by: Some(2),
  302. ..TaxonomyConfig::default()
  303. };
  304. let taxonomy_item = TaxonomyItem {
  305. name: "Something".to_string(),
  306. slug: "something".to_string(),
  307. permalink: "https://vincent.is/tags/something/".to_string(),
  308. pages: library.pages().keys().collect(),
  309. };
  310. let taxonomy = Taxonomy { kind: taxonomy_def, items: vec![taxonomy_item.clone()] };
  311. let paginator = Paginator::from_taxonomy(&taxonomy, &taxonomy_item, &library);
  312. assert_eq!(paginator.pagers.len(), 2);
  313. assert_eq!(paginator.pagers[0].index, 1);
  314. assert_eq!(paginator.pagers[0].pages.len(), 2);
  315. assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/tags/something/");
  316. assert_eq!(paginator.pagers[0].path, "tags/something");
  317. assert_eq!(paginator.pagers[1].index, 2);
  318. assert_eq!(paginator.pagers[1].pages.len(), 2);
  319. assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/tags/something/page/2/");
  320. assert_eq!(paginator.pagers[1].path, "tags/something/page/2/");
  321. }
  322. // https://github.com/getzola/zola/issues/866
  323. #[test]
  324. fn works_with_empty_paginate_path() {
  325. let (mut section, library) = create_library(false);
  326. section.meta.paginate_path = String::new();
  327. let paginator = Paginator::from_section(&section, &library);
  328. assert_eq!(paginator.pagers.len(), 2);
  329. assert_eq!(paginator.pagers[0].index, 1);
  330. assert_eq!(paginator.pagers[0].pages.len(), 2);
  331. assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/posts/");
  332. assert_eq!(paginator.pagers[0].path, "posts/");
  333. assert_eq!(paginator.pagers[1].index, 2);
  334. assert_eq!(paginator.pagers[1].pages.len(), 2);
  335. assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/posts/2/");
  336. assert_eq!(paginator.pagers[1].path, "posts/2/");
  337. let context = paginator.build_paginator_context(&paginator.pagers[0]);
  338. assert_eq!(context["base_url"], to_value("https://vincent.is/posts/").unwrap());
  339. }
  340. }