fifthtry_mdbook/preprocess/
cmd.rs1use 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#[derive(Debug, Clone, PartialEq)]
32pub struct CmdPreprocessor {
33 name: String,
34 cmd: String,
35}
36
37impl CmdPreprocessor {
38 pub fn new(name: String, cmd: String) -> CmdPreprocessor {
40 CmdPreprocessor { name, cmd }
41 }
42
43 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 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 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}