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.

343 lines
9.4KB

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