1 /// A page, can be a blog post or a basic page 2 use std::collections::HashMap; 3 use std::path::{Path, PathBuf}; 4 5 use lazy_static::lazy_static; 6 use regex::Regex; 7 use slotmap::DefaultKey; 8 use tera::{Context as TeraContext, Tera}; 9 10 use crate::library::Library; 11 use config::Config; 12 use errors::{Error, Result}; 13 use front_matter::{split_page_content, InsertAnchor, PageFrontMatter}; 14 use rendering::{render_content, Heading, RenderContext}; 15 use utils::site::get_reading_analytics; 16 use utils::slugs::slugify_paths; 17 use utils::templates::{render_template, ShortcodeDefinition}; has_anchor(headings: &[Heading], anchor: &str) -> bool18 19 use crate::content::file_info::FileInfo; 20 use crate::content::ser::SerializingPage; 21 use crate::content::{find_related_assets, has_anchor}; 22 use utils::fs::read_file; 23 24 lazy_static! { 25 // Based on https://regex101.com/r/H2n38Z/1/tests 26 // A regex parsing RFC3339 date followed by {_,-}, some characters and ended by .md 27 static ref RFC3339_DATE: Regex = Regex::new( 28 r"^(?P<datetime>(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])(T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|(\+|-)([01][0-9]|2[0-3]):([0-5][0-9])))?)\s?(_|-)(?P<slug>.+$)" 29 ).unwrap(); 30 31 static ref FOOTNOTES_RE: Regex = Regex::new(r"<sup\s*.*?>\s*.*?</sup>").unwrap(); 32 } 33 34 #[derive(Clone, Debug, Default, PartialEq)] 35 pub struct Page { find_related_assets(path: &Path, config: &Config, recursive: bool) -> Vec<PathBuf>36 /// All info about the actual file 37 pub file: FileInfo, 38 /// The front matter meta-data 39 pub meta: PageFrontMatter, 40 /// The list of parent sections 41 pub ancestors: Vec<DefaultKey>, 42 /// The actual content of the page, in markdown 43 pub raw_content: String, 44 /// All the non-md files we found next to the .md file 45 pub assets: Vec<PathBuf>, 46 /// All the non-md files we found next to the .md file 47 pub serialized_assets: Vec<String>, 48 /// The HTML rendered of the page 49 pub content: String, 50 /// The slug of that page. 51 /// First tries to find the slug in the meta and defaults to filename otherwise 52 pub slug: String, 53 /// The URL path of the page, always starting with a slash 54 pub path: String, 55 /// The components of the path of the page 56 pub components: Vec<String>, 57 /// The full URL for that page 58 pub permalink: String, 59 /// The summary for the article, defaults to None 60 /// When <!-- more --> is found in the text, will take the content up to that part 61 /// as summary 62 pub summary: Option<String>, 63 /// The earlier updated page, for pages sorted by updated date 64 pub earlier_updated: Option<DefaultKey>, 65 /// The later updated page, for pages sorted by updated date 66 pub later_updated: Option<DefaultKey>, 67 /// The earlier page, for pages sorted by date 68 pub earlier: Option<DefaultKey>, 69 /// The later page, for pages sorted by date 70 pub later: Option<DefaultKey>, 71 /// The previous page, for pages sorted by title 72 pub title_prev: Option<DefaultKey>, 73 /// The next page, for pages sorted by title 74 pub title_next: Option<DefaultKey>, 75 /// The lighter page, for pages sorted by weight 76 pub lighter: Option<DefaultKey>, 77 /// The heavier page, for pages sorted by weight 78 pub heavier: Option<DefaultKey>, 79 /// Toc made from the headings of the markdown file 80 pub toc: Vec<Heading>, 81 /// How many words in the raw content 82 pub word_count: Option<usize>, 83 /// How long would it take to read the raw content. 84 /// See `get_reading_analytics` on how it is calculated 85 pub reading_time: Option<usize>, 86 /// The language of that page. Equal to the default lang if the user doesn't setup `languages` in config. 87 /// Corresponds to the lang in the {slug}.{lang}.md file scheme 88 pub lang: String, 89 /// Contains all the translated version of that page 90 pub translations: Vec<DefaultKey>, 91 /// The list of all internal links (as path to markdown file), with optional anchor fragments. 92 /// We can only check the anchor after all pages have been built and their ToC compiled. 93 /// The page itself should exist otherwise it would have errored before getting there. 94 pub internal_links: Vec<(String, Option<String>)>, 95 /// The list of all links to external webpages. They can be validated by the `link_checker`. 96 pub external_links: Vec<String>, 97 } 98 99 impl Page { 100 pub fn new<P: AsRef<Path>>(file_path: P, meta: PageFrontMatter, base_path: &Path) -> Page { 101 let file_path = file_path.as_ref(); 102 103 Page { file: FileInfo::new_page(file_path, base_path), meta, ..Self::default() } 104 } 105 106 /// Parse a page given the content of the .md file 107 /// Files without front matter or with invalid front matter are considered 108 /// erroneous 109 pub fn parse( 110 file_path: &Path, 111 content: &str, 112 config: &Config, 113 base_path: &Path, 114 ) -> Result<Page> { 115 let (meta, content) = split_page_content(file_path, content)?; 116 let mut page = Page::new(file_path, meta, base_path); 117 118 page.lang = page.file.find_language(config)?; 119 120 page.raw_content = content.to_string(); 121 let (word_count, reading_time) = get_reading_analytics(&page.raw_content); 122 page.word_count = Some(word_count); 123 page.reading_time = Some(reading_time); 124 125 let mut slug_from_dated_filename = None; 126 let file_path = if page.file.name == "index" { 127 if let Some(parent) = page.file.path.parent() { 128 parent.file_name().unwrap().to_str().unwrap().to_string() 129 } else { 130 page.file.name.replace(".md", "") 131 } 132 } else { 133 page.file.name.replace(".md", "") 134 }; 135 if let Some(ref caps) = RFC3339_DATE.captures(&file_path) { 136 slug_from_dated_filename = Some(caps.name("slug").unwrap().as_str().to_string()); 137 if page.meta.date.is_none() { 138 page.meta.date = Some(caps.name("datetime").unwrap().as_str().to_string()); 139 page.meta.date_to_datetime(); 140 } 141 } 142 143 page.slug = { 144 if let Some(ref slug) = page.meta.slug { 145 slugify_paths(slug, config.slugify.paths) 146 } else if page.file.name == "index" { 147 if let Some(parent) = page.file.path.parent() { 148 if let Some(slug) = slug_from_dated_filename { 149 slugify_paths(&slug, config.slugify.paths) 150 } else { 151 slugify_paths( 152 parent.file_name().unwrap().to_str().unwrap(), 153 config.slugify.paths, 154 ) 155 } 156 } else { 157 slugify_paths(&page.file.name, config.slugify.paths) 158 } 159 } else if let Some(slug) = slug_from_dated_filename { 160 slugify_paths(&slug, config.slugify.paths) 161 } else { 162 slugify_paths(&page.file.name, config.slugify.paths) 163 } 164 }; 165 166 page.path = if let Some(ref p) = page.meta.path { 167 let path = p.trim(); 168 169 if path.starts_with('/') { 170 path.into() 171 } else { 172 format!("/{}", path) 173 } 174 } else { 175 let mut path = if page.file.components.is_empty() { 176 page.slug.clone() 177 } else { 178 format!("{}/{}", page.file.components.join("/"), page.slug) 179 }; 180 181 if page.lang != config.default_language { 182 path = format!("{}/{}", page.lang, path); 183 } 184 185 format!("/{}", path) 186 }; 187 188 if !page.path.ends_with('/') { 189 page.path = format!("{}/", page.path); 190 } 191 192 page.components = page 193 .path 194 .split('/') 195 .map(|p| p.to_string()) 196 .filter(|p| !p.is_empty()) 197 .collect::<Vec<_>>(); 198 page.permalink = config.make_permalink(&page.path); 199 200 Ok(page) 201 } 202 203 /// Read and parse a .md file into a Page struct 204 pub fn from_file<P: AsRef<Path>>(path: P, config: &Config, base_path: &Path) -> Result<Page> { 205 let path = path.as_ref(); 206 let content = read_file(path)?; 207 let mut page = Page::parse(path, &content, config, base_path)?; 208 209 if page.file.name == "index" { 210 let parent_dir = path.parent().unwrap(); 211 page.assets = find_related_assets(parent_dir, config, true); 212 page.serialized_assets = page.serialize_assets(base_path); 213 } else { 214 page.assets = vec![]; 215 } 216 217 Ok(page) 218 } 219 220 /// We need access to all pages url to render links relative to content 221 /// so that can't happen at the same time as parsing 222 pub fn render_markdown( 223 &mut self, 224 permalinks: &HashMap<String, String>, 225 tera: &Tera, 226 config: &Config, 227 anchor_insert: InsertAnchor, 228 shortcode_definitions: &HashMap<String, ShortcodeDefinition>, 229 ) -> Result<()> { 230 let mut context = RenderContext::new( 231 tera, 232 config, 233 &self.lang, 234 &self.permalink, 235 permalinks, 236 anchor_insert, 237 ); 238 context.set_shortcode_definitions(shortcode_definitions); 239 context.set_current_page_path(&self.file.relative); 240 context.tera_context.insert("page", &SerializingPage::from_page_basic(self, None)); 241 242 let res = render_content(&self.raw_content, &context).map_err(|e| { 243 Error::chain(format!("Failed to render content of {}", self.file.path.display()), e) 244 })?; 245 246 self.summary = res 247 .summary_len 248 .map(|l| &res.body[0..l]) 249 .map(|s| FOOTNOTES_RE.replace(s, "").into_owned()); 250 self.content = res.body; 251 self.toc = res.toc; 252 self.external_links = res.external_links; 253 self.internal_links = res.internal_links; 254 255 Ok(()) 256 } 257 258 /// Renders the page using the default layout, unless specified in front-matter 259 pub fn render_html(&self, tera: &Tera, config: &Config, library: &Library) -> Result<String> { 260 let tpl_name = match self.meta.template { 261 Some(ref l) => l, 262 None => "page.html", 263 }; 264 265 let mut context = TeraContext::new(); 266 context.insert("config", &config.serialize(&self.lang)); 267 context.insert("current_url", &self.permalink); 268 context.insert("current_path", &self.path); 269 context.insert("page", &self.to_serialized(library)); 270 context.insert("lang", &self.lang); 271 272 render_template(tpl_name, tera, context, &config.theme).map_err(|e| { 273 Error::chain(format!("Failed to render page '{}'", self.file.path.display()), e) 274 }) 275 } 276 277 /// Creates a vectors of asset URLs. 278 fn serialize_assets(&self, base_path: &Path) -> Vec<String> { 279 self.assets 280 .iter() 281 .filter_map(|asset| asset.strip_prefix(&self.file.path.parent().unwrap()).ok()) 282 .filter_map(|filename| filename.to_str()) 283 .map(|filename| { 284 let mut path = self.file.path.clone(); 285 // Popping the index.md from the path since file.parent would be one level too high 286 // for our need here 287 path.pop(); 288 path.push(filename); 289 path = path 290 .strip_prefix(&base_path.join("content")) 291 .expect("Should be able to stripe prefix") 292 .to_path_buf(); 293 path 294 }) 295 .map(|path| format!("/{}", path.display())) 296 .collect() 297 } 298 299 pub fn has_anchor(&self, anchor: &str) -> bool { 300 has_anchor(&self.toc, anchor) 301 } 302 303 pub fn to_serialized<'a>(&'a self, library: &'a Library) -> SerializingPage<'a> { 304 SerializingPage::from_page(self, library) 305 } 306 307 pub fn to_serialized_basic<'a>(&'a self, library: &'a Library) -> SerializingPage<'a> { 308 SerializingPage::from_page_basic(self, Some(library)) 309 } 310 } 311 312 #[cfg(test)] 313 mod tests { 314 use std::collections::HashMap; 315 use std::fs::{create_dir, File}; 316 use std::io::Write; 317 use std::path::{Path, PathBuf}; 318 319 use globset::{Glob, GlobSetBuilder}; 320 use tempfile::tempdir; 321 use tera::Tera; 322 323 use super::Page; 324 use config::{Config, LanguageOptions}; 325 use front_matter::InsertAnchor; 326 use utils::slugs::SlugifyStrategy; 327 328 #[test] 329 fn can_parse_a_valid_page() { 330 let config = Config::default_for_test(); 331 let content = r#" 332 +++ 333 title = "Hello" 334 description = "hey there" 335 slug = "hello-world" 336 +++ 337 Hello world"#; 338 let res = Page::parse(Path::new("post.md"), content, &config, &PathBuf::new()); 339 assert!(res.is_ok()); 340 let mut page = res.unwrap(); 341 page.render_markdown( 342 &HashMap::default(), 343 &Tera::default(), 344 &config, 345 InsertAnchor::None, 346 &HashMap::new(), 347 ) 348 .unwrap(); 349 350 assert_eq!(page.meta.title.unwrap(), "Hello".to_string()); 351 assert_eq!(page.meta.slug.unwrap(), "hello-world".to_string()); 352 assert_eq!(page.raw_content, "Hello world".to_string()); 353 assert_eq!(page.content, "<p>Hello world</p>\n".to_string()); 354 } 355 356 #[test] 357 fn test_can_make_url_from_sections_and_slug() { 358 let content = r#" 359 +++ 360 slug = "hello-world" 361 +++ 362 Hello world"#; 363 let mut conf = Config::default(); 364 conf.base_url = "http://hello.com/".to_string(); 365 let res = 366 Page::parse(Path::new("content/posts/intro/start.md"), content, &conf, &PathBuf::new()); 367 assert!(res.is_ok()); 368 let page = res.unwrap(); 369 assert_eq!(page.path, "/posts/intro/hello-world/"); 370 assert_eq!(page.components, vec!["posts", "intro", "hello-world"]); 371 assert_eq!(page.permalink, "http://hello.com/posts/intro/hello-world/"); 372 } 373 374 #[test] 375 fn can_make_url_from_slug_only() { 376 let content = r#" 377 +++ 378 slug = "hello-world" 379 +++ 380 Hello world"#; 381 let config = Config::default(); 382 let res = Page::parse(Path::new("start.md"), content, &config, &PathBuf::new()); 383 assert!(res.is_ok()); 384 let page = res.unwrap(); 385 assert_eq!(page.path, "/hello-world/"); 386 assert_eq!(page.components, vec!["hello-world"]); 387 assert_eq!(page.permalink, config.make_permalink("hello-world")); 388 } 389 390 #[test] 391 fn can_make_url_from_slug_only_with_no_special_chars() { 392 let content = r#" 393 +++ 394 slug = "hello-&-world" 395 +++ 396 Hello world"#; 397 let mut config = Config::default(); 398 config.slugify.paths = SlugifyStrategy::On; 399 let res = Page::parse(Path::new("start.md"), content, &config, &PathBuf::new()); 400 assert!(res.is_ok()); 401 let page = res.unwrap(); 402 assert_eq!(page.path, "/hello-world/"); 403 assert_eq!(page.components, vec!["hello-world"]); 404 assert_eq!(page.permalink, config.make_permalink("hello-world")); 405 } 406 407 #[test] 408 fn can_make_url_from_utf8_slug_frontmatter() { 409 let content = r#" 410 +++ 411 slug = "日本" 412 +++ 413 Hello world"#; 414 let mut config = Config::default(); 415 config.slugify.paths = SlugifyStrategy::Safe; 416 let res = Page::parse(Path::new("start.md"), content, &config, &PathBuf::new()); 417 assert!(res.is_ok()); 418 let page = res.unwrap(); 419 assert_eq!(page.path, "/日本/"); 420 assert_eq!(page.components, vec!["日本"]); 421 assert_eq!(page.permalink, config.make_permalink("日本")); 422 } 423 424 #[test] 425 fn can_make_url_from_path() { 426 let content = r#" 427 +++ 428 path = "hello-world" 429 +++ 430 Hello world"#; 431 let config = Config::default(); 432 let res = Page::parse( 433 Path::new("content/posts/intro/start.md"), 434 content, 435 &config, 436 &PathBuf::new(), 437 ); 438 assert!(res.is_ok()); 439 let page = res.unwrap(); 440 assert_eq!(page.path, "/hello-world/"); 441 assert_eq!(page.components, vec!["hello-world"]); 442 assert_eq!(page.permalink, config.make_permalink("hello-world")); 443 } 444 445 #[test] 446 fn can_make_url_from_path_starting_slash() { 447 let content = r#" 448 +++ 449 path = "/hello-world" 450 +++ 451 Hello world"#; 452 let config = Config::default(); 453 let res = Page::parse( 454 Path::new("content/posts/intro/start.md"), 455 content, 456 &config, 457 &PathBuf::new(), 458 ); 459 assert!(res.is_ok()); 460 let page = res.unwrap(); 461 assert_eq!(page.path, "/hello-world/"); 462 assert_eq!(page.permalink, config.make_permalink("hello-world")); 463 } 464 465 #[test] 466 fn errors_on_invalid_front_matter_format() { 467 // missing starting +++ 468 let content = r#" 469 title = "Hello" 470 description = "hey there" 471 slug = "hello-world" 472 +++ 473 Hello world"#; 474 let res = Page::parse(Path::new("start.md"), content, &Config::default(), &PathBuf::new()); 475 assert!(res.is_err()); 476 } 477 478 #[test] 479 fn can_make_slug_from_non_slug_filename() { 480 let mut config = Config::default(); 481 config.slugify.paths = SlugifyStrategy::On; 482 let res = 483 Page::parse(Path::new(" file with space.md"), "+++\n+++\n", &config, &PathBuf::new()); 484 assert!(res.is_ok()); 485 let page = res.unwrap(); 486 assert_eq!(page.slug, "file-with-space"); 487 assert_eq!(page.permalink, config.make_permalink(&page.slug)); 488 } 489 490 #[test] 491 fn can_make_path_from_utf8_filename() { 492 let mut config = Config::default(); 493 config.slugify.paths = SlugifyStrategy::Safe; 494 let res = Page::parse(Path::new("日本.md"), "+++\n+++\n", &config, &PathBuf::new()); 495 assert!(res.is_ok()); 496 let page = res.unwrap(); 497 assert_eq!(page.slug, "日本"); 498 assert_eq!(page.permalink, config.make_permalink(&page.slug)); 499 } 500 501 #[test] 502 fn can_specify_summary() { 503 let config = Config::default_for_test(); 504 let content = r#" 505 +++ 506 +++ 507 Hello world 508 <!-- more -->"# 509 .to_string(); 510 let res = Page::parse(Path::new("hello.md"), &content, &config, &PathBuf::new()); 511 assert!(res.is_ok()); 512 let mut page = res.unwrap(); 513 page.render_markdown( 514 &HashMap::default(), 515 &Tera::default(), 516 &config, 517 InsertAnchor::None, 518 &HashMap::new(), 519 ) 520 .unwrap(); 521 assert_eq!(page.summary, Some("<p>Hello world</p>\n".to_string())); 522 } 523 524 #[test] 525 fn strips_footnotes_in_summary() { 526 let config = Config::default_for_test(); 527 let content = r#" 528 +++ 529 +++ 530 This page has footnotes, here's one. [^1] 531 532 <!-- more --> 533 534 And here's another. [^2] 535 536 [^1]: This is the first footnote. 537 538 [^2]: This is the second footnote."# 539 .to_string(); 540 let res = Page::parse(Path::new("hello.md"), &content, &config, &PathBuf::new()); 541 assert!(res.is_ok()); 542 let mut page = res.unwrap(); 543 page.render_markdown( 544 &HashMap::default(), 545 &Tera::default(), 546 &config, 547 InsertAnchor::None, 548 &HashMap::new(), 549 ) 550 .unwrap(); 551 assert_eq!( 552 page.summary, 553 Some("<p>This page has footnotes, here\'s one. </p>\n".to_string()) 554 ); 555 } 556 557 #[test] 558 fn page_with_assets_gets_right_info() { 559 let tmp_dir = tempdir().expect("create temp dir"); 560 let path = tmp_dir.path(); 561 create_dir(&path.join("content")).expect("create content temp dir"); 562 create_dir(&path.join("content").join("posts")).expect("create posts temp dir"); 563 let nested_path = path.join("content").join("posts").join("with-assets"); 564 create_dir(&nested_path).expect("create nested temp dir"); 565 let mut f = File::create(nested_path.join("index.md")).unwrap(); 566 f.write_all(b"+++\n+++\n").unwrap(); 567 File::create(nested_path.join("example.js")).unwrap(); 568 File::create(nested_path.join("graph.jpg")).unwrap(); 569 File::create(nested_path.join("fail.png")).unwrap(); 570 571 let res = Page::from_file( 572 nested_path.join("index.md").as_path(), 573 &Config::default(), 574 &path.to_path_buf(), 575 ); 576 assert!(res.is_ok()); 577 let page = res.unwrap(); 578 assert_eq!(page.file.parent, path.join("content").join("posts")); 579 assert_eq!(page.slug, "with-assets"); 580 assert_eq!(page.assets.len(), 3); 581 assert!(page.serialized_assets[0].starts_with('/')); 582 assert_eq!(page.permalink, "http://a-website.com/posts/with-assets/"); 583 } 584 585 #[test] 586 fn page_with_assets_and_slug_overrides_path() { 587 let tmp_dir = tempdir().expect("create temp dir"); 588 let path = tmp_dir.path(); 589 create_dir(&path.join("content")).expect("create content temp dir"); 590 create_dir(&path.join("content").join("posts")).expect("create posts temp dir"); 591 let nested_path = path.join("content").join("posts").join("with-assets"); 592 create_dir(&nested_path).expect("create nested temp dir"); 593 let mut f = File::create(nested_path.join("index.md")).unwrap(); 594 f.write_all(b"+++\nslug=\"hey\"\n+++\n").unwrap(); 595 File::create(nested_path.join("example.js")).unwrap(); 596 File::create(nested_path.join("graph.jpg")).unwrap(); 597 File::create(nested_path.join("fail.png")).unwrap(); 598 599 let res = Page::from_file( 600 nested_path.join("index.md").as_path(), 601 &Config::default(), 602 &path.to_path_buf(), 603 ); 604 assert!(res.is_ok()); 605 let page = res.unwrap(); 606 assert_eq!(page.file.parent, path.join("content").join("posts")); 607 assert_eq!(page.slug, "hey"); 608 assert_eq!(page.assets.len(), 3); 609 assert_eq!(page.permalink, "http://a-website.com/posts/hey/"); 610 } 611 612 // https://github.com/getzola/zola/issues/674 613 #[test] 614 fn page_with_assets_uses_filepath_for_assets() { 615 let tmp_dir = tempdir().expect("create temp dir"); 616 let path = tmp_dir.path(); 617 create_dir(&path.join("content")).expect("create content temp dir"); 618 create_dir(&path.join("content").join("posts")).expect("create posts temp dir"); 619 let nested_path = path.join("content").join("posts").join("with_assets"); 620 create_dir(&nested_path).expect("create nested temp dir"); 621 let mut f = File::create(nested_path.join("index.md")).unwrap(); 622 f.write_all(b"+++\n+++\n").unwrap(); 623 File::create(nested_path.join("example.js")).unwrap(); 624 File::create(nested_path.join("graph.jpg")).unwrap(); 625 File::create(nested_path.join("fail.png")).unwrap(); 626 627 let res = Page::from_file( 628 nested_path.join("index.md").as_path(), 629 &Config::default(), 630 &path.to_path_buf(), 631 ); 632 assert!(res.is_ok()); 633 let page = res.unwrap(); 634 assert_eq!(page.file.parent, path.join("content").join("posts")); 635 assert_eq!(page.assets.len(), 3); 636 assert_eq!(page.serialized_assets.len(), 3); 637 // We should not get with-assets since that's the slugified version 638 assert!(page.serialized_assets[0].contains("with_assets")); 639 assert_eq!(page.permalink, "http://a-website.com/posts/with-assets/"); 640 } 641 642 // https://github.com/getzola/zola/issues/607 643 #[test] 644 fn page_with_assets_and_date_in_folder_name() { 645 let tmp_dir = tempdir().expect("create temp dir"); 646 let path = tmp_dir.path(); 647 create_dir(&path.join("content")).expect("create content temp dir"); 648 create_dir(&path.join("content").join("posts")).expect("create posts temp dir"); 649 let nested_path = path.join("content").join("posts").join("2013-06-02_with-assets"); 650 create_dir(&nested_path).expect("create nested temp dir"); 651 let mut f = File::create(nested_path.join("index.md")).unwrap(); 652 f.write_all(b"+++\n\n+++\n").unwrap(); 653 File::create(nested_path.join("example.js")).unwrap(); 654 File::create(nested_path.join("graph.jpg")).unwrap(); 655 File::create(nested_path.join("fail.png")).unwrap(); 656 657 let res = Page::from_file( 658 nested_path.join("index.md").as_path(), 659 &Config::default(), 660 &path.to_path_buf(), 661 ); 662 assert!(res.is_ok()); 663 let page = res.unwrap(); 664 assert_eq!(page.file.parent, path.join("content").join("posts")); 665 assert_eq!(page.slug, "with-assets"); 666 assert_eq!(page.meta.date, Some("2013-06-02".to_string())); 667 assert_eq!(page.assets.len(), 3); 668 assert_eq!(page.permalink, "http://a-website.com/posts/with-assets/"); 669 } 670 671 #[test] 672 fn page_with_ignored_assets_filters_out_correct_files() { 673 let tmp_dir = tempdir().expect("create temp dir"); 674 let path = tmp_dir.path(); 675 create_dir(&path.join("content")).expect("create content temp dir"); 676 create_dir(&path.join("content").join("posts")).expect("create posts temp dir"); 677 let nested_path = path.join("content").join("posts").join("with-assets"); 678 create_dir(&nested_path).expect("create nested temp dir"); 679 let mut f = File::create(nested_path.join("index.md")).unwrap(); 680 f.write_all(b"+++\nslug=\"hey\"\n+++\n").unwrap(); 681 File::create(nested_path.join("example.js")).unwrap(); 682 File::create(nested_path.join("graph.jpg")).unwrap(); 683 File::create(nested_path.join("fail.png")).unwrap(); 684 685 let mut gsb = GlobSetBuilder::new(); 686 gsb.add(Glob::new("*.{js,png}").unwrap()); 687 let mut config = Config::default(); 688 config.ignored_content_globset = Some(gsb.build().unwrap()); 689 690 let res = 691 Page::from_file(nested_path.join("index.md").as_path(), &config, &path.to_path_buf()); 692 693 assert!(res.is_ok()); 694 let page = res.unwrap(); 695 assert_eq!(page.assets.len(), 1); 696 assert_eq!(page.assets[0].file_name().unwrap().to_str(), Some("graph.jpg")); 697 } 698 699 #[test] 700 fn can_get_date_from_short_date_in_filename() { 701 let config = Config::default(); 702 let content = r#" 703 +++ 704 +++ 705 Hello world 706 <!-- more -->"# 707 .to_string(); 708 let res = Page::parse(Path::new("2018-10-08_hello.md"), &content, &config, &PathBuf::new()); 709 assert!(res.is_ok()); 710 let page = res.unwrap(); 711 712 assert_eq!(page.meta.date, Some("2018-10-08".to_string())); 713 assert_eq!(page.slug, "hello"); 714 } 715 716 // https://github.com/getzola/zola/pull/1323#issuecomment-779401063 717 #[test] 718 fn can_get_date_from_short_date_in_filename_respects_slugification_strategy() { 719 let mut config = Config::default(); 720 config.slugify.paths = SlugifyStrategy::Off; 721 let content = r#" 722 +++ 723 +++ 724 Hello world 725 <!-- more -->"# 726 .to_string(); 727 let res = 728 Page::parse(Path::new("2018-10-08_ こんにちは.md"), &content, &config, &PathBuf::new()); 729 assert!(res.is_ok()); 730 let page = res.unwrap(); 731 732 assert_eq!(page.meta.date, Some("2018-10-08".to_string())); 733 assert_eq!(page.slug, " こんにちは"); 734 } 735 736 #[test] 737 fn can_get_date_from_filename_with_spaces() { 738 let config = Config::default(); 739 let content = r#" 740 +++ 741 +++ 742 Hello world 743 <!-- more -->"# 744 .to_string(); 745 let res = 746 Page::parse(Path::new("2018-10-08 - hello.md"), &content, &config, &PathBuf::new()); 747 assert!(res.is_ok()); 748 let page = res.unwrap(); 749 750 assert_eq!(page.meta.date, Some("2018-10-08".to_string())); 751 assert_eq!(page.slug, "hello"); 752 } 753 754 #[test] 755 fn can_get_date_from_filename_with_spaces_respects_slugification() { 756 let mut config = Config::default(); 757 config.slugify.paths = SlugifyStrategy::Off; 758 let content = r#" 759 +++ 760 +++ 761 Hello world 762 <!-- more -->"# 763 .to_string(); 764 let res = 765 Page::parse(Path::new("2018-10-08 - hello.md"), &content, &config, &PathBuf::new()); 766 assert!(res.is_ok()); 767 let page = res.unwrap(); 768 769 assert_eq!(page.meta.date, Some("2018-10-08".to_string())); 770 assert_eq!(page.slug, " hello"); 771 } 772 773 #[test] 774 fn can_get_date_from_full_rfc3339_date_in_filename() { 775 let config = Config::default(); 776 let content = r#" 777 +++ 778 +++ 779 Hello world 780 <!-- more -->"# 781 .to_string(); 782 let res = Page::parse( 783 Path::new("2018-10-02T15:00:00Z-hello.md"), 784 &content, 785 &config, 786 &PathBuf::new(), 787 ); 788 assert!(res.is_ok()); 789 let page = res.unwrap(); 790 791 assert_eq!(page.meta.date, Some("2018-10-02T15:00:00Z".to_string())); 792 assert_eq!(page.slug, "hello"); 793 } 794 795 // https://github.com/getzola/zola/pull/1323#issuecomment-779401063 796 #[test] 797 fn can_get_date_from_full_rfc3339_date_in_filename_respects_slugification_strategy() { 798 let mut config = Config::default(); 799 config.slugify.paths = SlugifyStrategy::Off; 800 let content = r#" 801 +++ 802 +++ 803 Hello world 804 <!-- more -->"# 805 .to_string(); 806 let res = Page::parse( 807 Path::new("2018-10-02T15:00:00Z- こんにちは.md"), 808 &content, 809 &config, 810 &PathBuf::new(), 811 ); 812 assert!(res.is_ok()); 813 let page = res.unwrap(); 814 815 assert_eq!(page.meta.date, Some("2018-10-02T15:00:00Z".to_string())); 816 assert_eq!(page.slug, " こんにちは"); 817 } 818 819 #[test] 820 fn frontmatter_date_override_filename_date() { 821 let config = Config::default(); 822 let content = r#" 823 +++ 824 date = 2018-09-09 825 +++ 826 Hello world 827 <!-- more -->"# 828 .to_string(); 829 let res = Page::parse(Path::new("2018-10-08_hello.md"), &content, &config, &PathBuf::new()); 830 assert!(res.is_ok()); 831 let page = res.unwrap(); 832 833 assert_eq!(page.meta.date, Some("2018-09-09".to_string())); 834 assert_eq!(page.slug, "hello"); 835 } 836 837 #[test] 838 fn can_specify_language_in_filename() { 839 let mut config = Config::default(); 840 config.languages.insert("fr".to_owned(), LanguageOptions::default()); 841 let content = r#" 842 +++ 843 +++ 844 Bonjour le monde"# 845 .to_string(); 846 let res = Page::parse(Path::new("hello.fr.md"), &content, &config, &PathBuf::new()); 847 assert!(res.is_ok()); 848 let page = res.unwrap(); 849 assert_eq!(page.lang, "fr".to_string()); 850 assert_eq!(page.slug, "hello"); 851 assert_eq!(page.permalink, "http://a-website.com/fr/hello/"); 852 } 853 854 #[test] 855 fn can_specify_language_in_filename_with_date() { 856 let mut config = Config::default(); 857 config.languages.insert("fr".to_owned(), LanguageOptions::default()); 858 let content = r#" 859 +++ 860 +++ 861 Bonjour le monde"# 862 .to_string(); 863 let res = 864 Page::parse(Path::new("2018-10-08_hello.fr.md"), &content, &config, &PathBuf::new()); 865 assert!(res.is_ok()); 866 let page = res.unwrap(); 867 assert_eq!(page.meta.date, Some("2018-10-08".to_string())); 868 assert_eq!(page.lang, "fr".to_string()); 869 assert_eq!(page.slug, "hello"); 870 assert_eq!(page.permalink, "http://a-website.com/fr/hello/"); 871 } 872 873 #[test] 874 fn i18n_frontmatter_path_overrides_default_permalink() { 875 let mut config = Config::default(); 876 config.languages.insert("fr".to_owned(), LanguageOptions::default()); 877 let content = r#" 878 +++ 879 path = "bonjour" 880 +++ 881 Bonjour le monde"# 882 .to_string(); 883 let res = Page::parse(Path::new("hello.fr.md"), &content, &config, &PathBuf::new()); 884 assert!(res.is_ok()); 885 let page = res.unwrap(); 886 assert_eq!(page.lang, "fr".to_string()); 887 assert_eq!(page.slug, "hello"); 888 assert_eq!(page.permalink, "http://a-website.com/bonjour/"); 889 } 890 } 891