Merge pull request #491 from Michael-F-Bryan/book-representation-3

WIP: Book representation - Attempt 3
This commit is contained in:
Michael Bryan 2017-12-12 19:24:13 +11:00 committed by GitHub
commit d69bc9c7c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2119 additions and 815 deletions

View File

@ -26,6 +26,7 @@ lazy_static = "0.2"
log = "0.3" log = "0.3"
env_logger = "0.4.0" env_logger = "0.4.0"
toml = "0.4" toml = "0.4"
memchr = "2.0.1"
open = "1.1" open = "1.1"
regex = "0.2.1" regex = "0.2.1"
tempdir = "0.3.4" tempdir = "0.3.4"
@ -60,3 +61,6 @@ serve = ["iron", "staticfile", "ws"]
doc = false doc = false
name = "mdbook" name = "mdbook"
path = "src/bin/mdbook.rs" path = "src/bin/mdbook.rs"
[patch.crates-io]
pulldown-cmark = { git = "https://github.com/google/pulldown-cmark" }

View File

@ -1,3 +1,4 @@
[book]
title = "mdBook Documentation" title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust" description = "Create book from markdown files. Like Gitbook but implemented in Rust"
author = "Mathieu David" author = "Mathieu David"

View File

@ -1,5 +1,5 @@
use std::path::PathBuf; use std::path::PathBuf;
use clap::{ArgMatches, SubCommand, App}; use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook; use mdbook::MDBook;
use mdbook::errors::Result; use mdbook::errors::Result;
use {get_book_dir, open}; use {get_book_dir, open};
@ -10,32 +10,23 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.about("Build the book from the markdown files") .about("Build the book from the markdown files")
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'") .arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
.arg_from_usage( .arg_from_usage(
"-d, --dest-dir=[dest-dir] 'The output directory for your \ "-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book \
book{n}(Defaults to ./book when omitted)'",
)
.arg_from_usage(
"--no-create 'Will not create non-existent files linked from SUMMARY.md (deprecated: use book.toml instead)'",
)
.arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to Current Directory \
when omitted)'", when omitted)'",
) )
.arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'",
)
} }
// Build command implementation // Build command implementation
pub fn execute(args: &ArgMatches) -> Result<()> { pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config()?; let mut book = MDBook::load(&book_dir)?;
if let Some(dest_dir) = args.value_of("dest-dir") { if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = PathBuf::from(dest_dir); book.config.build.build_dir = PathBuf::from(dest_dir);
} }
// This flag is deprecated in favor of being set via `book.toml`.
if args.is_present("no-create") {
book.config.build.create_missing = false;
}
book.build()?; book.build()?;
if args.is_present("open") { if args.is_present("open") {

View File

@ -19,45 +19,31 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
// Init command implementation // Init command implementation
pub fn execute(args: &ArgMatches) -> Result<()> { pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir); let mut builder = MDBook::init(&book_dir);
// Call the function that does the initialization
book.init()?;
// If flag `--theme` is present, copy theme to src // If flag `--theme` is present, copy theme to src
if args.is_present("theme") { if args.is_present("theme") {
// Skip this if `--force` is present // Skip this if `--force` is present
if !args.is_present("force") { if !args.is_present("force") {
// Print warning // Print warning
print!("\nCopying the default theme to {:?}", book.get_source()); print!("\nCopying the default theme to {}", builder.config().book.src.display());
println!("could potentially overwrite files already present in that directory."); println!("This could potentially overwrite files already present in that directory.");
print!("\nAre you sure you want to continue? (y/n) "); print!("\nAre you sure you want to continue? (y/n) ");
// Read answer from user and exit if it's not 'yes' // Read answer from user and exit if it's not 'yes'
if !confirm() { if confirm() {
println!("\nSkipping...\n"); builder.copy_theme(true);
println!("All done, no errors...");
::std::process::exit(0);
} }
} }
// Call the function that copies the theme
book.copy_theme()?;
println!("\nTheme copied.");
} }
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root` println!("\nDo you want a .gitignore to be created? (y/n)");
let is_dest_inside_root = book.get_destination().starts_with(&book.root);
if !args.is_present("force") && is_dest_inside_root { if confirm() {
println!("\nDo you want a .gitignore to be created? (y/n)"); builder.create_gitignore(true);
if confirm() {
book.create_gitignore();
println!("\n.gitignore created.");
}
} }
builder.build()?;
println!("\nAll done, no errors..."); println!("\nAll done, no errors...");
Ok(()) Ok(())

View File

@ -1,17 +1,18 @@
#[macro_use] #[macro_use]
extern crate clap; extern crate clap;
extern crate env_logger; extern crate env_logger;
extern crate error_chain;
extern crate log; extern crate log;
extern crate mdbook; extern crate mdbook;
extern crate open; extern crate open;
use std::env; use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::io::{self, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use clap::{App, AppSettings, ArgMatches}; use clap::{App, AppSettings, ArgMatches};
use log::{LogLevelFilter, LogRecord}; use log::{LogLevelFilter, LogRecord};
use env_logger::LogBuilder; use env_logger::LogBuilder;
use error_chain::ChainedError;
pub mod build; pub mod build;
pub mod init; pub mod init;
@ -59,7 +60,8 @@ fn main() {
}; };
if let Err(e) = res { if let Err(e) = res {
writeln!(&mut io::stderr(), "An error occured:\n{}", e).ok(); eprintln!("{}", e.display_chain());
::std::process::exit(101); ::std::process::exit(101);
} }
} }

View File

@ -49,7 +49,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
const RELOAD_COMMAND: &'static str = "reload"; const RELOAD_COMMAND: &'static str = "reload";
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config()?; let mut book = MDBook::load(&book_dir)?;
if let Some(dest_dir) = args.value_of("dest-dir") { if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = PathBuf::from(dest_dir); book.config.build.build_dir = PathBuf::from(dest_dir);
@ -64,7 +64,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let address = format!("{}:{}", interface, port); let address = format!("{}:{}", interface, port);
let ws_address = format!("{}:{}", interface, ws_port); let ws_address = format!("{}:{}", interface, ws_port);
book.livereload = Some(format!(r#" book.livereload = Some(format!(
r#"
<script type="text/javascript"> <script type="text/javascript">
var socket = new WebSocket("ws://{}:{}"); var socket = new WebSocket("ws://{}:{}");
socket.onmessage = function (event) {{ socket.onmessage = function (event) {{
@ -94,7 +95,9 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let broadcaster = ws_server.broadcaster(); let broadcaster = ws_server.broadcaster();
std::thread::spawn(move || { ws_server.listen(&*ws_address).unwrap(); }); std::thread::spawn(move || {
ws_server.listen(&*ws_address).unwrap();
});
let serving_url = format!("http://{}", address); let serving_url = format!("http://{}", address);
println!("\nServing on: {}", serving_url); println!("\nServing on: {}", serving_url);

View File

@ -18,7 +18,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
.map(|v| v.collect()) .map(|v| v.collect())
.unwrap_or_default(); .unwrap_or_default();
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config()?; let mut book = MDBook::load(&book_dir)?;
book.test(library_paths)?; book.test(library_paths)?;

View File

@ -27,7 +27,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
// Watch command implementation // Watch command implementation
pub fn execute(args: &ArgMatches) -> Result<()> { pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args); let book_dir = get_book_dir(args);
let mut book = MDBook::new(&book_dir).read_config()?; let mut book = MDBook::load(&book_dir)?;
if let Some(dest_dir) = args.value_of("dest-dir") { if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = PathBuf::from(dest_dir); book.config.build.build_dir = PathBuf::from(dest_dir);
@ -69,8 +69,8 @@ where
}; };
// Add the source directory to the watcher // Add the source directory to the watcher
if let Err(e) = watcher.watch(book.get_source(), Recursive) { if let Err(e) = watcher.watch(book.source_dir(), Recursive) {
println!("Error while watching {:?}:\n {:?}", book.get_source(), e); println!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
::std::process::exit(0); ::std::process::exit(0);
}; };

414
src/book/book.rs Normal file
View File

@ -0,0 +1,414 @@
use std::fmt::{self, Display, Formatter};
use std::path::{Path, PathBuf};
use std::collections::VecDeque;
use std::fs::{self, File};
use std::io::{Read, Write};
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
use config::BuildConfig;
use errors::*;
/// Load a book into memory from its `src/` directory.
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
let src_dir = src_dir.as_ref();
let summary_md = src_dir.join("SUMMARY.md");
let mut summary_content = String::new();
File::open(summary_md)
.chain_err(|| "Couldn't open SUMMARY.md")?
.read_to_string(&mut summary_content)?;
let summary = parse_summary(&summary_content).chain_err(|| "Summary parsing failed")?;
if cfg.create_missing {
create_missing(&src_dir, &summary).chain_err(|| "Unable to create missing chapters")?;
}
load_book_from_disk(&summary, src_dir)
}
fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
let mut items: Vec<_> = summary
.prefix_chapters
.iter()
.chain(summary.numbered_chapters.iter())
.chain(summary.suffix_chapters.iter())
.collect();
while !items.is_empty() {
let next = items.pop().expect("already checked");
if let SummaryItem::Link(ref link) = *next {
let filename = src_dir.join(&link.location);
if !filename.exists() {
if let Some(parent) = filename.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
debug!("[*] Creating missing file {}", filename.display());
let mut f = File::create(&filename)?;
writeln!(f, "# {}", link.name)?;
}
items.extend(&link.nested_items);
}
}
Ok(())
}
/// 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()
}
/// Get a depth-first iterator over the items in the book.
pub fn iter(&self) -> BookItems {
BookItems {
items: self.sections.iter().collect(),
}
}
}
/// 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>,
/// The chapter's location, relative to the `SUMMARY.md` file.
pub path: PathBuf,
}
impl Chapter {
/// Create a new chapter with the provided content.
pub fn new<P: Into<PathBuf>>(name: &str, content: String, path: P) -> Chapter {
Chapter {
name: name.to_string(),
content: content,
path: path.into(),
..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.
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 mut chapters = Vec::new();
for summary_item in summary_items {
let chapter = load_summary_item(summary_item, src_dir)?;
chapters.push(chapter);
}
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 stripped = location
.strip_prefix(&src_dir)
.expect("Chapters are always inside a book");
let mut ch = Chapter::new(&link.name, content, stripped);
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)
}
/// A depth-first iterator over the items in a book.
///
/// # Note
///
/// This struct shouldn't be created directly, instead prefer the
/// [`Book::iter()`] method.
///
/// [`Book::iter()`]: struct.Book.html#method.iter
pub struct BookItems<'a> {
items: VecDeque<&'a BookItem>,
}
impl<'a> Iterator for BookItems<'a> {
type Item = &'a BookItem;
fn next(&mut self) -> Option<Self::Item> {
let item = self.items.pop_front();
if let Some(&BookItem::Chapter(ref ch)) = item {
// if we wanted a breadth-first iterator we'd `extend()` here
for sub_item in ch.sub_items.iter().rev() {
self.items.push_front(sub_item);
}
}
item
}
}
impl Display for Chapter {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if let Some(ref section_number) = self.number {
write!(f, "{} ", section_number)?;
}
write!(f, "{}", self.name)
}
}
#[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.nested_items.push(second.clone().into());
root.nested_items.push(SummaryItem::Separator);
root.nested_items.push(second.clone().into());
(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(), "chapter_1.md");
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])),
path: PathBuf::from("second.md"),
sub_items: Vec::new(),
};
let should_be = BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
number: None,
path: PathBuf::from("chapter_1.md"),
sub_items: vec![
BookItem::Chapter(nested.clone()),
BookItem::Separator,
BookItem::Chapter(nested.clone()),
],
});
let got = load_summary_item(&SummaryItem::Link(root), temp.path()).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),
path: PathBuf::from("chapter_1.md"),
..Default::default()
}),
],
};
let got = load_book_from_disk(&summary, temp.path()).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn book_iter_iterates_over_sequential_items() {
let book = Book {
sections: vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
..Default::default()
}),
BookItem::Separator,
],
};
let should_be: Vec<_> = book.sections.iter().collect();
let got: Vec<_> = book.iter().collect();
assert_eq!(got, should_be);
}
#[test]
fn iterate_over_nested_book_items() {
let book = Book {
sections: vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
number: None,
path: PathBuf::from("Chapter_1/index.md"),
sub_items: vec![
BookItem::Chapter(Chapter::new(
"Hello World",
String::new(),
"Chapter_1/hello.md",
)),
BookItem::Separator,
BookItem::Chapter(Chapter::new(
"Goodbye World",
String::new(),
"Chapter_1/goodbye.md",
)),
],
}),
BookItem::Separator,
],
};
let got: Vec<_> = book.iter().collect();
assert_eq!(got.len(), 5);
// checking the chapter names are in the order should be sufficient here...
let chapter_names: Vec<String> = got.into_iter()
.filter_map(|i| match *i {
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
_ => None,
})
.collect();
let should_be: Vec<_> = vec![
String::from("Chapter 1"),
String::from("Hello World"),
String::from("Goodbye World"),
];
assert_eq!(chapter_names, should_be);
}
}

View File

@ -1,86 +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);
}
}
}
}

185
src/book/init.rs Normal file
View File

@ -0,0 +1,185 @@
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::*;
/// A helper for setting up a new book and its directory structure.
#[derive(Debug, Clone, PartialEq)]
pub struct BookBuilder {
root: PathBuf,
create_gitignore: bool,
config: Config,
copy_theme: bool,
}
impl BookBuilder {
/// Create a new `BookBuilder` which will generate a book in the provided
/// root directory.
pub fn new<P: Into<PathBuf>>(root: P) -> BookBuilder {
BookBuilder {
root: root.into(),
create_gitignore: false,
config: Config::default(),
copy_theme: false,
}
}
/// Set the `Config` to be used.
pub fn with_config(&mut self, cfg: Config) -> &mut BookBuilder {
self.config = cfg;
self
}
/// Get the config used by the `BookBuilder`.
pub fn config(&self) -> &Config {
&self.config
}
/// Should the theme be copied into the generated book (so users can tweak
/// it)?
pub fn copy_theme(&mut self, copy: bool) -> &mut BookBuilder {
self.copy_theme = copy;
self
}
/// Should we create a `.gitignore` file?
pub fn create_gitignore(&mut self, create: bool) -> &mut BookBuilder {
self.create_gitignore = create;
self
}
/// Generate the actual book. This will:
///
/// - Create the directory structure.
/// - Stub out some dummy chapters and the `SUMMARY.md`.
/// - Create a `.gitignore` (if applicable)
/// - Create a themes directory and populate it (if applicable)
/// - Generate a `book.toml` file,
/// - Then load the book so we can build it or run tests.
pub fn build(&self) -> Result<MDBook> {
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()?;
match MDBook::load(&self.root) {
Ok(book) => Ok(book),
Err(e) => {
error!("{}", e);
panic!(
"The BookBuilder should always create a valid book. If you are seeing this it \
is a bug and should be reported."
);
}
}
}
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");
let mut f = File::create(self.root.join(".gitignore"))?;
writeln!(f, "{}", self.config.build.build_dir.display())?;
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(())
}
}

View File

@ -1,73 +1,82 @@
pub mod bookitem; //! The internal representation of a book and infrastructure for loading it from
//! disk and building it.
//!
//! For examples on using `MDBook`, consult the [top-level documentation][1].
//!
//! [1]: ../index.html
pub use self::bookitem::{BookItem, BookItems}; #![deny(missing_docs)]
mod summary;
mod book;
mod init;
pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
pub use self::init::BookBuilder;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::fs::{self, File};
use std::io::Write; use std::io::Write;
use std::process::Command; use std::process::Command;
use tempdir::TempDir; use tempdir::TempDir;
use {parse, theme, utils}; use utils;
use renderer::{HtmlHandlebars, Renderer}; use renderer::{HtmlHandlebars, Renderer};
use preprocess; use preprocess;
use errors::*; use errors::*;
use config::Config; use config::Config;
/// The object used to manage and build a book.
pub struct MDBook { pub struct MDBook {
/// The book's root directory.
pub root: PathBuf, pub root: PathBuf,
/// The configuration used to tweak now a book is built.
pub config: Config, pub config: Config,
pub content: Vec<BookItem>, book: Book,
renderer: Box<Renderer>, renderer: Box<Renderer>,
/// The URL used for live reloading when serving up the book.
pub livereload: Option<String>, pub livereload: Option<String>,
} }
impl MDBook { impl MDBook {
/// Create a new `MDBook` struct with root directory `root` /// Load a book from its root directory on disk.
/// pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
/// # Examples let book_root = book_root.into();
/// let config_location = book_root.join("book.toml");
/// ```no_run
/// # extern crate mdbook;
/// # use mdbook::MDBook;
/// # #[allow(unused_variables)]
/// # fn main() {
/// let book = MDBook::new("root_dir");
/// # }
/// ```
///
/// In this example, `root_dir` will be the root directory of our book
/// and is specified in function of the current working directory
/// by using a relative path instead of an
/// absolute path.
///
/// Default directory paths:
///
/// - source: `root/src`
/// - output: `root/book`
/// - theme: `root/theme`
///
/// 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 { let config = if config_location.exists() {
let root = root.into(); debug!("[*] Loading config from {}", config_location.display());
if !root.exists() || !root.is_dir() { Config::from_disk(&config_location)?
warn!("{:?} No directory with that name", root); } else {
Config::default()
};
if log_enabled!(::log::LogLevel::Trace) {
for line in format!("Config: {:#?}", config).lines() {
trace!("{}", line);
}
} }
MDBook { MDBook::load_with_config(book_root, config)
root: root, }
config: Config::default(),
content: vec![], /// Load a book from its root directory using a custom config.
pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
let book_root = book_root.into();
let src_dir = book_root.join(&config.book.src);
let book = book::load_book(&src_dir, &config.build)?;
Ok(MDBook {
root: book_root,
config: config,
book: book,
renderer: Box::new(HtmlHandlebars::new()), renderer: Box::new(HtmlHandlebars::new()),
livereload: None, livereload: None,
} })
} }
/// Returns a flat depth-first iterator over the elements of the book, /// Returns a flat depth-first iterator over the elements of the book,
@ -77,15 +86,14 @@ impl MDBook {
/// ```no_run /// ```no_run
/// # extern crate mdbook; /// # extern crate mdbook;
/// # use mdbook::MDBook; /// # use mdbook::MDBook;
/// # use mdbook::BookItem; /// # use mdbook::book::BookItem;
/// # #[allow(unused_variables)] /// # #[allow(unused_variables)]
/// # fn main() { /// # fn main() {
/// # let book = MDBook::new("mybook"); /// # let book = MDBook::load("mybook").unwrap();
/// for item in book.iter() { /// for item in book.iter() {
/// match item { /// match *item {
/// &BookItem::Chapter(ref section, ref chapter) => {}, /// BookItem::Chapter(ref chapter) => {},
/// &BookItem::Affix(ref chapter) => {}, /// BookItem::Separator => {},
/// &BookItem::Spacer => {},
/// } /// }
/// } /// }
/// ///
@ -98,17 +106,15 @@ impl MDBook {
/// // etc. /// // etc.
/// # } /// # }
/// ``` /// ```
pub fn iter(&self) -> BookItems { pub fn iter(&self) -> BookItems {
BookItems { self.book.iter()
items: &self.content[..],
current_index: 0,
stack: Vec::new(),
}
} }
/// `init()` creates some boilerplate files and directories /// `init()` gives you a `BookBuilder` which you can use to setup a new book
/// to get you started with your book. /// and its accompanying directory structure.
///
/// The `BookBuilder` creates some boilerplate files and directories to get
/// you started with your book.
/// ///
/// ```text /// ```text
/// book-test/ /// book-test/
@ -118,242 +124,59 @@ impl MDBook {
/// └── SUMMARY.md /// └── SUMMARY.md
/// ``` /// ```
/// ///
/// It uses the paths given as source and output directories /// It uses the path provided as the root directory for your book, then adds
/// and adds a `SUMMARY.md` and a /// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
/// `chapter_1.md` to the source directory. /// to get you started.
pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
pub fn init(&mut self) -> Result<()> { BookBuilder::new(book_root)
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.config.build.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 create_gitignore(&self) { /// Tells the renderer to build our book and put it in the build directory.
let gitignore = self.get_gitignore();
let destination = self.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`
if !gitignore.exists() && destination.starts_with(&self.root) {
let relative = destination
.strip_prefix(&self.root)
.expect("Could not strip the root prefix, path is not relative to root")
.to_str()
.expect("Could not convert to &str");
debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore);
let mut f = File::create(&gitignore).expect("Could not create file.");
debug!("[*]: Writing to .gitignore");
writeln!(f, "/{}", relative).expect("Could not write to file.");
}
}
/// The `build()` method is the one where everything happens.
/// First it parses `SUMMARY.md` to construct the book's structure
/// in the form of a `Vec<BookItem>` and then calls `render()`
/// method of the current renderer.
///
/// It is the renderer who generates all the output files.
pub fn build(&mut self) -> Result<()> { pub fn build(&mut self) -> Result<()> {
debug!("[fn]: build"); debug!("[fn]: build");
self.init()?; let dest = self.get_destination();
if dest.exists() {
// Clean output directory utils::fs::remove_dir_content(&dest).chain_err(|| "Unable to clear output directory")?;
utils::fs::remove_dir_content(&self.get_destination())?; }
self.renderer.render(self) self.renderer.render(self)
} }
// FIXME: This doesn't belong as part of `MDBook`. It is only used by the HTML renderer
pub fn get_gitignore(&self) -> PathBuf { #[doc(hidden)]
self.root.join(".gitignore")
}
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)?;
// header.hbs
let mut header = File::create(themedir.join("header.hbs"))?;
header.write_all(theme::HEADER)?;
// 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(())
}
pub fn write_file<P: AsRef<Path>>(&self, filename: P, content: &[u8]) -> Result<()> { 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) utils::fs::create_file(&path)?
.map_err(|e| e.into()) .write_all(content)
.map_err(|e| e.into())
} }
/// Parses the `book.json` file (if it exists) to extract /// You can change the default renderer to another one by using this method.
/// the configuration parameters. /// The only requirement is for your renderer to implement the [Renderer
/// The `book.json` file should be in the root directory of the book. /// trait](../../renderer/renderer/trait.Renderer.html)
/// The root directory is the one specified when creating a new `MDBook` pub fn set_renderer<R: Renderer + 'static>(mut self, renderer: R) -> Self {
self.renderer = Box::new(renderer);
pub fn read_config(mut self) -> Result<Self> {
let config_path = self.root.join("book.toml");
if config_path.exists() {
debug!("[*] Loading the config from {}", config_path.display());
self.config = Config::from_disk(&config_path)?;
} else {
self.config = Config::default();
}
Ok(self)
}
/// You can change the default renderer to another one
/// by using this method. The only requirement
/// is for your renderer to implement the
/// [Renderer trait](../../renderer/renderer/trait.Renderer.html)
///
/// ```no_run
/// extern crate mdbook;
/// use mdbook::MDBook;
/// use mdbook::renderer::HtmlHandlebars;
///
/// # #[allow(unused_variables)]
/// fn main() {
/// let book = MDBook::new("mybook")
/// .set_renderer(Box::new(HtmlHandlebars::new()));
///
/// // In this example we replace the default renderer
/// // by the default renderer...
/// // Don't forget to put your renderer in a Box
/// }
/// ```
///
/// **note:** Don't forget to put your renderer in a `Box`
/// before passing it to `set_renderer()`
pub fn set_renderer(mut self, renderer: Box<Renderer>) -> Self {
self.renderer = renderer;
self self
} }
/// Run `rustdoc` tests on the book, linking against the provided libraries.
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> { pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
// read in the chapters
self.parse_summary().chain_err(|| "Couldn't parse summary")?;
let library_args: Vec<&str> = (0..library_paths.len()) let library_args: Vec<&str> = (0..library_paths.len())
.map(|_| "-L") .map(|_| "-L")
.zip(library_paths.into_iter()) .zip(library_paths.into_iter())
.flat_map(|x| vec![x.0, x.1]) .flat_map(|x| vec![x.0, x.1])
.collect(); .collect();
let temp_dir = TempDir::new("mdbook")?; let temp_dir = TempDir::new("mdbook")?;
for item in self.iter() { for item in self.iter() {
if let BookItem::Chapter(_, ref ch) = *item { if let BookItem::Chapter(ref ch) = *item {
if !ch.path.as_os_str().is_empty() { if !ch.path.as_os_str().is_empty() {
let path = self.get_source().join(&ch.path); let path = self.source_dir().join(&ch.path);
let base = path.parent() let base = path.parent()
.ok_or_else(|| String::from("Invalid bookitem path!"))?; .ok_or_else(|| String::from("Invalid bookitem path!"))?;
let content = utils::fs::file_to_string(&path)?; let content = utils::fs::file_to_string(&path)?;
// Parse and expand links // Parse and expand links
let content = preprocess::links::replace_all(&content, base)?; let content = preprocess::links::replace_all(&content, base)?;
@ -364,14 +187,17 @@ impl MDBook {
let mut tmpf = utils::fs::create_file(&path)?; let mut tmpf = utils::fs::create_file(&path)?;
tmpf.write_all(content.as_bytes())?; tmpf.write_all(content.as_bytes())?;
let output = Command::new("rustdoc").arg(&path) let output = Command::new("rustdoc")
.arg("--test") .arg(&path)
.args(&library_args) .arg("--test")
.output()?; .args(&library_args)
.output()?;
if !output.status.success() { if !output.status.success() {
bail!(ErrorKind::Subprocess("Rustdoc returned an error".to_string(), bail!(ErrorKind::Subprocess(
output)); "Rustdoc returned an error".to_string(),
output
));
} }
} }
} }
@ -379,22 +205,19 @@ impl MDBook {
Ok(()) Ok(())
} }
// Construct book // FIXME: This doesn't belong under `MDBook`, it should really be passed to the renderer directly.
fn parse_summary(&mut self) -> Result<()> { #[doc(hidden)]
// When append becomes stable, use self.content.append() ...
let summary = self.get_source().join("SUMMARY.md");
self.content = parse::construct_bookitems(&summary)?;
Ok(())
}
pub fn get_destination(&self) -> PathBuf { pub fn get_destination(&self) -> PathBuf {
self.root.join(&self.config.build.build_dir) self.root.join(&self.config.build.build_dir)
} }
pub fn get_source(&self) -> PathBuf { /// Get the directory containing this book's source files.
pub fn source_dir(&self) -> PathBuf {
self.root.join(&self.config.book.src) self.root.join(&self.config.book.src)
} }
// FIXME: This belongs as part of the `HtmlConfig`.
#[doc(hidden)]
pub fn theme_dir(&self) -> PathBuf { pub fn theme_dir(&self) -> PathBuf {
match self.config.html_config().and_then(|h| h.theme) { match self.config.html_config().and_then(|h| h.theme) {
Some(d) => self.root.join(d), Some(d) => self.root.join(d),

717
src/book/summary.rs Normal file
View File

@ -0,0 +1,717 @@
use std::fmt::{self, Display, Formatter};
use std::iter::FromIterator;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use memchr::{self, Memchr};
use pulldown_cmark::{self, Event, Tag};
use errors::*;
/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be
/// used when loading a book from disk.
///
/// # Summary Format
///
/// **Title:** It's common practice to begin with a title, generally
/// "# Summary". It's not mandatory and the parser (currently) ignores it, so
/// you can too if you feel like it.
///
/// **Prefix Chapter:** Before the main numbered chapters you can add a couple
/// of elements that will not be numbered. This is useful for forewords,
/// introductions, etc. There are however some constraints. You can not nest
/// prefix chapters, they should all be on the root level. And you can not add
/// prefix chapters once you have added numbered chapters.
///
/// ```markdown
/// [Title of prefix element](relative/path/to/markdown.md)
/// ```
///
/// **Numbered Chapter:** Numbered chapters are the main content of the book,
/// they
/// will be numbered and can be nested, resulting in a nice hierarchy (chapters,
/// sub-chapters, etc.)
///
/// ```markdown
/// - [Title of the Chapter](relative/path/to/markdown.md)
/// ```
///
/// You can either use - or * to indicate a numbered chapter, the parser doesn't
/// care but you'll probably want to stay consistent.
///
/// **Suffix Chapter:** After the numbered chapters you can add a couple of
/// non-numbered chapters. They are the same as prefix chapters but come after
/// the numbered chapters instead of before.
///
/// All other elements are unsupported and will be ignored at best or result in
/// an error.
pub fn parse_summary(summary: &str) -> Result<Summary> {
let parser = SummaryParser::new(summary);
parser.parse()
}
/// The parsed `SUMMARY.md`, specifying how the book should be laid out.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Summary {
/// An optional title for the `SUMMARY.md`, currently just ignored.
pub title: Option<String>,
/// Chapters before the main text (e.g. an introduction).
pub prefix_chapters: Vec<SummaryItem>,
/// The main chapters in the document.
pub numbered_chapters: Vec<SummaryItem>,
/// Items which come after the main document (e.g. a conclusion).
pub suffix_chapters: Vec<SummaryItem>,
}
/// A struct representing an entry in the `SUMMARY.md`, possibly with nested
/// entries.
///
/// This is roughly the equivalent of `[Some section](./path/to/file.md)`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Link {
/// The name of the chapter.
pub name: String,
/// The location of the chapter's source file, taking the book's `src`
/// directory as the root.
pub location: PathBuf,
/// The section number, if this chapter is in the numbered section.
pub number: Option<SectionNumber>,
/// Any nested items this chapter may contain.
pub nested_items: Vec<SummaryItem>,
}
impl Link {
/// Create a new link with no nested items.
pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link {
Link {
name: name.into(),
location: location.as_ref().to_path_buf(),
number: None,
nested_items: Vec::new(),
}
}
}
impl Default for Link {
fn default() -> Self {
Link {
name: String::new(),
location: PathBuf::new(),
number: None,
nested_items: Vec::new(),
}
}
}
/// An item in `SUMMARY.md` which could be either a separator or a `Link`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SummaryItem {
/// A link to a chapter.
Link(Link),
/// A separator (`---`).
Separator,
}
impl SummaryItem {
fn maybe_link_mut(&mut self) -> Option<&mut Link> {
match *self {
SummaryItem::Link(ref mut l) => Some(l),
_ => None,
}
}
}
impl From<Link> for SummaryItem {
fn from(other: Link) -> SummaryItem {
SummaryItem::Link(other)
}
}
/// A recursive descent (-ish) parser for a `SUMMARY.md`.
///
///
/// # Grammar
///
/// The `SUMMARY.md` file has a grammar which looks something like this:
///
/// ```text
/// summary ::= title prefix_chapters numbered_chapters
/// suffix_chapters
/// title ::= "# " TEXT
/// | EPSILON
/// prefix_chapters ::= item*
/// suffix_chapters ::= item*
/// numbered_chapters ::= dotted_item+
/// dotted_item ::= INDENT* DOT_POINT item
/// item ::= link
/// | separator
/// separator ::= "---"
/// link ::= "[" TEXT "]" "(" TEXT ")"
/// DOT_POINT ::= "-"
/// | "*"
/// ```
///
/// > **Note:** the `TEXT` terminal is "normal" text, and should (roughly)
/// > match the following regex: "[^<>\n[]]+".
struct SummaryParser<'a> {
src: &'a str,
stream: pulldown_cmark::Parser<'a>,
}
/// Reads `Events` from the provided stream until the corresponding
/// `Event::End` is encountered which matches the `$delimiter` pattern.
///
/// This is the equivalent of doing
/// `$stream.take_while(|e| e != $delimeter).collect()` but it allows you to
/// use pattern matching and you won't get errors because `take_while()`
/// moves `$stream` out of self.
macro_rules! collect_events {
($stream:expr, start $delimiter:pat) => {
collect_events!($stream, Event::Start($delimiter))
};
($stream:expr, end $delimiter:pat) => {
collect_events!($stream, Event::End($delimiter))
};
($stream:expr, $delimiter:pat) => {
{
let mut events = Vec::new();
loop {
let event = $stream.next();
trace!("Next event: {:?}", event);
match event {
Some($delimiter) => break,
Some(other) => events.push(other),
None => {
debug!("Reached end of stream without finding the closing pattern, {}", stringify!($delimiter));
break;
}
}
}
events
}
}
}
impl<'a> SummaryParser<'a> {
fn new(text: &str) -> SummaryParser {
let pulldown_parser = pulldown_cmark::Parser::new(text);
SummaryParser {
src: text,
stream: pulldown_parser,
}
}
/// Get the current line and column to give the user more useful error
/// messages.
fn current_location(&self) -> (usize, usize) {
let byte_offset = self.stream.get_offset();
let previous_text = self.src[..byte_offset].as_bytes();
let line = Memchr::new(b'\n', previous_text).count() + 1;
let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0);
let col = self.src[start_of_line..byte_offset].chars().count();
(line, col)
}
/// Parse the text the `SummaryParser` was created with.
fn parse(mut self) -> Result<Summary> {
let title = self.parse_title();
let prefix_chapters = self.parse_affix(true)
.chain_err(|| "There was an error parsing the prefix chapters")?;
let numbered_chapters = self.parse_numbered()
.chain_err(|| "There was an error parsing the numbered chapters")?;
let suffix_chapters = self.parse_affix(false)
.chain_err(|| "There was an error parsing the suffix chapters")?;
Ok(Summary {
title,
prefix_chapters,
numbered_chapters,
suffix_chapters,
})
}
/// Parse the affix chapters. This expects the first event (start of
/// paragraph) to have already been consumed by the previous parser.
fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
let mut items = Vec::new();
debug!(
"[*] Parsing {} items",
if is_prefix { "prefix" } else { "suffix" }
);
loop {
match self.next_event() {
Some(Event::Start(Tag::List(..))) => {
if is_prefix {
// we've finished prefix chapters and are at the start
// of the numbered section.
break;
} else {
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
}
}
Some(Event::Start(Tag::Link(href, _))) => {
let link = self.parse_link(href.to_string())?;
items.push(SummaryItem::Link(link));
}
Some(Event::Start(Tag::Rule)) => items.push(SummaryItem::Separator),
Some(_) => {}
None => break,
}
}
Ok(items)
}
fn parse_link(&mut self, href: String) -> Result<Link> {
let link_content = collect_events!(self.stream, end Tag::Link(..));
let name = stringify_events(link_content);
Ok(Link {
name: name,
location: PathBuf::from(href.to_string()),
number: None,
nested_items: Vec::new(),
})
}
/// Parse the numbered chapters. This assumes the opening list tag has
/// already been consumed by a previous parser.
fn parse_numbered(&mut self) -> Result<Vec<SummaryItem>> {
let mut items = Vec::new();
let root_number = SectionNumber::default();
// we need to do this funny loop-match-if-let dance because a rule will
// close off any currently running list. Therefore we try to read the
// list items before the rule, then if we encounter a rule we'll add a
// separator and try to resume parsing numbered chapters if we start a
// list immediately afterwards.
//
// If you can think of a better way to do this then please make a PR :)
loop {
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?;
// if we've resumed after something like a rule the root sections
// will be numbered from 1. We need to manually go back and update
// them
update_section_numbers(&mut bunch_of_items, 0, items.len() as u32);
items.extend(bunch_of_items);
match self.next_event() {
Some(Event::Start(Tag::Paragraph)) => {
// we're starting the suffix chapters
break;
}
Some(Event::Start(other_tag)) => {
if Tag::Rule == other_tag {
items.push(SummaryItem::Separator);
}
trace!("Skipping contents of {:?}", other_tag);
// Skip over the contents of this tag
loop {
let next = self.next_event();
if next.is_none() || next == Some(Event::End(other_tag.clone())) {
break;
}
}
if let Some(Event::Start(Tag::List(..))) = self.next_event() {
continue;
} else {
break;
}
}
Some(_) => {
// something else... ignore
continue;
}
None => {
// EOF, bail...
break;
}
}
}
Ok(items)
}
fn next_event(&mut self) -> Option<Event<'a>> {
let next = self.stream.next();
trace!("Next event: {:?}", next);
next
}
fn parse_nested_numbered(&mut self, parent: &SectionNumber) -> Result<Vec<SummaryItem>> {
debug!("[*] Parsing numbered chapters at level {}", parent);
let mut items = Vec::new();
loop {
match self.next_event() {
Some(Event::Start(Tag::Item)) => {
let item = self.parse_nested_item(parent, items.len())?;
items.push(item);
}
Some(Event::Start(Tag::List(..))) => {
// recurse to parse the nested list
let (_, last_item) = get_last_link(&mut items)?;
let last_item_number = last_item
.number
.as_ref()
.expect("All numbered chapters have numbers");
let sub_items = self.parse_nested_numbered(last_item_number)?;
last_item.nested_items = sub_items;
}
Some(Event::End(Tag::List(..))) => break,
Some(_) => {}
None => break,
}
}
Ok(items)
}
fn parse_nested_item(
&mut self,
parent: &SectionNumber,
num_existing_items: usize,
) -> Result<SummaryItem> {
loop {
match self.next_event() {
Some(Event::Start(Tag::Paragraph)) => continue,
Some(Event::Start(Tag::Link(href, _))) => {
let mut link = self.parse_link(href.to_string())?;
let mut number = parent.clone();
number.0.push(num_existing_items as u32 + 1);
trace!(
"[*] Found chapter: {} {} ({})",
number,
link.name,
link.location.display()
);
link.number = Some(number);
return Ok(SummaryItem::Link(link));
}
other => {
warn!("Expected a start of a link, actually got {:?}", other);
bail!(self.parse_error(
"The link items for nested chapters must only contain a hyperlink"
));
}
}
}
}
fn parse_error<D: Display>(&self, msg: D) -> Error {
let (line, col) = self.current_location();
ErrorKind::ParseError(line, col, msg.to_string()).into()
}
/// Try to parse the title line.
fn parse_title(&mut self) -> Option<String> {
if let Some(Event::Start(Tag::Header(1))) = self.next_event() {
debug!("[*] Found a h1 in the SUMMARY");
let tags = collect_events!(self.stream, end Tag::Header(1));
Some(stringify_events(tags))
} else {
None
}
}
}
fn update_section_numbers(sections: &mut [SummaryItem], level: usize, by: u32) {
for section in sections {
if let SummaryItem::Link(ref mut link) = *section {
if let Some(ref mut number) = link.number {
number.0[level] += by;
}
update_section_numbers(&mut link.nested_items, level, by);
}
}
}
/// Gets a pointer to the last `Link` in a list of `SummaryItem`s, and its
/// index.
fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
links
.iter_mut()
.enumerate()
.filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
.rev()
.next()
.ok_or_else(|| {
"Unable to get last link because the list of SummaryItems doesn't contain any Links"
.into()
})
}
/// Removes the styling from a list of Markdown events and returns just the
/// plain text.
fn stringify_events(events: Vec<Event>) -> String {
events
.into_iter()
.filter_map(|t| match t {
Event::Text(text) => Some(text.into_owned()),
_ => None,
})
.collect()
}
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
/// a pretty `Display` impl.
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize)]
pub struct SectionNumber(pub Vec<u32>);
impl Display for SectionNumber {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if self.0.is_empty() {
write!(f, "0")
} else {
for item in &self.0 {
write!(f, "{}.", item)?;
}
Ok(())
}
}
}
impl Deref for SectionNumber {
type Target = Vec<u32>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for SectionNumber {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl FromIterator<u32> for SectionNumber {
fn from_iter<I: IntoIterator<Item = u32>>(it: I) -> Self {
SectionNumber(it.into_iter().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn section_number_has_correct_dotted_representation() {
let inputs = vec![
(vec![0], "0."),
(vec![1, 3], "1.3."),
(vec![1, 2, 3], "1.2.3."),
];
for (input, should_be) in inputs {
let section_number = SectionNumber(input).to_string();
assert_eq!(section_number, should_be);
}
}
#[test]
fn parse_initial_title() {
let src = "# Summary";
let should_be = String::from("Summary");
let mut parser = SummaryParser::new(src);
let got = parser.parse_title().unwrap();
assert_eq!(got, should_be);
}
#[test]
fn parse_title_with_styling() {
let src = "# My **Awesome** Summary";
let should_be = String::from("My Awesome Summary");
let mut parser = SummaryParser::new(src);
let got = parser.parse_title().unwrap();
assert_eq!(got, should_be);
}
#[test]
fn convert_markdown_events_to_a_string() {
let src = "Hello *World*, `this` is some text [and a link](./path/to/link)";
let should_be = "Hello World, this is some text and a link";
let events = pulldown_cmark::Parser::new(src).collect();
let got = stringify_events(events);
assert_eq!(got, should_be);
}
#[test]
fn parse_some_prefix_items() {
let src = "[First](./first.md)\n[Second](./second.md)\n";
let mut parser = SummaryParser::new(src);
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
..Default::default()
}),
SummaryItem::Link(Link {
name: String::from("Second"),
location: PathBuf::from("./second.md"),
..Default::default()
}),
];
let _ = parser.stream.next(); // step past first event
let got = parser.parse_affix(true).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn parse_prefix_items_with_a_separator() {
let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n";
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // step past first event
let got = parser.parse_affix(true).unwrap();
assert_eq!(got.len(), 3);
assert_eq!(got[1], SummaryItem::Separator);
}
#[test]
fn suffix_items_cannot_be_followed_by_a_list() {
let src = "[First](./first.md)\n- [Second](./second.md)\n";
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // step past first event
let got = parser.parse_affix(false);
assert!(got.is_err());
}
#[test]
fn parse_a_link() {
let src = "[First](./first.md)";
let should_be = Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
..Default::default()
};
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // skip past start of paragraph
let href = match parser.stream.next() {
Some(Event::Start(Tag::Link(href, _))) => href.to_string(),
other => panic!("Unreachable, {:?}", other),
};
let got = parser.parse_link(href).unwrap();
assert_eq!(got, should_be);
}
#[test]
fn parse_a_numbered_chapter() {
let src = "- [First](./first.md)\n";
let link = Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
number: Some(SectionNumber(vec![1])),
..Default::default()
};
let should_be = vec![SummaryItem::Link(link)];
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next();
let got = parser.parse_numbered().unwrap();
assert_eq!(got, should_be);
}
#[test]
fn parse_nested_numbered_chapters() {
let src = "- [First](./first.md)\n - [Nested](./nested.md)\n- [Second](./second.md)";
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
number: Some(SectionNumber(vec![1])),
nested_items: vec![
SummaryItem::Link(Link {
name: String::from("Nested"),
location: PathBuf::from("./nested.md"),
number: Some(SectionNumber(vec![1, 1])),
nested_items: Vec::new(),
}),
],
}),
SummaryItem::Link(Link {
name: String::from("Second"),
location: PathBuf::from("./second.md"),
number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(),
}),
];
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next();
let got = parser.parse_numbered().unwrap();
assert_eq!(got, should_be);
}
/// This test ensures the book will continue to pass because it breaks the
/// `SUMMARY.md` up using level 2 headers ([example]).
///
/// [example]: https://github.com/rust-lang/book/blob/2c942dc094f4ddcdc7aba7564f80782801197c99/second-edition/src/SUMMARY.md#basic-rust-literacy
#[test]
fn can_have_a_subheader_between_nested_items() {
extern crate env_logger;
env_logger::init().ok();
let src = "- [First](./first.md)\n\n## Subheading\n\n- [Second](./second.md)\n";
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(),
}),
SummaryItem::Link(Link {
name: String::from("Second"),
location: PathBuf::from("./second.md"),
number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(),
}),
];
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next();
let got = parser.parse_numbered().unwrap();
assert_eq!(got, should_be);
}
}

