Compare commits

...

82 Commits

Author SHA1 Message Date
Mathieu David
15d6227a11 Attempt to fix #119 replace \ with / in paths, so that Windows also uses / as separator (ugly hack) 2016-04-25 17:02:47 +02:00
Mathieu David
1b8af2bf57 Fix #120 destination and source directories can now be constructed correctly even if multiple directories do not exist on the path 2016-04-25 15:58:44 +02:00
Mathieu David
7f34512751 Merge branch 'master' of https://github.com/azerupi/mdBook 2016-04-25 15:51:27 +02:00
Mathieu David
876ea7895a Fix #131 where src and dest paths were not prefixed with the root directory if it was not the current directory 2016-04-25 15:50:34 +02:00
Mathieu David
bea80f6266 Merge pull request #130 from japaric/rustup
travis: use rustup instead of Travis built-in Rust support
2016-04-14 01:20:19 +02:00
Jorge Aparicio
334540835c travis: use rustup instead of Travis built-in Rust support
this ensures we install the correct set of standard crates when working
in the beta channel
2016-04-13 16:48:57 -05:00
Mathieu David
10e7a41d92 Bump version to 0.0.12, version 0.0.11 has been published to crates.io 2016-04-13 22:37:46 +02:00
Mathieu David
2ec5648587 Remove BookConfig field from MDBook
MDBook now stores the necessary information, BookConfig is not used as a field anymore. It is only used for parsing the configuration file. This allows to more easily replace the book.json config with the new tomlbased config
2016-04-05 12:44:14 +02:00
Mathieu David
6aa6546ce4 Merge pull request #128 from Bobo1239/serve-squashed
Implement Serve feature
2016-04-04 23:17:01 +02:00
Mathieu David
c071406fef Merge pull request #127 from japaric/mac32
travis: test/release on/for i686-apple-darwin
2016-04-04 19:23:36 +02:00
Boris-Chengbiao Zhou
c8051294b0 Switch from rust-websocket to ws-rs 2016-04-02 21:44:13 +02:00
Jorge Aparicio
807a2f116e travis: test/release on/for i686-apple-darwin 2016-04-02 09:33:05 -05:00
Boris-Chengbiao Zhou
2f43167b75 Add documentation for Serve feature 2016-04-02 05:43:21 +02:00
Boris-Chengbiao Zhou
e861880f95 Implement Serve feature 2016-04-02 05:20:46 +02:00
Mathieu David
c3564f1699 Add convenience function to read the content from a file into a string given a path 2016-03-27 18:40:50 +02:00
Mathieu David
15d26befcc Refactor: Move extern crate definitions to lib.rs 2016-03-27 18:22:17 +02:00
Mathieu David
925939e267 Merge pull request #124 from LucioFranco/insecure-content-fix
Fix for insecure content on HTTPS enabled sites
2016-03-23 23:22:44 +01:00
Lucio Franco
ceb139a848 Moved CDN's to https 2016-03-23 14:16:41 -06:00
Mathieu David
d0e39f469a Update installation instructions in the README
Closes #123
2016-03-22 14:06:32 +01:00
Mathieu David
c5752620d7 Merge pull request #122 from Bobo1239/fix_cooldown
Fix watch event cooldown
2016-03-19 19:40:58 +01:00
Mathieu David
0c93599242 Merge pull request #121 from Bobo1239/update_deps
Update dependencies
2016-03-19 19:38:01 +01:00
Boris Zhou
7f3a6c8130 Fix watch event cooldown 2016-03-19 18:28:34 +01:00
Boris-Chengbiao Zhou
b30a8bdc81 Update dependencies 2016-03-19 17:45:58 +01:00
Mathieu David
74fff81e4b Refactor: Move fs related functions from utils into their own submodule 2016-03-17 22:41:00 +01:00
Mathieu David
ad0794a0bd Add a rustfmt config and run rustfmt on the code base 2016-03-17 22:31:28 +01:00
Mathieu David
6bac41caa8 Merge pull request #118 from japaric/travis
Travis CI: expand to test and deploy for Linux and Mac
2016-03-08 17:25:52 +01:00
Jorge Aparicio
b094268b68 disable the i686-apple-darwin target 2016-03-08 09:21:50 -05:00
Jorge Aparicio
02a37e0ee9 disable doc tests when crossing 2016-03-08 08:57:35 -05:00
Mathieu David
9e34eccb3e Add windows (AppVeyor) build badge 2016-03-08 01:03:44 +01:00
Mathieu David
79fb92ed7c Merge pull request #117 from japaric/appveyor
set up AppVeyor to test and deploy on Windows
2016-03-08 00:35:54 +01:00
Jorge Aparicio
5e78697ab1 Travis CI: expand to test and deploy for Linux and Mac 2016-03-07 18:16:42 -05:00
Jorge Aparicio
469cb10d4a manually package artifact during before_deploy phase
The automatic packaging phase runs before the before_deploy phase which is too early so we can rely
on it.
2016-03-07 18:01:05 -05:00
Jorge Aparicio
0f9caf4410 set up AppVeyor to test and deploy on Windows 2016-03-07 17:14:05 -05:00
Mathieu David
f23a5f2729 Merge pull request #115 from vrinek/init-with-gitignore-take-2
Move `.gitignore` directly under the root folder
2016-03-07 12:09:25 +01:00
vrinek
bc41efe414 Move .gitignore directly under the root folder 2016-03-07 08:52:19 +00:00
Mathieu David
5316089e61 Modify wording of confirmation request before creation of .gitignore 2016-03-02 19:38:39 +01:00
Mathieu David
73ce3f814a Merge branch 'init-with-gitignore' of https://github.com/vrinek/mdBook into vrinek-init-with-gitignore 2016-03-02 19:20:21 +01:00
Mathieu David
075da959c9 bump version, v0.0.10 has been published to crates.io 2016-03-01 18:50:04 +01:00
Mathieu David
80deac90d9 Merge branch 'master' of https://github.com/azerupi/mdBook 2016-03-01 18:33:55 +01:00
Mathieu David
625f5081fa update notify and change dependency version restrictions 2016-03-01 18:32:43 +01:00
vrinek
1eb59428e6 Ask user to create .gitignore and skip on --force 2016-02-28 15:28:11 +00:00
Mathieu David
3e8151e8e3 Merge pull request #112 from jessestricker/feature-meta
Add description config option
2016-02-25 17:21:51 +01:00
Jesse Stricker
3c10a85735 Add documentation and example for description config 2016-02-25 15:01:16 +01:00
Jesse Stricker
330b1ad55d Add description config option 2016-02-25 14:32:49 +01:00
vrinek
596455f28c Generate simple .gitignore on init 2016-02-23 14:03:45 +00:00
Mathieu David
f24eb59753 Bump version number, v0.0.9 has been published to Crates.io 2016-02-22 19:03:31 +01:00
Mathieu David
01c5085725 Add an entry about the favicon in the docs 2016-02-22 18:01:36 +01:00
Mathieu David
9f17be2c32 Merge pull request #109 from jessestricker/feature-favicon
Add theme support for favicon
2016-02-22 17:28:46 +01:00
Jesse Stricker
88fabd76f0 Copy favicon on 'init --theme' 2016-02-22 17:20:54 +01:00
Jesse Stricker
f508db6113 Add favicon support to theme 2016-02-22 17:17:07 +01:00
Jesse Stricker
1083d1822d Add default favicon.png 2016-02-22 16:59:53 +01:00
Mathieu David
fc86b963bb Merge pull request #108 from funkill/styles
add rounding for first and last items in theme selector
2016-02-16 10:30:01 +01:00
funkill
f2b913c9dd add rounding for first and last items in theme selector 2016-02-16 11:15:08 +03:00
Mathieu David
dd0cfc14d4 bump version number to 0.0.9, v0.0.8 has been published to crates.io 2016-02-16 08:57:55 +01:00
Mathieu David
5891e4b5db Fix bug where theme-popup was under the navigation arrows making it impossible to change the theme 2016-02-16 08:50:57 +01:00
Mathieu David
394023f617 Bump version number from 0.0.7 to 0.0.8, version 0.0.7 has been published to Crates.io 2016-02-15 21:20:07 +01:00
Mathieu David
39a6fe4b3c Fix wildcard dependency on crossbeam 2016-02-15 21:18:43 +01:00
Mathieu David
75b98d7019 Merge pull request #106 from funkill/pulldown-mark-update
pulldown-mark version bump
2016-02-15 21:01:21 +01:00
funkill
814b21ad94 pulldown-mark version bump 2016-02-15 21:25:46 +03:00
Mathieu David
7364d41f0c Style tables, different header bg, alternate row color and border 2016-02-05 18:09:35 +01:00
Mathieu David
f6be4a7d7e Merge branch 'master' of https://github.com/azerupi/mdBook 2016-02-03 18:02:23 +01:00
Mathieu David
0b00c270d5 Fix a style bug caused by the insertion of the theme-popup div inside font awesome icon <i>
The div is now inserted after the <i>, the text color has also been changed to the foreground color and the "(default)" text that indicates the default theme is now grey to contrast with the theme name

