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.

348 lines
9.6KB

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