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