1 use std::collections::VecDeque;
2 use std::fmt::{self, Display, Formatter};
3 use std::fs::{self, File};
4 use std::io::{Read, Write};
5 use std::path::{Path, PathBuf};
6 
7 use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
8 use crate::config::BuildConfig;
9 use crate::errors::*;
10 
11 /// Load a book into memory from its `src/` directory.
load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book>12 pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
13     let src_dir = src_dir.as_ref();
14     let summary_md = src_dir.join("SUMMARY.md");
15 
16     let mut summary_content = String::new();
17     File::open(&summary_md)
18         .with_context(|| format!("Couldn't open SUMMARY.md in {:?} directory", src_dir))?
19         .read_to_string(&mut summary_content)?;
20 
21     let summary = parse_summary(&summary_content)
22         .with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
23 
24     if cfg.create_missing {
25         create_missing(src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
26     }
27 
28     load_book_from_disk(&summary, src_dir)
29 }
30 
create_missing(src_dir: &Path, summary: &Summary) -> Result<()>31 fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
32     let mut items: Vec<_> = summary
33         .prefix_chapters
34         .iter()
35         .chain(summary.numbered_chapters.iter())
36         .chain(summary.suffix_chapters.iter())
37         .collect();
38 
39     while !items.is_empty() {
40         let next = items.pop().expect("already checked");
41 
42         if let SummaryItem::Link(ref link) = *next {
43             if let Some(ref location) = link.location {
44                 let filename = src_dir.join(location);
45                 if !filename.exists() {
46                     if let Some(parent) = filename.parent() {
47                         if !parent.exists() {
48                             fs::create_dir_all(parent)?;
49                         }
50                     }
51                     debug!("Creating missing file {}", filename.display());
52 
53                     let mut f = File::create(&filename).with_context(|| {
54                         format!("Unable to create missing file: {}", filename.display())
55                     })?;
56                     writeln!(f, "# {}", link.name)?;
57                 }
58             }
59 
60             items.extend(&link.nested_items);
61         }
62     }
63 
64     Ok(())
65 }
66 
67 /// A dumb tree structure representing a book.
68 ///
69 /// For the moment a book is just a collection of [`BookItems`] which are
70 /// accessible by either iterating (immutably) over the book with [`iter()`], or
71 /// recursively applying a closure to each section to mutate the chapters, using
72 /// [`for_each_mut()`].
73 ///
74 /// [`iter()`]: #method.iter
75 /// [`for_each_mut()`]: #method.for_each_mut
76 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
77 pub struct Book {
78     /// The sections in this book.
79     pub sections: Vec<BookItem>,
80     __non_exhaustive: (),
81 }
82 
83 impl Book {
84     /// Create an empty book.
new() -> Self85     pub fn new() -> Self {
86         Default::default()
87     }
88 
89     /// Get a depth-first iterator over the items in the book.
iter(&self) -> BookItems<'_>90     pub fn iter(&self) -> BookItems<'_> {
91         BookItems {
92             items: self.sections.iter().collect(),
93         }
94     }
95 
96     /// Recursively apply a closure to each item in the book, allowing you to
97     /// mutate them.
98     ///
99     /// # Note
100     ///
101     /// Unlike the `iter()` method, this requires a closure instead of returning
102     /// an iterator. This is because using iterators can possibly allow you
103     /// to have iterator invalidation errors.
for_each_mut<F>(&mut self, mut func: F) where F: FnMut(&mut BookItem),104     pub fn for_each_mut<F>(&mut self, mut func: F)
105     where
106         F: FnMut(&mut BookItem),
107     {
108         for_each_mut(&mut func, &mut self.sections);
109     }
110 
111     /// Append a `BookItem` to the `Book`.
push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self112     pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
113         self.sections.push(item.into());
114         self
115     }
116 }
117 
for_each_mut<'a, F, I>(func: &mut F, items: I) where F: FnMut(&mut BookItem), I: IntoIterator<Item = &'a mut BookItem>,118 pub fn for_each_mut<'a, F, I>(func: &mut F, items: I)
119 where
120     F: FnMut(&mut BookItem),
121     I: IntoIterator<Item = &'a mut BookItem>,
122 {
123     for item in items {
124         if let BookItem::Chapter(ch) = item {
125             for_each_mut(func, &mut ch.sub_items);
126         }
127 
128         func(item);
129     }
130 }
131 
132 /// Enum representing any type of item which can be added to a book.
133 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
134 pub enum BookItem {
135     /// A nested chapter.
136     Chapter(Chapter),
137     /// A section separator.
138     Separator,
139     /// A part title.
140     PartTitle(String),
141 }
142 
143 impl From<Chapter> for BookItem {
from(other: Chapter) -> BookItem144     fn from(other: Chapter) -> BookItem {
145         BookItem::Chapter(other)
146     }
147 }
148 
149 /// The representation of a "chapter", usually mapping to a single file on
150 /// disk however it may contain multiple sub-chapters.
151 #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
152 pub struct Chapter {
153     /// The chapter's name.
154     pub name: String,
155     /// The chapter's contents.
156     pub content: String,
157     /// The chapter's section number, if it has one.
158     pub number: Option<SectionNumber>,
159     /// Nested items.
160     pub sub_items: Vec<BookItem>,
161     /// The chapter's location, relative to the `SUMMARY.md` file.
162     pub path: Option<PathBuf>,
163     /// The chapter's source file, relative to the `SUMMARY.md` file.
164     pub source_path: Option<PathBuf>,
165     /// An ordered list of the names of each chapter above this one in the hierarchy.
166     pub parent_names: Vec<String>,
167 }
168 
169 impl Chapter {
170     /// Create a new chapter with the provided content.
new<P: Into<PathBuf>>( name: &str, content: String, p: P, parent_names: Vec<String>, ) -> Chapter171     pub fn new<P: Into<PathBuf>>(
172         name: &str,
173         content: String,
174         p: P,
175         parent_names: Vec<String>,
176     ) -> Chapter {
177         let path: PathBuf = p.into();
178         Chapter {
179             name: name.to_string(),
180             content,
181             path: Some(path.clone()),
182             source_path: Some(path),
183             parent_names,
184             ..Default::default()
185         }
186     }
187 
188     /// Create a new draft chapter that is not attached to a source markdown file (and thus
189     /// has no content).
new_draft(name: &str, parent_names: Vec<String>) -> Self190     pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
191         Chapter {
192             name: name.to_string(),
193             content: String::new(),
194             path: None,
195             source_path: None,
196             parent_names,
197             ..Default::default()
198         }
199     }
200 
201     /// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
is_draft_chapter(&self) -> bool202     pub fn is_draft_chapter(&self) -> bool {
203         self.path.is_none()
204     }
205 }
206 
207 /// Use the provided `Summary` to load a `Book` from disk.
208 ///
209 /// You need to pass in the book's source directory because all the links in
210 /// `SUMMARY.md` give the chapter locations relative to it.
load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book>211 pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
212     debug!("Loading the book from disk");
213     let src_dir = src_dir.as_ref();
214 
215     let prefix = summary.prefix_chapters.iter();
216     let numbered = summary.numbered_chapters.iter();
217     let suffix = summary.suffix_chapters.iter();
218 
219     let summary_items = prefix.chain(numbered).chain(suffix);
220 
221     let mut chapters = Vec::new();
222 
223     for summary_item in summary_items {
224         let chapter = load_summary_item(summary_item, src_dir, Vec::new())?;
225         chapters.push(chapter);
226     }
227 
228     Ok(Book {
229         sections: chapters,
230         __non_exhaustive: (),
231     })
232 }
233 
load_summary_item<P: AsRef<Path> + Clone>( item: &SummaryItem, src_dir: P, parent_names: Vec<String>, ) -> Result<BookItem>234 fn load_summary_item<P: AsRef<Path> + Clone>(
235     item: &SummaryItem,
236     src_dir: P,
237     parent_names: Vec<String>,
238 ) -> Result<BookItem> {
239     match item {
240         SummaryItem::Separator => Ok(BookItem::Separator),
241         SummaryItem::Link(ref link) => {
242             load_chapter(link, src_dir, parent_names).map(BookItem::Chapter)
243         }
244         SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
245     }
246 }
247 
load_chapter<P: AsRef<Path>>( link: &Link, src_dir: P, parent_names: Vec<String>, ) -> Result<Chapter>248 fn load_chapter<P: AsRef<Path>>(
249     link: &Link,
250     src_dir: P,
251     parent_names: Vec<String>,
252 ) -> Result<Chapter> {
253     let src_dir = src_dir.as_ref();
254 
255     let mut ch = if let Some(ref link_location) = link.location {
256         debug!("Loading {} ({})", link.name, link_location.display());
257 
258         let location = if link_location.is_absolute() {
259             link_location.clone()
260         } else {
261             src_dir.join(link_location)
262         };
263 
264         let mut f = File::open(&location)
265             .with_context(|| format!("Chapter file not found, {}", link_location.display()))?;
266 
267         let mut content = String::new();
268         f.read_to_string(&mut content).with_context(|| {
269             format!("Unable to read \"{}\" ({})", link.name, location.display())
270         })?;
271 
272         if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
273             content.replace_range(..3, "");
274         }
275 
276         let stripped = location
277             .strip_prefix(&src_dir)
278             .expect("Chapters are always inside a book");
279 
280         Chapter::new(&link.name, content, stripped, parent_names.clone())
281     } else {
282         Chapter::new_draft(&link.name, parent_names.clone())
283     };
284 
285     let mut sub_item_parents = parent_names;
286 
287     ch.number = link.number.clone();
288 
289     sub_item_parents.push(link.name.clone());
290     let sub_items = link
291         .nested_items
292         .iter()
293         .map(|i| load_summary_item(i, src_dir, sub_item_parents.clone()))
294         .collect::<Result<Vec<_>>>()?;
295 
296     ch.sub_items = sub_items;
297 
298     Ok(ch)
299 }
300 
301 /// A depth-first iterator over the items in a book.
302 ///
303 /// # Note
304 ///
305 /// This struct shouldn't be created directly, instead prefer the
306 /// [`Book::iter()`] method.
307 pub struct BookItems<'a> {
308     items: VecDeque<&'a BookItem>,
309 }
310 
311 impl<'a> Iterator for BookItems<'a> {
312     type Item = &'a BookItem;
313 
next(&mut self) -> Option<Self::Item>314     fn next(&mut self) -> Option<Self::Item> {
315         let item = self.items.pop_front();
316 
317         if let Some(&BookItem::Chapter(ref ch)) = item {
318             // if we wanted a breadth-first iterator we'd `extend()` here
319             for sub_item in ch.sub_items.iter().rev() {
320                 self.items.push_front(sub_item);
321             }
322         }
323 
324         item
325     }
326 }
327 
328 impl Display for Chapter {
fmt(&self, f: &mut Formatter<'_>) -> fmt::Result329     fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
330         if let Some(ref section_number) = self.number {
331             write!(f, "{} ", section_number)?;
332         }
333 
334         write!(f, "{}", self.name)
335     }
336 }
337 
338 #[cfg(test)]
339 mod tests {
340     use super::*;
341     use std::io::Write;
342     use tempfile::{Builder as TempFileBuilder, TempDir};
343 
344     const DUMMY_SRC: &str = "
345 # Dummy Chapter
346 
347 this is some dummy text.
348 
349 And here is some \
350                                      more text.
351 ";
352 
353     /// Create a dummy `Link` in a temporary directory.
dummy_link() -> (Link, TempDir)354     fn dummy_link() -> (Link, TempDir) {
355         let temp = TempFileBuilder::new().prefix("book").tempdir().unwrap();
356 
357         let chapter_path = temp.path().join("chapter_1.md");
358         File::create(&chapter_path)
359             .unwrap()
360             .write_all(DUMMY_SRC.as_bytes())
361             .unwrap();
362 
363         let link = Link::new("Chapter 1", chapter_path);
364 
365         (link, temp)
366     }
367 
368     /// Create a nested `Link` written to a temporary directory.
nested_links() -> (Link, TempDir)369     fn nested_links() -> (Link, TempDir) {
370         let (mut root, temp_dir) = dummy_link();
371 
372         let second_path = temp_dir.path().join("second.md");
373 
374         File::create(&second_path)
375             .unwrap()
376             .write_all(b"Hello World!")
377             .unwrap();
378 
379         let mut second = Link::new("Nested Chapter 1", &second_path);
380         second.number = Some(SectionNumber(vec![1, 2]));
381 
382         root.nested_items.push(second.clone().into());
383         root.nested_items.push(SummaryItem::Separator);
384         root.nested_items.push(second.into());
385 
386         (root, temp_dir)
387     }
388 
389     #[test]
load_a_single_chapter_from_disk()390     fn load_a_single_chapter_from_disk() {
391         let (link, temp_dir) = dummy_link();
392         let should_be = Chapter::new(
393             "Chapter 1",
394             DUMMY_SRC.to_string(),
395             "chapter_1.md",
396             Vec::new(),
397         );
398 
399         let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
400         assert_eq!(got, should_be);
401     }
402 
403     #[test]
load_a_single_chapter_with_utf8_bom_from_disk()404     fn load_a_single_chapter_with_utf8_bom_from_disk() {
405         let temp_dir = TempFileBuilder::new().prefix("book").tempdir().unwrap();
406 
407         let chapter_path = temp_dir.path().join("chapter_1.md");
408         File::create(&chapter_path)
409             .unwrap()
410             .write_all(("\u{feff}".to_owned() + DUMMY_SRC).as_bytes())
411             .unwrap();
412 
413         let link = Link::new("Chapter 1", chapter_path);
414 
415         let should_be = Chapter::new(
416             "Chapter 1",
417             DUMMY_SRC.to_string(),
418             "chapter_1.md",
419             Vec::new(),
420         );
421 
422         let got = load_chapter(&link, temp_dir.path(), Vec::new()).unwrap();
423         assert_eq!(got, should_be);
424     }
425 
426     #[test]
cant_load_a_nonexistent_chapter()427     fn cant_load_a_nonexistent_chapter() {
428         let link = Link::new("Chapter 1", "/foo/bar/baz.md");
429 
430         let got = load_chapter(&link, "", Vec::new());
431         assert!(got.is_err());
432     }
433 
434     #[test]
load_recursive_link_with_separators()435     fn load_recursive_link_with_separators() {
436         let (root, temp) = nested_links();
437 
438         let nested = Chapter {
439             name: String::from("Nested Chapter 1"),
440             content: String::from("Hello World!"),
441             number: Some(SectionNumber(vec![1, 2])),
442             path: Some(PathBuf::from("second.md")),
443             source_path: Some(PathBuf::from("second.md")),
444             parent_names: vec![String::from("Chapter 1")],
445             sub_items: Vec::new(),
446         };
447         let should_be = BookItem::Chapter(Chapter {
448             name: String::from("Chapter 1"),
449             content: String::from(DUMMY_SRC),
450             number: None,
451             path: Some(PathBuf::from("chapter_1.md")),
452             source_path: Some(PathBuf::from("chapter_1.md")),
453             parent_names: Vec::new(),
454             sub_items: vec![
455                 BookItem::Chapter(nested.clone()),
456                 BookItem::Separator,
457                 BookItem::Chapter(nested),
458             ],
459         });
460 
461         let got = load_summary_item(&SummaryItem::Link(root), temp.path(), Vec::new()).unwrap();
462         assert_eq!(got, should_be);
463     }
464 
465     #[test]
load_a_book_with_a_single_chapter()466     fn load_a_book_with_a_single_chapter() {
467         let (link, temp) = dummy_link();
468         let summary = Summary {
469             numbered_chapters: vec![SummaryItem::Link(link)],
470             ..Default::default()
471         };
472         let should_be = Book {
473             sections: vec![BookItem::Chapter(Chapter {
474                 name: String::from("Chapter 1"),
475                 content: String::from(DUMMY_SRC),
476                 path: Some(PathBuf::from("chapter_1.md")),
477                 source_path: Some(PathBuf::from("chapter_1.md")),
478                 ..Default::default()
479             })],
480             ..Default::default()
481         };
482 
483         let got = load_book_from_disk(&summary, temp.path()).unwrap();
484 
485         assert_eq!(got, should_be);
486     }
487 
488     #[test]
book_iter_iterates_over_sequential_items()489     fn book_iter_iterates_over_sequential_items() {
490         let book = Book {
491             sections: vec![
492                 BookItem::Chapter(Chapter {
493                     name: String::from("Chapter 1"),
494                     content: String::from(DUMMY_SRC),
495                     ..Default::default()
496                 }),
497                 BookItem::Separator,
498             ],
499             ..Default::default()
500         };
501 
502         let should_be: Vec<_> = book.sections.iter().collect();
503 
504         let got: Vec<_> = book.iter().collect();
505 
506         assert_eq!(got, should_be);
507     }
508 
509     #[test]
iterate_over_nested_book_items()510     fn iterate_over_nested_book_items() {
511         let book = Book {
512             sections: vec![
513                 BookItem::Chapter(Chapter {
514                     name: String::from("Chapter 1"),
515                     content: String::from(DUMMY_SRC),
516                     number: None,
517                     path: Some(PathBuf::from("Chapter_1/index.md")),
518                     source_path: Some(PathBuf::from("Chapter_1/index.md")),
519                     parent_names: Vec::new(),
520                     sub_items: vec![
521                         BookItem::Chapter(Chapter::new(
522                             "Hello World",
523                             String::new(),
524                             "Chapter_1/hello.md",
525                             Vec::new(),
526                         )),
527                         BookItem::Separator,
528                         BookItem::Chapter(Chapter::new(
529                             "Goodbye World",
530                             String::new(),
531                             "Chapter_1/goodbye.md",
532                             Vec::new(),
533                         )),
534                     ],
535                 }),
536                 BookItem::Separator,
537             ],
538             ..Default::default()
539         };
540 
541         let got: Vec<_> = book.iter().collect();
542 
543         assert_eq!(got.len(), 5);
544 
545         // checking the chapter names are in the order should be sufficient here...
546         let chapter_names: Vec<String> = got
547             .into_iter()
548             .filter_map(|i| match *i {
549                 BookItem::Chapter(ref ch) => Some(ch.name.clone()),
550                 _ => None,
551             })
552             .collect();
553         let should_be: Vec<_> = vec![
554             String::from("Chapter 1"),
555             String::from("Hello World"),
556             String::from("Goodbye World"),
557         ];
558 
559         assert_eq!(chapter_names, should_be);
560     }
561 
562     #[test]
for_each_mut_visits_all_items()563     fn for_each_mut_visits_all_items() {
564         let mut book = Book {
565             sections: vec![
566                 BookItem::Chapter(Chapter {
567                     name: String::from("Chapter 1"),
568                     content: String::from(DUMMY_SRC),
569                     number: None,
570                     path: Some(PathBuf::from("Chapter_1/index.md")),
571                     source_path: Some(PathBuf::from("Chapter_1/index.md")),
572                     parent_names: Vec::new(),
573                     sub_items: vec![
574                         BookItem::Chapter(Chapter::new(
575                             "Hello World",
576                             String::new(),
577                             "Chapter_1/hello.md",
578                             Vec::new(),
579                         )),
580                         BookItem::Separator,
581                         BookItem::Chapter(Chapter::new(
582                             "Goodbye World",
583                             String::new(),
584                             "Chapter_1/goodbye.md",
585                             Vec::new(),
586                         )),
587                     ],
588                 }),
589                 BookItem::Separator,
590             ],
591             ..Default::default()
592         };
593 
594         let num_items = book.iter().count();
595         let mut visited = 0;
596 
597         book.for_each_mut(|_| visited += 1);
598 
599         assert_eq!(visited, num_items);
600     }
601 
602     #[test]
cant_load_chapters_with_an_empty_path()603     fn cant_load_chapters_with_an_empty_path() {
604         let (_, temp) = dummy_link();
605         let summary = Summary {
606             numbered_chapters: vec![SummaryItem::Link(Link {
607                 name: String::from("Empty"),
608                 location: Some(PathBuf::from("")),
609                 ..Default::default()
610             })],
611 
612             ..Default::default()
613         };
614 
615         let got = load_book_from_disk(&summary, temp.path());
616         assert!(got.is_err());
617     }
618 
619     #[test]
cant_load_chapters_when_the_link_is_a_directory()620     fn cant_load_chapters_when_the_link_is_a_directory() {
621         let (_, temp) = dummy_link();
622         let dir = temp.path().join("nested");
623         fs::create_dir(&dir).unwrap();
624 
625         let summary = Summary {
626             numbered_chapters: vec![SummaryItem::Link(Link {
627                 name: String::from("nested"),
628                 location: Some(dir),
629                 ..Default::default()
630             })],
631             ..Default::default()
632         };
633 
634         let got = load_book_from_disk(&summary, temp.path());
635         assert!(got.is_err());
636     }
637 }
638