Reverse pagination (#1147)

* mention code block output change

* Update snap

* Update themes gallery (#1082)

Co-authored-by: GitHub Action <action@github.com>

* Deployment guide for Vercel

* Change wording a bit

* Update themes gallery (#1122)

Co-authored-by: GitHub Action <action@github.com>

* 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 <bdjnks@gmail.com>
Co-authored-by: Vincent Prouillet <balthek@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Samyak Bakliwal <w3bcode@gmail.com>
Co-authored-by: René Ribaud <uggla@free.fr>
This commit is contained in:
Sam Vente 2020-09-01 21:00:21 +02:00 committed by GitHub
parent 5ec3a9ca65
commit c143d95c4e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 154 additions and 33 deletions

1
.gitignore vendored
View file

@ -27,3 +27,4 @@ stage
shell.nix shell.nix
# vim temporary files # vim temporary files
**/.*.sw* **/.*.sw*
.swp

View file

@ -71,7 +71,10 @@ pub fn split_section_content<'c>(
/// Split a file between the front matter and its content /// Split a file between the front matter and its content
/// Returns a parsed `PageFrontMatter` and the rest of the 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 (front_matter, content) = split_content(file_path, content)?;
let meta = PageFrontMatter::parse(&front_matter).map_err(|e| { let meta = PageFrontMatter::parse(&front_matter).map_err(|e| {
Error::chain( Error::chain(

View file

@ -37,8 +37,6 @@ pub struct PageFrontMatter {
/// Can't be an empty string if present /// Can't be an empty string if present
pub path: Option<String>, pub path: Option<String>,
pub taxonomies: HashMap<String, Vec<String>>, pub taxonomies: HashMap<String, Vec<String>>,
/// Integer to use to order content. Lowest is at the bottom, highest first
pub order: Option<usize>,
/// Integer to use to order content. Highest is at the bottom, lowest first /// Integer to use to order content. Highest is at the bottom, lowest first
pub weight: Option<usize>, pub weight: Option<usize>,
/// All aliases for that page. Zola will create HTML templates that will /// 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())); 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 { pub fn weight(&self) -> usize {
self.weight.unwrap() self.weight.unwrap()
} }
@ -134,7 +128,6 @@ impl Default for PageFrontMatter {
slug: None, slug: None,
path: None, path: None,
taxonomies: HashMap::new(), taxonomies: HashMap::new(),
order: None,
weight: None, weight: None,
aliases: Vec::new(), aliases: Vec::new(),
in_search_index: true, in_search_index: true,

View file

@ -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 /// How many pages to be displayed per paginated page. No pagination will happen if this isn't set
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub paginate_by: Option<usize>, pub paginate_by: Option<usize>,
/// 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`. /// Path to be used by pagination: the page number will be appended after it. Defaults to `page`.
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub paginate_path: String, pub paginate_path: String,
@ -100,6 +103,7 @@ impl Default for SectionFrontMatter {
weight: 0, weight: 0,
template: None, template: None,
paginate_by: None, paginate_by: None,
paginate_reversed: false,
paginate_path: DEFAULT_PAGINATE_PATH.to_string(), paginate_path: DEFAULT_PAGINATE_PATH.to_string(),
render: true, render: true,
redirect_to: None, redirect_to: None,

View file

@ -150,6 +150,11 @@ impl<'a> SerializingPage<'a> {
} }
} }
/// currently only used in testing
pub fn get_title(&'a self) -> &'a Option<String> {
&self.title
}
/// Same as from_page but does not fill sibling pages /// Same as from_page but does not fill sibling pages
pub fn from_page_basic(page: &'a Page, library: Option<&'a Library>) -> Self { pub fn from_page_basic(page: &'a Page, library: Option<&'a Library>) -> Self {
let mut year = None; let mut year = None;

View file

@ -12,6 +12,8 @@ use crate::content::{Section, SerializingPage, SerializingSection};
use crate::library::Library; use crate::library::Library;
use crate::taxonomies::{Taxonomy, TaxonomyItem}; use crate::taxonomies::{Taxonomy, TaxonomyItem};
use std::borrow::Cow;
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
enum PaginationRoot<'a> { enum PaginationRoot<'a> {
Section(&'a Section), Section(&'a Section),
@ -45,11 +47,13 @@ impl<'a> Pager<'a> {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct Paginator<'a> { pub struct Paginator<'a> {
/// All pages in the section/taxonomy /// All pages in the section/taxonomy
all_pages: &'a [DefaultKey], all_pages: Cow<'a, [DefaultKey]>,
/// Pages split in chunks of `paginate_by` /// Pages split in chunks of `paginate_by`
pub pagers: Vec<Pager<'a>>, pub pagers: Vec<Pager<'a>>,
/// How many content pages on a paginated page at max /// How many content pages on a paginated page at max
paginate_by: usize, paginate_by: usize,
/// whether to reverse before grouping
paginate_reversed: bool,
/// The thing we are creating the paginator for: section or taxonomy /// The thing we are creating the paginator for: section or taxonomy
root: PaginationRoot<'a>, root: PaginationRoot<'a>,
// Those below can be obtained from the root but it would make the code more complex than needed // 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 /// 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> { pub fn from_section(section: &'a Section, library: &'a Library) -> Paginator<'a> {
let paginate_by = section.meta.paginate_by.unwrap(); let paginate_by = section.meta.paginate_by.unwrap();
let paginate_reversed = section.meta.paginate_reversed;
let mut paginator = Paginator { let mut paginator = Paginator {
all_pages: &section.pages, all_pages: Cow::from(&section.pages[..]),
pagers: Vec::with_capacity(section.pages.len() / paginate_by), pagers: Vec::with_capacity(section.pages.len() / paginate_by),
paginate_by, paginate_by,
paginate_reversed,
root: PaginationRoot::Section(section), root: PaginationRoot::Section(section),
permalink: section.permalink.clone(), permalink: section.permalink.clone(),
path: section.path.clone(), path: section.path.clone(),
@ -91,9 +97,10 @@ impl<'a> Paginator<'a> {
) -> Paginator<'a> { ) -> Paginator<'a> {
let paginate_by = taxonomy.kind.paginate_by.unwrap(); let paginate_by = taxonomy.kind.paginate_by.unwrap();
let mut paginator = Paginator { let mut paginator = Paginator {
all_pages: &item.pages, all_pages: Cow::Borrowed(&item.pages),
pagers: Vec::with_capacity(item.pages.len() / paginate_by), pagers: Vec::with_capacity(item.pages.len() / paginate_by),
paginate_by, paginate_by,
paginate_reversed: false,
root: PaginationRoot::Taxonomy(taxonomy, item), root: PaginationRoot::Taxonomy(taxonomy, item),
permalink: item.permalink.clone(), permalink: item.permalink.clone(),
path: format!("/{}/{}/", taxonomy.kind.name, item.slug), path: format!("/{}/{}/", taxonomy.kind.name, item.slug),
@ -106,6 +113,7 @@ impl<'a> Paginator<'a> {
template: format!("{}/single.html", taxonomy.kind.name), template: format!("{}/single.html", taxonomy.kind.name),
}; };
// taxonomy paginators have no sorting so we won't have to reverse
paginator.fill_pagers(library); paginator.fill_pagers(library);
paginator paginator
} }
@ -116,8 +124,12 @@ impl<'a> Paginator<'a> {
// the pages in the current pagers // the pages in the current pagers
let mut current_page = vec![]; let mut current_page = vec![];
for key in self.all_pages { if self.paginate_reversed {
let page = library.get_page_by_key(*key); 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)); current_page.push(page.to_serialized_basic(library));
if current_page.len() == self.paginate_by { if current_page.len() == self.paginate_by {
@ -246,10 +258,11 @@ mod tests {
use super::Paginator; 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(); let mut f = SectionFrontMatter::default();
f.paginate_by = Some(2); f.paginate_by = Some(2);
f.paginate_path = "page".to_string(); f.paginate_path = "page".to_string();
f.paginate_reversed = paginate_reversed;
let mut s = Section::new("content/_index.md", f, &PathBuf::new()); let mut s = Section::new("content/_index.md", f, &PathBuf::new());
if !is_index { if !is_index {
s.path = "/posts/".to_string(); s.path = "/posts/".to_string();
@ -262,15 +275,22 @@ mod tests {
s s
} }
fn create_library(is_index: bool) -> (Section, Library) { fn create_library(
let mut library = Library::new(3, 0, false); is_index: bool,
library.insert_page(Page::default()); num_pages: usize,
library.insert_page(Page::default()); paginate_reversed: bool,
library.insert_page(Page::default()); ) -> (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(); let mut draft = Page::default();
draft.meta.draft = true; draft.meta.draft = true;
library.insert_page(draft); 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(); section.pages = library.pages().keys().collect();
library.insert_section(section.clone()); library.insert_section(section.clone());
@ -279,7 +299,7 @@ mod tests {
#[test] #[test]
fn test_can_create_paginator() { fn test_can_create_paginator() {
let (section, library) = create_library(false); let (section, library) = create_library(false, 3, false);
let paginator = Paginator::from_section(&section, &library); let paginator = Paginator::from_section(&section, &library);
assert_eq!(paginator.pagers.len(), 2); assert_eq!(paginator.pagers.len(), 2);
@ -294,9 +314,56 @@ mod tests {
assert_eq!(paginator.pagers[1].path, "/posts/page/2/"); 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(&section, &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::<Vec<String>>()
);
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::<Vec<String>>()
);
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::<Vec<String>>()
);
}
#[test] #[test]
fn test_can_create_paginator_for_index() { 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(&section, &library); let paginator = Paginator::from_section(&section, &library);
assert_eq!(paginator.pagers.len(), 2); assert_eq!(paginator.pagers.len(), 2);
@ -313,7 +380,7 @@ mod tests {
#[test] #[test]
fn test_can_build_paginator_context() { 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(&section, &library); let paginator = Paginator::from_section(&section, &library);
assert_eq!(paginator.pagers.len(), 2); assert_eq!(paginator.pagers.len(), 2);
@ -337,7 +404,7 @@ mod tests {
#[test] #[test]
fn test_can_create_paginator_for_taxonomy() { fn test_can_create_paginator_for_taxonomy() {
let (_, library) = create_library(false); let (_, library) = create_library(false, 3, false);
let taxonomy_def = TaxonomyConfig { let taxonomy_def = TaxonomyConfig {
name: "tags".to_string(), name: "tags".to_string(),
paginate_by: Some(2), paginate_by: Some(2),
@ -367,7 +434,7 @@ mod tests {
// https://github.com/getzola/zola/issues/866 // https://github.com/getzola/zola/issues/866
#[test] #[test]
fn works_with_empty_paginate_path() { 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(); section.meta.paginate_path = String::new();
let paginator = Paginator::from_section(&section, &library); let paginator = Paginator::from_section(&section, &library);
assert_eq!(paginator.pagers.len(), 2); assert_eq!(paginator.pagers.len(), 2);

View file

@ -19,7 +19,7 @@ fn can_parse_site() {
let library = site.library.read().unwrap(); let library = site.library.read().unwrap();
// Correct number of pages (sections do not count as pages, draft are ignored) // 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"); let posts_path = path.join("content").join("posts");
// Make sure the page with a url doesn't have any sections // 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()]); assert_eq!(asset_folder_post.file.components, vec!["posts".to_string()]);
// That we have the right number of sections // 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 // And that the sections are correct
let index_section = library.get_section(&path.join("content").join("_index.md")).unwrap(); 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_eq!(index_section.pages.len(), 3);
assert!(index_section.ancestors.is_empty()); assert!(index_section.ancestors.is_empty());
@ -582,7 +582,7 @@ fn can_build_site_with_pagination_for_taxonomy() {
"tags/a/page/1/index.html", "tags/a/page/1/index.html",
"http-equiv=\"refresh\" content=\"0; url=https://replace-this-with-your-url.com/tags/a/\"" "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", "Page size: 2"));
assert!(file_contains!(public, "tags/a/index.html", "Current index: 1")); assert!(file_contains!(public, "tags/a/index.html", "Current index: 1"));
assert!(!file_contains!(public, "tags/a/index.html", "has_prev")); 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!( assert!(file_contains!(
public, public,
"tags/a/index.html", "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); 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!( assert!(file_contains!(
public, public,
"sitemap.xml", "sitemap.xml",
"<loc>https://replace-this-with-your-url.com/tags/a/page/6/</loc>" "<loc>https://replace-this-with-your-url.com/tags/a/page/8/</loc>"
)); ));
// current_path // current_path
@ -721,7 +721,11 @@ fn can_build_site_with_html_minified() {
assert!(&public.exists()); assert!(&public.exists());
assert!(file_exists!(public, "index.html")); assert!(file_exists!(public, "index.html"));
assert!(file_contains!(public, "index.html", "<!DOCTYPE html><html lang=en><head><meta charset=UTF-8>")); assert!(file_contains!(
public,
"index.html",
"<!DOCTYPE html><html lang=en><head><meta charset=UTF-8>"
));
} }
#[test] #[test]

View file

@ -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 page gets `page.lighter` and `page.heavier` variables that contain the
pages with lighter and heavier weights, respectively. pages with lighter and heavier weights, respectively.
### Reversed sorting
When iterating through pages, you may wish to use the Tera `reverse` filter, 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, 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 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`. `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 subsections
Sorting sections is a bit less flexible: sections can only be sorted by `weight`, 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. and do not have variables that point to the heavier/lighter sections.

View file

@ -132,7 +132,6 @@ include_content = true
# become too big to load on the site. Defaults to not being set. # become too big to load on the site. Defaults to not being set.
# truncate_content_length = 100 # truncate_content_length = 100
# Optional translation object. Keys should be language codes. # Optional translation object. Keys should be language codes.
# Optional translation object. The key if present should be a language code. # Optional translation object. The key if present should be a language code.
# Example: # Example:

View file

@ -0,0 +1,4 @@
+++
title="Page number: 1"
weight=1
+++

View file

@ -0,0 +1,4 @@
+++
title="Page number: 2"
weight=2
+++

View file

@ -0,0 +1,4 @@
+++
title="Page number: 3"
weight=3
+++

View file

@ -0,0 +1,4 @@
+++
title="Page number: 4"
weight=4
+++

View file

@ -0,0 +1,4 @@
+++
title="Page number: 5"
weight=5
+++

View file

@ -0,0 +1,4 @@
+++
title="Page number: 6"
weight=6
+++

View file

@ -0,0 +1,4 @@
+++
title="Page number: 7"
weight=7
+++

View file

@ -0,0 +1,4 @@
+++
title="Page number: 8"
weight=8
+++

View file

@ -0,0 +1,4 @@
+++
title="Page number: 9"
weight=9
+++

View file

@ -0,0 +1,6 @@
+++
paginate_by = 2
template = "section_paginated.html"
sort_by = "weight"
paginate_reversed = true
+++