summaryrefslogtreecommitdiffstats
path: root/vendor/mdbook/src/book/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/mdbook/src/book/mod.rs')
-rw-r--r--vendor/mdbook/src/book/mod.rs836
1 files changed, 836 insertions, 0 deletions
diff --git a/vendor/mdbook/src/book/mod.rs b/vendor/mdbook/src/book/mod.rs
new file mode 100644
index 000000000..9745d2b7e
--- /dev/null
+++ b/vendor/mdbook/src/book/mod.rs
@@ -0,0 +1,836 @@
+//! The internal representation of a book and infrastructure for loading it from
+//! disk and building it.
+//!
+//! For examples on using `MDBook`, consult the [top-level documentation][1].
+//!
+//! [1]: ../index.html
+
+#[allow(clippy::module_inception)]
+mod book;
+mod init;
+mod summary;
+
+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 std::io::Write;
+use std::path::PathBuf;
+use std::process::Command;
+use std::string::ToString;
+use tempfile::Builder as TempFileBuilder;
+use toml::Value;
+use topological_sort::TopologicalSort;
+
+use crate::errors::*;
+use crate::preprocess::{
+ CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
+};
+use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
+use crate::utils;
+
+use crate::config::{Config, RustEdition};
+
+/// The object used to manage and build a book.
+pub struct MDBook {
+ /// The book's root directory.
+ pub root: PathBuf,
+ /// The configuration used to tweak now a book is built.
+ pub config: Config,
+ /// A representation of the book's contents in memory.
+ pub book: Book,
+ renderers: Vec<Box<dyn Renderer>>,
+
+ /// List of pre-processors to be run on the book.
+ preprocessors: Vec<Box<dyn Preprocessor>>,
+}
+
+impl MDBook {
+ /// Load a book from its root directory on disk.
+ pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
+ let book_root = book_root.into();
+ let config_location = book_root.join("book.toml");
+
+ // the book.json file is no longer used, so we should emit a warning to
+ // let people know to migrate to book.toml
+ if book_root.join("book.json").exists() {
+ warn!("It appears you are still using book.json for configuration.");
+ warn!("This format is no longer used, so you should migrate to the");
+ warn!("book.toml format.");
+ warn!("Check the user guide for migration information:");
+ warn!("\thttps://rust-lang.github.io/mdBook/format/config.html");
+ }
+
+ let mut config = if config_location.exists() {
+ debug!("Loading config from {}", config_location.display());
+ Config::from_disk(&config_location)?
+ } else {
+ Config::default()
+ };
+
+ config.update_from_env();
+
+ if config
+ .html_config()
+ .map_or(false, |html| html.google_analytics.is_some())
+ {
+ warn!(
+ "The output.html.google-analytics field has been deprecated; \
+ it will be removed in a future release.\n\
+ Consider placing the appropriate site tag code into the \
+ theme/head.hbs file instead.\n\
+ The tracking code may be found in the Google Analytics Admin page.\n\
+ "
+ );
+ }
+
+ if log_enabled!(log::Level::Trace) {
+ for line in format!("Config: {:#?}", config).lines() {
+ trace!("{}", line);
+ }
+ }
+
+ MDBook::load_with_config(book_root, config)
+ }
+
+ /// Load a book from its root directory using a custom `Config`.
+ pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
+ let root = book_root.into();
+
+ let src_dir = root.join(&config.book.src);
+ let book = book::load_book(&src_dir, &config.build)?;
+
+ let renderers = determine_renderers(&config);
+ let preprocessors = determine_preprocessors(&config)?;
+
+ Ok(MDBook {
+ root,
+ config,
+ book,
+ renderers,
+ preprocessors,
+ })
+ }
+
+ /// Load a book from its root directory using a custom `Config` and a custom summary.
+ pub fn load_with_config_and_summary<P: Into<PathBuf>>(
+ book_root: P,
+ config: Config,
+ summary: Summary,
+ ) -> Result<MDBook> {
+ let root = book_root.into();
+
+ let src_dir = root.join(&config.book.src);
+ let book = book::load_book_from_disk(&summary, &src_dir)?;
+
+ let renderers = determine_renderers(&config);
+ let preprocessors = determine_preprocessors(&config)?;
+
+ Ok(MDBook {
+ root,
+ config,
+ book,
+ renderers,
+ preprocessors,
+ })
+ }
+
+ /// Returns a flat depth-first iterator over the elements of the book,
+ /// it returns a [`BookItem`] enum:
+ /// `(section: String, bookitem: &BookItem)`
+ ///
+ /// ```no_run
+ /// # use mdbook::MDBook;
+ /// # use mdbook::book::BookItem;
+ /// # let book = MDBook::load("mybook").unwrap();
+ /// for item in book.iter() {
+ /// match *item {
+ /// BookItem::Chapter(ref chapter) => {},
+ /// BookItem::Separator => {},
+ /// BookItem::PartTitle(ref title) => {}
+ /// }
+ /// }
+ ///
+ /// // would print something like this:
+ /// // 1. Chapter 1
+ /// // 1.1 Sub Chapter
+ /// // 1.2 Sub Chapter
+ /// // 2. Chapter 2
+ /// //
+ /// // etc.
+ /// ```
+ pub fn iter(&self) -> BookItems<'_> {
+ self.book.iter()
+ }
+
+ /// `init()` gives you a `BookBuilder` which you can use to setup a new book
+ /// and its accompanying directory structure.
+ ///
+ /// The `BookBuilder` creates some boilerplate files and directories to get
+ /// you started with your book.
+ ///
+ /// ```text
+ /// book-test/
+ /// ├── book
+ /// └── src
+ /// ├── chapter_1.md
+ /// └── SUMMARY.md
+ /// ```
+ ///
+ /// It uses the path provided as the root directory for your book, then adds
+ /// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
+ /// to get you started.
+ pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
+ BookBuilder::new(book_root)
+ }
+
+ /// Tells the renderer to build our book and put it in the build directory.
+ pub fn build(&self) -> Result<()> {
+ info!("Book building has started");
+
+ for renderer in &self.renderers {
+ self.execute_build_process(&**renderer)?;
+ }
+
+ Ok(())
+ }
+
+ /// Run the entire build process for a particular [`Renderer`].
+ pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
+ let mut preprocessed_book = self.book.clone();
+ let preprocess_ctx = PreprocessorContext::new(
+ self.root.clone(),
+ self.config.clone(),
+ renderer.name().to_string(),
+ );
+
+ for preprocessor in &self.preprocessors {
+ if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
+ debug!("Running the {} preprocessor.", preprocessor.name());
+ preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
+ }
+ }
+
+ let name = renderer.name();
+ let build_dir = self.build_dir_for(name);
+
+ let mut render_context = RenderContext::new(
+ self.root.clone(),
+ preprocessed_book,
+ self.config.clone(),
+ build_dir,
+ );
+ render_context
+ .chapter_titles
+ .extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
+
+ info!("Running the {} backend", renderer.name());
+ renderer
+ .render(&render_context)
+ .with_context(|| "Rendering failed")
+ }
+
+ /// You can change the default renderer to another one by using this method.
+ /// 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
+ }
+
+ /// Register a [`Preprocessor`] to be used when rendering the book.
+ pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
+ self.preprocessors.push(Box::new(preprocessor));
+ self
+ }
+
+ /// Run `rustdoc` tests on the book, linking against the provided libraries.
+ pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
+ let library_args: Vec<&str> = (0..library_paths.len())
+ .map(|_| "-L")
+ .zip(library_paths.into_iter())
+ .flat_map(|x| vec![x.0, x.1])
+ .collect();
+
+ let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
+
+ // FIXME: Is "test" the proper renderer name to use here?
+ let preprocess_context =
+ PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
+
+ let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
+ // Index Preprocessor is disabled so that chapter paths continue to point to the
+ // actual markdown files.
+
+ let mut failed = false;
+ for item in book.iter() {
+ if let BookItem::Chapter(ref ch) = *item {
+ let chapter_path = match ch.path {
+ Some(ref path) if !path.as_os_str().is_empty() => path,
+ _ => continue,
+ };
+
+ let path = self.source_dir().join(&chapter_path);
+ info!("Testing file: {:?}", path);
+
+ // write preprocessed file to tempdir
+ let path = temp_dir.path().join(&chapter_path);
+ let mut tmpf = utils::fs::create_file(&path)?;
+ tmpf.write_all(ch.content.as_bytes())?;
+
+ let mut cmd = Command::new("rustdoc");
+ cmd.arg(&path).arg("--test").args(&library_args);
+
+ if let Some(edition) = self.config.rust.edition {
+ match edition {
+ RustEdition::E2015 => {
+ cmd.args(&["--edition", "2015"]);
+ }
+ RustEdition::E2018 => {
+ cmd.args(&["--edition", "2018"]);
+ }
+ RustEdition::E2021 => {
+ cmd.args(&["--edition", "2021"]);
+ }
+ }
+ }
+
+ let output = cmd.output()?;
+
+ if !output.status.success() {
+ failed = true;
+ error!(
+ "rustdoc returned an error:\n\
+ \n--- stdout\n{}\n--- stderr\n{}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ }
+ }
+ }
+ if failed {
+ bail!("One or more tests failed");
+ }
+ Ok(())
+ }
+
+ /// The logic for determining where a backend should put its build
+ /// artefacts.
+ ///
+ /// If there is only 1 renderer, put it in the directory pointed to by the
+ /// `build.build_dir` key in [`Config`]. If there is more than one then the
+ /// renderer gets its own directory within the main build dir.
+ ///
+ /// i.e. If there were only one renderer (in this case, the HTML renderer):
+ ///
+ /// - build/
+ /// - index.html
+ /// - ...
+ ///
+ /// Otherwise if there are multiple:
+ ///
+ /// - build/
+ /// - epub/
+ /// - my_awesome_book.epub
+ /// - html/
+ /// - index.html
+ /// - ...
+ /// - latex/
+ /// - my_awesome_book.tex
+ ///
+ pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
+ let build_dir = self.root.join(&self.config.build.build_dir);
+
+ if self.renderers.len() <= 1 {
+ build_dir
+ } else {
+ build_dir.join(backend_name)
+ }
+ }
+
+ /// Get the directory containing this book's source files.
+ pub fn source_dir(&self) -> PathBuf {
+ self.root.join(&self.config.book.src)
+ }
+
+ /// Get the directory containing the theme resources for the book.
+ pub fn theme_dir(&self) -> PathBuf {
+ self.config
+ .html_config()
+ .unwrap_or_default()
+ .theme_dir(&self.root)
+ }
+}
+
+/// 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();
+
+ if let Some(output_table) = config.get("output").and_then(Value::as_table) {
+ renderers.extend(output_table.iter().map(|(key, table)| {
+ 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)
+ }
+ }));
+ }
+
+ // if we couldn't find anything, add the HTML renderer as a default
+ if renderers.is_empty() {
+ renderers.push(Box::new(HtmlHandlebars::new()));
+ }
+
+ renderers
+}
+
+const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
+
+fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
+ let name = pre.name();
+ name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
+}
+
+/// Look at the `MDBook` and try to figure out what preprocessors to run.
+fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
+ // Collect the names of all preprocessors intended to be run, and the order
+ // in which they should be run.
+ let mut preprocessor_names = TopologicalSort::<String>::new();
+
+ if config.build.use_default_preprocessors {
+ for name in DEFAULT_PREPROCESSORS {
+ preprocessor_names.insert(name.to_string());
+ }
+ }
+
+ if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
+ for (name, table) in preprocessor_table.iter() {
+ preprocessor_names.insert(name.to_string());
+
+ let exists = |name| {
+ (config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
+ || preprocessor_table.contains_key(name)
+ };
+
+ if let Some(before) = table.get("before") {
+ let before = before.as_array().ok_or_else(|| {
+ Error::msg(format!(
+ "Expected preprocessor.{}.before to be an array",
+ name
+ ))
+ })?;
+ for after in before {
+ let after = after.as_str().ok_or_else(|| {
+ Error::msg(format!(
+ "Expected preprocessor.{}.before to contain strings",
+ name
+ ))
+ })?;
+
+ if !exists(after) {
+ // Only warn so that preprocessors can be toggled on and off (e.g. for
+ // troubleshooting) without having to worry about order too much.
+ warn!(
+ "preprocessor.{}.after contains \"{}\", which was not found",
+ name, after
+ );
+ } else {
+ preprocessor_names.add_dependency(name, after);
+ }
+ }
+ }
+
+ if let Some(after) = table.get("after") {
+ let after = after.as_array().ok_or_else(|| {
+ Error::msg(format!(
+ "Expected preprocessor.{}.after to be an array",
+ name
+ ))
+ })?;
+ for before in after {
+ let before = before.as_str().ok_or_else(|| {
+ Error::msg(format!(
+ "Expected preprocessor.{}.after to contain strings",
+ name
+ ))
+ })?;
+
+ if !exists(before) {
+ // See equivalent warning above for rationale
+ warn!(
+ "preprocessor.{}.before contains \"{}\", which was not found",
+ name, before
+ );
+ } else {
+ preprocessor_names.add_dependency(before, name);
+ }
+ }
+ }
+ }
+ }
+
+ // Now that all links have been established, queue preprocessors in a suitable order
+ let mut preprocessors = Vec::with_capacity(preprocessor_names.len());
+ // `pop_all()` returns an empty vector when no more items are not being depended upon
+ for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
+ .take_while(|names| !names.is_empty())
+ {
+ // The `topological_sort` crate does not guarantee a stable order for ties, even across
+ // runs of the same program. Thus, we break ties manually by sorting.
+ // Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
+ // values ([1]), which may not be an alphabetical sort.
+ // As mentioned in [1], doing so depends on locale, which is not desirable for deciding
+ // preprocessor execution order.
+ // [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
+ names.sort();
+ for name in names {
+ let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
+ "links" => Box::new(LinkPreprocessor::new()),
+ "index" => Box::new(IndexPreprocessor::new()),
+ _ => {
+ // The only way to request a custom preprocessor is through the `preprocessor`
+ // table, so it must exist, be a table, and contain the key.
+ let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name];
+ let command = get_custom_preprocessor_cmd(&name, table);
+ Box::new(CmdPreprocessor::new(name, command))
+ }
+ };
+ preprocessors.push(preprocessor);
+ }
+ }
+
+ // "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
+ // Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
+ if preprocessor_names.is_empty() {
+ Ok(preprocessors)
+ } else {
+ Err(Error::msg("Cyclic dependency detected in preprocessors"))
+ }
+}
+
+fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
+ table
+ .get("command")
+ .and_then(Value::as_str)
+ .map(ToString::to_string)
+ .unwrap_or_else(|| format!("mdbook-{}", key))
+}
+
+fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
+ // look for the `command` field, falling back to using the key
+ // prepended by "mdbook-"
+ let table_dot_command = table
+ .get("command")
+ .and_then(Value::as_str)
+ .map(ToString::to_string);
+
+ let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
+
+ Box::new(CmdRenderer::new(key.to_string(), command))
+}
+
+/// Check whether we should run a particular `Preprocessor` in combination
+/// with the renderer, falling back to `Preprocessor::supports_renderer()`
+/// method if the user doesn't say anything.
+///
+/// The `build.use-default-preprocessors` config option can be used to ensure
+/// default preprocessors always run if they support the renderer.
+fn preprocessor_should_run(
+ preprocessor: &dyn Preprocessor,
+ renderer: &dyn Renderer,
+ cfg: &Config,
+) -> bool {
+ // default preprocessors should be run by default (if supported)
+ if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
+ return preprocessor.supports_renderer(renderer.name());
+ }
+
+ let key = format!("preprocessor.{}.renderers", preprocessor.name());
+ let renderer_name = renderer.name();
+
+ if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
+ return explicit_renderers
+ .iter()
+ .filter_map(Value::as_str)
+ .any(|name| name == renderer_name);
+ }
+
+ preprocessor.supports_renderer(renderer_name)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::str::FromStr;
+ use toml::value::{Table, Value};
+
+ #[test]
+ fn config_defaults_to_html_renderer_if_empty() {
+ let cfg = Config::default();
+
+ // make sure we haven't got anything in the `output` table
+ assert!(cfg.get("output").is_none());
+
+ let got = determine_renderers(&cfg);
+
+ assert_eq!(got.len(), 1);
+ assert_eq!(got[0].name(), "html");
+ }
+
+ #[test]
+ fn add_a_random_renderer_to_the_config() {
+ let mut cfg = Config::default();
+ cfg.set("output.random", Table::new()).unwrap();
+
+ let got = determine_renderers(&cfg);
+
+ assert_eq!(got.len(), 1);
+ assert_eq!(got[0].name(), "random");
+ }
+
+ #[test]
+ fn add_a_random_renderer_with_custom_command_to_the_config() {
+ let mut cfg = Config::default();
+
+ let mut table = Table::new();
+ table.insert("command".to_string(), Value::String("false".to_string()));
+ cfg.set("output.random", table).unwrap();
+
+ let got = determine_renderers(&cfg);
+
+ assert_eq!(got.len(), 1);
+ assert_eq!(got[0].name(), "random");
+ }
+
+ #[test]
+ fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
+ let cfg = Config::default();
+
+ // make sure we haven't got anything in the `preprocessor` table
+ assert!(cfg.get("preprocessor").is_none());
+
+ let got = determine_preprocessors(&cfg);
+
+ assert!(got.is_ok());
+ assert_eq!(got.as_ref().unwrap().len(), 2);
+ assert_eq!(got.as_ref().unwrap()[0].name(), "index");
+ assert_eq!(got.as_ref().unwrap()[1].name(), "links");
+ }
+
+ #[test]
+ fn use_default_preprocessors_works() {
+ let mut cfg = Config::default();
+ cfg.build.use_default_preprocessors = false;
+
+ let got = determine_preprocessors(&cfg).unwrap();
+
+ assert_eq!(got.len(), 0);
+ }
+
+ #[test]
+ fn can_determine_third_party_preprocessors() {
+ let cfg_str = r#"
+ [book]
+ title = "Some Book"
+
+ [preprocessor.random]
+
+ [build]
+ build-dir = "outputs"
+ create-missing = false
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ // make sure the `preprocessor.random` table exists
+ assert!(cfg.get_preprocessor("random").is_some());
+
+ let got = determine_preprocessors(&cfg).unwrap();
+
+ assert!(got.into_iter().any(|p| p.name() == "random"));
+ }
+
+ #[test]
+ fn preprocessors_can_provide_their_own_commands() {
+ let cfg_str = r#"
+ [preprocessor.random]
+ command = "python random.py"
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ // make sure the `preprocessor.random` table exists
+ let random = cfg.get_preprocessor("random").unwrap();
+ let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
+
+ assert_eq!(random, "python random.py");
+ }
+
+ #[test]
+ fn preprocessor_before_must_be_array() {
+ let cfg_str = r#"
+ [preprocessor.random]
+ before = 0
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ assert!(determine_preprocessors(&cfg).is_err());
+ }
+
+ #[test]
+ fn preprocessor_after_must_be_array() {
+ let cfg_str = r#"
+ [preprocessor.random]
+ after = 0
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ assert!(determine_preprocessors(&cfg).is_err());
+ }
+
+ #[test]
+ fn preprocessor_order_is_honored() {
+ let cfg_str = r#"
+ [preprocessor.random]
+ before = [ "last" ]
+ after = [ "index" ]
+
+ [preprocessor.last]
+ after = [ "links", "index" ]
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ let preprocessors = determine_preprocessors(&cfg).unwrap();
+ let index = |name| {
+ preprocessors
+ .iter()
+ .enumerate()
+ .find(|(_, preprocessor)| preprocessor.name() == name)
+ .unwrap()
+ .0
+ };
+ let assert_before = |before, after| {
+ if index(before) >= index(after) {
+ eprintln!("Preprocessor order:");
+ for preprocessor in &preprocessors {
+ eprintln!(" {}", preprocessor.name());
+ }
+ panic!("{} should come before {}", before, after);
+ }
+ };
+
+ assert_before("index", "random");
+ assert_before("index", "last");
+ assert_before("random", "last");
+ assert_before("links", "last");
+ }
+
+ #[test]
+ fn cyclic_dependencies_are_detected() {
+ let cfg_str = r#"
+ [preprocessor.links]
+ before = [ "index" ]
+
+ [preprocessor.index]
+ before = [ "links" ]
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ assert!(determine_preprocessors(&cfg).is_err());
+ }
+
+ #[test]
+ fn dependencies_dont_register_undefined_preprocessors() {
+ let cfg_str = r#"
+ [preprocessor.links]
+ before = [ "random" ]
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ let preprocessors = determine_preprocessors(&cfg).unwrap();
+
+ assert!(!preprocessors
+ .iter()
+ .any(|preprocessor| preprocessor.name() == "random"));
+ }
+
+ #[test]
+ fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
+ let cfg_str = r#"
+ [preprocessor.random]
+ before = [ "links" ]
+
+ [build]
+ use-default-preprocessors = false
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ let preprocessors = determine_preprocessors(&cfg).unwrap();
+
+ assert!(!preprocessors
+ .iter()
+ .any(|preprocessor| preprocessor.name() == "links"));
+ }
+
+ #[test]
+ fn config_respects_preprocessor_selection() {
+ let cfg_str = r#"
+ [preprocessor.links]
+ renderers = ["html"]
+ "#;
+
+ let cfg = Config::from_str(cfg_str).unwrap();
+
+ // double-check that we can access preprocessor.links.renderers[0]
+ let html = cfg
+ .get_preprocessor("links")
+ .and_then(|links| links.get("renderers"))
+ .and_then(Value::as_array)
+ .and_then(|renderers| renderers.get(0))
+ .and_then(Value::as_str)
+ .unwrap();
+ assert_eq!(html, "html");
+ let html_renderer = HtmlHandlebars::default();
+ let pre = LinkPreprocessor::new();
+
+ let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
+ assert!(should_run);
+ }
+
+ struct BoolPreprocessor(bool);
+ impl Preprocessor for BoolPreprocessor {
+ fn name(&self) -> &str {
+ "bool-preprocessor"
+ }
+
+ fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
+ unimplemented!()
+ }
+
+ fn supports_renderer(&self, _renderer: &str) -> bool {
+ self.0
+ }
+ }
+
+ #[test]
+ fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
+ let cfg = Config::default();
+ let html = HtmlHandlebars::new();
+
+ let should_be = true;
+ let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
+ assert_eq!(got, should_be);
+
+ let should_be = false;
+ let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
+ assert_eq!(got, should_be);
+ }
+}