mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 12:41:16 -05:00
Compare commits
22 Commits
v0.2.1
...
update-rea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cffc385b0c | ||
|
|
e73928f933 | ||
|
|
29f8b791f1 | ||
|
|
877bf37d18 | ||
|
|
b1c7c54108 | ||
|
|
0c926b3e88 | ||
|
|
e4eddb3f26 | ||
|
|
adec78e7f5 | ||
|
|
5cd5e4764c | ||
|
|
132f4fd358 | ||
|
|
1d72cea972 | ||
|
|
1aa1194d79 | ||
|
|
304234c122 | ||
|
|
729c94a7e4 | ||
|
|
df874cdbdb | ||
|
|
5dce539928 | ||
|
|
206a00915b | ||
|
|
ced74ca4dd | ||
|
|
09667c9956 | ||
|
|
48c97dadd0 | ||
|
|
65198a7632 | ||
|
|
a0e7b19784 |
@@ -48,6 +48,57 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
|
||||
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
|
||||
|
||||
### Code Quality
|
||||
|
||||
We love code quality and Rust has some excellent tools to assist you with contributions.
|
||||
|
||||
#### Formatting Code with rustfmt
|
||||
|
||||
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
|
||||
This will ensure we have good quality source code that is better for us all to maintain.
|
||||
|
||||
[rustfmt](https://github.com/rust-lang-nursery/rustfmt) has a lot more information on the project.
|
||||
The quick guide is
|
||||
|
||||
1. Install it
|
||||
```
|
||||
rustup component add rustfmt-preview
|
||||
```
|
||||
1. You can now run `rustfmt` on a single file simply by...
|
||||
```
|
||||
rustfmt src/path/to/your/file.rs
|
||||
```
|
||||
... or you can format the entire project with
|
||||
```
|
||||
cargo fmt
|
||||
```
|
||||
When run through `cargo` it will format all bin and lib files in the current crate.
|
||||
|
||||
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang-nursery/rustfmt)
|
||||
|
||||
|
||||
#### Finding Issues with Clippy
|
||||
|
||||
Clippy is a code analyser/linter detecting mistakes, and therfore helps to improve your code.
|
||||
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will
|
||||
help us maintain awesome code.
|
||||
|
||||
The best documentation can be found over at [rust-clippy](https://github.com/rust-lang-nursery/rust-clippy)
|
||||
|
||||
1. To install
|
||||
```
|
||||
rustup update
|
||||
rustup install nightly
|
||||
rustup component add clippy-preview --toolchain=nightly
|
||||
```
|
||||
2. Running clippy
|
||||
As you may notice from the previous step, Clippy is on the nightly branch, so running it is like
|
||||
```
|
||||
cargo +nightly clippy
|
||||
```
|
||||
|
||||
Clippy has an ever growing list of checks, that are managed in [lint files](https://rust-lang-nursery.github.io/rust-clippy/master/index.html).
|
||||
|
||||
### Making a pull-request
|
||||
|
||||
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -507,7 +507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "mdbook"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2-alpha.0"
|
||||
dependencies = [
|
||||
"ammonia 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2-alpha.0"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
|
||||
53
README.md
53
README.md
@@ -145,6 +145,57 @@ explanation, check out the [User Guide].
|
||||
|
||||
Delete directory in which generated book is located.
|
||||
|
||||
### 3rd Party Plugins
|
||||
|
||||
The way a book is loaded and rendered can be configured by the user via third
|
||||
party plugins. These plugins are just programs which will be invoked during the
|
||||
build process and are split into roughly two categories, *preprocessors* and
|
||||
*renderers*.
|
||||
|
||||
Preprocessors are used to transform a book before it is sent to a renderer.
|
||||
One example would be to replace all occurrences of
|
||||
`{{#include some_file.ext}}` with the contents of that file. Some existing
|
||||
preprocessors are:
|
||||
|
||||
- `index` - a built-in preprocessor (enabled by default) which will transform
|
||||
all `README.md` chapters to `index.md` so `foo/README.md` can be accessed via
|
||||
the url `foo/` when published to a browser
|
||||
- `links` - a built-in preprocessor (enabled by default) for expanding the
|
||||
`{{# playpen}}` and `{{# include}}` helpers in a chapter.
|
||||
|
||||
Renderers are given the final book so they can do something with it. This is
|
||||
typically used for, as the name suggests, rendering the document in a particular
|
||||
format, however there's nothing stopping a renderer from doing static analysis
|
||||
of a book in order to validate links or run tests. Some existing renderers are:
|
||||
|
||||
- `html` - the built-in renderer which will generate a HTML version of the book
|
||||
- [`linkcheck`] - a backend which will check that all links are valid
|
||||
- [`epub`] - an experimental EPUB generator
|
||||
|
||||
> **Note for Developers:** Feel free to send us a PR if you've developed your
|
||||
> own plugin and want it mentioned here.
|
||||
|
||||
A preprocessor or renderer is enabled by installing the appropriate program and
|
||||
then mentioning it in the book's `book.toml` file.
|
||||
|
||||
```console
|
||||
$ cargo install mdbook-linkcheck
|
||||
$ edit book.toml && cat book.toml
|
||||
[book]
|
||||
title = "My Awesome Book"
|
||||
authors = ["Michael-F-Bryan"]
|
||||
|
||||
[output.html]
|
||||
|
||||
[output.linkcheck] # enable the "mdbook-linkcheck" renderer
|
||||
|
||||
$ mdbook build
|
||||
2018-10-20 13:57:51 [INFO] (mdbook::book): Book building has started
|
||||
2018-10-20 13:57:51 [INFO] (mdbook::book): Running the html backend
|
||||
2018-10-20 13:57:53 [INFO] (mdbook::book): Running the linkcheck backend
|
||||
```
|
||||
|
||||
For more information on the plugin system, consult the [User Guide].
|
||||
|
||||
### As a library
|
||||
|
||||
@@ -189,3 +240,5 @@ All the code in this repository is released under the ***Mozilla Public License
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[CLI docs]: http://rust-lang-nursery.github.io/mdBook/cli/init.html
|
||||
[master-docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
|
||||
[`linkcheck`]: https://crates.io/crates/mdbook-linkcheck
|
||||
[`epub`]: https://crates.io/crates/mdbook-epub
|
||||
@@ -26,7 +26,7 @@ before_script:
|
||||
- cargo install-update -a
|
||||
|
||||
script:
|
||||
- cd path/to/mybook && mdbook build && mdbook test
|
||||
- mdbook build path/to/mybook && mdbook test path/to/mybook
|
||||
```
|
||||
|
||||
## Deploying Your Book to GitHub Pages
|
||||
|
||||
@@ -42,5 +42,5 @@ explanation on the configuration system.
|
||||
|
||||
|
||||
[`MDBook`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.MDBook.html
|
||||
[API Docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
|
||||
[API Docs]: https://docs.rs/mdbook/*/mdbook/
|
||||
[config]: file:///home/michael/Documents/forks/mdBook/target/doc/mdbook/config/index.html
|
||||
|
||||
@@ -11,65 +11,79 @@ the book. Possible use cases are:
|
||||
mathjax equivalents
|
||||
|
||||
|
||||
## Implementing a Preprocessor
|
||||
## Hooking Into MDBook
|
||||
|
||||
A preprocessor is represented by the `Preprocessor` trait.
|
||||
MDBook uses a fairly simple mechanism for discovering third party plugins.
|
||||
A new table is added to `book.toml` (e.g. `preprocessor.foo` for the `foo`
|
||||
preprocessor) and then `mdbook` will try to invoke the `mdbook-foo` program as
|
||||
part of the build process.
|
||||
|
||||
```rust
|
||||
pub trait Preprocessor {
|
||||
fn name(&self) -> &str;
|
||||
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
|
||||
}
|
||||
While preprocessors can be hard-coded to specify which backend it should be run
|
||||
for (e.g. it doesn't make sense for MathJax to be used for non-HTML renderers)
|
||||
with the `preprocessor.foo.renderer` key.
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "My Book"
|
||||
authors = ["Michael-F-Bryan"]
|
||||
|
||||
[preprocessor.foo]
|
||||
# The command can also be specified manually
|
||||
command = "python3 /path/to/foo.py"
|
||||
# Only run the `foo` preprocessor for the HTML and EPUB renderer
|
||||
renderer = ["html", "epub"]
|
||||
```
|
||||
|
||||
Where the `PreprocessorContext` is defined as
|
||||
In typical unix style, all inputs to the plugin will be written to `stdin` as
|
||||
JSON and `mdbook` will read from `stdout` if it is expecting output.
|
||||
|
||||
The easiest way to get started is by creating your own implementation of the
|
||||
`Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
|
||||
translates inputs to the correct `Preprocessor` method. For convenience, there
|
||||
is [an example no-op preprocessor] in the `examples/` directory which can easily
|
||||
be adapted for other preprocessors.
|
||||
|
||||
<details>
|
||||
<summary>Example no-op preprocessor</summary>
|
||||
|
||||
```rust
|
||||
pub struct PreprocessorContext {
|
||||
pub root: PathBuf,
|
||||
pub config: Config,
|
||||
}
|
||||
// nop-preprocessors.rs
|
||||
|
||||
{{#include ../../../examples/nop-preprocessor.rs}}
|
||||
```
|
||||
</details>
|
||||
|
||||
## A complete Example
|
||||
## Hints For Implementing A Preprocessor
|
||||
|
||||
The magic happens within the `run(...)` method of the
|
||||
[`Preprocessor`][preprocessor-docs] trait implementation.
|
||||
By pulling in `mdbook` as a library, preprocessors can have access to the
|
||||
existing infrastructure for dealing with books.
|
||||
|
||||
As direct access to the chapters is not possible, you will probably end up
|
||||
iterating them using `for_each_mut(...)`:
|
||||
For example, a custom preprocessor could use the
|
||||
[`CmdPreprocessor::parse_input()`] function to deserialize the JSON written to
|
||||
`stdin`. Then each chapter of the `Book` can be mutated in-place via
|
||||
[`Book::for_each_mut()`], and then written to `stdout` with the `serde_json`
|
||||
crate.
|
||||
|
||||
Chapters can be accessed either directly (by recursively iterating over
|
||||
chapters) or via the `Book::for_each_mut()` convenience method.
|
||||
|
||||
The `chapter.content` is just a string which happens to be markdown. While it's
|
||||
entirely possible to use regular expressions or do a manual find & replace,
|
||||
you'll probably want to process the input into something more computer-friendly.
|
||||
The [`pulldown-cmark`][pc] crate implements a production-quality event-based
|
||||
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] allowing you to
|
||||
translate events back into markdown text.
|
||||
|
||||
The following code block shows how to remove all emphasis from markdown,
|
||||
without accidentally breaking the document.
|
||||
|
||||
```rust
|
||||
book.for_each_mut(|item: &mut BookItem| {
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
|
||||
res = Some(
|
||||
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
|
||||
Ok(md) => {
|
||||
chapter.content = md;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The `chapter.content` is just a markdown formatted string, and you will have to
|
||||
process it in some way. Even though it's entirely possible to implement some
|
||||
sort of manual find & replace operation, if that feels too unsafe you can use
|
||||
[`pulldown-cmark`][pc] to parse the string into events and work on them instead.
|
||||
|
||||
Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events
|
||||
back to a string.
|
||||
|
||||
The following code block shows how to remove all emphasis from markdown, and do
|
||||
so safely.
|
||||
|
||||
```rust
|
||||
fn remove_emphasis(num_removed_items: &mut i32, chapter: &mut Chapter) -> Result<String> {
|
||||
fn remove_emphasis(
|
||||
num_removed_items: &mut usize,
|
||||
chapter: &mut Chapter,
|
||||
) -> Result<String> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
|
||||
let events = Parser::new(&chapter.content).filter(|e| {
|
||||
let should_keep = match *e {
|
||||
Event::Start(Tag::Emphasis)
|
||||
@@ -83,15 +97,19 @@ fn remove_emphasis(num_removed_items: &mut i32, chapter: &mut Chapter) -> Result
|
||||
}
|
||||
should_keep
|
||||
});
|
||||
cmark(events, &mut buf, None)
|
||||
.map(|_| buf)
|
||||
.map_err(|err| Error::from(format!("Markdown serialization failed: {}", err)))
|
||||
|
||||
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
|
||||
Error::from(format!("Markdown serialization failed: {}", err))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
For everything else, have a look [at the complete example][example].
|
||||
|
||||
[preprocessor-docs]: https://docs.rs/mdbook/0.1.3/mdbook/preprocess/trait.Preprocessor.html
|
||||
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
|
||||
[pc]: https://crates.io/crates/pulldown-cmark
|
||||
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
|
||||
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs
|
||||
[an example no-op preprocessor]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||
[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input
|
||||
[`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut
|
||||
|
||||
@@ -14,6 +14,10 @@ description = "The example book covers examples."
|
||||
build-dir = "my-example-book"
|
||||
create-missing = false
|
||||
|
||||
[preprocess.index]
|
||||
|
||||
[preprocess.links]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["custom.css"]
|
||||
|
||||
@@ -27,7 +31,6 @@ It is important to note that **any** relative path specified in the in the
|
||||
configuration will always be taken relative from the root of the book where the
|
||||
configuration file is located.
|
||||
|
||||
|
||||
### General metadata
|
||||
|
||||
This is general information about your book.
|
||||
@@ -59,15 +62,25 @@ This controls the build process of your book.
|
||||
will be created when the book is built (i.e. `create-missing = true`). If this
|
||||
is `false` then the build process will instead exit with an error if any files
|
||||
do not exist.
|
||||
- **preprocess:** Specify which preprocessors to be applied. Default is
|
||||
`["links", "index"]`. To disable default preprocessors, pass an empty array
|
||||
`[]` in.
|
||||
- **use-default-preprocessors:** Disable the default preprocessors of (`links` &
|
||||
`index`) by setting this option to `false`.
|
||||
|
||||
If you have the same, and/or other preprocessors declared via their table
|
||||
of configuration, they will run instead.
|
||||
|
||||
- For clarity, with no preprocessor configuration, the default `links` and
|
||||
`index` will run.
|
||||
- Setting `use-default-preprocessors = false` will disable these
|
||||
default preprocessors from running.
|
||||
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
|
||||
`use-default-preprocessors` that `links` it will run.
|
||||
|
||||
## Configuring Preprocessors
|
||||
|
||||
The following preprocessors are available and included by default:
|
||||
|
||||
- `links`: Expand the `{{# playpen}}` and `{{# include}}` handlebars helpers in
|
||||
a chapter.
|
||||
- `links`: Expand the `{{ #playpen }}` and `{{ #include }}` handlebars
|
||||
helpers in a chapter to include the contents of a file.
|
||||
- `index`: Convert all chapter files named `README.md` into `index.md`. That is
|
||||
to say, all `README.md` would be rendered to an index file `index.html` in the
|
||||
rendered book.
|
||||
@@ -78,10 +91,53 @@ The following preprocessors are available and included by default:
|
||||
[build]
|
||||
build-dir = "build"
|
||||
create-missing = false
|
||||
preprocess = ["links", "index"]
|
||||
|
||||
[preprocess.links]
|
||||
|
||||
[preprocess.index]
|
||||
```
|
||||
|
||||
### Custom Preprocessor Configuration
|
||||
|
||||
Like renderers, preprocessor will need to be given its own table (e.g.
|
||||
`[preprocessor.mathjax]`). In the section, you may then pass extra
|
||||
configuration to the preprocessor by adding key-value pairs to the table.
|
||||
|
||||
For example
|
||||
|
||||
```
|
||||
[preprocess.links]
|
||||
# set the renderers this preprocessor will run for
|
||||
renderers = ["html"]
|
||||
some_extra_feature = true
|
||||
```
|
||||
|
||||
#### Locking a Preprocessor dependency to a renderer
|
||||
|
||||
You can explicitly specify that a preprocessor should run for a renderer by
|
||||
binding the two together.
|
||||
|
||||
```
|
||||
[preprocessor.mathjax]
|
||||
renderers = ["html"] # mathjax only makes sense with the HTML renderer
|
||||
```
|
||||
|
||||
### Provide Your Own Command
|
||||
|
||||
By default when you add a `[preprocessor.foo]` table to your `book.toml` file,
|
||||
`mdbook` will try to invoke the `mdbook-foo` executa`ble. If you want to use a
|
||||
different program name or pass in command-line arguments, this behaviour can
|
||||
be overridden by adding a `command` field.
|
||||
|
||||
```toml
|
||||
[preprocessor.random]
|
||||
command = "python random.py"
|
||||
```
|
||||
|
||||
## Configuring Renderers
|
||||
|
||||
### HTML renderer options
|
||||
|
||||
The HTML renderer has a couple of options as well. All the options for the
|
||||
renderer need to be specified under the TOML table `[output.html]`.
|
||||
|
||||
@@ -139,7 +195,8 @@ Available configuration options for the `[output.html.search]` table:
|
||||
- **copy-js:** Copy JavaScript files for the search implementation to the output
|
||||
directory. Defaults to `true`.
|
||||
|
||||
This shows all available options in the **book.toml**:
|
||||
This shows all available HTML output options in the **book.toml**:
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
@@ -176,6 +233,16 @@ heading-split-level = 3
|
||||
copy-js = true
|
||||
```
|
||||
|
||||
### Custom Renderers
|
||||
|
||||
A custom renderer can be enabled by adding a `[output.foo]` table to your
|
||||
`book.toml`. Similar to [preprocessors](#configuring-preprocessors) this will
|
||||
instruct `mdbook` to pass a representation of the book to `mdbook-foo` for
|
||||
rendering.
|
||||
|
||||
Custom renderers will have access to all configuration within their table
|
||||
(i.e. anything under `[output.foo]`), and the command to be invoked can be
|
||||
manually specified with the `command` field.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
@@ -214,4 +281,4 @@ book's title without needing to touch your `book.toml`.
|
||||
|
||||
The latter case may be useful in situations where `mdbook` is invoked from a
|
||||
script or CI, where it sometimes isn't possible to update the `book.toml` before
|
||||
building.
|
||||
building.
|
||||
@@ -1,4 +1,6 @@
|
||||
//! This program removes all forms of emphasis from the markdown of the book.
|
||||
//! An example preprocessor for removing all forms of emphasis from a markdown
|
||||
//! book.
|
||||
|
||||
extern crate mdbook;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate pulldown_cmark_to_cmark;
|
||||
@@ -6,89 +8,75 @@ extern crate pulldown_cmark_to_cmark;
|
||||
use mdbook::book::{Book, BookItem, Chapter};
|
||||
use mdbook::errors::{Error, Result};
|
||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
||||
use mdbook::MDBook;
|
||||
use pulldown_cmark::{Event, Parser, Tag};
|
||||
use pulldown_cmark_to_cmark::fmt::cmark;
|
||||
|
||||
use std::env::{args, args_os};
|
||||
use std::ffi::OsString;
|
||||
use std::process;
|
||||
const NAME: &str = "md-links-to-html-links";
|
||||
|
||||
fn main() {
|
||||
panic!("This example is intended to be part of a library");
|
||||
}
|
||||
|
||||
struct Deemphasize;
|
||||
|
||||
impl Preprocessor for Deemphasize {
|
||||
fn name(&self) -> &str {
|
||||
"md-links-to-html-links"
|
||||
NAME
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
|
||||
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
||||
eprintln!("Running '{}' preprocessor", self.name());
|
||||
let mut res: Option<_> = None;
|
||||
let mut num_removed_items = 0;
|
||||
book.for_each_mut(|item: &mut BookItem| {
|
||||
if let Some(Err(_)) = res {
|
||||
return;
|
||||
}
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
|
||||
res = Some(
|
||||
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
|
||||
Ok(md) => {
|
||||
chapter.content = md;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
process(&mut book.sections, &mut num_removed_items)?;
|
||||
|
||||
eprintln!(
|
||||
"{}: removed {} events from markdown stream.",
|
||||
self.name(),
|
||||
num_removed_items
|
||||
);
|
||||
match res {
|
||||
Some(res) => res,
|
||||
None => Ok(()),
|
||||
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
fn process<'a, I>(items: I, num_removed_items: &mut usize) -> Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = &'a mut BookItem> + 'a,
|
||||
{
|
||||
for item in items {
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
eprintln!("{}: processing chapter '{}'", NAME, chapter.name);
|
||||
|
||||
let md = remove_emphasis(num_removed_items, chapter)?;
|
||||
chapter.content = md;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_it(book: OsString) -> Result<()> {
|
||||
let mut book = MDBook::load(book)?;
|
||||
book.with_preprecessor(Deemphasize);
|
||||
book.build()
|
||||
}
|
||||
fn remove_emphasis(
|
||||
num_removed_items: &mut usize,
|
||||
chapter: &mut Chapter,
|
||||
) -> Result<String> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
|
||||
fn main() {
|
||||
if args_os().count() != 2 {
|
||||
eprintln!("USAGE: {} <book>", args().next().expect("executable"));
|
||||
return;
|
||||
}
|
||||
if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
let events = Parser::new(&chapter.content).filter(|e| {
|
||||
let should_keep = match *e {
|
||||
Event::Start(Tag::Emphasis)
|
||||
| Event::Start(Tag::Strong)
|
||||
| Event::End(Tag::Emphasis)
|
||||
| Event::End(Tag::Strong) => false,
|
||||
_ => true,
|
||||
};
|
||||
if !should_keep {
|
||||
*num_removed_items += 1;
|
||||
}
|
||||
should_keep
|
||||
});
|
||||
|
||||
impl Deemphasize {
|
||||
fn remove_emphasis(num_removed_items: &mut i32, chapter: &mut Chapter) -> Result<String> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
let events = Parser::new(&chapter.content).filter(|e| {
|
||||
let should_keep = match *e {
|
||||
Event::Start(Tag::Emphasis)
|
||||
| Event::Start(Tag::Strong)
|
||||
| Event::End(Tag::Emphasis)
|
||||
| Event::End(Tag::Strong) => false,
|
||||
_ => true,
|
||||
};
|
||||
if !should_keep {
|
||||
*num_removed_items += 1;
|
||||
}
|
||||
should_keep
|
||||
});
|
||||
cmark(events, &mut buf, None)
|
||||
.map(|_| buf)
|
||||
.map_err(|err| Error::from(format!("Markdown serialization failed: {}", err)))
|
||||
}
|
||||
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
|
||||
Error::from(format!("Markdown serialization failed: {}", err))
|
||||
})
|
||||
}
|
||||
|
||||
112
examples/nop-preprocessor.rs
Normal file
112
examples/nop-preprocessor.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
extern crate clap;
|
||||
extern crate mdbook;
|
||||
extern crate serde_json;
|
||||
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use mdbook::book::Book;
|
||||
use mdbook::errors::Error;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use std::io;
|
||||
use std::process;
|
||||
use nop_lib::Nop;
|
||||
|
||||
pub fn make_app() -> App<'static, 'static> {
|
||||
App::new("nop-preprocessor")
|
||||
.about("A mdbook preprocessor which does precisely nothing")
|
||||
.subcommand(
|
||||
SubCommand::with_name("supports")
|
||||
.arg(Arg::with_name("renderer").required(true))
|
||||
.about("Check whether a renderer is supported by this preprocessor"))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let matches = make_app().get_matches();
|
||||
|
||||
// Users will want to construct their own preprocessor here
|
||||
let preprocessor = Nop::new();
|
||||
|
||||
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
||||
handle_supports(&preprocessor, sub_args);
|
||||
} else {
|
||||
if let Err(e) = handle_preprocessing(&preprocessor) {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
|
||||
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
|
||||
|
||||
if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
|
||||
// We should probably use the `semver` crate to check compatibility
|
||||
// here...
|
||||
eprintln!(
|
||||
"Warning: The {} plugin was built against version {} of mdbook, \
|
||||
but we're being called from version {}",
|
||||
pre.name(),
|
||||
mdbook::MDBOOK_VERSION,
|
||||
ctx.mdbook_version
|
||||
);
|
||||
}
|
||||
|
||||
let processed_book = pre.run(&ctx, book)?;
|
||||
serde_json::to_writer(io::stdout(), &processed_book)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
|
||||
let renderer = sub_args.value_of("renderer").expect("Required argument");
|
||||
let supported = pre.supports_renderer(&renderer);
|
||||
|
||||
// Signal whether the renderer is supported by exiting with 1 or 0.
|
||||
if supported {
|
||||
process::exit(0);
|
||||
} else {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// The actual implementation of the `Nop` preprocessor. This would usually go
|
||||
/// in your main `lib.rs` file.
|
||||
mod nop_lib {
|
||||
use super::*;
|
||||
|
||||
/// A no-op preprocessor.
|
||||
pub struct Nop;
|
||||
|
||||
impl Nop {
|
||||
pub fn new() -> Nop {
|
||||
Nop
|
||||
}
|
||||
}
|
||||
|
||||
impl Preprocessor for Nop {
|
||||
fn name(&self) -> &str {
|
||||
"nop-preprocessor"
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
ctx: &PreprocessorContext,
|
||||
book: Book,
|
||||
) -> Result<Book, Error> {
|
||||
// In testing we want to tell the preprocessor to blow up by setting a
|
||||
// particular config value
|
||||
if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
|
||||
if nop_cfg.contains_key("blow-up") {
|
||||
return Err("Boom!!1!".into());
|
||||
}
|
||||
}
|
||||
|
||||
// we *are* a no-op preprocessor after all
|
||||
Ok(book)
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
renderer != "not-supported"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
243
src/book/mod.rs
243
src/book/mod.rs
@@ -20,7 +20,8 @@ use tempfile::Builder as TempFileBuilder;
|
||||
use toml::Value;
|
||||
|
||||
use errors::*;
|
||||
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor,
|
||||
PreprocessorContext, CmdPreprocessor};
|
||||
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
||||
use utils;
|
||||
|
||||
@@ -149,23 +150,39 @@ impl MDBook {
|
||||
pub fn build(&self) -> Result<()> {
|
||||
info!("Book building has started");
|
||||
|
||||
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 {
|
||||
info!("Running the {} backend", renderer.name());
|
||||
self.run_renderer(&preprocessed_book, renderer.as_ref())?;
|
||||
self.execute_build_process(&**renderer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_renderer(&self, preprocessed_book: &Book, renderer: &Renderer) -> Result<()> {
|
||||
/// Run the entire build process for a particular `Renderer`.
|
||||
fn execute_build_process(&self, renderer: &Renderer) -> Result<()> {
|
||||
let mut preprocessed_book = self.book.clone();
|
||||
let preprocess_ctx = PreprocessorContext::new(self.root.clone(),
|
||||
self.config.clone(),
|
||||
renderer.name().to_string());
|
||||
|
||||
for preprocessor in &self.preprocessors {
|
||||
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
|
||||
debug!("Running the {} preprocessor.", preprocessor.name());
|
||||
preprocessed_book =
|
||||
preprocessor.run(&preprocess_ctx, preprocessed_book)?;
|
||||
}
|
||||
}
|
||||
|
||||
info!("Running the {} backend", renderer.name());
|
||||
self.render(&preprocessed_book, renderer)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
&self,
|
||||
preprocessed_book: &Book,
|
||||
renderer: &Renderer,
|
||||
) -> Result<()> {
|
||||
let name = renderer.name();
|
||||
let build_dir = self.build_dir_for(name);
|
||||
if build_dir.exists() {
|
||||
@@ -215,13 +232,16 @@ impl MDBook {
|
||||
|
||||
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
||||
|
||||
let preprocess_context = PreprocessorContext::new(self.root.clone(), self.config.clone());
|
||||
// FIXME: Is "test" the proper renderer name to use here?
|
||||
let preprocess_context = PreprocessorContext::new(self.root.clone(),
|
||||
self.config.clone(),
|
||||
"test".to_string());
|
||||
|
||||
LinkPreprocessor::new().run(&preprocess_context, &mut self.book)?;
|
||||
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
|
||||
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
||||
// actual markdown files.
|
||||
|
||||
for item in self.iter() {
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
if !ch.path.as_os_str().is_empty() {
|
||||
let path = self.source_dir().join(&ch.path);
|
||||
@@ -330,30 +350,55 @@ fn default_preprocessors() -> Vec<Box<Preprocessor>> {
|
||||
]
|
||||
}
|
||||
|
||||
fn is_default_preprocessor(pre: &Preprocessor) -> bool {
|
||||
let name = pre.name();
|
||||
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
|
||||
}
|
||||
|
||||
/// 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 and
|
||||
// IndexPreprocessor. This allows you to disable default preprocessors
|
||||
// by setting "preprocess" to an empty list.
|
||||
None => return Ok(default_preprocessors()),
|
||||
};
|
||||
let mut preprocessors = Vec::new();
|
||||
|
||||
let mut preprocessors: Vec<Box<Preprocessor>> = Vec::new();
|
||||
if config.build.use_default_preprocessors {
|
||||
preprocessors.extend(default_preprocessors());
|
||||
}
|
||||
|
||||
for key in preprocess_list {
|
||||
match key.as_ref() {
|
||||
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
|
||||
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
|
||||
_ => bail!("{:?} is not a recognised preprocessor", key),
|
||||
if let Some(preprocessor_table) =
|
||||
config.get("preprocessor").and_then(|v| v.as_table())
|
||||
{
|
||||
for key in preprocessor_table.keys() {
|
||||
match key.as_ref() {
|
||||
"links" => {
|
||||
preprocessors.push(Box::new(LinkPreprocessor::new()))
|
||||
}
|
||||
"index" => {
|
||||
preprocessors.push(Box::new(IndexPreprocessor::new()))
|
||||
}
|
||||
name => preprocessors.push(interpret_custom_preprocessor(
|
||||
name,
|
||||
&preprocessor_table[name],
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(preprocessors)
|
||||
}
|
||||
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
|
||||
fn interpret_custom_preprocessor(
|
||||
key: &str,
|
||||
table: &Value,
|
||||
) -> Box<CmdPreprocessor> {
|
||||
let command = table
|
||||
.get("command")
|
||||
.and_then(|c| c.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
|
||||
Box::new(CmdPreprocessor::new(key.to_string(), command.to_string()))
|
||||
}
|
||||
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
// look for the `command` field, falling back to using the key
|
||||
// prepended by "mdbook-"
|
||||
let table_dot_command = table
|
||||
@@ -361,11 +406,37 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
|
||||
.and_then(|c| c.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
let command =
|
||||
table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
|
||||
Box::new(CmdRenderer::new(key.to_string(), command.to_string()))
|
||||
}
|
||||
|
||||
/// Check whether we should run a particular `Preprocessor` in combination
|
||||
/// with the renderer, falling back to `Preprocessor::supports_renderer()`
|
||||
/// method if the user doesn't say anything.
|
||||
///
|
||||
/// The `build.use-default-preprocessors` config option can be used to ensure
|
||||
/// default preprocessors always run if they support the renderer.
|
||||
fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg: &Config) -> bool {
|
||||
// default preprocessors should be run by default (if supported)
|
||||
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
|
||||
return preprocessor.supports_renderer(renderer.name());
|
||||
}
|
||||
|
||||
let key = format!("preprocessor.{}.renderers", preprocessor.name());
|
||||
let renderer_name = renderer.name();
|
||||
|
||||
if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
|
||||
return explicit_renderers.into_iter()
|
||||
.filter_map(|val| val.as_str())
|
||||
.any(|name| name == renderer_name);
|
||||
}
|
||||
|
||||
preprocessor.supports_renderer(renderer_name)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -413,8 +484,8 @@ mod tests {
|
||||
fn config_defaults_to_link_and_index_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());
|
||||
// make sure we haven't got anything in the `preprocessor` table
|
||||
assert!(cfg.get("preprocessor").is_none());
|
||||
|
||||
let got = determine_preprocessors(&cfg);
|
||||
|
||||
@@ -425,47 +496,107 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_doesnt_default_if_empty() {
|
||||
let cfg_str: &'static str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
fn use_default_preprocessors_works() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.build.use_default_preprocessors = false;
|
||||
|
||||
[build]
|
||||
build-dir = "outputs"
|
||||
create-missing = false
|
||||
preprocess = []
|
||||
"#;
|
||||
let got = determine_preprocessors(&cfg).unwrap();
|
||||
|
||||
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());
|
||||
assert_eq!(got.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_complains_if_unimplemented_preprocessor() {
|
||||
fn can_determine_third_party_preprocessors() {
|
||||
let cfg_str: &'static str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
|
||||
[preprocessor.random]
|
||||
|
||||
[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());
|
||||
// make sure the `preprocessor.random` table exists
|
||||
assert!(cfg.get_preprocessor("random").is_some());
|
||||
|
||||
let got = determine_preprocessors(&cfg);
|
||||
let got = determine_preprocessors(&cfg).unwrap();
|
||||
|
||||
assert!(got.is_err());
|
||||
assert!(got.into_iter().any(|p| p.name() == "random"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessors_can_provide_their_own_commands() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
command = "python random.py"
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// make sure the `preprocessor.random` table exists
|
||||
let random = cfg.get_preprocessor("random").unwrap();
|
||||
let random = interpret_custom_preprocessor(
|
||||
"random",
|
||||
&Value::Table(random.clone()),
|
||||
);
|
||||
|
||||
assert_eq!(random.cmd(), "python random.py");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_respects_preprocessor_selection() {
|
||||
let cfg_str: &'static str = r#"
|
||||
[preprocessor.links]
|
||||
renderers = ["html"]
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// double-check that we can access preprocessor.links.renderers[0]
|
||||
let html = cfg.get_preprocessor("links")
|
||||
.and_then(|links| links.get("renderers"))
|
||||
.and_then(|renderers| renderers.as_array())
|
||||
.and_then(|renderers| renderers.get(0))
|
||||
.and_then(|renderer| renderer.as_str())
|
||||
.unwrap();
|
||||
assert_eq!(html, "html");
|
||||
let html_renderer = HtmlHandlebars::default();
|
||||
let pre = LinkPreprocessor::new();
|
||||
|
||||
let should_run = preprocessor_should_run(&pre, &html_renderer, &cfg);
|
||||
assert!(should_run);
|
||||
}
|
||||
|
||||
struct BoolPreprocessor(bool);
|
||||
impl Preprocessor for BoolPreprocessor {
|
||||
fn name(&self) -> &str {
|
||||
"bool-preprocessor"
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, _book: Book) -> Result<Book> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, _renderer: &str) -> bool {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessor_should_run_falls_back_to_supports_renderer_method() {
|
||||
let cfg = Config::default();
|
||||
let html = HtmlHandlebars::new();
|
||||
|
||||
let should_be = true;
|
||||
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
let should_be = false;
|
||||
let got = preprocessor_should_run(&BoolPreprocessor(should_be), &html, &cfg);
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,6 +292,7 @@ impl<'a> SummaryParser<'a> {
|
||||
/// already been consumed by a previous parser.
|
||||
fn parse_numbered(&mut self) -> Result<Vec<SummaryItem>> {
|
||||
let mut items = Vec::new();
|
||||
let mut root_items = 0;
|
||||
let root_number = SectionNumber::default();
|
||||
|
||||
// we need to do this funny loop-match-if-let dance because a rule will
|
||||
@@ -308,9 +309,11 @@ impl<'a> SummaryParser<'a> {
|
||||
// if we've resumed after something like a rule the root sections
|
||||
// will be numbered from 1. We need to manually go back and update
|
||||
// them
|
||||
update_section_numbers(&mut bunch_of_items, 0, items.len() as u32);
|
||||
update_section_numbers(&mut bunch_of_items, 0, root_items);
|
||||
root_items += bunch_of_items.len() as u32;
|
||||
items.extend(bunch_of_items);
|
||||
|
||||
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Paragraph)) => {
|
||||
// we're starting the suffix chapters
|
||||
@@ -724,4 +727,40 @@ mod tests {
|
||||
let got = parser.parse_numbered();
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
/// Regression test for https://github.com/rust-lang-nursery/mdBook/issues/779
|
||||
/// Ensure section numbers are correctly incremented after a horizontal separator.
|
||||
#[test]
|
||||
fn keep_numbering_after_separator() {
|
||||
let src = "- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: PathBuf::from("./first.md"),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Separator,
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: PathBuf::from("./second.md"),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Separator,
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Third"),
|
||||
location: PathBuf::from("./third.md"),
|
||||
number: Some(SectionNumber(vec![3])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let _ = parser.stream.next();
|
||||
|
||||
let got = parser.parse_numbered().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +209,18 @@ impl Config {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the table associated with a particular renderer.
|
||||
pub fn get_renderer<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
|
||||
let key = format!("output.{}", index.as_ref());
|
||||
self.get(&key).and_then(|v| v.as_table())
|
||||
}
|
||||
|
||||
/// Get the table associated with a particular preprocessor.
|
||||
pub fn get_preprocessor<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
|
||||
let key = format!("preprocessor.{}", index.as_ref());
|
||||
self.get(&key).and_then(|v| v.as_table())
|
||||
}
|
||||
|
||||
fn from_legacy(mut table: Value) -> Config {
|
||||
let mut cfg = Config::default();
|
||||
|
||||
@@ -382,8 +394,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>>,
|
||||
/// Should the default preprocessors always be used when they are
|
||||
/// compatible with the renderer?
|
||||
pub use_default_preprocessors: bool,
|
||||
}
|
||||
|
||||
impl Default for BuildConfig {
|
||||
@@ -391,7 +404,7 @@ impl Default for BuildConfig {
|
||||
BuildConfig {
|
||||
build_dir: PathBuf::from("book"),
|
||||
create_missing: true,
|
||||
preprocess: None,
|
||||
use_default_preprocessors: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -551,7 +564,7 @@ mod tests {
|
||||
[build]
|
||||
build-dir = "outputs"
|
||||
create-missing = false
|
||||
preprocess = ["first_preprocessor", "second_preprocessor"]
|
||||
use-default-preprocessors = true
|
||||
|
||||
[output.html]
|
||||
theme = "./themedir"
|
||||
@@ -562,6 +575,10 @@ mod tests {
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
editor = "ace"
|
||||
|
||||
[preprocess.first]
|
||||
|
||||
[preprocess.second]
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
@@ -579,10 +596,7 @@ 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(),
|
||||
]),
|
||||
use_default_preprocessors: true,
|
||||
};
|
||||
let playpen_should_be = Playpen {
|
||||
editable: true,
|
||||
@@ -684,7 +698,7 @@ mod tests {
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("my-book"),
|
||||
create_missing: true,
|
||||
preprocess: None,
|
||||
use_default_preprocessors: true,
|
||||
};
|
||||
|
||||
let html_should_be = HtmlConfig {
|
||||
|
||||
@@ -114,6 +114,12 @@ pub mod renderer;
|
||||
pub mod theme;
|
||||
pub mod utils;
|
||||
|
||||
/// The current version of `mdbook`.
|
||||
///
|
||||
/// This is provided as a way for custom preprocessors and renderers to do
|
||||
/// compatibility checks.
|
||||
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub use book::BookItem;
|
||||
pub use book::MDBook;
|
||||
pub use config::Config;
|
||||
|
||||
154
src/preprocess/cmd.rs
Normal file
154
src/preprocess/cmd.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use book::Book;
|
||||
use errors::*;
|
||||
use serde_json;
|
||||
use shlex::Shlex;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
||||
/// A custom preprocessor which will shell out to a 3rd-party program.
|
||||
///
|
||||
/// # Preprocessing Protocol
|
||||
///
|
||||
/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will
|
||||
/// execute the shell command `$cmd supports $renderer`. If the renderer is
|
||||
/// supported, custom preprocessors should exit with a exit code of `0`,
|
||||
/// any other exit code be considered as unsupported.
|
||||
///
|
||||
/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)`
|
||||
/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors
|
||||
/// should then "return" a processed book by printing it to `stdout` as JSON.
|
||||
/// For convenience, the `CmdPreprocessor::parse_input()` function can be used
|
||||
/// to parse the input provided by `mdbook`.
|
||||
///
|
||||
/// Exiting with a non-zero exit code while preprocessing is considered an
|
||||
/// error. `stderr` is passed directly through to the user, so it can be used
|
||||
/// for logging or emitting warnings if desired.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// An example preprocessor is available in this project's `examples/`
|
||||
/// directory.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CmdPreprocessor {
|
||||
name: String,
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
impl CmdPreprocessor {
|
||||
/// Create a new `CmdPreprocessor`.
|
||||
pub fn new(name: String, cmd: String) -> CmdPreprocessor {
|
||||
CmdPreprocessor { name, cmd }
|
||||
}
|
||||
|
||||
/// A convenience function custom preprocessors can use to parse the input
|
||||
/// written to `stdin` by a `CmdRenderer`.
|
||||
pub fn parse_input<R: Read>(
|
||||
reader: R,
|
||||
) -> Result<(PreprocessorContext, Book)> {
|
||||
serde_json::from_reader(reader)
|
||||
.chain_err(|| "Unable to parse the input")
|
||||
}
|
||||
|
||||
fn write_input_to_child(
|
||||
&self,
|
||||
child: &mut Child,
|
||||
book: &Book,
|
||||
ctx: &PreprocessorContext,
|
||||
) {
|
||||
let stdin = child.stdin.take().expect("Child has stdin");
|
||||
|
||||
if let Err(e) = self.write_input(stdin, &book, &ctx) {
|
||||
// Looks like the backend hung up before we could finish
|
||||
// sending it the render context. Log the error and keep going
|
||||
warn!("Error writing the RenderContext to the backend, {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_input<W: Write>(&self, writer: W, book: &Book, ctx: &PreprocessorContext) -> Result<()> {
|
||||
serde_json::to_writer(writer, &(ctx, book))
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// The command this `Preprocessor` will invoke.
|
||||
pub fn cmd(&self) -> &str {
|
||||
&self.cmd
|
||||
}
|
||||
|
||||
fn command(&self) -> Result<Command> {
|
||||
let mut words = Shlex::new(&self.cmd);
|
||||
let executable = match words.next() {
|
||||
Some(e) => e,
|
||||
None => bail!("Command string was empty"),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(executable);
|
||||
|
||||
for arg in words {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Preprocessor for CmdPreprocessor {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
|
||||
let mut cmd = self.command()?;
|
||||
|
||||
let mut child = cmd
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.chain_err(|| format!("Unable to start the \"{}\" preprocessor. Is it installed?", self.name()))?;
|
||||
|
||||
self.write_input_to_child(&mut child, &book, ctx);
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.chain_err(|| "Error waiting for the preprocessor to complete")?;
|
||||
|
||||
trace!("{} exited with output: {:?}", self.cmd, output);
|
||||
ensure!(output.status.success(), "The preprocessor exited unsuccessfully");
|
||||
|
||||
serde_json::from_slice(&output.stdout).chain_err(|| "Unable to parse the preprocessed book")
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
debug!("Checking if the \"{}\" preprocessor supports \"{}\"", self.name(), renderer);
|
||||
|
||||
let mut cmd = match self.command() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!("Unable to create the command for the \"{}\" preprocessor, {}", self.name(), e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let outcome = cmd
|
||||
.arg("supports")
|
||||
.arg(renderer)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.status()
|
||||
.map(|status| status.code() == Some(0));
|
||||
|
||||
if let Err(ref e) = outcome {
|
||||
if e.kind() == io::ErrorKind::NotFound {
|
||||
warn!(
|
||||
"The command wasn't found, is the \"{}\" preprocessor installed?",
|
||||
self.name
|
||||
);
|
||||
warn!("\tCommand: {}", self.cmd);
|
||||
}
|
||||
}
|
||||
|
||||
outcome.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ use book::{Book, BookItem};
|
||||
pub struct IndexPreprocessor;
|
||||
|
||||
impl IndexPreprocessor {
|
||||
pub(crate) const NAME: &'static str = "index";
|
||||
|
||||
/// Create a new `IndexPreprocessor`.
|
||||
pub fn new() -> Self {
|
||||
IndexPreprocessor
|
||||
@@ -19,10 +21,10 @@ impl IndexPreprocessor {
|
||||
|
||||
impl Preprocessor for IndexPreprocessor {
|
||||
fn name(&self) -> &str {
|
||||
"index"
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
|
||||
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
||||
let source_dir = ctx.root.join(&ctx.config.book.src);
|
||||
book.for_each_mut(|section: &mut BookItem| {
|
||||
if let BookItem::Chapter(ref mut ch) = *section {
|
||||
@@ -37,7 +39,7 @@ impl Preprocessor for IndexPreprocessor {
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
pub struct LinkPreprocessor;
|
||||
|
||||
impl LinkPreprocessor {
|
||||
pub(crate) const NAME: &'static str = "links";
|
||||
|
||||
/// Create a new `LinkPreprocessor`.
|
||||
pub fn new() -> Self {
|
||||
LinkPreprocessor
|
||||
@@ -24,10 +26,10 @@ impl LinkPreprocessor {
|
||||
|
||||
impl Preprocessor for LinkPreprocessor {
|
||||
fn name(&self) -> &str {
|
||||
"links"
|
||||
Self::NAME
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
|
||||
fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
||||
let src_dir = ctx.root.join(&ctx.config.book.src);
|
||||
|
||||
book.for_each_mut(|section: &mut BookItem| {
|
||||
@@ -43,7 +45,7 @@ impl Preprocessor for LinkPreprocessor {
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +69,12 @@ where
|
||||
Ok(new_content) => {
|
||||
if depth < MAX_LINK_NESTED_DEPTH {
|
||||
if let Some(rel_path) = playpen.link.relative_path(path) {
|
||||
replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1));
|
||||
replaced.push_str(&replace_all(
|
||||
&new_content,
|
||||
rel_path,
|
||||
source,
|
||||
depth + 1,
|
||||
));
|
||||
} else {
|
||||
replaced.push_str(&new_content);
|
||||
}
|
||||
@@ -81,6 +88,10 @@ where
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error updating \"{}\", {}", playpen.link_text, e);
|
||||
for cause in e.iter().skip(1) {
|
||||
warn!("Caused By: {}", cause);
|
||||
}
|
||||
|
||||
// This should make sure we include the raw `{{# ... }}` snippet
|
||||
// in the page content if there are any errors.
|
||||
previous_end_index = playpen.start_index;
|
||||
@@ -107,10 +118,18 @@ impl<'a> LinkType<'a> {
|
||||
let base = base.as_ref();
|
||||
match self {
|
||||
LinkType::Escaped => None,
|
||||
LinkType::IncludeRange(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRange(p, _) => {
|
||||
Some(return_relative_path(base, &p))
|
||||
}
|
||||
LinkType::IncludeRangeFrom(p, _) => {
|
||||
Some(return_relative_path(base, &p))
|
||||
}
|
||||
LinkType::IncludeRangeTo(p, _) => {
|
||||
Some(return_relative_path(base, &p))
|
||||
}
|
||||
LinkType::IncludeRangeFull(p, _) => {
|
||||
Some(return_relative_path(base, &p))
|
||||
}
|
||||
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
|
||||
}
|
||||
}
|
||||
@@ -180,11 +199,15 @@ impl<'a> Link<'a> {
|
||||
|
||||
match (typ.as_str(), file_arg) {
|
||||
("include", Some(pth)) => Some(parse_include_path(pth)),
|
||||
("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
|
||||
("playpen", Some(pth)) => {
|
||||
Some(LinkType::Playpen(pth.into(), props))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
(Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => {
|
||||
(Some(mat), None, None)
|
||||
if mat.as_str().starts_with(ESCAPE_CHAR) =>
|
||||
{
|
||||
Some(LinkType::Escaped)
|
||||
}
|
||||
_ => None,
|
||||
@@ -205,20 +228,65 @@ impl<'a> Link<'a> {
|
||||
match self.link {
|
||||
// omit the escape char
|
||||
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
||||
LinkType::IncludeRange(ref pat, ref range) => file_to_string(base.join(pat))
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRangeFrom(ref pat, ref range) => file_to_string(base.join(pat))
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRangeTo(ref pat, ref range) => file_to_string(base.join(pat))
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRangeFull(ref pat, _) => file_to_string(base.join(pat))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRange(ref pat, ref range) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
file_to_string(&target)
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeRangeFrom(ref pat, ref range) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
file_to_string(&target)
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeRangeTo(ref pat, ref range) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
file_to_string(&target)
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeRangeFull(ref pat, _) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
file_to_string(&target).chain_err(|| {
|
||||
format!("Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display())
|
||||
})
|
||||
}
|
||||
LinkType::Playpen(ref pat, ref attrs) => {
|
||||
let contents = file_to_string(base.join(pat))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text))?;
|
||||
let target = base.join(pat);
|
||||
|
||||
let contents =
|
||||
file_to_string(&target).chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display()
|
||||
)
|
||||
})?;
|
||||
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
||||
Ok(format!(
|
||||
"```{}{}\n{}\n```\n",
|
||||
@@ -463,7 +531,10 @@ mod tests {
|
||||
Link {
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
|
||||
link: LinkType::Playpen(
|
||||
PathBuf::from("file.rs"),
|
||||
vec!["editable"]
|
||||
),
|
||||
link_text: "{{#playpen file.rs editable }}",
|
||||
},
|
||||
Link {
|
||||
@@ -473,7 +544,8 @@ mod tests {
|
||||
PathBuf::from("my.rs"),
|
||||
vec!["editable", "no_run", "should_panic"],
|
||||
),
|
||||
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
||||
link_text:
|
||||
"{{#playpen my.rs editable no_run should_panic}}",
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
pub use self::index::IndexPreprocessor;
|
||||
pub use self::links::LinkPreprocessor;
|
||||
pub use self::cmd::CmdPreprocessor;
|
||||
|
||||
mod index;
|
||||
mod links;
|
||||
mod cmd;
|
||||
|
||||
use book::Book;
|
||||
use config::Config;
|
||||
@@ -14,17 +16,29 @@ use std::path::PathBuf;
|
||||
|
||||
/// Extra information for a `Preprocessor` to give them more context when
|
||||
/// processing a book.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PreprocessorContext {
|
||||
/// The location of the book directory on disk.
|
||||
pub root: PathBuf,
|
||||
/// The book configuration (`book.toml`).
|
||||
pub config: Config,
|
||||
/// The `Renderer` this preprocessor is being used with.
|
||||
pub renderer: String,
|
||||
/// The calling `mdbook` version.
|
||||
pub mdbook_version: String,
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
impl PreprocessorContext {
|
||||
/// Create a new `PreprocessorContext`.
|
||||
pub(crate) fn new(root: PathBuf, config: Config) -> Self {
|
||||
PreprocessorContext { root, config }
|
||||
pub(crate) fn new(root: PathBuf, config: Config, renderer: String) -> Self {
|
||||
PreprocessorContext {
|
||||
root,
|
||||
config,
|
||||
renderer,
|
||||
mdbook_version: ::MDBOOK_VERSION.to_string(),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,5 +50,13 @@ pub trait Preprocessor {
|
||||
|
||||
/// Run this `Preprocessor`, allowing it to update the book before it is
|
||||
/// given to a renderer.
|
||||
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>;
|
||||
|
||||
/// A hint to `MDBook` whether this preprocessor is compatible with a
|
||||
/// particular renderer.
|
||||
///
|
||||
/// By default, always returns `true`.
|
||||
fn supports_renderer(&self, _renderer: &str) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ use book::Book;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
|
||||
const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// An arbitrary `mdbook` backend.
|
||||
///
|
||||
/// Although it's quite possible for you to import `mdbook` as a library and
|
||||
@@ -66,6 +64,7 @@ pub struct RenderContext {
|
||||
/// renderers to cache intermediate results, this directory is not
|
||||
/// guaranteed to be empty or even exist.
|
||||
pub destination: PathBuf,
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
impl RenderContext {
|
||||
@@ -78,9 +77,10 @@ impl RenderContext {
|
||||
RenderContext {
|
||||
book: book,
|
||||
config: config,
|
||||
version: MDBOOK_VERSION.to_string(),
|
||||
version: ::MDBOOK_VERSION.to_string(),
|
||||
root: root.into(),
|
||||
destination: destination.into(),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ this }}">
|
||||
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
|
||||
{{/each}}
|
||||
|
||||
{{#if mathjax_support}}
|
||||
@@ -235,7 +235,7 @@
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
{{#each additional_js}}
|
||||
<script type="text/javascript" src="{{ path_to_root }}{{this}}"></script>
|
||||
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
{{#if is_print}}
|
||||
|
||||
80
tests/build_process.rs
Normal file
80
tests/build_process.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
extern crate mdbook;
|
||||
|
||||
mod dummy_book;
|
||||
|
||||
use dummy_book::DummyBook;
|
||||
use mdbook::book::Book;
|
||||
use mdbook::config::Config;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
||||
use mdbook::renderer::{RenderContext, Renderer};
|
||||
use mdbook::MDBook;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
struct Spy(Arc<Mutex<Inner>>);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct Inner {
|
||||
run_count: usize,
|
||||
rendered_with: Vec<String>,
|
||||
}
|
||||
|
||||
impl Preprocessor for Spy {
|
||||
fn name(&self) -> &str {
|
||||
"dummy"
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
|
||||
let mut inner = self.0.lock().unwrap();
|
||||
inner.run_count += 1;
|
||||
inner.rendered_with.push(ctx.renderer.clone());
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for Spy {
|
||||
fn name(&self) -> &str {
|
||||
"dummy"
|
||||
}
|
||||
|
||||
fn render(&self, _ctx: &RenderContext) -> Result<()> {
|
||||
let mut inner = self.0.lock().unwrap();
|
||||
inner.run_count += 1;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mdbook_runs_preprocessors() {
|
||||
let spy: Arc<Mutex<Inner>> = Default::default();
|
||||
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let cfg = Config::default();
|
||||
|
||||
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
|
||||
book.with_preprecessor(Spy(Arc::clone(&spy)));
|
||||
book.build().unwrap();
|
||||
|
||||
let inner = spy.lock().unwrap();
|
||||
assert_eq!(inner.run_count, 1);
|
||||
assert_eq!(inner.rendered_with.len(), 1);
|
||||
assert_eq!(
|
||||
"html", inner.rendered_with[0],
|
||||
"We should have been run with the default HTML renderer"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mdbook_runs_renderers() {
|
||||
let spy: Arc<Mutex<Inner>> = Default::default();
|
||||
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let cfg = Config::default();
|
||||
|
||||
let mut book = MDBook::load_with_config(temp.path(), cfg).unwrap();
|
||||
book.with_renderer(Spy(Arc::clone(&spy)));
|
||||
book.build().unwrap();
|
||||
|
||||
let inner = spy.lock().unwrap();
|
||||
assert_eq!(inner.run_count, 1);
|
||||
}
|
||||
53
tests/custom_preprocessors.rs
Normal file
53
tests/custom_preprocessors.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
extern crate mdbook;
|
||||
|
||||
mod dummy_book;
|
||||
|
||||
use dummy_book::DummyBook;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
|
||||
use mdbook::MDBook;
|
||||
|
||||
fn example() -> CmdPreprocessor {
|
||||
CmdPreprocessor::new("nop-preprocessor".to_string(), "cargo run --example nop-preprocessor --".to_string())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn example_supports_whatever() {
|
||||
let cmd = example();
|
||||
|
||||
let got = cmd.supports_renderer("whatever");
|
||||
|
||||
assert_eq!(got, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn example_doesnt_support_not_supported() {
|
||||
let cmd = example();
|
||||
|
||||
let got = cmd.supports_renderer("not-supported");
|
||||
|
||||
assert_eq!(got, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ask_the_preprocessor_to_blow_up() {
|
||||
let dummy_book = DummyBook::new();
|
||||
let temp = dummy_book.build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
md.with_preprecessor(example());
|
||||
|
||||
md.config.set("preprocessor.nop-preprocessor.blow-up", true).unwrap();
|
||||
|
||||
let got = md.build();
|
||||
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_the_dummy_book() {
|
||||
let dummy_book = DummyBook::new();
|
||||
let temp = dummy_book.build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
md.with_preprecessor(example());
|
||||
|
||||
md.build().unwrap();
|
||||
}
|
||||
@@ -4,14 +4,8 @@ mod dummy_book;
|
||||
|
||||
use dummy_book::DummyBook;
|
||||
|
||||
use mdbook::book::Book;
|
||||
use mdbook::config::Config;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
||||
use mdbook::MDBook;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[test]
|
||||
fn mdbook_can_correctly_test_a_passing_book() {
|
||||
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
|
||||
@@ -27,30 +21,3 @@ 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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user