]> git.proxmox.com Git - rustc.git/blame - vendor/mdbook/src/book/mod.rs
New upstream version 1.64.0+dfsg1
[rustc.git] / vendor / mdbook / src / book / mod.rs
CommitLineData
2c00a5a8
XL
1//! The internal representation of a book and infrastructure for loading it from
2//! disk and building it.
3//!
4//! For examples on using `MDBook`, consult the [top-level documentation][1].
5//!
6//! [1]: ../index.html
7
f035d41b 8#[allow(clippy::module_inception)]
2c00a5a8
XL
9mod book;
10mod init;
9fa01778 11mod summary;
2c00a5a8
XL
12
13pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
2c00a5a8 14pub use self::init::BookBuilder;
9fa01778 15pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
2c00a5a8 16
2c00a5a8 17use std::io::Write;
9fa01778 18use std::path::PathBuf;
cc61c64b 19use std::process::Command;
dc9dc135 20use std::string::ToString;
83c7162d 21use tempfile::Builder as TempFileBuilder;
2c00a5a8 22use toml::Value;
a2a8927a 23use topological_sort::TopologicalSort;
cc61c64b 24
dc9dc135
XL
25use crate::errors::*;
26use crate::preprocess::{
9fa01778
XL
27 CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
28};
e74abb32 29use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
dc9dc135 30use crate::utils;
cc61c64b 31
f035d41b 32use crate::config::{Config, RustEdition};
cc61c64b 33
2c00a5a8 34/// The object used to manage and build a book.
cc61c64b 35pub struct MDBook {
2c00a5a8
XL
36 /// The book's root directory.
37 pub root: PathBuf,
38 /// The configuration used to tweak now a book is built.
39 pub config: Config,
40 /// A representation of the book's contents in memory.
41 pub book: Book,
dc9dc135 42 renderers: Vec<Box<dyn Renderer>>,
2c00a5a8 43
5869c6ff 44 /// List of pre-processors to be run on the book.
dc9dc135 45 preprocessors: Vec<Box<dyn Preprocessor>>,
cc61c64b
XL
46}
47
48impl MDBook {
2c00a5a8
XL
49 /// Load a book from its root directory on disk.
50 pub fn load<P: Into<PathBuf>>(book_root: P) -> Result<MDBook> {
51 let book_root = book_root.into();
52 let config_location = book_root.join("book.toml");
53
54 // the book.json file is no longer used, so we should emit a warning to
55 // let people know to migrate to book.toml
56 if book_root.join("book.json").exists() {
57 warn!("It appears you are still using book.json for configuration.");
58 warn!("This format is no longer used, so you should migrate to the");
59 warn!("book.toml format.");
60 warn!("Check the user guide for migration information:");
e74abb32 61 warn!("\thttps://rust-lang.github.io/mdBook/format/config.html");
2c00a5a8 62 }
cc61c64b 63
2c00a5a8
XL
64 let mut config = if config_location.exists() {
65 debug!("Loading config from {}", config_location.display());
66 Config::from_disk(&config_location)?
67 } else {
68 Config::default()
69 };
cc61c64b 70
2c00a5a8
XL
71 config.update_from_env();
72
a2a8927a
XL
73 if config
74 .html_config()
75 .map_or(false, |html| html.google_analytics.is_some())
76 {
77 warn!(
78 "The output.html.google-analytics field has been deprecated; \
79 it will be removed in a future release.\n\
80 Consider placing the appropriate site tag code into the \
81 theme/head.hbs file instead.\n\
82 The tracking code may be found in the Google Analytics Admin page.\n\
83 "
84 );
85 }
86
dc9dc135 87 if log_enabled!(log::Level::Trace) {
2c00a5a8
XL
88 for line in format!("Config: {:#?}", config).lines() {
89 trace!("{}", line);
90 }
cc61c64b
XL
91 }
92
2c00a5a8
XL
93 MDBook::load_with_config(book_root, config)
94 }
cc61c64b 95
5869c6ff 96 /// Load a book from its root directory using a custom `Config`.
2c00a5a8
XL
97 pub fn load_with_config<P: Into<PathBuf>>(book_root: P, config: Config) -> Result<MDBook> {
98 let root = book_root.into();
cc61c64b 99
2c00a5a8
XL
100 let src_dir = root.join(&config.book.src);
101 let book = book::load_book(&src_dir, &config.build)?;
102
103 let renderers = determine_renderers(&config);
104 let preprocessors = determine_preprocessors(&config)?;
105
106 Ok(MDBook {
107 root,
108 config,
109 book,
110 renderers,
111 preprocessors,
112 })
cc61c64b
XL
113 }
114
5869c6ff 115 /// Load a book from its root directory using a custom `Config` and a custom summary.
dc9dc135
XL
116 pub fn load_with_config_and_summary<P: Into<PathBuf>>(
117 book_root: P,
118 config: Config,
119 summary: Summary,
120 ) -> Result<MDBook> {
121 let root = book_root.into();
122
123 let src_dir = root.join(&config.book.src);
124 let book = book::load_book_from_disk(&summary, &src_dir)?;
125
126 let renderers = determine_renderers(&config);
127 let preprocessors = determine_preprocessors(&config)?;
128
129 Ok(MDBook {
130 root,
131 config,
132 book,
133 renderers,
134 preprocessors,
135 })
136 }
137
041b39d2 138 /// Returns a flat depth-first iterator over the elements of the book,
5869c6ff 139 /// it returns a [`BookItem`] enum:
cc61c64b
XL
140 /// `(section: String, bookitem: &BookItem)`
141 ///
142 /// ```no_run
cc61c64b 143 /// # use mdbook::MDBook;
2c00a5a8 144 /// # use mdbook::book::BookItem;
2c00a5a8 145 /// # let book = MDBook::load("mybook").unwrap();
cc61c64b 146 /// for item in book.iter() {
2c00a5a8
XL
147 /// match *item {
148 /// BookItem::Chapter(ref chapter) => {},
149 /// BookItem::Separator => {},
f035d41b 150 /// BookItem::PartTitle(ref title) => {}
cc61c64b
XL
151 /// }
152 /// }
153 ///
154 /// // would print something like this:
155 /// // 1. Chapter 1
156 /// // 1.1 Sub Chapter
157 /// // 1.2 Sub Chapter
158 /// // 2. Chapter 2
159 /// //
160 /// // etc.
cc61c64b 161 /// ```
dc9dc135 162 pub fn iter(&self) -> BookItems<'_> {
2c00a5a8 163 self.book.iter()
cc61c64b
XL
164 }
165
2c00a5a8
XL
166 /// `init()` gives you a `BookBuilder` which you can use to setup a new book
167 /// and its accompanying directory structure.
168 ///
169 /// The `BookBuilder` creates some boilerplate files and directories to get
170 /// you started with your book.
cc61c64b
XL
171 ///
172 /// ```text
173 /// book-test/
174 /// ├── book
175 /// └── src
176 /// ├── chapter_1.md
177 /// └── SUMMARY.md
178 /// ```
179 ///
2c00a5a8
XL
180 /// It uses the path provided as the root directory for your book, then adds
181 /// in a `src/` directory containing a `SUMMARY.md` and `chapter_1.md` file
182 /// to get you started.
183 pub fn init<P: Into<PathBuf>>(book_root: P) -> BookBuilder {
184 BookBuilder::new(book_root)
185 }
cc61c64b 186
2c00a5a8
XL
187 /// Tells the renderer to build our book and put it in the build directory.
188 pub fn build(&self) -> Result<()> {
189 info!("Book building has started");
cc61c64b 190
9fa01778
XL
191 for renderer in &self.renderers {
192 self.execute_build_process(&**renderer)?;
193 }
194
195 Ok(())
196 }
197
5869c6ff 198 /// Run the entire build process for a particular [`Renderer`].
f9f354fc 199 pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
2c00a5a8 200 let mut preprocessed_book = self.book.clone();
9fa01778
XL
201 let preprocess_ctx = PreprocessorContext::new(
202 self.root.clone(),
203 self.config.clone(),
204 renderer.name().to_string(),
205 );
cc61c64b 206
2c00a5a8 207 for preprocessor in &self.preprocessors {
9fa01778
XL
208 if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
209 debug!("Running the {} preprocessor.", preprocessor.name());
210 preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
211 }
cc61c64b
XL
212 }
213
2c00a5a8
XL
214 let name = renderer.name();
215 let build_dir = self.build_dir_for(name);
cc61c64b 216
94222f64 217 let mut render_context = RenderContext::new(
2c00a5a8 218 self.root.clone(),
94222f64 219 preprocessed_book,
2c00a5a8
XL
220 self.config.clone(),
221 build_dir,
222 );
94222f64
XL
223 render_context
224 .chapter_titles
225 .extend(preprocess_ctx.chapter_titles.borrow_mut().drain());
cc61c64b 226
94222f64 227 info!("Running the {} backend", renderer.name());
2c00a5a8
XL
228 renderer
229 .render(&render_context)
f035d41b 230 .with_context(|| "Rendering failed")
2c00a5a8 231 }
7cac9316 232
2c00a5a8 233 /// You can change the default renderer to another one by using this method.
5869c6ff
XL
234 /// The only requirement is that your renderer implement the [`Renderer`]
235 /// trait.
2c00a5a8
XL
236 pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
237 self.renderers.push(Box::new(renderer));
238 self
239 }
cc61c64b 240
5869c6ff 241 /// Register a [`Preprocessor`] to be used when rendering the book.
dc9dc135 242 pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
2c00a5a8
XL
243 self.preprocessors.push(Box::new(preprocessor));
244 self
cc61c64b
XL
245 }
246
2c00a5a8
XL
247 /// Run `rustdoc` tests on the book, linking against the provided libraries.
248 pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
249 let library_args: Vec<&str> = (0..library_paths.len())
250 .map(|_| "-L")
251 .zip(library_paths.into_iter())
252 .flat_map(|x| vec![x.0, x.1])
253 .collect();
cc61c64b 254
9fa01778 255 let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
cc61c64b 256
9fa01778
XL
257 // FIXME: Is "test" the proper renderer name to use here?
258 let preprocess_context =
259 PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
cc61c64b 260
9fa01778
XL
261 let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
262 // Index Preprocessor is disabled so that chapter paths continue to point to the
263 // actual markdown files.
cc61c64b 264
1b1a35ee 265 let mut failed = false;
9fa01778 266 for item in book.iter() {
2c00a5a8 267 if let BookItem::Chapter(ref ch) = *item {
f035d41b
XL
268 let chapter_path = match ch.path {
269 Some(ref path) if !path.as_os_str().is_empty() => path,
270 _ => continue,
271 };
272
273 let path = self.source_dir().join(&chapter_path);
274 info!("Testing file: {:?}", path);
275
276 // write preprocessed file to tempdir
277 let path = temp_dir.path().join(&chapter_path);
278 let mut tmpf = utils::fs::create_file(&path)?;
279 tmpf.write_all(ch.content.as_bytes())?;
280
281 let mut cmd = Command::new("rustdoc");
282 cmd.arg(&path).arg("--test").args(&library_args);
283
284 if let Some(edition) = self.config.rust.edition {
285 match edition {
286 RustEdition::E2015 => {
287 cmd.args(&["--edition", "2015"]);
288 }
289 RustEdition::E2018 => {
290 cmd.args(&["--edition", "2018"]);
291 }
94222f64 292 RustEdition::E2021 => {
a2a8927a 293 cmd.args(&["--edition", "2021"]);
94222f64 294 }
2c00a5a8
XL
295 }
296 }
f035d41b
XL
297
298 let output = cmd.output()?;
299
300 if !output.status.success() {
1b1a35ee
XL
301 failed = true;
302 error!(
f035d41b
XL
303 "rustdoc returned an error:\n\
304 \n--- stdout\n{}\n--- stderr\n{}",
305 String::from_utf8_lossy(&output.stdout),
306 String::from_utf8_lossy(&output.stderr)
307 );
308 }
2c00a5a8 309 }
cc61c64b 310 }
1b1a35ee
XL
311 if failed {
312 bail!("One or more tests failed");
313 }
2c00a5a8 314 Ok(())
cc61c64b
XL
315 }
316
2c00a5a8
XL
317 /// The logic for determining where a backend should put its build
318 /// artefacts.
cc61c64b 319 ///
2c00a5a8 320 /// If there is only 1 renderer, put it in the directory pointed to by the
5869c6ff 321 /// `build.build_dir` key in [`Config`]. If there is more than one then the
2c00a5a8
XL
322 /// renderer gets its own directory within the main build dir.
323 ///
324 /// i.e. If there were only one renderer (in this case, the HTML renderer):
325 ///
326 /// - build/
327 /// - index.html
328 /// - ...
329 ///
330 /// Otherwise if there are multiple:
331 ///
332 /// - build/
333 /// - epub/
334 /// - my_awesome_book.epub
335 /// - html/
336 /// - index.html
337 /// - ...
338 /// - latex/
339 /// - my_awesome_book.tex
340 ///
341 pub fn build_dir_for(&self, backend_name: &str) -> PathBuf {
342 let build_dir = self.root.join(&self.config.build.build_dir);
cc61c64b 343
2c00a5a8
XL
344 if self.renderers.len() <= 1 {
345 build_dir
346 } else {
347 build_dir.join(backend_name)
348 }
cc61c64b
XL
349 }
350
2c00a5a8
XL
351 /// Get the directory containing this book's source files.
352 pub fn source_dir(&self) -> PathBuf {
353 self.root.join(&self.config.book.src)
cc61c64b
XL
354 }
355
83c7162d 356 /// Get the directory containing the theme resources for the book.
2c00a5a8 357 pub fn theme_dir(&self) -> PathBuf {
83c7162d
XL
358 self.config
359 .html_config()
360 .unwrap_or_default()
361 .theme_dir(&self.root)
cc61c64b 362 }
2c00a5a8 363}
cc61c64b 364
2c00a5a8 365/// Look at the `Config` and try to figure out what renderers to use.
dc9dc135 366fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
416331ca 367 let mut renderers = Vec::new();
2c00a5a8 368
dc9dc135 369 if let Some(output_table) = config.get("output").and_then(Value::as_table) {
416331ca 370 renderers.extend(output_table.iter().map(|(key, table)| {
2c00a5a8 371 if key == "html" {
416331ca 372 Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
e74abb32
XL
373 } else if key == "markdown" {
374 Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
2c00a5a8 375 } else {
416331ca 376 interpret_custom_renderer(key, table)
2c00a5a8 377 }
416331ca 378 }));
cc61c64b
XL
379 }
380
2c00a5a8
XL
381 // if we couldn't find anything, add the HTML renderer as a default
382 if renderers.is_empty() {
383 renderers.push(Box::new(HtmlHandlebars::new()));
384 }
cc61c64b 385
2c00a5a8
XL
386 renderers
387}
cc61c64b 388
064997fb 389const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"];
9fa01778 390
dc9dc135 391fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
9fa01778
XL
392 let name = pre.name();
393 name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
2c00a5a8 394}
cc61c64b 395
2c00a5a8 396/// Look at the `MDBook` and try to figure out what preprocessors to run.
dc9dc135 397fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
a2a8927a
XL
398 // Collect the names of all preprocessors intended to be run, and the order
399 // in which they should be run.
400 let mut preprocessor_names = TopologicalSort::<String>::new();
9fa01778
XL
401
402 if config.build.use_default_preprocessors {
a2a8927a
XL
403 for name in DEFAULT_PREPROCESSORS {
404 preprocessor_names.insert(name.to_string());
405 }
9fa01778
XL
406 }
407
dc9dc135 408 if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
a2a8927a
XL
409 for (name, table) in preprocessor_table.iter() {
410 preprocessor_names.insert(name.to_string());
411
412 let exists = |name| {
413 (config.build.use_default_preprocessors && DEFAULT_PREPROCESSORS.contains(&name))
414 || preprocessor_table.contains_key(name)
415 };
416
417 if let Some(before) = table.get("before") {
418 let before = before.as_array().ok_or_else(|| {
419 Error::msg(format!(
420 "Expected preprocessor.{}.before to be an array",
421 name
422 ))
423 })?;
424 for after in before {
425 let after = after.as_str().ok_or_else(|| {
426 Error::msg(format!(
427 "Expected preprocessor.{}.before to contain strings",
428 name
429 ))
430 })?;
431
432 if !exists(after) {
433 // Only warn so that preprocessors can be toggled on and off (e.g. for
434 // troubleshooting) without having to worry about order too much.
435 warn!(
436 "preprocessor.{}.after contains \"{}\", which was not found",
437 name, after
438 );
439 } else {
440 preprocessor_names.add_dependency(name, after);
441 }
442 }
443 }
444
445 if let Some(after) = table.get("after") {
446 let after = after.as_array().ok_or_else(|| {
447 Error::msg(format!(
448 "Expected preprocessor.{}.after to be an array",
449 name
450 ))
451 })?;
452 for before in after {
453 let before = before.as_str().ok_or_else(|| {
454 Error::msg(format!(
455 "Expected preprocessor.{}.after to contain strings",
456 name
457 ))
458 })?;
459
460 if !exists(before) {
461 // See equivalent warning above for rationale
462 warn!(
463 "preprocessor.{}.before contains \"{}\", which was not found",
464 name, before
465 );
466 } else {
467 preprocessor_names.add_dependency(before, name);
468 }
469 }
9fa01778 470 }
ea8adc8c 471 }
cc61c64b
XL
472 }
473
a2a8927a
XL
474 // Now that all links have been established, queue preprocessors in a suitable order
475 let mut preprocessors = Vec::with_capacity(preprocessor_names.len());
476 // `pop_all()` returns an empty vector when no more items are not being depended upon
477 for mut names in std::iter::repeat_with(|| preprocessor_names.pop_all())
478 .take_while(|names| !names.is_empty())
479 {
480 // The `topological_sort` crate does not guarantee a stable order for ties, even across
481 // runs of the same program. Thus, we break ties manually by sorting.
482 // Careful: `str`'s default sorting, which we are implicitly invoking here, uses code point
483 // values ([1]), which may not be an alphabetical sort.
484 // As mentioned in [1], doing so depends on locale, which is not desirable for deciding
485 // preprocessor execution order.
486 // [1]: https://doc.rust-lang.org/stable/std/cmp/trait.Ord.html#impl-Ord-14
487 names.sort();
488 for name in names {
489 let preprocessor: Box<dyn Preprocessor> = match name.as_str() {
490 "links" => Box::new(LinkPreprocessor::new()),
491 "index" => Box::new(IndexPreprocessor::new()),
492 _ => {
493 // The only way to request a custom preprocessor is through the `preprocessor`
494 // table, so it must exist, be a table, and contain the key.
495 let table = &config.get("preprocessor").unwrap().as_table().unwrap()[&name];
496 let command = get_custom_preprocessor_cmd(&name, table);
497 Box::new(CmdPreprocessor::new(name, command))
498 }
499 };
500 preprocessors.push(preprocessor);
501 }
502 }
503
504 // "If `pop_all` returns an empty vector and `len` is not 0, there are cyclic dependencies."
505 // Normally, `len() == 0` is equivalent to `is_empty()`, so we'll use that.
506 if preprocessor_names.is_empty() {
507 Ok(preprocessors)
508 } else {
509 Err(Error::msg("Cyclic dependency detected in preprocessors"))
510 }
2c00a5a8 511}
cc61c64b 512
a2a8927a
XL
513fn get_custom_preprocessor_cmd(key: &str, table: &Value) -> String {
514 table
9fa01778 515 .get("command")
dc9dc135
XL
516 .and_then(Value::as_str)
517 .map(ToString::to_string)
a2a8927a 518 .unwrap_or_else(|| format!("mdbook-{}", key))
9fa01778
XL
519}
520
521fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
2c00a5a8
XL
522 // look for the `command` field, falling back to using the key
523 // prepended by "mdbook-"
524 let table_dot_command = table
525 .get("command")
dc9dc135
XL
526 .and_then(Value::as_str)
527 .map(ToString::to_string);
cc61c64b 528
2c00a5a8 529 let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
cc61c64b 530
f035d41b 531 Box::new(CmdRenderer::new(key.to_string(), command))
2c00a5a8 532}
ea8adc8c 533
9fa01778
XL
534/// Check whether we should run a particular `Preprocessor` in combination
535/// with the renderer, falling back to `Preprocessor::supports_renderer()`
536/// method if the user doesn't say anything.
537///
538/// The `build.use-default-preprocessors` config option can be used to ensure
539/// default preprocessors always run if they support the renderer.
dc9dc135
XL
540fn preprocessor_should_run(
541 preprocessor: &dyn Preprocessor,
542 renderer: &dyn Renderer,
543 cfg: &Config,
544) -> bool {
9fa01778
XL
545 // default preprocessors should be run by default (if supported)
546 if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
547 return preprocessor.supports_renderer(renderer.name());
548 }
549
550 let key = format!("preprocessor.{}.renderers", preprocessor.name());
551 let renderer_name = renderer.name();
552
553 if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
554 return explicit_renderers
555 .iter()
dc9dc135 556 .filter_map(Value::as_str)
9fa01778
XL
557 .any(|name| name == renderer_name);
558 }
559
560 preprocessor.supports_renderer(renderer_name)
561}
562
2c00a5a8
XL
563#[cfg(test)]
564mod tests {
565 use super::*;
dc9dc135 566 use std::str::FromStr;
2c00a5a8 567 use toml::value::{Table, Value};
cc61c64b 568
2c00a5a8
XL
569 #[test]
570 fn config_defaults_to_html_renderer_if_empty() {
571 let cfg = Config::default();
ea8adc8c 572
2c00a5a8
XL
573 // make sure we haven't got anything in the `output` table
574 assert!(cfg.get("output").is_none());
cc61c64b 575
2c00a5a8 576 let got = determine_renderers(&cfg);
cc61c64b 577
2c00a5a8
XL
578 assert_eq!(got.len(), 1);
579 assert_eq!(got[0].name(), "html");
cc61c64b
XL
580 }
581
2c00a5a8
XL
582 #[test]
583 fn add_a_random_renderer_to_the_config() {
584 let mut cfg = Config::default();
585 cfg.set("output.random", Table::new()).unwrap();
cc61c64b 586
2c00a5a8 587 let got = determine_renderers(&cfg);
cc61c64b 588
2c00a5a8
XL
589 assert_eq!(got.len(), 1);
590 assert_eq!(got[0].name(), "random");
cc61c64b
XL
591 }
592
2c00a5a8
XL
593 #[test]
594 fn add_a_random_renderer_with_custom_command_to_the_config() {
595 let mut cfg = Config::default();
cc61c64b 596
2c00a5a8
XL
597 let mut table = Table::new();
598 table.insert("command".to_string(), Value::String("false".to_string()));
599 cfg.set("output.random", table).unwrap();
cc61c64b 600
2c00a5a8 601 let got = determine_renderers(&cfg);
cc61c64b 602
2c00a5a8
XL
603 assert_eq!(got.len(), 1);
604 assert_eq!(got[0].name(), "random");
cc61c64b
XL
605 }
606
2c00a5a8 607 #[test]
9fa01778 608 fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
2c00a5a8 609 let cfg = Config::default();
cc61c64b 610
9fa01778
XL
611 // make sure we haven't got anything in the `preprocessor` table
612 assert!(cfg.get("preprocessor").is_none());
cc61c64b 613
2c00a5a8 614 let got = determine_preprocessors(&cfg);
cc61c64b 615
2c00a5a8 616 assert!(got.is_ok());
9fa01778 617 assert_eq!(got.as_ref().unwrap().len(), 2);
a2a8927a
XL
618 assert_eq!(got.as_ref().unwrap()[0].name(), "index");
619 assert_eq!(got.as_ref().unwrap()[1].name(), "links");
9fa01778
XL
620 }
621
622 #[test]
623 fn use_default_preprocessors_works() {
624 let mut cfg = Config::default();
625 cfg.build.use_default_preprocessors = false;
626
627 let got = determine_preprocessors(&cfg).unwrap();
628
629 assert_eq!(got.len(), 0);
cc61c64b
XL
630 }
631
2c00a5a8 632 #[test]
9fa01778 633 fn can_determine_third_party_preprocessors() {
dc9dc135 634 let cfg_str = r#"
2c00a5a8
XL
635 [book]
636 title = "Some Book"
cc61c64b 637
9fa01778
XL
638 [preprocessor.random]
639
2c00a5a8
XL
640 [build]
641 build-dir = "outputs"
642 create-missing = false
2c00a5a8 643 "#;
ea8adc8c 644
2c00a5a8 645 let cfg = Config::from_str(cfg_str).unwrap();
ea8adc8c 646
9fa01778
XL
647 // make sure the `preprocessor.random` table exists
648 assert!(cfg.get_preprocessor("random").is_some());
ea8adc8c 649
9fa01778 650 let got = determine_preprocessors(&cfg).unwrap();
ea8adc8c 651
9fa01778 652 assert!(got.into_iter().any(|p| p.name() == "random"));
ea8adc8c
XL
653 }
654
2c00a5a8 655 #[test]
9fa01778
XL
656 fn preprocessors_can_provide_their_own_commands() {
657 let cfg_str = r#"
658 [preprocessor.random]
659 command = "python random.py"
660 "#;
ea8adc8c 661
9fa01778
XL
662 let cfg = Config::from_str(cfg_str).unwrap();
663
664 // make sure the `preprocessor.random` table exists
665 let random = cfg.get_preprocessor("random").unwrap();
a2a8927a
XL
666 let random = get_custom_preprocessor_cmd("random", &Value::Table(random.clone()));
667
668 assert_eq!(random, "python random.py");
669 }
670
671 #[test]
672 fn preprocessor_before_must_be_array() {
673 let cfg_str = r#"
674 [preprocessor.random]
675 before = 0
676 "#;
677
678 let cfg = Config::from_str(cfg_str).unwrap();
679
680 assert!(determine_preprocessors(&cfg).is_err());
681 }
682
683 #[test]
684 fn preprocessor_after_must_be_array() {
685 let cfg_str = r#"
686 [preprocessor.random]
687 after = 0
688 "#;
689
690 let cfg = Config::from_str(cfg_str).unwrap();
691
692 assert!(determine_preprocessors(&cfg).is_err());
693 }
694
695 #[test]
696 fn preprocessor_order_is_honored() {
697 let cfg_str = r#"
698 [preprocessor.random]
699 before = [ "last" ]
700 after = [ "index" ]
701
702 [preprocessor.last]
703 after = [ "links", "index" ]
704 "#;
705
706 let cfg = Config::from_str(cfg_str).unwrap();
707
708 let preprocessors = determine_preprocessors(&cfg).unwrap();
709 let index = |name| {
710 preprocessors
711 .iter()
712 .enumerate()
713 .find(|(_, preprocessor)| preprocessor.name() == name)
714 .unwrap()
715 .0
716 };
717 let assert_before = |before, after| {
718 if index(before) >= index(after) {
719 eprintln!("Preprocessor order:");
720 for preprocessor in &preprocessors {
721 eprintln!(" {}", preprocessor.name());
722 }
723 panic!("{} should come before {}", before, after);
724 }
725 };
726
727 assert_before("index", "random");
728 assert_before("index", "last");
729 assert_before("random", "last");
730 assert_before("links", "last");
731 }
732
733 #[test]
734 fn cyclic_dependencies_are_detected() {
735 let cfg_str = r#"
736 [preprocessor.links]
737 before = [ "index" ]
738
739 [preprocessor.index]
740 before = [ "links" ]
741 "#;
742
743 let cfg = Config::from_str(cfg_str).unwrap();
744
745 assert!(determine_preprocessors(&cfg).is_err());
746 }
747
748 #[test]
749 fn dependencies_dont_register_undefined_preprocessors() {
750 let cfg_str = r#"
751 [preprocessor.links]
752 before = [ "random" ]
753 "#;
754
755 let cfg = Config::from_str(cfg_str).unwrap();
9fa01778 756
a2a8927a
XL
757 let preprocessors = determine_preprocessors(&cfg).unwrap();
758
064997fb 759 assert!(!preprocessors
a2a8927a 760 .iter()
064997fb 761 .any(|preprocessor| preprocessor.name() == "random"));
a2a8927a
XL
762 }
763
764 #[test]
765 fn dependencies_dont_register_builtin_preprocessors_if_disabled() {
766 let cfg_str = r#"
767 [preprocessor.random]
768 before = [ "links" ]
769
770 [build]
771 use-default-preprocessors = false
772 "#;
773
774 let cfg = Config::from_str(cfg_str).unwrap();
775
776 let preprocessors = determine_preprocessors(&cfg).unwrap();
777
064997fb 778 assert!(!preprocessors
a2a8927a 779 .iter()
064997fb 780 .any(|preprocessor| preprocessor.name() == "links"));
9fa01778
XL
781 }
782
783 #[test]
784 fn config_respects_preprocessor_selection() {
dc9dc135 785 let cfg_str = r#"
9fa01778
XL
786 [preprocessor.links]
787 renderers = ["html"]
2c00a5a8 788 "#;
ea8adc8c 789
2c00a5a8 790 let cfg = Config::from_str(cfg_str).unwrap();
ea8adc8c 791
9fa01778
XL
792 // double-check that we can access preprocessor.links.renderers[0]
793 let html = cfg
794 .get_preprocessor("links")
795 .and_then(|links| links.get("renderers"))
dc9dc135 796 .and_then(Value::as_array)
9fa01778 797 .and_then(|renderers| renderers.get(0))
dc9dc135 798 .and_then(Value::as_str)
9fa01778
XL
799 .unwrap();
800 assert_eq!(html, "html");
801 let html_renderer = HtmlHandlebars::default();
802 let pre = LinkPreprocessor::new();
803
804 let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
805 assert!(should_run);
806 }
ea8adc8c 807
9fa01778
XL
808 struct BoolPreprocessor(bool);
809 impl Preprocessor for BoolPreprocessor {
810 fn name(&self) -> &str {
811 "bool-preprocessor"
812 }
813
814 fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
815 unimplemented!()
816 }
817
818 fn supports_renderer(&self, _renderer: &str) -> bool {
819 self.0
820 }
821 }
822
823 #[test]
824 fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
825 let cfg = Config::default();
826 let html = HtmlHandlebars::new();
827
828 let should_be = true;
829 let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
830 assert_eq!(got, should_be);
cc61c64b 831
9fa01778
XL
832 let should_be = false;
833 let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
834 assert_eq!(got, should_be);
cc61c64b
XL
835 }
836}