Browse Source

Allow sorting pages by order and date

Closes #14, #43
index-subcmd
Vincent Prouillet 7 years ago
parent
commit
a0b70bfc7e
5 changed files with 246 additions and 49 deletions
  1. +1
    -1
      benches/gutenberg.rs
  2. +42
    -2
      src/front_matter.rs
  3. +156
    -38
      src/page.rs
  4. +12
    -5
      src/site.rs
  5. +35
    -3
      tests/front_matter.rs

+ 1
- 1
benches/gutenberg.rs View File

@@ -44,5 +44,5 @@ fn bench_populate_previous_and_next_pages(b: &mut test::Bencher) {
let mut pages = site.pages.values().cloned().collect::<Vec<_>>(); let mut pages = site.pages.values().cloned().collect::<Vec<_>>();
pages.sort_by(|a, b| a.partial_cmp(b).unwrap()); pages.sort_by(|a, b| a.partial_cmp(b).unwrap());


b.iter(|| populate_previous_and_next_pages(pages.as_slice(), false));
b.iter(|| populate_previous_and_next_pages(pages.as_slice()));
} }

+ 42
- 2
src/front_matter.rs View File

@@ -14,6 +14,13 @@ lazy_static! {
static ref PAGE_RE: Regex = Regex::new(r"^\r?\n?\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap(); static ref PAGE_RE: Regex = Regex::new(r"^\r?\n?\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap();
} }


#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SortBy {
Date,
Order,
None,
}


/// The front matter of every page /// The front matter of every page
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
@@ -37,6 +44,11 @@ pub struct FrontMatter {
pub draft: Option<bool>, pub draft: Option<bool>,
/// Only one category allowed /// Only one category allowed
pub category: Option<String>, pub category: Option<String>,
/// Whether to sort by "date", "order" or "none"
#[serde(skip_serializing)]
pub sort_by: Option<SortBy>,
/// Integer to use to order content. Lowest is at the bottom, highest first
pub order: Option<usize>,
/// Optional template, if we want to specify which template to render for that page /// Optional template, if we want to specify which template to render for that page
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub template: Option<String>, pub template: Option<String>,
@@ -71,7 +83,7 @@ impl FrontMatter {
} }


/// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime /// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime
pub fn parse_date(&self) -> Option<NaiveDateTime> {
pub fn date(&self) -> Option<NaiveDateTime> {
match self.date { match self.date {
Some(ref d) => { Some(ref d) => {
if d.contains('T') { if d.contains('T') {
@@ -83,12 +95,40 @@ impl FrontMatter {
None => None, None => None,
} }
} }

pub fn order(&self) -> usize {
self.order.unwrap()
}

pub fn sort_by(&self) -> SortBy {
match self.sort_by {
Some(ref s) => s.clone(),
None => SortBy::Date,
}
}
} }


impl Default for FrontMatter {
fn default() -> FrontMatter {
FrontMatter {
title: "default".to_string(),
description: " A default front matter".to_string(),
date: None,
slug: None,
url: None,
tags: None,
draft: None,
category: None,
sort_by: None,
order: None,
template: None,
extra: None,
}
}
}


/// Split a file between the front matter and its content /// Split a file between the front matter and its content
/// It will parse the front matter as well and returns any error encountered /// It will parse the front matter as well and returns any error encountered
/// TODO: add tests
pub fn split_content(file_path: &Path, content: &str) -> Result<(FrontMatter, String)> { pub fn split_content(file_path: &Path, content: &str) -> Result<(FrontMatter, String)> {
if !PAGE_RE.is_match(content) { if !PAGE_RE.is_match(content) {
bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", file_path.to_string_lossy()); bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", file_path.to_string_lossy());


+ 156
- 38
src/page.rs View File

@@ -12,7 +12,8 @@ use slug::slugify;


use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use config::Config; use config::Config;
use front_matter::{FrontMatter, split_content};
use front_matter::{FrontMatter, SortBy, split_content};
use section::Section;
use markdown::markdown_to_html; use markdown::markdown_to_html;
use utils::{read_file, find_content_components}; use utils::{read_file, find_content_components};


@@ -76,14 +77,10 @@ pub struct Page {
/// as summary /// as summary
pub summary: Option<String>, pub summary: Option<String>,


/// The previous page, by date globally
/// The previous page, by whatever sorting is used for the index/section
pub previous: Option<Box<Page>>, pub previous: Option<Box<Page>>,
/// The previous page, by date only for the section the page is in
pub previous_in_section: Option<Box<Page>>,
/// The next page, by date
/// The next page, by whatever sorting is used for the index/section
pub next: Option<Box<Page>>, pub next: Option<Box<Page>>,
/// The next page, by date only for the section the page is in
pub next_in_section: Option<Box<Page>>,
} }




@@ -104,9 +101,7 @@ impl Page {
summary: None, summary: None,
meta: meta, meta: meta,
previous: None, previous: None,
previous_in_section: None,
next: None, next: None,
next_in_section: None,
} }
} }


@@ -222,7 +217,7 @@ impl Page {


impl ser::Serialize for Page { impl ser::Serialize for Page {
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer { fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer {
let mut state = serializer.serialize_struct("page", 18)?;
let mut state = serializer.serialize_struct("page", 16)?;
state.serialize_field("content", &self.content)?; state.serialize_field("content", &self.content)?;
state.serialize_field("title", &self.meta.title)?; state.serialize_field("title", &self.meta.title)?;
state.serialize_field("description", &self.meta.description)?; state.serialize_field("description", &self.meta.description)?;
@@ -239,13 +234,63 @@ impl ser::Serialize for Page {
state.serialize_field("word_count", &word_count)?; state.serialize_field("word_count", &word_count)?;
state.serialize_field("reading_time", &reading_time)?; state.serialize_field("reading_time", &reading_time)?;
state.serialize_field("previous", &self.previous)?; state.serialize_field("previous", &self.previous)?;
state.serialize_field("previous_in_section", &self.previous_in_section)?;
state.serialize_field("next", &self.next)?; state.serialize_field("next", &self.next)?;
state.serialize_field("next_in_section", &self.next_in_section)?;
state.end() state.end()
} }
} }


/// Sort pages
/// TODO: write doc and tests
pub fn sort_pages(pages: Vec<Page>, section: Option<&Section>) -> Vec<Page> {
let sort_by = if let Some(ref sec) = section {
sec.meta.sort_by()
} else {
SortBy::Date
};

match sort_by {
SortBy::Date => {
let mut can_be_sorted = vec![];
let mut cannot_be_sorted = vec![];
for page in pages {
if page.meta.date.is_some() {
can_be_sorted.push(page);
} else {
cannot_be_sorted.push(page);
}
}
can_be_sorted.sort_by(|a, b| b.meta.date().unwrap().cmp(&a.meta.date().unwrap()));
can_be_sorted.append(&mut cannot_be_sorted);

can_be_sorted
},
SortBy::Order => {
let mut can_be_sorted = vec![];
let mut cannot_be_sorted = vec![];
for page in pages {
if page.meta.order.is_some() {
can_be_sorted.push(page);
} else {
cannot_be_sorted.push(page);
}
}
can_be_sorted.sort_by(|a, b| b.meta.order().cmp(&a.meta.order()));
can_be_sorted.append(&mut cannot_be_sorted);

can_be_sorted
},
SortBy::None => {
let mut p = vec![];
for page in pages {
p.push(page);
}

p
},
}
}

/// Used only by the RSS feed (I think)
impl PartialOrd for Page { impl PartialOrd for Page {
fn partial_cmp(&self, other: &Page) -> Option<Ordering> { fn partial_cmp(&self, other: &Page) -> Option<Ordering> {
if self.meta.date.is_none() { if self.meta.date.is_none() {
@@ -256,8 +301,8 @@ impl PartialOrd for Page {
return Some(Ordering::Greater); return Some(Ordering::Greater);
} }


let this_date = self.meta.parse_date().unwrap();
let other_date = other.meta.parse_date().unwrap();
let this_date = self.meta.date().unwrap();
let other_date = other.meta.date().unwrap();


if this_date > other_date { if this_date > other_date {
return Some(Ordering::Less); return Some(Ordering::Less);
@@ -273,36 +318,23 @@ impl PartialOrd for Page {


/// Horribly inefficient way to set previous and next on each pages /// Horribly inefficient way to set previous and next on each pages
/// So many clones /// So many clones
pub fn populate_previous_and_next_pages(input: &[Page], in_section: bool) -> Vec<Page> {
pub fn populate_previous_and_next_pages(input: &[Page]) -> Vec<Page> {
let pages = input.to_vec(); let pages = input.to_vec();
let mut res = Vec::new(); let mut res = Vec::new();


// the input is sorted from most recent to least recent already
// the input is already sorted
// We might put prev/next randomly if a page is missing date/order, probably fine
for (i, page) in input.iter().enumerate() { for (i, page) in input.iter().enumerate() {
let mut new_page = page.clone(); let mut new_page = page.clone();


if new_page.has_date() {
if i > 0 {
let next = &pages[i - 1];
if next.has_date() {
if in_section {
new_page.next_in_section = Some(Box::new(next.clone()));
} else {
new_page.next = Some(Box::new(next.clone()));
}
}
}
if i > 0 {
let next = &pages[i - 1];
new_page.next = Some(Box::new(next.clone()));
}


if i < input.len() - 1 {
let previous = &pages[i + 1];
if previous.has_date() {
if in_section {
new_page.previous_in_section = Some(Box::new(previous.clone()));
} else {
new_page.previous = Some(Box::new(previous.clone()));
}
}
}
if i < input.len() - 1 {
let previous = &pages[i + 1];
new_page.previous = Some(Box::new(previous.clone()));
} }
res.push(new_page); res.push(new_page);
} }
@@ -315,8 +347,23 @@ mod tests {
use tempdir::TempDir; use tempdir::TempDir;


use std::fs::File; use std::fs::File;
use std::path::Path;

use front_matter::{FrontMatter, SortBy};
use section::Section;
use super::{Page, find_related_assets, sort_pages, populate_previous_and_next_pages};


use super::{find_related_assets};
fn create_page_with_date(date: &str) -> Page {
let mut front_matter = FrontMatter::default();
front_matter.date = Some(date.to_string());
Page::new(front_matter)
}

fn create_page_with_order(order: usize) -> Page {
let mut front_matter = FrontMatter::default();
front_matter.order = Some(order);
Page::new(front_matter)
}


#[test] #[test]
fn test_find_related_assets() { fn test_find_related_assets() {
@@ -333,4 +380,75 @@ mod tests {
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "graph.jpg").count(), 1); assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "graph.jpg").count(), 1);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "fail.png").count(), 1); assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "fail.png").count(), 1);
} }

#[test]
fn test_can_default_sort() {
let input = vec![
create_page_with_date("2018-01-01"),
create_page_with_date("2017-01-01"),
create_page_with_date("2019-01-01"),
];
let pages = sort_pages(input, None);
// Should be sorted by date
assert_eq!(pages[0].clone().meta.date.unwrap(), "2019-01-01");
assert_eq!(pages[1].clone().meta.date.unwrap(), "2018-01-01");
assert_eq!(pages[2].clone().meta.date.unwrap(), "2017-01-01");
}

#[test]
fn test_can_sort_dates() {
let input = vec![
create_page_with_date("2018-01-01"),
create_page_with_date("2017-01-01"),
create_page_with_date("2019-01-01"),
];
let mut front_matter = FrontMatter::default();
front_matter.sort_by = Some(SortBy::Date);
let section = Section::new(Path::new("hey"), front_matter);
let pages = sort_pages(input, Some(&section));
// Should be sorted by date
assert_eq!(pages[0].clone().meta.date.unwrap(), "2019-01-01");
assert_eq!(pages[1].clone().meta.date.unwrap(), "2018-01-01");
assert_eq!(pages[2].clone().meta.date.unwrap(), "2017-01-01");
}

#[test]
fn test_can_sort_order() {
let input = vec![
create_page_with_order(2),
create_page_with_order(3),
create_page_with_order(1),
];
let mut front_matter = FrontMatter::default();
front_matter.sort_by = Some(SortBy::Order);
let section = Section::new(Path::new("hey"), front_matter);
let pages = sort_pages(input, Some(&section));
// Should be sorted by date
assert_eq!(pages[0].clone().meta.order.unwrap(), 3);
assert_eq!(pages[1].clone().meta.order.unwrap(), 2);
assert_eq!(pages[2].clone().meta.order.unwrap(), 1);
}

#[test]
fn test_populate_previous_and_next_pages() {
let input = vec![
create_page_with_order(3),
create_page_with_order(2),
create_page_with_order(1),
];
let pages = populate_previous_and_next_pages(input.as_slice());

assert!(pages[0].clone().next.is_none());
assert!(pages[0].clone().previous.is_some());
assert_eq!(pages[0].clone().previous.unwrap().meta.order.unwrap(), 2);

assert!(pages[1].clone().next.is_some());
assert!(pages[1].clone().previous.is_some());
assert_eq!(pages[1].clone().next.unwrap().meta.order.unwrap(), 3);
assert_eq!(pages[1].clone().previous.unwrap().meta.order.unwrap(), 1);

assert!(pages[2].clone().next.is_some());
assert!(pages[2].clone().previous.is_none());
assert_eq!(pages[2].clone().next.unwrap().meta.order.unwrap(), 2);
}
} }

+ 12
- 5
src/site.rs View File

@@ -10,7 +10,7 @@ use walkdir::WalkDir;


use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use config::{Config, get_config}; use config::{Config, get_config};
use page::{Page, populate_previous_and_next_pages};
use page::{Page, populate_previous_and_next_pages, sort_pages};
use utils::{create_file, create_directory}; use utils::{create_file, create_directory};
use section::{Section}; use section::{Section};
use filters; use filters;
@@ -200,8 +200,9 @@ impl Site {
} }


for (parent_path, section) in &mut self.sections { for (parent_path, section) in &mut self.sections {
section.pages.sort_by(|a, b| a.partial_cmp(b).unwrap());
section.pages = populate_previous_and_next_pages(section.pages.as_slice(), true);
// TODO: avoid this clone
let sorted_pages = sort_pages(section.pages.clone(), Some(&section));
section.pages = populate_previous_and_next_pages(sorted_pages.as_slice());


match grandparent_paths.get(parent_path) { match grandparent_paths.get(parent_path) {
Some(paths) => section.subsections.extend(paths.clone()), Some(paths) => section.subsections.extend(paths.clone()),
@@ -361,11 +362,13 @@ impl Site {
self.render_categories_and_tags(RenderList::Tags)?; self.render_categories_and_tags(RenderList::Tags)?;
} }


// Sort the pages
let sorted_pages = sort_pages(pages, self.index.as_ref());

// And finally the index page // And finally the index page
let mut context = Context::new(); let mut context = Context::new();
pages.sort_by(|a, b| a.partial_cmp(b).unwrap());


context.add("pages", &populate_previous_and_next_pages(&pages, false));
context.add("pages", &populate_previous_and_next_pages(sorted_pages.as_slice()));
context.add("sections", &self.sections.values().collect::<Vec<&Section>>()); context.add("sections", &self.sections.values().collect::<Vec<&Section>>());
context.add("config", &self.config); context.add("config", &self.config);
context.add("current_url", &self.config.base_url); context.add("current_url", &self.config.base_url);
@@ -446,6 +449,10 @@ impl Site {
.filter(|&(path, _)| pages_paths.contains(path)) .filter(|&(path, _)| pages_paths.contains(path))
.map(|(_, page)| page) .map(|(_, page)| page)
.collect(); .collect();
// TODO: how to sort categories and tag content?
// Have a setting in config.toml or a _category.md and _tag.md
// The latter is more in line with the rest of Gutenberg but order ordering
// doesn't really work across sections so default to partial ordering for now (date)
pages.sort_by(|a, b| a.partial_cmp(b).unwrap()); pages.sort_by(|a, b| a.partial_cmp(b).unwrap());


let mut context = Context::new(); let mut context = Context::new();


+ 35
- 3
tests/front_matter.rs View File

@@ -125,7 +125,7 @@ title = "Hello"
description = "hey there" description = "hey there"
date = "2016-10-10""#; date = "2016-10-10""#;
let res = FrontMatter::parse(content).unwrap(); let res = FrontMatter::parse(content).unwrap();
assert!(res.parse_date().is_some());
assert!(res.date().is_some());
} }


#[test] #[test]
@@ -135,7 +135,7 @@ title = "Hello"
description = "hey there" description = "hey there"
date = "2002-10-02T15:00:00Z""#; date = "2002-10-02T15:00:00Z""#;
let res = FrontMatter::parse(content).unwrap(); let res = FrontMatter::parse(content).unwrap();
assert!(res.parse_date().is_some());
assert!(res.date().is_some());
} }


#[test] #[test]
@@ -145,9 +145,41 @@ title = "Hello"
description = "hey there" description = "hey there"
date = "2002/10/12""#; date = "2002/10/12""#;
let res = FrontMatter::parse(content).unwrap(); let res = FrontMatter::parse(content).unwrap();
assert!(res.parse_date().is_none());
assert!(res.date().is_none());
} }


#[test]
fn test_cant_parse_sort_by_date() {
let content = r#"
title = "Hello"
description = "hey there"
sort_by = "date""#;
let res = FrontMatter::parse(content).unwrap();
assert!(res.sort_by.is_some());
assert!(res.sort_by.unwrap(), SortBy::Date);
}

#[test]
fn test_cant_parse_sort_by_order() {
let content = r#"
title = "Hello"
description = "hey there"
sort_by = "order""#;
let res = FrontMatter::parse(content).unwrap();
assert!(res.sort_by.is_some());
assert!(res.sort_by.unwrap(), SortBy::Order);
}

#[test]
fn test_cant_parse_sort_by_none() {
let content = r#"
title = "Hello"
description = "hey there"
sort_by = "none""#;
let res = FrontMatter::parse(content).unwrap();
assert!(res.sort_by.is_some());
assert!(res.sort_by.unwrap(), SortBy::None);
}


#[test] #[test]
fn test_can_split_content_valid() { fn test_can_split_content_valid() {


Loading…
Cancel
Save