Merge pull request #532 from JaimeValdemoros/implementing-preprocessors
implementing preprocessors
This commit is contained in:
commit
7b4b70a49d
128
src/book/mod.rs
128
src/book/mod.rs
|
@ -23,7 +23,7 @@ use toml::Value;
|
|||
|
||||
use utils;
|
||||
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
||||
use preprocess;
|
||||
use preprocess::{Preprocessor, LinkPreprocessor, PreprocessorContext};
|
||||
use errors::*;
|
||||
|
||||
use config::Config;
|
||||
|
@ -40,6 +40,9 @@ pub struct MDBook {
|
|||
|
||||
/// The URL used for live reloading when serving up the book.
|
||||
pub livereload: Option<String>,
|
||||
|
||||
/// List of pre-processors to be run on the book
|
||||
preprocessors: Vec<Box<Preprocessor>>
|
||||
}
|
||||
|
||||
impl MDBook {
|
||||
|
@ -85,6 +88,7 @@ impl MDBook {
|
|||
let livereload = None;
|
||||
|
||||
let renderers = determine_renderers(&config);
|
||||
let preprocessors = determine_preprocessors(&config)?;
|
||||
|
||||
Ok(MDBook {
|
||||
root,
|
||||
|
@ -92,6 +96,7 @@ impl MDBook {
|
|||
book,
|
||||
renderers,
|
||||
livereload,
|
||||
preprocessors,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -151,14 +156,22 @@ impl MDBook {
|
|||
pub fn build(&self) -> Result<()> {
|
||||
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 {
|
||||
self.run_renderer(renderer.as_ref())?;
|
||||
self.run_renderer(&preprocessed_book, renderer.as_ref())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_renderer(&self, renderer: &Renderer) -> Result<()> {
|
||||
fn run_renderer(&self, preprocessed_book: &Book, renderer: &Renderer) -> Result<()> {
|
||||
let name = renderer.name();
|
||||
let build_dir = self.build_dir_for(name);
|
||||
if build_dir.exists() {
|
||||
|
@ -174,7 +187,7 @@ impl MDBook {
|
|||
|
||||
let render_context = RenderContext::new(
|
||||
self.root.clone(),
|
||||
self.book.clone(),
|
||||
preprocessed_book.clone(),
|
||||
self.config.clone(),
|
||||
build_dir,
|
||||
);
|
||||
|
@ -185,13 +198,19 @@ impl MDBook {
|
|||
}
|
||||
|
||||
/// You can change the default renderer to another one by using this method.
|
||||
/// The only requirement is for your renderer to implement the [Renderer
|
||||
/// trait](../../renderer/renderer/trait.Renderer.html)
|
||||
/// The only requirement is for your renderer to implement the [`Renderer`
|
||||
/// trait](../renderer/trait.Renderer.html)
|
||||
pub fn with_renderer<R: Renderer + 'static>(&mut self, renderer: R) -> &mut Self {
|
||||
self.renderers.push(Box::new(renderer));
|
||||
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.
|
||||
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
|
||||
let library_args: Vec<&str> = (0..library_paths.len())
|
||||
|
@ -202,15 +221,15 @@ impl 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() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
if !ch.path.as_os_str().is_empty() {
|
||||
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)?;
|
||||
// Parse and expand links
|
||||
let content = preprocess::links::replace_all(&content, base)?;
|
||||
println!("[*]: Testing file: {:?}", path);
|
||||
|
||||
// write preprocessed file to tempdir
|
||||
|
@ -309,6 +328,34 @@ fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
|
|||
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> {
|
||||
// look for the `command` field, falling back to using the key
|
||||
// prepended by "mdbook-"
|
||||
|
@ -364,4 +411,65 @@ mod tests {
|
|||
assert_eq!(got.len(), 1);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -329,6 +329,9 @@ pub struct BuildConfig {
|
|||
/// Should non-existent markdown files specified in `SETTINGS.md` be created
|
||||
/// if they don't exist?
|
||||
pub create_missing: bool,
|
||||
/// Which preprocessors should be applied
|
||||
pub preprocess: Option<Vec<String>>,
|
||||
|
||||
}
|
||||
|
||||
impl Default for BuildConfig {
|
||||
|
@ -336,6 +339,7 @@ impl Default for BuildConfig {
|
|||
BuildConfig {
|
||||
build_dir: PathBuf::from("book"),
|
||||
create_missing: true,
|
||||
preprocess: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -422,6 +426,7 @@ mod tests {
|
|||
[build]
|
||||
build-dir = "outputs"
|
||||
create-missing = false
|
||||
preprocess = ["first_preprocessor", "second_preprocessor"]
|
||||
|
||||
[output.html]
|
||||
theme = "./themedir"
|
||||
|
@ -449,6 +454,8 @@ mod tests {
|
|||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("outputs"),
|
||||
create_missing: false,
|
||||
preprocess: Some(vec!["first_preprocessor".to_string(),
|
||||
"second_preprocessor".to_string()]),
|
||||
};
|
||||
let playpen_should_be = Playpen {
|
||||
editable: true,
|
||||
|
@ -550,6 +557,7 @@ mod tests {
|
|||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("my-book"),
|
||||
create_missing: true,
|
||||
preprocess: None,
|
||||
};
|
||||
|
||||
let html_should_be = HtmlConfig {
|
||||
|
|
|
@ -118,7 +118,7 @@ extern crate toml_query;
|
|||
#[macro_use]
|
||||
extern crate pretty_assertions;
|
||||
|
||||
mod preprocess;
|
||||
pub mod preprocess;
|
||||
pub mod book;
|
||||
pub mod config;
|
||||
pub mod renderer;
|
||||
|
|
|
@ -5,9 +5,45 @@ use utils::fs::file_to_string;
|
|||
use utils::take_lines;
|
||||
use errors::*;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use book::{Book, BookItem};
|
||||
|
||||
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,
|
||||
// the indices after that will not correspond,
|
||||
// we therefore have to store the difference to correct this
|
||||
|
|
|
@ -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<()>;
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
use renderer::html_handlebars::helpers;
|
||||
use preprocess;
|
||||
use renderer::{RenderContext, Renderer};
|
||||
use book::{Book, BookItem, Chapter};
|
||||
use config::{Config, HtmlConfig, Playpen};
|
||||
|
@ -50,12 +49,6 @@ impl HtmlHandlebars {
|
|||
match *item {
|
||||
BookItem::Chapter(ref ch) => {
|
||||
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);
|
||||
print_content.push_str(&content);
|
||||
|
||||
|
@ -322,7 +315,6 @@ impl Renderer for HtmlHandlebars {
|
|||
let ctx = RenderItemContext {
|
||||
handlebars: &handlebars,
|
||||
destination: destination.to_path_buf(),
|
||||
src_dir: src_dir.clone(),
|
||||
data: data.clone(),
|
||||
is_index: i == 0,
|
||||
html_config: html_config.clone(),
|
||||
|
@ -634,7 +626,6 @@ fn partition_source(s: &str) -> (String, String) {
|
|||
struct RenderItemContext<'a> {
|
||||
handlebars: &'a Handlebars,
|
||||
destination: PathBuf,
|
||||
src_dir: PathBuf,
|
||||
data: serde_json::Map<String, serde_json::Value>,
|
||||
is_index: bool,
|
||||
html_config: HtmlConfig,
|
||||
|
|
|
@ -3,8 +3,14 @@ extern crate mdbook;
|
|||
mod dummy_book;
|
||||
|
||||
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]
|
||||
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());
|
||||
}
|
||||
|
||||
#[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())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue