]>
Commit | Line | Data |
---|---|---|
2c00a5a8 XL |
1 | //! `mdbook`'s low level rendering interface. |
2 | //! | |
3 | //! # Note | |
4 | //! | |
5 | //! You usually don't need to work with this module directly. If you want to | |
6 | //! implement your own backend, then check out the [For Developers] section of | |
7 | //! the user guide. | |
8 | //! | |
9 | //! The definition for [RenderContext] may be useful though. | |
10 | //! | |
e74abb32 | 11 | //! [For Developers]: https://rust-lang.github.io/mdBook/for_developers/index.html |
2c00a5a8 XL |
12 | //! [RenderContext]: struct.RenderContext.html |
13 | ||
ea8adc8c | 14 | pub use self::html_handlebars::HtmlHandlebars; |
e74abb32 | 15 | pub use self::markdown_renderer::MarkdownRenderer; |
ea8adc8c XL |
16 | |
17 | mod html_handlebars; | |
e74abb32 | 18 | mod markdown_renderer; |
ea8adc8c | 19 | |
9fa01778 | 20 | use shlex::Shlex; |
94222f64 | 21 | use std::collections::HashMap; |
2c00a5a8 | 22 | use std::fs; |
f035d41b | 23 | use std::io::{self, ErrorKind, Read}; |
5869c6ff | 24 | use std::path::{Path, PathBuf}; |
2c00a5a8 | 25 | use std::process::{Command, Stdio}; |
2c00a5a8 | 26 | |
dc9dc135 XL |
27 | use crate::book::Book; |
28 | use crate::config::Config; | |
29 | use crate::errors::*; | |
6522a427 | 30 | use log::{error, info, trace, warn}; |
f035d41b | 31 | use toml::Value; |
2c00a5a8 | 32 | |
064997fb FG |
33 | use serde::{Deserialize, Serialize}; |
34 | ||
2c00a5a8 XL |
35 | /// An arbitrary `mdbook` backend. |
36 | /// | |
37 | /// Although it's quite possible for you to import `mdbook` as a library and | |
38 | /// provide your own renderer, there are two main renderer implementations that | |
39 | /// 99% of users will ever use: | |
40 | /// | |
5869c6ff XL |
41 | /// - [`HtmlHandlebars`] - the built-in HTML renderer |
42 | /// - [`CmdRenderer`] - a generic renderer which shells out to a program to do the | |
2c00a5a8 | 43 | /// actual rendering |
ea8adc8c | 44 | pub trait Renderer { |
2c00a5a8 XL |
45 | /// The `Renderer`'s name. |
46 | fn name(&self) -> &str; | |
47 | ||
48 | /// Invoke the `Renderer`, passing in all the necessary information for | |
49 | /// describing a book. | |
50 | fn render(&self, ctx: &RenderContext) -> Result<()>; | |
51 | } | |
52 | ||
53 | /// The context provided to all renderers. | |
54 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] | |
55 | pub struct RenderContext { | |
56 | /// Which version of `mdbook` did this come from (as written in `mdbook`'s | |
57 | /// `Cargo.toml`). Useful if you know the renderer is only compatible with | |
58 | /// certain versions of `mdbook`. | |
59 | pub version: String, | |
60 | /// The book's root directory. | |
61 | pub root: PathBuf, | |
62 | /// A loaded representation of the book itself. | |
63 | pub book: Book, | |
64 | /// The loaded configuration file. | |
65 | pub config: Config, | |
66 | /// Where the renderer *must* put any build artefacts generated. To allow | |
67 | /// renderers to cache intermediate results, this directory is not | |
68 | /// guaranteed to be empty or even exist. | |
69 | pub destination: PathBuf, | |
9fa01778 | 70 | #[serde(skip)] |
94222f64 XL |
71 | pub(crate) chapter_titles: HashMap<PathBuf, String>, |
72 | #[serde(skip)] | |
9fa01778 | 73 | __non_exhaustive: (), |
2c00a5a8 XL |
74 | } |
75 | ||
76 | impl RenderContext { | |
77 | /// Create a new `RenderContext`. | |
78 | pub fn new<P, Q>(root: P, book: Book, config: Config, destination: Q) -> RenderContext | |
79 | where | |
80 | P: Into<PathBuf>, | |
81 | Q: Into<PathBuf>, | |
82 | { | |
83 | RenderContext { | |
9fa01778 XL |
84 | book, |
85 | config, | |
dc9dc135 | 86 | version: crate::MDBOOK_VERSION.to_string(), |
2c00a5a8 XL |
87 | root: root.into(), |
88 | destination: destination.into(), | |
94222f64 | 89 | chapter_titles: HashMap::new(), |
9fa01778 | 90 | __non_exhaustive: (), |
2c00a5a8 XL |
91 | } |
92 | } | |
93 | ||
94 | /// Get the source directory's (absolute) path on disk. | |
95 | pub fn source_dir(&self) -> PathBuf { | |
96 | self.root.join(&self.config.book.src) | |
97 | } | |
98 | ||
99 | /// Load a `RenderContext` from its JSON representation. | |
100 | pub fn from_json<R: Read>(reader: R) -> Result<RenderContext> { | |
f035d41b | 101 | serde_json::from_reader(reader).with_context(|| "Unable to deserialize the `RenderContext`") |
2c00a5a8 XL |
102 | } |
103 | } | |
104 | ||
105 | /// A generic renderer which will shell out to an arbitrary executable. | |
106 | /// | |
107 | /// # Rendering Protocol | |
108 | /// | |
109 | /// When the renderer's `render()` method is invoked, `CmdRenderer` will spawn | |
110 | /// the `cmd` as a subprocess. The `RenderContext` is passed to the subprocess | |
111 | /// as a JSON string (using `serde_json`). | |
112 | /// | |
113 | /// > **Note:** The command used doesn't necessarily need to be a single | |
114 | /// > executable (i.e. `/path/to/renderer`). The `cmd` string lets you pass | |
115 | /// > in command line arguments, so there's no reason why it couldn't be | |
116 | /// > `python /path/to/renderer --from mdbook --to epub`. | |
117 | /// | |
118 | /// Anything the subprocess writes to `stdin` or `stdout` will be passed through | |
119 | /// to the user. While this gives the renderer maximum flexibility to output | |
120 | /// whatever it wants, to avoid spamming users it is recommended to avoid | |
121 | /// unnecessary output. | |
122 | /// | |
123 | /// To help choose the appropriate output level, the `RUST_LOG` environment | |
124 | /// variable will be passed through to the subprocess, if set. | |
125 | /// | |
126 | /// If the subprocess wishes to indicate that rendering failed, it should exit | |
127 | /// with a non-zero return code. | |
128 | #[derive(Debug, Clone, PartialEq)] | |
129 | pub struct CmdRenderer { | |
130 | name: String, | |
131 | cmd: String, | |
132 | } | |
133 | ||
134 | impl CmdRenderer { | |
135 | /// Create a new `CmdRenderer` which will invoke the provided `cmd` string. | |
136 | pub fn new(name: String, cmd: String) -> CmdRenderer { | |
137 | CmdRenderer { name, cmd } | |
138 | } | |
139 | ||
5869c6ff | 140 | fn compose_command(&self, root: &Path, destination: &Path) -> Result<Command> { |
2c00a5a8 | 141 | let mut words = Shlex::new(&self.cmd); |
5869c6ff XL |
142 | let exe = match words.next() { |
143 | Some(e) => PathBuf::from(e), | |
2c00a5a8 XL |
144 | None => bail!("Command string was empty"), |
145 | }; | |
146 | ||
5869c6ff XL |
147 | let exe = if exe.components().count() == 1 { |
148 | // Search PATH for the executable. | |
149 | exe | |
150 | } else { | |
151 | // Relative paths are preferred to be relative to the book root. | |
152 | let abs_exe = root.join(&exe); | |
153 | if abs_exe.exists() { | |
154 | abs_exe | |
155 | } else { | |
156 | // Historically paths were relative to the destination, but | |
157 | // this is not the preferred way. | |
158 | let legacy_path = destination.join(&exe); | |
159 | if legacy_path.exists() { | |
160 | warn!( | |
161 | "Renderer command `{}` uses a path relative to the \ | |
162 | renderer output directory `{}`. This was previously \ | |
163 | accepted, but has been deprecated. Relative executable \ | |
164 | paths should be relative to the book root.", | |
165 | exe.display(), | |
166 | destination.display() | |
167 | ); | |
168 | legacy_path | |
169 | } else { | |
170 | // Let this bubble through to later be handled by | |
171 | // handle_render_command_error. | |
94222f64 | 172 | abs_exe |
5869c6ff XL |
173 | } |
174 | } | |
175 | }; | |
176 | ||
177 | let mut cmd = Command::new(exe); | |
2c00a5a8 XL |
178 | |
179 | for arg in words { | |
180 | cmd.arg(arg); | |
181 | } | |
182 | ||
183 | Ok(cmd) | |
184 | } | |
185 | } | |
186 | ||
f035d41b XL |
187 | impl CmdRenderer { |
188 | fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> { | |
189 | if let ErrorKind::NotFound = error.kind() { | |
190 | // Look for "output.{self.name}.optional". | |
191 | // If it exists and is true, treat this as a warning. | |
192 | // Otherwise, fail the build. | |
193 | ||
194 | let optional_key = format!("output.{}.optional", self.name); | |
195 | ||
196 | let is_optional = match ctx.config.get(&optional_key) { | |
197 | Some(Value::Boolean(value)) => *value, | |
198 | _ => false, | |
199 | }; | |
200 | ||
201 | if is_optional { | |
202 | warn!( | |
203 | "The command `{}` for backend `{}` was not found, \ | |
204 | but was marked as optional.", | |
205 | self.cmd, self.name | |
206 | ); | |
207 | return Ok(()); | |
208 | } else { | |
209 | error!( | |
3dfed10e XL |
210 | "The command `{0}` wasn't found, is the \"{1}\" backend installed? \ |
211 | If you want to ignore this error when the \"{1}\" backend is not installed, \ | |
212 | set `optional = true` in the `[output.{1}]` section of the book.toml configuration file.", | |
f035d41b XL |
213 | self.cmd, self.name |
214 | ); | |
215 | } | |
216 | } | |
217 | Err(error).with_context(|| "Unable to start the backend")? | |
218 | } | |
219 | } | |
220 | ||
2c00a5a8 XL |
221 | impl Renderer for CmdRenderer { |
222 | fn name(&self) -> &str { | |
223 | &self.name | |
224 | } | |
225 | ||
226 | fn render(&self, ctx: &RenderContext) -> Result<()> { | |
227 | info!("Invoking the \"{}\" renderer", self.name); | |
228 | ||
229 | let _ = fs::create_dir_all(&ctx.destination); | |
230 | ||
9fa01778 | 231 | let mut child = match self |
5869c6ff | 232 | .compose_command(&ctx.root, &ctx.destination)? |
2c00a5a8 XL |
233 | .stdin(Stdio::piped()) |
234 | .stdout(Stdio::inherit()) | |
235 | .stderr(Stdio::inherit()) | |
236 | .current_dir(&ctx.destination) | |
9fa01778 XL |
237 | .spawn() |
238 | { | |
239 | Ok(c) => c, | |
f035d41b | 240 | Err(e) => return self.handle_render_command_error(ctx, e), |
9fa01778 | 241 | }; |
2c00a5a8 | 242 | |
f035d41b XL |
243 | let mut stdin = child.stdin.take().expect("Child has stdin"); |
244 | if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) { | |
245 | // Looks like the backend hung up before we could finish | |
246 | // sending it the render context. Log the error and keep going | |
247 | warn!("Error writing the RenderContext to the backend, {}", e); | |
2c00a5a8 XL |
248 | } |
249 | ||
f035d41b XL |
250 | // explicitly close the `stdin` file handle |
251 | drop(stdin); | |
252 | ||
2c00a5a8 XL |
253 | let status = child |
254 | .wait() | |
f035d41b | 255 | .with_context(|| "Error waiting for the backend to complete")?; |
2c00a5a8 XL |
256 | |
257 | trace!("{} exited with output: {:?}", self.cmd, status); | |
258 | ||
259 | if !status.success() { | |
260 | error!("Renderer exited with non-zero return code."); | |
261 | bail!("The \"{}\" renderer failed", self.name); | |
262 | } else { | |
263 | Ok(()) | |
264 | } | |
265 | } | |
ea8adc8c | 266 | } |