From c143d95c4e2ca0b773473ec311cfc5cb48a9a1bc Mon Sep 17 00:00:00 2001 From: Sam Vente Date: Tue, 1 Sep 2020 21:00:21 +0200 Subject: [PATCH] Reverse pagination (#1147) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * mention code block output change * Update snap * Update themes gallery (#1082) Co-authored-by: GitHub Action * Deployment guide for Vercel * Change wording a bit * Update themes gallery (#1122) Co-authored-by: GitHub Action * Add feed autodiscovery documentation (#1123) * Add feed autodiscovery documentation * Fix link in template * Docs/configuration update (#1126) * Update configuration documentation - Attempt to split the configuration file into sections to make it more readable and avoid configuration mistakes (#1056). - Move translation instructions to the right part. - Add a bit more explanations to the extra section. * Take into account @Keats feedbacks * Remove short notice about translation usage - A i18n page should be created to better explain it. * add fix for (#1135) Taxonomies with identical slugs now get merged (#1136) * add test and implementation for reverse pagination * incorporate review changes Co-authored-by: Michael Plotke Co-authored-by: Vincent Prouillet Co-authored-by: GitHub Action Co-authored-by: Samyak Bakliwal Co-authored-by: RenĂ© Ribaud --- .gitignore | 1 + components/front_matter/src/lib.rs | 5 +- components/front_matter/src/page.rs | 7 -- components/front_matter/src/section.rs | 4 + components/library/src/content/ser.rs | 5 + components/library/src/pagination/mod.rs | 101 +++++++++++++++--- components/site/tests/site.rs | 18 ++-- docs/content/documentation/content/section.md | 3 + .../getting-started/configuration.md | 1 - test_site/content/reverse-paginated/1.md | 4 + test_site/content/reverse-paginated/2.md | 4 + test_site/content/reverse-paginated/3.md | 4 + test_site/content/reverse-paginated/4.md | 4 + test_site/content/reverse-paginated/5.md | 4 + test_site/content/reverse-paginated/6.md | 4 + test_site/content/reverse-paginated/7.md | 4 + test_site/content/reverse-paginated/8.md | 4 + test_site/content/reverse-paginated/9.md | 4 + test_site/content/reverse-paginated/_index.md | 6 ++ 19 files changed, 154 insertions(+), 33 deletions(-) create mode 100644 test_site/content/reverse-paginated/1.md create mode 100644 test_site/content/reverse-paginated/2.md create mode 100644 test_site/content/reverse-paginated/3.md create mode 100644 test_site/content/reverse-paginated/4.md create mode 100644 test_site/content/reverse-paginated/5.md create mode 100644 test_site/content/reverse-paginated/6.md create mode 100644 test_site/content/reverse-paginated/7.md create mode 100644 test_site/content/reverse-paginated/8.md create mode 100644 test_site/content/reverse-paginated/9.md create mode 100644 test_site/content/reverse-paginated/_index.md diff --git a/.gitignore b/.gitignore index b60977ca..4d71ae82 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ stage shell.nix # vim temporary files **/.*.sw* +.swp diff --git a/components/front_matter/src/lib.rs b/components/front_matter/src/lib.rs index 4890da34..f0866797 100644 --- a/components/front_matter/src/lib.rs +++ b/components/front_matter/src/lib.rs @@ -71,7 +71,10 @@ pub fn split_section_content<'c>( /// Split a file between the front matter and its content /// Returns a parsed `PageFrontMatter` and the rest of the content -pub fn split_page_content<'c>(file_path: &Path, content: &'c str) -> Result<(PageFrontMatter, &'c str)> { +pub fn split_page_content<'c>( + file_path: &Path, + content: &'c str, +) -> Result<(PageFrontMatter, &'c str)> { let (front_matter, content) = split_content(file_path, content)?; let meta = PageFrontMatter::parse(&front_matter).map_err(|e| { Error::chain( diff --git a/components/front_matter/src/page.rs b/components/front_matter/src/page.rs index 044c1c3a..f38f1a2e 100644 --- a/components/front_matter/src/page.rs +++ b/components/front_matter/src/page.rs @@ -37,8 +37,6 @@ pub struct PageFrontMatter { /// Can't be an empty string if present pub path: Option, pub taxonomies: HashMap>, - /// Integer to use to order content. Lowest is at the bottom, highest first - pub order: Option, /// Integer to use to order content. Highest is at the bottom, lowest first pub weight: Option, /// All aliases for that page. Zola will create HTML templates that will @@ -112,10 +110,6 @@ impl PageFrontMatter { self.datetime_tuple = self.datetime.map(|dt| (dt.year(), dt.month(), dt.day())); } - pub fn order(&self) -> usize { - self.order.unwrap() - } - pub fn weight(&self) -> usize { self.weight.unwrap() } @@ -134,7 +128,6 @@ impl Default for PageFrontMatter { slug: None, path: None, taxonomies: HashMap::new(), - order: None, weight: None, aliases: Vec::new(), in_search_index: true, diff --git a/components/front_matter/src/section.rs b/components/front_matter/src/section.rs index a574216a..365dd524 100644 --- a/components/front_matter/src/section.rs +++ b/components/front_matter/src/section.rs @@ -28,6 +28,9 @@ pub struct SectionFrontMatter { /// How many pages to be displayed per paginated page. No pagination will happen if this isn't set #[serde(skip_serializing)] pub paginate_by: Option, + /// Whether to reverse the order of the pages before segmenting into pagers + #[serde(skip_serializing)] + pub paginate_reversed: bool, /// Path to be used by pagination: the page number will be appended after it. Defaults to `page`. #[serde(skip_serializing)] pub paginate_path: String, @@ -100,6 +103,7 @@ impl Default for SectionFrontMatter { weight: 0, template: None, paginate_by: None, + paginate_reversed: false, paginate_path: DEFAULT_PAGINATE_PATH.to_string(), render: true, redirect_to: None, diff --git a/components/library/src/content/ser.rs b/components/library/src/content/ser.rs index 39e6439c..23f8aca1 100644 --- a/components/library/src/content/ser.rs +++ b/components/library/src/content/ser.rs @@ -150,6 +150,11 @@ impl<'a> SerializingPage<'a> { } } + /// currently only used in testing + pub fn get_title(&'a self) -> &'a Option { + &self.title + } + /// Same as from_page but does not fill sibling pages pub fn from_page_basic(page: &'a Page, library: Option<&'a Library>) -> Self { let mut year = None; diff --git a/components/library/src/pagination/mod.rs b/components/library/src/pagination/mod.rs index eda1ebce..0a837d4b 100644 --- a/components/library/src/pagination/mod.rs +++ b/components/library/src/pagination/mod.rs @@ -12,6 +12,8 @@ use crate::content::{Section, SerializingPage, SerializingSection}; use crate::library::Library; use crate::taxonomies::{Taxonomy, TaxonomyItem}; +use std::borrow::Cow; + #[derive(Clone, Debug, PartialEq)] enum PaginationRoot<'a> { Section(&'a Section), @@ -45,11 +47,13 @@ impl<'a> Pager<'a> { #[derive(Clone, Debug, PartialEq)] pub struct Paginator<'a> { /// All pages in the section/taxonomy - all_pages: &'a [DefaultKey], + all_pages: Cow<'a, [DefaultKey]>, /// Pages split in chunks of `paginate_by` pub pagers: Vec>, /// How many content pages on a paginated page at max paginate_by: usize, + /// whether to reverse before grouping + paginate_reversed: bool, /// The thing we are creating the paginator for: section or taxonomy root: PaginationRoot<'a>, // Those below can be obtained from the root but it would make the code more complex than needed @@ -66,10 +70,12 @@ impl<'a> Paginator<'a> { /// It will always at least create one pager (the first) even if there are not enough pages to paginate pub fn from_section(section: &'a Section, library: &'a Library) -> Paginator<'a> { let paginate_by = section.meta.paginate_by.unwrap(); + let paginate_reversed = section.meta.paginate_reversed; let mut paginator = Paginator { - all_pages: §ion.pages, + all_pages: Cow::from(§ion.pages[..]), pagers: Vec::with_capacity(section.pages.len() / paginate_by), paginate_by, + paginate_reversed, root: PaginationRoot::Section(section), permalink: section.permalink.clone(), path: section.path.clone(), @@ -91,9 +97,10 @@ impl<'a> Paginator<'a> { ) -> Paginator<'a> { let paginate_by = taxonomy.kind.paginate_by.unwrap(); let mut paginator = Paginator { - all_pages: &item.pages, + all_pages: Cow::Borrowed(&item.pages), pagers: Vec::with_capacity(item.pages.len() / paginate_by), paginate_by, + paginate_reversed: false, root: PaginationRoot::Taxonomy(taxonomy, item), permalink: item.permalink.clone(), path: format!("/{}/{}/", taxonomy.kind.name, item.slug), @@ -106,6 +113,7 @@ impl<'a> Paginator<'a> { template: format!("{}/single.html", taxonomy.kind.name), }; + // taxonomy paginators have no sorting so we won't have to reverse paginator.fill_pagers(library); paginator } @@ -116,8 +124,12 @@ impl<'a> Paginator<'a> { // the pages in the current pagers let mut current_page = vec![]; - for key in self.all_pages { - let page = library.get_page_by_key(*key); + if self.paginate_reversed { + self.all_pages.to_mut().reverse(); + } + + for key in self.all_pages.to_mut().iter_mut() { + let page = library.get_page_by_key(key.clone()); current_page.push(page.to_serialized_basic(library)); if current_page.len() == self.paginate_by { @@ -246,10 +258,11 @@ mod tests { use super::Paginator; - fn create_section(is_index: bool) -> Section { + fn create_section(is_index: bool, paginate_reversed: bool) -> Section { let mut f = SectionFrontMatter::default(); f.paginate_by = Some(2); f.paginate_path = "page".to_string(); + f.paginate_reversed = paginate_reversed; let mut s = Section::new("content/_index.md", f, &PathBuf::new()); if !is_index { s.path = "/posts/".to_string(); @@ -262,15 +275,22 @@ mod tests { s } - fn create_library(is_index: bool) -> (Section, Library) { - let mut library = Library::new(3, 0, false); - library.insert_page(Page::default()); - library.insert_page(Page::default()); - library.insert_page(Page::default()); + fn create_library( + is_index: bool, + num_pages: usize, + paginate_reversed: bool, + ) -> (Section, Library) { + let mut library = Library::new(num_pages, 0, false); + for i in 1..=num_pages { + let mut page = Page::default(); + page.meta.title = Some(i.to_string()); + library.insert_page(page); + } + let mut draft = Page::default(); draft.meta.draft = true; library.insert_page(draft); - let mut section = create_section(is_index); + let mut section = create_section(is_index, paginate_reversed); section.pages = library.pages().keys().collect(); library.insert_section(section.clone()); @@ -279,7 +299,7 @@ mod tests { #[test] fn test_can_create_paginator() { - let (section, library) = create_library(false); + let (section, library) = create_library(false, 3, false); let paginator = Paginator::from_section(§ion, &library); assert_eq!(paginator.pagers.len(), 2); @@ -294,9 +314,56 @@ mod tests { assert_eq!(paginator.pagers[1].path, "/posts/page/2/"); } + #[test] + fn test_can_create_reversed_paginator() { + // 6 pages, 5 normal and 1 draft + let (section, library) = create_library(false, 5, true); + let paginator = Paginator::from_section(§ion, &library); + assert_eq!(paginator.pagers.len(), 3); + + assert_eq!(paginator.pagers[0].index, 1); + assert_eq!(paginator.pagers[0].pages.len(), 2); + assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/posts/"); + assert_eq!(paginator.pagers[0].path, "/posts/"); + assert_eq!( + vec!["".to_string(), "5".to_string()], + paginator.pagers[0] + .pages + .iter() + .map(|p| p.get_title().as_ref().unwrap_or(&"".to_string()).to_string()) + .collect::>() + ); + + assert_eq!(paginator.pagers[1].index, 2); + assert_eq!(paginator.pagers[1].pages.len(), 2); + assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/posts/page/2/"); + assert_eq!(paginator.pagers[1].path, "/posts/page/2/"); + assert_eq!( + vec!["4".to_string(), "3".to_string()], + paginator.pagers[1] + .pages + .iter() + .map(|p| p.get_title().as_ref().unwrap_or(&"".to_string()).to_string()) + .collect::>() + ); + + assert_eq!(paginator.pagers[2].index, 3); + assert_eq!(paginator.pagers[2].pages.len(), 2); + assert_eq!(paginator.pagers[2].permalink, "https://vincent.is/posts/page/3/"); + assert_eq!(paginator.pagers[2].path, "/posts/page/3/"); + assert_eq!( + vec!["2".to_string(), "1".to_string()], + paginator.pagers[2] + .pages + .iter() + .map(|p| p.get_title().as_ref().unwrap_or(&"".to_string()).to_string()) + .collect::>() + ); + } + #[test] fn test_can_create_paginator_for_index() { - let (section, library) = create_library(true); + let (section, library) = create_library(true, 3, false); let paginator = Paginator::from_section(§ion, &library); assert_eq!(paginator.pagers.len(), 2); @@ -313,7 +380,7 @@ mod tests { #[test] fn test_can_build_paginator_context() { - let (section, library) = create_library(false); + let (section, library) = create_library(false, 3, false); let paginator = Paginator::from_section(§ion, &library); assert_eq!(paginator.pagers.len(), 2); @@ -337,7 +404,7 @@ mod tests { #[test] fn test_can_create_paginator_for_taxonomy() { - let (_, library) = create_library(false); + let (_, library) = create_library(false, 3, false); let taxonomy_def = TaxonomyConfig { name: "tags".to_string(), paginate_by: Some(2), @@ -367,7 +434,7 @@ mod tests { // https://github.com/getzola/zola/issues/866 #[test] fn works_with_empty_paginate_path() { - let (mut section, library) = create_library(false); + let (mut section, library) = create_library(false, 3, false); section.meta.paginate_path = String::new(); let paginator = Paginator::from_section(§ion, &library); assert_eq!(paginator.pagers.len(), 2); diff --git a/components/site/tests/site.rs b/components/site/tests/site.rs index c7419d8e..1778f408 100644 --- a/components/site/tests/site.rs +++ b/components/site/tests/site.rs @@ -19,7 +19,7 @@ fn can_parse_site() { let library = site.library.read().unwrap(); // Correct number of pages (sections do not count as pages, draft are ignored) - assert_eq!(library.pages().len(), 23); + assert_eq!(library.pages().len(), 32); let posts_path = path.join("content").join("posts"); // Make sure the page with a url doesn't have any sections @@ -32,11 +32,11 @@ fn can_parse_site() { assert_eq!(asset_folder_post.file.components, vec!["posts".to_string()]); // That we have the right number of sections - assert_eq!(library.sections().len(), 11); + assert_eq!(library.sections().len(), 12); // And that the sections are correct let index_section = library.get_section(&path.join("content").join("_index.md")).unwrap(); - assert_eq!(index_section.subsections.len(), 4); + assert_eq!(index_section.subsections.len(), 5); assert_eq!(index_section.pages.len(), 3); assert!(index_section.ancestors.is_empty()); @@ -582,7 +582,7 @@ fn can_build_site_with_pagination_for_taxonomy() { "tags/a/page/1/index.html", "http-equiv=\"refresh\" content=\"0; url=https://replace-this-with-your-url.com/tags/a/\"" )); - assert!(file_contains!(public, "tags/a/index.html", "Num pagers: 6")); + assert!(file_contains!(public, "tags/a/index.html", "Num pagers: 8")); assert!(file_contains!(public, "tags/a/index.html", "Page size: 2")); assert!(file_contains!(public, "tags/a/index.html", "Current index: 1")); assert!(!file_contains!(public, "tags/a/index.html", "has_prev")); @@ -595,7 +595,7 @@ fn can_build_site_with_pagination_for_taxonomy() { assert!(file_contains!( public, "tags/a/index.html", - "Last: https://replace-this-with-your-url.com/tags/a/page/6/" + "Last: https://replace-this-with-your-url.com/tags/a/page/8/" )); assert_eq!(file_contains!(public, "tags/a/index.html", "has_prev"), false); @@ -603,7 +603,7 @@ fn can_build_site_with_pagination_for_taxonomy() { assert!(file_contains!( public, "sitemap.xml", - "https://replace-this-with-your-url.com/tags/a/page/6/" + "https://replace-this-with-your-url.com/tags/a/page/8/" )); // current_path @@ -721,7 +721,11 @@ fn can_build_site_with_html_minified() { assert!(&public.exists()); assert!(file_exists!(public, "index.html")); - assert!(file_contains!(public, "index.html", "")); + assert!(file_contains!( + public, + "index.html", + "" + )); } #[test] diff --git a/docs/content/documentation/content/section.md b/docs/content/documentation/content/section.md index 2d6952b1..80003aea 100644 --- a/docs/content/documentation/content/section.md +++ b/docs/content/documentation/content/section.md @@ -156,6 +156,7 @@ This will be sort all pages by their `weight` field, from lightest weight page gets `page.lighter` and `page.heavier` variables that contain the pages with lighter and heavier weights, respectively. +### Reversed sorting When iterating through pages, you may wish to use the Tera `reverse` filter, which reverses the order of the pages. For example, after using the `reverse` filter, pages sorted by weight will be sorted from lightest (at the top) to heaviest @@ -164,6 +165,8 @@ to newest (at the bottom). `reverse` has no effect on `page.later`/`page.earlier` or `page.heavier`/`page.lighter`. +If the section is paginated the `paginate_reversed=true` in the front matter of the relevant section should be set instead of using the filter. + ## Sorting subsections Sorting sections is a bit less flexible: sections can only be sorted by `weight`, and do not have variables that point to the heavier/lighter sections. diff --git a/docs/content/documentation/getting-started/configuration.md b/docs/content/documentation/getting-started/configuration.md index e1096074..102cd143 100644 --- a/docs/content/documentation/getting-started/configuration.md +++ b/docs/content/documentation/getting-started/configuration.md @@ -132,7 +132,6 @@ include_content = true # become too big to load on the site. Defaults to not being set. # truncate_content_length = 100 - # Optional translation object. Keys should be language codes. # Optional translation object. The key if present should be a language code. # Example: diff --git a/test_site/content/reverse-paginated/1.md b/test_site/content/reverse-paginated/1.md new file mode 100644 index 00000000..a2e08759 --- /dev/null +++ b/test_site/content/reverse-paginated/1.md @@ -0,0 +1,4 @@ ++++ +title="Page number: 1" +weight=1 ++++ diff --git a/test_site/content/reverse-paginated/2.md b/test_site/content/reverse-paginated/2.md new file mode 100644 index 00000000..f66a1858 --- /dev/null +++ b/test_site/content/reverse-paginated/2.md @@ -0,0 +1,4 @@ ++++ +title="Page number: 2" +weight=2 ++++ diff --git a/test_site/content/reverse-paginated/3.md b/test_site/content/reverse-paginated/3.md new file mode 100644 index 00000000..2afbe1b5 --- /dev/null +++ b/test_site/content/reverse-paginated/3.md @@ -0,0 +1,4 @@ ++++ +title="Page number: 3" +weight=3 ++++ diff --git a/test_site/content/reverse-paginated/4.md b/test_site/content/reverse-paginated/4.md new file mode 100644 index 00000000..d09d3d28 --- /dev/null +++ b/test_site/content/reverse-paginated/4.md @@ -0,0 +1,4 @@ ++++ +title="Page number: 4" +weight=4 ++++ diff --git a/test_site/content/reverse-paginated/5.md b/test_site/content/reverse-paginated/5.md new file mode 100644 index 00000000..623f36fc --- /dev/null +++ b/test_site/content/reverse-paginated/5.md @@ -0,0 +1,4 @@ ++++ +title="Page number: 5" +weight=5 ++++ diff --git a/test_site/content/reverse-paginated/6.md b/test_site/content/reverse-paginated/6.md new file mode 100644 index 00000000..920d2516 --- /dev/null +++ b/test_site/content/reverse-paginated/6.md @@ -0,0 +1,4 @@ ++++ +title="Page number: 6" +weight=6 ++++ diff --git a/test_site/content/reverse-paginated/7.md b/test_site/content/reverse-paginated/7.md new file mode 100644 index 00000000..ac812d5a --- /dev/null +++ b/test_site/content/reverse-paginated/7.md @@ -0,0 +1,4 @@ ++++ +title="Page number: 7" +weight=7 ++++ diff --git a/test_site/content/reverse-paginated/8.md b/test_site/content/reverse-paginated/8.md new file mode 100644 index 00000000..ab656c56 --- /dev/null +++ b/test_site/content/reverse-paginated/8.md @@ -0,0 +1,4 @@ ++++ +title="Page number: 8" +weight=8 ++++ diff --git a/test_site/content/reverse-paginated/9.md b/test_site/content/reverse-paginated/9.md new file mode 100644 index 00000000..f21f579b --- /dev/null +++ b/test_site/content/reverse-paginated/9.md @@ -0,0 +1,4 @@ ++++ +title="Page number: 9" +weight=9 ++++ diff --git a/test_site/content/reverse-paginated/_index.md b/test_site/content/reverse-paginated/_index.md new file mode 100644 index 00000000..e31aac22 --- /dev/null +++ b/test_site/content/reverse-paginated/_index.md @@ -0,0 +1,6 @@ ++++ +paginate_by = 2 +template = "section_paginated.html" +sort_by = "weight" +paginate_reversed = true ++++