Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implementing preprocessors #532

Merged

Conversation

JaimeValdemoros
Copy link
Contributor

No description provided.

for section in &mut book.sections {
match *section {
BookItem::Chapter(ref mut ch) => {
let content = ::std::mem::replace(&mut ch.content, String::new());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need to use mem::replace() here. You could store the replaced text in a temporary (let replaced = replace_all(&ch.content, base)) then update ch.content on the next line.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, thanks

@Michael-F-Bryan
Copy link
Contributor

@JaimeValdemoros I just had another skim through what you've done so far and it looks pretty good 👍

You may want to add a couple tests for the preprocessor discovery, because I feel like there are a couple edge cases we haven't accounted for. For example, what happens if the user passes in an unknown preprocessor, if you specify a preprocessor twice, or if one of the elements in the preprocessor array is actually a table?

The easiest way to write the test is probably to create a bunch of dummy configs (just raw strings), run Config::from_str() to load it, then run determine_preprocessors() and make sure we got the preprocessors we expected. You may also want to add a name() method to the Preprocessor trait so we can log something like "running the \"{}\" preprocessor", and it also helps figure out which preprocessors were found while testing.

@Michael-F-Bryan
Copy link
Contributor

Hi @JaimeValdemoros, this is just a friendly ping to see how you're going with everything 😀 How far through this issue would you say you are? And is there anything I can do to help?

@JaimeValdemoros
Copy link
Contributor Author

JaimeValdemoros commented Jan 14, 2018 via email

@JaimeValdemoros
Copy link
Contributor Author

@Michael-F-Bryan I've made a couple of small changes that felt a bit more sensible:

  • Moved the "preprocess" field under the "build" header rather than standalone (as an optional field)
  • Report an error if the preprocessor isn't recognised
  • Allow explicitly disabling the default option by setting an empty list
  • Added a 'name' field to the Preprocessor trait, matching the Renderer trait
  • Added some tests

Is there anything you think is still missing?

Copy link
Contributor

@Michael-F-Bryan Michael-F-Bryan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I'm really happy with this PR, it looks clean and well thought-out and easily testable 👍

I've made a couple comments/questions, but they're mainly to throw around ideas or ask your opinion.

We may also want to add an integration test which makes sure the preprocessors actually get called.

This could be as simple as creating a dummy Preprocessor which contains a Rc<AtomicBool> and will toggle the bool when it gets called. The test then does a build of the dummy book and asserts the bool is now true.

src/book/mod.rs Outdated
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(vec![Box::new(LinkPreprocessor::new())])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reckon we may want to pull this bit out into its own default_preprocessors() function, where we immediately return vec![Box::new(LinkPreprocessor::new())]. I imagine that'll make things easier to update in the future, and it's immediately obvious what the default preprocessors are.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - you're right, makes it a bit cleaner

src/book/mod.rs Outdated
_ => {}
}
for key in preprocess_list {
if key == "links" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on turning this if key == "links" {...} else { bail!() } into a match statement? Now that preprocessors are easier to work with, I imagine we'll be adding a bunch more in the future.

@@ -149,14 +154,23 @@ impl MDBook {
pub fn build(&self) -> Result<()> {
debug!("[fn]: build");

let mut preprocessed_book = self.book.clone();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like that we're not mutating the original book here and instead preprocessing its clone 👍

src/book/mod.rs Outdated
@@ -190,6 +204,13 @@ impl MDBook {
self
}

/// You can add a new preprocessor by using this method.
/// The only requirement is for your renderer to implement the Preprocessor trait.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doc-comment should probably be reworded in the "imperative" tense to describe what it does. So something like "Register a [Preprocessor] to be used when rendering the book", with the word "Preprocessor" being a link to the relevant trait so people can see the required methods and what types already implement Preprocessor.


pub trait Preprocessor {
fn name(&self) -> &str;
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm tossing up whether to include the Book as part of the PreprocessorContext. Do you think the book to be processed should be part of the context or should we keep it as a second argument?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's helpful to distinguish the fact that you only expect the Book itself to be mutated, and that the PreprocessorContext is additional information it can use to help in that mutation. Having a single parameter &mut PreprocessorContext obscures that slightly and has fewer guarantees about what the Preprocessor is allowed to do to the context.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah that's a good point. I completely forgot the Book is &mut while we pass an immutable reference for the PreprocessorContext.

use std::path::PathBuf;

pub struct PreprocessorContext {
pub src_dir: PathBuf
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably better for PreprocessorContext to contain a reference (or clone, if you don't want to deal with lifetimes) to the Config and the book's root. That way a preprocessor can hook into book.toml for configuration, and the src_dir can be discovered by combining config.book.src and the book root.

So something like this:

pub struct PreprocessorContext {
  pub config: Config,
  pub root: PathBuf,
  // maybe also include the `Book` here... I dunno
}

Copy link
Contributor Author

@JaimeValdemoros JaimeValdemoros Jan 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, given the other fields of the MDBook shouldn't be needed by the preprocessor. I've made it a clone for now since a borrow would be problematic here:

https://github.com/rust-lang-nursery/mdBook/blob/bf093e2f5f0e8c7f455b6e021cb72adb974836d4/src/book/mod.rs#L205

since we'd have a reference to an inner field of self in the Context and additionally trying to take a reference to itself to pass into iter.

@Michael-F-Bryan
Copy link
Contributor

@JaimeValdemoros, I've made a temporary fix (#549) to CI so travis won't fail and erroneously say that this PR is broken. If you want, you can git rebase on top of the latest master to pick up the fix and make CI work normally for this PR.

@JaimeValdemoros
Copy link
Contributor Author

Thanks @Michael-F-Bryan - this is the first time I've rebased onto a fork so hopefully I've done it right

tests/testing.rs Outdated
fn mdbook_runs_preprocessors() {

lazy_static! {
static ref HAS_RUN: Mutex<bool> = Mutex::new(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to use lazy_static!() here, an Arc<Mutex<bool>> should be enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, I was trying to have a dummy option in the Config and have the book add the DummyPreprocessor to the list when it found the dummy option, which did require the lazy_static since the test wasn't constructing it directly. I should've rethought the design when I switched to using with_preprocessor and building the DummyPreprocessor directly.

@Michael-F-Bryan
Copy link
Contributor

this is the first time I've rebased onto a fork so hopefully I've done it right

Looks like everything's okay 👍

@JaimeValdemoros
Copy link
Contributor Author

Just to list what I've done since the last checklist:

  • Factor out explicit default_preprocessors method
  • Match on preprocessor options instead of nested if's
  • Test to make sure preprocessors are being run
  • Use rustdoc link to Preprocessor trait (this worked on my machine but the Renderer link didn't work as it stood on my machine so probably needs checking to see if my machine does something odd)
  • Log to debug as preprocessors are being run

@Michael-F-Bryan
Copy link
Contributor

I just had another look through the code and I'm happy with it. I think I'll merge this now and then we can do a follow-up PR to document how preprocessors work in the user guide.

Awesome work @JaimeValdemoros, I'm really happy with this!

@Michael-F-Bryan Michael-F-Bryan changed the title WIP implementing preprocessors implementing preprocessors Jan 17, 2018
@Michael-F-Bryan Michael-F-Bryan merged commit 7b4b70a into rust-lang:master Jan 17, 2018
@sorin-davidoi sorin-davidoi mentioned this pull request Jan 19, 2018
3 tasks
Michael-F-Bryan added a commit to Michael-F-Bryan/mdBook that referenced this pull request Jan 21, 2018
Michael-F-Bryan added a commit that referenced this pull request Jan 21, 2018
…564)

* Looks like we forgot to recursively apply replace_all() in #532

* Removed some print statements

* Made sure we ignore the rendered dummy_book
Ruin0x11 pushed a commit to Ruin0x11/mdBook that referenced this pull request Aug 30, 2020
Ruin0x11 pushed a commit to Ruin0x11/mdBook that referenced this pull request Aug 30, 2020
…ust-lang#564)

* Looks like we forgot to recursively apply replace_all() in rust-lang#532

* Removed some print statements

* Made sure we ignore the rendered dummy_book
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants