Skip to content

Commit 09667c9

Browse files
Configurable preprocessor (#658)
* The preprocessor trait now returns a modified book instead of editing in place * A preprocessor is told which render it's running for * Made sure preprocessors get their renderer's name * Users can now manually specify whether a preprocessor should run for a renderer * You can normally use default preprocessors by default * Got my logic around the wrong way * Fixed the `build.use-default-preprocessors` flag
1 parent 48c97da commit 09667c9

File tree

8 files changed

+322
-145
lines changed

8 files changed

+322
-145
lines changed

examples/de-emphasize.rs

+61-57
Original file line numberDiff line numberDiff line change
@@ -14,81 +14,85 @@ use std::env::{args, args_os};
1414
use std::ffi::OsString;
1515
use std::process;
1616

17+
const NAME: &str = "md-links-to-html-links";
18+
19+
fn do_it(book: OsString) -> Result<()> {
20+
let mut book = MDBook::load(book)?;
21+
book.with_preprecessor(Deemphasize);
22+
book.build()
23+
}
24+
25+
fn main() {
26+
if args_os().count() != 2 {
27+
eprintln!("USAGE: {} <book>", args().next().expect("executable"));
28+
return;
29+
}
30+
if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) {
31+
eprintln!("{}", e);
32+
process::exit(1);
33+
}
34+
}
35+
1736
struct Deemphasize;
1837

1938
impl Preprocessor for Deemphasize {
2039
fn name(&self) -> &str {
21-
"md-links-to-html-links"
40+
NAME
2241
}
2342

24-
fn run(&self, _ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
43+
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
2544
eprintln!("Running '{}' preprocessor", self.name());
26-
let mut res: Option<_> = None;
2745
let mut num_removed_items = 0;
28-
book.for_each_mut(|item: &mut BookItem| {
29-
if let Some(Err(_)) = res {
30-
return;
31-
}
32-
if let BookItem::Chapter(ref mut chapter) = *item {
33-
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
34-
res = Some(
35-
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
36-
Ok(md) => {
37-
chapter.content = md;
38-
Ok(())
39-
}
40-
Err(err) => Err(err),
41-
},
42-
);
43-
}
44-
});
46+
47+
process(&mut book.sections, &mut num_removed_items)?;
48+
4549
eprintln!(
4650
"{}: removed {} events from markdown stream.",
4751
self.name(),
4852
num_removed_items
4953
);
50-
match res {
51-
Some(res) => res,
52-
None => Ok(()),
53-
}
54+
55+
Ok(book)
5456
}
5557
}
5658

57-
fn do_it(book: OsString) -> Result<()> {
58-
let mut book = MDBook::load(book)?;
59-
book.with_preprecessor(Deemphasize);
60-
book.build()
61-
}
59+
fn process<'a, I>(items: I, num_removed_items: &mut usize) -> Result<()>
60+
where
61+
I: IntoIterator<Item = &'a mut BookItem> + 'a,
62+
{
63+
for item in items {
64+
if let BookItem::Chapter(ref mut chapter) = *item {
65+
eprintln!("{}: processing chapter '{}'", NAME, chapter.name);
6266

63-
fn main() {
64-
if args_os().count() != 2 {
65-
eprintln!("USAGE: {} <book>", args().next().expect("executable"));
66-
return;
67-
}
68-
if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) {
69-
eprintln!("{}", e);
70-
process::exit(1);
67+
let md = remove_emphasis(num_removed_items, chapter)?;
68+
chapter.content = md;
69+
}
7170
}
71+
72+
Ok(())
7273
}
7374

74-
impl Deemphasize {
75-
fn remove_emphasis(num_removed_items: &mut i32, chapter: &mut Chapter) -> Result<String> {
76-
let mut buf = String::with_capacity(chapter.content.len());
77-
let events = Parser::new(&chapter.content).filter(|e| {
78-
let should_keep = match *e {
79-
Event::Start(Tag::Emphasis)
80-
| Event::Start(Tag::Strong)
81-
| Event::End(Tag::Emphasis)
82-
| Event::End(Tag::Strong) => false,
83-
_ => true,
84-
};
85-
if !should_keep {
86-
*num_removed_items += 1;
87-
}
88-
should_keep
89-
});
90-
cmark(events, &mut buf, None)
91-
.map(|_| buf)
92-
.map_err(|err| Error::from(format!("Markdown serialization failed: {}", err)))
93-
}
75+
fn remove_emphasis(
76+
num_removed_items: &mut usize,
77+
chapter: &mut Chapter,
78+
) -> Result<String> {
79+
let mut buf = String::with_capacity(chapter.content.len());
80+
81+
let events = Parser::new(&chapter.content).filter(|e| {
82+
let should_keep = match *e {
83+
Event::Start(Tag::Emphasis)
84+
| Event::Start(Tag::Strong)
85+
| Event::End(Tag::Emphasis)
86+
| Event::End(Tag::Strong) => false,
87+
_ => true,
88+
};
89+
if !should_keep {
90+
*num_removed_items += 1;
91+
}
92+
should_keep
93+
});
94+
95+
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
96+
Error::from(format!("Markdown serialization failed: {}", err))
97+
})
9498
}

