diff --git a/src/bin/rebuild.rs b/src/bin/rebuild.rs index 2273f1c..e742976 100644 --- a/src/bin/rebuild.rs +++ b/src/bin/rebuild.rs @@ -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 diff --git a/src/content/mod.rs b/src/content/mod.rs index f88347b..0213338 100644 --- a/src/content/mod.rs +++ b/src/content/mod.rs @@ -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}; + diff --git a/src/content/page.rs b/src/content/page.rs index 52fdadf..64cd1f5 100644 --- a/src/content/page.rs +++ b/src/content/page.rs @@ -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 { - 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, sort_by: SortBy) -> (Vec, Vec) { - 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 { - 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); - } -} diff --git a/src/content/sorting.rs b/src/content/sorting.rs new file mode 100644 index 0000000..9ed06fd --- /dev/null +++ b/src/content/sorting.rs @@ -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, sort_by: SortBy) -> (Vec, Vec) { + 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 { + 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); + } +} diff --git a/src/content/utils.rs b/src/content/utils.rs index e69de29..6f2be61 100644 --- a/src/content/utils.rs +++ b/src/content/utils.rs @@ -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 { + 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); + } +} diff --git a/src/front_matter/mod.rs b/src/front_matter/mod.rs index 7e20a93..6bc075b 100644 --- a/src/front_matter/mod.rs +++ b/src/front_matter/mod.rs @@ -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) diff --git a/src/front_matter/section.rs b/src/front_matter/section.rs index 1e4fd13..27e62fd 100644 --- a/src/front_matter/section.rs +++ b/src/front_matter/section.rs @@ -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)] diff --git a/src/lib.rs b/src/lib.rs index 497e94b..46b9ee0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/site.rs b/src/site.rs index 54a402a..f771195 100644 --- a/src/site.rs +++ b/src/site.rs @@ -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; diff --git a/src/utils.rs b/src/utils.rs index 38a4eb9..13e1c68 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -21,7 +21,6 @@ pub fn create_directory>(path: P) -> Result<()> { Ok(()) } - /// Return the content of a file, with error handling added pub fn read_file>(path: P) -> Result { let path = path.as_ref(); diff --git a/tests/page.rs b/tests/page.rs index 83abfa9..aa7e1c7 100644 --- a/tests/page.rs +++ b/tests/page.rs @@ -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#"