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.

217 lines
5.9KB

  1. use std::collections::HashMap;
  2. use chrono::prelude::*;
  3. use tera::Value;
  4. use toml;
  5. use errors::{Result};
  6. /// The front matter of every page
  7. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
  8. pub struct PageFrontMatter {
  9. /// <title> of the page
  10. pub title: Option<String>,
  11. /// Description in <meta> that appears when linked, e.g. on twitter
  12. pub description: Option<String>,
  13. /// Date if we want to order pages (ie blog post)
  14. pub date: Option<String>,
  15. /// The page slug. Will be used instead of the filename if present
  16. /// Can't be an empty string if present
  17. pub slug: Option<String>,
  18. /// The url the page appears at, overrides the slug if set in the front-matter
  19. /// otherwise is set after parsing front matter and sections
  20. /// Can't be an empty string if present
  21. pub url: Option<String>,
  22. /// Tags, not to be confused with categories
  23. pub tags: Option<Vec<String>>,
  24. /// Only one category allowed. Can't be an empty string if present
  25. pub category: Option<String>,
  26. /// Integer to use to order content. Lowest is at the bottom, highest first
  27. pub order: Option<usize>,
  28. /// Specify a template different from `page.html` to use for that page
  29. #[serde(skip_serializing)]
  30. pub template: Option<String>,
  31. /// Any extra parameter present in the front matter
  32. pub extra: Option<HashMap<String, Value>>,
  33. }
  34. impl PageFrontMatter {
  35. pub fn parse(toml: &str) -> Result<PageFrontMatter> {
  36. let f: PageFrontMatter = match toml::from_str(toml) {
  37. Ok(d) => d,
  38. Err(e) => bail!(e),
  39. };
  40. if let Some(ref slug) = f.slug {
  41. if slug == "" {
  42. bail!("`slug` can't be empty if present")
  43. }
  44. }
  45. if let Some(ref url) = f.url {
  46. if url == "" {
  47. bail!("`url` can't be empty if present")
  48. }
  49. }
  50. if let Some(ref category) = f.category {
  51. if category == "" {
  52. bail!("`category` can't be empty if present")
  53. }
  54. }
  55. Ok(f)
  56. }
  57. /// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime
  58. pub fn date(&self) -> Option<NaiveDateTime> {
  59. match self.date {
  60. Some(ref d) => {
  61. if d.contains('T') {
  62. DateTime::parse_from_rfc3339(d).ok().and_then(|s| Some(s.naive_local()))
  63. } else {
  64. NaiveDate::parse_from_str(d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0,0,0)))
  65. }
  66. },
  67. None => None,
  68. }
  69. }
  70. pub fn order(&self) -> usize {
  71. self.order.unwrap()
  72. }
  73. pub fn has_tags(&self) -> bool {
  74. match self.tags {
  75. Some(ref t) => !t.is_empty(),
  76. None => false
  77. }
  78. }
  79. }
  80. impl Default for PageFrontMatter {
  81. fn default() -> PageFrontMatter {
  82. PageFrontMatter {
  83. title: None,
  84. description: None,
  85. date: None,
  86. slug: None,
  87. url: None,
  88. tags: None,
  89. category: None,
  90. order: None,
  91. template: None,
  92. extra: None,
  93. }
  94. }
  95. }
  96. #[cfg(test)]
  97. mod tests {
  98. use super::PageFrontMatter;
  99. #[test]
  100. fn can_have_empty_front_matter() {
  101. let content = r#" "#;
  102. let res = PageFrontMatter::parse(content);
  103. assert!(res.is_ok());
  104. }
  105. #[test]
  106. fn can_parse_valid_front_matter() {
  107. let content = r#"
  108. title = "Hello"
  109. description = "hey there""#;
  110. let res = PageFrontMatter::parse(content);
  111. assert!(res.is_ok());
  112. let res = res.unwrap();
  113. assert_eq!(res.title.unwrap(), "Hello".to_string());
  114. assert_eq!(res.description.unwrap(), "hey there".to_string())
  115. }
  116. #[test]
  117. fn can_parse_tags() {
  118. let content = r#"
  119. title = "Hello"
  120. description = "hey there"
  121. slug = "hello-world"
  122. tags = ["rust", "html"]"#;
  123. let res = PageFrontMatter::parse(content);
  124. assert!(res.is_ok());
  125. let res = res.unwrap();
  126. assert_eq!(res.title.unwrap(), "Hello".to_string());
  127. assert_eq!(res.slug.unwrap(), "hello-world".to_string());
  128. assert_eq!(res.tags.unwrap(), ["rust".to_string(), "html".to_string()]);
  129. }
  130. #[test]
  131. fn errors_with_invalid_front_matter() {
  132. let content = r#"title = 1\n"#;
  133. let res = PageFrontMatter::parse(content);
  134. assert!(res.is_err());
  135. }
  136. #[test]
  137. fn errors_on_non_string_tag() {
  138. let content = r#"
  139. title = "Hello"
  140. description = "hey there"
  141. slug = "hello-world"
  142. tags = ["rust", 1]"#;
  143. let res = PageFrontMatter::parse(content);
  144. assert!(res.is_err());
  145. }
  146. #[test]
  147. fn errors_on_present_but_empty_slug() {
  148. let content = r#"
  149. title = "Hello"
  150. description = "hey there"
  151. slug = """#;
  152. let res = PageFrontMatter::parse(content);
  153. assert!(res.is_err());
  154. }
  155. #[test]
  156. fn errors_on_present_but_empty_url() {
  157. let content = r#"
  158. title = "Hello"
  159. description = "hey there"
  160. url = """#;
  161. let res = PageFrontMatter::parse(content);
  162. assert!(res.is_err());
  163. }
  164. #[test]
  165. fn can_parse_date_yyyy_mm_dd() {
  166. let content = r#"
  167. title = "Hello"
  168. description = "hey there"
  169. date = "2016-10-10""#;
  170. let res = PageFrontMatter::parse(content).unwrap();
  171. assert!(res.date().is_some());
  172. }
  173. #[test]
  174. fn can_parse_date_rfc3339() {
  175. let content = r#"
  176. title = "Hello"
  177. description = "hey there"
  178. date = "2002-10-02T15:00:00Z""#;
  179. let res = PageFrontMatter::parse(content).unwrap();
  180. assert!(res.date().is_some());
  181. }
  182. #[test]
  183. fn cannot_parse_random_date_format() {
  184. let content = r#"
  185. title = "Hello"
  186. description = "hey there"
  187. date = "2002/10/12""#;
  188. let res = PageFrontMatter::parse(content).unwrap();
  189. assert!(res.date().is_none());
  190. }
  191. }