Integrated the new Book structure into MDBook

This commit is contained in:
Michael Bryan 2017-07-08 19:39:05 +08:00
parent 01301d9951
commit 65a60bf81f
8 changed files with 127 additions and 467 deletions

View File

@ -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"

View File

@ -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<BookItem>,
}
#[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<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
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);
}
}
}
}

View File

@ -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<BookItem>,
book: Book,
renderer: Box<Renderer>,
livereload: Option<String>,
@ -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<P: Into<PathBuf>>(root: P) -> MDBook {
pub fn new<P: AsRef<Path>>(root: P) -> Result<MDBook> {
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<P: AsRef<Path>>(root: P) -> Result<MDBook> {
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<P: AsRef<Path>>(&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<T: Into<PathBuf>>(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<T: Into<PathBuf>>(mut self, source: T) -> Self {
@ -439,67 +401,69 @@ impl MDBook {
pub fn with_theme_path<T: Into<PathBuf>>(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<String> {
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(())
}

View File

@ -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.

View File

@ -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.

View File

@ -1,3 +0,0 @@
pub use self::summary::construct_bookitems;
pub mod summary;

View File

@ -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<Vec<BookItem>> {
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<i32>) -> Result<Vec<BookItem>> {
debug!("[fn]: parse_level");
let mut items: Vec<BookItem> = 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<i32> {
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<BookItem> {
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))
}

View File

@ -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 <base href=...>
// on the front page, however this case should be very very rare...
content = content
let content = ch.content
.lines()
.filter(|line| !line.contains("<base href="))
.collect::<Vec<&str>>()
@ -110,8 +104,9 @@ impl HtmlHandlebars {
info!(
"[*] Creating index.html from {:?} ✓",
book.get_destination()
.join(&ch.path.with_extension("html"))
book.get_destination().join(
ch.path().with_extension("html"),
)
);
Ok(())
@ -362,22 +357,18 @@ fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>
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_"));
},