use anyhow::{Context, Result, ensure};
use mdbook_core::book::Book;
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
use std::io::Write;
use std::path::PathBuf;
use std::process::{Child, Stdio};
use tracing::{debug, trace, warn};
#[derive(Debug, Clone, PartialEq)]
pub struct CmdPreprocessor {
name: String,
cmd: String,
root: PathBuf,
optional: bool,
}
impl CmdPreprocessor {
pub fn new(name: String, cmd: String, root: PathBuf, optional: bool) -> CmdPreprocessor {
CmdPreprocessor {
name,
cmd,
root,
optional,
}
}
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
let stdin = child.stdin.take().expect("Child has stdin");
if let Err(e) = self.write_input(stdin, book, ctx) {
warn!("Error writing the RenderContext to the backend, {}", e);
}
}
fn write_input<W: Write>(
&self,
writer: W,
book: &Book,
ctx: &PreprocessorContext,
) -> Result<()> {
serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into)
}
pub fn cmd(&self) -> &str {
&self.cmd
}
}
impl Preprocessor for CmdPreprocessor {
fn name(&self) -> &str {
&self.name
}
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
let mut child = match cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.current_dir(&self.root)
.spawn()
{
Ok(c) => c,
Err(e) => {
crate::handle_command_error(
e,
self.optional,
"preprocessor",
"preprocessor",
&self.name,
&self.cmd,
)?;
return Ok(book);
}
};
self.write_input_to_child(&mut child, &book, ctx);
let output = child.wait_with_output().with_context(|| {
format!(
"Error waiting for the \"{}\" preprocessor to complete",
self.name
)
})?;
trace!("{} exited with output: {:?}", self.cmd, output);
ensure!(
output.status.success(),
format!(
"The \"{}\" preprocessor exited unsuccessfully with {} status",
self.name, output.status
)
);
serde_json::from_slice(&output.stdout).with_context(|| {
format!(
"Unable to parse the preprocessed book from \"{}\" processor",
self.name
)
})
}
fn supports_renderer(&self, renderer: &str) -> Result<bool> {
debug!(
"Checking if the \"{}\" preprocessor supports \"{}\"",
self.name(),
renderer
);
let mut cmd = crate::compose_command(&self.cmd, &self.root)?;
match cmd
.arg("supports")
.arg(renderer)
.stdin(Stdio::null())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.current_dir(&self.root)
.status()
{
Ok(status) => Ok(status.code() == Some(0)),
Err(e) => {
crate::handle_command_error(
e,
self.optional,
"preprocessor",
"preprocessor",
&self.name,
&self.cmd,
)?;
Ok(false)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MDBook;
use std::path::Path;
fn guide() -> MDBook {
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../guide");
MDBook::load(example).unwrap()
}
#[test]
fn round_trip_write_and_parse_input() {
let md = guide();
let cmd = CmdPreprocessor::new(
"test".to_string(),
"test".to_string(),
md.root.clone(),
false,
);
let ctx = PreprocessorContext::new(
md.root.clone(),
md.config.clone(),
"some-renderer".to_string(),
);
let mut buffer = Vec::new();
cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
let (got_ctx, got_book) = mdbook_preprocessor::parse_input(buffer.as_slice()).unwrap();
assert_eq!(got_book, md.book);
assert_eq!(got_ctx, ctx);
}
}