From 1b5a58902f6b8fc13a235eb76d3b7bfb253818c7 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Mon, 3 Jul 2017 07:34:03 +0800 Subject: [PATCH] Created a Loader for loading a book using the Summary This is a squashed commit. It roughly encompasses the following changes. --- \# Book - Created another private submodule, mdbook::loader::book - This submodule contains the data types representing a Book - For now the Book just contains a list of BookItems (either chapters or separators) - A Chapter contains its name, contents (as one long string), an optional section number (only numbered chapters have numbers, obviously), and any nested chapters - There's a function for loading a single Chapter from disk using it's associated Link entry from the SUMMARY.md - Another function builds up the Book by recursively visiting all Links and separators in the Summary and joining them into a single Vec. This is the only non-dumb-data-type item which is actually exported from the book module \# Loader - Made the loader use the book::load_book_from_disk function for loading a book in the loader's directory. \# Tests - Made sure you can load from disk by writing some files to a temporary directory - Made sure the Loader can load the entire example-book from disk and doesn't crash or hit an error - Increased test coverage from 34.4% to 47.7% (as reported by cargo kcov) --- src/lib.rs | 29 ++---- src/loader/book.rs | 234 ++++++++++++++++++++++++++++++++++++++++++ src/loader/mod.rs | 30 ++++-- src/loader/summary.rs | 24 +++-- tests/loading.rs | 18 +++- 5 files changed, 301 insertions(+), 34 deletions(-) create mode 100644 src/loader/book.rs diff --git a/src/lib.rs b/src/lib.rs index 9a01bb93..2b4c5dd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,8 +3,7 @@ //! **mdBook** is similar to Gitbook but implemented in Rust. //! It offers a command line interface, but can also be used as a regular crate. //! -//! This is the API doc, but you can find a [less "low-level" documentation -//! here](../index.html) that +//! This is the API doc, but you can find a [less "low-level" documentation here](../index.html) that //! contains information about the command line tool, format, structure etc. //! It is also rendered with mdBook to showcase the features and default theme. //! @@ -26,23 +25,17 @@ //! # #[allow(unused_variables)] //! fn main() { //! let mut book = MDBook::new("my-book") // Path to root -//! .with_source("src") // Path from root to -//! source directory -//! .with_destination("book") // Path from root to -//! output directory -//! .read_config() // Parse book.toml -//! configuration file -//! .expect("I don't handle configuration file errors, -//! but you should!"); -//! +//! .with_source("src") // Path from root to source directory +//! .with_destination("book") // Path from root to output directory +//! .read_config() // Parse book.toml configuration file +//! .expect("I don't handle configuration file errors, but you should!"); //! book.build().unwrap(); // Render the book //! } //! ``` //! //! ## Implementing a new Renderer //! -//! If you want to create a new renderer for mdBook, the only thing you have to -//! do is to implement +//! If you want to create a new renderer for mdBook, the only thing you have to do is to implement //! the [Renderer trait](renderer/renderer/trait.Renderer.html) //! //! And then you can swap in your renderer like this: @@ -60,17 +53,15 @@ //! let book = MDBook::new("my-book").set_renderer(Box::new(your_renderer)); //! # } //! ``` -//! If you make a renderer, you get the book constructed in form of -//! `Vec` and you get +//! If you make a renderer, you get the book constructed in form of `Vec` and you get //! the book config in a `BookConfig` struct. //! -//! It's your responsability to create the necessary files in the correct +//! It's your responsibility to create the necessary files in the correct //! directories. //! //! ## utils //! -//! I have regrouped some useful functions in the [utils](utils/index.html) -//! module, like the +//! I have regrouped some useful functions in the [utils](utils/index.html) module, like the //! following function [`utils::fs::create_file(path: //! &Path)`](utils/fs/fn.create_file.html) //! @@ -99,6 +90,8 @@ extern crate tempdir; #[cfg(test)] #[macro_use] extern crate pretty_assertions; +#[cfg(test)] +extern crate tempdir; mod parse; mod preprocess; diff --git a/src/loader/book.rs b/src/loader/book.rs new file mode 100644 index 00000000..7721b44b --- /dev/null +++ b/src/loader/book.rs @@ -0,0 +1,234 @@ +#![allow(missing_docs, unused_variables, unused_imports, dead_code)] + +use std::path::Path; +use std::fs::File; +use std::io::Read; + +use loader::summary::{Summary, Link, SummaryItem, SectionNumber}; +use errors::*; + + +/// A dumb tree structure representing a book. +/// +/// For the moment a book is just a collection of `BookItems`. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Book { + /// The sections in this book. + pub sections: Vec, +} + +impl Book { + /// Create an empty book. + pub fn new() -> Self { + Default::default() + } +} + +/// Enum representing any type of item which can be added to a book. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum BookItem { + /// A nested chapter. + Chapter(Chapter), + /// A section separator. + Separator, +} + +/// The representation of a "chapter", usually mapping to a single file on +/// disk however it may contain multiple sub-chapters. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +pub struct Chapter { + /// The chapter's name. + pub name: String, + /// The chapter's contents. + pub content: String, + /// The chapter's section number, if it has one. + pub number: Option, + /// Nested items. + pub sub_items: Vec, +} + +impl Chapter { + /// Create a new chapter with the provided content. + pub fn new(name: &str, content: String) -> Chapter { + Chapter { + name: name.to_string(), + content: content, + ..Default::default() + } + } +} + +/// Use the provided `Summary` to load a `Book` from disk. +/// +/// You need to pass in the book's source directory because all the links in +/// `SUMMARY.md` give the chapter locations relative to it. +pub fn load_book_from_disk>(summary: &Summary, src_dir: P) -> Result { + debug!("[*] Loading the book from disk"); + let src_dir = src_dir.as_ref(); + + let prefix = summary.prefix_chapters.iter(); + let numbered = summary.numbered_chapters.iter(); + let suffix = summary.suffix_chapters.iter(); + + let summary_items = prefix.chain(numbered).chain(suffix); + + let chapters = summary_items + .map(|i| load_summary_item(i, src_dir)) + .collect::>() + .chain_err(|| "Couldn't load chapters from disk")?; + + Ok(Book { sections: chapters }) +} + +fn load_summary_item>(item: &SummaryItem, src_dir: P) -> Result { + match *item { + SummaryItem::Separator => Ok(BookItem::Separator), + SummaryItem::Link(ref link) => load_chapter(link, src_dir).map(|c| BookItem::Chapter(c)), + } +} + +fn load_chapter>(link: &Link, src_dir: P) -> Result { + debug!("[*] Loading {} ({})", link.name, link.location.display()); + let src_dir = src_dir.as_ref(); + + let location = if link.location.is_absolute() { + link.location.clone() + } else { + src_dir.join(&link.location) + }; + + let mut f = File::open(location).chain_err(|| { + format!("Chapter file not found, {}", link.location.display()) + })?; + + let mut content = String::new(); + f.read_to_string(&mut content)?; + + let mut ch = Chapter::new(&link.name, content); + ch.number = link.number.clone(); + + let sub_items = link.nested_items + .iter() + .map(|i| load_summary_item(i, src_dir)) + .collect::>>()?; + + ch.sub_items = sub_items; + + Ok(ch) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempdir::TempDir; + use std::io::Write; + + const DUMMY_SRC: &'static str = " +# Dummy Chapter + +this is some dummy text. + +And here is some more text. +"; + + /// Create a dummy `Link` in a temporary directory. + fn dummy_link() -> (Link, TempDir) { + let temp = TempDir::new("book").unwrap(); + + let chapter_path = temp.path().join("chapter_1.md"); + File::create(&chapter_path) + .unwrap() + .write(DUMMY_SRC.as_bytes()) + .unwrap(); + + let link = Link::new("Chapter 1", chapter_path); + + (link, temp) + } + + /// Create a nested `Link` written to a temporary directory. + fn nested_links() -> (Link, TempDir) { + let (mut root, temp_dir) = dummy_link(); + + let second_path = temp_dir.path().join("second.md"); + + File::create(&second_path) + .unwrap() + .write_all("Hello World!".as_bytes()) + .unwrap(); + + + let mut second = Link::new("Nested Chapter 1", &second_path); + second.number = Some(SectionNumber(vec![1, 2])); + + root.push_item(second.clone()); + root.push_item(SummaryItem::Separator); + root.push_item(second.clone()); + + (root, temp_dir) + } + + #[test] + fn load_a_single_chapter_from_disk() { + let (link, temp_dir) = dummy_link(); + let should_be = Chapter::new("Chapter 1", DUMMY_SRC.to_string()); + + let got = load_chapter(&link, temp_dir.path()).unwrap(); + assert_eq!(got, should_be); + } + + #[test] + fn cant_load_a_nonexistent_chapter() { + let link = Link::new("Chapter 1", "/foo/bar/baz.md"); + + let got = load_chapter(&link, ""); + assert!(got.is_err()); + } + + #[test] + fn load_recursive_link_with_separators() { + let (root, _temp) = nested_links(); + + let nested = Chapter { + name: String::from("Nested Chapter 1"), + content: String::from("Hello World!"), + number: Some(SectionNumber(vec![1, 2])), + sub_items: Vec::new(), + }; + let should_be = BookItem::Chapter(Chapter { + name: String::from("Chapter 1"), + content: String::from(DUMMY_SRC), + number: None, + sub_items: vec![ + BookItem::Chapter(nested.clone()), + BookItem::Separator, + BookItem::Chapter(nested.clone()), + ], + }); + + let got = load_summary_item(&SummaryItem::Link(root), "").unwrap(); + assert_eq!(got, should_be); + } + + #[test] + fn load_a_book_with_a_single_chapter() { + let (link, temp) = dummy_link(); + let summary = Summary { + numbered_chapters: vec![SummaryItem::Link(link)], + ..Default::default() + }; + let should_be = Book { + sections: vec![ + BookItem::Chapter(Chapter { + name: String::from("Chapter 1"), + content: String::from(DUMMY_SRC), + ..Default::default() + }), + ], + }; + + let got = load_book_from_disk(&summary, "").unwrap(); + + assert_eq!(got, should_be); + } +} \ No newline at end of file diff --git a/src/loader/mod.rs b/src/loader/mod.rs index 3c597d4b..eff82505 100644 --- a/src/loader/mod.rs +++ b/src/loader/mod.rs @@ -46,8 +46,10 @@ use std::io::Read; use errors::*; mod summary; +mod book; pub use self::summary::{Summary, Link, SummaryItem, parse_summary, SectionNumber}; +pub use self::book::{Book, load_book_from_disk, BookItem, Chapter}; /// The object in charge of parsing the source directory into a usable @@ -64,21 +66,33 @@ impl Loader { } /// Parse the summary file and use it to load a book from disk. - pub fn load(&self) -> Result<()> { - let summary = self.parse_summary().chain_err( + pub fn load(&self) -> Result { + let summary_md = self.find_summary().chain_err( + || "Couldn't find `SUMMARY.md`", + )?; + + let summary = self.parse_summary(&summary_md).chain_err( || "Couldn't parse `SUMMARY.md`", )?; - unimplemented!() + let src_dir = match summary_md.parent() { + Some(parent) => parent, + None => bail!("SUMMARY.md doesn't have a parent... wtf?"), + }; + load_book_from_disk(&summary, src_dir) } - /// Parse the `SUMMARY.md` file. - pub fn parse_summary(&self) -> Result { - let path = self.source_directory.join("SUMMARY.md"); - + /// Parse a `SUMMARY.md` file. + pub fn parse_summary>(&self, summary_md: P) -> Result { let mut summary_content = String::new(); - File::open(&path)?.read_to_string(&mut summary_content)?; + File::open(summary_md)?.read_to_string(&mut summary_content)?; summary::parse_summary(&summary_content) } + + fn find_summary(&self) -> Result { + // TODO: use Piston's find_folder to make locating SUMMARY.md easier. + // https://github.com/PistonDevelopers/find_folder + Ok(self.source_directory.join("SUMMARY.md")) + } } diff --git a/src/loader/summary.rs b/src/loader/summary.rs index 6510fe97..45cb1674 100644 --- a/src/loader/summary.rs +++ b/src/loader/summary.rs @@ -50,7 +50,7 @@ pub fn parse_summary(summary: &str) -> Result { } /// The parsed `SUMMARY.md`, specifying how the book should be laid out. -#[derive(Debug, Clone, Default, PartialEq)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct Summary { /// An optional title for the `SUMMARY.md`, currently just ignored. pub title: Option, @@ -66,7 +66,7 @@ pub struct Summary { /// entries. /// /// This is roughly the equivalent of `[Some section](./path/to/file.md)`. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Link { /// The name of the chapter. pub name: String, @@ -89,6 +89,11 @@ impl Link { nested_items: Vec::new(), } } + + /// Add an item to this link's `nested_items`. + pub fn push_item>(&mut self, item: I) { + self.nested_items.push(item.into()); + } } impl Default for Link { @@ -103,7 +108,7 @@ impl Default for Link { } /// An item in `SUMMARY.md` which could be either a separator or a `Link`. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum SummaryItem { /// A link to a chapter. Link(Link), @@ -120,6 +125,12 @@ impl SummaryItem { } } +impl From for SummaryItem { + fn from(other: Link) -> SummaryItem { + SummaryItem::Link(other) + } +} + #[derive(Debug, Copy, Clone, PartialEq)] enum State { Begin, @@ -310,8 +321,9 @@ impl<'a> SummaryParser<'a> { fn step_numbered(&mut self, event: Event, nesting: u32) -> Result<()> { match event { Event::Start(Tag::Item) => { - let it = self.parse_item() - .chain_err(|| "List items should only contain links")?; + let it = self.parse_item().chain_err( + || "List items should only contain links", + )?; debug!("[*] Found a chapter: {:?} ({})", it.name, it.location.display()); let section_number = self.push_numbered_section(SummaryItem::Link(it)); @@ -479,7 +491,7 @@ fn stringify_events(events: Vec) -> String { /// A section number like "1.2.3", basically just a newtype'd `Vec` with /// a pretty `Display` impl. -#[derive(Debug, PartialEq, Clone, Default)] +#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)] pub struct SectionNumber(pub Vec); impl Display for SectionNumber { diff --git a/tests/loading.rs b/tests/loading.rs index 581478b4..4ae35108 100644 --- a/tests/loading.rs +++ b/tests/loading.rs @@ -49,11 +49,14 @@ fn parse_summary_using_loader() { let temp = TempDir::new("book").unwrap(); let summary_md = temp.path().join("SUMMARY.md"); - File::create(&summary_md).unwrap().write_all(SUMMARY.as_bytes()).unwrap(); + File::create(&summary_md) + .unwrap() + .write_all(SUMMARY.as_bytes()) + .unwrap(); let loader = Loader::new(temp.path()); - let got = loader.parse_summary().unwrap(); + let got = loader.parse_summary(&summary_md).unwrap(); let should_be = expected_summary(); assert_eq!(got, should_be); @@ -107,3 +110,14 @@ fn expected_summary() -> Summary { ], } } + +#[test] +fn load_the_example_book() { + let example_src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("book-example") + .join("src"); + let loader = Loader::new(example_src_dir); + + let book = loader.load().unwrap(); + println!("{:#?}", book); +}