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.

323 lines
8.9KB

  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, Serialize, 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. /// Whether this page is a draft and should be ignored for pagination etc
  63. pub draft: bool,
  64. /// The page slug. Will be used instead of the filename if present
  65. /// Can't be an empty string if present
  66. pub slug: Option<String>,
  67. /// The path the page appears at, overrides the slug if set in the front-matter
  68. /// otherwise is set after parsing front matter and sections
  69. /// Can't be an empty string if present
  70. pub path: Option<String>,
  71. pub taxonomies: HashMap<String, Vec<String>>,
  72. /// Integer to use to order content. Lowest is at the bottom, highest first
  73. pub order: Option<usize>,
  74. /// Integer to use to order content. Highest is at the bottom, lowest first
  75. pub weight: Option<usize>,
  76. /// All aliases for that page. Gutenberg will create HTML templates that will
  77. /// redirect to this
  78. #[serde(skip_serializing)]
  79. pub aliases: Vec<String>,
  80. /// Specify a template different from `page.html` to use for that page
  81. #[serde(skip_serializing)]
  82. pub template: Option<String>,
  83. /// Whether the page is included in the search index
  84. /// Defaults to `true` but is only used if search if explicitly enabled in the config.
  85. #[serde(skip_serializing)]
  86. pub in_search_index: bool,
  87. /// Any extra parameter present in the front matter
  88. pub extra: Map<String, Value>,
  89. }
  90. impl PageFrontMatter {
  91. pub fn parse(toml: &str) -> Result<PageFrontMatter> {
  92. let mut f: PageFrontMatter = match toml::from_str(toml) {
  93. Ok(d) => d,
  94. Err(e) => bail!(e),
  95. };
  96. if let Some(ref slug) = f.slug {
  97. if slug == "" {
  98. bail!("`slug` can't be empty if present")
  99. }
  100. }
  101. if let Some(ref path) = f.path {
  102. if path == "" {
  103. bail!("`path` can't be empty if present")
  104. }
  105. }
  106. f.extra = match fix_toml_dates(f.extra) {
  107. Value::Object(o) => o,
  108. _ => unreachable!("Got something other than a table in page extra"),
  109. };
  110. Ok(f)
  111. }
  112. /// Converts the TOML datetime to a Chrono naive datetime
  113. pub fn date(&self) -> Option<NaiveDateTime> {
  114. if let Some(ref d) = self.date {
  115. if d.contains('T') {
  116. DateTime::parse_from_rfc3339(&d).ok().and_then(|s| Some(s.naive_local()))
  117. } else {
  118. NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0, 0, 0)))
  119. }
  120. } else {
  121. None
  122. }
  123. }
  124. pub fn order(&self) -> usize {
  125. self.order.unwrap()
  126. }
  127. pub fn weight(&self) -> usize {
  128. self.weight.unwrap()
  129. }
  130. }
  131. impl Default for PageFrontMatter {
  132. fn default() -> PageFrontMatter {
  133. PageFrontMatter {
  134. title: None,
  135. description: None,
  136. date: None,
  137. draft: false,
  138. slug: None,
  139. path: None,
  140. taxonomies: HashMap::new(),
  141. order: None,
  142. weight: None,
  143. aliases: Vec::new(),
  144. in_search_index: true,
  145. template: None,
  146. extra: Map::new(),
  147. }
  148. }
  149. }
  150. #[cfg(test)]
  151. mod tests {
  152. use tera::to_value;
  153. use super::PageFrontMatter;
  154. #[test]
  155. fn can_have_empty_front_matter() {
  156. let content = r#" "#;
  157. let res = PageFrontMatter::parse(content);
  158. println!("{:?}", res);
  159. assert!(res.is_ok());
  160. }
  161. #[test]
  162. fn can_parse_valid_front_matter() {
  163. let content = r#"
  164. title = "Hello"
  165. description = "hey there""#;
  166. let res = PageFrontMatter::parse(content);
  167. assert!(res.is_ok());
  168. let res = res.unwrap();
  169. assert_eq!(res.title.unwrap(), "Hello".to_string());
  170. assert_eq!(res.description.unwrap(), "hey there".to_string())
  171. }
  172. #[test]
  173. fn errors_with_invalid_front_matter() {
  174. let content = r#"title = 1\n"#;
  175. let res = PageFrontMatter::parse(content);
  176. assert!(res.is_err());
  177. }
  178. #[test]
  179. fn errors_on_present_but_empty_slug() {
  180. let content = r#"
  181. title = "Hello"
  182. description = "hey there"
  183. slug = """#;
  184. let res = PageFrontMatter::parse(content);
  185. assert!(res.is_err());
  186. }
  187. #[test]
  188. fn errors_on_present_but_empty_path() {
  189. let content = r#"
  190. title = "Hello"
  191. description = "hey there"
  192. path = """#;
  193. let res = PageFrontMatter::parse(content);
  194. assert!(res.is_err());
  195. }
  196. #[test]
  197. fn can_parse_date_yyyy_mm_dd() {
  198. let content = r#"
  199. title = "Hello"
  200. description = "hey there"
  201. date = 2016-10-10
  202. "#;
  203. let res = PageFrontMatter::parse(content).unwrap();
  204. assert!(res.date.is_some());
  205. }
  206. #[test]
  207. fn can_parse_date_rfc3339() {
  208. let content = r#"
  209. title = "Hello"
  210. description = "hey there"
  211. date = 2002-10-02T15:00:00Z
  212. "#;
  213. let res = PageFrontMatter::parse(content).unwrap();
  214. assert!(res.date.is_some());
  215. }
  216. #[test]
  217. fn cannot_parse_random_date_format() {
  218. let content = r#"
  219. title = "Hello"
  220. description = "hey there"
  221. date = 2002/10/12"#;
  222. let res = PageFrontMatter::parse(content);
  223. assert!(res.is_err());
  224. }
  225. #[test]
  226. fn cannot_parse_invalid_date_format() {
  227. let content = r#"
  228. title = "Hello"
  229. description = "hey there"
  230. date = 2002-14-01"#;
  231. let res = PageFrontMatter::parse(content);
  232. assert!(res.is_err());
  233. }
  234. #[test]
  235. fn cannot_parse_date_as_string() {
  236. let content = r#"
  237. title = "Hello"
  238. description = "hey there"
  239. date = "2002-14-01""#;
  240. let res = PageFrontMatter::parse(content);
  241. assert!(res.is_err());
  242. }
  243. #[test]
  244. fn can_parse_dates_in_extra() {
  245. let content = r#"
  246. title = "Hello"
  247. description = "hey there"
  248. [extra]
  249. some-date = 2002-14-01"#;
  250. let res = PageFrontMatter::parse(content);
  251. println!("{:?}", res);
  252. assert!(res.is_ok());
  253. assert_eq!(res.unwrap().extra["some-date"], to_value("2002-14-01").unwrap());
  254. }
  255. #[test]
  256. fn can_parse_nested_dates_in_extra() {
  257. let content = r#"
  258. title = "Hello"
  259. description = "hey there"
  260. [extra.something]
  261. some-date = 2002-14-01"#;
  262. let res = PageFrontMatter::parse(content);
  263. println!("{:?}", res);
  264. assert!(res.is_ok());
  265. assert_eq!(res.unwrap().extra["something"]["some-date"], to_value("2002-14-01").unwrap());
  266. }
  267. #[test]
  268. fn can_parse_taxonomies() {
  269. let content = r#"
  270. title = "Hello World"
  271. [taxonomies]
  272. tags = ["Rust", "JavaScript"]
  273. categories = ["Dev"]
  274. "#;
  275. let res = PageFrontMatter::parse(content);
  276. println!("{:?}", res);
  277. assert!(res.is_ok());
  278. let res2 = res.unwrap();
  279. assert_eq!(res2.taxonomies["categories"], vec!["Dev"]);
  280. assert_eq!(res2.taxonomies["tags"], vec!["Rust", "JavaScript"]);
  281. }
  282. }