mdbook/book/
book.rs

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