summary.rs
1 use crate::{errors::*, get_book_content_path}; 2 use log::{debug, trace, warn}; 3 use memchr::{self, Memchr}; 4 use pulldown_cmark::{self, Event, HeadingLevel, Tag}; 5 use serde::{Deserialize, Serialize}; 6 use std::fmt::{self, Display, Formatter}; 7 use std::iter::FromIterator; 8 use std::ops::{Deref, DerefMut}; 9 use std::path::{Path, PathBuf}; 10 11 pub fn get_summary_path(mdbook_root: impl AsRef<Path>) -> Option<PathBuf> { 12 let mdbook_root = mdbook_root.as_ref(); 13 let path = mdbook_root.join("SUMMARY.md"); 14 if path.exists() { 15 return Some(path); 16 } 17 let path = mdbook_root.join("src").join("SUMMARY.md"); 18 if path.exists() { 19 return Some(path); 20 } 21 None 22 } 23 24 /// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be 25 /// used when loading a book from disk.a 26 /// 27 /// # Summary Format 28 /// 29 /// **Title:** It's common practice to begin with a title, generally 30 /// "# Summary". It's not mandatory and the parser (currently) ignores it, so 31 /// you can too if you feel like it. 32 /// 33 /// **Prefix Chapter:** Before the main numbered chapters you can add a couple 34 /// of elements that will not be numbered. This is useful for forewords, 35 /// introductions, etc. There are however some constraints. You can not nest 36 /// prefix chapters, they should all be on the root level. And you can not add 37 /// prefix chapters once you have added numbered chapters. 38 /// 39 /// ```markdown 40 /// [Title of prefix element](relative/path/to/markdown.md) 41 /// ``` 42 /// 43 /// **Part Title:** An optional title for the next collect of numbered chapters. The numbered 44 /// chapters can be broken into as many parts as desired. 45 /// 46 /// **Numbered Chapter:** Numbered chapters are the main content of the book, 47 /// they 48 /// will be numbered and can be nested, resulting in a nice hierarchy (chapters, 49 /// sub-chapters, etc.) 50 /// 51 /// ```markdown 52 /// # Title of Part 53 /// 54 /// - [Title of the Chapter](relative/path/to/markdown.md) 55 /// ``` 56 /// 57 /// You can either use - or * to indicate a numbered chapter, the parser doesn't 58 /// care but you'll probably want to stay consistent. 59 /// 60 /// **Suffix Chapter:** After the numbered chapters you can add a couple of 61 /// non-numbered chapters. They are the same as prefix chapters but come after 62 /// the numbered chapters instead of before. 63 /// 64 /// All other elements are unsupported and will be ignored at best or result in 65 /// an error. 66 pub fn parse_summary(path: &Path, summary: &str) -> Result<Summary<PathBuf>> { 67 let parser = SummaryParser::new(Some(path), summary); 68 parser.parse() 69 } 70 71 /// The parsed `SUMMARY.md`, specifying how the book should be laid out. 72 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] 73 pub struct Summary<R> { 74 /// An optional title for the `SUMMARY.md`, currently just ignored. 75 pub title: Option<String>, 76 /// Chapters before the main text (e.g. an introduction). 77 pub prefix_chapters: Vec<SummaryItem<R>>, 78 /// The main numbered chapters of the book, broken into one or more possibly named parts. 79 pub numbered_chapters: Vec<SummaryItem<R>>, 80 /// Items which come after the main document (e.g. a conclusion). 81 pub suffix_chapters: Vec<SummaryItem<R>>, 82 } 83 84 /// A struct representing an entry in the `SUMMARY.md`, possibly with nested 85 /// entries. 86 /// 87 /// This is roughly the equivalent of `[Some section](./path/to/file.md)`. 88 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 89 pub struct Link<R> { 90 /// The name of the chapter. 91 pub name: String, 92 /// The location of the chapter's source file, taking the book's `src` 93 /// directory as the root. 94 pub location: Option<R>, 95 /// The section number, if this chapter is in the numbered section. 96 pub number: Option<SectionNumber>, 97 /// Any nested items this chapter may contain. 98 pub nested_items: Vec<SummaryItem<R>>, 99 } 100 101 impl<R> Link<R> { 102 /// Create a new link with no nested items. 103 pub fn new<S: Into<String>, P: Into<R>>(name: S, location: P) -> Link<R> { 104 Link { 105 name: name.into(), 106 location: Some(location.into()), 107 number: None, 108 nested_items: Vec::new(), 109 } 110 } 111 } 112 113 impl<R: Default> Default for Link<R> { 114 fn default() -> Self { 115 Link { 116 name: String::new(), 117 location: Some(R::default()), 118 number: None, 119 nested_items: Vec::new(), 120 } 121 } 122 } 123 124 /// An item in `SUMMARY.md` which could be either a separator or a `Link`. 125 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 126 pub enum SummaryItem<R> { 127 /// A link to a chapter. 128 Link(Link<R>), 129 /// A separator (`---`). 130 Separator, 131 /// A part title. 132 PartTitle(String), 133 } 134 135 impl<R> SummaryItem<R> { 136 pub fn maybe_link_mut(&mut self) -> Option<&mut Link<R>> { 137 match *self { 138 SummaryItem::Link(ref mut l) => Some(l), 139 _ => None, 140 } 141 } 142 143 pub fn maybe_link(&self) -> Option<&Link<R>> { 144 match *self { 145 SummaryItem::Link(ref l) => Some(l), 146 _ => None, 147 } 148 } 149 } 150 151 impl<R> From<Link<R>> for SummaryItem<R> { 152 fn from(other: Link<R>) -> SummaryItem<R> { 153 SummaryItem::Link(other) 154 } 155 } 156 157 /// A recursive descent (-ish) parser for a `SUMMARY.md`. 158 /// 159 /// 160 /// # Grammar 161 /// 162 /// The `SUMMARY.md` file has a grammar which looks something like this: 163 /// 164 /// ```text 165 /// summary ::= title prefix_chapters numbered_chapters 166 /// suffix_chapters 167 /// title ::= "# " TEXT 168 /// | EPSILON 169 /// prefix_chapters ::= item* 170 /// suffix_chapters ::= item* 171 /// numbered_chapters ::= part+ 172 /// part ::= title dotted_item+ 173 /// dotted_item ::= INDENT* DOT_POINT item 174 /// item ::= link 175 /// | separator 176 /// separator ::= "---" 177 /// link ::= "[" TEXT "]" "(" TEXT ")" 178 /// DOT_POINT ::= "-" 179 /// | "*" 180 /// ``` 181 /// 182 /// > **Note:** the `TEXT` terminal is "normal" text, and should (roughly) 183 /// > match the following regex: "[^<>\n[]]+". 184 struct SummaryParser<'a> { 185 src_path: Option<&'a Path>, 186 src: &'a str, 187 stream: pulldown_cmark::OffsetIter<'a, 'a>, 188 offset: usize, 189 190 /// We can't actually put an event back into the `OffsetIter` stream, so instead we store it 191 /// here until somebody calls `next_event` again. 192 back: Option<Event<'a>>, 193 } 194 195 /// Reads `Events` from the provided stream until the corresponding 196 /// `Event::End` is encountered which matches the `$delimiter` pattern. 197 /// 198 /// This is the equivalent of doing 199 /// `$stream.take_while(|e| e != $delimiter).collect()` but it allows you to 200 /// use pattern matching and you won't get errors because `take_while()` 201 /// moves `$stream` out of self. 202 macro_rules! collect_events { 203 ($stream:expr,start $delimiter:pat) => { 204 collect_events!($stream, Event::Start($delimiter)) 205 }; 206 ($stream:expr,end $delimiter:pat) => { 207 collect_events!($stream, Event::End($delimiter)) 208 }; 209 ($stream:expr, $delimiter:pat) => {{ 210 let mut events = Vec::new(); 211 212 loop { 213 let event = $stream.next().map(|(ev, _range)| ev); 214 trace!("Next event: {:?}", event); 215 216 match event { 217 Some($delimiter) => break, 218 Some(other) => events.push(other), 219 None => { 220 debug!( 221 "Reached end of stream without finding the closing pattern, {}", 222 stringify!($delimiter) 223 ); 224 break; 225 } 226 } 227 } 228 229 events 230 }}; 231 } 232 233 impl<'a> SummaryParser<'a> { 234 fn new(path: Option<&'a Path>, text: &'a str) -> SummaryParser<'a> { 235 let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter(); 236 237 SummaryParser { 238 src_path: path, 239 src: text, 240 stream: pulldown_parser, 241 offset: 0, 242 back: None, 243 } 244 } 245 246 /// Get the current line and column to give the user more useful error 247 /// messages. 248 fn current_location(&self) -> (usize, usize) { 249 let previous_text = self.src[..self.offset].as_bytes(); 250 let line = Memchr::new(b'\n', previous_text).count() + 1; 251 let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0); 252 let col = self.src[start_of_line..self.offset].chars().count(); 253 254 (line, col) 255 } 256 257 /// Parse the text the `SummaryParser` was created with. 258 fn parse(mut self) -> Result<Summary<PathBuf>> { 259 let title = self.parse_title(); 260 261 let prefix_chapters = self.parse_affix(true).map_err(|err| { 262 anyhow::anyhow!("There was an error parsing the prefix chapters: {err}") 263 })?; 264 let numbered_chapters = self.parse_parts().map_err(|err| { 265 anyhow::anyhow!("There was an error parsing the numbered chapters: {err}") 266 })?; 267 let suffix_chapters = self.parse_affix(false).map_err(|err| { 268 anyhow::anyhow!("There was an error parsing the suffix chapters: {err}") 269 })?; 270 271 Ok(Summary { 272 title, 273 prefix_chapters, 274 numbered_chapters, 275 suffix_chapters, 276 }) 277 } 278 279 /// Parse the affix chapters. 280 fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem<PathBuf>>> { 281 let mut items = Vec::new(); 282 debug!( 283 "Parsing {} items", 284 if is_prefix { "prefix" } else { "suffix" } 285 ); 286 287 loop { 288 match self.next_event() { 289 Some(ev @ Event::Start(Tag::List(..))) 290 | Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { 291 if is_prefix { 292 // we've finished prefix chapters and are at the start 293 // of the numbered section. 294 self.back(ev); 295 break; 296 } else { 297 bail!(self.parse_error("Suffix chapters cannot be followed by a list")); 298 } 299 } 300 Some(Event::Start(Tag::Link(_type, href, _title))) => { 301 let link = self.parse_link(href.to_string())?; 302 items.push(SummaryItem::Link(link)); 303 } 304 Some(Event::Rule) => items.push(SummaryItem::Separator), 305 Some(_) => {} 306 None => break, 307 } 308 } 309 310 Ok(items) 311 } 312 313 fn parse_parts(&mut self) -> Result<Vec<SummaryItem<PathBuf>>> { 314 let mut parts = vec![]; 315 316 // We want the section numbers to be continues through all parts. 317 let mut root_number = SectionNumber::default(); 318 let mut root_items = 0; 319 320 loop { 321 // Possibly match a title or the end of the "numbered chapters part". 322 let title = match self.next_event() { 323 Some(ev @ Event::Start(Tag::Paragraph)) => { 324 // we're starting the suffix chapters 325 self.back(ev); 326 break; 327 } 328 329 Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { 330 debug!("Found a h1 in the SUMMARY"); 331 332 let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..)); 333 Some(stringify_events(tags)) 334 } 335 336 Some(ev) => { 337 self.back(ev); 338 None 339 } 340 341 None => break, // EOF, bail... 342 }; 343 344 // Parse the rest of the part. 345 let numbered_chapters = self 346 .parse_numbered(&mut root_items, &mut root_number) 347 .map_err(|err| { 348 anyhow::anyhow!("There was an error parsing the numbered chapters: {err}") 349 })?; 350 351 if let Some(title) = title { 352 parts.push(SummaryItem::PartTitle(title)); 353 } 354 parts.extend(numbered_chapters); 355 } 356 357 Ok(parts) 358 } 359 360 /// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened. 361 fn parse_link(&mut self, href: String) -> Result<Link<PathBuf>> { 362 let href = href.replace("%20", " "); 363 let link_content = collect_events!(self.stream, end Tag::Link(..)); 364 let name = stringify_events(link_content); 365 366 let path = if href.is_empty() { 367 None 368 } else { 369 let path_buf = PathBuf::from(href.clone()); 370 if let Some(src_path) = self.src_path { 371 // check if it under the en directory 372 let full_path = get_book_content_path(PathBuf::from(&src_path)) 373 .unwrap() 374 .join(&path_buf); 375 if !full_path.exists() { 376 return Err(anyhow::anyhow!( 377 "The path {:?} does not exist (created from {href})", 378 full_path 379 )); 380 } 381 } 382 Some(path_buf) 383 }; 384 385 Ok(Link { 386 name, 387 location: path, 388 number: None, 389 nested_items: Vec::new(), 390 }) 391 } 392 393 /// Parse the numbered chapters. 394 fn parse_numbered( 395 &mut self, 396 root_items: &mut u32, 397 root_number: &mut SectionNumber, 398 ) -> Result<Vec<SummaryItem<PathBuf>>> { 399 let mut items = Vec::new(); 400 401 // For the first iteration, we want to just skip any opening paragraph tags, as that just 402 // marks the start of the list. But after that, another opening paragraph indicates that we 403 // have started a new part or the suffix chapters. 404 let mut first = true; 405 406 loop { 407 match self.next_event() { 408 Some(ev @ Event::Start(Tag::Paragraph)) => { 409 if !first { 410 // we're starting the suffix chapters 411 self.back(ev); 412 break; 413 } 414 } 415 // The expectation is that pulldown cmark will terminate a paragraph before a new 416 // heading, so we can always count on this to return without skipping headings. 417 Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { 418 // we're starting a new part 419 self.back(ev); 420 break; 421 } 422 Some(ev @ Event::Start(Tag::List(..))) => { 423 self.back(ev); 424 let mut bunch_of_items = self.parse_nested_numbered(root_number)?; 425 426 // if we've resumed after something like a rule the root sections 427 // will be numbered from 1. We need to manually go back and update 428 // them 429 update_section_numbers(&mut bunch_of_items, 0, *root_items); 430 *root_items += bunch_of_items.len() as u32; 431 items.extend(bunch_of_items); 432 } 433 Some(Event::Start(other_tag)) => { 434 trace!("Skipping contents of {:?}", other_tag); 435 436 // Skip over the contents of this tag 437 while let Some(event) = self.next_event() { 438 if event == Event::End(other_tag.clone()) { 439 break; 440 } 441 } 442 } 443 Some(Event::Rule) => { 444 items.push(SummaryItem::Separator); 445 } 446 447 // something else... ignore 448 Some(_) => {} 449 450 // EOF, bail... 451 None => { 452 break; 453 } 454 } 455 456 // From now on, we cannot accept any new paragraph opening tags. 457 first = false; 458 } 459 460 Ok(items) 461 } 462 463 /// Push an event back to the tail of the stream. 464 fn back(&mut self, ev: Event<'a>) { 465 assert!(self.back.is_none()); 466 trace!("Back: {:?}", ev); 467 self.back = Some(ev); 468 } 469 470 fn next_event(&mut self) -> Option<Event<'a>> { 471 let next = self.back.take().or_else(|| { 472 self.stream.next().map(|(ev, range)| { 473 self.offset = range.start; 474 ev 475 }) 476 }); 477 478 trace!("Next event: {:?}", next); 479 480 next 481 } 482 483 fn parse_nested_numbered( 484 &mut self, 485 parent: &SectionNumber, 486 ) -> Result<Vec<SummaryItem<PathBuf>>> { 487 debug!("Parsing numbered chapters at level {}", parent); 488 let mut items = Vec::new(); 489 490 loop { 491 match self.next_event() { 492 Some(Event::Start(Tag::Item)) => { 493 let item = self.parse_nested_item(parent, items.len())?; 494 items.push(item); 495 } 496 Some(Event::Start(Tag::List(..))) => { 497 // Skip this tag after comment bacause it is not nested. 498 if items.is_empty() { 499 continue; 500 } 501 // recurse to parse the nested list 502 let (_, last_item) = get_last_link(&mut items)?; 503 let last_item_number = last_item 504 .number 505 .as_ref() 506 .expect("All numbered chapters have numbers"); 507 508 let sub_items = self.parse_nested_numbered(last_item_number)?; 509 510 last_item.nested_items = sub_items; 511 } 512 Some(Event::End(Tag::List(..))) => break, 513 Some(_) => {} 514 None => break, 515 } 516 } 517 518 Ok(items) 519 } 520 521 fn parse_nested_item( 522 &mut self, 523 parent: &SectionNumber, 524 num_existing_items: usize, 525 ) -> Result<SummaryItem<PathBuf>> { 526 loop { 527 match self.next_event() { 528 Some(Event::Start(Tag::Paragraph)) => continue, 529 Some(Event::Start(Tag::Link(_type, href, _title))) => { 530 let mut link = self.parse_link(href.to_string())?; 531 532 let mut number = parent.clone(); 533 number.0.push(num_existing_items as u32 + 1); 534 trace!( 535 "Found chapter: {} {} ({})", 536 number, 537 link.name, 538 link.location 539 .as_ref() 540 .map(|p| p.to_str().unwrap_or("")) 541 .unwrap_or("[draft]") 542 ); 543 544 link.number = Some(number); 545 546 return Ok(SummaryItem::Link(link)); 547 } 548 other => { 549 warn!("Expected a start of a link, actually got {:?}", other); 550 bail!(self.parse_error( 551 "The link items for nested chapters must only contain a hyperlink" 552 )); 553 } 554 } 555 } 556 } 557 558 fn parse_error<D: Display>(&self, msg: D) -> Error { 559 let (line, col) = self.current_location(); 560 anyhow::anyhow!( 561 "failed to parse SUMMARY.md line {}, column {}: {}", 562 line, 563 col, 564 msg 565 ) 566 } 567 568 /// Try to parse the title line. 569 fn parse_title(&mut self) -> Option<String> { 570 loop { 571 match self.next_event() { 572 Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { 573 debug!("Found a h1 in the SUMMARY"); 574 575 let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..)); 576 return Some(stringify_events(tags)); 577 } 578 // Skip a HTML element such as a comment line. 579 Some(Event::Html(_)) => {} 580 // Otherwise, no title. 581 _ => return None, 582 } 583 } 584 } 585 } 586 587 fn update_section_numbers<R>(sections: &mut [SummaryItem<R>], level: usize, by: u32) { 588 for section in sections { 589 if let SummaryItem::Link(ref mut link) = *section { 590 if let Some(ref mut number) = link.number { 591 number.0[level] += by; 592 } 593 594 update_section_numbers(&mut link.nested_items, level, by); 595 } 596 } 597 } 598 599 /// Gets a pointer to the last `Link` in a list of `SummaryItem`s, and its 600 /// index. 601 fn get_last_link<R>(links: &mut [SummaryItem<R>]) -> Result<(usize, &mut Link<R>)> { 602 links 603 .iter_mut() 604 .enumerate() 605 .filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l))).next_back() 606 .ok_or_else(|| 607 anyhow::anyhow!("Unable to get last link because the list of SummaryItems doesn't contain any Links") 608 ) 609 } 610 611 /// Removes the styling from a list of Markdown events and returns just the 612 /// plain text. 613 fn stringify_events(events: Vec<Event<'_>>) -> String { 614 events 615 .into_iter() 616 .filter_map(|t| match t { 617 Event::Text(text) | Event::Code(text) => Some(text.into_string()), 618 Event::SoftBreak => Some(String::from(" ")), 619 _ => None, 620 }) 621 .collect() 622 } 623 624 /// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with 625 /// a pretty `Display` impl. 626 #[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] 627 pub struct SectionNumber(pub Vec<u32>); 628 629 impl Display for SectionNumber { 630 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 631 if self.0.is_empty() { 632 write!(f, "0") 633 } else { 634 for item in &self.0 { 635 write!(f, "{}.", item)?; 636 } 637 Ok(()) 638 } 639 } 640 } 641 642 impl Deref for SectionNumber { 643 type Target = Vec<u32>; 644 fn deref(&self) -> &Self::Target { 645 &self.0 646 } 647 } 648 649 impl DerefMut for SectionNumber { 650 fn deref_mut(&mut self) -> &mut Self::Target { 651 &mut self.0 652 } 653 } 654 655 impl FromIterator<u32> for SectionNumber { 656 fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self { 657 SectionNumber(it.into_iter().collect()) 658 } 659 } 660 661 #[cfg(test)] 662 mod tests { 663 use super::*; 664 665 #[test] 666 fn section_number_has_correct_dotted_representation() { 667 let inputs = vec![ 668 (vec![0], "0."), 669 (vec![1, 3], "1.3."), 670 (vec![1, 2, 3], "1.2.3."), 671 ]; 672 673 for (input, should_be) in inputs { 674 let section_number = SectionNumber(input).to_string(); 675 assert_eq!(section_number, should_be); 676 } 677 } 678 679 #[test] 680 fn parse_initial_title() { 681 let src = "# Summary"; 682 let should_be = String::from("Summary"); 683 684 let mut parser = SummaryParser::new(None, src); 685 let got = parser.parse_title().unwrap(); 686 687 assert_eq!(got, should_be); 688 } 689 690 #[test] 691 fn parse_title_with_styling() { 692 let src = "# My **Awesome** Summary"; 693 let should_be = String::from("My Awesome Summary"); 694 695 let mut parser = SummaryParser::new(None, src); 696 let got = parser.parse_title().unwrap(); 697 698 assert_eq!(got, should_be); 699 } 700 701 #[test] 702 fn convert_markdown_events_to_a_string() { 703 let src = "Hello *World*, `this` is some text [and a link](./path/to/link)"; 704 let should_be = "Hello World, this is some text and a link"; 705 706 let events = pulldown_cmark::Parser::new(src).collect(); 707 let got = stringify_events(events); 708 709 assert_eq!(got, should_be); 710 } 711 712 #[test] 713 fn parse_some_prefix_items() { 714 let src = "[First](./first.md)\n[Second](./second.md)\n"; 715 let mut parser = SummaryParser::new(None, src); 716 717 let should_be = vec![ 718 SummaryItem::Link(Link { 719 name: String::from("First"), 720 location: Some(PathBuf::from("./first.md")), 721 ..Default::default() 722 }), 723 SummaryItem::Link(Link { 724 name: String::from("Second"), 725 location: Some(PathBuf::from("./second.md")), 726 ..Default::default() 727 }), 728 ]; 729 730 let got = parser.parse_affix(true).unwrap(); 731 732 assert_eq!(got, should_be); 733 } 734 735 #[test] 736 fn parse_prefix_items_with_a_separator() { 737 let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n"; 738 let mut parser = SummaryParser::new(None, src); 739 740 let got = parser.parse_affix(true).unwrap(); 741 742 assert_eq!(got.len(), 3); 743 assert_eq!(got[1], SummaryItem::Separator); 744 } 745 746 #[test] 747 fn suffix_items_cannot_be_followed_by_a_list() { 748 let src = "[First](./first.md)\n- [Second](./second.md)\n"; 749 let mut parser = SummaryParser::new(None, src); 750 751 let got = parser.parse_affix(false); 752 753 assert!(got.is_err()); 754 } 755 756 #[test] 757 fn parse_a_link() { 758 let src = "[First](./first.md)"; 759 let should_be = Link { 760 name: String::from("First"), 761 location: Some(PathBuf::from("./first.md")), 762 ..Default::default() 763 }; 764 765 let mut parser = SummaryParser::new(None, src); 766 let _ = parser.stream.next(); // Discard opening paragraph 767 768 let href = match parser.stream.next() { 769 Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(), 770 other => panic!("Unreachable, {:?}", other), 771 }; 772 773 let got = parser.parse_link(href).unwrap(); 774 assert_eq!(got, should_be); 775 } 776 777 #[test] 778 fn parse_a_numbered_chapter() { 779 let src = "- [First](./first.md)\n"; 780 let link = Link { 781 name: String::from("First"), 782 location: Some(PathBuf::from("./first.md")), 783 number: Some(SectionNumber(vec![1])), 784 ..Default::default() 785 }; 786 let should_be = vec![SummaryItem::Link(link)]; 787 788 let mut parser = SummaryParser::new(None, src); 789 let got = parser 790 .parse_numbered(&mut 0, &mut SectionNumber::default()) 791 .unwrap(); 792 793 assert_eq!(got, should_be); 794 } 795 796 #[test] 797 fn parse_nested_numbered_chapters() { 798 let src = "- [First](./first.md)\n - [Nested](./nested.md)\n- [Second](./second.md)"; 799 800 let should_be = vec![ 801 SummaryItem::Link(Link { 802 name: String::from("First"), 803 location: Some(PathBuf::from("./first.md")), 804 number: Some(SectionNumber(vec![1])), 805 nested_items: vec![SummaryItem::Link(Link { 806 name: String::from("Nested"), 807 location: Some(PathBuf::from("./nested.md")), 808 number: Some(SectionNumber(vec![1, 1])), 809 nested_items: Vec::new(), 810 })], 811 }), 812 SummaryItem::Link(Link { 813 name: String::from("Second"), 814 location: Some(PathBuf::from("./second.md")), 815 number: Some(SectionNumber(vec![2])), 816 nested_items: Vec::new(), 817 }), 818 ]; 819 820 let mut parser = SummaryParser::new(None, src); 821 let got = parser 822 .parse_numbered(&mut 0, &mut SectionNumber::default()) 823 .unwrap(); 824 825 assert_eq!(got, should_be); 826 } 827 828 #[test] 829 fn parse_numbered_chapters_separated_by_comment() { 830 let src = "- [First](./first.md)\n<!-- this is a comment -->\n- [Second](./second.md)"; 831 832 let should_be = vec![ 833 SummaryItem::Link(Link { 834 name: String::from("First"), 835 location: Some(PathBuf::from("./first.md")), 836 number: Some(SectionNumber(vec![1])), 837 nested_items: Vec::new(), 838 }), 839 SummaryItem::Link(Link { 840 name: String::from("Second"), 841 location: Some(PathBuf::from("./second.md")), 842 number: Some(SectionNumber(vec![2])), 843 nested_items: Vec::new(), 844 }), 845 ]; 846 847 let mut parser = SummaryParser::new(None, src); 848 let got = parser 849 .parse_numbered(&mut 0, &mut SectionNumber::default()) 850 .unwrap(); 851 852 assert_eq!(got, should_be); 853 } 854 855 #[test] 856 fn parse_titled_parts() { 857 let src = "- [First](./first.md)\n- [Second](./second.md)\n\ 858 # Title 2\n- [Third](./third.md)\n\t- [Fourth](./fourth.md)"; 859 860 let should_be = vec![ 861 SummaryItem::Link(Link { 862 name: String::from("First"), 863 location: Some(PathBuf::from("./first.md")), 864 number: Some(SectionNumber(vec![1])), 865 nested_items: Vec::new(), 866 }), 867 SummaryItem::Link(Link { 868 name: String::from("Second"), 869 location: Some(PathBuf::from("./second.md")), 870 number: Some(SectionNumber(vec![2])), 871 nested_items: Vec::new(), 872 }), 873 SummaryItem::PartTitle(String::from("Title 2")), 874 SummaryItem::Link(Link { 875 name: String::from("Third"), 876 location: Some(PathBuf::from("./third.md")), 877 number: Some(SectionNumber(vec![3])), 878 nested_items: vec![SummaryItem::Link(Link { 879 name: String::from("Fourth"), 880 location: Some(PathBuf::from("./fourth.md")), 881 number: Some(SectionNumber(vec![3, 1])), 882 nested_items: Vec::new(), 883 })], 884 }), 885 ]; 886 887 let mut parser = SummaryParser::new(None, src); 888 let got = parser.parse_parts().unwrap(); 889 890 assert_eq!(got, should_be); 891 } 892 893 /// This test ensures the book will continue to pass because it breaks the 894 /// `SUMMARY.md` up using level 2 headers ([example]). 895 /// 896 /// [example]: https://github.com/rust-lang/book/blob/2c942dc094f4ddcdc7aba7564f80782801197c99/second-edition/src/SUMMARY.md#basic-rust-literacy 897 #[test] 898 fn can_have_a_subheader_between_nested_items() { 899 let src = "- [First](./first.md)\n\n## Subheading\n\n- [Second](./second.md)\n"; 900 let should_be = vec![ 901 SummaryItem::Link(Link { 902 name: String::from("First"), 903 location: Some(PathBuf::from("./first.md")), 904 number: Some(SectionNumber(vec![1])), 905 nested_items: Vec::new(), 906 }), 907 SummaryItem::Link(Link { 908 name: String::from("Second"), 909 location: Some(PathBuf::from("./second.md")), 910 number: Some(SectionNumber(vec![2])), 911 nested_items: Vec::new(), 912 }), 913 ]; 914 915 let mut parser = SummaryParser::new(None, src); 916 let got = parser 917 .parse_numbered(&mut 0, &mut SectionNumber::default()) 918 .unwrap(); 919 920 assert_eq!(got, should_be); 921 } 922 923 #[test] 924 fn an_empty_link_location_is_a_draft_chapter() { 925 let src = "- [Empty]()\n"; 926 let mut parser = SummaryParser::new(None, src); 927 928 let got = parser.parse_numbered(&mut 0, &mut SectionNumber::default()); 929 let should_be = vec![SummaryItem::Link(Link { 930 name: String::from("Empty"), 931 location: None, 932 number: Some(SectionNumber(vec![1])), 933 nested_items: Vec::new(), 934 })]; 935 936 assert!(got.is_ok()); 937 assert_eq!(got.unwrap(), should_be); 938 } 939 940 /// Regression test for https://github.com/rust-lang/mdBook/issues/779 941 /// Ensure section numbers are correctly incremented after a horizontal separator. 942 #[test] 943 fn keep_numbering_after_separator() { 944 let src = 945 "- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n"; 946 let should_be = vec![ 947 SummaryItem::Link(Link { 948 name: String::from("First"), 949 location: Some(PathBuf::from("./first.md")), 950 number: Some(SectionNumber(vec![1])), 951 nested_items: Vec::new(), 952 }), 953 SummaryItem::Separator, 954 SummaryItem::Link(Link { 955 name: String::from("Second"), 956 location: Some(PathBuf::from("./second.md")), 957 number: Some(SectionNumber(vec![2])), 958 nested_items: Vec::new(), 959 }), 960 SummaryItem::Separator, 961 SummaryItem::Link(Link { 962 name: String::from("Third"), 963 location: Some(PathBuf::from("./third.md")), 964 number: Some(SectionNumber(vec![3])), 965 nested_items: Vec::new(), 966 }), 967 ]; 968 969 let mut parser = SummaryParser::new(None, src); 970 let got = parser 971 .parse_numbered(&mut 0, &mut SectionNumber::default()) 972 .unwrap(); 973 974 assert_eq!(got, should_be); 975 } 976 977 /// Regression test for https://github.com/rust-lang/mdBook/issues/1218 978 /// Ensure chapter names spread across multiple lines have spaces between all the words. 979 #[test] 980 fn add_space_for_multi_line_chapter_names() { 981 let src = "- [Chapter\ntitle](./chapter.md)"; 982 let should_be = vec![SummaryItem::Link(Link { 983 name: String::from("Chapter title"), 984 location: Some(PathBuf::from("./chapter.md")), 985 number: Some(SectionNumber(vec![1])), 986 nested_items: Vec::new(), 987 })]; 988 989 let mut parser = SummaryParser::new(None, src); 990 let got = parser 991 .parse_numbered(&mut 0, &mut SectionNumber::default()) 992 .unwrap(); 993 994 assert_eq!(got, should_be); 995 } 996 997 #[test] 998 fn allow_space_in_link_destination() { 999 let src = "- [test1](./test%20link1.md)\n- [test2](<./test link2.md>)"; 1000 let should_be = vec![ 1001 SummaryItem::Link(Link { 1002 name: String::from("test1"), 1003 location: Some(PathBuf::from("./test link1.md")), 1004 number: Some(SectionNumber(vec![1])), 1005 nested_items: Vec::new(), 1006 }), 1007 SummaryItem::Link(Link { 1008 name: String::from("test2"), 1009 location: Some(PathBuf::from("./test link2.md")), 1010 number: Some(SectionNumber(vec![2])), 1011 nested_items: Vec::new(), 1012 }), 1013 ]; 1014 let mut parser = SummaryParser::new(None, src); 1015 let got = parser 1016 .parse_numbered(&mut 0, &mut SectionNumber::default()) 1017 .unwrap(); 1018 1019 assert_eq!(got, should_be); 1020 } 1021 1022 #[test] 1023 fn skip_html_comments() { 1024 let src = r#"<!-- 1025 # Title - En 1026 --> 1027 # Title - Local 1028 1029 <!-- 1030 [Prefix 00-01 - En](ch00-01.md) 1031 [Prefix 00-02 - En](ch00-02.md) 1032 --> 1033 [Prefix 00-01 - Local](ch00-01.md) 1034 [Prefix 00-02 - Local](ch00-02.md) 1035 1036 <!-- 1037 ## Section Title - En 1038 --> 1039 ## Section Title - Localized 1040 1041 <!-- 1042 - [Ch 01-00 - En](ch01-00.md) 1043 - [Ch 01-01 - En](ch01-01.md) 1044 - [Ch 01-02 - En](ch01-02.md) 1045 --> 1046 - [Ch 01-00 - Local](ch01-00.md) 1047 - [Ch 01-01 - Local](ch01-01.md) 1048 - [Ch 01-02 - Local](ch01-02.md) 1049 1050 <!-- 1051 - [Ch 02-00 - En](ch02-00.md) 1052 --> 1053 - [Ch 02-00 - Local](ch02-00.md) 1054 1055 <!-- 1056 [Appendix A - En](appendix-01.md) 1057 [Appendix B - En](appendix-02.md) 1058 -->` 1059 [Appendix A - Local](appendix-01.md) 1060 [Appendix B - Local](appendix-02.md) 1061 "#; 1062 1063 let mut parser = SummaryParser::new(None, src); 1064 1065 // ---- Title ---- 1066 let title = parser.parse_title(); 1067 assert_eq!(title, Some(String::from("Title - Local"))); 1068 1069 // ---- Prefix Chapters ---- 1070 1071 let new_affix_item = |name, location| { 1072 SummaryItem::Link(Link { 1073 name: String::from(name), 1074 location: Some(PathBuf::from(location)), 1075 ..Default::default() 1076 }) 1077 }; 1078 1079 let should_be = vec![ 1080 new_affix_item("Prefix 00-01 - Local", "ch00-01.md"), 1081 new_affix_item("Prefix 00-02 - Local", "ch00-02.md"), 1082 ]; 1083 1084 let got = parser.parse_affix(true).unwrap(); 1085 assert_eq!(got, should_be); 1086 1087 // ---- Numbered Chapters ---- 1088 1089 let new_numbered_item = |name, location, numbers: &[u32], nested_items| { 1090 SummaryItem::Link(Link { 1091 name: String::from(name), 1092 location: Some(PathBuf::from(location)), 1093 number: Some(SectionNumber(numbers.to_vec())), 1094 nested_items, 1095 }) 1096 }; 1097 1098 let ch01_nested = vec![ 1099 new_numbered_item("Ch 01-01 - Local", "ch01-01.md", &[1, 1], vec![]), 1100 new_numbered_item("Ch 01-02 - Local", "ch01-02.md", &[1, 2], vec![]), 1101 ]; 1102 1103 let should_be = vec![ 1104 new_numbered_item("Ch 01-00 - Local", "ch01-00.md", &[1], ch01_nested), 1105 new_numbered_item("Ch 02-00 - Local", "ch02-00.md", &[2], vec![]), 1106 ]; 1107 let got = parser.parse_parts().unwrap(); 1108 assert_eq!(got, should_be); 1109 1110 // ---- Suffix Chapters ---- 1111 1112 let should_be = vec![ 1113 new_affix_item("Appendix A - Local", "appendix-01.md"), 1114 new_affix_item("Appendix B - Local", "appendix-02.md"), 1115 ]; 1116 1117 let got = parser.parse_affix(false).unwrap(); 1118 assert_eq!(got, should_be); 1119 } 1120 }