Merge pull request #532 from JaimeValdemoros/implementing-preprocessors

implementing preprocessors
This commit is contained in:
Michael Bryan 2018-01-18 07:18:54 +08:00 committed by GitHub
commit 7b4b70a49d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 224 additions and 23 deletions

View File

@ -23,7 +23,7 @@ use toml::Value;
use utils; use utils;
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer}; use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
use preprocess; use preprocess::{Preprocessor, LinkPreprocessor, PreprocessorContext};
use errors::*; use errors::*;
use config::Config; use config::Config;
@ -40,6 +40,9 @@ pub struct MDBook {
/// The URL used for live reloading when serving up the book. /// The URL used for live reloading when serving up the book.
pub livereload: Option<String>, pub livereload: Option<String>,
/// List of pre-processors to be run on the book
preprocessors: Vec<Box<Preprocessor>>
} }
impl MDBook { impl MDBook {
@ -85,6 +88,7 @@ impl MDBook {
let livereload = None; let livereload = None;
let renderers = determine_renderers(&config); let renderers = determine_renderers(&config);
let preprocessors = determine_preprocessors(&config)?;
Ok(MDBook { Ok(MDBook {
root, root,
@ -92,6 +96,7 @@ impl MDBook {
book, book,
renderers, renderers,
livereload, livereload,
preprocessors,
}) })
} }
@ -151,14 +156,22 @@ impl MDBook {
pub fn build(&self) -> Result<()> { pub fn build(&self) -> Result<()> {
debug!("[fn]: build"); debug!("[fn]: build");
let mut preprocessed_book = self.book.clone();
let preprocess_ctx = PreprocessorContext::new(self.root.clone(), self.config.clone());
for preprocessor in &self.preprocessors {
debug!("Running the {} preprocessor.", preprocessor.name());
preprocessor.run(&preprocess_ctx, &mut preprocessed_book)?;
}
for renderer in &self.renderers { for renderer in &self.renderers {
self.run_renderer(renderer.as_ref())?; self.run_renderer(&preprocessed_book, renderer.as_ref())?;
} }
Ok(()) Ok(())
} }
fn run_renderer(&self, renderer: &Renderer) -> Result<()> { fn run_renderer(&self, preprocessed_book: &Book, renderer: &Renderer) -> Result<()> {
let name = renderer.name(); let name = renderer.name();
let build_dir = self.build_dir_for(name); let build_dir = self.build_dir_for(name);
if build_dir.exists() { if build_dir.exists() {
@ -174,7 +187,7 @@ impl MDBook {
let render_context = RenderContext::new( let render_context = RenderContext::new(
self.root.clone(), self.root.clone(),
self.book.clone(), preprocessed_book.clone(),
self.config.clone(), self.config.clone(),
build_dir, build_dir,
); );
@ -185,13 +198,19 @@ impl MDBook {
} }
/// You can change the default renderer to another one by using this method. /// You can change the default renderer to another one by using this method.
/// The only requirement is for your renderer to implement the [Renderer /// The only requirement is for your renderer to implement the [`Renderer`
/// trait](../../renderer/renderer/trait.Renderer.html) /// trait](../renderer/trait.Renderer.html)
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self { pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
self.renderers.push(Box::new(renderer)); self.renderers.push(Box::new(renderer));
self self
} }
/// Register a [`Preprocessor`](../preprocess/trait.Preprocessor.html) to be used when rendering the book.
pub fn with_preprecessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
self.preprocessors.push(Box::new(preprocessor));
self
}
/// Run `rustdoc` tests on the book, linking against the provided libraries. /// Run `rustdoc` tests on the book, linking against the provided libraries.
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> { pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
let library_args: Vec<&str> = (0..library_paths.len()) let library_args: Vec<&str> = (0..library_paths.len())
@ -202,15 +221,15 @@ impl MDBook {
let temp_dir = TempDir::new("mdbook")?; let temp_dir = TempDir::new("mdbook")?;
let preprocess_context = PreprocessorContext::new(self.root.clone(), self.config.clone());
LinkPreprocessor::new().run(&preprocess_context, &mut self.book)?;
for item in self.iter() { for item in self.iter() {
if let BookItem::Chapter(ref ch) = *item { if let BookItem::Chapter(ref ch) = *item {
if !ch.path.as_os_str().is_empty() { if !ch.path.as_os_str().is_empty() {
let path = self.source_dir().join(&ch.path); let path = self.source_dir().join(&ch.path);
let base = path.parent()
.ok_or_else(|| String::from("Invalid bookitem path!"))?;
let content = utils::fs::file_to_string(&path)?; let content = utils::fs::file_to_string(&path)?;
// Parse and expand links
let content = preprocess::links::replace_all(&content, base)?;
println!("[*]: Testing file: {:?}", path); println!("[*]: Testing file: {:?}", path);
// write preprocessed file to tempdir // write preprocessed file to tempdir
@ -309,6 +328,34 @@ fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
renderers renderers
} }
fn default_preprocessors() -> Vec<Box<Preprocessor>> {
vec![Box::new(LinkPreprocessor::new())]
}
/// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
let preprocess_list = match config.build.preprocess {
Some(ref p) => p,
// If no preprocessor field is set, default to the LinkPreprocessor. This allows you
// to disable the LinkPreprocessor by setting "preprocess" to an empty list.
None => return Ok(default_preprocessors())
};
let mut preprocessors: Vec<Box<Preprocessor>> = Vec::new();
for key in preprocess_list {
match key.as_ref() {
"links" => {
preprocessors.push(Box::new(LinkPreprocessor::new()))
}
_ => bail!("{:?} is not a recognised preprocessor", key),
}
}
Ok(preprocessors)
}
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> { fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
// look for the `command` field, falling back to using the key // look for the `command` field, falling back to using the key
// prepended by "mdbook-" // prepended by "mdbook-"
@ -364,4 +411,65 @@ mod tests {
assert_eq!(got.len(), 1); assert_eq!(got.len(), 1);
assert_eq!(got[0].name(), "random"); assert_eq!(got[0].name(), "random");
} }
#[test]
fn config_defaults_to_link_preprocessor_if_not_set() {
let cfg = Config::default();
// make sure we haven't got anything in the `output` table
assert!(cfg.build.preprocess.is_none());
let got = determine_preprocessors(&cfg);
assert!(got.is_ok());
assert_eq!(got.as_ref().unwrap().len(), 1);
assert_eq!(got.as_ref().unwrap()[0].name(), "links");
}
#[test]
fn config_doesnt_default_if_empty() {
let cfg_str: &'static str = r#"
[book]
title = "Some Book"
[build]
build-dir = "outputs"
create-missing = false
preprocess = []
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure we have something in the `output` table
assert!(cfg.build.preprocess.is_some());
let got = determine_preprocessors(&cfg);
assert!(got.is_ok());
assert!(got.unwrap().is_empty());
}
#[test]
fn config_complains_if_unimplemented_preprocessor() {
let cfg_str: &'static str = r#"
[book]
title = "Some Book"
[build]
build-dir = "outputs"
create-missing = false
preprocess = ["random"]
"#;
let cfg = Config::from_str(cfg_str).unwrap();
// make sure we have something in the `output` table
assert!(cfg.build.preprocess.is_some());
let got = determine_preprocessors(&cfg);
assert!(got.is_err());
}
} }

View File

@ -329,6 +329,9 @@ pub struct BuildConfig {
/// Should non-existent markdown files specified in `SETTINGS.md` be created /// Should non-existent markdown files specified in `SETTINGS.md` be created
/// if they don't exist? /// if they don't exist?
pub create_missing: bool, pub create_missing: bool,
/// Which preprocessors should be applied
pub preprocess: Option<Vec<String>>,
} }
impl Default for BuildConfig { impl Default for BuildConfig {
@ -336,6 +339,7 @@ impl Default for BuildConfig {
BuildConfig { BuildConfig {
build_dir: PathBuf::from("book"), build_dir: PathBuf::from("book"),
create_missing: true, create_missing: true,
preprocess: None,
} }
} }
} }
@ -422,6 +426,7 @@ mod tests {
[build] [build]
build-dir = "outputs" build-dir = "outputs"
create-missing = false create-missing = false
preprocess = ["first_preprocessor", "second_preprocessor"]
[output.html] [output.html]
theme = "./themedir" theme = "./themedir"
@ -449,6 +454,8 @@ mod tests {
let build_should_be = BuildConfig { let build_should_be = BuildConfig {
build_dir: PathBuf::from("outputs"), build_dir: PathBuf::from("outputs"),
create_missing: false, create_missing: false,
preprocess: Some(vec!["first_preprocessor".to_string(),
"second_preprocessor".to_string()]),
}; };
let playpen_should_be = Playpen { let playpen_should_be = Playpen {
editable: true, editable: true,
@ -550,6 +557,7 @@ mod tests {
let build_should_be = BuildConfig { let build_should_be = BuildConfig {
build_dir: PathBuf::from("my-book"), build_dir: PathBuf::from("my-book"),
create_missing: true, create_missing: true,
preprocess: None,
}; };
let html_should_be = HtmlConfig { let html_should_be = HtmlConfig {

View File

@ -118,7 +118,7 @@ extern crate toml_query;
#[macro_use] #[macro_use]
extern crate pretty_assertions; extern crate pretty_assertions;
mod preprocess; pub mod preprocess;
pub mod book; pub mod book;
pub mod config; pub mod config;
pub mod renderer; pub mod renderer;

View File

@ -5,9 +5,45 @@ use utils::fs::file_to_string;
use utils::take_lines; use utils::take_lines;
use errors::*; use errors::*;
use super::{Preprocessor, PreprocessorContext};
use book::{Book, BookItem};
const ESCAPE_CHAR: char = '\\'; const ESCAPE_CHAR: char = '\\';
pub fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> Result<String> { pub struct LinkPreprocessor;
impl LinkPreprocessor {
pub fn new() -> Self {
LinkPreprocessor
}
}
impl Preprocessor for LinkPreprocessor {
fn name(&self) -> &str {
"links"
}
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
let src_dir = ctx.root.join(&ctx.config.book.src);
for section in &mut book.sections {
match *section {
BookItem::Chapter(ref mut ch) => {
let base = ch.path.parent()
.map(|dir| src_dir.join(dir))
.ok_or_else(|| String::from("Invalid bookitem path!"))?;
let content = replace_all(&ch.content, base)?;
ch.content = content
}
_ => {}
}
}
Ok(())
}
}
fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> Result<String> {
// When replacing one thing in a string by something with a different length, // When replacing one thing in a string by something with a different length,
// the indices after that will not correspond, // the indices after that will not correspond,
// we therefore have to store the difference to correct this // we therefore have to store the difference to correct this

View File

@ -1 +1,25 @@
pub mod links; pub use self::links::LinkPreprocessor;
mod links;
use book::Book;
use config::Config;
use errors::*;
use std::path::PathBuf;
pub struct PreprocessorContext {
pub root: PathBuf,
pub config: Config,
}
impl PreprocessorContext {
pub fn new(root: PathBuf, config: Config) -> Self {
PreprocessorContext { root, config }
}
}
pub trait Preprocessor {
fn name(&self) -> &str;
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
}

View File

@ -1,5 +1,4 @@
use renderer::html_handlebars::helpers; use renderer::html_handlebars::helpers;
use preprocess;
use renderer::{RenderContext, Renderer}; use renderer::{RenderContext, Renderer};
use book::{Book, BookItem, Chapter}; use book::{Book, BookItem, Chapter};
use config::{Config, HtmlConfig, Playpen}; use config::{Config, HtmlConfig, Playpen};
@ -50,12 +49,6 @@ impl HtmlHandlebars {
match *item { match *item {
BookItem::Chapter(ref ch) => { BookItem::Chapter(ref ch) => {
let content = ch.content.clone(); let content = ch.content.clone();
let base = ch.path.parent()
.map(|dir| ctx.src_dir.join(dir))
.expect("All chapters must have a parent directory");
// Parse and expand links
let content = preprocess::links::replace_all(&content, base)?;
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes); let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
print_content.push_str(&content); print_content.push_str(&content);
@ -322,7 +315,6 @@ impl Renderer for HtmlHandlebars {
let ctx = RenderItemContext { let ctx = RenderItemContext {
handlebars: &handlebars, handlebars: &handlebars,
destination: destination.to_path_buf(), destination: destination.to_path_buf(),
src_dir: src_dir.clone(),
data: data.clone(), data: data.clone(),
is_index: i == 0, is_index: i == 0,
html_config: html_config.clone(), html_config: html_config.clone(),
@ -634,7 +626,6 @@ fn partition_source(s: &str) -> (String, String) {
struct RenderItemContext<'a> { struct RenderItemContext<'a> {
handlebars: &'a Handlebars, handlebars: &'a Handlebars,
destination: PathBuf, destination: PathBuf,
src_dir: PathBuf,
data: serde_json::Map<String, serde_json::Value>, data: serde_json::Map<String, serde_json::Value>,
is_index: bool, is_index: bool,
html_config: HtmlConfig, html_config: HtmlConfig,

View File

@ -3,8 +3,14 @@ extern crate mdbook;
mod dummy_book; mod dummy_book;
use dummy_book::DummyBook; use dummy_book::DummyBook;
use mdbook::MDBook;
use mdbook::MDBook;
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::book::Book;
use mdbook::config::Config;
use mdbook::errors::*;
use std::sync::{Arc, Mutex};
#[test] #[test]
fn mdbook_can_correctly_test_a_passing_book() { fn mdbook_can_correctly_test_a_passing_book() {
@ -21,3 +27,31 @@ fn mdbook_detects_book_with_failing_tests() {
assert!(md.test(vec![]).is_err()); assert!(md.test(vec![]).is_err());
} }
#[test]
fn mdbook_runs_preprocessors() {
let has_run: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
struct DummyPreprocessor(Arc<Mutex<bool>>);
impl Preprocessor for DummyPreprocessor {
fn name(&self) -> &str {
"dummy"
}
fn run(&self, _ctx: &PreprocessorContext, _book: &mut Book) -> Result<()> {
*self.0.lock().unwrap() = true;
Ok(())
}
}
let temp = DummyBook::new().build().unwrap();
let cfg = Config::default();
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
book.with_preprecessor(DummyPreprocessor(Arc::clone(&has_run)));
book.build().unwrap();
assert!(*has_run.lock().unwrap())
}