mdbook/book/
summary.rs

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