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.

231 lines
6.4KB

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