mdbook_driver/builtin_preprocessors/
cmd.rs1use 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#[derive(Debug, Clone, PartialEq)]
14pub struct CmdPreprocessor {
15 name: String,
16 cmd: String,
17 root: PathBuf,
18 optional: bool,
19}
20
21impl CmdPreprocessor {
22 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 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 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 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}