src/book/mod.rs

+135-37
Original file line numberDiff line numberDiff line change
@@ -149,23 +149,39 @@ impl MDBook {
149149
pub fn build(&self) -> Result<()> {
150150
info!("Book building has started");
151151

152+
for renderer in &self.renderers {
153+
self.execute_build_process(&**renderer)?;
154+
}
155+
156+
Ok(())
157+
}
158+
159+
/// Run the entire build process for a particular `Renderer`.
160+
fn execute_build_process(&self, renderer: &Renderer) -> Result<()> {
152161
let mut preprocessed_book = self.book.clone();
153-
let preprocess_ctx = PreprocessorContext::new(self.root.clone(), self.config.clone());
162+
let preprocess_ctx = PreprocessorContext::new(self.root.clone(),
163+
self.config.clone(),
164+
renderer.name().to_string());
154165

155166
for preprocessor in &self.preprocessors {
156-
debug!("Running the {} preprocessor.", preprocessor.name());
157-
preprocessor.run(&preprocess_ctx, &mut preprocessed_book)?;
167+
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
168+
debug!("Running the {} preprocessor.", preprocessor.name());
169+
preprocessed_book =
170+
preprocessor.run(&preprocess_ctx, preprocessed_book)?;
171+
}
158172
}
159173

160-
for renderer in &self.renderers {
161-
info!("Running the {} backend", renderer.name());
162-
self.run_renderer(&preprocessed_book, renderer.as_ref())?;
163-
}
174+
info!("Running the {} backend", renderer.name());
175+
self.render(&preprocessed_book, renderer)?;
164176

165177
Ok(())
166178
}
167179

