@@ -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 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 section::{Section}; | |||
pub use utils::{create_file}; | |||
@@ -11,7 +11,7 @@ use slug::slugify; | |||
use errors::{Result, ResultExt}; | |||
use config::Config; | |||
use front_matter::{FrontMatter, SortBy, split_content}; | |||
use front_matter::{PageFrontMatter, SortBy, split_page_content}; | |||
use markdown::markdown_to_html; | |||
use utils::{read_file, find_content_components}; | |||
@@ -41,6 +41,8 @@ fn find_related_assets(path: &Path) -> Vec<PathBuf> { | |||
#[derive(Clone, Debug, PartialEq)] | |||
pub struct Page { | |||
/// The front matter meta-data | |||
pub meta: PageFrontMatter, | |||
/// The .md path | |||
pub file_path: PathBuf, | |||
/// The .md path, starting from the content directory, with / slashes | |||
@@ -60,8 +62,6 @@ pub struct Page { | |||
pub assets: Vec<PathBuf>, | |||
/// The HTML rendered of the page | |||
pub content: String, | |||
/// The front matter meta-data | |||
pub meta: FrontMatter, | |||
/// The slug of that page. | |||
/// First tries to find the slug in the meta and defaults to filename otherwise | |||
@@ -83,8 +83,9 @@ pub struct Page { | |||
impl Page { | |||
pub fn new(meta: FrontMatter) -> Page { | |||
pub fn new(meta: PageFrontMatter) -> Page { | |||
Page { | |||
meta: meta, | |||
file_path: PathBuf::new(), | |||
relative_path: String::new(), | |||
parent_path: PathBuf::new(), | |||
@@ -97,7 +98,6 @@ impl Page { | |||
path: "".to_string(), | |||
permalink: "".to_string(), | |||
summary: None, | |||
meta: meta, | |||
previous: None, | |||
next: None, | |||
} | |||
@@ -122,7 +122,7 @@ impl Page { | |||
/// erroneous | |||
pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Page> { | |||
// 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); | |||
page.file_path = file_path.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 { | |||
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer { | |||
let mut state = serializer.serialize_struct("page", 16)?; | |||
@@ -318,17 +340,17 @@ mod tests { | |||
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}; | |||
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()); | |||
Page::new(front_matter) | |||
} | |||
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); | |||
Page::new(front_matter) | |||
} | |||
@@ -154,14 +154,14 @@ impl<'a> Paginator<'a> { | |||
mod tests { | |||
use tera::{to_value}; | |||
use front_matter::FrontMatter; | |||
use front_matter::SectionFrontMatter; | |||
use page::Page; | |||
use section::Section; | |||
use super::{Paginator}; | |||
fn create_section(is_index: bool) -> Section { | |||
let mut f = FrontMatter::default(); | |||
let mut f = SectionFrontMatter::default(); | |||
f.paginate_by = Some(2); | |||
f.paginate_path = Some("page".to_string()); | |||
let mut s = Section::new("content/_index.md", f); | |||
@@ -178,9 +178,9 @@ mod tests { | |||
#[test] | |||
fn test_can_create_paginator() { | |||
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 paginator = Paginator::new(pages.as_slice(), §ion); | |||
@@ -200,9 +200,9 @@ mod tests { | |||
#[test] | |||
fn test_can_create_paginator_for_index() { | |||
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 paginator = Paginator::new(pages.as_slice(), §ion); | |||
@@ -222,9 +222,9 @@ mod tests { | |||
#[test] | |||
fn test_can_build_paginator_context() { | |||
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 paginator = Paginator::new(pages.as_slice(), §ion); | |||
@@ -6,7 +6,7 @@ use tera::{Tera, Context}; | |||
use serde::ser::{SerializeStruct, self}; | |||
use config::Config; | |||
use front_matter::{FrontMatter, split_content}; | |||
use front_matter::{SectionFrontMatter, split_section_content}; | |||
use errors::{Result, ResultExt}; | |||
use utils::{read_file, find_content_components}; | |||
use markdown::markdown_to_html; | |||
@@ -15,6 +15,8 @@ use page::{Page}; | |||
#[derive(Clone, Debug, PartialEq)] | |||
pub struct Section { | |||
/// The front matter meta-data | |||
pub meta: SectionFrontMatter, | |||
/// The _index.md full path | |||
pub file_path: PathBuf, | |||
/// The .md path, starting from the content directory, with / slashes | |||
@@ -31,8 +33,6 @@ pub struct Section { | |||
pub raw_content: String, | |||
/// The HTML rendered of the page | |||
pub content: String, | |||
/// The front matter meta-data | |||
pub meta: FrontMatter, | |||
/// All direct pages of that section | |||
pub pages: Vec<Page>, | |||
/// All pages that cannot be sorted in this section | |||
@@ -42,10 +42,11 @@ pub struct 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(); | |||
Section { | |||
meta: meta, | |||
file_path: file_path.to_path_buf(), | |||
relative_path: "".to_string(), | |||
parent_path: file_path.parent().unwrap().to_path_buf(), | |||
@@ -54,7 +55,6 @@ impl Section { | |||
permalink: "".to_string(), | |||
raw_content: "".to_string(), | |||
content: "".to_string(), | |||
meta: meta, | |||
pages: vec![], | |||
ignored_pages: vec![], | |||
subsections: vec![], | |||
@@ -62,7 +62,7 @@ impl 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); | |||
section.raw_content = content.clone(); | |||
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 | |||
fn default() -> Section { | |||
Section { | |||
meta: SectionFrontMatter::default(), | |||
file_path: PathBuf::new(), | |||
relative_path: "".to_string(), | |||
parent_path: PathBuf::new(), | |||
@@ -162,7 +163,6 @@ impl Default for Section { | |||
permalink: "".to_string(), | |||
raw_content: "".to_string(), | |||
content: "".to_string(), | |||
meta: FrontMatter::default(), | |||
pages: vec![], | |||
ignored_pages: 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()); | |||
} |