Move stuff around a bit more

This commit is contained in:
Vincent Prouillet 2017-05-15 12:23:19 +09:00
parent 07f11755d6
commit c35b76406e
11 changed files with 269 additions and 281 deletions

View file

@ -96,17 +96,14 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
// - any page that was referencing the section (index, etc)
let relative_path = site.pages[path].relative_path.clone();
site.permalinks.remove(&relative_path);
match site.pages.remove(path) {
Some(p) => {
if p.meta.has_tags() || p.meta.category.is_some() {
site.populate_tags_and_categories();
}
if let Some(p) = site.pages.remove(path) {
if p.meta.has_tags() || p.meta.category.is_some() {
site.populate_tags_and_categories();
}
if site.find_parent_section(&p).is_some() {
site.populate_sections();
}
},
None => ()
if site.find_parent_section(&p).is_some() {
site.populate_sections();
}
};
}
// Deletion is something that doesn't happen all the time so we

View file

@ -4,7 +4,11 @@
mod page;
mod pagination;
mod section;
mod sorting;
mod utils;
pub use self::page::{Page, sort_pages, populate_previous_and_next_pages};
pub use self::page::{Page};
pub use self::section::{Section};
pub use self::pagination::{Paginator, Pager};
pub use self::sorting::{SortBy, sort_pages, populate_previous_and_next_pages};

View file

