diff options
Diffstat (limited to 'vendor/mdbook/src/renderer/mod.rs')
-rw-r--r-- | vendor/mdbook/src/renderer/mod.rs | 265 |
1 files changed, 265 insertions, 0 deletions
diff --git a/vendor/mdbook/src/renderer/mod.rs b/vendor/mdbook/src/renderer/mod.rs new file mode 100644 index 000000000..15465fbce --- /dev/null +++ b/vendor/mdbook/src/renderer/mod.rs @@ -0,0 +1,265 @@ +//! `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.github.io/mdBook/for_developers/index.html +//! [RenderContext]: struct.RenderContext.html + +pub use self::html_handlebars::HtmlHandlebars; +pub use self::markdown_renderer::MarkdownRenderer; + +mod html_handlebars; +mod markdown_renderer; + +use shlex::Shlex; +use std::collections::HashMap; +use std::fs; +use std::io::{self, ErrorKind, Read}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +use crate::book::Book; +use crate::config::Config; +use crate::errors::*; +use toml::Value; + +use serde::{Deserialize, Serialize}; + +/// 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 +pub trait Renderer { + /// 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, + #[serde(skip)] + pub(crate) chapter_titles: HashMap<PathBuf, String>, + #[serde(skip)] + __non_exhaustive: (), +} + +impl RenderContext { + /// Create a new `RenderContext`. + pub fn new<P, Q>(root: P, book: Book, config: Config, destination: Q) -> RenderContext + where + P: Into<PathBuf>, + Q: Into<PathBuf>, + { + RenderContext { + book, + config, + version: crate::MDBOOK_VERSION.to_string(), + root: root.into(), + destination: destination.into(), + chapter_titles: HashMap::new(), + __non_exhaustive: (), + } + } + + /// 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).with_context(|| "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, root: &Path, destination: &Path) -> Result<Command> { + let mut words = Shlex::new(&self.cmd); + let exe = match words.next() { + Some(e) => PathBuf::from(e), + None => bail!("Command string was empty"), + }; + + 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 + } + } + }; + + let mut cmd = Command::new(exe); + + for arg in words { + cmd.arg(arg); + } + + Ok(cmd) + } +} + +impl CmdRenderer { + fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> { + 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!( + "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.", + self.cmd, self.name + ); + } + } + Err(error).with_context(|| "Unable to start the backend")? + } +} + +impl Renderer for CmdRenderer { + fn name(&self) -> &str { + &self.name + } + + fn render(&self, ctx: &RenderContext) -> Result<()> { + info!("Invoking the \"{}\" renderer", self.name); + + let _ = fs::create_dir_all(&ctx.destination); + + let mut child = match self + .compose_command(&ctx.root, &ctx.destination)? + .stdin(Stdio::piped()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .current_dir(&ctx.destination) + .spawn() + { + Ok(c) => c, + Err(e) => return self.handle_render_command_error(ctx, e), + }; + + 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); + } + + // explicitly close the `stdin` file handle + drop(stdin); + + let status = child + .wait() + .with_context(|| "Error waiting for the backend to complete")?; + + trace!("{} exited with output: {:?}", self.cmd, status); + + if !status.success() { + error!("Renderer exited with non-zero return code."); + bail!("The \"{}\" renderer failed", self.name); + } else { + Ok(()) + } + } +} |