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