@ -1,6 +1,5 @@
/// A page, can be a blog post or a basic page
use std::collections::HashMap;
use std::fs::{read_dir};
use std::path::{Path, PathBuf};
use std::result::Result as StdResult;
@ -11,32 +10,12 @@ use slug::slugify;
use errors::{Result, ResultExt};
use config::Config;
use front_matter::{PageFrontMatter, SortBy, split_page_content};
use front_matter::{PageFrontMatter, split_page_content};
use markdown::markdown_to_html;
use utils::{read_file, find_content_components};
use content::utils::{find_related_assets, get_reading_analytics};
/// Looks into the current folder for the path and see if there's anything that is not a .md
/// file. Those will be copied next to the rendered .html file
fn find_related_assets(path: &Path) -> Vec<PathBuf> {
let mut assets = vec![];
for entry in read_dir(path).unwrap().filter_map(|e| e.ok()) {
let entry_path = entry.path();
if entry_path.is_file() {
match entry_path.extension() {
Some(e) => match e.to_str() {
Some("md") => continue,
_ => assets.push(entry_path.to_path_buf()),
},
None => continue,
}
}
}
assets
}
#[derive(Clone, Debug, PartialEq)]
pub struct Page {
@ -102,20 +81,6 @@ impl Page {
}
}
pub fn has_date(&self) -> bool {
self.meta.date.is_some()
}
/// Get word count and estimated reading time
pub fn get_reading_analytics(&self) -> (usize, usize) {
// Only works for latin language but good enough for a start
let word_count: usize = self.raw_content.split_whitespace().count();
// https://help.medium.com/hc/en-us/articles/214991667-Read-time
// 275 seems a bit too high though
(word_count, (word_count / 200))
}
/// Parse a page given the content of the .md file
/// Files without front matter or with invalid front matter are considered
/// erroneous
@ -253,7 +218,7 @@ impl ser::Serialize for Page {
state.serialize_field("draft", &self.meta.draft)?;
state.serialize_field("category", &self.meta.category)?;
state.serialize_field("extra", &self.meta.extra)?;
let (word_count, reading_time) = self.get_reading_analytics();
let (word_count, reading_time) = get_reading_analytics(&self.raw_content);
state.serialize_field("word_count", &word_count)?;
state.serialize_field("reading_time", &reading_time)?;
state.serialize_field("previous", &self.previous)?;
@ -261,182 +226,3 @@ impl ser::Serialize for Page {
state.end()
}
}
/// Sort pages using the method for the given section
///
/// Any pages that doesn't have a date when the sorting method is date or order
/// when the sorting method is order will be ignored.
pub fn sort_pages(pages: Vec<Page>, sort_by: SortBy) -> (Vec<Page>, Vec<Page>) {
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, cannot_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, cannot_be_sorted)
},
SortBy::None => (pages, vec![])
}
}
/// Horribly inefficient way to set previous and next on each pages
/// So many clones
pub fn populate_previous_and_next_pages(input: &[Page]) -> Vec<Page> {
let pages = input.to_vec();
let mut res = Vec::new();
// 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 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];
new_page.previous = Some(Box::new(previous.clone()));
}
res.push(new_page);
}
res
}
#[cfg(test)]
mod tests {
use std::fs::File;
use tempdir::TempDir;
use front_matter::{PageFrontMatter, SortBy};
use super::{Page, find_related_assets, sort_pages, populate_previous_and_next_pages};
fn create_page_with_date(date: &str) -> Page {
let mut front_matter = PageFrontMatter::default();
front_matter.date = Some(date.to_string());
Page::new(front_matter)
}
fn create_page_with_order(order: usize) -> Page {
let mut front_matter = PageFrontMatter::default();
front_matter.order = Some(order);
Page::new(front_matter)
}
#[test]
fn can_find_related_assets() {
let tmp_dir = TempDir::new("example").expect("create temp dir");
File::create(tmp_dir.path().join("index.md")).unwrap();
File::create(tmp_dir.path().join("example.js")).unwrap();
File::create(tmp_dir.path().join("graph.jpg")).unwrap();
File::create(tmp_dir.path().join("fail.png")).unwrap();
let assets = find_related_assets(tmp_dir.path());
assert_eq!(assets.len(), 3);
assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 3);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "example.js").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);
}
#[test]
fn can_sort_by_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 (pages, _) = sort_pages(input, SortBy::Date);
// 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 can_sort_by_order() {
let input = vec![
create_page_with_order(2),
create_page_with_order(3),
create_page_with_order(1),
];
let (pages, _) = sort_pages(input, SortBy::Order);
// 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 can_sort_by_none() {
let input = vec![
create_page_with_order(2),
create_page_with_order(3),
create_page_with_order(1),
];
let (pages, _) = sort_pages(input, SortBy::None);
// Should be sorted by date
assert_eq!(pages[0].clone().meta.order.unwrap(), 2);
assert_eq!(pages[1].clone().meta.order.unwrap(), 3);
assert_eq!(pages[2].clone().meta.order.unwrap(), 1);
}
#[test]
fn ignore_page_with_missing_field() {
let input = vec![
create_page_with_order(2),
create_page_with_order(3),
create_page_with_date("2019-01-01"),
];
let (pages, unsorted) = sort_pages(input, SortBy::Order);
assert_eq!(pages.len(), 2);
assert_eq!(unsorted.len(), 1);
}
#[test]
fn can_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);
}
}

169
src/content/sorting.rs Normal file
View file

