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.

221 lines
6.3KB

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