Fixes #97
2016-02-03 17:55:19 +01:00
Mathieu David
8d2ca521c0 Merge pull request #100 from yurrriq/patch-1
Fix "it's" typos in README.md
2016-01-28 11:58:17 +01:00
Eric Bailey
276cd8d490 Update README.md
Replace two contractions with possessive pronouns: it's => its.
2016-01-28 00:55:25 -06:00
Mathieu David
e958fc8605 update readme 2016-01-13 22:40:30 +01:00
Mathieu David
e286b208da Bump version from 0.0.6 to 0.0.7, v0.0.6 has been published to Crates.io 2016-01-03 14:18:36 +01:00
Mathieu David
3fd1d4606c Fix tests after removing PathExt from utils 2016-01-03 14:08:17 +01:00
Mathieu David
78b6148463 Basic formatting for tables + Styling for blockquotes
Added basic formatting for tables so that they have some padding and are aligned in the center of the page.
I did not add color or borders because I am not sure how tables should look like.

A lot of people in IntermezzOS want asides, blockquotes are probably the easiest way to do that. I have thus styled blockquotes for all the color themes.
2016-01-03 13:47:59 +01:00
Mathieu David
78e1897b47 Remove code that has better equivalent in std
Path_Ext has been stabilized in the Standard Library, the temporary copy I had can go.

I found a fs::create_dir_all method that does exactly what create_path was doing, but better... create_path is thus replaced with that.
2016-01-03 13:02:04 +01:00
Mathieu David
d000fc8bac Updated pulldown-cmark to version 0.0.5
Version 0.0.5 contains table and footnotes support, both options are now enabled in mdBook.
2016-01-03 12:02:39 +01:00
Mathieu David
5170e6b675 Fix #89, bug introduced earlier where all headers are black in all color themes 2016-01-01 11:02:24 +01:00
Mathieu David
a7f329d337 Add href to heading anchors so that the url for the anchor is displayed in the url bar when clicking the header 2016-01-01 02:17:40 +01:00
Mathieu David
bb0c878e06 #29 update doc with an example of runnable rust code 2016-01-01 01:57:21 +01:00
Mathieu David
2a7463c45b #29 Add a way to escape {{#playpen ... } using a backslash in front: \{{#playpen ... }} 2016-01-01 01:40:37 +01:00
Mathieu David
db7424e947 Continue #29, playpens are now runnable 2016-01-01 00:32:12 +01:00
Mathieu David
0ac0301d72 Continue #29, Rust files can now be loaded with {{#playpen file.rs}}, they will be displayed as other code snippets included with markdown backticks except they have a playpen css class 2015-12-31 19:25:02 +01:00
Mathieu David
38b2dee17e Continue #29 Check that the rust file exists and read to string 2015-12-31 14:14:56 +01:00
Mathieu David
0cb234de5d Add tests for find_playpens 2015-12-31 12:02:25 +01:00
Mathieu David
ee4a7fb35c Start implementing #29 support for embedding playpen, implemented the function that parses the markdown to find playpen links 2015-12-30 22:40:23 +01:00
Mathieu David
9c5c8a6804 Bump version to 0.0.6, v0.0.5 has been published to Crates.io 2015-12-30 17:26:04 +01:00
Mathieu David
ae6334f358 Fix bug where we would not check if there was actually a page to navigate to when using arrow keys 2015-12-30 17:19:43 +01:00
Mathieu David
98cd2f0c27 Version bump to 0.0.5, v0.0.4 has been published to crates.io 2015-12-30 17:05:09 +01:00
41 changed files with 2005 additions and 774 deletions

View File

@@ -1,20 +1,92 @@
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
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
- os: linux
env: TARGET=x86_64-unknown-linux-musl CHANNEL=stable
# Beta channel
- os: osx
env: TARGET=i686-apple-darwin CHANNEL=beta
- os: linux
env: TARGET=i686-unknown-linux-gnu CHANNEL=beta
addons:
apt:
packages: *i686_unknown_linux_gnu
- os: osx
env: TARGET=x86_64-apple-darwin CHANNEL=beta
- os: linux
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=beta
- os: linux
env: TARGET=x86_64-unknown-linux-musl CHANNEL=beta
# Nightly channel
- os: osx
env: TARGET=i686-apple-darwin CHANNEL=nightly
- os: linux
env: TARGET=i686-unknown-linux-gnu CHANNEL=nightly
addons:
apt:
packages: *i686_unknown_linux_gnu
- os: osx
env: TARGET=x86_64-apple-darwin CHANNEL=nightly
- os: linux
env: TARGET=x86_64-unknown-linux-gnu CHANNEL=nightly
- os: linux
env: TARGET=x86_64-unknown-linux-musl CHANNEL=nightly
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 $TRAVIS_RUST_VERSION = "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

View File

