mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 17:21:52 -05:00
Compare commits
204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b84b8fa82 | ||
|
|
a4a708bdda | ||
|
|
a3b925e3ab | ||
|
|
cba988f009 | ||
|
|
4525810737 | ||
|
|
5d72d966ad | ||
|
|
15dcca87d8 | ||
|
|
c6e81337fb | ||
|
|
9602acce80 | ||
|
|
65d7e86024 | ||
|
|
b5ec813d2f | ||
|
|
41735b4579 | ||
|
|
9cb232058b | ||
|
|
ef402c16e8 | ||
|
|
df5472ab5a | ||
|
|
d768963c30 | ||
|
|
8e7ec6e1fd | ||
|
|
80f01d70c6 | ||
|
|
40f275bf21 | ||
|
|
af8300c0b4 | ||
|
|
793a88260c | ||
|
|
1ec776244d | ||
|
|
4af107b0ca | ||
|
|
35e2807138 | ||
|
|
1632d2e339 | ||
|
|
7c3932cef9 | ||
|
|
ed1a216121 | ||
|
|
f814e96459 | ||
|
|
a7272e0ff5 | ||
|
|
1cf4774737 | ||
|
|
c6a5d12002 | ||
|
|
b120ce7397 | ||
|
|
c7916c4818 | ||
|
|
56f597b90c | ||
|
|
c5f9625feb | ||
|
|
79f00eeea3 | ||
|
|
677fa42458 | ||
|
|
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 | ||
|
|
db0306a6d2 | ||
|
|
02c5c971e7 | ||
|
|
5350d62591 | ||
|
|
9c8a563223 | ||
|
|
b4948b680f | ||
|
|
b6df992420 | ||
|
|
b0e5f375ba | ||
|
|
a4a277cb50 | ||
|
|
b9e22bb8f2 | ||
|
|
ab29e92071 | ||
|
|
03373c6bf2 | ||
|
|
425b583625 | ||
|
|
dfef0d7585 | ||
|
|
9b49acc2c9 | ||
|
|
1ae0d4f637 | ||
|
|
f9aa9a6843 | ||
|
|
9b1e224680 | ||
|
|
cfcf6d952f | ||
|
|
e3f398cff2 | ||
|
|
6bc088db6e | ||
|
|
e34bef0e53 | ||
|
|
15d6227a11 | ||
|
|
1b8af2bf57 | ||
|
|
7f34512751 | ||
|
|
876ea7895a | ||
|
|
bea80f6266 | ||
|
|
334540835c | ||
|
|
10e7a41d92 | ||
|
|
2ec5648587 | ||
|
|
6aa6546ce4 | ||
|
|
c071406fef | ||
|
|
c8051294b0 | ||
|
|
807a2f116e | ||
|
|
2f43167b75 | ||
|
|
e861880f95 | ||
|
|
c3564f1699 | ||
|
|
15d26befcc | ||
|
|
925939e267 | ||
|
|
ceb139a848 | ||
|
|
d0e39f469a | ||
|
|
c5752620d7 | ||
|
|
0c93599242 | ||
|
|
7f3a6c8130 | ||
|
|
b30a8bdc81 | ||
|
|
74fff81e4b | ||
|
|
ad0794a0bd | ||
|
|
6bac41caa8 | ||
|
|
b094268b68 | ||
|
|
02a37e0ee9 | ||
|
|
9e34eccb3e | ||
|
|
79fb92ed7c | ||
|
|
5e78697ab1 | ||
|
|
469cb10d4a | ||
|
|
0f9caf4410 | ||
|
|
f23a5f2729 | ||
|
|
bc41efe414 | ||
|
|
5316089e61 | ||
|
|
73ce3f814a | ||
|
|
075da959c9 | ||
|
|
1eb59428e6 | ||
|
|
596455f28c |
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[attr]rust text eol=lf whitespace=tab-in-indent,trailing-space,tabwidth=4
|
||||
|
||||
* text=auto eol=lf
|
||||
*.rs rust
|
||||
113
.travis.yml
113
.travis.yml
@@ -1,20 +1,101 @@
|
||||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
matrix:
|
||||
allow_failure:
|
||||
- rust: nightly
|
||||
sudo: false
|
||||
|
||||
language: generic
|
||||
|
||||
env:
|
||||
global:
|
||||
secure: l3/qEC4krRerllLQzni8j5AjngFi6pluWvBWj//1mJLoIEYwxlQ9mYxEdd9BqccWWFn3K0bVYCVC/64+tP6sRfLkZCe2gPUtwe7ITwCDbapUxmkiRObVJCs5yMQZt6idyhHUDKAXKgNCrusfI2BM3tKGBfRK7Cnn/R/7p/U9+q7D1sgJtUKp6ypVzK6A3jLNp3dFLFI19a5KmbZMVsaa7tOhtdDJjjr7ebsc9z7HMW5/OItiWU3FSauVQQlUMaCiEgFuIG7H7OnBAYWB/gNEtLuwfLqU9UjtWk/njNNRnmJ7m3y5HbQhv5H5F5mJUOq9XFlPLwPwyTeVztSGdQm6k8Pp2pgKBUjY27afBl9BWU+msmN6k0oXfhvIebiBPe/x2udiKeFik1xqOOEU1q9dF0sZiuPxCSM1n7tgWklJ8epgaRQaMPPQw9pO/2H5/ynHCJqBlw6WcdiqWtwAyyr/GEx62u/cg5IVkqb7KLmYsWzjS8wYG4CYs1eIxCw2xPZxP0FGuUXvxTBUPipFze6Z7FqxVauXtVe2D7c1P4738HZP660rmR0GYtHtKLny1QxCCK9sxd9JmcezFCSz4YeQ1od9xc0OzGJ2ullKNGizmGfYmgL6X8faNylLIEdaiHAcY16xV3L0g3fXL1Qg360UHQyj7GIv+0nqQnf+H9xRTTU=
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- nodejs
|
||||
- npm
|
||||
- PROJECT_NAME=mdBook
|
||||
- secure: l3/qEC4krRerllLQzni8j5AjngFi6pluWvBWj//1mJLoIEYwxlQ9mYxEdd9BqccWWFn3K0bVYCVC/64+tP6sRfLkZCe2gPUtwe7ITwCDbapUxmkiRObVJCs5yMQZt6idyhHUDKAXKgNCrusfI2BM3tKGBfRK7Cnn/R/7p/U9+q7D1sgJtUKp6ypVzK6A3jLNp3dFLFI19a5KmbZMVsaa7tOhtdDJjjr7ebsc9z7HMW5/OItiWU3FSauVQQlUMaCiEgFuIG7H7OnBAYWB/gNEtLuwfLqU9UjtWk/njNNRnmJ7m3y5HbQhv5H5F5mJUOq9XFlPLwPwyTeVztSGdQm6k8Pp2pgKBUjY27afBl9BWU+msmN6k0oXfhvIebiBPe/x2udiKeFik1xqOOEU1q9dF0sZiuPxCSM1n7tgWklJ8epgaRQaMPPQw9pO/2H5/ynHCJqBlw6WcdiqWtwAyyr/GEx62u/cg5IVkqb7KLmYsWzjS8wYG4CYs1eIxCw2xPZxP0FGuUXvxTBUPipFze6Z7FqxVauXtVe2D7c1P4738HZP660rmR0GYtHtKLny1QxCCK9sxd9JmcezFCSz4YeQ1od9xc0OzGJ2ullKNGizmGfYmgL6X8faNylLIEdaiHAcY16xV3L0g3fXL1Qg360UHQyj7GIv+0nqQnf+H9xRTTU=
|
||||
|
||||
matrix:
|
||||
include:
|
||||
# Stable channel
|
||||
- os: osx
|
||||
env: TARGET=i686-apple-darwin CHANNEL=stable
|
||||
- os: linux
|
||||
env: TARGET=i686-unknown-linux-gnu CHANNEL=stable
|
||||
addons:
|
||||
apt:
|
||||
packages: &i686_unknown_linux_gnu
|
||||
- gcc-multilib
|
||||
- os: osx
|
||||
env: TARGET=x86_64-apple-darwin CHANNEL=stable
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=stable
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- nodejs
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=stable
|
||||
# Beta channel
|
||||
- os: osx
|
||||
env: TARGET=i686-apple-darwin CHANNEL=beta
|
||||
- os: linux
|
||||
env: TARGET=i686-unknown-linux-gnu CHANNEL=beta
|
||||
addons:
|
||||
apt:
|
||||
packages: *i686_unknown_linux_gnu
|
||||
- os: osx
|
||||
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
|
||||
- os: linux
|
||||
env: TARGET=i686-unknown-linux-gnu CHANNEL=nightly
|
||||
addons:
|
||||
apt:
|
||||
packages: *i686_unknown_linux_gnu
|
||||
- os: osx
|
||||
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:
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=stable
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=beta
|
||||
- os: linux
|
||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=nightly
|
||||
|
||||
install:
|
||||
- npm install stylus nib
|
||||
- export PATH="$PATH:$HOME/.cargo/bin"
|
||||
- bash ci/install.sh
|
||||
|
||||
script:
|
||||
- bash ci/script.sh
|
||||
|
||||
after_success:
|
||||
- test $TRAVIS_PULL_REQUEST == "false" && test $TRAVIS_BRANCH == "master" && bash deploy.sh
|
||||
- test "$TRAVIS_PULL_REQUEST" == "false" &&
|
||||
test "$TRAVIS_BRANCH" == "master" &&
|
||||
test "$TARGET" == "x86_64-unknown-linux-gnu" &&
|
||||
test "$CHANNEL" = "stable" &&
|
||||
npm install stylus nib &&
|
||||
bash deploy.sh
|
||||
|
||||
before_deploy:
|
||||
- bash ci/before_deploy.sh
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: Z1k7WqX7z+tT4+SzTh4tBBzf11VaADB4AWuEczHtylaEb/0hRs8gaiHCNSVHm/QTp0QPWQR2Vw7uKMhVuxG7I8X7h31j3A7ulYBh/iVk0DVIrtrn2Q4WOED9CpoXLuLtk2nxo9MBViFW7mw4nJe9H2Tn9o/9oEYBuwzekvW5mh4muqUuCVTr8eQVYbs3jbC9pQy5oYjOLeUnlL9Cey5VN/nAhzAtyFP+6lIMri0PKit4JtkFou/O1MEpFYlP3VGC2lFiWuByocPKBT/L45FecS9qoHq+i6+ZCPDH2eu46nuYsDbLKAkPdGvf1MdPBPwoj0vSnZbgaTisQ4hIoBngQQQPZlPaGtcdd6g6asxSfnbA9cQhClI5oZJmg+ksxQE+peE8pnbmZ10Ix0PpIkkfWdQeMdUUCQarOTkTK54Munw+X+kp1lH19j6+krQPLBYr95fPRd4b5tWsJD2+pb/UOYFEEJxMNoUHyLCrtdCO7imOwrSUcv51+Z8UudqfPpKQeszrJcntL4owip35r3sF5TsE9YfW5qssLC164IylvP32y1AcfL1jqg8b+zrqLZKanjvDOJ1dtHHuwKqxcwf7PhAf0YjAtVSH9OIYcDzmDa0EMLrq7EK0fs6NAeb5qt6CML7pZrRS3fmOxN53Fbmj81qm6TmjQjDe4dmZlELgNow=
|
||||
file: ${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}.tar.gz
|
||||
# don't delete the artifacts from previous phases
|
||||
skip_cleanup: true
|
||||
# deploy when a new tag is pushed
|
||||
on:
|
||||
condition: $CHANNEL = stable
|
||||
tags: true
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
|
||||
28
Cargo.toml
28
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.0.10"
|
||||
version = "0.0.20"
|
||||
authors = ["Mathieu David <mathieudavid@mathieudavid.org>"]
|
||||
description = "create books from markdown files (like Gitbook)"
|
||||
documentation = "http://azerupi.github.io/mdBook/index.html"
|
||||
@@ -15,28 +15,38 @@ exclude = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
clap = "1.5.3"
|
||||
handlebars = "0.12.0"
|
||||
rustc-serialize = "0.3.16"
|
||||
pulldown-cmark = "0.0.6"
|
||||
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.4", optional = true }
|
||||
time = { version = "0.1.33", 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.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"]
|
||||
default = ["output", "watch", "serve"]
|
||||
debug = []
|
||||
output = []
|
||||
regenerate-css = []
|
||||
watch = ["notify", "time", "crossbeam"]
|
||||
serve = ["iron", "staticfile", "ws"]
|
||||
|
||||
[[bin]]
|
||||
doc = false
|
||||
|
||||
91
README.md
91
README.md
@@ -1,53 +1,80 @@
|
||||
# mdBook [](https://travis-ci.org/azerupi/mdBook) [](https://crates.io/crates/mdbook) [](LICENSE)
|
||||
# mdBook
|
||||
|
||||
mdBook is a utility to create modern online books from markdown files.
|
||||
<table>
|
||||
<tr>
|
||||
<td><strong>Linux / OS X</strong></td>
|
||||
<td>
|
||||
<a href="https://travis-ci.org/azerupi/mdBook"><img src="https://travis-ci.org/azerupi/mdBook.svg?branch=master"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Windows</strong></td>
|
||||
<td>
|
||||
<a href="https://ci.appveyor.com/project/azerupi/mdbook/"><img src="https://ci.appveyor.com/api/projects/status/o38racsnbcospyc8/branch/master?svg=true"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a href="https://crates.io/crates/mdbook"><img src="https://img.shields.io/crates/v/mdbook.svg"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/crates/l/mdbook.svg"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
**This project is still in its early days.**
|
||||
For more information about what is left on my to-do list, check the issue tracker
|
||||
mdBook is a utility to create modern online books from Markdown files.
|
||||
|
||||
**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
|
||||
|
||||
There are 2 ways to install mdBook but both require [Rust and Cargo](https://www.rust-lang.org/) to be installed.
|
||||
There are multiple ways to install mdBook.
|
||||
|
||||
##### Install from Crates.io
|
||||
1. **Binaries**
|
||||
Binaries are available for download [here](https://github.com/azerupi/mdBook/releases). Make sure to put the path to the binary into your `PATH`.
|
||||
|
||||
Once you have installed Rust, type the following in the terminal:
|
||||
```
|
||||
cargo install mdbook
|
||||
```
|
||||
2. **From Crates.io**
|
||||
This requires [Rust and Cargo](https://www.rust-lang.org/) to be installed. Once you have installed Rust, type the following in the terminal:
|
||||
```
|
||||
cargo install mdbook
|
||||
```
|
||||
|
||||
This will download and compile mdBook for you, the only thing you that will be left to do is add the Cargo bin directory to your path.
|
||||
This will download and compile mdBook for you, the only thing left to do is to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
##### Install from git
|
||||
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`.
|
||||
|
||||
First, clone the repository on your computer:
|
||||
4. **For Contributions**
|
||||
If you want to contribute to mdBook you will have to clone the repository on your local machine:
|
||||
|
||||
```
|
||||
git clone --depth 1 https://github.com/azerupi/mdBook.git
|
||||
```
|
||||
```
|
||||
git clone https://github.com/azerupi/mdBook.git
|
||||
```
|
||||
`cd` into `mdBook/` and run
|
||||
|
||||
Then `cd` into the directory and run:
|
||||
```
|
||||
cargo build
|
||||
```
|
||||
|
||||
```
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
The executable will be in `./target/release/mdbook`.
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
|
||||
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
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.
|
||||
mdBook will primarily be used as a command line tool, even though it exposes all its functionality as a Rust crate for integration in other projects.
|
||||
|
||||
Here are the main commands you will want to run, for a more exhaustive explanation, check out the [documentation](http://azerupi.github.io/mdBook/).
|
||||
Here are the main commands you will want to run. For a more exhaustive explanation, check out the [documentation](http://azerupi.github.io/mdBook/).
|
||||
|
||||
- `mdbook init`
|
||||
|
||||
@@ -61,7 +88,7 @@ Here are the main commands you will want to run, for a more exhaustive explanati
|
||||
└── SUMMARY.md
|
||||
```
|
||||
|
||||
`book` and `src` are both directories. `src` contains the markdown files that will be used to render the ouput to the `book` directory.
|
||||
`book` and `src` are both directories. `src` contains the markdown files that will be used to render the output to the `book` directory.
|
||||
|
||||
Please, take a look at the [**Documentation**](http://azerupi.github.io/mdBook/cli/init.html) for more information and some neat tricks.
|
||||
|
||||
@@ -73,9 +100,13 @@ Here are the main commands you will want to run, for a more exhaustive explanati
|
||||
|
||||
When you run this command, mdbook will watch your markdown files to rebuild the book on every change. This avoids having to come back to the terminal to type `mdbook build` over and over again.
|
||||
|
||||
- `mdbook serve`
|
||||
|
||||
Does the same thing as `mdbook watch` but additionally serves the book at `http://localhost:3000` (port is changeable) and reloads the browser when a change occurs.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -83,6 +114,8 @@ See the [Documentation](http://azerupi.github.io/mdBook/lib/lib.html) and the [A
|
||||
|
||||
Contributions are highly appreciated and encouraged! Don't hesitate to participate to discussions in the issues, propose new features and ask for help.
|
||||
|
||||
If you are not very confident with Rust, **I will be glad to mentor as best as I can if you decide to tackle an issue or new feature.**
|
||||
|
||||
People who are not familiar with the code can look at [issues that are tagged **easy**](https://github.com/azerupi/mdBook/labels/Easy). A lot of issues are also related to web development, so people that are not comfortable with Rust can also participate! :wink:
|
||||
|
||||
You can pick any issue you want to work on. Usually it's a good idea to ask if someone is already working on it and if not to claim the issue.
|
||||
@@ -90,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.
|
||||
|
||||
60
appveyor.yml
Normal file
60
appveyor.yml
Normal file
@@ -0,0 +1,60 @@
|
||||
environment:
|
||||
global:
|
||||
PROJECT_NAME: mdBook
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: stable
|
||||
# Beta channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: beta
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: beta
|
||||
# Nightly channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: nightly
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: nightly
|
||||
|
||||
# Install Rust and Cargo
|
||||
install:
|
||||
- ps: Start-FileDownload "https://static.rust-lang.org/dist/channel-rust-stable"
|
||||
- ps: $env:RUST_VERSION = Get-Content channel-rust-stable | select -first 1 | %{$_.split('-')[1]}
|
||||
- if NOT "%RUST_CHANNEL%" == "stable" set RUST_VERSION=%RUST_CHANNEL%
|
||||
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-${env:RUST_VERSION}-${env:TARGET}.exe"
|
||||
- rust-%RUST_VERSION%-%TARGET%.exe /VERYSILENT /NORESTART /DIR="C:\Program Files (x86)\Rust"
|
||||
- SET PATH=%PATH%;C:\Program Files (x86)\Rust\bin
|
||||
- rustc -V
|
||||
- cargo -V
|
||||
|
||||
build: false
|
||||
|
||||
# Equivalent to Travis' `script` phase
|
||||
test_script:
|
||||
- cargo build --verbose
|
||||
- cargo test --verbose
|
||||
|
||||
before_deploy:
|
||||
# Generate artifacts for release
|
||||
- cargo build --release
|
||||
- mkdir staging
|
||||
- copy target\release\mdbook.exe staging
|
||||
- cd staging
|
||||
- 7z a ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip *
|
||||
- appveyor PushArtifact ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip
|
||||
|
||||
deploy:
|
||||
description: 'Windows release'
|
||||
artifact: /.*\.zip/
|
||||
auth_token:
|
||||
secure: QQhjKVyz7mpjlyGhlXytbFQQfKFQWTahHkD+B0NzIUoEVqO7ZLWjnoWasvLqW4nE
|
||||
provider: GitHub
|
||||
on:
|
||||
RUST_CHANNEL: stable
|
||||
appveyor_repo_tag: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
@@ -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.
|
||||
32
ci/before_deploy.sh
Normal file
32
ci/before_deploy.sh
Normal file
@@ -0,0 +1,32 @@
|
||||
# `before_deploy` phase: here we package the build artifacts
|
||||
|
||||
set -ex
|
||||
|
||||
mktempd() {
|
||||
echo $(mktemp -d 2>/dev/null || mktemp -d -t tmp)
|
||||
}
|
||||
|
||||
mk_artifacts() {
|
||||
cargo build --target $TARGET --release
|
||||
}
|
||||
|
||||
mk_tarball() {
|
||||
local td=$(mktempd)
|
||||
local out_dir=$(pwd)
|
||||
|
||||
cp target/$TARGET/release/mdbook $td
|
||||
|
||||
pushd $td
|
||||
|
||||
tar czf $out_dir/${PROJECT_NAME}-${TRAVIS_TAG}-${TARGET}.tar.gz *
|
||||
|
||||
popd $td
|
||||
rm -r $td
|
||||
}
|
||||
|
||||
main() {
|
||||
mk_artifacts
|
||||
mk_tarball
|
||||
}
|
||||
|
||||
main
|
||||
58
ci/install.sh
Normal file
58
ci/install.sh
Normal file
@@ -0,0 +1,58 @@
|
||||
# `install` phase: install stuff needed for the `script` phase
|
||||
|
||||
set -ex
|
||||
|
||||
case "$TRAVIS_OS_NAME" in
|
||||
linux)
|
||||
host=x86_64-unknown-linux-gnu
|
||||
;;
|
||||
osx)
|
||||
host=x86_64-apple-darwin
|
||||
;;
|
||||
esac
|
||||
|
||||
mktempd() {
|
||||
echo $(mktemp -d 2>/dev/null || mktemp -d -t tmp)
|
||||
}
|
||||
|
||||
install_rustup() {
|
||||
local td=$(mktempd)
|
||||
|
||||
pushd $td
|
||||
curl -O https://static.rust-lang.org/rustup/dist/$host/rustup-setup
|
||||
chmod +x rustup-setup
|
||||
./rustup-setup -y
|
||||
popd
|
||||
|
||||
rm -r $td
|
||||
|
||||
rustup default $CHANNEL
|
||||
rustc -V
|
||||
cargo -V
|
||||
}
|
||||
|
||||
install_standard_crates() {
|
||||
if [ "$host" != "$TARGET" ]; then
|
||||
if [ ! "$CHANNEL" = "stable" ]; then
|
||||
rustup target add $TARGET
|
||||
else
|
||||
local version=$(rustc -V | cut -d' ' -f2)
|
||||
local tarball=rust-std-${version}-${TARGET}
|
||||
|
||||
local td=$(mktempd)
|
||||
curl -s https://static.rust-lang.org/dist/${tarball}.tar.gz | \
|
||||
tar --strip-components 1 -C $td -xz
|
||||
|
||||
$td/install.sh --prefix=$(rustc --print sysroot)
|
||||
|
||||
rm -r $td
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
install_rustup
|
||||
install_standard_crates
|
||||
}
|
||||
|
||||
main
|
||||
45
ci/script.sh
Normal file
45
ci/script.sh
Normal file
@@ -0,0 +1,45 @@
|
||||
# `script` phase: you usually build, test and generate docs in this phase
|
||||
|
||||
set -ex
|
||||
|
||||
# NOTE Workaround for rust-lang/rust#31907 - disable doc tests when cross compiling
|
||||
# This has been fixed in the nightly channel but it would take a while to reach the other channels
|
||||
disable_cross_doctests() {
|
||||
local host
|
||||
case "$TRAVIS_OS_NAME" in
|
||||
linux)
|
||||
host=x86_64-unknown-linux-gnu
|
||||
;;
|
||||
osx)
|
||||
host=x86_64-apple-darwin
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$host" != "$TARGET" ] && [ "$CHANNEL" != "nightly" ]; then
|
||||
if [ "$TRAVIS_OS_NAME" = "osx" ]; then
|
||||
brew install gnu-sed --default-names
|
||||
fi
|
||||
|
||||
find src -name '*.rs' -type f | xargs sed -i -e 's:\(//.\s*```\):\1 ignore,:g'
|
||||
fi
|
||||
}
|
||||
|
||||
run_test_suite() {
|
||||
# Extra test without default features to avoid bitrot. We only test on a single target (but with
|
||||
# all the channels) to avoid significantly increasing the build times
|
||||
if [ $TARGET = x86_64-unknown-linux-gnu ]; then
|
||||
cargo build --target $TARGET --no-default-features --verbose
|
||||
cargo test --target $TARGET --no-default-features --verbose
|
||||
cargo clean
|
||||
fi
|
||||
|
||||
cargo build --target $TARGET --verbose
|
||||
cargo test --target $TARGET --verbose
|
||||
}
|
||||
|
||||
main() {
|
||||
disable_cross_doctests
|
||||
run_test_suite
|
||||
}
|
||||
|
||||
main
|
||||
@@ -15,7 +15,7 @@ cargo doc
|
||||
|
||||
echo -e "${CYAN}Running mdbook build${NC}"
|
||||
# Run mdbook to generate the book
|
||||
target/debug/mdbook build book-example/
|
||||
target/"$TARGET"/debug/mdbook build book-example/
|
||||
|
||||
echo -e "${CYAN}Copying book to target/doc${NC}"
|
||||
# Copy files from rendered book to doc root
|
||||
|
||||
15
rustfmt.toml
Normal file
15
rustfmt.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
write_mode = "Overwrite"
|
||||
|
||||
max_width = 120
|
||||
ideal_width = 120
|
||||
fn_call_width = 100
|
||||
|
||||
fn_args_density = "Compressed"
|
||||
|
||||
enum_trailing_comma = true
|
||||
match_block_trailing_comma = true
|
||||
struct_trailing_comma = "Always"
|
||||
wrap_comments = true
|
||||
|
||||
report_todo = "Always"
|
||||
report_fixme = "Always"
|
||||
@@ -1,27 +1,40 @@
|
||||
#[macro_use]
|
||||
extern crate mdbook;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate crossbeam;
|
||||
extern crate log;
|
||||
extern crate env_logger;
|
||||
extern crate open;
|
||||
|
||||
// Dependencies for the Watch feature
|
||||
#[cfg(feature = "watch")]
|
||||
extern crate notify;
|
||||
#[cfg(feature = "watch")]
|
||||
extern crate time;
|
||||
#[cfg(feature = "watch")]
|
||||
extern crate crossbeam;
|
||||
|
||||
// Dependencies for the Serve feature
|
||||
#[cfg(feature = "serve")]
|
||||
extern crate iron;
|
||||
#[cfg(feature = "serve")]
|
||||
extern crate staticfile;
|
||||
#[cfg(feature = "serve")]
|
||||
extern crate ws;
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::ffi::OsStr;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use clap::{App, ArgMatches, SubCommand, AppSettings};
|
||||
|
||||
// Uses for the Watch feature
|
||||
#[cfg(feature = "watch")]
|
||||
use notify::Watcher;
|
||||
#[cfg(feature = "watch")]
|
||||
use std::time::Duration;
|
||||
#[cfg(feature = "watch")]
|
||||
use std::sync::mpsc::channel;
|
||||
|
||||
|
||||
@@ -30,42 +43,61 @@ 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")
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
// Get the version from our Cargo.toml using clap's crate_version!() macro
|
||||
.version(&*format!("v{}", crate_version!()))
|
||||
.subcommand_required(true)
|
||||
.after_help("For more information about a specific command, try `mdbook <command> --help`")
|
||||
.setting(AppSettings::SubcommandRequired)
|
||||
.after_help("For more information about a specific command, try `mdbook <command> --help`\nSource code for mdbook available at: https://github.com/azerupi/mdBook")
|
||||
.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("--no-create 'Will not create non-existent files linked from SUMMARY.md'")
|
||||
.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 omitted)'")
|
||||
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
|
||||
.arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
|
||||
.arg_from_usage("-w, --websocket-port=[ws-port] 'Use another port for the websocket connection (livereload){n}(Defaults to 3001)'")
|
||||
.arg_from_usage("-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'")
|
||||
.arg_from_usage("-a, --address=[address] 'Address that the browser can reach the websocket server from{n}(Defaults to the interface address)'")
|
||||
.arg_from_usage("-o, --open 'Open the book server in a web browser'"))
|
||||
.subcommand(SubCommand::with_name("test")
|
||||
.about("Test that code samples compile"))
|
||||
.get_matches();
|
||||
|
||||
// Check which subcomamnd the user ran...
|
||||
let res = match matches.subcommand() {
|
||||
("init", Some(sub_matches)) => init(sub_matches),
|
||||
("init", Some(sub_matches)) => init(sub_matches),
|
||||
("build", Some(sub_matches)) => build(sub_matches),
|
||||
#[cfg(feature = "watch")]
|
||||
("watch", Some(sub_matches)) => watch(sub_matches),
|
||||
#[cfg(feature = "serve")]
|
||||
("serve", Some(sub_matches)) => serve(sub_matches),
|
||||
("test", Some(sub_matches)) => test(sub_matches),
|
||||
(_, _) => unreachable!()
|
||||
(_, _) => unreachable!(),
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
writeln!(&mut io::stderr(), "An error occured:\n{}", e).ok();
|
||||
::std::process::exit(101);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +109,7 @@ fn confirm() -> bool {
|
||||
io::stdin().read_line(&mut s).ok();
|
||||
match &*s.trim() {
|
||||
"Y" | "y" | "yes" | "Yes" => true,
|
||||
_ => false
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +126,7 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
// If flag `--theme` is present, copy theme to src
|
||||
if args.is_present("theme") {
|
||||
|
||||
// Skip this id `--force` is present
|
||||
// Skip this if `--force` is present
|
||||
if !args.is_present("force") {
|
||||
// Print warning
|
||||
print!("\nCopying the default theme to {:?}", book.get_src());
|
||||
@@ -115,6 +147,18 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
|
||||
}
|
||||
|
||||
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`
|
||||
let is_dest_inside_root = book.get_dest().starts_with(book.get_root());
|
||||
|
||||
if !args.is_present("force") && is_dest_inside_root {
|
||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||
|
||||
if confirm() {
|
||||
book.create_gitignore();
|
||||
println!("\n.gitignore created.");
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nAll done, no errors...");
|
||||
|
||||
Ok(())
|
||||
@@ -124,10 +168,23 @@ 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
|
||||
};
|
||||
|
||||
if args.is_present("no-create") {
|
||||
book.create_missing = false;
|
||||
}
|
||||
|
||||
try!(book.build());
|
||||
|
||||
if args.is_present("open") {
|
||||
open(book.get_dest().join("index.html"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -138,68 +195,101 @@ fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::new(&book_dir).read_config();
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
|
||||
None => book
|
||||
};
|
||||
|
||||
let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);
|
||||
if args.is_present("open") {
|
||||
try!(book.build());
|
||||
open(book.get_dest().join("index.html"));
|
||||
}
|
||||
|
||||
match w {
|
||||
Ok(mut watcher) => {
|
||||
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!("");
|
||||
});
|
||||
|
||||
// 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);
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 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_dir.join("book.json")) {
|
||||
// do nothing if book.json is not found
|
||||
}
|
||||
|
||||
let previous_time = time::get_time().sec;
|
||||
// Watch command implementation
|
||||
#[cfg(feature = "serve")]
|
||||
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
const RELOAD_COMMAND: &'static str = "reload";
|
||||
|
||||
crossbeam::scope(|scope| {
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(event) => {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::new(&book_dir).read_config();
|
||||
|
||||
// Skip the event if an event has already been issued in the last second
|
||||
if time::get_time().sec - previous_time < 1 { continue }
|
||||
let mut book = match args.value_of("dest-dir") {
|
||||
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
|
||||
None => book
|
||||
};
|
||||
|
||||
if let Some(path) = event.path {
|
||||
// Trigger the build process in a new thread (to keep receiving events)
|
||||
scope.spawn(move || {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
match build(args) {
|
||||
Err(e) => println!("Error while building: {:?}", e),
|
||||
_ => {}
|
||||
}
|
||||
println!("");
|
||||
});
|
||||
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");
|
||||
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("An error occured: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let address = format!("{}:{}", interface, port);
|
||||
let ws_address = format!("{}:{}", interface, ws_port);
|
||||
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
::std::process::exit(0);
|
||||
}
|
||||
}
|
||||
book.set_livereload(format!(r#"
|
||||
<script type="text/javascript">
|
||||
var socket = new WebSocket("ws://{}:{}");
|
||||
socket.onmessage = function (event) {{
|
||||
if (event.data === "{}") {{
|
||||
socket.close();
|
||||
location.reload(true); // force reload from server (not from cache)
|
||||
}}
|
||||
}};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
window.onbeforeunload = function() {{
|
||||
socket.close();
|
||||
}}
|
||||
</script>
|
||||
"#, public_address, ws_port, RELOAD_COMMAND).to_owned());
|
||||
|
||||
try!(book.build());
|
||||
|
||||
let staticfile = staticfile::Static::new(book.get_dest());
|
||||
let iron = iron::Iron::new(staticfile);
|
||||
let _iron = iron.http(&*address).unwrap();
|
||||
|
||||
let ws_server = ws::WebSocket::new(|_| {
|
||||
|_| {
|
||||
Ok(())
|
||||
}
|
||||
}).unwrap();
|
||||
|
||||
let broadcaster = ws_server.broadcaster();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
ws_server.listen(&*ws_address).unwrap();
|
||||
});
|
||||
|
||||
println!("\nServing on {}", address);
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
@@ -212,17 +302,79 @@ fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
||||
if let Some(dir) = args.value_of("dir") {
|
||||
// Check if path is relative from current dir, or absolute...
|
||||
let p = Path::new(dir);
|
||||
if p.is_relative() {
|
||||
env::current_dir().unwrap().join(dir)
|
||||
env::current_dir().unwrap().join(dir)
|
||||
} else {
|
||||
p.to_path_buf()
|
||||
p.to_path_buf()
|
||||
}
|
||||
} else {
|
||||
env::current_dir().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
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(&Path, &mut MDBook) -> ()
|
||||
{
|
||||
use notify::RecursiveMode::*;
|
||||
use notify::DebouncedEvent::*;
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
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,32 +1,41 @@
|
||||
extern crate rustc_serialize;
|
||||
use self::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 {
|
||||
root: PathBuf,
|
||||
pub dest: PathBuf,
|
||||
pub src: PathBuf,
|
||||
pub theme_path: PathBuf,
|
||||
|
||||
pub title: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
root: PathBuf,
|
||||
dest: PathBuf,
|
||||
src: PathBuf,
|
||||
|
||||
pub indent_spaces: i32,
|
||||
multilingual: bool,
|
||||
}
|
||||
|
||||
|
||||
impl BookConfig {
|
||||
pub fn new(root: &Path) -> Self {
|
||||
BookConfig {
|
||||
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(),
|
||||
root: root.to_owned(),
|
||||
dest: PathBuf::from("book"),
|
||||
src: PathBuf::from("src"),
|
||||
indent_spaces: 4, // indentation used for SUMMARY.md
|
||||
|
||||
indent_spaces: 4, // indentation used for SUMMARY.md
|
||||
multilingual: false,
|
||||
}
|
||||
}
|
||||
@@ -35,45 +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() {
|
||||
|
||||
// Convert to JSON
|
||||
if let Ok(config) = Json::from_str(&data) {
|
||||
// Extract data
|
||||
debug!("[*]: Reading config");
|
||||
let data = read_file(root.join("book.toml"));
|
||||
self.parse_from_toml_string(&data);
|
||||
|
||||
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("\"", "") }
|
||||
} else if root.join("book.json").exists() {
|
||||
|
||||
// Destination
|
||||
if let Some(a) = config.find_path(&["dest"]) {
|
||||
let dest = PathBuf::from(&a.to_string().replace("\"", ""));
|
||||
debug!("[*]: Reading config");
|
||||
let data = read_file(root.join("book.json"));
|
||||
self.parse_from_json_string(&data);
|
||||
|
||||
// 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); },
|
||||
}
|
||||
} else {
|
||||
debug!("[*]: No book.toml or book.json was found, using defaults.");
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
self.parse_from_btreemap(&config);
|
||||
|
||||
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
|
||||
@@ -106,4 +184,42 @@ impl BookConfig {
|
||||
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,8 +1,6 @@
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use self::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 {
|
||||
@@ -27,7 +25,6 @@ pub struct BookItems<'a> {
|
||||
|
||||
|
||||
impl Chapter {
|
||||
|
||||
pub fn new(name: String, path: PathBuf) -> Self {
|
||||
|
||||
Chapter {
|
||||
@@ -39,15 +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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,10 +60,10 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
Some((parent_items, parent_idx)) => {
|
||||
self.items = parent_items;
|
||||
self.current_index = parent_idx + 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
} else {
|
||||
let cur = self.items.get(self.current_index).unwrap();
|
||||
let cur = &self.items[self.current_index];
|
||||
|
||||
match *cur {
|
||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
|
||||
@@ -79,10 +73,10 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
},
|
||||
BookItem::Spacer => {
|
||||
self.current_index += 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return Some(cur)
|
||||
return Some(cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,370 +0,0 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{self, File};
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::io::ErrorKind;
|
||||
use std::process::Command;
|
||||
|
||||
use {BookConfig, BookItem, theme, parse, utils};
|
||||
use book::BookItems;
|
||||
use renderer::{Renderer, HtmlHandlebars};
|
||||
|
||||
|
||||
pub struct MDBook {
|
||||
config: BookConfig,
|
||||
pub content: Vec<BookItem>,
|
||||
renderer: Box<Renderer>,
|
||||
}
|
||||
|
||||
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`
|
||||
///
|
||||
/// 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);
|
||||
}
|
||||
|
||||
MDBook {
|
||||
content: vec![],
|
||||
config: BookConfig::new(root)
|
||||
.set_src(&root.join("src"))
|
||||
.set_dest(&root.join("book"))
|
||||
.to_owned(),
|
||||
renderer: Box::new(HtmlHandlebars::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a flat depth-first iterator over the elements of the book, it returns an [BookItem enum](bookitem.html):
|
||||
/// `(section: String, bookitem: &BookItem)`
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate mdbook;
|
||||
/// # use mdbook::MDBook;
|
||||
/// # use mdbook::BookItem;
|
||||
/// # use std::path::Path;
|
||||
/// # fn main() {
|
||||
/// # let mut book = MDBook::new(Path::new("mybook"));
|
||||
/// for item in book.iter() {
|
||||
/// match item {
|
||||
/// &BookItem::Chapter(ref section, ref chapter) => {},
|
||||
/// &BookItem::Affix(ref chapter) => {},
|
||||
/// &BookItem::Spacer => {},
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // would print something like this:
|
||||
/// // 1. Chapter 1
|
||||
/// // 1.1 Sub Chapter
|
||||
/// // 1.2 Sub Chapter
|
||||
/// // 2. Chapter 2
|
||||
/// //
|
||||
/// // etc.
|
||||
/// # }
|
||||
/// ```
|
||||
|
||||
pub fn iter(&self) -> BookItems {
|
||||
BookItems {
|
||||
items: &self.content[..],
|
||||
current_index: 0,
|
||||
stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `init()` creates some boilerplate files and directories to get you started with your book.
|
||||
///
|
||||
/// ```text
|
||||
/// book-test/
|
||||
/// ├── book
|
||||
/// └── src
|
||||
/// ├── chapter_1.md
|
||||
/// └── SUMMARY.md
|
||||
/// ```
|
||||
///
|
||||
/// It uses the paths given as source and output directories and adds a `SUMMARY.md` and a
|
||||
/// `chapter_1.md` to the source directory.
|
||||
|
||||
pub fn init(&mut self) -> Result<(), Box<Error>> {
|
||||
|
||||
debug!("[fn]: init");
|
||||
|
||||
if !self.config.get_root().exists() {
|
||||
fs::create_dir_all(self.config.get_root()).unwrap();
|
||||
output!("{:?} created", self.config.get_root());
|
||||
}
|
||||
|
||||
{
|
||||
let dest = self.config.get_dest();
|
||||
let src = self.config.get_src();
|
||||
|
||||
if !dest.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", dest);
|
||||
try!(fs::create_dir(&dest));
|
||||
}
|
||||
|
||||
if !src.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", src);
|
||||
try!(fs::create_dir(&src));
|
||||
}
|
||||
|
||||
let summary = src.join("SUMMARY.md");
|
||||
|
||||
if !summary.exists() {
|
||||
|
||||
// Summary does not exist, create it
|
||||
|
||||
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", src.join("SUMMARY.md"));
|
||||
let mut f = try!(File::create(&src.join("SUMMARY.md")));
|
||||
|
||||
debug!("[*]: Writing to SUMMARY.md");
|
||||
|
||||
try!(writeln!(f, "# Summary"));
|
||||
try!(writeln!(f, ""));
|
||||
try!(writeln!(f, "- [Chapter 1](./chapter_1.md)"));
|
||||
}
|
||||
}
|
||||
|
||||
// parse SUMMARY.md, and create the missing item related file
|
||||
try!(self.parse_summary());
|
||||
|
||||
debug!("[*]: constructing paths for missing files");
|
||||
for item in self.iter() {
|
||||
debug!("[*]: item: {:?}", item);
|
||||
match *item {
|
||||
BookItem::Spacer => continue,
|
||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
|
||||
if ch.path != PathBuf::new() {
|
||||
let path = self.config.get_src().join(&ch.path);
|
||||
|
||||
if !path.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create file", path);
|
||||
try!(::std::fs::create_dir_all(path.parent().unwrap()));
|
||||
let mut f = try!(File::create(path));
|
||||
|
||||
//debug!("[*]: Writing to {:?}", path);
|
||||
try!(writeln!(f, "# {}", ch.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("[*]: init done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The `build()` method is the one where everything happens. First it parses `SUMMARY.md` to
|
||||
/// construct the book's structure in the form of a `Vec<BookItem>` and then calls `render()`
|
||||
/// method of the current renderer.
|
||||
///
|
||||
/// It is the renderer who generates all the output files.
|
||||
|
||||
pub fn build(&mut self) -> Result<(), Box<Error>> {
|
||||
debug!("[fn]: build");
|
||||
|
||||
try!(self.init());
|
||||
|
||||
// Clean output directory
|
||||
try!(utils::remove_dir_content(&self.config.get_dest()));
|
||||
|
||||
try!(self.renderer.render(&self));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn copy_theme(&self) -> Result<(), Box<Error>> {
|
||||
debug!("[fn]: copy_theme");
|
||||
|
||||
let theme_dir = self.config.get_src().join("theme");
|
||||
|
||||
if !theme_dir.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", theme_dir);
|
||||
try!(fs::create_dir(&theme_dir));
|
||||
}
|
||||
|
||||
// index.hbs
|
||||
let mut index = try!(File::create(&theme_dir.join("index.hbs")));
|
||||
try!(index.write_all(theme::INDEX));
|
||||
|
||||
// book.css
|
||||
let mut css = try!(File::create(&theme_dir.join("book.css")));
|
||||
try!(css.write_all(theme::CSS));
|
||||
|
||||
// favicon.png
|
||||
let mut favicon = try!(File::create(&theme_dir.join("favicon.png")));
|
||||
try!(favicon.write_all(theme::FAVICON));
|
||||
|
||||
// book.js
|
||||
let mut js = try!(File::create(&theme_dir.join("book.js")));
|
||||
try!(js.write_all(theme::JS));
|
||||
|
||||
// highlight.css
|
||||
let mut highlight_css = try!(File::create(&theme_dir.join("highlight.css")));
|
||||
try!(highlight_css.write_all(theme::HIGHLIGHT_CSS));
|
||||
|
||||
// highlight.js
|
||||
let mut highlight_js = try!(File::create(&theme_dir.join("highlight.js")));
|
||||
try!(highlight_js.write_all(theme::HIGHLIGHT_JS));
|
||||
|
||||
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`
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate mdbook;
|
||||
/// # use mdbook::MDBook;
|
||||
/// # use std::path::Path;
|
||||
/// # fn main() {
|
||||
/// let mut book = MDBook::new(Path::new("root_dir"));
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// In this example, `root_dir` will be the root directory of our book and is specified in function
|
||||
/// of the current working directory by using a relative path instead of an absolute path.
|
||||
|
||||
pub fn read_config(mut self) -> Self {
|
||||
let root = self.config.get_root().to_owned();
|
||||
self.config.read_config(&root);
|
||||
self
|
||||
}
|
||||
|
||||
/// You can change the default renderer to another one by using this method. The only requirement
|
||||
/// is for your renderer to implement the [Renderer trait](../../renderer/renderer/trait.Renderer.html)
|
||||
///
|
||||
/// ```no_run
|
||||
/// extern crate mdbook;
|
||||
/// use mdbook::MDBook;
|
||||
/// use mdbook::renderer::HtmlHandlebars;
|
||||
/// # use std::path::Path;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let mut book = MDBook::new(Path::new("mybook"))
|
||||
/// .set_renderer(Box::new(HtmlHandlebars::new()));
|
||||
///
|
||||
/// // In this example we replace the default renderer by the default renderer...
|
||||
/// // Don't forget to put your renderer in a Box
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// **note:** Don't forget to put your renderer in a `Box` before passing it to `set_renderer()`
|
||||
|
||||
pub fn set_renderer(mut self, renderer: Box<Renderer>) -> Self {
|
||||
self.renderer = renderer;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn test(&mut self) -> Result<(), Box<Error>> {
|
||||
// read in the chapters
|
||||
try!(self.parse_summary());
|
||||
for item in self.iter() {
|
||||
|
||||
match *item {
|
||||
BookItem::Chapter(_, ref ch) => {
|
||||
if ch.path != PathBuf::new() {
|
||||
|
||||
let path = self.get_src().join(&ch.path);
|
||||
|
||||
println!("[*]: Testing file: {:?}", path);
|
||||
|
||||
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>);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_dest(mut self, dest: &Path) -> Self {
|
||||
|
||||
// Handle absolute and relative paths
|
||||
match dest.is_absolute() {
|
||||
true => { self.config.set_dest(dest); },
|
||||
false => {
|
||||
let dest = self.config.get_root().join(dest).to_owned();
|
||||
self.config.set_dest(&dest);
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_dest(&self) -> &Path {
|
||||
self.config.get_dest()
|
||||
}
|
||||
|
||||
pub fn set_src(mut self, src: &Path) -> Self {
|
||||
|
||||
// Handle absolute and relative paths
|
||||
match src.is_absolute() {
|
||||
true => { self.config.set_src(src); },
|
||||
false => {
|
||||
let src = self.config.get_root().join(src).to_owned();
|
||||
self.config.set_src(&src);
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_src(&self) -> &Path {
|
||||
self.config.get_src()
|
||||
}
|
||||
|
||||
pub fn set_title(mut self, title: &str) -> Self {
|
||||
self.config.title = title.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_title(&self) -> &str {
|
||||
&self.config.title
|
||||
}
|
||||
|
||||
pub fn set_author(mut self, author: &str) -> Self {
|
||||
self.config.author = author.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_author(&self) -> &str {
|
||||
&self.config.author
|
||||
}
|
||||
|
||||
pub fn set_description(mut self, description: &str) -> Self {
|
||||
self.config.description = description.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_description(&self) -> &str {
|
||||
&self.config.description
|
||||
}
|
||||
|
||||
// Construct book
|
||||
fn parse_summary(&mut self) -> Result<(), Box<Error>> {
|
||||
// When append becomes stable, use self.content.append() ...
|
||||
self.content = try!(parse::construct_bookitems(&self.config.get_src().join("SUMMARY.md")));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
480
src/book/mod.rs
480
src/book/mod.rs
@@ -1,7 +1,483 @@
|
||||
pub mod mdbook;
|
||||
pub mod bookitem;
|
||||
pub mod bookconfig;
|
||||
|
||||
pub mod bookconfig_test;
|
||||
|
||||
pub use self::bookitem::{BookItem, BookItems};
|
||||
pub use self::bookconfig::BookConfig;
|
||||
pub use self::mdbook::MDBook;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{self, File};
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::io::ErrorKind;
|
||||
use std::process::Command;
|
||||
|
||||
use {theme, parse, utils};
|
||||
use renderer::{Renderer, HtmlHandlebars};
|
||||
|
||||
|
||||
pub struct MDBook {
|
||||
root: PathBuf,
|
||||
dest: PathBuf,
|
||||
src: PathBuf,
|
||||
theme_path: PathBuf,
|
||||
|
||||
pub title: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
|
||||
pub content: Vec<BookItem>,
|
||||
renderer: Box<Renderer>,
|
||||
|
||||
livereload: Option<String>,
|
||||
|
||||
/// Should `mdbook build` create files referenced from SUMMARY.md if they
|
||||
/// don't exist
|
||||
pub create_missing: bool,
|
||||
}
|
||||
|
||||
impl MDBook {
|
||||
/// Create a new `MDBook` struct with root directory `root`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate mdbook;
|
||||
/// # use mdbook::MDBook;
|
||||
/// # use std::path::Path;
|
||||
/// # fn main() {
|
||||
/// let book = MDBook::new(Path::new("root_dir"));
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// In this example, `root_dir` will be the root directory of our book and is specified in function
|
||||
/// of the current working directory by using a relative path instead of an absolute path.
|
||||
///
|
||||
/// 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() {
|
||||
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(),
|
||||
description: String::new(),
|
||||
|
||||
content: vec![],
|
||||
renderer: Box::new(HtmlHandlebars::new()),
|
||||
|
||||
livereload: None,
|
||||
create_missing: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a flat depth-first iterator over the elements of the book, it returns an [BookItem enum](bookitem.html):
|
||||
/// `(section: String, bookitem: &BookItem)`
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate mdbook;
|
||||
/// # use mdbook::MDBook;
|
||||
/// # use mdbook::BookItem;
|
||||
/// # use std::path::Path;
|
||||
/// # fn main() {
|
||||
/// # let mut book = MDBook::new(Path::new("mybook"));
|
||||
/// for item in book.iter() {
|
||||
/// match item {
|
||||
/// &BookItem::Chapter(ref section, ref chapter) => {},
|
||||
/// &BookItem::Affix(ref chapter) => {},
|
||||
/// &BookItem::Spacer => {},
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // would print something like this:
|
||||
/// // 1. Chapter 1
|
||||
/// // 1.1 Sub Chapter
|
||||
/// // 1.2 Sub Chapter
|
||||
/// // 2. Chapter 2
|
||||
/// //
|
||||
/// // etc.
|
||||
/// # }
|
||||
/// ```
|
||||
|
||||
pub fn iter(&self) -> BookItems {
|
||||
BookItems {
|
||||
items: &self.content[..],
|
||||
current_index: 0,
|
||||
stack: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `init()` creates some boilerplate files and directories to get you started with your book.
|
||||
///
|
||||
/// ```text
|
||||
/// book-test/
|
||||
/// ├── book
|
||||
/// └── src
|
||||
/// ├── chapter_1.md
|
||||
/// └── SUMMARY.md
|
||||
/// ```
|
||||
///
|
||||
/// It uses the paths given as source and output directories and adds a `SUMMARY.md` and a
|
||||
/// `chapter_1.md` to the source directory.
|
||||
|
||||
pub fn init(&mut self) -> Result<(), Box<Error>> {
|
||||
|
||||
debug!("[fn]: init");
|
||||
|
||||
if !self.root.exists() {
|
||||
fs::create_dir_all(&self.root).unwrap();
|
||||
info!("{:?} created", &self.root);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
if !self.dest.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", self.dest);
|
||||
try!(fs::create_dir_all(&self.dest));
|
||||
}
|
||||
|
||||
if !self.src.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", self.src);
|
||||
try!(fs::create_dir_all(&self.src));
|
||||
}
|
||||
|
||||
let summary = self.src.join("SUMMARY.md");
|
||||
|
||||
if !summary.exists() {
|
||||
|
||||
// Summary does not exist, create it
|
||||
|
||||
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", self.src.join("SUMMARY.md"));
|
||||
let mut f = try!(File::create(&self.src.join("SUMMARY.md")));
|
||||
|
||||
debug!("[*]: Writing to SUMMARY.md");
|
||||
|
||||
try!(writeln!(f, "# Summary"));
|
||||
try!(writeln!(f, ""));
|
||||
try!(writeln!(f, "- [Chapter 1](./chapter_1.md)"));
|
||||
}
|
||||
}
|
||||
|
||||
// parse SUMMARY.md, and create the missing item related file
|
||||
try!(self.parse_summary());
|
||||
|
||||
debug!("[*]: constructing paths for missing files");
|
||||
for item in self.iter() {
|
||||
debug!("[*]: item: {:?}", item);
|
||||
let ch = match *item {
|
||||
BookItem::Spacer => continue,
|
||||
BookItem::Chapter(_, ref ch) |
|
||||
BookItem::Affix(ref ch) => ch,
|
||||
};
|
||||
if ch.path.as_os_str().is_empty() {
|
||||
let path = self.src.join(&ch.path);
|
||||
|
||||
if !path.exists() {
|
||||
if !self.create_missing {
|
||||
return Err(format!(
|
||||
"'{}' referenced from SUMMARY.md does not exist.",
|
||||
path.to_string_lossy()).into());
|
||||
}
|
||||
debug!("[*]: {:?} does not exist, trying to create file", path);
|
||||
try!(::std::fs::create_dir_all(path.parent().unwrap()));
|
||||
let mut f = try!(File::create(path));
|
||||
|
||||
// debug!("[*]: Writing to {:?}", path);
|
||||
try!(writeln!(f, "# {}", ch.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug!("[*]: init done");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_gitignore(&self) {
|
||||
let gitignore = self.get_gitignore();
|
||||
|
||||
if !gitignore.exists() {
|
||||
// Gitignore does not exist, create it
|
||||
|
||||
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`. If it
|
||||
// is not, `strip_prefix` will return an Error.
|
||||
if !self.get_dest().starts_with(&self.root) {
|
||||
return;
|
||||
}
|
||||
|
||||
let relative = self.get_dest()
|
||||
.strip_prefix(&self.root)
|
||||
.expect("Destination is not relative to root.");
|
||||
let relative = relative.to_str()
|
||||
.expect("Path could not be yielded into a string slice.");
|
||||
|
||||
debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore);
|
||||
|
||||
let mut f = File::create(&gitignore).expect("Could not create file.");
|
||||
|
||||
debug!("[*]: Writing to .gitignore");
|
||||
|
||||
writeln!(f, "{}", relative).expect("Could not write to file.");
|
||||
}
|
||||
}
|
||||
|
||||
/// The `build()` method is the one where everything happens. First it parses `SUMMARY.md` to
|
||||
/// construct the book's structure in the form of a `Vec<BookItem>` and then calls `render()`
|
||||
/// method of the current renderer.
|
||||
///
|
||||
/// It is the renderer who generates all the output files.
|
||||
pub fn build(&mut self) -> Result<(), Box<Error>> {
|
||||
debug!("[fn]: build");
|
||||
|
||||
try!(self.init());
|
||||
|
||||
// Clean output directory
|
||||
try!(utils::fs::remove_dir_content(&self.dest));
|
||||
|
||||
try!(self.renderer.render(&self));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn get_gitignore(&self) -> PathBuf {
|
||||
self.root.join(".gitignore")
|
||||
}
|
||||
|
||||
pub fn copy_theme(&self) -> Result<(), Box<Error>> {
|
||||
debug!("[fn]: copy_theme");
|
||||
|
||||
let theme_dir = self.src.join("theme");
|
||||
|
||||
if !theme_dir.exists() {
|
||||
debug!("[*]: {:?} does not exist, trying to create directory", theme_dir);
|
||||
try!(fs::create_dir(&theme_dir));
|
||||
}
|
||||
|
||||
// index.hbs
|
||||
let mut index = try!(File::create(&theme_dir.join("index.hbs")));
|
||||
try!(index.write_all(theme::INDEX));
|
||||
|
||||
// book.css
|
||||
let mut css = try!(File::create(&theme_dir.join("book.css")));
|
||||
try!(css.write_all(theme::CSS));
|
||||
|
||||
// favicon.png
|
||||
let mut favicon = try!(File::create(&theme_dir.join("favicon.png")));
|
||||
try!(favicon.write_all(theme::FAVICON));
|
||||
|
||||
// book.js
|
||||
let mut js = try!(File::create(&theme_dir.join("book.js")));
|
||||
try!(js.write_all(theme::JS));
|
||||
|
||||
// highlight.css
|
||||
let mut highlight_css = try!(File::create(&theme_dir.join("highlight.css")));
|
||||
try!(highlight_css.write_all(theme::HIGHLIGHT_CSS));
|
||||
|
||||
// highlight.js
|
||||
let mut highlight_js = try!(File::create(&theme_dir.join("highlight.js")));
|
||||
try!(highlight_js.write_all(theme::HIGHLIGHT_JS));
|
||||
|
||||
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`
|
||||
|
||||
pub fn read_config(mut self) -> Self {
|
||||
|
||||
let config = BookConfig::new(&self.root)
|
||||
.read_config(&self.root)
|
||||
.to_owned();
|
||||
|
||||
self.title = config.title;
|
||||
self.description = config.description;
|
||||
self.author = config.author;
|
||||
|
||||
self.dest = config.dest;
|
||||
self.src = config.src;
|
||||
self.theme_path = config.theme_path;
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// You can change the default renderer to another one by using this method. The only requirement
|
||||
/// is for your renderer to implement the [Renderer trait](../../renderer/renderer/trait.Renderer.html)
|
||||
///
|
||||
/// ```no_run
|
||||
/// extern crate mdbook;
|
||||
/// use mdbook::MDBook;
|
||||
/// use mdbook::renderer::HtmlHandlebars;
|
||||
/// # use std::path::Path;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let mut book = MDBook::new(Path::new("mybook"))
|
||||
/// .set_renderer(Box::new(HtmlHandlebars::new()));
|
||||
///
|
||||
/// // In this example we replace the default renderer by the default renderer...
|
||||
/// // Don't forget to put your renderer in a Box
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// **note:** Don't forget to put your renderer in a `Box` before passing it to `set_renderer()`
|
||||
|
||||
pub fn set_renderer(mut self, renderer: Box<Renderer>) -> Self {
|
||||
self.renderer = renderer;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn test(&mut self) -> Result<(), Box<Error>> {
|
||||
// read in the chapters
|
||||
try!(self.parse_summary());
|
||||
for item in self.iter() {
|
||||
|
||||
if let BookItem::Chapter(_, ref ch) = *item {
|
||||
if ch.path != PathBuf::new() {
|
||||
|
||||
let path = self.get_src().join(&ch.path);
|
||||
|
||||
println!("[*]: Testing file: {:?}", path);
|
||||
|
||||
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>);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
pub fn set_dest(mut self, dest: &Path) -> Self {
|
||||
|
||||
// Handle absolute and relative paths
|
||||
if dest.is_absolute() {
|
||||
self.dest = dest.to_owned();
|
||||
} else {
|
||||
let dest = self.root.join(dest).to_owned();
|
||||
self.dest = dest;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_dest(&self) -> &Path {
|
||||
&self.dest
|
||||
}
|
||||
|
||||
pub fn set_src(mut self, src: &Path) -> Self {
|
||||
|
||||
// Handle absolute and relative paths
|
||||
if src.is_absolute() {
|
||||
self.src = src.to_owned();
|
||||
} else {
|
||||
let src = self.root.join(src).to_owned();
|
||||
self.src = src;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_src(&self) -> &Path {
|
||||
&self.src
|
||||
}
|
||||
|
||||
pub fn set_title(mut self, title: &str) -> Self {
|
||||
self.title = title.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
pub fn set_author(mut self, author: &str) -> Self {
|
||||
self.author = author.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_author(&self) -> &str {
|
||||
&self.author
|
||||
}
|
||||
|
||||
pub fn set_description(mut self, description: &str) -> Self {
|
||||
self.description = description.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_description(&self) -> &str {
|
||||
&self.description
|
||||
}
|
||||
|
||||
pub fn set_livereload(&mut self, livereload: String) -> &mut Self {
|
||||
self.livereload = Some(livereload);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unset_livereload(&mut self) -> &Self {
|
||||
self.livereload = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_livereload(&self) -> Option<&String> {
|
||||
self.livereload.as_ref()
|
||||
}
|
||||
|
||||
pub fn 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
|
||||
fn parse_summary(&mut self) -> Result<(), Box<Error>> {
|
||||
// When append becomes stable, use self.content.append() ...
|
||||
self.content = try!(parse::construct_bookitems(&self.src.join("SUMMARY.md")));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
10
src/lib.rs
10
src/lib.rs
@@ -63,14 +63,20 @@
|
||||
//! I have regrouped some useful functions in the [utils](utils/index.html) module, like the following function
|
||||
//!
|
||||
//! ```ignore
|
||||
//! utils::create_path(path: &Path)
|
||||
//! utils::fs::create_path(path: &Path)
|
||||
//! ```
|
||||
//! This function creates all the directories in a given path if they do not exist
|
||||
//!
|
||||
//! Make sure to take a look at it.
|
||||
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
extern crate serde_json;
|
||||
extern crate handlebars;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate regex;
|
||||
|
||||
#[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)*) => ();
|
||||
}
|
||||
@@ -26,7 +26,9 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
|
||||
// if level < current_level we remove the last digit of section, exit the current function,
|
||||
// and return the parsed level to the calling function.
|
||||
if level < current_level { break }
|
||||
if level < current_level {
|
||||
break;
|
||||
}
|
||||
|
||||
// if level > current_level we call ourselves to go one level deeper
|
||||
if level > current_level {
|
||||
@@ -35,20 +37,22 @@ 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));
|
||||
|
||||
// Remove the last number from the section, because we got back to our level..
|
||||
section.pop();
|
||||
continue
|
||||
continue;
|
||||
} else {
|
||||
return Err(Error::new( ErrorKind::Other, format!(
|
||||
"Your summary.md is messed up\n\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."
|
||||
)))
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
Prefix, \
|
||||
Suffix and Spacer elements can only exist on the root level.\n
|
||||
\
|
||||
Prefix elements can only exist before any chapter and there can be \
|
||||
no chapters after suffix elements."));
|
||||
};
|
||||
|
||||
} else {
|
||||
@@ -59,26 +63,32 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
match parsed_item {
|
||||
// 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
|
||||
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."
|
||||
)))
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
\
|
||||
Prefix, Suffix and Spacer elements can only exist on the \
|
||||
root level.\n
|
||||
Prefix \
|
||||
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
|
||||
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."
|
||||
)))
|
||||
return Err(Error::new(ErrorKind::Other,
|
||||
"Your summary.md is messed up\n\n
|
||||
\
|
||||
Prefix, Suffix and Spacer elements can only exist on the \
|
||||
root level.\n
|
||||
Prefix \
|
||||
elements can only exist before any chapter and there can be \
|
||||
no chapters after suffix elements."))
|
||||
},
|
||||
|
||||
// Set section = -1 after suffix
|
||||
BookItem::Affix(_) if section[0] > 0 => {
|
||||
section[0] = -1;
|
||||
}
|
||||
},
|
||||
|
||||
_ => {},
|
||||
}
|
||||
@@ -86,12 +96,12 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
match parsed_item {
|
||||
BookItem::Chapter(_, ch) => {
|
||||
// Increment section
|
||||
let len = section.len() -1;
|
||||
let len = section.len() - 1;
|
||||
section[len] += 1;
|
||||
let s = section.iter().fold("".to_owned(), |s, i| s + &i.to_string() + ".");
|
||||
BookItem::Chapter(s, ch)
|
||||
}
|
||||
_ => parsed_item
|
||||
},
|
||||
_ => parsed_item,
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -131,11 +141,7 @@ fn level(line: &str, spaces_in_tab: i32) -> Result<i32> {
|
||||
debug!("[SUMMARY.md]:");
|
||||
debug!("\t[line]: {}", line);
|
||||
debug!("[*]: There is an indentation error on this line. Indentation should be {} spaces", spaces_in_tab);
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Indentation error on line:\n\n{}", line)
|
||||
)
|
||||
)
|
||||
return Err(Error::new(ErrorKind::Other, format!("Indentation error on line:\n\n{}", line)));
|
||||
}
|
||||
|
||||
Ok(level)
|
||||
@@ -146,12 +152,12 @@ fn parse_line(l: &str) -> Option<BookItem> {
|
||||
debug!("[fn]: parse_line");
|
||||
|
||||
// Remove leading and trailing spaces or tabs
|
||||
let line = l.trim_matches(|c: char| { c == ' ' || c == '\t' });
|
||||
let line = l.trim_matches(|c: char| c == ' ' || c == '\t');
|
||||
|
||||
// Spacers are "------"
|
||||
if line.starts_with("--") {
|
||||
debug!("[*]: Line is spacer");
|
||||
return Some(BookItem::Spacer)
|
||||
return Some(BookItem::Spacer);
|
||||
}
|
||||
|
||||
if let Some(c) = line.chars().nth(0) {
|
||||
@@ -161,18 +167,22 @@ fn parse_line(l: &str) -> Option<BookItem> {
|
||||
debug!("[*]: Line is list element");
|
||||
|
||||
if let Some((name, path)) = read_link(line) {
|
||||
return Some(BookItem::Chapter("0".to_owned(), Chapter::new(name, path)))
|
||||
} else { return None }
|
||||
return Some(BookItem::Chapter("0".to_owned(), Chapter::new(name, path)));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
// Non-list element
|
||||
'[' => {
|
||||
debug!("[*]: Line is a link element");
|
||||
|
||||
if let Some((name, path)) = read_link(line) {
|
||||
return Some(BookItem::Affix(Chapter::new(name, path)))
|
||||
} else { return None }
|
||||
}
|
||||
_ => {}
|
||||
return Some(BookItem::Affix(Chapter::new(name, path)));
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,32 +195,31 @@ fn read_link(line: &str) -> Option<(String, PathBuf)> {
|
||||
|
||||
// In the future, support for list item that is not a link
|
||||
// Not sure if I should error on line I can't parse or just ignore them...
|
||||
if let Some(i) = line.find('[') { start_delimitor = i; }
|
||||
else {
|
||||
if let Some(i) = line.find('[') {
|
||||
start_delimitor = i;
|
||||
} else {
|
||||
debug!("[*]: '[' not found, this line is not a link. Ignoring...");
|
||||
return None
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(i) = line[start_delimitor..].find("](") {
|
||||
end_delimitor = start_delimitor +i;
|
||||
}
|
||||
else {
|
||||
end_delimitor = start_delimitor + i;
|
||||
} else {
|
||||
debug!("[*]: '](' not found, this line is not a link. Ignoring...");
|
||||
return None
|
||||
return None;
|
||||
}
|
||||
|
||||
let name = line[start_delimitor + 1 .. end_delimitor].to_owned();
|
||||
let name = line[start_delimitor + 1..end_delimitor].to_owned();
|
||||
|
||||
start_delimitor = end_delimitor + 1;
|
||||
if let Some(i) = line[start_delimitor..].find(')') {
|
||||
end_delimitor = start_delimitor + i;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
debug!("[*]: ')' not found, this line is not a link. Ignoring...");
|
||||
return None
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = PathBuf::from(line[start_delimitor + 1 .. end_delimitor].to_owned());
|
||||
let path = PathBuf::from(line[start_delimitor + 1..end_delimitor].to_owned());
|
||||
|
||||
Some((name, path))
|
||||
}
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
extern crate handlebars;
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use renderer::html_handlebars::helpers;
|
||||
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 std::collections::HashMap;
|
||||
|
||||
use self::handlebars::{Handlebars, JsonRender};
|
||||
use self::rustc_serialize::json::{Json, ToJson};
|
||||
use handlebars::Handlebars;
|
||||
|
||||
use serde_json;
|
||||
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HtmlHandlebars;
|
||||
|
||||
impl HtmlHandlebars {
|
||||
@@ -31,7 +33,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");
|
||||
@@ -50,8 +52,9 @@ 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()) {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Unexpected error when constructing destination path")))
|
||||
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")));
|
||||
}
|
||||
|
||||
// Render a file for every entry in the book
|
||||
@@ -59,7 +62,8 @@ impl Renderer for HtmlHandlebars {
|
||||
for item in book.iter() {
|
||||
|
||||
match *item {
|
||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
|
||||
BookItem::Chapter(_, ref ch) |
|
||||
BookItem::Affix(ref ch) => {
|
||||
if ch.path != PathBuf::new() {
|
||||
|
||||
let path = book.get_src().join(&ch.path);
|
||||
@@ -80,169 +84,114 @@ 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::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::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()));
|
||||
// Do several kinds of post-processing
|
||||
let rendered = build_header_links(rendered, filename.to_str().unwrap_or(""));
|
||||
let rendered = fix_anchor_links(rendered, filename.to_str().unwrap_or(""));
|
||||
let rendered = fix_code_blocks(rendered);
|
||||
let rendered = add_playpen_pre(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);
|
||||
.read_to_string(&mut content);
|
||||
|
||||
// This could cause a problem when someone displays code containing <base href=...>
|
||||
// on the front page, however this case should be very very rare...
|
||||
content = content.lines().filter(|line| !line.contains("<base href=")).collect::<Vec<&str>>().join("\n");
|
||||
content = content.lines()
|
||||
.filter(|line| !line.contains("<base href="))
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
// 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::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::create_file(&book.get_dest().join("print").with_extension("html")));
|
||||
try!(file.write_all(&rendered.into_bytes()));
|
||||
output!("[*] Creating print.html ✓");
|
||||
|
||||
// do several kinds of post-processing
|
||||
let rendered = build_header_links(rendered, "print.html");
|
||||
let rendered = fix_anchor_links(rendered, "print.html");
|
||||
let rendered = fix_code_blocks(rendered);
|
||||
let rendered = add_playpen_pre(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::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::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::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::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::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::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::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::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"]));
|
||||
try!(utils::fs::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"]));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
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(), json!(livereload));
|
||||
}
|
||||
|
||||
let mut chapters = vec![];
|
||||
|
||||
@@ -252,31 +201,154 @@ 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_"));
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
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();
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
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>();
|
||||
|
||||
let id_count = *id_counter.get(&id).unwrap_or(&0);
|
||||
id_counter.insert(id.clone(), id_count + 1);
|
||||
|
||||
let id = if id_count > 0 {
|
||||
format!("{}-{}", id, id_count)
|
||||
} else {
|
||||
id
|
||||
};
|
||||
|
||||
format!("<a class=\"header\" href=\"{filename}#{id}\" id=\"{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()
|
||||
}
|
||||
|
||||
fn add_playpen_pre(html: String) -> String {
|
||||
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
regex.replace_all(&html, |caps: &Captures| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
|
||||
if classes.contains("language-rust") && !classes.contains("ignore") {
|
||||
// wrap the contents in an external pre block
|
||||
|
||||
if text.contains("fn main") {
|
||||
format!("<pre class=\"playpen\">{}</pre>", text)
|
||||
} else {
|
||||
// we need to inject our own main
|
||||
let (attrs, code) = partition_source(code);
|
||||
format!("<pre class=\"playpen\"><code class=\"{}\"># #![allow(unused_variables)]
|
||||
{}#fn main() {{
|
||||
{}
|
||||
#}}</code></pre>", classes, attrs, code)
|
||||
}
|
||||
} else {
|
||||
// not language-rust, so no-op
|
||||
format!("{}", text)
|
||||
}
|
||||
}).into_owned()
|
||||
}
|
||||
|
||||
fn partition_source(s: &str) -> (String, String) {
|
||||
let mut after_header = false;
|
||||
let mut before = String::new();
|
||||
let mut after = String::new();
|
||||
|
||||
for line in s.lines() {
|
||||
let trimline = line.trim();
|
||||
let header = trimline.chars().all(|c| c.is_whitespace()) ||
|
||||
trimline.starts_with("#![");
|
||||
if !header || after_header {
|
||||
after_header = true;
|
||||
after.push_str(line);
|
||||
after.push_str("\n");
|
||||
} else {
|
||||
before.push_str(line);
|
||||
before.push_str("\n");
|
||||
}
|
||||
}
|
||||
|
||||
(before, after)
|
||||
}
|
||||
@@ -1,33 +1,31 @@
|
||||
extern crate handlebars;
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{VecDeque, BTreeMap};
|
||||
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Renderable};
|
||||
|
||||
use self::rustc_serialize::json::{self, ToJson};
|
||||
use self::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{ desc: "Could not decode the JSON data".to_owned()}),
|
||||
Err(_) => return Err(RenderError::new("Could not decode the JSON data")),
|
||||
};
|
||||
let mut previous: Option<BTreeMap<String, String>> = None;
|
||||
|
||||
@@ -41,7 +39,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
|
||||
if path == ¤t {
|
||||
|
||||
debug!("[*]: Found current chapter");
|
||||
if let Some(previous) = previous{
|
||||
if let Some(previous) = previous {
|
||||
|
||||
debug!("[*]: Creating BTreeMap to inject in context");
|
||||
// Create new BTreeMap to extend the context: 'title' and 'link'
|
||||
@@ -51,47 +49,50 @@ 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");
|
||||
return Err(RenderError{ desc: "No title found for chapter in JSON data".to_owned() })
|
||||
}
|
||||
return Err(RenderError::new("No title found for chapter in JSON data"));
|
||||
},
|
||||
};
|
||||
|
||||
// Chapter link
|
||||
|
||||
match previous.get("path") {
|
||||
Some(p) => {
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
let path = Path::new(p).with_extension("html");
|
||||
debug!("[*]: Inserting link: {:?}", path);
|
||||
|
||||
match path.to_str() {
|
||||
Some(p) => { previous_chapter.insert("link".to_owned(), p.to_json()); },
|
||||
None => return Err(RenderError{ desc: "Link could not be converted to str".to_owned() })
|
||||
Some(p) => {
|
||||
previous_chapter.insert("link".to_owned(), json!(p.replace("\\", "/")));
|
||||
},
|
||||
None => return Err(RenderError::new("Link could not be converted to str")),
|
||||
}
|
||||
},
|
||||
None => return Err(RenderError{ desc: "No path found for chapter in JSON data".to_owned() })
|
||||
None => return Err(RenderError::new("No path found for chapter in JSON data")),
|
||||
}
|
||||
|
||||
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{ desc: "Error with the handlebars template".to_owned() })
|
||||
None => return Err(RenderError::new("Error with the handlebars template")),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
previous = Some(item.clone());
|
||||
}
|
||||
},
|
||||
@@ -107,24 +108,24 @@ 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{ desc: "Could not decode the JSON data".to_owned() }),
|
||||
Err(_) => return Err(RenderError::new("Could not decode the JSON data")),
|
||||
};
|
||||
let mut previous: Option<BTreeMap<String, String>> = None;
|
||||
|
||||
@@ -140,7 +141,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
|
||||
|
||||
let previous_path = match previous.get("path") {
|
||||
Some(p) => p,
|
||||
None => return Err(RenderError{ desc: "No path found for chapter in JSON data".to_owned() })
|
||||
None => return Err(RenderError::new("No path found for chapter in JSON data")),
|
||||
};
|
||||
|
||||
if previous_path == ¤t {
|
||||
@@ -153,9 +154,9 @@ 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());
|
||||
}
|
||||
None => return Err(RenderError{ desc: "No title found for chapter in JSON data".to_owned() })
|
||||
next_chapter.insert("title".to_owned(), json!(n));
|
||||
},
|
||||
None => return Err(RenderError::new("No title found for chapter in JSON data")),
|
||||
}
|
||||
|
||||
|
||||
@@ -163,25 +164,29 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
|
||||
debug!("[*]: Inserting link: {:?}", link);
|
||||
|
||||
match link.to_str() {
|
||||
Some(l) => { next_chapter.insert("link".to_owned(), l.to_json()); },
|
||||
None => return Err(RenderError{ desc: "Link could not converted to str".to_owned() })
|
||||
Some(l) => {
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
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{ desc: "Error with the handlebars template".to_owned() })
|
||||
None => return Err(RenderError::new("Error with the handlebars template")),
|
||||
}
|
||||
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
extern crate handlebars;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
@@ -14,29 +12,37 @@ pub fn render_playpen(s: &str, path: &Path) -> String {
|
||||
for playpen in find_playpens(s, path) {
|
||||
|
||||
if playpen.escaped {
|
||||
replaced.push_str(&s[previous_end_index..playpen.start_index-1]);
|
||||
replaced.push_str(&s[previous_end_index..playpen.start_index - 1]);
|
||||
replaced.push_str(&s[playpen.start_index..playpen.end_index]);
|
||||
previous_end_index = playpen.end_index;
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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());
|
||||
continue
|
||||
warn!("[-] No file exists for {{{{#playpen }}}}\n {}", playpen.rust_file.to_str().unwrap());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Open file & read file
|
||||
let mut file = if let Ok(f) = File::open(&playpen.rust_file) { f } else { continue };
|
||||
let mut file = if let Ok(f) = File::open(&playpen.rust_file) {
|
||||
f
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let mut file_content = String::new();
|
||||
if let Err(_) = file.read_to_string(&mut file_content) { continue };
|
||||
if file.read_to_string(&mut file_content).is_err() {
|
||||
continue;
|
||||
};
|
||||
|
||||
let replacement = String::new() + "<pre class=\"playpen\"><code class=\"language-rust\">" + &file_content + "</code></pre>";
|
||||
let replacement = String::new() + "<pre><code class=\"language-rust\">" + &file_content +
|
||||
"</code></pre>";
|
||||
|
||||
replaced.push_str(&s[previous_end_index..playpen.start_index]);
|
||||
replaced.push_str(&replacement);
|
||||
previous_end_index = playpen.end_index;
|
||||
//println!("Playpen{{ {}, {}, {:?}, {} }}", playpen.start_index, playpen.end_index, playpen.rust_file, playpen.editable);
|
||||
// println!("Playpen{{ {}, {}, {:?}, {} }}", playpen.start_index, playpen.end_index, playpen.rust_file,
|
||||
// playpen.editable);
|
||||
}
|
||||
|
||||
replaced.push_str(&s[previous_end_index..]);
|
||||
@@ -45,7 +51,7 @@ pub fn render_playpen(s: &str, path: &Path) -> String {
|
||||
}
|
||||
|
||||
#[derive(PartialOrd, PartialEq, Debug)]
|
||||
struct Playpen{
|
||||
struct Playpen {
|
||||
start_index: usize,
|
||||
end_index: usize,
|
||||
rust_file: PathBuf,
|
||||
@@ -61,38 +67,45 @@ fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> {
|
||||
let mut escaped = false;
|
||||
|
||||
if i > 0 {
|
||||
if let Some(c) = s[i-1..].chars().nth(0) {
|
||||
if c == '\\' { escaped = true }
|
||||
if let Some(c) = s[i - 1..].chars().nth(0) {
|
||||
if c == '\\' {
|
||||
escaped = true
|
||||
}
|
||||
}
|
||||
}
|
||||
// DON'T forget the "+ i" else you have an index out of bounds error !!
|
||||
let end_i = if let Some(n) = s[i..].find("}}") { n } else { continue } + i + 2;
|
||||
let end_i = if let Some(n) = s[i..].find("}}") {
|
||||
n
|
||||
} else {
|
||||
continue;
|
||||
} + i + 2;
|
||||
|
||||
debug!("s[{}..{}] = {}", i, end_i, s[i..end_i].to_string());
|
||||
|
||||
// If there is nothing between "{{#playpen" and "}}" skip
|
||||
if end_i-2 - (i+10) < 1 { continue }
|
||||
if s[i+10..end_i-2].trim().len() == 0 { continue }
|
||||
|
||||
debug!("{}", s[i+10..end_i-2].to_string());
|
||||
|
||||
// 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};
|
||||
if end_i - 2 - (i + 10) < 1 {
|
||||
continue;
|
||||
}
|
||||
if s[i + 10..end_i - 2].trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
playpens.push(
|
||||
Playpen{
|
||||
start_index: i,
|
||||
end_index: end_i,
|
||||
rust_file: base_path.join(PathBuf::from(params[0])),
|
||||
editable: editable,
|
||||
escaped: escaped
|
||||
}
|
||||
)
|
||||
debug!("{}", s[i + 10..end_i - 2].to_string());
|
||||
|
||||
// Split on whitespaces
|
||||
let params: Vec<&str> = s[i + 10..end_i - 2].split_whitespace().collect();
|
||||
let editable = params
|
||||
.get(1)
|
||||
.map(|p| p.find("editable").is_some())
|
||||
.unwrap_or(false);
|
||||
|
||||
playpens.push(Playpen {
|
||||
start_index: i,
|
||||
end_index: end_i,
|
||||
rust_file: base_path.join(PathBuf::from(params[0])),
|
||||
editable: editable,
|
||||
escaped: escaped,
|
||||
})
|
||||
}
|
||||
|
||||
playpens
|
||||
@@ -101,8 +114,7 @@ fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> {
|
||||
|
||||
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//
|
||||
|
||||
@@ -130,10 +142,21 @@ fn test_find_playpens_simple_playpen() {
|
||||
|
||||
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("")));
|
||||
|
||||
assert!(find_playpens(s, Path::new("")) == vec![
|
||||
Playpen{start_index: 22, end_index: 42, rust_file: PathBuf::from("file.rs"), editable: false, escaped: false},
|
||||
Playpen{start_index: 47, end_index: 68, rust_file: PathBuf::from("test.rs"), editable: false, escaped: false}
|
||||
]);
|
||||
assert!(find_playpens(s, Path::new("")) ==
|
||||
vec![Playpen {
|
||||
start_index: 22,
|
||||
end_index: 42,
|
||||
rust_file: PathBuf::from("file.rs"),
|
||||
editable: false,
|
||||
escaped: false,
|
||||
},
|
||||
Playpen {
|
||||
start_index: 47,
|
||||
end_index: 68,
|
||||
rust_file: PathBuf::from("test.rs"),
|
||||
editable: false,
|
||||
escaped: false,
|
||||
}]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -142,10 +165,21 @@ fn test_find_playpens_complex_playpen() {
|
||||
|
||||
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("dir")));
|
||||
|
||||
assert!(find_playpens(s, Path::new("dir")) == vec![
|
||||
Playpen{start_index: 22, end_index: 51, rust_file: PathBuf::from("dir/file.rs"), editable: true, escaped: false},
|
||||
Playpen{start_index: 56, end_index: 86, rust_file: PathBuf::from("dir/test.rs"), editable: true, escaped: false}
|
||||
]);
|
||||
assert!(find_playpens(s, Path::new("dir")) ==
|
||||
vec![Playpen {
|
||||
start_index: 22,
|
||||
end_index: 51,
|
||||
rust_file: PathBuf::from("dir/file.rs"),
|
||||
editable: true,
|
||||
escaped: false,
|
||||
},
|
||||
Playpen {
|
||||
start_index: 56,
|
||||
end_index: 86,
|
||||
rust_file: PathBuf::from("dir/test.rs"),
|
||||
editable: true,
|
||||
escaped: false,
|
||||
}]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -154,7 +188,8 @@ fn test_find_playpens_escaped_playpen() {
|
||||
|
||||
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("")));
|
||||
|
||||
assert!(find_playpens(s, Path::new("")) == vec![
|
||||
assert!(find_playpens(s, Path::new("")) ==
|
||||
vec![
|
||||
Playpen{start_index: 39, end_index: 68, rust_file: PathBuf::from("file.rs"), editable: true, escaped: true},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,130 +1,138 @@
|
||||
extern crate handlebars;
|
||||
extern crate rustc_serialize;
|
||||
extern crate pulldown_cmark;
|
||||
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{VecDeque, BTreeMap};
|
||||
|
||||
use self::rustc_serialize::json;
|
||||
use self::handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
|
||||
use self::pulldown_cmark::{Parser, html, Event, Tag};
|
||||
use serde_json;
|
||||
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper};
|
||||
use pulldown_cmark::{Parser, html, Event, Tag};
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
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()));
|
||||
// get value from context data
|
||||
// rc.get_path() is current json parent path, you should always use it like this
|
||||
// param is the key of value you want to display
|
||||
let chapters = rc.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();
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String, String>> = serde_json::from_str(&chapters.to_string()).unwrap();
|
||||
|
||||
let mut current_level = 1;
|
||||
let mut current_level = 1;
|
||||
|
||||
for item in decoded {
|
||||
for item in decoded {
|
||||
|
||||
// Spacer
|
||||
if let Some(_) = item.get("spacer") {
|
||||
try!(rc.writer.write("<li class=\"spacer\"></li>".as_bytes()));
|
||||
continue
|
||||
}
|
||||
|
||||
let level = if let Some(s) = item.get("section") { s.len() / 2 } else { 1 };
|
||||
|
||||
if level > current_level {
|
||||
try!(rc.writer.write("<li>".as_bytes()));
|
||||
try!(rc.writer.write("<ul class=\"section\">".as_bytes()));
|
||||
try!(rc.writer.write("<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()));
|
||||
current_level = current_level - 1;
|
||||
// Spacer
|
||||
if item.get("spacer").is_some() {
|
||||
try!(rc.writer.write_all("<li class=\"spacer\"></li>".as_bytes()));
|
||||
continue;
|
||||
}
|
||||
try!(rc.writer.write("<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(">".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()));
|
||||
let level = if let Some(s) = item.get("section") {
|
||||
s.matches(".").count()
|
||||
} else {
|
||||
1
|
||||
};
|
||||
|
||||
// Add link
|
||||
try!(rc.writer.write(
|
||||
Path::new(
|
||||
item.get("path")
|
||||
.expect("Error: path should be Some(_)")
|
||||
).with_extension("html")
|
||||
.to_str().unwrap().as_bytes()
|
||||
));
|
||||
|
||||
try!(rc.writer.write("\"".as_bytes()));
|
||||
|
||||
if path == ¤t {
|
||||
try!(rc.writer.write(" class=\"active\"".as_bytes()));
|
||||
if level > current_level {
|
||||
while level > current_level {
|
||||
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_all("<li>".as_bytes()));
|
||||
} else if level < current_level {
|
||||
while level < current_level {
|
||||
try!(rc.writer.write_all("</ul>".as_bytes()));
|
||||
try!(rc.writer.write_all("</li>".as_bytes()));
|
||||
current_level -= 1;
|
||||
}
|
||||
try!(rc.writer.write_all("<li>".as_bytes()));
|
||||
} else {
|
||||
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_all(">".as_bytes()));
|
||||
}
|
||||
|
||||
try!(rc.writer.write(">".as_bytes()));
|
||||
true
|
||||
// Link
|
||||
let path_exists = if let Some(path) = item.get("path") {
|
||||
if !path.is_empty() {
|
||||
try!(rc.writer.write_all("<a href=\"".as_bytes()));
|
||||
|
||||
// Add link
|
||||
try!(rc.writer.write_all(Path::new(item.get("path")
|
||||
.expect("Error: path should be Some(_)"))
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
.replace("\\", "/")
|
||||
.as_bytes()));
|
||||
|
||||
try!(rc.writer.write_all("\"".as_bytes()));
|
||||
|
||||
if path == ¤t {
|
||||
try!(rc.writer.write_all(" class=\"active\"".as_bytes()));
|
||||
}
|
||||
|
||||
try!(rc.writer.write_all(">".as_bytes()));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Section does not necessarily exist
|
||||
if let Some(section) = item.get("section") {
|
||||
try!(rc.writer.write_all("<strong>".as_bytes()));
|
||||
try!(rc.writer.write_all(section.as_bytes()));
|
||||
try!(rc.writer.write_all("</strong> ".as_bytes()));
|
||||
}
|
||||
}else {
|
||||
false
|
||||
};
|
||||
|
||||
// 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()));
|
||||
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) |
|
||||
Event::InlineHtml(_) |
|
||||
Event::Text(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
|
||||
// render markdown to html
|
||||
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
|
||||
html::push_html(&mut markdown_parsed_name, parser);
|
||||
|
||||
// write to the handlebars template
|
||||
try!(rc.writer.write_all(markdown_parsed_name.as_bytes()));
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
try!(rc.writer.write_all("</a>".as_bytes()));
|
||||
}
|
||||
|
||||
try!(rc.writer.write_all("</li>".as_bytes()));
|
||||
|
||||
}
|
||||
while current_level > 1 {
|
||||
try!(rc.writer.write_all("</ul>".as_bytes()));
|
||||
try!(rc.writer.write_all("</li>".as_bytes()));
|
||||
current_level -= 1;
|
||||
}
|
||||
|
||||
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,
|
||||
_ => false,
|
||||
}
|
||||
});
|
||||
|
||||
// render markdown to html
|
||||
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
|
||||
html::push_html(&mut markdown_parsed_name, parser);
|
||||
|
||||
// write to the handlebars template
|
||||
try!(rc.writer.write(markdown_parsed_name.as_bytes()));
|
||||
}
|
||||
|
||||
if path_exists {
|
||||
try!(rc.writer.write("</a>".as_bytes()));
|
||||
}
|
||||
|
||||
try!(rc.writer.write("</li>".as_bytes()));
|
||||
|
||||
current_level = level;
|
||||
try!(rc.writer.write_all("</ul>".as_bytes()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
try!(rc.writer.write("</ul>".as_bytes()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
pub use self::renderer::Renderer;
|
||||
pub use self::html_handlebars::HtmlHandlebars;
|
||||
|
||||
pub mod renderer;
|
||||
mod html_handlebars;
|
||||
|
||||
use std::error::Error;
|
||||
|
||||
pub trait Renderer {
|
||||
fn render(&self, book: &::book::MDBook) -> Result<(), Box<Error>>;
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
use std::error::Error;
|
||||
|
||||
pub trait Renderer {
|
||||
fn render(&self, book: &::book::MDBook) -> Result<(), Box<Error>>;
|
||||
}
|
||||
@@ -3,6 +3,10 @@ body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
color: #333;
|
||||
}
|
||||
code {
|
||||
font-family: "Source Code Pro", "Menlo", "DejaVu Sans Mono", monospace;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
.left {
|
||||
float: left;
|
||||
}
|
||||
@@ -72,7 +76,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 +93,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 +268,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,3 +765,56 @@ table thead td {
|
||||
.rust pre > .result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@media only print {
|
||||
#sidebar,
|
||||
#menu-bar,
|
||||
.nav-chapters,
|
||||
.mobile-nav-chapters {
|
||||
display: none;
|
||||
}
|
||||
#page-wrapper {
|
||||
left: 0;
|
||||
overflow-y: initial;
|
||||
}
|
||||
#content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.page {
|
||||
overflow-y: initial;
|
||||
}
|
||||
code {
|
||||
background-color: #666;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact;
|
||||
}
|
||||
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,
|
||||
@@ -29,6 +33,7 @@ $( document ).ready(function() {
|
||||
};
|
||||
|
||||
$(document).on('keydown', function (e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
switch (e.keyCode) {
|
||||
case KEY_CODES.NEXT_KEY:
|
||||
e.preventDefault();
|
||||
@@ -51,17 +56,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...
|
||||
wrapper.attr("href", $(location).attr('href') + "#" + $(this).text());
|
||||
return wrapper;
|
||||
});
|
||||
|
||||
|
||||
// Toggle sidebar
|
||||
$("#sidebar-toggle").click(function(event){
|
||||
if ( html.hasClass("sidebar-hidden") ) {
|
||||
@@ -214,6 +208,18 @@ function run_rust_code(code_block) {
|
||||
result_block = code_block.find(".result");
|
||||
}
|
||||
|
||||
let text = code_block.find(".language-rust").text();
|
||||
|
||||
let params = {
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
code: text,
|
||||
};
|
||||
|
||||
if(text.includes("#![feature")) {
|
||||
params.version = "nightly";
|
||||
}
|
||||
|
||||
result_block.text("Running...");
|
||||
|
||||
$.ajax({
|
||||
@@ -222,7 +228,7 @@ function run_rust_code(code_block) {
|
||||
crossDomain: true,
|
||||
dataType: "json",
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({version: "stable", optimize: "0", code: code_block.find(".language-rust").text() }),
|
||||
data: JSON.stringify(params),
|
||||
success: function(response){
|
||||
result_block.text(response.result);
|
||||
}
|
||||
|
||||
@@ -12,96 +12,59 @@
|
||||
|
||||
|
||||
/* Atelier-Dune Comment */
|
||||
.hljs-comment {
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #AAA;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Red */
|
||||
.hljs-variable,
|
||||
|
||||
.hljs-template-variable,
|
||||
.hljs-attribute,
|
||||
.hljs-tag,
|
||||
.hljs-regexp,
|
||||
.hljs-name,
|
||||
.ruby .hljs-constant,
|
||||
.xml .hljs-tag .hljs-title,
|
||||
.xml .hljs-pi,
|
||||
.xml .hljs-doctype,
|
||||
.html .hljs-doctype,
|
||||
.css .hljs-id,
|
||||
.css .hljs-class,
|
||||
.css .hljs-pseudo {
|
||||
.hljs-regexp,
|
||||
.hljs-link,
|
||||
.hljs-name,
|
||||
.hljs-selector-id,
|
||||
.hljs-selector-class {
|
||||
color: #d73737;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Orange */
|
||||
.hljs-number,
|
||||
.hljs-preprocessor,
|
||||
.hljs-meta,
|
||||
.hljs-built_in,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-params,
|
||||
.hljs-attribute,
|
||||
.hljs-constant {
|
||||
.hljs-type,
|
||||
.hljs-params {
|
||||
color: #b65611;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Yellow */
|
||||
.ruby .hljs-class .hljs-title,
|
||||
.css .hljs-rule .hljs-attribute {
|
||||
color: #ae9513;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Green */
|
||||
.hljs-string,
|
||||
.hljs-value,
|
||||
.hljs-inheritance,
|
||||
.ruby .hljs-symbol,
|
||||
.xml .hljs-cdata {
|
||||
color: #2a9292;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Aqua */
|
||||
.hljs-title,
|
||||
.css .hljs-hexcolor {
|
||||
color: #1fad83;
|
||||
.hljs-symbol,
|
||||
.hljs-bullet {
|
||||
color: #60ac39;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Blue */
|
||||
.hljs-function,
|
||||
.python .hljs-decorator,
|
||||
.python .hljs-title,
|
||||
.ruby .hljs-function .hljs-title,
|
||||
.ruby .hljs-title .hljs-keyword,
|
||||
.perl .hljs-sub,
|
||||
.javascript .hljs-title,
|
||||
.coffeescript .hljs-title {
|
||||
.hljs-title,
|
||||
.hljs-section {
|
||||
color: #6684e1;
|
||||
}
|
||||
|
||||
/* Atelier-Dune Purple */
|
||||
.hljs-keyword,
|
||||
.javascript .hljs-function {
|
||||
.hljs-selector-tag {
|
||||
color: #b854d4;
|
||||
}
|
||||
|
||||
.coffeescript .javascript,
|
||||
.javascript .xml,
|
||||
.tex .hljs-formula,
|
||||
.xml .javascript,
|
||||
.xml .vbscript,
|
||||
.xml .css,
|
||||
.xml .hljs-cdata {
|
||||
opacity: 0.5;
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* markdown */
|
||||
.hljs-header {
|
||||
color: #A30000;
|
||||
}
|
||||
|
||||
.hljs-link_label {
|
||||
color: #33CCCC;
|
||||
}
|
||||
|
||||
.hljs-link_url {
|
||||
color: #CC66FF;
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
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">
|
||||
@@ -10,12 +10,13 @@
|
||||
<base href="{{ path_to_root }}">
|
||||
|
||||
<link rel="stylesheet" href="book.css">
|
||||
<link href='http://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800' rel='stylesheet' type='text/css'>
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
|
||||
|
||||
<link rel="shortcut icon" href="{{ favicon }}">
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
|
||||
|
||||
<link rel="stylesheet" href="highlight.css">
|
||||
<link rel="stylesheet" href="tomorrow-night.css">
|
||||
@@ -24,14 +25,14 @@
|
||||
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
|
||||
<!-- Fetch JQuery from CDN but have a local fallback -->
|
||||
<script src="http://code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
<script>
|
||||
if (typeof jQuery == 'undefined') {
|
||||
document.write(unescape("%3Cscript src='jquery.js'%3E%3C/script%3E"));
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<body class="light">
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
<script type="text/javascript">
|
||||
var theme = localStorage.getItem('theme');
|
||||
@@ -107,6 +108,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
{{{livereload}}}
|
||||
|
||||
<script src="highlight.js"></script>
|
||||
<script src="book.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -53,13 +53,7 @@ impl Theme {
|
||||
|
||||
// Check if the given path exists
|
||||
if !src.exists() || !src.is_dir() {
|
||||
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
|
||||
return theme;
|
||||
}
|
||||
|
||||
// Check for individual files if they exist
|
||||
@@ -73,7 +67,7 @@ impl Theme {
|
||||
// book.js
|
||||
if let Ok(mut f) = File::open(&src.join("book.js")) {
|
||||
theme.js.clear();
|
||||
let _ = f.read_to_end(&mut theme.js);
|
||||
let _ = f.read_to_end(&mut theme.js);
|
||||
}
|
||||
|
||||
// book.css
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
@import 'nav-icons'
|
||||
@import 'theme-popup'
|
||||
@import 'themes'
|
||||
@import 'print'
|
||||
|
||||
@@ -3,6 +3,11 @@ html, body {
|
||||
color: #333
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Source Code Pro", "Menlo", "DejaVu Sans Mono", monospace;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.left {
|
||||
float: left
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
230
src/utils/fs.rs
Normal file
230
src/utils/fs.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use std::path::{Path, Component};
|
||||
use std::error::Error;
|
||||
use std::io::{self, Read};
|
||||
use std::fs::{self, File};
|
||||
|
||||
/// Takes a path to a file and try to read the file into a String
|
||||
|
||||
pub fn file_to_string(path: &Path) -> Result<String, Box<Error>> {
|
||||
let mut file = match File::open(path) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
debug!("[*]: Failed to open {:?}", path);
|
||||
return Err(Box::new(e));
|
||||
},
|
||||
};
|
||||
|
||||
let mut content = String::new();
|
||||
|
||||
if let Err(e) = file.read_to_string(&mut content) {
|
||||
debug!("[*]: Failed to read {:?}", path);
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
/// Takes a path and returns a path containing just enough `../` to point to the root of the given path.
|
||||
///
|
||||
/// This is mostly interesting for a relative path to point back to the directory from where the
|
||||
/// path starts.
|
||||
///
|
||||
/// ```ignore
|
||||
/// let mut path = Path::new("some/relative/path");
|
||||
///
|
||||
/// println!("{}", path_to_root(&path));
|
||||
/// ```
|
||||
///
|
||||
/// **Outputs**
|
||||
///
|
||||
/// ```text
|
||||
/// "../../"
|
||||
/// ```
|
||||
///
|
||||
/// **note:** it's not very fool-proof, if you find a situation where it doesn't return the correct
|
||||
/// path. Consider [submitting a new issue](https://github.com/azerupi/mdBook/issues) or a
|
||||
/// [pull-request](https://github.com/azerupi/mdBook/pulls) to improve it.
|
||||
|
||||
pub fn path_to_root(path: &Path) -> String {
|
||||
debug!("[fn]: path_to_root");
|
||||
// Remove filename and add "../" for every directory
|
||||
|
||||
path.to_path_buf()
|
||||
.parent()
|
||||
.expect("")
|
||||
.components()
|
||||
.fold(String::new(), |mut s, c| {
|
||||
match c {
|
||||
Component::Normal(_) => s.push_str("../"),
|
||||
_ => {
|
||||
debug!("[*]: Other path component... {:?}", c);
|
||||
},
|
||||
}
|
||||
s
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 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) -> io::Result<File> {
|
||||
debug!("[fn]: create_file");
|
||||
|
||||
// Construct path
|
||||
if let Some(p) = path.parent() {
|
||||
debug!("Parent directory is: {:?}", p);
|
||||
|
||||
try!(fs::create_dir_all(p));
|
||||
}
|
||||
|
||||
debug!("[*]: Create file: {:?}", path);
|
||||
File::create(path)
|
||||
}
|
||||
|
||||
/// Removes all the content of a directory but not the directory itself
|
||||
|
||||
pub fn remove_dir_content(dir: &Path) -> Result<(), Box<Error>> {
|
||||
for item in try!(fs::read_dir(dir)) {
|
||||
if let Ok(item) = item {
|
||||
let item = item.path();
|
||||
if item.is_dir() {
|
||||
try!(fs::remove_dir_all(item));
|
||||
} else {
|
||||
try!(fs::remove_file(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// Copies all files of a directory to another one except the files with the extensions given in the
|
||||
/// `ext_blacklist` array
|
||||
|
||||
pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blacklist: &[&str]) -> Result<(), Box<Error>> {
|
||||
debug!("[fn] copy_files_except_ext");
|
||||
// Check that from and to are different
|
||||
if from == to {
|
||||
return Ok(());
|
||||
}
|
||||
debug!("[*] Loop");
|
||||
for entry in try!(fs::read_dir(from)) {
|
||||
let entry = try!(entry);
|
||||
debug!("[*] {:?}", entry.path());
|
||||
let metadata = try!(entry.metadata());
|
||||
|
||||
// If the entry is a dir and the recursive option is enabled, call itself
|
||||
if metadata.is_dir() && recursive {
|
||||
if entry.path() == to.to_path_buf() {
|
||||
continue;
|
||||
}
|
||||
debug!("[*] is dir");
|
||||
|
||||
// check if output dir already exists
|
||||
if !to.join(entry.file_name()).exists() {
|
||||
try!(fs::create_dir(&to.join(entry.file_name())));
|
||||
}
|
||||
|
||||
try!(copy_files_except_ext(&from.join(entry.file_name()),
|
||||
&to.join(entry.file_name()),
|
||||
true,
|
||||
ext_blacklist));
|
||||
} else if metadata.is_file() {
|
||||
|
||||
// Check if it is in the blacklist
|
||||
if let Some(ext) = entry.path().extension() {
|
||||
if ext_blacklist.contains(&ext.to_str().unwrap()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
// tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate tempdir;
|
||||
|
||||
use super::copy_files_except_ext;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn copy_files_except_ext_test() {
|
||||
let tmp = match tempdir::TempDir::new("") {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic!("Could not create a temp dir"),
|
||||
};
|
||||
|
||||
// Create a couple of files
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.txt")) {
|
||||
panic!("Could not create file.txt")
|
||||
}
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.md")) {
|
||||
panic!("Could not create file.md")
|
||||
}
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.png")) {
|
||||
panic!("Could not create file.png")
|
||||
}
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir")) {
|
||||
panic!("Could not create sub_dir")
|
||||
}
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir/file.png")) {
|
||||
panic!("Could not create sub_dir/file.png")
|
||||
}
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir_exists")) {
|
||||
panic!("Could not create sub_dir_exists")
|
||||
}
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) {
|
||||
panic!("Could not create sub_dir_exists/file.txt")
|
||||
}
|
||||
|
||||
// Create output dir
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("output")) {
|
||||
panic!("Could not create output")
|
||||
}
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) {
|
||||
panic!("Could not create output/sub_dir_exists")
|
||||
}
|
||||
|
||||
match copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"]) {
|
||||
Err(e) => panic!("Error while executing the function:\n{:?}", e),
|
||||
Ok(_) => {},
|
||||
}
|
||||
|
||||
// Check if the correct files where created
|
||||
if !(&tmp.path().join("output/file.txt")).exists() {
|
||||
panic!("output/file.txt should exist")
|
||||
}
|
||||
if (&tmp.path().join("output/file.md")).exists() {
|
||||
panic!("output/file.md should not exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/file.png")).exists() {
|
||||
panic!("output/file.png should exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/sub_dir/file.png")).exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
182
src/utils/mod.rs
182
src/utils/mod.rs
@@ -1,133 +1,6 @@
|
||||
extern crate pulldown_cmark;
|
||||
pub mod fs;
|
||||
|
||||
use std::path::{Path, Component};
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::fs::{self, metadata, File};
|
||||
|
||||
use self::pulldown_cmark::{Parser, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
|
||||
|
||||
/// Takes a path and returns a path containing just enough `../` to point to the root of the given path.
|
||||
///
|
||||
/// This is mostly interesting for a relative path to point back to the directory from where the
|
||||
/// path starts.
|
||||
///
|
||||
/// ```ignore
|
||||
/// let mut path = Path::new("some/relative/path");
|
||||
///
|
||||
/// println!("{}", path_to_root(&path));
|
||||
/// ```
|
||||
///
|
||||
/// **Outputs**
|
||||
///
|
||||
/// ```text
|
||||
/// "../../"
|
||||
/// ```
|
||||
///
|
||||
/// **note:** it's not very fool-proof, if you find a situation where it doesn't return the correct
|
||||
/// path. Consider [submitting a new issue](https://github.com/azerupi/mdBook/issues) or a
|
||||
/// [pull-request](https://github.com/azerupi/mdBook/pulls) to improve it.
|
||||
|
||||
pub fn path_to_root(path: &Path) -> String {
|
||||
debug!("[fn]: path_to_root");
|
||||
// Remove filename and add "../" for every directory
|
||||
|
||||
path.to_path_buf().parent().expect("")
|
||||
.components().fold(String::new(), |mut s, c| {
|
||||
match c {
|
||||
Component::Normal(_) => s.push_str("../"),
|
||||
_ => {
|
||||
debug!("[*]: Other path component... {:?}", c);
|
||||
}
|
||||
}
|
||||
s
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 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>> {
|
||||
debug!("[fn]: create_file");
|
||||
|
||||
// Construct path
|
||||
if let Some(p) = path.parent() {
|
||||
debug!("Parent directory is: {:?}", p);
|
||||
|
||||
try!(fs::create_dir_all(p));
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Removes all the content of a directory but not the directory itself
|
||||
|
||||
pub fn remove_dir_content(dir: &Path) -> Result<(), Box<Error>> {
|
||||
for item in try!(fs::read_dir(dir)) {
|
||||
if let Ok(item) = item {
|
||||
let item = item.path();
|
||||
if item.is_dir() { try!(fs::remove_dir_all(item)); } else { try!(fs::remove_file(item)); }
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// Copies all files of a directory to another one except the files with the extensions given in the
|
||||
/// `ext_blacklist` array
|
||||
|
||||
pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blacklist: &[&str]) -> Result<(), Box<Error>> {
|
||||
debug!("[fn] copy_files_except_ext");
|
||||
// Check that from and to are different
|
||||
if from == to { return Ok(()) }
|
||||
debug!("[*] Loop");
|
||||
for entry in try!(fs::read_dir(from)) {
|
||||
let entry = try!(entry);
|
||||
debug!("[*] {:?}", entry.path());
|
||||
let metadata = try!(entry.metadata());
|
||||
|
||||
// If the entry is a dir and the recursive option is enabled, call itself
|
||||
if metadata.is_dir() && recursive {
|
||||
if entry.path() == to.to_path_buf() { continue }
|
||||
debug!("[*] is dir");
|
||||
|
||||
// check if output dir already exists
|
||||
if !to.join(entry.file_name()).exists() {
|
||||
try!(fs::create_dir(&to.join(entry.file_name())));
|
||||
}
|
||||
|
||||
try!(copy_files_except_ext(
|
||||
&from.join(entry.file_name()),
|
||||
&to.join(entry.file_name()),
|
||||
true,
|
||||
ext_blacklist
|
||||
));
|
||||
} else if metadata.is_file() {
|
||||
|
||||
// Check if it is in the blacklist
|
||||
if let Some(ext) = entry.path().extension() {
|
||||
if ext_blacklist.contains(&ext.to_str().unwrap()) { 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..."))));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
use pulldown_cmark::{Parser, html, Options, OPTION_ENABLE_TABLES, OPTION_ENABLE_FOOTNOTES};
|
||||
|
||||
|
||||
///
|
||||
@@ -141,56 +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
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
// tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
extern crate tempdir;
|
||||
|
||||
use super::copy_files_except_ext;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn copy_files_except_ext_test() {
|
||||
let tmp = match tempdir::TempDir::new("") {
|
||||
Ok(t) => t,
|
||||
Err(_) => panic!("Could not create a temp dir"),
|
||||
};
|
||||
|
||||
// Create a couple of files
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.txt")) { panic!("Could not create file.txt") }
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.md")) { panic!("Could not create file.md") }
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("file.png")) { panic!("Could not create file.png") }
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir")) { panic!("Could not create sub_dir") }
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir/file.png")) { panic!("Could not create sub_dir/file.png") }
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("sub_dir_exists")) { panic!("Could not create sub_dir_exists") }
|
||||
if let Err(_) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) { panic!("Could not create sub_dir_exists/file.txt") }
|
||||
|
||||
// Create output dir
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("output")) { panic!("Could not create output") }
|
||||
if let Err(_) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) { panic!("Could not create output/sub_dir_exists") }
|
||||
|
||||
match copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"]) {
|
||||
Err(e) => panic!("Error while executing the function:\n{:?}", e),
|
||||
Ok(_) => {},
|
||||
}
|
||||
|
||||
// Check if the correct files where created
|
||||
if !(&tmp.path().join("output/file.txt")).exists() { panic!("output/file.txt should exist") }
|
||||
if (&tmp.path().join("output/file.md")).exists() { panic!("output/file.md should not exist") }
|
||||
if !(&tmp.path().join("output/file.png")).exists() { panic!("output/file.png should exist") }
|
||||
if !(&tmp.path().join("output/sub_dir/file.png")).exists() { panic!("output/sub_dir/file.png should exist") }
|
||||
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() { panic!("output/sub_dir/file.png should exist") }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user