mdbook_driver/builtin_preprocessors/
cmd.rs

1use anyhow::{Context, Result, ensure};
2use mdbook_core::book::Book;
3use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
4use std::io::Write;
5use std::path::PathBuf;
6use std::process::{Child, Stdio};
7use tracing::{debug, trace, warn};
8
9/// A custom preprocessor which will shell out to a 3rd-party program.
10///
11/// See <https://rust-lang.github.io/mdBook/for_developers/preprocessors.html>
12/// for a description of the preprocessor protocol.
13#[derive(Debug, Clone, PartialEq)]
14pub struct CmdPreprocessor {
15    name: String,
16    cmd: String,
17    root: PathBuf,
18    optional: bool,
19}
20
21impl CmdPreprocessor {
22    /// Create a new `CmdPreprocessor`.
23    pub fn new(name: String, cmd: String, root: PathBuf, optional: bool) -> CmdPreprocessor {
24        CmdPreprocessor {
25            name,
26            cmd,
27            root,
28            optional,
29        }
30    }
31
32    fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
33        let stdin = child.stdin.take().expect("Child has stdin");
34
35        if let Err(e) = self.write_input(stdin, book, ctx) {
36            // Looks like the backend hung up before we could finish
37            // sending it the render context. Log the error and keep going
38            warn!("Error writing the RenderContext to the backend, {}", e);
39        }
40    }
41
42    fn write_input<W: Write>(
43        &self,
44        writer: W,
45        book: &Book,
46        ctx: &PreprocessorContext,
47    ) -> Result<()> {
48        serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into)
49    }
50
51    /// The command this `Preprocessor` will invoke.
52    pub fn cmd(&self) -> &str {
53        &self.cmd
54    }
55}
56
57impl Preprocessor for CmdPreprocessor {
58    fn name(&self) -> &str {
59        &self.name
60    }
61
62    fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
63        let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
64
65        let mut child = match cmd
66            .stdin(Stdio::piped())
67            .stdout(Stdio::piped())
68            .stderr(Stdio::inherit())
69            .current_dir(&self.root)
70            .spawn()
71        {
72            Ok(c) => c,
73            Err(e) => {
74                crate::handle_command_error(
75                    e,
76                    self.optional,
77                    "preprocessor",
78                    "preprocessor",
79                    &self.name,
80                    &self.cmd,
81                )?;
82                // This should normally not be reached, since the validation
83                // for NotFound should have already happened when running the
84                // "supports" command.
85                return Ok(book);
86            }
87        };
88
89        self.write_input_to_child(&mut child, &book, ctx);
90
91        let output = child.wait_with_output().with_context(|| {
92            format!(
93                "Error waiting for the \"{}\" preprocessor to complete",
94                self.name
95            )
96        })?;
97
98        trace!("{} exited with output: {:?}", self.cmd, output);
99        ensure!(
100            output.status.success(),
101            format!(
102                "The \"{}\" preprocessor exited unsuccessfully with {} status",
103                self.name, output.status
104            )
105        );
106
107        serde_json::from_slice(&output.stdout).with_context(|| {
108            format!(
109                "Unable to parse the preprocessed book from \"{}\" processor",
110                self.name
111            )
112        })
113    }
114
115    fn supports_renderer(&self, renderer: &str) -> Result<bool> {
116        debug!(
117            "Checking if the \"{}\" preprocessor supports \"{}\"",
118            self.name(),
119            renderer
120        );
121
122        let mut cmd = crate::compose_command(&self.cmd, &self.root)?;
123
124        match cmd
125            .arg("supports")
126            .arg(renderer)
127            .stdin(Stdio::null())
128            .stdout(Stdio::inherit())
129            .stderr(Stdio::inherit())
130            .current_dir(&self.root)
131            .status()
132        {
133            Ok(status) => Ok(status.code() == Some(0)),
134            Err(e) => {
135                crate::handle_command_error(
136                    e,
137                    self.optional,
138                    "preprocessor",
139                    "preprocessor",
140                    &self.name,
141                    &self.cmd,
142                )?;
143                Ok(false)
144            }
145        }
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use crate::MDBook;
153    use std::path::Path;
154
155    fn guide() -> MDBook {
156        let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../guide");
157        MDBook::load(example).unwrap()
158    }
159
160    #[test]
161    fn round_trip_write_and_parse_input() {
162        let md = guide();
163        let cmd = CmdPreprocessor::new(
164            "test".to_string(),
165            "test".to_string(),
166            md.root.clone(),
167            false,
168        );
169        let ctx = PreprocessorContext::new(
170            md.root.clone(),
171            md.config.clone(),
172            "some-renderer".to_string(),
173        );
174
175        let mut buffer = Vec::new();
176        cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
177
178        let (got_ctx, got_book) = mdbook_preprocessor::parse_input(buffer.as_slice()).unwrap();
179
180        assert_eq!(got_book, md.book);
181        assert_eq!(got_ctx, ctx);
182    }
183}