mdbook_preprocessor_boilerplate/
lib.rs

1//! Boilerplate code for [mdbook](https://rust-lang.github.io/mdBook/index.html) preprocessors.
2//!
3//! Handles the CLI, checks whether the renderer is supported, checks the mdbook version, and runs
4//! your preprocessor. All you need to do is implement the [mdbook_preprocessor::Preprocessor] trait.
5//!
6//! # Example
7//!
8//! The following is functionally identical to the [No-Op Preprocessor Example](https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs)
9//! given by mdbook.
10//!
11//! ```no_run
12//! use mdbook_preprocessor::{book::Book, Preprocessor, PreprocessorContext};
13//! use anyhow::{bail, Result};
14//!
15//! fn main() -> Result<()> {
16//!     mdbook_preprocessor_boilerplate::run(
17//!         NoOpPreprocessor,
18//!         "An mdbook preprocessor that does nothing" // CLI description
19//!     )
20//! }
21//!
22//! struct NoOpPreprocessor;
23//!
24//! impl Preprocessor for NoOpPreprocessor {
25//!     fn name(&self) -> &str {
26//!         "nop-preprocessor"
27//!     }
28//!
29//!     fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
30//!         // In testing we want to tell the preprocessor to blow up by setting a
31//!         // particular config value
32//!         if let Ok(Some(true)) = ctx.config.get(&format!("{}.blow-up", self.name())) {
33//!             anyhow::bail!("Boom!!1!");
34//!         }
35//!
36//!         // we *are* a no-op preprocessor after all
37//!         Ok(book)
38//!     }
39//!
40//!     fn supports_renderer(&self, renderer: &str) -> Result<bool> {
41//!         Ok(renderer != "not-supported")
42//!     }
43//! }
44//! ```
45
46use clap::{Arg, ArgMatches, Command};
47use mdbook_preprocessor::{Preprocessor, errors::Result, parse_input};
48use semver::{Version, VersionReq};
49use std::{io, process};
50
51/// Checks renderer support and runs the preprocessor.
52pub fn run(preprocessor: impl Preprocessor, description: &'static str) -> Result<()> {
53    let name = preprocessor.name().to_string();
54    let args = Command::new(name)
55        .about(description)
56        .subcommand(
57            Command::new("supports")
58                .arg(Arg::new("renderer").required(true))
59                .about("Check whether a renderer is supported by this preprocessor"),
60        )
61        .get_matches();
62
63    if let Some(supports) = args.subcommand_matches("supports") {
64        handle_supports(preprocessor, supports);
65    } else {
66        handle_preprocessing(preprocessor)
67    }
68}
69
70fn handle_preprocessing(pre: impl Preprocessor) -> Result<()> {
71    let (ctx, book) = parse_input(io::stdin())?;
72
73    let book_version = Version::parse(&ctx.mdbook_version)?;
74    let version_req = VersionReq::parse(mdbook_preprocessor::MDBOOK_VERSION)?;
75
76    if !version_req.matches(&book_version) {
77        eprintln!(
78            "Warning: The {} plugin was built against version {} of mdbook, \
79             but we're being called from version {}",
80            pre.name(),
81            mdbook_preprocessor::MDBOOK_VERSION,
82            ctx.mdbook_version
83        );
84    }
85
86    let processed_book = pre.run(&ctx, book)?;
87    let out = serde_json::to_string(&processed_book)?;
88    println!("{}", out);
89
90    Ok(())
91}
92
93fn handle_supports(pre: impl Preprocessor, sub_args: &ArgMatches) -> ! {
94    let renderer = sub_args
95        .get_one::<String>("renderer")
96        .expect("Required argument");
97    let supported = pre.supports_renderer(renderer);
98
99    // Signal whether the renderer is supported by exiting with 1 or 0.
100    if matches!(supported, Ok(true)) {
101        process::exit(0);
102    } else {
103        process::exit(1);
104    }
105}