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.

341 lines
9.5KB

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