168-
fn run_renderer(&self, preprocessed_book: &Book, renderer: &Renderer) -> Result<()> {
180+
fn render(
181+
&self,
182+
preprocessed_book: &Book,
183+
renderer: &Renderer,
184+
) -> Result<()> {
169185
let name = renderer.name();
170186
let build_dir = self.build_dir_for(name);
171187
if build_dir.exists() {
@@ -215,13 +231,16 @@ impl MDBook {
215231

216232
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
217233

218-
let preprocess_context = PreprocessorContext::new(self.root.clone(), self.config.clone());
234+
// FIXME: Is "test" the proper renderer name to use here?
235+
let preprocess_context = PreprocessorContext::new(self.root.clone(),
236+
self.config.clone(),
237+
"test".to_string());
219238

220-
LinkPreprocessor::new().run(&preprocess_context, &mut self.book)?;
239+
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
221240
// Index Preprocessor is disabled so that chapter paths continue to point to the
222241
// actual markdown files.
223242

224-
for item in self.iter() {
243+
for item in book.iter() {
225244
if let BookItem::Chapter(ref ch) = *item {
226245
if !ch.path.as_os_str().is_empty() {
227246
let path = self.source_dir().join(&ch.path);
@@ -330,19 +349,32 @@ fn default_preprocessors() -> Vec<Box<Preprocessor>> {
330349
]
331350
}
332351

352+
fn is_default_preprocessor(pre: &Preprocessor) -> bool {
353+
let name = pre.name();
354+
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
355+
}
356+
333357
/// Look at the `MDBook` and try to figure out what preprocessors to run.
334358
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
335-
let preprocess_list = match config.build.preprocess {
336-
Some(ref p) => p,
359+
let preprocessor_keys = config.get("preprocessor")
360+
.and_then(|value| value.as_table())
361+
.map(|table| table.keys());
362+
363+
let mut preprocessors = if config.build.use_default_preprocessors {
364+
default_preprocessors()
365+
} else {
366+
Vec::new()
367+
};
368+
369+
let preprocessor_keys = match preprocessor_keys {
370+
Some(keys) => keys,
337371
// If no preprocessor field is set, default to the LinkPreprocessor and
338372
// IndexPreprocessor. This allows you to disable default preprocessors
339373
// by setting "preprocess" to an empty list.
340-
None => return Ok(default_preprocessors()),
374+
None => return Ok(preprocessors),
341375
};
342376

343-
let mut preprocessors: Vec<Box<Preprocessor>> = Vec::new();
344-
345-
for key in preprocess_list {
377+
for key in preprocessor_keys {
346378
match key.as_ref() {
347379
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
348380
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
@@ -366,6 +398,31 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
366398
Box::new(CmdRenderer::new(key.to_string(), command.to_string()))
367399
}
368400

401+
/// Check whether we should run a particular `Preprocessor` in combination
402+
/// with the renderer, falling back to `Preprocessor::supports_renderer()`
403+
/// method if the user doesn't say anything.
404+
///
405+
/// The `build.use-default-preprocessors` config option can be used to ensure
406+
/// default preprocessors always run if they support the renderer.
407+
fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg: &Config) -> bool {
408+
// default preprocessors should be run by default (if supported)
409+
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
410+
return preprocessor.supports_renderer(renderer.name());
411+
}
412+
413+
let key = format!("preprocessor.{}.renderers", preprocessor.name());
414+
let renderer_name = renderer.name();
415+
416+
if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
417+
return explicit_renderers.into_iter()
418+
.filter_map(|val| val.as_str())
419+
.any(|name| name == renderer_name);
420+
}
421+
422+
preprocessor.supports_renderer(renderer_name)
423+
}
424+
425+
369426
#[cfg(test)]
370427
mod tests {
371428
use super::*;
@@ -413,8 +470,8 @@ mod tests {
413470
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
414471
let cfg = Config::default();
415472

416-
// make sure we haven't got anything in the `output` table
417-
assert!(cfg.build.preprocess.is_none());
473+
// make sure we haven't got anything in the `preprocessor` table
474+
assert!(cfg.get("preprocessor").is_none());
418475

419476
let got = determine_preprocessors(&cfg);
420477

@@ -425,47 +482,88 @@ mod tests {
425482
}
426483

427484
#[test]
428-
fn config_doesnt_default_if_empty() {
485+
fn use_default_preprocessors_works() {
486+
let mut cfg = Config::default();
487+
cfg.build.use_default_preprocessors = false;
488+
489+
let got = determine_preprocessors(&cfg).unwrap();
490+
491+
assert_eq!(got.len(), 0);
492+
}
493+
494+
#[test]
495+
fn config_complains_if_unimplemented_preprocessor() {
429496
let cfg_str: &'static str = r#"
430497
[book]
431498
title = "Some Book"
432499
500+
[preprocessor.random]
501+
433502
[build]
434503
build-dir = "outputs"
435504
create-missing = false
436-
preprocess = []
437505
"#;
438506

439507
let cfg = Config::from_str(cfg_str).unwrap();
440508

441-
// make sure we have something in the `output` table
442-
assert!(cfg.build.preprocess.is_some());
509+
// make sure the `preprocessor.random` table exists
510+
assert!(cfg.get_preprocessor("random").is_some());
443511

444512
let got = determine_preprocessors(&cfg);
445513

446-
assert!(got.is_ok());
447-
assert!(got.unwrap().is_empty());
514+
assert!(got.is_err());
448515
}
449516

450517
#[test]
451-
fn config_complains_if_unimplemented_preprocessor() {
518+
fn config_respects_preprocessor_selection() {
452519
let cfg_str: &'static str = r#"
453-
[book]
454-
title = "Some Book"
455-
456-
[build]
457-
build-dir = "outputs"
458-
create-missing = false
459-
preprocess = ["random"]
520+
[preprocessor.links]
521+
renderers = ["html"]
460522
"#;
461523

462524
let cfg = Config::from_str(cfg_str).unwrap();
463525

464-
// make sure we have something in the `output` table
465-
assert!(cfg.build.preprocess.is_some());
526+
// double-check that we can access preprocessor.links.renderers[0]
527+
let html = cfg.get_preprocessor("links")
528+
.and_then(|links| links.get("renderers"))
529+
.and_then(|renderers| renderers.as_array())
530+
.and_then(|renderers| renderers.get(0))
531+
.and_then(|renderer| renderer.as_str())
532+
.unwrap();
533+
assert_eq!(html, "html");
534+
let html_renderer = HtmlHandlebars::default();
535+
let pre = LinkPreprocessor::new();
536+
537+
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
538+
assert!(should_run);
539+
}
466540

467-
let got = determine_preprocessors(&cfg);
541+
struct BoolPreprocessor(bool);
542+
impl Preprocessor for BoolPreprocessor {
543+
fn name(&self) -> &str {
544+
"bool-preprocessor"
545+
}
468546

469-
assert!(got.is_err());
547+
fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
548+
unimplemented!()
549+
}
550+
551+
fn supports_renderer(&self, _renderer: &str) -> bool {
552+
self.0
553+
}
554+
}
555+
556+
#[test]
557+
fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
558+
let cfg = Config::default();
559+
let html = HtmlHandlebars::new();
560+
561+
let should_be = true;
562+
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
563+
assert_eq!(got, should_be);
564+
565+
let should_be = false;
566+
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
567+
assert_eq!(got, should_be);
470568
}
471569
}

0 commit comments

Comments
 (0)