@@ -1,6 +1,6 @@
[package]
name = "mdbook"
version = "0.0.4"
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,36 +15,34 @@ exclude = [
]
[dependencies]
clap = "~1.5.3"
handlebars = "~0.12.0"
rustc-serialize = "~0.3.16"
pulldown-cmark = "~0.0.3"
clap = "2.2.1"
handlebars = "0.16.0"
rustc-serialize = "0.3.18"
pulldown-cmark = "0.0.7"
# Watch feature
[dependencies.notify]
notify = "^2.4.1"
optional = true
notify = { version = "2.5.5", optional = true }
time = { version = "0.1.34", optional = true }
crossbeam = { version = "0.2.8", optional = true }
[dependencies.time]
time = "^0.1.33"
optional = true
# Serve feature
iron = { version = "0.3", optional = true }
staticfile = { version = "0.2", optional = true }
ws = { version = "0.4.6", optional = true}
[dependencies.crossbeam]
time = "^0.2.0"
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

136
README.md
View File

@@ -1,79 +1,125 @@
# mdBook [![Travis-CI](https://travis-ci.org/azerupi/mdBook.svg?branch=master)](https://travis-ci.org/azerupi/mdBook) [![Crates.io version](https://img.shields.io/crates/v/mdbook.svg)](https://crates.io/crates/mdbook) [![License](https://img.shields.io/crates/l/mdbook.svg)](LICENSE)
# mdBook
Personal implementation of Gitbook in Rust
<table>
<tr>
<td><strong>Linux / OS X</strong></td>
<td>
<a href="https://travis-ci.org/azerupi/mdBook"><img src="https://travis-ci.org/azerupi/mdBook.svg?branch=master"></a>
</td>
</tr>
<tr>
<td><strong>Windows</strong></td>
<td>
<a href="https://ci.appveyor.com/project/azerupi/mdbook/"><img src="https://ci.appveyor.com/api/projects/status/o38racsnbcospyc8/branch/master?svg=true"></a>
</td>
</tr>
<tr>
<td colspan="2">
<a href="https://crates.io/crates/mdbook"><img src="https://img.shields.io/crates/v/mdbook.svg"></a>
<a href="LICENSE"><img src="https://img.shields.io/crates/l/mdbook.svg"></a>
</td>
</tr>
</table>
**This project is still in it's early days.**
For more information about what is left on my to-do list, check the issue tracker
mdBook is a utility to create modern online books from markdown files.
**This project is still evolving.**
For more information, check the issue tracker.
## Example
## What does it look like?
To have an idea of what a rendered book looks like,take a look at the [**Documentation**](http://azerupi.github.io/mdBook/). It is rendered by the latest version of mdBook.
The [**Documentation**](http://azerupi.github.io/mdBook/) for mdBook has been written in markdown and is using mdBook to generate the online book-like website you can read. The documentation uses the latest version on github and showcases the available features.
## Installation
```
cargo install mdbook
```
There are multiple ways to install mdBook.
If you want to regenerate the css (stylesheet), clone the git repo locally and make sure that you installed `stylus` and `nib` from `npm` because it is used to compile the stylesheets
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`.
Install [node.js](https://nodejs.org/en/)
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
```
```
npm install -g stylus nib
```
This will download and compile mdBook for you, the only thing left to do is to add the Cargo bin directory to your `PATH`.
Then build with the `regenerate-css` feature:
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***!
```
cargo build --release --features="regenerate-css"
```
```
cargo install --git https://github.com/azerupi/mdBook.git
```
Again, make sure to add the Cargo bin directory to your `PATH`
## Structure
4. **For Contributions**
If you want to contribute to mdBook you will have to clone the repository on your local machine:
There are two main parts of this project:
```
git clone https://github.com/azerupi/mdBook.git
```
`cd` into `mdBook/` and run
- **The library:** The crate is structured so that all the code that actually does something is part of the library. You could therefore easily hook mdbook into your existing project, extend it's functionality by wrapping it in some other code, etc.
- **The binary:** Is just a wrapper around the library functionality providing a nice and easy command line interface.
```
cargo build
```
### Command line interface
the resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`
#### init
If you run `mdbook init` in a directory, it will create a couple of folders and files you can start with.
This is the strucutre it creates at the moment:
```
book-test/
├── book
└── src
├── chapter_1.md
└── SUMMARY.md
```
`book` and `src` are both directories. `src` contains the markdown files that will be used to render the ouput to the `book` directory.
Please, take a look at the [**Documentation**](http://azerupi.github.io/mdBook/cli/init.html) for more information.
## Usage
#### build
mdBook will primaraly be used as a command line tool, even though it exposes all its functionality as a Rust crate for integration in other projects.
Use `mdbook build` in the directory to render the book. You can find more information in the [**Documentation**](http://azerupi.github.io/mdBook/cli/build.html)
Here are the main commands you will want to run, for a more exhaustive explanation, check out the [documentation](http://azerupi.github.io/mdBook/).
- `mdbook init`
The init command will create a directory with the minimal boilerplate to start with.
```
book-test/
├── book
└── src
├── chapter_1.md
└── SUMMARY.md
```
`book` and `src` are both directories. `src` contains the markdown files that will be used to render the ouput to the `book` directory.
Please, take a look at the [**Documentation**](http://azerupi.github.io/mdBook/cli/init.html) for more information and some neat tricks.
- `mdbook build`
This is the command you will run to render your book, it reads the `SUMMARY.md` file to understand the structure of your book, takes the markdown files in the source directory as input and outputs static html pages that you can upload to a server.
- `mdbook watch`
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 webapp for example. Since the command line interface is just a wrapper around the library functionality, when you use this crate as a library you have full access to all the functionality of the command line interface with and easy to use API and more!
Aside from the command line interface, this crate can also be used as a library. This means that you could integrate it in an existing project, like a web-app for example. Since the command line interface is just a wrapper around the library functionality, when you use this crate as a library you have full access to all the functionality of the command line interface with and easy to use API and more!
See the [**Documentation**](http://azerupi.github.io/mdBook/lib/lib.html) and the [**API docs**](http://azerupi.github.io/mdBook/mdbook/index.html) for more information.
See the [Documentation](http://azerupi.github.io/mdBook/lib/lib.html) and the [API docs](http://azerupi.github.io/mdBook/mdbook/index.html) for more information.
## Contributions
Contributions are highly apreciated. Here are some ideas:
Contributions are highly appreciated and encouraged! Don't hesitate to participate to discussions in the issues, propose new features and ask for help.
- **Create new renderers**, at the moment I have only created a renderer that uses [handlebars](https://github.com/sunng87/handlebars-rust), [pulldown-cmark](https://github.com/google/pulldown-cmark) and renders to html. But you could create a renderer that uses another template engine, markdown parser or even outputs to another format like pdf.
- **Add tests** I have not much experience in writing tests, all help to write meaningful tests is thus very welcome
- **write documentation** documentation can always be improved
- **Smaller tasks** I try to add a lot of the remaining tasks on the issue tracker with the label: [`Enhancement`](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AEnhancement). Just pick one that looks interesting. The majority of the tasks are small enough to be tackled by people who are unfamiliar with the project.
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.
If you have an idea for improvement, create a new issue. Or a pull request if you can :)
## License

60
appveyor.yml Normal file
View 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

View File

@@ -1,4 +1,5 @@
{
"title": "mdBook Documentation",
"description": "Create book from markdown files. Like Gitbook but implemented in Rust",
"author": "Mathieu David"
}

View File

@@ -13,6 +13,7 @@
- [index.hbs](format/theme/index-hbs.md)
- [Syntax highlighting](format/theme/syntax-highlighting.md)
- [MathJax Support](format/mathjax.md)
- [Rust code specific features](format/rust.md)
- [Rust Library](lib/lib.md)
-----------
[Contributors](misc/contributors.md)

View File

@@ -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*

View File

@@ -0,0 +1,6 @@
fn main() {
println!("Hello World!");
#
# // You can even hide lines! :D
# println!("I am hidden! Expand the code snippet to see me");
}

View File

@@ -0,0 +1,42 @@
# Rust code specific features
## Hiding code lines
There is a feature in mdBook that let's you hide code lines by prepending them with a `#`.
```bash
#fn main() {
let x = 5;
let y = 6;
println!("{}", x + y);
#}
```
Will render as
```rust
#fn main() {
let x = 5;
let y = 7;
println!("{}", x + y);
#}
```
## Inserting runnable Rust files
With the following syntax, you can insert runnable Rust files into your book:
```hbs
\{{#playpen file.rs}}
```
The path to the Rust file has to be relative from the current source file.
When play is clicked, the code snippet will be send to the [Rust Playpen]() to be compiled and run. The result is send back and displayed directly underneath the code.
Here is what a rendered code snippet looks like:
{{#playpen example.rs}}

View File

@@ -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
View 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
View 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

37
ci/script.sh Normal file
View File

@@ -0,0 +1,37 @@
# `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() {
cargo build --target $TARGET --verbose
cargo test --target $TARGET --verbose
}
main() {
disable_cross_doctests
run_test_suite
}
main

View File

@@ -15,13 +15,13 @@ 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
cp -R book-example/book/* target/doc/
cp -R book-example/book/* target/$TARGET/doc/
cd target/doc
cd target/$TARGET/doc
echo -e "${CYAN}Initializing Git${NC}"
git init

15
rustfmt.toml Normal file
View 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"

View File

@@ -10,13 +10,20 @@ extern crate notify;
#[cfg(feature = "watch")]
extern crate time;
// 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,7 +43,7 @@ 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)
.setting(AppSettings::SubcommandRequired)
.after_help("For more information about a specific command, try `mdbook <command> --help`")
.subcommand(SubCommand::with_name("init")
.about("Create boilerplate structure and files in the directory")
@@ -50,18 +57,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 +91,7 @@ fn confirm() -> bool {
io::stdin().read_line(&mut s).ok();
match &*s.trim() {
"Y" | "y" | "yes" | "Yes" => true,
_ => false
_ => false,
}
}
@@ -94,7 +108,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 +129,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 +162,83 @@ 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();
});
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 +251,70 @@ 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!
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);
},
}
}

View File

@@ -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
}
}

View File

@@ -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);
}
}
}

View File

@@ -9,16 +9,25 @@ use std::process::Command;
use {BookConfig, BookItem, theme, parse, utils};
use book::BookItems;
use renderer::{Renderer, HtmlHandlebars};
use utils::{PathExt, create_path};
pub struct MDBook {
config: BookConfig,
root: PathBuf,
dest: PathBuf,
src: PathBuf,
pub title: String,
pub author: String,
pub description: String,
pub content: Vec<BookItem>,
renderer: Box<Renderer>,
#[cfg(feature = "serve")]
livereload: Option<String>,
}
impl MDBook {
/// Create a new `MDBook` struct with root directory `root`
///
/// - The default source directory is set to `root/src`
@@ -33,12 +42,17 @@ impl MDBook {
}
MDBook {
root: root.to_owned(),
dest: root.join("book"),
src: root.join("src"),
title: String::new(),
author: String::new(),
description: String::new(),
content: vec![],
config: BookConfig::new(root)
.set_src(&root.join("src"))
.set_dest(&root.join("book"))
.to_owned(),
renderer: Box::new(HtmlHandlebars::new()),
livereload: None,
}
}
@@ -95,33 +109,31 @@ impl MDBook {
debug!("[fn]: init");
if !self.config.get_root().exists() {
create_path(self.config.get_root()).unwrap();
output!("{:?} created", self.config.get_root());
if !self.root.exists() {
fs::create_dir_all(&self.root).unwrap();
output!("{:?} created", &self.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 !self.dest.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", self.dest);
try!(fs::create_dir_all(&self.dest));
}
if !src.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", src);
try!(fs::create_dir(&src));
if !self.src.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", self.src);
try!(fs::create_dir_all(&self.src));
}
let summary = src.join("SUMMARY.md");
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", src.join("SUMMARY.md"));
let mut f = try!(File::create(&src.join("SUMMARY.md")));
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");
@@ -139,20 +151,21 @@ impl MDBook {
debug!("[*]: item: {:?}", item);
match *item {
BookItem::Spacer => continue,
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
BookItem::Chapter(_, ref ch) |
BookItem::Affix(ref ch) => {
if ch.path != PathBuf::new() {
let path = self.config.get_src().join(&ch.path);
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);
// debug!("[*]: Writing to {:?}", path);
try!(writeln!(f, "# {}", ch.name));
}
}
}
},
}
}
@@ -160,19 +173,46 @@ impl MDBook {
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::remove_dir_content(&self.config.get_dest()));
try!(utils::fs::remove_dir_content(&self.dest));
try!(self.renderer.render(&self));
@@ -180,10 +220,14 @@ impl MDBook {
}
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.config.get_src().join("theme");
let theme_dir = self.src.join("theme");
if !theme_dir.exists() {
debug!("[*]: {:?} does not exist, trying to create directory", theme_dir);
@@ -198,6 +242,10 @@ impl MDBook {
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));
@@ -230,8 +278,18 @@ impl MDBook {
/// 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);
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
}
@@ -274,9 +332,9 @@ impl MDBook {
println!("[*]: Testing file: {:?}", path);
let output_result = Command::new("rustdoc")
.arg(&path)
.arg("--test")
.output();
.arg(&path)
.arg("--test")
.output();
let output = try!(output_result);
if !output.status.success() {
@@ -286,72 +344,105 @@ impl MDBook {
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.config.set_dest(dest); },
true => {
self.dest = dest.to_owned();
},
false => {
let dest = self.config.get_root().join(dest).to_owned();
self.config.set_dest(&dest);
}
let dest = self.root.join(dest).to_owned();
self.dest = dest;
},
}
self
}
pub fn get_dest(&self) -> &Path {
self.config.get_dest()
&self.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); },
true => {
self.src = src.to_owned();
},
false => {
let src = self.config.get_root().join(src).to_owned();
self.config.set_src(&src);
}
let src = self.root.join(src).to_owned();
self.src = src;
},
}
self
}
pub fn get_src(&self) -> &Path {
self.config.get_src()
&self.src
}
pub fn set_title(mut self, title: &str) -> Self {
self.config.title = title.to_owned();
self.title = title.to_owned();
self
}
pub fn get_title(&self) -> &str {
&self.config.title
&self.title
}
pub fn set_author(mut self, author: &str) -> Self {
self.config.author = author.to_owned();
self.author = author.to_owned();
self
}
pub fn get_author(&self) -> &str {
&self.config.author
&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.config.get_src().join("SUMMARY.md")));
self.content = try!(parse::construct_bookitems(&self.src.join("SUMMARY.md")));
Ok(())
}
}

View File

@@ -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;

View File

@@ -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))
}

View File

@@ -1,6 +1,3 @@
extern crate handlebars;
extern crate rustc_serialize;
use renderer::html_handlebars::helpers;
use renderer::Renderer;
use book::MDBook;
@@ -8,13 +5,13 @@ use book::bookitem::BookItem;
use {utils, theme};
use std::path::{Path, PathBuf};
use std::fs::File;
use std::fs::{self, File};
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, JsonRender};
use rustc_serialize::json::{Json, ToJson};
pub struct HtmlHandlebars;
@@ -50,8 +47,9 @@ impl Renderer for HtmlHandlebars {
// Check if dest directory exists
debug!("[*]: Check if destination directory exists");
if let Err(_) = utils::create_path(book.get_dest()) {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Unexpected error when constructing destination path")))
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")));
}
// Render a file for every entry in the book
@@ -71,6 +69,11 @@ impl Renderer for HtmlHandlebars {
debug!("[*]: Reading file");
try!(f.read_to_string(&mut content));
// Parse for playpen links
if let Some(p) = path.parent() {
content = helpers::playpen::render_playpen(&content, p);
}
// Render markdown using the pulldown-cmark crate
content = utils::render_markdown(&content);
print_content.push_str(&content);
@@ -78,8 +81,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")))
},
}
@@ -89,7 +97,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");
@@ -97,7 +105,7 @@ 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()));
@@ -109,23 +117,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;
}
}
}
_ => {}
},
_ => {},
}
}
@@ -141,12 +150,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 ✓");
@@ -154,55 +163,133 @@ impl Renderer for HtmlHandlebars {
debug!("[*] Copy static files");
// JavaScript
let mut js_file = try!(File::create(book.get_dest().join("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 = try!(File::create(book.get_dest().join("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 = try!(File::create(book.get_dest().join("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));
// Font Awesome local fallback
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/css/font-awesome").with_extension("css")));
try!(font_awesome.write_all(theme::FONT_AWESOME));
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.eot")));
try!(font_awesome.write_all(theme::FONT_AWESOME_EOT));
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.svg")));
try!(font_awesome.write_all(theme::FONT_AWESOME_SVG));
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.ttf")));
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.woff")));
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF));
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/fontawesome-webfont.woff2")));
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF2));
let mut font_awesome = try!(utils::create_file(&book.get_dest().join("_FontAwesome/fonts/FontAwesome.ttf")));
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
// syntax highlighting
let mut highlight_css = try!(File::create(book.get_dest().join("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 = try!(File::create(book.get_dest().join("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 = try!(File::create(book.get_dest().join("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::fs::create_file(&book.get_dest()
.join("_FontAwesome/css/font-awesome.css")) {
f
} else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create font-awesome.css")));
};
try!(font_awesome.write_all(theme::FONT_AWESOME));
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
.join("_FontAwesome/fonts/fontawesome-webfon\
t.eot")) {
f
} else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.eot")));
};
try!(font_awesome.write_all(theme::FONT_AWESOME_EOT));
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
.join("_FontAwesome/fonts/fontawesome-webfon\
t.svg")) {
f
} else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.svg")));
};
try!(font_awesome.write_all(theme::FONT_AWESOME_SVG));
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
.join("_FontAwesome/fonts/fontawesome-webfon\
t.ttf")) {
f
} else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.ttf")));
};
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
.join("_FontAwesome/fonts/fontawesome-webfon\
t.woff")) {
f
} else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff")));
};
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF));
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
.join("_FontAwesome/fonts/fontawesome-webfon\
t.woff2")) {
f
} else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create fontawesome-webfont.woff2")));
};
try!(font_awesome.write_all(theme::FONT_AWESOME_WOFF2));
let mut font_awesome = if let Ok(f) = utils::fs::create_file(&book.get_dest()
.join("_FontAwesome/fonts/FontAwesome.ttf")) {
f
} else {
return Err(Box::new(io::Error::new(io::ErrorKind::Other, "Could not create FontAwesome.ttf")));
};
try!(font_awesome.write_all(theme::FONT_AWESOME_TTF));
// 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![];
@@ -214,7 +301,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"))),
}
},
@@ -222,13 +311,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());
}
},
}

View File

@@ -1,2 +1,3 @@
pub mod navigation;
pub mod toc;
pub mod playpen;

View File

@@ -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
@@ -19,15 +16,15 @@ pub fn previous(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext
let chapters = c.navigate(rc.get_path(), "chapters");
let current = c.navigate(rc.get_path(), "path")
.to_string()
.replace("\"", "");
.to_string()
.replace("\"", "");
debug!("[*]: Decode chapters from JSON");
// 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 { desc: "Could not decode the JSON data".to_owned() }),
};
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 == &current {
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,32 @@ 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 { desc: "No title found for chapter in JSON data".to_owned() });
},
};
// 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 {
desc: "Link could not be converted to str".to_owned(),
})
},
}
},
None => return Err(RenderError{ desc: "No path found for chapter in JSON data".to_owned() })
None => {
return Err(RenderError { desc: "No path found for chapter in JSON data".to_owned() })
},
}
debug!("[*]: Inject in context");
@@ -84,14 +90,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 { desc: "Error with the handlebars template".to_owned() }),
}
}
break;
}
else {
} else {
previous = Some(item.clone());
}
},
@@ -117,14 +122,14 @@ pub fn next(c: &Context, _h: &Helper, r: &Handlebars, rc: &mut RenderContext) ->
let chapters = c.navigate(rc.get_path(), "chapters");
let current = c.navigate(rc.get_path(), "path")
.to_string()
.replace("\"", "");
.to_string()
.replace("\"", "");
debug!("[*]: Decode chapters from JSON");
// 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 { desc: "Could not decode the JSON data".to_owned() }),
};
let mut previous: Option<BTreeMap<String, String>> = None;
@@ -140,7 +145,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 { desc: "No path found for chapter in JSON data".to_owned() }),
};
if previous_path == &current {
@@ -154,8 +159,10 @@ 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 { desc: "No title found for chapter in JSON data".to_owned() })
},
}
@@ -163,8 +170,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 { desc: "Link could not converted to str".to_owned() }),
}
debug!("[*]: Inject in context");
@@ -178,10 +188,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 { desc: "Error with the handlebars template".to_owned() }),
}
break
break;
}
}

View File

@@ -0,0 +1,200 @@
use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::Read;
pub fn render_playpen(s: &str, path: &Path) -> String {
// When replacing one thing in a string by something with a different length, the indices
// after that will not correspond, we therefore have to store the difference to correct this
let mut previous_end_index = 0;
let mut replaced = String::new();
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[playpen.start_index..playpen.end_index]);
previous_end_index = playpen.end_index;
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;
}
// Open file & read file
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;
};
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);
}
replaced.push_str(&s[previous_end_index..]);
replaced
}
#[derive(PartialOrd, PartialEq, Debug)]
struct Playpen {
start_index: usize,
end_index: usize,
rust_file: PathBuf,
editable: bool,
escaped: bool,
}
fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> {
let mut playpens = vec![];
for (i, _) in s.match_indices("{{#playpen") {
debug!("[*]: find_playpen");
let mut escaped = false;
if i > 0 {
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;
debug!("s[{}..{}] = {}", i, end_i, s[i..end_i].to_string());
// If there is nothing between "{{#playpen" and "}}" skip
if end_i - 2 - (i + 10) < 1 {
continue;
}
if s[i + 10..end_i - 2].trim().len() == 0 {
continue;
}
debug!("{}", s[i + 10..end_i - 2].to_string());
// Split on whitespaces
let params: Vec<&str> = s[i + 10..end_i - 2].split_whitespace().collect();
let mut editable = false;
if params.len() > 1 {
editable = if let Some(_) = params[1].find("editable") {
true
} else {
false
};
}
playpens.push(Playpen {
start_index: i,
end_index: end_i,
rust_file: base_path.join(PathBuf::from(params[0])),
editable: editable,
escaped: escaped,
})
}
playpens
}
// ---------------------------------------------------------------------------------
// Tests
//
#[test]
fn test_find_playpens_no_playpen() {
let s = "Some random text without playpen...";
assert!(find_playpens(s, Path::new("")) == vec![]);
}
#[test]
fn test_find_playpens_partial_playpen() {
let s = "Some random text with {{#playpen...";
assert!(find_playpens(s, Path::new("")) == vec![]);
}
#[test]
fn test_find_playpens_empty_playpen() {
let s = "Some random text with {{#playpen}} and {{#playpen }}...";
assert!(find_playpens(s, Path::new("")) == vec![]);
}
#[test]
fn test_find_playpens_simple_playpen() {
let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";
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,
}]);
}
#[test]
fn test_find_playpens_complex_playpen() {
let s = "Some random text with {{#playpen file.rs editable}} and {{#playpen test.rs editable }}...";
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,
}]);
}
#[test]
fn test_find_playpens_escaped_playpen() {
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("")));
assert!(find_playpens(s, Path::new("")) ==
vec![
Playpen{start_index: 39, end_index: 68, rust_file: PathBuf::from("file.rs"), editable: true, escaped: true},
]);
}

View File

@@ -1,130 +1,131 @@
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 == &current {
try!(rc.writer.write(" class=\"active\"".as_bytes()));
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;
}
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 == &current {
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()));
current_level = level;
}
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(())
}
}

View File

@@ -1,5 +1,5 @@
use std::error::Error;
pub trait Renderer {
fn render(&self, book: &::book::MDBook) -> Result<(), Box<Error>>;
fn render(&self, book: &::book::MDBook) -> Result<(), Box<Error>>;
}

View File

@@ -25,6 +25,17 @@ h5 {
.header + .header h5 {
margin-top: 1em;
}
table {
margin: 0 auto;
border-collapse: collapse;
}
table td {
padding: 3px 20px;
border: 1px solid;
}
table thead td {
font-weight: 700;
}
.sidebar {
position: absolute;
left: 0;
@@ -240,8 +251,9 @@ h5 {
right: 15px;
}
.theme-popup {
position: fixed;
left: -40px;
position: relative;
left: 10px;
z-index: 1000;
-webkit-border-radius: 4px;
border-radius: 4px;
font-size: 0.7em;
@@ -252,6 +264,15 @@ h5 {
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;
@@ -285,29 +306,18 @@ h5 {
}
}
.light {
/* Inline code */
color: #333;
background-color: #fff;
/* Inline code */
}
.light :not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
-webkit-border-radius: 3px;
border-radius: 3px;
.light .content .header:link,
.light .content .header:visited {
color: #333;
pointer: cursor;
}
.light pre {
position: relative;
}
.light pre > i {
position: absolute;
right: 5px;
top: 5px;
color: #364149;
cursor: pointer;
}
.light pre > i :hover {
color: #008cff;
.light .content .header:link:hover,
.light .content .header:visited:hover {
text-decoration: none;
}
.light .sidebar {
background-color: #fafafa;
@@ -351,36 +361,78 @@ h5 {
color: #4183c4;
}
.light .theme-popup {
color: #333;
background: #fafafa;
border: 1px solid #ccc;
}
.light .theme-popup .theme:hover {
background-color: #e6e6e6;
}
.coal {
/* Inline code */
color: #98a3ad;
background-color: #141617;
.light .theme-popup .default {
color: #ccc;
}
.coal :not(pre) > .hljs {
.light blockquote {
margin: 20px 0;
padding: 0 20px;
color: #333;
background-color: #f2f7f9;
border-top: 0.1em solid #e1edf1;
border-bottom: 0.1em solid #e1edf1;
}
.light table td {
border-color: #f2f2f2;
}
.light table tbody tr:nth-child(2n) {
background: #f7f7f7;
}
.light table thead {
background: #ccc;
}
.light table thead td {
border: none;
}
.light table thead tr {
border: 1px #ccc solid;
}
.light :not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
-webkit-border-radius: 3px;
border-radius: 3px;
}
.coal pre {
.light pre {
position: relative;
}
.coal pre > i {
.light pre > .buttons {
position: absolute;
right: 5px;
top: 5px;
color: #a1adb8;
color: #364149;
cursor: pointer;
}
.coal pre > i :hover {
color: #3473ad;
.light pre > .buttons :hover {
color: #008cff;
}
.light pre > .buttons i {
margin-left: 8px;
}
.light pre > .result {
margin-top: 10px;
}
.coal {
color: #98a3ad;
background-color: #141617;
/* Inline code */
}
.coal .content .header:link,
.coal .content .header:visited {
color: #98a3ad;
pointer: cursor;
}
.coal .content .header:link:hover,
.coal .content .header:visited:hover {
text-decoration: none;
}
.coal .sidebar {
background-color: #292c2f;
@@ -424,36 +476,78 @@ h5 {
color: #2b79a2;
}
.coal .theme-popup {
color: #98a3ad;
background: #141617;
border: 1px solid #43484d;
}
.coal .theme-popup .theme:hover {
background-color: #1f2124;
}
.navy {
/* Inline code */
color: #bcbdd0;
background-color: #161923;
.coal .theme-popup .default {
color: #43484d;
}
.navy :not(pre) > .hljs {
.coal blockquote {
margin: 20px 0;
padding: 0 20px;
color: #98a3ad;
background-color: #242637;
border-top: 0.1em solid #2c2f44;
border-bottom: 0.1em solid #2c2f44;
}
.coal table td {
border-color: #1f2223;
}
.coal table tbody tr:nth-child(2n) {
background: #1b1d1e;
}
.coal table thead {
background: #3f4649;
}
.coal table thead td {
border: none;
}
.coal table thead tr {
border: 1px #3f4649 solid;
}
.coal :not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
-webkit-border-radius: 3px;
border-radius: 3px;
}
.navy pre {
.coal pre {
position: relative;
}
.navy pre > i {
.coal pre > .buttons {
position: absolute;
right: 5px;
top: 5px;
color: #c8c9db;
color: #a1adb8;
cursor: pointer;
}
.navy pre > i :hover {
color: #2b79a2;
.coal pre > .buttons :hover {
color: #3473ad;
}
.coal pre > .buttons i {
margin-left: 8px;
}
.coal pre > .result {
margin-top: 10px;
}
.navy {
color: #bcbdd0;
background-color: #161923;
/* Inline code */
}
.navy .content .header:link,
.navy .content .header:visited {
color: #bcbdd0;
pointer: cursor;
}
.navy .content .header:link:hover,
.navy .content .header:visited:hover {
text-decoration: none;
}
.navy .sidebar {
background-color: #282d3f;
@@ -497,36 +591,78 @@ h5 {
color: #2b79a2;
}
.navy .theme-popup {
color: #bcbdd0;
background: #161923;
border: 1px solid #737480;
}
.navy .theme-popup .theme:hover {
background-color: #282e40;
}
.rust {
/* Inline code */
color: #262625;
background-color: #e1e1db;
.navy .theme-popup .default {
color: #737480;
}
.rust :not(pre) > .hljs {
.navy blockquote {
margin: 20px 0;
padding: 0 20px;
color: #bcbdd0;
background-color: #262933;
border-top: 0.1em solid #2f333f;
border-bottom: 0.1em solid #2f333f;
}
.navy table td {
border-color: #1f2331;
}
.navy table tbody tr:nth-child(2n) {
background: #1b1f2b;
}
.navy table thead {
background: #39415b;
}
.navy table thead td {
border: none;
}
.navy table thead tr {
border: 1px #39415b solid;
}
.navy :not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
-webkit-border-radius: 3px;
border-radius: 3px;
}
.rust pre {
.navy pre {
position: relative;
}
.rust pre > i {
.navy pre > .buttons {
position: absolute;
right: 5px;
top: 5px;
color: #c8c9db;
cursor: pointer;
}
.rust pre > i :hover {
color: #e69f67;
.navy pre > .buttons :hover {
color: #2b79a2;
}
.navy pre > .buttons i {
margin-left: 8px;
}
.navy pre > .result {
margin-top: 10px;
}
.rust {
color: #262625;
background-color: #e1e1db;
/* Inline code */
}
.rust .content .header:link,
.rust .content .header:visited {
color: #262625;
pointer: cursor;
}
.rust .content .header:link:hover,
.rust .content .header:visited:hover {
text-decoration: none;
}
.rust .sidebar {
background-color: #3b2e2a;
@@ -570,9 +706,62 @@ h5 {
color: #2b79a2;
}
.rust .theme-popup {
color: #262625;
background: #e1e1db;
border: 1px solid #b38f6b;
}
.rust .theme-popup .theme:hover {
background-color: #99908a;
}
.rust .theme-popup .default {
color: #737480;
}
.rust blockquote {
margin: 20px 0;
padding: 0 20px;
color: #262625;
background-color: #c1c1bb;
border-top: 0.1em solid #b8b8b1;
border-bottom: 0.1em solid #b8b8b1;
}
.rust table td {
border-color: #d7d7cf;
}
.rust table tbody tr:nth-child(2n) {
background: #dbdbd4;
}
.rust table thead {
background: #b3a497;
}
.rust table thead td {
border: none;
}
.rust table thead tr {
border: 1px #b3a497 solid;
}
.rust :not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
-webkit-border-radius: 3px;
border-radius: 3px;
}
.rust pre {
position: relative;
}
.rust pre > .buttons {
position: absolute;
right: 5px;
top: 5px;
color: #c8c9db;
cursor: pointer;
}
.rust pre > .buttons :hover {
color: #e69f67;
}
.rust pre > .buttons i {
margin-left: 8px;
}
.rust pre > .result {
margin-top: 10px;
}

View File

@@ -32,11 +32,15 @@ $( document ).ready(function() {
switch (e.keyCode) {
case KEY_CODES.NEXT_KEY:
e.preventDefault();
window.location.href = $('.nav-chapters.next').attr('href');
if($('.nav-chapters.next').length) {
window.location.href = $('.nav-chapters.next').attr('href');
}
break;
case KEY_CODES.PREVIOUS_KEY:
e.preventDefault();
window.location.href = $('.nav-chapters.previous').attr('href');
if($('.nav-chapters.previous').length) {
window.location.href = $('.nav-chapters.previous').attr('href');
}
break;
}
});
@@ -52,6 +56,8 @@ $( document ).ready(function() {
content.find("h1, h2, h3, h4, h5").wrap(function(){
var wrapper = $("<a class=\"header\">");
wrapper.attr("name", $(this).text());
// Add so that when you click the link actually shows up in the url bar...
wrapper.attr("href", $(location).attr('href') + "#" + $(this).text());
return wrapper;
});
@@ -99,13 +105,13 @@ $( document ).ready(function() {
$('.theme-popup').remove();
} else {
var popup = $('<div class="theme-popup"></div>')
.append($('<div class="theme" id="light">Light (default)<div>'))
.append($('<div class="theme" id="light">Light <span class="default">(default)</span><div>'))
.append($('<div class="theme" id="rust">Rust</div>'))
.append($('<div class="theme" id="coal">Coal</div>'))
.append($('<div class="theme" id="navy">Navy</div>'));
$(this).append(popup);
popup.insertAfter(this);
$('.theme').click(function(){
var theme = $(this).attr('id');
@@ -136,18 +142,20 @@ $( document ).ready(function() {
$("code.language-rust").each(function(i, block){
var code_block = $(this);
var pre_block = $(this).parent();
// hide lines
var lines = $(this).html().split("\n");
var lines = code_block.html().split("\n");
var first_non_hidden_line = false;
var lines_hidden = false;
for(var n = 0; n < lines.length; n++){
if($.trim(lines[n])[0] == hiding_character){
if(first_non_hidden_line){
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].substr(1) + "</span>";
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)#/, "$1") + "</span>";
}
else {
lines[n] = "<span class=\"hidden\">" + lines[n].substr(1) + "\n" + "</span>";
lines[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)#/, "$1") + "\n" + "</span>";
}
lines_hidden = true;
}
@@ -158,25 +166,65 @@ $( document ).ready(function() {
first_non_hidden_line = true;
}
}
$(this).html(lines.join(""));
code_block.html(lines.join(""));
// If no lines were hidden, return
if(!lines_hidden) { return; }
// add expand button
$(this).parent().prepend("<i class=\"fa fa-expand\"></i>");
pre_block.prepend("<div class=\"buttons\"><i class=\"fa fa-expand\"></i></div>");
$(this).parent().find("i").click(function(e){
pre_block.find("i").click(function(e){
if( $(this).hasClass("fa-expand") ) {
$(this).removeClass("fa-expand").addClass("fa-compress");
$(this).parent().find("span.hidden").removeClass("hidden").addClass("unhidden");
pre_block.find("span.hidden").removeClass("hidden").addClass("unhidden");
}
else {
$(this).removeClass("fa-compress").addClass("fa-expand");
$(this).parent().find("span.unhidden").removeClass("unhidden").addClass("hidden");
pre_block.find("span.unhidden").removeClass("unhidden").addClass("hidden");
}
});
});
// Process playpen code blocks
$(".playpen").each(function(block){
var pre_block = $(this);
// Add play button
var buttons = pre_block.find(".buttons");
if( buttons.length === 0 ) {
pre_block.prepend("<div class=\"buttons\"></div>");
buttons = pre_block.find(".buttons");
}
buttons.prepend("<i class=\"fa fa-play play-button\"></i>");
buttons.find(".play-button").click(function(e){
run_rust_code(pre_block);
});
});
});
function run_rust_code(code_block) {
var result_block = code_block.find(".result");
if(result_block.length === 0) {
code_block.append("<code class=\"result hljs language-bash\"></code>");
result_block = code_block.find(".result");
}
result_block.text("Running...");
$.ajax({
url: "https://play.rust-lang.org/evaluate.json",
method: "POST",
crossDomain: true,
dataType: "json",
contentType: "application/json",
data: JSON.stringify({version: "stable", optimize: "0", code: code_block.find(".language-rust").text() }),
success: function(response){
result_block.text(response.result);
}
});
}

BIN
src/theme/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -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,7 +24,7 @@
<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"));
@@ -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>

View File

@@ -2,10 +2,10 @@ use std::path::Path;
use std::fs::File;
use std::io::Read;
use utils::{PathExt};
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");
@@ -28,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>,
@@ -42,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(),
@@ -51,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
@@ -71,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
@@ -80,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();

View File

@@ -19,3 +19,17 @@ h2, h3 { margin-top: 2.5em }
h4, h5 { margin-top: 2em }
.header + .header h3, .header + .header h4, .header + .header h5 { margin-top: 1em }
table {
margin: 0 auto;
border-collapse: collapse;
td {
padding: 3px 20px;
border: 1px solid;
}
thead {
td { font-weight: 700; }
}
}

View File

@@ -1,7 +1,9 @@
.theme-popup {
position: fixed
left: -40px
position: relative
left: 10px
z-index: 1000;
border-radius: 4px
font-size: 0.7em

View File

@@ -1,31 +1,17 @@
.{unquote($theme-name)} {
/* Inline code */
:not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
border-radius: 3px;
}
pre {
position: relative;
}
pre > i {
position: absolute;
right: 5px;
top: 5px;
color: $sidebar-fg;
cursor: pointer;
:hover {
color: $sidebar-active;
}
}
color: $fg
background-color: $bg
.content .header:link, .content .header:visited {
color: $fg;
pointer: cursor;
&:hover {
text-decoration: none;
}
}
.sidebar {
background-color: $sidebar-bg
color: $sidebar-fg
@@ -75,9 +61,68 @@
}
.theme-popup {
color: $fg
background: $theme-popup-bg
border: 1px solid $theme-popup-border
.theme:hover { background-color: $theme-hover }
.default { color: $icons }
}
blockquote {
margin: 20px 0;
padding: 0 20px;
color: $fg;
background-color: $quote-bg;
border-top: .1em solid $quote-border;
border-bottom: .1em solid $quote-border;
}
table {
td {
border-color: $table-border-color;
}
// Alternate background colors for rows
tbody tr:nth-child(2n) {
background: $table-alternate-bg;
}
thead {
background: $table-header-bg;
td { border: none; }
tr { border: 1px $table-header-bg solid; }
}
}
/* Inline code */
:not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
border-radius: 3px;
}
pre {
position: relative;
& > .buttons {
position: absolute;
right: 5px;
top: 5px;
color: $sidebar-fg;
cursor: pointer;
:hover { color: $sidebar-active; }
i { margin-left: 8px; }
}
& > .result { margin-top: 10px; }
}
}

View File

@@ -18,4 +18,11 @@ $theme-popup-bg = #141617
$theme-popup-border = #43484d
$theme-hover = #1f2124
$quote-bg = #242637
$quote-border = lighten($quote-bg, 5%)
$table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
@import 'base'

View File

@@ -18,4 +18,11 @@ $theme-popup-bg = #fafafa
$theme-popup-border = #cccccc
$theme-hover = #e6e6e6
$quote-bg = #f2f7f9
$quote-border = darken($quote-bg, 5%)
$table-border-color = darken($bg, 5%)
$table-header-bg = darken($bg, 20%)
$table-alternate-bg = darken($bg, 3%)
@import 'base'

View File

@@ -18,4 +18,11 @@ $theme-popup-bg = #161923
$theme-popup-border = #737480
$theme-hover = #282e40
$quote-bg = #262933
$quote-border = lighten($quote-bg, 5%)
$table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
@import 'base'

View File

@@ -18,4 +18,11 @@ $theme-popup-bg = #e1e1db
$theme-popup-border = #b38f6b
$theme-hover = #99908a
$quote-bg = #c1c1bb
$quote-border = darken($quote-bg, 5%)
$table-border-color = darken($bg, 5%)
$table-header-bg = #b3a497
$table-alternate-bg = darken($bg, 3%)
@import 'base'

238
src/utils/fs.rs Normal file
View 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")
}
}
}

View File

@@ -1,185 +1,6 @@
extern crate pulldown_cmark;
pub mod fs;
use std::path::{Path, PathBuf, Component};
use std::error::Error;
use std::fs::{self, metadata, File};
use self::pulldown_cmark::{Parser, html};
/// This is copied from the rust source code until Path_ Ext stabilizes.
/// You can use it, but be aware that it will be removed when those features go to rust stable
pub trait PathExt {
fn exists(&self) -> bool;
fn is_file(&self) -> bool;
fn is_dir(&self) -> bool;
}
impl PathExt for Path {
fn exists(&self) -> bool {
metadata(self).is_ok()
}
fn is_file(&self) -> bool {
metadata(self).map(|s| s.is_file()).unwrap_or(false)
}
fn is_dir(&self) -> bool {
metadata(self).map(|s| s.is_dir()).unwrap_or(false)
}
}
/// 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 checks for every component in a path if the directory exists,
/// if it does not it is created.
pub fn create_path(path: &Path) -> Result<(), Box<Error>> {
debug!("[fn]: create_path");
// Create directories if they do not exist
let mut constructed_path = PathBuf::new();
for component in path.components() {
let dir;
match component {
Component::Normal(_) => { dir = PathBuf::from(component.as_os_str()); },
Component::RootDir => {
debug!("[*]: Root directory");
// This doesn't look very compatible with Windows...
constructed_path.push("/");
continue
},
_ => continue,
}
constructed_path.push(&dir);
debug!("[*]: {:?}", constructed_path);
if !constructed_path.exists() || !constructed_path.is_dir() {
try!(fs::create_dir(&constructed_path));
debug!("[*]: Directory created {:?}", constructed_path);
} else {
debug!("[*]: Directory exists {:?}", constructed_path);
continue
}
}
debug!("[*]: Constructed path: {:?}", constructed_path);
Ok(())
}
/// 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() {
try!(create_path(p));
}
debug!("[*]: Create file: {:?}", path);
let f = try!(File::create(path));
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...")));
//try!(create_path(&to.join(entry.path())));
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};
///
@@ -188,57 +9,12 @@ pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blackl
pub fn render_markdown(text: &str) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2);
let p = Parser::new(&text);
let mut opts = Options::empty();
opts.insert(OPTION_ENABLE_TABLES);
opts.insert(OPTION_ENABLE_FOOTNOTES);
let p = Parser::new_ext(&text, opts);
html::push_html(&mut s, p);
s
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
// tests
#[cfg(test)]
mod tests {
extern crate tempdir;
use super::copy_files_except_ext;
use super::PathExt;
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") }
}
}