Separate Page and Section front matter into 2 structs

Fix #61
This commit is contained in:
Vincent Prouillet 2017-05-13 13:01:38 +09:00
parent bb3cba1ad5
commit 299c3c8b22
9 changed files with 477 additions and 441 deletions

View file

@ -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()))
}

122
src/front_matter/mod.rs Normal file
View file

@ -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());
}
}

206
src/front_matter/page.rs Normal file
View file

@ -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());
}
}

View file

@ -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,
}
}
}

View file

@ -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};

View 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)
} }

View file

@ -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::default(),
Page::new(FrontMatter::default()), Page::default(),
Page::new(FrontMatter::default()), Page::default(),
]; ];
let section = create_section(false); let section = create_section(false);
let paginator = Paginator::new(pages.as_slice(), &section); let paginator = Paginator::new(pages.as_slice(), &section);
@ -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::default(),
Page::new(FrontMatter::default()), Page::default(),
Page::new(FrontMatter::default()), Page::default(),
]; ];
let section = create_section(true); let section = create_section(true);
let paginator = Paginator::new(pages.as_slice(), &section); let paginator = Paginator::new(pages.as_slice(), &section);
@ -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::default(),
Page::new(FrontMatter::default()), Page::default(),
Page::new(FrontMatter::default()), Page::default(),
]; ];
let section = create_section(false); let section = create_section(false);
let paginator = Paginator::new(pages.as_slice(), &section); let paginator = Paginator::new(pages.as_slice(), &section);

View file

@ -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(&section.file_path); section.components = find_content_components(&section.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![],

View file

@ -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());
}