Skip to content

feat: Allow selective running of backends/renderers. #2754

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/remove-emphasis/test.rs
Original file line number Diff line number Diff line change
@@ -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."));
}
8 changes: 8 additions & 0 deletions guide/src/cli/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions guide/src/cli/serve.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
8 changes: 8 additions & 0 deletions guide/src/cli/watch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
56 changes: 42 additions & 14 deletions src/book/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -39,12 +41,21 @@ pub struct MDBook {
pub config: Config,
/// A representation of the book's contents in memory.
pub book: Book,
renderers: Vec<Box<dyn Renderer>>,
renderers: HashMap<String, Box<dyn Renderer>>,

/// List of pre-processors to be run on the book.
preprocessors: Vec<Box<dyn Preprocessor>>,
}

/// Set of backends/renderes to run.
pub enum ActiveBackends {
/// Run all available backends.
AllAvailable,

/// Run specific set of backends.
Specific(BTreeSet<String>),
}

impl MDBook {
/// Load a book from its root directory on disk.
pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
Expand Down Expand Up @@ -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<String> = 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(())
Expand Down Expand Up @@ -245,7 +270,8 @@ impl MDBook {
/// The only requirement is that your renderer implement the [`Renderer`]
/// trait.
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
self.renderers.push(Box::new(renderer));
self.renderers
.insert(renderer.name().to_string(), Box::new(renderer));
self
}

Expand Down Expand Up @@ -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<Box<dyn Renderer>> {
let mut renderers = Vec::new();
fn determine_renderers(config: &Config) -> HashMap<String, Box<dyn Renderer>> {
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<dyn Renderer>
} else if key == "markdown" {
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
} 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
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand Down
6 changes: 4 additions & 2 deletions src/cmd/build.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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()
}

Expand All @@ -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?
Expand Down
16 changes: 16 additions & 0 deletions src/cmd/command_prelude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
18 changes: 13 additions & 5 deletions src/cmd/serve.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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()
}
Expand All @@ -55,6 +56,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let port = args.get_one::<String>("port").unwrap();
let hostname = args.get_one::<String>("hostname").unwrap();
let open_browser = args.get_flag("open");
let active_backends = get_backends(args);

let address = format!("{hostname}:{port}");

Expand All @@ -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()?
Expand Down Expand Up @@ -101,9 +103,15 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
#[cfg(feature = "watch")]
{
let watcher = watch::WatcherKind::from_str(args.get_one::<String>("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();
Expand Down
20 changes: 14 additions & 6 deletions src/cmd/watch.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,6 +14,7 @@ pub fn make_subcommand() -> Command {
.arg_dest_dir()
.arg_root_dir()
.arg_open()
.arg_backends()
.arg_watcher()
}

Expand All @@ -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::<PathBuf>("dest-dir") {
book.config.build.build_dir = dest_dir.into();
Expand All @@ -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");
Expand All @@ -55,7 +58,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
}

let watcher = WatcherKind::from_str(args.get_one::<String>("watcher").unwrap());
rebuild_on_change(watcher, &book_dir, &update_config, &|| {});
rebuild_on_change(watcher, &book_dir, &update_config, &active_backends, &|| {});

Ok(())
}
Expand All @@ -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)
}
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/cmd/watch/native.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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::*;
Expand Down Expand Up @@ -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();
Expand Down
Loading
Loading