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.

245 lines
6.6KB

  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<toml::value::Datetime>,
  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 path 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 path: 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 path) = f.path {
  54. if path == "" {
  55. bail!("`path` 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 TOML datetime to a Chrono naive datetime
  66. pub fn date(&self) -> Option<NaiveDateTime> {
  67. if let Some(ref d) = self.date {
  68. let d2 = d.to_string();
  69. if d2.contains('T') {
  70. DateTime::parse_from_rfc3339(&d2).ok().and_then(|s| Some(s.naive_local()))
  71. } else {
  72. NaiveDate::parse_from_str(&d2, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0, 0, 0)))
  73. }
  74. } else {
  75. 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. path: 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_path() {
  171. let content = r#"
  172. title = "Hello"
  173. description = "hey there"
  174. path = """#;
  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. "#;
  185. let res = PageFrontMatter::parse(content).unwrap();
  186. assert!(res.date.is_some());
  187. }
  188. #[test]
  189. fn can_parse_date_rfc3339() {
  190. let content = r#"
  191. title = "Hello"
  192. description = "hey there"
  193. date = 2002-10-02T15:00:00Z
  194. "#;
  195. let res = PageFrontMatter::parse(content).unwrap();
  196. assert!(res.date.is_some());
  197. }
  198. #[test]
  199. fn cannot_parse_random_date_format() {
  200. let content = r#"
  201. title = "Hello"
  202. description = "hey there"
  203. date = 2002/10/12"#;
  204. let res = PageFrontMatter::parse(content);
  205. assert!(res.is_err());
  206. }
  207. #[test]
  208. fn cannot_parse_invalid_date_format() {
  209. let content = r#"
  210. title = "Hello"
  211. description = "hey there"
  212. date = 2002-14-01"#;
  213. let res = PageFrontMatter::parse(content);
  214. assert!(res.is_err());
  215. }
  216. }