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.

210 lines
6.1KB

  1. /// A page, can be a blog post or a basic page
  2. use std::collections::HashMap;
  3. use std::default::Default;
  4. use std::fs::File;
  5. use std::io::prelude::*;
  6. use std::path::Path;
  7. use pulldown_cmark as cmark;
  8. use regex::Regex;
  9. use tera::{Tera, Value, Context};
  10. use errors::{Result, ResultExt};
  11. use config::Config;
  12. use front_matter::parse_front_matter;
  13. lazy_static! {
  14. static ref DELIM_RE: Regex = Regex::new(r"\+\+\+\s*\r?\n").unwrap();
  15. }
  16. #[derive(Debug, PartialEq, Serialize, Deserialize)]
  17. pub struct Page {
  18. // .md filepath, excluding the content/ bit
  19. pub filepath: String,
  20. // the name of the .md file
  21. pub filename: String,
  22. // the directories above our .md file are called sections
  23. // for example a file at content/kb/solutions/blabla.md will have 2 sections:
  24. // `kb` and `solutions`
  25. pub sections: Vec<String>,
  26. // <title> of the page
  27. pub title: String,
  28. // The page slug
  29. pub slug: String,
  30. // the actual content of the page
  31. pub raw_content: String,
  32. // the HTML rendered of the page
  33. pub content: String,
  34. // tags, not to be confused with categories
  35. pub tags: Vec<String>,
  36. // whether this page should be public or not
  37. pub is_draft: bool,
  38. // any extra parameter present in the front matter
  39. // it will be passed to the template context
  40. pub extra: HashMap<String, Value>,
  41. // the url the page appears at, overrides the slug if set
  42. pub url: Option<String>,
  43. // only one category allowed
  44. pub category: Option<String>,
  45. // optional date if we want to order pages (ie blog post)
  46. pub date: Option<String>,
  47. // optional layout, if we want to specify which tpl to render for that page
  48. pub layout: Option<String>,
  49. // description that appears when linked, e.g. on twitter
  50. pub description: Option<String>,
  51. }
  52. impl Default for Page {
  53. fn default() -> Page {
  54. Page {
  55. filepath: "".to_string(),
  56. filename: "".to_string(),
  57. sections: vec![],
  58. title: "".to_string(),
  59. slug: "".to_string(),
  60. raw_content: "".to_string(),
  61. content: "".to_string(),
  62. tags: vec![],
  63. is_draft: false,
  64. extra: HashMap::new(),
  65. url: None,
  66. category: None,
  67. date: None,
  68. layout: None,
  69. description: None,
  70. }
  71. }
  72. }
  73. impl Page {
  74. // Parse a page given the content of the .md file
  75. // Files without front matter or with invalid front matter are considered
  76. // erroneous
  77. pub fn from_str(filepath: &str, content: &str) -> Result<Page> {
  78. // 1. separate front matter from content
  79. if !DELIM_RE.is_match(content) {
  80. bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", filepath);
  81. }
  82. // 2. extract the front matter and the content
  83. let splits: Vec<&str> = DELIM_RE.splitn(content, 2).collect();
  84. let front_matter = splits[0];
  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.filepath = filepath.to_string();
  89. let path = Path::new(filepath);
  90. page.filename = path.file_stem().expect("Couldn't get file stem").to_string_lossy().to_string();
  91. // find out if we have sections
  92. for section in path.parent().unwrap().components() {
  93. page.sections.push(section.as_ref().to_string_lossy().to_string());
  94. }
  95. page.raw_content = content.to_string();
  96. parse_front_matter(front_matter, &mut page)
  97. .chain_err(|| format!("Error when parsing front matter of file `{}`", filepath))?;
  98. page.content = {
  99. let mut html = String::new();
  100. let parser = cmark::Parser::new(&page.raw_content);
  101. cmark::html::push_html(&mut html, parser);
  102. html
  103. };
  104. Ok(page)
  105. }
  106. pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Page> {
  107. let path = path.as_ref();
  108. let mut content = String::new();
  109. File::open(path)
  110. .chain_err(|| format!("Failed to open '{:?}'", path.display()))?
  111. .read_to_string(&mut content)?;
  112. // Remove the content string from name
  113. // Maybe get a path as an arg instead and use strip_prefix?
  114. Page::from_str(&path.strip_prefix("content").unwrap().to_string_lossy(), &content)
  115. }
  116. fn get_layout_name(&self) -> String {
  117. match self.layout {
  118. Some(ref l) => l.to_string(),
  119. None => "single.html".to_string()
  120. }
  121. }
  122. pub fn render_html(&mut self, tera: &Tera, config: &Config) -> Result<String> {
  123. let tpl = self.get_layout_name();
  124. let mut context = Context::new();
  125. context.add("site", config);
  126. context.add("page", self);
  127. tera.render(&tpl, context)
  128. .chain_err(|| "Error while rendering template")
  129. }
  130. }
  131. #[cfg(test)]
  132. mod tests {
  133. use super::{Page};
  134. #[test]
  135. fn test_can_parse_a_valid_page() {
  136. let content = r#"
  137. title = "Hello"
  138. slug = "hello-world"
  139. +++
  140. Hello world"#;
  141. let res = Page::from_str("post.md", content);
  142. assert!(res.is_ok());
  143. let page = res.unwrap();
  144. assert_eq!(page.title, "Hello".to_string());
  145. assert_eq!(page.slug, "hello-world".to_string());
  146. assert_eq!(page.raw_content, "Hello world".to_string());
  147. assert_eq!(page.content, "<p>Hello world</p>\n".to_string());
  148. }
  149. #[test]
  150. fn test_can_find_one_parent_directory() {
  151. let content = r#"
  152. title = "Hello"
  153. slug = "hello-world"
  154. +++
  155. Hello world"#;
  156. let res = Page::from_str("posts/intro.md", content);
  157. assert!(res.is_ok());
  158. let page = res.unwrap();
  159. assert_eq!(page.sections, vec!["posts".to_string()]);
  160. }
  161. #[test]
  162. fn test_can_find_multiplie_parent_directories() {
  163. let content = r#"
  164. title = "Hello"
  165. slug = "hello-world"
  166. +++
  167. Hello world"#;
  168. let res = Page::from_str("posts/intro/start.md", content);
  169. assert!(res.is_ok());
  170. let page = res.unwrap();
  171. assert_eq!(page.sections, vec!["posts".to_string(), "intro".to_string()]);
  172. }
  173. }