//! Summary parser for mdBook.
//!
//! This is used to parse the
//! [`SUMMARY.md`](https://rust-lang.github.io/mdBook/format/summary.html)
//! file structure for [mdBook](https://rust-lang.github.io/mdBook/).

use anyhow::{Context, Error, Result, bail};
pub use mdbook_core::book::SectionNumber;
use memchr::Memchr;
use pulldown_cmark::{DefaultBrokenLinkCallback, Event, HeadingLevel, Tag, TagEnd};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use tracing::{debug, trace, warn};

/// 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. However, there are some constraints. You cannot nest
/// prefix chapters; they should all be on the root level. And you cannot 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 collection 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)]
#[non_exhaustive]
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)]
#[non_exhaustive]
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`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
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, DefaultBrokenLinkCallback>,
    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: &'a str) -> SummaryParser<'a> {
        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")?;

        let mut files = HashSet::new();
        for part in [&prefix_chapters, &numbered_chapters, &suffix_chapters] {
            Self::check_for_duplicates(&part, &mut files)?;
        }

        Ok(Summary {
            title,
            prefix_chapters,
            numbered_chapters,
            suffix_chapters,
        })
    }

    /// Recursively check for duplicate files in the summary items.
    fn check_for_duplicates<'b>(
        items: &'b [SummaryItem],
        files: &mut HashSet<&'b PathBuf>,
    ) -> Result<()> {
        for item in items {
            if let SummaryItem::Link(link) = item {
                if let Some(location) = &link.location {
                    if !files.insert(location) {
                        bail!(anyhow::anyhow!(
                            "Duplicate file in SUMMARY.md: {:?}",
                            location
                        ));
                    }
                }
                // Recursively check nested items
                Self::check_for_duplicates(&link.nested_items, files)?;
            }
        }
        Ok(())
    }

    /// 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 {
                        level: 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 { dest_url, .. })) => {
                    let link = self.parse_link(dest_url.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 {
                    level: HeadingLevel::H1,
                    ..
                })) => {
                    debug!("Found a h1 in the SUMMARY");

                    let tags = collect_events!(self.stream, end TagEnd::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 TagEnd::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 {
                        level: 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().into()) {
                            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 because 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(TagEnd::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 { dest_url, .. })) => {
                    let mut link = self.parse_link(dest_url.to_string());

                    let mut number = parent.clone();
                    number.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 {
                    level: HeadingLevel::H1,
                    ..
                })) => {
                    debug!("Found a h1 in the SUMMARY");

                    let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
                    return Some(stringify_events(tags));
                }
                // Skip a HTML element such as a comment line.
                Some(Event::Html(_) | Event::InlineHtml(_))
                | Some(Event::Start(Tag::HtmlBlock) | Event::End(TagEnd::HtmlBlock)) => {}
                // Otherwise, no title.
                Some(ev) => {
                    self.back(ev);
                    return None;
                }
                _ => return None,
            }
        }
    }
}

fn update_section_numbers(items: &mut [SummaryItem], level: usize, by: u32) {
    for item in items {
        if let SummaryItem::Link(ref mut link) = *item {
            if let Some(ref mut number) = link.number {
                number[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)))
        .next_back()
        .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()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[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());
        let error_message = got.err().unwrap().to_string();
        assert_eq!(
            error_message,
            "failed to parse SUMMARY.md line 2, column 1: Suffix chapters cannot be followed by a list"
        );
    }

    #[test]
    fn expected_a_start_of_a_link() {
        let src = "- Title\n";
        let mut parser = SummaryParser::new(src);

        let got = parser.parse_affix(false);

        assert!(got.is_err());
        let error_message = got.err().unwrap().to_string();
        assert_eq!(
            error_message,
            "failed to parse SUMMARY.md line 1, column 0: Suffix chapters cannot be followed by a list"
        );
    }

    #[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 { dest_url, .. }), _range)) => dest_url.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::new([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::new([1])),
                nested_items: vec![SummaryItem::Link(Link {
                    name: String::from("Nested"),
                    location: Some(PathBuf::from("./nested.md")),
                    number: Some(SectionNumber::new([1, 1])),
                    nested_items: Vec::new(),
                })],
            }),
            SummaryItem::Link(Link {
                name: String::from("Second"),
                location: Some(PathBuf::from("./second.md")),
                number: Some(SectionNumber::new([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::new([1])),
                nested_items: Vec::new(),
            }),
            SummaryItem::Link(Link {
                name: String::from("Second"),
                location: Some(PathBuf::from("./second.md")),
                number: Some(SectionNumber::new([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::new([1])),
                nested_items: Vec::new(),
            }),
            SummaryItem::Link(Link {
                name: String::from("Second"),
                location: Some(PathBuf::from("./second.md")),
                number: Some(SectionNumber::new([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::new([3])),
                nested_items: vec![SummaryItem::Link(Link {
                    name: String::from("Fourth"),
                    location: Some(PathBuf::from("./fourth.md")),
                    number: Some(SectionNumber::new([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::new([1])),
                nested_items: Vec::new(),
            }),
            SummaryItem::Link(Link {
                name: String::from("Second"),
                location: Some(PathBuf::from("./second.md")),
                number: Some(SectionNumber::new([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::new([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::new([1])),
                nested_items: Vec::new(),
            }),
            SummaryItem::Separator,
            SummaryItem::Link(Link {
                name: String::from("Second"),
                location: Some(PathBuf::from("./second.md")),
                number: Some(SectionNumber::new([2])),
                nested_items: Vec::new(),
            }),
            SummaryItem::Separator,
            SummaryItem::Link(Link {
                name: String::from("Third"),
                location: Some(PathBuf::from("./third.md")),
                number: Some(SectionNumber::new([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::new([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::new([1])),
                nested_items: Vec::new(),
            }),
            SummaryItem::Link(Link {
                name: String::from("test2"),
                location: Some(PathBuf::from("./test link2.md")),
                number: Some(SectionNumber::new([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::new(numbers)),
                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);
    }

    #[test]
    fn duplicate_entries_1() {
        let src = r#"
# Summary
- [A](./a.md)
- [A](./a.md)
"#;

        let res = parse_summary(src);
        assert!(res.is_err());
        let error_message = res.err().unwrap().to_string();
        assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
    }

    #[test]
    fn duplicate_entries_2() {
        let src = r#"
# Summary
- [A](./a.md)
  - [A](./a.md)
"#;

        let res = parse_summary(src);
        assert!(res.is_err());
        let error_message = res.err().unwrap().to_string();
        assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
    }
    #[test]
    fn duplicate_entries_3() {
        let src = r#"
# Summary
- [A](./a.md)
- [B](./b.md)
  - [A](./a.md)
"#;

        let res = parse_summary(src);
        assert!(res.is_err());
        let error_message = res.err().unwrap().to_string();
        assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
    }

    #[test]
    fn duplicate_entries_4() {
        let src = r#"
# Summary
[A](./a.md)
- [B](./b.md)
- [A](./a.md)
"#;

        let res = parse_summary(src);
        assert!(res.is_err());
        let error_message = res.err().unwrap().to_string();
        assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
    }

    #[test]
    fn duplicate_entries_5() {
        let src = r#"
# Summary
[A](./a.md)

# hi
- [B](./b.md)

# bye

---

[A](./a.md)
"#;

        let res = parse_summary(src);
        assert!(res.is_err());
        let error_message = res.err().unwrap().to_string();
        assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
    }
}
