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.

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