feat(pagination): pagination on tags

This commit is contained in:
Geobert Quach 2018-11-04 20:35:47 +00:00 committed by Geobert Quach
parent 2e5048bc85
commit 1a9683524b
31 changed files with 485 additions and 24 deletions

View file

@ -15,6 +15,7 @@ use super::pagination_config;
use super::slug;
const PATH_ALIAS: &str = "/{{parent}}/{{name}}{{ext}}";
lazy_static! {
static ref PERMALINK_ALIASES: HashMap<&'static str, &'static str> = [("path", PATH_ALIAS),]
.iter()

View file

@ -12,6 +12,7 @@ pub const DEFAULT_PER_PAGE: i32 = 10;
pub enum Include {
None,
All,
Tags,
}
impl Into<&'static str> for Include {
@ -19,6 +20,7 @@ impl Into<&'static str> for Include {
match self {
Include::None => "",
Include::All => "all",
Include::Tags => "tags",
}
}
}

View file

@ -5,15 +5,20 @@ use liquid;
use cobalt_model::pagination_config::Include;
use cobalt_model::pagination_config::PaginationConfig;
use cobalt_model::permalink;
use cobalt_model::slug;
use cobalt_model::SortOrder;
use document;
use document::Document;
use error::*;
use std::collections::HashMap;
#[derive(Default, Clone, Debug)]
pub struct Paginator {
pub pages: Option<Vec<liquid::value::Value>>,
pub indexes: Option<Vec<String>>,
pub indexes: Option<Vec<Paginator>>,
pub index: usize,
pub index_title: Option<liquid::value::Value>,
pub index_permalink: String,
pub previous_index: usize,
pub previous_index_permalink: Option<String>,
@ -32,6 +37,7 @@ impl Paginator {
indexes: None, // list of the available indexes, use when include is tags for instance
index: 0,
index_permalink: String::new(),
index_title: None,
previous_index: 0,
previous_index_permalink: None,
next_index: 0,
@ -48,9 +54,10 @@ impl Paginator {
doc: &Document,
config: &PaginationConfig,
total_pages: usize,
index_title: Option<&liquid::value::Value>,
) -> Result<()> {
self.first_index_permalink = doc.url_path.to_string();
self.last_index_permalink = interpret_permalink(&config, &doc, total_pages)?;
self.last_index_permalink = interpret_permalink(&config, &doc, total_pages, index_title)?;
Ok(())
}
@ -60,10 +67,12 @@ impl Paginator {
all_pages: &[&liquid::value::Value],
config: &PaginationConfig,
doc: &Document,
index_title: Option<&liquid::value::Value>,
) -> Result<()> {
self.index = index;
self.pages = Some(all_pages.into_iter().map(|p| (*p).clone()).collect());
self.index_permalink = interpret_permalink(&config, &doc, index)?;
self.index_title = index_title.map(|i| i.clone());
self.index_permalink = interpret_permalink(&config, &doc, index, index_title)?;
Ok(())
}
@ -73,17 +82,20 @@ impl Paginator {
total_indexes: usize,
doc: &Document,
config: &PaginationConfig,
index_title: Option<&liquid::value::Value>,
) -> Result<()> {
if index > 1 {
// we have a previous index
self.previous_index_permalink = Some(interpret_permalink(&config, &doc, index - 1)?);
self.previous_index_permalink =
Some(interpret_permalink(&config, &doc, index - 1, index_title)?);
self.previous_index = index - 1;
}
if index < total_indexes {
// we have a next index
self.next_index = index + 1;
self.next_index_permalink = Some(interpret_permalink(&config, &doc, index + 1)?);
self.next_index_permalink =
Some(interpret_permalink(&config, &doc, index + 1, index_title)?);
}
Ok(())
}
@ -102,9 +114,11 @@ impl Into<liquid::value::Object> for Paginator {
"indexes".into(),
liquid::value::Value::Array(
indexes
.iter()
.cloned()
.map(liquid::value::Value::scalar)
.into_iter()
.map(|paginator| {
let v: liquid::value::Object = paginator.into();
liquid::value::Value::Object(v)
})
.collect(),
),
);
@ -117,6 +131,9 @@ impl Into<liquid::value::Object> for Paginator {
"index_permalink".into(),
liquid::value::Value::scalar(self.index_permalink),
);
if let Some(index_title) = self.index_title {
object.insert("index_title".into(), index_title);
}
if let Some(previous_index_permalink) = self.previous_index_permalink {
object.insert(
"previous_index".into(),
@ -170,17 +187,26 @@ pub fn generate_paginators(
match config.include {
Include::All => {
sort_posts(&mut all_posts, &config);
create_all_paginators(&all_posts, &doc, &config)
create_all_paginators(&all_posts, &doc, &config, None)
}
Include::Tags => create_tags_paginators(&all_posts, &doc, &config),
Include::None => {
unreachable!("PaginationConfigBuilder should have lead to a None for pagination.")
}
}
}
fn extract_value<'a>(a: &'a liquid::value::Value, key: &str) -> Option<&'a liquid::value::Scalar> {
fn extract_value<'a>(a: &'a liquid::value::Value, key: &str) -> Option<&'a liquid::value::Value> {
let key = liquid::value::Scalar::new(key.to_owned());
a.get(&key).and_then(|sort_key| sort_key.as_scalar())
a.get(&key)
}
fn extract_scalar<'a>(a: &'a liquid::value::Value, key: &str) -> Option<&'a liquid::value::Scalar> {
extract_value(a, key).and_then(|sort_key| sort_key.as_scalar())
}
fn extract_tags<'a>(value: &'a liquid::value::Value) -> Option<&'a liquid::value::Array> {
extract_value(value, "tags").and_then(|tags| tags.as_array())
}
// sort posts by multiple criteria
@ -202,7 +228,7 @@ fn sort_posts(posts: &mut Vec<&liquid::value::Value>, config: &PaginationConfig)
let keys = &config.sort_by;
let mut cmp = Ordering::Less;
for k in keys {
cmp = match (extract_value(a, &k), extract_value(b, &k)) {
cmp = match (extract_scalar(a, &k), extract_scalar(b, &k)) {
(Some(a), Some(b)) => order(a, b),
(None, None) => Ordering::Equal,
(_, None) => Ordering::Greater,
@ -216,10 +242,71 @@ fn sort_posts(posts: &mut Vec<&liquid::value::Value>, config: &PaginationConfig)
})
}
fn distribute_posts_by_tags<'a>(
all_posts: &[&'a liquid::value::Value],
) -> Result<HashMap<String, Vec<&'a liquid::value::Value>>> {
let mut per_tags: HashMap<String, Vec<&'a liquid::value::Value>> = HashMap::new();
for post in all_posts {
if let Some(tags) = extract_tags(post) {
for tag in tags {
let tag = tag.as_scalar().ok_or("Should have string tags")?.to_str();
// add_to_tag(&tag.to_string(), post, &mut per_tags)
let cur_tag = per_tags.entry(tag.to_string()).or_insert(vec![]);
cur_tag.push(post);
}
}
}
Ok(per_tags)
}
#[derive(Default, Debug)]
struct TagPaginators {
firsts_of_tags: Vec<Paginator>,
paginators: Vec<Paginator>,
}
fn create_tags_paginators(
all_posts: &[&liquid::value::Value],
doc: &Document,
pagination_cfg: &PaginationConfig,
) -> Result<Vec<Paginator>> {
let mut per_tags = distribute_posts_by_tags(&all_posts)?;
// create all other paginators
let mut tag_paginators: TagPaginators = per_tags
.iter_mut()
.try_fold(TagPaginators::default(), |mut acc, (tag, posts)| {
sort_posts(posts, &pagination_cfg);
let cur_tag_paginators = create_all_paginators(
posts,
doc,
&pagination_cfg,
Some(&liquid::value::Value::scalar(tag.to_owned())),
)?;
acc.firsts_of_tags.push(cur_tag_paginators[0].clone());
acc.paginators.extend(cur_tag_paginators.into_iter());
Ok(acc)
})
.or_else(|e: Error| Err(e))?;
tag_paginators.firsts_of_tags.sort_unstable_by_key(|p| {
if let Some(ref index_title) = p.index_title {
Some(slug::slugify(index_title.to_str()).to_lowercase())
} else {
None
}
});
let mut first = Paginator::default();
first.indexes = Some(tag_paginators.firsts_of_tags);
tag_paginators.paginators.insert(0, first);
Ok(tag_paginators.paginators)
}
fn create_all_paginators(
all_posts: &[&liquid::value::Value],
doc: &Document,
pagination_cfg: &PaginationConfig,
index_title: Option<&liquid::value::Value>,
) -> Result<Vec<Paginator>> {
let total_pages = all_posts.len();
// f32 used here in order to not lose information to ceil the result,
@ -229,7 +316,15 @@ fn create_all_paginators(
.chunks(pagination_cfg.per_page as usize)
.enumerate()
.map(|(i, chunk)| {
create_paginator(i, total_indexes, total_pages, &pagination_cfg, &doc, &chunk)
create_paginator(
i,
total_indexes,
total_pages,
&pagination_cfg,
&doc,
&chunk,
index_title,
)
})
.collect();
paginators
@ -242,22 +337,22 @@ fn create_paginator(
config: &PaginationConfig,
doc: &Document,
all_posts: &[&liquid::value::Value],
index_title: Option<&liquid::value::Value>,
) -> Result<Paginator> {
let index = i + 1;
let mut paginator = Paginator::new(total_indexes, total_pages);
paginator.set_first_last(&doc, &config, total_indexes)?;
paginator.set_current_index_info(index, &all_posts, &config, &doc)?;
paginator.set_previous_next_info(index, total_indexes, &doc, &config)?;
paginator.set_first_last(&doc, &config, total_indexes, index_title)?;
paginator.set_current_index_info(index, &all_posts, &config, &doc, index_title)?;
paginator.set_previous_next_info(index, total_indexes, &doc, &config, index_title)?;
Ok(paginator)
}
fn pagination_attributes(index_num: i32, include: Include) -> liquid::value::Object {
let i: &str = include.into();
fn pagination_attributes(page_num: i32, include: String) -> liquid::value::Object {
let attributes: liquid::value::Object = vec![
("num".into(), liquid::value::Value::scalar(index_num)),
("include".into(), liquid::value::Value::scalar(i)),
("num".into(), liquid::value::Value::scalar(page_num)),
("include".into(), liquid::value::Value::scalar(include)),
]
.into_iter()
.collect();
@ -267,13 +362,25 @@ fn pagination_attributes(index_num: i32, include: Include) -> liquid::value::Obj
fn interpret_permalink(
config: &PaginationConfig,
doc: &Document,
index_num: usize,
page_num: usize,
index: Option<&liquid::value::Value>,
) -> Result<String> {
Ok(if index_num == 1 {
Ok(if page_num == 1 {
if let Some(index) = index {
let include: &str = config.include.into();
format!("{}/{}", include, slug::slugify(index.to_str()))
} else {
doc.url_path.clone()
}
} else {
let mut attributes = document::permalink_attributes(&doc.front, &doc.file_path);
let pagination_attr = pagination_attributes(index_num as i32, config.include);
let include: &str = config.include.into();
let include: String = if let Some(index) = index {
format!("{}/{}", include, slug::slugify(index.to_str()))
} else {
include.to_owned()
};
let pagination_attr = pagination_attributes(page_num as i32, include);
attributes.extend(pagination_attr.into_iter());
permalink::explode_permalink(&config.permalink, &attributes)?
})

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<h1>{{ page.permalink }}</h1>
{{ page.content }}
</body>
</html>

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>My blog - {{ page.title }}</title>
</head>
<body>
{{ page.content }}
</body>
</html>

View file

@ -0,0 +1,30 @@
---
layout: default.liquid
pagination:
include: Tags
---
{% if paginator.indexes %}
<ul>
{% for ptag in paginator.indexes %}
<li><a href="/{{ ptag.index_permalink }}/">{{ ptag.index_title }} ({{ ptag.total_pages }})</a>
{% endfor %}
</ul>
{% else %}
<ul>
{% for page in paginator.pages %}
<li><a href="/{{ page.permalink }}">{{ page.title }}</a>
{% endfor %}
</ul>
<div>
{% if paginator.previous_index %}
<a href="{{ paginator.previous_index_permalink }}"
class="left arrow">&#8592;</a>
{% endif %}
{% if paginator.next_index %}
<a href="{{ paginator.next_index_permalink }}"
class="right arrow">&#8594;</a>
{% endif %}
<span>{{ paginator.index }} / {{ paginator.total_indexes }}</span>
</div>
{% endif %}

View file

@ -0,0 +1,12 @@
---
layout: posts.liquid
title: My first Blogpost
published_date: 2016-01-01 21:00:00 +0100
tags: [tag1]
---
# {{ page.title }}
Hey there this is my first blogpost and this is super awesome.
My Blog is lorem ipsum like, yes it is..

View file

@ -0,0 +1,12 @@
---
layout: posts.liquid
title: My fith Blogpost
published_date: 2016-06-01 21:00:00 +0100
tags: [tag4, tag5]
---
# {{ page.title }}
Hey there this is my first blogpost and this is super awesome.
My Blog is lorem ipsum like, yes it is..

View file

@ -0,0 +1,12 @@
---
layout: posts.liquid
title: My fourth Blogpost
published_date: 2016-05-29 23:00:00 +0100
tags: [tag4]
---
# {{ page.title }}
Hey there this is my first blogpost and this is super awesome.
My Blog is lorem ipsum like, yes it is..

View file

@ -0,0 +1,12 @@
---
layout: posts.liquid
title: My second Blogpost
published_date: 2016-01-02 10:00:00 +0100
tags: [tag1, tag2]
---
# {{ page.title }}
Hey there this is my first blogpost and this is super awesome.
My Blog is lorem ipsum like, yes it is..

View file

@ -0,0 +1,12 @@
---
layout: posts.liquid
title: My third Blogpost
published_date: 2016-05-27 23:00:00 +0100
tags: [tag1, tag2, tag3]
---
# {{ page.title }}
Hey there this is my first blogpost and this is super awesome.
My Blog is lorem ipsum like, yes it is..

View file

@ -299,3 +299,9 @@ pub fn pagination_less_per_page() {
pub fn pagination_all_sort_by_title() {
run_test("pagination_all_sort_by_title").expect("Build error");
}
#[cfg(feature = "pagination-unstable")]
#[test]
pub fn pagination_tags() {
run_test("pagination_tags").expect("Build error");
}

View file

@ -22,6 +22,7 @@
<span>2 / 2</span>
</div>
</body>
</html>

View file

@ -40,6 +40,7 @@
<span>1 / 2</span>
</div>
</body>
</html>

View file

@ -25,6 +25,7 @@
<span>1 / 1</span>
</div>
</body>
</html>

View file

@ -25,6 +25,7 @@
<span>1 / 1</span>
</div>
</body>
</html>

View file

@ -25,6 +25,7 @@
<span>2 / 4</span>
</div>
</body>
</html>

View file

@ -25,6 +25,7 @@
<span>3 / 4</span>
</div>
</body>
</html>

View file

@ -22,6 +22,7 @@
<span>4 / 4</span>
</div>
</body>
</html>

View file

@ -22,6 +22,7 @@
<span>1 / 4</span>
</div>
</body>
</html>

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<h1>index.html</h1>
<ul>
<li><a href="/tags/tag1/">tag1 (3)</a>
<li><a href="/tags/tag2/">tag2 (2)</a>
<li><a href="/tags/tag3/">tag3 (1)</a>
<li><a href="/tags/tag4/">tag4 (2)</a>
<li><a href="/tags/tag5/">tag5 (1)</a>
</ul>
</body>
</html>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>My blog - My first Blogpost</title>
</head>
<body>
<h1>My first Blogpost</h1>
<p>Hey there this is my first blogpost and this is super awesome.</p>
<p>My Blog is lorem ipsum like, yes it is..</p>
</body>
</html>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>My blog - My fith Blogpost</title>
</head>
<body>
<h1>My fith Blogpost</h1>
<p>Hey there this is my first blogpost and this is super awesome.</p>
<p>My Blog is lorem ipsum like, yes it is..</p>
</body>
</html>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>My blog - My fourth Blogpost</title>
</head>
<body>
<h1>My fourth Blogpost</h1>
<p>Hey there this is my first blogpost and this is super awesome.</p>
<p>My Blog is lorem ipsum like, yes it is..</p>
</body>
</html>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>My blog - My second Blogpost</title>
</head>
<body>
<h1>My second Blogpost</h1>
<p>Hey there this is my first blogpost and this is super awesome.</p>
<p>My Blog is lorem ipsum like, yes it is..</p>
</body>
</html>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>My blog - My third Blogpost</title>
</head>
<body>
<h1>My third Blogpost</h1>
<p>Hey there this is my first blogpost and this is super awesome.</p>
<p>My Blog is lorem ipsum like, yes it is..</p>
</body>
</html>

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<h1>index.html</h1>
<ul>
<li><a href="/posts/my-third-blogpost.html">My third Blogpost</a>
<li><a href="/posts/my-second-blogpost.html">My second Blogpost</a>
<li><a href="/posts/my-first-blogpost.html">My first Blogpost</a>
</ul>
<div>
<span>1 / 1</span>
</div>
</body>
</html>

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<h1>index.html</h1>
<ul>
<li><a href="/posts/my-third-blogpost.html">My third Blogpost</a>
<li><a href="/posts/my-second-blogpost.html">My second Blogpost</a>
</ul>
<div>
<span>1 / 1</span>
</div>
</body>
</html>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<h1>index.html</h1>
<ul>
<li><a href="/posts/my-third-blogpost.html">My third Blogpost</a>
</ul>
<div>
<span>1 / 1</span>
</div>
</body>
</html>

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<h1>index.html</h1>
<ul>
<li><a href="/posts/my-fith-blogpost.html">My fith Blogpost</a>
<li><a href="/posts/my-fourth-blogpost.html">My fourth Blogpost</a>
</ul>
<div>
<span>1 / 1</span>
</div>
</body>
</html>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<h1>index.html</h1>
<ul>
<li><a href="/posts/my-fith-blogpost.html">My fith Blogpost</a>
</ul>
<div>
<span>1 / 1</span>
</div>
</body>
</html>