1 use std::collections::HashMap;
2 
3 use chrono::prelude::*;
4 use serde_derive::Deserialize;
5 use tera::{Map, Value};
6 
7 use errors::{bail, Result};
8 use utils::de::{fix_toml_dates, from_toml_datetime};
9 
10 use crate::RawFrontMatter;
11 
12 /// The front matter of every page
13 #[derive(Debug, Clone, PartialEq, Deserialize)]
14 #[serde(default)]
15 pub struct PageFrontMatter {
16     /// <title> of the page
17     pub title: Option<String>,
18     /// Description in <meta> that appears when linked, e.g. on twitter
19     pub description: Option<String>,
20     /// Updated date
21     #[serde(default, deserialize_with = "from_toml_datetime")]
22     pub updated: Option<String>,
23     /// Chrono converted update datatime
24     #[serde(default, skip_deserializing)]
25     pub updated_datetime: Option<NaiveDateTime>,
26     /// The converted update datetime into a (year, month, day) tuple
27     #[serde(default, skip_deserializing)]
28     pub updated_datetime_tuple: Option<(i32, u32, u32)>,
29     /// Date if we want to order pages (ie blog post)
30     #[serde(default, deserialize_with = "from_toml_datetime")]
deserialize<T>(&self) -> Result<T> where T: serde::de::DeserializeOwned,31     pub date: Option<String>,
32     /// Chrono converted datetime
33     #[serde(default, skip_deserializing)]
34     pub datetime: Option<NaiveDateTime>,
35     /// The converted date into a (year, month, day) tuple
36     #[serde(default, skip_deserializing)]
37     pub datetime_tuple: Option<(i32, u32, u32)>,
38     /// Whether this page is a draft
39     pub draft: bool,
40     /// The page slug. Will be used instead of the filename if present
41     /// Can't be an empty string if present
42     pub slug: Option<String>,
43     /// The path the page appears at, overrides the slug if set in the front-matter
44     /// otherwise is set after parsing front matter and sections
45     /// Can't be an empty string if present
46     pub path: Option<String>,
47     pub taxonomies: HashMap<String, Vec<String>>,
48     /// Integer to use to order content. Highest is at the bottom, lowest first
49     pub weight: Option<usize>,
50     /// All aliases for that page. Zola will create HTML templates that will
51     /// redirect to this
52     #[serde(skip_serializing)]
53     pub aliases: Vec<String>,
54     /// Specify a template different from `page.html` to use for that page
55     #[serde(skip_serializing)]
56     pub template: Option<String>,
57     /// Whether the page is included in the search index
58     /// Defaults to `true` but is only used if search if explicitly enabled in the config.
59     #[serde(skip_serializing)]
60     pub in_search_index: bool,
61     /// Any extra parameter present in the front matter
62     pub extra: Map<String, Value>,
63 }
64 
65 /// Parse a string for a datetime coming from one of the supported TOML format
66 /// There are three alternatives:
67 /// 1. an offset datetime (plain RFC3339)
68 /// 2. a local datetime (RFC3339 with timezone omitted)
69 /// 3. a local date (YYYY-MM-DD).
70 /// This tries each in order.
71 fn parse_datetime(d: &str) -> Option<NaiveDateTime> {
72     DateTime::parse_from_rfc3339(d)
73         .or_else(|_| DateTime::parse_from_rfc3339(format!("{}Z", d).as_ref()))
74         .map(|s| s.naive_local())
75         .or_else(|_| NaiveDate::parse_from_str(d, "%Y-%m-%d").map(|s| s.and_hms(0, 0, 0)))
76         .ok()
77 }
78 
79 impl PageFrontMatter {
80     pub fn parse(raw: &RawFrontMatter) -> Result<PageFrontMatter> {
81         let mut f: PageFrontMatter = raw.deserialize()?;
82 
83         if let Some(ref slug) = f.slug {
84             if slug.is_empty() {
85                 bail!("`slug` can't be empty if present")
86             }
87         }
88 
89         if let Some(ref path) = f.path {
90             if path.is_empty() {
91                 bail!("`path` can't be empty if present")
92             }
93         }
94 
95         f.extra = match fix_toml_dates(f.extra) {
96             Value::Object(o) => o,
97             _ => unreachable!("Got something other than a table in page extra"),
98         };
99 
split_section_content<'c>( file_path: &Path, content: &'c str, ) -> Result<(SectionFrontMatter, &'c str)>100         f.date_to_datetime();
101 
102         if let Some(ref date) = f.date {
103             if f.datetime.is_none() {
104                 bail!("`date` could not be parsed: {}.", date);
105             }
106         }
107 
108         Ok(f)
109     }
110 
111     /// Converts the TOML datetime to a Chrono naive datetime
112     /// Also grabs the year/month/day tuple that will be used in serialization
113     pub fn date_to_datetime(&mut self) {
114         self.datetime = self.date.as_ref().map(|s| s.as_ref()).and_then(parse_datetime);
115         self.datetime_tuple = self.datetime.map(|dt| (dt.year(), dt.month(), dt.day()));
split_page_content<'c>( file_path: &Path, content: &'c str, ) -> Result<(PageFrontMatter, &'c str)>116 
117         self.updated_datetime = self.updated.as_ref().map(|s| s.as_ref()).and_then(parse_datetime);
118         self.updated_datetime_tuple =
119             self.updated_datetime.map(|dt| (dt.year(), dt.month(), dt.day()));
120     }
121 
122     pub fn weight(&self) -> usize {
123         self.weight.unwrap()
124     }
125 }
126 
127 impl Default for PageFrontMatter {
128     fn default() -> PageFrontMatter {
129         PageFrontMatter {
130             title: None,
131             description: None,
132             updated: None,
133             updated_datetime: None,
134             updated_datetime_tuple: None,
135             date: None,
136             datetime: None,
137             datetime_tuple: None,
138             draft: false,
139             slug: None,
140             path: None,
141             taxonomies: HashMap::new(),
142             weight: None,
143             aliases: Vec::new(),
144             in_search_index: true,
145             template: None,
146             extra: Map::new(),
147         }
148     }
149 }
150 
151 #[cfg(test)]
152 mod tests {
can_split_page_content_valid(content: &str)153     use super::PageFrontMatter;
154     use super::RawFrontMatter;
155     use tera::to_value;
156     use test_case::test_case;
157 
158     #[test_case(&RawFrontMatter::Toml(r#"  "#); "toml")]
159     #[test_case(&RawFrontMatter::Toml(r#"  "#); "yaml")]
160     fn can_have_empty_front_matter(content: &RawFrontMatter) {
161         let res = PageFrontMatter::parse(content);
162         println!("{:?}", res);
163         assert!(res.is_ok());
164     }
165 
166     #[test_case(&RawFrontMatter::Toml(r#"
167 title = "Hello"
168 description = "hey there"
169 "#); "toml")]
170     #[test_case(&RawFrontMatter::Yaml(r#"
can_split_section_content_valid(content: &str)171 title: Hello
172 description: hey there
173 "#); "yaml")]
174     fn can_parse_valid_front_matter(content: &RawFrontMatter) {
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 
182     #[test_case(&RawFrontMatter::Toml(r#"title = |\n"#); "toml")]
183     #[test_case(&RawFrontMatter::Yaml(r#"title: |\n"#); "yaml")]
184     fn errors_with_invalid_front_matter(content: &RawFrontMatter) {
185         let res = PageFrontMatter::parse(content);
186         assert!(res.is_err());
187     }
188 
189     #[test_case(&RawFrontMatter::Toml(r#"
190 title = "Hello"
191 description = "hey there"
192 slug = ""
193 "#); "toml")]
194     #[test_case(&RawFrontMatter::Yaml(r#"
195 title: Hello
196 description: hey there
197 slug: ""
198 "#); "yaml")]
199     fn errors_on_present_but_empty_slug(content: &RawFrontMatter) {
200         let res = PageFrontMatter::parse(content);
201         assert!(res.is_err());
202     }
can_split_content_with_only_frontmatter_valid(content: &str)203 
204     #[test_case(&RawFrontMatter::Toml(r#"
205 title = "Hello"
206 description = "hey there"
207 path = ""
208 "#); "toml")]
209     #[test_case(&RawFrontMatter::Yaml(r#"
210 title: Hello
211 description: hey there
212 path: ""
213 "#); "yaml")]
214     fn errors_on_present_but_empty_path(content: &RawFrontMatter) {
215         let res = PageFrontMatter::parse(content);
216         assert!(res.is_err());
217     }
218 
219     #[test_case(&RawFrontMatter::Toml(r#"
220 title = "Hello"
221 description = "hey there"
222 date = 2016-10-10
223 "#); "toml")]
224     #[test_case(&RawFrontMatter::Yaml(r#"
225 title: Hello
226 description: hey there
227 date: 2016-10-10
228 "#); "yaml")]
229     fn can_parse_date_yyyy_mm_dd(content: &RawFrontMatter) {
230         let res = PageFrontMatter::parse(content).unwrap();
231         assert!(res.datetime.is_some());
232     }
233 
234     #[test_case(&RawFrontMatter::Toml(r#"
235 title = "Hello"
236 description = "hey there"
can_split_content_lazily(content: &str, expected: &str)237 date = 2002-10-02T15:00:00Z
238 "#); "toml")]
239     #[test_case(&RawFrontMatter::Yaml(r#"
240 title: Hello
241 description: hey there
242 date: 2002-10-02T15:00:00Z
243 "#); "yaml")]
244     fn can_parse_date_rfc3339(content: &RawFrontMatter) {
245         let res = PageFrontMatter::parse(content).unwrap();
246         assert!(res.datetime.is_some());
247     }
248 
249     #[test_case(&RawFrontMatter::Toml(r#"
250 title = "Hello"
251 description = "hey there"
252 date = 2002-10-02T15:00:00
253 "#); "toml")]
254     #[test_case(&RawFrontMatter::Yaml(r#"
255 title: Hello
256 description: hey there
257 date: 2002-10-02T15:00:00
258 "#); "yaml")]
259     fn can_parse_date_rfc3339_without_timezone(content: &RawFrontMatter) {
260         let res = PageFrontMatter::parse(content).unwrap();
261         assert!(res.datetime.is_some());
262     }
263 
264     #[test_case(&RawFrontMatter::Toml(r#"
265 title = "Hello"
266 description = "hey there"
267 date = 2002-10-02 15:00:00+02:00
268 "#); "toml")]
269     #[test_case(&RawFrontMatter::Yaml(r#"
270 title: Hello
271 description: hey there
272 date: 2002-10-02 15:00:00+02:00
273 "#); "yaml")]
274     fn can_parse_date_rfc3339_with_space(content: &RawFrontMatter) {
275         let res = PageFrontMatter::parse(content).unwrap();
276         assert!(res.datetime.is_some());
errors_if_cannot_locate_frontmatter(content: &str)277     }
278 
279     #[test_case(&RawFrontMatter::Toml(r#"
280 title = "Hello"
281 description = "hey there"
282 date = 2002-10-02 15:00:00
283 "#); "toml")]
284     #[test_case(&RawFrontMatter::Yaml(r#"
285 title: Hello
286 description: hey there
287 date: 2002-10-02 15:00:00
288 "#); "yaml")]
289     fn can_parse_date_rfc3339_with_space_without_timezone(content: &RawFrontMatter) {
290         let res = PageFrontMatter::parse(content).unwrap();
291         assert!(res.datetime.is_some());
292     }
293 
294     #[test_case(&RawFrontMatter::Toml(r#"
295 title = "Hello"
296 description = "hey there"
297 date = 2002-10-02T15:00:00.123456Z
298 "#); "toml")]
299     #[test_case(&RawFrontMatter::Yaml(r#"
300 title: Hello
301 description: hey there
302 date: 2002-10-02T15:00:00.123456Z
303 "#); "yaml")]
304     fn can_parse_date_rfc3339_with_microseconds(content: &RawFrontMatter) {
305         let res = PageFrontMatter::parse(content).unwrap();
306         assert!(res.datetime.is_some());
307     }
308 
309     #[test_case(&RawFrontMatter::Toml(r#"
310 title = "Hello"
311 description = "hey there"
312 date = 2002/10/12
313 "#); "toml")]
314     #[test_case(&RawFrontMatter::Yaml(r#"
315 title: Hello
316 description: hey there
317 date: 2002/10/12
318 "#); "yaml")]
319     fn cannot_parse_random_date_format(content: &RawFrontMatter) {
320         let res = PageFrontMatter::parse(content);
321         assert!(res.is_err());
322     }
323 
324     #[test_case(&RawFrontMatter::Toml(r#"
325 title = "Hello"
326 description = "hey there"
327 date = 2002-14-01
328 "#); "toml")]
329     #[test_case(&RawFrontMatter::Yaml(r#"
330 title: Hello
331 description: hey there
332 date: 2002-14-01
333 "#); "yaml")]
334     fn cannot_parse_invalid_date_format(content: &RawFrontMatter) {
335         let res = PageFrontMatter::parse(content);
336         assert!(res.is_err());
337     }
338 
339     #[test_case(&RawFrontMatter::Toml(r#"
340 title = "Hello"
341 description = "hey there"
342 date = "2016-10-10"
343 "#); "toml")]
344     #[test_case(&RawFrontMatter::Yaml(r#"
345 title: Hello
346 description: hey there
347 date: "2016-10-10"
348 "#); "yaml")]
349     fn can_parse_valid_date_as_string(content: &RawFrontMatter) {
350         let res = PageFrontMatter::parse(content).unwrap();
351         assert!(res.date.is_some());
352     }
353 
354     #[test_case(&RawFrontMatter::Toml(r#"
355 title = "Hello"
356 description = "hey there"
357 date = "2002-14-01"
358 "#); "toml")]
359     #[test_case(&RawFrontMatter::Yaml(r#"
360 title: Hello
361 description: hey there
362 date: "2002-14-01"
363 "#); "yaml")]
364     fn cannot_parse_invalid_date_as_string(content: &RawFrontMatter) {
365         let res = PageFrontMatter::parse(content);
366         assert!(res.is_err());
367     }
368 
369     #[test_case(&RawFrontMatter::Toml(r#"
370 title = "Hello"
371 description = "hey there"
372 
373 [extra]
374 some-date = 2002-14-01
375 "#); "toml")]
376     #[test_case(&RawFrontMatter::Yaml(r#"
377 title: Hello
378 description: hey there
379 
380 extra:
381     some-date: 2002-14-01
382 "#); "yaml")]
383     fn can_parse_dates_in_extra(content: &RawFrontMatter) {
384         let res = PageFrontMatter::parse(content);
385         println!("{:?}", res);
386         assert!(res.is_ok());
387         assert_eq!(res.unwrap().extra["some-date"], to_value("2002-14-01").unwrap());
388     }
389 
390     #[test_case(&RawFrontMatter::Toml(r#"
391 title = "Hello"
392 description = "hey there"
393 
394 [extra.something]
395 some-date = 2002-14-01
396 "#); "toml")]
397     #[test_case(&RawFrontMatter::Yaml(r#"
398 title: Hello
399 description: hey there
400 
401 extra:
402     something:
403         some-date: 2002-14-01
404 "#); "yaml")]
405     fn can_parse_nested_dates_in_extra(content: &RawFrontMatter) {
406         let res = PageFrontMatter::parse(content);
407         println!("{:?}", res);
408         assert!(res.is_ok());
409         assert_eq!(res.unwrap().extra["something"]["some-date"], to_value("2002-14-01").unwrap());
410     }
411 
412     #[test_case(&RawFrontMatter::Toml(r#"
413 title = "Hello"
414 description = "hey there"
415 
416 [extra]
417 date_example = 2020-05-04
418 [[extra.questions]]
419 date = 2020-05-03
420 name = "Who is the prime minister of Uganda?"
421 "#); "toml")]
422     #[test_case(&RawFrontMatter::Yaml(r#"
423 title: Hello
424 description: hey there
425 
426 extra:
427     date_example: 2020-05-04
428     questions:
429         - date: 2020-05-03
430           name: "Who is the prime minister of Uganda?"
431 "#); "yaml")]
432     fn can_parse_fully_nested_dates_in_extra(content: &RawFrontMatter) {
433         let res = PageFrontMatter::parse(content);
434         println!("{:?}", res);
435         assert!(res.is_ok());
436         assert_eq!(res.unwrap().extra["questions"][0]["date"], to_value("2020-05-03").unwrap());
437     }
438 
439     #[test_case(&RawFrontMatter::Toml(r#"
440 title = "Hello World"
441 
442 [taxonomies]
443 tags = ["Rust", "JavaScript"]
444 categories = ["Dev"]
445 "#); "toml")]
446     #[test_case(&RawFrontMatter::Yaml(r#"
447 title: Hello World
448 
449 taxonomies:
450     tags:
451         - Rust
452         - JavaScript
453     categories:
454         - Dev
455 "#); "yaml")]
456     fn can_parse_taxonomies(content: &RawFrontMatter) {
457         let res = PageFrontMatter::parse(content);
458         println!("{:?}", res);
459         assert!(res.is_ok());
460         let res2 = res.unwrap();
461         assert_eq!(res2.taxonomies["categories"], vec!["Dev"]);
462         assert_eq!(res2.taxonomies["tags"], vec!["Rust", "JavaScript"]);
463     }
464 }
465