/ libs / mdbook-shared / src / summary.rs
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  }