diff --git a/examples/remove-emphasis/test.rs b/examples/remove-emphasis/test.rs index 1741712b02..adfe5eefe6 100644 --- a/examples/remove-emphasis/test.rs +++ b/examples/remove-emphasis/test.rs @@ -1,11 +1,12 @@ #[test] fn remove_emphasis_works() { // Tests that the remove-emphasis example works as expected. + use mdbook::book::ActiveBackends; // Workaround for https://github.com/rust-lang/mdBook/issues/1424 std::env::set_current_dir("examples/remove-emphasis").unwrap(); let book = mdbook::MDBook::load(".").unwrap(); - book.build().unwrap(); + book.render(&ActiveBackends::AllAvailable).unwrap(); let ch1 = std::fs::read_to_string("book/chapter_1.html").unwrap(); assert!(ch1.contains("This has light emphasis and bold emphasis.")); } diff --git a/guide/src/cli/build.md b/guide/src/cli/build.md index 36e053fd7c..318cd2699a 100644 --- a/guide/src/cli/build.md +++ b/guide/src/cli/build.md @@ -34,6 +34,14 @@ book. Relative paths are interpreted relative to the book's root directory. If not specified it will default to the value of the `build.build-dir` key in `book.toml`, or to `./book`. +#### `--backend` + +By default, all backends configured in the `book.toml` config file will be executed. +If this flag is given, only the specified backend will be run. This flag +may be given multiple times to run multiple backends. Providing a name of +a backend that is not configured results in an error. For more information +about backends, see [here](./format/configuration/renderers.md). + ------------------- ***Note:*** *The build command copies all files (excluding files with `.md` extension) from the source directory diff --git a/guide/src/cli/serve.md b/guide/src/cli/serve.md index 4603df8e76..f2c8d2438b 100644 --- a/guide/src/cli/serve.md +++ b/guide/src/cli/serve.md @@ -56,3 +56,11 @@ ignoring temporary files created by some editors. ***Note:*** *Only the `.gitignore` from the book root directory is used. Global `$HOME/.gitignore` or `.gitignore` files in parent directories are not used.* + +#### `--backend` + +By default, all backends configured in the `book.toml` config file will be executed. +If this flag is given, only the specified backend will be run. This flag +may be given multiple times to run multiple backends. Providing a name of +a backend that is not configured results in an error. For more information +about backends, see [here](./format/configuration/renderers.md). diff --git a/guide/src/cli/watch.md b/guide/src/cli/watch.md index be2f5be450..6d0cd6d5b2 100644 --- a/guide/src/cli/watch.md +++ b/guide/src/cli/watch.md @@ -39,3 +39,11 @@ ignoring temporary files created by some editors. _Note: Only `.gitignore` from book root directory is used. Global `$HOME/.gitignore` or `.gitignore` files in parent directories are not used._ + +#### `--backend` + +By default, all backends configured in the `book.toml` config file will be executed. +If this flag is given, only the specified backend will be run. This flag +may be given multiple times to run multiple backends. Providing a name of +a backend that is not configured results in an error. For more information +about backends, see [here](./format/configuration/renderers.md). diff --git a/src/book/mod.rs b/src/book/mod.rs index da88767a8d..73241c3d2d 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -13,7 +13,9 @@ pub use self::book::{load_book, Book, BookItem, BookItems, Chapter}; pub use self::init::BookBuilder; pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; +use anyhow::anyhow; use log::{debug, error, info, log_enabled, trace, warn}; +use std::collections::{BTreeSet, HashMap}; use std::ffi::OsString; use std::io::{IsTerminal, Write}; use std::path::{Path, PathBuf}; @@ -39,12 +41,21 @@ pub struct MDBook { pub config: Config, /// A representation of the book's contents in memory. pub book: Book, - renderers: Vec>, + renderers: HashMap>, /// List of pre-processors to be run on the book. preprocessors: Vec>, } +/// Set of backends/renderes to run. +pub enum ActiveBackends { + /// Run all available backends. + AllAvailable, + + /// Run specific set of backends. + Specific(BTreeSet), +} + impl MDBook { /// Load a book from its root directory on disk. pub fn load>(book_root: P) -> Result { @@ -190,12 +201,26 @@ impl MDBook { BookBuilder::new(book_root) } - /// Tells the renderer to build our book and put it in the build directory. - pub fn build(&self) -> Result<()> { + /// Tells all renderers/backends to build our book and put it in the build directory. + pub fn render_all(&self) -> Result<()> { + self.render(&ActiveBackends::AllAvailable) + } + + /// Tells a set of renderers/backends to build our book and put it in the build directory. + pub fn render(&self, backends: &ActiveBackends) -> Result<()> { info!("Book building has started"); - for renderer in &self.renderers { - self.execute_build_process(&**renderer)?; + let backends: BTreeSet = match backends { + ActiveBackends::AllAvailable => self.renderers.keys().cloned().collect(), + ActiveBackends::Specific(backends) => backends.clone(), + }; + + for backend in backends { + if let Some(backend) = &self.renderers.get(&backend) { + self.execute_build_process(&***backend)?; + } else { + return Err(anyhow!("backend {backend} does not exist!")); + } } Ok(()) @@ -245,7 +270,8 @@ impl MDBook { /// The only requirement is that your renderer implement the [`Renderer`] /// trait. pub fn with_renderer(&mut self, renderer: R) -> &mut Self { - self.renderers.push(Box::new(renderer)); + self.renderers + .insert(renderer.name().to_string(), Box::new(renderer)); self } @@ -430,24 +456,26 @@ impl MDBook { } /// Look at the `Config` and try to figure out what renderers to use. -fn determine_renderers(config: &Config) -> Vec> { - let mut renderers = Vec::new(); +fn determine_renderers(config: &Config) -> HashMap> { + let mut renderers = HashMap::new(); if let Some(output_table) = config.get("output").and_then(Value::as_table) { renderers.extend(output_table.iter().map(|(key, table)| { - if key == "html" { + let renderer = if key == "html" { Box::new(HtmlHandlebars::new()) as Box } else if key == "markdown" { Box::new(MarkdownRenderer::new()) as Box } else { interpret_custom_renderer(key, table) - } + }; + (renderer.name().to_string(), renderer) })); } // if we couldn't find anything, add the HTML renderer as a default if renderers.is_empty() { - renderers.push(Box::new(HtmlHandlebars::new())); + let r = Box::new(HtmlHandlebars::new()); + renderers.insert(r.name().to_string(), r); } renderers @@ -637,7 +665,7 @@ mod tests { let got = determine_renderers(&cfg); assert_eq!(got.len(), 1); - assert_eq!(got[0].name(), "html"); + assert_eq!(got["html"].name(), "html"); } #[test] @@ -648,7 +676,7 @@ mod tests { let got = determine_renderers(&cfg); assert_eq!(got.len(), 1); - assert_eq!(got[0].name(), "random"); + assert_eq!(got["random"].name(), "random"); } #[test] @@ -662,7 +690,7 @@ mod tests { let got = determine_renderers(&cfg); assert_eq!(got.len(), 1); - assert_eq!(got[0].name(), "random"); + assert_eq!(got["random"].name(), "random"); } #[test] diff --git a/src/cmd/build.rs b/src/cmd/build.rs index e40e5c0c72..11dbb38709 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -1,5 +1,5 @@ use super::command_prelude::*; -use crate::{get_book_dir, open}; +use crate::{get_backends, get_book_dir, open}; use mdbook::errors::Result; use mdbook::MDBook; use std::path::PathBuf; @@ -10,6 +10,7 @@ pub fn make_subcommand() -> Command { .about("Builds a book from its markdown files") .arg_dest_dir() .arg_root_dir() + .arg_backends() .arg_open() } @@ -22,7 +23,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> { book.config.build.build_dir = dest_dir.into(); } - book.build()?; + let active_backends = get_backends(args); + book.render(&active_backends)?; if args.get_flag("open") { // FIXME: What's the right behaviour if we don't use the HTML renderer? diff --git a/src/cmd/command_prelude.rs b/src/cmd/command_prelude.rs index 3719942598..fdab2d5f92 100644 --- a/src/cmd/command_prelude.rs +++ b/src/cmd/command_prelude.rs @@ -37,6 +37,22 @@ pub trait CommandExt: Sized { self._arg(arg!(-o --open "Opens the compiled book in a web browser")) } + fn arg_backends(self) -> Self { + self._arg( + Arg::new("backend") + .short('b') + .long("backend") + .value_name("backend") + .action(clap::ArgAction::Append) + .value_parser(clap::value_parser!(String)) + .help( + "Backend to use.\n\ + This option may be given multiple times to run multiple backends.\n\ + If omitted, mdBook uses all configured backends.", + ), + ) + } + #[cfg(any(feature = "watch", feature = "serve"))] fn arg_watcher(self) -> Self { #[cfg(feature = "watch")] diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index beab121f15..9d1e684c24 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -1,7 +1,7 @@ use super::command_prelude::*; #[cfg(feature = "watch")] use super::watch; -use crate::{get_book_dir, open}; +use crate::{get_backends, get_book_dir, open}; use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; use axum::routing::get; use axum::Router; @@ -43,6 +43,7 @@ pub fn make_subcommand() -> Command { .value_parser(NonEmptyStringValueParser::new()) .help("Port to use for HTTP connections"), ) + .arg_backends() .arg_open() .arg_watcher() } @@ -55,6 +56,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { let port = args.get_one::("port").unwrap(); let hostname = args.get_one::("hostname").unwrap(); let open_browser = args.get_flag("open"); + let active_backends = get_backends(args); let address = format!("{hostname}:{port}"); @@ -69,7 +71,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { book.config.set("output.html.site-url", "/").unwrap(); }; update_config(&mut book); - book.build()?; + book.render(&active_backends)?; let sockaddr: SocketAddr = address .to_socket_addrs()? @@ -101,9 +103,15 @@ pub fn execute(args: &ArgMatches) -> Result<()> { #[cfg(feature = "watch")] { let watcher = watch::WatcherKind::from_str(args.get_one::("watcher").unwrap()); - watch::rebuild_on_change(watcher, &book_dir, &update_config, &move || { - let _ = tx.send(Message::text("reload")); - }); + watch::rebuild_on_change( + watcher, + &book_dir, + &update_config, + &active_backends, + &move || { + let _ = tx.send(Message::text("reload")); + }, + ); } let _ = thread_handle.join(); diff --git a/src/cmd/watch.rs b/src/cmd/watch.rs index 7adb2bbb58..61c9530ad2 100644 --- a/src/cmd/watch.rs +++ b/src/cmd/watch.rs @@ -1,7 +1,7 @@ use super::command_prelude::*; -use crate::{get_book_dir, open}; -use mdbook::errors::Result; +use crate::{get_backends, get_book_dir, open}; use mdbook::MDBook; +use mdbook::{book::ActiveBackends, errors::Result}; use std::path::{Path, PathBuf}; mod native; @@ -14,6 +14,7 @@ pub fn make_subcommand() -> Command { .arg_dest_dir() .arg_root_dir() .arg_open() + .arg_backends() .arg_watcher() } @@ -37,6 +38,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> { let book_dir = get_book_dir(args); let mut book = MDBook::load(&book_dir)?; + let active_backends = get_backends(args); + let update_config = |book: &mut MDBook| { if let Some(dest_dir) = args.get_one::("dest-dir") { book.config.build.build_dir = dest_dir.into(); @@ -45,7 +48,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { update_config(&mut book); if args.get_flag("open") { - book.build()?; + book.render(&active_backends)?; let path = book.build_dir_for("html").join("index.html"); if !path.exists() { error!("No chapter available to open"); @@ -55,7 +58,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> { } let watcher = WatcherKind::from_str(args.get_one::("watcher").unwrap()); - rebuild_on_change(watcher, &book_dir, &update_config, &|| {}); + rebuild_on_change(watcher, &book_dir, &update_config, &active_backends, &|| {}); Ok(()) } @@ -64,11 +67,16 @@ pub fn rebuild_on_change( kind: WatcherKind, book_dir: &Path, update_config: &dyn Fn(&mut MDBook), + backends: &ActiveBackends, post_build: &dyn Fn(), ) { match kind { - WatcherKind::Poll => self::poller::rebuild_on_change(book_dir, update_config, post_build), - WatcherKind::Native => self::native::rebuild_on_change(book_dir, update_config, post_build), + WatcherKind::Poll => { + self::poller::rebuild_on_change(book_dir, update_config, backends, post_build) + } + WatcherKind::Native => { + self::native::rebuild_on_change(book_dir, update_config, backends, post_build) + } } } diff --git a/src/cmd/watch/native.rs b/src/cmd/watch/native.rs index fad8d7ce85..aa53285f64 100644 --- a/src/cmd/watch/native.rs +++ b/src/cmd/watch/native.rs @@ -1,6 +1,7 @@ //! A filesystem watcher using native operating system facilities. use ignore::gitignore::Gitignore; +use mdbook::book::ActiveBackends; use mdbook::MDBook; use std::path::{Path, PathBuf}; use std::sync::mpsc::channel; @@ -10,6 +11,7 @@ use std::time::Duration; pub fn rebuild_on_change( book_dir: &Path, update_config: &dyn Fn(&mut MDBook), + backends: &ActiveBackends, post_build: &dyn Fn(), ) { use notify::RecursiveMode::*; @@ -90,7 +92,7 @@ pub fn rebuild_on_change( match MDBook::load(book_dir) { Ok(mut b) => { update_config(&mut b); - if let Err(e) = b.build() { + if let Err(e) = b.render(backends) { error!("failed to build the book: {e:?}"); } else { post_build(); diff --git a/src/cmd/watch/poller.rs b/src/cmd/watch/poller.rs index 5e1d149748..14d45811f5 100644 --- a/src/cmd/watch/poller.rs +++ b/src/cmd/watch/poller.rs @@ -5,6 +5,7 @@ //! had problems correctly reporting changes. use ignore::gitignore::Gitignore; +use mdbook::book::ActiveBackends; use mdbook::MDBook; use pathdiff::diff_paths; use std::collections::HashMap; @@ -17,6 +18,7 @@ use walkdir::WalkDir; pub fn rebuild_on_change( book_dir: &Path, update_config: &dyn Fn(&mut MDBook), + backends: &ActiveBackends, post_build: &dyn Fn(), ) { let mut book = MDBook::load(book_dir).unwrap_or_else(|e| { @@ -60,7 +62,7 @@ pub fn rebuild_on_change( match MDBook::load(book_dir) { Ok(mut b) => { update_config(&mut b); - if let Err(e) = b.build() { + if let Err(e) = b.render(backends) { error!("failed to build the book: {e:?}"); } else { post_build(); diff --git a/src/lib.rs b/src/lib.rs index 8a8cb3c912..0c25d119bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,7 +54,7 @@ //! //! let mut md = MDBook::load(root_dir) //! .expect("Unable to load the book"); -//! md.build().expect("Building failed"); +//! md.render_all().expect("Building failed"); //! ``` //! //! ## Implementing a new Backend diff --git a/src/main.rs b/src/main.rs index 3e576c5b53..1b6876d111 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ use clap::{Arg, ArgMatches, Command}; use clap_complete::Shell; use env_logger::Builder; use log::LevelFilter; +use mdbook::book::ActiveBackends; use mdbook::utils; use std::env; use std::ffi::OsStr; @@ -133,6 +134,14 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf { } } +fn get_backends(args: &ArgMatches) -> ActiveBackends { + if let Some(backends_iter) = args.get_many("backend") { + ActiveBackends::Specific(backends_iter.cloned().collect()) + } else { + ActiveBackends::AllAvailable + } +} + fn open>(path: P) { info!("Opening web browser"); if let Err(e) = opener::open(path) { diff --git a/tests/testsuite/book_test.rs b/tests/testsuite/book_test.rs index 427c38d18a..2faa57a86c 100644 --- a/tests/testsuite/book_test.rs +++ b/tests/testsuite/book_test.rs @@ -206,7 +206,7 @@ impl BookTest { /// Builds the book in the temp directory. pub fn build(&mut self) -> &mut Self { let book = self.load_book(); - book.build() + book.render_all() .unwrap_or_else(|e| panic!("book failed to build: {e:?}")); self.built = true; self diff --git a/tests/testsuite/preprocessor.rs b/tests/testsuite/preprocessor.rs index db8322af80..4b93a8aa0d 100644 --- a/tests/testsuite/preprocessor.rs +++ b/tests/testsuite/preprocessor.rs @@ -34,7 +34,7 @@ fn runs_preprocessors() { let spy: Arc> = Default::default(); let mut book = test.load_book(); book.with_preprocessor(Spy(Arc::clone(&spy))); - book.build().unwrap(); + book.render_all().unwrap(); let inner = spy.lock().unwrap(); assert_eq!(inner.run_count, 1); diff --git a/tests/testsuite/renderer.rs b/tests/testsuite/renderer.rs index 1e1624474f..ee0d816472 100644 --- a/tests/testsuite/renderer.rs +++ b/tests/testsuite/renderer.rs @@ -33,7 +33,7 @@ fn runs_renderers() { let spy: Arc> = Default::default(); let mut book = test.load_book(); book.with_renderer(Spy(Arc::clone(&spy))); - book.build().unwrap(); + book.render_all().unwrap(); let inner = spy.lock().unwrap(); assert_eq!(inner.run_count, 1); diff --git a/tests/testsuite/search.rs b/tests/testsuite/search.rs index 8ab571b2f4..2340bb2fb6 100644 --- a/tests/testsuite/search.rs +++ b/tests/testsuite/search.rs @@ -124,7 +124,7 @@ fn with_no_source_path() { parent_names: Vec::new(), }; book.book.sections.push(BookItem::Chapter(chapter)); - book.build().unwrap(); + book.render_all().unwrap(); } // Checks that invalid settings in search chapter is rejected.