2018-01-07 22:10:48 +08:00
|
|
|
//! `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.
|
|
|
|
|
//!
|
2019-10-29 08:04:16 -05:00
|
|
|
//! [For Developers]: https://rust-lang.github.io/mdBook/for_developers/index.html
|
2018-01-07 22:10:48 +08:00
|
|
|
//! [RenderContext]: struct.RenderContext.html
|
|
|
|
|
|
2015-07-19 00:08:38 +02:00
|
|
|
pub use self::html_handlebars::HtmlHandlebars;
|
2019-08-30 06:20:53 -04:00
|
|
|
pub use self::markdown_renderer::MarkdownRenderer;
|
2015-07-19 00:08:38 +02:00
|
|
|
|
2015-08-05 22:35:26 +02:00
|
|
|
mod html_handlebars;
|
2019-08-30 06:20:53 -04:00
|
|
|
mod markdown_renderer;
|
2016-04-27 14:19:59 +02:00
|
|
|
|
2018-07-23 12:45:01 -05:00
|
|
|
use shlex::Shlex;
|
2021-03-23 18:36:45 -07:00
|
|
|
use std::collections::HashMap;
|
2018-01-16 07:26:01 +08:00
|
|
|
use std::fs;
|
2019-12-31 11:16:59 +01:00
|
|
|
use std::io::{self, ErrorKind, Read};
|
2020-12-30 17:26:59 -08:00
|
|
|
use std::path::{Path, PathBuf};
|
2018-01-14 19:14:27 +08:00
|
|
|
use std::process::{Command, Stdio};
|
2018-01-07 22:10:48 +08:00
|
|
|
|
2019-05-26 01:50:41 +07:00
|
|
|
use crate::book::Book;
|
|
|
|
|
use crate::config::Config;
|
|
|
|
|
use crate::errors::*;
|
2019-12-31 11:16:59 +01:00
|
|
|
use toml::Value;
|
2016-04-27 14:19:59 +02:00
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
/// 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:
|
|
|
|
|
///
|
2021-01-10 14:51:30 -08:00
|
|
|
/// - [`HtmlHandlebars`] - the built-in HTML renderer
|
|
|
|
|
/// - [`CmdRenderer`] - a generic renderer which shells out to a program to do the
|
2018-01-07 22:10:48 +08:00
|
|
|
/// actual rendering
|
2016-04-27 14:19:59 +02:00
|
|
|
pub trait Renderer {
|
2018-01-07 22:10:48 +08:00
|
|
|
/// 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,
|
2018-10-20 11:21:24 +08:00
|
|
|
#[serde(skip)]
|
2021-03-23 18:36:45 -07:00
|
|
|
pub(crate) chapter_titles: HashMap<PathBuf, String>,
|
|
|
|
|
#[serde(skip)]
|
2018-09-16 14:27:37 +08:00
|
|
|
__non_exhaustive: (),
|
2018-01-07 22:10:48 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl RenderContext {
|
|
|
|
|
/// Create a new `RenderContext`.
|
2018-01-26 01:11:48 +08:00
|
|
|
pub fn new<P, Q>(root: P, book: Book, config: Config, destination: Q) -> RenderContext
|
2018-01-07 22:10:48 +08:00
|
|
|
where
|
|
|
|
|
P: Into<PathBuf>,
|
|
|
|
|
Q: Into<PathBuf>,
|
|
|
|
|
{
|
|
|
|
|
RenderContext {
|
2018-12-04 00:10:09 +01:00
|
|
|
book,
|
|
|
|
|
config,
|
2019-05-26 01:50:41 +07:00
|
|
|
version: crate::MDBOOK_VERSION.to_string(),
|
2018-01-07 22:10:48 +08:00
|
|
|
root: root.into(),
|
|
|
|
|
destination: destination.into(),
|
2021-03-23 18:36:45 -07:00
|
|
|
chapter_titles: HashMap::new(),
|
2018-09-16 14:27:37 +08:00
|
|
|
__non_exhaustive: (),
|
2018-01-07 22:10:48 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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> {
|
2020-05-20 14:32:00 -07:00
|
|
|
serde_json::from_reader(reader).with_context(|| "Unable to deserialize the `RenderContext`")
|
2018-01-07 22:10:48 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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 }
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-30 17:26:59 -08:00
|
|
|
fn compose_command(&self, root: &Path, destination: &Path) -> Result<Command> {
|
2018-01-07 22:10:48 +08:00
|
|
|
let mut words = Shlex::new(&self.cmd);
|
2020-12-30 17:26:59 -08:00
|
|
|
let exe = match words.next() {
|
|
|
|
|
Some(e) => PathBuf::from(e),
|
2018-01-07 22:10:48 +08:00
|
|
|
None => bail!("Command string was empty"),
|
|
|
|
|
};
|
|
|
|
|
|
2020-12-30 17:26:59 -08:00
|
|
|
let exe = if exe.components().count() == 1 {
|
|
|
|
|
// Search PATH for the executable.
|
|
|
|
|
exe
|
|
|
|
|
} else {
|
|
|
|
|
// Relative paths are preferred to be relative to the book root.
|
|
|
|
|
let abs_exe = root.join(&exe);
|
|
|
|
|
if abs_exe.exists() {
|
|
|
|
|
abs_exe
|
|
|
|
|
} else {
|
|
|
|
|
// Historically paths were relative to the destination, but
|
|
|
|
|
// this is not the preferred way.
|
|
|
|
|
let legacy_path = destination.join(&exe);
|
|
|
|
|
if legacy_path.exists() {
|
|
|
|
|
warn!(
|
|
|
|
|
"Renderer command `{}` uses a path relative to the \
|
|
|
|
|
renderer output directory `{}`. This was previously \
|
|
|
|
|
accepted, but has been deprecated. Relative executable \
|
|
|
|
|
paths should be relative to the book root.",
|
|
|
|
|
exe.display(),
|
|
|
|
|
destination.display()
|
|
|
|
|
);
|
|
|
|
|
legacy_path
|
|
|
|
|
} else {
|
|
|
|
|
// Let this bubble through to later be handled by
|
|
|
|
|
// handle_render_command_error.
|
|
|
|
|
abs_exe.to_path_buf()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut cmd = Command::new(exe);
|
2018-01-07 22:10:48 +08:00
|
|
|
|
|
|
|
|
for arg in words {
|
|
|
|
|
cmd.arg(arg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(cmd)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-12-31 11:16:59 +01:00
|
|
|
impl CmdRenderer {
|
|
|
|
|
fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> {
|
2020-05-10 08:29:50 -07:00
|
|
|
if let ErrorKind::NotFound = error.kind() {
|
|
|
|
|
// Look for "output.{self.name}.optional".
|
|
|
|
|
// If it exists and is true, treat this as a warning.
|
|
|
|
|
// Otherwise, fail the build.
|
|
|
|
|
|
|
|
|
|
let optional_key = format!("output.{}.optional", self.name);
|
|
|
|
|
|
|
|
|
|
let is_optional = match ctx.config.get(&optional_key) {
|
|
|
|
|
Some(Value::Boolean(value)) => *value,
|
|
|
|
|
_ => false,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if is_optional {
|
|
|
|
|
warn!(
|
|
|
|
|
"The command `{}` for backend `{}` was not found, \
|
|
|
|
|
but was marked as optional.",
|
|
|
|
|
self.cmd, self.name
|
|
|
|
|
);
|
|
|
|
|
return Ok(());
|
|
|
|
|
} else {
|
|
|
|
|
error!(
|
2020-07-24 23:18:22 -03:00
|
|
|
"The command `{0}` wasn't found, is the \"{1}\" backend installed? \
|
|
|
|
|
If you want to ignore this error when the \"{1}\" backend is not installed, \
|
|
|
|
|
set `optional = true` in the `[output.{1}]` section of the book.toml configuration file.",
|
2020-05-10 08:29:50 -07:00
|
|
|
self.cmd, self.name
|
|
|
|
|
);
|
2019-12-31 11:16:59 +01:00
|
|
|
}
|
|
|
|
|
}
|
2020-05-20 14:32:00 -07:00
|
|
|
Err(error).with_context(|| "Unable to start the backend")?
|
2019-12-31 11:16:59 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
impl Renderer for CmdRenderer {
|
|
|
|
|
fn name(&self) -> &str {
|
|
|
|
|
&self.name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
2018-01-20 21:46:44 +08:00
|
|
|
info!("Invoking the \"{}\" renderer", self.name);
|
2018-01-07 22:10:48 +08:00
|
|
|
|
2018-01-16 07:26:01 +08:00
|
|
|
let _ = fs::create_dir_all(&ctx.destination);
|
|
|
|
|
|
2018-08-02 20:22:49 -05:00
|
|
|
let mut child = match self
|
2020-12-30 17:26:59 -08:00
|
|
|
.compose_command(&ctx.root, &ctx.destination)?
|
2018-01-14 19:14:27 +08:00
|
|
|
.stdin(Stdio::piped())
|
|
|
|
|
.stdout(Stdio::inherit())
|
|
|
|
|
.stderr(Stdio::inherit())
|
2018-01-07 22:10:48 +08:00
|
|
|
.current_dir(&ctx.destination)
|
2018-07-23 12:45:01 -05:00
|
|
|
.spawn()
|
|
|
|
|
{
|
|
|
|
|
Ok(c) => c,
|
2019-12-31 11:16:59 +01:00
|
|
|
Err(e) => return self.handle_render_command_error(ctx, e),
|
2018-07-23 12:45:01 -05:00
|
|
|
};
|
2018-01-07 22:10:48 +08:00
|
|
|
|
2020-05-10 09:23:40 -07:00
|
|
|
let mut stdin = child.stdin.take().expect("Child has stdin");
|
|
|
|
|
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
|
|
|
|
|
// Looks like the backend hung up before we could finish
|
|
|
|
|
// sending it the render context. Log the error and keep going
|
|
|
|
|
warn!("Error writing the RenderContext to the backend, {}", e);
|
2018-01-14 19:14:27 +08:00
|
|
|
}
|
|
|
|
|
|
2020-05-10 09:23:40 -07:00
|
|
|
// explicitly close the `stdin` file handle
|
|
|
|
|
drop(stdin);
|
|
|
|
|
|
2018-01-14 19:14:27 +08:00
|
|
|
let status = child
|
|
|
|
|
.wait()
|
2020-05-20 14:32:00 -07:00
|
|
|
.with_context(|| "Error waiting for the backend to complete")?;
|
2018-01-14 19:14:27 +08:00
|
|
|
|
2018-01-07 22:10:48 +08:00
|
|
|
trace!("{} exited with output: {:?}", self.cmd, status);
|
|
|
|
|
|
|
|
|
|
if !status.success() {
|
|
|
|
|
error!("Renderer exited with non-zero return code.");
|
2018-01-20 21:46:44 +08:00
|
|
|
bail!("The \"{}\" renderer failed", self.name);
|
2018-01-07 22:10:48 +08:00
|
|
|
} else {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-04-27 14:19:59 +02:00
|
|
|
}
|