Skip to content

Commit d7892f5

Browse files
committed
Add optional field for preprocessors
This adds the `optional` field to the preprocessor configuration to mirror the same option for the `output` table. Missing preprocessors are now an error unless the `optional` field is set. This should help with inadvertently building a book when a missing preprocessor that you expect to be installed.
1 parent 0a29ba6 commit d7892f5

File tree

6 files changed

+87
-27
lines changed

6 files changed

+87
-27
lines changed

crates/mdbook-driver/src/builtin_preprocessors/cmd.rs

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use anyhow::{Context, Result, ensure};
22
use log::{debug, trace, warn};
33
use mdbook_core::book::Book;
44
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
5-
use std::io::{self, Write};
5+
use std::io::Write;
66
use std::path::PathBuf;
77
use std::process::{Child, Stdio};
88

@@ -34,12 +34,18 @@ pub struct CmdPreprocessor {
3434
name: String,
3535
cmd: String,
3636
root: PathBuf,
37+
optional: bool,
3738
}
3839

3940
impl CmdPreprocessor {
4041
/// Create a new `CmdPreprocessor`.
41-
pub fn new(name: String, cmd: String, root: PathBuf) -> CmdPreprocessor {
42-
CmdPreprocessor { name, cmd, root }
42+
pub fn new(name: String, cmd: String, root: PathBuf, optional: bool) -> CmdPreprocessor {
43+
CmdPreprocessor {
44+
name,
45+
cmd,
46+
root,
47+
optional,
48+
}
4349
}
4450

4551
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
@@ -75,18 +81,29 @@ impl Preprocessor for CmdPreprocessor {
7581
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
7682
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
7783

78-
let mut child = cmd
84+
let mut child = match cmd
7985
.stdin(Stdio::piped())
8086
.stdout(Stdio::piped())
8187
.stderr(Stdio::inherit())
8288
.current_dir(&self.root)
8389
.spawn()
84-
.with_context(|| {
85-
format!(
86-
"Unable to start the \"{}\" preprocessor. Is it installed?",
87-
self.name()
88-
)
89-
})?;
90+
{
91+
Ok(c) => c,
92+
Err(e) => {
93+
crate::handle_command_error(
94+
e,
95+
self.optional,
96+
"preprocessor",
97+
"preprocessor",
98+
&self.name,
99+
&self.cmd,
100+
)?;
101+
// This should normally not be reached, since the validation
102+
// for NotFound should have already happened when running the
103+
// "supports" command.
104+
return Ok(book);
105+
}
106+
};
90107

91108
self.write_input_to_child(&mut child, &book, ctx);
92109

@@ -123,27 +140,28 @@ impl Preprocessor for CmdPreprocessor {
123140

124141
let mut cmd = crate::compose_command(&self.cmd, &self.root)?;
125142

126-
let outcome = cmd
143+
match cmd
127144
.arg("supports")
128145
.arg(renderer)
129146
.stdin(Stdio::null())
130147
.stdout(Stdio::inherit())
131148
.stderr(Stdio::inherit())
132149
.current_dir(&self.root)
133150
.status()
134-
.map(|status| status.code() == Some(0));
135-
136-
if let Err(ref e) = outcome {
137-
if e.kind() == io::ErrorKind::NotFound {
138-
warn!(
139-
"The command wasn't found, is the \"{}\" preprocessor installed?",
140-
self.name
141-
);
142-
warn!("\tCommand: {}", self.cmd);
151+
{
152+
Ok(status) => Ok(status.code() == Some(0)),
153+
Err(e) => {
154+
crate::handle_command_error(
155+
e,
156+
self.optional,
157+
"preprocessor",
158+
"preprocessor",
159+
&self.name,
160+
&self.cmd,
161+
)?;
162+
Ok(false)
143163
}
144164
}
145-
146-
Ok(outcome.unwrap_or(false))
147165
}
148166
}
149167

@@ -161,7 +179,12 @@ mod tests {
161179
#[test]
162180
fn round_trip_write_and_parse_input() {
163181
let md = guide();
164-
let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string(), md.root.clone());
182+
let cmd = CmdPreprocessor::new(
183+
"test".to_string(),
184+
"test".to_string(),
185+
md.root.clone(),
186+
false,
187+
);
165188
let ctx = PreprocessorContext::new(
166189
md.root.clone(),
167190
md.config.clone(),

crates/mdbook-driver/src/mdbook.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,8 @@ struct PreprocessorConfig {
437437
before: Vec<String>,
438438
#[serde(default)]
439439
after: Vec<String>,
440+
#[serde(default)]
441+
optional: bool,
440442
}
441443

442444
/// Look at the `MDBook` and try to figure out what preprocessors to run.
@@ -513,7 +515,12 @@ fn determine_preprocessors(config: &Config, root: &Path) -> Result<Vec<Box<dyn P
513515
.command
514516
.to_owned()
515517
.unwrap_or_else(|| format!("mdbook-{name}"));
516-
Box::new(CmdPreprocessor::new(name, command, root.to_owned()))
518+
Box::new(CmdPreprocessor::new(
519+
name,
520+
command,
521+
root.to_owned(),
522+
table.optional,
523+
))
517524
}
518525
};
519526
preprocessors.push(preprocessor);

guide/src/format/configuration/preprocessors.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,18 @@ be overridden by adding a `command` field.
6464
command = "python random.py"
6565
```
6666

67+
### Optional preprocessors
68+
69+
If you enable a preprocessor that isn't installed, the default behavior is to throw an error.
70+
This behavior can be changed by marking the preprocessor as optional:
71+
72+
```toml
73+
[preprocessor.example]
74+
optional = true
75+
```
76+
77+
This demotes the error to a warning.
78+
6779
## Require A Certain Order
6880

6981
The order in which preprocessors are run can be controlled with the `before` and `after` fields.

tests/testsuite/preprocessor.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ fn example() -> CmdPreprocessor {
7676
"nop-preprocessor".to_string(),
7777
"cargo run --quiet --example nop-preprocessor --".to_string(),
7878
std::env::current_dir().unwrap(),
79+
false,
7980
)
8081
}
8182

@@ -154,11 +155,25 @@ fn relative_command_path() {
154155
#[test]
155156
fn missing_preprocessor() {
156157
BookTest::from_dir("preprocessor/missing_preprocessor").run("build", |cmd| {
157-
cmd.expect_stdout(str![[""]])
158+
cmd.expect_failure()
159+
.expect_stdout(str![[""]])
158160
.expect_stderr(str![[r#"
159161
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
160-
[TIMESTAMP] [WARN] (mdbook_driver::builtin_preprocessors::cmd): The command wasn't found, is the "missing" preprocessor installed?
161-
[TIMESTAMP] [WARN] (mdbook_driver::builtin_preprocessors::cmd): [TAB]Command: trduyvbhijnorgevfuhn
162+
[TIMESTAMP] [ERROR] (mdbook_driver): The command `trduyvbhijnorgevfuhn` wasn't found, is the `missing` preprocessor installed? If you want to ignore this error when the `missing` preprocessor is not installed, set `optional = true` in the `[preprocessor.missing]` section of the book.toml configuration file.
163+
[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Unable to run the preprocessor `missing`
164+
[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: [NOT_FOUND]
165+
166+
"#]]);
167+
});
168+
}
169+
170+
// Optional missing is not an error.
171+
#[test]
172+
fn missing_optional_not_fatal() {
173+
BookTest::from_dir("preprocessor/missing_optional_not_fatal").run("build", |cmd| {
174+
cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#"
175+
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
176+
[TIMESTAMP] [WARN] (mdbook_driver): The command `trduyvbhijnorgevfuhn` for preprocessor `missing` was not found, but is marked as optional.
162177
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend
163178
[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book`
164179
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[preprocessor.missing]
2+
command = "trduyvbhijnorgevfuhn"
3+
optional = true

tests/testsuite/preprocessor/missing_optional_not_fatal/src/SUMMARY.md

Whitespace-only changes.

0 commit comments

Comments
 (0)