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