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.

342 lines
9.6KB

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