You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

294 lines
8.9KB

  1. /// A page, can be a blog post or a basic page
  2. use std::collections::{HashMap, BTreeMap};
  3. use std::default::Default;
  4. // use pulldown_cmark as cmark;
  5. use regex::Regex;
  6. use toml::{Parser, Value as TomlValue};
  7. use tera::{Tera, Value, to_value, Context};
  8. use errors::{Result};
  9. use errors::ErrorKind::InvalidFrontMatter;
  10. use config::Config;
  11. lazy_static! {
  12. static ref DELIM_RE: Regex = Regex::new(r"\+\+\+\s*\r?\n").unwrap();
  13. }
  14. // Converts from one value (Toml) to another (Tera)
  15. // Used to fill the Page::extra map
  16. fn toml_to_tera(val: &TomlValue) -> Value {
  17. match *val {
  18. TomlValue::String(ref s) | TomlValue::Datetime(ref s) => to_value(s),
  19. TomlValue::Boolean(ref b) => to_value(b),
  20. TomlValue::Integer(ref n) => to_value(n),
  21. TomlValue::Float(ref n) => to_value(n),
  22. TomlValue::Array(ref arr) => to_value(&arr.into_iter().map(toml_to_tera).collect::<Vec<_>>()),
  23. TomlValue::Table(ref table) => {
  24. to_value(&table.into_iter().map(|(k, v)| {
  25. (k, toml_to_tera(v))
  26. }).collect::<BTreeMap<_,_>>())
  27. }
  28. }
  29. }
  30. #[derive(Debug, PartialEq)]
  31. struct Page {
  32. // <title> of the page
  33. title: String,
  34. // the url the page appears at (slug form)
  35. url: String,
  36. // the actual content of the page
  37. content: String,
  38. // tags, not to be confused with categories
  39. tags: Vec<String>,
  40. // whether this page should be public or not
  41. is_draft: bool,
  42. // any extra parameter present in the front matter
  43. // it will be passed to the template context
  44. extra: HashMap<String, Value>,
  45. // only one category allowed
  46. category: Option<String>,
  47. // optional date if we want to order pages (ie blog post)
  48. date: Option<String>,
  49. // optional layout, if we want to specify which html to render for that page
  50. layout: Option<String>,
  51. // description that appears when linked, e.g. on twitter
  52. description: Option<String>,
  53. }
  54. impl Default for Page {
  55. fn default() -> Page {
  56. Page {
  57. title: "".to_string(),
  58. url: "".to_string(),
  59. content: "".to_string(),
  60. tags: vec![],
  61. is_draft: false,
  62. extra: HashMap::new(),
  63. category: None,
  64. date: None,
  65. layout: None,
  66. description: None,
  67. }
  68. }
  69. }
  70. impl Page {
  71. // Parse a page given the content of the .md file
  72. // Files without front matter or with invalid front matter are considered
  73. // erroneous
  74. pub fn from_str(filename: &str, content: &str) -> Result<Page> {
  75. // 1. separate front matter from content
  76. if !DELIM_RE.is_match(content) {
  77. return Err(InvalidFrontMatter(filename.to_string()).into());
  78. }
  79. // 2. extract the front matter and the content
  80. let splits: Vec<&str> = DELIM_RE.splitn(content, 2).collect();
  81. let front_matter = splits[0];
  82. if front_matter.trim() == "" {
  83. return Err(InvalidFrontMatter(filename.to_string()).into());
  84. }
  85. let content = splits[1];
  86. // 2. create our page, parse front matter and assign all of that
  87. let mut page = Page::default();
  88. page.content = content.to_string();
  89. // Keeps track of required fields: title, url
  90. let mut num_required_fields = 2;
  91. let mut parser = Parser::new(&front_matter);
  92. if let Some(value) = parser.parse() {
  93. for (key, value) in value.iter() {
  94. if key == "title" {
  95. page.title = value
  96. .as_str()
  97. .ok_or(InvalidFrontMatter(filename.to_string()))?
  98. .to_string();
  99. num_required_fields -= 1;
  100. } else if key == "url" {
  101. page.url = value
  102. .as_str()
  103. .ok_or(InvalidFrontMatter(filename.to_string()))?
  104. .to_string();
  105. num_required_fields -= 1;
  106. } else if key == "draft" {
  107. page.is_draft = value
  108. .as_bool()
  109. .ok_or(InvalidFrontMatter(filename.to_string()))?;
  110. } else if key == "category" {
  111. page.category = Some(
  112. value
  113. .as_str()
  114. .ok_or(InvalidFrontMatter(filename.to_string()))?.to_string()
  115. );
  116. } else if key == "layout" {
  117. page.layout = Some(
  118. value
  119. .as_str()
  120. .ok_or(InvalidFrontMatter(filename.to_string()))?.to_string()
  121. );
  122. } else if key == "description" {
  123. page.description = Some(
  124. value
  125. .as_str()
  126. .ok_or(InvalidFrontMatter(filename.to_string()))?.to_string()
  127. );
  128. } else if key == "date" {
  129. page.date = Some(
  130. value
  131. .as_datetime()
  132. .ok_or(InvalidFrontMatter(filename.to_string()))?.to_string()
  133. );
  134. } else if key == "tags" {
  135. let toml_tags = value
  136. .as_slice()
  137. .ok_or(InvalidFrontMatter(filename.to_string()))?;
  138. for tag in toml_tags {
  139. page.tags.push(
  140. tag
  141. .as_str()
  142. .ok_or(InvalidFrontMatter(filename.to_string()))?
  143. .to_string()
  144. );
  145. }
  146. } else {
  147. page.extra.insert(key.to_string(), toml_to_tera(value));
  148. }
  149. }
  150. } else {
  151. // TODO: handle error in parsing TOML
  152. println!("parse errors: {:?}", parser.errors);
  153. }
  154. if num_required_fields > 0 {
  155. println!("Not all required fields");
  156. return Err(InvalidFrontMatter(filename.to_string()).into());
  157. }
  158. Ok(page)
  159. }
  160. // pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> {
  161. //
  162. // }
  163. }
  164. #[cfg(test)]
  165. mod tests {
  166. use super::{Page};
  167. use tera::to_value;
  168. #[test]
  169. fn test_can_parse_a_valid_page() {
  170. let content = r#"
  171. title = "Hello"
  172. url = "hello-world"
  173. +++
  174. Hello world"#;
  175. let res = Page::from_str("", content);
  176. assert!(res.is_ok());
  177. let page = res.unwrap();
  178. assert_eq!(page.title, "Hello".to_string());
  179. assert_eq!(page.url, "hello-world".to_string());
  180. assert_eq!(page.content, "Hello world".to_string());
  181. }
  182. #[test]
  183. fn test_can_parse_tags() {
  184. let content = r#"
  185. title = "Hello"
  186. url = "hello-world"
  187. tags = ["rust", "html"]
  188. +++
  189. Hello world"#;
  190. let res = Page::from_str("", content);
  191. assert!(res.is_ok());
  192. let page = res.unwrap();
  193. assert_eq!(page.title, "Hello".to_string());
  194. assert_eq!(page.url, "hello-world".to_string());
  195. assert_eq!(page.content, "Hello world".to_string());
  196. assert_eq!(page.tags, ["rust".to_string(), "html".to_string()]);
  197. }
  198. #[test]
  199. fn test_can_parse_extra_attributes_in_frontmatter() {
  200. let content = r#"
  201. title = "Hello"
  202. url = "hello-world"
  203. language = "en"
  204. authors = ["Bob", "Alice"]
  205. +++
  206. Hello world"#;
  207. let res = Page::from_str("", content);
  208. assert!(res.is_ok());
  209. let page = res.unwrap();
  210. assert_eq!(page.title, "Hello".to_string());
  211. assert_eq!(page.url, "hello-world".to_string());
  212. assert_eq!(page.extra.get("language").unwrap(), &to_value("en"));
  213. assert_eq!(
  214. page.extra.get("authors").unwrap(),
  215. &to_value(["Bob".to_string(), "Alice".to_string()])
  216. );
  217. }
  218. #[test]
  219. fn test_ignore_pages_with_no_front_matter() {
  220. let content = r#"Hello world"#;
  221. let res = Page::from_str("", content);
  222. assert!(res.is_err());
  223. }
  224. #[test]
  225. fn test_ignores_pages_with_empty_front_matter() {
  226. let content = r#"+++\nHello world"#;
  227. let res = Page::from_str("", content);
  228. assert!(res.is_err());
  229. }
  230. #[test]
  231. fn test_ignores_pages_with_invalid_front_matter() {
  232. let content = r#"title = 1\n+++\nHello world"#;
  233. let res = Page::from_str("", content);
  234. assert!(res.is_err());
  235. }
  236. #[test]
  237. fn test_ignores_pages_with_missing_required_value_front_matter() {
  238. let content = r#"
  239. title = ""
  240. +++
  241. Hello world"#;
  242. let res = Page::from_str("", content);
  243. assert!(res.is_err());
  244. }
  245. #[test]
  246. fn test_errors_on_non_string_tag() {
  247. let content = r#"
  248. title = "Hello"
  249. url = "hello-world"
  250. tags = ["rust", 1]
  251. +++
  252. Hello world"#;
  253. let res = Page::from_str("", content);
  254. assert!(res.is_err());
  255. }
  256. }