1use super::{Preprocessor, PreprocessorContext};
2use crate::book::Book;
3use crate::errors::*;
4use log::{debug, trace, warn};
5use shlex::Shlex;
6use std::io::{self, Read, Write};
7use std::process::{Child, Command, Stdio};
8
9/// A custom preprocessor which will shell out to a 3rd-party program.
10///
11/// # Preprocessing Protocol
12///
13/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will
14/// execute the shell command `$cmd supports $renderer`. If the renderer is
15/// supported, custom preprocessors should exit with a exit code of `0`,
16/// any other exit code be considered as unsupported.
17///
18/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)`
19/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors
20/// should then "return" a processed book by printing it to `stdout` as JSON.
21/// For convenience, the `CmdPreprocessor::parse_input()` function can be used
22/// to parse the input provided by `mdbook`.
23///
24/// Exiting with a non-zero exit code while preprocessing is considered an
25/// error. `stderr` is passed directly through to the user, so it can be used
26/// for logging or emitting warnings if desired.
27///
28/// # Examples
29///
30/// An example preprocessor is available in this project's `examples/`
31/// directory.
32#[derive(Debug, Clone, PartialEq)]
33pub struct CmdPreprocessor {
34 name: String,
35 cmd: String,
36}
37
38impl CmdPreprocessor {
39 /// Create a new `CmdPreprocessor`.
40 pub fn new(name: String, cmd: String) -> CmdPreprocessor {
41 CmdPreprocessor { name, cmd }
42 }
43
44 /// A convenience function custom preprocessors can use to parse the input
45 /// written to `stdin` by a `CmdRenderer`.
46 pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
47 serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
48 }
49
50 fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
51 let stdin = child.stdin.take().expect("Child has stdin");
52
53 if let Err(e) = self.write_input(stdin, book, ctx) {
54 // Looks like the backend hung up before we could finish
55 // sending it the render context. Log the error and keep going
56 warn!("Error writing the RenderContext to the backend, {}", e);
57 }
58 }
59
60 fn write_input<W: Write>(
61 &self,
62 writer: W,
63 book: &Book,
64 ctx: &PreprocessorContext,
65 ) -> Result<()> {
66 serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into)
67 }
68
69 /// The command this `Preprocessor` will invoke.
70 pub fn cmd(&self) -> &str {
71 &self.cmd
72 }
73
74 fn command(&self) -> Result<Command> {
75 let mut words = Shlex::new(&self.cmd);
76 let executable = match words.next() {
77 Some(e) => e,
78 None => bail!("Command string was empty"),
79 };
80
81 let mut cmd = Command::new(executable);
82
83 for arg in words {
84 cmd.arg(arg);
85 }
86
87 Ok(cmd)
88 }
89}
90
91impl Preprocessor for CmdPreprocessor {
92 fn name(&self) -> &str {
93 &self.name
94 }
95
96 fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
97 let mut cmd = self.command()?;
98
99 let mut child = cmd
100 .stdin(Stdio::piped())
101 .stdout(Stdio::piped())
102 .stderr(Stdio::inherit())
103 .spawn()
104 .with_context(|| {
105 format!(
106 "Unable to start the \"{}\" preprocessor. Is it installed?",
107 self.name()
108 )
109 })?;
110
111 self.write_input_to_child(&mut child, &book, ctx);
112
113 let output = child.wait_with_output().with_context(|| {
114 format!(
115 "Error waiting for the \"{}\" preprocessor to complete",
116 self.name
117 )
118 })?;
119
120 trace!("{} exited with output: {:?}", self.cmd, output);
121 ensure!(
122 output.status.success(),
123 format!(
124 "The \"{}\" preprocessor exited unsuccessfully with {} status",
125 self.name, output.status
126 )
127 );
128
129 serde_json::from_slice(&output.stdout).with_context(|| {
130 format!(
131 "Unable to parse the preprocessed book from \"{}\" processor",
132 self.name
133 )
134 })
135 }
136
137 fn supports_renderer(&self, renderer: &str) -> bool {
138 debug!(
139 "Checking if the \"{}\" preprocessor supports \"{}\"",
140 self.name(),
141 renderer
142 );
143
144 let mut cmd = match self.command() {
145 Ok(c) => c,
146 Err(e) => {
147 warn!(
148 "Unable to create the command for the \"{}\" preprocessor, {}",
149 self.name(),
150 e
151 );
152 return false;
153 }
154 };
155
156 let outcome = cmd
157 .arg("supports")
158 .arg(renderer)
159 .stdin(Stdio::null())
160 .stdout(Stdio::inherit())
161 .stderr(Stdio::inherit())
162 .status()
163 .map(|status| status.code() == Some(0));
164
165 if let Err(ref e) = outcome {
166 if e.kind() == io::ErrorKind::NotFound {
167 warn!(
168 "The command wasn't found, is the \"{}\" preprocessor installed?",
169 self.name
170 );
171 warn!("\tCommand: {}", self.cmd);
172 }
173 }
174
175 outcome.unwrap_or(false)
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::MDBook;
183 use std::path::Path;
184
185 fn guide() -> MDBook {
186 let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("guide");
187 MDBook::load(example).unwrap()
188 }
189
190 #[test]
191 fn round_trip_write_and_parse_input() {
192 let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string());
193 let md = guide();
194 let ctx = PreprocessorContext::new(
195 md.root.clone(),
196 md.config.clone(),
197 "some-renderer".to_string(),
198 );
199
200 let mut buffer = Vec::new();
201 cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
202
203 let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
204
205 assert_eq!(got_book, md.book);
206 assert_eq!(got_ctx, ctx);
207 }
208}
209