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}