View File

@ -3,7 +3,7 @@ use std::fs::File;
use std::io::Read; use std::io::Read;
use toml::{self, Value}; use toml::{self, Value};
use toml::value::Table; use toml::value::Table;
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use errors::*; 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 { fn is_legacy_format(table: &Table) -> bool {
let top_level_items = ["title", "author", "authors"]; let top_level_items = ["title", "author", "authors"];

View File

@ -1,11 +1,11 @@
//! # mdBook //! # mdBook
//! //!
//! **mdBook** is similar to Gitbook but implemented in Rust. //! **mdBook** is similar to GitBook but implemented in Rust.
//! It offers a command line interface, but can also be used as a regular crate. //! 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) //! This is the API doc, the [user guide] is also available if you want
//! that contains information about the command line tool, format, structure etc. //! information about the command line tool, format, structure etc. It is also
//! It is also rendered with mdBook to showcase the features and default theme. //! rendered with mdBook to showcase the features and default theme.
//! //!
//! Some reasons why you would want to use the crate (over the cli): //! Some reasons why you would want to use the crate (over the cli):
//! //!
@ -15,30 +15,46 @@
//! - Write a new Renderer //! - Write a new Renderer
//! - ... //! - ...
//! //!
//! ## Example //! # Examples
//! //!
//! ```no_run //! If creating a new book from scratch, you'll want to get a `BookBuilder` via
//! extern crate mdbook; //! the `MDBook::init()` method.
//! //!
//! ```rust,no_run
//! use mdbook::MDBook; //! use mdbook::MDBook;
//! use std::path::PathBuf; //! use mdbook::config::Config;
//! //!
//! fn main() { //! let root_dir = "/path/to/book/root";
//! let mut md = MDBook::new("my-book");
//! //!
//! // tweak the book configuration a bit //! // create a default config and change a couple things
//! md.config.book.src = PathBuf::from("source"); //! let mut cfg = Config::default();
//! md.config.build.build_dir = PathBuf::from("book"); //! cfg.book.title = Some("My Book".to_string());
//! cfg.book.authors.push("Michael-F-Bryan".to_string());
//! //!
//! // Render the book //! MDBook::init(root_dir)
//! md.build().unwrap(); //! .create_gitignore(true)
//! } //! .with_config(cfg)
//! .build()
//! .expect("Book generation failed");
//! ```
//!
//! You can also load an existing book and build it.
//!
//! ```rust,no_run
//! use mdbook::MDBook;
//!
//! let root_dir = "/path/to/book/root";
//!
//! let mut md = MDBook::load(root_dir)
//! .expect("Unable to load the book");
//! md.build().expect("Building failed");
//! ``` //! ```
//! //!
//! ## Implementing a new Renderer //! ## 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
//! the [Renderer trait](renderer/renderer/trait.Renderer.html) //! do is to implement the [Renderer](renderer/renderer/trait.Renderer.html)
//! trait.
//! //!
//! And then you can swap in your renderer like this: //! And then you can swap in your renderer like this:
//! //!
@ -52,25 +68,30 @@
//! # fn main() { //! # fn main() {
//! # let your_renderer = HtmlHandlebars::new(); //! # let your_renderer = HtmlHandlebars::new();
//! # //! #
//! let book = MDBook::new("my-book").set_renderer(Box::new(your_renderer)); //! let mut book = MDBook::load("my-book").unwrap();
//! book.set_renderer(your_renderer);
//! # } //! # }
//! ``` //! ```
//! 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 directories. //! 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
//! directories.
//! //!
//! ## utils //! ## 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)
//! following function [`utils::fs::create_file(path: //! module, like the following function [`utils::fs::create_file(path:
//! &Path)`](utils/fs/fn.create_file.html) //! &Path)`](utils/fs/fn.create_file.html).
//! //!
//! This function creates a file and returns it. But before creating the file //! This function creates a file and returns it. But before creating the file
//! it checks every directory in the path to see if it exists, and if it does //! it checks every directory in the path to see if it exists, and if it does
//! not it will be created. //! not it will be created.
//! //!
//! Make sure to take a look at it. //! Make sure to take a look at it.
//!
//! [user guide]: https://rust-lang-nursery.github.io/mdBook/
#[macro_use] #[macro_use]
extern crate error_chain; extern crate error_chain;
@ -79,6 +100,7 @@ extern crate handlebars;
extern crate lazy_static; extern crate lazy_static;
#[macro_use] #[macro_use]
extern crate log; extern crate log;
extern crate memchr;
extern crate pulldown_cmark; extern crate pulldown_cmark;
extern crate regex; extern crate regex;
extern crate serde; extern crate serde;
@ -89,7 +111,10 @@ extern crate serde_json;
extern crate tempdir; extern crate tempdir;
extern crate toml; extern crate toml;
mod parse; #[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
mod preprocess; mod preprocess;
pub mod book; pub mod book;
pub mod config; pub mod config;
@ -116,6 +141,11 @@ pub mod errors {
description("A subprocess failed") description("A subprocess failed")
display("{}: {}", message, String::from_utf8_lossy(&output.stdout)) display("{}: {}", message, String::from_utf8_lossy(&output.stdout))
} }
ParseError(line: usize, col: usize, message: String) {
description("A SUMMARY.md parsing error")
display("Error at line {}, column {}: {}", line, col, message)
}
} }
} }

