From 65a60bf81fe9048b7de70e6153e2bd9f93834351 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sat, 8 Jul 2017 19:39:05 +0800 Subject: [PATCH] Integrated the new Book structure into MDBook --- Cargo.toml | 3 +- src/book/bookitem.rs | 87 ------- src/book/mod.rs | 206 +++++++---------- src/lib.rs | 4 +- src/loader/book.rs | 23 ++ src/parse/mod.rs | 3 - src/parse/summary.rs | 231 ------------------- src/renderer/html_handlebars/hbs_renderer.rs | 37 ++- 8 files changed, 127 insertions(+), 467 deletions(-) delete mode 100644 src/book/bookitem.rs delete mode 100644 src/parse/mod.rs delete mode 100644 src/parse/summary.rs diff --git a/Cargo.toml b/Cargo.toml index 1ba96365..5db6ab2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ env_logger = "0.4.0" toml = { version = "0.4", features = ["serde"] } open = "1.1" regex = "0.2.1" +tempdir = "0.3.4" # Watch feature notify = { version = "4.0", optional = true } @@ -41,7 +42,7 @@ ws = { version = "0.7", optional = true} # Tests [dev-dependencies] -tempdir = "0.3.4" +pretty_assertions = "0.2.1" [build-dependencies] error-chain = "0.10" diff --git a/src/book/bookitem.rs b/src/book/bookitem.rs deleted file mode 100644 index 7fe7ab55..00000000 --- a/src/book/bookitem.rs +++ /dev/null @@ -1,87 +0,0 @@ -use serde::{Serialize, Serializer}; -use serde::ser::SerializeStruct; -use std::path::PathBuf; - - -#[derive(Debug, Clone)] -pub enum BookItem { - Chapter(String, Chapter), // String = section - Affix(Chapter), - Spacer, -} - -#[derive(Debug, Clone)] -pub struct Chapter { - pub name: String, - pub path: PathBuf, - pub sub_items: Vec, -} - -#[derive(Debug, Clone)] -pub struct BookItems<'a> { - pub items: &'a [BookItem], - pub current_index: usize, - pub stack: Vec<(&'a [BookItem], usize)>, -} - - -impl Chapter { - pub fn new(name: String, path: PathBuf) -> Self { - - Chapter { - name: name, - path: path, - sub_items: vec![], - } - } -} - - -impl Serialize for Chapter { - fn serialize(&self, serializer: S) -> ::std::result::Result - where S: Serializer - { - let mut struct_ = serializer.serialize_struct("Chapter", 2)?; - struct_.serialize_field("name", &self.name)?; - struct_.serialize_field("path", &self.path)?; - struct_.end() - } -} - - - -// Shamelessly copied from Rustbook -// (https://github.com/rust-lang/rust/blob/master/src/rustbook/book.rs) -impl<'a> Iterator for BookItems<'a> { - type Item = &'a BookItem; - - fn next(&mut self) -> Option<&'a BookItem> { - loop { - if self.current_index >= self.items.len() { - match self.stack.pop() { - None => return None, - Some((parent_items, parent_idx)) => { - self.items = parent_items; - self.current_index = parent_idx + 1; - }, - } - } else { - let cur = &self.items[self.current_index]; - - match *cur { - BookItem::Chapter(_, ref ch) | - BookItem::Affix(ref ch) => { - self.stack.push((self.items, self.current_index)); - self.items = &ch.sub_items[..]; - self.current_index = 0; - }, - BookItem::Spacer => { - self.current_index += 1; - }, - } - - return Some(cur); - } - } - } -} diff --git a/src/book/mod.rs b/src/book/mod.rs index d5effb93..884cabb2 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -1,25 +1,25 @@ -pub mod bookitem; - -pub use self::bookitem::{BookItem, BookItems}; +// pub use self::bookitem::{BookItem, BookItems}; use std::path::{Path, PathBuf}; use std::fs::{self, File}; use std::io::{Read, Write}; use std::process::Command; -use {theme, parse, utils}; +use {theme, utils}; use renderer::{Renderer, HtmlHandlebars}; +use tempdir::TempDir; use errors::*; use config::BookConfig; use config::tomlconfig::TomlConfig; use config::jsonconfig::JsonConfig; +use loader::{self, Book, BookItem, BookItems, Chapter}; pub struct MDBook { config: BookConfig, - pub content: Vec, + book: Book, renderer: Box, livereload: Option, @@ -57,22 +57,22 @@ impl MDBook { /// They can both be changed by using [`set_src()`](#method.set_src) and /// [`set_dest()`](#method.set_dest) - pub fn new>(root: P) -> MDBook { + pub fn new>(root: P) -> Result { - let root = root.into(); + let root = root.as_ref(); if !root.exists() || !root.is_dir() { - warn!("{:?} No directory with that name", root); + bail!("{:?} No directory with that name", root); } - MDBook { + let book = loader::load_book(root.join("src"))?; + + Ok(MDBook { config: BookConfig::new(root), - - content: vec![], + book: book, renderer: Box::new(HtmlHandlebars::new()), - livereload: None, create_missing: true, - } + }) } /// Returns a flat depth-first iterator over the elements of the book, @@ -105,11 +105,7 @@ impl MDBook { /// ``` pub fn iter(&self) -> BookItems { - BookItems { - items: &self.content[..], - current_index: 0, - stack: Vec::new(), - } + self.book.iter() } /// `init()` creates some boilerplate files and directories @@ -127,86 +123,51 @@ impl MDBook { /// and adds a `SUMMARY.md` and a /// `chapter_1.md` to the source directory. - pub fn init(&mut self) -> Result<()> { + pub fn init>(root: P) -> Result { + let root = root.as_ref(); debug!("[fn]: init"); - if !self.config.get_root().exists() { - fs::create_dir_all(&self.config.get_root()).unwrap(); - info!("{:?} created", &self.config.get_root()); + if !root.exists() { + fs::create_dir_all(&root).unwrap(); + info!("{} created", root.display()); } - { - - if !self.get_destination().exists() { - debug!("[*]: {:?} does not exist, trying to create directory", self.get_destination()); - fs::create_dir_all(self.get_destination())?; - } - - - if !self.config.get_source().exists() { - debug!("[*]: {:?} does not exist, trying to create directory", self.config.get_source()); - fs::create_dir_all(self.config.get_source())?; - } - - let summary = self.config.get_source().join("SUMMARY.md"); - - if !summary.exists() { - - // Summary does not exist, create it - debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", &summary); - let mut f = File::create(&summary)?; - - debug!("[*]: Writing to SUMMARY.md"); - - writeln!(f, "# Summary")?; - writeln!(f, "")?; - writeln!(f, "- [Chapter 1](./chapter_1.md)")?; + for dir in &["book", "src"] { + let dir_name = root.join(dir); + if !dir_name.exists() { + debug!("[*]: {} does not exist, trying to create directory", dir_name.display()); + fs::create_dir_all(dir_name)?; } } - // parse SUMMARY.md, and create the missing item related file - self.parse_summary()?; + debug!("[*]: Creating SUMMARY.md"); + let mut summary = File::create(root.join("src").join("SUMMARY.md"))?; + writeln!(summary, "# Summary")?; + writeln!(summary, "")?; + writeln!(summary, "- [Chapter 1](./chapter_1.md")?; - debug!("[*]: constructing paths for missing files"); - for item in self.iter() { - debug!("[*]: item: {:?}", item); - let ch = match *item { - BookItem::Spacer => continue, - BookItem::Chapter(_, ref ch) | - BookItem::Affix(ref ch) => ch, - }; - if !ch.path.as_os_str().is_empty() { - let path = self.config.get_source().join(&ch.path); - - if !path.exists() { - if !self.create_missing { - return Err(format!("'{}' referenced from SUMMARY.md does not exist.", path.to_string_lossy()) - .into()); - } - debug!("[*]: {:?} does not exist, trying to create file", path); - ::std::fs::create_dir_all(path.parent().unwrap())?; - let mut f = File::create(path)?; - - // debug!("[*]: Writing to {:?}", path); - writeln!(f, "# {}", ch.name)?; - } - } - } + debug!("[*]: Creating a chapter"); + let mut chapter_1 = File::create(root.join("src").join("chapter_1.md"))?; + writeln!(chapter_1, "# Chapter 1")?; + writeln!(chapter_1, "")?; + writeln!(chapter_1, "TODO: Create some content.")?; debug!("[*]: init done"); - Ok(()) + + MDBook::new(root) } pub fn create_gitignore(&self) { let gitignore = self.get_gitignore(); - let destination = self.config.get_html_config() - .get_destination(); + let destination = self.config.get_html_config().get_destination(); - // Check that the gitignore does not extist and that the destination path begins with the root path - // We assume tha if it does begin with the root path it is contained within. This assumption - // will not hold true for paths containing double dots to go back up e.g. `root/../destination` + // Check that the gitignore does not exist and that the destination + // path begins with the root path We assume tha if it does begin with + // the root path it is contained within. This assumption will not hold + // true for paths containing double dots to go back up e.g. + // `root/../destination`. if !gitignore.exists() && destination.starts_with(self.config.get_root()) { let relative = destination @@ -234,8 +195,6 @@ impl MDBook { pub fn build(&mut self) -> Result<()> { debug!("[fn]: build"); - self.init()?; - // Clean output directory utils::fs::remove_dir_content(self.config.get_html_config().get_destination())?; @@ -284,12 +243,13 @@ impl MDBook { } pub fn write_file>(&self, filename: P, content: &[u8]) -> Result<()> { - let path = self.get_destination() - .join(filename); + let path = self.get_destination().join(filename); - utils::fs::create_file(&path)? - .write_all(content) - .map_err(|e| e.into()) + utils::fs::create_file(&path)?.write_all(content).map_err( + |e| { + e.into() + }, + ) } /// Parses the `book.json` file (if it exists) to extract @@ -375,6 +335,7 @@ impl MDBook { } } } + Ok(()) } @@ -385,15 +346,16 @@ impl MDBook { pub fn with_destination>(mut self, destination: T) -> Self { let root = self.config.get_root().to_owned(); - self.config.get_mut_html_config() - .set_destination(&root, &destination.into()); + self.config.get_mut_html_config().set_destination( + &root, + &destination.into(), + ); self } pub fn get_destination(&self) -> &Path { - self.config.get_html_config() - .get_destination() + self.config.get_html_config().get_destination() } pub fn with_source>(mut self, source: T) -> Self { @@ -439,67 +401,69 @@ impl MDBook { pub fn with_theme_path>(mut self, theme_path: T) -> Self { let root = self.config.get_root().to_owned(); - self.config.get_mut_html_config() - .set_theme(&root, &theme_path.into()); + self.config.get_mut_html_config().set_theme( + &root, + &theme_path.into(), + ); self } pub fn get_theme_path(&self) -> &Path { - self.config.get_html_config() - .get_theme() + self.config.get_html_config().get_theme() } pub fn with_curly_quotes(mut self, curly_quotes: bool) -> Self { - self.config.get_mut_html_config() - .set_curly_quotes(curly_quotes); + self.config.get_mut_html_config().set_curly_quotes( + curly_quotes, + ); self } pub fn get_curly_quotes(&self) -> bool { - self.config.get_html_config() - .get_curly_quotes() + self.config.get_html_config().get_curly_quotes() } pub fn with_mathjax_support(mut self, mathjax_support: bool) -> Self { - self.config.get_mut_html_config() - .set_mathjax_support(mathjax_support); + self.config.get_mut_html_config().set_mathjax_support( + mathjax_support, + ); self } pub fn get_mathjax_support(&self) -> bool { - self.config.get_html_config() - .get_mathjax_support() + self.config.get_html_config().get_mathjax_support() } pub fn get_google_analytics_id(&self) -> Option { - self.config.get_html_config() - .get_google_analytics_id() + self.config.get_html_config().get_google_analytics_id() } pub fn has_additional_js(&self) -> bool { - self.config.get_html_config() - .has_additional_js() + self.config.get_html_config().has_additional_js() } pub fn get_additional_js(&self) -> &[PathBuf] { - self.config.get_html_config() - .get_additional_js() + self.config.get_html_config().get_additional_js() } pub fn has_additional_css(&self) -> bool { - self.config.get_html_config() - .has_additional_css() + self.config.get_html_config().has_additional_css() } pub fn get_additional_css(&self) -> &[PathBuf] { - self.config.get_html_config() - .get_additional_css() - } - - // Construct book - fn parse_summary(&mut self) -> Result<()> { - // When append becomes stable, use self.content.append() ... - self.content = parse::construct_bookitems(&self.get_source().join("SUMMARY.md"))?; - Ok(()) + self.config.get_html_config().get_additional_css() } } + +fn test_chapter(ch: &Chapter, tmp: &TempDir) -> Result<()> { + let path = tmp.path().join(&ch.name); + File::create(&path)?.write_all(ch.content.as_bytes())?; + + let output = Command::new("rustdoc").arg(&path).arg("--test").output()?; + + if !output.status.success() { + bail!(ErrorKind::Subprocess("Rustdoc returned an error".to_string(), output)); + } + + Ok(()) +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 9ad46c3d..1db6e614 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,6 +87,8 @@ extern crate serde; extern crate serde_json; #[cfg(test)] +#[macro_use] +extern crate pretty_assertions; extern crate tempdir; mod parse; @@ -99,7 +101,7 @@ pub mod utils; pub mod loader; pub use book::MDBook; -pub use book::BookItem; +pub use loader::{Book, BookItem}; pub use renderer::Renderer; /// The error types used through out this crate. diff --git a/src/loader/book.rs b/src/loader/book.rs index 803c4020..0ff4a89c 100644 --- a/src/loader/book.rs +++ b/src/loader/book.rs @@ -2,6 +2,7 @@ use std::path::Path; use std::collections::VecDeque; use std::fs::File; use std::io::Read; +use std::fmt; use loader::summary::{Summary, Link, SummaryItem, SectionNumber}; use errors::*; @@ -60,6 +61,28 @@ impl Chapter { ..Default::default() } } + + /// Get this chapter's location in the book structure, relative to the + /// root. + /// + /// # Note + /// + /// This **may not** be the same as the source file's location on disk! + /// Rather, it reflects the chapter's location in the `Book` tree + /// structure. + pub fn path(&self) -> &Path { + unimplemented!() + } +} + +impl fmt::Display for Chapter { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(ref number) = self.number { + write!(f, "{}) ", number)?; + } + + write!(f, "{}", self.name) + } } /// Use the provided `Summary` to load a `Book` from disk. diff --git a/src/parse/mod.rs b/src/parse/mod.rs deleted file mode 100644 index c8c8aab7..00000000 --- a/src/parse/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub use self::summary::construct_bookitems; - -pub mod summary; diff --git a/src/parse/summary.rs b/src/parse/summary.rs deleted file mode 100644 index cc8452d4..00000000 --- a/src/parse/summary.rs +++ /dev/null @@ -1,231 +0,0 @@ -use std::path::PathBuf; -use std::fs::File; -use std::io::{Read, Result, Error, ErrorKind}; -use book::bookitem::{BookItem, Chapter}; - -pub fn construct_bookitems(path: &PathBuf) -> Result> { - debug!("[fn]: construct_bookitems"); - let mut summary = String::new(); - File::open(path)?.read_to_string(&mut summary)?; - - debug!("[*]: Parse SUMMARY.md"); - let top_items = parse_level(&mut summary.split('\n').collect(), 0, vec![0])?; - debug!("[*]: Done parsing SUMMARY.md"); - Ok(top_items) -} - -fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec) -> Result> { - debug!("[fn]: parse_level"); - let mut items: Vec = vec![]; - - // Construct the book recursively - while !summary.is_empty() { - let item: BookItem; - // Indentation level of the line to parse - let level = level(summary[0], 4)?; - - // if level < current_level we remove the last digit of section, - // exit the current function, - // and return the parsed level to the calling function. - if level < current_level { - break; - } - - // if level > current_level we call ourselves to go one level deeper - if level > current_level { - // Level can not be root level !! - // Add a sub-number to section - section.push(0); - let last = items - .pop() - .expect("There should be at least one item since this can't be the root level"); - - if let BookItem::Chapter(ref s, ref ch) = last { - let mut ch = ch.clone(); - ch.sub_items = parse_level(summary, level, section.clone())?; - items.push(BookItem::Chapter(s.clone(), ch)); - - // Remove the last number from the section, because we got back to our level.. - section.pop(); - continue; - } else { - return Err(Error::new(ErrorKind::Other, - "Your summary.md is messed up\n\n - Prefix, \ - Suffix and Spacer elements can only exist on the root level.\n - \ - Prefix elements can only exist before any chapter and there can be \ - no chapters after suffix elements.")); - }; - - } else { - // level and current_level are the same, parse the line - item = if let Some(parsed_item) = parse_line(summary[0]) { - - // Eliminate possible errors and set section to -1 after suffix - match parsed_item { - // error if level != 0 and BookItem is != Chapter - BookItem::Affix(_) | - BookItem::Spacer if level > 0 => { - return Err(Error::new(ErrorKind::Other, - "Your summary.md is messed up\n\n - \ - Prefix, Suffix and Spacer elements can only exist on the \ - root level.\n - Prefix \ - elements can only exist before any chapter and there can be \ - no chapters after suffix elements.")) - }, - - // error if BookItem == Chapter and section == -1 - BookItem::Chapter(_, _) if section[0] == -1 => { - return Err(Error::new(ErrorKind::Other, - "Your summary.md is messed up\n\n - \ - Prefix, Suffix and Spacer elements can only exist on the \ - root level.\n - Prefix \ - elements can only exist before any chapter and there can be \ - no chapters after suffix elements.")) - }, - - // Set section = -1 after suffix - BookItem::Affix(_) if section[0] > 0 => { - section[0] = -1; - }, - - _ => {}, - } - - match parsed_item { - BookItem::Chapter(_, ch) => { - // Increment section - let len = section.len() - 1; - section[len] += 1; - let s = section - .iter() - .fold("".to_owned(), |s, i| s + &i.to_string() + "."); - BookItem::Chapter(s, ch) - }, - _ => parsed_item, - } - - } else { - // If parse_line does not return Some(_) continue... - summary.remove(0); - continue; - }; - } - - summary.remove(0); - items.push(item) - } - debug!("[*]: Level: {:?}", items); - Ok(items) -} - - -fn level(line: &str, spaces_in_tab: i32) -> Result { - debug!("[fn]: level"); - let mut spaces = 0; - let mut level = 0; - - for ch in line.chars() { - match ch { - ' ' => spaces += 1, - '\t' => level += 1, - _ => break, - } - if spaces >= spaces_in_tab { - level += 1; - spaces = 0; - } - } - - // If there are spaces left, there is an indentation error - if spaces > 0 { - debug!("[SUMMARY.md]:"); - debug!("\t[line]: {}", line); - debug!("[*]: There is an indentation error on this line. Indentation should be {} spaces", spaces_in_tab); - return Err(Error::new(ErrorKind::Other, format!("Indentation error on line:\n\n{}", line))); - } - - Ok(level) -} - - -fn parse_line(l: &str) -> Option { - debug!("[fn]: parse_line"); - - // Remove leading and trailing spaces or tabs - let line = l.trim_matches(|c: char| c == ' ' || c == '\t'); - - // Spacers are "------" - if line.starts_with("--") { - debug!("[*]: Line is spacer"); - return Some(BookItem::Spacer); - } - - if let Some(c) = line.chars().nth(0) { - match c { - // List item - '-' | '*' => { - debug!("[*]: Line is list element"); - - if let Some((name, path)) = read_link(line) { - return Some(BookItem::Chapter("0".to_owned(), Chapter::new(name, path))); - } else { - return None; - } - }, - // Non-list element - '[' => { - debug!("[*]: Line is a link element"); - - if let Some((name, path)) = read_link(line) { - return Some(BookItem::Affix(Chapter::new(name, path))); - } else { - return None; - } - }, - _ => {}, - } - } - - None -} - -fn read_link(line: &str) -> Option<(String, PathBuf)> { - let mut start_delimitor; - let mut end_delimitor; - - // In the future, support for list item that is not a link - // Not sure if I should error on line I can't parse or just ignore them... - if let Some(i) = line.find('[') { - start_delimitor = i; - } else { - debug!("[*]: '[' not found, this line is not a link. Ignoring..."); - return None; - } - - if let Some(i) = line[start_delimitor..].find("](") { - end_delimitor = start_delimitor + i; - } else { - debug!("[*]: '](' not found, this line is not a link. Ignoring..."); - return None; - } - - let name = line[start_delimitor + 1..end_delimitor].to_owned(); - - start_delimitor = end_delimitor + 1; - if let Some(i) = line[start_delimitor..].find(')') { - end_delimitor = start_delimitor + i; - } else { - debug!("[*]: ')' not found, this line is not a link. Ignoring..."); - return None; - } - - let path = PathBuf::from(line[start_delimitor + 1..end_delimitor].to_owned()); - - Some((name, path)) -} diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 42e6d5db..50621958 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -2,7 +2,7 @@ use renderer::html_handlebars::helpers; use preprocess; use renderer::Renderer; use book::MDBook; -use book::bookitem::{BookItem, Chapter}; +use loader::{BookItem, Chapter}; use utils; use theme::{self, Theme}; use errors::*; @@ -28,8 +28,7 @@ impl HtmlHandlebars { HtmlHandlebars } - fn render_item(&self, item: &BookItem, mut ctx: RenderItemContext, print_content: &mut String) - -> Result<()> { + fn render_item(&self, item: &BookItem, mut ctx: RenderItemContext, print_content: &mut String) -> Result<()> { // FIXME: This should be made DRY-er and rely less on mutable state match *item { BookItem::Chapter(_, ref ch) | @@ -92,15 +91,10 @@ impl HtmlHandlebars { fn render_index(&self, book: &MDBook, ch: &Chapter, destination: &Path) -> Result<()> { debug!("[*]: index.html"); - let mut content = String::new(); - - File::open(destination.join(&ch.path.with_extension("html")))? - .read_to_string(&mut content)?; - // This could cause a problem when someone displays // code containing // on the front page, however this case should be very very rare... - content = content + let content = ch.content .lines() .filter(|line| !line.contains(" Result<()> { let mut data = Vec::new(); @@ -362,22 +357,18 @@ fn make_data(book: &MDBook) -> Result let mut chapter = BTreeMap::new(); match *item { - BookItem::Affix(ref ch) => { + BookItem::Chapter(ref ch) => { + if let Some(ref section) = ch.number { + chapter.insert("section".to_owned(), json!(section.to_string())); + } chapter.insert("name".to_owned(), json!(ch.name)); - let path = ch.path.to_str().ok_or_else(|| { + + let path = ch.path().to_str().ok_or_else(|| { io::Error::new(io::ErrorKind::Other, "Could not convert path to str") })?; chapter.insert("path".to_owned(), json!(path)); }, - BookItem::Chapter(ref s, ref ch) => { - chapter.insert("section".to_owned(), json!(s)); - chapter.insert("name".to_owned(), json!(ch.name)); - let path = ch.path.to_str().ok_or_else(|| { - io::Error::new(io::ErrorKind::Other, "Could not convert path to str") - })?; - chapter.insert("path".to_owned(), json!(path)); - }, - BookItem::Spacer => { + BookItem::Separator => { chapter.insert("spacer".to_owned(), json!("_spacer_")); },