]>
Commit | Line | Data |
---|---|---|
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 |
9 | mod book; |
10 | mod init; | |
9fa01778 | 11 | mod summary; |
2c00a5a8 XL |
12 | |
13 | pub use self::book::{load_book, Book, BookItem, BookItems, Chapter}; | |
2c00a5a8 | 14 | pub use self::init::BookBuilder; |
9fa01778 | 15 | pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem}; |
2c00a5a8 | 16 | |
2c00a5a8 | 17 | use std::io::Write; |
9fa01778 | 18 | use std::path::PathBuf; |
cc61c64b | 19 | use std::process::Command; |
dc9dc135 | 20 | use std::string::ToString; |
83c7162d | 21 | use tempfile::Builder as TempFileBuilder; |
2c00a5a8 | 22 | use toml::Value; |
a2a8927a | 23 | use topological_sort::TopologicalSort; |
cc61c64b | 24 | |
dc9dc135 XL |
25 | use crate::errors::*; |
26 | use crate::preprocess::{ | |
9fa01778 XL |
27 | CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext, |
28 | }; | |
e74abb32 | 29 | use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer}; |
dc9dc135 | 30 | use crate::utils; |
cc61c64b | 31 | |
f035d41b | 32 | use crate::config::{Config, RustEdition}; |
cc61c64b | 33 | |
2c00a5a8 | 34 | /// The object used to manage and build a book. |
cc61c64b | 35 | pub 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 | ||
48 | impl 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 | 366 | fn 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 | 389 | const DEFAULT_PREPROCESSORS: &[&str] = &["links", "index"]; |
9fa01778 | 390 | |
dc9dc135 | 391 | fn 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 | 397 | fn 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 |
513 | fn 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 | ||
521 | fn 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 |
540 | fn 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)] |
564 | mod 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 | } |