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.

287 lines
8.0KB

  1. use std::collections::HashMap;
  2. use chrono::prelude::*;
  3. use tera::{Map, Value};
  4. use toml;
  5. use errors::Result;
  6. use utils::de::{fix_toml_dates, from_toml_datetime};
  7. /// The front matter of every page
  8. #[derive(Debug, Clone, PartialEq, Deserialize)]
  9. #[serde(default)]
  10. pub struct PageFrontMatter {
  11. /// <title> of the page
  12. pub title: Option<String>,
  13. /// Description in <meta> that appears when linked, e.g. on twitter
  14. pub description: Option<String>,
  15. /// Date if we want to order pages (ie blog post)
  16. #[serde(default, deserialize_with = "from_toml_datetime")]
  17. pub date: Option<String>,
  18. /// Chrono converted datetime
  19. #[serde(default, skip_deserializing)]
  20. pub datetime: Option<NaiveDateTime>,
  21. /// The converted date into a (year, month, day) tuple
  22. #[serde(default, skip_deserializing)]
  23. pub datetime_tuple: Option<(i32, u32, u32)>,
  24. /// Whether this page is a draft and should be ignored for pagination etc
  25. pub draft: bool,
  26. /// The page slug. Will be used instead of the filename if present
  27. /// Can't be an empty string if present
  28. pub slug: Option<String>,
  29. /// The path the page appears at, overrides the slug if set in the front-matter
  30. /// otherwise is set after parsing front matter and sections
  31. /// Can't be an empty string if present
  32. pub path: Option<String>,
  33. pub taxonomies: HashMap<String, Vec<String>>,
  34. /// Integer to use to order content. Lowest is at the bottom, highest first
  35. pub order: Option<usize>,
  36. /// Integer to use to order content. Highest is at the bottom, lowest first
  37. pub weight: Option<usize>,
  38. /// All aliases for that page. Zola will create HTML templates that will
  39. /// redirect to this
  40. #[serde(skip_serializing)]
  41. pub aliases: Vec<String>,
  42. /// Specify a template different from `page.html` to use for that page
  43. #[serde(skip_serializing)]
  44. pub template: Option<String>,
  45. /// Whether the page is included in the search index
  46. /// Defaults to `true` but is only used if search if explicitly enabled in the config.
  47. #[serde(skip_serializing)]
  48. pub in_search_index: bool,
  49. /// Any extra parameter present in the front matter
  50. pub extra: Map<String, Value>,
  51. }
  52. impl PageFrontMatter {
  53. pub fn parse(toml: &str) -> Result<PageFrontMatter> {
  54. let mut f: PageFrontMatter = match toml::from_str(toml) {
  55. Ok(d) => d,
  56. Err(e) => bail!(e),
  57. };
  58. if let Some(ref slug) = f.slug {
  59. if slug == "" {
  60. bail!("`slug` can't be empty if present")
  61. }
  62. }
  63. if let Some(ref path) = f.path {
  64. if path == "" {
  65. bail!("`path` can't be empty if present")
  66. }
  67. }
  68. f.extra = match fix_toml_dates(f.extra) {
  69. Value::Object(o) => o,
  70. _ => unreachable!("Got something other than a table in page extra"),
  71. };
  72. f.date_to_datetime();
  73. Ok(f)
  74. }
  75. /// Converts the TOML datetime to a Chrono naive datetime
  76. /// Also grabs the year/month/day tuple that will be used in serialization
  77. pub fn date_to_datetime(&mut self) {
  78. self.datetime = if let Some(ref d) = self.date {
  79. if d.contains('T') {
  80. DateTime::parse_from_rfc3339(&d).ok().and_then(|s| Some(s.naive_local()))
  81. } else {
  82. NaiveDate::parse_from_str(&d, "%Y-%m-%d")
  83. .ok()
  84. .and_then(|s| Some(s.and_hms(0, 0, 0)))
  85. }
  86. } else {
  87. None
  88. };
  89. self.datetime_tuple = if let Some(ref dt) = self.datetime {
  90. Some((dt.year(), dt.month(), dt.day()))
  91. } else {
  92. None
  93. };
  94. }
  95. pub fn order(&self) -> usize {
  96. self.order.unwrap()
  97. }
  98. pub fn weight(&self) -> usize {
  99. self.weight.unwrap()
  100. }
  101. }
  102. impl Default for PageFrontMatter {
  103. fn default() -> PageFrontMatter {
  104. PageFrontMatter {
  105. title: None,
  106. description: None,
  107. date: None,
  108. datetime: None,
  109. datetime_tuple: None,
  110. draft: false,
  111. slug: None,
  112. path: None,
  113. taxonomies: HashMap::new(),
  114. order: None,
  115. weight: None,
  116. aliases: Vec::new(),
  117. in_search_index: true,
  118. template: None,
  119. extra: Map::new(),
  120. }
  121. }
  122. }
  123. #[cfg(test)]
  124. mod tests {
  125. use super::PageFrontMatter;
  126. use tera::to_value;
  127. #[test]
  128. fn can_have_empty_front_matter() {
  129. let content = r#" "#;
  130. let res = PageFrontMatter::parse(content);
  131. println!("{:?}", res);
  132. assert!(res.is_ok());
  133. }
  134. #[test]
  135. fn can_parse_valid_front_matter() {
  136. let content = r#"
  137. title = "Hello"
  138. description = "hey there""#;
  139. let res = PageFrontMatter::parse(content);
  140. assert!(res.is_ok());
  141. let res = res.unwrap();
  142. assert_eq!(res.title.unwrap(), "Hello".to_string());
  143. assert_eq!(res.description.unwrap(), "hey there".to_string())
  144. }
  145. #[test]
  146. fn errors_with_invalid_front_matter() {
  147. let content = r#"title = 1\n"#;
  148. let res = PageFrontMatter::parse(content);
  149. assert!(res.is_err());
  150. }
  151. #[test]
  152. fn errors_on_present_but_empty_slug() {
  153. let content = r#"
  154. title = "Hello"
  155. description = "hey there"
  156. slug = """#;
  157. let res = PageFrontMatter::parse(content);
  158. assert!(res.is_err());
  159. }
  160. #[test]
  161. fn errors_on_present_but_empty_path() {
  162. let content = r#"
  163. title = "Hello"
  164. description = "hey there"
  165. path = """#;
  166. let res = PageFrontMatter::parse(content);
  167. assert!(res.is_err());
  168. }
  169. #[test]
  170. fn can_parse_date_yyyy_mm_dd() {
  171. let content = r#"
  172. title = "Hello"
  173. description = "hey there"
  174. date = 2016-10-10
  175. "#;
  176. let res = PageFrontMatter::parse(content).unwrap();
  177. assert!(res.date.is_some());
  178. }
  179. #[test]
  180. fn can_parse_date_rfc3339() {
  181. let content = r#"
  182. title = "Hello"
  183. description = "hey there"
  184. date = 2002-10-02T15:00:00Z
  185. "#;
  186. let res = PageFrontMatter::parse(content).unwrap();
  187. assert!(res.date.is_some());
  188. }
  189. #[test]
  190. fn cannot_parse_random_date_format() {
  191. let content = r#"
  192. title = "Hello"
  193. description = "hey there"
  194. date = 2002/10/12"#;
  195. let res = PageFrontMatter::parse(content);
  196. assert!(res.is_err());
  197. }
  198. #[test]
  199. fn cannot_parse_invalid_date_format() {
  200. let content = r#"
  201. title = "Hello"
  202. description = "hey there"
  203. date = 2002-14-01"#;
  204. let res = PageFrontMatter::parse(content);
  205. assert!(res.is_err());
  206. }
  207. #[test]
  208. fn cannot_parse_date_as_string() {
  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. #[test]
  217. fn can_parse_dates_in_extra() {
  218. let content = r#"
  219. title = "Hello"
  220. description = "hey there"
  221. [extra]
  222. some-date = 2002-14-01"#;
  223. let res = PageFrontMatter::parse(content);
  224. println!("{:?}", res);
  225. assert!(res.is_ok());
  226. assert_eq!(res.unwrap().extra["some-date"], to_value("2002-14-01").unwrap());
  227. }
  228. #[test]
  229. fn can_parse_nested_dates_in_extra() {
  230. let content = r#"
  231. title = "Hello"
  232. description = "hey there"
  233. [extra.something]
  234. some-date = 2002-14-01"#;
  235. let res = PageFrontMatter::parse(content);
  236. println!("{:?}", res);
  237. assert!(res.is_ok());
  238. assert_eq!(res.unwrap().extra["something"]["some-date"], to_value("2002-14-01").unwrap());
  239. }
  240. #[test]
  241. fn can_parse_taxonomies() {
  242. let content = r#"
  243. title = "Hello World"
  244. [taxonomies]
  245. tags = ["Rust", "JavaScript"]
  246. categories = ["Dev"]
  247. "#;
  248. let res = PageFrontMatter::parse(content);
  249. println!("{:?}", res);
  250. assert!(res.is_ok());
  251. let res2 = res.unwrap();
  252. assert_eq!(res2.taxonomies["categories"], vec!["Dev"]);
  253. assert_eq!(res2.taxonomies["tags"], vec!["Rust", "JavaScript"]);
  254. }
  255. }