View File

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

View File

@ -1,239 +0,0 @@
use std::path::PathBuf;
use std::fs::File;
use std::io::{Error, ErrorKind, Read, Result};
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,14 +2,14 @@ use renderer::html_handlebars::helpers;
use preprocess; use preprocess;
use renderer::Renderer; use renderer::Renderer;
use book::MDBook; use book::MDBook;
use book::bookitem::{BookItem, Chapter}; use book::{BookItem, Chapter};
use config::{Config, Playpen, HtmlConfig}; use config::{Config, Playpen, HtmlConfig};
use {utils, theme}; use {utils, theme};
use theme::{Theme, playpen_editor}; use theme::{Theme, playpen_editor};
use errors::*; use errors::*;
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use std::ascii::AsciiExt; #[allow(unused_imports)] use std::ascii::AsciiExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::{self, Read}; use std::io::{self, Read};
@ -35,13 +35,12 @@ impl HtmlHandlebars {
-> Result<()> { -> Result<()> {
// FIXME: This should be made DRY-er and rely less on mutable state // FIXME: This should be made DRY-er and rely less on mutable state
match *item { match *item {
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) BookItem::Chapter(ref ch) =>
if !ch.path.as_os_str().is_empty() =>
{ {
let path = ctx.book.get_source().join(&ch.path); let content = ch.content.clone();
let content = utils::fs::file_to_string(&path)?; let base = ch.path.parent()
let base = path.parent() .map(|dir| ctx.src_dir.join(dir))
.ok_or_else(|| String::from("Invalid bookitem path!"))?; .expect("All chapters must have a parent directory");
// Parse and expand links // Parse and expand links
let content = preprocess::links::replace_all(&content, base)?; let content = preprocess::links::replace_all(&content, base)?;
@ -242,13 +241,14 @@ impl HtmlHandlebars {
impl Renderer for HtmlHandlebars { impl Renderer for HtmlHandlebars {
fn render(&self, book: &MDBook) -> Result<()> { fn render(&self, book: &MDBook) -> Result<()> {
let html_config = book.config.html_config().unwrap_or_default(); let html_config = book.config.html_config().unwrap_or_default();
let src_dir = book.root.join(&book.config.book.src);
debug!("[fn]: render"); debug!("[fn]: render");
let mut handlebars = Handlebars::new(); let mut handlebars = Handlebars::new();
let theme_dir = match html_config.theme { let theme_dir = match html_config.theme {
Some(ref theme) => theme, Some(ref theme) => theme.to_path_buf(),
None => Path::new("theme"), None => src_dir.join("theme"),
}; };
let theme = theme::Theme::new(theme_dir); let theme = theme::Theme::new(theme_dir);
@ -285,6 +285,7 @@ impl Renderer for HtmlHandlebars {
book: book, book: book,
handlebars: &handlebars, handlebars: &handlebars,
destination: destination.to_path_buf(), destination: destination.to_path_buf(),
src_dir: src_dir.clone(),
data: data.clone(), data: data.clone(),
is_index: i == 0, is_index: i == 0,
html_config: html_config.clone(), html_config: html_config.clone(),
@ -317,7 +318,7 @@ impl Renderer for HtmlHandlebars {
self.copy_additional_css_and_js(book)?; self.copy_additional_css_and_js(book)?;
// Copy all remaining files // Copy all remaining files
let src = book.get_source(); let src = book.source_dir();
utils::fs::copy_files_except_ext(&src, &destination, true, &["md"])?; utils::fs::copy_files_except_ext(&src, &destination, true, &["md"])?;
Ok(()) Ok(())
@ -397,7 +398,11 @@ fn make_data(book: &MDBook, config: &Config) -> Result<serde_json::Map<String, s
let mut chapter = BTreeMap::new(); let mut chapter = BTreeMap::new();
match *item { 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)); 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, io::Error::new(io::ErrorKind::Other,
@ -406,17 +411,7 @@ fn make_data(book: &MDBook, config: &Config) -> Result<serde_json::Map<String, s
})?; })?;
chapter.insert("path".to_owned(), json!(path)); chapter.insert("path".to_owned(), json!(path));
} }
BookItem::Chapter(ref s, ref ch) => { BookItem::Separator => {
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 => {
chapter.insert("spacer".to_owned(), json!("_spacer_")); chapter.insert("spacer".to_owned(), json!("_spacer_"));
} }
} }
@ -604,6 +599,7 @@ struct RenderItemContext<'a> {
handlebars: &'a Handlebars, handlebars: &'a Handlebars,
book: &'a MDBook, book: &'a MDBook,
destination: PathBuf, destination: PathBuf,
src_dir: PathBuf,
data: serde_json::Map<String, serde_json::Value>, data: serde_json::Map<String, serde_json::Value>,
is_index: bool, is_index: bool,
html_config: HtmlConfig, html_config: HtmlConfig,

View File

@ -114,3 +114,13 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
Ok(()) Ok(())
} }
pub fn new_copy_of_example_book() -> Result<TempDir> {
let temp = TempDir::new("book-example")?;
let book_example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
recursive_copy(book_example, temp.path())?;
Ok(temp)
}

View File

@ -4,7 +4,8 @@
- [First Chapter](./first/index.md) - [First Chapter](./first/index.md)
- [Nested Chapter](./first/nested.md) - [Nested Chapter](./first/nested.md)
---
- [Second Chapter](./second.md) - [Second Chapter](./second.md)
---
[Conclusion](./conclusion.md) [Conclusion](./conclusion.md)

View File

@ -0,0 +1,6 @@
fn main() {
println!("Hello World!");
#
# // You can even hide lines! :D
# println!("I am hidden! Expand the code snippet to see me");
}

View File

@ -1 +1,5 @@
# Second Chapter # Second Chapter
This makes sure you can insert runnable Rust files.
{{#playpen example.rs}}

View File

@ -2,7 +2,9 @@ extern crate mdbook;
extern crate tempdir; extern crate tempdir;
use std::path::PathBuf; use std::path::PathBuf;
use std::fs;
use mdbook::MDBook; use mdbook::MDBook;
use mdbook::config::Config;
use tempdir::TempDir; use tempdir::TempDir;
@ -17,8 +19,7 @@ fn base_mdbook_init_should_create_default_content() {
assert!(!temp.path().join(file).exists()); assert!(!temp.path().join(file).exists());
} }
let mut md = MDBook::new(temp.path()); MDBook::init(temp.path()).build().unwrap();
md.init().unwrap();
for file in &created_files { for file in &created_files {
let target = temp.path().join(file); let target = temp.path().join(file);
@ -35,30 +36,35 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
let temp = TempDir::new("mdbook").unwrap(); let temp = TempDir::new("mdbook").unwrap();
for file in &created_files { for file in &created_files {
assert!(!temp.path().join(file).exists(), assert!(
"{} shouldn't exist yet!", !temp.path().join(file).exists(),
file); "{} shouldn't exist yet!",
file
);
} }
let mut md = MDBook::new(temp.path()); let mut cfg = Config::default();
md.config.book.src = PathBuf::from("in"); cfg.book.src = PathBuf::from("in");
md.config.build.build_dir = PathBuf::from("out"); cfg.build.build_dir = PathBuf::from("out");
md.init().unwrap(); MDBook::init(temp.path()).with_config(cfg).build().unwrap();
for file in &created_files { for file in &created_files {
let target = temp.path().join(file); let target = temp.path().join(file);
assert!(target.exists(), "{} should have been created by `mdbook init`", file); assert!(
target.exists(),
"{} should have been created by `mdbook init`",
file
);
} }
} }
#[test] #[test]
fn book_toml_isnt_required() { fn book_toml_isnt_required() {
let temp = TempDir::new("mdbook").unwrap(); let temp = TempDir::new("mdbook").unwrap();
let mut md = MDBook::new(temp.path()); let mut md = MDBook::init(temp.path()).build().unwrap();
md.init().unwrap();
assert!(!temp.path().join("book.toml").exists()); let _ = fs::remove_file(temp.path().join("book.toml"));
md.read_config().unwrap().build().unwrap(); md.build().unwrap();
} }

View File

@ -0,0 +1,49 @@
//! Some integration tests to make sure the `SUMMARY.md` parser can deal with
//! some real-life examples.
extern crate env_logger;
extern crate error_chain;
extern crate mdbook;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use mdbook::book;
macro_rules! summary_md_test {
($name:ident, $filename:expr) => {
#[test]
fn $name() {
env_logger::init().ok();
let filename = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("summary_md_files")
.join($filename);
if !filename.exists() {
panic!("{} Doesn't exist", filename.display());
}
let mut content = String::new();
File::open(&filename)
.unwrap()
.read_to_string(&mut content)
.unwrap();
if let Err(e) = book::parse_summary(&content) {
use error_chain::ChainedError;
eprintln!("Error parsing {}", filename.display());
eprintln!();
eprintln!("{}", e.display_chain());
panic!();
}
}
};
}
summary_md_test!(rust_by_example, "rust_by_example.md");
summary_md_test!(rust_ffi_guide, "rust_ffi_guide.md");
summary_md_test!(example_book, "example_book.md");
summary_md_test!(the_book_2nd_edition, "the_book-2nd_edition.md");

View File

@ -2,38 +2,38 @@ extern crate mdbook;
#[macro_use] #[macro_use]
extern crate pretty_assertions; extern crate pretty_assertions;
extern crate select; extern crate select;
extern crate tempdir;
extern crate walkdir; extern crate walkdir;
mod dummy_book; mod dummy_book;
use dummy_book::{assert_contains_strings, DummyBook}; use dummy_book::{assert_contains_strings, DummyBook};
use std::fs::{File, remove_file}; use std::fs;
use std::io::Write;
use std::path::Path; use std::path::Path;
use std::ffi::OsStr; use std::ffi::OsStr;
use walkdir::{DirEntry, WalkDir, WalkDirIterator}; use walkdir::{DirEntry, WalkDir, WalkDirIterator};
use select::document::Document; use select::document::Document;
use select::predicate::{Class, Name, Predicate}; use select::predicate::{Class, Name, Predicate};
use tempdir::TempDir;
use mdbook::errors::*; use mdbook::errors::*;
use mdbook::utils::fs::file_to_string; use mdbook::utils::fs::file_to_string;
use mdbook::config::Config;
use mdbook::MDBook; use mdbook::MDBook;
const BOOK_ROOT: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/dummy_book"); 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] = &[
"2. Second Chapter", "1. First Chapter",
"Conclusion", "2. Second Chapter",
"Introduction"]; "Conclusion",
"Introduction",
];
const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter"]; const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter"];
/// Make sure you can load the dummy book and build it without panicking. /// Make sure you can load the dummy book and build it without panicking.
#[test] #[test]
fn build_the_dummy_book() { fn build_the_dummy_book() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::new(temp.path()); let mut md = MDBook::load(temp.path()).unwrap();
md.build().unwrap(); md.build().unwrap();
} }
@ -41,7 +41,7 @@ fn build_the_dummy_book() {
#[test] #[test]
fn by_default_mdbook_generates_rendered_content_in_the_book_directory() { fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::new(temp.path()); let mut md = MDBook::load(temp.path()).unwrap();
assert!(!temp.path().join("book").exists()); assert!(!temp.path().join("book").exists());
md.build().unwrap(); md.build().unwrap();
@ -53,15 +53,17 @@ fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
#[test] #[test]
fn make_sure_bottom_level_files_contain_links_to_chapters() { fn make_sure_bottom_level_files_contain_links_to_chapters() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::new(temp.path()); let mut md = MDBook::load(temp.path()).unwrap();
md.build().unwrap(); md.build().unwrap();
let dest = temp.path().join("book"); let dest = temp.path().join("book");
let links = vec![r#"href="intro.html""#, let links = vec![
r#"href="./first/index.html""#, r#"href="intro.html""#,
r#"href="./first/nested.html""#, r#"href="first/index.html""#,
r#"href="./second.html""#, r#"href="first/nested.html""#,
r#"href="./conclusion.html""#]; r#"href="second.html""#,
r#"href="conclusion.html""#,
];
let files_in_bottom_dir = vec!["index.html", "intro.html", "second.html", "conclusion.html"]; let files_in_bottom_dir = vec!["index.html", "intro.html", "second.html", "conclusion.html"];
@ -73,16 +75,18 @@ fn make_sure_bottom_level_files_contain_links_to_chapters() {
#[test] #[test]
fn check_correct_cross_links_in_nested_dir() { fn check_correct_cross_links_in_nested_dir() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::new(temp.path()); let mut md = MDBook::load(temp.path()).unwrap();
md.build().unwrap(); md.build().unwrap();
let first = temp.path().join("book").join("first"); let first = temp.path().join("book").join("first");
let links = vec![r#"<base href="../">"#, let links = vec![
r#"href="intro.html""#, r#"<base href="../">"#,
r#"href="./first/index.html""#, r#"href="intro.html""#,
r#"href="./first/nested.html""#, r#"href="first/index.html""#,
r#"href="./second.html""#, r#"href="first/nested.html""#,
r#"href="./conclusion.html""#]; r#"href="second.html""#,
r#"href="conclusion.html""#,
];
let files_in_nested_dir = vec!["index.html", "nested.html"]; let files_in_nested_dir = vec!["index.html", "nested.html"];
@ -90,17 +94,25 @@ fn check_correct_cross_links_in_nested_dir() {
assert_contains_strings(first.join(filename), &links); assert_contains_strings(first.join(filename), &links);
} }
assert_contains_strings(first.join("index.html"), assert_contains_strings(
&[r##"href="./first/index.html#some-section" id="some-section""##]); first.join("index.html"),
&[
r##"href="first/index.html#some-section" id="some-section""##,
],
);
assert_contains_strings(first.join("nested.html"), assert_contains_strings(
&[r##"href="./first/nested.html#some-section" id="some-section""##]); first.join("nested.html"),
&[
r##"href="first/nested.html#some-section" id="some-section""##,
],
);
} }
#[test] #[test]
fn rendered_code_has_playpen_stuff() { fn rendered_code_has_playpen_stuff() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::new(temp.path()); let mut md = MDBook::load(temp.path()).unwrap();
md.build().unwrap(); md.build().unwrap();
let nested = temp.path().join("book/first/nested.html"); let nested = temp.path().join("book/first/nested.html");
@ -114,14 +126,16 @@ fn rendered_code_has_playpen_stuff() {
#[test] #[test]
fn chapter_content_appears_in_rendered_document() { fn chapter_content_appears_in_rendered_document() {
let content = vec![("index.html", "Here's some interesting text"), let content = vec![
("second.html", "Second Chapter"), ("index.html", "Here's some interesting text"),
("first/nested.html", "testable code"), ("second.html", "Second Chapter"),
("first/index.html", "more text"), ("first/nested.html", "testable code"),
("conclusion.html", "Conclusion")]; ("first/index.html", "more text"),
("conclusion.html", "Conclusion"),
];
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::new(temp.path()); let mut md = MDBook::load(temp.path()).unwrap();
md.build().unwrap(); md.build().unwrap();
let destination = temp.path().join("book"); let destination = temp.path().join("book");
@ -153,21 +167,22 @@ fn chapter_files_were_rendered_to_html() {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let src = Path::new(BOOK_ROOT).join("src"); let src = Path::new(BOOK_ROOT).join("src");
let chapter_files = WalkDir::new(&src).into_iter() let chapter_files = WalkDir::new(&src)
.filter_entry(|entry| entry_ends_with(entry, ".md")) .into_iter()
.filter_map(|entry| entry.ok()) .filter_entry(|entry| entry_ends_with(entry, ".md"))
.map(|entry| entry.path().to_path_buf()) .filter_map(|entry| entry.ok())
.filter(|path| { .map(|entry| entry.path().to_path_buf())
path.file_name().and_then(OsStr::to_str) .filter(|path| path.file_name().and_then(OsStr::to_str) != Some("SUMMARY.md"));
!= Some("SUMMARY.md")
});
for chapter in chapter_files { for chapter in chapter_files {
let rendered_location = temp.path().join(chapter.strip_prefix(&src).unwrap()) let rendered_location = temp.path()
.with_extension("html"); .join(chapter.strip_prefix(&src).unwrap())
assert!(rendered_location.exists(), .with_extension("html");
"{} doesn't exits", assert!(
rendered_location.display()); rendered_location.exists(),
"{} doesn't exits",
rendered_location.display()
);
} }
} }
@ -178,10 +193,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 /// Read the main page (`book/index.html`) and expose it as a DOM which we
/// can search with the `select` crate /// can search with the `select` crate
fn root_index_html() -> Result<Document> { fn root_index_html() -> Result<Document> {
let temp = DummyBook::new().build() let temp = DummyBook::new()
.chain_err(|| "Couldn't create the dummy book")?; .build()
MDBook::new(temp.path()).build() .chain_err(|| "Couldn't create the dummy book")?;
.chain_err(|| "Book building failed")?; MDBook::load(temp.path())?
.build()
.chain_err(|| "Book building failed")?;
let index_page = temp.path().join("book").join("index.html"); let index_page = temp.path().join("book").join("index.html");
let html = file_to_string(&index_page).chain_err(|| "Unable to read index.html")?; let html = file_to_string(&index_page).chain_err(|| "Unable to read index.html")?;
@ -197,9 +214,9 @@ fn check_second_toc_level() {
let pred = descendants!(Class("chapter"), Name("li"), Name("li"), Name("a")); let pred = descendants!(Class("chapter"), Name("li"), Name("li"), Name("a"));
let mut children_of_children: Vec<_> = let mut children_of_children: Vec<_> = doc.find(pred)
doc.find(pred).map(|elem| elem.text().trim().to_string()) .map(|elem| elem.text().trim().to_string())
.collect(); .collect();
children_of_children.sort(); children_of_children.sort();
assert_eq!(children_of_children, should_be); assert_eq!(children_of_children, should_be);
@ -215,8 +232,9 @@ fn check_first_toc_level() {
let pred = descendants!(Class("chapter"), Name("li"), Name("a")); 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)
.collect(); .map(|elem| elem.text().trim().to_string())
.collect();
children.sort(); children.sort();
assert_eq!(children, should_be); assert_eq!(children, should_be);
@ -227,8 +245,8 @@ fn check_spacers() {
let doc = root_index_html().unwrap(); let doc = root_index_html().unwrap();
let should_be = 1; let should_be = 1;
let num_spacers = let num_spacers = doc.find(Class("chapter").descendant(Name("li").and(Class("spacer"))))
doc.find(Class("chapter").descendant(Name("li").and(Class("spacer")))).count(); .count();
assert_eq!(num_spacers, should_be); assert_eq!(num_spacers, should_be);
} }
@ -236,45 +254,53 @@ fn check_spacers() {
/// not exist. /// not exist.
#[test] #[test]
fn failure_on_missing_file() { fn failure_on_missing_file() {
let (md, _temp) = create_missing_setup(Some(false)); let temp = DummyBook::new().build().unwrap();
fs::remove_file(temp.path().join("src").join("intro.md")).unwrap();
// On failure, `build()` does not return a specific error, so assume let mut cfg = Config::default();
// any error is a failure due to a missing file. cfg.build.create_missing = false;
assert!(md.read_config().unwrap().build().is_err());
let got = MDBook::load_with_config(temp.path(), cfg);
assert!(got.is_err());
} }
/// Ensure a missing file is created if `create-missing` is true. /// Ensure a missing file is created if `create-missing` is true.
#[test] #[test]
fn create_missing_file_with_config() { fn create_missing_file_with_config() {
let (md, temp) = create_missing_setup(Some(true));
md.read_config().unwrap().build().unwrap();
assert!(temp.path().join("src").join("intro.md").exists());
}
/// Ensure a missing file is created if `create-missing` is not set (the default
/// is true).
#[test]
fn create_missing_file_without_config() {
let (md, temp) = create_missing_setup(None);
md.read_config().unwrap().build().unwrap();
assert!(temp.path().join("src").join("intro.md").exists());
}
fn create_missing_setup(create_missing: Option<bool>) -> (MDBook, TempDir) {
let temp = DummyBook::new().build().unwrap(); let temp = DummyBook::new().build().unwrap();
let md = MDBook::new(temp.path()); fs::remove_file(temp.path().join("src").join("intro.md")).unwrap();
let mut file = File::create(temp.path().join("book.toml")).unwrap(); let mut cfg = Config::default();
match create_missing { cfg.build.create_missing = true;
Some(true) => file.write_all(b"[build]\ncreate-missing = true\n").unwrap(),
Some(false) => file.write_all(b"[build]\ncreate-missing = false\n").unwrap(),
None => (),
}
file.flush().unwrap();
remove_file(temp.path().join("src").join("intro.md")).unwrap(); assert!(!temp.path().join("src").join("intro.md").exists());
let _md = MDBook::load_with_config(temp.path(), cfg).unwrap();
(md, temp) assert!(temp.path().join("src").join("intro.md").exists());
}
/// This makes sure you can include a Rust file with `{{#playpen example.rs}}`.
/// Specification is in `book-example/src/format/rust.md`
#[test]
fn able_to_include_rust_files_in_chapters() {
let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
md.build().unwrap();
let second = temp.path().join("book/second.html");
let playpen_strings = &[
r#"class="playpen""#,
r#"println!(&quot;Hello World!&quot;);"#,
];
assert_contains_strings(second, playpen_strings);
}
#[test]
fn example_book_can_build() {
let example_book_dir = dummy_book::new_copy_of_example_book().unwrap();
let mut md = MDBook::load(example_book_dir.path()).unwrap();
let got = md.build();
assert!(got.is_ok());
} }

View File

@ -0,0 +1,20 @@
# Summary
- [mdBook](README.md)
- [Command Line Tool](cli/cli-tool.md)
- [init](cli/init.md)
- [build](cli/build.md)
- [watch](cli/watch.md)
- [serve](cli/serve.md)
- [test](cli/test.md)
- [Format](format/format.md)
- [SUMMARY.md](format/summary.md)
- [Configuration](format/config.md)
- [Theme](format/theme/theme.md)
- [index.hbs](format/theme/index-hbs.md)
- [Syntax highlighting](format/theme/syntax-highlighting.md)
- [MathJax Support](format/mathjax.md)
- [Rust code specific features](format/rust.md)
- [Rust Library](lib/lib.md)
-----------
[Contributors](misc/contributors.md)

View File

@ -0,0 +1,191 @@
# Summary
[Introduction](index.md)
- [Hello World](hello.md)
- [Comments](hello/comment.md)
- [Formatted print](hello/print.md)
- [Debug](hello/print/print_debug.md)
- [Display](hello/print/print_display.md)
- [Testcase: List](hello/print/print_display/testcase_list.md)
- [Formatting](hello/print/fmt.md)
- [Primitives](primitives.md)
- [Literals and operators](primitives/literals.md)
- [Tuples](primitives/tuples.md)
- [Arrays and Slices](primitives/array.md)
- [Custom Types](custom_types.md)
- [Structures](custom_types/structs.md)
- [Enums](custom_types/enum.md)
- [use](custom_types/enum/enum_use.md)
- [C-like](custom_types/enum/c_like.md)
- [Testcase: linked-list](custom_types/enum/testcase_linked_list.md)
- [constants](custom_types/constants.md)
- [Variable Bindings](variable_bindings.md)
- [Mutability](variable_bindings/mut.md)
- [Scope and Shadowing](variable_bindings/scope.md)
- [Declare first](variable_bindings/declare.md)
- [Types](types.md)
- [Casting](types/cast.md)
- [Literals](types/literals.md)
- [Inference](types/inference.md)
- [Aliasing](types/alias.md)
- [Conversion](conversion.md)
- [From and Into](conversion/from_into.md)
- [To and From String](conversion/string.md)
- [Expressions](expression.md)
- [Flow Control](flow_control.md)
- [if/else](flow_control/if_else.md)
- [loop](flow_control/loop.md)
- [Nesting and labels](flow_control/loop/nested.md)
- [Returning from loops](flow_control/loop/return.md)
- [while](flow_control/while.md)
- [for and range](flow_control/for.md)
- [match](flow_control/match.md)
- [Destructuring](flow_control/match/destructuring.md)
- [tuples](flow_control/match/destructuring/destructure_tuple.md)
- [enums](flow_control/match/destructuring/destructure_enum.md)
- [pointers/ref](flow_control/match/destructuring/destructure_pointers.md)
- [structs](flow_control/match/destructuring/destructure_structures.md)
- [Guards](flow_control/match/guard.md)
- [Binding](flow_control/match/binding.md)
- [if let](flow_control/if_let.md)
- [while let](flow_control/while_let.md)
- [Functions](fn.md)
- [Methods](fn/methods.md)
- [Closures](fn/closures.md)
- [Capturing](fn/closures/capture.md)
- [As input parameters](fn/closures/input_parameters.md)
- [Type anonymity](fn/closures/anonymity.md)
- [Input functions](fn/closures/input_functions.md)
- [As output parameters](fn/closures/output_parameters.md)
- [Examples in `std`](fn/closures/closure_examples.md)
- [Iterator::any](fn/closures/closure_examples/iter_any.md)
- [Iterator::find](fn/closures/closure_examples/iter_find.md)
- [Higher Order Functions](fn/hof.md)
- [Modules](mod.md)
- [Visibility](mod/visibility.md)
- [Struct visibility](mod/struct_visibility.md)
- [The `use` declaration](mod/use.md)
- [`super` and `self`](mod/super.md)
- [File hierarchy](mod/split.md)
- [Crates](crates.md)
- [Library](crates/lib.md)
- [`extern crate`](crates/link.md)
- [Attributes](attribute.md)
- [`dead_code`](attribute/unused.md)
- [Crates](attribute/crate.md)
- [`cfg`](attribute/cfg.md)
- [Custom](attribute/cfg/custom.md)
- [Generics](generics.md)
- [Functions](generics/gen_fn.md)
- [Implementation](generics/impl.md)
- [Traits](generics/gen_trait.md)
- [Bounds](generics/bounds.md)
- [Testcase: empty bounds](generics/bounds/testcase_empty.md)
- [Multiple bounds](generics/multi_bounds.md)
- [Where clauses](generics/where.md)
- [New Type Idiom](generics/new_types.md)
- [Associated items](generics/assoc_items.md)
- [The Problem](generics/assoc_items/the_problem.md)
- [Associated types](generics/assoc_items/types.md)
- [Phantom type parameters](generics/phantom.md)
- [Testcase: unit clarification](generics/phantom/testcase_units.md)
- [Scoping rules](scope.md)
- [RAII](scope/raii.md)
- [Ownership and moves](scope/move.md)
- [Mutability](scope/move/mut.md)
- [Borrowing](scope/borrow.md)
- [Mutability](scope/borrow/mut.md)
- [Freezing](scope/borrow/freeze.md)
- [Aliasing](scope/borrow/alias.md)
- [The ref pattern](scope/borrow/ref.md)
- [Lifetimes](scope/lifetime.md)
- [Explicit annotation](scope/lifetime/explicit.md)
- [Functions](scope/lifetime/fn.md)
- [Methods](scope/lifetime/methods.md)
- [Structs](scope/lifetime/struct.md)
- [Bounds](scope/lifetime/lifetime_bounds.md)
- [Coercion](scope/lifetime/lifetime_coercion.md)
- [static](scope/lifetime/static_lifetime.md)
- [elision](scope/lifetime/elision.md)
- [Traits](trait.md)
- [Derive](trait/derive.md)
- [Operator Overloading](trait/ops.md)
- [Drop](trait/drop.md)
- [Iterators](trait/iter.md)
- [Clone](trait/clone.md)
- [macro_rules!](macros.md)
- [Syntax](macro/syntax.md)
- [Designators](macros/designators.md)
- [Overload](macros/overload.md)
- [Repeat](macros/repeat.md)
- [DRY (Don't Repeat Yourself)](macros/dry.md)
- [DSL (Domain Specific Languages)](macros/dsl.md)
- [Variadics](macros/variadics.md)
- [Error handling](error.md)
- [`panic`](error/panic.md)
- [`Option` & `unwrap`](error/option_unwrap.md)
- [Combinators: `map`](error/option_unwrap/map.md)
- [Combinators: `and_then`](error/option_unwrap/and_then.md)
- [`Result`](error/result.md)
- [`map` for `Result`](error/result/result_map.md)
- [aliases for `Result`](error/result/result_alias.md)
- [Early returns](error/result/early_returns.md)
- [Introducing `?`](error/result/enter_question_mark.md)
- [Multiple error types](error/multiple_error_types.md)
- [Pulling `Result`s out of `Option`s](error/multiple_error_types/option_result.md)
- [Defining an error type](error/multiple_error_types/define_error_type.md)
- [`Box`ing errors](error/multiple_error_types/boxing_errors.md)
- [Other uses of `?`](error/multiple_error_types/reenter_question_mark.md)
- [Wrapping errors](error/multiple_error_types/wrap_error.md)
- [Iterating over `Result`s](error/iter_result.md)
- [Std library types](std.md)
- [Box, stack and heap](std/box.md)
- [Vectors](std/vec.md)
- [Strings](std/str.md)
- [`Option`](std/option.md)
- [`Result`](std/result.md)
- [`?`](std/result/question_mark.md)
- [`panic!`](std/panic.md)
- [HashMap](std/hash.md)
- [Alternate/custom key types](std/hash/alt_key_types.md)
- [HashSet](std/hash/hashset.md)
- [Std misc](std_misc.md)
- [Threads](std_misc/threads.md)
- [Testcase: map-reduce](std_misc/threads/testcase_mapreduce.md)
- [Channels](std_misc/channels.md)
- [Path](std_misc/path.md)
- [File I/O](std_misc/file.md)
- [`open`](std_misc/file/open.md)
- [`create`](std_misc/file/create.md)
- [Child processes](std_misc/process.md)
- [Pipes](std_misc/process/pipe.md)
- [Wait](std_misc/process/wait.md)
- [Filesystem Operations](std_misc/fs.md)
- [Program arguments](std_misc/arg.md)
- [Argument parsing](std_misc/arg/matching.md)
- [Foreign Function Interface](std_misc/ffi.md)
- [Meta](meta.md)
- [Documentation](meta/doc.md)
- [Testing](meta/test.md)
- [Unsafe Operations](unsafe.md)

View File

@ -0,0 +1,19 @@
# Summary
- [Overview](./overview.md)
- [Setting Up](./setting_up.md)
- [Core Client Library](./client.md)
- [Constructing a Basic Request](./basic_request.md)
- [Sending the Request](./send_basic.md)
- [Generating a Header File](./cbindgen.md)
- [Better Error Handling](./error_handling.md)
- [Asynchronous Operations](./async.md)
- [More Complex Requests](./complex_request.md)
- [Testing](./testing.md)
- [Dynamic Loading & Plugins](./dynamic_loading.md)
---
- [Break All The Things!!1!](./fun/index.md)
- [Problems](./fun/problems.md)
- [Solutions](./fun/solutions.md)

View File

@ -0,0 +1,130 @@
# The Rust Programming Language
## Getting started
- [Introduction](ch01-00-introduction.md)
- [Installation](ch01-01-installation.md)
- [Hello, World!](ch01-02-hello-world.md)
- [Guessing Game Tutorial](ch02-00-guessing-game-tutorial.md)
- [Common Programming Concepts](ch03-00-common-programming-concepts.md)
- [Variables and Mutability](ch03-01-variables-and-mutability.md)
- [Data Types](ch03-02-data-types.md)
- [How Functions Work](ch03-03-how-functions-work.md)
- [Comments](ch03-04-comments.md)
- [Control Flow](ch03-05-control-flow.md)
- [Understanding Ownership](ch04-00-understanding-ownership.md)
- [What is Ownership?](ch04-01-what-is-ownership.md)
- [References & Borrowing](ch04-02-references-and-borrowing.md)
- [Slices](ch04-03-slices.md)
- [Using Structs to Structure Related Data](ch05-00-structs.md)
- [Defining and Instantiating Structs](ch05-01-defining-structs.md)
- [An Example Program Using Structs](ch05-02-example-structs.md)
- [Method Syntax](ch05-03-method-syntax.md)
- [Enums and Pattern Matching](ch06-00-enums.md)
- [Defining an Enum](ch06-01-defining-an-enum.md)
- [The `match` Control Flow Operator](ch06-02-match.md)
- [Concise Control Flow with `if let`](ch06-03-if-let.md)
## Basic Rust Literacy
- [Modules](ch07-00-modules.md)
- [`mod` and the Filesystem](ch07-01-mod-and-the-filesystem.md)
- [Controlling Visibility with `pub`](ch07-02-controlling-visibility-with-pub.md)
- [Referring to Names in Different Modules](ch07-03-importing-names-with-use.md)
- [Common Collections](ch08-00-common-collections.md)
- [Vectors](ch08-01-vectors.md)
- [Strings](ch08-02-strings.md)
- [Hash Maps](ch08-03-hash-maps.md)
- [Error Handling](ch09-00-error-handling.md)
- [Unrecoverable Errors with `panic!`](ch09-01-unrecoverable-errors-with-panic.md)
- [Recoverable Errors with `Result`](ch09-02-recoverable-errors-with-result.md)
- [To `panic!` or Not To `panic!`](ch09-03-to-panic-or-not-to-panic.md)
- [Generic Types, Traits, and Lifetimes](ch10-00-generics.md)
- [Generic Data Types](ch10-01-syntax.md)
- [Traits: Defining Shared Behavior](ch10-02-traits.md)
- [Validating References with Lifetimes](ch10-03-lifetime-syntax.md)
- [Testing](ch11-00-testing.md)
- [Writing tests](ch11-01-writing-tests.md)
- [Running tests](ch11-02-running-tests.md)
- [Test Organization](ch11-03-test-organization.md)
- [An I/O Project: Building a Command Line Program](ch12-00-an-io-project.md)
- [Accepting Command Line Arguments](ch12-01-accepting-command-line-arguments.md)
- [Reading a File](ch12-02-reading-a-file.md)
- [Refactoring to Improve Modularity and Error Handling](ch12-03-improving-error-handling-and-modularity.md)
- [Developing the Librarys Functionality with Test Driven Development](ch12-04-testing-the-librarys-functionality.md)
- [Working with Environment Variables](ch12-05-working-with-environment-variables.md)
- [Writing Error Messages to Standard Error Instead of Standard Output](ch12-06-writing-to-stderr-instead-of-stdout.md)
## Thinking in Rust
- [Functional Language Features: Iterators and Closures](ch13-00-functional-features.md)
- [Closures: Anonymous Functions that Can Capture Their Environment](ch13-01-closures.md)
- [Processing a Series of Items with Iterators](ch13-02-iterators.md)
- [Improving Our I/O Project](ch13-03-improving-our-io-project.md)
- [Comparing Performance: Loops vs. Iterators](ch13-04-performance.md)
- [More about Cargo and Crates.io](ch14-00-more-about-cargo.md)
- [Customizing Builds with Release Profiles](ch14-01-release-profiles.md)
- [Publishing a Crate to Crates.io](ch14-02-publishing-to-crates-io.md)
- [Cargo Workspaces](ch14-03-cargo-workspaces.md)
- [Installing Binaries from Crates.io with `cargo install`](ch14-04-installing-binaries.md)
- [Extending Cargo with Custom Commands](ch14-05-extending-cargo.md)
- [Smart Pointers](ch15-00-smart-pointers.md)
- [`Box<T>` Points to Data on the Heap and Has a Known Size](ch15-01-box.md)
- [The `Deref` Trait Allows Access to the Data Through a Reference](ch15-02-deref.md)
- [The `Drop` Trait Runs Code on Cleanup](ch15-03-drop.md)
- [`Rc<T>`, the Reference Counted Smart Pointer](ch15-04-rc.md)
- [`RefCell<T>` and the Interior Mutability Pattern](ch15-05-interior-mutability.md)
- [Creating Reference Cycles and Leaking Memory is Safe](ch15-06-reference-cycles.md)
- [Fearless Concurrency](ch16-00-concurrency.md)
- [Threads](ch16-01-threads.md)
- [Message Passing](ch16-02-message-passing.md)
- [Shared State](ch16-03-shared-state.md)
- [Extensible Concurrency: `Sync` and `Send`](ch16-04-extensible-concurrency-sync-and-send.md)
- [Is Rust an Object-Oriented Programming Language?](ch17-00-oop.md)
- [What Does Object-Oriented Mean?](ch17-01-what-is-oo.md)
- [Trait Objects for Using Values of Different Types](ch17-02-trait-objects.md)
- [Object-Oriented Design Pattern Implementations](ch17-03-oo-design-patterns.md)
## Advanced Topics
- [Patterns Match the Structure of Values](ch18-00-patterns.md)
- [All the Places Patterns May be Used](ch18-01-all-the-places-for-patterns.md)
- [Refutability: Whether a Pattern Might Fail to Match](ch18-02-refutability.md)
- [All the Pattern Syntax](ch18-03-pattern-syntax.md)
- [Advanced Features](ch19-00-advanced-features.md)
- [Unsafe Rust](ch19-01-unsafe-rust.md)
- [Advanced Lifetimes](ch19-02-advanced-lifetimes.md)
- [Advanced Traits](ch19-03-advanced-traits.md)
- [Advanced Types](ch19-04-advanced-types.md)
- [Advanced Functions & Closures](ch19-05-advanced-functions-and-closures.md)
- [Final Project: Building a Multithreaded Web Server](ch20-00-final-project-a-web-server.md)
- [A Single Threaded Web Server](ch20-01-single-threaded.md)
- [How Slow Requests Affect Throughput](ch20-02-slow-requests.md)
- [Designing the Thread Pool Interface](ch20-03-designing-the-interface.md)
- [Creating the Thread Pool and Storing Threads](ch20-04-storing-threads.md)
- [Sending Requests to Threads Via Channels](ch20-05-sending-requests-via-channels.md)
- [Graceful Shutdown and Cleanup](ch20-06-graceful-shutdown-and-cleanup.md)
- [Appendix](appendix-00.md)
- [A - Keywords](appendix-01-keywords.md)
- [B - Operators and Symbols](appendix-02-operators.md)
- [C - Derivable Traits](appendix-03-derivable-traits.md)
- [D - Macros](appendix-04-macros.md)
- [E - Translations](appendix-05-translation.md)
- [F - Newest Features](appendix-06-newest-features.md)

View File

@ -9,7 +9,7 @@ use mdbook::MDBook;
#[test] #[test]
fn mdbook_can_correctly_test_a_passing_book() { fn mdbook_can_correctly_test_a_passing_book() {
let temp = DummyBook::new().with_passing_test(true).build().unwrap(); let temp = DummyBook::new().with_passing_test(true).build().unwrap();
let mut md = MDBook::new(temp.path()); let mut md = MDBook::load(temp.path()).unwrap();
assert!(md.test(vec![]).is_ok()); assert!(md.test(vec![]).is_ok());
} }
@ -17,7 +17,7 @@ fn mdbook_can_correctly_test_a_passing_book() {
#[test] #[test]
fn mdbook_detects_book_with_failing_tests() { fn mdbook_detects_book_with_failing_tests() {
let temp = DummyBook::new().with_passing_test(false).build().unwrap(); let temp = DummyBook::new().with_passing_test(false).build().unwrap();
let mut md: MDBook = MDBook::new(temp.path()); let mut md: MDBook = MDBook::load(temp.path()).unwrap();
assert!(md.test(vec![]).is_err()); assert!(md.test(vec![]).is_err());
} }