From 8b21da9950576a71aefd098cfcef37f7a74fc569 Mon Sep 17 00:00:00 2001 From: Michael Bryan Date: Sat, 18 Nov 2017 21:22:30 +0800 Subject: [PATCH] Fleshed out book creation --- src/book/init.rs | 221 ++++++++++++++++++++------------------- src/config.rs | 20 +++- tests/rendered_output.rs | 121 ++++++++++++--------- 3 files changed, 203 insertions(+), 159 deletions(-) diff --git a/src/book/init.rs b/src/book/init.rs index a94d787d..c97b533b 100644 --- a/src/book/init.rs +++ b/src/book/init.rs @@ -1,6 +1,11 @@ +use std::fs::{self, File}; use std::path::PathBuf; +use std::io::Write; +use toml; + use config::Config; use super::MDBook; +use theme; use errors::*; @@ -44,113 +49,113 @@ impl BookBuilder { } pub fn build(&self) -> Result { - unimplemented!() + info!("Creating a new book with stub content"); + + self.create_directory_structure() + .chain_err(|| "Unable to create directory structure")?; + + self.create_stub_files() + .chain_err(|| "Unable to create stub files")?; + + if self.create_gitignore { + self.build_gitignore() + .chain_err(|| "Unable to create .gitignore")?; + } + + if self.copy_theme { + self.copy_across_theme() + .chain_err(|| "Unable to copy across the theme")?; + } + + self.write_book_toml()?; + + let book = MDBook::load(&self.root) + .expect("The BookBuilder should always create a valid book. \ + If you are seeing this it is a bug and should be reported."); + + Ok(book) + } + + fn write_book_toml(&self) -> Result<()> { + debug!("[*] Writing book.toml"); + let book_toml = self.root.join("book.toml"); + let cfg = toml::to_vec(&self.config) + .chain_err(|| "Unable to serialize the config")?; + + File::create(book_toml) + .chain_err(|| "Couldn't create book.toml")? + .write_all(&cfg) + .chain_err(|| "Unable to write config to book.toml")?; + Ok(()) + } + + fn copy_across_theme(&self) -> Result<()> { + debug!("[*] Copying theme"); + + let themedir = self.config.html_config() + .and_then(|html| html.theme) + .unwrap_or_else(|| self.config.book.src.join("theme")); + let themedir = self.root.join(themedir); + + if !themedir.exists() { + debug!("[*]: {:?} does not exist, creating the directory", + themedir); fs::create_dir(&themedir)?; + } + + let mut index = File::create(themedir.join("index.hbs"))?; + index.write_all(theme::INDEX)?; + + let mut css = File::create(themedir.join("book.css"))?; + css.write_all(theme::CSS)?; + + let mut favicon = File::create(themedir.join("favicon.png"))?; + favicon.write_all(theme::FAVICON)?; + + let mut js = File::create(themedir.join("book.js"))?; + js.write_all(theme::JS)?; + + let mut highlight_css = File::create(themedir.join("highlight.css"))?; + highlight_css.write_all(theme::HIGHLIGHT_CSS)?; + + let mut highlight_js = File::create(themedir.join("highlight.js"))?; + highlight_js.write_all(theme::HIGHLIGHT_JS)?; + + Ok(()) + } + + fn build_gitignore(&self) -> Result<()> { + debug!("[*]: Creating .gitignore"); + + Ok(()) + } + + fn create_stub_files(&self) -> Result<()> { + debug!("[*] Creating example book contents"); + let src_dir = self.root.join(&self.config.book.src); + + let summary = src_dir.join("SUMMARY.md"); + let mut f = File::create(&summary).chain_err(|| "Unable to create SUMMARY.md")?; + writeln!(f, "# Summary")?; + writeln!(f, "")?; + writeln!(f, "- [Chapter 1](./chapter_1.md)")?; + + let chapter_1 = src_dir.join("chapter_1.md"); + let mut f = File::create(&chapter_1).chain_err(|| "Unable to create chapter_1.md")?; + writeln!(f, "# Chapter 1")?; + + Ok(()) + } + + fn create_directory_structure(&self) -> Result<()> { + debug!("[*]: Creating directory tree"); + fs::create_dir_all(&self.root)?; + + let src = self.root.join(&self.config.book.src); + fs::create_dir_all(&src)?; + + let build = self.root.join(&self.config.build.build_dir); + fs::create_dir_all(&build)?; + + Ok(()) } } - -// contents of old `init()` function: - -// debug!("[fn]: init"); - -// if !self.root.exists() { -// fs::create_dir_all(&self.root).unwrap(); -// info!("{:?} created", self.root.display()); -// } - -// { -// let dest = self.get_destination(); -// if !dest.exists() { -// debug!("[*]: {} does not exist, trying to create directory", -// dest.display()); fs::create_dir_all(dest)?; -// } - - -// let src = self.get_source(); -// if !src.exists() { -// debug!("[*]: {} does not exist, trying to create directory", -// src.display()); fs::create_dir_all(&src)?; -// } - -// let summary = src.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)")?; -// } -// } - -// // parse SUMMARY.md, and create the missing item related file -// self.parse_summary()?; - -// 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.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!("[*]: init done"); -// Ok(()) - -// pub fn copy_theme(&self) -> Result<()> { -// debug!("[fn]: copy_theme"); - -// let themedir = self.theme_dir(); - -// if !themedir.exists() { -// debug!("[*]: {:?} does not exist, trying to create directory", -// themedir); fs::create_dir(&themedir)?; -// } - -// // index.hbs -// let mut index = File::create(themedir.join("index.hbs"))?; -// index.write_all(theme::INDEX)?; - -// // book.css -// let mut css = File::create(themedir.join("book.css"))?; -// css.write_all(theme::CSS)?; - -// // favicon.png -// let mut favicon = File::create(themedir.join("favicon.png"))?; -// favicon.write_all(theme::FAVICON)?; - -// // book.js -// let mut js = File::create(themedir.join("book.js"))?; -// js.write_all(theme::JS)?; - -// // highlight.css -// let mut highlight_css = File::create(themedir.join("highlight.css"))?; -// highlight_css.write_all(theme::HIGHLIGHT_CSS)?; - -// // highlight.js -// let mut highlight_js = File::create(themedir.join("highlight.js"))?; -// highlight_js.write_all(theme::HIGHLIGHT_JS)?; - -// Ok(()) -// } diff --git a/src/config.rs b/src/config.rs index d23e7a80..acc8beea 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,7 +3,7 @@ use std::fs::File; use std::io::Read; use toml::{self, Value}; use toml::value::Table; -use serde::{Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use errors::*; @@ -187,6 +187,24 @@ impl<'de> Deserialize<'de> for Config { } } +impl Serialize for Config { + fn serialize(&self, s: S) -> ::std::result::Result { + let mut table = self.rest.clone(); + + let book_config = match Value::try_from(self.book.clone()) { + Ok(cfg) => cfg, + Err(_) => { + use serde::ser::Error; + return Err(S::Error::custom("Unable to serialize the BookConfig")); + } + }; + + table.insert("book".to_string(), book_config); + + Value::Table(table).serialize(s) + } +} + fn is_legacy_format(table: &Table) -> bool { let top_level_items = ["title", "author", "authors"]; diff --git a/tests/rendered_output.rs b/tests/rendered_output.rs index 5a2a6f91..a68031ca 100644 --- a/tests/rendered_output.rs +++ b/tests/rendered_output.rs @@ -9,7 +9,7 @@ mod dummy_book; use dummy_book::{assert_contains_strings, DummyBook}; -use std::fs::{File, remove_file}; +use std::fs::{remove_file, File}; use std::io::Write; use std::path::Path; use std::ffi::OsStr; @@ -23,10 +23,12 @@ use mdbook::MDBook; const BOOK_ROOT: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/dummy_book"); -const TOC_TOP_LEVEL: &[&'static str] = &["1. First Chapter", - "2. Second Chapter", - "Conclusion", - "Introduction"]; +const TOC_TOP_LEVEL: &[&'static str] = &[ + "1. First Chapter", + "2. Second Chapter", + "Conclusion", + "Introduction", +]; const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter"]; /// Make sure you can load the dummy book and build it without panicking. @@ -57,11 +59,13 @@ fn make_sure_bottom_level_files_contain_links_to_chapters() { md.build().unwrap(); let dest = temp.path().join("book"); - let links = vec![r#"href="intro.html""#, - r#"href="./first/index.html""#, - r#"href="./first/nested.html""#, - r#"href="./second.html""#, - r#"href="./conclusion.html""#]; + let links = vec![ + r#"href="intro.html""#, + r#"href="./first/index.html""#, + r#"href="./first/nested.html""#, + r#"href="./second.html""#, + r#"href="./conclusion.html""#, + ]; let files_in_bottom_dir = vec!["index.html", "intro.html", "second.html", "conclusion.html"]; @@ -77,12 +81,14 @@ fn check_correct_cross_links_in_nested_dir() { md.build().unwrap(); let first = temp.path().join("book").join("first"); - let links = vec![r#""#, - r#"href="intro.html""#, - r#"href="./first/index.html""#, - r#"href="./first/nested.html""#, - r#"href="./second.html""#, - r#"href="./conclusion.html""#]; + let links = vec![ + r#""#, + r#"href="intro.html""#, + r#"href="./first/index.html""#, + r#"href="./first/nested.html""#, + r#"href="./second.html""#, + r#"href="./conclusion.html""#, + ]; let files_in_nested_dir = vec!["index.html", "nested.html"]; @@ -90,11 +96,19 @@ fn check_correct_cross_links_in_nested_dir() { assert_contains_strings(first.join(filename), &links); } - assert_contains_strings(first.join("index.html"), - &[r##"href="./first/index.html#some-section" id="some-section""##]); + assert_contains_strings( + first.join("index.html"), + &[ + r##"href="./first/index.html#some-section" id="some-section""##, + ], + ); - assert_contains_strings(first.join("nested.html"), - &[r##"href="./first/nested.html#some-section" id="some-section""##]); + assert_contains_strings( + first.join("nested.html"), + &[ + r##"href="./first/nested.html#some-section" id="some-section""##, + ], + ); } #[test] @@ -114,11 +128,13 @@ fn rendered_code_has_playpen_stuff() { #[test] fn chapter_content_appears_in_rendered_document() { - let content = vec![("index.html", "Here's some interesting text"), - ("second.html", "Second Chapter"), - ("first/nested.html", "testable code"), - ("first/index.html", "more text"), - ("conclusion.html", "Conclusion")]; + let content = vec![ + ("index.html", "Here's some interesting text"), + ("second.html", "Second Chapter"), + ("first/nested.html", "testable code"), + ("first/index.html", "more text"), + ("conclusion.html", "Conclusion"), + ]; let temp = DummyBook::new().build().unwrap(); let mut md = MDBook::load(temp.path()).unwrap(); @@ -153,21 +169,22 @@ fn chapter_files_were_rendered_to_html() { let temp = DummyBook::new().build().unwrap(); let src = Path::new(BOOK_ROOT).join("src"); - let chapter_files = WalkDir::new(&src).into_iter() - .filter_entry(|entry| entry_ends_with(entry, ".md")) - .filter_map(|entry| entry.ok()) - .map(|entry| entry.path().to_path_buf()) - .filter(|path| { - path.file_name().and_then(OsStr::to_str) - != Some("SUMMARY.md") - }); + let chapter_files = WalkDir::new(&src) + .into_iter() + .filter_entry(|entry| entry_ends_with(entry, ".md")) + .filter_map(|entry| entry.ok()) + .map(|entry| entry.path().to_path_buf()) + .filter(|path| path.file_name().and_then(OsStr::to_str) != Some("SUMMARY.md")); for chapter in chapter_files { - let rendered_location = temp.path().join(chapter.strip_prefix(&src).unwrap()) - .with_extension("html"); - assert!(rendered_location.exists(), - "{} doesn't exits", - rendered_location.display()); + let rendered_location = temp.path() + .join(chapter.strip_prefix(&src).unwrap()) + .with_extension("html"); + assert!( + rendered_location.exists(), + "{} doesn't exits", + rendered_location.display() + ); } } @@ -178,10 +195,12 @@ fn entry_ends_with(entry: &DirEntry, ending: &str) -> bool { /// Read the main page (`book/index.html`) and expose it as a DOM which we /// can search with the `select` crate fn root_index_html() -> Result { - let temp = DummyBook::new().build() - .chain_err(|| "Couldn't create the dummy book")?; - MDBook::load(temp.path())?.build() - .chain_err(|| "Book building failed")?; + let temp = DummyBook::new() + .build() + .chain_err(|| "Couldn't create the dummy book")?; + MDBook::load(temp.path())? + .build() + .chain_err(|| "Book building failed")?; let index_page = temp.path().join("book").join("index.html"); let html = file_to_string(&index_page).chain_err(|| "Unable to read index.html")?; @@ -197,9 +216,9 @@ fn check_second_toc_level() { let pred = descendants!(Class("chapter"), Name("li"), Name("li"), Name("a")); - let mut children_of_children: Vec<_> = - doc.find(pred).map(|elem| elem.text().trim().to_string()) - .collect(); + let mut children_of_children: Vec<_> = doc.find(pred) + .map(|elem| elem.text().trim().to_string()) + .collect(); children_of_children.sort(); assert_eq!(children_of_children, should_be); @@ -215,8 +234,9 @@ fn check_first_toc_level() { let pred = descendants!(Class("chapter"), Name("li"), Name("a")); - let mut children: Vec<_> = doc.find(pred).map(|elem| elem.text().trim().to_string()) - .collect(); + let mut children: Vec<_> = doc.find(pred) + .map(|elem| elem.text().trim().to_string()) + .collect(); children.sort(); assert_eq!(children, should_be); @@ -227,8 +247,8 @@ fn check_spacers() { let doc = root_index_html().unwrap(); let should_be = 1; - let num_spacers = - doc.find(Class("chapter").descendant(Name("li").and(Class("spacer")))).count(); + let num_spacers = doc.find(Class("chapter").descendant(Name("li").and(Class("spacer")))) + .count(); assert_eq!(num_spacers, should_be); } @@ -269,7 +289,8 @@ fn create_missing_setup(create_missing: Option) -> (MDBook, TempDir) { let mut file = File::create(temp.path().join("book.toml")).unwrap(); match create_missing { Some(true) => file.write_all(b"[build]\ncreate-missing = true\n").unwrap(), - Some(false) => file.write_all(b"[build]\ncreate-missing = false\n").unwrap(), + Some(false) => file.write_all(b"[build]\ncreate-missing = false\n") + .unwrap(), None => (), } file.flush().unwrap();