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