1use anyhow::{Context, Error, Result, bail};
8pub use mdbook_core::book::SectionNumber;
9use memchr::Memchr;
10use pulldown_cmark::{DefaultBrokenLinkCallback, Event, HeadingLevel, Tag, TagEnd};
11use serde::{Deserialize, Serialize};
12use std::collections::HashSet;
13use std::fmt::Display;
14use std::path::{Path, PathBuf};
15use tracing::{debug, trace, warn};
16
17pub fn parse_summary(summary: &str) -> Result<Summary> {
60 let parser = SummaryParser::new(summary);
61 parser.parse()
62}
63
64#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
66#[non_exhaustive]
67pub struct Summary {
68 pub title: Option<String>,
70 pub prefix_chapters: Vec<SummaryItem>,
72 pub numbered_chapters: Vec<SummaryItem>,
74 pub suffix_chapters: Vec<SummaryItem>,
76}
77
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
83#[non_exhaustive]
84pub struct Link {
85 pub name: String,
87 pub location: Option<PathBuf>,
90 pub number: Option<SectionNumber>,
92 pub nested_items: Vec<SummaryItem>,
94}
95
96impl Link {
97 pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link {
99 Link {
100 name: name.into(),
101 location: Some(location.as_ref().to_path_buf()),
102 number: None,
103 nested_items: Vec::new(),
104 }
105 }
106}
107
108impl Default for Link {
109 fn default() -> Self {
110 Link {
111 name: String::new(),
112 location: Some(PathBuf::new()),
113 number: None,
114 nested_items: Vec::new(),
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
121#[non_exhaustive]
122pub enum SummaryItem {
123 Link(Link),
125 Separator,
127 PartTitle(String),
129}
130
131impl SummaryItem {
132 fn maybe_link_mut(&mut self) -> Option<&mut Link> {
133 match *self {
134 SummaryItem::Link(ref mut l) => Some(l),
135 _ => None,
136 }
137 }
138}
139
140impl From<Link> for SummaryItem {
141 fn from(other: Link) -> SummaryItem {
142 SummaryItem::Link(other)
143 }
144}
145
146struct SummaryParser<'a> {
174 src: &'a str,
175 stream: pulldown_cmark::OffsetIter<'a, DefaultBrokenLinkCallback>,
176 offset: usize,
177
178 back: Option<Event<'a>>,
181}
182
183macro_rules! collect_events {
191 ($stream:expr,start $delimiter:pat) => {
192 collect_events!($stream, Event::Start($delimiter))
193 };
194 ($stream:expr,end $delimiter:pat) => {
195 collect_events!($stream, Event::End($delimiter))
196 };
197 ($stream:expr, $delimiter:pat) => {{
198 let mut events = Vec::new();
199
200 loop {
201 let event = $stream.next().map(|(ev, _range)| ev);
202 trace!("Next event: {:?}", event);
203
204 match event {
205 Some($delimiter) => break,
206 Some(other) => events.push(other),
207 None => {
208 debug!(
209 "Reached end of stream without finding the closing pattern, {}",
210 stringify!($delimiter)
211 );
212 break;
213 }
214 }
215 }
216
217 events
218 }};
219}
220
221impl<'a> SummaryParser<'a> {
222 fn new(text: &'a str) -> SummaryParser<'a> {
223 let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter();
224
225 SummaryParser {
226 src: text,
227 stream: pulldown_parser,
228 offset: 0,
229 back: None,
230 }
231 }
232
233 fn current_location(&self) -> (usize, usize) {
236 let previous_text = self.src[..self.offset].as_bytes();
237 let line = Memchr::new(b'\n', previous_text).count() + 1;
238 let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0);
239 let col = self.src[start_of_line..self.offset].chars().count();
240
241 (line, col)
242 }
243
244 fn parse(mut self) -> Result<Summary> {
246 let title = self.parse_title();
247
248 let prefix_chapters = self
249 .parse_affix(true)
250 .with_context(|| "There was an error parsing the prefix chapters")?;
251 let numbered_chapters = self
252 .parse_parts()
253 .with_context(|| "There was an error parsing the numbered chapters")?;
254 let suffix_chapters = self
255 .parse_affix(false)
256 .with_context(|| "There was an error parsing the suffix chapters")?;
257
258 let mut files = HashSet::new();
259 for part in [&prefix_chapters, &numbered_chapters, &suffix_chapters] {
260 Self::check_for_duplicates(&part, &mut files)?;
261 }
262
263 Ok(Summary {
264 title,
265 prefix_chapters,
266 numbered_chapters,
267 suffix_chapters,
268 })
269 }
270
271 fn check_for_duplicates<'b>(
273 items: &'b [SummaryItem],
274 files: &mut HashSet<&'b PathBuf>,
275 ) -> Result<()> {
276 for item in items {
277 if let SummaryItem::Link(link) = item {
278 if let Some(location) = &link.location {
279 if !files.insert(location) {
280 bail!(anyhow::anyhow!(
281 "Duplicate file in SUMMARY.md: {:?}",
282 location
283 ));
284 }
285 }
286 Self::check_for_duplicates(&link.nested_items, files)?;
288 }
289 }
290 Ok(())
291 }
292
293 fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
295 let mut items = Vec::new();
296 debug!(
297 "Parsing {} items",
298 if is_prefix { "prefix" } else { "suffix" }
299 );
300
301 loop {
302 match self.next_event() {
303 Some(ev @ Event::Start(Tag::List(..)))
304 | Some(
305 ev @ Event::Start(Tag::Heading {
306 level: HeadingLevel::H1,
307 ..
308 }),
309 ) => {
310 if is_prefix {
311 self.back(ev);
314 break;
315 } else {
316 bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
317 }
318 }
319 Some(Event::Start(Tag::Link { dest_url, .. })) => {
320 let link = self.parse_link(dest_url.to_string());
321 items.push(SummaryItem::Link(link));
322 }
323 Some(Event::Rule) => items.push(SummaryItem::Separator),
324 Some(_) => {}
325 None => break,
326 }
327 }
328
329 Ok(items)
330 }
331
332 fn parse_parts(&mut self) -> Result<Vec<SummaryItem>> {
333 let mut parts = vec![];
334
335 let mut root_number = SectionNumber::default();
337 let mut root_items = 0;
338
339 loop {
340 let title = match self.next_event() {
342 Some(ev @ Event::Start(Tag::Paragraph)) => {
343 self.back(ev);
345 break;
346 }
347
348 Some(Event::Start(Tag::Heading {
349 level: HeadingLevel::H1,
350 ..
351 })) => {
352 debug!("Found a h1 in the SUMMARY");
353
354 let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
355 Some(stringify_events(tags))
356 }
357
358 Some(ev) => {
359 self.back(ev);
360 None
361 }
362
363 None => break, };
365
366 let numbered_chapters = self
368 .parse_numbered(&mut root_items, &mut root_number)
369 .with_context(|| "There was an error parsing the numbered chapters")?;
370
371 if let Some(title) = title {
372 parts.push(SummaryItem::PartTitle(title));
373 }
374 parts.extend(numbered_chapters);
375 }
376
377 Ok(parts)
378 }
379
380 fn parse_link(&mut self, href: String) -> Link {
382 let href = href.replace("%20", " ");
383 let link_content = collect_events!(self.stream, end TagEnd::Link);
384 let name = stringify_events(link_content);
385
386 let path = if href.is_empty() {
387 None
388 } else {
389 Some(PathBuf::from(href))
390 };
391
392 Link {
393 name,
394 location: path,
395 number: None,
396 nested_items: Vec::new(),
397 }
398 }
399
400 fn parse_numbered(
402 &mut self,
403 root_items: &mut u32,
404 root_number: &mut SectionNumber,
405 ) -> Result<Vec<SummaryItem>> {
406 let mut items = Vec::new();
407
408 let mut first = true;
412
413 loop {
414 match self.next_event() {
415 Some(ev @ Event::Start(Tag::Paragraph)) => {
416 if !first {
417 self.back(ev);
419 break;
420 }
421 }
422 Some(
425 ev @ Event::Start(Tag::Heading {
426 level: HeadingLevel::H1,
427 ..
428 }),
429 ) => {
430 self.back(ev);
432 break;
433 }
434 Some(ev @ Event::Start(Tag::List(..))) => {
435 self.back(ev);
436 let mut bunch_of_items = self.parse_nested_numbered(root_number)?;
437
438 update_section_numbers(&mut bunch_of_items, 0, *root_items);
442 *root_items += bunch_of_items.len() as u32;
443 items.extend(bunch_of_items);
444 }
445 Some(Event::Start(other_tag)) => {
446 trace!("Skipping contents of {:?}", other_tag);
447
448 while let Some(event) = self.next_event() {
450 if event == Event::End(other_tag.clone().into()) {
451 break;
452 }
453 }
454 }
455 Some(Event::Rule) => {
456 items.push(SummaryItem::Separator);
457 }
458
459 Some(_) => {}
461
462 None => {
464 break;
465 }
466 }
467
468 first = false;
470 }
471
472 Ok(items)
473 }
474
475 fn back(&mut self, ev: Event<'a>) {
477 assert!(self.back.is_none());
478 trace!("Back: {:?}", ev);
479 self.back = Some(ev);
480 }
481
482 fn next_event(&mut self) -> Option<Event<'a>> {
483 let next = self.back.take().or_else(|| {
484 self.stream.next().map(|(ev, range)| {
485 self.offset = range.start;
486 ev
487 })
488 });
489
490 trace!("Next event: {:?}", next);
491
492 next
493 }
494
495 fn parse_nested_numbered(&mut self, parent: &SectionNumber) -> Result<Vec<SummaryItem>> {
496 debug!("Parsing numbered chapters at level {}", parent);
497 let mut items = Vec::new();
498
499 loop {
500 match self.next_event() {
501 Some(Event::Start(Tag::Item)) => {
502 let item = self.parse_nested_item(parent, items.len())?;
503 items.push(item);
504 }
505 Some(Event::Start(Tag::List(..))) => {
506 if items.is_empty() {
508 continue;
509 }
510 let (_, last_item) = get_last_link(&mut items)?;
512 let last_item_number = last_item
513 .number
514 .as_ref()
515 .expect("All numbered chapters have numbers");
516
517 let sub_items = self.parse_nested_numbered(last_item_number)?;
518
519 last_item.nested_items = sub_items;
520 }
521 Some(Event::End(TagEnd::List(..))) => break,
522 Some(_) => {}
523 None => break,
524 }
525 }
526
527 Ok(items)
528 }
529
530 fn parse_nested_item(
531 &mut self,
532 parent: &SectionNumber,
533 num_existing_items: usize,
534 ) -> Result<SummaryItem> {
535 loop {
536 match self.next_event() {
537 Some(Event::Start(Tag::Paragraph)) => continue,
538 Some(Event::Start(Tag::Link { dest_url, .. })) => {
539 let mut link = self.parse_link(dest_url.to_string());
540
541 let mut number = parent.clone();
542 number.push(num_existing_items as u32 + 1);
543 trace!(
544 "Found chapter: {} {} ({})",
545 number,
546 link.name,
547 link.location
548 .as_ref()
549 .map(|p| p.to_str().unwrap_or(""))
550 .unwrap_or("[draft]")
551 );
552
553 link.number = Some(number);
554
555 return Ok(SummaryItem::Link(link));
556 }
557 other => {
558 warn!("Expected a start of a link, actually got {:?}", other);
559 bail!(self.parse_error(
560 "The link items for nested chapters must only contain a hyperlink"
561 ));
562 }
563 }
564 }
565 }
566
567 fn parse_error<D: Display>(&self, msg: D) -> Error {
568 let (line, col) = self.current_location();
569 anyhow::anyhow!(
570 "failed to parse SUMMARY.md line {}, column {}: {}",
571 line,
572 col,
573 msg
574 )
575 }
576
577 fn parse_title(&mut self) -> Option<String> {
579 loop {
580 match self.next_event() {
581 Some(Event::Start(Tag::Heading {
582 level: HeadingLevel::H1,
583 ..
584 })) => {
585 debug!("Found a h1 in the SUMMARY");
586
587 let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
588 return Some(stringify_events(tags));
589 }
590 Some(Event::Html(_) | Event::InlineHtml(_))
592 | Some(Event::Start(Tag::HtmlBlock) | Event::End(TagEnd::HtmlBlock)) => {}
593 Some(ev) => {
595 self.back(ev);
596 return None;
597 }
598 _ => return None,
599 }
600 }
601 }
602}
603
604fn update_section_numbers(items: &mut [SummaryItem], level: usize, by: u32) {
605 for item in items {
606 if let SummaryItem::Link(ref mut link) = *item {
607 if let Some(ref mut number) = link.number {
608 number[level] += by;
609 }
610
611 update_section_numbers(&mut link.nested_items, level, by);
612 }
613 }
614}
615
616fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
619 links
620 .iter_mut()
621 .enumerate()
622 .filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
623 .next_back()
624 .ok_or_else(|| {
625 anyhow::anyhow!(
626 "Unable to get last link because the list of SummaryItems \
627 doesn't contain any Links"
628 )
629 })
630}
631
632fn stringify_events(events: Vec<Event<'_>>) -> String {
635 events
636 .into_iter()
637 .filter_map(|t| match t {
638 Event::Text(text) | Event::Code(text) => Some(text.into_string()),
639 Event::SoftBreak => Some(String::from(" ")),
640 _ => None,
641 })
642 .collect()
643}
644
645#[cfg(test)]
646mod tests {
647 use super::*;
648
649 #[test]
650 fn parse_initial_title() {
651 let src = "# Summary";
652 let should_be = String::from("Summary");
653
654 let mut parser = SummaryParser::new(src);
655 let got = parser.parse_title().unwrap();
656
657 assert_eq!(got, should_be);
658 }
659
660 #[test]
661 fn no_initial_title() {
662 let src = "[Link]()";
663 let mut parser = SummaryParser::new(src);
664
665 assert!(parser.parse_title().is_none());
666 assert!(matches!(
667 parser.next_event(),
668 Some(Event::Start(Tag::Paragraph))
669 ));
670 }
671
672 #[test]
673 fn parse_title_with_styling() {
674 let src = "# My **Awesome** Summary";
675 let should_be = String::from("My Awesome Summary");
676
677 let mut parser = SummaryParser::new(src);
678 let got = parser.parse_title().unwrap();
679
680 assert_eq!(got, should_be);
681 }
682
683 #[test]
684 fn convert_markdown_events_to_a_string() {
685 let src = "Hello *World*, `this` is some text [and a link](./path/to/link)";
686 let should_be = "Hello World, this is some text and a link";
687
688 let events = pulldown_cmark::Parser::new(src).collect();
689 let got = stringify_events(events);
690
691 assert_eq!(got, should_be);
692 }
693
694 #[test]
695 fn parse_some_prefix_items() {
696 let src = "[First](./first.md)\n[Second](./second.md)\n";
697 let mut parser = SummaryParser::new(src);
698
699 let should_be = vec![
700 SummaryItem::Link(Link {
701 name: String::from("First"),
702 location: Some(PathBuf::from("./first.md")),
703 ..Default::default()
704 }),
705 SummaryItem::Link(Link {
706 name: String::from("Second"),
707 location: Some(PathBuf::from("./second.md")),
708 ..Default::default()
709 }),
710 ];
711
712 let got = parser.parse_affix(true).unwrap();
713
714 assert_eq!(got, should_be);
715 }
716
717 #[test]
718 fn parse_prefix_items_with_a_separator() {
719 let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n";
720 let mut parser = SummaryParser::new(src);
721
722 let got = parser.parse_affix(true).unwrap();
723
724 assert_eq!(got.len(), 3);
725 assert_eq!(got[1], SummaryItem::Separator);
726 }
727
728 #[test]
729 fn suffix_items_cannot_be_followed_by_a_list() {
730 let src = "[First](./first.md)\n- [Second](./second.md)\n";
731 let mut parser = SummaryParser::new(src);
732
733 let got = parser.parse_affix(false);
734
735 assert!(got.is_err());
736 let error_message = got.err().unwrap().to_string();
737 assert_eq!(
738 error_message,
739 "failed to parse SUMMARY.md line 2, column 1: Suffix chapters cannot be followed by a list"
740 );
741 }
742
743 #[test]
744 fn expected_a_start_of_a_link() {
745 let src = "- Title\n";
746 let mut parser = SummaryParser::new(src);
747
748 let got = parser.parse_affix(false);
749
750 assert!(got.is_err());
751 let error_message = got.err().unwrap().to_string();
752 assert_eq!(
753 error_message,
754 "failed to parse SUMMARY.md line 1, column 0: Suffix chapters cannot be followed by a list"
755 );
756 }
757
758 #[test]
759 fn parse_a_link() {
760 let src = "[First](./first.md)";
761 let should_be = Link {
762 name: String::from("First"),
763 location: Some(PathBuf::from("./first.md")),
764 ..Default::default()
765 };
766
767 let mut parser = SummaryParser::new(src);
768 let _ = parser.stream.next(); let href = match parser.stream.next() {
771 Some((Event::Start(Tag::Link { dest_url, .. }), _range)) => dest_url.to_string(),
772 other => panic!("Unreachable, {other:?}"),
773 };
774
775 let got = parser.parse_link(href);
776 assert_eq!(got, should_be);
777 }
778
779 #[test]
780 fn parse_a_numbered_chapter() {
781 let src = "- [First](./first.md)\n";
782 let link = Link {
783 name: String::from("First"),
784 location: Some(PathBuf::from("./first.md")),
785 number: Some(SectionNumber::new([1])),
786 ..Default::default()
787 };
788 let should_be = vec![SummaryItem::Link(link)];
789
790 let mut parser = SummaryParser::new(src);
791 let got = parser
792 .parse_numbered(&mut 0, &mut SectionNumber::default())
793 .unwrap();
794
795 assert_eq!(got, should_be);
796 }
797
798 #[test]
799 fn parse_nested_numbered_chapters() {
800 let src = "- [First](./first.md)\n - [Nested](./nested.md)\n- [Second](./second.md)";
801
802 let should_be = vec![
803 SummaryItem::Link(Link {
804 name: String::from("First"),
805 location: Some(PathBuf::from("./first.md")),
806 number: Some(SectionNumber::new([1])),
807 nested_items: vec![SummaryItem::Link(Link {
808 name: String::from("Nested"),
809 location: Some(PathBuf::from("./nested.md")),
810 number: Some(SectionNumber::new([1, 1])),
811 nested_items: Vec::new(),
812 })],
813 }),
814 SummaryItem::Link(Link {
815 name: String::from("Second"),
816 location: Some(PathBuf::from("./second.md")),
817 number: Some(SectionNumber::new([2])),
818 nested_items: Vec::new(),
819 }),
820 ];
821
822 let mut parser = SummaryParser::new(src);
823 let got = parser
824 .parse_numbered(&mut 0, &mut SectionNumber::default())
825 .unwrap();
826
827 assert_eq!(got, should_be);
828 }
829
830 #[test]
831 fn parse_numbered_chapters_separated_by_comment() {
832 let src = "- [First](./first.md)\n<!-- this is a comment -->\n- [Second](./second.md)";
833
834 let should_be = vec![
835 SummaryItem::Link(Link {
836 name: String::from("First"),
837 location: Some(PathBuf::from("./first.md")),
838 number: Some(SectionNumber::new([1])),
839 nested_items: Vec::new(),
840 }),
841 SummaryItem::Link(Link {
842 name: String::from("Second"),
843 location: Some(PathBuf::from("./second.md")),
844 number: Some(SectionNumber::new([2])),
845 nested_items: Vec::new(),
846 }),
847 ];
848
849 let mut parser = SummaryParser::new(src);
850 let got = parser
851 .parse_numbered(&mut 0, &mut SectionNumber::default())
852 .unwrap();
853
854 assert_eq!(got, should_be);
855 }
856
857 #[test]
858 fn parse_titled_parts() {
859 let src = "- [First](./first.md)\n- [Second](./second.md)\n\
860 # Title 2\n- [Third](./third.md)\n\t- [Fourth](./fourth.md)";
861
862 let should_be = vec![
863 SummaryItem::Link(Link {
864 name: String::from("First"),
865 location: Some(PathBuf::from("./first.md")),
866 number: Some(SectionNumber::new([1])),
867 nested_items: Vec::new(),
868 }),
869 SummaryItem::Link(Link {
870 name: String::from("Second"),
871 location: Some(PathBuf::from("./second.md")),
872 number: Some(SectionNumber::new([2])),
873 nested_items: Vec::new(),
874 }),
875 SummaryItem::PartTitle(String::from("Title 2")),
876 SummaryItem::Link(Link {
877 name: String::from("Third"),
878 location: Some(PathBuf::from("./third.md")),
879 number: Some(SectionNumber::new([3])),
880 nested_items: vec![SummaryItem::Link(Link {
881 name: String::from("Fourth"),
882 location: Some(PathBuf::from("./fourth.md")),
883 number: Some(SectionNumber::new([3, 1])),
884 nested_items: Vec::new(),
885 })],
886 }),
887 ];
888
889 let mut parser = SummaryParser::new(src);
890 let got = parser.parse_parts().unwrap();
891
892 assert_eq!(got, should_be);
893 }
894
895 #[test]
900 fn can_have_a_subheader_between_nested_items() {
901 let src = "- [First](./first.md)\n\n## Subheading\n\n- [Second](./second.md)\n";
902 let should_be = vec![
903 SummaryItem::Link(Link {
904 name: String::from("First"),
905 location: Some(PathBuf::from("./first.md")),
906 number: Some(SectionNumber::new([1])),
907 nested_items: Vec::new(),
908 }),
909 SummaryItem::Link(Link {
910 name: String::from("Second"),
911 location: Some(PathBuf::from("./second.md")),
912 number: Some(SectionNumber::new([2])),
913 nested_items: Vec::new(),
914 }),
915 ];
916
917 let mut parser = SummaryParser::new(src);
918 let got = parser
919 .parse_numbered(&mut 0, &mut SectionNumber::default())
920 .unwrap();
921
922 assert_eq!(got, should_be);
923 }
924
925 #[test]
926 fn an_empty_link_location_is_a_draft_chapter() {
927 let src = "- [Empty]()\n";
928 let mut parser = SummaryParser::new(src);
929
930 let got = parser.parse_numbered(&mut 0, &mut SectionNumber::default());
931 let should_be = vec![SummaryItem::Link(Link {
932 name: String::from("Empty"),
933 location: None,
934 number: Some(SectionNumber::new([1])),
935 nested_items: Vec::new(),
936 })];
937
938 assert!(got.is_ok());
939 assert_eq!(got.unwrap(), should_be);
940 }
941
942 #[test]
945 fn keep_numbering_after_separator() {
946 let src =
947 "- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
948 let should_be = vec![
949 SummaryItem::Link(Link {
950 name: String::from("First"),
951 location: Some(PathBuf::from("./first.md")),
952 number: Some(SectionNumber::new([1])),
953 nested_items: Vec::new(),
954 }),
955 SummaryItem::Separator,
956 SummaryItem::Link(Link {
957 name: String::from("Second"),
958 location: Some(PathBuf::from("./second.md")),
959 number: Some(SectionNumber::new([2])),
960 nested_items: Vec::new(),
961 }),
962 SummaryItem::Separator,
963 SummaryItem::Link(Link {
964 name: String::from("Third"),
965 location: Some(PathBuf::from("./third.md")),
966 number: Some(SectionNumber::new([3])),
967 nested_items: Vec::new(),
968 }),
969 ];
970
971 let mut parser = SummaryParser::new(src);
972 let got = parser
973 .parse_numbered(&mut 0, &mut SectionNumber::default())
974 .unwrap();
975
976 assert_eq!(got, should_be);
977 }
978
979 #[test]
982 fn add_space_for_multi_line_chapter_names() {
983 let src = "- [Chapter\ntitle](./chapter.md)";
984 let should_be = vec![SummaryItem::Link(Link {
985 name: String::from("Chapter title"),
986 location: Some(PathBuf::from("./chapter.md")),
987 number: Some(SectionNumber::new([1])),
988 nested_items: Vec::new(),
989 })];
990
991 let mut parser = SummaryParser::new(src);
992 let got = parser
993 .parse_numbered(&mut 0, &mut SectionNumber::default())
994 .unwrap();
995
996 assert_eq!(got, should_be);
997 }
998
999 #[test]
1000 fn allow_space_in_link_destination() {
1001 let src = "- [test1](./test%20link1.md)\n- [test2](<./test link2.md>)";
1002 let should_be = vec![
1003 SummaryItem::Link(Link {
1004 name: String::from("test1"),
1005 location: Some(PathBuf::from("./test link1.md")),
1006 number: Some(SectionNumber::new([1])),
1007 nested_items: Vec::new(),
1008 }),
1009 SummaryItem::Link(Link {
1010 name: String::from("test2"),
1011 location: Some(PathBuf::from("./test link2.md")),
1012 number: Some(SectionNumber::new([2])),
1013 nested_items: Vec::new(),
1014 }),
1015 ];
1016 let mut parser = SummaryParser::new(src);
1017 let got = parser
1018 .parse_numbered(&mut 0, &mut SectionNumber::default())
1019 .unwrap();
1020
1021 assert_eq!(got, should_be);
1022 }
1023
1024 #[test]
1025 fn skip_html_comments() {
1026 let src = r#"<!--
1027# Title - En
1028-->
1029# Title - Local
1030
1031<!--
1032[Prefix 00-01 - En](ch00-01.md)
1033[Prefix 00-02 - En](ch00-02.md)
1034-->
1035[Prefix 00-01 - Local](ch00-01.md)
1036[Prefix 00-02 - Local](ch00-02.md)
1037
1038<!--
1039## Section Title - En
1040-->
1041## Section Title - Localized
1042
1043<!--
1044- [Ch 01-00 - En](ch01-00.md)
1045 - [Ch 01-01 - En](ch01-01.md)
1046 - [Ch 01-02 - En](ch01-02.md)
1047-->
1048- [Ch 01-00 - Local](ch01-00.md)
1049 - [Ch 01-01 - Local](ch01-01.md)
1050 - [Ch 01-02 - Local](ch01-02.md)
1051
1052<!--
1053- [Ch 02-00 - En](ch02-00.md)
1054-->
1055- [Ch 02-00 - Local](ch02-00.md)
1056
1057<!--
1058[Appendix A - En](appendix-01.md)
1059[Appendix B - En](appendix-02.md)
1060-->`
1061[Appendix A - Local](appendix-01.md)
1062[Appendix B - Local](appendix-02.md)
1063"#;
1064
1065 let mut parser = SummaryParser::new(src);
1066
1067 let title = parser.parse_title();
1069 assert_eq!(title, Some(String::from("Title - Local")));
1070
1071 let new_affix_item = |name, location| {
1074 SummaryItem::Link(Link {
1075 name: String::from(name),
1076 location: Some(PathBuf::from(location)),
1077 ..Default::default()
1078 })
1079 };
1080
1081 let should_be = vec![
1082 new_affix_item("Prefix 00-01 - Local", "ch00-01.md"),
1083 new_affix_item("Prefix 00-02 - Local", "ch00-02.md"),
1084 ];
1085
1086 let got = parser.parse_affix(true).unwrap();
1087 assert_eq!(got, should_be);
1088
1089 let new_numbered_item = |name, location, numbers: &[u32], nested_items| {
1092 SummaryItem::Link(Link {
1093 name: String::from(name),
1094 location: Some(PathBuf::from(location)),
1095 number: Some(SectionNumber::new(numbers)),
1096 nested_items,
1097 })
1098 };
1099
1100 let ch01_nested = vec![
1101 new_numbered_item("Ch 01-01 - Local", "ch01-01.md", &[1, 1], vec![]),
1102 new_numbered_item("Ch 01-02 - Local", "ch01-02.md", &[1, 2], vec![]),
1103 ];
1104
1105 let should_be = vec![
1106 new_numbered_item("Ch 01-00 - Local", "ch01-00.md", &[1], ch01_nested),
1107 new_numbered_item("Ch 02-00 - Local", "ch02-00.md", &[2], vec![]),
1108 ];
1109 let got = parser.parse_parts().unwrap();
1110 assert_eq!(got, should_be);
1111
1112 let should_be = vec![
1115 new_affix_item("Appendix A - Local", "appendix-01.md"),
1116 new_affix_item("Appendix B - Local", "appendix-02.md"),
1117 ];
1118
1119 let got = parser.parse_affix(false).unwrap();
1120 assert_eq!(got, should_be);
1121 }
1122
1123 #[test]
1124 fn duplicate_entries_1() {
1125 let src = r#"
1126# Summary
1127- [A](./a.md)
1128- [A](./a.md)
1129"#;
1130
1131 let res = parse_summary(src);
1132 assert!(res.is_err());
1133 let error_message = res.err().unwrap().to_string();
1134 assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
1135 }
1136
1137 #[test]
1138 fn duplicate_entries_2() {
1139 let src = r#"
1140# Summary
1141- [A](./a.md)
1142 - [A](./a.md)
1143"#;
1144
1145 let res = parse_summary(src);
1146 assert!(res.is_err());
1147 let error_message = res.err().unwrap().to_string();
1148 assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
1149 }
1150 #[test]
1151 fn duplicate_entries_3() {
1152 let src = r#"
1153# Summary
1154- [A](./a.md)
1155- [B](./b.md)
1156 - [A](./a.md)
1157"#;
1158
1159 let res = parse_summary(src);
1160 assert!(res.is_err());
1161 let error_message = res.err().unwrap().to_string();
1162 assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
1163 }
1164
1165 #[test]
1166 fn duplicate_entries_4() {
1167 let src = r#"
1168# Summary
1169[A](./a.md)
1170- [B](./b.md)
1171- [A](./a.md)
1172"#;
1173
1174 let res = parse_summary(src);
1175 assert!(res.is_err());
1176 let error_message = res.err().unwrap().to_string();
1177 assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
1178 }
1179
1180 #[test]
1181 fn duplicate_entries_5() {
1182 let src = r#"
1183# Summary
1184[A](./a.md)
1185
1186# hi
1187- [B](./b.md)
1188
1189# bye
1190
1191---
1192
1193[A](./a.md)
1194"#;
1195
1196 let res = parse_summary(src);
1197 assert!(res.is_err());
1198 let error_message = res.err().unwrap().to_string();
1199 assert_eq!(error_message, r#"Duplicate file in SUMMARY.md: "./a.md""#);
1200 }
1201}