Skip to main content

fifthtry_mdbook/preprocess/
cmd.rs

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