/ libs / mdbook-shared / src / book.rs
book.rs
  1  use std::collections::{HashMap, 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::build_opts::BuildOpts;
  9  // use crate::config::Config;
 10  use crate::errors::*;
 11  
 12  /// Load a book into memory from its `src/` directory.
 13  pub fn load_book<P: AsRef<Path>>(
 14      root_dir: P,
 15      cfg: &Config,
 16      build_opts: &BuildOpts,
 17  ) -> Result<LoadedBook> {
 18      if cfg.has_localized_dir_structure() {
 19          match build_opts.language_ident {
 20              // Build a single book's translation.
 21              Some(_) => Ok(LoadedBook::Single(load_single_book_translation(
 22                  &root_dir,
 23                  cfg,
 24                  &build_opts.language_ident,
 25              )?)),
 26              // Build all available translations at once.
 27              None => {
 28                  let mut translations = HashMap::new();
 29                  for (lang_ident, _) in cfg.language.0.iter() {
 30                      let book =
 31                          load_single_book_translation(&root_dir, cfg, &Some(lang_ident.clone()))?;
 32                      translations.insert(lang_ident.clone(), book);
 33                  }
 34                  Ok(LoadedBook::Localized(LocalizedBooks(translations)))
 35              }
 36          }
 37      } else {
 38          Ok(LoadedBook::Single(load_single_book_translation(
 39              &root_dir, cfg, &None,
 40          )?))
 41      }
 42  }
 43  
 44  fn load_single_book_translation<P: AsRef<Path>>(
 45      root_dir: P,
 46      cfg: &Config,
 47      language_ident: &Option<String>,
 48  ) -> Result<Book> {
 49      let localized_src_dir = root_dir
 50          .as_ref()
 51          .join(cfg.get_localized_src_path(language_ident.as_ref()).unwrap());
 52      let fallback_src_dir = root_dir.as_ref().join(cfg.get_fallback_src_path());
 53  
 54      let summary_md = localized_src_dir.join("SUMMARY.md");
 55  
 56      let mut summary_content = String::new();
 57      File::open(&summary_md)
 58          .with_context(|| {
 59              format!(
 60                  "Couldn't open SUMMARY.md in {:?} directory",
 61                  localized_src_dir
 62              )
 63          })?
 64          .read_to_string(&mut summary_content)?;
 65  
 66      let summary = parse_summary(&summary_content)
 67          .with_context(|| format!("Summary parsing failed for file={:?}", summary_md))?;
 68  
 69      if cfg.build.create_missing {
 70          create_missing(&localized_src_dir, &summary)
 71              .with_context(|| "Unable to create missing chapters")?;
 72      }
 73  
 74      load_book_from_disk(&summary, localized_src_dir, fallback_src_dir, cfg)
 75  }
 76  
 77  fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
 78      let mut items: Vec<_> = summary
 79          .prefix_chapters
 80          .iter()
 81          .chain(summary.numbered_chapters.iter())
 82          .chain(summary.suffix_chapters.iter())
 83          .collect();
 84  
 85      while !items.is_empty() {
 86          let next = items.pop().expect("already checked");
 87  
 88          if let SummaryItem::Link(ref link) = *next {
 89              if let Some(ref location) = link.location {
 90                  let filename = src_dir.join(location);
 91                  if !filename.exists() {
 92                      create_missing_link(&filename, link)?;
 93                  }
 94              }
 95  
 96              items.extend(&link.nested_items);
 97          }
 98      }
 99  
100      Ok(())
101  }
102  
103  fn create_missing_link(filename: &Path, link: &Link) -> Result<()> {
104      if let Some(parent) = filename.parent() {
105          if !parent.exists() {
106              fs::create_dir_all(parent).map_err(|e| {
107                  Error::from(format!(
108                      "Unable to create missing directory {:?}: {}",
109                      parent, e
110                  ))
111              })?;
112          }
113      }
114      debug!("Creating missing file {}", filename.display());
115  
116      let mut f = File::create(&filename)?;
117      writeln!(f, "# {}", link.name)?;
118  
119      Ok(())
120  }
121  
122  /// A dumb tree structure representing a book.
123  ///
124  /// For the moment a book is just a collection of [`BookItems`] which are
125  /// accessible by either iterating (immutably) over the book with [`iter()`], or
126  /// recursively applying a closure to each section to mutate the chapters, using
127  /// [`for_each_mut()`].
128  ///
129  ///
130  /// [`iter()`]: #method.iter
131  /// [`for_each_mut()`]: #method.for_each_mut
132  #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
133  pub struct Book {
134      /// The sections in this book.
135      pub sections: Vec<BookItem>,
136      /// Chapter title overrides for this book.
137      #[serde(default)]
138      pub chapter_titles: HashMap<PathBuf, String>,
139      __non_exhaustive: (),
140  }
141  
142  impl Book {
143      /// Create an empty book.
144      pub fn new() -> Self {
145          Default::default()
146      }
147  
148      /// Get a depth-first iterator over the items in the book.
149      pub fn iter(&self) -> BookItems<'_> {
150          BookItems {
151              items: self.sections.iter().collect(),
152          }
153      }
154  
155      /// Recursively apply a closure to each item in the book, allowing you to
156      /// mutate them.
157      ///
158      /// # Note
159      ///
160      /// Unlike the `iter()` method, this requires a closure instead of returning
161      /// an iterator. This is because using iterators can possibly allow you
162      /// to have iterator invalidation errors.
163      pub fn for_each_mut<F>(&mut self, mut func: F)
164      where
165          F: FnMut(&mut BookItem),
166      {
167          for_each_mut(&mut func, &mut self.sections);
168      }
169  
170      /// Append a `BookItem` to the `Book`.
171      pub fn push_item<I: Into<BookItem>>(&mut self, item: I) -> &mut Self {
172          self.sections.push(item.into());
173          self
174      }
175  }
176  
177  pub fn for_each_mut<'a, F, I>(func: &mut F, items: I)
178  where
179      F: FnMut(&mut BookItem),
180      I: IntoIterator<Item = &'a mut BookItem>,
181  {
182      for item in items {
183          if let BookItem::Chapter(ch) = item {
184              for_each_mut(func, &mut ch.sub_items);
185          }
186  
187          func(item);
188      }
189  }
190  
191  /// A collection of `Books`, each one a single localization.
192  #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
193  pub struct LocalizedBooks(pub HashMap<String, Book>);
194  
195  impl LocalizedBooks {
196      /// Get a depth-first iterator over the items in the book.
197      pub fn iter(&self) -> BookItems<'_> {
198          let mut items = VecDeque::new();
199  
200          for (_, book) in self.0.iter() {
201              items.extend(book.iter().items);
202          }
203  
204          BookItems { items: items }
205      }
206  
207      /// Recursively apply a closure to each item in the book, allowing you to
208      /// mutate them.
209      ///
210      /// # Note
211      ///
212      /// Unlike the `iter()` method, this requires a closure instead of returning
213      /// an iterator. This is because using iterators can possibly allow you
214      /// to have iterator invalidation errors.
215      pub fn for_each_mut<F>(&mut self, mut func: F)
216      where
217          F: FnMut(&mut BookItem),
218      {
219          for (_, book) in self.0.iter_mut() {
220              book.for_each_mut(&mut func);
221          }
222      }
223  }
224  
225  /// A book which has been loaded and is ready for rendering.
226  ///
227  /// This exists because the result of loading a book directory can be multiple
228  /// books, each one representing a separate translation, or a single book with
229  /// no translations.
230  #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
231  pub enum LoadedBook {
232      /// The book was loaded with all translations.
233      Localized(LocalizedBooks),
234      /// The book was loaded without any additional translations.
235      Single(Book),
236  }
237  
238  impl LoadedBook {
239      /// Get a depth-first iterator over the items in the book.
240      pub fn iter(&self) -> BookItems<'_> {
241          match self {
242              LoadedBook::Localized(books) => books.iter(),
243              LoadedBook::Single(book) => book.iter(),
244          }
245      }
246  
247      /// Recursively apply a closure to each item in the book, allowing you to
248      /// mutate them.
249      ///
250      /// # Note
251      ///
252      /// Unlike the `iter()` method, this requires a closure instead of returning
253      /// an iterator. This is because using iterators can possibly allow you
254      /// to have iterator invalidation errors.
255      pub fn for_each_mut<F>(&mut self, mut func: F)
256      where
257          F: FnMut(&mut BookItem),
258      {
259          match self {
260              LoadedBook::Localized(books) => books.for_each_mut(&mut func),
261              LoadedBook::Single(book) => book.for_each_mut(&mut func),
262          }
263      }
264  
265      /// Returns one of the books loaded. Used for compatibility.
266      pub fn first(&self) -> &Book {
267          match self {
268              LoadedBook::Localized(books) => books.0.iter().next().unwrap().1,
269              LoadedBook::Single(book) => &book,
270          }
271      }
272  }
273  
274  /// Enum representing any type of item which can be added to a book.
275  #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
276  pub enum BookItem {
277      /// A nested chapter.
278      Chapter(Chapter),
279      /// A section separator.
280      Separator,
281      /// A part title.
282      PartTitle(String),
283  }
284  
285  impl From<Chapter> for BookItem {
286      fn from(other: Chapter) -> BookItem {
287          BookItem::Chapter(other)
288      }
289  }
290  
291  /// The representation of a "chapter", usually mapping to a single file on
292  /// disk however it may contain multiple sub-chapters.
293  #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
294  pub struct Chapter<R> {
295      /// The chapter's name.
296      pub name: String,
297      /// The chapter's contents.
298      pub content: String,
299      /// The chapter's section number, if it has one.
300      pub number: Option<SectionNumber>,
301      /// Nested items.
302      pub sub_items: Vec<BookItem>,
303      /// The chapter's route
304      pub path: Option<R>,
305      /// An ordered list of the names of each chapter above this one in the hierarchy.
306      pub parent_names: Vec<String>,
307  }
308  
309  impl<R> Chapter<R> {
310      /// Create a new chapter with the provided content.
311      pub fn new<P: Into<R>>(
312          name: &str,
313          content: String,
314          p: P,
315          parent_names: Vec<String>,
316      ) -> Chapter {
317          let path: R = p.into();
318          Chapter {
319              name: name.to_string(),
320              content,
321              path: Some(path.clone()),
322              parent_names,
323              ..Default::default()
324          }
325      }
326  
327      /// Create a new draft chapter that is not attached to a source markdown file (and thus
328      /// has no content).
329      pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
330          Chapter {
331              name: name.to_string(),
332              content: String::new(),
333              path: None,
334              parent_names,
335              ..Default::default()
336          }
337      }
338  
339      /// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file.
340      pub fn is_draft_chapter(&self) -> bool {
341          self.path.is_none()
342      }
343  }
344  
345  /// Use the provided `Summary` to load a `Book` from disk.
346  ///
347  /// You need to pass in the book's source directory because all the links in
348  /// `SUMMARY.md` give the chapter locations relative to it.
349  pub(crate) fn load_book_from_disk<P: AsRef<Path>>(
350      summary: &Summary,
351      localized_src_dir: P,
352      fallback_src_dir: P,
353      cfg: &Config,
354  ) -> Result<Book> {
355      debug!("Loading the book from disk");
356  
357      let prefix = summary.prefix_chapters.iter();
358      let numbered = summary.numbered_chapters.iter();
359      let suffix = summary.suffix_chapters.iter();
360  
361      let summary_items = prefix.chain(numbered).chain(suffix);
362  
363      let mut chapters = Vec::new();
364  
365      for summary_item in summary_items {
366          let chapter = load_summary_item(
367              summary_item,
368              localized_src_dir.as_ref(),
369              fallback_src_dir.as_ref(),
370              Vec::new(),
371              cfg,
372          )?;
373          chapters.push(chapter);
374      }
375  
376      Ok(Book {
377          sections: chapters,
378          chapter_titles: HashMap::new(),
379          __non_exhaustive: (),
380      })
381  }
382  
383  fn load_summary_item<P: AsRef<Path> + Clone>(
384      item: &SummaryItem,
385      localized_src_dir: P,
386      fallback_src_dir: P,
387      parent_names: Vec<String>,
388      cfg: &Config,
389  ) -> Result<BookItem> {
390      match item {
391          SummaryItem::Separator => Ok(BookItem::Separator),
392          SummaryItem::Link(ref link) => {
393              load_chapter(link, localized_src_dir, fallback_src_dir, parent_names, cfg)
394                  .map(BookItem::Chapter)
395          }
396          SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
397      }
398  }
399  
400  fn load_chapter<P: AsRef<Path>>(
401      link: &Link,
402      localized_src_dir: P,
403      fallback_src_dir: P,
404      parent_names: Vec<String>,
405      cfg: &Config,
406  ) -> Result<Chapter> {
407      let src_dir_localized = localized_src_dir.as_ref();
408      let src_dir_fallback = fallback_src_dir.as_ref();
409  
410      let mut ch = if let Some(ref link_location) = link.location {
411          debug!("Loading {} ({})", link.name, link_location.display());
412  
413          let mut src_dir = src_dir_localized;
414          let mut location = if link_location.is_absolute() {
415              link_location.clone()
416          } else {
417              src_dir.join(link_location)
418          };
419  
420          if !location.exists() && !link_location.is_absolute() {
421              src_dir = src_dir_fallback;
422              location = src_dir.join(link_location);
423              debug!(
424                  "Falling back to default translation in path \"{}\"",
425                  location.display()
426              );
427          }
428          if !location.exists() && cfg.build.create_missing {
429              create_missing_link(&location, &link)
430                  .with_context(|| "Unable to create missing link reference")?;
431          }
432  
433          let mut f = File::open(&location)
434              .with_context(|| format!("Chapter file not found, {}", link_location.display()))?;
435  
436          let mut content = String::new();
437          f.read_to_string(&mut content).with_context(|| {
438              format!("Unable to read \"{}\" ({})", link.name, location.display())
439          })?;
440  
441          if content.as_bytes().starts_with(b"\xef\xbb\xbf") {
442              content.replace_range(..3, "");
443          }
444  
445          let stripped = location
446              .strip_prefix(&src_dir)
447              .expect("Chapters are always inside a book");
448  
449          Chapter::new(&link.name, content, stripped, parent_names.clone())
450      } else {
451          Chapter::new_draft(&link.name, parent_names.clone())
452      };
453  
454      let mut sub_item_parents = parent_names;
455  
456      ch.number = link.number.clone();
457  
458      sub_item_parents.push(link.name.clone());
459      let sub_items = link
460          .nested_items
461          .iter()
462          .map(|i| {
463              load_summary_item(
464                  i,
465                  src_dir_localized,
466                  src_dir_fallback,
467                  sub_item_parents.clone(),
468                  cfg,
469              )
470          })
471          .collect::<Result<Vec<_>>>()?;
472  
473      ch.sub_items = sub_items;
474  
475      Ok(ch)
476  }
477  
478  /// A depth-first iterator over the items in a book.
479  ///
480  /// # Note
481  ///
482  /// This struct shouldn't be created directly, instead prefer the
483  /// [`Book::iter()`] method.
484  pub struct BookItems<'a> {
485      items: VecDeque<&'a BookItem>,
486  }
487  
488  impl<'a> Iterator for BookItems<'a> {
489      type Item = &'a BookItem;
490  
491      fn next(&mut self) -> Option<Self::Item> {
492          let item = self.items.pop_front();
493  
494          if let Some(&BookItem::Chapter(ref ch)) = item {
495              // if we wanted a breadth-first iterator we'd `extend()` here
496              for sub_item in ch.sub_items.iter().rev() {
497                  self.items.push_front(sub_item);
498              }
499          }
500  
501          item
502      }
503  }
504  
505  impl Display for Chapter {
506      fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
507          if let Some(ref section_number) = self.number {
508              write!(f, "{} ", section_number)?;
509          }
510  
511          write!(f, "{}", self.name)
512      }
513  }