diff options
Diffstat (limited to '')
-rw-r--r-- | vendor/mdbook/src/book/summary.rs | 1097 |
1 files changed, 1097 insertions, 0 deletions
diff --git a/vendor/mdbook/src/book/summary.rs b/vendor/mdbook/src/book/summary.rs new file mode 100644 index 000000000..2bd81580f --- /dev/null +++ b/vendor/mdbook/src/book/summary.rs @@ -0,0 +1,1097 @@ +use crate::errors::*; +use memchr::{self, Memchr}; +use pulldown_cmark::{self, Event, HeadingLevel, Tag}; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display, Formatter}; +use std::iter::FromIterator; +use std::ops::{Deref, DerefMut}; +use std::path::{Path, PathBuf}; + +/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be +/// used when loading a book from disk. +/// +/// # Summary Format +/// +/// **Title:** It's common practice to begin with a title, generally +/// "# Summary". It's not mandatory and the parser (currently) ignores it, so +/// you can too if you feel like it. +/// +/// **Prefix Chapter:** Before the main numbered chapters you can add a couple +/// of elements that will not be numbered. This is useful for forewords, +/// introductions, etc. There are however some constraints. You can not nest +/// prefix chapters, they should all be on the root level. And you can not add +/// prefix chapters once you have added numbered chapters. +/// +/// ```markdown +/// [Title of prefix element](relative/path/to/markdown.md) +/// ``` +/// +/// **Part Title:** An optional title for the next collect of numbered chapters. The numbered +/// chapters can be broken into as many parts as desired. +/// +/// **Numbered Chapter:** Numbered chapters are the main content of the book, +/// they +/// will be numbered and can be nested, resulting in a nice hierarchy (chapters, +/// sub-chapters, etc.) +/// +/// ```markdown +/// # Title of Part +/// +/// - [Title of the Chapter](relative/path/to/markdown.md) +/// ``` +/// +/// You can either use - or * to indicate a numbered chapter, the parser doesn't +/// care but you'll probably want to stay consistent. +/// +/// **Suffix Chapter:** After the numbered chapters you can add a couple of +/// non-numbered chapters. They are the same as prefix chapters but come after +/// the numbered chapters instead of before. +/// +/// All other elements are unsupported and will be ignored at best or result in +/// an error. +pub fn parse_summary(summary: &str) -> Result<Summary> { + let parser = SummaryParser::new(summary); + parser.parse() +} + +/// The parsed `SUMMARY.md`, specifying how the book should be laid out. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Summary { + /// An optional title for the `SUMMARY.md`, currently just ignored. + pub title: Option<String>, + /// Chapters before the main text (e.g. an introduction). + pub prefix_chapters: Vec<SummaryItem>, + /// The main numbered chapters of the book, broken into one or more possibly named parts. + pub numbered_chapters: Vec<SummaryItem>, + /// Items which come after the main document (e.g. a conclusion). + pub suffix_chapters: Vec<SummaryItem>, +} + +/// A struct representing an entry in the `SUMMARY.md`, possibly with nested +/// entries. +/// +/// This is roughly the equivalent of `[Some section](./path/to/file.md)`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Link { + /// The name of the chapter. + pub name: String, + /// The location of the chapter's source file, taking the book's `src` + /// directory as the root. + pub location: Option<PathBuf>, + /// The section number, if this chapter is in the numbered section. + pub number: Option<SectionNumber>, + /// Any nested items this chapter may contain. + pub nested_items: Vec<SummaryItem>, +} + +impl Link { + /// Create a new link with no nested items. + pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link { + Link { + name: name.into(), + location: Some(location.as_ref().to_path_buf()), + number: None, + nested_items: Vec::new(), + } + } +} + +impl Default for Link { + fn default() -> Self { + Link { + name: String::new(), + location: Some(PathBuf::new()), + number: None, + nested_items: Vec::new(), + } + } +} + +/// An item in `SUMMARY.md` which could be either a separator or a `Link`. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum SummaryItem { + /// A link to a chapter. + Link(Link), + /// A separator (`---`). + Separator, + /// A part title. + PartTitle(String), +} + +impl SummaryItem { + fn maybe_link_mut(&mut self) -> Option<&mut Link> { + match *self { + SummaryItem::Link(ref mut l) => Some(l), + _ => None, + } + } +} + +impl From<Link> for SummaryItem { + fn from(other: Link) -> SummaryItem { + SummaryItem::Link(other) + } +} + +/// A recursive descent (-ish) parser for a `SUMMARY.md`. +/// +/// +/// # Grammar +/// +/// The `SUMMARY.md` file has a grammar which looks something like this: +/// +/// ```text +/// summary ::= title prefix_chapters numbered_chapters +/// suffix_chapters +/// title ::= "# " TEXT +/// | EPSILON +/// prefix_chapters ::= item* +/// suffix_chapters ::= item* +/// numbered_chapters ::= part+ +/// part ::= title dotted_item+ +/// dotted_item ::= INDENT* DOT_POINT item +/// item ::= link +/// | separator +/// separator ::= "---" +/// link ::= "[" TEXT "]" "(" TEXT ")" +/// DOT_POINT ::= "-" +/// | "*" +/// ``` +/// +/// > **Note:** the `TEXT` terminal is "normal" text, and should (roughly) +/// > match the following regex: "[^<>\n[]]+". +struct SummaryParser<'a> { + src: &'a str, + stream: pulldown_cmark::OffsetIter<'a, 'a>, + offset: usize, + + /// We can't actually put an event back into the `OffsetIter` stream, so instead we store it + /// here until somebody calls `next_event` again. + back: Option<Event<'a>>, +} + +/// Reads `Events` from the provided stream until the corresponding +/// `Event::End` is encountered which matches the `$delimiter` pattern. +/// +/// This is the equivalent of doing +/// `$stream.take_while(|e| e != $delimiter).collect()` but it allows you to +/// use pattern matching and you won't get errors because `take_while()` +/// moves `$stream` out of self. +macro_rules! collect_events { + ($stream:expr,start $delimiter:pat) => { + collect_events!($stream, Event::Start($delimiter)) + }; + ($stream:expr,end $delimiter:pat) => { + collect_events!($stream, Event::End($delimiter)) + }; + ($stream:expr, $delimiter:pat) => {{ + let mut events = Vec::new(); + + loop { + let event = $stream.next().map(|(ev, _range)| ev); + trace!("Next event: {:?}", event); + + match event { + Some($delimiter) => break, + Some(other) => events.push(other), + None => { + debug!( + "Reached end of stream without finding the closing pattern, {}", + stringify!($delimiter) + ); + break; + } + } + } + + events + }}; +} + +impl<'a> SummaryParser<'a> { + fn new(text: &str) -> SummaryParser<'_> { + let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter(); + + SummaryParser { + src: text, + stream: pulldown_parser, + offset: 0, + back: None, + } + } + + /// Get the current line and column to give the user more useful error + /// messages. + fn current_location(&self) -> (usize, usize) { + let previous_text = self.src[..self.offset].as_bytes(); + let line = Memchr::new(b'\n', previous_text).count() + 1; + let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0); + let col = self.src[start_of_line..self.offset].chars().count(); + + (line, col) + } + + /// Parse the text the `SummaryParser` was created with. + fn parse(mut self) -> Result<Summary> { + let title = self.parse_title(); + + let prefix_chapters = self + .parse_affix(true) + .with_context(|| "There was an error parsing the prefix chapters")?; + let numbered_chapters = self + .parse_parts() + .with_context(|| "There was an error parsing the numbered chapters")?; + let suffix_chapters = self + .parse_affix(false) + .with_context(|| "There was an error parsing the suffix chapters")?; + + Ok(Summary { + title, + prefix_chapters, + numbered_chapters, + suffix_chapters, + }) + } + + /// Parse the affix chapters. + fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> { + let mut items = Vec::new(); + debug!( + "Parsing {} items", + if is_prefix { "prefix" } else { "suffix" } + ); + + loop { + match self.next_event() { + Some(ev @ Event::Start(Tag::List(..))) + | Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { + if is_prefix { + // we've finished prefix chapters and are at the start + // of the numbered section. + self.back(ev); + break; + } else { + bail!(self.parse_error("Suffix chapters cannot be followed by a list")); + } + } + Some(Event::Start(Tag::Link(_type, href, _title))) => { + let link = self.parse_link(href.to_string()); + items.push(SummaryItem::Link(link)); + } + Some(Event::Rule) => items.push(SummaryItem::Separator), + Some(_) => {} + None => break, + } + } + + Ok(items) + } + + fn parse_parts(&mut self) -> Result<Vec<SummaryItem>> { + let mut parts = vec![]; + + // We want the section numbers to be continues through all parts. + let mut root_number = SectionNumber::default(); + let mut root_items = 0; + + loop { + // Possibly match a title or the end of the "numbered chapters part". + let title = match self.next_event() { + Some(ev @ Event::Start(Tag::Paragraph)) => { + // we're starting the suffix chapters + self.back(ev); + break; + } + + Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { + debug!("Found a h1 in the SUMMARY"); + + let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..)); + Some(stringify_events(tags)) + } + + Some(ev) => { + self.back(ev); + None + } + + None => break, // EOF, bail... + }; + + // Parse the rest of the part. + let numbered_chapters = self + .parse_numbered(&mut root_items, &mut root_number) + .with_context(|| "There was an error parsing the numbered chapters")?; + + if let Some(title) = title { + parts.push(SummaryItem::PartTitle(title)); + } + parts.extend(numbered_chapters); + } + + Ok(parts) + } + + /// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened. + fn parse_link(&mut self, href: String) -> Link { + let href = href.replace("%20", " "); + let link_content = collect_events!(self.stream, end Tag::Link(..)); + let name = stringify_events(link_content); + + let path = if href.is_empty() { + None + } else { + Some(PathBuf::from(href)) + }; + + Link { + name, + location: path, + number: None, + nested_items: Vec::new(), + } + } + + /// Parse the numbered chapters. + fn parse_numbered( + &mut self, + root_items: &mut u32, + root_number: &mut SectionNumber, + ) -> Result<Vec<SummaryItem>> { + let mut items = Vec::new(); + + // For the first iteration, we want to just skip any opening paragraph tags, as that just + // marks the start of the list. But after that, another opening paragraph indicates that we + // have started a new part or the suffix chapters. + let mut first = true; + + loop { + match self.next_event() { + Some(ev @ Event::Start(Tag::Paragraph)) => { + if !first { + // we're starting the suffix chapters + self.back(ev); + break; + } + } + // The expectation is that pulldown cmark will terminate a paragraph before a new + // heading, so we can always count on this to return without skipping headings. + Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { + // we're starting a new part + self.back(ev); + break; + } + Some(ev @ Event::Start(Tag::List(..))) => { + self.back(ev); + let mut bunch_of_items = self.parse_nested_numbered(root_number)?; + + // if we've resumed after something like a rule the root sections + // will be numbered from 1. We need to manually go back and update + // them + update_section_numbers(&mut bunch_of_items, 0, *root_items); + *root_items += bunch_of_items.len() as u32; + items.extend(bunch_of_items); + } + Some(Event::Start(other_tag)) => { + trace!("Skipping contents of {:?}", other_tag); + + // Skip over the contents of this tag + while let Some(event) = self.next_event() { + if event == Event::End(other_tag.clone()) { + break; + } + } + } + Some(Event::Rule) => { + items.push(SummaryItem::Separator); + } + + // something else... ignore + Some(_) => {} + + // EOF, bail... + None => { + break; + } + } + + // From now on, we cannot accept any new paragraph opening tags. + first = false; + } + + Ok(items) + } + + /// Push an event back to the tail of the stream. + fn back(&mut self, ev: Event<'a>) { + assert!(self.back.is_none()); + trace!("Back: {:?}", ev); + self.back = Some(ev); + } + + fn next_event(&mut self) -> Option<Event<'a>> { + let next = self.back.take().or_else(|| { + self.stream.next().map(|(ev, range)| { + self.offset = range.start; + ev + }) + }); + + trace!("Next event: {:?}", next); + + next + } + + fn parse_nested_numbered(&mut self, parent: &SectionNumber) -> Result<Vec<SummaryItem>> { + debug!("Parsing numbered chapters at level {}", parent); + let mut items = Vec::new(); + + loop { + match self.next_event() { + Some(Event::Start(Tag::Item)) => { + let item = self.parse_nested_item(parent, items.len())?; + items.push(item); + } + Some(Event::Start(Tag::List(..))) => { + // Skip this tag after comment bacause it is not nested. + if items.is_empty() { + continue; + } + // recurse to parse the nested list + let (_, last_item) = get_last_link(&mut items)?; + let last_item_number = last_item + .number + .as_ref() + .expect("All numbered chapters have numbers"); + + let sub_items = self.parse_nested_numbered(last_item_number)?; + + last_item.nested_items = sub_items; + } + Some(Event::End(Tag::List(..))) => break, + Some(_) => {} + None => break, + } + } + + Ok(items) + } + + fn parse_nested_item( + &mut self, + parent: &SectionNumber, + num_existing_items: usize, + ) -> Result<SummaryItem> { + loop { + match self.next_event() { + Some(Event::Start(Tag::Paragraph)) => continue, + Some(Event::Start(Tag::Link(_type, href, _title))) => { + let mut link = self.parse_link(href.to_string()); + + let mut number = parent.clone(); + number.0.push(num_existing_items as u32 + 1); + trace!( + "Found chapter: {} {} ({})", + number, + link.name, + link.location + .as_ref() + .map(|p| p.to_str().unwrap_or("")) + .unwrap_or("[draft]") + ); + + link.number = Some(number); + + return Ok(SummaryItem::Link(link)); + } + other => { + warn!("Expected a start of a link, actually got {:?}", other); + bail!(self.parse_error( + "The link items for nested chapters must only contain a hyperlink" + )); + } + } + } + } + + fn parse_error<D: Display>(&self, msg: D) -> Error { + let (line, col) = self.current_location(); + anyhow::anyhow!( + "failed to parse SUMMARY.md line {}, column {}: {}", + line, + col, + msg + ) + } + + /// Try to parse the title line. + fn parse_title(&mut self) -> Option<String> { + loop { + match self.next_event() { + Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => { + debug!("Found a h1 in the SUMMARY"); + + let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..)); + return Some(stringify_events(tags)); + } + // Skip a HTML element such as a comment line. + Some(Event::Html(_)) => {} + // Otherwise, no title. + Some(ev) => { + self.back(ev); + return None; + } + _ => return None, + } + } + } +} + +fn update_section_numbers(sections: &mut [SummaryItem], level: usize, by: u32) { + for section in sections { + if let SummaryItem::Link(ref mut link) = *section { + if let Some(ref mut number) = link.number { + number.0[level] += by; + } + + update_section_numbers(&mut link.nested_items, level, by); + } + } +} + +/// Gets a pointer to the last `Link` in a list of `SummaryItem`s, and its +/// index. +fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> { + links + .iter_mut() + .enumerate() + .filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l))) + .rev() + .next() + .ok_or_else(|| + anyhow::anyhow!("Unable to get last link because the list of SummaryItems doesn't contain any Links") + ) +} + +/// Removes the styling from a list of Markdown events and returns just the +/// plain text. +fn stringify_events(events: Vec<Event<'_>>) -> String { + events + .into_iter() + .filter_map(|t| match t { + Event::Text(text) | Event::Code(text) => Some(text.into_string()), + Event::SoftBreak => Some(String::from(" ")), + _ => None, + }) + .collect() +} + +/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with +/// a pretty `Display` impl. +#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] +pub struct SectionNumber(pub Vec<u32>); + +impl Display for SectionNumber { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if self.0.is_empty() { + write!(f, "0") + } else { + for item in &self.0 { + write!(f, "{}.", item)?; + } + Ok(()) + } + } +} + +impl Deref for SectionNumber { + type Target = Vec<u32>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SectionNumber { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl FromIterator<u32> for SectionNumber { + fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self { + SectionNumber(it.into_iter().collect()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn section_number_has_correct_dotted_representation() { + let inputs = vec![ + (vec![0], "0."), + (vec![1, 3], "1.3."), + (vec![1, 2, 3], "1.2.3."), + ]; + + for (input, should_be) in inputs { + let section_number = SectionNumber(input).to_string(); + assert_eq!(section_number, should_be); + } + } + + #[test] + fn parse_initial_title() { + let src = "# Summary"; + let should_be = String::from("Summary"); + + let mut parser = SummaryParser::new(src); + let got = parser.parse_title().unwrap(); + + assert_eq!(got, should_be); + } + + #[test] + fn no_initial_title() { + let src = "[Link]()"; + let mut parser = SummaryParser::new(src); + + assert!(parser.parse_title().is_none()); + assert!(matches!( + parser.next_event(), + Some(Event::Start(Tag::Paragraph)) + )); + } + + #[test] + fn parse_title_with_styling() { + let src = "# My **Awesome** Summary"; + let should_be = String::from("My Awesome Summary"); + + let mut parser = SummaryParser::new(src); + let got = parser.parse_title().unwrap(); + + assert_eq!(got, should_be); + } + + #[test] + fn convert_markdown_events_to_a_string() { + let src = "Hello *World*, `this` is some text [and a link](./path/to/link)"; + let should_be = "Hello World, this is some text and a link"; + + let events = pulldown_cmark::Parser::new(src).collect(); + let got = stringify_events(events); + + assert_eq!(got, should_be); + } + + #[test] + fn parse_some_prefix_items() { + let src = "[First](./first.md)\n[Second](./second.md)\n"; + let mut parser = SummaryParser::new(src); + + let should_be = vec![ + SummaryItem::Link(Link { + name: String::from("First"), + location: Some(PathBuf::from("./first.md")), + ..Default::default() + }), + SummaryItem::Link(Link { + name: String::from("Second"), + location: Some(PathBuf::from("./second.md")), + ..Default::default() + }), + ]; + + let got = parser.parse_affix(true).unwrap(); + + assert_eq!(got, should_be); + } + + #[test] + fn parse_prefix_items_with_a_separator() { + let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n"; + let mut parser = SummaryParser::new(src); + + let got = parser.parse_affix(true).unwrap(); + + assert_eq!(got.len(), 3); + assert_eq!(got[1], SummaryItem::Separator); + } + + #[test] + fn suffix_items_cannot_be_followed_by_a_list() { + let src = "[First](./first.md)\n- [Second](./second.md)\n"; + let mut parser = SummaryParser::new(src); + + let got = parser.parse_affix(false); + + assert!(got.is_err()); + } + + #[test] + fn parse_a_link() { + let src = "[First](./first.md)"; + let should_be = Link { + name: String::from("First"), + location: Some(PathBuf::from("./first.md")), + ..Default::default() + }; + + let mut parser = SummaryParser::new(src); + let _ = parser.stream.next(); // Discard opening paragraph + + let href = match parser.stream.next() { + Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(), + other => panic!("Unreachable, {:?}", other), + }; + + let got = parser.parse_link(href); + assert_eq!(got, should_be); + } + + #[test] + fn parse_a_numbered_chapter() { + let src = "- [First](./first.md)\n"; + let link = Link { + name: String::from("First"), + location: Some(PathBuf::from("./first.md")), + number: Some(SectionNumber(vec![1])), + ..Default::default() + }; + let should_be = vec![SummaryItem::Link(link)]; + + let mut parser = SummaryParser::new(src); + let got = parser + .parse_numbered(&mut 0, &mut SectionNumber::default()) + .unwrap(); + + assert_eq!(got, should_be); + } + + #[test] + fn parse_nested_numbered_chapters() { + let src = "- [First](./first.md)\n - [Nested](./nested.md)\n- [Second](./second.md)"; + + let should_be = vec![ + SummaryItem::Link(Link { + name: String::from("First"), + location: Some(PathBuf::from("./first.md")), + number: Some(SectionNumber(vec![1])), + nested_items: vec![SummaryItem::Link(Link { + name: String::from("Nested"), + location: Some(PathBuf::from("./nested.md")), + number: Some(SectionNumber(vec![1, 1])), + nested_items: Vec::new(), + })], + }), + SummaryItem::Link(Link { + name: String::from("Second"), + location: Some(PathBuf::from("./second.md")), + number: Some(SectionNumber(vec![2])), + nested_items: Vec::new(), + }), + ]; + + let mut parser = SummaryParser::new(src); + let got = parser + .parse_numbered(&mut 0, &mut SectionNumber::default()) + .unwrap(); + + assert_eq!(got, should_be); + } + + #[test] + fn parse_numbered_chapters_separated_by_comment() { + let src = "- [First](./first.md)\n<!-- this is a comment -->\n- [Second](./second.md)"; + + let should_be = vec![ + SummaryItem::Link(Link { + name: String::from("First"), + location: Some(PathBuf::from("./first.md")), + number: Some(SectionNumber(vec![1])), + nested_items: Vec::new(), + }), + SummaryItem::Link(Link { + name: String::from("Second"), + location: Some(PathBuf::from("./second.md")), + number: Some(SectionNumber(vec![2])), + nested_items: Vec::new(), + }), + ]; + + let mut parser = SummaryParser::new(src); + let got = parser + .parse_numbered(&mut 0, &mut SectionNumber::default()) + .unwrap(); + + assert_eq!(got, should_be); + } + + #[test] + fn parse_titled_parts() { + let src = "- [First](./first.md)\n- [Second](./second.md)\n\ + # Title 2\n- [Third](./third.md)\n\t- [Fourth](./fourth.md)"; + + let should_be = vec![ + SummaryItem::Link(Link { + name: String::from("First"), + location: Some(PathBuf::from("./first.md")), + number: Some(SectionNumber(vec![1])), + nested_items: Vec::new(), + }), + SummaryItem::Link(Link { + name: String::from("Second"), + location: Some(PathBuf::from("./second.md")), + number: Some(SectionNumber(vec![2])), + nested_items: Vec::new(), + }), + SummaryItem::PartTitle(String::from("Title 2")), + SummaryItem::Link(Link { + name: String::from("Third"), + location: Some(PathBuf::from("./third.md")), + number: Some(SectionNumber(vec![3])), + nested_items: vec![SummaryItem::Link(Link { + name: String::from("Fourth"), + location: Some(PathBuf::from("./fourth.md")), + number: Some(SectionNumber(vec![3, 1])), + nested_items: Vec::new(), + })], + }), + ]; + + let mut parser = SummaryParser::new(src); + let got = parser.parse_parts().unwrap(); + + assert_eq!(got, should_be); + } + + /// This test ensures the book will continue to pass because it breaks the + /// `SUMMARY.md` up using level 2 headers ([example]). + /// + /// [example]: https://github.com/rust-lang/book/blob/2c942dc094f4ddcdc7aba7564f80782801197c99/second-edition/src/SUMMARY.md#basic-rust-literacy + #[test] + fn can_have_a_subheader_between_nested_items() { + let src = "- [First](./first.md)\n\n## Subheading\n\n- [Second](./second.md)\n"; + let should_be = vec![ + SummaryItem::Link(Link { + name: String::from("First"), + location: Some(PathBuf::from("./first.md")), + number: Some(SectionNumber(vec![1])), + nested_items: Vec::new(), + }), + SummaryItem::Link(Link { + name: String::from("Second"), + location: Some(PathBuf::from("./second.md")), + number: Some(SectionNumber(vec![2])), + nested_items: Vec::new(), + }), + ]; + + let mut parser = SummaryParser::new(src); + let got = parser + .parse_numbered(&mut 0, &mut SectionNumber::default()) + .unwrap(); + + assert_eq!(got, should_be); + } + + #[test] + fn an_empty_link_location_is_a_draft_chapter() { + let src = "- [Empty]()\n"; + let mut parser = SummaryParser::new(src); + + let got = parser.parse_numbered(&mut 0, &mut SectionNumber::default()); + let should_be = vec![SummaryItem::Link(Link { + name: String::from("Empty"), + location: None, + number: Some(SectionNumber(vec![1])), + nested_items: Vec::new(), + })]; + + assert!(got.is_ok()); + assert_eq!(got.unwrap(), should_be); + } + + /// Regression test for https://github.com/rust-lang/mdBook/issues/779 + /// Ensure section numbers are correctly incremented after a horizontal separator. + #[test] + fn keep_numbering_after_separator() { + let src = + "- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n"; + let should_be = vec![ + SummaryItem::Link(Link { + name: String::from("First"), + location: Some(PathBuf::from("./first.md")), + number: Some(SectionNumber(vec![1])), + nested_items: Vec::new(), + }), + SummaryItem::Separator, + SummaryItem::Link(Link { + name: String::from("Second"), + location: Some(PathBuf::from("./second.md")), + number: Some(SectionNumber(vec![2])), + nested_items: Vec::new(), + }), + SummaryItem::Separator, + SummaryItem::Link(Link { + name: String::from("Third"), + location: Some(PathBuf::from("./third.md")), + number: Some(SectionNumber(vec![3])), + nested_items: Vec::new(), + }), + ]; + + let mut parser = SummaryParser::new(src); + let got = parser + .parse_numbered(&mut 0, &mut SectionNumber::default()) + .unwrap(); + + assert_eq!(got, should_be); + } + + /// Regression test for https://github.com/rust-lang/mdBook/issues/1218 + /// Ensure chapter names spread across multiple lines have spaces between all the words. + #[test] + fn add_space_for_multi_line_chapter_names() { + let src = "- [Chapter\ntitle](./chapter.md)"; + let should_be = vec![SummaryItem::Link(Link { + name: String::from("Chapter title"), + location: Some(PathBuf::from("./chapter.md")), + number: Some(SectionNumber(vec![1])), + nested_items: Vec::new(), + })]; + + let mut parser = SummaryParser::new(src); + let got = parser + .parse_numbered(&mut 0, &mut SectionNumber::default()) + .unwrap(); + + assert_eq!(got, should_be); + } + + #[test] + fn allow_space_in_link_destination() { + let src = "- [test1](./test%20link1.md)\n- [test2](<./test link2.md>)"; + let should_be = vec![ + SummaryItem::Link(Link { + name: String::from("test1"), + location: Some(PathBuf::from("./test link1.md")), + number: Some(SectionNumber(vec![1])), + nested_items: Vec::new(), + }), + SummaryItem::Link(Link { + name: String::from("test2"), + location: Some(PathBuf::from("./test link2.md")), + number: Some(SectionNumber(vec![2])), + nested_items: Vec::new(), + }), + ]; + let mut parser = SummaryParser::new(src); + let got = parser + .parse_numbered(&mut 0, &mut SectionNumber::default()) + .unwrap(); + + assert_eq!(got, should_be); + } + + #[test] + fn skip_html_comments() { + let src = r#"<!-- +# Title - En +--> +# Title - Local + +<!-- +[Prefix 00-01 - En](ch00-01.md) +[Prefix 00-02 - En](ch00-02.md) +--> +[Prefix 00-01 - Local](ch00-01.md) +[Prefix 00-02 - Local](ch00-02.md) + +<!-- +## Section Title - En +--> +## Section Title - Localized + +<!-- +- [Ch 01-00 - En](ch01-00.md) + - [Ch 01-01 - En](ch01-01.md) + - [Ch 01-02 - En](ch01-02.md) +--> +- [Ch 01-00 - Local](ch01-00.md) + - [Ch 01-01 - Local](ch01-01.md) + - [Ch 01-02 - Local](ch01-02.md) + +<!-- +- [Ch 02-00 - En](ch02-00.md) +--> +- [Ch 02-00 - Local](ch02-00.md) + +<!-- +[Appendix A - En](appendix-01.md) +[Appendix B - En](appendix-02.md) +-->` +[Appendix A - Local](appendix-01.md) +[Appendix B - Local](appendix-02.md) +"#; + + let mut parser = SummaryParser::new(src); + + // ---- Title ---- + let title = parser.parse_title(); + assert_eq!(title, Some(String::from("Title - Local"))); + + // ---- Prefix Chapters ---- + + let new_affix_item = |name, location| { + SummaryItem::Link(Link { + name: String::from(name), + location: Some(PathBuf::from(location)), + ..Default::default() + }) + }; + + let should_be = vec![ + new_affix_item("Prefix 00-01 - Local", "ch00-01.md"), + new_affix_item("Prefix 00-02 - Local", "ch00-02.md"), + ]; + + let got = parser.parse_affix(true).unwrap(); + assert_eq!(got, should_be); + + // ---- Numbered Chapters ---- + + let new_numbered_item = |name, location, numbers: &[u32], nested_items| { + SummaryItem::Link(Link { + name: String::from(name), + location: Some(PathBuf::from(location)), + number: Some(SectionNumber(numbers.to_vec())), + nested_items, + }) + }; + + let ch01_nested = vec![ + new_numbered_item("Ch 01-01 - Local", "ch01-01.md", &[1, 1], vec![]), + new_numbered_item("Ch 01-02 - Local", "ch01-02.md", &[1, 2], vec![]), + ]; + + let should_be = vec![ + new_numbered_item("Ch 01-00 - Local", "ch01-00.md", &[1], ch01_nested), + new_numbered_item("Ch 02-00 - Local", "ch02-00.md", &[2], vec![]), + ]; + let got = parser.parse_parts().unwrap(); + assert_eq!(got, should_be); + + // ---- Suffix Chapters ---- + + let should_be = vec![ + new_affix_item("Appendix A - Local", "appendix-01.md"), + new_affix_item("Appendix B - Local", "appendix-02.md"), + ]; + + let got = parser.parse_affix(false).unwrap(); + assert_eq!(got, should_be); + } +} |