Fleshed out book creation

This commit is contained in:
Michael Bryan 2017-11-18 21:22:30 +08:00
parent 47eb4788cb
commit 8b21da9950
No known key found for this signature in database
GPG Key ID: E9C602B0D9A998DC
3 changed files with 203 additions and 159 deletions

View File

@ -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<MDBook> {
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(())
// }

View File

@ -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<S: Serializer>(&self, s: S) -> ::std::result::Result<S::Ok, S::Error> {
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"];

View File

@ -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",
const TOC_TOP_LEVEL: &[&'static str] = &[
"1. First Chapter",
"2. Second Chapter",
"Conclusion",
"Introduction"];
"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""#,
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""#];
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#"<base href="../">"#,
let links = vec![
r#"<base href="../">"#,
r#"href="intro.html""#,
r#"href="./first/index.html""#,
r#"href="./first/nested.html""#,
r#"href="./second.html""#,
r#"href="./conclusion.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"),
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")];
("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()
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")
});
.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())
let rendered_location = temp.path()
.join(chapter.strip_prefix(&src).unwrap())
.with_extension("html");
assert!(rendered_location.exists(),
assert!(
rendered_location.exists(),
"{} doesn't exits",
rendered_location.display());
rendered_location.display()
);
}
}
@ -178,9 +195,11 @@ 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<Document> {
let temp = DummyBook::new().build()
let temp = DummyBook::new()
.build()
.chain_err(|| "Couldn't create the dummy book")?;
MDBook::load(temp.path())?.build()
MDBook::load(temp.path())?
.build()
.chain_err(|| "Book building failed")?;
let index_page = temp.path().join("book").join("index.html");
@ -197,8 +216,8 @@ 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())
let mut children_of_children: Vec<_> = doc.find(pred)
.map(|elem| elem.text().trim().to_string())
.collect();
children_of_children.sort();
@ -215,7 +234,8 @@ 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())
let mut children: Vec<_> = doc.find(pred)
.map(|elem| elem.text().trim().to_string())
.collect();
children.sort();
@ -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<bool>) -> (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();