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.

289 lines
8.7KB

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