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.

286 lines
8.0KB

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