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