mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 15:01:45 -05:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
80deac90d9 | ||
|
|
625f5081fa | ||
|
|
1eb59428e6 | ||
|
|
3e8151e8e3 | ||
|
|
3c10a85735 | ||
|
|
330b1ad55d | ||
|
|
596455f28c | ||
|
|
f24eb59753 | ||
|
|
01c5085725 | ||
|
|
9f17be2c32 | ||
|
|
88fabd76f0 | ||
|
|
f508db6113 | ||
|
|
1083d1822d | ||
|
|
fc86b963bb | ||
|
|
f2b913c9dd | ||
|
|
dd0cfc14d4 |
108
.travis.yml
108
.travis.yml
@@ -1,20 +1,96 @@
|
||||
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
|
||||
- npm
|
||||
# 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
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
26
Cargo.toml
26
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.0.8"
|
||||
version = "0.0.12"
|
||||
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,34 @@ exclude = [
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
clap = "~1.5.3"
|
||||
handlebars = "~0.12.0"
|
||||
rustc-serialize = "~0.3.16"
|
||||
pulldown-cmark = "~0.0.6"
|
||||
clap = "2.2.1"
|
||||
handlebars = "0.20.0"
|
||||
rustc-serialize = "0.3.18"
|
||||
pulldown-cmark = "0.0.8"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "~2.4.1", optional = true }
|
||||
time = { version = "~0.1.33", optional = true }
|
||||
crossbeam = { version = "~0.2.8", optional = true }
|
||||
notify = { version = "2.5.5", optional = true }
|
||||
time = { version = "0.1.34", optional = true }
|
||||
crossbeam = { version = "0.2.8", optional = true }
|
||||
|
||||
# Serve feature
|
||||
iron = { version = "0.4", optional = true }
|
||||
staticfile = { version = "0.3", optional = true }
|
||||
ws = { version = "0.5.1", optional = true}
|
||||
|
||||
|
||||
# Tests
|
||||
[dev-dependencies]
|
||||
tempdir = "~0.3.4"
|
||||
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
|
||||
|
||||
77
README.md
77
README.md
@@ -1,9 +1,30 @@
|
||||
# mdBook [](https://travis-ci.org/azerupi/mdBook) [](https://crates.io/crates/mdbook) [](LICENSE)
|
||||
# mdBook
|
||||
|
||||
<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>
|
||||
|
||||
mdBook is a utility to create modern online books from markdown files.
|
||||
|
||||
**This project is still in its early days.**
|
||||
For more information about what is left on my to-do list, check the issue tracker
|
||||
**This project is still evolving.**
|
||||
For more information, check the issue tracker.
|
||||
|
||||
|
||||
## What does it look like?
|
||||
@@ -12,34 +33,40 @@ The [**Documentation**](http://azerupi.github.io/mdBook/) for mdBook has been wr
|
||||
|
||||
## 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`
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +100,10 @@ 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 occures.
|
||||
|
||||
### 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!
|
||||
@@ -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.
|
||||
|
||||
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,4 +1,5 @@
|
||||
{
|
||||
"title": "mdBook Documentation",
|
||||
"description": "Create book from markdown files. Like Gitbook but implemented in Rust",
|
||||
"author": "Mathieu David"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ Here is an example of what a ***book.json*** file might look like:
|
||||
{
|
||||
"title": "Example book",
|
||||
"author": "Name",
|
||||
"description": "The example book covers examples.",
|
||||
"dest": "output/my-book"
|
||||
}
|
||||
```
|
||||
@@ -16,6 +17,7 @@ Here is an example of what a ***book.json*** file might look like:
|
||||
|
||||
- **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
|
||||
|
||||
***note:*** *the supported configurable parameters are scarce at the moment, but more will be added in the future*
|
||||
|
||||
@@ -14,6 +14,7 @@ Here are the files you can override:
|
||||
- ***book.js*** is mostly used to add client side functionality, like hiding / un-hiding the sidebar, changing the theme, ...
|
||||
- ***highlight.js*** is the JavaScript that is used to highlight code snippets, you should not need to modify this.
|
||||
- ***highlight.css*** is the theme used for the code highlighting
|
||||
- ***favicon.png*** the favicon that will be used
|
||||
|
||||
Generally, when you want to tweak the theme, you don't need to override all the files. If you only need changes in the stylesheet,
|
||||
there is no point in overriding all the other files. Because custom files take precedence over built-in ones, they will not get updated with new fixes / features.
|
||||
|
||||
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"
|
||||
@@ -2,21 +2,29 @@
|
||||
extern crate mdbook;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate crossbeam;
|
||||
|
||||
// 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::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")]
|
||||
@@ -36,8 +44,8 @@ fn main() {
|
||||
.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
|
||||
@@ -50,18 +58,25 @@ fn main() {
|
||||
.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)'"))
|
||||
.subcommand(SubCommand::with_name("serve")
|
||||
.about("Serve the book at http://localhost:3000. Rebuild and reload on change.")
|
||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'")
|
||||
.arg_from_usage("-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)'"))
|
||||
.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 {
|
||||
@@ -77,7 +92,7 @@ fn confirm() -> bool {
|
||||
io::stdin().read_line(&mut s).ok();
|
||||
match &*s.trim() {
|
||||
"Y" | "y" | "yes" | "Yes" => true,
|
||||
_ => false
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +109,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 +130,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(())
|
||||
@@ -136,70 +163,85 @@ fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
#[cfg(feature = "watch")]
|
||||
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::new(&book_dir).read_config();
|
||||
let mut book = MDBook::new(&book_dir).read_config();
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
trigger_on_change(&mut book, |event, book| {
|
||||
if let Some(path) = event.path {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
match book.build() {
|
||||
Err(e) => println!("Error while building: {:?}", e),
|
||||
_ => {},
|
||||
}
|
||||
println!("");
|
||||
}
|
||||
});
|
||||
|
||||
let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
match w {
|
||||
Ok(mut watcher) => {
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.get_src()) {
|
||||
println!("Error while watching {:?}:\n {:?}", book.get_src(), e);
|
||||
::std::process::exit(0);
|
||||
};
|
||||
// Watch command implementation
|
||||
#[cfg(feature = "serve")]
|
||||
fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
const RELOAD_COMMAND: &'static str = "reload";
|
||||
|
||||
// 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 book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::new(&book_dir).read_config();
|
||||
let port = args.value_of("port").unwrap_or("3000");
|
||||
let ws_port = args.value_of("ws-port").unwrap_or("3001");
|
||||
|
||||
let previous_time = time::get_time().sec;
|
||||
let address = format!("localhost:{}", port);
|
||||
let ws_address = format!("localhost:{}", ws_port);
|
||||
|
||||
crossbeam::scope(|scope| {
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(event) => {
|
||||
book.set_livereload(format!(r#"
|
||||
<script type="text/javascript">
|
||||
var socket = new WebSocket("ws://localhost:{}");
|
||||
socket.onmessage = function (event) {{
|
||||
if (event.data === "{}") {{
|
||||
socket.close();
|
||||
location.reload(true); // force reload from server (not from cache)
|
||||
}}
|
||||
}};
|
||||
|
||||
// Skip the event if an event has already been issued in the last second
|
||||
if time::get_time().sec - previous_time < 1 { continue }
|
||||
window.onbeforeunload = function() {{
|
||||
socket.close();
|
||||
}}
|
||||
</script>
|
||||
"#, ws_port, RELOAD_COMMAND).to_owned());
|
||||
|
||||
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!("");
|
||||
});
|
||||
try!(book.build());
|
||||
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("An error occured: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let staticfile = staticfile::Static::new(book.get_dest());
|
||||
let iron = iron::Iron::new(staticfile);
|
||||
let _iron = iron.http(&*address).unwrap();
|
||||
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
::std::process::exit(0);
|
||||
}
|
||||
}
|
||||
let ws_server = ws::WebSocket::new(|_| {
|
||||
|_| {
|
||||
Ok(())
|
||||
}
|
||||
}).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
let broadcaster = ws_server.broadcaster();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
ws_server.listen(&*ws_address).unwrap();
|
||||
});
|
||||
|
||||
println!("\nServing on {}", address);
|
||||
|
||||
trigger_on_change(&mut book, move |event, book| {
|
||||
if let Some(path) = event.path {
|
||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||
match book.build() {
|
||||
Err(e) => println!("Error while building: {:?}", e),
|
||||
_ => broadcaster.send(RELOAD_COMMAND).unwrap(),
|
||||
}
|
||||
println!("");
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||
@@ -212,17 +254,71 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Calls the closure when a book source file is changed. This is blocking!
|
||||
#[cfg(feature = "watch")]
|
||||
fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
||||
where F: Fn(notify::Event, &mut MDBook) -> ()
|
||||
{
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);
|
||||
|
||||
match w {
|
||||
Ok(mut watcher) => {
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.get_src()) {
|
||||
println!("Error while watching {:?}:\n {:?}", book.get_src(), e);
|
||||
::std::process::exit(0);
|
||||
};
|
||||
|
||||
// Add the book.json file to the watcher if it exists, because it's not
|
||||
// located in the source directory
|
||||
if let Err(_) = watcher.watch(book.get_root().join("book.json")) {
|
||||
// do nothing if book.json is not found
|
||||
}
|
||||
|
||||
let mut previous_time = time::get_time();
|
||||
|
||||
println!("\nListening for changes...\n");
|
||||
|
||||
loop {
|
||||
match rx.recv() {
|
||||
Ok(event) => {
|
||||
// Skip the event if an event has already been issued in the last second
|
||||
let time = time::get_time();
|
||||
if time - previous_time < time::Duration::seconds(1) {
|
||||
continue;
|
||||
} else {
|
||||
previous_time = time;
|
||||
}
|
||||
|
||||
closure(event, book);
|
||||
},
|
||||
Err(e) => {
|
||||
println!("An error occured: {:?}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
println!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
::std::process::exit(0);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
extern crate rustc_serialize;
|
||||
use self::rustc_serialize::json::Json;
|
||||
use rustc_serialize::json::Json;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -8,9 +7,10 @@ use std::path::{Path, PathBuf};
|
||||
pub struct BookConfig {
|
||||
pub title: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
root: PathBuf,
|
||||
dest: PathBuf,
|
||||
src: PathBuf,
|
||||
pub dest: PathBuf,
|
||||
pub src: PathBuf,
|
||||
pub indent_spaces: i32,
|
||||
multilingual: bool,
|
||||
}
|
||||
@@ -21,10 +21,11 @@ impl BookConfig {
|
||||
BookConfig {
|
||||
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
|
||||
dest: root.join("book"),
|
||||
src: root.join("src"),
|
||||
indent_spaces: 4, // indentation used for SUMMARY.md
|
||||
multilingual: false,
|
||||
}
|
||||
}
|
||||
@@ -38,7 +39,7 @@ impl BookConfig {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
debug!("[*]: Failed to open {:?}", root.join("book.json"));
|
||||
return self
|
||||
return self;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -47,16 +48,25 @@ impl BookConfig {
|
||||
|
||||
// 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 let Err(_) = config_file.read_to_string(&mut data) {
|
||||
return self;
|
||||
}
|
||||
|
||||
// Convert to JSON
|
||||
if let Ok(config) = Json::from_str(&data) {
|
||||
// Extract data
|
||||
|
||||
debug!("[*]: Extracting data from config");
|
||||
// Title & author
|
||||
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("\"", "") }
|
||||
// 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("\"", "")
|
||||
}
|
||||
|
||||
// Destination
|
||||
if let Some(a) = config.find_path(&["dest"]) {
|
||||
@@ -68,7 +78,9 @@ impl BookConfig {
|
||||
let dest = self.get_root().join(&dest).to_owned();
|
||||
self.set_dest(&dest);
|
||||
},
|
||||
false => { self.set_dest(&dest); },
|
||||
false => {
|
||||
self.set_dest(&dest);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,5 +114,4 @@ impl BookConfig {
|
||||
self.src = src.to_owned();
|
||||
self
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use self::rustc_serialize::json::{Json, ToJson};
|
||||
use rustc_serialize::json::{Json, ToJson};
|
||||
use std::path::PathBuf;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@@ -27,7 +25,6 @@ pub struct BookItems<'a> {
|
||||
|
||||
|
||||
impl Chapter {
|
||||
|
||||
pub fn new(name: String, path: PathBuf) -> Self {
|
||||
|
||||
Chapter {
|
||||
@@ -40,13 +37,14 @@ 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.insert("path".to_owned(),
|
||||
self.path
|
||||
.to_str()
|
||||
.expect("Json conversion failed for path")
|
||||
.to_json());
|
||||
m.to_json()
|
||||
}
|
||||
}
|
||||
@@ -66,7 +64,7 @@ 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();
|
||||
@@ -79,10 +77,10 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
},
|
||||
BookItem::Spacer => {
|
||||
self.current_index += 1;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return Some(cur)
|
||||
return Some(cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,357 +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));
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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(())
|
||||
}
|
||||
|
||||
}
|
||||
450
src/book/mod.rs
450
src/book/mod.rs
@@ -1,7 +1,453 @@
|
||||
pub mod mdbook;
|
||||
pub mod bookitem;
|
||||
pub mod bookconfig;
|
||||
|
||||
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,
|
||||
|
||||
pub title: String,
|
||||
pub author: String,
|
||||
pub description: String,
|
||||
|
||||
pub content: Vec<BookItem>,
|
||||
renderer: Box<Renderer>,
|
||||
|
||||
livereload: Option<String>,
|
||||
}
|
||||
|
||||
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 {
|
||||
root: root.to_owned(),
|
||||
dest: root.join("book"),
|
||||
src: root.join("src"),
|
||||
|
||||
title: String::new(),
|
||||
author: String::new(),
|
||||
description: String::new(),
|
||||
|
||||
content: vec![],
|
||||
renderer: Box::new(HtmlHandlebars::new()),
|
||||
|
||||
livereload: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
output!("{:?} 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);
|
||||
match *item {
|
||||
BookItem::Spacer => continue,
|
||||
BookItem::Chapter(_, ref ch) |
|
||||
BookItem::Affix(ref ch) => {
|
||||
if ch.path != PathBuf::new() {
|
||||
let path = self.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(())
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 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 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
|
||||
}
|
||||
|
||||
/// 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 get_root(&self) -> &Path {
|
||||
&self.root
|
||||
}
|
||||
|
||||
pub fn set_dest(mut self, dest: &Path) -> Self {
|
||||
|
||||
// Handle absolute and relative paths
|
||||
match dest.is_absolute() {
|
||||
true => {
|
||||
self.dest = dest.to_owned();
|
||||
},
|
||||
false => {
|
||||
let dest = self.root.join(dest).to_owned();
|
||||
self.dest = dest;
|
||||
},
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_dest(&self) -> &Path {
|
||||
&self.dest
|
||||
}
|
||||
|
||||
pub fn set_src(mut self, src: &Path) -> Self {
|
||||
|
||||
// Handle absolute and relative paths
|
||||
match src.is_absolute() {
|
||||
true => {
|
||||
self.src = src.to_owned();
|
||||
},
|
||||
false => {
|
||||
let src = self.root.join(src).to_owned();
|
||||
self.src = src;
|
||||
},
|
||||
}
|
||||
|
||||
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> {
|
||||
match self.livereload {
|
||||
Some(ref livereload) => Some(&livereload),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
// 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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,14 +63,17 @@
|
||||
//! 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.
|
||||
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
extern crate rustc_serialize;
|
||||
extern crate handlebars;
|
||||
extern crate pulldown_cmark;
|
||||
|
||||
#[macro_use] pub mod macros;
|
||||
pub mod book;
|
||||
mod parse;
|
||||
pub mod renderer;
|
||||
|
||||
@@ -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 {
|
||||
@@ -42,13 +44,15 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||
|
||||
// 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,
|
||||
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.")));
|
||||
};
|
||||
|
||||
} 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,
|
||||
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.")))
|
||||
},
|
||||
|
||||
// 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,
|
||||
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.")))
|
||||
},
|
||||
|
||||
// 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,6 +1,3 @@
|
||||
extern crate handlebars;
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use renderer::html_handlebars::helpers;
|
||||
use renderer::Renderer;
|
||||
use book::MDBook;
|
||||
@@ -13,8 +10,8 @@ use std::error::Error;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use self::handlebars::{Handlebars, JsonRender};
|
||||
use self::rustc_serialize::json::{Json, ToJson};
|
||||
use handlebars::Handlebars;
|
||||
use rustc_serialize::json::{Json, ToJson};
|
||||
|
||||
|
||||
pub struct HtmlHandlebars;
|
||||
@@ -51,7 +48,8 @@ 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")))
|
||||
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 +57,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);
|
||||
@@ -83,8 +82,13 @@ impl Renderer for HtmlHandlebars {
|
||||
// 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"))),
|
||||
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")))
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +98,7 @@ impl Renderer for HtmlHandlebars {
|
||||
|
||||
// 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());
|
||||
data.insert("path_to_root".to_owned(), utils::fs::path_to_root(&ch.path).to_json());
|
||||
|
||||
// Rendere the handlebars template with the data
|
||||
debug!("[*]: Render template");
|
||||
@@ -102,7 +106,8 @@ impl Renderer for HtmlHandlebars {
|
||||
|
||||
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")));
|
||||
let mut file =
|
||||
try!(utils::fs::create_file(&book.get_dest().join(&ch.path).with_extension("html")));
|
||||
output!("[*] Creating {:?} ✓", &book.get_dest().join(&ch.path).with_extension("html"));
|
||||
|
||||
try!(file.write_all(&rendered.into_bytes()));
|
||||
@@ -114,23 +119,24 @@ impl Renderer for HtmlHandlebars {
|
||||
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()));
|
||||
|
||||
output!(
|
||||
"[*] Creating index.html from {:?} ✓",
|
||||
book.get_dest().join(&ch.path.with_extension("html"))
|
||||
);
|
||||
output!("[*] Creating index.html from {:?} ✓",
|
||||
book.get_dest().join(&ch.path.with_extension("html")));
|
||||
index = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,12 +152,12 @@ impl Renderer for HtmlHandlebars {
|
||||
|
||||
// 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());
|
||||
data.insert("path_to_root".to_owned(), utils::fs::path_to_root(Path::new("print.md")).to_json());
|
||||
|
||||
// Rendere the handlebars template with the data
|
||||
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")));
|
||||
let mut file = try!(utils::fs::create_file(&book.get_dest().join("print").with_extension("html")));
|
||||
try!(file.write_all(&rendered.into_bytes()));
|
||||
output!("[*] Creating print.html ✓");
|
||||
|
||||
@@ -159,82 +165,128 @@ impl Renderer for HtmlHandlebars {
|
||||
|
||||
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")))
|
||||
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")))
|
||||
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")))
|
||||
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")))
|
||||
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")))
|
||||
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")))
|
||||
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")))
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/css/font-awesome.css")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create font-awesome.css")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME));
|
||||
let mut font_awesome = if let Ok(f) = utils::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")))
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/fonts/fontawesome-webfont.eot")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.eot")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_EOT));
|
||||
let mut font_awesome = if let Ok(f) = utils::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")))
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/fonts/fontawesome-webfont.svg")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.svg")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_SVG));
|
||||
let mut font_awesome = if let Ok(f) = utils::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")))
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/fonts/fontawesome-webfont.ttf")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.ttf")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
|
||||
let mut font_awesome = if let Ok(f) = utils::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")))
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/fonts/fontawesome-webfont.woff")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF));
|
||||
let mut font_awesome = if let Ok(f) = utils::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")))
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/fonts/fontawesome-webfont.woff2")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff2")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF2));
|
||||
let mut font_awesome = if let Ok(f) = utils::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")))
|
||||
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
|
||||
.join("_FontAwesome/fonts/FontAwesome.ttf")) {
|
||||
f
|
||||
} else {
|
||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create FontAwesome.ttf")));
|
||||
};
|
||||
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
|
||||
|
||||
// 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<BTreeMap<String, Json>, Box<Error>> {
|
||||
debug!("[fn]: make_data");
|
||||
|
||||
let mut data = BTreeMap::new();
|
||||
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());
|
||||
if let Some(livereload) = book.get_livereload() {
|
||||
data.insert("livereload".to_owned(), livereload.to_json());
|
||||
}
|
||||
|
||||
let mut chapters = vec![];
|
||||
|
||||
@@ -246,7 +298,9 @@ fn make_data(book: &MDBook) -> Result<BTreeMap<String,Json>, Box<Error>> {
|
||||
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()); },
|
||||
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"))),
|
||||
}
|
||||
},
|
||||
@@ -254,13 +308,15 @@ fn make_data(book: &MDBook) -> Result<BTreeMap<String,Json>, Box<Error>> {
|
||||
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()); },
|
||||
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"))),
|
||||
}
|
||||
},
|
||||
BookItem::Spacer => {
|
||||
chapter.insert("spacer".to_owned(), "_spacer_".to_json());
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
extern crate handlebars;
|
||||
extern crate rustc_serialize;
|
||||
|
||||
use std::path::Path;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use self::rustc_serialize::json::{self, ToJson};
|
||||
use self::handlebars::{Handlebars, RenderError, RenderContext, Helper, Context, Renderable};
|
||||
use rustc_serialize::json::{self, ToJson};
|
||||
use handlebars::{Handlebars, RenderError, RenderContext, Helper, Context, Renderable};
|
||||
|
||||
// Handlebars helper for navigation
|
||||
|
||||
@@ -27,7 +24,7 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String, String>> = match json::decode(&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 +38,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'
|
||||
@@ -55,23 +52,26 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
|
||||
},
|
||||
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(), p.replace("\\", "/").to_json());
|
||||
},
|
||||
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");
|
||||
@@ -84,14 +84,13 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
|
||||
Some(t) => {
|
||||
try!(t.render(&updated_context, 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());
|
||||
}
|
||||
},
|
||||
@@ -124,7 +123,7 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String, String>> = match json::decode(&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 +139,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 {
|
||||
@@ -154,8 +153,8 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
|
||||
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() })
|
||||
},
|
||||
None => return Err(RenderError::new("No title found for chapter in JSON data")),
|
||||
}
|
||||
|
||||
|
||||
@@ -163,8 +162,11 @@ 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(), l.replace("\\", "/").to_json());
|
||||
},
|
||||
None => return Err(RenderError::new("Link could not converted to str")),
|
||||
}
|
||||
|
||||
debug!("[*]: Inject in context");
|
||||
@@ -178,10 +180,10 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
|
||||
Some(t) => {
|
||||
try!(t.render(&updated_context, 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
|
||||
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 let Err(_) = file.read_to_string(&mut file_content) {
|
||||
continue;
|
||||
};
|
||||
|
||||
let replacement = String::new() + "<pre class=\"playpen\"><code class=\"language-rust\">" + &file_content + "</code></pre>";
|
||||
let replacement = String::new() + "<pre class=\"playpen\"><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,50 @@ 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 }
|
||||
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());
|
||||
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 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};
|
||||
editable = if let Some(_) = params[1].find("editable") {
|
||||
true
|
||||
} else {
|
||||
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.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 +119,7 @@ fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> {
|
||||
|
||||
|
||||
|
||||
//
|
||||
//---------------------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------------------
|
||||
// Tests
|
||||
//
|
||||
|
||||
@@ -130,10 +147,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 +170,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 +193,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 self::rustc_serialize::json;
|
||||
use self::handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
|
||||
use self::pulldown_cmark::{Parser, html, Event, Tag};
|
||||
use rustc_serialize::json;
|
||||
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
|
||||
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, c: &Context, _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 = 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()));
|
||||
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String,String>> = json::decode(&chapters.to_string()).unwrap();
|
||||
// Decode json format
|
||||
let decoded: Vec<BTreeMap<String, String>> = json::decode(&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 let Some(_) = item.get("spacer") {
|
||||
try!(rc.writer.write("<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.len() / 2
|
||||
} 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("<li>".as_bytes()));
|
||||
try!(rc.writer.write("<ul class=\"section\">".as_bytes()));
|
||||
current_level += 1;
|
||||
}
|
||||
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 -= 1;
|
||||
}
|
||||
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()));
|
||||
true
|
||||
}
|
||||
|
||||
// Link
|
||||
let path_exists = if let Some(path) = item.get("path") {
|
||||
if !path.is_empty() {
|
||||
try!(rc.writer.write("<a href=\"".as_bytes()));
|
||||
|
||||
// Add link
|
||||
try!(rc.writer.write(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("\"".as_bytes()));
|
||||
|
||||
if path == ¤t {
|
||||
try!(rc.writer.write(" class=\"active\"".as_bytes()));
|
||||
}
|
||||
|
||||
try!(rc.writer.write(">".as_bytes()));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} 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()));
|
||||
}
|
||||
}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) => 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()));
|
||||
|
||||
}
|
||||
while current_level > 1 {
|
||||
try!(rc.writer.write("</ul>".as_bytes()));
|
||||
try!(rc.writer.write("</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("</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>>;
|
||||
}
|
||||
@@ -264,6 +264,15 @@ table thead td {
|
||||
line-height: 25px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.theme-popup .theme:hover:first-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;
|
||||
@@ -756,3 +765,30 @@ table thead td {
|
||||
.rust pre > .result {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
#sidebar {
|
||||
display: none;
|
||||
}
|
||||
#page-wrapper {
|
||||
left: 0;
|
||||
overflow-y: initial;
|
||||
}
|
||||
#content {
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#menu-bar {
|
||||
display: none;
|
||||
}
|
||||
.page {
|
||||
overflow-y: initial;
|
||||
}
|
||||
.nav-chapters {
|
||||
display: none;
|
||||
}
|
||||
.mobile-nav-chapters {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,9 @@ $( document ).ready(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());
|
||||
// Remove any existing anchor then append the new one
|
||||
// ensuring eg. no spaces are present within it ie. they become %20
|
||||
wrapper.attr("href", $(location).attr('href').split("#")[0] + "#" + encodeURIComponent($(this).text().trim()) );
|
||||
return wrapper;
|
||||
});
|
||||
|
||||
|
||||
BIN
src/theme/favicon.png
Normal file
BIN
src/theme/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
@@ -4,16 +4,18 @@
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="description" content="{% block description %}{% endblock %}">
|
||||
<meta name="description" content="{{ description }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<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 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">
|
||||
@@ -22,14 +24,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');
|
||||
@@ -105,6 +107,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Livereload script (if served using the cli tool) -->
|
||||
{{{livereload}}}
|
||||
|
||||
<script src="highlight.js"></script>
|
||||
<script src="book.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::io::Read;
|
||||
|
||||
pub static INDEX: &'static [u8] = include_bytes!("index.hbs");
|
||||
pub static CSS: &'static [u8] = include_bytes!("book.css");
|
||||
pub static FAVICON: &'static [u8] = include_bytes!("favicon.png");
|
||||
pub static JS: &'static [u8] = include_bytes!("book.js");
|
||||
pub static HIGHLIGHT_JS: &'static [u8] = include_bytes!("highlight.js");
|
||||
pub static TOMORROW_NIGHT_CSS: &'static [u8] = include_bytes!("tomorrow-night.css");
|
||||
@@ -27,6 +28,7 @@ pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/
|
||||
pub struct Theme {
|
||||
pub index: Vec<u8>,
|
||||
pub css: Vec<u8>,
|
||||
pub favicon: Vec<u8>,
|
||||
pub js: Vec<u8>,
|
||||
pub highlight_css: Vec<u8>,
|
||||
pub tomorrow_night_css: Vec<u8>,
|
||||
@@ -41,6 +43,7 @@ impl Theme {
|
||||
let mut theme = Theme {
|
||||
index: INDEX.to_owned(),
|
||||
css: CSS.to_owned(),
|
||||
favicon: FAVICON.to_owned(),
|
||||
js: JS.to_owned(),
|
||||
highlight_css: HIGHLIGHT_CSS.to_owned(),
|
||||
tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
|
||||
@@ -50,13 +53,13 @@ impl Theme {
|
||||
|
||||
// Check if the given path exists
|
||||
if !src.exists() || !src.is_dir() {
|
||||
return theme
|
||||
return theme;
|
||||
}
|
||||
|
||||
let src = src.join("theme");
|
||||
// If src does exist, check if there is a theme directory in it
|
||||
if !src.exists() || !src.is_dir() {
|
||||
return theme
|
||||
return theme;
|
||||
}
|
||||
|
||||
// Check for individual files if they exist
|
||||
@@ -70,7 +73,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
|
||||
@@ -79,6 +82,12 @@ impl Theme {
|
||||
let _ = f.read_to_end(&mut theme.css);
|
||||
}
|
||||
|
||||
// favicon.png
|
||||
if let Ok(mut f) = File::open(&src.join("favicon.png")) {
|
||||
theme.favicon.clear();
|
||||
let _ = f.read_to_end(&mut theme.favicon);
|
||||
}
|
||||
|
||||
// highlight.js
|
||||
if let Ok(mut f) = File::open(&src.join("highlight.js")) {
|
||||
theme.highlight_js.clear();
|
||||
|
||||
238
src/utils/fs.rs
Normal file
238
src/utils/fs.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
use std::path::{Path, Component};
|
||||
use std::error::Error;
|
||||
use std::io::{self, Read};
|
||||
use std::fs::{self, metadata, 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) -> 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(())
|
||||
}
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
180
src/utils/mod.rs
180
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};
|
||||
|
||||
|
||||
///
|
||||
@@ -145,52 +18,3 @@ pub fn render_markdown(text: &str) -> String {
|
||||
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