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<SummaryItem>. 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)
This commit is contained in:
parent
6d0d4bf379
commit
1b5a58902f
29
src/lib.rs
29
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<BookItems>` and you get
|
||||
//! If you make a renderer, you get the book constructed in form of `Vec<BookItems>` 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;
|
||||
|
|
|
@ -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<BookItem>,
|
||||
}
|
||||
|
||||
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<SectionNumber>,
|
||||
/// Nested items.
|
||||
pub sub_items: Vec<BookItem>,
|
||||
}
|
||||
|
||||
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<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
|
||||
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::<Result<_>>()
|
||||
.chain_err(|| "Couldn't load chapters from disk")?;
|
||||
|
||||
Ok(Book { sections: chapters })
|
||||
}
|
||||
|
||||
fn load_summary_item<P: AsRef<Path>>(item: &SummaryItem, src_dir: P) -> Result<BookItem> {
|
||||
match *item {
|
||||
SummaryItem::Separator => Ok(BookItem::Separator),
|
||||
SummaryItem::Link(ref link) => load_chapter(link, src_dir).map(|c| BookItem::Chapter(c)),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_chapter<P: AsRef<Path>>(link: &Link, src_dir: P) -> Result<Chapter> {
|
||||
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::<Result<Vec<_>>>()?;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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<Book> {
|
||||
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<Summary> {
|
||||
let path = self.source_directory.join("SUMMARY.md");
|
||||
|
||||
/// Parse a `SUMMARY.md` file.
|
||||
pub fn parse_summary<P: AsRef<Path>>(&self, summary_md: P) -> Result<Summary> {
|
||||
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<PathBuf> {
|
||||
// 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"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,7 +50,7 @@ pub fn parse_summary(summary: &str) -> Result<Summary> {
|
|||
}
|
||||
|
||||
/// 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<String>,
|
||||
|
@ -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<I: Into<SummaryItem>>(&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<Link> 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<Event>) -> String {
|
|||
|
||||
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
|
||||
/// a pretty `Display` impl.
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SectionNumber(pub Vec<u32>);
|
||||
|
||||
impl Display for SectionNumber {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue