Merge pull request #491 from Michael-F-Bryan/book-representation-3
WIP: Book representation - Attempt 3
This commit is contained in:
commit
d69bc9c7c3
|
@ -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" }
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
[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"
|
||||||
|
|
||||||
[output.html]
|
[output.html]
|
||||||
mathjax-support = true
|
mathjax-support = true
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)?;
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
381
src/book/mod.rs
381
src/book/mod.rs
|
@ -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),
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"];
|
||||||
|
|
||||||
|
|
86
src/lib.rs
86
src/lib.rs
|
@ -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");
|
//!
|
||||||
//!
|
//! // create a default config and change a couple things
|
||||||
//! // tweak the book configuration a bit
|
//! let mut cfg = Config::default();
|
||||||
//! md.config.book.src = PathBuf::from("source");
|
//! cfg.book.title = Some("My Book".to_string());
|
||||||
//! md.config.build.build_dir = PathBuf::from("book");
|
//! 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub use self::summary::construct_bookitems;
|
|
||||||
|
|
||||||
pub mod summary;
|
|
|
@ -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))
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
|
@ -1 +1,5 @@
|
||||||
# Second Chapter
|
# Second Chapter
|
||||||
|
|
||||||
|
This makes sure you can insert runnable Rust files.
|
||||||
|
|
||||||
|
{{#playpen example.rs}}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
|
@ -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!("Hello World!");"#,
|
||||||
|
];
|
||||||
|
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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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 Library’s 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)
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue