zola/src/page.rs

289 lines
8.7 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
2016-12-06 11:53:14 +00:00
use std::collections::{HashMap, BTreeMap};
use std::default::Default;
2016-12-06 08:27:03 +00:00
2016-12-06 11:53:14 +00:00
// use pulldown_cmark as cmark;
2016-12-06 08:27:03 +00:00
use regex::Regex;
2016-12-06 11:53:14 +00:00
use toml::{Parser, Value as TomlValue};
use tera::{Value, to_value};
2016-12-06 08:27:03 +00:00
2016-12-06 11:53:14 +00:00
use errors::{Result};
use errors::ErrorKind::InvalidFrontMatter;
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
// Converts from one value (Toml) to another (Tera)
// Used to fill the Page::extra map
fn toml_to_tera(val: &TomlValue) -> Value {
match *val {
TomlValue::String(ref s) | TomlValue::Datetime(ref s) => to_value(s),
TomlValue::Boolean(ref b) => to_value(b),
TomlValue::Integer(ref n) => to_value(n),
TomlValue::Float(ref n) => to_value(n),
TomlValue::Array(ref arr) => to_value(&arr.into_iter().map(toml_to_tera).collect::<Vec<_>>()),
TomlValue::Table(ref table) => {
to_value(&table.into_iter().map(|(k, v)| {
(k, toml_to_tera(v))
}).collect::<BTreeMap<_,_>>())
}
}
}
2016-12-06 08:27:03 +00:00
#[derive(Debug, PartialEq)]
struct Page {
// <title> of the page
title: String,
// the url the page appears at (slug form)
url: String,
// the actual content of the page
content: String,
// tags, not to be confused with categories
tags: Vec<String>,
2016-12-06 11:53:14 +00:00
// whether this page should be public or not
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
2016-12-06 11:53:14 +00:00
extra: HashMap<String, Value>,
2016-12-06 08:27:03 +00:00
// only one category allowed
category: Option<String>,
2016-12-06 11:53:14 +00:00
// optional date if we want to order pages (ie blog post)
date: Option<String>,
2016-12-06 08:27:03 +00:00
// optional layout, if we want to specify which html to render for that page
layout: Option<String>,
// description that appears when linked, e.g. on twitter
description: Option<String>,
}
2016-12-06 11:53:14 +00:00
impl Default for Page {
fn default() -> Page {
Page {
title: "".to_string(),
url: "".to_string(),
content: "".to_string(),
tags: vec![],
is_draft: false,
extra: HashMap::new(),
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
2016-12-06 11:53:14 +00:00
pub fn from_str(filename: &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) {
2016-12-06 11:53:14 +00:00
return Err(InvalidFrontMatter(filename.to_string()).into());
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];
2016-12-06 11:53:14 +00:00
if front_matter.trim() == "" {
return Err(InvalidFrontMatter(filename.to_string()).into());
}
2016-12-06 08:27:03 +00:00
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.content = content.to_string();
// Keeps track of required fields: title, url
let mut num_required_fields = 2;
2016-12-06 08:27:03 +00:00
let mut parser = Parser::new(&front_matter);
2016-12-06 11:53:14 +00:00
2016-12-06 08:27:03 +00:00
if let Some(value) = parser.parse() {
2016-12-06 11:53:14 +00:00
for (key, value) in value.iter() {
if key == "title" {
page.title = value
.as_str()
.ok_or(InvalidFrontMatter(filename.to_string()))?
.to_string();
num_required_fields -= 1;
} else if key == "url" {
page.url = value
.as_str()
.ok_or(InvalidFrontMatter(filename.to_string()))?
.to_string();
num_required_fields -= 1;
} else if key == "draft" {
page.is_draft = value
.as_bool()
.ok_or(InvalidFrontMatter(filename.to_string()))?;
} else if key == "category" {
page.category = Some(
value
.as_str()
.ok_or(InvalidFrontMatter(filename.to_string()))?.to_string()
);
} else if key == "layout" {
page.layout = Some(
value
.as_str()
.ok_or(InvalidFrontMatter(filename.to_string()))?.to_string()
);
} else if key == "description" {
page.description = Some(
value
.as_str()
.ok_or(InvalidFrontMatter(filename.to_string()))?.to_string()
);
} else if key == "date" {
page.date = Some(
value
.as_datetime()
.ok_or(InvalidFrontMatter(filename.to_string()))?.to_string()
);
} else if key == "tags" {
let toml_tags = value
.as_slice()
.ok_or(InvalidFrontMatter(filename.to_string()))?;
for tag in toml_tags {
page.tags.push(
tag
.as_str()
.ok_or(InvalidFrontMatter(filename.to_string()))?
.to_string()
);
}
} else {
page.extra.insert(key.to_string(), toml_to_tera(value));
}
}
2016-12-06 08:27:03 +00:00
} else {
// TODO: handle error in parsing TOML
println!("parse errors: {:?}", parser.errors);
}
2016-12-06 11:53:14 +00:00
if num_required_fields > 0 {
println!("Not all required fields");
return Err(InvalidFrontMatter(filename.to_string()).into());
}
Ok(page)
2016-12-06 08:27:03 +00:00
}
}
#[cfg(test)]
mod tests {
2016-12-06 11:53:14 +00:00
use super::{Page};
use tera::to_value;
#[test]
fn test_can_parse_a_valid_page() {
let content = r#"
title = "Hello"
url = "hello-world"
+++
Hello world"#;
let res = Page::from_str("", content);
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.url, "hello-world".to_string());
assert_eq!(page.content, "Hello world".to_string());
}
#[test]
fn test_can_parse_tags() {
let content = r#"
title = "Hello"
url = "hello-world"
tags = ["rust", "html"]
+++
Hello world"#;
let res = Page::from_str("", content);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.title, "Hello".to_string());
assert_eq!(page.url, "hello-world".to_string());
assert_eq!(page.content, "Hello world".to_string());
assert_eq!(page.tags, ["rust".to_string(), "html".to_string()]);
}
#[test]
fn test_can_parse_extra_attributes_in_frontmatter() {
let content = r#"
title = "Hello"
url = "hello-world"
language = "en"
authors = ["Bob", "Alice"]
+++
Hello world"#;
let res = Page::from_str("", content);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.title, "Hello".to_string());
assert_eq!(page.url, "hello-world".to_string());
assert_eq!(page.extra.get("language").unwrap(), &to_value("en"));
assert_eq!(
page.extra.get("authors").unwrap(),
&to_value(["Bob".to_string(), "Alice".to_string()])
);
}
#[test]
fn test_ignore_pages_with_no_front_matter() {
let content = r#"Hello world"#;
let res = Page::from_str("", content);
assert!(res.is_err());
}
#[test]
fn test_ignores_pages_with_empty_front_matter() {
let content = r#"+++\nHello world"#;
let res = Page::from_str("", content);
assert!(res.is_err());
}
2016-12-06 08:27:03 +00:00
#[test]
2016-12-06 11:53:14 +00:00
fn test_ignores_pages_with_invalid_front_matter() {
let content = r#"title = 1\n+++\nHello world"#;
let res = Page::from_str("", content);
assert!(res.is_err());
}
2016-12-06 08:27:03 +00:00
2016-12-06 11:53:14 +00:00
#[test]
fn test_ignores_pages_with_missing_required_value_front_matter() {
let content = r#"
title = ""
+++
Hello world"#;
let res = Page::from_str("", content);
assert!(res.is_err());
}
#[test]
fn test_errors_on_non_string_tag() {
let content = r#"
title = "Hello"
url = "hello-world"
tags = ["rust", 1]
+++
Hello world"#;
let res = Page::from_str("", content);
assert!(res.is_err());
2016-12-06 08:27:03 +00:00
}
}