zola/src/page.rs

221 lines
6.3 KiB
Rust
Raw Normal View History

2016-12-06 08:27:03 +00:00
/// A page, can be a blog post or a basic page
use std::collections::HashMap;
2016-12-06 11:53:14 +00:00
use std::default::Default;
use std::fs::File;
use std::io::prelude::*;
2016-12-13 06:22:24 +00:00
use std::path::Path;
2016-12-06 08:27:03 +00:00
2016-12-13 06:22:24 +00:00
use pulldown_cmark as cmark;
2016-12-06 08:27:03 +00:00
use regex::Regex;
use tera::{Tera, Value, Context};
2016-12-06 08:27:03 +00:00
use errors::{Result, ResultExt};
2016-12-06 12:48:23 +00:00
use config::Config;
use front_matter::parse_front_matter;
2016-12-06 08:27:03 +00:00
lazy_static! {
static ref DELIM_RE: Regex = Regex::new(r"\+\+\+\s*\r?\n").unwrap();
}
2016-12-06 11:53:14 +00:00
2016-12-13 09:05:59 +00:00
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Page {
// .md filepath, excluding the content/ bit
2016-12-13 09:05:59 +00:00
#[serde(skip_serializing)]
pub filepath: String,
2016-12-13 06:22:24 +00:00
// the name of the .md file
2016-12-13 09:05:59 +00:00
#[serde(skip_serializing)]
2016-12-13 06:22:24 +00:00
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`
2016-12-13 09:05:59 +00:00
#[serde(skip_serializing)]
2016-12-13 06:22:24 +00:00
pub sections: Vec<String>,
2016-12-13 09:05:59 +00:00
// the actual content of the page, in markdown
#[serde(skip_serializing)]
pub raw_content: String,
2016-12-06 08:27:03 +00:00
// <title> of the page
pub title: String,
// The page slug
pub slug: String,
2016-12-13 06:22:24 +00:00
// the HTML rendered of the page
pub content: String,
2016-12-13 06:22:24 +00:00
2016-12-06 08:27:03 +00:00
// tags, not to be confused with categories
pub tags: Vec<String>,
2016-12-06 11:53:14 +00:00
// whether this page should be public or not
pub is_draft: bool,
2016-12-06 08:27:03 +00:00
// any extra parameter present in the front matter
// it will be passed to the template context
pub extra: HashMap<String, Value>,
2016-12-06 08:27:03 +00:00
// the url the page appears at, overrides the slug if set
pub url: Option<String>,
2016-12-06 08:27:03 +00:00
// only one category allowed
pub category: Option<String>,
2016-12-06 11:53:14 +00:00
// optional date if we want to order pages (ie blog post)
pub date: Option<String>,
2016-12-13 06:22:24 +00:00
// optional layout, if we want to specify which tpl to render for that page
2016-12-13 09:05:59 +00:00
#[serde(skip_serializing)]
pub layout: Option<String>,
2016-12-06 08:27:03 +00:00
// description that appears when linked, e.g. on twitter
pub description: Option<String>,
2016-12-06 08:27:03 +00:00
}
2016-12-06 11:53:14 +00:00
impl Default for Page {
fn default() -> Page {
Page {
filepath: "".to_string(),
2016-12-13 06:22:24 +00:00
filename: "".to_string(),
sections: vec![],
2016-12-06 11:53:14 +00:00
title: "".to_string(),
slug: "".to_string(),
2016-12-13 06:22:24 +00:00
raw_content: "".to_string(),
2016-12-06 11:53:14 +00:00
content: "".to_string(),
tags: vec![],
is_draft: false,
extra: HashMap::new(),
url: None,
2016-12-06 11:53:14 +00:00
category: None,
date: None,
layout: None,
description: None,
}
}
}
2016-12-06 08:27:03 +00:00
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> {
2016-12-06 08:27:03 +00:00
// 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);
2016-12-06 08:27:03 +00:00
}
// 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];
2016-12-06 11:53:14 +00:00
// 2. create our page, parse front matter and assign all of that
let mut page = Page::default();
page.filepath = filepath.to_string();
2016-12-13 06:22:24 +00:00
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))?;
2016-12-06 11:53:14 +00:00
2016-12-13 06:22:24 +00:00
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)
}
2016-12-06 11:53:14 +00:00
2016-12-13 06:22:24 +00:00
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Page> {
let path = path.as_ref();
let mut content = String::new();
File::open(path)
2016-12-13 06:22:24 +00:00
.chain_err(|| format!("Failed to open '{:?}'", path.display()))?
.read_to_string(&mut content)?;
2016-12-06 08:27:03 +00:00
2016-12-13 06:22:24 +00:00
// 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)
}
2016-12-06 08:27:03 +00:00
fn get_layout_name(&self) -> String {
match self.layout {
Some(ref l) => l.to_string(),
2016-12-13 06:22:24 +00:00
None => "single.html".to_string()
2016-12-06 11:53:14 +00:00
}
2016-12-06 08:27:03 +00:00
}
2016-12-06 12:48:23 +00:00
2016-12-13 06:22:24 +00:00
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);
2016-12-13 06:22:24 +00:00
tera.render(&tpl, context)
.chain_err(|| "Error while rendering template")
}
2016-12-06 08:27:03 +00:00
}
2016-12-13 09:05:59 +00:00
// Order pages by date, no-op for now
pub fn order_pages(pages: Vec<Page>) -> Vec<Page> {
pages
}
2016-12-06 08:27:03 +00:00
#[cfg(test)]
mod tests {
2016-12-06 11:53:14 +00:00
use super::{Page};
#[test]
fn test_can_parse_a_valid_page() {
let content = r#"
title = "Hello"
slug = "hello-world"
2016-12-06 11:53:14 +00:00
+++
Hello world"#;
2016-12-13 06:22:24 +00:00
let res = Page::from_str("post.md", content);
2016-12-06 11:53:14 +00:00
assert!(res.is_ok());
let page = res.unwrap();
2016-12-06 08:27:03 +00:00
2016-12-06 11:53:14 +00:00
assert_eq!(page.title, "Hello".to_string());
assert_eq!(page.slug, "hello-world".to_string());
2016-12-13 06:22:24 +00:00
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()]);
2016-12-06 11:53:14 +00:00
}
2016-12-06 08:27:03 +00:00
}