2017-12-11 08:24:43 +08:00
|
|
|
//! The internal representation of a book and infrastructure for loading it from
|
|
|
|
//! disk and building it.
|
|
|
|
//!
|
2017-12-11 08:29:30 +08:00
|
|
|
//! For examples on using `MDBook`, consult the [top-level documentation][1].
|
2017-12-11 08:24:43 +08:00
|
|
|
//!
|
2017-12-11 08:29:30 +08:00
|
|
|
//! [1]: ../index.html
|
2017-12-11 08:24:43 +08:00
|
|
|
|
|
|
|
#![deny(missing_docs)]
|
|
|
|
|
2017-11-18 20:01:50 +08:00
|
|
|
mod summary;
|
|
|
|
mod book;
|
2017-11-18 20:41:04 +08:00
|
|
|
mod init;
|
2016-12-23 16:15:32 +08:00
|
|
|
|
2017-12-11 09:26:11 +08:00
|
|
|
pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
|
|
|
|
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
2017-11-18 20:41:04 +08:00
|
|
|
pub use self::init::BookBuilder;
|
2016-04-27 05:04:27 +08:00
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
use std::path::PathBuf;
|
2017-09-30 21:54:25 +08:00
|
|
|
use std::io::Write;
|
2016-04-27 05:04:27 +08:00
|
|
|
use std::process::Command;
|
2017-08-07 07:36:29 +08:00
|
|
|
use tempdir::TempDir;
|
2018-01-07 22:10:48 +08:00
|
|
|
use toml::Value;
|
2016-04-27 05:04:27 +08:00
|
|
|
|
2017-11-18 22:07:08 +08:00
|
|
|
use utils;
|
2018-01-07 22:10:48 +08:00
|
|
|
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
2018-01-07 23:24:37 +08:00
|
|
|
use preprocess::{self, Preprocessor};
|
2017-06-25 00:04:57 +08:00
|
|
|
use errors::*;
|
2016-04-27 05:04:27 +08:00
|
|
|
|
2017-09-30 21:13:00 +08:00
|
|
|
use config::Config;
|
2016-04-27 05:04:27 +08:00
|
|
|
|
2017-12-11 08:24:43 +08:00
|
|
|
/// The object used to manage and build a book.
|
2017-05-19 05:52:38 +08:00
|
|
|
pub struct MDBook {
|
2017-12-11 08:24:43 +08:00
|
|
|
/// The book's root directory.
|
2017-09-30 21:13:00 +08:00
|
|
|
pub root: PathBuf,
|
2017-12-11 08:24:43 +08:00
|
|
|
/// The configuration used to tweak now a book is built.
|
2017-09-30 21:36:03 +08:00
|
|
|
pub config: Config,
|
2018-01-07 22:10:48 +08:00
|
|
|
/// A representation of the book's contents in memory.
|
|
|
|
pub book: Book,
|
|
|
|
renderers: Vec<Box<Renderer>>,
|
2016-04-27 05:04:27 +08:00
|
|
|
|
2017-12-11 08:24:43 +08:00
|
|
|
/// The URL used for live reloading when serving up the book.
|
2017-09-30 21:36:03 +08:00
|
|
|
pub livereload: Option<String>,
|
2018-01-07 23:24:37 +08:00
|
|
|
|
|
|
|
/// List of pre-processors to be run on the book
|
|
|
|
preprocessors: Vec<Box<Preprocessor>>
|
2016-04-27 05:04:27 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl MDBook {
|
2017-11-18 20:01:50 +08:00
|
|
|
/// Load a book from its root directory on disk.
|
|
|
|
pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
|
|
|
|
let book_root = book_root.into();
|
|
|
|
let config_location = book_root.join("book.toml");
|
2016-04-27 05:04:27 +08:00
|
|
|
|
2017-12-13 08:55:37 +08:00
|
|
|
// the book.json file is no longer used, so we should emit a warning to
|
|
|
|
// let people know to migrate to book.toml
|
|
|
|
if book_root.join("book.json").exists() {
|
|
|
|
warn!("It appears you are still using book.json for configuration.");
|
|
|
|
warn!("This format is no longer used, so you should migrate to the");
|
|
|
|
warn!("book.toml format.");
|
|
|
|
warn!("Check the user guide for migration information:");
|
|
|
|
warn!("\thttps://rust-lang-nursery.github.io/mdBook/format/config.html");
|
|
|
|
}
|
|
|
|
|
2018-01-14 02:38:43 +08:00
|
|
|
let mut config = if config_location.exists() {
|
2017-12-11 08:42:36 +08:00
|
|
|
debug!("[*] Loading config from {}", config_location.display());
|
2017-11-18 20:01:50 +08:00
|
|
|
Config::from_disk(&config_location)?
|
|
|
|
} else {
|
|
|
|
Config::default()
|
|
|
|
};
|
2016-04-27 05:04:27 +08:00
|
|
|
|
2018-01-14 02:38:43 +08:00
|
|
|
config.update_from_env();
|
|
|
|
|
2017-12-30 18:43:46 +08:00
|
|
|
if log_enabled!(::log::Level::Trace) {
|
2017-12-11 08:42:36 +08:00
|
|
|
for line in format!("Config: {:#?}", config).lines() {
|
|
|
|
trace!("{}", line);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-12-11 07:32:35 +08:00
|
|
|
MDBook::load_with_config(book_root, config)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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> {
|
2018-01-07 22:10:48 +08:00
|
|
|
let root = book_root.into();
|
2017-12-11 07:32:35 +08:00
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
let src_dir = root.join(&config.book.src);
|
2017-12-11 07:32:35 +08:00
|
|
|
let book = book::load_book(&src_dir, &config.build)?;
|
2018-01-07 22:10:48 +08:00
|
|
|
let livereload = None;
|
|
|
|
|
|
|
|
let renderers = determine_renderers(&config);
|
2016-04-27 05:04:27 +08:00
|
|
|
|
2018-01-07 23:24:37 +08:00
|
|
|
let preprocessors = vec![];
|
|
|
|
|
2017-11-18 20:41:04 +08:00
|
|
|
Ok(MDBook {
|
2018-01-07 22:10:48 +08:00
|
|
|
root,
|
|
|
|
config,
|
|
|
|
book,
|
|
|
|
renderers,
|
|
|
|
livereload,
|
2018-01-07 23:24:37 +08:00
|
|
|
preprocessors,
|
2017-11-18 20:41:04 +08:00
|
|
|
})
|
2016-04-27 05:04:27 +08:00
|
|
|
}
|
|
|
|
|
2017-05-19 19:04:37 +08:00
|
|
|
/// Returns a flat depth-first iterator over the elements of the book,
|
|
|
|
/// it returns an [BookItem enum](bookitem.html):
|
2016-04-27 05:04:27 +08:00
|
|
|
/// `(section: String, bookitem: &BookItem)`
|
|
|
|
///
|
|
|
|
/// ```no_run
|
|
|
|
/// # extern crate mdbook;
|
|
|
|
/// # use mdbook::MDBook;
|
2017-11-18 20:01:50 +08:00
|
|
|
/// # use mdbook::book::BookItem;
|
2017-06-06 17:58:08 +08:00
|
|
|
/// # #[allow(unused_variables)]
|
2016-04-27 05:04:27 +08:00
|
|
|
/// # fn main() {
|
2017-11-18 22:16:35 +08:00
|
|
|
/// # let book = MDBook::load("mybook").unwrap();
|
2016-04-27 05:04:27 +08:00
|
|
|
/// for item in book.iter() {
|
2017-11-18 20:01:50 +08:00
|
|
|
/// match *item {
|
|
|
|
/// BookItem::Chapter(ref chapter) => {},
|
2017-11-18 22:16:35 +08:00
|
|
|
/// BookItem::Separator => {},
|
2016-04-27 05:04:27 +08:00
|
|
|
/// }
|
|
|
|
/// }
|
|
|
|
///
|
|
|
|
/// // would print something like this:
|
|
|
|
/// // 1. Chapter 1
|
|
|
|
/// // 1.1 Sub Chapter
|
|
|
|
/// // 1.2 Sub Chapter
|
|
|
|
/// // 2. Chapter 2
|
|
|
|
/// //
|
|
|
|
/// // etc.
|
|
|
|
/// # }
|
|
|
|
/// ```
|
|
|
|
pub fn iter(&self) -> BookItems {
|
2017-11-18 20:01:50 +08:00
|
|
|
self.book.iter()
|
2016-04-27 05:04:27 +08:00
|
|
|
}
|
|
|
|
|
2017-11-18 20:41:04 +08:00
|
|
|
/// `init()` gives you a `BookBuilder` which you can use to setup a new book
|
|
|
|
/// and its accompanying directory structure.
|
|
|
|
///
|
|
|
|
/// The `BookBuilder` creates some boilerplate files and directories to get
|
|
|
|
/// you started with your book.
|
2016-04-27 05:04:27 +08:00
|
|
|
///
|
|
|
|
/// ```text
|
|
|
|
/// book-test/
|
|
|
|
/// ├── book
|
|
|
|
/// └── src
|
|
|
|
/// ├── chapter_1.md
|
|
|
|
/// └── SUMMARY.md
|
|
|
|
/// ```
|
|
|
|
///
|
2017-11-18 20:41:04 +08:00
|
|
|
/// It uses the path provided as the root directory for your book, then adds
|
|
|
|
/// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
|
|
|
|
/// to get you started.
|
|
|
|
pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
|
|
|
|
BookBuilder::new(book_root)
|
2016-04-27 05:04:27 +08:00
|
|
|
}
|
|
|
|
|
2017-11-18 22:07:08 +08:00
|
|
|
/// Tells the renderer to build our book and put it in the build directory.
|
2018-01-07 22:10:48 +08:00
|
|
|
pub fn build(&self) -> Result<()> {
|
2016-04-27 05:04:27 +08:00
|
|
|
debug!("[fn]: build");
|
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
for renderer in &self.renderers {
|
|
|
|
self.run_renderer(renderer.as_ref())?;
|
2017-11-18 22:07:08 +08:00
|
|
|
}
|
2017-06-05 01:48:41 +08:00
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
Ok(())
|
2016-04-27 05:04:27 +08:00
|
|
|
}
|
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
fn run_renderer(&self, renderer: &Renderer) -> Result<()> {
|
|
|
|
let name = renderer.name();
|
|
|
|
let build_dir = self.build_dir_for(name);
|
|
|
|
if build_dir.exists() {
|
|
|
|
debug!(
|
|
|
|
"Cleaning build dir for the \"{}\" renderer ({})",
|
|
|
|
name,
|
|
|
|
build_dir.display()
|
|
|
|
);
|
|
|
|
|
|
|
|
utils::fs::remove_dir_content(&build_dir)
|
|
|
|
.chain_err(|| "Unable to clear output directory")?;
|
|
|
|
}
|
2017-05-19 05:52:38 +08:00
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
let render_context = RenderContext::new(
|
|
|
|
self.root.clone(),
|
|
|
|
self.book.clone(),
|
|
|
|
self.config.clone(),
|
|
|
|
build_dir,
|
|
|
|
);
|
|
|
|
|
|
|
|
renderer
|
|
|
|
.render(&render_context)
|
|
|
|
.chain_err(|| "Rendering failed")
|
2017-01-01 14:27:38 +08:00
|
|
|
}
|
|
|
|
|
2017-11-18 22:16:35 +08:00
|
|
|
/// 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)
|
2018-01-07 22:10:48 +08:00
|
|
|
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
|
|
|
|
self.renderers.push(Box::new(renderer));
|
2016-04-27 05:04:27 +08:00
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2018-01-07 23:24:37 +08:00
|
|
|
/// You can add a new preprocessor by using this method.
|
|
|
|
/// The only requirement is for your renderer to implement the Preprocessor trait.
|
|
|
|
pub fn with_preprecessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
|
|
|
self.preprocessors.push(Box::new(preprocessor));
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2017-12-11 08:24:43 +08:00
|
|
|
/// Run `rustdoc` tests on the book, linking against the provided libraries.
|
2017-06-19 22:06:15 +08:00
|
|
|
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
|
2017-11-18 22:59:45 +08:00
|
|
|
let library_args: Vec<&str> = (0..library_paths.len())
|
|
|
|
.map(|_| "-L")
|
|
|
|
.zip(library_paths.into_iter())
|
|
|
|
.flat_map(|x| vec![x.0, x.1])
|
|
|
|
.collect();
|
2017-12-10 20:13:46 +08:00
|
|
|
|
2017-08-07 07:36:29 +08:00
|
|
|
let temp_dir = TempDir::new("mdbook")?;
|
2017-12-10 20:13:46 +08:00
|
|
|
|
2016-04-27 05:04:27 +08:00
|
|
|
for item in self.iter() {
|
2017-11-18 20:01:50 +08:00
|
|
|
if let BookItem::Chapter(ref ch) = *item {
|
2017-08-07 07:36:29 +08:00
|
|
|
if !ch.path.as_os_str().is_empty() {
|
2017-12-11 08:24:43 +08:00
|
|
|
let path = self.source_dir().join(&ch.path);
|
2017-11-18 22:59:45 +08:00
|
|
|
let base = path.parent()
|
|
|
|
.ok_or_else(|| String::from("Invalid bookitem path!"))?;
|
2017-08-07 07:36:29 +08:00
|
|
|
let content = utils::fs::file_to_string(&path)?;
|
|
|
|
// Parse and expand links
|
|
|
|
let content = preprocess::links::replace_all(&content, base)?;
|
2017-02-16 11:01:26 +08:00
|
|
|
println!("[*]: Testing file: {:?}", path);
|
2016-04-27 05:04:27 +08:00
|
|
|
|
2017-09-30 21:13:00 +08:00
|
|
|
// write preprocessed file to tempdir
|
2017-08-07 07:36:29 +08:00
|
|
|
let path = temp_dir.path().join(&ch.path);
|
|
|
|
let mut tmpf = utils::fs::create_file(&path)?;
|
|
|
|
tmpf.write_all(content.as_bytes())?;
|
|
|
|
|
2017-11-18 22:59:45 +08:00
|
|
|
let output = Command::new("rustdoc")
|
|
|
|
.arg(&path)
|
|
|
|
.arg("--test")
|
|
|
|
.args(&library_args)
|
|
|
|
.output()?;
|
2016-04-27 05:04:27 +08:00
|
|
|
|
2017-02-16 11:01:26 +08:00
|
|
|
if !output.status.success() {
|
2017-11-18 20:41:04 +08:00
|
|
|
bail!(ErrorKind::Subprocess(
|
|
|
|
"Rustdoc returned an error".to_string(),
|
|
|
|
output
|
|
|
|
));
|
2016-04-27 05:04:27 +08:00
|
|
|
}
|
2017-02-16 11:01:26 +08:00
|
|
|
}
|
2016-04-27 05:04:27 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
/// The logic for determining where a backend should put its build
|
|
|
|
/// artefacts.
|
|
|
|
///
|
|
|
|
/// If there is only 1 renderer, put it in the directory pointed to by the
|
|
|
|
/// `build.build_dir` key in `Config`. If there is more than one then the
|
|
|
|
/// renderer gets its own directory within the main build dir.
|
|
|
|
///
|
|
|
|
/// i.e. If there were only one renderer (in this case, the HTML renderer):
|
|
|
|
///
|
|
|
|
/// - build/
|
|
|
|
/// - index.html
|
|
|
|
/// - ...
|
|
|
|
///
|
|
|
|
/// Otherwise if there are multiple:
|
|
|
|
///
|
|
|
|
/// - build/
|
|
|
|
/// - epub/
|
|
|
|
/// - my_awesome_book.epub
|
|
|
|
/// - html/
|
|
|
|
/// - index.html
|
|
|
|
/// - ...
|
|
|
|
/// - latex/
|
|
|
|
/// - my_awesome_book.tex
|
|
|
|
///
|
|
|
|
pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
|
|
|
|
let build_dir = self.root.join(&self.config.build.build_dir);
|
|
|
|
|
|
|
|
if self.renderers.len() <= 1 {
|
|
|
|
build_dir
|
|
|
|
} else {
|
|
|
|
build_dir.join(backend_name)
|
|
|
|
}
|
2017-05-20 19:56:01 +08:00
|
|
|
}
|
|
|
|
|
2017-12-11 08:24:43 +08:00
|
|
|
/// Get the directory containing this book's source files.
|
|
|
|
pub fn source_dir(&self) -> PathBuf {
|
2017-09-30 21:13:00 +08:00
|
|
|
self.root.join(&self.config.book.src)
|
2017-05-20 19:56:01 +08:00
|
|
|
}
|
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
// FIXME: This really belongs as part of the `HtmlConfig`.
|
2017-12-11 08:24:43 +08:00
|
|
|
#[doc(hidden)]
|
2017-09-30 21:36:03 +08:00
|
|
|
pub fn theme_dir(&self) -> PathBuf {
|
|
|
|
match self.config.html_config().and_then(|h| h.theme) {
|
|
|
|
Some(d) => self.root.join(d),
|
|
|
|
None => self.root.join("theme"),
|
|
|
|
}
|
2016-04-27 05:04:27 +08:00
|
|
|
}
|
|
|
|
}
|
2018-01-07 22:10:48 +08:00
|
|
|
|
|
|
|
/// Look at the `Config` and try to figure out what renderers to use.
|
|
|
|
fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
|
|
|
|
let mut renderers: Vec<Box<Renderer>> = Vec::new();
|
|
|
|
|
|
|
|
if let Some(output_table) = config.get("output").and_then(|o| o.as_table()) {
|
|
|
|
for (key, table) in output_table.iter() {
|
|
|
|
// the "html" backend has its own Renderer
|
|
|
|
if key == "html" {
|
|
|
|
renderers.push(Box::new(HtmlHandlebars::new()));
|
|
|
|
} else {
|
|
|
|
let renderer = interpret_custom_renderer(key, table);
|
|
|
|
renderers.push(renderer);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we couldn't find anything, add the HTML renderer as a default
|
|
|
|
if renderers.is_empty() {
|
|
|
|
renderers.push(Box::new(HtmlHandlebars::new()));
|
|
|
|
}
|
|
|
|
|
|
|
|
renderers
|
|
|
|
}
|
|
|
|
|
|
|
|
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
|
|
|
|
// look for the `command` field, falling back to using the key
|
|
|
|
// prepended by "mdbook-"
|
|
|
|
let table_dot_command = table
|
|
|
|
.get("command")
|
|
|
|
.and_then(|c| c.as_str())
|
|
|
|
.map(|s| s.to_string());
|
|
|
|
|
|
|
|
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
|
|
|
|
|
|
|
Box::new(CmdRenderer::new(key.to_string(), command.to_string()))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use toml::value::{Table, Value};
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn config_defaults_to_html_renderer_if_empty() {
|
|
|
|
let cfg = Config::default();
|
|
|
|
|
|
|
|
// make sure we haven't got anything in the `output` table
|
|
|
|
assert!(cfg.get("output").is_none());
|
|
|
|
|
|
|
|
let got = determine_renderers(&cfg);
|
|
|
|
|
|
|
|
assert_eq!(got.len(), 1);
|
|
|
|
assert_eq!(got[0].name(), "html");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn add_a_random_renderer_to_the_config() {
|
|
|
|
let mut cfg = Config::default();
|
|
|
|
cfg.set("output.random", Table::new()).unwrap();
|
|
|
|
|
|
|
|
let got = determine_renderers(&cfg);
|
|
|
|
|
|
|
|
assert_eq!(got.len(), 1);
|
|
|
|
assert_eq!(got[0].name(), "random");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn add_a_random_renderer_with_custom_command_to_the_config() {
|
|
|
|
let mut cfg = Config::default();
|
|
|
|
|
|
|
|
let mut table = Table::new();
|
|
|
|
table.insert("command".to_string(), Value::String("false".to_string()));
|
|
|
|
cfg.set("output.random", table).unwrap();
|
|
|
|
|
|
|
|
let got = determine_renderers(&cfg);
|
|
|
|
|
|
|
|
assert_eq!(got.len(), 1);
|
|
|
|
assert_eq!(got[0].name(), "random");
|
|
|
|
}
|
|
|
|
}
|