Skip to content

Commit

Permalink
feat(pagination): pagination on dates
Browse files Browse the repository at this point in the history
  • Loading branch information
Geobert Quach committed Aug 23, 2019
1 parent 3403461 commit 4327ec1
Show file tree
Hide file tree
Showing 20 changed files with 571 additions and 2 deletions.
5 changes: 5 additions & 0 deletions src/cobalt_model/frontmatter.rs
Expand Up @@ -457,6 +457,11 @@ impl FrontmatterBuilder {
if !cfg!(feature = "pagination-unstable") && fm.pagination.is_some() {
failure::bail!("Unsupported `pagination` field");
} else {
if let Some(pagination) = &fm.pagination {
if !pagination_config::is_date_index_sorted(&pagination.date_index) {
failure::bail!("date_index is not correctly sorted: Year > Month > Day...");
}
}
Ok(fm)
}
}
Expand Down
30 changes: 30 additions & 0 deletions src/cobalt_model/pagination_config.rs
Expand Up @@ -7,13 +7,18 @@ pub const DEFAULT_PERMALINK_SUFFIX: &str = "{{num}}/";
pub const DEFAULT_SORT: &str = "published_date";
pub const DEFAULT_PER_PAGE: i32 = 10;

lazy_static! {
static ref DEFAULT_DATE_INDEX: Vec<DateIndex> = vec![DateIndex::Year, DateIndex::Month];
}

#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub enum Include {
None,
All,
Tags,
Categories,
Dates,
}

impl Into<&'static str> for Include {
Expand All @@ -23,6 +28,7 @@ impl Into<&'static str> for Include {
Include::All => "all",
Include::Tags => "tags",
Include::Categories => "categories",
Include::Dates => "dates",
}
}
}
Expand All @@ -33,6 +39,23 @@ impl Default for Include {
}
}

#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(deny_unknown_fields)]
pub enum DateIndex {
Year,
Month,
Day,
Hour,
Minute,
}

// TODO to be replaced by a call to `is_sorted()` once it's stabilized
pub fn is_date_index_sorted(v: &Vec<DateIndex>) -> bool {
let mut copy = v.clone();
copy.sort_unstable();
copy.eq(v)
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(deny_unknown_fields, default)]
pub struct PaginationConfigBuilder {
Expand All @@ -46,6 +69,8 @@ pub struct PaginationConfigBuilder {
pub order: Option<SortOrder>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_by: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub date_index: Option<Vec<DateIndex>>,
}

impl PaginationConfigBuilder {
Expand Down Expand Up @@ -114,6 +139,7 @@ impl PaginationConfigBuilder {
permalink_suffix,
order,
sort_by,
date_index,
} = self;

let include = include.unwrap_or(Include::None);
Expand All @@ -125,13 +151,15 @@ impl PaginationConfigBuilder {
permalink_suffix.unwrap_or_else(|| DEFAULT_PERMALINK_SUFFIX.to_owned());
let order = order.unwrap_or(SortOrder::Desc);
let sort_by = sort_by.unwrap_or_else(|| vec![DEFAULT_SORT.to_owned()]);
let date_index = date_index.unwrap_or_else(|| DEFAULT_DATE_INDEX.to_vec());
Some(PaginationConfig {
include,
per_page,
front_permalink: permalink.to_owned(),
permalink_suffix,
order,
sort_by,
date_index,
})
}
}
Expand All @@ -145,6 +173,7 @@ pub struct PaginationConfig {
pub permalink_suffix: String,
pub order: SortOrder,
pub sort_by: Vec<String>,
pub date_index: Vec<DateIndex>,
}

impl Default for PaginationConfig {
Expand All @@ -156,6 +185,7 @@ impl Default for PaginationConfig {
front_permalink: Default::default(),
order: SortOrder::Desc,
sort_by: vec![DEFAULT_SORT.to_owned()],
date_index: DEFAULT_DATE_INDEX.to_vec(),
}
}
}
173 changes: 173 additions & 0 deletions src/pagination/dates.rs
@@ -0,0 +1,173 @@
use chrono::Datelike;
use chrono::Timelike;

use crate::cobalt_model::pagination_config::DateIndex;

use crate::cobalt_model::DateTime;
use crate::document::Document;

use super::*;
use helpers::extract_scalar;
use paginator::Paginator;

