From a0b70bfc7e24261487cce40dfbbf9be2ba7a7ad6 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Mon, 24 Apr 2017 18:11:51 +0900 Subject: [PATCH] Allow sorting pages by order and date Closes #14, #43 --- benches/gutenberg.rs | 2 +- src/front_matter.rs | 44 +++++++++- src/page.rs | 194 +++++++++++++++++++++++++++++++++--------- src/site.rs | 17 ++-- tests/front_matter.rs | 38 ++++++++- 5 files changed, 246 insertions(+), 49 deletions(-) diff --git a/benches/gutenberg.rs b/benches/gutenberg.rs index 704e404..969d34b 100644 --- a/benches/gutenberg.rs +++ b/benches/gutenberg.rs @@ -44,5 +44,5 @@ fn bench_populate_previous_and_next_pages(b: &mut test::Bencher) { let mut pages = site.pages.values().cloned().collect::>(); 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())); } diff --git a/src/front_matter.rs b/src/front_matter.rs index e7eb499..2d138d8 100644 --- a/src/front_matter.rs +++ b/src/front_matter.rs @@ -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(); } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SortBy { + Date, + Order, + None, +} /// The front matter of every page #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -37,6 +44,11 @@ pub struct FrontMatter { pub draft: Option, /// Only one category allowed pub category: Option, + /// Whether to sort by "date", "order" or "none" + #[serde(skip_serializing)] + pub sort_by: Option, + /// Integer to use to order content. Lowest is at the bottom, highest first + pub order: Option, /// Optional template, if we want to specify which template to render for that page #[serde(skip_serializing)] pub template: Option, @@ -71,7 +83,7 @@ impl FrontMatter { } /// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime - pub fn parse_date(&self) -> Option { + pub fn date(&self) -> Option { match self.date { Some(ref d) => { if d.contains('T') { @@ -83,12 +95,40 @@ impl FrontMatter { 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 /// 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)> { if !PAGE_RE.is_match(content) { bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", file_path.to_string_lossy()); diff --git a/src/page.rs b/src/page.rs index c5bd3ee..1296bf5 100644 --- a/src/page.rs +++ b/src/page.rs @@ -12,7 +12,8 @@ use slug::slugify; use errors::{Result, ResultExt}; 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 utils::{read_file, find_content_components}; @@ -76,14 +77,10 @@ pub struct Page { /// as summary pub summary: Option, - /// The previous page, by date globally + /// The previous page, by whatever sorting is used for the index/section pub previous: Option>, - /// The previous page, by date only for the section the page is in - pub previous_in_section: Option>, - /// The next page, by date + /// The next page, by whatever sorting is used for the index/section pub next: Option>, - /// The next page, by date only for the section the page is in - pub next_in_section: Option>, } @@ -104,9 +101,7 @@ impl Page { summary: None, meta: meta, previous: None, - previous_in_section: None, next: None, - next_in_section: None, } } @@ -222,7 +217,7 @@ impl Page { impl ser::Serialize for Page { fn serialize(&self, serializer: S) -> StdResult 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("title", &self.meta.title)?; 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("reading_time", &reading_time)?; 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_in_section", &self.next_in_section)?; state.end() } } +/// Sort pages +/// TODO: write doc and tests +pub fn sort_pages(pages: Vec, section: Option<&Section>) -> Vec { + 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 { fn partial_cmp(&self, other: &Page) -> Option { if self.meta.date.is_none() { @@ -256,8 +301,8 @@ impl PartialOrd for Page { 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 { return Some(Ordering::Less); @@ -273,36 +318,23 @@ impl PartialOrd for Page { /// Horribly inefficient way to set previous and next on each pages /// So many clones -pub fn populate_previous_and_next_pages(input: &[Page], in_section: bool) -> Vec { +pub fn populate_previous_and_next_pages(input: &[Page]) -> Vec { let pages = input.to_vec(); 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() { 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); } @@ -315,8 +347,23 @@ mod tests { use tempdir::TempDir; 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] 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() == "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(§ion)); + // 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(§ion)); + // 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); + } } diff --git a/src/site.rs b/src/site.rs index 8d01a10..23e6552 100644 --- a/src/site.rs +++ b/src/site.rs @@ -10,7 +10,7 @@ use walkdir::WalkDir; use errors::{Result, ResultExt}; 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 section::{Section}; use filters; @@ -200,8 +200,9 @@ impl Site { } 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(§ion)); + section.pages = populate_previous_and_next_pages(sorted_pages.as_slice()); match grandparent_paths.get(parent_path) { Some(paths) => section.subsections.extend(paths.clone()), @@ -361,11 +362,13 @@ impl Site { 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 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::>()); context.add("config", &self.config); context.add("current_url", &self.config.base_url); @@ -446,6 +449,10 @@ impl Site { .filter(|&(path, _)| pages_paths.contains(path)) .map(|(_, page)| page) .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()); let mut context = Context::new(); diff --git a/tests/front_matter.rs b/tests/front_matter.rs index 39514ea..575d473 100644 --- a/tests/front_matter.rs +++ b/tests/front_matter.rs @@ -125,7 +125,7 @@ title = "Hello" description = "hey there" date = "2016-10-10""#; let res = FrontMatter::parse(content).unwrap(); - assert!(res.parse_date().is_some()); + assert!(res.date().is_some()); } #[test] @@ -135,7 +135,7 @@ title = "Hello" description = "hey there" date = "2002-10-02T15:00:00Z""#; let res = FrontMatter::parse(content).unwrap(); - assert!(res.parse_date().is_some()); + assert!(res.date().is_some()); } #[test] @@ -145,9 +145,41 @@ title = "Hello" description = "hey there" date = "2002/10/12""#; 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] fn test_can_split_content_valid() {