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