mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 17:21:52 -05:00
Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8bba0b94d | ||
|
|
e5a973a18d | ||
|
|
e218257e42 | ||
|
|
1345c05b18 | ||
|
|
5e3a3f3482 | ||
|
|
7f46071faa | ||
|
|
5674da2afb | ||
|
|
01341a7705 | ||
|
|
0c624d0f74 | ||
|
|
58cfef00f2 | ||
|
|
6af3eea24b | ||
|
|
c88656284c | ||
|
|
14a28080c1 | ||
|
|
7fa36f82b0 | ||
|
|
3a30e65eef | ||
|
|
fab24f5224 | ||
|
|
cfa4295d79 | ||
|
|
d7f38d08fd | ||
|
|
864be6cf42 | ||
|
|
ec42e2f771 | ||
|
|
aba153a271 | ||
|
|
280dabecd7 | ||
|
|
38b3516b60 | ||
|
|
d609988264 | ||
|
|
95fd292b4f | ||
|
|
f3fb1f1e16 | ||
|
|
152ebba762 | ||
|
|
23d25c853e | ||
|
|
b97a8205f6 | ||
|
|
82faec6b5a | ||
|
|
32814f6f71 | ||
|
|
ac6f15cb27 | ||
|
|
0d6185ac96 | ||
|
|
4b31ae6789 | ||
|
|
1afa2debc1 | ||
|
|
3a71371946 | ||
|
|
9a318adc03 | ||
|
|
c7b4147ba7 | ||
|
|
1ac2602360 | ||
|
|
09729aaca5 | ||
|
|
3ffd24df63 | ||
|
|
fe8d46b8e6 | ||
|
|
21bc3d47c8 | ||
|
|
f2b87f7944 | ||
|
|
894a03655e | ||
|
|
6b2572e78d | ||
|
|
fe287a1eca | ||
|
|
375502a6fa | ||
|
|
a6e1844aad | ||
|
|
d92852867b | ||
|
|
0f0750df52 | ||
|
|
712adcf737 | ||
|
|
3a0cfc87df | ||
|
|
b1e384b03b | ||
|
|
6410e792d7 | ||
|
|
b75243f1f5 | ||
|
|
f1df53a4bb | ||
|
|
8a178e311d | ||
|
|
53ec61ac70 | ||
|
|
97d46e79b7 | ||
|
|
552e39c897 | ||
|
|
791487bc84 | ||
|
|
f67ae7c71a | ||
|
|
e53dcdcf4d | ||
|
|
85d8e2ebd3 | ||
|
|
a9e5dc63f1 | ||
|
|
cf35e08abc | ||
|
|
c986b3afc4 | ||
|
|
08b5d14f7e | ||
|
|
f9101ca62c | ||
|
|
f0c0d71326 | ||
|
|
d2f3eb5007 | ||
|
|
67aee5c192 | ||
|
|
30eb85711e | ||
|
|
b0d33e76ec | ||
|
|
eb65f3fd1e | ||
|
|
2600c62cf9 | ||
|
|
ecae442d25 | ||
|
|
f26f41fde3 | ||
|
|
b91f817bfd | ||
|
|
528945d67d | ||
|
|
4852e9e65a | ||
|
|
e54b6643e1 | ||
|
|
c7a95ccb8b | ||
|
|
81a8f946b7 | ||
|
|
04a643805a | ||
|
|
49608b560b | ||
|
|
9e634a4e83 | ||
|
|
c2c721025d | ||
|
|
2dfc25fc6e | ||
|
|
8f8893bab2 | ||
|
|
4153db2624 | ||
|
|
db11ff27f4 | ||
|
|
b584f6eb9c | ||
|
|
a7ae0b99c4 | ||
|
|
9732a3bc7d | ||
|
|
f3f9c93765 | ||
|
|
a0d8013242 | ||
|
|
1b9d55bcd5 | ||
|
|
a459a3606e | ||
|
|
1e6bccd924 | ||
|
|
6d77b7fd83 | ||
|
|
f9ea6135c3 | ||
|
|
317023cd0e | ||
|
|
0b88b043d0 | ||
|
|
ac725cb39d |
@@ -26,7 +26,8 @@ matrix:
|
||||
apt:
|
||||
packages:
|
||||
- nodejs
|
||||
- npm
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=stable
|
||||
# Beta channel
|
||||
- os: osx
|
||||
env: TARGET=i686-apple-darwin CHANNEL=beta
|
||||
@@ -39,6 +40,8 @@ matrix:
|
||||
env: TARGET=x86_64-apple-darwin CHANNEL=beta
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=beta
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=beta
|
||||
# Nightly channel
|
||||
- os: osx
|
||||
env: TARGET=i686-apple-darwin CHANNEL=nightly
|
||||
@@ -51,6 +54,8 @@ matrix:
|
||||
env: TARGET=x86_64-apple-darwin CHANNEL=nightly
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=nightly
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=nightly
|
||||
|
||||
# Musl builds fail due to a bug in Rust (https://github.com/azerupi/mdBook/issues/158)
|
||||
allow_failures:
|
||||
|
||||
24
Cargo.toml
24
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.0.12"
|
||||
version = "0.0.17"
|
||||
authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
|
||||
description = "create books from markdown files (like Gitbook)"
|
||||
documentation = "http://azerupi.github.io/mdBook/index.html"
|
||||
@@ -15,27 +15,31 @@ exclude = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
clap = "2.2.1"
|
||||
handlebars = "0.20.0"
|
||||
rustc-serialize = "0.3.18"
|
||||
clap = "2.19.2"
|
||||
handlebars = { version = "0.25.0", features = ["serde_type"] }
|
||||
serde = "0.9"
|
||||
serde_json = "0.9"
|
||||
pulldown-cmark = "0.0.8"
|
||||
log = "0.3"
|
||||
env_logger = "0.4.0"
|
||||
toml = { version = "0.3", features = ["serde"] }
|
||||
open = "1.1"
|
||||
regex = "0.2.1"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "2.5.5", optional = true }
|
||||
notify = { version = "4.0", optional = true }
|
||||
time = { version = "0.1.34", optional = true }
|
||||
crossbeam = { version = "0.2.8", optional = true }
|
||||
|
||||
# Serve feature
|
||||
iron = { version = "0.4", optional = true }
|
||||
staticfile = { version = "0.3", optional = true }
|
||||
ws = { version = "0.5.1", optional = true}
|
||||
|
||||
iron = { version = "0.5", optional = true }
|
||||
staticfile = { version = "0.4", optional = true }
|
||||
ws = { version = "0.6", optional = true}
|
||||
|
||||
# Tests
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3.4"
|
||||
|
||||
|
||||
[features]
|
||||
default = ["output", "watch", "serve"]
|
||||
debug = []
|
||||
|
||||
20
README.md
20
README.md
@@ -21,15 +21,15 @@
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
mdBook is a utility to create modern online books from markdown files.
|
||||
mdBook is a utility to create modern online books from Markdown files.
|
||||
|
||||
**This project is still evolving.**
|
||||
For more information, check the issue tracker.
|
||||
**This project is still evolving.**
|
||||
See [#90](https://github.com/azerupi/mdBook/issues/90)
|
||||
|
||||
|
||||
## 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 [**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.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -47,12 +47,12 @@ There are multiple ways to install mdBook.
|
||||
This will download and compile mdBook for you, the only thing left to do is to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
3. **From Git**
|
||||
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***!
|
||||
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
|
||||
```
|
||||
Again, make sure to add the Cargo bin directory to your `PATH`
|
||||
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
4. **For Contributions**
|
||||
If you want to contribute to mdBook you will have to clone the repository on your local machine:
|
||||
@@ -66,7 +66,7 @@ There are multiple ways to install mdBook.
|
||||
cargo build
|
||||
```
|
||||
|
||||
the resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
|
||||
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ There are multiple ways to install mdBook.
|
||||
|
||||
mdBook will primaraly 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 [documentation](http://azerupi.github.io/mdBook/).
|
||||
|
||||
- `mdbook init`
|
||||
|
||||
@@ -106,7 +106,7 @@ Here are the main commands you will want to run, for a more exhaustive explanati
|
||||
|
||||
### As a library
|
||||
|
||||
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 and easy to use API and more!
|
||||
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.
|
||||
|
||||
@@ -123,4 +123,4 @@ You can pick any issue you want to work on. Usually it's a good idea to ask if s
|
||||
|
||||
## License
|
||||
|
||||
All the code is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE](LICENSE) file
|
||||
All the code is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE](LICENSE) file.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "mdBook Documentation",
|
||||
"description": "Create book from markdown files. Like Gitbook but implemented in Rust",
|
||||
"author": "Mathieu David"
|
||||
}
|
||||
3
book-example/book.toml
Normal file
3
book-example/book.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
author = "Mathieu David"
|
||||
@@ -1,10 +1,13 @@
|
||||
# Summary
|
||||
|
||||
[Introduction](misc/introduction.md)
|
||||
|
||||
- [mdBook](README.md)
|
||||
- [Command Line Tool](cli/cli-tool.md)
|
||||
- [init](cli/init.md)
|
||||
- [build](cli/build.md)
|
||||
- [watch](cli/watch.md)
|
||||
- [serve](cli/serve.md)
|
||||
- [test](cli/test.md)
|
||||
- [Format](format/format.md)
|
||||
- [SUMMARY.md](format/summary.md)
|
||||
|
||||
@@ -21,6 +21,15 @@ current working directory.
|
||||
mdbook build path/to/book
|
||||
```
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) option, mdbook will open the rendered book in
|
||||
your default web browser after building it.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
|
||||
|
||||
-------------------
|
||||
|
||||
***note:*** *make sure to run the build command in the root directory and not in the source directory*
|
||||
|
||||
@@ -22,7 +22,7 @@ configuration files, etc.
|
||||
- The `book` directory is where your book is rendered. All the output is ready to be uploaded
|
||||
to a server to be seen by your audience.
|
||||
|
||||
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is discussed in more detail in another [chapter](../format/summary.html).
|
||||
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is discussed in more detail in another [chapter](format/summary.html).
|
||||
|
||||
#### Tip & Trick: Hidden Feature
|
||||
When a `SUMMARY.md` file already exists, the `init` command will first parse it and generate the missing files according to the paths used in the `SUMMARY.md`. This allows you to think and create the whole structure of your book and then let mdBook generate it for you.
|
||||
|
||||
40
book-example/src/cli/serve.md
Normal file
40
book-example/src/cli/serve.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# The serve command
|
||||
|
||||
The `serve` command is useful when you want to preview your book. It also does hot reloading of the webpage whenever a file changes.
|
||||
It achieves this by serving the books content over `localhost:3000` (unless otherwise configured, see below) and runs a websocket server on `localhost:3001` which triggers the reloads.
|
||||
This preferred by many for writing books with mdbook because it allows for you to see the result of your work instantly after every file change.
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
Like `watch`, `serve` can take a directory as argument to use instead of the
|
||||
current working directory.
|
||||
|
||||
```bash
|
||||
mdbook serve path/to/book
|
||||
```
|
||||
|
||||
|
||||
#### Server options
|
||||
|
||||
`serve` has four options: the http port, the websocket port, the interface to serve on, and the public address of the server so that the browser may reach the websocket server.
|
||||
|
||||
For example: suppose you had an nginx server for SSL termination which has a public address of 192.168.1.100 on port 80 and proxied that to 127.0.0.1 on port 8000. To run use the nginx proxy do:
|
||||
|
||||
```bash
|
||||
mdbook server path/to/book -p 8000 -i 127.0.0.1 -a 192.168.1.100
|
||||
```
|
||||
|
||||
If you were to want live reloading for this you would need to proxy the websocket calls through nginx as well from `192.168.1.100:<WS_PORT>` to `127.0.0.1:<WS_PORT>`. The `-w` flag allows for the websocket port to be configured.
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) option, mdbook will open the book in your
|
||||
your default web browser after starting the server.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
|
||||
|
||||
-----
|
||||
|
||||
***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)*
|
||||
@@ -10,7 +10,7 @@ mdBook supports a `test` command that will run all available tests in mdBook. At
|
||||
- checking for unused files
|
||||
- ...
|
||||
|
||||
In the future I would like the user to be able to enable / disable test from the `book.json` configuration file and support custom tests.
|
||||
In the future I would like the user to be able to enable / disable test from the `book.toml` configuration file and support custom tests.
|
||||
|
||||
**How to use it:**
|
||||
```bash
|
||||
|
||||
@@ -12,6 +12,14 @@ current working directory.
|
||||
mdbook watch path/to/book
|
||||
```
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) option, mdbook will open the rendered book in
|
||||
your default web browser.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
|
||||
|
||||
-----
|
||||
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
# Configuration
|
||||
|
||||
You can configure the parameters for your book in the ***book.json*** file.
|
||||
You can configure the parameters for your book in the ***book.toml*** file.
|
||||
|
||||
Here is an example of what a ***book.json*** file might look like:
|
||||
We encourage using the TOML format, but JSON is also recognized and parsed.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Example book",
|
||||
"author": "Name",
|
||||
"description": "The example book covers examples.",
|
||||
"dest": "output/my-book"
|
||||
}
|
||||
Here is an example of what a ***book.toml*** file might look like:
|
||||
|
||||
```toml
|
||||
title = "Example book"
|
||||
author = "Name"
|
||||
description = "The example book covers examples."
|
||||
dest = "output/my-book"
|
||||
```
|
||||
|
||||
#### Supported variables
|
||||
|
||||
- **title:** title of the book
|
||||
- **author:** author of the book
|
||||
- **description:** description, which is added as meta in the html head of each page.
|
||||
- **dest:** path to the directory where you want your book to be rendered. If a relative path is given it will be relative to the parent directory of the source directory
|
||||
If relative paths are given, they will be relative to the book's root, i.e. the
|
||||
parent directory of the source directory.
|
||||
|
||||
- **title:** The title of the book.
|
||||
- **author:** The author of the book.
|
||||
- **description:** The description, which is added as meta in the html head of each page.
|
||||
- **src:** The path to the book's source files (chapters in Markdown, SUMMARY.md, etc.). Defaults to `root/src`.
|
||||
- **dest:** The path to the directory where you want your book to be rendered. Defaults to `root/book`.
|
||||
- **theme_path:** The path to a custom theme directory. Defaults to `root/theme`.
|
||||
|
||||
***note:*** *the supported configurable parameters are scarce at the moment, but more will be added in the future*
|
||||
|
||||
@@ -4,5 +4,5 @@ In this section you will learn how to:
|
||||
|
||||
- Structure your book correctly
|
||||
- Format your `SUMMARY.md` file
|
||||
- Configure your book using `book.json`
|
||||
- Configure your book using `book.toml`
|
||||
- Customize your theme
|
||||
|
||||
@@ -16,6 +16,6 @@ To indicate a block equation
|
||||
|
||||
use
|
||||
|
||||
```
|
||||
```bash
|
||||
\\[ \mu = \frac{1}{N} \sum_{i=0} x_i \\]
|
||||
```
|
||||
|
||||
@@ -5,23 +5,23 @@
|
||||
There is a feature in mdBook that let's you hide code lines by prepending them with a `#`.
|
||||
|
||||
```bash
|
||||
#fn main() {
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 6;
|
||||
|
||||
println!("{}", x + y);
|
||||
#}
|
||||
# }
|
||||
```
|
||||
|
||||
Will render as
|
||||
|
||||
```rust
|
||||
#fn main() {
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 7;
|
||||
|
||||
println!("{}", x + y);
|
||||
#}
|
||||
# }
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,6 @@ allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
|
||||
```
|
||||
You can either use `-` or `*` to indicate a numbered chapter.
|
||||
|
||||
4. ***Sufix Chapter*** After the numbered chapters you can add a couple of non-numbered chapters. They are the same as prefix chapters but come after the numbered chapters instead of before.
|
||||
4. ***Suffix Chapter*** After the numbered chapters you can add a couple of non-numbered chapters. They are the same as prefix chapters but come after the numbered chapters instead of before.
|
||||
|
||||
All other elements are unsupported and will be ignored at best or result in an error.
|
||||
|
||||
@@ -19,7 +19,8 @@ Here is a list of the properties that are exposed:
|
||||
|
||||
- ***language*** Language of the book in the form `en`. To use in <code class="language-html">\<html lang="{{ language }}"></code> for example.
|
||||
At the moment it is hardcoded.
|
||||
- ***title*** Title of the book, as specified in `book.json`
|
||||
- ***title*** Title of the book, as specified in `book.toml`
|
||||
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
||||
|
||||
- ***path*** Relative path to the original markdown file from the source directory
|
||||
- ***content*** This is the rendered markdown.
|
||||
|
||||
@@ -28,26 +28,26 @@ There is a feature in mdBook that let's you hide code lines by prepending them w
|
||||
|
||||
|
||||
```bash
|
||||
#fn main() {
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 6;
|
||||
|
||||
println!("{}", x + y);
|
||||
#}
|
||||
# }
|
||||
```
|
||||
|
||||
Will render as
|
||||
|
||||
```rust
|
||||
#fn main() {
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 7;
|
||||
|
||||
println!("{}", x + y);
|
||||
#}
|
||||
# }
|
||||
```
|
||||
|
||||
**At the moment, this only works for code examples that are annotated with `rust`. Because it would collide with semantics of some programming languages. In the future, we want to make this configurable through the `book.json` so that everyone can benefit from it.**
|
||||
**At the moment, this only works for code examples that are annotated with `rust`. Because it would collide with semantics of some programming languages. In the future, we want to make this configurable through the `book.toml` so that everyone can benefit from it.**
|
||||
|
||||
|
||||
## Improve default theme
|
||||
|
||||
@@ -13,7 +13,7 @@ fn main() {
|
||||
let mut book = MDBook::new(Path::new("my-book")) // Path to root
|
||||
.set_src(Path::new("src")) // Path from root to source directory
|
||||
.set_dest(Path::new("book")) // Path from root to output directory
|
||||
.read_config(); // Parse book.json file for configuration
|
||||
.read_config(); // Parse book.toml or book.json file for configuration
|
||||
|
||||
book.build().unwrap(); // Render the book
|
||||
}
|
||||
|
||||
3
book-example/src/misc/introduction.md
Normal file
3
book-example/src/misc/introduction.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Introduction
|
||||
|
||||
A frontmatter chapter.
|
||||
@@ -1,7 +1,9 @@
|
||||
#[macro_use]
|
||||
extern crate mdbook;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate log;
|
||||
extern crate env_logger;
|
||||
extern crate open;
|
||||
|
||||
// Dependencies for the Watch feature
|
||||
#[cfg(feature = "watch")]
|
||||
@@ -21,6 +23,7 @@ extern crate ws;
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
@@ -30,6 +33,8 @@ use clap::{App, ArgMatches, SubCommand, AppSettings};
|
||||
#[cfg(feature = "watch")]
|
||||
use notify::Watcher;
|
||||
#[cfg(feature = "watch")]
|
||||
use std::time::Duration;
|
||||
#[cfg(feature = "watch")]
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
|
||||
@@ -38,6 +43,8 @@ use mdbook::MDBook;
|
||||
const NAME: &'static str = "mdbook";
|
||||
|
||||
fn main() {
|
||||
env_logger::init().unwrap();
|
||||
|
||||
// Create a list of valid arguments and sub-commands
|
||||
let matches = App::new(NAME)
|
||||
.about("Create a book in form of a static website from markdown files")
|
||||
@@ -49,20 +56,28 @@ fn main() {
|
||||
.subcommand(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 ommitted)'")
|
||||
.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'"))
|
||||
.subcommand(SubCommand::with_name("build")
|
||||
.about("Build the book from the markdown files")
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
|
||||
.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("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'"))
|
||||
.subcommand(SubCommand::with_name("watch")
|
||||
.about("Watch the files for changes")
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
|
||||
.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("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'"))
|
||||
.subcommand(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 ommitted)'")
|
||||
.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("-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'"))
|
||||
.subcommand(SubCommand::with_name("test")
|
||||
.about("Test that code samples compile"))
|
||||
.get_matches();
|
||||
@@ -81,6 +96,7 @@ fn main() {
|
||||
|
||||
if let Err(e) = res {
|
||||
writeln!(&mut io::stderr(), "An error occured:\n{}", e).ok();
|
||||
::std::process::exit(101);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,10 +167,19 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
// Build command implementation
|
||||
fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir).read_config();
|
||||
let book = MDBook::new(&book_dir).read_config();
|
||||
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
|
||||
None => book
|
||||
};
|
||||
|
||||
try!(book.build());
|
||||
|
||||
if args.is_present("open") {
|
||||
open(book.get_dest().join("index.html"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -163,17 +188,24 @@ fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
#[cfg(feature = "watch")]
|
||||
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir).read_config();
|
||||
let book = MDBook::new(&book_dir).read_config();
|
||||
|
||||
trigger_on_change(&mut book, |event, book| {
|
||||
if let Some(path) = event.path {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
match book.build() {
|
||||
Err(e) => println!("Error while building: {:?}", e),
|
||||
_ => {},
|
||||
}
|
||||
println!("");
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
|
||||
None => book
|
||||
};
|
||||
|
||||
if args.is_present("open") {
|
||||
try!(book.build());
|
||||
open(book.get_dest().join("index.html"));
|
||||
}
|
||||
|
||||
trigger_on_change(&mut book, |path, book| {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
if let Err(e) = book.build() {
|
||||
println!("Error while building: {:?}", e);
|
||||
}
|
||||
println!("");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -186,16 +218,25 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
const RELOAD_COMMAND: &'static str = "reload";
|
||||
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir).read_config();
|
||||
let book = MDBook::new(&book_dir).read_config();
|
||||
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
|
||||
None => book
|
||||
};
|
||||
|
||||
let port = args.value_of("port").unwrap_or("3000");
|
||||
let ws_port = args.value_of("ws-port").unwrap_or("3001");
|
||||
let interface = args.value_of("interface").unwrap_or("localhost");
|
||||
let public_address = args.value_of("address").unwrap_or(interface);
|
||||
let open_browser = args.is_present("open");
|
||||
|
||||
let address = format!("localhost:{}", port);
|
||||
let ws_address = format!("localhost:{}", ws_port);
|
||||
let address = format!("{}:{}", interface, port);
|
||||
let ws_address = format!("{}:{}", interface, ws_port);
|
||||
|
||||
book.set_livereload(format!(r#"
|
||||
<script type="text/javascript">
|
||||
var socket = new WebSocket("ws://localhost:{}");
|
||||
var socket = new WebSocket("ws://{}:{}");
|
||||
socket.onmessage = function (event) {{
|
||||
if (event.data === "{}") {{
|
||||
socket.close();
|
||||
@@ -207,7 +248,7 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
socket.close();
|
||||
}}
|
||||
</script>
|
||||
"#, ws_port, RELOAD_COMMAND).to_owned());
|
||||
"#, public_address, ws_port, RELOAD_COMMAND).to_owned());
|
||||
|
||||
try!(book.build());
|
||||
|
||||
@@ -229,15 +270,17 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
|
||||
println!("\nServing on {}", address);
|
||||
|
||||
trigger_on_change(&mut book, move |event, book| {
|
||||
if let Some(path) = event.path {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
match book.build() {
|
||||
Err(e) => println!("Error while building: {:?}", e),
|
||||
_ => broadcaster.send(RELOAD_COMMAND).unwrap(),
|
||||
}
|
||||
println!("");
|
||||
if open_browser {
|
||||
open(format!("http://{}", address));
|
||||
}
|
||||
|
||||
trigger_on_change(&mut book, move |path, book| {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
match book.build() {
|
||||
Err(e) => println!("Error while building: {:?}", e),
|
||||
_ => broadcaster.send(RELOAD_COMMAND).unwrap(),
|
||||
}
|
||||
println!("");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -268,57 +311,65 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
fn open<P: AsRef<OsStr>>(path: P) {
|
||||
if let Err(e) = open::that(path) {
|
||||
println!("Error opening web browser: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calls the closure when a book source file is changed. This is blocking!
|
||||
#[cfg(feature = "watch")]
|
||||
fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
||||
where F: Fn(notify::Event, &mut MDBook) -> ()
|
||||
where F: Fn(&Path, &mut MDBook) -> ()
|
||||
{
|
||||
use notify::RecursiveMode::*;
|
||||
use notify::DebouncedEvent::*;
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);
|
||||
|
||||
match w {
|
||||
Ok(mut watcher) => {
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.get_src()) {
|
||||
println!("Error while watching {:?}:\n {:?}", book.get_src(), e);
|
||||
::std::process::exit(0);
|
||||
};
|
||||
|
||||
// Add the book.json file to the watcher if it exists, because it's not
|
||||
// located in the source directory
|
||||
if let Err(_) = watcher.watch(book.get_root().join("book.json")) {
|
||||
// do nothing if book.json is not found
|
||||
}
|
||||
|
||||
let mut previous_time = time::get_time();
|
||||
|
||||
println!("\nListening for changes...\n");
|
||||
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(event) => {
|
||||
// Skip the event if an event has already been issued in the last second
|
||||
let time = time::get_time();
|
||||
if time - previous_time < time::Duration::seconds(1) {
|
||||
continue;
|
||||
} else {
|
||||
previous_time = time;
|
||||
}
|
||||
|
||||
closure(event, book);
|
||||
},
|
||||
Err(e) => {
|
||||
println!("An error occured: {:?}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) {
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
println!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
::std::process::exit(0);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.get_src(), Recursive) {
|
||||
println!("Error while watching {:?}:\n {:?}", book.get_src(), e);
|
||||
::std::process::exit(0);
|
||||
};
|
||||
|
||||
// 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() {
|
||||
// do nothing if book.json is not found
|
||||
}
|
||||
if watcher.watch(book.get_root().join("book.toml"), NonRecursive).is_err() {
|
||||
// do nothing if book.toml is not found
|
||||
}
|
||||
|
||||
println!("\nListening for changes...\n");
|
||||
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(event) => match event {
|
||||
NoticeWrite(path) |
|
||||
NoticeRemove(path) |
|
||||
Create(path) |
|
||||
Write(path) |
|
||||
Remove(path) |
|
||||
Rename(_, path) => {
|
||||
closure(&path, book);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("An error occured: {:?}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,40 @@
|
||||
use rustc_serialize::json::Json;
|
||||
extern crate toml;
|
||||
|
||||
use std::process::exit;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use serde_json;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BookConfig {
|
||||
pub title: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
root: PathBuf,
|
||||
pub dest: PathBuf,
|
||||
pub src: PathBuf,
|
||||
pub theme_path: PathBuf,
|
||||
|
||||
pub title: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
|
||||
pub indent_spaces: i32,
|
||||
multilingual: bool,
|
||||
}
|
||||
|
||||
|
||||
impl BookConfig {
|
||||
pub fn new(root: &Path) -> Self {
|
||||
BookConfig {
|
||||
title: String::new(),
|
||||
author: String::new(),
|
||||
description: String::new(),
|
||||
root: root.to_owned(),
|
||||
dest: root.join("book"),
|
||||
src: root.join("src"),
|
||||
theme_path: root.join("theme"),
|
||||
|
||||
title: String::new(),
|
||||
author: String::new(),
|
||||
description: String::new(),
|
||||
|
||||
indent_spaces: 4, // indentation used for SUMMARY.md
|
||||
multilingual: false,
|
||||
}
|
||||
@@ -34,55 +44,114 @@ impl BookConfig {
|
||||
|
||||
debug!("[fn]: read_config");
|
||||
|
||||
// If the file does not exist, return early
|
||||
let mut config_file = match File::open(root.join("book.json")) {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
debug!("[*]: Failed to open {:?}", root.join("book.json"));
|
||||
return self;
|
||||
},
|
||||
let read_file = |path: PathBuf| -> String {
|
||||
let mut data = String::new();
|
||||
let mut f: File = match File::open(&path) {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
error!("[*]: Failed to open {:?}", &path);
|
||||
exit(2);
|
||||
}
|
||||
};
|
||||
if f.read_to_string(&mut data).is_err() {
|
||||
error!("[*]: Failed to read {:?}", &path);
|
||||
exit(2);
|
||||
}
|
||||
data
|
||||
};
|
||||
|
||||
debug!("[*]: Reading config");
|
||||
let mut data = String::new();
|
||||
// Read book.toml or book.json if exists
|
||||
|
||||
// Just return if an error occured.
|
||||
// I would like to propagate the error, but I have to return `&self`
|
||||
if let Err(_) = config_file.read_to_string(&mut data) {
|
||||
return self;
|
||||
if root.join("book.toml").exists() {
|
||||
|
||||
debug!("[*]: Reading config");
|
||||
let data = read_file(root.join("book.toml"));
|
||||
self.parse_from_toml_string(&data);
|
||||
|
||||
} else if root.join("book.json").exists() {
|
||||
|
||||
debug!("[*]: Reading config");
|
||||
let data = read_file(root.join("book.json"));
|
||||
self.parse_from_json_string(&data);
|
||||
|
||||
} else {
|
||||
debug!("[*]: No book.toml or book.json was found, using defaults.");
|
||||
}
|
||||
|
||||
// Convert to JSON
|
||||
if let Ok(config) = Json::from_str(&data) {
|
||||
// Extract data
|
||||
self
|
||||
}
|
||||
|
||||
debug!("[*]: Extracting data from config");
|
||||
// Title, author, description
|
||||
if let Some(a) = config.find_path(&["title"]) {
|
||||
self.title = a.to_string().replace("\"", "")
|
||||
}
|
||||
if let Some(a) = config.find_path(&["author"]) {
|
||||
self.author = a.to_string().replace("\"", "")
|
||||
}
|
||||
if let Some(a) = config.find_path(&["description"]) {
|
||||
self.description = a.to_string().replace("\"", "")
|
||||
pub fn parse_from_toml_string(&mut self, data: &str) -> &mut Self {
|
||||
let config = match toml::from_str(data) {
|
||||
Ok(x) => {x},
|
||||
Err(e) => {
|
||||
error!("[*]: Toml parse errors in book.toml: {:?}", e);
|
||||
exit(2);
|
||||
}
|
||||
};
|
||||
|
||||
// Destination
|
||||
if let Some(a) = config.find_path(&["dest"]) {
|
||||
let dest = PathBuf::from(&a.to_string().replace("\"", ""));
|
||||
self.parse_from_btreemap(&config);
|
||||
|
||||
// If path is relative make it absolute from the parent directory of src
|
||||
match dest.is_relative() {
|
||||
true => {
|
||||
let dest = self.get_root().join(&dest).to_owned();
|
||||
self.set_dest(&dest);
|
||||
},
|
||||
false => {
|
||||
self.set_dest(&dest);
|
||||
},
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Parses the string to JSON and converts it to BTreeMap<String, toml::Value>.
|
||||
pub fn parse_from_json_string(&mut self, data: &str) -> &mut Self {
|
||||
|
||||
let c: serde_json::Value = match serde_json::from_str(data) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
error!("[*]: JSON parse errors in book.json: {:?}", e);
|
||||
exit(2);
|
||||
}
|
||||
};
|
||||
|
||||
let config = json_object_to_btreemap(c.as_object().unwrap());
|
||||
self.parse_from_btreemap(&config);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn parse_from_btreemap(&mut self, config: &BTreeMap<String, toml::Value>) -> &mut Self {
|
||||
|
||||
// Title, author, description
|
||||
if let Some(a) = config.get("title") {
|
||||
self.title = a.to_string().replace("\"", "");
|
||||
}
|
||||
if let Some(a) = config.get("author") {
|
||||
self.author = a.to_string().replace("\"", "");
|
||||
}
|
||||
if let Some(a) = config.get("description") {
|
||||
self.description = a.to_string().replace("\"", "");
|
||||
}
|
||||
|
||||
// Destination folder
|
||||
if let Some(a) = config.get("dest") {
|
||||
let mut dest = PathBuf::from(&a.to_string().replace("\"", ""));
|
||||
|
||||
// If path is relative make it absolute from the parent directory of src
|
||||
if dest.is_relative() {
|
||||
dest = self.get_root().join(&dest);
|
||||
}
|
||||
self.set_dest(&dest);
|
||||
}
|
||||
|
||||
// Source folder
|
||||
if let Some(a) = config.get("src") {
|
||||
let mut src = PathBuf::from(&a.to_string().replace("\"", ""));
|
||||
if src.is_relative() {
|
||||
src = self.get_root().join(&src);
|
||||
}
|
||||
self.set_src(&src);
|
||||
}
|
||||
|
||||
// Theme path folder
|
||||
if let Some(a) = config.get("theme_path") {
|
||||
let mut theme_path = PathBuf::from(&a.to_string().replace("\"", ""));
|
||||
if theme_path.is_relative() {
|
||||
theme_path = self.get_root().join(&theme_path);
|
||||
}
|
||||
self.set_theme_path(&theme_path);
|
||||
}
|
||||
|
||||
self
|
||||
@@ -114,4 +183,43 @@ impl BookConfig {
|
||||
self.src = src.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_theme_path(&self) -> &Path {
|
||||
&self.theme_path
|
||||
}
|
||||
|
||||
pub fn set_theme_path(&mut self, theme_path: &Path) -> &mut Self {
|
||||
self.theme_path = theme_path.to_owned();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn json_object_to_btreemap(json: &serde_json::Map<String, serde_json::Value>) -> BTreeMap<String, toml::Value> {
|
||||
let mut config: BTreeMap<String, toml::Value> = BTreeMap::new();
|
||||
|
||||
for (key, value) in json.iter() {
|
||||
config.insert(
|
||||
String::from_str(key).unwrap(),
|
||||
json_value_to_toml_value(value.to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
pub fn json_value_to_toml_value(json: serde_json::Value) -> toml::Value {
|
||||
match json {
|
||||
serde_json::Value::Null => toml::Value::String("".to_string()),
|
||||
serde_json::Value::Bool(x) => toml::Value::Boolean(x),
|
||||
serde_json::Value::Number(ref x) if x.is_i64() => toml::Value::Integer(x.as_i64().unwrap()),
|
||||
serde_json::Value::Number(ref x) if x.is_u64() => toml::Value::Integer(x.as_i64().unwrap()),
|
||||
serde_json::Value::Number(x) => toml::Value::Float(x.as_f64().unwrap()),
|
||||
serde_json::Value::String(x) => toml::Value::String(x),
|
||||
serde_json::Value::Array(x) => {
|
||||
toml::Value::Array(x.iter().map(|v| json_value_to_toml_value(v.to_owned())).collect())
|
||||
},
|
||||
serde_json::Value::Object(x) => {
|
||||
toml::Value::Table(json_object_to_btreemap(&x))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
349
src/book/bookconfig_test.rs
Normal file
349
src/book/bookconfig_test.rs
Normal file
@@ -0,0 +1,349 @@
|
||||
#![cfg(test)]
|
||||
|
||||
use std::path::Path;
|
||||
use serde_json;
|
||||
use book::bookconfig::*;
|
||||
|
||||
#[test]
|
||||
fn it_parses_json_config() {
|
||||
let text = r#"
|
||||
{
|
||||
"title": "mdBook Documentation",
|
||||
"description": "Create book from markdown files. Like Gitbook but implemented in Rust",
|
||||
"author": "Mathieu David"
|
||||
}"#;
|
||||
|
||||
// TODO don't require path argument, take pwd
|
||||
let mut config = BookConfig::new(Path::new("."));
|
||||
|
||||
config.parse_from_json_string(&text.to_string());
|
||||
|
||||
let mut expected = BookConfig::new(Path::new("."));
|
||||
expected.title = "mdBook Documentation".to_string();
|
||||
expected.author = "Mathieu David".to_string();
|
||||
expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string();
|
||||
|
||||
assert_eq!(format!("{:#?}", config), format!("{:#?}", expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_parses_toml_config() {
|
||||
let text = r#"
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
author = "Mathieu David"
|
||||
"#;
|
||||
|
||||
// TODO don't require path argument, take pwd
|
||||
let mut config = BookConfig::new(Path::new("."));
|
||||
|
||||
config.parse_from_toml_string(&text.to_string());
|
||||
|
||||
let mut expected = BookConfig::new(Path::new("."));
|
||||
expected.title = "mdBook Documentation".to_string();
|
||||
expected.author = "Mathieu David".to_string();
|
||||
expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string();
|
||||
|
||||
assert_eq!(format!("{:#?}", config), format!("{:#?}", expected));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_parses_json_nested_array_to_toml() {
|
||||
|
||||
// Example from:
|
||||
// toml-0.2.1/tests/valid/arrays-nested.json
|
||||
|
||||
let text = r#"
|
||||
{
|
||||
"nest": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "array", "value": [
|
||||
{"type": "string", "value": "a"}
|
||||
]},
|
||||
{"type": "array", "value": [
|
||||
{"type": "string", "value": "b"}
|
||||
]}
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let c: serde_json::Value = serde_json::from_str(&text).unwrap();
|
||||
|
||||
let result = json_object_to_btreemap(&c.as_object().unwrap());
|
||||
|
||||
let expected = r#"{
|
||||
"nest": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"a"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"b"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
}"#;
|
||||
|
||||
assert_eq!(format!("{:#?}", result), expected);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn it_parses_json_arrays_to_toml() {
|
||||
|
||||
// Example from:
|
||||
// toml-0.2.1/tests/valid/arrays.json
|
||||
|
||||
let text = r#"
|
||||
{
|
||||
"ints": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "integer", "value": "1"},
|
||||
{"type": "integer", "value": "2"},
|
||||
{"type": "integer", "value": "3"}
|
||||
]
|
||||
},
|
||||
"floats": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "float", "value": "1.1"},
|
||||
{"type": "float", "value": "2.1"},
|
||||
{"type": "float", "value": "3.1"}
|
||||
]
|
||||
},
|
||||
"strings": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "string", "value": "a"},
|
||||
{"type": "string", "value": "b"},
|
||||
{"type": "string", "value": "c"}
|
||||
]
|
||||
},
|
||||
"dates": {
|
||||
"type": "array",
|
||||
"value": [
|
||||
{"type": "datetime", "value": "1987-07-05T17:45:00Z"},
|
||||
{"type": "datetime", "value": "1979-05-27T07:32:00Z"},
|
||||
{"type": "datetime", "value": "2006-06-01T11:00:00Z"}
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let c: serde_json::Value = serde_json::from_str(&text).unwrap();
|
||||
|
||||
let result = json_object_to_btreemap(&c.as_object().unwrap());
|
||||
|
||||
let expected = r#"{
|
||||
"dates": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"datetime"
|
||||
),
|
||||
"value": String(
|
||||
"1987-07-05T17:45:00Z"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"datetime"
|
||||
),
|
||||
"value": String(
|
||||
"1979-05-27T07:32:00Z"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"datetime"
|
||||
),
|
||||
"value": String(
|
||||
"2006-06-01T11:00:00Z"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
"floats": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"float"
|
||||
),
|
||||
"value": String(
|
||||
"1.1"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"float"
|
||||
),
|
||||
"value": String(
|
||||
"2.1"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"float"
|
||||
),
|
||||
"value": String(
|
||||
"3.1"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
"ints": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"integer"
|
||||
),
|
||||
"value": String(
|
||||
"1"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"integer"
|
||||
),
|
||||
"value": String(
|
||||
"2"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"integer"
|
||||
),
|
||||
"value": String(
|
||||
"3"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
),
|
||||
"strings": Table(
|
||||
{
|
||||
"type": String(
|
||||
"array"
|
||||
),
|
||||
"value": Array(
|
||||
[
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"a"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"b"
|
||||
)
|
||||
}
|
||||
),
|
||||
Table(
|
||||
{
|
||||
"type": String(
|
||||
"string"
|
||||
),
|
||||
"value": String(
|
||||
"c"
|
||||
)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
)
|
||||
}"#;
|
||||
|
||||
assert_eq!(format!("{:#?}", result), expected);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use rustc_serialize::json::{Json, ToJson};
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde::ser::SerializeStruct;
|
||||
use std::path::PathBuf;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum BookItem {
|
||||
@@ -36,16 +36,12 @@ impl Chapter {
|
||||
}
|
||||
|
||||
|
||||
impl ToJson for Chapter {
|
||||
fn to_json(&self) -> Json {
|
||||
let mut m: BTreeMap<String, Json> = BTreeMap::new();
|
||||
m.insert("name".to_owned(), self.name.to_json());
|
||||
m.insert("path".to_owned(),
|
||||
self.path
|
||||
.to_str()
|
||||
.expect("Json conversion failed for path")
|
||||
.to_json());
|
||||
m.to_json()
|
||||
impl Serialize for Chapter {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
|
||||
let mut struct_ = try!(serializer.serialize_struct("Chapter", 2));
|
||||
try!(struct_.serialize_field("name", &self.name));
|
||||
try!(struct_.serialize_field("path", &self.path));
|
||||
struct_.end()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +63,7 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
},
|
||||
}
|
||||
} else {
|
||||
let cur = self.items.get(self.current_index).unwrap();
|
||||
let cur = &self.items[self.current_index];
|
||||
|
||||
match *cur {
|
||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
|
||||
|
||||
103
src/book/mod.rs
103
src/book/mod.rs
@@ -1,6 +1,8 @@
|
||||
pub mod bookitem;
|
||||
pub mod bookconfig;
|
||||
|
||||
pub mod bookconfig_test;
|
||||
|
||||
pub use self::bookitem::{BookItem, BookItems};
|
||||
pub use self::bookconfig::BookConfig;
|
||||
|
||||
@@ -20,6 +22,7 @@ pub struct MDBook {
|
||||
root: PathBuf,
|
||||
dest: PathBuf,
|
||||
src: PathBuf,
|
||||
theme_path: PathBuf,
|
||||
|
||||
pub title: String,
|
||||
pub author: String,
|
||||
@@ -34,21 +37,25 @@ pub struct MDBook {
|
||||
impl MDBook {
|
||||
/// Create a new `MDBook` struct with root directory `root`
|
||||
///
|
||||
/// - The default source directory is set to `root/src`
|
||||
/// - The default output directory is set to `root/book`
|
||||
/// Default directory paths:
|
||||
///
|
||||
/// - source: `root/src`
|
||||
/// - output: `root/book`
|
||||
/// - theme: `root/theme`
|
||||
///
|
||||
/// They can both be changed by using [`set_src()`](#method.set_src) and [`set_dest()`](#method.set_dest)
|
||||
|
||||
pub fn new(root: &Path) -> MDBook {
|
||||
|
||||
if !root.exists() || !root.is_dir() {
|
||||
output!("{:?} No directory with that name", root);
|
||||
warn!("{:?} No directory with that name", root);
|
||||
}
|
||||
|
||||
MDBook {
|
||||
root: root.to_owned(),
|
||||
dest: root.join("book"),
|
||||
src: root.join("src"),
|
||||
theme_path: root.join("theme"),
|
||||
|
||||
title: String::new(),
|
||||
author: String::new(),
|
||||
@@ -116,7 +123,7 @@ impl MDBook {
|
||||
|
||||
if !self.root.exists() {
|
||||
fs::create_dir_all(&self.root).unwrap();
|
||||
output!("{:?} created", &self.root);
|
||||
info!("{:?} created", &self.root);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -266,6 +273,16 @@ impl MDBook {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_file<P: AsRef<Path>>(&self, filename: P, content: &[u8]) -> Result<(), Box<Error>> {
|
||||
let path = self.get_dest().join(filename);
|
||||
try!(utils::fs::create_file(&path).and_then(|mut file| {
|
||||
file.write_all(content)
|
||||
}).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::Other, format!("Could not create {}: {}", path.display(), e))
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Parses the `book.json` file (if it exists) to extract the configuration parameters.
|
||||
/// The `book.json` file should be in the root directory of the book.
|
||||
/// The root directory is the one specified when creating a new `MDBook`
|
||||
@@ -294,6 +311,7 @@ impl MDBook {
|
||||
|
||||
self.dest = config.dest;
|
||||
self.src = config.src;
|
||||
self.theme_path = config.theme_path;
|
||||
|
||||
self
|
||||
}
|
||||
@@ -328,29 +346,26 @@ impl MDBook {
|
||||
try!(self.parse_summary());
|
||||
for item in self.iter() {
|
||||
|
||||
match *item {
|
||||
BookItem::Chapter(_, ref ch) => {
|
||||
if ch.path != PathBuf::new() {
|
||||
if let BookItem::Chapter(_, ref ch) = *item {
|
||||
if ch.path != PathBuf::new() {
|
||||
|
||||
let path = self.get_src().join(&ch.path);
|
||||
let path = self.get_src().join(&ch.path);
|
||||
|
||||
println!("[*]: Testing file: {:?}", path);
|
||||
println!("[*]: Testing file: {:?}", path);
|
||||
|
||||
let output_result = Command::new("rustdoc")
|
||||
.arg(&path)
|
||||
.arg("--test")
|
||||
.output();
|
||||
let output = try!(output_result);
|
||||
let output_result = Command::new("rustdoc")
|
||||
.arg(&path)
|
||||
.arg("--test")
|
||||
.output();
|
||||
let output = try!(output_result);
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(Box::new(io::Error::new(ErrorKind::Other, format!(
|
||||
"{}\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)))) as Box<Error>);
|
||||
}
|
||||
if !output.status.success() {
|
||||
return Err(Box::new(io::Error::new(ErrorKind::Other, format!(
|
||||
"{}\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)))) as Box<Error>);
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -363,14 +378,11 @@ impl MDBook {
|
||||
pub fn set_dest(mut self, dest: &Path) -> Self {
|
||||
|
||||
// Handle absolute and relative paths
|
||||
match dest.is_absolute() {
|
||||
true => {
|
||||
self.dest = dest.to_owned();
|
||||
},
|
||||
false => {
|
||||
let dest = self.root.join(dest).to_owned();
|
||||
self.dest = dest;
|
||||
},
|
||||
if dest.is_absolute() {
|
||||
self.dest = dest.to_owned();
|
||||
} else {
|
||||
let dest = self.root.join(dest).to_owned();
|
||||
self.dest = dest;
|
||||
}
|
||||
|
||||
self
|
||||
@@ -383,14 +395,11 @@ impl MDBook {
|
||||
pub fn set_src(mut self, src: &Path) -> Self {
|
||||
|
||||
// Handle absolute and relative paths
|
||||
match src.is_absolute() {
|
||||
true => {
|
||||
self.src = src.to_owned();
|
||||
},
|
||||
false => {
|
||||
let src = self.root.join(src).to_owned();
|
||||
self.src = src;
|
||||
},
|
||||
if src.is_absolute() {
|
||||
self.src = src.to_owned();
|
||||
} else {
|
||||
let src = self.root.join(src).to_owned();
|
||||
self.src = src;
|
||||
}
|
||||
|
||||
self
|
||||
@@ -438,10 +447,20 @@ impl MDBook {
|
||||
}
|
||||
|
||||
pub fn get_livereload(&self) -> Option<&String> {
|
||||
match self.livereload {
|
||||
Some(ref livereload) => Some(&livereload),
|
||||
None => None,
|
||||
}
|
||||
self.livereload.as_ref()
|
||||
}
|
||||
|
||||
pub fn set_theme_path(mut self, theme_path: &Path) -> Self {
|
||||
self.theme_path = if theme_path.is_absolute() {
|
||||
theme_path.to_owned()
|
||||
} else {
|
||||
self.root.join(theme_path).to_owned()
|
||||
};
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_theme_path(&self) -> &Path {
|
||||
&self.theme_path
|
||||
}
|
||||
|
||||
// Construct book
|
||||
|
||||
@@ -69,11 +69,14 @@
|
||||
//!
|
||||
//! Make sure to take a look at it.
|
||||
|
||||
extern crate rustc_serialize;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
extern crate handlebars;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate regex;
|
||||
|
||||
#[macro_use] pub mod macros;
|
||||
#[macro_use] extern crate log;
|
||||
pub mod book;
|
||||
mod parse;
|
||||
pub mod renderer;
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
#[cfg(feature = "debug")]
|
||||
macro_rules! debug {
|
||||
($fmt:expr) => (println!($fmt));
|
||||
($fmt:expr, $($arg:tt)*) => (println!($fmt, $($arg)*));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "debug"))]
|
||||
macro_rules! debug {
|
||||
($fmt:expr) => ();
|
||||
($fmt:expr, $($arg:tt)*) => ();
|
||||
}
|
||||
|
||||
#[cfg(feature = "output")]
|
||||
macro_rules! output {
|
||||
($fmt:expr) => (println!($fmt));
|
||||
($fmt:expr, $($arg:tt)*) => (println!($fmt, $($arg)*));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "output"))]
|
||||
macro_rules! output {
|
||||
($fmt:expr) => ();
|
||||
($fmt:expr, $($arg:tt)*) => ();
|
||||
}
|
||||
@@ -37,7 +37,7 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
section.push(0);
|
||||
let last = items.pop().expect("There should be at least one item since this can't be the root level");
|
||||
|
||||
item = if let BookItem::Chapter(ref s, ref ch) = last {
|
||||
if let BookItem::Chapter(ref s, ref ch) = last {
|
||||
let mut ch = ch.clone();
|
||||
ch.sub_items = try!(parse_level(summary, level, section.clone()));
|
||||
items.push(BookItem::Chapter(s.clone(), ch));
|
||||
@@ -47,12 +47,12 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
continue;
|
||||
} else {
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
format!("Your summary.md is messed up\n\n
|
||||
"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 {
|
||||
@@ -64,25 +64,25 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
// error if level != 0 and BookItem is != Chapter
|
||||
BookItem::Affix(_) | BookItem::Spacer if level > 0 => {
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
format!("Your summary.md is messed up\n\n
|
||||
"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,
|
||||
format!("Your summary.md is messed up\n\n
|
||||
"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
|
||||
|
||||
@@ -3,17 +3,21 @@ use renderer::Renderer;
|
||||
use book::MDBook;
|
||||
use book::bookitem::BookItem;
|
||||
use {utils, theme};
|
||||
use regex::{Regex, Captures};
|
||||
|
||||
use std::ascii::AsciiExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{self, File};
|
||||
use std::error::Error;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::io::{self, Read};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use handlebars::Handlebars;
|
||||
use rustc_serialize::json::{Json, ToJson};
|
||||
|
||||
use serde_json;
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HtmlHandlebars;
|
||||
|
||||
impl HtmlHandlebars {
|
||||
@@ -28,7 +32,7 @@ impl Renderer for HtmlHandlebars {
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
// Load theme
|
||||
let theme = theme::Theme::new(book.get_src());
|
||||
let theme = theme::Theme::new(book.get_theme_path());
|
||||
|
||||
// Register template
|
||||
debug!("[*]: Register handlebars template");
|
||||
@@ -47,7 +51,7 @@ impl Renderer for HtmlHandlebars {
|
||||
|
||||
// Check if dest directory exists
|
||||
debug!("[*]: Check if destination directory exists");
|
||||
if let Err(_) = fs::create_dir_all(book.get_dest()) {
|
||||
if fs::create_dir_all(book.get_dest()).is_err() {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other,
|
||||
"Unexpected error when constructing destination path")));
|
||||
}
|
||||
@@ -79,44 +83,35 @@ impl Renderer for HtmlHandlebars {
|
||||
content = utils::render_markdown(&content);
|
||||
print_content.push_str(&content);
|
||||
|
||||
// Remove content from previous file and render content for this one
|
||||
data.remove("path");
|
||||
match ch.path.to_str() {
|
||||
Some(p) => {
|
||||
data.insert("path".to_owned(), p.to_json());
|
||||
},
|
||||
None => {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other,
|
||||
"Could not convert path to str")))
|
||||
},
|
||||
}
|
||||
// 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"))?;
|
||||
data.insert("path".to_owned(), json!(path));
|
||||
data.insert("content".to_owned(), json!(content));
|
||||
data.insert("chapter_title".to_owned(), json!(ch.name));
|
||||
data.insert("path_to_root".to_owned(), json!(utils::fs::path_to_root(&ch.path)));
|
||||
|
||||
|
||||
// Remove content from previous file and render content for this one
|
||||
data.remove("content");
|
||||
data.insert("content".to_owned(), content.to_json());
|
||||
|
||||
// Remove path to root from previous file and render content for this one
|
||||
data.remove("path_to_root");
|
||||
data.insert("path_to_root".to_owned(), utils::fs::path_to_root(&ch.path).to_json());
|
||||
|
||||
// Rendere the handlebars template with the data
|
||||
// Render the handlebars template with the data
|
||||
debug!("[*]: Render template");
|
||||
let rendered = try!(handlebars.render("index", &data));
|
||||
|
||||
debug!("[*]: Create file {:?}", &book.get_dest().join(&ch.path).with_extension("html"));
|
||||
// Write to file
|
||||
let mut file =
|
||||
try!(utils::fs::create_file(&book.get_dest().join(&ch.path).with_extension("html")));
|
||||
output!("[*] Creating {:?} ✓", &book.get_dest().join(&ch.path).with_extension("html"));
|
||||
let filename = Path::new(&ch.path).with_extension("html");
|
||||
|
||||
try!(file.write_all(&rendered.into_bytes()));
|
||||
// create links for headers and fix anchors
|
||||
let rendered = build_header_links(rendered, filename.to_str().unwrap_or(""));
|
||||
let rendered = fix_anchor_links(rendered, filename.to_str().unwrap_or(""));
|
||||
|
||||
// fix code blocks
|
||||
let rendered = fix_code_blocks(rendered);
|
||||
|
||||
// Write to file
|
||||
info!("[*] Creating {:?} ✓", filename.display());
|
||||
try!(book.write_file(filename, &rendered.into_bytes()));
|
||||
|
||||
// Create an index.html from the first element in SUMMARY.md
|
||||
if index {
|
||||
debug!("[*]: index.html");
|
||||
|
||||
let mut index_file = try!(File::create(book.get_dest().join("index.html")));
|
||||
let mut content = String::new();
|
||||
let _source = try!(File::open(book.get_dest().join(&ch.path.with_extension("html"))))
|
||||
.read_to_string(&mut content);
|
||||
@@ -128,10 +123,10 @@ impl Renderer for HtmlHandlebars {
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
try!(index_file.write_all(content.as_bytes()));
|
||||
try!(book.write_file("index.html", content.as_bytes()));
|
||||
|
||||
output!("[*] Creating index.html from {:?} ✓",
|
||||
book.get_dest().join(&ch.path.with_extension("html")));
|
||||
info!("[*] Creating index.html from {:?} ✓",
|
||||
book.get_dest().join(&ch.path.with_extension("html")));
|
||||
index = false;
|
||||
}
|
||||
}
|
||||
@@ -142,132 +137,41 @@ impl Renderer for HtmlHandlebars {
|
||||
|
||||
// Print version
|
||||
|
||||
// Remove content from previous file and render content for this one
|
||||
data.remove("path");
|
||||
data.insert("path".to_owned(), "print.md".to_json());
|
||||
// Update the context with data for this file
|
||||
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"))));
|
||||
|
||||
// Remove content from previous file and render content for this one
|
||||
data.remove("content");
|
||||
data.insert("content".to_owned(), print_content.to_json());
|
||||
|
||||
// Remove path to root from previous file and render content for this one
|
||||
data.remove("path_to_root");
|
||||
data.insert("path_to_root".to_owned(), utils::fs::path_to_root(Path::new("print.md")).to_json());
|
||||
|
||||
// Rendere the handlebars template with the data
|
||||
// Render the handlebars template with the data
|
||||
debug!("[*]: Render template");
|
||||
|
||||
let rendered = try!(handlebars.render("index", &data));
|
||||
let mut file = try!(utils::fs::create_file(&book.get_dest().join("print").with_extension("html")));
|
||||
try!(file.write_all(&rendered.into_bytes()));
|
||||
output!("[*] Creating print.html ✓");
|
||||
let rendered = build_header_links(rendered, "print.html");
|
||||
let rendered = fix_anchor_links(rendered, "print.html");
|
||||
|
||||
// fix code blocks
|
||||
let rendered = fix_code_blocks(rendered);
|
||||
|
||||
try!(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");
|
||||
// JavaScript
|
||||
let mut js_file = if let Ok(f) = File::create(book.get_dest().join("book.js")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create book.js")));
|
||||
};
|
||||
try!(js_file.write_all(&theme.js));
|
||||
|
||||
// Css
|
||||
let mut css_file = if let Ok(f) = File::create(book.get_dest().join("book.css")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create book.css")));
|
||||
};
|
||||
try!(css_file.write_all(&theme.css));
|
||||
|
||||
// Favicon
|
||||
let mut favicon_file = if let Ok(f) = File::create(book.get_dest().join("favicon.png")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create favicon.png")));
|
||||
};
|
||||
try!(favicon_file.write_all(&theme.favicon));
|
||||
|
||||
// JQuery local fallback
|
||||
let mut jquery = if let Ok(f) = File::create(book.get_dest().join("jquery.js")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create jquery.js")));
|
||||
};
|
||||
try!(jquery.write_all(&theme.jquery));
|
||||
|
||||
// syntax highlighting
|
||||
let mut highlight_css = if let Ok(f) = File::create(book.get_dest().join("highlight.css")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create highlight.css")));
|
||||
};
|
||||
try!(highlight_css.write_all(&theme.highlight_css));
|
||||
|
||||
let mut tomorrow_night_css = if let Ok(f) = File::create(book.get_dest().join("tomorrow-night.css")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create tomorrow-night.css")));
|
||||
};
|
||||
try!(tomorrow_night_css.write_all(&theme.tomorrow_night_css));
|
||||
|
||||
let mut highlight_js = if let Ok(f) = File::create(book.get_dest().join("highlight.js")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create highlight.js")));
|
||||
};
|
||||
try!(highlight_js.write_all(&theme.highlight_js));
|
||||
|
||||
// Font Awesome local fallback
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/css/font-awesome.css")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create font-awesome.css")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME));
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/fonts/fontawesome-webfont.eot")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.eot")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_EOT));
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/fonts/fontawesome-webfont.svg")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.svg")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_SVG));
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/fonts/fontawesome-webfont.ttf")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.ttf")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/fonts/fontawesome-webfont.woff")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF));
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/fonts/fontawesome-webfont.woff2")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff2")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF2));
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/fonts/FontAwesome.ttf")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create FontAwesome.ttf")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
|
||||
try!(book.write_file("book.js", &theme.js));
|
||||
try!(book.write_file("book.css", &theme.css));
|
||||
try!(book.write_file("favicon.png", &theme.favicon));
|
||||
try!(book.write_file("jquery.js", &theme.jquery));
|
||||
try!(book.write_file("highlight.css", &theme.highlight_css));
|
||||
try!(book.write_file("tomorrow-night.css", &theme.tomorrow_night_css));
|
||||
try!(book.write_file("highlight.js", &theme.highlight_js));
|
||||
try!(book.write_file("_FontAwesome/css/font-awesome.css", theme::FONT_AWESOME));
|
||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.eot", theme::FONT_AWESOME_EOT));
|
||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.svg", theme::FONT_AWESOME_SVG));
|
||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.ttf", theme::FONT_AWESOME_TTF));
|
||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff", theme::FONT_AWESOME_WOFF));
|
||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2", theme::FONT_AWESOME_WOFF2));
|
||||
try!(book.write_file("_FontAwesome/fonts/FontAwesome.ttf", theme::FONT_AWESOME_TTF));
|
||||
|
||||
// Copy all remaining files
|
||||
try!(utils::fs::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"]));
|
||||
@@ -276,16 +180,16 @@ impl Renderer for HtmlHandlebars {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_data(book: &MDBook) -> Result<BTreeMap<String, Json>, Box<Error>> {
|
||||
fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>, Box<Error>> {
|
||||
debug!("[fn]: make_data");
|
||||
|
||||
let mut data = BTreeMap::new();
|
||||
data.insert("language".to_owned(), "en".to_json());
|
||||
data.insert("title".to_owned(), book.get_title().to_json());
|
||||
data.insert("description".to_owned(), book.get_description().to_json());
|
||||
data.insert("favicon".to_owned(), "favicon.png".to_json());
|
||||
let mut data = serde_json::Map::new();
|
||||
data.insert("language".to_owned(), json!("en"));
|
||||
data.insert("title".to_owned(), json!(book.get_title()));
|
||||
data.insert("description".to_owned(), json!(book.get_description()));
|
||||
data.insert("favicon".to_owned(), json!("favicon.png"));
|
||||
if let Some(livereload) = book.get_livereload() {
|
||||
data.insert("livereload".to_owned(), livereload.to_json());
|
||||
data.insert("livereload".to_owned(), json!(livereload));
|
||||
}
|
||||
|
||||
let mut chapters = vec![];
|
||||
@@ -296,26 +200,20 @@ fn make_data(book: &MDBook) -> Result<BTreeMap<String, Json>, Box<Error>> {
|
||||
|
||||
match *item {
|
||||
BookItem::Affix(ref ch) => {
|
||||
chapter.insert("name".to_owned(), ch.name.to_json());
|
||||
match ch.path.to_str() {
|
||||
Some(p) => {
|
||||
chapter.insert("path".to_owned(), p.to_json());
|
||||
},
|
||||
None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))),
|
||||
}
|
||||
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"))?;
|
||||
chapter.insert("path".to_owned(), json!(path));
|
||||
},
|
||||
BookItem::Chapter(ref s, ref ch) => {
|
||||
chapter.insert("section".to_owned(), s.to_json());
|
||||
chapter.insert("name".to_owned(), ch.name.to_json());
|
||||
match ch.path.to_str() {
|
||||
Some(p) => {
|
||||
chapter.insert("path".to_owned(), p.to_json());
|
||||
},
|
||||
None => return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))),
|
||||
}
|
||||
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"))?;
|
||||
chapter.insert("path".to_owned(), json!(path));
|
||||
},
|
||||
BookItem::Spacer => {
|
||||
chapter.insert("spacer".to_owned(), "_spacer_".to_json());
|
||||
chapter.insert("spacer".to_owned(), json!("_spacer_"));
|
||||
},
|
||||
|
||||
}
|
||||
@@ -323,8 +221,74 @@ fn make_data(book: &MDBook) -> Result<BTreeMap<String, Json>, Box<Error>> {
|
||||
chapters.push(chapter);
|
||||
}
|
||||
|
||||
data.insert("chapters".to_owned(), chapters.to_json());
|
||||
data.insert("chapters".to_owned(), json!(chapters));
|
||||
|
||||
debug!("[*]: JSON constructed");
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn build_header_links(html: String, filename: &str) -> String {
|
||||
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
||||
|
||||
regex.replace_all(&html, |caps: &Captures| {
|
||||
let level = &caps[1];
|
||||
let text = &caps[2];
|
||||
let mut id = text.to_string();
|
||||
let repl_sub = vec!["<em>", "</em>", "<code>", "</code>",
|
||||
"<strong>", "</strong>",
|
||||
"<", ">", "&", "'", """];
|
||||
for sub in repl_sub {
|
||||
id = id.replace(sub, "");
|
||||
}
|
||||
let id = id.chars().filter_map(|c| {
|
||||
if c.is_alphanumeric() || c == '-' || c == '_' {
|
||||
if c.is_ascii() {
|
||||
Some(c.to_ascii_lowercase())
|
||||
} else {
|
||||
Some(c)
|
||||
}
|
||||
} else if c.is_whitespace() && c.is_ascii() {
|
||||
Some('-')
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect::<String>();
|
||||
|
||||
format!("<a class=\"header\" href=\"{filename}#{id}\" name=\"{id}\"><h{level}>{text}</h{level}></a>",
|
||||
level=level, id=id, text=text, filename=filename)
|
||||
}).into_owned()
|
||||
}
|
||||
|
||||
// anchors to the same page (href="#anchor") do not work because of
|
||||
// <base href="../"> pointing to the root folder. This function *fixes*
|
||||
// that in a very inelegant way
|
||||
fn fix_anchor_links(html: String, filename: &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];
|
||||
|
||||
format!("<a{before}href=\"{filename}#{anchor}\"{after}>",
|
||||
before=before, filename=filename, anchor=anchor, after=after)
|
||||
}).into_owned()
|
||||
}
|
||||
|
||||
|
||||
// The rust book uses annotations for rustdoc to test code snippets, like the following:
|
||||
// ```rust,should_panic
|
||||
// fn main() {
|
||||
// // Code here
|
||||
// }
|
||||
// ```
|
||||
// This function replaces all commas by spaces in the code block classes
|
||||
fn fix_code_blocks(html: String) -> 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];
|
||||
|
||||
format!("<code{before}class=\"{classes}\"{after}>", before=before, classes=classes, after=after)
|
||||
}).into_owned()
|
||||
}
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{VecDeque, BTreeMap};
|
||||
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Renderable};
|
||||
|
||||
use rustc_serialize::json::{self, ToJson};
|
||||
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Context, Renderable};
|
||||
|
||||
// Handlebars helper for navigation
|
||||
|
||||
pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: previous (handlebars helper)");
|
||||
|
||||
debug!("[*]: Get data from context");
|
||||
// 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 = c.navigate(rc.get_path(), "chapters");
|
||||
let chapters = rc.context().navigate(rc.get_path(), &VecDeque::new(), "chapters").to_owned();
|
||||
|
||||
let current = c.navigate(rc.get_path(), "path")
|
||||
let current = rc.context().navigate(rc.get_path(), &VecDeque::new(), "path")
|
||||
.to_string()
|
||||
.replace("\"", "");
|
||||
|
||||
|
||||
debug!("[*]: Decode chapters from JSON");
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String, String>> = match json::decode(&chapters.to_string()) {
|
||||
let decoded: Vec<BTreeMap<String, String>> = match serde_json::from_str(&chapters.to_string()) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return Err(RenderError::new("Could not decode the JSON data")),
|
||||
};
|
||||
@@ -48,7 +49,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
|
||||
match previous.get("name") {
|
||||
Some(n) => {
|
||||
debug!("[*]: Inserting title: {}", n);
|
||||
previous_chapter.insert("title".to_owned(), n.to_json())
|
||||
previous_chapter.insert("title".to_owned(), json!(n))
|
||||
},
|
||||
None => {
|
||||
debug!("[*]: No title found for chapter");
|
||||
@@ -66,7 +67,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
|
||||
|
||||
match path.to_str() {
|
||||
Some(p) => {
|
||||
previous_chapter.insert("link".to_owned(), p.replace("\\", "/").to_json());
|
||||
previous_chapter.insert("link".to_owned(), json!(p.replace("\\", "/")));
|
||||
},
|
||||
None => return Err(RenderError::new("Link could not be converted to str")),
|
||||
}
|
||||
@@ -76,13 +77,14 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
|
||||
|
||||
debug!("[*]: Inject in context");
|
||||
// Inject in current context
|
||||
let updated_context = c.extend(&previous_chapter);
|
||||
let updated_context = rc.context().extend(&previous_chapter);
|
||||
|
||||
debug!("[*]: Render template");
|
||||
// Render template
|
||||
match _h.template() {
|
||||
Some(t) => {
|
||||
try!(t.render(&updated_context, r, rc));
|
||||
*rc.context_mut() = updated_context;
|
||||
try!(t.render(r, rc));
|
||||
},
|
||||
None => return Err(RenderError::new("Error with the handlebars template")),
|
||||
}
|
||||
@@ -106,22 +108,22 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
|
||||
|
||||
|
||||
|
||||
pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
debug!("[fn]: next (handlebars helper)");
|
||||
|
||||
debug!("[*]: Get data from context");
|
||||
// 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 = c.navigate(rc.get_path(), "chapters");
|
||||
let chapters = rc.context().navigate(rc.get_path(), &VecDeque::new(), "chapters").to_owned();
|
||||
|
||||
let current = c.navigate(rc.get_path(), "path")
|
||||
let current = rc.context().navigate(rc.get_path(), &VecDeque::new(), "path")
|
||||
.to_string()
|
||||
.replace("\"", "");
|
||||
|
||||
debug!("[*]: Decode chapters from JSON");
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String, String>> = match json::decode(&chapters.to_string()) {
|
||||
let decoded: Vec<BTreeMap<String, String>> = match serde_json::from_str(&chapters.to_string()) {
|
||||
Ok(data) => data,
|
||||
Err(_) => return Err(RenderError::new("Could not decode the JSON data")),
|
||||
};
|
||||
@@ -152,7 +154,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
|
||||
match item.get("name") {
|
||||
Some(n) => {
|
||||
debug!("[*]: Inserting title: {}", n);
|
||||
next_chapter.insert("title".to_owned(), n.to_json());
|
||||
next_chapter.insert("title".to_owned(), json!(n));
|
||||
},
|
||||
None => return Err(RenderError::new("No title found for chapter in JSON data")),
|
||||
}
|
||||
@@ -164,21 +166,22 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
|
||||
match link.to_str() {
|
||||
Some(l) => {
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
next_chapter.insert("link".to_owned(), l.replace("\\", "/").to_json());
|
||||
next_chapter.insert("link".to_owned(), json!(l.replace("\\", "/")));
|
||||
},
|
||||
None => return Err(RenderError::new("Link could not converted to str")),
|
||||
}
|
||||
|
||||
debug!("[*]: Inject in context");
|
||||
// Inject in current context
|
||||
let updated_context = c.extend(&next_chapter);
|
||||
let updated_context = rc.context().extend(&next_chapter);
|
||||
|
||||
debug!("[*]: Render template");
|
||||
|
||||
// Render template
|
||||
match _h.template() {
|
||||
Some(t) => {
|
||||
try!(t.render(&updated_context, r, rc));
|
||||
*rc.context_mut() = updated_context;
|
||||
try!(t.render(r, rc));
|
||||
},
|
||||
None => return Err(RenderError::new("Error with the handlebars template")),
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ pub fn render_playpen(s: &str, path: &Path) -> String {
|
||||
|
||||
// Check if the file exists
|
||||
if !playpen.rust_file.exists() || !playpen.rust_file.is_file() {
|
||||
output!("[-] No file exists for {{{{#playpen }}}}\n {}", playpen.rust_file.to_str().unwrap());
|
||||
warn!("[-] No file exists for {{{{#playpen }}}}\n {}", playpen.rust_file.to_str().unwrap());
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn render_playpen(s: &str, path: &Path) -> String {
|
||||
continue;
|
||||
};
|
||||
let mut file_content = String::new();
|
||||
if let Err(_) = file.read_to_string(&mut file_content) {
|
||||
if file.read_to_string(&mut file_content).is_err() {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -86,7 +86,7 @@ fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> {
|
||||
if end_i - 2 - (i + 10) < 1 {
|
||||
continue;
|
||||
}
|
||||
if s[i + 10..end_i - 2].trim().len() == 0 {
|
||||
if s[i + 10..end_i - 2].trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -94,15 +94,10 @@ fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> {
|
||||
|
||||
// Split on whitespaces
|
||||
let params: Vec<&str> = s[i + 10..end_i - 2].split_whitespace().collect();
|
||||
let mut editable = false;
|
||||
|
||||
if params.len() > 1 {
|
||||
editable = if let Some(_) = params[1].find("editable") {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
}
|
||||
let editable = params
|
||||
.get(1)
|
||||
.map(|p| p.find("editable").is_some())
|
||||
.unwrap_or(false);
|
||||
|
||||
playpens.push(Playpen {
|
||||
start_index: i,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{VecDeque, BTreeMap};
|
||||
|
||||
use rustc_serialize::json;
|
||||
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper};
|
||||
use pulldown_cmark::{Parser, html, Event, Tag};
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
@@ -10,25 +10,25 @@ use pulldown_cmark::{Parser, html, Event, Tag};
|
||||
pub struct RenderToc;
|
||||
|
||||
impl HelperDef for RenderToc {
|
||||
fn call(&self, c: &Context, _h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||
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 = c.navigate(rc.get_path(), "chapters");
|
||||
let current = c.navigate(rc.get_path(), "path").to_string().replace("\"", "");
|
||||
try!(rc.writer.write("<ul class=\"chapter\">".as_bytes()));
|
||||
let chapters = rc.context().navigate(rc.get_path(), &VecDeque::new(), "chapters").to_owned();
|
||||
let current = rc.context().navigate(rc.get_path(), &VecDeque::new(), "path").to_string().replace("\"", "");
|
||||
try!(rc.writer.write_all("<ul class=\"chapter\">".as_bytes()));
|
||||
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String, String>> = json::decode(&chapters.to_string()).unwrap();
|
||||
let decoded: Vec<BTreeMap<String, String>> = serde_json::from_str(&chapters.to_string()).unwrap();
|
||||
|
||||
let mut current_level = 1;
|
||||
|
||||
for item in decoded {
|
||||
|
||||
// Spacer
|
||||
if let Some(_) = item.get("spacer") {
|
||||
try!(rc.writer.write("<li class=\"spacer\"></li>".as_bytes()));
|
||||
if item.get("spacer").is_some() {
|
||||
try!(rc.writer.write_all("<li class=\"spacer\"></li>".as_bytes()));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -40,33 +40,33 @@ impl HelperDef for RenderToc {
|
||||
|
||||
if level > current_level {
|
||||
while level > current_level {
|
||||
try!(rc.writer.write("<li>".as_bytes()));
|
||||
try!(rc.writer.write("<ul class=\"section\">".as_bytes()));
|
||||
try!(rc.writer.write_all("<li>".as_bytes()));
|
||||
try!(rc.writer.write_all("<ul class=\"section\">".as_bytes()));
|
||||
current_level += 1;
|
||||
}
|
||||
try!(rc.writer.write("<li>".as_bytes()));
|
||||
try!(rc.writer.write_all("<li>".as_bytes()));
|
||||
} else if level < current_level {
|
||||
while level < current_level {
|
||||
try!(rc.writer.write("</ul>".as_bytes()));
|
||||
try!(rc.writer.write("</li>".as_bytes()));
|
||||
try!(rc.writer.write_all("</ul>".as_bytes()));
|
||||
try!(rc.writer.write_all("</li>".as_bytes()));
|
||||
current_level -= 1;
|
||||
}
|
||||
try!(rc.writer.write("<li>".as_bytes()));
|
||||
try!(rc.writer.write_all("<li>".as_bytes()));
|
||||
} else {
|
||||
try!(rc.writer.write("<li".as_bytes()));
|
||||
if let None = item.get("section") {
|
||||
try!(rc.writer.write(" class=\"affix\"".as_bytes()));
|
||||
try!(rc.writer.write_all("<li".as_bytes()));
|
||||
if item.get("section").is_none() {
|
||||
try!(rc.writer.write_all(" class=\"affix\"".as_bytes()));
|
||||
}
|
||||
try!(rc.writer.write(">".as_bytes()));
|
||||
try!(rc.writer.write_all(">".as_bytes()));
|
||||
}
|
||||
|
||||
// Link
|
||||
let path_exists = if let Some(path) = item.get("path") {
|
||||
if !path.is_empty() {
|
||||
try!(rc.writer.write("<a href=\"".as_bytes()));
|
||||
try!(rc.writer.write_all("<a href=\"".as_bytes()));
|
||||
|
||||
// Add link
|
||||
try!(rc.writer.write(Path::new(item.get("path")
|
||||
try!(rc.writer.write_all(Path::new(item.get("path")
|
||||
.expect("Error: path should be Some(_)"))
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
@@ -75,13 +75,13 @@ impl HelperDef for RenderToc {
|
||||
.replace("\\", "/")
|
||||
.as_bytes()));
|
||||
|
||||
try!(rc.writer.write("\"".as_bytes()));
|
||||
try!(rc.writer.write_all("\"".as_bytes()));
|
||||
|
||||
if path == ¤t {
|
||||
try!(rc.writer.write(" class=\"active\"".as_bytes()));
|
||||
try!(rc.writer.write_all(" class=\"active\"".as_bytes()));
|
||||
}
|
||||
|
||||
try!(rc.writer.write(">".as_bytes()));
|
||||
try!(rc.writer.write_all(">".as_bytes()));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -92,21 +92,21 @@ impl HelperDef for RenderToc {
|
||||
|
||||
// Section does not necessarily exist
|
||||
if let Some(section) = item.get("section") {
|
||||
try!(rc.writer.write("<strong>".as_bytes()));
|
||||
try!(rc.writer.write(section.as_bytes()));
|
||||
try!(rc.writer.write("</strong> ".as_bytes()));
|
||||
try!(rc.writer.write_all("<strong>".as_bytes()));
|
||||
try!(rc.writer.write_all(section.as_bytes()));
|
||||
try!(rc.writer.write_all("</strong> ".as_bytes()));
|
||||
}
|
||||
|
||||
if let Some(name) = item.get("name") {
|
||||
// Render only inline code blocks
|
||||
|
||||
// filter all events that are not inline code blocks
|
||||
let parser = Parser::new(&name).filter(|event| {
|
||||
match event {
|
||||
&Event::Start(Tag::Code) |
|
||||
&Event::End(Tag::Code) => true,
|
||||
&Event::InlineHtml(_) => true,
|
||||
&Event::Text(_) => true,
|
||||
let parser = Parser::new(name).filter(|event| {
|
||||
match *event {
|
||||
Event::Start(Tag::Code) |
|
||||
Event::End(Tag::Code) |
|
||||
Event::InlineHtml(_) |
|
||||
Event::Text(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
@@ -116,23 +116,23 @@ impl HelperDef for RenderToc {
|
||||
html::push_html(&mut markdown_parsed_name, parser);
|
||||
|
||||
// write to the handlebars template
|
||||
try!(rc.writer.write(markdown_parsed_name.as_bytes()));
|
||||
try!(rc.writer.write_all(markdown_parsed_name.as_bytes()));
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
try!(rc.writer.write("</a>".as_bytes()));
|
||||
try!(rc.writer.write_all("</a>".as_bytes()));
|
||||
}
|
||||
|
||||
try!(rc.writer.write("</li>".as_bytes()));
|
||||
try!(rc.writer.write_all("</li>".as_bytes()));
|
||||
|
||||
}
|
||||
while current_level > 1 {
|
||||
try!(rc.writer.write("</ul>".as_bytes()));
|
||||
try!(rc.writer.write("</li>".as_bytes()));
|
||||
try!(rc.writer.write_all("</ul>".as_bytes()));
|
||||
try!(rc.writer.write_all("</li>".as_bytes()));
|
||||
current_level -= 1;
|
||||
}
|
||||
|
||||
try!(rc.writer.write("</ul>".as_bytes()));
|
||||
try!(rc.writer.write_all("</ul>".as_bytes()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ table thead td {
|
||||
.chapter {
|
||||
list-style: none outside none;
|
||||
padding-left: 0;
|
||||
line-height: 1.9em;
|
||||
line-height: 2.2em;
|
||||
}
|
||||
.chapter li a {
|
||||
padding: 5px 0;
|
||||
@@ -89,7 +89,7 @@ table thead td {
|
||||
.section {
|
||||
list-style: none outside none;
|
||||
padding-left: 20px;
|
||||
line-height: 2.5em;
|
||||
line-height: 1.9em;
|
||||
}
|
||||
.section li {
|
||||
-o-text-overflow: ellipsis;
|
||||
@@ -264,15 +264,11 @@ table thead td {
|
||||
line-height: 25px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.theme-popup .theme:hover:first-child {
|
||||
.theme-popup .theme:hover:first-child,
|
||||
.theme-popup .theme:hover:last-child {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
}
|
||||
.theme-popup .theme:hover:last-child {
|
||||
border-bottom-left-radius: inherit;
|
||||
border-bottom-right-radius: inherit;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1250px) {
|
||||
.nav-chapters {
|
||||
display: none;
|
||||
@@ -765,9 +761,11 @@ table thead td {
|
||||
.rust pre > .result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
#sidebar {
|
||||
@media only print {
|
||||
#sidebar,
|
||||
#menu-bar,
|
||||
.nav-chapters,
|
||||
.mobile-nav-chapters {
|
||||
display: none;
|
||||
}
|
||||
#page-wrapper {
|
||||
@@ -779,16 +777,40 @@ table thead td {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#menu-bar {
|
||||
display: none;
|
||||
}
|
||||
.page {
|
||||
overflow-y: initial;
|
||||
}
|
||||
.nav-chapters {
|
||||
display: none;
|
||||
code {
|
||||
background-color: #666;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
.mobile-nav-chapters {
|
||||
display: none;
|
||||
a,
|
||||
a:visited,
|
||||
a:active,
|
||||
a:hover {
|
||||
color: #4183c4;
|
||||
text-decoration: none;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
page-break-inside: avoid;
|
||||
page-break-after: avoid;
|
||||
/*break-after: avoid*/
|
||||
}
|
||||
pre,
|
||||
code {
|
||||
page-break-inside: avoid;
|
||||
white-space: pre-wrap /* CSS 3 */;
|
||||
white-space: -moz-pre-wrap /* Mozilla, since 1999 */;
|
||||
white-space: -pre-wrap /* Opera 4-6 */;
|
||||
white-space: -o-pre-wrap /* Opera 7 */;
|
||||
word-wrap: break-word /* Internet Explorer 5.5+ */;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ $( document ).ready(function() {
|
||||
$('code').each(function(i, block) {
|
||||
hljs.highlightBlock(block);
|
||||
});
|
||||
|
||||
// Adding the hljs class gives code blocks the color css
|
||||
// even if highlighting doesn't apply
|
||||
$('code').addClass('hljs');
|
||||
|
||||
var KEY_CODES = {
|
||||
PREVIOUS_KEY: 37,
|
||||
@@ -51,19 +55,6 @@ $( document ).ready(function() {
|
||||
var page_wrapper = $("#page-wrapper");
|
||||
var content = $("#content");
|
||||
|
||||
|
||||
// Add anchors for all content headers
|
||||
content.find("h1, h2, h3, h4, h5").wrap(function(){
|
||||
var wrapper = $("<a class=\"header\">");
|
||||
wrapper.attr("name", $(this).text());
|
||||
// Add so that when you click the link actually shows up in the url bar...
|
||||
// Remove any existing anchor then append the new one
|
||||
// ensuring eg. no spaces are present within it ie. they become %20
|
||||
wrapper.attr("href", $(location).attr('href').split("#")[0] + "#" + encodeURIComponent($(this).text().trim()) );
|
||||
return wrapper;
|
||||
});
|
||||
|
||||
|
||||
// Toggle sidebar
|
||||
$("#sidebar-toggle").click(function(event){
|
||||
if ( html.hasClass("sidebar-hidden") ) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
<html lang="{{ language }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
<title>{{ chapter_title }} - {{ title }}</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="description" content="{{ description }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
@@ -56,12 +56,6 @@ impl Theme {
|
||||
return theme;
|
||||
}
|
||||
|
||||
let src = src.join("theme");
|
||||
// If src does exist, check if there is a theme directory in it
|
||||
if !src.exists() || !src.is_dir() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
// Check for individual files if they exist
|
||||
|
||||
// index.hbs
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
@import 'nav-icons'
|
||||
@import 'theme-popup'
|
||||
@import 'themes'
|
||||
@import 'print'
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
@media only print {
|
||||
|
||||
.sidebar,
|
||||
.menu-bar,
|
||||
#sidebar,
|
||||
#menu-bar,
|
||||
.nav-chapters,
|
||||
.mobile-nav-chapters {
|
||||
display: none
|
||||
display: none
|
||||
}
|
||||
|
||||
.page-wrapper {
|
||||
left: 0
|
||||
#page-wrapper {
|
||||
left: 0;
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 100%
|
||||
#content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page {
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #666666
|
||||
border-radius: 5px
|
||||
background-color: #666666
|
||||
border-radius: 5px
|
||||
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact
|
||||
}
|
||||
|
||||
a, a:visited, a:active, a:hover {
|
||||
color: #4183c4
|
||||
text-decoration: none
|
||||
color: #4183c4
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
.chapter {
|
||||
list-style: none outside none
|
||||
padding-left: 0
|
||||
line-height: 1.9em
|
||||
line-height: 2.2em
|
||||
|
||||
li a {
|
||||
padding: 5px 0
|
||||
@@ -54,7 +54,7 @@
|
||||
.section {
|
||||
list-style: none outside none
|
||||
padding-left: 20px
|
||||
line-height: 2.5em
|
||||
line-height: 1.9em
|
||||
|
||||
li {
|
||||
text-overflow: ellipsis
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
left: 10px
|
||||
|
||||
z-index: 1000;
|
||||
|
||||
|
||||
border-radius: 4px
|
||||
font-size: 0.7em
|
||||
|
||||
@@ -12,7 +12,14 @@
|
||||
padding: 2px 10px
|
||||
line-height: 25px
|
||||
white-space: nowrap
|
||||
|
||||
&:hover:first-child,
|
||||
&:hover:last-child {
|
||||
border-top-left-radius: inherit;
|
||||
border-top-right-radius: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1250px) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::{Path, Component};
|
||||
use std::error::Error;
|
||||
use std::io::{self, Read};
|
||||
use std::fs::{self, metadata, File};
|
||||
use std::fs::{self, File};
|
||||
|
||||
/// Takes a path to a file and try to read the file into a String
|
||||
|
||||
@@ -69,7 +69,7 @@ pub fn path_to_root(path: &Path) -> String {
|
||||
/// This function creates a file and returns it. But before creating the file it checks every
|
||||
/// directory in the path to see if it exists, and if it does not it will be created.
|
||||
|
||||
pub fn create_file(path: &Path) -> Result<File, Box<Error>> {
|
||||
pub fn create_file(path: &Path) -> io::Result<File> {
|
||||
debug!("[fn]: create_file");
|
||||
|
||||
// Construct path
|
||||
@@ -80,15 +80,7 @@ pub fn create_file(path: &Path) -> Result<File, Box<Error>> {
|
||||
}
|
||||
|
||||
debug!("[*]: Create file: {:?}", path);
|
||||
let f = match File::create(path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
debug!("File::create: {}", e);
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, format!("{}", e))));
|
||||
},
|
||||
};
|
||||
|
||||
Ok(f)
|
||||
File::create(path)
|
||||
}
|
||||
|
||||
/// Removes all the content of a directory but not the directory itself
|
||||
@@ -147,15 +139,15 @@ pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blackl
|
||||
if ext_blacklist.contains(&ext.to_str().unwrap()) {
|
||||
continue;
|
||||
}
|
||||
debug!("[*] creating path for file: {:?}",
|
||||
&to.join(entry.path().file_name().expect("a file should have a file name...")));
|
||||
|
||||
output!("[*] Copying file: {:?}\n to {:?}",
|
||||
entry.path(),
|
||||
&to.join(entry.path().file_name().expect("a file should have a file name...")));
|
||||
try!(fs::copy(entry.path(),
|
||||
&to.join(entry.path().file_name().expect("a file should have a file name..."))));
|
||||
}
|
||||
debug!("[*] creating path for file: {:?}",
|
||||
&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...")));
|
||||
try!(fs::copy(entry.path(),
|
||||
&to.join(entry.path().file_name().expect("a file should have a file name..."))));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -14,7 +14,7 @@ pub fn render_markdown(text: &str) -> String {
|
||||
opts.insert(OPTION_ENABLE_TABLES);
|
||||
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
||||
|
||||
let p = Parser::new_ext(&text, opts);
|
||||
let p = Parser::new_ext(text, opts);
|
||||
html::push_html(&mut s, p);
|
||||
s
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user