@@ -1,177 +0,0 @@ | |||||
use std::collections::HashMap; | |||||
use std::path::Path; | |||||
use toml; | |||||
use tera::Value; | |||||
use chrono::prelude::*; | |||||
use regex::Regex; | |||||
use errors::{Result, ResultExt}; | |||||
lazy_static! { | |||||
static ref PAGE_RE: Regex = Regex::new(r"^\r?\n?\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap(); | |||||
} | |||||
#[derive(Debug, Copy, 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)] | |||||
pub struct FrontMatter { | |||||
/// <title> of the page | |||||
pub title: Option<String>, | |||||
/// Description in <meta> that appears when linked, e.g. on twitter | |||||
pub description: Option<String>, | |||||
/// Date if we want to order pages (ie blog post) | |||||
pub date: Option<String>, | |||||
/// The page slug. Will be used instead of the filename if present | |||||
/// Can't be an empty string if present | |||||
pub slug: Option<String>, | |||||
/// The url the page appears at, overrides the slug if set in the front-matter | |||||
/// otherwise is set after parsing front matter and sections | |||||
/// Can't be an empty string if present | |||||
pub url: Option<String>, | |||||
/// Tags, not to be confused with categories | |||||
pub tags: Option<Vec<String>>, | |||||
/// Whether this page is a draft and should be published or not | |||||
pub draft: Option<bool>, | |||||
/// Only one category allowed | |||||
pub category: Option<String>, | |||||
/// Whether to sort by "date", "order" or "none". Defaults to `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 | |||||
#[serde(skip_serializing)] | |||||
pub template: Option<String>, | |||||
/// How many pages to be displayed per paginated page. No pagination will happen if this isn't set | |||||
#[serde(skip_serializing)] | |||||
pub paginate_by: Option<usize>, | |||||
/// Path to be used by pagination: the page number will be appended after it. Defaults to `page`. | |||||
#[serde(skip_serializing)] | |||||
pub paginate_path: Option<String>, | |||||
/// Whether to render that page/section or not. Defaults to `true`. | |||||
#[serde(skip_serializing)] | |||||
pub render: Option<bool>, | |||||
/// Any extra parameter present in the front matter | |||||
pub extra: Option<HashMap<String, Value>>, | |||||
} | |||||
impl FrontMatter { | |||||
pub fn parse(toml: &str) -> Result<FrontMatter> { | |||||
let mut f: FrontMatter = match toml::from_str(toml) { | |||||
Ok(d) => d, | |||||
Err(e) => bail!(e), | |||||
}; | |||||
if let Some(ref slug) = f.slug { | |||||
if slug == "" { | |||||
bail!("`slug` can't be empty if present") | |||||
} | |||||
} | |||||
if let Some(ref url) = f.url { | |||||
if url == "" { | |||||
bail!("`url` can't be empty if present") | |||||
} | |||||
} | |||||
if f.paginate_path.is_none() { | |||||
f.paginate_path = Some("page".to_string()); | |||||
} | |||||
if f.render.is_none() { | |||||
f.render = Some(true); | |||||
} | |||||
Ok(f) | |||||
} | |||||
/// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime | |||||
pub fn date(&self) -> Option<NaiveDateTime> { | |||||
match self.date { | |||||
Some(ref d) => { | |||||
if d.contains('T') { | |||||
DateTime::parse_from_rfc3339(d).ok().and_then(|s| Some(s.naive_local())) | |||||
} else { | |||||
NaiveDate::parse_from_str(d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0,0,0))) | |||||
} | |||||
}, | |||||
None => None, | |||||
} | |||||
} | |||||
pub fn order(&self) -> usize { | |||||
self.order.unwrap() | |||||
} | |||||
/// Returns the current sorting method, defaults to `None` (== no sorting) | |||||
pub fn sort_by(&self) -> SortBy { | |||||
match self.sort_by { | |||||
Some(ref s) => *s, | |||||
None => SortBy::None, | |||||
} | |||||
} | |||||
/// Only applies to section, whether it is paginated or not. | |||||
pub fn is_paginated(&self) -> bool { | |||||
match self.paginate_by { | |||||
Some(v) => v > 0, | |||||
None => false | |||||
} | |||||
} | |||||
pub fn should_render(&self) -> bool { | |||||
self.render.unwrap() | |||||
} | |||||
} | |||||
impl Default for FrontMatter { | |||||
fn default() -> FrontMatter { | |||||
FrontMatter { | |||||
title: None, | |||||
description: None, | |||||
date: None, | |||||
slug: None, | |||||
url: None, | |||||
tags: None, | |||||
draft: None, | |||||
category: None, | |||||
sort_by: None, | |||||
order: None, | |||||
template: None, | |||||
paginate_by: None, | |||||
paginate_path: Some("page".to_string()), | |||||
render: Some(true), | |||||
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 | |||||
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()); | |||||
} | |||||
// 2. extract the front matter and the content | |||||
let caps = PAGE_RE.captures(content).unwrap(); | |||||
// caps[0] is the full match | |||||
let front_matter = &caps[1]; | |||||
let content = &caps[2]; | |||||
// 3. create our page, parse front matter and assign all of that | |||||
let meta = FrontMatter::parse(front_matter) | |||||
.chain_err(|| format!("Error when parsing front matter of file `{}`", file_path.to_string_lossy()))?; | |||||
Ok((meta, content.to_string())) | |||||
} |
@@ -0,0 +1,122 @@ | |||||
use std::path::Path; | |||||
use regex::Regex; | |||||
use errors::{Result, ResultExt}; | |||||
mod page; | |||||
mod section; | |||||
pub use self::page::PageFrontMatter; | |||||
pub use self::section::{SectionFrontMatter, SortBy}; | |||||
lazy_static! { | |||||
static ref PAGE_RE: Regex = Regex::new(r"^[[:space:]]*\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap(); | |||||
} | |||||
/// Split a file between the front matter and its content | |||||
/// Will return an error if the front matter wasn't found | |||||
fn split_content(file_path: &Path, content: &str) -> Result<(String, String)> { | |||||
if !PAGE_RE.is_match(content) { | |||||
bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", file_path.to_string_lossy()); | |||||
} | |||||
// 2. extract the front matter and the content | |||||
let caps = PAGE_RE.captures(content).unwrap(); | |||||
// caps[0] is the full match | |||||
// caps[1] => front matter | |||||
// caps[2] => content | |||||
Ok((caps[1].to_string(), caps[2].to_string())) | |||||
} | |||||
/// Split a file between the front matter and its 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) | |||||
.chain_err(|| format!("Error when parsing front matter of section `{}`", file_path.to_string_lossy()))?; | |||||
Ok((meta, content)) | |||||
} | |||||
/// Split a file between the front matter and its 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) | |||||
.chain_err(|| format!("Error when parsing front matter of section `{}`", file_path.to_string_lossy()))?; | |||||
Ok((meta, content)) | |||||
} | |||||
#[cfg(test)] | |||||
mod tests { | |||||
use std::path::Path; | |||||
use super::{split_section_content, split_page_content}; | |||||
#[test] | |||||
fn can_split_page_content_valid() { | |||||
let content = r#" | |||||
+++ | |||||
title = "Title" | |||||
description = "hey there" | |||||
date = "2002/10/12" | |||||
+++ | |||||
Hello | |||||
"#; | |||||
let (front_matter, content) = split_page_content(Path::new(""), content).unwrap(); | |||||
assert_eq!(content, "Hello\n"); | |||||
assert_eq!(front_matter.title.unwrap(), "Title"); | |||||
} | |||||
#[test] | |||||
fn can_split_section_content_valid() { | |||||
let content = r#" | |||||
+++ | |||||
paginate_by = 10 | |||||
+++ | |||||
Hello | |||||
"#; | |||||
let (front_matter, content) = split_section_content(Path::new(""), content).unwrap(); | |||||
assert_eq!(content, "Hello\n"); | |||||
assert!(front_matter.is_paginated()); | |||||
} | |||||
#[test] | |||||
fn can_split_content_with_only_frontmatter_valid() { | |||||
let content = r#" | |||||
+++ | |||||
title = "Title" | |||||
description = "hey there" | |||||
date = "2002/10/12" | |||||
+++"#; | |||||
let (front_matter, content) = split_page_content(Path::new(""), content).unwrap(); | |||||
assert_eq!(content, ""); | |||||
assert_eq!(front_matter.title.unwrap(), "Title"); | |||||
} | |||||
#[test] | |||||
fn can_split_content_lazily() { | |||||
let content = r#" | |||||
+++ | |||||
title = "Title" | |||||
description = "hey there" | |||||
date = "2002-10-02T15:00:00Z" | |||||
+++ | |||||
+++"#; | |||||
let (front_matter, content) = split_page_content(Path::new(""), content).unwrap(); | |||||
assert_eq!(content, "+++"); | |||||
assert_eq!(front_matter.title.unwrap(), "Title"); | |||||
} | |||||
#[test] | |||||
fn errors_if_cannot_locate_frontmatter() { | |||||
let content = r#" | |||||
+++ | |||||
title = "Title" | |||||
description = "hey there" | |||||
date = "2002/10/12""#; | |||||
let res = split_page_content(Path::new(""), content); | |||||
assert!(res.is_err()); | |||||
} | |||||
} |
@@ -0,0 +1,206 @@ | |||||
use std::collections::HashMap; | |||||
use chrono::prelude::*; | |||||
use tera::Value; | |||||
use toml; | |||||
use errors::{Result}; | |||||
/// The front matter of every page | |||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | |||||
pub struct PageFrontMatter { | |||||
/// <title> of the page | |||||
pub title: Option<String>, | |||||
/// Description in <meta> that appears when linked, e.g. on twitter | |||||
pub description: Option<String>, | |||||
/// Date if we want to order pages (ie blog post) | |||||
pub date: Option<String>, | |||||
/// The page slug. Will be used instead of the filename if present | |||||
/// Can't be an empty string if present | |||||
pub slug: Option<String>, | |||||
/// The url the page appears at, overrides the slug if set in the front-matter | |||||
/// otherwise is set after parsing front matter and sections | |||||
/// Can't be an empty string if present | |||||
pub url: Option<String>, | |||||
/// Tags, not to be confused with categories | |||||
pub tags: Option<Vec<String>>, | |||||
/// Whether this page is a draft and should be published or not | |||||
pub draft: Option<bool>, | |||||
/// Only one category allowed | |||||
pub category: Option<String>, | |||||
/// 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 | |||||
#[serde(skip_serializing)] | |||||
pub template: Option<String>, | |||||
/// Any extra parameter present in the front matter | |||||
pub extra: Option<HashMap<String, Value>>, | |||||
} | |||||
impl PageFrontMatter { | |||||
pub fn parse(toml: &str) -> Result<PageFrontMatter> { | |||||
let f: PageFrontMatter = match toml::from_str(toml) { | |||||
Ok(d) => d, | |||||
Err(e) => bail!(e), | |||||
}; | |||||
if let Some(ref slug) = f.slug { | |||||
if slug == "" { | |||||
bail!("`slug` can't be empty if present") | |||||
} | |||||
} | |||||
if let Some(ref url) = f.url { | |||||
if url == "" { | |||||
bail!("`url` can't be empty if present") | |||||
} | |||||
} | |||||
Ok(f) | |||||
} | |||||
/// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime | |||||
pub fn date(&self) -> Option<NaiveDateTime> { | |||||
match self.date { | |||||
Some(ref d) => { | |||||
if d.contains('T') { | |||||
DateTime::parse_from_rfc3339(d).ok().and_then(|s| Some(s.naive_local())) | |||||
} else { | |||||
NaiveDate::parse_from_str(d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0,0,0))) | |||||
} | |||||
}, | |||||
None => None, | |||||
} | |||||
} | |||||
pub fn order(&self) -> usize { | |||||
self.order.unwrap() | |||||
} | |||||
} | |||||
impl Default for PageFrontMatter { | |||||
fn default() -> PageFrontMatter { | |||||
PageFrontMatter { | |||||
title: None, | |||||
description: None, | |||||
date: None, | |||||
slug: None, | |||||
url: None, | |||||
tags: None, | |||||
draft: None, | |||||
category: None, | |||||
order: None, | |||||
template: None, | |||||
extra: None, | |||||
} | |||||
} | |||||
} | |||||
#[cfg(test)] | |||||
mod tests { | |||||
use super::PageFrontMatter; | |||||
#[test] | |||||
fn can_have_empty_front_matter() { | |||||
let content = r#" "#; | |||||
let res = PageFrontMatter::parse(content); | |||||
assert!(res.is_ok()); | |||||
} | |||||
#[test] | |||||
fn can_parse_valid_front_matter() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there""#; | |||||
let res = PageFrontMatter::parse(content); | |||||
assert!(res.is_ok()); | |||||
let res = res.unwrap(); | |||||
assert_eq!(res.title.unwrap(), "Hello".to_string()); | |||||
assert_eq!(res.description.unwrap(), "hey there".to_string()) | |||||
} | |||||
#[test] | |||||
fn can_parse_tags() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
slug = "hello-world" | |||||
tags = ["rust", "html"]"#; | |||||
let res = PageFrontMatter::parse(content); | |||||
assert!(res.is_ok()); | |||||
let res = res.unwrap(); | |||||
assert_eq!(res.title.unwrap(), "Hello".to_string()); | |||||
assert_eq!(res.slug.unwrap(), "hello-world".to_string()); | |||||
assert_eq!(res.tags.unwrap(), ["rust".to_string(), "html".to_string()]); | |||||
} | |||||
#[test] | |||||
fn errors_with_invalid_front_matter() { | |||||
let content = r#"title = 1\n"#; | |||||
let res = PageFrontMatter::parse(content); | |||||
assert!(res.is_err()); | |||||
} | |||||
#[test] | |||||
fn errors_on_non_string_tag() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
slug = "hello-world" | |||||
tags = ["rust", 1]"#; | |||||
let res = PageFrontMatter::parse(content); | |||||
assert!(res.is_err()); | |||||
} | |||||
#[test] | |||||
fn errors_on_present_but_empty_slug() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
slug = """#; | |||||
let res = PageFrontMatter::parse(content); | |||||
assert!(res.is_err()); | |||||
} | |||||
#[test] | |||||
fn errors_on_present_but_empty_url() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
url = """#; | |||||
let res = PageFrontMatter::parse(content); | |||||
assert!(res.is_err()); | |||||
} | |||||
#[test] | |||||
fn can_parse_date_yyyy_mm_dd() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
date = "2016-10-10""#; | |||||
let res = PageFrontMatter::parse(content).unwrap(); | |||||
assert!(res.date().is_some()); | |||||
} | |||||
#[test] | |||||
fn can_parse_date_rfc3339() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
date = "2002-10-02T15:00:00Z""#; | |||||
let res = PageFrontMatter::parse(content).unwrap(); | |||||
assert!(res.date().is_some()); | |||||
} | |||||
#[test] | |||||
fn cannot_parse_random_date_format() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
date = "2002/10/12""#; | |||||
let res = PageFrontMatter::parse(content).unwrap(); | |||||
assert!(res.date().is_none()); | |||||
} | |||||
} |
@@ -0,0 +1,99 @@ | |||||
use std::collections::HashMap; | |||||
use tera::Value; | |||||
use toml; | |||||
use errors::{Result}; | |||||
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)] | |||||
pub struct SectionFrontMatter { | |||||
/// <title> of the page | |||||
pub title: Option<String>, | |||||
/// Description in <meta> that appears when linked, e.g. on twitter | |||||
pub description: Option<String>, | |||||
/// Whether to sort by "date", "order" or "none". Defaults to `none`. | |||||
#[serde(skip_serializing)] | |||||
pub sort_by: Option<SortBy>, | |||||
/// Optional template, if we want to specify which template to render for that page | |||||
#[serde(skip_serializing)] | |||||
pub template: Option<String>, | |||||
/// How many pages to be displayed per paginated page. No pagination will happen if this isn't set | |||||
#[serde(skip_serializing)] | |||||
pub paginate_by: Option<usize>, | |||||
/// Path to be used by pagination: the page number will be appended after it. Defaults to `page`. | |||||
#[serde(skip_serializing)] | |||||
pub paginate_path: Option<String>, | |||||
/// Whether to render that section or not. Defaults to `true`. | |||||
/// Useful when the section is only there to organize things but is not meant | |||||
/// to be used directly, like a posts section in a personal site | |||||
#[serde(skip_serializing)] | |||||
pub render: Option<bool>, | |||||
/// Any extra parameter present in the front matter | |||||
pub extra: Option<HashMap<String, Value>>, | |||||
} | |||||
impl SectionFrontMatter { | |||||
pub fn parse(toml: &str) -> Result<SectionFrontMatter> { | |||||
let mut f: SectionFrontMatter = match toml::from_str(toml) { | |||||
Ok(d) => d, | |||||
Err(e) => bail!(e), | |||||
}; | |||||
if f.paginate_path.is_none() { | |||||
f.paginate_path = Some(DEFAULT_PAGINATE_PATH.to_string()); | |||||
} | |||||
if f.render.is_none() { | |||||
f.render = Some(true); | |||||
} | |||||
if f.sort_by.is_none() { | |||||
f.sort_by = Some(SortBy::None); | |||||
} | |||||
Ok(f) | |||||
} | |||||
/// Returns the current sorting method, defaults to `None` (== no sorting) | |||||
pub fn sort_by(&self) -> SortBy { | |||||
self.sort_by.unwrap() | |||||
} | |||||
/// Only applies to section, whether it is paginated or not. | |||||
pub fn is_paginated(&self) -> bool { | |||||
match self.paginate_by { | |||||
Some(v) => v > 0, | |||||
None => false | |||||
} | |||||
} | |||||
pub fn should_render(&self) -> bool { | |||||
self.render.unwrap() | |||||
} | |||||
} | |||||
impl Default for SectionFrontMatter { | |||||
fn default() -> SectionFrontMatter { | |||||
SectionFrontMatter { | |||||
title: None, | |||||
description: None, | |||||
sort_by: None, | |||||
template: None, | |||||
paginate_by: None, | |||||
paginate_path: Some(DEFAULT_PAGINATE_PATH.to_string()), | |||||
render: Some(true), | |||||
extra: None, | |||||
} | |||||
} | |||||
} |
@@ -33,7 +33,7 @@ mod templates; | |||||
pub use site::{Site}; | pub use site::{Site}; | ||||
pub use config::{Config, get_config}; | pub use config::{Config, get_config}; | ||||
pub use front_matter::{FrontMatter, split_content, SortBy}; | |||||
pub use front_matter::{PageFrontMatter, SectionFrontMatter, split_page_content, split_section_content, SortBy}; | |||||
pub use page::{Page, populate_previous_and_next_pages}; | pub use page::{Page, populate_previous_and_next_pages}; | ||||
pub use section::{Section}; | pub use section::{Section}; | ||||
pub use utils::{create_file}; | pub use utils::{create_file}; | ||||
@@ -11,7 +11,7 @@ use slug::slugify; | |||||
use errors::{Result, ResultExt}; | use errors::{Result, ResultExt}; | ||||
use config::Config; | use config::Config; | ||||
use front_matter::{FrontMatter, SortBy, split_content}; | |||||
use front_matter::{PageFrontMatter, SortBy, split_page_content}; | |||||
use markdown::markdown_to_html; | use markdown::markdown_to_html; | ||||
use utils::{read_file, find_content_components}; | use utils::{read_file, find_content_components}; | ||||
@@ -41,6 +41,8 @@ fn find_related_assets(path: &Path) -> Vec<PathBuf> { | |||||
#[derive(Clone, Debug, PartialEq)] | #[derive(Clone, Debug, PartialEq)] | ||||
pub struct Page { | pub struct Page { | ||||
/// The front matter meta-data | |||||
pub meta: PageFrontMatter, | |||||
/// The .md path | /// The .md path | ||||
pub file_path: PathBuf, | pub file_path: PathBuf, | ||||
/// The .md path, starting from the content directory, with / slashes | /// The .md path, starting from the content directory, with / slashes | ||||
@@ -60,8 +62,6 @@ pub struct Page { | |||||
pub assets: Vec<PathBuf>, | pub assets: Vec<PathBuf>, | ||||
/// The HTML rendered of the page | /// The HTML rendered of the page | ||||
pub content: String, | pub content: String, | ||||
/// The front matter meta-data | |||||
pub meta: FrontMatter, | |||||
/// The slug of that page. | /// The slug of that page. | ||||
/// First tries to find the slug in the meta and defaults to filename otherwise | /// First tries to find the slug in the meta and defaults to filename otherwise | ||||
@@ -83,8 +83,9 @@ pub struct Page { | |||||
impl Page { | impl Page { | ||||
pub fn new(meta: FrontMatter) -> Page { | |||||
pub fn new(meta: PageFrontMatter) -> Page { | |||||
Page { | Page { | ||||
meta: meta, | |||||
file_path: PathBuf::new(), | file_path: PathBuf::new(), | ||||
relative_path: String::new(), | relative_path: String::new(), | ||||
parent_path: PathBuf::new(), | parent_path: PathBuf::new(), | ||||
@@ -97,7 +98,6 @@ impl Page { | |||||
path: "".to_string(), | path: "".to_string(), | ||||
permalink: "".to_string(), | permalink: "".to_string(), | ||||
summary: None, | summary: None, | ||||
meta: meta, | |||||
previous: None, | previous: None, | ||||
next: None, | next: None, | ||||
} | } | ||||
@@ -122,7 +122,7 @@ impl Page { | |||||
/// erroneous | /// erroneous | ||||
pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Page> { | pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Page> { | ||||
// 1. separate front matter from content | // 1. separate front matter from content | ||||
let (meta, content) = split_content(file_path, content)?; | |||||
let (meta, content) = split_page_content(file_path, content)?; | |||||
let mut page = Page::new(meta); | let mut page = Page::new(meta); | ||||
page.file_path = file_path.to_path_buf(); | page.file_path = file_path.to_path_buf(); | ||||
page.parent_path = page.file_path.parent().unwrap().to_path_buf(); | page.parent_path = page.file_path.parent().unwrap().to_path_buf(); | ||||
@@ -217,6 +217,28 @@ impl Page { | |||||
} | } | ||||
} | } | ||||
impl Default for Page { | |||||
fn default() -> Page { | |||||
Page { | |||||
meta: PageFrontMatter::default(), | |||||
file_path: PathBuf::new(), | |||||
relative_path: String::new(), | |||||
parent_path: PathBuf::new(), | |||||
file_name: "".to_string(), | |||||
components: vec![], | |||||
raw_content: "".to_string(), | |||||
assets: vec![], | |||||
content: "".to_string(), | |||||
slug: "".to_string(), | |||||
path: "".to_string(), | |||||
permalink: "".to_string(), | |||||
summary: None, | |||||
previous: None, | |||||
next: None, | |||||
} | |||||
} | |||||
} | |||||
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", 16)?; | let mut state = serializer.serialize_struct("page", 16)?; | ||||
@@ -318,17 +340,17 @@ mod tests { | |||||
use std::fs::File; | use std::fs::File; | ||||
use front_matter::{FrontMatter, SortBy}; | |||||
use front_matter::{PageFrontMatter, SortBy}; | |||||
use super::{Page, find_related_assets, sort_pages, populate_previous_and_next_pages}; | use super::{Page, find_related_assets, sort_pages, populate_previous_and_next_pages}; | ||||
fn create_page_with_date(date: &str) -> Page { | fn create_page_with_date(date: &str) -> Page { | ||||
let mut front_matter = FrontMatter::default(); | |||||
let mut front_matter = PageFrontMatter::default(); | |||||
front_matter.date = Some(date.to_string()); | front_matter.date = Some(date.to_string()); | ||||
Page::new(front_matter) | Page::new(front_matter) | ||||
} | } | ||||
fn create_page_with_order(order: usize) -> Page { | fn create_page_with_order(order: usize) -> Page { | ||||
let mut front_matter = FrontMatter::default(); | |||||
let mut front_matter = PageFrontMatter::default(); | |||||
front_matter.order = Some(order); | front_matter.order = Some(order); | ||||
Page::new(front_matter) | Page::new(front_matter) | ||||
} | } | ||||
@@ -154,14 +154,14 @@ impl<'a> Paginator<'a> { | |||||
mod tests { | mod tests { | ||||
use tera::{to_value}; | use tera::{to_value}; | ||||
use front_matter::FrontMatter; | |||||
use front_matter::SectionFrontMatter; | |||||
use page::Page; | use page::Page; | ||||
use section::Section; | use section::Section; | ||||
use super::{Paginator}; | use super::{Paginator}; | ||||
fn create_section(is_index: bool) -> Section { | fn create_section(is_index: bool) -> Section { | ||||
let mut f = FrontMatter::default(); | |||||
let mut f = SectionFrontMatter::default(); | |||||
f.paginate_by = Some(2); | f.paginate_by = Some(2); | ||||
f.paginate_path = Some("page".to_string()); | f.paginate_path = Some("page".to_string()); | ||||
let mut s = Section::new("content/_index.md", f); | let mut s = Section::new("content/_index.md", f); | ||||
@@ -178,9 +178,9 @@ mod tests { | |||||
#[test] | #[test] | ||||
fn test_can_create_paginator() { | fn test_can_create_paginator() { | ||||
let pages = vec![ | let pages = vec![ | ||||
Page::new(FrontMatter::default()), | |||||
Page::new(FrontMatter::default()), | |||||
Page::new(FrontMatter::default()), | |||||
Page::default(), | |||||
Page::default(), | |||||
Page::default(), | |||||
]; | ]; | ||||
let section = create_section(false); | let section = create_section(false); | ||||
let paginator = Paginator::new(pages.as_slice(), §ion); | let paginator = Paginator::new(pages.as_slice(), §ion); | ||||
@@ -200,9 +200,9 @@ mod tests { | |||||
#[test] | #[test] | ||||
fn test_can_create_paginator_for_index() { | fn test_can_create_paginator_for_index() { | ||||
let pages = vec![ | let pages = vec![ | ||||
Page::new(FrontMatter::default()), | |||||
Page::new(FrontMatter::default()), | |||||
Page::new(FrontMatter::default()), | |||||
Page::default(), | |||||
Page::default(), | |||||
Page::default(), | |||||
]; | ]; | ||||
let section = create_section(true); | let section = create_section(true); | ||||
let paginator = Paginator::new(pages.as_slice(), §ion); | let paginator = Paginator::new(pages.as_slice(), §ion); | ||||
@@ -222,9 +222,9 @@ mod tests { | |||||
#[test] | #[test] | ||||
fn test_can_build_paginator_context() { | fn test_can_build_paginator_context() { | ||||
let pages = vec![ | let pages = vec![ | ||||
Page::new(FrontMatter::default()), | |||||
Page::new(FrontMatter::default()), | |||||
Page::new(FrontMatter::default()), | |||||
Page::default(), | |||||
Page::default(), | |||||
Page::default(), | |||||
]; | ]; | ||||
let section = create_section(false); | let section = create_section(false); | ||||
let paginator = Paginator::new(pages.as_slice(), §ion); | let paginator = Paginator::new(pages.as_slice(), §ion); | ||||
@@ -6,7 +6,7 @@ use tera::{Tera, Context}; | |||||
use serde::ser::{SerializeStruct, self}; | use serde::ser::{SerializeStruct, self}; | ||||
use config::Config; | use config::Config; | ||||
use front_matter::{FrontMatter, split_content}; | |||||
use front_matter::{SectionFrontMatter, split_section_content}; | |||||
use errors::{Result, ResultExt}; | use errors::{Result, ResultExt}; | ||||
use utils::{read_file, find_content_components}; | use utils::{read_file, find_content_components}; | ||||
use markdown::markdown_to_html; | use markdown::markdown_to_html; | ||||
@@ -15,6 +15,8 @@ use page::{Page}; | |||||
#[derive(Clone, Debug, PartialEq)] | #[derive(Clone, Debug, PartialEq)] | ||||
pub struct Section { | pub struct Section { | ||||
/// The front matter meta-data | |||||
pub meta: SectionFrontMatter, | |||||
/// The _index.md full path | /// The _index.md full path | ||||
pub file_path: PathBuf, | pub file_path: PathBuf, | ||||
/// The .md path, starting from the content directory, with / slashes | /// The .md path, starting from the content directory, with / slashes | ||||
@@ -31,8 +33,6 @@ pub struct Section { | |||||
pub raw_content: String, | pub raw_content: String, | ||||
/// The HTML rendered of the page | /// The HTML rendered of the page | ||||
pub content: String, | pub content: String, | ||||
/// The front matter meta-data | |||||
pub meta: FrontMatter, | |||||
/// All direct pages of that section | /// All direct pages of that section | ||||
pub pages: Vec<Page>, | pub pages: Vec<Page>, | ||||
/// All pages that cannot be sorted in this section | /// All pages that cannot be sorted in this section | ||||
@@ -42,10 +42,11 @@ pub struct Section { | |||||
} | } | ||||
impl Section { | impl Section { | ||||
pub fn new<P: AsRef<Path>>(file_path: P, meta: FrontMatter) -> Section { | |||||
pub fn new<P: AsRef<Path>>(file_path: P, meta: SectionFrontMatter) -> Section { | |||||
let file_path = file_path.as_ref(); | let file_path = file_path.as_ref(); | ||||
Section { | Section { | ||||
meta: meta, | |||||
file_path: file_path.to_path_buf(), | file_path: file_path.to_path_buf(), | ||||
relative_path: "".to_string(), | relative_path: "".to_string(), | ||||
parent_path: file_path.parent().unwrap().to_path_buf(), | parent_path: file_path.parent().unwrap().to_path_buf(), | ||||
@@ -54,7 +55,6 @@ impl Section { | |||||
permalink: "".to_string(), | permalink: "".to_string(), | ||||
raw_content: "".to_string(), | raw_content: "".to_string(), | ||||
content: "".to_string(), | content: "".to_string(), | ||||
meta: meta, | |||||
pages: vec![], | pages: vec![], | ||||
ignored_pages: vec![], | ignored_pages: vec![], | ||||
subsections: vec![], | subsections: vec![], | ||||
@@ -62,7 +62,7 @@ impl Section { | |||||
} | } | ||||
pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Section> { | pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Section> { | ||||
let (meta, content) = split_content(file_path, content)?; | |||||
let (meta, content) = split_section_content(file_path, content)?; | |||||
let mut section = Section::new(file_path, meta); | let mut section = Section::new(file_path, meta); | ||||
section.raw_content = content.clone(); | section.raw_content = content.clone(); | ||||
section.components = find_content_components(§ion.file_path); | section.components = find_content_components(§ion.file_path); | ||||
@@ -154,6 +154,7 @@ impl Default for Section { | |||||
/// Used to create a default index section if there is no _index.md in the root content directory | /// Used to create a default index section if there is no _index.md in the root content directory | ||||
fn default() -> Section { | fn default() -> Section { | ||||
Section { | Section { | ||||
meta: SectionFrontMatter::default(), | |||||
file_path: PathBuf::new(), | file_path: PathBuf::new(), | ||||
relative_path: "".to_string(), | relative_path: "".to_string(), | ||||
parent_path: PathBuf::new(), | parent_path: PathBuf::new(), | ||||
@@ -162,7 +163,6 @@ impl Default for Section { | |||||
permalink: "".to_string(), | permalink: "".to_string(), | ||||
raw_content: "".to_string(), | raw_content: "".to_string(), | ||||
content: "".to_string(), | content: "".to_string(), | ||||
meta: FrontMatter::default(), | |||||
pages: vec![], | pages: vec![], | ||||
ignored_pages: vec![], | ignored_pages: vec![], | ||||
subsections: vec![], | subsections: vec![], | ||||
@@ -1,236 +0,0 @@ | |||||
extern crate gutenberg; | |||||
extern crate tera; | |||||
use std::path::Path; | |||||
use gutenberg::{FrontMatter, split_content, SortBy}; | |||||
use tera::to_value; | |||||
#[test] | |||||
fn test_can_parse_a_valid_front_matter() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there""#; | |||||
let res = FrontMatter::parse(content); | |||||
println!("{:?}", res); | |||||
assert!(res.is_ok()); | |||||
let res = res.unwrap(); | |||||
assert_eq!(res.title.unwrap(), "Hello".to_string()); | |||||
assert_eq!(res.description.unwrap(), "hey there".to_string()); | |||||
} | |||||
#[test] | |||||
fn test_can_parse_tags() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
slug = "hello-world" | |||||
tags = ["rust", "html"]"#; | |||||
let res = FrontMatter::parse(content); | |||||
assert!(res.is_ok()); | |||||
let res = res.unwrap(); | |||||
assert_eq!(res.title.unwrap(), "Hello".to_string()); | |||||
assert_eq!(res.slug.unwrap(), "hello-world".to_string()); | |||||
assert_eq!(res.tags.unwrap(), ["rust".to_string(), "html".to_string()]); | |||||
} | |||||
#[test] | |||||
fn test_can_parse_extra_attributes_in_frontmatter() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
slug = "hello-world" | |||||
[extra] | |||||
language = "en" | |||||
authors = ["Bob", "Alice"]"#; | |||||
let res = FrontMatter::parse(content); | |||||
assert!(res.is_ok()); | |||||
let res = res.unwrap(); | |||||
assert_eq!(res.title.unwrap(), "Hello".to_string()); | |||||
assert_eq!(res.slug.unwrap(), "hello-world".to_string()); | |||||
let extra = res.extra.unwrap(); | |||||
assert_eq!(extra["language"], to_value("en").unwrap()); | |||||
assert_eq!( | |||||
extra["authors"], | |||||
to_value(["Bob".to_string(), "Alice".to_string()]).unwrap() | |||||
); | |||||
} | |||||
#[test] | |||||
fn test_is_ok_with_url_instead_of_slug() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
url = "hello-world""#; | |||||
let res = FrontMatter::parse(content); | |||||
assert!(res.is_ok()); | |||||
let res = res.unwrap(); | |||||
assert!(res.slug.is_none()); | |||||
assert_eq!(res.url.unwrap(), "hello-world".to_string()); | |||||
} | |||||
#[test] | |||||
fn test_is_ok_with_empty_front_matter() { | |||||
let content = r#" "#; | |||||
let res = FrontMatter::parse(content); | |||||
assert!(res.is_ok()); | |||||
} | |||||
#[test] | |||||
fn test_errors_with_invalid_front_matter() { | |||||
let content = r#"title = 1\n"#; | |||||
let res = FrontMatter::parse(content); | |||||
assert!(res.is_err()); | |||||
} | |||||
#[test] | |||||
fn test_errors_on_non_string_tag() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
slug = "hello-world" | |||||
tags = ["rust", 1]"#; | |||||
let res = FrontMatter::parse(content); | |||||
assert!(res.is_err()); | |||||
} | |||||
#[test] | |||||
fn test_errors_on_present_but_empty_slug() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
slug = """#; | |||||
let res = FrontMatter::parse(content); | |||||
assert!(res.is_err()); | |||||
} | |||||
#[test] | |||||
fn test_errors_on_present_but_empty_url() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
url = """#; | |||||
let res = FrontMatter::parse(content); | |||||
assert!(res.is_err()); | |||||
} | |||||
#[test] | |||||
fn test_parse_date_yyyy_mm_dd() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
date = "2016-10-10""#; | |||||
let res = FrontMatter::parse(content).unwrap(); | |||||
assert!(res.date().is_some()); | |||||
} | |||||
#[test] | |||||
fn test_parse_date_rfc3339() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
date = "2002-10-02T15:00:00Z""#; | |||||
let res = FrontMatter::parse(content).unwrap(); | |||||
assert!(res.date().is_some()); | |||||
} | |||||
#[test] | |||||
fn test_cant_parse_random_date_format() { | |||||
let content = r#" | |||||
title = "Hello" | |||||
description = "hey there" | |||||
date = "2002/10/12""#; | |||||
let res = FrontMatter::parse(content).unwrap(); | |||||
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_eq!(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_eq!(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_eq!(res.sort_by.unwrap(), SortBy::None); | |||||
} | |||||
#[test] | |||||
fn test_can_split_content_valid() { | |||||
let content = r#" | |||||
+++ | |||||
title = "Title" | |||||
description = "hey there" | |||||
date = "2002/10/12" | |||||
+++ | |||||
Hello | |||||
"#; | |||||
let (front_matter, content) = split_content(Path::new(""), content).unwrap(); | |||||
assert_eq!(content, "Hello\n"); | |||||
assert_eq!(front_matter.title.unwrap(), "Title"); | |||||
} | |||||
#[test] | |||||
fn test_can_split_content_with_only_frontmatter_valid() { | |||||
let content = r#" | |||||
+++ | |||||
title = "Title" | |||||
description = "hey there" | |||||
date = "2002/10/12" | |||||
+++"#; | |||||
let (front_matter, content) = split_content(Path::new(""), content).unwrap(); | |||||
assert_eq!(content, ""); | |||||
assert_eq!(front_matter.title.unwrap(), "Title"); | |||||
} | |||||
#[test] | |||||
fn test_can_split_content_lazily() { | |||||
let content = r#" | |||||
+++ | |||||
title = "Title" | |||||
description = "hey there" | |||||
date = "2002-10-02T15:00:00Z" | |||||
+++ | |||||
+++"#; | |||||
let (front_matter, content) = split_content(Path::new(""), content).unwrap(); | |||||
assert_eq!(content, "+++"); | |||||
assert_eq!(front_matter.title.unwrap(), "Title"); | |||||
} | |||||
#[test] | |||||
fn test_error_if_cannot_locate_frontmatter() { | |||||
let content = r#" | |||||
+++ | |||||
title = "Title" | |||||
description = "hey there" | |||||
date = "2002/10/12" | |||||
"#; | |||||
let res = split_content(Path::new(""), content); | |||||
assert!(res.is_err()); | |||||
} |