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<'_> = SummaryParser::new(text: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: &mut Link) => 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: &mut SummaryItem in sections {
553 if let SummaryItem::Link(ref mut link: &mut Link) = *section {
554 if let Some(ref mut number: &mut SectionNumber) = 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 eventsimpl Iterator
581 .into_iter()
582 .filter_map(|t: Event<'_>| match t {
583 Event::Text(text: CowStr<'_>) | Event::Code(text: CowStr<'_>) => 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: &u32 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}
1099