mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 15:01:45 -05:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f84f66041 | ||
|
|
e735bc6d3e | ||
|
|
803df90efa | ||
|
|
b614b0fd65 | ||
|
|
32df76d077 | ||
|
|
dacc274e0d | ||
|
|
b0b09bad3f | ||
|
|
93874edebf | ||
|
|
1aa9c92ac1 | ||
|
|
5ce05a79be | ||
|
|
c51e080783 | ||
|
|
dd5d94393d | ||
|
|
3d5eb48e32 | ||
|
|
d56ff94ce6 | ||
|
|
fb99276f52 | ||
|
|
5eff572dbb | ||
|
|
238dfb7d1d | ||
|
|
c777913136 | ||
|
|
c25c5d72c8 | ||
|
|
3aa6436679 | ||
|
|
d37821c194 | ||
|
|
1b5137c84e | ||
|
|
18c725ee12 | ||
|
|
1743f2a39f | ||
|
|
cee3296a32 | ||
|
|
ddb0834da8 | ||
|
|
b74c2c18ef | ||
|
|
c056b5cbd0 | ||
|
|
8d7970b32d | ||
|
|
1d22a9a040 | ||
|
|
6059883229 | ||
|
|
79dd03e8e9 | ||
|
|
aecc403fb8 | ||
|
|
cd711bfb1c | ||
|
|
afd9ccb7b1 | ||
|
|
cb5ae21b89 | ||
|
|
dd3bef8000 | ||
|
|
7e5892bd35 | ||
|
|
56cee872e8 | ||
|
|
a554390aa2 | ||
|
|
c64384abc3 | ||
|
|
ba7d40284b | ||
|
|
8f6523a94c | ||
|
|
8fbc59720d | ||
|
|
ac9c150902 | ||
|
|
f2e56c887b | ||
|
|
b4a12fa723 | ||
|
|
382fc4139b |
@@ -5,7 +5,6 @@ cache: cargo
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
os:
|
||||
|
||||
@@ -5,22 +5,22 @@ Welcome stranger!
|
||||
If you have come here to learn how to contribute to mdBook, we have some tips for you!
|
||||
|
||||
First of all, don't hesitate to ask questions!
|
||||
Use the [issue tracker](https://github.com/azerupi/mdBook/issues), no question is too simple.
|
||||
If we don't respond in a couple of days, ping us @azerupi, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
|
||||
Use the [issue tracker](https://github.com/rust-lang-nursery/mdBook/issues), no question is too simple.
|
||||
If we don't respond in a couple of days, ping us @Michael-F-Bryan, @budziq, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
|
||||
|
||||
### Issues to work on
|
||||
|
||||
Any issue is up for the grabbing, but if you are starting out, you might be interested in the
|
||||
[E-Easy issues](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
||||
[E-Easy issues](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
||||
Those are issues that are considered more straightforward for beginners to Rust or the codebase itself.
|
||||
These issues can be a good launching pad for more involved issues. Easy tasks for a first time contribution
|
||||
include documentation improvements, new tests, examples, updating dependencies, etc.
|
||||
|
||||
If you come from a web development background, you might be interested in issues related to web technologies tagged
|
||||
[A-JavaScript](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||
[A-Style](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
|
||||
[A-HTML](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
||||
[A-Mobile](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
|
||||
[A-JavaScript](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||
[A-Style](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
|
||||
[A-HTML](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
||||
[A-Mobile](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
|
||||
|
||||
When you decide you want to work on a specific issue, ping us on that issue so that we can assign it to you.
|
||||
Again, do not hesitate to ask questions. We will gladly mentor anyone that want to tackle an issue.
|
||||
@@ -41,7 +41,7 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
|
||||
0. Clone this repository with git.
|
||||
|
||||
```
|
||||
git clone https://github.com/azerupi/mdBook.git
|
||||
git clone https://github.com/rust-lang-nursery/mdBook.git
|
||||
```
|
||||
0. Navigate into the newly created `mdBook` directory
|
||||
0. Run `cargo build`
|
||||
@@ -55,7 +55,7 @@ mdBook doesn't use CSS directly but uses [Stylus](http://stylus-lang.com/), a CS
|
||||
|
||||
When you want to change the style, it is important to not change the CSS directly because any manual modification to
|
||||
the CSS files will be overwritten when compiling the stylus files. Instead, you should make your changes directly in the
|
||||
[stylus files](https://github.com/azerupi/mdBook/tree/master/src/theme/stylus) and regenerate the CSS.
|
||||
[stylus files](https://github.com/rust-lang-nursery/mdBook/tree/master/src/theme/stylus) and regenerate the CSS.
|
||||
|
||||
For this to work, you first need [Node and NPM](https://nodejs.org/en/) installed on your machine.
|
||||
Then run the following command to install both [stylus](http://stylus-lang.com/) and [nib](https://tj.github.io/nib/), you might need `sudo` to install successfully.
|
||||
|
||||
11
Cargo.toml
11
Cargo.toml
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.0.26"
|
||||
version = "0.0.27"
|
||||
authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
|
||||
description = "create books from markdown files (like Gitbook)"
|
||||
documentation = "http://azerupi.github.io/mdBook/index.html"
|
||||
repository = "https://github.com/azerupi/mdBook"
|
||||
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
|
||||
repository = "https://github.com/rust-lang-nursery/mdBook"
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
@@ -43,6 +43,11 @@ ws = { version = "0.7", optional = true}
|
||||
[build-dependencies]
|
||||
error-chain = "0.11"
|
||||
|
||||
[dev-dependencies]
|
||||
select = "0.4"
|
||||
pretty_assertions = "0.4"
|
||||
walkdir = "1.0"
|
||||
|
||||
[features]
|
||||
default = ["output", "watch", "serve"]
|
||||
debug = []
|
||||
|
||||
20
README.md
20
README.md
@@ -4,7 +4,7 @@
|
||||
<tr>
|
||||
<td><strong>Linux / OS X</strong></td>
|
||||
<td>
|
||||
<a href="https://travis-ci.org/azerupi/mdBook"><img src="https://travis-ci.org/azerupi/mdBook.svg?branch=master"></a>
|
||||
<a href="https://travis-ci.org/rust-lang-nursery/mdBook"><img src="https://travis-ci.org/rust-lang-nursery/mdBook.svg?branch=master"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -16,7 +16,7 @@
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a href="https://crates.io/crates/mdbook"><img src="https://img.shields.io/crates/v/mdbook.svg"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/azerupi/mdBook.svg"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/rust-lang-nursery/mdBook.svg"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -26,14 +26,14 @@ mdBook is a utility to create modern online books from Markdown files.
|
||||
|
||||
## What does it look like?
|
||||
|
||||
The [**Documentation**](http://azerupi.github.io/mdBook/) for mdBook has been written in Markdown and is using mdBook to generate the online book-like website you can read. The documentation uses the latest version on GitHub and showcases the available features.
|
||||
The [**User Guide**](https://rust-lang-nursery.github.io/mdBook/) for mdBook has been written in Markdown and is using mdBook to generate the online book-like website you can read. The documentation uses the latest version on GitHub and showcases the available features.
|
||||
|
||||
## Installation
|
||||
|
||||
There are multiple ways to install mdBook.
|
||||
|
||||
1. **Binaries**
|
||||
Binaries are available for download [here](https://github.com/azerupi/mdBook/releases). Make sure to put the path to the binary into your `PATH`.
|
||||
Binaries are available for download [here](https://github.com/rust-lang-nursery/mdBook/releases). Make sure to put the path to the binary into your `PATH`.
|
||||
|
||||
2. **From Crates.io**
|
||||
This requires [Rust and Cargo](https://www.rust-lang.org/) to be installed. Once you have installed Rust, type the following in the terminal:
|
||||
@@ -55,7 +55,7 @@ There are multiple ways to install mdBook.
|
||||
The version published to crates.io will ever so slightly be behind the version hosted here on GitHub. If you need the latest version you can build the git version of mdBook yourself. Cargo makes this ***super easy***!
|
||||
|
||||
```
|
||||
cargo install --git https://github.com/azerupi/mdBook.git
|
||||
cargo install --git https://github.com/rust-lang-nursery/mdBook.git
|
||||
```
|
||||
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
@@ -63,7 +63,7 @@ There are multiple ways to install mdBook.
|
||||
If you want to contribute to mdBook you will have to clone the repository on your local machine:
|
||||
|
||||
```
|
||||
git clone https://github.com/azerupi/mdBook.git
|
||||
git clone https://github.com/rust-lang-nursery/mdBook.git
|
||||
```
|
||||
`cd` into `mdBook/` and run
|
||||
|
||||
@@ -79,7 +79,7 @@ There are multiple ways to install mdBook.
|
||||
|
||||
mdBook will primarily be used as a command line tool, even though it exposes all its functionality as a Rust crate for integration in other projects.
|
||||
|
||||
Here are the main commands you will want to run. For a more exhaustive explanation, check out the [documentation](http://azerupi.github.io/mdBook/).
|
||||
Here are the main commands you will want to run. For a more exhaustive explanation, check out the [User Guide](http://rust-lang-nursery.github.io/mdBook/).
|
||||
|
||||
- `mdbook init`
|
||||
|
||||
@@ -95,7 +95,7 @@ Here are the main commands you will want to run. For a more exhaustive explanati
|
||||
|
||||
`book` and `src` are both directories. `src` contains the markdown files that will be used to render the output to the `book` directory.
|
||||
|
||||
Please, take a look at the [**Documentation**](http://azerupi.github.io/mdBook/cli/init.html) for more information and some neat tricks.
|
||||
Please, take a look at the [**Documentation**](http://rust-lang-nursery.github.io/mdBook/cli/init.html) for more information and some neat tricks.
|
||||
|
||||
- `mdbook build`
|
||||
|
||||
@@ -113,13 +113,13 @@ Here are the main commands you will want to run. For a more exhaustive explanati
|
||||
|
||||
Aside from the command line interface, this crate can also be used as a library. This means that you could integrate it in an existing project, like a web-app for example. Since the command line interface is just a wrapper around the library functionality, when you use this crate as a library you have full access to all the functionality of the command line interface with an easy to use API and more!
|
||||
|
||||
See the [Documentation](http://azerupi.github.io/mdBook/lib/lib.html) and the [API docs](http://azerupi.github.io/mdBook/mdbook/index.html) for more information.
|
||||
See the [User Guide](https://rust-lang-nursery.github.io/mdBook/) and the [API docs](https://docs.rs/mdbook/*/mdbook/) for more information.
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are highly appreciated and encouraged! Don't hesitate to participate to discussions in the issues, propose new features and ask for help.
|
||||
|
||||
If you are just starting out with Rust, there are a series of issus that are tagged [E-Easy](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy) and **we will gladly mentor you** so that you can successfully go through the process of fixing a bug or adding a new feature! Let us know if you need any help.
|
||||
If you are just starting out with Rust, there are a series of issus that are tagged [E-Easy](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy) and **we will gladly mentor you** so that you can successfully go through the process of fixing a bug or adding a new feature! Let us know if you need any help.
|
||||
|
||||
For more info about contributing, check out our [contribution guide](CONTRIBUTING.md) who helps you go through the build and contribution process!
|
||||
|
||||
|
||||
@@ -4,12 +4,11 @@
|
||||
|
||||
What you are reading serves as an example of the output of mdBook and at the same time as a high-level documentation.
|
||||
|
||||
mdBook is free and open source, you can find the source code on [Github](https://github.com/azerupi/mdBook). Issues and feature requests can be posted on the [Github Issue tracker](https://github.com/azerupi/mdBook/issues).
|
||||
mdBook is free and open source, you can find the source code on [Github](https://github.com/rust-lang-nursery/mdBook). Issues and feature requests can be posted on the [Github Issue tracker](https://github.com/rust-lang-nursery/mdBook/issues).
|
||||
|
||||
## API docs
|
||||
|
||||
Alongside this book you can also read the [API docs](mdbook/index.html) generated by Rustdoc if you would like
|
||||
to use mdBook as a crate or write a new renderer and need a more low-level overview.
|
||||
Alongside this book you can also read the [API docs](https://docs.rs/mdbook/*/mdbook/) generated by Rustdoc if you would like to use mdBook as a crate or write a new renderer and need a more low-level overview.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -24,10 +24,10 @@ Run `mdbook help` in your terminal to verify if it works. Congratulations, you h
|
||||
|
||||
### Install Git version
|
||||
|
||||
The **[git version](https://github.com/azerupi/mdBook)** contains all the latest bug-fixes and features, that will be released in the next version on **Crates.io**, if you can't wait until the next release. You can build the git version yourself. Open your terminal and navigate to the directory of you choice. We need to clone the git repository and then build it with Cargo.
|
||||
The **[git version](https://github.com/rust-lang-nursery/mdBook)** contains all the latest bug-fixes and features, that will be released in the next version on **Crates.io**, if you can't wait until the next release. You can build the git version yourself. Open your terminal and navigate to the directory of you choice. We need to clone the git repository and then build it with Cargo.
|
||||
|
||||
```bash
|
||||
git clone --depth=1 https://github.com/azerupi/mdBook.git
|
||||
git clone --depth=1 https://github.com/rust-lang-nursery/mdBook.git
|
||||
cd mdBook
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
@@ -37,4 +37,4 @@ The `--dest-dir` (`-d`) option allows you to change the output directory for you
|
||||
|
||||
-----
|
||||
|
||||
***note:*** *the `serve` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/azerupi/mdBook/issues)*
|
||||
***note:*** *the `serve` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/rust-lang-nursery/mdBook/issues)*
|
||||
|
||||
@@ -23,4 +23,4 @@ The `--dest-dir` (`-d`) option allows you to change the output directory for you
|
||||
|
||||
-----
|
||||
|
||||
***note:*** *the `watch` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/azerupi/mdBook/issues)*
|
||||
***note:*** *the `watch` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/rust-lang-nursery/mdBook/issues)*
|
||||
|
||||
@@ -2,20 +2,19 @@
|
||||
|
||||
You can configure the parameters for your book in the ***book.toml*** file.
|
||||
|
||||
>**Note:**
|
||||
JSON configuration files were previously supported but have been deprecated in favor of
|
||||
the TOML configuration file. If you are still using JSON we strongly encourage you to migrate to
|
||||
the TOML configuration because JSON support will be removed in the future.
|
||||
|
||||
Here is an example of what a ***book.toml*** file might look like:
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
author = "John Doe"
|
||||
description = "The example book covers examples."
|
||||
|
||||
[build]
|
||||
build-dir = "my-example-book"
|
||||
create-missing = false
|
||||
|
||||
[output.html]
|
||||
destination = "my-example-book"
|
||||
additional-css = ["custom.css"]
|
||||
```
|
||||
|
||||
@@ -24,66 +23,133 @@ additional-css = ["custom.css"]
|
||||
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.
|
||||
|
||||
- **title:** The title of the book
|
||||
- **author:** The author of the book
|
||||
- **description:** A description for the book, which is added as meta information in the html `<head>` of each page
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
title = "Example book"
|
||||
author = "John Doe"
|
||||
description = "The example book covers examples."
|
||||
```
|
||||
|
||||
Some books may have multiple authors, there is an alternative key called `authors` plural that lets you specify an array
|
||||
of authors.
|
||||
- **authors:** The author(s) of the book
|
||||
- **description:** A description for the book, which is added as meta
|
||||
information in the html `<head>` of each page
|
||||
- **src:** By default, the source directory is found in the directory named
|
||||
`src` directly under the root folder. But this is configurable with the `src`
|
||||
key in the configuration file.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
|
||||
```
|
||||
|
||||
### Source directory
|
||||
By default, the source directory is found in the directory named `src` directly under the root folder. But this is configurable
|
||||
with the `source` key in the configuration file.
|
||||
### Build options
|
||||
|
||||
This controls the build process of your book.
|
||||
|
||||
- **build-dir:** The directory to put the rendered book in. By default this is
|
||||
`book/` in the book's root directory.
|
||||
- **create-missing:** By default, any missing files specified in `SUMMARY.md`
|
||||
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.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
source = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
|
||||
[build]
|
||||
build-dir = "build"
|
||||
create-missing = false
|
||||
```
|
||||
|
||||
### HTML renderer options
|
||||
The HTML renderer has a couple of options aswell. All the options for the renderer need to be specified under the TOML table `[output.html]`.
|
||||
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]`.
|
||||
|
||||
The following configuration options are available:
|
||||
|
||||
- **`destination`:** By default, the HTML book will be rendered in the `root/book` directory, but this option lets you specify another
|
||||
destination fodler.
|
||||
- **`theme`:** mdBook comes with a default theme and all the resource files needed for it. But if this option is set, mdBook will selectively overwrite the theme files with the ones found in the specified folder.
|
||||
- **`curly-quotes`:** Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans. Defaults to `false`.
|
||||
- **`google-analytics`:** If you use Google Analytics, this option lets you enable it by simply specifying your ID in the configuration file.
|
||||
- **`additional-css`:** If you need to slightly change the appearance of your book without overwriting the whole style, you can specify a set of stylesheets that will be loaded after the default ones where you can surgically change the style.
|
||||
- **`additional-js`:** If you need to add some behaviour to your book without removing the current behaviour, you can specify a set of javascript files that will be loaded alongside the default one.
|
||||
pub playpen: Playpen,
|
||||
|
||||
- **theme:** mdBook comes with a default theme and all the resource files
|
||||
needed for it. But if this option is set, mdBook will selectively overwrite
|
||||
the theme files with the ones found in the specified folder.
|
||||
- **curly-quotes:** Convert straight quotes to curly quotes, except for
|
||||
those that occur in code blocks and code spans. Defaults to `false`.
|
||||
- **google-analytics:** If you use Google Analytics, this option lets you
|
||||
enable it by simply specifying your ID in the configuration file.
|
||||
- **additional-css:** If you need to slightly change the appearance of your
|
||||
book without overwriting the whole style, you can specify a set of
|
||||
stylesheets that will be loaded after the default ones where you can
|
||||
surgically change the style.
|
||||
- **additional-js:** If you need to add some behaviour to your book without
|
||||
removing the current behaviour, you can specify a set of javascript files
|
||||
that will be loaded alongside the default one.
|
||||
- **playpen:** A subtable for configuring various playpen settings.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
[output.html]
|
||||
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
|
||||
theme = "my-theme"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
|
||||
[output.html.playpen]
|
||||
editor = "./path/to/editor"
|
||||
editable = false
|
||||
```
|
||||
|
||||
|
||||
## For Developers
|
||||
|
||||
If you are developing a plugin or alternate backend then whenever your code is
|
||||
called you will almost certainly be passed a reference to the book's `Config`.
|
||||
This can be treated roughly as a nested hashmap which lets you call methods like
|
||||
`get()` and `get_mut()` to get access to the config's contents.
|
||||
|
||||
By convention, plugin developers will have their settings as a subtable inside
|
||||
`plugins` (e.g. a link checker would put its settings in `plugins.link_check`)
|
||||
and backends should put their configuration under `output`, like the HTML
|
||||
renderer does in the previous examples.
|
||||
|
||||
As an example, some hypothetical `random` renderer would typically want to load
|
||||
its settings from the `Config` at the very start of its rendering process. The
|
||||
author can take advantage of serde to deserialize the generic `toml::Value`
|
||||
object retrieved from `Config` into a struct specific to its use case.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
struct RandomOutput {
|
||||
foo: u32,
|
||||
bar: String,
|
||||
baz: Vec<bool>,
|
||||
}
|
||||
|
||||
let src = r#"
|
||||
[output.random]
|
||||
foo = 5
|
||||
bar = "Hello World"
|
||||
baz = [true, true, false]
|
||||
"#;
|
||||
|
||||
let book_config = Config::from_str(src)?; // usually passed in by mdbook
|
||||
let random: Value = book_config.get("output.random").unwrap_or_default();
|
||||
let got: RandomOutput = random.try_into()?;
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
if let Some(baz) = book_config.get_deserialized::<Vec<bool>>("output.random.baz") {
|
||||
println!("{:?}", baz); // prints [true, true, false]
|
||||
|
||||
// do something interesting with baz
|
||||
}
|
||||
|
||||
// start the rendering process
|
||||
```
|
||||
|
||||
@@ -87,5 +87,5 @@ In addition to the properties you can access, there are some handlebars helpers
|
||||
|
||||
------
|
||||
|
||||
*If you would like me to expose other properties or helpers, please [create a new issue](https://github.com/azerupi/mdBook/issues)
|
||||
*If you would like me to expose other properties or helpers, please [create a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
|
||||
and I will consider it.*
|
||||
|
||||
@@ -53,7 +53,7 @@ Will render as
|
||||
## Improve default theme
|
||||
|
||||
If you think the default theme doesn't look quite right for a specific language, or could be improved.
|
||||
Feel free to [submit a new issue](https://github.com/azerupi/mdBook/issues) explaining what you have in mind and I will take a look at it.
|
||||
Feel free to [submit a new issue](https://github.com/rust-lang-nursery/mdBook/issues) explaining what you have in mind and I will take a look at it.
|
||||
|
||||
You could also create a pull-request with the proposed improvements.
|
||||
|
||||
|
||||
@@ -11,3 +11,5 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add
|
||||
- Wayne Nilsen ([waynenilsen](https://github.com/waynenilsen))
|
||||
- [funnkill](https://github.com/funkill)
|
||||
- Fu Gangqiang ([FuGangqiang](https://github.com/FuGangqiang))
|
||||
- [Michael-F-Bryan](https://github.com/Michael-F-Bryan)
|
||||
- [Chris Spiegel](https://github.com/cspiegel)
|
||||
|
||||
40
build.rs
40
build.rs
@@ -32,26 +32,23 @@ error_chain!{
|
||||
}
|
||||
|
||||
fn program_exists(program: &str) -> Result<()> {
|
||||
execs::cmd(program)
|
||||
.arg("-v")
|
||||
.output()
|
||||
.chain_err(|| format!("Please install '{}'!", program))?;
|
||||
execs::cmd(program).arg("-v")
|
||||
.output()
|
||||
.chain_err(|| format!("Please install '{}'!", program))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn npm_package_exists(package: &str) -> Result<()> {
|
||||
let status = execs::cmd("npm")
|
||||
.args(&["list", "-g"])
|
||||
.arg(package)
|
||||
.output();
|
||||
let status = execs::cmd("npm").args(&["list", "-g"])
|
||||
.arg(package)
|
||||
.output();
|
||||
|
||||
match status {
|
||||
Ok(ref out) if out.status.success() => Ok(()),
|
||||
_ => {
|
||||
bail!("Missing npm package '{0}' \
|
||||
install with: 'npm -g install {0}'",
|
||||
bail!("Missing npm package '{0}' install with: 'npm -g install {0}'",
|
||||
package)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +56,7 @@ pub enum Resource<'a> {
|
||||
Program(&'a str),
|
||||
Package(&'a str),
|
||||
}
|
||||
use Resource::{Program, Package};
|
||||
use Resource::{Package, Program};
|
||||
|
||||
impl<'a> Resource<'a> {
|
||||
pub fn exists(&self) -> Result<()> {
|
||||
@@ -71,7 +68,6 @@ impl<'a> Resource<'a> {
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
|
||||
if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") {
|
||||
// Check dependencies
|
||||
Program("npm").exists()?;
|
||||
@@ -85,15 +81,15 @@ fn run() -> Result<()> {
|
||||
let theme_dir = Path::new(&manifest_dir).join("src/theme/");
|
||||
let stylus_dir = theme_dir.join("stylus/book.styl");
|
||||
|
||||
if !execs::cmd("stylus")
|
||||
.arg(stylus_dir)
|
||||
.arg("--out")
|
||||
.arg(theme_dir)
|
||||
.arg("--use")
|
||||
.arg("nib")
|
||||
.status()?
|
||||
.success() {
|
||||
bail!("Stylus encoutered an error");
|
||||
if !execs::cmd("stylus").arg(stylus_dir)
|
||||
.arg("--out")
|
||||
.arg(theme_dir)
|
||||
.arg("--use")
|
||||
.arg("nib")
|
||||
.status()?
|
||||
.success()
|
||||
{
|
||||
bail!("Stylus encountered an error");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -28,7 +28,7 @@ git init
|
||||
git config user.name "Mathieu David"
|
||||
git config user.email "mathieudavid@mathieudavid.org"
|
||||
|
||||
git remote add upstream "https://$GH_TOKEN@github.com/azerupi/mdBook.git"
|
||||
git remote add upstream "https://$GH_TOKEN@github.com/rust-lang-nursery/mdBook.git"
|
||||
git fetch upstream
|
||||
git reset upstream/gh-pages
|
||||
|
||||
|
||||
21
rustfmt.toml
21
rustfmt.toml
@@ -1,16 +1,7 @@
|
||||
write_mode = "Overwrite"
|
||||
array_layout = "Visual"
|
||||
chain_indent = "Visual"
|
||||
fn_args_layout = "Visual"
|
||||
fn_call_style = "Visual"
|
||||
format_strings = true
|
||||
generics_indent = "Visual"
|
||||
|
||||
max_width = 120
|
||||
ideal_width = 120
|
||||
fn_call_width = 100
|
||||
|
||||
fn_args_density = "Compressed"
|
||||
|
||||
enum_trailing_comma = true
|
||||
match_block_trailing_comma = true
|
||||
struct_trailing_comma = "Always"
|
||||
wrap_comments = true
|
||||
use_try_shorthand = true
|
||||
|
||||
report_todo = "Always"
|
||||
report_fixme = "Always"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use clap::{ArgMatches, SubCommand, App};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
@@ -8,28 +9,31 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("build")
|
||||
.about("Build the book from the markdown files")
|
||||
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
|
||||
.arg_from_usage("--no-create 'Will not create non-existent files linked from SUMMARY.md'")
|
||||
.arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'")
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'The output directory for your \
|
||||
book{n}(Defaults to ./book when omitted)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"--no-create 'Will not create non-existent files linked from SUMMARY.md (deprecated: use book.toml instead)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'A directory for your book{n}(Defaults to Current Directory \
|
||||
when omitted)'",
|
||||
)
|
||||
}
|
||||
|
||||
// Build command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::new(&book_dir).read_config()?;
|
||||
let mut book = MDBook::new(&book_dir).read_config()?;
|
||||
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.with_destination(dest_dir),
|
||||
None => book,
|
||||
};
|
||||
|
||||
if args.is_present("no-create") {
|
||||
book.create_missing = false;
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = PathBuf::from(dest_dir);
|
||||
}
|
||||
|
||||
if args.is_present("curly-quotes") {
|
||||
book = book.with_curly_quotes(true);
|
||||
// This flag is deprecated in favor of being set via `book.toml`.
|
||||
if args.is_present("no-create") {
|
||||
book.config.build.create_missing = false;
|
||||
}
|
||||
|
||||
book.build()?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use clap::{ArgMatches, SubCommand, App};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use get_book_dir;
|
||||
@@ -10,14 +10,14 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("init")
|
||||
.about("Create boilerplate structure and files in the directory")
|
||||
// the {n} denotes a newline which will properly aligned in all help messages
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory \
|
||||
when omitted)'")
|
||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
||||
.arg_from_usage("--force 'skip confirmation prompts'")
|
||||
}
|
||||
|
||||
// Init command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir);
|
||||
|
||||
@@ -26,7 +26,6 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
|
||||
// If flag `--theme` is present, copy theme to src
|
||||
if args.is_present("theme") {
|
||||
|
||||
// Skip this if `--force` is present
|
||||
if !args.is_present("force") {
|
||||
// Print warning
|
||||
@@ -45,11 +44,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
// Call the function that copies the theme
|
||||
book.copy_theme()?;
|
||||
println!("\nTheme copied.");
|
||||
|
||||
}
|
||||
|
||||
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`
|
||||
let is_dest_inside_root = book.get_destination().starts_with(book.get_root());
|
||||
let is_dest_inside_root = book.get_destination().starts_with(&book.root);
|
||||
|
||||
if !args.is_present("force") && is_dest_inside_root {
|
||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
extern crate mdbook;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate log;
|
||||
extern crate env_logger;
|
||||
extern crate log;
|
||||
extern crate mdbook;
|
||||
extern crate open;
|
||||
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use clap::{App, ArgMatches, AppSettings};
|
||||
use log::{LogRecord, LogLevelFilter};
|
||||
use clap::{App, AppSettings, ArgMatches};
|
||||
use log::{LogLevelFilter, LogRecord};
|
||||
use env_logger::LogBuilder;
|
||||
|
||||
pub mod build;
|
||||
@@ -33,7 +33,10 @@ fn main() {
|
||||
// Get the version from our Cargo.toml using clap's crate_version!() macro
|
||||
.version(concat!("v",crate_version!()))
|
||||
.setting(AppSettings::SubcommandRequired)
|
||||
.after_help("For more information about a specific command, try `mdbook <command> --help`\nSource code for mdbook available at: https://github.com/azerupi/mdBook")
|
||||
.after_help("For more information about a specific command, \
|
||||
try `mdbook <command> --help`\n\
|
||||
Source code for mdbook available \
|
||||
at: https://github.com/rust-lang-nursery/mdBook")
|
||||
.subcommand(init::make_subcommand())
|
||||
.subcommand(build::make_subcommand())
|
||||
.subcommand(test::make_subcommand());
|
||||
@@ -71,7 +74,7 @@ fn init_logger() {
|
||||
builder.format(format).filter(None, LogLevelFilter::Info);
|
||||
|
||||
if let Ok(var) = env::var("RUST_LOG") {
|
||||
builder.parse(&var);
|
||||
builder.parse(&var);
|
||||
}
|
||||
|
||||
builder.init().unwrap();
|
||||
|
||||
@@ -3,9 +3,10 @@ extern crate staticfile;
|
||||
extern crate ws;
|
||||
|
||||
use std;
|
||||
use std::path::Path;
|
||||
use self::iron::{Iron, AfterMiddleware, IronResult, IronError, Request, Response, status, Set, Chain};
|
||||
use clap::{ArgMatches, SubCommand, App};
|
||||
use std::path::PathBuf;
|
||||
use self::iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response,
|
||||
Set};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use {get_book_dir, open};
|
||||
@@ -17,14 +18,29 @@ struct ErrorRecover;
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("serve")
|
||||
.about("Serve the book at http://localhost:3000. Rebuild and reload on change.")
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")
|
||||
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
|
||||
.arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'")
|
||||
.about(
|
||||
"Serve the book at http://localhost:3000. Rebuild and reload on change.",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'A directory for your book{n}(Defaults to \
|
||||
Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'The output directory for \
|
||||
your book{n}(Defaults to ./book when omitted)'",
|
||||
)
|
||||
.arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
|
||||
.arg_from_usage("-w, --websocket-port=[ws-port] 'Use another port for the websocket connection (livereload){n}(Defaults to 3001)'")
|
||||
.arg_from_usage("-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'")
|
||||
.arg_from_usage("-a, --address=[address] 'Address that the browser can reach the websocket server from{n}(Defaults to the interface address)'")
|
||||
.arg_from_usage(
|
||||
"-w, --websocket-port=[ws-port] 'Use another port for the \
|
||||
websocket connection (livereload){n}(Defaults to 3001)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"-a, --address=[address] 'Address that the browser can reach the \
|
||||
websocket server from{n}(Defaults to the interface address)'",
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Open the book server in a web browser'")
|
||||
}
|
||||
|
||||
@@ -33,15 +49,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
const RELOAD_COMMAND: &'static str = "reload";
|
||||
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::new(&book_dir).read_config()?;
|
||||
let mut book = MDBook::new(&book_dir).read_config()?;
|
||||
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.with_destination(Path::new(dest_dir)),
|
||||
None => book,
|
||||
};
|
||||
|
||||
if args.is_present("curly-quotes") {
|
||||
book = book.with_curly_quotes(true);
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = PathBuf::from(dest_dir);
|
||||
}
|
||||
|
||||
let port = args.value_of("port").unwrap_or("3000");
|
||||
@@ -53,7 +64,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let address = format!("{}:{}", interface, port);
|
||||
let ws_address = format!("{}:{}", interface, ws_port);
|
||||
|
||||
book.set_livereload(format!(r#"
|
||||
book.livereload = Some(format!(r#"
|
||||
<script type="text/javascript">
|
||||
var socket = new WebSocket("ws://{}:{}");
|
||||
socket.onmessage = function (event) {{
|
||||
@@ -68,9 +79,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
}}
|
||||
</script>
|
||||
"#,
|
||||
public_address,
|
||||
ws_port,
|
||||
RELOAD_COMMAND));
|
||||
public_address,
|
||||
ws_port,
|
||||
RELOAD_COMMAND
|
||||
));
|
||||
|
||||
book.build()?;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use clap::{ArgMatches, SubCommand, App};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use get_book_dir;
|
||||
@@ -7,12 +7,16 @@ use get_book_dir;
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("test")
|
||||
.about("Test that code samples compile")
|
||||
.arg_from_usage("-L, --library-path [DIR]... 'directory to add to crate search path'")
|
||||
.arg_from_usage(
|
||||
"-L, --library-path [DIR]... 'directory to add to crate search path'",
|
||||
)
|
||||
}
|
||||
|
||||
// test command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let library_paths: Vec<&str> = args.values_of("library-path").map(|v| v.collect()).unwrap_or_default();
|
||||
let library_paths: Vec<&str> = args.values_of("library-path")
|
||||
.map(|v| v.collect())
|
||||
.unwrap_or_default();
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir).read_config()?;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
extern crate notify;
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use self::notify::Watcher;
|
||||
use std::time::Duration;
|
||||
use std::sync::mpsc::channel;
|
||||
use clap::{ArgMatches, SubCommand, App};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::MDBook;
|
||||
use mdbook::errors::Result;
|
||||
use {get_book_dir, open};
|
||||
@@ -14,23 +14,23 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("watch")
|
||||
.about("Watch the files for changes")
|
||||
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
|
||||
.arg_from_usage("--curly-quotes 'Convert straight quotes to curly quotes, except for those that occur in code blocks and code spans'")
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'The output directory for \
|
||||
your book{n}(Defaults to ./book when omitted)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'A directory for your book{n}(Defaults to \
|
||||
Current Directory when omitted)'",
|
||||
)
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::new(&book_dir).read_config()?;
|
||||
let mut book = MDBook::new(&book_dir).read_config()?;
|
||||
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.with_destination(dest_dir),
|
||||
None => book,
|
||||
};
|
||||
|
||||
if args.is_present("curly-quotes") {
|
||||
book = book.with_curly_quotes(true);
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = PathBuf::from(dest_dir);
|
||||
}
|
||||
|
||||
if args.is_present("open") {
|
||||
@@ -51,7 +51,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
|
||||
// Calls the closure when a book source file is changed. This is blocking!
|
||||
pub fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
||||
where F: Fn(&Path, &mut MDBook) -> ()
|
||||
where
|
||||
F: Fn(&Path, &mut MDBook) -> (),
|
||||
{
|
||||
use self::notify::RecursiveMode::*;
|
||||
use self::notify::DebouncedEvent::*;
|
||||
@@ -63,8 +64,8 @@ pub fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
println!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
::std::process::exit(0);
|
||||
},
|
||||
::std::process::exit(0)
|
||||
}
|
||||
};
|
||||
|
||||
// Add the source directory to the watcher
|
||||
@@ -74,19 +75,19 @@ pub fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
||||
};
|
||||
|
||||
// Add the theme directory to the watcher
|
||||
watcher.watch(book.get_theme_path(), Recursive).unwrap_or_default();
|
||||
|
||||
watcher.watch(book.theme_dir(), Recursive)
|
||||
.unwrap_or_default();
|
||||
|
||||
// Add the book.{json,toml} file to the watcher if it exists, because it's not
|
||||
// located in the source directory
|
||||
if watcher
|
||||
.watch(book.get_root().join("book.json"), NonRecursive)
|
||||
.is_err() {
|
||||
if watcher.watch(book.root.join("book.json"), NonRecursive)
|
||||
.is_err()
|
||||
{
|
||||
// do nothing if book.json is not found
|
||||
}
|
||||
if watcher
|
||||
.watch(book.get_root().join("book.toml"), NonRecursive)
|
||||
.is_err() {
|
||||
if watcher.watch(book.root.join("book.toml"), NonRecursive)
|
||||
.is_err()
|
||||
{
|
||||
// do nothing if book.toml is not found
|
||||
}
|
||||
|
||||
@@ -96,18 +97,15 @@ pub fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
||||
match rx.recv() {
|
||||
Ok(event) => {
|
||||
match event {
|
||||
Create(path) |
|
||||
Write(path) |
|
||||
Remove(path) |
|
||||
Rename(_, path) => {
|
||||
Create(path) | Write(path) | Remove(path) | Rename(_, path) => {
|
||||
closure(&path, book);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
println!("An error occured: {:?}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ pub struct BookItems<'a> {
|
||||
|
||||
impl Chapter {
|
||||
pub fn new(name: String, path: PathBuf) -> Self {
|
||||
|
||||
Chapter {
|
||||
name: name,
|
||||
path: path,
|
||||
@@ -39,7 +38,8 @@ impl Chapter {
|
||||
|
||||
impl Serialize for Chapter {
|
||||
fn serialize<S>(&self, serializer: S) -> ::std::result::Result<S::Ok, S::Error>
|
||||
where S: Serializer
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut struct_ = serializer.serialize_struct("Chapter", 2)?;
|
||||
struct_.serialize_field("name", &self.name)?;
|
||||
@@ -63,21 +63,20 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
Some((parent_items, parent_idx)) => {
|
||||
self.items = parent_items;
|
||||
self.current_index = parent_idx + 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let cur = &self.items[self.current_index];
|
||||
|
||||
match *cur {
|
||||
BookItem::Chapter(_, ref ch) |
|
||||
BookItem::Affix(ref ch) => {
|
||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
|
||||
self.stack.push((self.items, self.current_index));
|
||||
self.items = &ch.sub_items[..];
|
||||
self.current_index = 0;
|
||||
},
|
||||
}
|
||||
BookItem::Spacer => {
|
||||
self.current_index += 1;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return Some(cur);
|
||||
|
||||
305
src/book/mod.rs
305
src/book/mod.rs
@@ -4,31 +4,25 @@ pub use self::bookitem::{BookItem, BookItems};
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use std::io::Write;
|
||||
use std::process::Command;
|
||||
use tempdir::TempDir;
|
||||
|
||||
use {theme, parse, utils};
|
||||
use renderer::{Renderer, HtmlHandlebars};
|
||||
use {parse, theme, utils};
|
||||
use renderer::{HtmlHandlebars, Renderer};
|
||||
use preprocess;
|
||||
use errors::*;
|
||||
|
||||
use config::BookConfig;
|
||||
use config::tomlconfig::TomlConfig;
|
||||
use config::htmlconfig::HtmlConfig;
|
||||
use config::jsonconfig::JsonConfig;
|
||||
use config::Config;
|
||||
|
||||
pub struct MDBook {
|
||||
config: BookConfig,
|
||||
pub root: PathBuf,
|
||||
pub config: Config,
|
||||
|
||||
pub content: Vec<BookItem>,
|
||||
renderer: Box<Renderer>,
|
||||
|
||||
livereload: Option<String>,
|
||||
|
||||
/// Should `mdbook build` create files referenced from SUMMARY.md if they
|
||||
/// don't exist
|
||||
pub create_missing: bool,
|
||||
pub livereload: Option<String>,
|
||||
}
|
||||
|
||||
impl MDBook {
|
||||
@@ -60,20 +54,19 @@ impl MDBook {
|
||||
/// [`set_dest()`](#method.set_dest)
|
||||
|
||||
pub fn new<P: Into<PathBuf>>(root: P) -> MDBook {
|
||||
|
||||
let root = root.into();
|
||||
if !root.exists() || !root.is_dir() {
|
||||
warn!("{:?} No directory with that name", root);
|
||||
}
|
||||
|
||||
MDBook {
|
||||
config: BookConfig::new(root),
|
||||
root: root,
|
||||
config: Config::default(),
|
||||
|
||||
content: vec![],
|
||||
renderer: Box::new(HtmlHandlebars::new()),
|
||||
|
||||
livereload: None,
|
||||
create_missing: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,33 +123,33 @@ impl MDBook {
|
||||
/// `chapter_1.md` to the source directory.
|
||||
|
||||
pub fn init(&mut self) -> Result<()> {
|
||||
|
||||
debug!("[fn]: init");
|
||||
|
||||
if !self.config.get_root().exists() {
|
||||
fs::create_dir_all(&self.config.get_root()).unwrap();
|
||||
info!("{:?} created", &self.config.get_root());
|
||||
if !self.root.exists() {
|
||||
fs::create_dir_all(&self.root).unwrap();
|
||||
info!("{:?} created", self.root.display());
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
if !self.get_destination().exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", self.get_destination());
|
||||
fs::create_dir_all(self.get_destination())?;
|
||||
let dest = self.get_destination();
|
||||
if !dest.exists() {
|
||||
debug!("[*]: {} does not exist, trying to create directory", dest.display());
|
||||
fs::create_dir_all(dest)?;
|
||||
}
|
||||
|
||||
|
||||
if !self.config.get_source().exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", self.config.get_source());
|
||||
fs::create_dir_all(self.config.get_source())?;
|
||||
let src = self.get_source();
|
||||
if !src.exists() {
|
||||
debug!("[*]: {} does not exist, trying to create directory", src.display());
|
||||
fs::create_dir_all(&src)?;
|
||||
}
|
||||
|
||||
let summary = self.config.get_source().join("SUMMARY.md");
|
||||
let summary = src.join("SUMMARY.md");
|
||||
|
||||
if !summary.exists() {
|
||||
|
||||
// Summary does not exist, create it
|
||||
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", &summary);
|
||||
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md",
|
||||
&summary);
|
||||
let mut f = File::create(&summary)?;
|
||||
|
||||
debug!("[*]: Writing to SUMMARY.md");
|
||||
@@ -175,16 +168,16 @@ impl MDBook {
|
||||
debug!("[*]: item: {:?}", item);
|
||||
let ch = match *item {
|
||||
BookItem::Spacer => continue,
|
||||
BookItem::Chapter(_, ref ch) |
|
||||
BookItem::Affix(ref ch) => ch,
|
||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => ch,
|
||||
};
|
||||
if !ch.path.as_os_str().is_empty() {
|
||||
let path = self.config.get_source().join(&ch.path);
|
||||
let path = self.get_source().join(&ch.path);
|
||||
|
||||
if !path.exists() {
|
||||
if !self.create_missing {
|
||||
return Err(format!("'{}' referenced from SUMMARY.md does not exist.", path.to_string_lossy())
|
||||
.into());
|
||||
if !self.config.build.create_missing {
|
||||
return Err(
|
||||
format!("'{}' referenced from SUMMARY.md does not exist.", path.to_string_lossy()).into(),
|
||||
);
|
||||
}
|
||||
debug!("[*]: {:?} does not exist, trying to create file", path);
|
||||
::std::fs::create_dir_all(path.parent().unwrap())?;
|
||||
@@ -203,16 +196,17 @@ impl MDBook {
|
||||
pub fn create_gitignore(&self) {
|
||||
let gitignore = self.get_gitignore();
|
||||
|
||||
let destination = self.config.get_html_config()
|
||||
.get_destination();
|
||||
|
||||
// Check that the gitignore does not extist and that the destination path begins with the root path
|
||||
// We assume tha if it does begin with the root path it is contained within. This assumption
|
||||
// will not hold true for paths containing double dots to go back up e.g. `root/../destination`
|
||||
if !gitignore.exists() && destination.starts_with(self.config.get_root()) {
|
||||
let destination = self.get_destination();
|
||||
|
||||
// Check that the gitignore does not extist and that the destination path
|
||||
// begins with the root path
|
||||
// We assume tha if it does begin with the root path it is contained within.
|
||||
// This assumption
|
||||
// will not hold true for paths containing double dots to go back up e.g.
|
||||
// `root/../destination`
|
||||
if !gitignore.exists() && destination.starts_with(&self.root) {
|
||||
let relative = destination
|
||||
.strip_prefix(self.config.get_root())
|
||||
.strip_prefix(&self.root)
|
||||
.expect("Could not strip the root prefix, path is not relative to root")
|
||||
.to_str()
|
||||
.expect("Could not convert to &str");
|
||||
@@ -239,59 +233,63 @@ impl MDBook {
|
||||
self.init()?;
|
||||
|
||||
// Clean output directory
|
||||
utils::fs::remove_dir_content(self.config.get_html_config().get_destination())?;
|
||||
utils::fs::remove_dir_content(&self.get_destination())?;
|
||||
|
||||
self.renderer.render(self)
|
||||
}
|
||||
|
||||
|
||||
pub fn get_gitignore(&self) -> PathBuf {
|
||||
self.config.get_root().join(".gitignore")
|
||||
self.root.join(".gitignore")
|
||||
}
|
||||
|
||||
pub fn copy_theme(&self) -> Result<()> {
|
||||
debug!("[fn]: copy_theme");
|
||||
|
||||
let themedir = self.config.get_html_config().get_theme();
|
||||
let themedir = self.theme_dir();
|
||||
|
||||
if !themedir.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", themedir);
|
||||
debug!("[*]: {:?} does not exist, trying to create directory",
|
||||
themedir);
|
||||
fs::create_dir(&themedir)?;
|
||||
}
|
||||
|
||||
// index.hbs
|
||||
let mut index = File::create(&themedir.join("index.hbs"))?;
|
||||
let mut index = File::create(themedir.join("index.hbs"))?;
|
||||
index.write_all(theme::INDEX)?;
|
||||
|
||||
// header.hbs
|
||||
let mut header = File::create(themedir.join("header.hbs"))?;
|
||||
header.write_all(theme::HEADER)?;
|
||||
|
||||
// book.css
|
||||
let mut css = File::create(&themedir.join("book.css"))?;
|
||||
let mut css = File::create(themedir.join("book.css"))?;
|
||||
css.write_all(theme::CSS)?;
|
||||
|
||||
// favicon.png
|
||||
let mut favicon = File::create(&themedir.join("favicon.png"))?;
|
||||
let mut favicon = File::create(themedir.join("favicon.png"))?;
|
||||
favicon.write_all(theme::FAVICON)?;
|
||||
|
||||
// book.js
|
||||
let mut js = File::create(&themedir.join("book.js"))?;
|
||||
let mut js = File::create(themedir.join("book.js"))?;
|
||||
js.write_all(theme::JS)?;
|
||||
|
||||
// highlight.css
|
||||
let mut highlight_css = File::create(&themedir.join("highlight.css"))?;
|
||||
let mut highlight_css = File::create(themedir.join("highlight.css"))?;
|
||||
highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
|
||||
|
||||
// highlight.js
|
||||
let mut highlight_js = File::create(&themedir.join("highlight.js"))?;
|
||||
let mut highlight_js = File::create(themedir.join("highlight.js"))?;
|
||||
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_file<P: AsRef<Path>>(&self, filename: P, content: &[u8]) -> Result<()> {
|
||||
let path = self.get_destination()
|
||||
.join(filename);
|
||||
let path = self.get_destination().join(filename);
|
||||
|
||||
utils::fs::create_file(&path)?
|
||||
.write_all(content)
|
||||
.map_err(|e| e.into())
|
||||
utils::fs::create_file(&path)?.write_all(content)
|
||||
.map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Parses the `book.json` file (if it exists) to extract
|
||||
@@ -300,26 +298,9 @@ impl MDBook {
|
||||
/// The root directory is the one specified when creating a new `MDBook`
|
||||
|
||||
pub fn read_config(mut self) -> Result<Self> {
|
||||
|
||||
let toml = self.get_root().join("book.toml");
|
||||
let json = self.get_root().join("book.json");
|
||||
|
||||
if toml.exists() {
|
||||
let mut file = File::open(toml)?;
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content)?;
|
||||
|
||||
let parsed_config = TomlConfig::from_toml(&content)?;
|
||||
self.config.fill_from_tomlconfig(parsed_config);
|
||||
} else if json.exists() {
|
||||
warn!("The JSON configuration file is deprecated, please use the TOML configuration.");
|
||||
let mut file = File::open(json)?;
|
||||
let mut content = String::new();
|
||||
file.read_to_string(&mut content)?;
|
||||
|
||||
let parsed_config = JsonConfig::from_json(&content)?;
|
||||
self.config.fill_from_jsonconfig(parsed_config);
|
||||
}
|
||||
let config_path = self.root.join("book.toml");
|
||||
debug!("[*] Loading the config from {}", config_path.display());
|
||||
self.config = Config::from_disk(&config_path)?;
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
@@ -356,34 +337,36 @@ impl MDBook {
|
||||
pub fn test(&mut self, library_paths: Vec<&str>) -> Result<()> {
|
||||
// read in the chapters
|
||||
self.parse_summary().chain_err(|| "Couldn't parse summary")?;
|
||||
let library_args: Vec<&str> = (0..library_paths.len()).map(|_| "-L")
|
||||
.zip(library_paths.into_iter())
|
||||
.flat_map(|x| vec![x.0, x.1])
|
||||
.collect();
|
||||
let library_args: Vec<&str> = (0..library_paths.len())
|
||||
.map(|_| "-L")
|
||||
.zip(library_paths.into_iter())
|
||||
.flat_map(|x| vec![x.0, x.1])
|
||||
.collect();
|
||||
let temp_dir = TempDir::new("mdbook")?;
|
||||
for item in self.iter() {
|
||||
|
||||
if let BookItem::Chapter(_, ref ch) = *item {
|
||||
if !ch.path.as_os_str().is_empty() {
|
||||
|
||||
let path = self.get_source().join(&ch.path);
|
||||
let base = path.parent().ok_or_else(
|
||||
|| String::from("Invalid bookitem 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
|
||||
// write preprocessed file to tempdir
|
||||
let path = temp_dir.path().join(&ch.path);
|
||||
let mut tmpf = utils::fs::create_file(&path)?;
|
||||
tmpf.write_all(content.as_bytes())?;
|
||||
|
||||
let output = Command::new("rustdoc").arg(&path).arg("--test").args(&library_args).output()?;
|
||||
let output = Command::new("rustdoc").arg(&path)
|
||||
.arg("--test")
|
||||
.args(&library_args)
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(ErrorKind::Subprocess("Rustdoc returned an error".to_string(), output));
|
||||
bail!(ErrorKind::Subprocess("Rustdoc returned an error".to_string(),
|
||||
output));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -391,132 +374,26 @@ impl MDBook {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_root(&self) -> &Path {
|
||||
self.config.get_root()
|
||||
}
|
||||
|
||||
|
||||
pub fn with_destination<T: Into<PathBuf>>(mut self, destination: T) -> Self {
|
||||
let root = self.config.get_root().to_owned();
|
||||
self.config.get_mut_html_config()
|
||||
.set_destination(&root, &destination.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn get_destination(&self) -> &Path {
|
||||
self.config.get_html_config()
|
||||
.get_destination()
|
||||
}
|
||||
|
||||
pub fn with_source<T: Into<PathBuf>>(mut self, source: T) -> Self {
|
||||
self.config.set_source(source);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_source(&self) -> &Path {
|
||||
self.config.get_source()
|
||||
}
|
||||
|
||||
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
|
||||
self.config.set_title(title);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_title(&self) -> &str {
|
||||
self.config.get_title()
|
||||
}
|
||||
|
||||
pub fn with_description<T: Into<String>>(mut self, description: T) -> Self {
|
||||
self.config.set_description(description);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_description(&self) -> &str {
|
||||
self.config.get_description()
|
||||
}
|
||||
|
||||
pub fn set_livereload(&mut self, livereload: String) -> &mut Self {
|
||||
self.livereload = Some(livereload);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unset_livereload(&mut self) -> &Self {
|
||||
self.livereload = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_livereload(&self) -> Option<&String> {
|
||||
self.livereload.as_ref()
|
||||
}
|
||||
|
||||
pub fn with_theme_path<T: Into<PathBuf>>(mut self, theme_path: T) -> Self {
|
||||
let root = self.config.get_root().to_owned();
|
||||
self.config.get_mut_html_config()
|
||||
.set_theme(&root, &theme_path.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_theme_path(&self) -> &Path {
|
||||
self.config.get_html_config()
|
||||
.get_theme()
|
||||
}
|
||||
|
||||
pub fn with_curly_quotes(mut self, curly_quotes: bool) -> Self {
|
||||
self.config.get_mut_html_config()
|
||||
.set_curly_quotes(curly_quotes);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_curly_quotes(&self) -> bool {
|
||||
self.config.get_html_config()
|
||||
.get_curly_quotes()
|
||||
}
|
||||
|
||||
pub fn with_mathjax_support(mut self, mathjax_support: bool) -> Self {
|
||||
self.config.get_mut_html_config()
|
||||
.set_mathjax_support(mathjax_support);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_mathjax_support(&self) -> bool {
|
||||
self.config.get_html_config()
|
||||
.get_mathjax_support()
|
||||
}
|
||||
|
||||
pub fn get_google_analytics_id(&self) -> Option<String> {
|
||||
self.config.get_html_config()
|
||||
.get_google_analytics_id()
|
||||
}
|
||||
|
||||
pub fn has_additional_js(&self) -> bool {
|
||||
self.config.get_html_config()
|
||||
.has_additional_js()
|
||||
}
|
||||
|
||||
pub fn get_additional_js(&self) -> &[PathBuf] {
|
||||
self.config.get_html_config()
|
||||
.get_additional_js()
|
||||
}
|
||||
|
||||
pub fn has_additional_css(&self) -> bool {
|
||||
self.config.get_html_config()
|
||||
.has_additional_css()
|
||||
}
|
||||
|
||||
pub fn get_additional_css(&self) -> &[PathBuf] {
|
||||
self.config.get_html_config()
|
||||
.get_additional_css()
|
||||
}
|
||||
|
||||
pub fn get_html_config(&self) -> &HtmlConfig {
|
||||
self.config.get_html_config()
|
||||
}
|
||||
|
||||
// Construct book
|
||||
fn parse_summary(&mut self) -> Result<()> {
|
||||
// When append becomes stable, use self.content.append() ...
|
||||
self.content = parse::construct_bookitems(&self.get_source().join("SUMMARY.md"))?;
|
||||
let summary = self.get_source().join("SUMMARY.md");
|
||||
self.content = parse::construct_bookitems(&summary)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_destination(&self) -> PathBuf {
|
||||
self.root.join(&self.config.build.build_dir)
|
||||
}
|
||||
|
||||
pub fn get_source(&self) -> PathBuf {
|
||||
self.root.join(&self.config.book.src)
|
||||
}
|
||||
|
||||
pub fn theme_dir(&self) -> PathBuf {
|
||||
match self.config.html_config().and_then(|h| h.theme) {
|
||||
Some(d) => self.root.join(d),
|
||||
None => self.root.join("theme"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
425
src/config.rs
Normal file
425
src/config.rs
Normal file
@@ -0,0 +1,425 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use toml::{self, Value};
|
||||
use toml::value::Table;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use errors::*;
|
||||
|
||||
/// The overall configuration object for MDBook.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct Config {
|
||||
/// Metadata about the book.
|
||||
pub book: BookConfig,
|
||||
pub build: BuildConfig,
|
||||
rest: Table,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load a `Config` from some string.
|
||||
pub fn from_str(src: &str) -> Result<Config> {
|
||||
toml::from_str(src).chain_err(|| Error::from("Invalid configuration file"))
|
||||
}
|
||||
|
||||
/// Load the configuration file from disk.
|
||||
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
|
||||
let mut buffer = String::new();
|
||||
File::open(config_file).chain_err(|| "Unable to open the configuration file")?
|
||||
.read_to_string(&mut buffer)
|
||||
.chain_err(|| "Couldn't read the file")?;
|
||||
|
||||
Config::from_str(&buffer)
|
||||
}
|
||||
|
||||
/// Fetch an arbitrary item from the `Config` as a `toml::Value`.
|
||||
///
|
||||
/// You can use dotted indices to access nested items (e.g.
|
||||
/// `output.html.playpen` will fetch the "playpen" out of the html output
|
||||
/// table).
|
||||
pub fn get(&self, key: &str) -> Option<&Value> {
|
||||
let pieces: Vec<_> = key.split(".").collect();
|
||||
recursive_get(&pieces, &self.rest)
|
||||
}
|
||||
|
||||
/// Fetch a value from the `Config` so you can mutate it.
|
||||
pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Value> {
|
||||
let pieces: Vec<_> = key.split(".").collect();
|
||||
recursive_get_mut(&pieces, &mut self.rest)
|
||||
}
|
||||
|
||||
/// Convenience method for getting the html renderer's configuration.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This is for compatibility only. It will be removed completely once the
|
||||
/// rendering and plugin system is established.
|
||||
pub fn html_config(&self) -> Option<HtmlConfig> {
|
||||
self.get_deserialized("output.html").ok()
|
||||
}
|
||||
|
||||
/// Convenience function to fetch a value from the config and deserialize it
|
||||
/// into some arbitrary type.
|
||||
pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
|
||||
let name = name.as_ref();
|
||||
|
||||
if let Some(value) = self.get(name) {
|
||||
value.clone()
|
||||
.try_into()
|
||||
.chain_err(|| "Couldn't deserialize the value")
|
||||
} else {
|
||||
bail!("Key not found, {:?}", name)
|
||||
}
|
||||
}
|
||||
|
||||
fn from_legacy(mut table: Table) -> Config {
|
||||
let mut cfg = Config::default();
|
||||
|
||||
// we use a macro here instead of a normal loop because the $out
|
||||
// variable can be different types. This way we can make type inference
|
||||
// figure out what try_into() deserializes to.
|
||||
macro_rules! get_and_insert {
|
||||
($table:expr, $key:expr => $out:expr) => {
|
||||
if let Some(value) = $table.remove($key).and_then(|v| v.try_into().ok()) {
|
||||
$out = value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get_and_insert!(table, "title" => cfg.book.title);
|
||||
get_and_insert!(table, "authors" => cfg.book.authors);
|
||||
get_and_insert!(table, "source" => cfg.book.src);
|
||||
get_and_insert!(table, "description" => cfg.book.description);
|
||||
|
||||
// This complicated chain of and_then's is so we can move
|
||||
// "output.html.destination" to "build.build_dir" and parse it into a
|
||||
// PathBuf.
|
||||
let destination: Option<PathBuf> = table.get_mut("output")
|
||||
.and_then(|output| output.as_table_mut())
|
||||
.and_then(|output| output.get_mut("html"))
|
||||
.and_then(|html| html.as_table_mut())
|
||||
.and_then(|html| html.remove("destination"))
|
||||
.and_then(|dest| dest.try_into().ok());
|
||||
|
||||
if let Some(dest) = destination {
|
||||
cfg.build.build_dir = dest;
|
||||
}
|
||||
|
||||
cfg.rest = table;
|
||||
cfg
|
||||
}
|
||||
}
|
||||
|
||||
fn recursive_get<'a>(key: &[&str], table: &'a Table) -> Option<&'a Value> {
|
||||
if key.is_empty() {
|
||||
return None;
|
||||
} else if key.len() == 1 {
|
||||
return table.get(key[0]);
|
||||
}
|
||||
|
||||
let first = key[0];
|
||||
let rest = &key[1..];
|
||||
|
||||
if let Some(&Value::Table(ref nested)) = table.get(first) {
|
||||
recursive_get(rest, nested)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn recursive_get_mut<'a>(key: &[&str], table: &'a mut Table) -> Option<&'a mut Value> {
|
||||
// TODO: Figure out how to abstract over mutability to reduce copy-pasta
|
||||
if key.is_empty() {
|
||||
return None;
|
||||
} else if key.len() == 1 {
|
||||
return table.get_mut(key[0]);
|
||||
}
|
||||
|
||||
let first = key[0];
|
||||
let rest = &key[1..];
|
||||
|
||||
if let Some(&mut Value::Table(ref mut nested)) = table.get_mut(first) {
|
||||
recursive_get_mut(rest, nested)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Config {
|
||||
fn deserialize<D: Deserializer<'de>>(de: D) -> ::std::result::Result<Self, D::Error> {
|
||||
let raw = Value::deserialize(de)?;
|
||||
|
||||
let mut table = match raw {
|
||||
Value::Table(t) => t,
|
||||
_ => {
|
||||
use serde::de::Error;
|
||||
return Err(D::Error::custom(
|
||||
"A config file should always be a toml table",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if is_legacy_format(&table) {
|
||||
warn!("It looks like you are using the legacy book.toml format.");
|
||||
warn!("We'll parse it for now, but you should probably convert to the new format.");
|
||||
warn!("See the mdbook documentation for more details, although as a rule of thumb");
|
||||
warn!("just move all top level configuration entries like `title`, `author` and");
|
||||
warn!("`description` under a table called `[book]`, move the `destination` entry");
|
||||
warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
|
||||
warn!("`[build]`, and it should all work.");
|
||||
warn!("Documentation: http://rust-lang-nursery.github.io/mdBook/format/config.html");
|
||||
return Ok(Config::from_legacy(table));
|
||||
}
|
||||
|
||||
let book: BookConfig = table.remove("book")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let build: BuildConfig = table.remove("build")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Config {
|
||||
book: book,
|
||||
build: build,
|
||||
rest: table,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn is_legacy_format(table: &Table) -> bool {
|
||||
let top_level_items = ["title", "author", "authors"];
|
||||
|
||||
top_level_items.iter().any(|key| table.contains_key(&key.to_string()))
|
||||
}
|
||||
|
||||
|
||||
/// Configuration options which are specific to the book and required for
|
||||
/// loading it from disk.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct BookConfig {
|
||||
/// The book's title.
|
||||
pub title: Option<String>,
|
||||
/// The book's authors.
|
||||
pub authors: Vec<String>,
|
||||
/// An optional description for the book.
|
||||
pub description: Option<String>,
|
||||
/// Location of the book source relative to the book's root directory.
|
||||
pub src: PathBuf,
|
||||
/// Does this book support more than one language?
|
||||
pub multilingual: bool,
|
||||
}
|
||||
|
||||
impl Default for BookConfig {
|
||||
fn default() -> BookConfig {
|
||||
BookConfig {
|
||||
title: None,
|
||||
authors: Vec::new(),
|
||||
description: None,
|
||||
src: PathBuf::from("src"),
|
||||
multilingual: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the build procedure.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct BuildConfig {
|
||||
/// Where to put built artefacts relative to the book's root directory.
|
||||
pub build_dir: PathBuf,
|
||||
/// Should non-existent markdown files specified in `SETTINGS.md` be created
|
||||
/// if they don't exist?
|
||||
pub create_missing: bool,
|
||||
}
|
||||
|
||||
impl Default for BuildConfig {
|
||||
fn default() -> BuildConfig {
|
||||
BuildConfig {
|
||||
build_dir: PathBuf::from("book"),
|
||||
create_missing: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct HtmlConfig {
|
||||
pub theme: Option<PathBuf>,
|
||||
pub curly_quotes: bool,
|
||||
pub mathjax_support: bool,
|
||||
pub google_analytics: Option<String>,
|
||||
pub additional_css: Vec<PathBuf>,
|
||||
pub additional_js: Vec<PathBuf>,
|
||||
pub playpen: Playpen,
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the the HTML renderer handles the playpen.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Playpen {
|
||||
pub editor: PathBuf,
|
||||
pub editable: bool,
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const COMPLEX_CONFIG: &'static str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
|
||||
description = "A completely useless book"
|
||||
multilingual = true
|
||||
src = "source"
|
||||
|
||||
[build]
|
||||
build-dir = "outputs"
|
||||
create-missing = false
|
||||
|
||||
[output.html]
|
||||
theme = "./themedir"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["./foo/bar/baz.css"]
|
||||
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
editor = "ace"
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn load_a_complex_config_file() {
|
||||
let src = COMPLEX_CONFIG;
|
||||
|
||||
let book_should_be = BookConfig {
|
||||
title: Some(String::from("Some Book")),
|
||||
authors: vec![String::from("Michael-F-Bryan <michaelfbryan@gmail.com>")],
|
||||
description: Some(String::from("A completely useless book")),
|
||||
multilingual: true,
|
||||
src: PathBuf::from("source"),
|
||||
..Default::default()
|
||||
};
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("outputs"),
|
||||
create_missing: false,
|
||||
};
|
||||
let playpen_should_be = Playpen {
|
||||
editable: true,
|
||||
editor: PathBuf::from("ace"),
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
|
||||
theme: Some(PathBuf::from("./themedir")),
|
||||
playpen: playpen_should_be,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
|
||||
assert_eq!(got.book, book_should_be);
|
||||
assert_eq!(got.build, build_should_be);
|
||||
assert_eq!(got.html_config().unwrap(), html_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_arbitrary_output_type() {
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
struct RandomOutput {
|
||||
foo: u32,
|
||||
bar: String,
|
||||
baz: Vec<bool>,
|
||||
}
|
||||
|
||||
let src = r#"
|
||||
[output.random]
|
||||
foo = 5
|
||||
bar = "Hello World"
|
||||
baz = [true, true, false]
|
||||
"#;
|
||||
|
||||
let should_be = RandomOutput {
|
||||
foo: 5,
|
||||
bar: String::from("Hello World"),
|
||||
baz: vec![true, true, false],
|
||||
};
|
||||
|
||||
let cfg = Config::from_str(src).unwrap();
|
||||
let got: RandomOutput = cfg.get_deserialized("output.random").unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
let baz: Vec<bool> = cfg.get_deserialized("output.random.baz").unwrap();
|
||||
let baz_should_be = vec![true, true, false];
|
||||
|
||||
assert_eq!(baz, baz_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutate_some_stuff() {
|
||||
// really this is just a sanity check to make sure the borrow checker
|
||||
// is happy...
|
||||
let src = COMPLEX_CONFIG;
|
||||
let mut config = Config::from_str(src).unwrap();
|
||||
let key = "output.html.playpen.editable";
|
||||
|
||||
assert_eq!(config.get(key).unwrap(), &Value::Boolean(true));
|
||||
*config.get_mut(key).unwrap() = Value::Boolean(false);
|
||||
assert_eq!(config.get(key).unwrap(), &Value::Boolean(false));
|
||||
}
|
||||
|
||||
/// The config file format has slightly changed (metadata stuff is now under
|
||||
/// the `book` table instead of being at the top level) so we're adding a
|
||||
/// **temporary** compatibility check. You should be able to still load the
|
||||
/// old format, emitting a warning.
|
||||
#[test]
|
||||
fn can_still_load_the_previous_format() {
|
||||
let src = r#"
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
source = "./source"
|
||||
|
||||
[output.html]
|
||||
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
|
||||
theme = "my-theme"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
"#;
|
||||
|
||||
let book_should_be = BookConfig {
|
||||
title: Some(String::from("mdBook Documentation")),
|
||||
description: Some(String::from(
|
||||
"Create book from markdown files. Like Gitbook but implemented in Rust",
|
||||
)),
|
||||
authors: vec![String::from("Mathieu David")],
|
||||
src: PathBuf::from("./source"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("my-book"),
|
||||
create_missing: true,
|
||||
};
|
||||
|
||||
let html_should_be = HtmlConfig {
|
||||
theme: Some(PathBuf::from("my-theme")),
|
||||
curly_quotes: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")],
|
||||
additional_js: vec![PathBuf::from("custom.js")],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.book, book_should_be);
|
||||
assert_eq!(got.build, build_should_be);
|
||||
assert_eq!(got.html_config().unwrap(), html_should_be);
|
||||
}
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
use std::path::{PathBuf, Path};
|
||||
|
||||
use super::HtmlConfig;
|
||||
use super::tomlconfig::TomlConfig;
|
||||
use super::jsonconfig::JsonConfig;
|
||||
|
||||
/// Configuration struct containing all the configuration options available in mdBook.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BookConfig {
|
||||
root: PathBuf,
|
||||
source: PathBuf,
|
||||
|
||||
title: String,
|
||||
authors: Vec<String>,
|
||||
description: String,
|
||||
|
||||
multilingual: bool,
|
||||
indent_spaces: i32,
|
||||
|
||||
html_config: HtmlConfig,
|
||||
}
|
||||
|
||||
impl BookConfig {
|
||||
/// Creates a new `BookConfig` struct with as root path the path given as parameter.
|
||||
/// The source directory is `root/src` and the destination for the rendered book is `root/book`.
|
||||
///
|
||||
/// ```
|
||||
/// # use std::path::PathBuf;
|
||||
/// # use mdbook::config::{BookConfig, HtmlConfig};
|
||||
/// #
|
||||
/// let root = PathBuf::from("directory/to/my/book");
|
||||
/// let config = BookConfig::new(&root);
|
||||
///
|
||||
/// assert_eq!(config.get_root(), &root);
|
||||
/// assert_eq!(config.get_source(), PathBuf::from("directory/to/my/book/src"));
|
||||
/// assert_eq!(config.get_html_config(), &HtmlConfig::new(PathBuf::from("directory/to/my/book")));
|
||||
/// ```
|
||||
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
|
||||
let root: PathBuf = root.into();
|
||||
let htmlconfig = HtmlConfig::new(&root);
|
||||
|
||||
BookConfig {
|
||||
root: root.clone(),
|
||||
source: root.join("src"),
|
||||
|
||||
title: String::new(),
|
||||
authors: Vec::new(),
|
||||
description: String::new(),
|
||||
|
||||
multilingual: false,
|
||||
indent_spaces: 4,
|
||||
|
||||
html_config: htmlconfig,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder method to set the source directory
|
||||
pub fn with_source<T: Into<PathBuf>>(mut self, source: T) -> Self {
|
||||
self.source = source.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder method to set the book's title
|
||||
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
|
||||
self.title = title.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder method to set the book's description
|
||||
pub fn with_description<T: Into<String>>(mut self, description: T) -> Self {
|
||||
self.description = description.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder method to set the book's authors
|
||||
pub fn with_authors<T: Into<Vec<String>>>(mut self, authors: T) -> Self {
|
||||
self.authors = authors.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn from_tomlconfig<T: Into<PathBuf>>(root: T, tomlconfig: TomlConfig) -> Self {
|
||||
let root = root.into();
|
||||
let mut config = BookConfig::new(&root);
|
||||
config.fill_from_tomlconfig(tomlconfig);
|
||||
config
|
||||
}
|
||||
|
||||
pub fn fill_from_tomlconfig(&mut self, tomlconfig: TomlConfig) -> &mut Self {
|
||||
|
||||
if let Some(s) = tomlconfig.source {
|
||||
self.set_source(s);
|
||||
}
|
||||
|
||||
if let Some(t) = tomlconfig.title {
|
||||
self.set_title(t);
|
||||
}
|
||||
|
||||
if let Some(d) = tomlconfig.description {
|
||||
self.set_description(d);
|
||||
}
|
||||
|
||||
if let Some(a) = tomlconfig.authors {
|
||||
self.set_authors(a);
|
||||
}
|
||||
|
||||
if let Some(a) = tomlconfig.author {
|
||||
self.set_authors(vec![a]);
|
||||
}
|
||||
|
||||
if let Some(tomlhtmlconfig) = tomlconfig.output.and_then(|o| o.html) {
|
||||
let root = self.root.clone();
|
||||
self.get_mut_html_config()
|
||||
.fill_from_tomlconfig(root, tomlhtmlconfig);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// The JSON configuration file is **deprecated** and should not be used anymore.
|
||||
/// Please, migrate to the TOML configuration file.
|
||||
pub fn from_jsonconfig<T: Into<PathBuf>>(root: T, jsonconfig: JsonConfig) -> Self {
|
||||
let root = root.into();
|
||||
let mut config = BookConfig::new(&root);
|
||||
config.fill_from_jsonconfig(jsonconfig);
|
||||
config
|
||||
}
|
||||
|
||||
/// The JSON configuration file is **deprecated** and should not be used anymore.
|
||||
/// Please, migrate to the TOML configuration file.
|
||||
pub fn fill_from_jsonconfig(&mut self, jsonconfig: JsonConfig) -> &mut Self {
|
||||
|
||||
if let Some(s) = jsonconfig.src {
|
||||
self.set_source(s);
|
||||
}
|
||||
|
||||
if let Some(t) = jsonconfig.title {
|
||||
self.set_title(t);
|
||||
}
|
||||
|
||||
if let Some(d) = jsonconfig.description {
|
||||
self.set_description(d);
|
||||
}
|
||||
|
||||
if let Some(a) = jsonconfig.author {
|
||||
self.set_authors(vec![a]);
|
||||
}
|
||||
|
||||
if let Some(d) = jsonconfig.dest {
|
||||
let root = self.get_root().to_owned();
|
||||
self.get_mut_html_config()
|
||||
.set_destination(&root, &d);
|
||||
}
|
||||
|
||||
if let Some(d) = jsonconfig.theme_path {
|
||||
let root = self.get_root().to_owned();
|
||||
self.get_mut_html_config()
|
||||
.set_theme(&root, &d);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_root<T: Into<PathBuf>>(&mut self, root: T) -> &mut Self {
|
||||
self.root = root.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
pub fn set_source<T: Into<PathBuf>>(&mut self, source: T) -> &mut Self {
|
||||
let mut source = source.into();
|
||||
|
||||
// If the source path is relative, start with the root path
|
||||
if source.is_relative() {
|
||||
source = self.root.join(source);
|
||||
}
|
||||
|
||||
self.source = source;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_source(&self) -> &Path {
|
||||
&self.source
|
||||
}
|
||||
|
||||
pub fn set_title<T: Into<String>>(&mut self, title: T) -> &mut Self {
|
||||
self.title = title.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
pub fn set_description<T: Into<String>>(&mut self, description: T) -> &mut Self {
|
||||
self.description = description.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_description(&self) -> &str {
|
||||
&self.description
|
||||
}
|
||||
|
||||
pub fn set_authors<T: Into<Vec<String>>>(&mut self, authors: T) -> &mut Self {
|
||||
self.authors = authors.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the authors of the book as specified in the configuration file
|
||||
pub fn get_authors(&self) -> &[String] {
|
||||
self.authors.as_slice()
|
||||
}
|
||||
|
||||
pub fn set_html_config(&mut self, htmlconfig: HtmlConfig) -> &mut Self {
|
||||
self.html_config = htmlconfig;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the configuration for the HTML renderer or None of there isn't any
|
||||
pub fn get_html_config(&self) -> &HtmlConfig {
|
||||
&self.html_config
|
||||
}
|
||||
|
||||
pub fn get_mut_html_config(&mut self) -> &mut HtmlConfig {
|
||||
&mut self.html_config
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
use std::path::{PathBuf, Path};
|
||||
|
||||
use super::tomlconfig::TomlHtmlConfig;
|
||||
use super::playpenconfig::PlaypenConfig;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct HtmlConfig {
|
||||
destination: PathBuf,
|
||||
theme: PathBuf,
|
||||
curly_quotes: bool,
|
||||
mathjax_support: bool,
|
||||
google_analytics: Option<String>,
|
||||
additional_css: Vec<PathBuf>,
|
||||
additional_js: Vec<PathBuf>,
|
||||
playpen: PlaypenConfig,
|
||||
}
|
||||
|
||||
impl HtmlConfig {
|
||||
/// Creates a new `HtmlConfig` struct containing the configuration parameters for the HTML renderer.
|
||||
///
|
||||
/// ```
|
||||
/// # use std::path::PathBuf;
|
||||
/// # use mdbook::config::HtmlConfig;
|
||||
/// #
|
||||
/// let output = PathBuf::from("root/book");
|
||||
/// let config = HtmlConfig::new(PathBuf::from("root"));
|
||||
///
|
||||
/// assert_eq!(config.get_destination(), &output);
|
||||
/// ```
|
||||
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
|
||||
let root = root.into();
|
||||
let theme = root.join("theme");
|
||||
HtmlConfig {
|
||||
destination: root.clone().join("book"),
|
||||
theme: theme.clone(),
|
||||
curly_quotes: false,
|
||||
mathjax_support: false,
|
||||
google_analytics: None,
|
||||
additional_css: Vec::new(),
|
||||
additional_js: Vec::new(),
|
||||
playpen: PlaypenConfig::new(theme),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_from_tomlconfig<T: Into<PathBuf>>(&mut self, root: T, tomlconfig: TomlHtmlConfig) -> &mut Self {
|
||||
let root = root.into();
|
||||
|
||||
if let Some(d) = tomlconfig.destination {
|
||||
self.set_destination(&root, &d);
|
||||
}
|
||||
|
||||
if let Some(t) = tomlconfig.theme {
|
||||
self.set_theme(&root, &t);
|
||||
}
|
||||
|
||||
if let Some(curly_quotes) = tomlconfig.curly_quotes {
|
||||
self.curly_quotes = curly_quotes;
|
||||
}
|
||||
|
||||
if let Some(mathjax_support) = tomlconfig.mathjax_support {
|
||||
self.mathjax_support = mathjax_support;
|
||||
}
|
||||
|
||||
if tomlconfig.google_analytics.is_some() {
|
||||
self.google_analytics = tomlconfig.google_analytics;
|
||||
}
|
||||
|
||||
if let Some(stylepaths) = tomlconfig.additional_css {
|
||||
for path in stylepaths {
|
||||
if path.is_relative() {
|
||||
self.additional_css.push(root.join(path));
|
||||
} else {
|
||||
self.additional_css.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(scriptpaths) = tomlconfig.additional_js {
|
||||
for path in scriptpaths {
|
||||
if path.is_relative() {
|
||||
self.additional_js.push(root.join(path));
|
||||
} else {
|
||||
self.additional_js.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(playpen) = tomlconfig.playpen {
|
||||
self.playpen.fill_from_tomlconfig(&self.theme, playpen);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_destination<T: Into<PathBuf>>(&mut self, root: T, destination: T) -> &mut Self {
|
||||
let d = destination.into();
|
||||
if d.is_relative() {
|
||||
self.destination = root.into().join(d);
|
||||
} else {
|
||||
self.destination = d;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_destination(&self) -> &Path {
|
||||
&self.destination
|
||||
}
|
||||
|
||||
pub fn get_theme(&self) -> &Path {
|
||||
&self.theme
|
||||
}
|
||||
|
||||
pub fn set_theme<T: Into<PathBuf>>(&mut self, root: T, theme: T) -> &mut Self {
|
||||
let d = theme.into();
|
||||
if d.is_relative() {
|
||||
self.theme = root.into().join(d);
|
||||
} else {
|
||||
self.theme = d;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_curly_quotes(&self) -> bool {
|
||||
self.curly_quotes
|
||||
}
|
||||
|
||||
pub fn set_curly_quotes(&mut self, curly_quotes: bool) {
|
||||
self.curly_quotes = curly_quotes;
|
||||
}
|
||||
|
||||
pub fn get_mathjax_support(&self) -> bool {
|
||||
self.mathjax_support
|
||||
}
|
||||
|
||||
pub fn set_mathjax_support(&mut self, mathjax_support: bool) {
|
||||
self.mathjax_support = mathjax_support;
|
||||
}
|
||||
|
||||
pub fn get_google_analytics_id(&self) -> Option<String> {
|
||||
self.google_analytics.clone()
|
||||
}
|
||||
|
||||
pub fn set_google_analytics_id(&mut self, id: Option<String>) -> &mut Self {
|
||||
self.google_analytics = id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_additional_css(&self) -> bool {
|
||||
!self.additional_css.is_empty()
|
||||
}
|
||||
|
||||
pub fn get_additional_css(&self) -> &[PathBuf] {
|
||||
&self.additional_css
|
||||
}
|
||||
|
||||
pub fn has_additional_js(&self) -> bool {
|
||||
!self.additional_js.is_empty()
|
||||
}
|
||||
|
||||
pub fn get_additional_js(&self) -> &[PathBuf] {
|
||||
&self.additional_js
|
||||
}
|
||||
|
||||
pub fn get_playpen_config(&self) -> &PlaypenConfig {
|
||||
&self.playpen
|
||||
}
|
||||
|
||||
pub fn get_mut_playpen_config(&mut self) -> &mut PlaypenConfig {
|
||||
&mut self.playpen
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
extern crate serde_json;
|
||||
use std::path::PathBuf;
|
||||
use errors::*;
|
||||
|
||||
/// The JSON configuration is **deprecated** and will be removed in the near future.
|
||||
/// Please migrate to the TOML configuration.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct JsonConfig {
|
||||
pub src: Option<PathBuf>,
|
||||
pub dest: Option<PathBuf>,
|
||||
|
||||
pub title: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub description: Option<String>,
|
||||
|
||||
pub theme_path: Option<PathBuf>,
|
||||
pub google_analytics: Option<String>,
|
||||
}
|
||||
|
||||
|
||||
/// Returns a `JsonConfig` from a JSON string
|
||||
///
|
||||
/// ```
|
||||
/// # use mdbook::config::jsonconfig::JsonConfig;
|
||||
/// # use std::path::PathBuf;
|
||||
/// let json = r#"{
|
||||
/// "title": "Some title",
|
||||
/// "dest": "htmlbook"
|
||||
/// }"#;
|
||||
///
|
||||
/// let config = JsonConfig::from_json(&json).expect("Should parse correctly");
|
||||
/// assert_eq!(config.title, Some(String::from("Some title")));
|
||||
/// assert_eq!(config.dest, Some(PathBuf::from("htmlbook")));
|
||||
/// ```
|
||||
impl JsonConfig {
|
||||
pub fn from_json(input: &str) -> Result<Self> {
|
||||
let config: JsonConfig = serde_json::from_str(input)
|
||||
.chain_err(|| "Could not parse JSON")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
pub mod bookconfig;
|
||||
pub mod htmlconfig;
|
||||
pub mod playpenconfig;
|
||||
pub mod tomlconfig;
|
||||
pub mod jsonconfig;
|
||||
|
||||
// Re-export the config structs
|
||||
pub use self::bookconfig::BookConfig;
|
||||
pub use self::htmlconfig::HtmlConfig;
|
||||
pub use self::playpenconfig::PlaypenConfig;
|
||||
pub use self::tomlconfig::TomlConfig;
|
||||
@@ -1,68 +0,0 @@
|
||||
use std::path::{PathBuf, Path};
|
||||
|
||||
use super::tomlconfig::TomlPlaypenConfig;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct PlaypenConfig {
|
||||
editor: PathBuf,
|
||||
editable: bool,
|
||||
}
|
||||
|
||||
impl PlaypenConfig {
|
||||
/// Creates a new `PlaypenConfig` for playpen configuration.
|
||||
///
|
||||
/// ```
|
||||
/// # use std::path::PathBuf;
|
||||
/// # use mdbook::config::PlaypenConfig;
|
||||
/// #
|
||||
/// let editor = PathBuf::from("root/editor");
|
||||
/// let config = PlaypenConfig::new(PathBuf::from("root"));
|
||||
///
|
||||
/// assert_eq!(config.get_editor(), &editor);
|
||||
/// assert_eq!(config.is_editable(), false);
|
||||
/// ```
|
||||
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
|
||||
PlaypenConfig {
|
||||
editor: root.into().join("editor"),
|
||||
editable: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_from_tomlconfig<T: Into<PathBuf>>(&mut self, root: T, tomlplaypenconfig: TomlPlaypenConfig) -> &mut Self {
|
||||
let root = root.into();
|
||||
|
||||
if let Some(editor) = tomlplaypenconfig.editor {
|
||||
if editor.is_relative() {
|
||||
self.editor = root.join(editor);
|
||||
} else {
|
||||
self.editor = editor;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(editable) = tomlplaypenconfig.editable {
|
||||
self.editable = editable;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn is_editable(&self) -> bool {
|
||||
self.editable
|
||||
}
|
||||
|
||||
pub fn get_editor(&self) -> &Path {
|
||||
&self.editor
|
||||
}
|
||||
|
||||
pub fn set_editor<T: Into<PathBuf>>(&mut self, root: T, editor: T) -> &mut Self {
|
||||
let editor = editor.into();
|
||||
|
||||
if editor.is_relative() {
|
||||
self.editor = root.into().join(editor);
|
||||
} else {
|
||||
self.editor = editor;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
extern crate toml;
|
||||
use std::path::PathBuf;
|
||||
use errors::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TomlConfig {
|
||||
pub source: Option<PathBuf>,
|
||||
|
||||
pub title: Option<String>,
|
||||
pub author: Option<String>,
|
||||
pub authors: Option<Vec<String>>,
|
||||
pub description: Option<String>,
|
||||
|
||||
pub output: Option<TomlOutputConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TomlOutputConfig {
|
||||
pub html: Option<TomlHtmlConfig>,
|
||||
}
|
||||
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TomlHtmlConfig {
|
||||
pub destination: Option<PathBuf>,
|
||||
pub theme: Option<PathBuf>,
|
||||
pub google_analytics: Option<String>,
|
||||
pub curly_quotes: Option<bool>,
|
||||
pub mathjax_support: Option<bool>,
|
||||
pub additional_css: Option<Vec<PathBuf>>,
|
||||
pub additional_js: Option<Vec<PathBuf>>,
|
||||
pub playpen: Option<TomlPlaypenConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct TomlPlaypenConfig {
|
||||
pub editor: Option<PathBuf>,
|
||||
pub editable: Option<bool>,
|
||||
}
|
||||
|
||||
/// Returns a `TomlConfig` from a TOML string
|
||||
///
|
||||
/// ```
|
||||
/// # use mdbook::config::tomlconfig::TomlConfig;
|
||||
/// # use std::path::PathBuf;
|
||||
/// let toml = r#"title="Some title"
|
||||
/// [output.html]
|
||||
/// destination = "htmlbook" "#;
|
||||
///
|
||||
/// let config = TomlConfig::from_toml(&toml).expect("Should parse correctly");
|
||||
/// assert_eq!(config.title, Some(String::from("Some title")));
|
||||
/// assert_eq!(config.output.unwrap().html.unwrap().destination, Some(PathBuf::from("htmlbook")));
|
||||
/// ```
|
||||
impl TomlConfig {
|
||||
pub fn from_toml(input: &str) -> Result<Self> {
|
||||
let config: TomlConfig = toml::from_str(input)
|
||||
.chain_err(|| "Could not parse TOML")?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
33
src/lib.rs
33
src/lib.rs
@@ -3,8 +3,8 @@
|
||||
//! **mdBook** is similar to Gitbook but implemented in Rust.
|
||||
//! It offers a command line interface, but can also be used as a regular crate.
|
||||
//!
|
||||
//! This is the API doc, but you can find a [less "low-level" documentation here](../index.html) that
|
||||
//! contains information about the command line tool, format, structure etc.
|
||||
//! This is the API doc, but you can find a [less "low-level" documentation here](../index.html)
|
||||
//! that contains information about the command line tool, format, structure etc.
|
||||
//! It is also rendered with mdBook to showcase the features and default theme.
|
||||
//!
|
||||
//! Some reasons why you would want to use the crate (over the cli):
|
||||
@@ -21,16 +21,17 @@
|
||||
//! extern crate mdbook;
|
||||
//!
|
||||
//! use mdbook::MDBook;
|
||||
//! use std::path::PathBuf;
|
||||
//!
|
||||
//! # #[allow(unused_variables)]
|
||||
//! fn main() {
|
||||
//! let mut book = MDBook::new("my-book") // Path to root
|
||||
//! .with_source("src") // Path from root to source directory
|
||||
//! .with_destination("book") // Path from root to output directory
|
||||
//! .read_config() // Parse book.toml configuration file
|
||||
//! .expect("I don't handle configuration file errors, but you should!");
|
||||
//!
|
||||
//! book.build().unwrap(); // Render the book
|
||||
//! let mut md = MDBook::new("my-book");
|
||||
//!
|
||||
//! // tweak the book configuration a bit
|
||||
//! md.config.book.src = PathBuf::from("source");
|
||||
//! md.config.build.build_dir = PathBuf::from("book");
|
||||
//!
|
||||
//! // Render the book
|
||||
//! md.build().unwrap();
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
@@ -80,12 +81,13 @@ extern crate lazy_static;
|
||||
extern crate log;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate regex;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
extern crate tempdir;
|
||||
extern crate toml;
|
||||
|
||||
mod parse;
|
||||
mod preprocess;
|
||||
@@ -105,7 +107,7 @@ pub mod errors {
|
||||
foreign_links {
|
||||
Io(::std::io::Error);
|
||||
HandlebarsRender(::handlebars::RenderError);
|
||||
HandlebarsTemplate(::handlebars::TemplateError);
|
||||
HandlebarsTemplate(Box<::handlebars::TemplateError>);
|
||||
Utf8(::std::string::FromUtf8Error);
|
||||
}
|
||||
|
||||
@@ -116,4 +118,11 @@ pub mod errors {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Box to halve the size of Error
|
||||
impl From<::handlebars::TemplateError> for Error {
|
||||
fn from(e: ::handlebars::TemplateError) -> Error {
|
||||
From::from(Box::new(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Result, Error, ErrorKind};
|
||||
use std::io::{Error, ErrorKind, Read, Result};
|
||||
use book::bookitem::{BookItem, Chapter};
|
||||
|
||||
pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> {
|
||||
@@ -14,7 +14,10 @@ pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> {
|
||||
Ok(top_items)
|
||||
}
|
||||
|
||||
fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32>) -> Result<Vec<BookItem>> {
|
||||
fn parse_level(summary: &mut Vec<&str>,
|
||||
current_level: i32,
|
||||
mut section: Vec<i32>)
|
||||
-> Result<Vec<BookItem>> {
|
||||
debug!("[fn]: parse_level");
|
||||
let mut items: Vec<BookItem> = vec![];
|
||||
|
||||
@@ -36,9 +39,9 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
// Level can not be root level !!
|
||||
// Add a sub-number to section
|
||||
section.push(0);
|
||||
let last = items
|
||||
.pop()
|
||||
.expect("There should be at least one item since this can't be the root level");
|
||||
let last = items.pop().expect(
|
||||
"There should be at least one item since this can't be the root level",
|
||||
);
|
||||
|
||||
if let BookItem::Chapter(ref s, ref ch) = last {
|
||||
let mut ch = ch.clone();
|
||||
@@ -49,52 +52,57 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
section.pop();
|
||||
continue;
|
||||
} else {
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
Prefix, \
|
||||
Suffix and Spacer elements can only exist on the root level.\n
|
||||
Suffix and Spacer elements can only exist \
|
||||
on the root level.\n
|
||||
\
|
||||
Prefix elements can only exist before any chapter and there can be \
|
||||
no chapters after suffix elements."));
|
||||
Prefix elements can only exist before \
|
||||
any chapter and there can be \
|
||||
no chapters after suffix elements.",
|
||||
));
|
||||
};
|
||||
|
||||
} else {
|
||||
// level and current_level are the same, parse the line
|
||||
item = if let Some(parsed_item) = parse_line(summary[0]) {
|
||||
|
||||
// Eliminate possible errors and set section to -1 after suffix
|
||||
match parsed_item {
|
||||
// error if level != 0 and BookItem is != Chapter
|
||||
BookItem::Affix(_) |
|
||||
BookItem::Spacer if level > 0 => {
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
BookItem::Affix(_) | BookItem::Spacer if level > 0 => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
\
|
||||
Prefix, Suffix and Spacer elements can only exist on the \
|
||||
root level.\n
|
||||
Prefix, Suffix and Spacer elements \
|
||||
can only exist on the root level.\n
|
||||
Prefix \
|
||||
elements can only exist before any chapter and there can be \
|
||||
no chapters after suffix elements."))
|
||||
},
|
||||
elements can only exist before any chapter and \
|
||||
there can be no chapters after suffix elements.",
|
||||
))
|
||||
}
|
||||
|
||||
// error if BookItem == Chapter and section == -1
|
||||
BookItem::Chapter(_, _) if section[0] == -1 => {
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
\
|
||||
Prefix, Suffix and Spacer elements can only exist on the \
|
||||
root level.\n
|
||||
Prefix, Suffix and Spacer elements can only \
|
||||
exist on the root level.\n
|
||||
Prefix \
|
||||
elements can only exist before any chapter and there can be \
|
||||
no chapters after suffix elements."))
|
||||
},
|
||||
elements can only exist before any chapter and \
|
||||
there can be no chapters after suffix elements.",
|
||||
))
|
||||
}
|
||||
|
||||
// Set section = -1 after suffix
|
||||
BookItem::Affix(_) if section[0] > 0 => {
|
||||
section[0] = -1;
|
||||
},
|
||||
}
|
||||
|
||||
_ => {},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match parsed_item {
|
||||
@@ -102,14 +110,12 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
// Increment section
|
||||
let len = section.len() - 1;
|
||||
section[len] += 1;
|
||||
let s = section
|
||||
.iter()
|
||||
.fold("".to_owned(), |s, i| s + &i.to_string() + ".");
|
||||
let s = section.iter()
|
||||
.fold("".to_owned(), |s, i| s + &i.to_string() + ".");
|
||||
BookItem::Chapter(s, ch)
|
||||
},
|
||||
}
|
||||
_ => parsed_item,
|
||||
}
|
||||
|
||||
} else {
|
||||
// If parse_line does not return Some(_) continue...
|
||||
summary.remove(0);
|
||||
@@ -146,8 +152,10 @@ fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
|
||||
if spaces > 0 {
|
||||
debug!("[SUMMARY.md]:");
|
||||
debug!("\t[line]: {}", line);
|
||||
debug!("[*]: There is an indentation error on this line. Indentation should be {} spaces", spaces_in_tab);
|
||||
return Err(Error::new(ErrorKind::Other, format!("Indentation error on line:\n\n{}", line)));
|
||||
debug!("[*]: There is an indentation error on this line. Indentation should be {} spaces",
|
||||
spaces_in_tab);
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
format!("Indentation error on line:\n\n{}", line)));
|
||||
}
|
||||
|
||||
Ok(level)
|
||||
@@ -177,7 +185,7 @@ fn parse_line(l: &str) -> Option<BookItem> {
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
}
|
||||
// Non-list element
|
||||
'[' => {
|
||||
debug!("[*]: Line is a link element");
|
||||
@@ -187,8 +195,8 @@ fn parse_line(l: &str) -> Option<BookItem> {
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ struct Link<'a> {
|
||||
|
||||
impl<'a> Link<'a> {
|
||||
fn from_capture(cap: Captures<'a>) -> Option<Link<'a>> {
|
||||
|
||||
let link_type = match (cap.get(0), cap.get(1), cap.get(2)) {
|
||||
(_, Some(typ), Some(rest)) => {
|
||||
let mut path_props = rest.as_str().split_whitespace();
|
||||
@@ -51,21 +50,22 @@ impl<'a> Link<'a> {
|
||||
("playpen", Some(pth)) => Some(LinkType::Playpen(pth, props)),
|
||||
_ => None,
|
||||
}
|
||||
},
|
||||
(Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => Some(LinkType::Escaped),
|
||||
}
|
||||
(Some(mat), None, None) if mat.as_str().starts_with(ESCAPE_CHAR) => {
|
||||
Some(LinkType::Escaped)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
link_type.and_then(|lnk| {
|
||||
cap.get(0)
|
||||
.map(|mat| {
|
||||
Link {
|
||||
start_index: mat.start(),
|
||||
end_index: mat.end(),
|
||||
link: lnk,
|
||||
link_text: mat.as_str(),
|
||||
}
|
||||
})
|
||||
cap.get(0).map(|mat| {
|
||||
Link {
|
||||
start_index: mat.start(),
|
||||
end_index: mat.end(),
|
||||
link: lnk,
|
||||
link_text: mat.as_str(),
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -75,14 +75,22 @@ impl<'a> Link<'a> {
|
||||
// omit the escape char
|
||||
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
||||
LinkType::Include(ref pat) => {
|
||||
file_to_string(base.join(pat)).chain_err(|| format!("Could not read file for link {}", self.link_text))
|
||||
},
|
||||
file_to_string(base.join(pat)).chain_err(|| {
|
||||
format!("Could not read file for \
|
||||
link {}",
|
||||
self.link_text)
|
||||
})
|
||||
}
|
||||
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 contents = file_to_string(base.join(pat)).chain_err(|| {
|
||||
format!("Could not \
|
||||
read file \
|
||||
for link {}",
|
||||
self.link_text)
|
||||
})?;
|
||||
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
||||
Ok(format!("```{}{}\n{}\n```\n", ftype, attrs.join(","), contents))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,7 +198,8 @@ fn test_find_links_escaped_link() {
|
||||
|
||||
#[test]
|
||||
fn test_find_playpens_with_properties() {
|
||||
let s = "Some random text with escaped playpen {{#playpen file.rs editable }} and some more\n text {{#playpen my.rs editable no_run should_panic}} ...";
|
||||
let s = "Some random text with escaped playpen {{#playpen file.rs editable }} and some more\n \
|
||||
text {{#playpen my.rs editable no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
@@ -202,16 +211,19 @@ fn test_find_playpens_with_properties() {
|
||||
link_text: "{{#playpen file.rs editable }}",
|
||||
},
|
||||
Link {
|
||||
start_index: 90,
|
||||
end_index: 137,
|
||||
link: LinkType::Playpen(PathBuf::from("my.rs"), vec!["editable", "no_run", "should_panic"]),
|
||||
start_index: 89,
|
||||
end_index: 136,
|
||||
link: LinkType::Playpen(PathBuf::from("my.rs"),
|
||||
vec!["editable", "no_run", "should_panic"]),
|
||||
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
||||
}]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_all_link_types() {
|
||||
let s = "Some random text with escaped playpen {{#include file.rs}} and \\{{#contents are insignifficant in escaped link}} some more\n text {{#playpen my.rs editable no_run should_panic}} ...";
|
||||
let s = "Some random text with escaped playpen {{#include file.rs}} and \\{{#contents are \
|
||||
insignifficant in escaped link}} some more\n text {{#playpen my.rs editable no_run \
|
||||
should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
@@ -234,7 +246,8 @@ fn test_find_all_link_types() {
|
||||
Link {
|
||||
start_index: 130,
|
||||
end_index: 177,
|
||||
link: LinkType::Playpen(PathBuf::from("my.rs"), vec!["editable", "no_run", "should_panic"]),
|
||||
link: LinkType::Playpen(PathBuf::from("my.rs"),
|
||||
vec!["editable", "no_run", "should_panic"]),
|
||||
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ use preprocess;
|
||||
use renderer::Renderer;
|
||||
use book::MDBook;
|
||||
use book::bookitem::{BookItem, Chapter};
|
||||
use config::PlaypenConfig;
|
||||
use config::{Config, Playpen, HtmlConfig};
|
||||
use {utils, theme};
|
||||
use theme::{Theme, playpen_editor};
|
||||
use errors::*;
|
||||
use regex::{Regex, Captures};
|
||||
use regex::{Captures, Regex};
|
||||
|
||||
use std::ascii::AsciiExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -28,54 +28,61 @@ impl HtmlHandlebars {
|
||||
HtmlHandlebars
|
||||
}
|
||||
|
||||
fn render_item(&self, item: &BookItem, mut ctx: RenderItemContext, print_content: &mut String)
|
||||
-> Result<()> {
|
||||
fn render_item(&self,
|
||||
item: &BookItem,
|
||||
mut ctx: RenderItemContext,
|
||||
print_content: &mut String)
|
||||
-> Result<()> {
|
||||
// FIXME: This should be made DRY-er and rely less on mutable state
|
||||
match *item {
|
||||
BookItem::Chapter(_, ref ch) |
|
||||
BookItem::Affix(ref ch) if !ch.path.as_os_str().is_empty() => {
|
||||
|
||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch)
|
||||
if !ch.path.as_os_str().is_empty() =>
|
||||
{
|
||||
let path = ctx.book.get_source().join(&ch.path);
|
||||
let content = utils::fs::file_to_string(&path)?;
|
||||
let base = path.parent().ok_or_else(
|
||||
|| String::from("Invalid bookitem path!"),
|
||||
)?;
|
||||
let base = path.parent()
|
||||
.ok_or_else(|| String::from("Invalid bookitem path!"))?;
|
||||
|
||||
// Parse and expand links
|
||||
let content = preprocess::links::replace_all(&content, base)?;
|
||||
let content = utils::render_markdown(&content, ctx.book.get_curly_quotes());
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
print_content.push_str(&content);
|
||||
|
||||
// Update the context with data for this file
|
||||
let path = ch.path.to_str().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::Other, "Could not convert path to str")
|
||||
})?;
|
||||
io::Error::new(io::ErrorKind::Other,
|
||||
"Could not convert path \
|
||||
to str")
|
||||
})?;
|
||||
|
||||
// Non-lexical lifetimes needed :'(
|
||||
// Non-lexical lifetimes needed :'(
|
||||
let title: String;
|
||||
{
|
||||
let book_title = ctx.data.get("book_title").and_then(serde_json::Value::as_str).unwrap_or("");
|
||||
let book_title = ctx.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
title = ch.name.clone() + " - " + book_title;
|
||||
}
|
||||
|
||||
|
||||
ctx.data.insert("path".to_owned(), json!(path));
|
||||
ctx.data.insert("content".to_owned(), json!(content));
|
||||
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
|
||||
ctx.data.insert("title".to_owned(), json!(title));
|
||||
ctx.data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&ch.path)),
|
||||
);
|
||||
ctx.data.insert("path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&ch.path)));
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("[*]: Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
|
||||
let filepath = Path::new(&ch.path).with_extension("html");
|
||||
let rendered = self.post_process(rendered,
|
||||
&normalize_path(filepath.to_str()
|
||||
.ok_or(Error::from(format!("Bad file name: {}", filepath.display())))?),
|
||||
ctx.book.get_html_config().get_playpen_config()
|
||||
let rendered = self.post_process(
|
||||
rendered,
|
||||
&normalize_path(filepath.to_str().ok_or_else(|| Error::from(
|
||||
format!("Bad file name: {}", filepath.display()),
|
||||
))?),
|
||||
&ctx.book.config.html_config().unwrap_or_default().playpen,
|
||||
);
|
||||
|
||||
// Write to file
|
||||
@@ -85,8 +92,8 @@ impl HtmlHandlebars {
|
||||
if ctx.is_index {
|
||||
self.render_index(ctx.book, ch, &ctx.destination)?;
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -104,84 +111,64 @@ impl HtmlHandlebars {
|
||||
// This could cause a problem when someone displays
|
||||
// code containing <base href=...>
|
||||
// on the front page, however this case should be very very rare...
|
||||
content = content
|
||||
.lines()
|
||||
.filter(|line| !line.contains("<base href="))
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
content = content.lines()
|
||||
.filter(|line| !line.contains("<base href="))
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
book.write_file("index.html", content.as_bytes())?;
|
||||
|
||||
info!(
|
||||
"[*] Creating index.html from {:?} ✓",
|
||||
book.get_destination()
|
||||
.join(&ch.path.with_extension("html"))
|
||||
);
|
||||
info!("[*] Creating index.html from {:?} ✓",
|
||||
book.get_destination().join(&ch.path.with_extension("html")));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn post_process(&self, rendered: String, filepath: &str, playpen_config: &PlaypenConfig) -> String {
|
||||
let rendered = build_header_links(&rendered, &filepath);
|
||||
let rendered = fix_anchor_links(&rendered, &filepath);
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
|
||||
fn post_process(&self,
|
||||
rendered: String,
|
||||
filepath: &str,
|
||||
playpen_config: &Playpen)
|
||||
-> String {
|
||||
let rendered = build_header_links(&rendered, filepath);
|
||||
let rendered = fix_anchor_links(&rendered, filepath);
|
||||
let rendered = fix_code_blocks(&rendered);
|
||||
let rendered = add_playpen_pre(&rendered, playpen_config);
|
||||
|
||||
rendered
|
||||
}
|
||||
|
||||
fn copy_static_files(&self, book: &MDBook, theme: &Theme) -> Result<()> {
|
||||
fn copy_static_files(&self, book: &MDBook, theme: &Theme, html_config: &HtmlConfig) -> Result<()> {
|
||||
book.write_file("book.js", &theme.js)?;
|
||||
book.write_file("book.css", &theme.css)?;
|
||||
book.write_file("favicon.png", &theme.favicon)?;
|
||||
book.write_file("jquery.js", &theme.jquery)?;
|
||||
book.write_file("highlight.css", &theme.highlight_css)?;
|
||||
book.write_file(
|
||||
"tomorrow-night.css",
|
||||
&theme.tomorrow_night_css,
|
||||
)?;
|
||||
book.write_file(
|
||||
"ayu-highlight.css",
|
||||
&theme.ayu_highlight_css,
|
||||
)?;
|
||||
book.write_file("tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||
book.write_file("ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||
book.write_file("highlight.js", &theme.highlight_js)?;
|
||||
book.write_file("clipboard.min.js", &theme.clipboard_js)?;
|
||||
book.write_file("store.js", &theme.store_js)?;
|
||||
book.write_file(
|
||||
"_FontAwesome/css/font-awesome.css",
|
||||
theme::FONT_AWESOME,
|
||||
)?;
|
||||
book.write_file(
|
||||
"_FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT,
|
||||
)?;
|
||||
book.write_file(
|
||||
"_FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG,
|
||||
)?;
|
||||
book.write_file(
|
||||
"_FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
book.write_file(
|
||||
"_FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF,
|
||||
)?;
|
||||
book.write_file(
|
||||
"_FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2,
|
||||
)?;
|
||||
book.write_file(
|
||||
"_FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
book.write_file("_FontAwesome/css/font-awesome.css", theme::FONT_AWESOME)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.eot",
|
||||
theme::FONT_AWESOME_EOT)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.svg",
|
||||
theme::FONT_AWESOME_SVG)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.ttf",
|
||||
theme::FONT_AWESOME_TTF)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff",
|
||||
theme::FONT_AWESOME_WOFF)?;
|
||||
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2",
|
||||
theme::FONT_AWESOME_WOFF2)?;
|
||||
book.write_file("_FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF)?;
|
||||
|
||||
let playpen_config = book.get_html_config().get_playpen_config();
|
||||
let playpen_config = &html_config.playpen;
|
||||
|
||||
// Ace is a very large dependency, so only load it when requested
|
||||
if playpen_config.is_editable() {
|
||||
if playpen_config.editable {
|
||||
// Load the editor
|
||||
let editor = playpen_editor::PlaypenEditor::new(playpen_config.get_editor());
|
||||
let editor = playpen_editor::PlaypenEditor::new(&playpen_config.editor);
|
||||
book.write_file("editor.js", &editor.js)?;
|
||||
book.write_file("ace.js", &editor.ace_js)?;
|
||||
book.write_file("mode-rust.js", &editor.mode_rust_js)?;
|
||||
@@ -199,15 +186,14 @@ impl HtmlHandlebars {
|
||||
let mut f = File::open(custom_file)?;
|
||||
f.read_to_end(&mut data)?;
|
||||
|
||||
let name = match custom_file.strip_prefix(book.get_root()) {
|
||||
let name = match custom_file.strip_prefix(&book.root) {
|
||||
Ok(p) => p.to_str().expect("Could not convert to str"),
|
||||
Err(_) => {
|
||||
custom_file
|
||||
.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str")
|
||||
},
|
||||
custom_file.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str")
|
||||
}
|
||||
};
|
||||
|
||||
book.write_file(name, &data)?;
|
||||
@@ -216,14 +202,17 @@ impl HtmlHandlebars {
|
||||
}
|
||||
|
||||
/// Update the context with data for this file
|
||||
fn configure_print_version(&self, data: &mut serde_json::Map<String, serde_json::Value>, print_content: &str) {
|
||||
fn configure_print_version(&self,
|
||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||
print_content: &str) {
|
||||
// Make sure that the Print chapter does not display the title from
|
||||
// the last rendered chapter by removing it from its context
|
||||
data.remove("title");
|
||||
data.insert("is_print".to_owned(), json!(true));
|
||||
data.insert("path".to_owned(), json!("print.md"));
|
||||
data.insert("content".to_owned(), json!(print_content));
|
||||
data.insert("path_to_root".to_owned(), json!(utils::fs::path_to_root(Path::new("print.md"))));
|
||||
data.insert("path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(Path::new("print.md"))));
|
||||
}
|
||||
|
||||
fn register_hbs_helpers(&self, handlebars: &mut Handlebars) {
|
||||
@@ -235,10 +224,11 @@ impl HtmlHandlebars {
|
||||
/// Copy across any additional CSS and JavaScript files which the book
|
||||
/// has been configured to use.
|
||||
fn copy_additional_css_and_js(&self, book: &MDBook) -> Result<()> {
|
||||
let custom_files = book.get_additional_css().iter().chain(
|
||||
book.get_additional_js()
|
||||
.iter(),
|
||||
);
|
||||
let html = book.config.html_config().unwrap_or_default();
|
||||
|
||||
let custom_files = html.additional_css
|
||||
.iter()
|
||||
.chain(html.additional_js.iter());
|
||||
|
||||
for custom_file in custom_files {
|
||||
self.write_custom_file(custom_file, book)?;
|
||||
@@ -251,31 +241,44 @@ impl HtmlHandlebars {
|
||||
|
||||
impl Renderer for HtmlHandlebars {
|
||||
fn render(&self, book: &MDBook) -> Result<()> {
|
||||
let html_config = book.config.html_config().unwrap_or_default();
|
||||
|
||||
debug!("[fn]: render");
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
let theme = theme::Theme::new(book.get_theme_path());
|
||||
let theme_dir = match html_config.theme {
|
||||
Some(ref theme) => theme,
|
||||
None => Path::new("theme"),
|
||||
};
|
||||
|
||||
debug!("[*]: Register handlebars template");
|
||||
let theme = theme::Theme::new(theme_dir);
|
||||
|
||||
debug!("[*]: Register the index handlebars template");
|
||||
handlebars.register_template_string(
|
||||
"index",
|
||||
String::from_utf8(theme.index.clone())?,
|
||||
)?;
|
||||
|
||||
debug!("[*]: Register the header handlebars template");
|
||||
handlebars.register_partial(
|
||||
"header",
|
||||
String::from_utf8(theme.header.clone())?,
|
||||
)?;
|
||||
|
||||
debug!("[*]: Register handlebars helpers");
|
||||
self.register_hbs_helpers(&mut handlebars);
|
||||
|
||||
let mut data = make_data(book)?;
|
||||
let mut data = make_data(book, &book.config)?;
|
||||
|
||||
// Print version
|
||||
let mut print_content = String::new();
|
||||
|
||||
// TODO: The Renderer trait should really pass in where it wants us to build to...
|
||||
let destination = book.get_destination();
|
||||
|
||||
debug!("[*]: Check if destination directory exists");
|
||||
if fs::create_dir_all(&destination).is_err() {
|
||||
bail!("Unexpected error when constructing destination path");
|
||||
}
|
||||
fs::create_dir_all(&destination)
|
||||
.chain_err(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
for (i, item) in book.iter().enumerate() {
|
||||
let ctx = RenderItemContext {
|
||||
@@ -284,108 +287,107 @@ impl Renderer for HtmlHandlebars {
|
||||
destination: destination.to_path_buf(),
|
||||
data: data.clone(),
|
||||
is_index: i == 0,
|
||||
html_config: html_config.clone(),
|
||||
};
|
||||
self.render_item(item, ctx, &mut print_content)?;
|
||||
}
|
||||
|
||||
// Print version
|
||||
self.configure_print_version(&mut data, &print_content);
|
||||
data.insert("title".to_owned(), json!(book.get_title()));
|
||||
if let Some(ref title) = book.config.book.title {
|
||||
data.insert("title".to_owned(), json!(title));
|
||||
}
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("[*]: Render template");
|
||||
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
|
||||
let rendered = self.post_process(rendered, "print.html",
|
||||
book.get_html_config().get_playpen_config());
|
||||
let rendered = self.post_process(rendered,
|
||||
"print.html",
|
||||
&html_config.playpen);
|
||||
|
||||
book.write_file(
|
||||
Path::new("print").with_extension("html"),
|
||||
&rendered.into_bytes(),
|
||||
)?;
|
||||
book.write_file(Path::new("print").with_extension("html"),
|
||||
&rendered.into_bytes())?;
|
||||
info!("[*] Creating print.html ✓");
|
||||
|
||||
// Copy static files (js, css, images, ...)
|
||||
debug!("[*] Copy static files");
|
||||
self.copy_static_files(book, &theme)?;
|
||||
self.copy_static_files(book, &theme, &html_config)?;
|
||||
self.copy_additional_css_and_js(book)?;
|
||||
|
||||
// Copy all remaining files
|
||||
utils::fs::copy_files_except_ext(book.get_source(), destination, true, &["md"])?;
|
||||
let src = book.get_source();
|
||||
utils::fs::copy_files_except_ext(&src, &destination, true, &["md"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
fn make_data(book: &MDBook, config: &Config) -> Result<serde_json::Map<String, serde_json::Value>> {
|
||||
debug!("[fn]: make_data");
|
||||
let html = config.html_config().unwrap_or_default();
|
||||
|
||||
let mut data = serde_json::Map::new();
|
||||
data.insert("language".to_owned(), json!("en"));
|
||||
data.insert("book_title".to_owned(), json!(book.get_title()));
|
||||
data.insert("description".to_owned(), json!(book.get_description()));
|
||||
data.insert("book_title".to_owned(), json!(config.book.title.clone().unwrap_or_default()));
|
||||
data.insert("description".to_owned(), json!(config.book.description.clone().unwrap_or_default()));
|
||||
data.insert("favicon".to_owned(), json!("favicon.png"));
|
||||
if let Some(livereload) = book.get_livereload() {
|
||||
if let Some(ref livereload) = book.livereload {
|
||||
data.insert("livereload".to_owned(), json!(livereload));
|
||||
}
|
||||
|
||||
// Add google analytics tag
|
||||
if let Some(ref ga) = book.get_google_analytics_id() {
|
||||
if let Some(ref ga) = config.html_config().and_then(|html| html.google_analytics) {
|
||||
data.insert("google_analytics".to_owned(), json!(ga));
|
||||
}
|
||||
|
||||
if book.get_mathjax_support() {
|
||||
if html.mathjax_support {
|
||||
data.insert("mathjax_support".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional style
|
||||
if book.has_additional_css() {
|
||||
if !html.additional_css.is_empty() {
|
||||
let mut css = Vec::new();
|
||||
for style in book.get_additional_css() {
|
||||
match style.strip_prefix(book.get_root()) {
|
||||
for style in &html.additional_css {
|
||||
match style.strip_prefix(&book.root) {
|
||||
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => {
|
||||
css.push(
|
||||
style
|
||||
.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str"),
|
||||
)
|
||||
},
|
||||
css.push(style.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str"))
|
||||
}
|
||||
}
|
||||
}
|
||||
data.insert("additional_css".to_owned(), json!(css));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional script
|
||||
if book.has_additional_js() {
|
||||
if !html.additional_js.is_empty() {
|
||||
let mut js = Vec::new();
|
||||
for script in book.get_additional_js() {
|
||||
match script.strip_prefix(book.get_root()) {
|
||||
for script in &html.additional_js {
|
||||
match script.strip_prefix(&book.root) {
|
||||
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => {
|
||||
js.push(
|
||||
script
|
||||
.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str"),
|
||||
)
|
||||
},
|
||||
js.push(script.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str"))
|
||||
}
|
||||
}
|
||||
}
|
||||
data.insert("additional_js".to_owned(), json!(js));
|
||||
}
|
||||
|
||||
if book.get_html_config().get_playpen_config().is_editable() {
|
||||
if html.playpen.editable {
|
||||
data.insert("playpens_editable".to_owned(), json!(true));
|
||||
data.insert("editor_js".to_owned(), json!("editor.js"));
|
||||
data.insert("ace_js".to_owned(), json!("ace.js"));
|
||||
data.insert("mode_rust_js".to_owned(), json!("mode-rust.js"));
|
||||
data.insert("theme_dawn_js".to_owned(), json!("theme-dawn.js"));
|
||||
data.insert("theme_tomorrow_night_js".to_owned(), json!("theme-tomorrow_night.js"));
|
||||
data.insert("theme_tomorrow_night_js".to_owned(),
|
||||
json!("theme-tomorrow_night.js"));
|
||||
}
|
||||
|
||||
let mut chapters = vec![];
|
||||
@@ -398,22 +400,25 @@ fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>
|
||||
BookItem::Affix(ref ch) => {
|
||||
chapter.insert("name".to_owned(), json!(ch.name));
|
||||
let path = ch.path.to_str().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::Other, "Could not convert path to str")
|
||||
})?;
|
||||
io::Error::new(io::ErrorKind::Other,
|
||||
"Could not convert path \
|
||||
to str")
|
||||
})?;
|
||||
chapter.insert("path".to_owned(), json!(path));
|
||||
},
|
||||
}
|
||||
BookItem::Chapter(ref s, ref ch) => {
|
||||
chapter.insert("section".to_owned(), json!(s));
|
||||
chapter.insert("name".to_owned(), json!(ch.name));
|
||||
let path = ch.path.to_str().ok_or_else(|| {
|
||||
io::Error::new(io::ErrorKind::Other, "Could not convert path to str")
|
||||
})?;
|
||||
io::Error::new(io::ErrorKind::Other,
|
||||
"Could not convert path \
|
||||
to str")
|
||||
})?;
|
||||
chapter.insert("path".to_owned(), json!(path));
|
||||
},
|
||||
}
|
||||
BookItem::Spacer => {
|
||||
chapter.insert("spacer".to_owned(), json!("_spacer_"));
|
||||
},
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
chapters.push(chapter);
|
||||
@@ -431,21 +436,22 @@ fn build_header_links(html: &str, filepath: &str) -> String {
|
||||
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let level = caps[1].parse().expect(
|
||||
"Regex should ensure we only ever get numbers here",
|
||||
);
|
||||
regex.replace_all(html, |caps: &Captures| {
|
||||
let level = caps[1].parse()
|
||||
.expect("Regex should ensure we only ever get numbers here");
|
||||
|
||||
wrap_header_with_link(level, &caps[2], &mut id_counter, filepath)
|
||||
})
|
||||
.into_owned()
|
||||
wrap_header_with_link(level, &caps[2], &mut id_counter, filepath)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Wraps a single header tag with a link, making sure each tag gets its own
|
||||
/// unique ID by appending an auto-incremented number (if necessary).
|
||||
fn wrap_header_with_link(level: usize, content: &str, id_counter: &mut HashMap<String, usize>, filepath: &str)
|
||||
-> String {
|
||||
fn wrap_header_with_link(level: usize,
|
||||
content: &str,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
filepath: &str)
|
||||
-> String {
|
||||
let raw_id = id_from_content(content);
|
||||
|
||||
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
|
||||
@@ -466,31 +472,29 @@ fn wrap_header_with_link(level: usize, content: &str, id_counter: &mut HashMap<S
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate an id for use with anchors which is derived from a "normalised"
|
||||
/// Generate an id for use with anchors which is derived from a "normalised"
|
||||
/// string.
|
||||
fn id_from_content(content: &str) -> String {
|
||||
let mut content = content.to_string();
|
||||
|
||||
// Skip any tags or html-encoded stuff
|
||||
const REPL_SUB: &[&str] = &[
|
||||
"<em>",
|
||||
"</em>",
|
||||
"<code>",
|
||||
"</code>",
|
||||
"<strong>",
|
||||
"</strong>",
|
||||
"<",
|
||||
">",
|
||||
"&",
|
||||
"'",
|
||||
""",
|
||||
];
|
||||
const REPL_SUB: &[&str] = &["<em>",
|
||||
"</em>",
|
||||
"<code>",
|
||||
"</code>",
|
||||
"<strong>",
|
||||
"</strong>",
|
||||
"<",
|
||||
">",
|
||||
"&",
|
||||
"'",
|
||||
"""];
|
||||
for sub in REPL_SUB {
|
||||
content = content.replace(sub, "");
|
||||
}
|
||||
|
||||
// Remove spaces and hastags indicating a header
|
||||
let trimmed = content.trim().trim_left_matches("#").trim();
|
||||
let trimmed = content.trim().trim_left_matches('#').trim();
|
||||
|
||||
normalize_id(trimmed)
|
||||
}
|
||||
@@ -500,21 +504,18 @@ fn id_from_content(content: &str) -> String {
|
||||
// that in a very inelegant way
|
||||
fn fix_anchor_links(html: &str, filepath: &str) -> String {
|
||||
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let before = &caps[1];
|
||||
let anchor = &caps[2];
|
||||
let after = &caps[3];
|
||||
regex.replace_all(html, |caps: &Captures| {
|
||||
let before = &caps[1];
|
||||
let anchor = &caps[2];
|
||||
let after = &caps[3];
|
||||
|
||||
format!(
|
||||
"<a{before}href=\"{filepath}#{anchor}\"{after}>",
|
||||
format!("<a{before}href=\"{filepath}#{anchor}\"{after}>",
|
||||
before = before,
|
||||
filepath = filepath,
|
||||
anchor = anchor,
|
||||
after = after
|
||||
)
|
||||
})
|
||||
.into_owned()
|
||||
after = after)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
|
||||
@@ -528,46 +529,53 @@ fn fix_anchor_links(html: &str, filepath: &str) -> String {
|
||||
// This function replaces all commas by spaces in the code block classes
|
||||
fn fix_code_blocks(html: &str) -> String {
|
||||
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let before = &caps[1];
|
||||
let classes = &caps[2].replace(",", " ");
|
||||
let after = &caps[3];
|
||||
regex.replace_all(html, |caps: &Captures| {
|
||||
let before = &caps[1];
|
||||
let classes = &caps[2].replace(",", " ");
|
||||
let after = &caps[3];
|
||||
|
||||
format!(r#"<code{before}class="{classes}"{after}>"#, before = before, classes = classes, after = after)
|
||||
})
|
||||
.into_owned()
|
||||
format!(r#"<code{before}class="{classes}"{after}>"#,
|
||||
before = before,
|
||||
classes = classes,
|
||||
after = after)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn add_playpen_pre(html: &str, playpen_config: &PlaypenConfig) -> String {
|
||||
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
regex.replace_all(html, |caps: &Captures| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
|
||||
if (classes.contains("language-rust") && !classes.contains("ignore")) || classes.contains("mdbook-runnable") {
|
||||
// wrap the contents in an external pre block
|
||||
if playpen_config.is_editable() &&
|
||||
classes.contains("editable") || text.contains("fn main") || text.contains("quick_main!") {
|
||||
format!("<pre class=\"playpen\">{}</pre>", text)
|
||||
} else {
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
|
||||
format!("<pre class=\"playpen\"><code class=\"{}\">\n# #![allow(unused_variables)]\n\
|
||||
{}#fn main() {{\n\
|
||||
{}\
|
||||
#}}</code></pre>",
|
||||
classes, attrs, code)
|
||||
}
|
||||
if (classes.contains("language-rust") && !classes.contains("ignore")) ||
|
||||
classes.contains("mdbook-runnable")
|
||||
{
|
||||
// wrap the contents in an external pre block
|
||||
if playpen_config.editable && classes.contains("editable") ||
|
||||
text.contains("fn main") || text.contains("quick_main!")
|
||||
{
|
||||
format!("<pre class=\"playpen\">{}</pre>", text)
|
||||
} else {
|
||||
// not language-rust, so no-op
|
||||
text.to_owned()
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
|
||||
format!("<pre class=\"playpen\"><code class=\"{}\">\n# \
|
||||
#![allow(unused_variables)]\n\
|
||||
{}#fn main() {{\n\
|
||||
{}\
|
||||
#}}</code></pre>",
|
||||
classes,
|
||||
attrs,
|
||||
code)
|
||||
}
|
||||
})
|
||||
.into_owned()
|
||||
} else {
|
||||
// not language-rust, so no-op
|
||||
text.to_owned()
|
||||
}
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn partition_source(s: &str) -> (String, String) {
|
||||
@@ -598,6 +606,7 @@ struct RenderItemContext<'a> {
|
||||
destination: PathBuf,
|
||||
data: serde_json::Map<String, serde_json::Value>,
|
||||
is_index: bool,
|
||||
html_config: HtmlConfig,
|
||||
}
|
||||
|
||||
pub fn normalize_path(path: &str) -> String {
|
||||
@@ -609,16 +618,14 @@ pub fn normalize_path(path: &str) -> String {
|
||||
|
||||
pub fn normalize_id(content: &str) -> String {
|
||||
content.chars()
|
||||
.filter_map(|ch|
|
||||
if ch.is_alphanumeric() || ch == '_' || ch == '-' {
|
||||
Some(ch.to_ascii_lowercase())
|
||||
} else if ch.is_whitespace() {
|
||||
Some('-')
|
||||
} else {
|
||||
None
|
||||
}
|
||||
)
|
||||
.collect::<String>()
|
||||
.filter_map(|ch| if ch.is_alphanumeric() || ch == '_' || ch == '-' {
|
||||
Some(ch.to_ascii_lowercase())
|
||||
} else if ch.is_whitespace() {
|
||||
Some('-')
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
|
||||
@@ -643,15 +650,15 @@ mod tests {
|
||||
),
|
||||
(
|
||||
"<h4></h4>",
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#" id=""><h4></h4></a>"##
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#" id=""><h4></h4></a>"##,
|
||||
),
|
||||
(
|
||||
"<h4><em>Hï</em></h4>",
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#hï" id="hï"><h4><em>Hï</em></h4></a>"##
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1><h3>Foo</h3>",
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a><a class="header" href="./some_chapter/some_section.html#foo-1" id="foo-1"><h3>Foo</h3></a>"##
|
||||
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a><a class="header" href="./some_chapter/some_section.html#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
|
||||
),
|
||||
];
|
||||
|
||||
@@ -668,7 +675,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn anchor_generation() {
|
||||
assert_eq!(id_from_content("## `--passes`: add more rustdoc passes"), "--passes-add-more-rustdoc-passes");
|
||||
assert_eq!(id_from_content("## Method-call expressions"), "method-call-expressions");
|
||||
assert_eq!(id_from_content("## `--passes`: add more rustdoc passes"),
|
||||
"--passes-add-more-rustdoc-passes");
|
||||
assert_eq!(id_from_content("## Method-call expressions"),
|
||||
"method-call-expressions");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,159 +2,233 @@ use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Renderable, Context};
|
||||
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
|
||||
|
||||
|
||||
// Handlebars helper for navigation
|
||||
type StringMap = BTreeMap<String, String>;
|
||||
|
||||
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: previous (handlebars helper)");
|
||||
|
||||
debug!("[*]: Get data from context");
|
||||
let chapters = rc.evaluate_absolute("chapters")
|
||||
.and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
|
||||
let current = rc.evaluate_absolute("path")?
|
||||
.as_str().ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
let mut previous: Option<BTreeMap<String, String>> = None;
|
||||
|
||||
debug!("[*]: Search for current Chapter");
|
||||
// Search for current chapter and return previous entry
|
||||
for item in chapters {
|
||||
|
||||
match item.get("path") {
|
||||
Some(path) if !path.is_empty() => {
|
||||
if path == ¤t {
|
||||
|
||||
debug!("[*]: Found current chapter");
|
||||
if let Some(previous) = previous {
|
||||
|
||||
debug!("[*]: Creating BTreeMap to inject in context");
|
||||
// Create new BTreeMap to extend the context: 'title' and 'link'
|
||||
let mut previous_chapter = BTreeMap::new();
|
||||
|
||||
// Chapter title
|
||||
previous
|
||||
.get("name").ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
|
||||
.and_then(|n| {
|
||||
previous_chapter.insert("title".to_owned(), json!(n));
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
|
||||
// Chapter link
|
||||
previous
|
||||
.get("path").ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
|
||||
.and_then(|p| {
|
||||
Path::new(p)
|
||||
.with_extension("html")
|
||||
.to_str().ok_or_else(|| RenderError::new("Link could not be converted to str"))
|
||||
.and_then(|p| {
|
||||
previous_chapter
|
||||
.insert("link".to_owned(), json!(p.replace("\\", "/")));
|
||||
Ok(())
|
||||
})
|
||||
})?;
|
||||
|
||||
|
||||
debug!("[*]: Render template");
|
||||
// Render template
|
||||
_h.template()
|
||||
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
||||
.and_then(|t| {
|
||||
let mut local_rc = rc.with_context(Context::wraps(&previous_chapter)?);
|
||||
t.render(r, &mut local_rc)
|
||||
})?;
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
previous = Some(item.clone());
|
||||
}
|
||||
},
|
||||
_ => continue,
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
/// Target for `find_chapter`.
|
||||
enum Target {
|
||||
Previous,
|
||||
Next,
|
||||
}
|
||||
|
||||
impl Target {
|
||||
/// Returns target if found.
|
||||
fn find(&self,
|
||||
base_path: &String,
|
||||
current_path: &String,
|
||||
current_item: &StringMap,
|
||||
previous_item: &StringMap,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
match self {
|
||||
&Target::Next => {
|
||||
let previous_path = previous_item.get("path").ok_or_else(|| {
|
||||
RenderError::new("No path found for chapter in JSON data")
|
||||
})?;
|
||||
|
||||
if previous_path == base_path {
|
||||
return Ok(Some(current_item.clone()));
|
||||
}
|
||||
},
|
||||
|
||||
&Target::Previous => {
|
||||
if current_path == base_path {
|
||||
return Ok(Some(previous_item.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: next (handlebars helper)");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn find_chapter(
|
||||
rc: &mut RenderContext,
|
||||
target: Target
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
debug!("[*]: Get data from context");
|
||||
let chapters = rc.evaluate_absolute("chapters")
|
||||
.and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
let current = rc.evaluate_absolute("path")?
|
||||
.as_str().ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
let mut previous: Option<BTreeMap<String, String>> = None;
|
||||
let chapters = rc.evaluate_absolute("chapters").and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
|
||||
let base_path = rc.evaluate_absolute("path")?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
let mut previous: Option<StringMap> = None;
|
||||
|
||||
debug!("[*]: Search for chapter");
|
||||
|
||||
debug!("[*]: Search for current Chapter");
|
||||
// Search for current chapter and return previous entry
|
||||
for item in chapters {
|
||||
|
||||
match item.get("path") {
|
||||
|
||||
Some(path) if !path.is_empty() => {
|
||||
|
||||
if let Some(previous) = previous {
|
||||
|
||||
let previous_path = previous
|
||||
.get("path").ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
|
||||
|
||||
if previous_path == ¤t {
|
||||
|
||||
debug!("[*]: Found current chapter");
|
||||
debug!("[*]: Creating BTreeMap to inject in context");
|
||||
// Create new BTreeMap to extend the context: 'title' and 'link'
|
||||
let mut next_chapter = BTreeMap::new();
|
||||
|
||||
item.get("name").ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
|
||||
.and_then(|n| {
|
||||
next_chapter.insert("title".to_owned(), json!(n));
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Path::new(path)
|
||||
.with_extension("html")
|
||||
.to_str().ok_or_else(|| RenderError::new("Link could not converted to str"))
|
||||
.and_then(|l| {
|
||||
debug!("[*]: Inserting link: {:?}", l);
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
next_chapter.insert("link".to_owned(), json!(l.replace("\\", "/")));
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
debug!("[*]: Render template");
|
||||
|
||||
// Render template
|
||||
_h.template().ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
||||
.and_then(|t| {
|
||||
let mut local_rc = rc.with_context(Context::wraps(&next_chapter)?);
|
||||
t.render(r, &mut local_rc)
|
||||
})?;
|
||||
break;
|
||||
if let Some(item) = target.find(&base_path, &path, &item, &previous)? {
|
||||
return Ok(Some(item));
|
||||
}
|
||||
}
|
||||
|
||||
previous = Some(item.clone());
|
||||
},
|
||||
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn render(
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
rc: &mut RenderContext,
|
||||
chapter: &StringMap,
|
||||
) -> Result<(), RenderError> {
|
||||
debug!("[*]: Creating BTreeMap to inject in context");
|
||||
|
||||
let mut context = BTreeMap::new();
|
||||
|
||||
chapter.get("name")
|
||||
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
|
||||
.map(|name| context.insert("title".to_owned(), json!(name)))?;
|
||||
|
||||
chapter.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
|
||||
.and_then(|p| {
|
||||
Path::new(p).with_extension("html")
|
||||
.to_str()
|
||||
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
|
||||
.map(|p| context.insert("link".to_owned(), json!(p.replace("\\", "/"))))
|
||||
})?;
|
||||
|
||||
debug!("[*]: Render template");
|
||||
|
||||
_h.template()
|
||||
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
||||
.and_then(|t| {
|
||||
let mut local_rc = rc.with_context(Context::wraps(&context)?);
|
||||
t.render(r, &mut local_rc)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: previous (handlebars helper)");
|
||||
|
||||
if let Some(previous) = find_chapter(rc, Target::Previous)? {
|
||||
render(_h, r, rc, &previous)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: next (handlebars helper)");
|
||||
|
||||
if let Some(next) = find_chapter(rc, Target::Next)? {
|
||||
render(_h, r, rc, &next)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
static TEMPLATE: &'static str =
|
||||
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
|
||||
|
||||
#[test]
|
||||
fn test_next_previous() {
|
||||
let data = json!({
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
|
||||
assert_eq!(
|
||||
h.template_render(TEMPLATE, &data).unwrap(),
|
||||
"one: one.html|three: three.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_first() {
|
||||
let data = json!({
|
||||
"name": "one",
|
||||
"path": "one.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
|
||||
assert_eq!(
|
||||
h.template_render(TEMPLATE, &data).unwrap(),
|
||||
"|two: two.html");
|
||||
}
|
||||
#[test]
|
||||
fn test_last() {
|
||||
let data = json!({
|
||||
"name": "three",
|
||||
"path": "three.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
h.register_helper("next", Box::new(next));
|
||||
|
||||
assert_eq!(
|
||||
h.template_render(TEMPLATE, &data).unwrap(),
|
||||
"two: two.html|");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper};
|
||||
use pulldown_cmark::{Parser, html, Event, Tag};
|
||||
use handlebars::{Handlebars, Helper, HelperDef, RenderContext, RenderError};
|
||||
use pulldown_cmark::{html, Event, Parser, Tag};
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -11,29 +11,26 @@ pub struct RenderToc;
|
||||
|
||||
impl HelperDef for RenderToc {
|
||||
fn call(&self, _h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
|
||||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = rc.evaluate_absolute("chapters")
|
||||
.and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
let chapters = rc.evaluate_absolute("chapters").and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
let current = rc.evaluate_absolute("path")?
|
||||
.as_str().ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
rc.writer.write_all(b"<ul class=\"chapter\">")?;
|
||||
|
||||
let mut current_level = 1;
|
||||
|
||||
for item in chapters {
|
||||
|
||||
// Spacer
|
||||
if item.get("spacer").is_some() {
|
||||
rc.writer
|
||||
.write_all(b"<li class=\"spacer\"></li>")?;
|
||||
rc.writer.write_all(b"<li class=\"spacer\"></li>")?;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -126,7 +123,6 @@ impl HelperDef for RenderToc {
|
||||
}
|
||||
|
||||
rc.writer.write_all(b"</li>")?;
|
||||
|
||||
}
|
||||
while current_level > 1 {
|
||||
rc.writer.write_all(b"</ul>")?;
|
||||
|
||||
@@ -266,7 +266,7 @@ table thead td {
|
||||
left: 315px;
|
||||
}
|
||||
.theme-popup {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -186,15 +186,17 @@ $( document ).ready(function() {
|
||||
if(!lines_hidden) { return; }
|
||||
|
||||
// add expand button
|
||||
pre_block.prepend("<div class=\"buttons\"><i class=\"fa fa-expand\"></i></div>");
|
||||
pre_block.prepend("<div class=\"buttons\"><i class=\"fa fa-expand\" title=\"Show hidden lines\"></i></div>");
|
||||
|
||||
pre_block.find("i").click(function(e){
|
||||
if( $(this).hasClass("fa-expand") ) {
|
||||
$(this).removeClass("fa-expand").addClass("fa-compress");
|
||||
$(this).attr("title", "Hide lines");
|
||||
pre_block.find("span.hidden").removeClass("hidden").addClass("unhidden");
|
||||
}
|
||||
else {
|
||||
$(this).removeClass("fa-compress").addClass("fa-expand");
|
||||
$(this).attr("title", "Show hidden lines");
|
||||
pre_block.find("span.unhidden").removeClass("unhidden").addClass("hidden");
|
||||
}
|
||||
});
|
||||
@@ -209,12 +211,12 @@ $( document ).ready(function() {
|
||||
pre_block.prepend("<div class=\"buttons\"></div>");
|
||||
buttons = pre_block.find(".buttons");
|
||||
}
|
||||
buttons.prepend("<i class=\"fa fa-play play-button hidden\"></i>");
|
||||
buttons.prepend("<i class=\"fa fa-copy clip-button\"><i class=\"tooltiptext\"></i></i>");
|
||||
buttons.prepend("<i class=\"fa fa-play play-button hidden\" title=\"Run this code\"></i>");
|
||||
buttons.prepend("<i class=\"fa fa-copy clip-button\" title=\"Copy to clipboard\"><i class=\"tooltiptext\"></i></i>");
|
||||
|
||||
let code_block = pre_block.find("code").first();
|
||||
if (window.ace && code_block.hasClass("editable")) {
|
||||
buttons.prepend("<i class=\"fa fa-history reset-button\"></i>");
|
||||
buttons.prepend("<i class=\"fa fa-history reset-button\" title=\"Undo changes\"></i>");
|
||||
}
|
||||
|
||||
buttons.find(".play-button").click(function(e){
|
||||
|
||||
1
src/theme/header.hbs
Normal file
1
src/theme/header.hbs
Normal file
@@ -0,0 +1 @@
|
||||
{{!-- Put your header HTML text here --}}
|
||||
@@ -51,11 +51,6 @@
|
||||
<!-- Fetch store.js from local - TODO add CDN when 2.x.x is available on cdnjs -->
|
||||
<script src="store.js"></script>
|
||||
|
||||
<!-- Custom JS script -->
|
||||
{{#each additional_js}}
|
||||
<script type="text/javascript" src="{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
</head>
|
||||
<body class="light">
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
@@ -79,10 +74,11 @@
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page" tabindex="-1">
|
||||
{{> header}}
|
||||
<div id="menu-bar" class="menu-bar">
|
||||
<div class="left-buttons">
|
||||
<i id="sidebar-toggle" class="fa fa-bars"></i>
|
||||
<i id="theme-toggle" class="fa fa-paint-brush"></i>
|
||||
<i id="sidebar-toggle" class="fa fa-bars" title="Toggle sidebar"></i>
|
||||
<i id="theme-toggle" class="fa fa-paint-brush" title="Change theme"></i>
|
||||
</div>
|
||||
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
@@ -100,13 +96,13 @@
|
||||
|
||||
<!-- Mobile navigation buttons -->
|
||||
{{#previous}}
|
||||
<a rel="prev" href="{{link}}" class="mobile-nav-chapters previous">
|
||||
<a rel="prev" href="{{link}}" class="mobile-nav-chapters previous" title="Previous chapter">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
|
||||
{{#next}}
|
||||
<a rel="next" href="{{link}}" class="mobile-nav-chapters next">
|
||||
<a rel="next" href="{{link}}" class="mobile-nav-chapters next" title="Next chapter">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
{{/next}}
|
||||
@@ -139,14 +135,21 @@
|
||||
{{{livereload}}}
|
||||
|
||||
{{#if google_analytics}}
|
||||
<!-- Google Analytics Tag -->
|
||||
<script>
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
var localAddrs = ["localhost", "127.0.0.1", ""];
|
||||
|
||||
ga('create', '{{google_analytics}}', 'auto');
|
||||
ga('send', 'pageview');
|
||||
// make sure we don't activate google analytics if the developer is
|
||||
// inspecting the book locally...
|
||||
if (localAddrs.indexOf(document.location.hostname) === -1) {
|
||||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
|
||||
ga('create', '{{google_analytics}}', 'auto');
|
||||
ga('send', 'pageview');
|
||||
}
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
@@ -168,5 +171,11 @@
|
||||
|
||||
<script src="highlight.js"></script>
|
||||
<script src="book.js"></script>
|
||||
|
||||
<!-- Custom JS script -->
|
||||
{{#each additional_js}}
|
||||
<script type="text/javascript" src="{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::io::Read;
|
||||
use errors::*;
|
||||
|
||||
pub static INDEX: &'static [u8] = include_bytes!("index.hbs");
|
||||
pub static HEADER: &'static [u8] = include_bytes!("header.hbs");
|
||||
pub static CSS: &'static [u8] = include_bytes!("book.css");
|
||||
pub static FAVICON: &'static [u8] = include_bytes!("favicon.png");
|
||||
pub static JS: &'static [u8] = include_bytes!("book.js");
|
||||
@@ -18,11 +19,16 @@ pub static JQUERY: &'static [u8] = include_bytes!("jquery.js");
|
||||
pub static CLIPBOARD_JS: &'static [u8] = include_bytes!("clipboard.min.js");
|
||||
pub static STORE_JS: &'static [u8] = include_bytes!("store.js");
|
||||
pub static FONT_AWESOME: &'static [u8] = include_bytes!("_FontAwesome/css/font-awesome.min.css");
|
||||
pub static FONT_AWESOME_EOT: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.eot");
|
||||
pub static FONT_AWESOME_SVG: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff");
|
||||
pub static FONT_AWESOME_WOFF2: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff2");
|
||||
pub static FONT_AWESOME_EOT: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.eot");
|
||||
pub static FONT_AWESOME_SVG: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff");
|
||||
pub static FONT_AWESOME_WOFF2: &'static [u8] =
|
||||
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff2");
|
||||
pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/FontAwesome.otf");
|
||||
|
||||
|
||||
@@ -35,6 +41,7 @@ pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Theme {
|
||||
pub index: Vec<u8>,
|
||||
pub header: Vec<u8>,
|
||||
pub css: Vec<u8>,
|
||||
pub favicon: Vec<u8>,
|
||||
pub js: Vec<u8>,
|
||||
@@ -61,6 +68,7 @@ impl Theme {
|
||||
{
|
||||
let files = vec![
|
||||
(theme_dir.join("index.hbs"), &mut theme.index),
|
||||
(theme_dir.join("header.hbs"), &mut theme.header),
|
||||
(theme_dir.join("book.js"), &mut theme.js),
|
||||
(theme_dir.join("book.css"), &mut theme.css),
|
||||
(theme_dir.join("favicon.png"), &mut theme.favicon),
|
||||
@@ -92,6 +100,7 @@ impl Default for Theme {
|
||||
fn default() -> Theme {
|
||||
Theme {
|
||||
index: INDEX.to_owned(),
|
||||
header: HEADER.to_owned(),
|
||||
css: CSS.to_owned(),
|
||||
favicon: FAVICON.to_owned(),
|
||||
js: JS.to_owned(),
|
||||
@@ -163,6 +172,7 @@ mod tests {
|
||||
|
||||
let empty = Theme {
|
||||
index: Vec::new(),
|
||||
header: Vec::new(),
|
||||
css: Vec::new(),
|
||||
favicon: Vec::new(),
|
||||
js: Vec::new(),
|
||||
|
||||
@@ -47,13 +47,12 @@ impl PlaypenEditor {
|
||||
|
||||
// Check for individual files if they exist
|
||||
{
|
||||
let files = vec![
|
||||
(src.join("editor.js"), &mut editor.js),
|
||||
(src.join("ace.js"), &mut editor.ace_js),
|
||||
(src.join("mode-rust.js"), &mut editor.mode_rust_js),
|
||||
(src.join("theme-dawn.js"), &mut editor.theme_dawn_js),
|
||||
(src.join("theme-tomorrow_night.js"), &mut editor.theme_tomorrow_night_js),
|
||||
];
|
||||
let files = vec![(src.join("editor.js"), &mut editor.js),
|
||||
(src.join("ace.js"), &mut editor.ace_js),
|
||||
(src.join("mode-rust.js"), &mut editor.mode_rust_js),
|
||||
(src.join("theme-dawn.js"), &mut editor.theme_dawn_js),
|
||||
(src.join("theme-tomorrow_night.js"),
|
||||
&mut editor.theme_tomorrow_night_js)];
|
||||
|
||||
for (filename, dest) in files {
|
||||
if !filename.exists() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.theme-popup {
|
||||
position: relative
|
||||
position: absolute
|
||||
left: 10px
|
||||
|
||||
z-index: 1000;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::{Path, PathBuf, Component};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
use errors::*;
|
||||
use std::io::Read;
|
||||
use std::fs::{self, File};
|
||||
@@ -11,7 +11,7 @@ pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
Err(e) => {
|
||||
debug!("[*]: Failed to open {:?}", path);
|
||||
bail!(e);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
let mut content = String::new();
|
||||
@@ -44,8 +44,8 @@ pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
|
||||
///
|
||||
/// **note:** it's not very fool-proof, if you find a situation where
|
||||
/// it doesn't return the correct path.
|
||||
/// Consider [submitting a new issue](https://github.com/azerupi/mdBook/issues)
|
||||
/// or a [pull-request](https://github.com/azerupi/mdBook/pulls) to improve it.
|
||||
/// Consider [submitting a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
|
||||
/// or a [pull-request](https://github.com/rust-lang-nursery/mdBook/pulls) to improve it.
|
||||
|
||||
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||
debug!("[fn]: path_to_root");
|
||||
@@ -56,14 +56,14 @@ pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||
.expect("")
|
||||
.components()
|
||||
.fold(String::new(), |mut s, c| {
|
||||
match c {
|
||||
Component::Normal(_) => s.push_str("../"),
|
||||
_ => {
|
||||
debug!("[*]: Other path component... {:?}", c);
|
||||
},
|
||||
match c {
|
||||
Component::Normal(_) => s.push_str("../"),
|
||||
_ => {
|
||||
debug!("[*]: Other path component... {:?}", c);
|
||||
}
|
||||
s
|
||||
})
|
||||
}
|
||||
s
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -107,7 +107,10 @@ pub fn remove_dir_content(dir: &Path) -> Result<()> {
|
||||
/// Copies all files of a directory to another one except the files
|
||||
/// with the extensions given in the `ext_blacklist` array
|
||||
|
||||
pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blacklist: &[&str])
|
||||
pub fn copy_files_except_ext(from: &Path,
|
||||
to: &Path,
|
||||
recursive: bool,
|
||||
ext_blacklist: &[&str])
|
||||
-> Result<()> {
|
||||
debug!("[fn] copy_files_except_ext");
|
||||
// Check that from and to are different
|
||||
@@ -132,9 +135,11 @@ pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blackl
|
||||
fs::create_dir(&to.join(entry.file_name()))?;
|
||||
}
|
||||
|
||||
copy_files_except_ext(&from.join(entry.file_name()), &to.join(entry.file_name()), true, ext_blacklist)?;
|
||||
copy_files_except_ext(&from.join(entry.file_name()),
|
||||
&to.join(entry.file_name()),
|
||||
true,
|
||||
ext_blacklist)?;
|
||||
} else if metadata.is_file() {
|
||||
|
||||
// Check if it is in the blacklist
|
||||
if let Some(ext) = entry.path().extension() {
|
||||
if ext_blacklist.contains(&ext.to_str().unwrap()) {
|
||||
@@ -142,22 +147,19 @@ pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blackl
|
||||
}
|
||||
}
|
||||
debug!("[*] creating path for file: {:?}",
|
||||
&to.join(entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")));
|
||||
&to.join(entry.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")));
|
||||
|
||||
info!("[*] Copying file: {:?}\n to {:?}",
|
||||
entry.path(),
|
||||
&to.join(entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")));
|
||||
&to.join(entry.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")));
|
||||
fs::copy(entry.path(),
|
||||
&to.join(entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")))?;
|
||||
&to.join(entry.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -216,7 +218,7 @@ mod tests {
|
||||
|
||||
match copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"]) {
|
||||
Err(e) => panic!("Error while executing the function:\n{:?}", e),
|
||||
Ok(_) => {},
|
||||
Ok(_) => {}
|
||||
}
|
||||
|
||||
// Check if the correct files where created
|
||||
@@ -235,6 +237,5 @@ mod tests {
|
||||
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod fs;
|
||||
|
||||
use pulldown_cmark::{Parser, Event, Tag, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
|
||||
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
|
||||
OPTION_ENABLE_TABLES};
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
||||
@@ -17,7 +18,8 @@ pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
||||
|
||||
let p = Parser::new_ext(text, opts);
|
||||
let mut converter = EventQuoteConverter::new(curly_quotes);
|
||||
let events = p.map(clean_codeblock_headers).map(|event| converter.convert(event));
|
||||
let events = p.map(clean_codeblock_headers)
|
||||
.map(|event| converter.convert(event));
|
||||
|
||||
html::push_html(&mut s, events);
|
||||
s
|
||||
@@ -30,7 +32,10 @@ struct EventQuoteConverter {
|
||||
|
||||
impl EventQuoteConverter {
|
||||
fn new(enabled: bool) -> Self {
|
||||
EventQuoteConverter { enabled: enabled, convert_text: true }
|
||||
EventQuoteConverter {
|
||||
enabled: enabled,
|
||||
convert_text: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert<'a>(&mut self, event: Event<'a>) -> Event<'a> {
|
||||
@@ -39,17 +44,17 @@ impl EventQuoteConverter {
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Start(Tag::CodeBlock(_)) |
|
||||
Event::Start(Tag::Code) => {
|
||||
Event::Start(Tag::CodeBlock(_)) | Event::Start(Tag::Code) => {
|
||||
self.convert_text = false;
|
||||
event
|
||||
},
|
||||
Event::End(Tag::CodeBlock(_)) |
|
||||
Event::End(Tag::Code) => {
|
||||
}
|
||||
Event::End(Tag::CodeBlock(_)) | Event::End(Tag::Code) => {
|
||||
self.convert_text = true;
|
||||
event
|
||||
},
|
||||
Event::Text(ref text) if self.convert_text => Event::Text(Cow::from(convert_quotes_to_curly(text))),
|
||||
}
|
||||
Event::Text(ref text) if self.convert_text => {
|
||||
Event::Text(Cow::from(convert_quotes_to_curly(text)))
|
||||
}
|
||||
_ => event,
|
||||
}
|
||||
}
|
||||
@@ -58,13 +63,10 @@ impl EventQuoteConverter {
|
||||
fn clean_codeblock_headers(event: Event) -> Event {
|
||||
match event {
|
||||
Event::Start(Tag::CodeBlock(ref info)) => {
|
||||
let info: String = info
|
||||
.chars()
|
||||
.filter(|ch| !ch.is_whitespace())
|
||||
.collect();
|
||||
let info: String = info.chars().filter(|ch| !ch.is_whitespace()).collect();
|
||||
|
||||
Event::Start(Tag::CodeBlock(Cow::from(info)))
|
||||
},
|
||||
}
|
||||
_ => event,
|
||||
}
|
||||
}
|
||||
@@ -74,20 +76,31 @@ fn convert_quotes_to_curly(original_text: &str) -> String {
|
||||
// We'll consider the start to be "whitespace".
|
||||
let mut preceded_by_whitespace = true;
|
||||
|
||||
original_text
|
||||
.chars()
|
||||
.map(|original_char| {
|
||||
let converted_char = match original_char {
|
||||
'\'' => if preceded_by_whitespace { '‘' } else { '’' },
|
||||
'"' => if preceded_by_whitespace { '“' } else { '”' },
|
||||
_ => original_char,
|
||||
};
|
||||
original_text.chars()
|
||||
.map(|original_char| {
|
||||
let converted_char = match original_char {
|
||||
'\'' => {
|
||||
if preceded_by_whitespace {
|
||||
'‘'
|
||||
} else {
|
||||
'’'
|
||||
}
|
||||
}
|
||||
'"' => {
|
||||
if preceded_by_whitespace {
|
||||
'“'
|
||||
} else {
|
||||
'”'
|
||||
}
|
||||
}
|
||||
_ => original_char,
|
||||
};
|
||||
|
||||
preceded_by_whitespace = original_char.is_whitespace();
|
||||
preceded_by_whitespace = original_char.is_whitespace();
|
||||
|
||||
converted_char
|
||||
})
|
||||
.collect()
|
||||
converted_char
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -146,7 +159,8 @@ more text with spaces
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre>
|
||||
let expected =
|
||||
r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre>
|
||||
"#;
|
||||
assert_eq!(render_markdown(input, false), expected);
|
||||
assert_eq!(render_markdown(input, true), expected);
|
||||
@@ -159,7 +173,8 @@ more text with spaces
|
||||
```
|
||||
"#;
|
||||
|
||||
let expected = r#"<pre><code class="language-rust,no_run,,,should_panic,,property_3"></code></pre>
|
||||
let expected =
|
||||
r#"<pre><code class="language-rust,no_run,,,should_panic,,property_3"></code></pre>
|
||||
"#;
|
||||
assert_eq!(render_markdown(input, false), expected);
|
||||
assert_eq!(render_markdown(input, true), expected);
|
||||
@@ -168,7 +183,7 @@ more text with spaces
|
||||
#[test]
|
||||
fn rust_code_block_without_properties_has_proper_html_class() {
|
||||
let input = r#"
|
||||
```rust
|
||||
```rust
|
||||
```
|
||||
"#;
|
||||
|
||||
@@ -183,7 +198,6 @@ more text with spaces
|
||||
"#;
|
||||
assert_eq!(render_markdown(input, false), expected);
|
||||
assert_eq!(render_markdown(input, true), expected);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,12 +206,14 @@ more text with spaces
|
||||
|
||||
#[test]
|
||||
fn it_converts_single_quotes() {
|
||||
assert_eq!(convert_quotes_to_curly("'one', 'two'"), "‘one’, ‘two’");
|
||||
assert_eq!(convert_quotes_to_curly("'one', 'two'"),
|
||||
"‘one’, ‘two’");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_converts_double_quotes() {
|
||||
assert_eq!(convert_quotes_to_curly(r#""one", "two""#), "“one”, “two”");
|
||||
assert_eq!(convert_quotes_to_curly(r#""one", "two""#),
|
||||
"“one”, “two”");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
extern crate mdbook;
|
||||
extern crate tempdir;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
use mdbook::MDBook;
|
||||
use tempdir::TempDir;
|
||||
|
||||
// Tests that config values unspecified in the configuration file do not overwrite
|
||||
// values specified earlier.
|
||||
#[test]
|
||||
fn do_not_overwrite_unspecified_config_values() {
|
||||
let dir = TempDir::new("mdbook").expect("Could not create a temp dir");
|
||||
|
||||
let book = MDBook::new(dir.path())
|
||||
.with_source("bar")
|
||||
.with_destination("baz")
|
||||
.with_mathjax_support(true);
|
||||
|
||||
assert_eq!(book.get_root(), dir.path());
|
||||
assert_eq!(book.get_source(), dir.path().join("bar"));
|
||||
assert_eq!(book.get_destination(), dir.path().join("baz"));
|
||||
|
||||
// Test when trying to read a config file that does not exist
|
||||
let book = book.read_config().expect("Error reading the config file");
|
||||
|
||||
assert_eq!(book.get_root(), dir.path());
|
||||
assert_eq!(book.get_source(), dir.path().join("bar"));
|
||||
assert_eq!(book.get_destination(), dir.path().join("baz"));
|
||||
assert_eq!(book.get_mathjax_support(), true);
|
||||
|
||||
// Try with a partial config file
|
||||
let file_path = dir.path().join("book.toml");
|
||||
let mut f = File::create(file_path).expect("Could not create config file");
|
||||
f.write_all(br#"source = "barbaz""#).expect("Could not write to config file");
|
||||
f.sync_all().expect("Could not sync the file");
|
||||
|
||||
let book = book.read_config().expect("Error reading the config file");
|
||||
|
||||
assert_eq!(book.get_root(), dir.path());
|
||||
assert_eq!(book.get_source(), dir.path().join("barbaz"));
|
||||
assert_eq!(book.get_destination(), dir.path().join("baz"));
|
||||
assert_eq!(book.get_mathjax_support(), true);
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
//! This will create an entire book in a temporary directory using some
|
||||
//! dummy contents from the `tests/dummy-book/` directory.
|
||||
|
||||
// Not all features are used in all test crates, so...
|
||||
#![allow(dead_code, unused_extern_crates)]
|
||||
|
||||
extern crate tempdir;
|
||||
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
|
||||
use tempdir::TempDir;
|
||||
|
||||
|
||||
const SUMMARY_MD: &'static str = include_str!("book/SUMMARY.md");
|
||||
const INTRO: &'static str = include_str!("book/intro.md");
|
||||
const FIRST: &'static str = include_str!("book/first/index.md");
|
||||
const NESTED: &'static str = include_str!("book/first/nested.md");
|
||||
const SECOND: &'static str = include_str!("book/second.md");
|
||||
const CONCLUSION: &'static str = include_str!("book/conclusion.md");
|
||||
|
||||
|
||||
/// Create a dummy book in a temporary directory, using the contents of
|
||||
/// `SUMMARY_MD` as a guide.
|
||||
///
|
||||
/// The "Nested Chapter" file contains a code block with a single
|
||||
/// `assert!($TEST_STATUS)`. If you want to check MDBook's testing
|
||||
/// functionality, `$TEST_STATUS` can be substitute for either `true` or
|
||||
/// `false`. This is done using the `passing_test` parameter.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DummyBook {
|
||||
passing_test: bool,
|
||||
}
|
||||
|
||||
impl DummyBook {
|
||||
/// Create a new `DummyBook` with all the defaults.
|
||||
pub fn new() -> DummyBook {
|
||||
DummyBook::default()
|
||||
}
|
||||
|
||||
/// Whether the doc-test included in the "Nested Chapter" should pass or
|
||||
/// fail (it passes by default).
|
||||
pub fn with_passing_test(&mut self, test_passes: bool) -> &mut Self {
|
||||
self.passing_test = test_passes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Write a book to a temporary directory using the provided settings.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// If this fails for any reason it will `panic!()`. If we can't write to a
|
||||
/// temporary directory then chances are you've got bigger problems...
|
||||
pub fn build(&self) -> TempDir {
|
||||
let temp = TempDir::new("dummy_book").unwrap();
|
||||
|
||||
let src = temp.path().join("src");
|
||||
create_dir_all(&src).unwrap();
|
||||
|
||||
let first = src.join("first");
|
||||
create_dir_all(&first).unwrap();
|
||||
|
||||
let to_substitute = if self.passing_test { "true" } else { "false" };
|
||||
let nested_text = NESTED.replace("$TEST_STATUS", to_substitute);
|
||||
|
||||
let inputs = vec![
|
||||
(src.join("SUMMARY.md"), SUMMARY_MD),
|
||||
(src.join("intro.md"), INTRO),
|
||||
(first.join("index.md"), FIRST),
|
||||
(first.join("nested.md"), &nested_text),
|
||||
(src.join("second.md"), SECOND),
|
||||
(src.join("conclusion.md"), CONCLUSION),
|
||||
];
|
||||
|
||||
for (path, content) in inputs {
|
||||
File::create(path)
|
||||
.unwrap()
|
||||
.write_all(content.as_bytes())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
temp
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DummyBook {
|
||||
fn default() -> DummyBook {
|
||||
DummyBook { passing_test: true }
|
||||
}
|
||||
}
|
||||
116
tests/dummy_book/mod.rs
Normal file
116
tests/dummy_book/mod.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
//! This will create an entire book in a temporary directory using some
|
||||
//! dummy contents from the `tests/dummy-book/` directory.
|
||||
|
||||
// Not all features are used in all test crates, so...
|
||||
#![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
|
||||
extern crate mdbook;
|
||||
extern crate tempdir;
|
||||
extern crate walkdir;
|
||||
|
||||
use std::path::Path;
|
||||
use std::fs::{self, File};
|
||||
use std::io::{Read, Write};
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils::fs::file_to_string;
|
||||
|
||||
// The funny `self::` here is because we've got an `extern crate ...` and are
|
||||
// in a submodule
|
||||
use self::tempdir::TempDir;
|
||||
use self::mdbook::MDBook;
|
||||
use self::walkdir::WalkDir;
|
||||
|
||||
|
||||
/// Create a dummy book in a temporary directory, using the contents of
|
||||
/// `SUMMARY_MD` as a guide.
|
||||
///
|
||||
/// The "Nested Chapter" file contains a code block with a single
|
||||
/// `assert!($TEST_STATUS)`. If you want to check MDBook's testing
|
||||
/// functionality, `$TEST_STATUS` can be substitute for either `true` or
|
||||
/// `false`. This is done using the `passing_test` parameter.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DummyBook {
|
||||
passing_test: bool,
|
||||
}
|
||||
|
||||
impl DummyBook {
|
||||
/// Create a new `DummyBook` with all the defaults.
|
||||
pub fn new() -> DummyBook {
|
||||
DummyBook { passing_test: true }
|
||||
}
|
||||
|
||||
/// Whether the doc-test included in the "Nested Chapter" should pass or
|
||||
/// fail (it passes by default).
|
||||
pub fn with_passing_test(&mut self, test_passes: bool) -> &mut DummyBook {
|
||||
self.passing_test = test_passes;
|
||||
self
|
||||
}
|
||||
|
||||
/// Write a book to a temporary directory using the provided settings.
|
||||
pub fn build(&self) -> Result<TempDir> {
|
||||
let temp = TempDir::new("dummy_book").chain_err(|| "Unable to create temp directory")?;
|
||||
|
||||
let dummy_book_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/dummy_book");
|
||||
recursive_copy(&dummy_book_root, temp.path()).chain_err(|| {
|
||||
"Couldn't copy files into a \
|
||||
temporary directory"
|
||||
})?;
|
||||
|
||||
let sub_pattern = if self.passing_test { "true" } else { "false" };
|
||||
let file_containing_test = temp.path().join("src/first/nested.md");
|
||||
replace_pattern_in_file(&file_containing_test, "$TEST_STATUS", sub_pattern)?;
|
||||
|
||||
Ok(temp)
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()> {
|
||||
let contents = file_to_string(filename)?;
|
||||
File::create(filename)?.write_all(contents.replace(from, to).as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read the contents of the provided file into memory and then iterate through
|
||||
/// the list of strings asserting that the file contains all of them.
|
||||
pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
|
||||
let filename = filename.as_ref();
|
||||
let content = file_to_string(filename).expect("Couldn't read the file's contents");
|
||||
|
||||
for s in strings {
|
||||
assert!(content.contains(s),
|
||||
"Searching for {:?} in {}\n\n{}",
|
||||
s,
|
||||
filename.display(),
|
||||
content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Recursively copy an entire directory tree to somewhere else (a la `cp -r`).
|
||||
fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()> {
|
||||
let from = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
|
||||
for entry in WalkDir::new(&from) {
|
||||
let entry = entry.chain_err(|| "Unable to inspect directory entry")?;
|
||||
|
||||
let original_location = entry.path();
|
||||
let relative = original_location.strip_prefix(&from)
|
||||
.expect("`original_location` is inside the `from` \
|
||||
directory");
|
||||
let new_location = to.join(relative);
|
||||
|
||||
if original_location.is_file() {
|
||||
if let Some(parent) = new_location.parent() {
|
||||
fs::create_dir_all(parent).chain_err(|| "Couldn't create directory")?;
|
||||
}
|
||||
|
||||
fs::copy(&original_location, &new_location).chain_err(|| {
|
||||
"Unable to copy file contents"
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
- [First Chapter](./first/index.md)
|
||||
- [Nested Chapter](./first/nested.md)
|
||||
---
|
||||
- [Second Chapter](./second.md)
|
||||
|
||||
[Conclusion](./conclusion.md)
|
||||
@@ -1,24 +0,0 @@
|
||||
//! Helpers for tests which exercise the overall application, in particular
|
||||
//! the `MDBook` initialization and build/rendering process.
|
||||
|
||||
|
||||
use std::path::Path;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
|
||||
/// Read the contents of the provided file into memory and then iterate through
|
||||
/// the list of strings asserting that the file contains all of them.
|
||||
pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
|
||||
let filename = filename.as_ref();
|
||||
|
||||
let mut content = String::new();
|
||||
File::open(&filename)
|
||||
.expect("Couldn't open the provided file")
|
||||
.read_to_string(&mut content)
|
||||
.expect("Couldn't read the file's contents");
|
||||
|
||||
for s in strings {
|
||||
assert!(content.contains(s), "Searching for {:?} in {}\n\n{}", s, filename.display(), content);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
extern crate mdbook;
|
||||
extern crate tempdir;
|
||||
|
||||
use tempdir::TempDir;
|
||||
use std::path::PathBuf;
|
||||
use mdbook::MDBook;
|
||||
use tempdir::TempDir;
|
||||
|
||||
|
||||
/// Run `mdbook init` in an empty directory and make sure the default files
|
||||
@@ -20,7 +21,9 @@ fn base_mdbook_init_should_create_default_content() {
|
||||
md.init().unwrap();
|
||||
|
||||
for file in &created_files {
|
||||
assert!(temp.path().join(file).exists(), "{} doesn't exist", file);
|
||||
let target = temp.path().join(file);
|
||||
println!("{}", target.display());
|
||||
assert!(target.exists(), "{} doesn't exist", file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,16 +35,19 @@ fn run_mdbook_init_with_custom_book_and_src_locations() {
|
||||
|
||||
let temp = TempDir::new("mdbook").unwrap();
|
||||
for file in &created_files {
|
||||
assert!(!temp.path().join(file).exists(), "{} shouldn't exist yet!", file);
|
||||
assert!(!temp.path().join(file).exists(),
|
||||
"{} shouldn't exist yet!",
|
||||
file);
|
||||
}
|
||||
|
||||
let mut md = MDBook::new(temp.path())
|
||||
.with_source("in")
|
||||
.with_destination("out");
|
||||
let mut md = MDBook::new(temp.path());
|
||||
md.config.book.src = PathBuf::from("in");
|
||||
md.config.build.build_dir = PathBuf::from("out");
|
||||
|
||||
md.init().unwrap();
|
||||
|
||||
for file in &created_files {
|
||||
assert!(temp.path().join(file).exists(), "{} should have been created by `mdbook init`", file);
|
||||
let target = temp.path().join(file);
|
||||
assert!(target.exists(), "{} should have been created by `mdbook init`", file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
extern crate mdbook;
|
||||
use mdbook::config::BookConfig;
|
||||
use mdbook::config::jsonconfig::JsonConfig;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Tests that the `src` key is correctly parsed in the JSON config
|
||||
#[test]
|
||||
fn from_json_source() {
|
||||
let json = r#"{
|
||||
"src": "source"
|
||||
}"#;
|
||||
|
||||
let parsed = JsonConfig::from_json(json).expect("This should parse");
|
||||
let config = BookConfig::from_jsonconfig("root", parsed);
|
||||
|
||||
assert_eq!(config.get_source(), PathBuf::from("root/source"));
|
||||
}
|
||||
|
||||
// Tests that the `title` key is correctly parsed in the JSON config
|
||||
#[test]
|
||||
fn from_json_title() {
|
||||
let json = r#"{
|
||||
"title": "Some title"
|
||||
}"#;
|
||||
|
||||
let parsed = JsonConfig::from_json(json).expect("This should parse");
|
||||
let config = BookConfig::from_jsonconfig("root", parsed);
|
||||
|
||||
assert_eq!(config.get_title(), "Some title");
|
||||
}
|
||||
|
||||
// Tests that the `description` key is correctly parsed in the JSON config
|
||||
#[test]
|
||||
fn from_json_description() {
|
||||
let json = r#"{
|
||||
"description": "This is a description"
|
||||
}"#;
|
||||
|
||||
let parsed = JsonConfig::from_json(json).expect("This should parse");
|
||||
let config = BookConfig::from_jsonconfig("root", parsed);
|
||||
|
||||
assert_eq!(config.get_description(), "This is a description");
|
||||
}
|
||||
|
||||
// Tests that the `author` key is correctly parsed in the JSON config
|
||||
#[test]
|
||||
fn from_json_author() {
|
||||
let json = r#"{
|
||||
"author": "John Doe"
|
||||
}"#;
|
||||
|
||||
let parsed = JsonConfig::from_json(json).expect("This should parse");
|
||||
let config = BookConfig::from_jsonconfig("root", parsed);
|
||||
|
||||
assert_eq!(config.get_authors(), &[String::from("John Doe")]);
|
||||
}
|
||||
|
||||
// Tests that the `dest` key is correctly parsed in the JSON config
|
||||
#[test]
|
||||
fn from_json_destination() {
|
||||
let json = r#"{
|
||||
"dest": "htmlbook"
|
||||
}"#;
|
||||
|
||||
let parsed = JsonConfig::from_json(json).expect("This should parse");
|
||||
let config = BookConfig::from_jsonconfig("root", parsed);
|
||||
|
||||
let htmlconfig = config.get_html_config();
|
||||
|
||||
assert_eq!(htmlconfig.get_destination(), PathBuf::from("root/htmlbook"));
|
||||
}
|
||||
|
||||
// Tests that the `theme_path` key is correctly parsed in the JSON config
|
||||
#[test]
|
||||
fn from_json_output_html_theme() {
|
||||
let json = r#"{
|
||||
"theme_path": "theme"
|
||||
}"#;
|
||||
|
||||
let parsed = JsonConfig::from_json(json).expect("This should parse");
|
||||
let config = BookConfig::from_jsonconfig("root", parsed);
|
||||
|
||||
let htmlconfig = config.get_html_config();
|
||||
|
||||
assert_eq!(htmlconfig.get_theme(), &PathBuf::from("root/theme"));
|
||||
}
|
||||
@@ -1,18 +1,38 @@
|
||||
extern crate mdbook;
|
||||
#[macro_use]
|
||||
extern crate pretty_assertions;
|
||||
extern crate select;
|
||||
extern crate tempdir;
|
||||
extern crate walkdir;
|
||||
|
||||
mod dummy;
|
||||
mod helpers;
|
||||
mod dummy_book;
|
||||
|
||||
use dummy::DummyBook;
|
||||
use helpers::assert_contains_strings;
|
||||
use dummy_book::{assert_contains_strings, DummyBook};
|
||||
|
||||
use std::fs::{File, remove_file};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::ffi::OsStr;
|
||||
use walkdir::{DirEntry, WalkDir, WalkDirIterator};
|
||||
use select::document::Document;
|
||||
use select::predicate::{Class, Name, Predicate};
|
||||
use tempdir::TempDir;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils::fs::file_to_string;
|
||||
use mdbook::MDBook;
|
||||
|
||||
|
||||
const BOOK_ROOT: &'static str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/dummy_book");
|
||||
const TOC_TOP_LEVEL: &[&'static str] = &["1. First Chapter",
|
||||
"2. Second Chapter",
|
||||
"Conclusion",
|
||||
"Introduction"];
|
||||
const TOC_SECOND_LEVEL: &[&'static str] = &["1.1. Nested Chapter"];
|
||||
|
||||
/// Make sure you can load the dummy book and build it without panicking.
|
||||
#[test]
|
||||
fn build_the_dummy_book() {
|
||||
let temp = DummyBook::default().build();
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
|
||||
md.build().unwrap();
|
||||
@@ -20,7 +40,7 @@ fn build_the_dummy_book() {
|
||||
|
||||
#[test]
|
||||
fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
|
||||
let temp = DummyBook::default().build();
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
|
||||
assert!(!temp.path().join("book").exists());
|
||||
@@ -32,18 +52,16 @@ fn by_default_mdbook_generates_rendered_content_in_the_book_directory() {
|
||||
|
||||
#[test]
|
||||
fn make_sure_bottom_level_files_contain_links_to_chapters() {
|
||||
let temp = DummyBook::default().build();
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
md.build().unwrap();
|
||||
|
||||
let dest = temp.path().join("book");
|
||||
let links = vec![
|
||||
r#"href="intro.html""#,
|
||||
r#"href="./first/index.html""#,
|
||||
r#"href="./first/nested.html""#,
|
||||
r#"href="./second.html""#,
|
||||
r#"href="./conclusion.html""#,
|
||||
];
|
||||
let links = vec![r#"href="intro.html""#,
|
||||
r#"href="./first/index.html""#,
|
||||
r#"href="./first/nested.html""#,
|
||||
r#"href="./second.html""#,
|
||||
r#"href="./conclusion.html""#];
|
||||
|
||||
let files_in_bottom_dir = vec!["index.html", "intro.html", "second.html", "conclusion.html"];
|
||||
|
||||
@@ -54,19 +72,17 @@ fn make_sure_bottom_level_files_contain_links_to_chapters() {
|
||||
|
||||
#[test]
|
||||
fn check_correct_cross_links_in_nested_dir() {
|
||||
let temp = DummyBook::default().build();
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
md.build().unwrap();
|
||||
|
||||
let first = temp.path().join("book").join("first");
|
||||
let links = vec![
|
||||
r#"<base href="../">"#,
|
||||
r#"href="intro.html""#,
|
||||
r#"href="./first/index.html""#,
|
||||
r#"href="./first/nested.html""#,
|
||||
r#"href="./second.html""#,
|
||||
r#"href="./conclusion.html""#,
|
||||
];
|
||||
let links = vec![r#"<base href="../">"#,
|
||||
r#"href="intro.html""#,
|
||||
r#"href="./first/index.html""#,
|
||||
r#"href="./first/nested.html""#,
|
||||
r#"href="./second.html""#,
|
||||
r#"href="./conclusion.html""#];
|
||||
|
||||
let files_in_nested_dir = vec!["index.html", "nested.html"];
|
||||
|
||||
@@ -74,24 +90,16 @@ fn check_correct_cross_links_in_nested_dir() {
|
||||
assert_contains_strings(first.join(filename), &links);
|
||||
}
|
||||
|
||||
assert_contains_strings(
|
||||
first.join("index.html"),
|
||||
&[
|
||||
r##"href="./first/index.html#some-section" id="some-section""##
|
||||
],
|
||||
);
|
||||
assert_contains_strings(first.join("index.html"),
|
||||
&[r##"href="./first/index.html#some-section" id="some-section""##]);
|
||||
|
||||
assert_contains_strings(
|
||||
first.join("nested.html"),
|
||||
&[
|
||||
r##"href="./first/nested.html#some-section" id="some-section""##
|
||||
],
|
||||
);
|
||||
assert_contains_strings(first.join("nested.html"),
|
||||
&[r##"href="./first/nested.html#some-section" id="some-section""##]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rendered_code_has_playpen_stuff() {
|
||||
let temp = DummyBook::default().build();
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
md.build().unwrap();
|
||||
|
||||
@@ -106,15 +114,13 @@ fn rendered_code_has_playpen_stuff() {
|
||||
|
||||
#[test]
|
||||
fn chapter_content_appears_in_rendered_document() {
|
||||
let content = vec![
|
||||
("index.html", "Here's some interesting text"),
|
||||
("second.html", "Second Chapter"),
|
||||
("first/nested.html", "testable code"),
|
||||
("first/index.html", "more text"),
|
||||
("conclusion.html", "Conclusion"),
|
||||
];
|
||||
let content = vec![("index.html", "Here's some interesting text"),
|
||||
("second.html", "Second Chapter"),
|
||||
("first/nested.html", "testable code"),
|
||||
("first/index.html", "more text"),
|
||||
("conclusion.html", "Conclusion")];
|
||||
|
||||
let temp = DummyBook::default().build();
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
md.build().unwrap();
|
||||
|
||||
@@ -125,3 +131,150 @@ fn chapter_content_appears_in_rendered_document() {
|
||||
assert_contains_strings(path, &[text]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Apply a series of predicates to some root predicate, where each
|
||||
/// successive predicate is the descendant of the last one. Similar to how you
|
||||
/// might do `ul.foo li a` in CSS to access all anchor tags in the `foo` list.
|
||||
macro_rules! descendants {
|
||||
($root:expr, $($child:expr),*) => {
|
||||
$root
|
||||
$(
|
||||
.descendant($child)
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// Make sure that all `*.md` files (excluding `SUMMARY.md`) were rendered
|
||||
/// and placed in the `book` directory with their extensions set to `*.html`.
|
||||
#[test]
|
||||
fn chapter_files_were_rendered_to_html() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let src = Path::new(BOOK_ROOT).join("src");
|
||||
|
||||
let chapter_files = WalkDir::new(&src).into_iter()
|
||||
.filter_entry(|entry| entry_ends_with(entry, ".md"))
|
||||
.filter_map(|entry| entry.ok())
|
||||
.map(|entry| entry.path().to_path_buf())
|
||||
.filter(|path| {
|
||||
path.file_name().and_then(OsStr::to_str)
|
||||
!= Some("SUMMARY.md")
|
||||
});
|
||||
|
||||
for chapter in chapter_files {
|
||||
let rendered_location = temp.path().join(chapter.strip_prefix(&src).unwrap())
|
||||
.with_extension("html");
|
||||
assert!(rendered_location.exists(),
|
||||
"{} doesn't exits",
|
||||
rendered_location.display());
|
||||
}
|
||||
}
|
||||
|
||||
fn entry_ends_with(entry: &DirEntry, ending: &str) -> bool {
|
||||
entry.file_name().to_string_lossy().ends_with(ending)
|
||||
}
|
||||
|
||||
/// Read the main page (`book/index.html`) and expose it as a DOM which we
|
||||
/// can search with the `select` crate
|
||||
fn root_index_html() -> Result<Document> {
|
||||
let temp = DummyBook::new().build()
|
||||
.chain_err(|| "Couldn't create the dummy book")?;
|
||||
MDBook::new(temp.path()).build()
|
||||
.chain_err(|| "Book building failed")?;
|
||||
|
||||
let index_page = temp.path().join("book").join("index.html");
|
||||
let html = file_to_string(&index_page).chain_err(|| "Unable to read index.html")?;
|
||||
|
||||
Ok(Document::from(html.as_str()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_second_toc_level() {
|
||||
let doc = root_index_html().unwrap();
|
||||
let mut should_be = Vec::from(TOC_SECOND_LEVEL);
|
||||
should_be.sort();
|
||||
|
||||
let pred = descendants!(Class("chapter"), Name("li"), Name("li"), Name("a"));
|
||||
|
||||
let mut children_of_children: Vec<_> =
|
||||
doc.find(pred).map(|elem| elem.text().trim().to_string())
|
||||
.collect();
|
||||
children_of_children.sort();
|
||||
|
||||
assert_eq!(children_of_children, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_first_toc_level() {
|
||||
let doc = root_index_html().unwrap();
|
||||
let mut should_be = Vec::from(TOC_TOP_LEVEL);
|
||||
|
||||
should_be.extend(TOC_SECOND_LEVEL);
|
||||
should_be.sort();
|
||||
|
||||
let pred = descendants!(Class("chapter"), Name("li"), Name("a"));
|
||||
|
||||
let mut children: Vec<_> = doc.find(pred).map(|elem| elem.text().trim().to_string())
|
||||
.collect();
|
||||
children.sort();
|
||||
|
||||
assert_eq!(children, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_spacers() {
|
||||
let doc = root_index_html().unwrap();
|
||||
let should_be = 1;
|
||||
|
||||
let num_spacers =
|
||||
doc.find(Class("chapter").descendant(Name("li").and(Class("spacer")))).count();
|
||||
assert_eq!(num_spacers, should_be);
|
||||
}
|
||||
|
||||
/// Ensure building fails if `create-missing` is false and one of the files does
|
||||
/// not exist.
|
||||
#[test]
|
||||
fn failure_on_missing_file() {
|
||||
let (md, _temp) = create_missing_setup(Some(false));
|
||||
|
||||
// On failure, `build()` does not return a specific error, so assume
|
||||
// any error is a failure due to a missing file.
|
||||
assert!(md.read_config().unwrap().build().is_err());
|
||||
}
|
||||
|
||||
/// Ensure a missing file is created if `create-missing` is true.
|
||||
#[test]
|
||||
fn create_missing_file_with_config() {
|
||||
let (md, temp) = create_missing_setup(Some(true));
|
||||
|
||||
md.read_config().unwrap().build().unwrap();
|
||||
assert!(temp.path().join("src").join("intro.md").exists());
|
||||
}
|
||||
|
||||
/// Ensure a missing file is created if `create-missing` is not set (the default
|
||||
/// is true).
|
||||
#[test]
|
||||
fn create_missing_file_without_config() {
|
||||
let (md, temp) = create_missing_setup(None);
|
||||
|
||||
md.read_config().unwrap().build().unwrap();
|
||||
assert!(temp.path().join("src").join("intro.md").exists());
|
||||
}
|
||||
|
||||
fn create_missing_setup(create_missing: Option<bool>) -> (MDBook, TempDir) {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let md = MDBook::new(temp.path());
|
||||
|
||||
let mut file = File::create(temp.path().join("book.toml")).unwrap();
|
||||
match create_missing {
|
||||
Some(true) => file.write_all(b"[build]\ncreate-missing = true\n").unwrap(),
|
||||
Some(false) => file.write_all(b"[build]\ncreate-missing = false\n").unwrap(),
|
||||
None => (),
|
||||
}
|
||||
file.flush().unwrap();
|
||||
|
||||
remove_file(temp.path().join("src").join("intro.md")).unwrap();
|
||||
|
||||
(md, temp)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
extern crate mdbook;
|
||||
extern crate tempdir;
|
||||
|
||||
mod dummy;
|
||||
mod dummy_book;
|
||||
|
||||
use dummy::DummyBook;
|
||||
use dummy_book::DummyBook;
|
||||
use mdbook::MDBook;
|
||||
|
||||
|
||||
#[test]
|
||||
fn mdbook_can_correctly_test_a_passing_book() {
|
||||
let temp = DummyBook::default()
|
||||
.with_passing_test(true)
|
||||
.build();
|
||||
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
|
||||
let mut md = MDBook::new(temp.path());
|
||||
|
||||
assert!(md.test(vec![]).is_ok());
|
||||
@@ -19,9 +16,7 @@ fn mdbook_can_correctly_test_a_passing_book() {
|
||||
|
||||
#[test]
|
||||
fn mdbook_detects_book_with_failing_tests() {
|
||||
let temp = DummyBook::default()
|
||||
.with_passing_test(false)
|
||||
.build();
|
||||
let temp = DummyBook::new().with_passing_test(false).build().unwrap();
|
||||
let mut md: MDBook = MDBook::new(temp.path());
|
||||
|
||||
assert!(md.test(vec![]).is_err());
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
extern crate mdbook;
|
||||
use mdbook::config::BookConfig;
|
||||
use mdbook::config::tomlconfig::TomlConfig;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Tests that the `source` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_source() {
|
||||
let toml = r#"source = "source""#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
assert_eq!(config.get_source(), PathBuf::from("root/source"));
|
||||
}
|
||||
|
||||
// Tests that the `title` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_title() {
|
||||
let toml = r#"title = "Some title""#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
assert_eq!(config.get_title(), "Some title");
|
||||
}
|
||||
|
||||
// Tests that the `description` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_description() {
|
||||
let toml = r#"description = "This is a description""#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
assert_eq!(config.get_description(), "This is a description");
|
||||
}
|
||||
|
||||
// Tests that the `author` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_author() {
|
||||
let toml = r#"author = "John Doe""#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
assert_eq!(config.get_authors(), &[String::from("John Doe")]);
|
||||
}
|
||||
|
||||
// Tests that the `authors` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_authors() {
|
||||
let toml = r#"authors = ["John Doe", "Jane Doe"]"#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
assert_eq!(config.get_authors(), &[String::from("John Doe"), String::from("Jane Doe")]);
|
||||
}
|
||||
|
||||
// Tests that the default `playpen` config is correct in the TOML config
|
||||
#[test]
|
||||
fn from_toml_playpen_default() {
|
||||
let toml = "";
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
let playpenconfig = config.get_html_config().get_playpen_config();
|
||||
|
||||
assert_eq!(playpenconfig.get_editor(), PathBuf::from("root/theme/editor"));
|
||||
assert_eq!(playpenconfig.is_editable(), false);
|
||||
}
|
||||
|
||||
// Tests that the `playpen.editor` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_playpen_editor() {
|
||||
let toml = r#"[output.html.playpen]
|
||||
editor = "editordir""#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
let playpenconfig = config.get_html_config().get_playpen_config();
|
||||
|
||||
assert_eq!(playpenconfig.get_editor(), PathBuf::from("root/theme/editordir"));
|
||||
}
|
||||
|
||||
// Tests that the `playpen.editable` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_playpen_editable() {
|
||||
let toml = r#"[output.html.playpen]
|
||||
editable = true"#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
let playpenconfig = config.get_html_config().get_playpen_config();
|
||||
|
||||
assert_eq!(playpenconfig.is_editable(), true);
|
||||
}
|
||||
|
||||
// Tests that the `output.html.destination` key is correcly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_output_html_destination() {
|
||||
let toml = r#"[output.html]
|
||||
destination = "htmlbook""#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
let htmlconfig = config.get_html_config();
|
||||
|
||||
assert_eq!(htmlconfig.get_destination(), PathBuf::from("root/htmlbook"));
|
||||
}
|
||||
|
||||
// Tests that the `output.html.theme` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_output_html_theme() {
|
||||
let toml = r#"[output.html]
|
||||
theme = "theme""#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
let htmlconfig = config.get_html_config();
|
||||
|
||||
assert_eq!(htmlconfig.get_theme(), &PathBuf::from("root/theme"));
|
||||
}
|
||||
|
||||
// Tests that the `output.html.curly-quotes` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_output_html_curly_quotes() {
|
||||
let toml = r#"[output.html]
|
||||
curly-quotes = true"#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
let htmlconfig = config.get_html_config();
|
||||
|
||||
assert_eq!(htmlconfig.get_curly_quotes(), true);
|
||||
}
|
||||
|
||||
// Tests that the `output.html.mathjax-support` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_output_html_mathjax_support() {
|
||||
let toml = r#"[output.html]
|
||||
mathjax-support = true"#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
let htmlconfig = config.get_html_config();
|
||||
|
||||
assert_eq!(htmlconfig.get_mathjax_support(), true);
|
||||
}
|
||||
|
||||
// Tests that the `output.html.google-analytics` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_output_html_google_analytics() {
|
||||
let toml = r#"[output.html]
|
||||
google-analytics = "123456""#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
let htmlconfig = config.get_html_config();
|
||||
|
||||
assert_eq!(htmlconfig.get_google_analytics_id().expect("the google-analytics key was provided"), String::from("123456"));
|
||||
}
|
||||
|
||||
// Tests that the `output.html.additional-css` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_output_html_additional_stylesheet() {
|
||||
let toml = r#"[output.html]
|
||||
additional-css = ["custom.css", "two/custom.css"]"#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
let htmlconfig = config.get_html_config();
|
||||
|
||||
assert_eq!(htmlconfig.get_additional_css(), &[PathBuf::from("root/custom.css"), PathBuf::from("root/two/custom.css")]);
|
||||
}
|
||||
|
||||
// Tests that the `output.html.additional-js` key is correctly parsed in the TOML config
|
||||
#[test]
|
||||
fn from_toml_output_html_additional_scripts() {
|
||||
let toml = r#"[output.html]
|
||||
additional-js = ["custom.js", "two/custom.js"]"#;
|
||||
|
||||
let parsed = TomlConfig::from_toml(toml).expect("This should parse");
|
||||
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||
|
||||
let htmlconfig = config.get_html_config();
|
||||
|
||||
assert_eq!(htmlconfig.get_additional_js(), &[PathBuf::from("root/custom.js"), PathBuf::from("root/two/custom.js")]);
|
||||
}
|
||||
Reference in New Issue
Block a user