@ -0,0 +1,169 @@
use content::Page;
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SortBy {
Date,
Order,
None,
}
/// Sort pages using the method for the given section
///
/// Any pages that doesn't have a date when the sorting method is date or order
/// when the sorting method is order will be ignored.
pub fn sort_pages(pages: Vec<Page>, sort_by: SortBy) -> (Vec<Page>, Vec<Page>) {
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, cannot_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, cannot_be_sorted)
},
SortBy::None => (pages, vec![])
}
}
/// Horribly inefficient way to set previous and next on each pages
/// So many clones
pub fn populate_previous_and_next_pages(input: &[Page]) -> Vec<Page> {
let pages = input.to_vec();
let mut res = Vec::new();
// 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 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];
new_page.previous = Some(Box::new(previous.clone()));
}
res.push(new_page);
}
res
}
#[cfg(test)]
mod tests {
use front_matter::{PageFrontMatter};
use content::Page;
use super::{SortBy, sort_pages, populate_previous_and_next_pages};
fn create_page_with_date(date: &str) -> Page {
let mut front_matter = PageFrontMatter::default();
front_matter.date = Some(date.to_string());
Page::new(front_matter)
}
fn create_page_with_order(order: usize) -> Page {
let mut front_matter = PageFrontMatter::default();
front_matter.order = Some(order);
Page::new(front_matter)
}
#[test]
fn can_sort_by_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 (pages, _) = sort_pages(input, SortBy::Date);
// 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 can_sort_by_order() {
let input = vec![
create_page_with_order(2),
create_page_with_order(3),
create_page_with_order(1),
];
let (pages, _) = sort_pages(input, SortBy::Order);
// 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 can_sort_by_none() {
let input = vec![
create_page_with_order(2),
create_page_with_order(3),
create_page_with_order(1),
];
let (pages, _) = sort_pages(input, SortBy::None);
// Should be sorted by date
assert_eq!(pages[0].clone().meta.order.unwrap(), 2);
assert_eq!(pages[1].clone().meta.order.unwrap(), 3);
assert_eq!(pages[2].clone().meta.order.unwrap(), 1);
}
#[test]
fn ignore_page_with_missing_field() {
let input = vec![
create_page_with_order(2),
create_page_with_order(3),
create_page_with_date("2019-01-01"),
];
let (pages, unsorted) = sort_pages(input, SortBy::Order);
assert_eq!(pages.len(), 2);
assert_eq!(unsorted.len(), 1);
}
#[test]
fn can_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);
}
}

View file

@ -0,0 +1,77 @@
use std::fs::read_dir;
use std::path::{Path, PathBuf};
/// Looks into the current folder for the path and see if there's anything that is not a .md
/// file. Those will be copied next to the rendered .html file
pub fn find_related_assets(path: &Path) -> Vec<PathBuf> {
let mut assets = vec![];
for entry in read_dir(path).unwrap().filter_map(|e| e.ok()) {
let entry_path = entry.path();
if entry_path.is_file() {
match entry_path.extension() {
Some(e) => match e.to_str() {
Some("md") => continue,
_ => assets.push(entry_path.to_path_buf()),
},
None => continue,
}
}
}
assets
}
/// Get word count and estimated reading time
pub fn get_reading_analytics(content: &str) -> (usize, usize) {
// Only works for latin language but good enough for a start
let word_count: usize = content.split_whitespace().count();
// https://help.medium.com/hc/en-us/articles/214991667-Read-time
// 275 seems a bit too high though
(word_count, (word_count / 200))
}
#[cfg(test)]
mod tests {
use std::fs::File;
use tempdir::TempDir;
use super::{find_related_assets, get_reading_analytics};
#[test]
fn can_find_related_assets() {
let tmp_dir = TempDir::new("example").expect("create temp dir");
File::create(tmp_dir.path().join("index.md")).unwrap();
File::create(tmp_dir.path().join("example.js")).unwrap();
File::create(tmp_dir.path().join("graph.jpg")).unwrap();
File::create(tmp_dir.path().join("fail.png")).unwrap();
let assets = find_related_assets(tmp_dir.path());
assert_eq!(assets.len(), 3);
assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 3);
assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "example.js").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);
}
#[test]
fn reading_analytics_short_text() {
let (word_count, reading_time) = get_reading_analytics("Hello World");
assert_eq!(word_count, 2);
assert_eq!(reading_time, 0);
}
#[test]
fn reading_analytics_long_text() {
let mut content = String::new();
for _ in 0..1000 {
content.push_str(" Hello world");
}
let (word_count, reading_time) = get_reading_analytics(&content);
assert_eq!(word_count, 2000);
assert_eq!(reading_time, 10);
}
}

View file

