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
14pub 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#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
80pub struct Book {
81 pub sections: Vec<BookItem>,
83 __non_exhaustive: (),
84}
85
86impl Book {
87 pub fn new() -> Self {
89 Default::default()
90 }
91
92 pub fn iter(&self) -> BookItems<'_> {
94 BookItems {
95 items: self.sections.iter().collect(),
96 }
97 }
98
99 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137pub enum BookItem {
138 Chapter(Chapter),
140 Separator,
142 PartTitle(String),
144}
145
146impl From<Chapter> for BookItem {
147 fn from(other: Chapter) -> BookItem {
148 BookItem::Chapter(other)
149 }
150}
151
152#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
155pub struct Chapter {
156 pub name: String,
158 pub content: String,
160 pub number: Option<SectionNumber>,
162 pub sub_items: Vec<BookItem>,
164 pub path: Option<PathBuf>,
166 pub source_path: Option<PathBuf>,
168 pub parent_names: Vec<String>,
170}
171
172impl Chapter {
173 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 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 pub fn is_draft_chapter(&self) -> bool {
206 self.path.is_none()
207 }
208}
209
210pub(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
304pub 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 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 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 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 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}