/// A page, can be a blog post or a basic page use std::collections::HashMap; use std::default::Default; use std::fs::File; use std::io::prelude::*; use std::path::Path; use pulldown_cmark as cmark; use regex::Regex; use tera::{Tera, Value, Context}; use errors::{Result, ResultExt}; use config::Config; use front_matter::parse_front_matter; lazy_static! { static ref DELIM_RE: Regex = Regex::new(r"\+\+\+\s*\r?\n").unwrap(); } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Page { // .md filepath, excluding the content/ bit #[serde(skip_serializing)] pub filepath: String, // the name of the .md file #[serde(skip_serializing)] pub filename: String, // the directories above our .md file are called sections // for example a file at content/kb/solutions/blabla.md will have 2 sections: // `kb` and `solutions` #[serde(skip_serializing)] pub sections: Vec, // the actual content of the page, in markdown #[serde(skip_serializing)] pub raw_content: String, // of the page pub title: String, // The page slug pub slug: String, // the HTML rendered of the page pub content: String, // tags, not to be confused with categories pub tags: Vec<String>, // whether this page should be public or not pub is_draft: bool, // any extra parameter present in the front matter // it will be passed to the template context pub extra: HashMap<String, Value>, // the url the page appears at, overrides the slug if set pub url: Option<String>, // only one category allowed pub category: Option<String>, // optional date if we want to order pages (ie blog post) pub date: Option<String>, // optional layout, if we want to specify which tpl to render for that page #[serde(skip_serializing)] pub layout: Option<String>, // description that appears when linked, e.g. on twitter pub description: Option<String>, } impl Default for Page { fn default() -> Page { Page { filepath: "".to_string(), filename: "".to_string(), sections: vec![], title: "".to_string(), slug: "".to_string(), raw_content: "".to_string(), content: "".to_string(), tags: vec![], is_draft: false, extra: HashMap::new(), url: None, category: None, date: None, layout: None, description: None, } } } impl Page { // Parse a page given the content of the .md file // Files without front matter or with invalid front matter are considered // erroneous pub fn from_str(filepath: &str, content: &str) -> Result<Page> { // 1. separate front matter from content if !DELIM_RE.is_match(content) { bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", filepath); } // 2. extract the front matter and the content let splits: Vec<&str> = DELIM_RE.splitn(content, 2).collect(); let front_matter = splits[0]; let content = splits[1]; // 2. create our page, parse front matter and assign all of that let mut page = Page::default(); page.filepath = filepath.to_string(); let path = Path::new(filepath); page.filename = path.file_stem().expect("Couldn't get file stem").to_string_lossy().to_string(); // find out if we have sections for section in path.parent().unwrap().components() { page.sections.push(section.as_ref().to_string_lossy().to_string()); } page.raw_content = content.to_string(); parse_front_matter(front_matter, &mut page) .chain_err(|| format!("Error when parsing front matter of file `{}`", filepath))?; page.content = { let mut html = String::new(); let parser = cmark::Parser::new(&page.raw_content); cmark::html::push_html(&mut html, parser); html }; Ok(page) } pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Page> { let path = path.as_ref(); let mut content = String::new(); File::open(path) .chain_err(|| format!("Failed to open '{:?}'", path.display()))? .read_to_string(&mut content)?; // Remove the content string from name // Maybe get a path as an arg instead and use strip_prefix? Page::from_str(&path.strip_prefix("content").unwrap().to_string_lossy(), &content) } fn get_layout_name(&self) -> String { match self.layout { Some(ref l) => l.to_string(), None => "single.html".to_string() } } pub fn render_html(&mut self, tera: &Tera, config: &Config) -> Result<String> { let tpl = self.get_layout_name(); let mut context = Context::new(); context.add("site", config); context.add("page", self); tera.render(&tpl, context) .chain_err(|| "Error while rendering template") } } // Order pages by date, no-op for now pub fn order_pages(pages: Vec<Page>) -> Vec<Page> { pages } #[cfg(test)] mod tests { use super::{Page}; #[test] fn test_can_parse_a_valid_page() { let content = r#" title = "Hello" slug = "hello-world" +++ Hello world"#; let res = Page::from_str("post.md", content); assert!(res.is_ok()); let page = res.unwrap(); assert_eq!(page.title, "Hello".to_string()); assert_eq!(page.slug, "hello-world".to_string()); assert_eq!(page.raw_content, "Hello world".to_string()); assert_eq!(page.content, "<p>Hello world</p>\n".to_string()); } #[test] fn test_can_find_one_parent_directory() { let content = r#" title = "Hello" slug = "hello-world" +++ Hello world"#; let res = Page::from_str("posts/intro.md", content); assert!(res.is_ok()); let page = res.unwrap(); assert_eq!(page.sections, vec!["posts".to_string()]); } #[test] fn test_can_find_multiplie_parent_directories() { let content = r#" title = "Hello" slug = "hello-world" +++ Hello world"#; let res = Page::from_str("posts/intro/start.md", content); assert!(res.is_ok()); let page = res.unwrap(); assert_eq!(page.sections, vec!["posts".to_string(), "intro".to_string()]); } }