#[derive(Debug, Clone)]
struct DateIndexHolder<'a> {
value: u32,
field: Option<DateIndex>,
posts: Vec<&'a liquid::value::Value>,
sub_date: Vec<DateIndexHolder<'a>>,
}

impl<'a> DateIndexHolder<'a> {
fn new(value: u32, field: Option<DateIndex>) -> Self {
DateIndexHolder {
value,
field,
posts: vec![],
sub_date: vec![],
}
}
}

fn extract_published_date<'a>(value: &'a liquid::value::Value) -> Option<DateTime> {
if let Some(published_date) = extract_scalar(&value, "published_date") {
published_date.to_date().map(|d| d.into())
} else {
None
}
}

pub fn create_dates_paginators(
all_posts: &[&liquid::value::Value],
doc: &Document,
pagination_cfg: &PaginationConfig,
) -> Result<Vec<Paginator>> {
let mut root_date = distribute_posts_by_dates(&all_posts, &pagination_cfg)?;
walk_dates(&mut root_date, &pagination_cfg, &doc, None)
}

fn format_date_holder(d: &DateIndexHolder) -> liquid::value::Value {
let field = d
.field
.expect("Should not be called with the root DateIndexHolder");
let formatted = match field {
DateIndex::Year => d.value.to_string(),
_ => format!("{:02}", d.value),
};
liquid::value::Value::scalar(formatted)
}

fn date_fields_to_array(date: &Vec<DateIndexHolder>) -> liquid::value::Array {
date.iter().map(|d| format_date_holder(&d)).collect()
}

fn walk_dates(
date_holder: &mut DateIndexHolder,
config: &PaginationConfig,
doc: &Document,
parent_dates: Option<Vec<DateIndexHolder>>,
) -> Result<Vec<Paginator>> {
let mut cur_date_holder_paginators: Vec<Paginator> = vec![];
let mut current_date = if let Some(parent_dates) = parent_dates {
parent_dates
} else {
vec![]
};
if let Some(_field) = date_holder.field {
sort_posts(&mut date_holder.posts, &config);
current_date.push(date_holder.clone());
let index_title = liquid::value::Value::array(date_fields_to_array(&current_date));
let cur_date_paginators =
create_all_paginators(&date_holder.posts, &doc, &config, Some(&index_title))?;
if !cur_date_paginators.is_empty() {
cur_date_holder_paginators.extend(cur_date_paginators.into_iter());
} else {
let mut p = Paginator::default();
p.index_title = Some(index_title);
cur_date_holder_paginators.push(p);
}
} else {
cur_date_holder_paginators.push(Paginator::default());
}
for mut dh in &mut date_holder.sub_date {
let mut sub_paginators_holder =
walk_dates(&mut dh, &config, &doc, Some(current_date.clone()))?;

if let Some(indexes) = cur_date_holder_paginators[0].indexes.as_mut() {
indexes.push(sub_paginators_holder[0].clone());
} else {
cur_date_holder_paginators[0].indexes = Some(vec![sub_paginators_holder[0].clone()]);
}
cur_date_holder_paginators.append(&mut sub_paginators_holder);
}
Ok(cur_date_holder_paginators)
}

fn find_or_create_date_holder_and_put_post<'a, 'b>(
date_holder: &'b mut DateIndexHolder<'a>,
published_date: &DateTime,
wanted_field: &DateIndex,
post: &'a liquid::value::Value,
) {
let value = get_date_field_value(&published_date, &wanted_field);
let mut not_found = true;
for mut dh in date_holder.sub_date.iter_mut() {
let dh_field = dh
.field
.expect("Only root has None, we should always have a field");
if dh_field < *wanted_field {
// not at the level we want but still need to find the correct parent
// parent should have been created in a previous loop
let parent_value = get_date_field_value(&published_date, &dh_field);
if dh.value == parent_value {
find_or_create_date_holder_and_put_post(
&mut dh,
&published_date,
wanted_field,
&post,
);
not_found = false;
}
} else if dh_field == *wanted_field && dh.value == value {
dh.posts.push(post);
not_found = false;
}
}
// not found create one
if not_found {
let mut holder = DateIndexHolder::new(value, Some(*wanted_field));
holder.posts.push(post);
date_holder.sub_date.push(holder);
}
}

