Add support for alternative backends (#507)
* Added a mechanism for creating alternate backends * Added a CmdRenderer and the ability to have multiple renderers * Made MDBook::load() autodetect renderers * Added a couple methods to RenderContext * Converted RenderContext.version to a String * Made sure all alternate renderers are invoked as `mdbook-*` * Factored out the logic for determining which renderer to use * Added tests for renderer detection * Made it so `mdbook test` works on the book-example again * Updated the "For Developers" docs * Removed `[output.epub]` from the example book's book.toml * Added a bit more info on how backends should work * Added a `destination` key to the RenderContext * Altered how we wait for an alternate backend to finish * Refactored the Renderer trait to not use MDBook and moved livereload to the template * Moved info for developers out of the book.toml format chapter * MOAR docs * MDBook::build() no longer takes &mut self * Replaced a bunch of println!()'s with proper log macros * Cleaned up the build() method and backend discovery * Added a couple notes and doc-comments * Found a race condition when backends exit really quickly * Added support for backends with arguments * Fixed a funny doc-comment
This commit is contained in:
parent
dedc208a6a
commit
fd7e8d1b7b
|
@ -32,6 +32,8 @@ open = "1.1"
|
|||
regex = "0.2.1"
|
||||
tempdir = "0.3.4"
|
||||
itertools = "0.7.4"
|
||||
tempfile = "2.2.0"
|
||||
shlex = "0.1.1"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "4.0", optional = true }
|
||||
|
|
|
@ -15,6 +15,6 @@
|
|||
- [Syntax highlighting](format/theme/syntax-highlighting.md)
|
||||
- [MathJax Support](format/mathjax.md)
|
||||
- [Rust code specific features](format/rust.md)
|
||||
- [Rust Library](lib/lib.md)
|
||||
- [For Developers](lib/index.md)
|
||||
-----------
|
||||
[Contributors](misc/contributors.md)
|
||||
|
|
|
@ -69,8 +69,6 @@ renderer need to be specified under the TOML table `[output.html]`.
|
|||
|
||||
The following configuration options are available:
|
||||
|
||||
pub playpen: Playpen,
|
||||
|
||||
- **theme:** mdBook comes with a default theme and all the resource files
|
||||
needed for it. But if this option is set, mdBook will selectively overwrite
|
||||
the theme files with the ones found in the specified folder.
|
||||
|
@ -105,51 +103,3 @@ additional-js = ["custom.js"]
|
|||
editor = "./path/to/editor"
|
||||
editable = false
|
||||
```
|
||||
|
||||
|
||||
## For Developers
|
||||
|
||||
If you are developing a plugin or alternate backend then whenever your code is
|
||||
called you will almost certainly be passed a reference to the book's `Config`.
|
||||
This can be treated roughly as a nested hashmap which lets you call methods like
|
||||
`get()` and `get_mut()` to get access to the config's contents.
|
||||
|
||||
By convention, plugin developers will have their settings as a subtable inside
|
||||
`plugins` (e.g. a link checker would put its settings in `plugins.link_check`)
|
||||
and backends should put their configuration under `output`, like the HTML
|
||||
renderer does in the previous examples.
|
||||
|
||||
As an example, some hypothetical `random` renderer would typically want to load
|
||||
its settings from the `Config` at the very start of its rendering process. The
|
||||
author can take advantage of serde to deserialize the generic `toml::Value`
|
||||
object retrieved from `Config` into a struct specific to its use case.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
struct RandomOutput {
|
||||
foo: u32,
|
||||
bar: String,
|
||||
baz: Vec<bool>,
|
||||
}
|
||||
|
||||
let src = r#"
|
||||
[output.random]
|
||||
foo = 5
|
||||
bar = "Hello World"
|
||||
baz = [true, true, false]
|
||||
"#;
|
||||
|
||||
let book_config = Config::from_str(src)?; // usually passed in by mdbook
|
||||
let random: Value = book_config.get("output.random").unwrap_or_default();
|
||||
let got: RandomOutput = random.try_into()?;
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
if let Some(baz) = book_config.get_deserialized::<Vec<bool>>("output.random.baz") {
|
||||
println!("{:?}", baz); // prints [true, true, false]
|
||||
|
||||
// do something interesting with baz
|
||||
}
|
||||
|
||||
// start the rendering process
|
||||
```
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
# For Developers
|
||||
|
||||
While `mdbook` is mainly used as a command line tool, you can also import the
|
||||
underlying library directly and use that to manage a book.
|
||||
|
||||
- Creating custom backends
|
||||
- Automatically generating and reloading a book on the fly
|
||||
- Integration with existing projects
|
||||
|
||||
The best source for examples on using the `mdbook` crate from your own Rust
|
||||
programs is the [API Docs].
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
The mechanism for using alternative backends is very simple, you add an extra
|
||||
table to your `book.toml` and the `MDBook::load()` function will automatically
|
||||
detect the backends being used.
|
||||
|
||||
For example, if you wanted to use a hypothetical `latex` backend you would add
|
||||
an empty `output.latex` table to `book.toml`.
|
||||
|
||||
```toml
|
||||
# book.toml
|
||||
|
||||
[book]
|
||||
...
|
||||
|
||||
[output.latex]
|
||||
```
|
||||
|
||||
And then during the rendering stage `mdbook` will run the `mdbook-latex`
|
||||
program, piping it a JSON serialized [RenderContext] via stdin.
|
||||
|
||||
You can set the command used via the `command` key.
|
||||
|
||||
```toml
|
||||
# book.toml
|
||||
|
||||
[book]
|
||||
...
|
||||
|
||||
[output.latex]
|
||||
command = "python3 my_plugin.py"
|
||||
```
|
||||
|
||||
If no backend is supplied (i.e. there are no `output.*` tables), `mdbook` will
|
||||
fall back to the `html` backend.
|
||||
|
||||
### The `Config` Struct
|
||||
|
||||
If you are developing a plugin or alternate backend then whenever your code is
|
||||
called you will almost certainly be passed a reference to the book's `Config`.
|
||||
This can be treated roughly as a nested hashmap which lets you call methods like
|
||||
`get()` and `get_mut()` to get access to the config's contents.
|
||||
|
||||
By convention, plugin developers will have their settings as a subtable inside
|
||||
`plugins` (e.g. a link checker would put its settings in `plugins.link_check`)
|
||||
and backends should put their configuration under `output`, like the HTML
|
||||
renderer does in the previous examples.
|
||||
|
||||
As an example, some hypothetical `random` renderer would typically want to load
|
||||
its settings from the `Config` at the very start of its rendering process. The
|
||||
author can take advantage of serde to deserialize the generic `toml::Value`
|
||||
object retrieved from `Config` into a struct specific to its use case.
|
||||
|
||||
```rust
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate toml;
|
||||
extern crate mdbook;
|
||||
|
||||
use toml::Value;
|
||||
use mdbook::config::Config;
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
struct RandomOutput {
|
||||
foo: u32,
|
||||
bar: String,
|
||||
baz: Vec<bool>,
|
||||
}
|
||||
|
||||
# fn run() -> Result<(), Box<::std::error::Error>> {
|
||||
let src = r#"
|
||||
[output.random]
|
||||
foo = 5
|
||||
bar = "Hello World"
|
||||
baz = [true, true, false]
|
||||
"#;
|
||||
|
||||
let book_config = Config::from_str(src)?; // usually passed in via the RenderContext
|
||||
let random = book_config.get("output.random")
|
||||
.cloned()
|
||||
.ok_or("output.random not found")?;
|
||||
let got: RandomOutput = random.try_into()?;
|
||||
|
||||
let should_be = RandomOutput {
|
||||
foo: 5,
|
||||
bar: "Hello World".to_string(),
|
||||
baz: vec![true, true, false]
|
||||
};
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
let baz: Vec<bool> = book_config.get_deserialized("output.random.baz")?;
|
||||
println!("{:?}", baz); // prints [true, true, false]
|
||||
|
||||
// do something interesting with baz
|
||||
# Ok(())
|
||||
# }
|
||||
# fn main() { run().unwrap() }
|
||||
```
|
||||
|
||||
|
||||
## Render Context
|
||||
|
||||
The `RenderContext` encapsulates all the information a backend needs to know
|
||||
in order to generate output. Its Rust definition looks something like this:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RenderContext {
|
||||
pub version: String,
|
||||
pub root: PathBuf,
|
||||
pub book: Book,
|
||||
pub config: Config,
|
||||
pub destination: PathBuf,
|
||||
}
|
||||
```
|
||||
|
||||
A backend will receive the `RenderContext` via `stdin` as one big JSON blob. If
|
||||
possible, it is recommended to import the `mdbook` crate and use the
|
||||
`RenderContext::from_json()` method. This way you should always be able to
|
||||
deserialize the `RenderContext`, and as a bonus will also have access to the
|
||||
methods already defined on the underlying types.
|
||||
|
||||
Although backends are told the book's root directory on disk, it is *strongly
|
||||
discouraged* to load chapter content from the filesystem. The `root` key is
|
||||
provided as an escape hatch for certain plugins which may load additional,
|
||||
non-markdown, files.
|
||||
|
||||
|
||||
## Output Directory
|
||||
|
||||
To make things more deterministic, a backend will be told where it should place
|
||||
its generated artefacts.
|
||||
|
||||
The general algorithm for deciding the output directory goes something like
|
||||
this:
|
||||
|
||||
- If there is only one backend:
|
||||
- `destination` is `config.build.build_dir` (usually `book/`)
|
||||
- Otherwise:
|
||||
- `destination` is `config.build.build_dir` joined with the backend's name
|
||||
(e.g. `build/latex/` for the "latex" backend)
|
||||
|
||||
|
||||
## Output and Signalling Failure
|
||||
|
||||
To signal that the plugin failed it just needs to exit with a non-zero return
|
||||
code.
|
||||
|
||||
All output from the plugin's subprocess is immediately passed through to the
|
||||
user, so it is encouraged for plugins to follow the ["rule of silence"] and
|
||||
by default only tell the user about things they directly need to respond to
|
||||
(e.g. an error in generation or a warning).
|
||||
|
||||
This "silent by default" behaviour can be overridden via the `RUST_LOG`
|
||||
environment variable (which `mdbook` will pass through to the backend if set)
|
||||
as is typical with Rust applications.
|
||||
|
||||
|
||||
[API Docs]: https://docs.rs/mdbook
|
||||
[RenderContext]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html
|
||||
["rule of silence"]: http://www.linfo.org/rule_of_silence.html
|
|
@ -1,24 +0,0 @@
|
|||
# Rust Library
|
||||
|
||||
mdBook is not only a command line tool, it can be used as a crate. You can extend it,
|
||||
integrate it in current projects. Here is a short example:
|
||||
|
||||
```rust,ignore
|
||||
extern crate mdbook;
|
||||
|
||||
use mdbook::MDBook;
|
||||
use std::path::Path;
|
||||
|
||||
# #[allow(unused_variables)]
|
||||
fn main() {
|
||||
let mut book = MDBook::new("my-book") // Path to root
|
||||
.with_source("src") // Path from root to source directory
|
||||
.with_destination("book") // Path from root to output directory
|
||||
.read_config() // Parse book.toml or book.json configuration file
|
||||
.expect("I don't handle configuration file error, but you should!");
|
||||
|
||||
book.build().unwrap(); // Render the book
|
||||
}
|
||||
```
|
||||
|
||||
Check here for the [API docs](mdbook/index.html) generated by rustdoc.
|
|
@ -30,7 +30,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
book.build()?;
|
||||
|
||||
if args.is_present("open") {
|
||||
open(book.get_destination().join("index.html"));
|
||||
// FIXME: What's the right behaviour if we don't use the HTML renderer?
|
||||
open(book.build_dir_for("html").join("index.html"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -26,7 +26,11 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
// Skip this if `--force` is present
|
||||
if !args.is_present("force") {
|
||||
// Print warning
|
||||
print!("\nCopying the default theme to {}", builder.config().book.src.display());
|
||||
println!();
|
||||
println!(
|
||||
"Copying the default theme to {}",
|
||||
builder.config().book.src.display()
|
||||
);
|
||||
println!("This could potentially overwrite files already present in that directory.");
|
||||
print!("\nAre you sure you want to continue? (y/n) ");
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ use clap::{App, AppSettings, ArgMatches};
|
|||
use chrono::Local;
|
||||
use log::LevelFilter;
|
||||
use env_logger::Builder;
|
||||
use error_chain::ChainedError;
|
||||
use mdbook::utils;
|
||||
|
||||
pub mod build;
|
||||
pub mod init;
|
||||
|
@ -64,7 +64,7 @@ fn main() {
|
|||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
eprintln!("{}", e.display_chain());
|
||||
utils::log_backtrace(&e);
|
||||
|
||||
::std::process::exit(101);
|
||||
}
|
||||
|
@ -101,12 +101,12 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
|||
p.to_path_buf()
|
||||
}
|
||||
} else {
|
||||
env::current_dir().unwrap()
|
||||
env::current_dir().expect("Unable to determine the current directory")
|
||||
}
|
||||
}
|
||||
|
||||
fn open<P: AsRef<OsStr>>(path: P) {
|
||||
if let Err(e) = open::that(path) {
|
||||
println!("Error opening web browser: {}", e);
|
||||
error!("Error opening web browser: {}", e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use self::iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Re
|
|||
Set};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::utils;
|
||||
use mdbook::errors::*;
|
||||
use {get_book_dir, open};
|
||||
#[cfg(feature = "watch")]
|
||||
|
@ -38,8 +39,6 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
|||
|
||||
// Watch command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
const RELOAD_COMMAND: &'static str = "reload";
|
||||
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
|
@ -52,29 +51,13 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
let address = format!("{}:{}", interface, port);
|
||||
let ws_address = format!("{}:{}", interface, ws_port);
|
||||
|
||||
let livereload = Some(format!(
|
||||
r#"
|
||||
<script type="text/javascript">
|
||||
var socket = new WebSocket("ws://{}:{}");
|
||||
socket.onmessage = function (event) {{
|
||||
if (event.data === "{}") {{
|
||||
socket.close();
|
||||
location.reload(true); // force reload from server (not from cache)
|
||||
}}
|
||||
}};
|
||||
|
||||
window.onbeforeunload = function() {{
|
||||
socket.close();
|
||||
}}
|
||||
</script>
|
||||
"#,
|
||||
public_address, ws_port, RELOAD_COMMAND
|
||||
));
|
||||
book.livereload = livereload.clone();
|
||||
let livereload_url = format!("ws://{}:{}", public_address, ws_port);
|
||||
book.config
|
||||
.set("output.html.livereload-url", &livereload_url)?;
|
||||
|
||||
book.build()?;
|
||||
|
||||
let mut chain = Chain::new(staticfile::Static::new(book.get_destination()));
|
||||
let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html")));
|
||||
chain.link_after(ErrorRecover);
|
||||
let _iron = Iron::new(chain)
|
||||
.http(&*address)
|
||||
|
@ -90,7 +73,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
});
|
||||
|
||||
let serving_url = format!("http://{}", address);
|
||||
println!("\nServing on: {}", serving_url);
|
||||
info!("Serving on: {}", serving_url);
|
||||
|
||||
if open_browser {
|
||||
open(serving_url);
|
||||
|
@ -98,26 +81,25 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
|
||||
#[cfg(feature = "watch")]
|
||||
watch::trigger_on_change(&mut book, move |path, book_dir| {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
info!("File changed: {:?}", path);
|
||||
info!("Building book...");
|
||||
|
||||
// FIXME: This area is really ugly because we need to re-set livereload :(
|
||||
|
||||
let livereload = livereload.clone();
|
||||
let livereload_url = livereload_url.clone();
|
||||
|
||||
let result = MDBook::load(&book_dir)
|
||||
.map(move |mut b| {
|
||||
b.livereload = livereload;
|
||||
b
|
||||
.and_then(move |mut b| {
|
||||
b.config.set("output.html.livereload-url", &livereload_url)?;
|
||||
Ok(b)
|
||||
})
|
||||
.and_then(|mut b| b.build());
|
||||
.and_then(|b| b.build());
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to load the book");
|
||||
error!("Error: {}", e);
|
||||
for cause in e.iter().skip(1) {
|
||||
error!("\tCaused By: {}", cause);
|
||||
}
|
||||
utils::log_backtrace(&e);
|
||||
} else {
|
||||
let _ = broadcaster.send(RELOAD_COMMAND);
|
||||
let _ = broadcaster.send("reload");
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use std::time::Duration;
|
|||
use std::sync::mpsc::channel;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::utils;
|
||||
use mdbook::errors::Result;
|
||||
use {get_book_dir, open};
|
||||
|
||||
|
@ -22,21 +23,21 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
|||
// Watch command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
let book = MDBook::load(&book_dir)?;
|
||||
|
||||
if args.is_present("open") {
|
||||
book.build()?;
|
||||
open(book.get_destination().join("index.html"));
|
||||
open(book.build_dir_for("html").join("index.html"));
|
||||
}
|
||||
|
||||
trigger_on_change(&book, |path, book_dir| {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
let result = MDBook::load(&book_dir).and_then(|mut b| b.build());
|
||||
info!("File changed: {:?}\nBuilding book...\n", path);
|
||||
let result = MDBook::load(&book_dir).and_then(|b| b.build());
|
||||
|
||||
if let Err(e) = result {
|
||||
println!("Error while building: {}", e);
|
||||
error!("Unable to build the book");
|
||||
utils::log_backtrace(&e);
|
||||
}
|
||||
println!();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
@ -56,14 +57,14 @@ where
|
|||
let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
println!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
error!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
::std::process::exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.source_dir(), Recursive) {
|
||||
println!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
|
||||
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
|
||||
::std::process::exit(1);
|
||||
};
|
||||
|
||||
|
@ -72,9 +73,10 @@ where
|
|||
// Add the book.toml file to the watcher if it exists
|
||||
let _ = watcher.watch(book.root.join("book.toml"), NonRecursive);
|
||||
|
||||
println!("\nListening for changes...\n");
|
||||
info!("Listening for changes...");
|
||||
|
||||
for event in rx.iter() {
|
||||
debug!("Received filesystem event: {:?}", event);
|
||||
match event {
|
||||
Create(path) | Write(path) | Remove(path) | Rename(_, path) => {
|
||||
closure(&path, &book.root);
|
||||
|
|
190
src/book/mod.rs
190
src/book/mod.rs
|
@ -15,13 +15,14 @@ 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::PathBuf;
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use tempdir::TempDir;
|
||||
use toml::Value;
|
||||
|
||||
use utils;
|
||||
use renderer::{HtmlHandlebars, Renderer};
|
||||
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
||||
use preprocess;
|
||||
use errors::*;
|
||||
|
||||
|
@ -33,9 +34,9 @@ pub struct MDBook {
|
|||
pub root: PathBuf,
|
||||
/// The configuration used to tweak now a book is built.
|
||||
pub config: Config,
|
||||
|
||||
book: Book,
|
||||
renderer: Box<Renderer>,
|
||||
/// A representation of the book's contents in memory.
|
||||
pub book: Book,
|
||||
renderers: Vec<Box<Renderer>>,
|
||||
|
||||
/// The URL used for live reloading when serving up the book.
|
||||
pub livereload: Option<String>,
|
||||
|
@ -75,17 +76,20 @@ impl MDBook {
|
|||
|
||||
/// 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 root = book_root.into();
|
||||
|
||||
let src_dir = book_root.join(&config.book.src);
|
||||
let src_dir = root.join(&config.book.src);
|
||||
let book = book::load_book(&src_dir, &config.build)?;
|
||||
let livereload = None;
|
||||
|
||||
let renderers = determine_renderers(&config);
|
||||
|
||||
Ok(MDBook {
|
||||
root: book_root,
|
||||
config: config,
|
||||
book: book,
|
||||
renderer: Box::new(HtmlHandlebars::new()),
|
||||
livereload: None,
|
||||
root,
|
||||
config,
|
||||
book,
|
||||
renderers,
|
||||
livereload,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -142,32 +146,47 @@ impl MDBook {
|
|||
}
|
||||
|
||||
/// Tells the renderer to build our book and put it in the build directory.
|
||||
pub fn build(&mut self) -> Result<()> {
|
||||
pub fn build(&self) -> Result<()> {
|
||||
debug!("[fn]: build");
|
||||
|
||||
let dest = self.get_destination();
|
||||
if dest.exists() {
|
||||
utils::fs::remove_dir_content(&dest).chain_err(|| "Unable to clear output directory")?;
|
||||
for renderer in &self.renderers {
|
||||
self.run_renderer(renderer.as_ref())?;
|
||||
}
|
||||
|
||||
self.renderer.render(self)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// FIXME: This doesn't belong as part of `MDBook`. It is only used by the HTML renderer
|
||||
#[doc(hidden)]
|
||||
pub fn write_file<P: AsRef<Path>>(&self, filename: P, content: &[u8]) -> Result<()> {
|
||||
let path = self.get_destination().join(filename);
|
||||
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::create_file(&path)?
|
||||
.write_all(content)
|
||||
.map_err(|e| e.into())
|
||||
utils::fs::remove_dir_content(&build_dir)
|
||||
.chain_err(|| "Unable to clear output directory")?;
|
||||
}
|
||||
|
||||
let render_context = RenderContext::new(
|
||||
self.root.clone(),
|
||||
self.book.clone(),
|
||||
self.config.clone(),
|
||||
build_dir,
|
||||
);
|
||||
|
||||
renderer
|
||||
.render(&render_context)
|
||||
.chain_err(|| "Rendering failed")
|
||||
}
|
||||
|
||||
/// 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)
|
||||
pub fn set_renderer<R: Renderer + 'static>(mut self, renderer: R) -> Self {
|
||||
self.renderer = Box::new(renderer);
|
||||
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
|
||||
self.renderers.push(Box::new(renderer));
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -215,10 +234,38 @@ impl MDBook {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
// FIXME: This doesn't belong under `MDBook`, it should really be passed to the renderer directly.
|
||||
#[doc(hidden)]
|
||||
pub fn get_destination(&self) -> PathBuf {
|
||||
self.root.join(&self.config.build.build_dir)
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the directory containing this book's source files.
|
||||
|
@ -226,7 +273,7 @@ impl MDBook {
|
|||
self.root.join(&self.config.book.src)
|
||||
}
|
||||
|
||||
// FIXME: This belongs as part of the `HtmlConfig`.
|
||||
// FIXME: This really belongs as part of the `HtmlConfig`.
|
||||
#[doc(hidden)]
|
||||
pub fn theme_dir(&self) -> PathBuf {
|
||||
match self.config.html_config().and_then(|h| h.theme) {
|
||||
|
@ -235,3 +282,84 @@ impl MDBook {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,8 @@ impl Config {
|
|||
/// Load the configuration file from disk.
|
||||
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
|
||||
let mut buffer = String::new();
|
||||
File::open(config_file).chain_err(|| "Unable to open the configuration file")?
|
||||
File::open(config_file)
|
||||
.chain_err(|| "Unable to open the configuration file")?
|
||||
.read_to_string(&mut buffer)
|
||||
.chain_err(|| "Couldn't read the file")?;
|
||||
|
||||
|
@ -53,7 +54,8 @@ impl Config {
|
|||
/// # Note
|
||||
///
|
||||
/// This is for compatibility only. It will be removed completely once the
|
||||
/// rendering and plugin system is established.
|
||||
/// HTML renderer is refactored to be less coupled to `mdbook` internals.
|
||||
#[doc(hidden)]
|
||||
pub fn html_config(&self) -> Option<HtmlConfig> {
|
||||
self.get_deserialized("output.html").ok()
|
||||
}
|
||||
|
@ -64,7 +66,8 @@ impl Config {
|
|||
let name = name.as_ref();
|
||||
|
||||
if let Some(value) = self.get(name) {
|
||||
value.clone()
|
||||
value
|
||||
.clone()
|
||||
.try_into()
|
||||
.chain_err(|| "Couldn't deserialize the value")
|
||||
} else {
|
||||
|
@ -72,6 +75,19 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
/// Set a config key, clobbering any existing values along the way.
|
||||
///
|
||||
/// The only way this can fail is if we can't serialize `value` into a
|
||||
/// `toml::Value`.
|
||||
pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
|
||||
let pieces: Vec<_> = index.as_ref().split(".").collect();
|
||||
let value =
|
||||
Value::try_from(value).chain_err(|| "Unable to represent the item as a JSON Value")?;
|
||||
recursive_set(&pieces, &mut self.rest, value);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn from_legacy(mut table: Table) -> Config {
|
||||
let mut cfg = Config::default();
|
||||
|
||||
|
@ -94,7 +110,8 @@ impl Config {
|
|||
// This complicated chain of and_then's is so we can move
|
||||
// "output.html.destination" to "build.build_dir" and parse it into a
|
||||
// PathBuf.
|
||||
let destination: Option<PathBuf> = table.get_mut("output")
|
||||
let destination: Option<PathBuf> = table
|
||||
.get_mut("output")
|
||||
.and_then(|output| output.as_table_mut())
|
||||
.and_then(|output| output.get_mut("html"))
|
||||
.and_then(|html| html.as_table_mut())
|
||||
|
@ -110,6 +127,32 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
/// Recursively walk down a table and try to set some `foo.bar.baz` value.
|
||||
///
|
||||
/// If at any table along the way doesn't exist (or isn't itself a `Table`!) an
|
||||
/// empty `Table` will be inserted. e.g. if the `foo` table didn't contain a
|
||||
/// nested table called `bar`, we'd insert one and then keep recursing.
|
||||
fn recursive_set(key: &[&str], table: &mut Table, value: Value) {
|
||||
if key.is_empty() {
|
||||
unreachable!();
|
||||
} else if key.len() == 1 {
|
||||
table.insert(key[0].to_string(), value);
|
||||
} else {
|
||||
let first = key[0];
|
||||
let rest = &key[1..];
|
||||
|
||||
// if `table[first]` isn't a table, replace whatever is there with a
|
||||
// new table.
|
||||
if table.get(first).and_then(|t| t.as_table()).is_none() {
|
||||
table.insert(first.to_string(), Value::Table(Table::new()));
|
||||
}
|
||||
|
||||
let nested = table.get_mut(first).and_then(|t| t.as_table_mut()).unwrap();
|
||||
recursive_set(rest, nested, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// The "getter" version of `recursive_set()`.
|
||||
fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> {
|
||||
if key.is_empty() {
|
||||
return None;
|
||||
|
@ -127,6 +170,7 @@ fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The mutable version of `recursive_get()`.
|
||||
fn recursive_get_mut<'a>(key: &[&str], table: &'a mut Table) -> Option<&'a mut Value> {
|
||||
// TODO: Figure out how to abstract over mutability to reduce copy-pasta
|
||||
if key.is_empty() {
|
||||
|
@ -171,11 +215,13 @@ impl<'de> Deserialize<'de> for Config {
|
|||
return Ok(Config::from_legacy(table));
|
||||
}
|
||||
|
||||
let book: BookConfig = table.remove("book")
|
||||
let book: BookConfig = table
|
||||
.remove("book")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let build: BuildConfig = table.remove("build")
|
||||
let build: BuildConfig = table
|
||||
.remove("build")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
|
@ -208,10 +254,11 @@ impl Serialize for Config {
|
|||
fn is_legacy_format(table: &Table) -> bool {
|
||||
let top_level_items = ["title", "author", "authors"];
|
||||
|
||||
top_level_items.iter().any(|key| table.contains_key(&key.to_string()))
|
||||
top_level_items
|
||||
.iter()
|
||||
.any(|key| table.contains_key(&key.to_string()))
|
||||
}
|
||||
|
||||
|
||||
/// Configuration options which are specific to the book and required for
|
||||
/// loading it from disk.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
@ -271,6 +318,14 @@ pub struct HtmlConfig {
|
|||
pub additional_css: Vec<PathBuf>,
|
||||
pub additional_js: Vec<PathBuf>,
|
||||
pub playpen: Playpen,
|
||||
/// This is used as a bit of a workaround for the `mdbook serve` command.
|
||||
/// Basically, because you set the websocket port from the command line, the
|
||||
/// `mdbook serve` command needs a way to let the HTML renderer know where
|
||||
/// to point livereloading at, if it has been enabled.
|
||||
///
|
||||
/// This config item *should not be edited* by the end user.
|
||||
#[doc(hidden)]
|
||||
pub livereload_url: Option<String>,
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the the HTML renderer handles the playpen.
|
||||
|
@ -290,7 +345,6 @@ impl Default for Playpen {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -450,4 +504,17 @@ mod tests {
|
|||
assert_eq!(got.build, build_should_be);
|
||||
assert_eq!(got.html_config().unwrap(), html_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_a_config_item() {
|
||||
let mut cfg = Config::default();
|
||||
let key = "foo.bar.baz";
|
||||
let value = "Something Interesting";
|
||||
|
||||
assert!(cfg.get(key).is_none());
|
||||
cfg.set(key, value).unwrap();
|
||||
|
||||
let got: String = cfg.get_deserialized(key).unwrap();
|
||||
assert_eq!(got, value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
//! # let your_renderer = HtmlHandlebars::new();
|
||||
//! #
|
||||
//! let mut book = MDBook::load("my-book").unwrap();
|
||||
//! book.set_renderer(your_renderer);
|
||||
//! book.with_renderer(your_renderer);
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
|
@ -109,7 +109,9 @@ extern crate serde;
|
|||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
extern crate shlex;
|
||||
extern crate tempdir;
|
||||
extern crate tempfile;
|
||||
extern crate toml;
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
use renderer::html_handlebars::helpers;
|
||||
use preprocess;
|
||||
use renderer::Renderer;
|
||||
use book::MDBook;
|
||||
use book::{BookItem, Chapter};
|
||||
use config::{Config, Playpen, HtmlConfig};
|
||||
use {utils, theme};
|
||||
use theme::{Theme, playpen_editor};
|
||||
use renderer::{RenderContext, Renderer};
|
||||
use book::{Book, BookItem, Chapter};
|
||||
use config::{Config, HtmlConfig, Playpen};
|
||||
use {theme, utils};
|
||||
use theme::{playpen_editor, Theme};
|
||||
use errors::*;
|
||||
use regex::{Captures, Regex};
|
||||
|
||||
#[allow(unused_imports)] use std::ascii::AsciiExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{self, Read};
|
||||
use std::io::{self, Read, Write};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -28,15 +27,28 @@ impl HtmlHandlebars {
|
|||
HtmlHandlebars
|
||||
}
|
||||
|
||||
fn render_item(&self,
|
||||
fn write_file<P: AsRef<Path>>(
|
||||
&self,
|
||||
build_dir: &Path,
|
||||
filename: P,
|
||||
content: &[u8],
|
||||
) -> Result<()> {
|
||||
let path = build_dir.join(filename);
|
||||
|
||||
utils::fs::create_file(&path)?
|
||||
.write_all(content)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
fn render_item(
|
||||
&self,
|
||||
item: &BookItem,
|
||||
mut ctx: RenderItemContext,
|
||||
print_content: &mut String)
|
||||
-> Result<()> {
|
||||
print_content: &mut String,
|
||||
) -> Result<()> {
|
||||
// FIXME: This should be made DRY-er and rely less on mutable state
|
||||
match *item {
|
||||
BookItem::Chapter(ref ch) =>
|
||||
{
|
||||
BookItem::Chapter(ref ch) => {
|
||||
let content = ch.content.clone();
|
||||
let base = ch.path.parent()
|
||||
.map(|dir| ctx.src_dir.join(dir))
|
||||
|
@ -83,18 +95,18 @@ impl HtmlHandlebars {
|
|||
let filepath = Path::new(&ch.path).with_extension("html");
|
||||
let rendered = self.post_process(
|
||||
rendered,
|
||||
&normalize_path(filepath.to_str().ok_or_else(|| Error::from(
|
||||
format!("Bad file name: {}", filepath.display()),
|
||||
))?),
|
||||
&ctx.book.config.html_config().unwrap_or_default().playpen,
|
||||
&normalize_path(filepath.to_str().ok_or_else(|| {
|
||||
Error::from(format!("Bad file name: {}", filepath.display()))
|
||||
})?),
|
||||
&ctx.html_config.playpen,
|
||||
);
|
||||
|
||||
// Write to file
|
||||
info!("[*] Creating {:?} ✓", filepath.display());
|
||||
ctx.book.write_file(filepath, &rendered.into_bytes())?;
|
||||
self.write_file(&ctx.destination, filepath, &rendered.into_bytes())?;
|
||||
|
||||
if ctx.is_index {
|
||||
self.render_index(ctx.book, ch, &ctx.destination)?;
|
||||
self.render_index(ch, &ctx.destination)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
@ -104,7 +116,7 @@ impl HtmlHandlebars {
|
|||
}
|
||||
|
||||
/// Create an index.html from the first element in SUMMARY.md
|
||||
fn render_index(&self, book: &MDBook, ch: &Chapter, destination: &Path) -> Result<()> {
|
||||
fn render_index(&self, ch: &Chapter, destination: &Path) -> Result<()> {
|
||||
debug!("[*]: index.html");
|
||||
|
||||
let mut content = String::new();
|
||||
|
@ -120,10 +132,10 @@ impl HtmlHandlebars {
|
|||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
book.write_file("index.html", content.as_bytes())?;
|
||||
self.write_file(destination, "index.html", content.as_bytes())?;
|
||||
|
||||
info!("[*] Creating index.html from {:?} ✓",
|
||||
book.get_destination().join(&ch.path.with_extension("html")));
|
||||
destination.join(&ch.path.with_extension("html")));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -142,30 +154,57 @@ impl HtmlHandlebars {
|
|||
rendered
|
||||
}
|
||||
|
||||
fn copy_static_files(&self, book: &MDBook, theme: &Theme, html_config: &HtmlConfig) -> Result<()> {
|
||||
book.write_file("book.js", &theme.js)?;
|
||||
book.write_file("book.css", &theme.css)?;
|
||||
book.write_file("favicon.png", &theme.favicon)?;
|
||||
book.write_file("jquery.js", &theme.jquery)?;
|
||||
book.write_file("highlight.css", &theme.highlight_css)?;
|
||||
book.write_file("tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||
book.write_file("ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||
book.write_file("highlight.js", &theme.highlight_js)?;
|
||||
book.write_file("clipboard.min.js", &theme.clipboard_js)?;
|
||||
book.write_file("store.js", &theme.store_js)?;
|
||||
book.write_file("_FontAwesome/css/font-awesome.css", theme::FONT_AWESOME)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2)?;
|
||||
book.write_file("_FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF)?;
|
||||
fn copy_static_files(
|
||||
&self,
|
||||
destination: &Path,
|
||||
theme: &Theme,
|
||||
html_config: &HtmlConfig,
|
||||
) -> Result<()> {
|
||||
self.write_file(destination, "book.js", &theme.js)?;
|
||||
self.write_file(destination, "book.css", &theme.css)?;
|
||||
self.write_file(destination, "favicon.png", &theme.favicon)?;
|
||||
self.write_file(destination, "jquery.js", &theme.jquery)?;
|
||||
self.write_file(destination, "highlight.css", &theme.highlight_css)?;
|
||||
self.write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||
self.write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||
self.write_file(destination, "highlight.js", &theme.highlight_js)?;
|
||||
self.write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
|
||||
self.write_file(destination, "store.js", &theme.store_js)?;
|
||||
self.write_file(
|
||||
destination,
|
||||
"_FontAwesome/css/font-awesome.css",
|
||||
theme::FONT_AWESOME,
|
||||
)?;
|
||||
self.write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT,
|
||||
)?;
|
||||
self.write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG,
|
||||
)?;
|
||||
self.write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
self.write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF,
|
||||
)?;
|
||||
self.write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2,
|
||||
)?;
|
||||
self.write_file(
|
||||
destination,
|
||||
"_FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
|
||||
let playpen_config = &html_config.playpen;
|
||||
|
||||
|
@ -173,38 +212,19 @@ impl HtmlHandlebars {
|
|||
if playpen_config.editable {
|
||||
// Load the editor
|
||||
let editor = playpen_editor::PlaypenEditor::new(&playpen_config.editor);
|
||||
book.write_file("editor.js", &editor.js)?;
|
||||
book.write_file("ace.js", &editor.ace_js)?;
|
||||
book.write_file("mode-rust.js", &editor.mode_rust_js)?;
|
||||
book.write_file("theme-dawn.js", &editor.theme_dawn_js)?;
|
||||
book.write_file("theme-tomorrow_night.js", &editor.theme_tomorrow_night_js)?;
|
||||
self.write_file(destination, "editor.js", &editor.js)?;
|
||||
self.write_file(destination, "ace.js", &editor.ace_js)?;
|
||||
self.write_file(destination, "mode-rust.js", &editor.mode_rust_js)?;
|
||||
self.write_file(destination, "theme-dawn.js", &editor.theme_dawn_js)?;
|
||||
self.write_file(destination,
|
||||
"theme-tomorrow_night.js",
|
||||
&editor.theme_tomorrow_night_js,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to write a file to the build directory, normalizing
|
||||
/// the path to be relative to the book root.
|
||||
fn write_custom_file(&self, custom_file: &Path, book: &MDBook) -> Result<()> {
|
||||
let mut data = Vec::new();
|
||||
let mut f = File::open(custom_file)?;
|
||||
f.read_to_end(&mut data)?;
|
||||
|
||||
let name = match custom_file.strip_prefix(&book.root) {
|
||||
Ok(p) => p.to_str().expect("Could not convert to str"),
|
||||
Err(_) => {
|
||||
custom_file.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str")
|
||||
}
|
||||
};
|
||||
|
||||
book.write_file(name, &data)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the context with data for this file
|
||||
fn configure_print_version(&self,
|
||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||
|
@ -227,27 +247,42 @@ impl HtmlHandlebars {
|
|||
|
||||
/// Copy across any additional CSS and JavaScript files which the book
|
||||
/// has been configured to use.
|
||||
fn copy_additional_css_and_js(&self, book: &MDBook) -> Result<()> {
|
||||
let html = book.config.html_config().unwrap_or_default();
|
||||
fn copy_additional_css_and_js(&self, html: &HtmlConfig, destination: &Path) -> Result<()> {
|
||||
let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
|
||||
|
||||
let custom_files = html.additional_css
|
||||
.iter()
|
||||
.chain(html.additional_js.iter());
|
||||
debug!("Copying additional CSS and JS");
|
||||
|
||||
for custom_file in custom_files {
|
||||
self.write_custom_file(&custom_file, book)
|
||||
.chain_err(|| format!("Copying {} failed", custom_file.display()))?;
|
||||
let output_location = destination.join(custom_file);
|
||||
debug!(
|
||||
"Copying {} -> {}",
|
||||
custom_file.display(),
|
||||
output_location.display()
|
||||
);
|
||||
|
||||
fs::copy(custom_file, &output_location).chain_err(|| {
|
||||
format!(
|
||||
"Unable to copy {} to {}",
|
||||
custom_file.display(),
|
||||
output_location.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Renderer for HtmlHandlebars {
|
||||
fn render(&self, book: &MDBook) -> Result<()> {
|
||||
let html_config = book.config.html_config().unwrap_or_default();
|
||||
let src_dir = book.root.join(&book.config.book.src);
|
||||
fn name(&self) -> &str {
|
||||
"html"
|
||||
}
|
||||
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||
let html_config = ctx.config.html_config().unwrap_or_default();
|
||||
let src_dir = ctx.root.join(&ctx.config.book.src);
|
||||
let destination = &ctx.destination;
|
||||
let book = &ctx.book;
|
||||
|
||||
debug!("[fn]: render");
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
@ -274,21 +309,17 @@ impl Renderer for HtmlHandlebars {
|
|||
debug!("[*]: Register handlebars helpers");
|
||||
self.register_hbs_helpers(&mut handlebars);
|
||||
|
||||
let mut data = make_data(book, &book.config)?;
|
||||
let mut data = make_data(&ctx.root, &book, &ctx.config, &html_config)?;
|
||||
|
||||
// Print version
|
||||
let mut print_content = String::new();
|
||||
|
||||
// TODO: The Renderer trait should really pass in where it wants us to build to...
|
||||
let destination = book.get_destination();
|
||||
|
||||
debug!("[*]: Check if destination directory exists");
|
||||
fs::create_dir_all(&destination)
|
||||
.chain_err(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
for (i, item) in book.iter().enumerate() {
|
||||
let ctx = RenderItemContext {
|
||||
book: book,
|
||||
handlebars: &handlebars,
|
||||
destination: destination.to_path_buf(),
|
||||
src_dir: src_dir.clone(),
|
||||
|
@ -301,7 +332,7 @@ impl Renderer for HtmlHandlebars {
|
|||
|
||||
// Print version
|
||||
self.configure_print_version(&mut data, &print_content);
|
||||
if let Some(ref title) = book.config.book.title {
|
||||
if let Some(ref title) = ctx.config.book.title {
|
||||
data.insert("title".to_owned(), json!(title));
|
||||
}
|
||||
|
||||
|
@ -314,25 +345,23 @@ impl Renderer for HtmlHandlebars {
|
|||
"print.html",
|
||||
&html_config.playpen);
|
||||
|
||||
book.write_file(Path::new("print").with_extension("html"),
|
||||
&rendered.into_bytes())?;
|
||||
self.write_file(&destination, "print.html", &rendered.into_bytes())?;
|
||||
info!("[*] Creating print.html ✓");
|
||||
|
||||
debug!("[*] Copy static files");
|
||||
self.copy_static_files(book, &theme, &html_config)
|
||||
self.copy_static_files(&destination, &theme, &html_config)
|
||||
.chain_err(|| "Unable to copy across static files")?;
|
||||
self.copy_additional_css_and_js(book)
|
||||
self.copy_additional_css_and_js(&html_config, &destination)
|
||||
.chain_err(|| "Unable to copy across additional CSS and JS")?;
|
||||
|
||||
// Copy all remaining files
|
||||
let src = book.source_dir();
|
||||
utils::fs::copy_files_except_ext(&src, &destination, true, &["md"])?;
|
||||
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_data(book: &MDBook, config: &Config) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
debug!("[fn]: make_data");
|
||||
let html = config.html_config().unwrap_or_default();
|
||||
|
||||
|
@ -341,7 +370,7 @@ fn make_data(book: &MDBook, config: &Config) -> Result<serde_json::Map<String, s
|
|||
data.insert("book_title".to_owned(), json!(config.book.title.clone().unwrap_or_default()));
|
||||
data.insert("description".to_owned(), json!(config.book.description.clone().unwrap_or_default()));
|
||||
data.insert("favicon".to_owned(), json!("favicon.png"));
|
||||
if let Some(ref livereload) = book.livereload {
|
||||
if let Some(ref livereload) = html_config.livereload_url {
|
||||
data.insert("livereload".to_owned(), json!(livereload));
|
||||
}
|
||||
|
||||
|
@ -358,7 +387,7 @@ fn make_data(book: &MDBook, config: &Config) -> Result<serde_json::Map<String, s
|
|||
if !html.additional_css.is_empty() {
|
||||
let mut css = Vec::new();
|
||||
for style in &html.additional_css {
|
||||
match style.strip_prefix(&book.root) {
|
||||
match style.strip_prefix(root) {
|
||||
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => {
|
||||
css.push(style.file_name()
|
||||
|
@ -375,7 +404,7 @@ fn make_data(book: &MDBook, config: &Config) -> Result<serde_json::Map<String, s
|
|||
if !html.additional_js.is_empty() {
|
||||
let mut js = Vec::new();
|
||||
for script in &html.additional_js {
|
||||
match script.strip_prefix(&book.root) {
|
||||
match script.strip_prefix(root) {
|
||||
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => {
|
||||
js.push(script.file_name()
|
||||
|
@ -604,7 +633,6 @@ fn partition_source(s: &str) -> (String, String) {
|
|||
|
||||
struct RenderItemContext<'a> {
|
||||
handlebars: &'a Handlebars,
|
||||
book: &'a MDBook,
|
||||
destination: PathBuf,
|
||||
src_dir: PathBuf,
|
||||
data: serde_json::Map<String, serde_json::Value>,
|
||||
|
|
|
@ -1,9 +1,180 @@
|
|||
//! `mdbook`'s low level rendering interface.
|
||||
//!
|
||||
//! # Note
|
||||
//!
|
||||
//! You usually don't need to work with this module directly. If you want to
|
||||
//! implement your own backend, then check out the [For Developers] section of
|
||||
//! the user guide.
|
||||
//!
|
||||
//! The definition for [RenderContext] may be useful though.
|
||||
//!
|
||||
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/lib/index.html
|
||||
//! [RenderContext]: struct.RenderContext.html
|
||||
|
||||
pub use self::html_handlebars::HtmlHandlebars;
|
||||
|
||||
mod html_handlebars;
|
||||
|
||||
use errors::*;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use serde_json;
|
||||
use tempfile;
|
||||
use shlex::Shlex;
|
||||
|
||||
use errors::*;
|
||||
use config::Config;
|
||||
use book::Book;
|
||||
|
||||
/// An arbitrary `mdbook` backend.
|
||||
///
|
||||
/// Although it's quite possible for you to import `mdbook` as a library and
|
||||
/// provide your own renderer, there are two main renderer implementations that
|
||||
/// 99% of users will ever use:
|
||||
///
|
||||
/// - [HtmlHandlebars] - the built-in HTML renderer
|
||||
/// - [CmdRenderer] - a generic renderer which shells out to a program to do the
|
||||
/// actual rendering
|
||||
///
|
||||
/// [HtmlHandlebars]: struct.HtmlHandlebars.html
|
||||
/// [CmdRenderer]: struct.CmdRenderer.html
|
||||
pub trait Renderer {
|
||||
fn render(&self, book: &::book::MDBook) -> Result<()>;
|
||||
/// The `Renderer`'s name.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Invoke the `Renderer`, passing in all the necessary information for
|
||||
/// describing a book.
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()>;
|
||||
}
|
||||
|
||||
/// The context provided to all renderers.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RenderContext {
|
||||
/// Which version of `mdbook` did this come from (as written in `mdbook`'s
|
||||
/// `Cargo.toml`). Useful if you know the renderer is only compatible with
|
||||
/// certain versions of `mdbook`.
|
||||
pub version: String,
|
||||
/// The book's root directory.
|
||||
pub root: PathBuf,
|
||||
/// A loaded representation of the book itself.
|
||||
pub book: Book,
|
||||
/// The loaded configuration file.
|
||||
pub config: Config,
|
||||
/// Where the renderer *must* put any build artefacts generated. To allow
|
||||
/// renderers to cache intermediate results, this directory is not
|
||||
/// guaranteed to be empty or even exist.
|
||||
pub destination: PathBuf,
|
||||
}
|
||||
|
||||
impl RenderContext {
|
||||
/// Create a new `RenderContext`.
|
||||
pub(crate) fn new<P, Q>(root: P, book: Book, config: Config, destination: Q) -> RenderContext
|
||||
where
|
||||
P: Into<PathBuf>,
|
||||
Q: Into<PathBuf>,
|
||||
{
|
||||
RenderContext {
|
||||
book: book,
|
||||
config: config,
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
root: root.into(),
|
||||
destination: destination.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the source directory's (absolute) path on disk.
|
||||
pub fn source_dir(&self) -> PathBuf {
|
||||
self.root.join(&self.config.book.src)
|
||||
}
|
||||
|
||||
/// Load a `RenderContext` from its JSON representation.
|
||||
pub fn from_json<R: Read>(reader: R) -> Result<RenderContext> {
|
||||
serde_json::from_reader(reader).chain_err(|| "Unable to deserialize the `RenderContext`")
|
||||
}
|
||||
}
|
||||
|
||||
/// A generic renderer which will shell out to an arbitrary executable.
|
||||
///
|
||||
/// # Rendering Protocol
|
||||
///
|
||||
/// When the renderer's `render()` method is invoked, `CmdRenderer` will spawn
|
||||
/// the `cmd` as a subprocess. The `RenderContext` is passed to the subprocess
|
||||
/// as a JSON string (using `serde_json`).
|
||||
///
|
||||
/// > **Note:** The command used doesn't necessarily need to be a single
|
||||
/// > executable (i.e. `/path/to/renderer`). The `cmd` string lets you pass
|
||||
/// > in command line arguments, so there's no reason why it couldn't be
|
||||
/// > `python /path/to/renderer --from mdbook --to epub`.
|
||||
///
|
||||
/// Anything the subprocess writes to `stdin` or `stdout` will be passed through
|
||||
/// to the user. While this gives the renderer maximum flexibility to output
|
||||
/// whatever it wants, to avoid spamming users it is recommended to avoid
|
||||
/// unnecessary output.
|
||||
///
|
||||
/// To help choose the appropriate output level, the `RUST_LOG` environment
|
||||
/// variable will be passed through to the subprocess, if set.
|
||||
///
|
||||
/// If the subprocess wishes to indicate that rendering failed, it should exit
|
||||
/// with a non-zero return code.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CmdRenderer {
|
||||
name: String,
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
impl CmdRenderer {
|
||||
/// Create a new `CmdRenderer` which will invoke the provided `cmd` string.
|
||||
pub fn new(name: String, cmd: String) -> CmdRenderer {
|
||||
CmdRenderer { name, cmd }
|
||||
}
|
||||
|
||||
fn compose_command(&self) -> Result<Command> {
|
||||
let mut words = Shlex::new(&self.cmd);
|
||||
let executable = match words.next() {
|
||||
Some(e) => e,
|
||||
None => bail!("Command string was empty"),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(executable);
|
||||
|
||||
for arg in words {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for CmdRenderer {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||
info!("Invoking the \"{}\" renderer", self.cmd);
|
||||
|
||||
// We need to write the RenderContext to a temporary file here instead
|
||||
// of passing it in via a pipe. This prevents a race condition where
|
||||
// some quickly executing command (e.g. `/bin/true`) may exit before we
|
||||
// finish writing the render context (closing the stdin pipe and
|
||||
// throwing a write error).
|
||||
let mut temp = tempfile::tempfile().chain_err(|| "Unable to create a temporary file")?;
|
||||
serde_json::to_writer(&mut temp, &ctx)
|
||||
.chain_err(|| "Unable to serialize the RenderContext")?;
|
||||
|
||||
let status = self.compose_command()?
|
||||
.stdin(temp)
|
||||
.current_dir(&ctx.destination)
|
||||
.status()
|
||||
.chain_err(|| "Unable to start the renderer")?;
|
||||
|
||||
trace!("{} exited with output: {:?}", self.cmd, status);
|
||||
|
||||
if !status.success() {
|
||||
error!("Renderer exited with non-zero return code.");
|
||||
bail!("The \"{}\" renderer failed", self.cmd);
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,8 +139,22 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
{{#if livereload}}
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
{{{livereload}}}
|
||||
<script type="text/javascript">
|
||||
var socket = new WebSocket("{{{livereload}}}");
|
||||
socket.onmessage = function (event) {
|
||||
if (event.data === "reload") {
|
||||
socket.close();
|
||||
location.reload(true); // force reload from server (not from cache)
|
||||
}
|
||||
};
|
||||
|
||||
window.onbeforeunload = function() {
|
||||
socket.close();
|
||||
}
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if google_analytics}}
|
||||
<!-- Google Analytics Tag -->
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
pub mod fs;
|
||||
mod string;
|
||||
use errors::Error;
|
||||
|
||||
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
|
||||
OPTION_ENABLE_TABLES};
|
||||
|
@ -7,10 +8,7 @@ use std::borrow::Cow;
|
|||
|
||||
pub use self::string::{RangeArgument, take_lines};
|
||||
|
||||
///
|
||||
///
|
||||
/// Wrapper around the pulldown-cmark parser and renderer to render markdown
|
||||
|
||||
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
|
||||
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
||||
let mut s = String::with_capacity(text.len() * 3 / 2);
|
||||
|
||||
|
@ -105,6 +103,15 @@ fn convert_quotes_to_curly(original_text: &str) -> String {
|
|||
.collect()
|
||||
}
|
||||
|
||||
/// Prints a "backtrace" of some `Error`.
|
||||
pub fn log_backtrace(e: &Error) {
|
||||
error!("Error: {}", e);
|
||||
|
||||
for cause in e.iter().skip(1) {
|
||||
error!("\tCaused By: {}", cause);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod render_markdown {
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
//! Integration tests to make sure alternate backends work.
|
||||
|
||||
extern crate mdbook;
|
||||
extern crate tempdir;
|
||||
|
||||
use tempdir::TempDir;
|
||||
use mdbook::config::Config;
|
||||
use mdbook::MDBook;
|
||||
|
||||
#[test]
|
||||
fn passing_alternate_backend() {
|
||||
let (md, _temp) = dummy_book_with_backend("passing", "true");
|
||||
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failing_alternate_backend() {
|
||||
let (md, _temp) = dummy_book_with_backend("failing", "false");
|
||||
|
||||
md.build().unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternate_backend_with_arguments() {
|
||||
let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!");
|
||||
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
||||
fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) {
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
|
||||
let mut config = Config::default();
|
||||
config
|
||||
.set(format!("output.{}.command", name), command)
|
||||
.unwrap();
|
||||
|
||||
let md = MDBook::init(temp.path())
|
||||
.with_config(config)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
(md, temp)
|
||||
}
|
|
@ -62,7 +62,7 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
|
|||
#[test]
|
||||
fn book_toml_isnt_required() {
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
let mut md = MDBook::init(temp.path()).build().unwrap();
|
||||
let md = MDBook::init(temp.path()).build().unwrap();
|
||||
|
||||
let _ = fs::remove_file(temp.path().join("book.toml"));
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ use mdbook::utils::fs::file_to_string;
|
|||
use mdbook::config::Config;
|
||||
use mdbook::MDBook;
|
||||
|
||||
|
||||
const BOOK_ROOT: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/dummy_book");
|
||||
const TOC_TOP_LEVEL: &[&'static str] = &[
|
||||
"1. First Chapter",
|
||||
|
@ -36,7 +35,7 @@ const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter"];
|
|||
#[test]
|
||||
fn build_the_dummy_book() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
@ -44,7 +43,7 @@ fn build_the_dummy_book() {
|
|||
#[test]
|
||||
fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
|
||||
assert!(!temp.path().join("book").exists());
|
||||
md.build().unwrap();
|
||||
|
@ -56,7 +55,7 @@ fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
|
|||
#[test]
|
||||
fn make_sure_bottom_level_files_contain_links_to_chapters() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let dest = temp.path().join("book");
|
||||
|
@ -78,7 +77,7 @@ fn make_sure_bottom_level_files_contain_links_to_chapters() {
|
|||
#[test]
|
||||
fn check_correct_cross_links_in_nested_dir() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let first = temp.path().join("book").join("first");
|
||||
|
@ -115,7 +114,7 @@ fn check_correct_cross_links_in_nested_dir() {
|
|||
#[test]
|
||||
fn rendered_code_has_playpen_stuff() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let nested = temp.path().join("book/first/nested.html");
|
||||
|
@ -138,7 +137,7 @@ fn chapter_content_appears_in_rendered_document() {
|
|||
];
|
||||
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let destination = temp.path().join("book");
|
||||
|
@ -149,7 +148,6 @@ fn chapter_content_appears_in_rendered_document() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// Apply a series of predicates to some root predicate, where each
|
||||
/// successive predicate is the descendant of the last one. Similar to how you
|
||||
/// might do `ul.foo li a` in CSS to access all anchor tags in the `foo` list.
|
||||
|
@ -162,7 +160,6 @@ macro_rules! descendants {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
/// Make sure that all `*.md` files (excluding `SUMMARY.md`) were rendered
|
||||
/// and placed in the `book` directory with their extensions set to `*.html`.
|
||||
#[test]
|
||||
|
@ -286,7 +283,7 @@ fn create_missing_file_with_config() {
|
|||
#[test]
|
||||
fn able_to_include_rust_files_in_chapters() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let second = temp.path().join("book/second.html");
|
||||
|
@ -302,10 +299,9 @@ fn able_to_include_rust_files_in_chapters() {
|
|||
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 md = MDBook::load(example_book_dir.path()).unwrap();
|
||||
|
||||
let got = md.build();
|
||||
assert!(got.is_ok());
|
||||
md.build().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
Loading…
Reference in New Issue