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 | use crate::utils::bracket_escape; |
11 | use log::debug; |
12 | use serde::{Deserialize, Serialize}; |
13 | |
14 | /// Load a book into memory from its `src/` directory. |
15 | pub 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 | |
34 | fn 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)] |
78 | pub struct Book { |
79 | /// The sections in this book. |
80 | pub sections: Vec<BookItem>, |
81 | __non_exhaustive: (), |
82 | } |
83 | |
84 | impl 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 | |
119 | pub fn for_each_mut<'a, F, I>(func: &mut F, items: I) |
120 | where |
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)] |
135 | pub enum BookItem { |
136 | /// A nested chapter. |
137 | Chapter(Chapter), |
138 | /// A section separator. |
139 | Separator, |
140 | /// A part title. |
141 | PartTitle(String), |
142 | } |
143 | |
144 | impl 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)] |
153 | pub 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 | |
170 | impl 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. |
212 | pub(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 | |
235 | fn 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 | |
249 | fn 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. |
308 | pub struct BookItems<'a> { |
309 | items: VecDeque<&'a BookItem>, |
310 | } |
311 | |
312 | impl<'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 | |
329 | impl 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)] |
340 | mod 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 | |
348 | this is some dummy text. |
349 | |
350 | And 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 | |