mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-27 09:05:40 -05:00
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.
This commit is contained in:
@@ -2,7 +2,7 @@ use anyhow::{Context, Result, ensure};
|
||||
use log::{debug, trace, warn};
|
||||
use mdbook_core::book::Book;
|
||||
use mdbook_preprocessor::{Preprocessor, PreprocessorContext};
|
||||
use std::io::{self, Write};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Stdio};
|
||||
|
||||
@@ -34,12 +34,18 @@ pub struct CmdPreprocessor {
|
||||
name: String,
|
||||
cmd: String,
|
||||
root: PathBuf,
|
||||
optional: bool,
|
||||
}
|
||||
|
||||
impl CmdPreprocessor {
|
||||
/// Create a new `CmdPreprocessor`.
|
||||
pub fn new(name: String, cmd: String, root: PathBuf) -> CmdPreprocessor {
|
||||
CmdPreprocessor { name, cmd, root }
|
||||
pub fn new(name: String, cmd: String, root: PathBuf, optional: bool) -> CmdPreprocessor {
|
||||
CmdPreprocessor {
|
||||
name,
|
||||
cmd,
|
||||
root,
|
||||
optional,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
|
||||
@@ -75,18 +81,29 @@ impl Preprocessor for CmdPreprocessor {
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
|
||||
let mut cmd = crate::compose_command(&self.cmd, &ctx.root)?;
|
||||
|
||||
let mut child = cmd
|
||||
let mut child = match cmd
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.current_dir(&self.root)
|
||||
.spawn()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Unable to start the \"{}\" preprocessor. Is it installed?",
|
||||
self.name()
|
||||
)
|
||||
})?;
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
crate::handle_command_error(
|
||||
e,
|
||||
self.optional,
|
||||
"preprocessor",
|
||||
"preprocessor",
|
||||
&self.name,
|
||||
&self.cmd,
|
||||
)?;
|
||||
// This should normally not be reached, since the validation
|
||||
// for NotFound should have already happened when running the
|
||||
// "supports" command.
|
||||
return Ok(book);
|
||||
}
|
||||
};
|
||||
|
||||
self.write_input_to_child(&mut child, &book, ctx);
|
||||
|
||||
@@ -123,7 +140,7 @@ impl Preprocessor for CmdPreprocessor {
|
||||
|
||||
let mut cmd = crate::compose_command(&self.cmd, &self.root)?;
|
||||
|
||||
let outcome = cmd
|
||||
match cmd
|
||||
.arg("supports")
|
||||
.arg(renderer)
|
||||
.stdin(Stdio::null())
|
||||
@@ -131,19 +148,20 @@ impl Preprocessor for CmdPreprocessor {
|
||||
.stderr(Stdio::inherit())
|
||||
.current_dir(&self.root)
|
||||
.status()
|
||||
.map(|status| status.code() == Some(0));
|
||||
|
||||
if let Err(ref e) = outcome {
|
||||
if e.kind() == io::ErrorKind::NotFound {
|
||||
warn!(
|
||||
"The command wasn't found, is the \"{}\" preprocessor installed?",
|
||||
self.name
|
||||
);
|
||||
warn!("\tCommand: {}", self.cmd);
|
||||
{
|
||||
Ok(status) => Ok(status.code() == Some(0)),
|
||||
Err(e) => {
|
||||
crate::handle_command_error(
|
||||
e,
|
||||
self.optional,
|
||||
"preprocessor",
|
||||
"preprocessor",
|
||||
&self.name,
|
||||
&self.cmd,
|
||||
)?;
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(outcome.unwrap_or(false))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +179,12 @@ mod tests {
|
||||
#[test]
|
||||
fn round_trip_write_and_parse_input() {
|
||||
let md = guide();
|
||||
let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string(), md.root.clone());
|
||||
let cmd = CmdPreprocessor::new(
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
md.root.clone(),
|
||||
false,
|
||||
);
|
||||
let ctx = PreprocessorContext::new(
|
||||
md.root.clone(),
|
||||
md.config.clone(),
|
||||
|
||||
@@ -437,6 +437,8 @@ struct PreprocessorConfig {
|
||||
before: Vec<String>,
|
||||
#[serde(default)]
|
||||
after: Vec<String>,
|
||||
#[serde(default)]
|
||||
optional: bool,
|
||||
}
|
||||
|
||||
/// 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
|
||||
.command
|
||||
.to_owned()
|
||||
.unwrap_or_else(|| format!("mdbook-{name}"));
|
||||
Box::new(CmdPreprocessor::new(name, command, root.to_owned()))
|
||||
Box::new(CmdPreprocessor::new(
|
||||
name,
|
||||
command,
|
||||
root.to_owned(),
|
||||
table.optional,
|
||||
))
|
||||
}
|
||||
};
|
||||
preprocessors.push(preprocessor);
|
||||
|
||||
@@ -64,6 +64,18 @@ be overridden by adding a `command` field.
|
||||
command = "python random.py"
|
||||
```
|
||||
|
||||
### Optional preprocessors
|
||||
|
||||
If you enable a preprocessor that isn't installed, the default behavior is to throw an error.
|
||||
This behavior can be changed by marking the preprocessor as optional:
|
||||
|
||||
```toml
|
||||
[preprocessor.example]
|
||||
optional = true
|
||||
```
|
||||
|
||||
This demotes the error to a warning.
|
||||
|
||||
## Require A Certain Order
|
||||
|
||||
The order in which preprocessors are run can be controlled with the `before` and `after` fields.
|
||||
|
||||
@@ -76,6 +76,7 @@ fn example() -> CmdPreprocessor {
|
||||
"nop-preprocessor".to_string(),
|
||||
"cargo run --quiet --example nop-preprocessor --".to_string(),
|
||||
std::env::current_dir().unwrap(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -154,11 +155,25 @@ fn relative_command_path() {
|
||||
#[test]
|
||||
fn missing_preprocessor() {
|
||||
BookTest::from_dir("preprocessor/missing_preprocessor").run("build", |cmd| {
|
||||
cmd.expect_stdout(str![[""]])
|
||||
cmd.expect_failure()
|
||||
.expect_stdout(str![[""]])
|
||||
.expect_stderr(str![[r#"
|
||||
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
|
||||
[TIMESTAMP] [WARN] (mdbook_driver::builtin_preprocessors::cmd): The command wasn't found, is the "missing" preprocessor installed?
|
||||
[TIMESTAMP] [WARN] (mdbook_driver::builtin_preprocessors::cmd): [TAB]Command: trduyvbhijnorgevfuhn
|
||||
[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.
|
||||
[TIMESTAMP] [ERROR] (mdbook_core::utils): Error: Unable to run the preprocessor `missing`
|
||||
[TIMESTAMP] [ERROR] (mdbook_core::utils): [TAB]Caused By: [NOT_FOUND]
|
||||
|
||||
"#]]);
|
||||
});
|
||||
}
|
||||
|
||||
// Optional missing is not an error.
|
||||
#[test]
|
||||
fn missing_optional_not_fatal() {
|
||||
BookTest::from_dir("preprocessor/missing_optional_not_fatal").run("build", |cmd| {
|
||||
cmd.expect_stdout(str![[""]]).expect_stderr(str![[r#"
|
||||
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Book building has started
|
||||
[TIMESTAMP] [WARN] (mdbook_driver): The command `trduyvbhijnorgevfuhn` for preprocessor `missing` was not found, but is marked as optional.
|
||||
[TIMESTAMP] [INFO] (mdbook_driver::mdbook): Running the html backend
|
||||
[TIMESTAMP] [INFO] (mdbook_html::html_handlebars::hbs_renderer): HTML book written to `[ROOT]/book`
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[preprocessor.missing]
|
||||
command = "trduyvbhijnorgevfuhn"
|
||||
optional = true
|
||||
Reference in New Issue
Block a user