fn get_date_field_value(date: &DateTime, field: &DateIndex) -> u32 {
match field {
DateIndex::Year => {
if date.year() < 0 {
panic!("Negative year is not supported");
}
date.year() as u32
}
DateIndex::Month => date.month(),
DateIndex::Day => date.day(),
DateIndex::Hour => date.hour(),
DateIndex::Minute => date.minute(),
}
}

fn distribute_posts_by_dates<'a>(
all_posts: &[&'a liquid::value::Value],
pagination_cfg: &PaginationConfig,
) -> Result<DateIndexHolder<'a>> {
let date_index = &pagination_cfg.date_index;
let mut root = DateIndexHolder::new(0u32, None);
for post in all_posts {
if let Some(published_date) = extract_published_date(&post) {
for idx in date_index {
find_or_create_date_holder_and_put_post(&mut root, &published_date, &idx, &post);
}
}
}
Ok(root)
}
5 changes: 4 additions & 1 deletion src/pagination/mod.rs
Expand Up @@ -5,11 +5,13 @@ use crate::cobalt_model::pagination_config::PaginationConfig;
use crate::cobalt_model::permalink;
use crate::cobalt_model::slug;
use crate::cobalt_model::SortOrder;

use crate::document;
use crate::document::Document;
use crate::error::*;
use crate::error::Result;

mod categories;
mod dates;
mod helpers;
mod paginator;
mod tags;
Expand All @@ -33,6 +35,7 @@ pub fn generate_paginators(
}
Include::Tags => tags::create_tags_paginators(&all_posts, &doc, &config),
Include::Categories => categories::create_categories_paginators(&all_posts, &doc, &config),
Include::Dates => dates::create_dates_paginators(&all_posts, &doc, &config),
Include::None => {
unreachable!("PaginationConfigBuilder should have lead to a None for pagination.")
}
Expand Down
2 changes: 1 addition & 1 deletion src/pagination/tags.rs
Expand Up @@ -55,7 +55,7 @@ pub fn create_tags_paginators(
acc.paginators.extend(cur_tag_paginators.into_iter());
Ok(acc)
})
.or_else(std::result::Result::<_, Error>::Err)?;
.or_else(std::result::Result::<_, failure::Error>::Err)?;

tag_paginators.firsts_of_tags.sort_unstable_by_key(|p| {
if let Some(ref index_title) = p.index_title {
Expand Down
12 changes: 12 additions & 0 deletions tests/fixtures/pagination_dates/_layouts/default.liquid
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>test</title>
</head>
<body>
<h1>{{ page.permalink }}</h1>

{{ page.content }}
</body>
</html>

10 changes: 10 additions & 0 deletions tests/fixtures/pagination_dates/_layouts/posts.liquid
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>My blog - {{ page.title }}</title>
</head>
<body>
{{ page.content }}
</body>
</html>

44 changes: 44 additions & 0 deletions tests/fixtures/pagination_dates/index.liquid
@@ -0,0 +1,44 @@
---
permalink: /dates.html
layout: default.liquid
pagination:
include: Dates
permalink_suffix: ./_p/{{num}}
---
{% if paginator.index_title %}
{% capture date_title %}{% for field in paginator.index_title %}{{ field | prepend: "-" }}{% endfor %}{% endcapture %}
{% capture date_clean %}{{ date_title | remove_first: "-" }}{% endcapture %}
<div>Date: {{ date_clean | date: "%Y/%B" }}</div>
{% if paginator.indexes %}
<div>Sub-dates:</div>
{% endif %}
{% endif %}
{% if paginator.indexes %}
All years:
<ul>
{% for pdate in paginator.indexes %}
<li><a href="/{{ pdate.index_permalink }}/">{{ pdate.index_title | last }} ({{ pdate.total_pages }})</a>
{% endfor %}
</ul>
{% endif %}

{% if paginator.pages %}
<div>Posts in this date:</div>
<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 %}
12 changes: 12 additions & 0 deletions tests/fixtures/pagination_dates/posts/my-first-blogpost.md
@@ -0,0 +1,12 @@
---
layout: posts.liquid

title: My first Blogpost
published_date: 2016-01-01 21:00:00 +0100
categories: [catA]
---
# {{ page.title }}

Hey there this is my first blogpost and this is super awesome.

My Blog is lorem ipsum like, yes it is..

0 comments on commit 4327ec1

Please sign in to comment.