@ -8,7 +8,7 @@ mod page;
mod section;
pub use self::page::PageFrontMatter;
pub use self::section::{SectionFrontMatter, SortBy};
pub use self::section::{SectionFrontMatter};
lazy_static! {
static ref PAGE_RE: Regex = Regex::new(r"^[[:space:]]*\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap();
@ -30,7 +30,7 @@ fn split_content(file_path: &Path, content: &str) -> Result<(String, String)> {
}
/// Split a file between the front matter and its content.
/// Returns a parsed SectionFrontMatter and the rest of the content
/// Returns a parsed `SectionFrontMatter` and the rest of the content
pub fn split_section_content(file_path: &Path, content: &str) -> Result<(SectionFrontMatter, String)> {
let (front_matter, content) = split_content(file_path, content)?;
let meta = SectionFrontMatter::parse(&front_matter)
@ -39,7 +39,7 @@ pub fn split_section_content(file_path: &Path, content: &str) -> Result<(Section
}
/// 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(file_path: &Path, content: &str) -> Result<(PageFrontMatter, String)> {
let (front_matter, content) = split_content(file_path, content)?;
let meta = PageFrontMatter::parse(&front_matter)

View file

@ -4,16 +4,10 @@ use tera::Value;
use toml;
use errors::{Result};
use content::SortBy;
static DEFAULT_PAGINATE_PATH: &'static str = "page";
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SortBy {
Date,
Order,
None,
}
/// The front matter of every section
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

View file

@ -31,7 +31,7 @@ mod templates;
pub use site::{Site};
pub use config::{Config, get_config};
pub use front_matter::{PageFrontMatter, SectionFrontMatter, split_page_content, split_section_content, SortBy};
pub use content::{Page, Section, sort_pages};
pub use front_matter::{PageFrontMatter, SectionFrontMatter, split_page_content, split_section_content};
pub use content::{Page, Section, SortBy, sort_pages, populate_previous_and_next_pages};
pub use utils::{create_file};
pub use markdown::markdown_to_html;

View file

@ -11,8 +11,7 @@ use walkdir::WalkDir;
use errors::{Result, ResultExt};
use config::{Config, get_config};
use utils::{create_file, create_directory};
use content::{Page, Section, Paginator, populate_previous_and_next_pages, sort_pages};
use front_matter::{SortBy};
use content::{Page, Section, Paginator, SortBy, populate_previous_and_next_pages, sort_pages};
use templates::{GUTENBERG_TERA, global_fns, render_redirect_template};
@ -239,7 +238,7 @@ impl Site {
/// Sorts the pages of the section at the given path
/// By default will sort all sections but can be made to only sort a single one by providing a path
pub fn sort_sections_pages(&mut self, only: Option<&Path>) {
for (path, section) in self.sections.iter_mut() {
for (path, section) in &mut self.sections {
if let Some(p) = only {
if p != path {
continue;

View file

@ -21,7 +21,6 @@ pub fn create_directory<P: AsRef<Path>>(path: P) -> Result<()> {
Ok(())
}
/// Return the content of a file, with error handling added
pub fn read_file<P: AsRef<Path>>(path: P) -> Result<String> {
let path = path.as_ref();

View file

@ -163,43 +163,6 @@ Hello world"#;
assert_eq!(page.permalink, format!("{}{}", Config::default().base_url, "file-with-space"));
}
#[test]
fn test_reading_analytics_short() {
let content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world"#;
let res = Page::parse(Path::new("hello.md"), content, &Config::default());
assert!(res.is_ok());
let mut page = res.unwrap();
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap();
let (word_count, reading_time) = page.get_reading_analytics();
assert_eq!(word_count, 2);
assert_eq!(reading_time, 0);
}
#[test]
fn test_reading_analytics_long() {
let mut content = r#"
+++
title = "Hello"
description = "hey there"
+++
Hello world"#.to_string();
for _ in 0..1000 {
content.push_str(" Hello world");
}
let res = Page::parse(Path::new("hello.md"), &content, &Config::default());
assert!(res.is_ok());
let mut page = res.unwrap();
page.render_markdown(&HashMap::default(), &Tera::default(), &Config::default()).unwrap();
let (word_count, reading_time) = page.get_reading_analytics();
assert_eq!(word_count, 2002);
assert_eq!(reading_time, 10);
}
#[test]
fn test_automatic_summary_is_empty_string() {
let content = r#"