Merge branch 'master' into serve_static
This commit is contained in:
commit
80fa26db27
|
@ -57,15 +57,6 @@ matrix:
|
||||||
- os: linux
|
- os: linux
|
||||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=nightly
|
env: TARGET=x86_64-unknown-linux-musl CHANNEL=nightly
|
||||||
|
|
||||||
# Musl builds fail due to a bug in Rust (https://github.com/azerupi/mdBook/issues/158)
|
|
||||||
allow_failures:
|
|
||||||
- os: linux
|
|
||||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=stable
|
|
||||||
- os: linux
|
|
||||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=beta
|
|
||||||
- os: linux
|
|
||||||
env: TARGET=x86_64-unknown-linux-musl CHANNEL=nightly
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
- export PATH="$PATH:$HOME/.cargo/bin"
|
- export PATH="$PATH:$HOME/.cargo/bin"
|
||||||
- bash ci/install.sh
|
- bash ci/install.sh
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
Welcome stranger!
|
||||||
|
|
||||||
|
If you have come here to learn how to contribute to mdBook, we have some tips for you!
|
||||||
|
|
||||||
|
First of all, don't hesitate to ask questions!
|
||||||
|
Use the [issue tracker](https://github.com/azerupi/mdBook/issues), no question is too simple.
|
||||||
|
If we don't respond in a couple of days, ping us @azerupi, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
|
||||||
|
|
||||||
|
### Issues to work on
|
||||||
|
|
||||||
|
Any issue is up for the grabbing, but if you are starting out, you might be interested in the
|
||||||
|
[E-Easy issues](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
||||||
|
Those are issues that are considered more straightforward for beginners to Rust or the codebase itself.
|
||||||
|
These issues can be a good launching pad for more involved issues. Easy tasks for a first time contribution
|
||||||
|
include documentation improvements, new tests, examples, updating dependencies, etc.
|
||||||
|
|
||||||
|
If you come from a web development background, you might be interested in issues related to web technologies tagged
|
||||||
|
[A-JavaScript](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||||
|
[A-Style](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
|
||||||
|
[A-HTML](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
||||||
|
[A-Mobile](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
|
||||||
|
|
||||||
|
When you decide you want to work on a specific issue, ping us on that issue so that we can assign it to you.
|
||||||
|
Again, do not hesitate to ask questions. We will gladly mentor anyone that want to tackle an issue.
|
||||||
|
|
||||||
|
Issues on the issue tracker are categorized with the following labels:
|
||||||
|
|
||||||
|
- **A**-prefixed labels state which area of the project an issue relates to.
|
||||||
|
- **E**-prefixed labels show an estimate of the experience necessary to fix the issue.
|
||||||
|
- **M**-prefixed labels are meta-issues used for questions, discussions, or tracking issues
|
||||||
|
- **S**-prefixed labels show the status of the issue
|
||||||
|
- **T**-prefixed labels show the type of issue
|
||||||
|
|
||||||
|
### Building mdBook
|
||||||
|
|
||||||
|
mdBook builds on stable Rust, if you want to build mdBook from source, here are the steps to follow:
|
||||||
|
|
||||||
|
1. Navigate to the directory of your choice
|
||||||
|
0. Clone this repository with git.
|
||||||
|
|
||||||
|
```
|
||||||
|
git clone https://github.com/azerupi/mdBook.git
|
||||||
|
```
|
||||||
|
0. Navigate into the newly created `mdBook` directory
|
||||||
|
0. Run `cargo build`
|
||||||
|
|
||||||
|
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
|
||||||
|
|
||||||
|
|
||||||
|
### Making changes to the style
|
||||||
|
|
||||||
|
mdBook doesn't use CSS directly but uses [Stylus](http://stylus-lang.com/), a CSS-preprocessor which compiles to CSS.
|
||||||
|
|
||||||
|
When you want to change the style, it is important to not change the CSS directly because any manual modification to
|
||||||
|
the CSS files will be overwritten when compiling the stylus files. Instead, you should make your changes directly in the
|
||||||
|
[stylus files](https://github.com/azerupi/mdBook/tree/master/src/theme/stylus) and regenerate the CSS.
|
||||||
|
|
||||||
|
For this to work, you first need [Node and NPM](https://nodejs.org/en/) installed on your machine.
|
||||||
|
Then run the following command to install both [stylus](http://stylus-lang.com/) and [nib](https://tj.github.io/nib/), you might need `sudo` to install successfully.
|
||||||
|
|
||||||
|
```
|
||||||
|
npm install -g stylus nib
|
||||||
|
```
|
||||||
|
|
||||||
|
When that finished, you can simply regenerate the CSS files by building mdBook with the following command:
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo build --features=regenerate-css
|
||||||
|
```
|
||||||
|
|
||||||
|
This should automatically call the appropriate stylus command to recompile the files to CSS and include them in the project.
|
||||||
|
|
||||||
|
### Making a pull-request
|
||||||
|
|
||||||
|
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
|
||||||
|
One of the core maintainers will then approve the changes or request some changes before it gets merged.
|
||||||
|
|
||||||
|
If you want to make your pull-request even better, you might want to run [Clippy](https://github.com/Manishearth/rust-clippy)
|
||||||
|
and [rustfmt](https://github.com/rust-lang-nursery/rustfmt) on the code first.
|
||||||
|
This is not a requirement though and will never block a pull-request from being merged.
|
||||||
|
|
||||||
|
That's it, happy contributions! :tada: :tada: :tada:
|
18
Cargo.toml
18
Cargo.toml
|
@ -15,14 +15,15 @@ exclude = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "2.19.2"
|
clap = "2.24"
|
||||||
handlebars = { version = "0.25.0", features = ["serde_type"] }
|
handlebars = "0.26"
|
||||||
serde = "0.9"
|
serde = "1.0"
|
||||||
serde_json = "0.9"
|
serde_derive = "1.0"
|
||||||
pulldown-cmark = "0.0.8"
|
serde_json = "1.0"
|
||||||
|
pulldown-cmark = "0.0.14"
|
||||||
log = "0.3"
|
log = "0.3"
|
||||||
env_logger = "0.4.0"
|
env_logger = "0.4.0"
|
||||||
toml = { version = "0.3", features = ["serde"] }
|
toml = { version = "0.4", features = ["serde"] }
|
||||||
open = "1.1"
|
open = "1.1"
|
||||||
regex = "0.2.1"
|
regex = "0.2.1"
|
||||||
|
|
||||||
|
@ -35,12 +36,15 @@ crossbeam = { version = "0.2.8", optional = true }
|
||||||
iron = { version = "0.5", optional = true }
|
iron = { version = "0.5", optional = true }
|
||||||
mount = { version = "0.3", optional = true }
|
mount = { version = "0.3", optional = true }
|
||||||
staticfile = { version = "0.4", optional = true }
|
staticfile = { version = "0.4", optional = true }
|
||||||
ws = { version = "0.6", optional = true}
|
ws = { version = "0.7", optional = true}
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempdir = "0.3.4"
|
tempdir = "0.3.4"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
error-chain = "0.10"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["output", "watch", "serve"]
|
default = ["output", "watch", "serve"]
|
||||||
debug = []
|
debug = []
|
||||||
|
|
11
README.md
11
README.md
|
@ -23,9 +23,6 @@
|
||||||
|
|
||||||
mdBook is a utility to create modern online books from Markdown files.
|
mdBook is a utility to create modern online books from Markdown files.
|
||||||
|
|
||||||
**This project is still evolving.**
|
|
||||||
See [#90](https://github.com/azerupi/mdBook/issues/90)
|
|
||||||
|
|
||||||
|
|
||||||
## What does it look like?
|
## What does it look like?
|
||||||
|
|
||||||
|
@ -114,13 +111,11 @@ See the [Documentation](http://azerupi.github.io/mdBook/lib/lib.html) and the [A
|
||||||
|
|
||||||
Contributions are highly appreciated and encouraged! Don't hesitate to participate to discussions in the issues, propose new features and ask for help.
|
Contributions are highly appreciated and encouraged! Don't hesitate to participate to discussions in the issues, propose new features and ask for help.
|
||||||
|
|
||||||
If you are not very confident with Rust, **I will be glad to mentor as best as I can if you decide to tackle an issue or new feature.**
|
If you are just starting out with Rust, there are a series of issus that are tagged [E-Easy](https://github.com/azerupi/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy) and **we will gladly mentor you** so that you can successfully go through the process of fixing a bug or adding a new feature! Let us know if you need any help.
|
||||||
|
|
||||||
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:
|
For more info about contributing, check out our [contribution guide](CONTRIBUTING.md) who helps you go through the build and contribution process!
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
All the code is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE](LICENSE) file.
|
All the code in this repository is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE](LICENSE) file.
|
||||||
|
|
|
@ -2,27 +2,83 @@
|
||||||
|
|
||||||
You can configure the parameters for your book in the ***book.toml*** file.
|
You can configure the parameters for your book in the ***book.toml*** file.
|
||||||
|
|
||||||
We encourage using the TOML format, but JSON is also recognized and parsed.
|
**Note:** JSON configuration files were previously supported but have been deprecated in favor of
|
||||||
|
the TOML configuration file. If you are still using JSON we strongly encourage you to migrate to
|
||||||
|
the TOML configuration because JSON support will be removed in the future.
|
||||||
|
|
||||||
Here is an example of what a ***book.toml*** file might look like:
|
Here is an example of what a ***book.toml*** file might look like:
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
title = "Example book"
|
title = "Example book"
|
||||||
author = "Name"
|
author = "John Doe"
|
||||||
description = "The example book covers examples."
|
description = "The example book covers examples."
|
||||||
dest = "output/my-book"
|
|
||||||
|
[output.html]
|
||||||
|
destination = "my-example-book"
|
||||||
|
additional-css = ["custom.css"]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Supported variables
|
## Supported configuration options
|
||||||
|
|
||||||
If relative paths are given, they will be relative to the book's root, i.e. the
|
It is important to note that **any** relative path specified in the in the configuration will
|
||||||
parent directory of the source directory.
|
always be taken relative from the root of the book where the configuration file is located.
|
||||||
|
|
||||||
- **title:** The title of the book.
|
### General metadata
|
||||||
- **author:** The author of the book.
|
|
||||||
- **description:** The description, which is added as meta in the html head of each page.
|
- **title:** The title of the book
|
||||||
- **src:** The path to the book's source files (chapters in Markdown, SUMMARY.md, etc.). Defaults to `root/src`.
|
- **author:** The author of the book
|
||||||
- **dest:** The path to the directory where you want your book to be rendered. Defaults to `root/book`.
|
- **description:** A description for the book, which is added as meta information in the html `<head>` of each page
|
||||||
- **theme_path:** The path to a custom theme directory. Defaults to `root/theme`.
|
|
||||||
|
**book.toml**
|
||||||
|
```toml
|
||||||
|
title = "Example book"
|
||||||
|
author = "John Doe"
|
||||||
|
description = "The example book covers examples."
|
||||||
|
```
|
||||||
|
|
||||||
|
Some books may have multiple authors, there is an alternative key called `authors` plural that lets you specify an array
|
||||||
|
of authors.
|
||||||
|
|
||||||
|
**book.toml**
|
||||||
|
```toml
|
||||||
|
title = "Example book"
|
||||||
|
authors = ["John Doe", "Jane Doe"]
|
||||||
|
description = "The example book covers examples."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source directory
|
||||||
|
By default, the source directory is found in the directory named `src` directly under the root folder. But this is configurable
|
||||||
|
with the `source` key in the configuration file.
|
||||||
|
|
||||||
|
**book.toml**
|
||||||
|
```toml
|
||||||
|
title = "Example book"
|
||||||
|
authors = ["John Doe", "Jane Doe"]
|
||||||
|
description = "The example book covers examples."
|
||||||
|
|
||||||
|
source = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML renderer options
|
||||||
|
The HTML renderer has a couple of options aswell. All the options for the renderer need to be specified under the TOML table `[output.html]`.
|
||||||
|
The following configuration options are available:
|
||||||
|
|
||||||
|
- **destination:** By default, the HTML book will be rendered in the `root/book` directory, but this option lets you specify another
|
||||||
|
destination fodler.
|
||||||
|
- **theme:** mdBook comes with a default theme and all the resource files needed for it. But if this option is set, mdBook will selectively overwrite the theme files with the ones found in the specified folder.
|
||||||
|
- **google-analytics:** If you use Google Analytics, this option lets you enable it by simply specifying your ID in the configuration file.
|
||||||
|
- **additional-css:** If you need to slightly change the appearance of your book without overwriting the whole style, you can specify a set of stylesheets that will be loaded after the default ones where you can surgically change the style.
|
||||||
|
|
||||||
|
**book.toml**
|
||||||
|
```toml
|
||||||
|
title = "Example book"
|
||||||
|
authors = ["John Doe", "Jane Doe"]
|
||||||
|
description = "The example book covers examples."
|
||||||
|
|
||||||
|
[output.html]
|
||||||
|
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
|
||||||
|
theme = "my-theme"
|
||||||
|
google-analytics = "123456"
|
||||||
|
additional-css = ["custom.css", "custom2.css"]
|
||||||
|
```
|
||||||
|
|
||||||
***note:*** *the supported configurable parameters are scarce at the moment, but more will be added in the future*
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ The default renderer uses a [handlebars](http://handlebarsjs.com/) template to r
|
||||||
included in the mdBook binary.
|
included in the mdBook binary.
|
||||||
|
|
||||||
The theme is totally customizable, you can selectively replace every file from the theme by your own by adding a
|
The theme is totally customizable, you can selectively replace every file from the theme by your own by adding a
|
||||||
`theme` directory in your source folder. Create a new file with the name of the file you want to override
|
`theme` directory next to `src` folder in your project root. Create a new file with the name of the file you want to override
|
||||||
and now that file will be used instead of the default file.
|
and now that file will be used instead of the default file.
|
||||||
|
|
||||||
Here are the files you can override:
|
Here are the files you can override:
|
||||||
|
|
|
@ -19,4 +19,4 @@ fn main() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Check here for the [API docs](../mdbook/index.html) generated by rustdoc.
|
Check here for the [API docs](mdbook/index.html) generated by rustdoc.
|
||||||
|
|
84
build.rs
84
build.rs
|
@ -3,27 +3,93 @@
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate error_chain;
|
||||||
|
|
||||||
fn main() {
|
#[cfg(windows)]
|
||||||
|
mod execs {
|
||||||
|
pub const NPM: &'static str = "npm.cmd";
|
||||||
|
pub const STYLUS: &'static str = "stylus.cmd";
|
||||||
|
}
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
mod execs {
|
||||||
|
pub const NPM: &'static str = "npm";
|
||||||
|
pub const STYLUS: &'static str = "stylus";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
error_chain!{
|
||||||
|
foreign_links {
|
||||||
|
Io(std::io::Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn program_exists(program: &str) -> Result<()> {
|
||||||
|
Command::new(program)
|
||||||
|
.arg("-v")
|
||||||
|
.output()
|
||||||
|
.chain_err(|| format!("Please install '{}'!", program))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn npm_package_exists(package: &str) -> Result<()> {
|
||||||
|
let status = Command::new(execs::NPM)
|
||||||
|
.args(&["list", "-g"])
|
||||||
|
.arg(package)
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match status {
|
||||||
|
Ok(ref out) if out.status.success() => Ok(()),
|
||||||
|
_ => {
|
||||||
|
bail!("Missing npm package '{0}' \
|
||||||
|
install with: 'npm -g install {0}'",
|
||||||
|
package)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Resource<'a> {
|
||||||
|
Program(&'a str),
|
||||||
|
Package(&'a str),
|
||||||
|
}
|
||||||
|
use Resource::{Program, Package};
|
||||||
|
|
||||||
|
impl<'a> Resource<'a> {
|
||||||
|
pub fn exists(&self) -> Result<()> {
|
||||||
|
match *self {
|
||||||
|
Program(name) => program_exists(name),
|
||||||
|
Package(name) => npm_package_exists(name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<()> {
|
||||||
|
|
||||||
if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") {
|
if let Ok(_) = env::var("CARGO_FEATURE_REGENERATE_CSS") {
|
||||||
|
// Check dependencies
|
||||||
|
Program(execs::NPM).exists()?;
|
||||||
|
Program("node").exists().or(Program("nodejs").exists())?;
|
||||||
|
Package("nib").exists()?;
|
||||||
|
Package("stylus").exists()?;
|
||||||
|
|
||||||
// Compile stylus stylesheet to css
|
// Compile stylus stylesheet to css
|
||||||
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
let manifest_dir = env::var("CARGO_MANIFEST_DIR")
|
||||||
|
.chain_err(|| "Please run the script with: 'cargo build'!")?;
|
||||||
let theme_dir = Path::new(&manifest_dir).join("src/theme/");
|
let theme_dir = Path::new(&manifest_dir).join("src/theme/");
|
||||||
let stylus_dir = theme_dir.join("stylus/book.styl");
|
let stylus_dir = theme_dir.join("stylus/book.styl");
|
||||||
|
|
||||||
if !Command::new("stylus")
|
if !Command::new(execs::STYLUS)
|
||||||
.arg(format!("{}", stylus_dir.to_str().unwrap()))
|
.arg(stylus_dir)
|
||||||
.arg("--out")
|
.arg("--out")
|
||||||
.arg(format!("{}", theme_dir.to_str().unwrap()))
|
.arg(theme_dir)
|
||||||
.arg("--use")
|
.arg("--use")
|
||||||
.arg("nib")
|
.arg("nib")
|
||||||
.status().unwrap()
|
.status()?
|
||||||
.success() {
|
.success() {
|
||||||
panic!("Stylus encoutered an error");
|
bail!("Stylus encoutered an error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quick_main!(run);
|
||||||
|
|
|
@ -10,6 +10,7 @@ enum_trailing_comma = true
|
||||||
match_block_trailing_comma = true
|
match_block_trailing_comma = true
|
||||||
struct_trailing_comma = "Always"
|
struct_trailing_comma = "Always"
|
||||||
wrap_comments = true
|
wrap_comments = true
|
||||||
|
use_try_shorthand = true
|
||||||
|
|
||||||
report_todo = "Always"
|
report_todo = "Always"
|
||||||
report_fixme = "Always"
|
report_fixme = "Always"
|
||||||
|
|
|
@ -124,7 +124,7 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
let mut book = MDBook::new(&book_dir);
|
let mut book = MDBook::new(&book_dir);
|
||||||
|
|
||||||
// Call the function that does the initialization
|
// Call the function that does the initialization
|
||||||
try!(book.init());
|
book.init()?;
|
||||||
|
|
||||||
// If flag `--theme` is present, copy theme to src
|
// If flag `--theme` is present, copy theme to src
|
||||||
if args.is_present("theme") {
|
if args.is_present("theme") {
|
||||||
|
@ -132,7 +132,7 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
// Skip this if `--force` is present
|
// Skip this if `--force` is present
|
||||||
if !args.is_present("force") {
|
if !args.is_present("force") {
|
||||||
// Print warning
|
// Print warning
|
||||||
print!("\nCopying the default theme to {:?}", book.get_src());
|
print!("\nCopying the default theme to {:?}", book.get_source());
|
||||||
println!("could potentially overwrite files already present in that directory.");
|
println!("could potentially overwrite files already present in that directory.");
|
||||||
print!("\nAre you sure you want to continue? (y/n) ");
|
print!("\nAre you sure you want to continue? (y/n) ");
|
||||||
|
|
||||||
|
@ -145,13 +145,15 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the function that copies the theme
|
// Call the function that copies the theme
|
||||||
try!(book.copy_theme());
|
book.copy_theme()?;
|
||||||
println!("\nTheme copied.");
|
println!("\nTheme copied.");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`
|
// 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());
|
let is_dest_inside_root = book.get_destination()
|
||||||
|
.map(|p| p.starts_with(book.get_root()))
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
if !args.is_present("force") && is_dest_inside_root {
|
if !args.is_present("force") && is_dest_inside_root {
|
||||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||||
|
@ -171,21 +173,23 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
// Build command implementation
|
// Build command implementation
|
||||||
fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
|
fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
let book = MDBook::new(&book_dir).read_config();
|
let book = MDBook::new(&book_dir).read_config()?;
|
||||||
|
|
||||||
let mut book = match args.value_of("dest-dir") {
|
let mut book = match args.value_of("dest-dir") {
|
||||||
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
|
Some(dest_dir) => book.with_destination(Path::new(dest_dir)),
|
||||||
None => book
|
None => book,
|
||||||
};
|
};
|
||||||
|
|
||||||
if args.is_present("no-create") {
|
if args.is_present("no-create") {
|
||||||
book.create_missing = false;
|
book.create_missing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try!(book.build());
|
book.build()?;
|
||||||
|
|
||||||
if args.is_present("open") {
|
if let Some(d) = book.get_destination() {
|
||||||
open(book.get_dest().join("index.html"));
|
if args.is_present("open") {
|
||||||
|
open(d.join("index.html"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -196,16 +200,18 @@ fn build(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
|
fn watch(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
let book = MDBook::new(&book_dir).read_config();
|
let book = MDBook::new(&book_dir).read_config()?;
|
||||||
|
|
||||||
let mut book = match args.value_of("dest-dir") {
|
let mut book = match args.value_of("dest-dir") {
|
||||||
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
|
Some(dest_dir) => book.with_destination(Path::new(dest_dir)),
|
||||||
None => book
|
None => book,
|
||||||
};
|
};
|
||||||
|
|
||||||
if args.is_present("open") {
|
if args.is_present("open") {
|
||||||
try!(book.build());
|
book.build()?;
|
||||||
open(book.get_dest().join("index.html"));
|
if let Some(d) = book.get_destination() {
|
||||||
|
open(d.join("index.html"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger_on_change(&mut book, |path, book| {
|
trigger_on_change(&mut book, |path, book| {
|
||||||
|
@ -226,15 +232,20 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
const RELOAD_COMMAND: &'static str = "reload";
|
const RELOAD_COMMAND: &'static str = "reload";
|
||||||
|
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
let book = MDBook::new(&book_dir).read_config();
|
let book = MDBook::new(&book_dir).read_config()?;
|
||||||
|
|
||||||
let mut book = match args.value_of("dest-dir") {
|
let mut book = match args.value_of("dest-dir") {
|
||||||
Some(dest_dir) => book.set_dest(Path::new(dest_dir)),
|
Some(dest_dir) => book.with_destination(Path::new(dest_dir)),
|
||||||
None => book
|
None => book,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let None = book.get_destination() {
|
||||||
|
println!("The HTML renderer is not set up, impossible to serve the files.");
|
||||||
|
std::process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
let port = args.value_of("port").unwrap_or("3000");
|
let port = args.value_of("port").unwrap_or("3000");
|
||||||
let ws_port = args.value_of("ws-port").unwrap_or("3001");
|
let ws_port = args.value_of("websocket-port").unwrap_or("3001");
|
||||||
let interface = args.value_of("interface").unwrap_or("localhost");
|
let interface = args.value_of("interface").unwrap_or("localhost");
|
||||||
let public_address = args.value_of("address").unwrap_or(interface);
|
let public_address = args.value_of("address").unwrap_or(interface);
|
||||||
let open_browser = args.is_present("open");
|
let open_browser = args.is_present("open");
|
||||||
|
@ -257,12 +268,16 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
socket.close();
|
socket.close();
|
||||||
}}
|
}}
|
||||||
</script>
|
</script>
|
||||||
"#, public_address, ws_port, RELOAD_COMMAND).to_owned());
|
"#,
|
||||||
|
public_address,
|
||||||
|
ws_port,
|
||||||
|
RELOAD_COMMAND));
|
||||||
|
|
||||||
try!(book.build());
|
book.build()?;
|
||||||
|
|
||||||
|
let staticfile = staticfile::Static::new(book.get_destination().expect("destination is present, checked before"));
|
||||||
let mut mount = mount::Mount::new();
|
let mut mount = mount::Mount::new();
|
||||||
mount.mount("/", staticfile::Static::new(book.get_dest()));
|
mount.mount("/", staticfile);
|
||||||
if let Some(values) = static_objs {
|
if let Some(values) = static_objs {
|
||||||
for value in values {
|
for value in values {
|
||||||
let path = Path::new(value);
|
let path = Path::new(value);
|
||||||
|
@ -276,24 +291,20 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let iron = iron::Iron::new(mount);
|
let iron = iron::Iron::new(mount);
|
||||||
|
|
||||||
let _iron = iron.http(&*address).unwrap();
|
let _iron = iron.http(&*address).unwrap();
|
||||||
|
|
||||||
let ws_server = ws::WebSocket::new(|_| {
|
let ws_server = ws::WebSocket::new(|_| |_| Ok(())).unwrap();
|
||||||
|_| {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}).unwrap();
|
|
||||||
|
|
||||||
let broadcaster = ws_server.broadcaster();
|
let broadcaster = ws_server.broadcaster();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || { ws_server.listen(&*ws_address).unwrap(); });
|
||||||
ws_server.listen(&*ws_address).unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
println!("\nServing on {}", address);
|
let serving_url = format!("http://{}", address);
|
||||||
|
println!("\nServing on: {}", serving_url);
|
||||||
|
|
||||||
if open_browser {
|
if open_browser {
|
||||||
open(format!("http://{}", address));
|
open(serving_url);
|
||||||
}
|
}
|
||||||
|
|
||||||
trigger_on_change(&mut book, move |path, book| {
|
trigger_on_change(&mut book, move |path, book| {
|
||||||
|
@ -311,9 +322,9 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
|
|
||||||
fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
|
fn test(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
let mut book = MDBook::new(&book_dir).read_config();
|
let mut book = MDBook::new(&book_dir).read_config()?;
|
||||||
|
|
||||||
try!(book.test());
|
book.test()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -356,21 +367,31 @@ fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("Error while trying to watch the files:\n\n\t{:?}", e);
|
println!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||||
::std::process::exit(0);
|
::std::process::exit(0);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the source directory to the watcher
|
// Add the source directory to the watcher
|
||||||
if let Err(e) = watcher.watch(book.get_src(), Recursive) {
|
if let Err(e) = watcher.watch(book.get_source(), Recursive) {
|
||||||
println!("Error while watching {:?}:\n {:?}", book.get_src(), e);
|
println!("Error while watching {:?}:\n {:?}", book.get_source(), e);
|
||||||
::std::process::exit(0);
|
::std::process::exit(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add the theme directory to the watcher
|
||||||
|
if let Some(t) = book.get_theme_path() {
|
||||||
|
watcher.watch(t, Recursive).unwrap_or_default();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Add the book.{json,toml} file to the watcher if it exists, because it's not
|
// Add the book.{json,toml} file to the watcher if it exists, because it's not
|
||||||
// located in the source directory
|
// located in the source directory
|
||||||
if watcher.watch(book.get_root().join("book.json"), NonRecursive).is_err() {
|
if watcher
|
||||||
|
.watch(book.get_root().join("book.json"), NonRecursive)
|
||||||
|
.is_err() {
|
||||||
// do nothing if book.json is not found
|
// do nothing if book.json is not found
|
||||||
}
|
}
|
||||||
if watcher.watch(book.get_root().join("book.toml"), NonRecursive).is_err() {
|
if watcher
|
||||||
|
.watch(book.get_root().join("book.toml"), NonRecursive)
|
||||||
|
.is_err() {
|
||||||
// do nothing if book.toml is not found
|
// do nothing if book.toml is not found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -378,16 +399,18 @@ fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match rx.recv() {
|
match rx.recv() {
|
||||||
Ok(event) => match event {
|
Ok(event) => {
|
||||||
NoticeWrite(path) |
|
match event {
|
||||||
NoticeRemove(path) |
|
NoticeWrite(path) |
|
||||||
Create(path) |
|
NoticeRemove(path) |
|
||||||
Write(path) |
|
Create(path) |
|
||||||
Remove(path) |
|
Write(path) |
|
||||||
Rename(_, path) => {
|
Remove(path) |
|
||||||
closure(&path, book);
|
Rename(_, path) => {
|
||||||
|
closure(&path, book);
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("An error occured: {:?}", e);
|
println!("An error occured: {:?}", e);
|
||||||
|
|
|
@ -1,225 +0,0 @@
|
||||||
extern crate toml;
|
|
||||||
|
|
||||||
use std::process::exit;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct BookConfig {
|
|
||||||
root: PathBuf,
|
|
||||||
pub dest: PathBuf,
|
|
||||||
pub src: PathBuf,
|
|
||||||
pub theme_path: PathBuf,
|
|
||||||
|
|
||||||
pub title: String,
|
|
||||||
pub author: String,
|
|
||||||
pub description: String,
|
|
||||||
|
|
||||||
pub indent_spaces: i32,
|
|
||||||
multilingual: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BookConfig {
|
|
||||||
pub fn new(root: &Path) -> Self {
|
|
||||||
BookConfig {
|
|
||||||
root: root.to_owned(),
|
|
||||||
dest: root.join("book"),
|
|
||||||
src: root.join("src"),
|
|
||||||
theme_path: root.join("theme"),
|
|
||||||
|
|
||||||
title: String::new(),
|
|
||||||
author: String::new(),
|
|
||||||
description: String::new(),
|
|
||||||
|
|
||||||
indent_spaces: 4, // indentation used for SUMMARY.md
|
|
||||||
multilingual: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_config(&mut self, root: &Path) -> &mut Self {
|
|
||||||
|
|
||||||
debug!("[fn]: read_config");
|
|
||||||
|
|
||||||
let read_file = |path: PathBuf| -> String {
|
|
||||||
let mut data = String::new();
|
|
||||||
let mut f: File = match File::open(&path) {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(_) => {
|
|
||||||
error!("[*]: Failed to open {:?}", &path);
|
|
||||||
exit(2);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if f.read_to_string(&mut data).is_err() {
|
|
||||||
error!("[*]: Failed to read {:?}", &path);
|
|
||||||
exit(2);
|
|
||||||
}
|
|
||||||
data
|
|
||||||
};
|
|
||||||
|
|
||||||
// Read book.toml or book.json if exists
|
|
||||||
|
|
||||||
if root.join("book.toml").exists() {
|
|
||||||
|
|
||||||
debug!("[*]: Reading config");
|
|
||||||
let data = read_file(root.join("book.toml"));
|
|
||||||
self.parse_from_toml_string(&data);
|
|
||||||
|
|
||||||
} else if root.join("book.json").exists() {
|
|
||||||
|
|
||||||
debug!("[*]: Reading config");
|
|
||||||
let data = read_file(root.join("book.json"));
|
|
||||||
self.parse_from_json_string(&data);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
debug!("[*]: No book.toml or book.json was found, using defaults.");
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_from_toml_string(&mut self, data: &str) -> &mut Self {
|
|
||||||
let config = match toml::from_str(data) {
|
|
||||||
Ok(x) => {x},
|
|
||||||
Err(e) => {
|
|
||||||
error!("[*]: Toml parse errors in book.toml: {:?}", e);
|
|
||||||
exit(2);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.parse_from_btreemap(&config);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses the string to JSON and converts it to BTreeMap<String, toml::Value>.
|
|
||||||
pub fn parse_from_json_string(&mut self, data: &str) -> &mut Self {
|
|
||||||
|
|
||||||
let c: serde_json::Value = match serde_json::from_str(data) {
|
|
||||||
Ok(x) => x,
|
|
||||||
Err(e) => {
|
|
||||||
error!("[*]: JSON parse errors in book.json: {:?}", e);
|
|
||||||
exit(2);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = json_object_to_btreemap(c.as_object().unwrap());
|
|
||||||
self.parse_from_btreemap(&config);
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_from_btreemap(&mut self, config: &BTreeMap<String, toml::Value>) -> &mut Self {
|
|
||||||
|
|
||||||
// Title, author, description
|
|
||||||
if let Some(a) = config.get("title") {
|
|
||||||
self.title = a.to_string().replace("\"", "");
|
|
||||||
}
|
|
||||||
if let Some(a) = config.get("author") {
|
|
||||||
self.author = a.to_string().replace("\"", "");
|
|
||||||
}
|
|
||||||
if let Some(a) = config.get("description") {
|
|
||||||
self.description = a.to_string().replace("\"", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destination folder
|
|
||||||
if let Some(a) = config.get("dest") {
|
|
||||||
let mut dest = PathBuf::from(&a.to_string().replace("\"", ""));
|
|
||||||
|
|
||||||
// If path is relative make it absolute from the parent directory of src
|
|
||||||
if dest.is_relative() {
|
|
||||||
dest = self.get_root().join(&dest);
|
|
||||||
}
|
|
||||||
self.set_dest(&dest);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source folder
|
|
||||||
if let Some(a) = config.get("src") {
|
|
||||||
let mut src = PathBuf::from(&a.to_string().replace("\"", ""));
|
|
||||||
if src.is_relative() {
|
|
||||||
src = self.get_root().join(&src);
|
|
||||||
}
|
|
||||||
self.set_src(&src);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Theme path folder
|
|
||||||
if let Some(a) = config.get("theme_path") {
|
|
||||||
let mut theme_path = PathBuf::from(&a.to_string().replace("\"", ""));
|
|
||||||
if theme_path.is_relative() {
|
|
||||||
theme_path = self.get_root().join(&theme_path);
|
|
||||||
}
|
|
||||||
self.set_theme_path(&theme_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_root(&self) -> &Path {
|
|
||||||
&self.root
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_root(&mut self, root: &Path) -> &mut Self {
|
|
||||||
self.root = root.to_owned();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_dest(&self) -> &Path {
|
|
||||||
&self.dest
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_dest(&mut self, dest: &Path) -> &mut Self {
|
|
||||||
self.dest = dest.to_owned();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_src(&self) -> &Path {
|
|
||||||
&self.src
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_src(&mut self, src: &Path) -> &mut Self {
|
|
||||||
self.src = src.to_owned();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_theme_path(&self) -> &Path {
|
|
||||||
&self.theme_path
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_theme_path(&mut self, theme_path: &Path) -> &mut Self {
|
|
||||||
self.theme_path = theme_path.to_owned();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn json_object_to_btreemap(json: &serde_json::Map<String, serde_json::Value>) -> BTreeMap<String, toml::Value> {
|
|
||||||
let mut config: BTreeMap<String, toml::Value> = BTreeMap::new();
|
|
||||||
|
|
||||||
for (key, value) in json.iter() {
|
|
||||||
config.insert(
|
|
||||||
String::from_str(key).unwrap(),
|
|
||||||
json_value_to_toml_value(value.to_owned())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
config
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn json_value_to_toml_value(json: serde_json::Value) -> toml::Value {
|
|
||||||
match json {
|
|
||||||
serde_json::Value::Null => toml::Value::String("".to_string()),
|
|
||||||
serde_json::Value::Bool(x) => toml::Value::Boolean(x),
|
|
||||||
serde_json::Value::Number(ref x) if x.is_i64() => toml::Value::Integer(x.as_i64().unwrap()),
|
|
||||||
serde_json::Value::Number(ref x) if x.is_u64() => toml::Value::Integer(x.as_i64().unwrap()),
|
|
||||||
serde_json::Value::Number(x) => toml::Value::Float(x.as_f64().unwrap()),
|
|
||||||
serde_json::Value::String(x) => toml::Value::String(x),
|
|
||||||
serde_json::Value::Array(x) => {
|
|
||||||
toml::Value::Array(x.iter().map(|v| json_value_to_toml_value(v.to_owned())).collect())
|
|
||||||
},
|
|
||||||
serde_json::Value::Object(x) => {
|
|
||||||
toml::Value::Table(json_object_to_btreemap(&x))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,349 +0,0 @@
|
||||||
#![cfg(test)]
|
|
||||||
|
|
||||||
use std::path::Path;
|
|
||||||
use serde_json;
|
|
||||||
use book::bookconfig::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_parses_json_config() {
|
|
||||||
let text = r#"
|
|
||||||
{
|
|
||||||
"title": "mdBook Documentation",
|
|
||||||
"description": "Create book from markdown files. Like Gitbook but implemented in Rust",
|
|
||||||
"author": "Mathieu David"
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
// TODO don't require path argument, take pwd
|
|
||||||
let mut config = BookConfig::new(Path::new("."));
|
|
||||||
|
|
||||||
config.parse_from_json_string(&text.to_string());
|
|
||||||
|
|
||||||
let mut expected = BookConfig::new(Path::new("."));
|
|
||||||
expected.title = "mdBook Documentation".to_string();
|
|
||||||
expected.author = "Mathieu David".to_string();
|
|
||||||
expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string();
|
|
||||||
|
|
||||||
assert_eq!(format!("{:#?}", config), format!("{:#?}", expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_parses_toml_config() {
|
|
||||||
let text = r#"
|
|
||||||
title = "mdBook Documentation"
|
|
||||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
|
||||||
author = "Mathieu David"
|
|
||||||
"#;
|
|
||||||
|
|
||||||
// TODO don't require path argument, take pwd
|
|
||||||
let mut config = BookConfig::new(Path::new("."));
|
|
||||||
|
|
||||||
config.parse_from_toml_string(&text.to_string());
|
|
||||||
|
|
||||||
let mut expected = BookConfig::new(Path::new("."));
|
|
||||||
expected.title = "mdBook Documentation".to_string();
|
|
||||||
expected.author = "Mathieu David".to_string();
|
|
||||||
expected.description = "Create book from markdown files. Like Gitbook but implemented in Rust".to_string();
|
|
||||||
|
|
||||||
assert_eq!(format!("{:#?}", config), format!("{:#?}", expected));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_parses_json_nested_array_to_toml() {
|
|
||||||
|
|
||||||
// Example from:
|
|
||||||
// toml-0.2.1/tests/valid/arrays-nested.json
|
|
||||||
|
|
||||||
let text = r#"
|
|
||||||
{
|
|
||||||
"nest": {
|
|
||||||
"type": "array",
|
|
||||||
"value": [
|
|
||||||
{"type": "array", "value": [
|
|
||||||
{"type": "string", "value": "a"}
|
|
||||||
]},
|
|
||||||
{"type": "array", "value": [
|
|
||||||
{"type": "string", "value": "b"}
|
|
||||||
]}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
let c: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
||||||
|
|
||||||
let result = json_object_to_btreemap(&c.as_object().unwrap());
|
|
||||||
|
|
||||||
let expected = r#"{
|
|
||||||
"nest": Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"array"
|
|
||||||
),
|
|
||||||
"value": Array(
|
|
||||||
[
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"array"
|
|
||||||
),
|
|
||||||
"value": Array(
|
|
||||||
[
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"string"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"a"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"array"
|
|
||||||
),
|
|
||||||
"value": Array(
|
|
||||||
[
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"string"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"b"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
assert_eq!(format!("{:#?}", result), expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_parses_json_arrays_to_toml() {
|
|
||||||
|
|
||||||
// Example from:
|
|
||||||
// toml-0.2.1/tests/valid/arrays.json
|
|
||||||
|
|
||||||
let text = r#"
|
|
||||||
{
|
|
||||||
"ints": {
|
|
||||||
"type": "array",
|
|
||||||
"value": [
|
|
||||||
{"type": "integer", "value": "1"},
|
|
||||||
{"type": "integer", "value": "2"},
|
|
||||||
{"type": "integer", "value": "3"}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"floats": {
|
|
||||||
"type": "array",
|
|
||||||
"value": [
|
|
||||||
{"type": "float", "value": "1.1"},
|
|
||||||
{"type": "float", "value": "2.1"},
|
|
||||||
{"type": "float", "value": "3.1"}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"strings": {
|
|
||||||
"type": "array",
|
|
||||||
"value": [
|
|
||||||
{"type": "string", "value": "a"},
|
|
||||||
{"type": "string", "value": "b"},
|
|
||||||
{"type": "string", "value": "c"}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dates": {
|
|
||||||
"type": "array",
|
|
||||||
"value": [
|
|
||||||
{"type": "datetime", "value": "1987-07-05T17:45:00Z"},
|
|
||||||
{"type": "datetime", "value": "1979-05-27T07:32:00Z"},
|
|
||||||
{"type": "datetime", "value": "2006-06-01T11:00:00Z"}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
let c: serde_json::Value = serde_json::from_str(&text).unwrap();
|
|
||||||
|
|
||||||
let result = json_object_to_btreemap(&c.as_object().unwrap());
|
|
||||||
|
|
||||||
let expected = r#"{
|
|
||||||
"dates": Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"array"
|
|
||||||
),
|
|
||||||
"value": Array(
|
|
||||||
[
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"datetime"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"1987-07-05T17:45:00Z"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"datetime"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"1979-05-27T07:32:00Z"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"datetime"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"2006-06-01T11:00:00Z"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
"floats": Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"array"
|
|
||||||
),
|
|
||||||
"value": Array(
|
|
||||||
[
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"float"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"1.1"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"float"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"2.1"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"float"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"3.1"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
"ints": Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"array"
|
|
||||||
),
|
|
||||||
"value": Array(
|
|
||||||
[
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"integer"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"1"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"integer"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"2"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"integer"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"3"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
"strings": Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"array"
|
|
||||||
),
|
|
||||||
"value": Array(
|
|
||||||
[
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"string"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"a"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"string"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"b"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Table(
|
|
||||||
{
|
|
||||||
"type": String(
|
|
||||||
"string"
|
|
||||||
),
|
|
||||||
"value": String(
|
|
||||||
"c"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}"#;
|
|
||||||
|
|
||||||
assert_eq!(format!("{:#?}", result), expected);
|
|
||||||
}
|
|
|
@ -37,10 +37,12 @@ impl Chapter {
|
||||||
|
|
||||||
|
|
||||||
impl Serialize for Chapter {
|
impl Serialize for Chapter {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
let mut struct_ = try!(serializer.serialize_struct("Chapter", 2));
|
where S: Serializer
|
||||||
try!(struct_.serialize_field("name", &self.name));
|
{
|
||||||
try!(struct_.serialize_field("path", &self.path));
|
let mut struct_ = serializer.serialize_struct("Chapter", 2)?;
|
||||||
|
struct_.serialize_field("name", &self.name)?;
|
||||||
|
struct_.serialize_field("path", &self.path)?;
|
||||||
struct_.end()
|
struct_.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +68,8 @@ impl<'a> Iterator for BookItems<'a> {
|
||||||
let cur = &self.items[self.current_index];
|
let cur = &self.items[self.current_index];
|
||||||
|
|
||||||
match *cur {
|
match *cur {
|
||||||
BookItem::Chapter(_, ref ch) | BookItem::Affix(ref ch) => {
|
BookItem::Chapter(_, ref ch) |
|
||||||
|
BookItem::Affix(ref ch) => {
|
||||||
self.stack.push((self.items, self.current_index));
|
self.stack.push((self.items, self.current_index));
|
||||||
self.items = &ch.sub_items[..];
|
self.items = &ch.sub_items[..];
|
||||||
self.current_index = 0;
|
self.current_index = 0;
|
||||||
|
|
393
src/book/mod.rs
393
src/book/mod.rs
|
@ -1,32 +1,25 @@
|
||||||
pub mod bookitem;
|
pub mod bookitem;
|
||||||
pub mod bookconfig;
|
|
||||||
|
|
||||||
pub mod bookconfig_test;
|
|
||||||
|
|
||||||
pub use self::bookitem::{BookItem, BookItems};
|
pub use self::bookitem::{BookItem, BookItems};
|
||||||
pub use self::bookconfig::BookConfig;
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Write;
|
use std::io::{Read, Write};
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use {theme, parse, utils};
|
use {theme, parse, utils};
|
||||||
use renderer::{Renderer, HtmlHandlebars};
|
use renderer::{Renderer, HtmlHandlebars};
|
||||||
|
|
||||||
|
use config::{BookConfig, HtmlConfig};
|
||||||
|
use config::tomlconfig::TomlConfig;
|
||||||
|
use config::jsonconfig::JsonConfig;
|
||||||
|
|
||||||
|
|
||||||
pub struct MDBook {
|
pub struct MDBook {
|
||||||
root: PathBuf,
|
config: BookConfig,
|
||||||
dest: PathBuf,
|
|
||||||
src: PathBuf,
|
|
||||||
theme_path: PathBuf,
|
|
||||||
|
|
||||||
pub title: String,
|
|
||||||
pub author: String,
|
|
||||||
pub description: String,
|
|
||||||
|
|
||||||
pub content: Vec<BookItem>,
|
pub content: Vec<BookItem>,
|
||||||
renderer: Box<Renderer>,
|
renderer: Box<Renderer>,
|
||||||
|
@ -52,8 +45,10 @@ impl MDBook {
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// In this example, `root_dir` will be the root directory of our book and is specified in function
|
/// In this example, `root_dir` will be the root directory of our book
|
||||||
/// of the current working directory by using a relative path instead of an absolute path.
|
/// and is specified in function of the current working directory
|
||||||
|
/// by using a relative path instead of an
|
||||||
|
/// absolute path.
|
||||||
///
|
///
|
||||||
/// Default directory paths:
|
/// Default directory paths:
|
||||||
///
|
///
|
||||||
|
@ -61,7 +56,8 @@ impl MDBook {
|
||||||
/// - output: `root/book`
|
/// - output: `root/book`
|
||||||
/// - theme: `root/theme`
|
/// - theme: `root/theme`
|
||||||
///
|
///
|
||||||
/// They can both be changed by using [`set_src()`](#method.set_src) and [`set_dest()`](#method.set_dest)
|
/// They can both be changed by using [`set_src()`](#method.set_src) and
|
||||||
|
/// [`set_dest()`](#method.set_dest)
|
||||||
|
|
||||||
pub fn new(root: &Path) -> MDBook {
|
pub fn new(root: &Path) -> MDBook {
|
||||||
|
|
||||||
|
@ -70,14 +66,7 @@ impl MDBook {
|
||||||
}
|
}
|
||||||
|
|
||||||
MDBook {
|
MDBook {
|
||||||
root: root.to_owned(),
|
config: BookConfig::new(root),
|
||||||
dest: root.join("book"),
|
|
||||||
src: root.join("src"),
|
|
||||||
theme_path: root.join("theme"),
|
|
||||||
|
|
||||||
title: String::new(),
|
|
||||||
author: String::new(),
|
|
||||||
description: String::new(),
|
|
||||||
|
|
||||||
content: vec![],
|
content: vec![],
|
||||||
renderer: Box::new(HtmlHandlebars::new()),
|
renderer: Box::new(HtmlHandlebars::new()),
|
||||||
|
@ -87,7 +76,8 @@ impl MDBook {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a flat depth-first iterator over the elements of the book, it returns an [BookItem enum](bookitem.html):
|
/// Returns a flat depth-first iterator over the elements of the book,
|
||||||
|
/// it returns an [BookItem enum](bookitem.html):
|
||||||
/// `(section: String, bookitem: &BookItem)`
|
/// `(section: String, bookitem: &BookItem)`
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
|
@ -123,7 +113,8 @@ impl MDBook {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `init()` creates some boilerplate files and directories to get you started with your book.
|
/// `init()` creates some boilerplate files and directories
|
||||||
|
/// to get you started with your book.
|
||||||
///
|
///
|
||||||
/// ```text
|
/// ```text
|
||||||
/// book-test/
|
/// book-test/
|
||||||
|
@ -133,49 +124,52 @@ impl MDBook {
|
||||||
/// └── SUMMARY.md
|
/// └── SUMMARY.md
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// It uses the paths given as source and output directories and adds a `SUMMARY.md` and a
|
/// It uses the paths given as source and output directories
|
||||||
|
/// and adds a `SUMMARY.md` and a
|
||||||
/// `chapter_1.md` to the source directory.
|
/// `chapter_1.md` to the source directory.
|
||||||
|
|
||||||
pub fn init(&mut self) -> Result<(), Box<Error>> {
|
pub fn init(&mut self) -> Result<(), Box<Error>> {
|
||||||
|
|
||||||
debug!("[fn]: init");
|
debug!("[fn]: init");
|
||||||
|
|
||||||
if !self.root.exists() {
|
if !self.config.get_root().exists() {
|
||||||
fs::create_dir_all(&self.root).unwrap();
|
fs::create_dir_all(&self.config.get_root()).unwrap();
|
||||||
info!("{:?} created", &self.root);
|
info!("{:?} created", &self.config.get_root());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
||||||
if !self.dest.exists() {
|
if let Some(htmlconfig) = self.config.get_html_config() {
|
||||||
debug!("[*]: {:?} does not exist, trying to create directory", self.dest);
|
if !htmlconfig.get_destination().exists() {
|
||||||
try!(fs::create_dir_all(&self.dest));
|
debug!("[*]: {:?} does not exist, trying to create directory", htmlconfig.get_destination());
|
||||||
|
fs::create_dir_all(htmlconfig.get_destination())?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.src.exists() {
|
|
||||||
debug!("[*]: {:?} does not exist, trying to create directory", self.src);
|
if !self.config.get_source().exists() {
|
||||||
try!(fs::create_dir_all(&self.src));
|
debug!("[*]: {:?} does not exist, trying to create directory", self.config.get_source());
|
||||||
|
fs::create_dir_all(self.config.get_source())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let summary = self.src.join("SUMMARY.md");
|
let summary = self.config.get_source().join("SUMMARY.md");
|
||||||
|
|
||||||
if !summary.exists() {
|
if !summary.exists() {
|
||||||
|
|
||||||
// Summary does not exist, create it
|
// Summary does not exist, create it
|
||||||
|
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", &summary);
|
||||||
debug!("[*]: {:?} does not exist, trying to create SUMMARY.md", self.src.join("SUMMARY.md"));
|
let mut f = File::create(&summary)?;
|
||||||
let mut f = try!(File::create(&self.src.join("SUMMARY.md")));
|
|
||||||
|
|
||||||
debug!("[*]: Writing to SUMMARY.md");
|
debug!("[*]: Writing to SUMMARY.md");
|
||||||
|
|
||||||
try!(writeln!(f, "# Summary"));
|
writeln!(f, "# Summary")?;
|
||||||
try!(writeln!(f, ""));
|
writeln!(f, "")?;
|
||||||
try!(writeln!(f, "- [Chapter 1](./chapter_1.md)"));
|
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse SUMMARY.md, and create the missing item related file
|
// parse SUMMARY.md, and create the missing item related file
|
||||||
try!(self.parse_summary());
|
self.parse_summary()?;
|
||||||
|
|
||||||
debug!("[*]: constructing paths for missing files");
|
debug!("[*]: constructing paths for missing files");
|
||||||
for item in self.iter() {
|
for item in self.iter() {
|
||||||
|
@ -186,20 +180,19 @@ impl MDBook {
|
||||||
BookItem::Affix(ref ch) => ch,
|
BookItem::Affix(ref ch) => ch,
|
||||||
};
|
};
|
||||||
if !ch.path.as_os_str().is_empty() {
|
if !ch.path.as_os_str().is_empty() {
|
||||||
let path = self.src.join(&ch.path);
|
let path = self.config.get_source().join(&ch.path);
|
||||||
|
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
if !self.create_missing {
|
if !self.create_missing {
|
||||||
return Err(format!(
|
return Err(format!("'{}' referenced from SUMMARY.md does not exist.", path.to_string_lossy())
|
||||||
"'{}' referenced from SUMMARY.md does not exist.",
|
.into());
|
||||||
path.to_string_lossy()).into());
|
|
||||||
}
|
}
|
||||||
debug!("[*]: {:?} does not exist, trying to create file", path);
|
debug!("[*]: {:?} does not exist, trying to create file", path);
|
||||||
try!(::std::fs::create_dir_all(path.parent().unwrap()));
|
::std::fs::create_dir_all(path.parent().unwrap())?;
|
||||||
let mut f = try!(File::create(path));
|
let mut f = File::create(path)?;
|
||||||
|
|
||||||
// debug!("[*]: Writing to {:?}", path);
|
// debug!("[*]: Writing to {:?}", path);
|
||||||
try!(writeln!(f, "# {}", ch.name));
|
writeln!(f, "# {}", ch.name)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,20 +204,23 @@ impl MDBook {
|
||||||
pub fn create_gitignore(&self) {
|
pub fn create_gitignore(&self) {
|
||||||
let gitignore = self.get_gitignore();
|
let gitignore = self.get_gitignore();
|
||||||
|
|
||||||
if !gitignore.exists() {
|
// If the HTML renderer is not set, return
|
||||||
// Gitignore does not exist, create it
|
if self.config.get_html_config().is_none() { return; }
|
||||||
|
|
||||||
// Because of `src/book/mdbook.rs#L37-L39`, `dest` will always start with `root`. If it
|
let destination = self.config.get_html_config()
|
||||||
// is not, `strip_prefix` will return an Error.
|
.expect("The HtmlConfig does exist, checked just before")
|
||||||
if !self.get_dest().starts_with(&self.root) {
|
.get_destination();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let relative = self.get_dest()
|
// Check that the gitignore does not extist and that the destination path begins with the root path
|
||||||
.strip_prefix(&self.root)
|
// We assume tha if it does begin with the root path it is contained within. This assumption
|
||||||
.expect("Destination is not relative to root.");
|
// will not hold true for paths containing double dots to go back up e.g. `root/../destination`
|
||||||
let relative = relative.to_str()
|
if !gitignore.exists() && destination.starts_with(self.config.get_root()) {
|
||||||
.expect("Path could not be yielded into a string slice.");
|
|
||||||
|
let relative = destination
|
||||||
|
.strip_prefix(self.config.get_root())
|
||||||
|
.expect("Could not strip the root prefix, path is not relative to root")
|
||||||
|
.to_str()
|
||||||
|
.expect("Could not convert to &str");
|
||||||
|
|
||||||
debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore);
|
debug!("[*]: {:?} does not exist, trying to create .gitignore", gitignore);
|
||||||
|
|
||||||
|
@ -236,99 +232,116 @@ impl MDBook {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The `build()` method is the one where everything happens. First it parses `SUMMARY.md` to
|
/// The `build()` method is the one where everything happens.
|
||||||
/// construct the book's structure in the form of a `Vec<BookItem>` and then calls `render()`
|
/// 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.
|
/// method of the current renderer.
|
||||||
///
|
///
|
||||||
/// It is the renderer who generates all the output files.
|
/// It is the renderer who generates all the output files.
|
||||||
pub fn build(&mut self) -> Result<(), Box<Error>> {
|
pub fn build(&mut self) -> Result<(), Box<Error>> {
|
||||||
debug!("[fn]: build");
|
debug!("[fn]: build");
|
||||||
|
|
||||||
try!(self.init());
|
self.init()?;
|
||||||
|
|
||||||
// Clean output directory
|
// Clean output directory
|
||||||
try!(utils::fs::remove_dir_content(&self.dest));
|
if let Some(htmlconfig) = self.config.get_html_config() {
|
||||||
|
utils::fs::remove_dir_content(htmlconfig.get_destination())?;
|
||||||
|
}
|
||||||
|
|
||||||
try!(self.renderer.render(&self));
|
self.renderer.render(&self)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn get_gitignore(&self) -> PathBuf {
|
pub fn get_gitignore(&self) -> PathBuf {
|
||||||
self.root.join(".gitignore")
|
self.config.get_root().join(".gitignore")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn copy_theme(&self) -> Result<(), Box<Error>> {
|
pub fn copy_theme(&self) -> Result<(), Box<Error>> {
|
||||||
debug!("[fn]: copy_theme");
|
debug!("[fn]: copy_theme");
|
||||||
|
|
||||||
let theme_dir = self.src.join("theme");
|
if let Some(themedir) = self.config.get_html_config().and_then(HtmlConfig::get_theme) {
|
||||||
|
|
||||||
if !theme_dir.exists() {
|
if !themedir.exists() {
|
||||||
debug!("[*]: {:?} does not exist, trying to create directory", theme_dir);
|
debug!("[*]: {:?} does not exist, trying to create directory", themedir);
|
||||||
try!(fs::create_dir(&theme_dir));
|
fs::create_dir(&themedir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// index.hbs
|
||||||
|
let mut index = File::create(&themedir.join("index.hbs"))?;
|
||||||
|
index.write_all(theme::INDEX)?;
|
||||||
|
|
||||||
|
// book.css
|
||||||
|
let mut css = File::create(&themedir.join("book.css"))?;
|
||||||
|
css.write_all(theme::CSS)?;
|
||||||
|
|
||||||
|
// favicon.png
|
||||||
|
let mut favicon = File::create(&themedir.join("favicon.png"))?;
|
||||||
|
favicon.write_all(theme::FAVICON)?;
|
||||||
|
|
||||||
|
// book.js
|
||||||
|
let mut js = File::create(&themedir.join("book.js"))?;
|
||||||
|
js.write_all(theme::JS)?;
|
||||||
|
|
||||||
|
// highlight.css
|
||||||
|
let mut highlight_css = File::create(&themedir.join("highlight.css"))?;
|
||||||
|
highlight_css.write_all(theme::HIGHLIGHT_CSS)?;
|
||||||
|
|
||||||
|
// highlight.js
|
||||||
|
let mut highlight_js = File::create(&themedir.join("highlight.js"))?;
|
||||||
|
highlight_js.write_all(theme::HIGHLIGHT_JS)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// index.hbs
|
|
||||||
let mut index = try!(File::create(&theme_dir.join("index.hbs")));
|
|
||||||
try!(index.write_all(theme::INDEX));
|
|
||||||
|
|
||||||
// book.css
|
|
||||||
let mut css = try!(File::create(&theme_dir.join("book.css")));
|
|
||||||
try!(css.write_all(theme::CSS));
|
|
||||||
|
|
||||||
// favicon.png
|
|
||||||
let mut favicon = try!(File::create(&theme_dir.join("favicon.png")));
|
|
||||||
try!(favicon.write_all(theme::FAVICON));
|
|
||||||
|
|
||||||
// book.js
|
|
||||||
let mut js = try!(File::create(&theme_dir.join("book.js")));
|
|
||||||
try!(js.write_all(theme::JS));
|
|
||||||
|
|
||||||
// highlight.css
|
|
||||||
let mut highlight_css = try!(File::create(&theme_dir.join("highlight.css")));
|
|
||||||
try!(highlight_css.write_all(theme::HIGHLIGHT_CSS));
|
|
||||||
|
|
||||||
// highlight.js
|
|
||||||
let mut highlight_js = try!(File::create(&theme_dir.join("highlight.js")));
|
|
||||||
try!(highlight_js.write_all(theme::HIGHLIGHT_JS));
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_file<P: AsRef<Path>>(&self, filename: P, content: &[u8]) -> Result<(), Box<Error>> {
|
pub fn write_file<P: AsRef<Path>>(&self, filename: P, content: &[u8]) -> Result<(), Box<Error>> {
|
||||||
let path = self.get_dest().join(filename);
|
let path = self.get_destination()
|
||||||
try!(utils::fs::create_file(&path).and_then(|mut file| {
|
.ok_or(String::from("HtmlConfig not set, could not find a destination"))?
|
||||||
file.write_all(content)
|
.join(filename);
|
||||||
}).map_err(|e| {
|
|
||||||
io::Error::new(io::ErrorKind::Other, format!("Could not create {}: {}", path.display(), e))
|
utils::fs::create_file(&path)
|
||||||
}));
|
.and_then(|mut file| file.write_all(content))
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("Could not create {}: {}", path.display(), e)))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parses the `book.json` file (if it exists) to extract the configuration parameters.
|
/// Parses the `book.json` file (if it exists) to extract
|
||||||
|
/// the configuration parameters.
|
||||||
/// The `book.json` file should be in the root directory of the book.
|
/// The `book.json` file should be in the root directory of the book.
|
||||||
/// The root directory is the one specified when creating a new `MDBook`
|
/// The root directory is the one specified when creating a new `MDBook`
|
||||||
|
|
||||||
pub fn read_config(mut self) -> Self {
|
pub fn read_config(mut self) -> Result<Self, Box<Error>> {
|
||||||
|
|
||||||
let config = BookConfig::new(&self.root)
|
let toml = self.get_root().join("book.toml");
|
||||||
.read_config(&self.root)
|
let json = self.get_root().join("book.json");
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
self.title = config.title;
|
if toml.exists() {
|
||||||
self.description = config.description;
|
let mut file = File::open(toml)?;
|
||||||
self.author = config.author;
|
let mut content = String::new();
|
||||||
|
file.read_to_string(&mut content)?;
|
||||||
|
|
||||||
self.dest = config.dest;
|
let parsed_config = TomlConfig::from_toml(&content)?;
|
||||||
self.src = config.src;
|
self.config.fill_from_tomlconfig(parsed_config);
|
||||||
self.theme_path = config.theme_path;
|
} else if json.exists() {
|
||||||
|
warn!("The JSON configuration file is deprecated, please use the TOML configuration.");
|
||||||
|
let mut file = File::open(json)?;
|
||||||
|
let mut content = String::new();
|
||||||
|
file.read_to_string(&mut content)?;
|
||||||
|
|
||||||
self
|
let parsed_config = JsonConfig::from_json(&content)?;
|
||||||
|
self.config.fill_from_jsonconfig(parsed_config);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// You can change the default renderer to another one by using this method. The only requirement
|
/// You can change the default renderer to another one
|
||||||
/// is for your renderer to implement the [Renderer trait](../../renderer/renderer/trait.Renderer.html)
|
/// by using this method. The only requirement
|
||||||
|
/// is for your renderer to implement the
|
||||||
|
/// [Renderer trait](../../renderer/renderer/trait.Renderer.html)
|
||||||
///
|
///
|
||||||
/// ```no_run
|
/// ```no_run
|
||||||
/// extern crate mdbook;
|
/// extern crate mdbook;
|
||||||
|
@ -340,12 +353,14 @@ impl MDBook {
|
||||||
/// let mut book = MDBook::new(Path::new("mybook"))
|
/// let mut book = MDBook::new(Path::new("mybook"))
|
||||||
/// .set_renderer(Box::new(HtmlHandlebars::new()));
|
/// .set_renderer(Box::new(HtmlHandlebars::new()));
|
||||||
///
|
///
|
||||||
/// // In this example we replace the default renderer by the default renderer...
|
/// // In this example we replace the default renderer
|
||||||
/// // Don't forget to put your renderer in a Box
|
/// // by the default renderer...
|
||||||
|
/// // Don't forget to put your renderer in a Box
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// **note:** Don't forget to put your renderer in a `Box` before passing it to `set_renderer()`
|
/// **note:** Don't forget to put your renderer in a `Box`
|
||||||
|
/// before passing it to `set_renderer()`
|
||||||
|
|
||||||
pub fn set_renderer(mut self, renderer: Box<Renderer>) -> Self {
|
pub fn set_renderer(mut self, renderer: Box<Renderer>) -> Self {
|
||||||
self.renderer = renderer;
|
self.renderer = renderer;
|
||||||
|
@ -354,27 +369,25 @@ impl MDBook {
|
||||||
|
|
||||||
pub fn test(&mut self) -> Result<(), Box<Error>> {
|
pub fn test(&mut self) -> Result<(), Box<Error>> {
|
||||||
// read in the chapters
|
// read in the chapters
|
||||||
try!(self.parse_summary());
|
self.parse_summary()?;
|
||||||
for item in self.iter() {
|
for item in self.iter() {
|
||||||
|
|
||||||
if let BookItem::Chapter(_, ref ch) = *item {
|
if let BookItem::Chapter(_, ref ch) = *item {
|
||||||
if ch.path != PathBuf::new() {
|
if ch.path != PathBuf::new() {
|
||||||
|
|
||||||
let path = self.get_src().join(&ch.path);
|
let path = self.get_source().join(&ch.path);
|
||||||
|
|
||||||
println!("[*]: Testing file: {:?}", path);
|
println!("[*]: Testing file: {:?}", path);
|
||||||
|
|
||||||
let output_result = Command::new("rustdoc")
|
let output_result = Command::new("rustdoc").arg(&path).arg("--test").output();
|
||||||
.arg(&path)
|
let output = output_result?;
|
||||||
.arg("--test")
|
|
||||||
.output();
|
|
||||||
let output = try!(output_result);
|
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(Box::new(io::Error::new(ErrorKind::Other, format!(
|
return Err(Box::new(io::Error::new(ErrorKind::Other,
|
||||||
"{}\n{}",
|
format!("{}\n{}",
|
||||||
String::from_utf8_lossy(&output.stdout),
|
String::from_utf8_lossy(&output.stdout),
|
||||||
String::from_utf8_lossy(&output.stderr)))) as Box<Error>);
|
String::from_utf8_lossy(&output.stderr)))) as
|
||||||
|
Box<Error>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -383,68 +396,55 @@ impl MDBook {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_root(&self) -> &Path {
|
pub fn get_root(&self) -> &Path {
|
||||||
&self.root
|
self.config.get_root()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_dest(mut self, dest: &Path) -> Self {
|
|
||||||
|
|
||||||
// Handle absolute and relative paths
|
pub fn with_destination<T: Into<PathBuf>>(mut self, destination: T) -> Self {
|
||||||
if dest.is_absolute() {
|
let root = self.config.get_root().to_owned();
|
||||||
self.dest = dest.to_owned();
|
if let Some(htmlconfig) = self.config.get_mut_html_config() {
|
||||||
|
htmlconfig.set_destination(&root, &destination.into());
|
||||||
} else {
|
} else {
|
||||||
let dest = self.root.join(dest).to_owned();
|
error!("There is no HTML renderer set...");
|
||||||
self.dest = dest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_dest(&self) -> &Path {
|
|
||||||
&self.dest
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_src(mut self, src: &Path) -> Self {
|
pub fn get_destination(&self) -> Option<&Path> {
|
||||||
|
if let Some(htmlconfig) = self.config.get_html_config() {
|
||||||
// Handle absolute and relative paths
|
return Some(htmlconfig.get_destination());
|
||||||
if src.is_absolute() {
|
|
||||||
self.src = src.to_owned();
|
|
||||||
} else {
|
|
||||||
let src = self.root.join(src).to_owned();
|
|
||||||
self.src = src;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_source<T: Into<PathBuf>>(mut self, source: T) -> Self {
|
||||||
|
self.config.set_source(source);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_src(&self) -> &Path {
|
pub fn get_source(&self) -> &Path {
|
||||||
&self.src
|
self.config.get_source()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_title(mut self, title: &str) -> Self {
|
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
|
||||||
self.title = title.to_owned();
|
self.config.set_title(title);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_title(&self) -> &str {
|
pub fn get_title(&self) -> &str {
|
||||||
&self.title
|
self.config.get_title()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_author(mut self, author: &str) -> Self {
|
pub fn with_description<T: Into<String>>(mut self, description: T) -> Self {
|
||||||
self.author = author.to_owned();
|
self.config.set_description(description);
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_author(&self) -> &str {
|
|
||||||
&self.author
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_description(mut self, description: &str) -> Self {
|
|
||||||
self.description = description.to_owned();
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_description(&self) -> &str {
|
pub fn get_description(&self) -> &str {
|
||||||
&self.description
|
self.config.get_description()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_livereload(&mut self, livereload: String) -> &mut Self {
|
pub fn set_livereload(&mut self, livereload: String) -> &mut Self {
|
||||||
|
@ -461,23 +461,68 @@ impl MDBook {
|
||||||
self.livereload.as_ref()
|
self.livereload.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_theme_path(mut self, theme_path: &Path) -> Self {
|
pub fn with_theme_path<T: Into<PathBuf>>(mut self, theme_path: T) -> Self {
|
||||||
self.theme_path = if theme_path.is_absolute() {
|
let root = self.config.get_root().to_owned();
|
||||||
theme_path.to_owned()
|
if let Some(htmlconfig) = self.config.get_mut_html_config() {
|
||||||
|
htmlconfig.set_theme(&root, &theme_path.into());
|
||||||
} else {
|
} else {
|
||||||
self.root.join(theme_path).to_owned()
|
error!("There is no HTML renderer set...");
|
||||||
};
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_theme_path(&self) -> &Path {
|
pub fn get_theme_path(&self) -> Option<&PathBuf> {
|
||||||
&self.theme_path
|
if let Some(htmlconfig) = self.config.get_html_config() {
|
||||||
|
return htmlconfig.get_theme();
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_google_analytics_id(&self) -> Option<String> {
|
||||||
|
if let Some(htmlconfig) = self.config.get_html_config() {
|
||||||
|
return htmlconfig.get_google_analytics_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_additional_js(&self) -> bool {
|
||||||
|
if let Some(htmlconfig) = self.config.get_html_config() {
|
||||||
|
return htmlconfig.has_additional_js();
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_additional_js(&self) -> &[PathBuf] {
|
||||||
|
if let Some(htmlconfig) = self.config.get_html_config() {
|
||||||
|
return htmlconfig.get_additional_js();
|
||||||
|
}
|
||||||
|
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_additional_css(&self) -> bool {
|
||||||
|
if let Some(htmlconfig) = self.config.get_html_config() {
|
||||||
|
return htmlconfig.has_additional_css();
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_additional_css(&self) -> &[PathBuf] {
|
||||||
|
if let Some(htmlconfig) = self.config.get_html_config() {
|
||||||
|
return htmlconfig.get_additional_css();
|
||||||
|
}
|
||||||
|
|
||||||
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct book
|
// Construct book
|
||||||
fn parse_summary(&mut self) -> Result<(), Box<Error>> {
|
fn parse_summary(&mut self) -> Result<(), Box<Error>> {
|
||||||
// When append becomes stable, use self.content.append() ...
|
// When append becomes stable, use self.content.append() ...
|
||||||
self.content = try!(parse::construct_bookitems(&self.src.join("SUMMARY.md")));
|
self.content = parse::construct_bookitems(&self.get_source().join("SUMMARY.md"))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,232 @@
|
||||||
|
use std::path::{PathBuf, Path};
|
||||||
|
|
||||||
|
use super::HtmlConfig;
|
||||||
|
use super::tomlconfig::TomlConfig;
|
||||||
|
use super::jsonconfig::JsonConfig;
|
||||||
|
|
||||||
|
/// Configuration struct containing all the configuration options available in mdBook.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct BookConfig {
|
||||||
|
root: PathBuf,
|
||||||
|
source: PathBuf,
|
||||||
|
|
||||||
|
title: String,
|
||||||
|
authors: Vec<String>,
|
||||||
|
description: String,
|
||||||
|
|
||||||
|
multilingual: bool,
|
||||||
|
indent_spaces: i32,
|
||||||
|
|
||||||
|
html_config: Option<HtmlConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BookConfig {
|
||||||
|
/// Creates a new `BookConfig` struct with as root path the path given as parameter.
|
||||||
|
/// The source directory is `root/src` and the destination for the rendered book is `root/book`.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use std::path::PathBuf;
|
||||||
|
/// # use mdbook::config::{BookConfig, HtmlConfig};
|
||||||
|
/// #
|
||||||
|
/// let root = PathBuf::from("directory/to/my/book");
|
||||||
|
/// let config = BookConfig::new(&root);
|
||||||
|
///
|
||||||
|
/// assert_eq!(config.get_root(), &root);
|
||||||
|
/// assert_eq!(config.get_source(), PathBuf::from("directory/to/my/book/src"));
|
||||||
|
/// assert_eq!(config.get_html_config(), Some(&HtmlConfig::new(PathBuf::from("directory/to/my/book"))));
|
||||||
|
/// ```
|
||||||
|
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
|
||||||
|
let root: PathBuf = root.into();
|
||||||
|
let htmlconfig = HtmlConfig::new(&root);
|
||||||
|
|
||||||
|
BookConfig {
|
||||||
|
root: root.clone(),
|
||||||
|
source: root.join("src"),
|
||||||
|
|
||||||
|
title: String::new(),
|
||||||
|
authors: Vec::new(),
|
||||||
|
description: String::new(),
|
||||||
|
|
||||||
|
multilingual: false,
|
||||||
|
indent_spaces: 4,
|
||||||
|
|
||||||
|
html_config: Some(htmlconfig),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder method to set the source directory
|
||||||
|
pub fn with_source<T: Into<PathBuf>>(mut self, source: T) -> Self {
|
||||||
|
self.source = source.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder method to set the book's title
|
||||||
|
pub fn with_title<T: Into<String>>(mut self, title: T) -> Self {
|
||||||
|
self.title = title.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder method to set the book's description
|
||||||
|
pub fn with_description<T: Into<String>>(mut self, description: T) -> Self {
|
||||||
|
self.description = description.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder method to set the book's authors
|
||||||
|
pub fn with_authors<T: Into<Vec<String>>>(mut self, authors: T) -> Self {
|
||||||
|
self.authors = authors.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_tomlconfig<T: Into<PathBuf>>(root: T, tomlconfig: TomlConfig) -> Self {
|
||||||
|
let root = root.into();
|
||||||
|
let mut config = BookConfig::new(&root);
|
||||||
|
config.fill_from_tomlconfig(tomlconfig);
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_from_tomlconfig(&mut self, tomlconfig: TomlConfig) -> &mut Self {
|
||||||
|
|
||||||
|
if let Some(s) = tomlconfig.source {
|
||||||
|
self.set_source(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(t) = tomlconfig.title {
|
||||||
|
self.set_title(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(d) = tomlconfig.description {
|
||||||
|
self.set_description(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(a) = tomlconfig.authors {
|
||||||
|
self.set_authors(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(a) = tomlconfig.author {
|
||||||
|
self.set_authors(vec![a]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(tomlhtmlconfig) = tomlconfig.output.and_then(|o| o.html) {
|
||||||
|
let root = self.root.clone();
|
||||||
|
if let Some(htmlconfig) = self.get_mut_html_config() {
|
||||||
|
htmlconfig.fill_from_tomlconfig(root, tomlhtmlconfig);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The JSON configuration file is **deprecated** and should not be used anymore.
|
||||||
|
/// Please, migrate to the TOML configuration file.
|
||||||
|
pub fn from_jsonconfig<T: Into<PathBuf>>(root: T, jsonconfig: JsonConfig) -> Self {
|
||||||
|
let root = root.into();
|
||||||
|
let mut config = BookConfig::new(&root);
|
||||||
|
config.fill_from_jsonconfig(jsonconfig);
|
||||||
|
config
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The JSON configuration file is **deprecated** and should not be used anymore.
|
||||||
|
/// Please, migrate to the TOML configuration file.
|
||||||
|
pub fn fill_from_jsonconfig(&mut self, jsonconfig: JsonConfig) -> &mut Self {
|
||||||
|
|
||||||
|
if let Some(s) = jsonconfig.src {
|
||||||
|
self.set_source(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(t) = jsonconfig.title {
|
||||||
|
self.set_title(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(d) = jsonconfig.description {
|
||||||
|
self.set_description(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(a) = jsonconfig.author {
|
||||||
|
self.set_authors(vec![a]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(d) = jsonconfig.dest {
|
||||||
|
let root = self.get_root().to_owned();
|
||||||
|
if let Some(htmlconfig) = self.get_mut_html_config() {
|
||||||
|
htmlconfig.set_destination(&root, &d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(d) = jsonconfig.theme_path {
|
||||||
|
let root = self.get_root().to_owned();
|
||||||
|
if let Some(htmlconfig) = self.get_mut_html_config() {
|
||||||
|
htmlconfig.set_theme(&root, &d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_root<T: Into<PathBuf>>(&mut self, root: T) -> &mut Self {
|
||||||
|
self.root = root.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_root(&self) -> &Path {
|
||||||
|
&self.root
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_source<T: Into<PathBuf>>(&mut self, source: T) -> &mut Self {
|
||||||
|
let mut source = source.into();
|
||||||
|
|
||||||
|
// If the source path is relative, start with the root path
|
||||||
|
if source.is_relative() {
|
||||||
|
source = self.root.join(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.source = source;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_source(&self) -> &Path {
|
||||||
|
&self.source
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_title<T: Into<String>>(&mut self, title: T) -> &mut Self {
|
||||||
|
self.title = title.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_title(&self) -> &str {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_description<T: Into<String>>(&mut self, description: T) -> &mut Self {
|
||||||
|
self.description = description.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_description(&self) -> &str {
|
||||||
|
&self.description
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_authors<T: Into<Vec<String>>>(&mut self, authors: T) -> &mut Self {
|
||||||
|
self.authors = authors.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the authors of the book as specified in the configuration file
|
||||||
|
pub fn get_authors(&self) -> &[String] {
|
||||||
|
self.authors.as_slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_html_config(&mut self, htmlconfig: HtmlConfig) -> &mut Self {
|
||||||
|
self.html_config = Some(htmlconfig);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the configuration for the HTML renderer or None of there isn't any
|
||||||
|
pub fn get_html_config(&self) -> Option<&HtmlConfig> {
|
||||||
|
self.html_config.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut_html_config(&mut self) -> Option<&mut HtmlConfig> {
|
||||||
|
self.html_config.as_mut()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
use std::path::{PathBuf, Path};
|
||||||
|
|
||||||
|
use super::tomlconfig::TomlHtmlConfig;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct HtmlConfig {
|
||||||
|
destination: PathBuf,
|
||||||
|
theme: Option<PathBuf>,
|
||||||
|
google_analytics: Option<String>,
|
||||||
|
additional_css: Vec<PathBuf>,
|
||||||
|
additional_js: Vec<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HtmlConfig {
|
||||||
|
/// Creates a new `HtmlConfig` struct containing the configuration parameters for the HTML renderer.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use std::path::PathBuf;
|
||||||
|
/// # use mdbook::config::HtmlConfig;
|
||||||
|
/// #
|
||||||
|
/// let output = PathBuf::from("root/book");
|
||||||
|
/// let config = HtmlConfig::new(PathBuf::from("root"));
|
||||||
|
///
|
||||||
|
/// assert_eq!(config.get_destination(), &output);
|
||||||
|
/// ```
|
||||||
|
pub fn new<T: Into<PathBuf>>(root: T) -> Self {
|
||||||
|
HtmlConfig {
|
||||||
|
destination: root.into().join("book"),
|
||||||
|
theme: None,
|
||||||
|
google_analytics: None,
|
||||||
|
additional_css: Vec::new(),
|
||||||
|
additional_js: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fill_from_tomlconfig<T: Into<PathBuf>>(&mut self, root: T, tomlconfig: TomlHtmlConfig) -> &mut Self {
|
||||||
|
let root = root.into();
|
||||||
|
|
||||||
|
if let Some(d) = tomlconfig.destination {
|
||||||
|
if d.is_relative() {
|
||||||
|
self.destination = root.join(d);
|
||||||
|
} else {
|
||||||
|
self.destination = d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(t) = tomlconfig.theme {
|
||||||
|
if t.is_relative() {
|
||||||
|
self.theme = Some(root.join(t));
|
||||||
|
} else {
|
||||||
|
self.theme = Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tomlconfig.google_analytics.is_some() {
|
||||||
|
self.google_analytics = tomlconfig.google_analytics;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(stylepaths) = tomlconfig.additional_css {
|
||||||
|
for path in stylepaths {
|
||||||
|
if path.is_relative() {
|
||||||
|
self.additional_css.push(root.join(path));
|
||||||
|
} else {
|
||||||
|
self.additional_css.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(scriptpaths) = tomlconfig.additional_js {
|
||||||
|
for path in scriptpaths {
|
||||||
|
if path.is_relative() {
|
||||||
|
self.additional_js.push(root.join(path));
|
||||||
|
} else {
|
||||||
|
self.additional_js.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_destination<T: Into<PathBuf>>(&mut self, root: T, destination: T) -> &mut Self {
|
||||||
|
let d = destination.into();
|
||||||
|
if d.is_relative() {
|
||||||
|
self.destination = root.into().join(d);
|
||||||
|
} else {
|
||||||
|
self.destination = d;
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_destination(&self) -> &Path {
|
||||||
|
&self.destination
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: How to get a `Option<&Path>` ?
|
||||||
|
pub fn get_theme(&self) -> Option<&PathBuf> {
|
||||||
|
self.theme.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_theme<T: Into<PathBuf>>(&mut self, root: T, theme: T) -> &mut Self {
|
||||||
|
let d = theme.into();
|
||||||
|
if d.is_relative() {
|
||||||
|
self.theme = Some(root.into().join(d));
|
||||||
|
} else {
|
||||||
|
self.theme = Some(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_google_analytics_id(&self) -> Option<String> {
|
||||||
|
self.google_analytics.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_google_analytics_id(&mut self, id: Option<String>) -> &mut Self {
|
||||||
|
self.google_analytics = id;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_additional_css(&self) -> bool {
|
||||||
|
!self.additional_css.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_additional_css(&self) -> &[PathBuf] {
|
||||||
|
&self.additional_css
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_additional_js(&self) -> bool {
|
||||||
|
!self.additional_js.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_additional_js(&self) -> &[PathBuf] {
|
||||||
|
&self.additional_js
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
extern crate serde_json;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// The JSON configuration is **deprecated** and will be removed in the near future.
|
||||||
|
/// Please migrate to the TOML configuration.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct JsonConfig {
|
||||||
|
pub src: Option<PathBuf>,
|
||||||
|
pub dest: Option<PathBuf>,
|
||||||
|
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub author: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
pub theme_path: Option<PathBuf>,
|
||||||
|
pub google_analytics: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Returns a JsonConfig from a JSON string
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use mdbook::config::jsonconfig::JsonConfig;
|
||||||
|
/// # use std::path::PathBuf;
|
||||||
|
/// let json = r#"{
|
||||||
|
/// "title": "Some title",
|
||||||
|
/// "dest": "htmlbook"
|
||||||
|
/// }"#;
|
||||||
|
///
|
||||||
|
/// let config = JsonConfig::from_json(&json).expect("Should parse correctly");
|
||||||
|
/// assert_eq!(config.title, Some(String::from("Some title")));
|
||||||
|
/// assert_eq!(config.dest, Some(PathBuf::from("htmlbook")));
|
||||||
|
/// ```
|
||||||
|
impl JsonConfig {
|
||||||
|
pub fn from_json(input: &str) -> Result<Self, String> {
|
||||||
|
let config: JsonConfig = serde_json::from_str(input)
|
||||||
|
.map_err(|e| format!("Could not parse JSON: {}", e))?;
|
||||||
|
|
||||||
|
return Ok(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
pub mod bookconfig;
|
||||||
|
pub mod htmlconfig;
|
||||||
|
pub mod tomlconfig;
|
||||||
|
pub mod jsonconfig;
|
||||||
|
|
||||||
|
// Re-export the config structs
|
||||||
|
pub use self::bookconfig::BookConfig;
|
||||||
|
pub use self::htmlconfig::HtmlConfig;
|
||||||
|
pub use self::tomlconfig::TomlConfig;
|
|
@ -0,0 +1,53 @@
|
||||||
|
extern crate toml;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct TomlConfig {
|
||||||
|
pub source: Option<PathBuf>,
|
||||||
|
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub author: Option<String>,
|
||||||
|
pub authors: Option<Vec<String>>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
|
||||||
|
pub output: Option<TomlOutputConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct TomlOutputConfig {
|
||||||
|
pub html: Option<TomlHtmlConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct TomlHtmlConfig {
|
||||||
|
pub destination: Option<PathBuf>,
|
||||||
|
pub theme: Option<PathBuf>,
|
||||||
|
pub google_analytics: Option<String>,
|
||||||
|
pub additional_css: Option<Vec<PathBuf>>,
|
||||||
|
pub additional_js: Option<Vec<PathBuf>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a TomlConfig from a TOML string
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// # use mdbook::config::tomlconfig::TomlConfig;
|
||||||
|
/// # use std::path::PathBuf;
|
||||||
|
/// let toml = r#"title="Some title"
|
||||||
|
/// [output.html]
|
||||||
|
/// destination = "htmlbook" "#;
|
||||||
|
///
|
||||||
|
/// let config = TomlConfig::from_toml(&toml).expect("Should parse correctly");
|
||||||
|
/// assert_eq!(config.title, Some(String::from("Some title")));
|
||||||
|
/// assert_eq!(config.output.unwrap().html.unwrap().destination, Some(PathBuf::from("htmlbook")));
|
||||||
|
/// ```
|
||||||
|
impl TomlConfig {
|
||||||
|
pub fn from_toml(input: &str) -> Result<Self, String> {
|
||||||
|
let config: TomlConfig = toml::from_str(input)
|
||||||
|
.map_err(|e| format!("Could not parse TOML: {}", e))?;
|
||||||
|
|
||||||
|
return Ok(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
22
src/lib.rs
22
src/lib.rs
|
@ -24,12 +24,13 @@
|
||||||
//! use std::path::Path;
|
//! use std::path::Path;
|
||||||
//!
|
//!
|
||||||
//! fn main() {
|
//! fn main() {
|
||||||
//! let mut book = MDBook::new(Path::new("my-book")) // Path to root
|
//! let mut book = MDBook::new(Path::new("my-book")) // Path to root
|
||||||
//! .set_src(Path::new("src")) // Path from root to source directory
|
//! .with_source(Path::new("src")) // Path from root to source directory
|
||||||
//! .set_dest(Path::new("book")) // Path from root to output directory
|
//! .with_destination(Path::new("book")) // Path from root to output directory
|
||||||
//! .read_config(); // Parse book.json file for configuration
|
//! .read_config() // Parse book.json file for configuration
|
||||||
|
//! .expect("I don't handle the error for the configuration file, but you should!");
|
||||||
//!
|
//!
|
||||||
//! book.build().unwrap(); // Render the book
|
//! book.build().unwrap(); // Render the book
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
@ -69,15 +70,19 @@
|
||||||
//!
|
//!
|
||||||
//! Make sure to take a look at it.
|
//! Make sure to take a look at it.
|
||||||
|
|
||||||
extern crate serde;
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate serde_json;
|
extern crate serde_derive;
|
||||||
|
extern crate serde;
|
||||||
|
#[macro_use] extern crate serde_json;
|
||||||
|
|
||||||
extern crate handlebars;
|
extern crate handlebars;
|
||||||
extern crate pulldown_cmark;
|
extern crate pulldown_cmark;
|
||||||
extern crate regex;
|
extern crate regex;
|
||||||
|
|
||||||
#[macro_use] extern crate log;
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
pub mod book;
|
pub mod book;
|
||||||
|
pub mod config;
|
||||||
mod parse;
|
mod parse;
|
||||||
pub mod renderer;
|
pub mod renderer;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
@ -85,5 +90,4 @@ pub mod utils;
|
||||||
|
|
||||||
pub use book::MDBook;
|
pub use book::MDBook;
|
||||||
pub use book::BookItem;
|
pub use book::BookItem;
|
||||||
pub use book::BookConfig;
|
|
||||||
pub use renderer::Renderer;
|
pub use renderer::Renderer;
|
||||||
|
|
|
@ -6,10 +6,10 @@ use book::bookitem::{BookItem, Chapter};
|
||||||
pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> {
|
pub fn construct_bookitems(path: &PathBuf) -> Result<Vec<BookItem>> {
|
||||||
debug!("[fn]: construct_bookitems");
|
debug!("[fn]: construct_bookitems");
|
||||||
let mut summary = String::new();
|
let mut summary = String::new();
|
||||||
try!(try!(File::open(path)).read_to_string(&mut summary));
|
File::open(path)?.read_to_string(&mut summary)?;
|
||||||
|
|
||||||
debug!("[*]: Parse SUMMARY.md");
|
debug!("[*]: Parse SUMMARY.md");
|
||||||
let top_items = try!(parse_level(&mut summary.split('\n').collect(), 0, vec![0]));
|
let top_items = parse_level(&mut summary.split('\n').collect(), 0, vec![0])?;
|
||||||
debug!("[*]: Done parsing SUMMARY.md");
|
debug!("[*]: Done parsing SUMMARY.md");
|
||||||
Ok(top_items)
|
Ok(top_items)
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,10 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||||
while !summary.is_empty() {
|
while !summary.is_empty() {
|
||||||
let item: BookItem;
|
let item: BookItem;
|
||||||
// Indentation level of the line to parse
|
// Indentation level of the line to parse
|
||||||
let level = try!(level(summary[0], 4));
|
let level = level(summary[0], 4)?;
|
||||||
|
|
||||||
// if level < current_level we remove the last digit of section, exit the current function,
|
// if level < current_level we remove the last digit of section,
|
||||||
|
// exit the current function,
|
||||||
// and return the parsed level to the calling function.
|
// and return the parsed level to the calling function.
|
||||||
if level < current_level {
|
if level < current_level {
|
||||||
break;
|
break;
|
||||||
|
@ -35,11 +36,13 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||||
// Level can not be root level !!
|
// Level can not be root level !!
|
||||||
// Add a sub-number to section
|
// Add a sub-number to section
|
||||||
section.push(0);
|
section.push(0);
|
||||||
let last = items.pop().expect("There should be at least one item since this can't be the root level");
|
let last = items
|
||||||
|
.pop()
|
||||||
|
.expect("There should be at least one item since this can't be the root level");
|
||||||
|
|
||||||
if let BookItem::Chapter(ref s, ref ch) = last {
|
if let BookItem::Chapter(ref s, ref ch) = last {
|
||||||
let mut ch = ch.clone();
|
let mut ch = ch.clone();
|
||||||
ch.sub_items = try!(parse_level(summary, level, section.clone()));
|
ch.sub_items = parse_level(summary, level, section.clone())?;
|
||||||
items.push(BookItem::Chapter(s.clone(), ch));
|
items.push(BookItem::Chapter(s.clone(), ch));
|
||||||
|
|
||||||
// Remove the last number from the section, because we got back to our level..
|
// Remove the last number from the section, because we got back to our level..
|
||||||
|
@ -62,7 +65,8 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||||
// Eliminate possible errors and set section to -1 after suffix
|
// Eliminate possible errors and set section to -1 after suffix
|
||||||
match parsed_item {
|
match parsed_item {
|
||||||
// error if level != 0 and BookItem is != Chapter
|
// error if level != 0 and BookItem is != Chapter
|
||||||
BookItem::Affix(_) | BookItem::Spacer if level > 0 => {
|
BookItem::Affix(_) |
|
||||||
|
BookItem::Spacer if level > 0 => {
|
||||||
return Err(Error::new(ErrorKind::Other,
|
return Err(Error::new(ErrorKind::Other,
|
||||||
"Your summary.md is messed up\n\n
|
"Your summary.md is messed up\n\n
|
||||||
\
|
\
|
||||||
|
@ -98,7 +102,9 @@ fn parse_level(summary: &mut Vec<&str>, current_level: i32, mut section: Vec<i32
|
||||||
// Increment section
|
// Increment section
|
||||||
let len = section.len() - 1;
|
let len = section.len() - 1;
|
||||||
section[len] += 1;
|
section[len] += 1;
|
||||||
let s = section.iter().fold("".to_owned(), |s, i| s + &i.to_string() + ".");
|
let s = section
|
||||||
|
.iter()
|
||||||
|
.fold("".to_owned(), |s, i| s + &i.to_string() + ".");
|
||||||
BookItem::Chapter(s, ch)
|
BookItem::Chapter(s, ch)
|
||||||
},
|
},
|
||||||
_ => parsed_item,
|
_ => parsed_item,
|
||||||
|
|
|
@ -37,7 +37,8 @@ impl Renderer for HtmlHandlebars {
|
||||||
|
|
||||||
// Register template
|
// Register template
|
||||||
debug!("[*]: Register handlebars template");
|
debug!("[*]: Register handlebars template");
|
||||||
try!(handlebars.register_template_string("index", try!(String::from_utf8(theme.index))));
|
handlebars
|
||||||
|
.register_template_string("index", String::from_utf8(theme.index)?)?;
|
||||||
|
|
||||||
// Register helpers
|
// Register helpers
|
||||||
debug!("[*]: Register handlebars helpers");
|
debug!("[*]: Register handlebars helpers");
|
||||||
|
@ -45,14 +46,14 @@ impl Renderer for HtmlHandlebars {
|
||||||
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
|
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
|
||||||
handlebars.register_helper("next", Box::new(helpers::navigation::next));
|
handlebars.register_helper("next", Box::new(helpers::navigation::next));
|
||||||
|
|
||||||
let mut data = try!(make_data(book));
|
let mut data = make_data(book)?;
|
||||||
|
|
||||||
// Print version
|
// Print version
|
||||||
let mut print_content: String = String::new();
|
let mut print_content: String = String::new();
|
||||||
|
|
||||||
// Check if dest directory exists
|
// Check if dest directory exists
|
||||||
debug!("[*]: Check if destination directory exists");
|
debug!("[*]: Check if destination directory exists");
|
||||||
if fs::create_dir_all(book.get_dest()).is_err() {
|
if fs::create_dir_all(book.get_destination().expect("If the HTML renderer is called, one would assume the HtmlConfig is set... (2)")).is_err() {
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other,
|
return Err(Box::new(io::Error::new(io::ErrorKind::Other,
|
||||||
"Unexpected error when constructing destination path")));
|
"Unexpected error when constructing destination path")));
|
||||||
}
|
}
|
||||||
|
@ -66,14 +67,14 @@ impl Renderer for HtmlHandlebars {
|
||||||
BookItem::Affix(ref ch) => {
|
BookItem::Affix(ref ch) => {
|
||||||
if ch.path != PathBuf::new() {
|
if ch.path != PathBuf::new() {
|
||||||
|
|
||||||
let path = book.get_src().join(&ch.path);
|
let path = book.get_source().join(&ch.path);
|
||||||
|
|
||||||
debug!("[*]: Opening file: {:?}", path);
|
debug!("[*]: Opening file: {:?}", path);
|
||||||
let mut f = try!(File::open(&path));
|
let mut f = File::open(&path)?;
|
||||||
let mut content: String = String::new();
|
let mut content: String = String::new();
|
||||||
|
|
||||||
debug!("[*]: Reading file");
|
debug!("[*]: Reading file");
|
||||||
try!(f.read_to_string(&mut content));
|
f.read_to_string(&mut content)?;
|
||||||
|
|
||||||
// Parse for playpen links
|
// Parse for playpen links
|
||||||
if let Some(p) = path.parent() {
|
if let Some(p) = path.parent() {
|
||||||
|
@ -85,8 +86,10 @@ impl Renderer for HtmlHandlebars {
|
||||||
print_content.push_str(&content);
|
print_content.push_str(&content);
|
||||||
|
|
||||||
// Update the context with data for this file
|
// Update the context with data for this file
|
||||||
let path = ch.path.to_str().ok_or_else(||
|
let path =
|
||||||
io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))?;
|
ch.path
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))?;
|
||||||
data.insert("path".to_owned(), json!(path));
|
data.insert("path".to_owned(), json!(path));
|
||||||
data.insert("content".to_owned(), json!(content));
|
data.insert("content".to_owned(), json!(content));
|
||||||
data.insert("chapter_title".to_owned(), json!(ch.name));
|
data.insert("chapter_title".to_owned(), json!(ch.name));
|
||||||
|
@ -94,7 +97,7 @@ impl Renderer for HtmlHandlebars {
|
||||||
|
|
||||||
// Render the handlebars template with the data
|
// Render the handlebars template with the data
|
||||||
debug!("[*]: Render template");
|
debug!("[*]: Render template");
|
||||||
let rendered = try!(handlebars.render("index", &data));
|
let rendered = handlebars.render("index", &data)?;
|
||||||
|
|
||||||
let filename = Path::new(&ch.path).with_extension("html");
|
let filename = Path::new(&ch.path).with_extension("html");
|
||||||
|
|
||||||
|
@ -106,27 +109,36 @@ impl Renderer for HtmlHandlebars {
|
||||||
|
|
||||||
// Write to file
|
// Write to file
|
||||||
info!("[*] Creating {:?} ✓", filename.display());
|
info!("[*] Creating {:?} ✓", filename.display());
|
||||||
try!(book.write_file(filename, &rendered.into_bytes()));
|
book.write_file(filename, &rendered.into_bytes())?;
|
||||||
|
|
||||||
// Create an index.html from the first element in SUMMARY.md
|
// Create an index.html from the first element in SUMMARY.md
|
||||||
if index {
|
if index {
|
||||||
debug!("[*]: index.html");
|
debug!("[*]: index.html");
|
||||||
|
|
||||||
let mut content = String::new();
|
let mut content = String::new();
|
||||||
let _source = try!(File::open(book.get_dest().join(&ch.path.with_extension("html"))))
|
|
||||||
.read_to_string(&mut content);
|
|
||||||
|
|
||||||
// This could cause a problem when someone displays code containing <base href=...>
|
let _source = File::open(
|
||||||
|
book.get_destination()
|
||||||
|
.expect("If the HTML renderer is called, one would assume the HtmlConfig is set... (3)")
|
||||||
|
.join(&ch.path.with_extension("html"))
|
||||||
|
)?.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...
|
// on the front page, however this case should be very very rare...
|
||||||
content = content.lines()
|
content = content
|
||||||
|
.lines()
|
||||||
.filter(|line| !line.contains("<base href="))
|
.filter(|line| !line.contains("<base href="))
|
||||||
.collect::<Vec<&str>>()
|
.collect::<Vec<&str>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
try!(book.write_file("index.html", content.as_bytes()));
|
book.write_file("index.html", content.as_bytes())?;
|
||||||
|
|
||||||
info!("[*] Creating index.html from {:?} ✓",
|
info!("[*] Creating index.html from {:?} ✓",
|
||||||
book.get_dest().join(&ch.path.with_extension("html")));
|
book.get_destination()
|
||||||
|
.expect("If the HTML renderer is called, one would assume the HtmlConfig is set... (4)")
|
||||||
|
.join(&ch.path.with_extension("html"))
|
||||||
|
);
|
||||||
index = false;
|
index = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -145,7 +157,7 @@ impl Renderer for HtmlHandlebars {
|
||||||
// Render the handlebars template with the data
|
// Render the handlebars template with the data
|
||||||
debug!("[*]: Render template");
|
debug!("[*]: Render template");
|
||||||
|
|
||||||
let rendered = try!(handlebars.render("index", &data));
|
let rendered = handlebars.render("index", &data)?;
|
||||||
|
|
||||||
// do several kinds of post-processing
|
// do several kinds of post-processing
|
||||||
let rendered = build_header_links(rendered, "print.html");
|
let rendered = build_header_links(rendered, "print.html");
|
||||||
|
@ -153,29 +165,57 @@ impl Renderer for HtmlHandlebars {
|
||||||
let rendered = fix_code_blocks(rendered);
|
let rendered = fix_code_blocks(rendered);
|
||||||
let rendered = add_playpen_pre(rendered);
|
let rendered = add_playpen_pre(rendered);
|
||||||
|
|
||||||
try!(book.write_file(Path::new("print").with_extension("html"), &rendered.into_bytes()));
|
book.write_file(Path::new("print").with_extension("html"), &rendered.into_bytes())?;
|
||||||
info!("[*] Creating print.html ✓");
|
info!("[*] Creating print.html ✓");
|
||||||
|
|
||||||
// Copy static files (js, css, images, ...)
|
// Copy static files (js, css, images, ...)
|
||||||
|
|
||||||
debug!("[*] Copy static files");
|
debug!("[*] Copy static files");
|
||||||
try!(book.write_file("book.js", &theme.js));
|
book.write_file("book.js", &theme.js)?;
|
||||||
try!(book.write_file("book.css", &theme.css));
|
book.write_file("book.css", &theme.css)?;
|
||||||
try!(book.write_file("favicon.png", &theme.favicon));
|
book.write_file("favicon.png", &theme.favicon)?;
|
||||||
try!(book.write_file("jquery.js", &theme.jquery));
|
book.write_file("jquery.js", &theme.jquery)?;
|
||||||
try!(book.write_file("highlight.css", &theme.highlight_css));
|
book.write_file("highlight.css", &theme.highlight_css)?;
|
||||||
try!(book.write_file("tomorrow-night.css", &theme.tomorrow_night_css));
|
book.write_file("tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||||
try!(book.write_file("highlight.js", &theme.highlight_js));
|
book.write_file("ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||||
try!(book.write_file("_FontAwesome/css/font-awesome.css", theme::FONT_AWESOME));
|
book.write_file("highlight.js", &theme.highlight_js)?;
|
||||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.eot", theme::FONT_AWESOME_EOT));
|
book.write_file("clipboard.min.js", &theme.clipboard_js)?;
|
||||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.svg", theme::FONT_AWESOME_SVG));
|
book.write_file("store.js", &theme.store_js)?;
|
||||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.ttf", theme::FONT_AWESOME_TTF));
|
book.write_file("_FontAwesome/css/font-awesome.css", theme::FONT_AWESOME)?;
|
||||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff", theme::FONT_AWESOME_WOFF));
|
book.write_file("_FontAwesome/fonts/fontawesome-webfont.eot", theme::FONT_AWESOME_EOT)?;
|
||||||
try!(book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2", theme::FONT_AWESOME_WOFF2));
|
book.write_file("_FontAwesome/fonts/fontawesome-webfont.svg", theme::FONT_AWESOME_SVG)?;
|
||||||
try!(book.write_file("_FontAwesome/fonts/FontAwesome.ttf", theme::FONT_AWESOME_TTF));
|
book.write_file("_FontAwesome/fonts/fontawesome-webfont.ttf", theme::FONT_AWESOME_TTF)?;
|
||||||
|
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff", theme::FONT_AWESOME_WOFF)?;
|
||||||
|
book.write_file("_FontAwesome/fonts/fontawesome-webfont.woff2", theme::FONT_AWESOME_WOFF2)?;
|
||||||
|
book.write_file("_FontAwesome/fonts/FontAwesome.ttf", theme::FONT_AWESOME_TTF)?;
|
||||||
|
|
||||||
|
for custom_file in book.get_additional_css()
|
||||||
|
.iter()
|
||||||
|
.chain(book.get_additional_js().iter()) {
|
||||||
|
let mut data = Vec::new();
|
||||||
|
let mut f = File::open(custom_file)?;
|
||||||
|
f.read_to_end(&mut data)?;
|
||||||
|
|
||||||
|
let name = match custom_file.strip_prefix(book.get_root()) {
|
||||||
|
Ok(p) => p.to_str().expect("Could not convert to str"),
|
||||||
|
Err(_) => {
|
||||||
|
custom_file
|
||||||
|
.file_name()
|
||||||
|
.expect("File has a file name")
|
||||||
|
.to_str()
|
||||||
|
.expect("Could not convert to str")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
book.write_file(name, &data)?;
|
||||||
|
}
|
||||||
|
|
||||||
// Copy all remaining files
|
// Copy all remaining files
|
||||||
try!(utils::fs::copy_files_except_ext(book.get_src(), book.get_dest(), true, &["md"]));
|
utils::fs::copy_files_except_ext(
|
||||||
|
book.get_source(),
|
||||||
|
book.get_destination()
|
||||||
|
.expect("If the HTML renderer is called, one would assume the HtmlConfig is set... (5)"), true, &["md"]
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -193,6 +233,35 @@ fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>
|
||||||
data.insert("livereload".to_owned(), json!(livereload));
|
data.insert("livereload".to_owned(), json!(livereload));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add google analytics tag
|
||||||
|
if let Some(ref ga) = book.get_google_analytics_id() {
|
||||||
|
data.insert("google_analytics".to_owned(), json!(ga));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add check to see if there is an additional style
|
||||||
|
if book.has_additional_css() {
|
||||||
|
let mut css = Vec::new();
|
||||||
|
for style in book.get_additional_css() {
|
||||||
|
match style.strip_prefix(book.get_root()) {
|
||||||
|
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
|
||||||
|
Err(_) => css.push(style.file_name().expect("File has a file name").to_str().expect("Could not convert to str")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.insert("additional_css".to_owned(), json!(css));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add check to see if there is an additional script
|
||||||
|
if book.has_additional_js() {
|
||||||
|
let mut js = Vec::new();
|
||||||
|
for script in book.get_additional_js() {
|
||||||
|
match script.strip_prefix(book.get_root()) {
|
||||||
|
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
|
||||||
|
Err(_) => js.push(script.file_name().expect("File has a file name").to_str().expect("Could not convert to str")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.insert("additional_js".to_owned(), json!(js));
|
||||||
|
}
|
||||||
|
|
||||||
let mut chapters = vec![];
|
let mut chapters = vec![];
|
||||||
|
|
||||||
for item in book.iter() {
|
for item in book.iter() {
|
||||||
|
@ -202,15 +271,17 @@ fn make_data(book: &MDBook) -> Result<serde_json::Map<String, serde_json::Value>
|
||||||
match *item {
|
match *item {
|
||||||
BookItem::Affix(ref ch) => {
|
BookItem::Affix(ref ch) => {
|
||||||
chapter.insert("name".to_owned(), json!(ch.name));
|
chapter.insert("name".to_owned(), json!(ch.name));
|
||||||
let path = ch.path.to_str().ok_or_else(||
|
let path = ch.path
|
||||||
io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))?;
|
.to_str()
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))?;
|
||||||
chapter.insert("path".to_owned(), json!(path));
|
chapter.insert("path".to_owned(), json!(path));
|
||||||
},
|
},
|
||||||
BookItem::Chapter(ref s, ref ch) => {
|
BookItem::Chapter(ref s, ref ch) => {
|
||||||
chapter.insert("section".to_owned(), json!(s));
|
chapter.insert("section".to_owned(), json!(s));
|
||||||
chapter.insert("name".to_owned(), json!(ch.name));
|
chapter.insert("name".to_owned(), json!(ch.name));
|
||||||
let path = ch.path.to_str().ok_or_else(||
|
let path = ch.path
|
||||||
io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))?;
|
.to_str()
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not convert path to str"))?;
|
||||||
chapter.insert("path".to_owned(), json!(path));
|
chapter.insert("path".to_owned(), json!(path));
|
||||||
},
|
},
|
||||||
BookItem::Spacer => {
|
BookItem::Spacer => {
|
||||||
|
@ -232,42 +303,55 @@ fn build_header_links(html: String, filename: &str) -> String {
|
||||||
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
||||||
let mut id_counter = HashMap::new();
|
let mut id_counter = HashMap::new();
|
||||||
|
|
||||||
regex.replace_all(&html, |caps: &Captures| {
|
regex
|
||||||
let level = &caps[1];
|
.replace_all(&html, |caps: &Captures| {
|
||||||
let text = &caps[2];
|
let level = &caps[1];
|
||||||
let mut id = text.to_string();
|
let text = &caps[2];
|
||||||
let repl_sub = vec!["<em>", "</em>", "<code>", "</code>",
|
let mut id = text.to_string();
|
||||||
"<strong>", "</strong>",
|
let repl_sub = vec!["<em>",
|
||||||
"<", ">", "&", "'", """];
|
"</em>",
|
||||||
for sub in repl_sub {
|
"<code>",
|
||||||
id = id.replace(sub, "");
|
"</code>",
|
||||||
}
|
"<strong>",
|
||||||
let id = id.chars().filter_map(|c| {
|
"</strong>",
|
||||||
if c.is_alphanumeric() || c == '-' || c == '_' {
|
"<",
|
||||||
if c.is_ascii() {
|
">",
|
||||||
Some(c.to_ascii_lowercase())
|
"&",
|
||||||
} else {
|
"'",
|
||||||
Some(c)
|
"""];
|
||||||
}
|
for sub in repl_sub {
|
||||||
} else if c.is_whitespace() && c.is_ascii() {
|
id = id.replace(sub, "");
|
||||||
Some('-')
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}).collect::<String>();
|
let id = id.chars()
|
||||||
|
.filter_map(|c| if c.is_alphanumeric() || c == '-' || c == '_' {
|
||||||
|
if c.is_ascii() {
|
||||||
|
Some(c.to_ascii_lowercase())
|
||||||
|
} else {
|
||||||
|
Some(c)
|
||||||
|
}
|
||||||
|
} else if c.is_whitespace() && c.is_ascii() {
|
||||||
|
Some('-')
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
let id_count = *id_counter.get(&id).unwrap_or(&0);
|
let id_count = *id_counter.get(&id).unwrap_or(&0);
|
||||||
id_counter.insert(id.clone(), id_count + 1);
|
id_counter.insert(id.clone(), id_count + 1);
|
||||||
|
|
||||||
let id = if id_count > 0 {
|
let id = if id_count > 0 {
|
||||||
format!("{}-{}", id, id_count)
|
format!("{}-{}", id, id_count)
|
||||||
} else {
|
} else {
|
||||||
id
|
id
|
||||||
};
|
};
|
||||||
|
|
||||||
format!("<a class=\"header\" href=\"{filename}#{id}\" id=\"{id}\"><h{level}>{text}</h{level}></a>",
|
format!("<a class=\"header\" href=\"{filename}#{id}\" id=\"{id}\"><h{level}>{text}</h{level}></a>",
|
||||||
level=level, id=id, text=text, filename=filename)
|
level = level,
|
||||||
}).into_owned()
|
id = id,
|
||||||
|
text = text,
|
||||||
|
filename = filename)
|
||||||
|
})
|
||||||
|
.into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
// anchors to the same page (href="#anchor") do not work because of
|
// anchors to the same page (href="#anchor") do not work because of
|
||||||
|
@ -275,18 +359,24 @@ fn build_header_links(html: String, filename: &str) -> String {
|
||||||
// that in a very inelegant way
|
// that in a very inelegant way
|
||||||
fn fix_anchor_links(html: String, filename: &str) -> String {
|
fn fix_anchor_links(html: String, filename: &str) -> String {
|
||||||
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
|
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
|
||||||
regex.replace_all(&html, |caps: &Captures| {
|
regex
|
||||||
let before = &caps[1];
|
.replace_all(&html, |caps: &Captures| {
|
||||||
let anchor = &caps[2];
|
let before = &caps[1];
|
||||||
let after = &caps[3];
|
let anchor = &caps[2];
|
||||||
|
let after = &caps[3];
|
||||||
|
|
||||||
format!("<a{before}href=\"{filename}#{anchor}\"{after}>",
|
format!("<a{before}href=\"{filename}#{anchor}\"{after}>",
|
||||||
before=before, filename=filename, anchor=anchor, after=after)
|
before = before,
|
||||||
}).into_owned()
|
filename = filename,
|
||||||
|
anchor = anchor,
|
||||||
|
after = after)
|
||||||
|
})
|
||||||
|
.into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// The rust book uses annotations for rustdoc to test code snippets, like the following:
|
// The rust book uses annotations for rustdoc to test code snippets,
|
||||||
|
// like the following:
|
||||||
// ```rust,should_panic
|
// ```rust,should_panic
|
||||||
// fn main() {
|
// fn main() {
|
||||||
// // Code here
|
// // Code here
|
||||||
|
@ -295,40 +385,48 @@ fn fix_anchor_links(html: String, filename: &str) -> String {
|
||||||
// This function replaces all commas by spaces in the code block classes
|
// This function replaces all commas by spaces in the code block classes
|
||||||
fn fix_code_blocks(html: String) -> String {
|
fn fix_code_blocks(html: String) -> String {
|
||||||
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
||||||
regex.replace_all(&html, |caps: &Captures| {
|
regex
|
||||||
let before = &caps[1];
|
.replace_all(&html, |caps: &Captures| {
|
||||||
let classes = &caps[2].replace(",", " ");
|
let before = &caps[1];
|
||||||
let after = &caps[3];
|
let classes = &caps[2].replace(",", " ");
|
||||||
|
let after = &caps[3];
|
||||||
|
|
||||||
format!("<code{before}class=\"{classes}\"{after}>", before=before, classes=classes, after=after)
|
format!("<code{before}class=\"{classes}\"{after}>", before = before, classes = classes, after = after)
|
||||||
}).into_owned()
|
})
|
||||||
|
.into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_playpen_pre(html: String) -> String {
|
fn add_playpen_pre(html: String) -> String {
|
||||||
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||||
regex.replace_all(&html, |caps: &Captures| {
|
regex
|
||||||
let text = &caps[1];
|
.replace_all(&html, |caps: &Captures| {
|
||||||
let classes = &caps[2];
|
let text = &caps[1];
|
||||||
let code = &caps[3];
|
let classes = &caps[2];
|
||||||
|
let code = &caps[3];
|
||||||
|
|
||||||
if classes.contains("language-rust") && !classes.contains("ignore") {
|
if classes.contains("language-rust") && !classes.contains("ignore") {
|
||||||
// wrap the contents in an external pre block
|
// wrap the contents in an external pre block
|
||||||
|
|
||||||
if text.contains("fn main") {
|
if text.contains("fn main") || text.contains("quick_main!") {
|
||||||
format!("<pre class=\"playpen\">{}</pre>", text)
|
format!("<pre class=\"playpen\">{}</pre>", text)
|
||||||
} else {
|
} else {
|
||||||
// we need to inject our own main
|
// we need to inject our own main
|
||||||
let (attrs, code) = partition_source(code);
|
let (attrs, code) = partition_source(code);
|
||||||
format!("<pre class=\"playpen\"><code class=\"{}\"># #![allow(unused_variables)]
|
format!("<pre class=\"playpen\"><code class=\"{}\"># #![allow(unused_variables)]
|
||||||
{}#fn main() {{
|
{}#fn main() {{
|
||||||
{}
|
\
|
||||||
#}}</code></pre>", classes, attrs, code)
|
{}
|
||||||
|
#}}</code></pre>",
|
||||||
|
classes,
|
||||||
|
attrs,
|
||||||
|
code)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// not language-rust, so no-op
|
||||||
|
format!("{}", text)
|
||||||
}
|
}
|
||||||
} else {
|
})
|
||||||
// not language-rust, so no-op
|
.into_owned()
|
||||||
format!("{}", text)
|
|
||||||
}
|
|
||||||
}).into_owned()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn partition_source(s: &str) -> (String, String) {
|
fn partition_source(s: &str) -> (String, String) {
|
||||||
|
@ -338,8 +436,7 @@ fn partition_source(s: &str) -> (String, String) {
|
||||||
|
|
||||||
for line in s.lines() {
|
for line in s.lines() {
|
||||||
let trimline = line.trim();
|
let trimline = line.trim();
|
||||||
let header = trimline.chars().all(|c| c.is_whitespace()) ||
|
let header = trimline.chars().all(|c| c.is_whitespace()) || trimline.starts_with("#![");
|
||||||
trimline.starts_with("#![");
|
|
||||||
if !header || after_header {
|
if !header || after_header {
|
||||||
after_header = true;
|
after_header = true;
|
||||||
after.push_str(line);
|
after.push_str(line);
|
||||||
|
|
|
@ -14,9 +14,12 @@ pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(
|
||||||
// get value from context data
|
// get value from context data
|
||||||
// rc.get_path() is current json parent path, you should always use it like this
|
// 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
|
// param is the key of value you want to display
|
||||||
let chapters = rc.context().navigate(rc.get_path(), &VecDeque::new(), "chapters").to_owned();
|
let chapters = rc.context()
|
||||||
|
.navigate(rc.get_path(), &VecDeque::new(), "chapters")
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
let current = rc.context().navigate(rc.get_path(), &VecDeque::new(), "path")
|
let current = rc.context()
|
||||||
|
.navigate(rc.get_path(), &VecDeque::new(), "path")
|
||||||
.to_string()
|
.to_string()
|
||||||
.replace("\"", "");
|
.replace("\"", "");
|
||||||
|
|
||||||
|
@ -84,7 +87,7 @@ pub fn previous(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(
|
||||||
match _h.template() {
|
match _h.template() {
|
||||||
Some(t) => {
|
Some(t) => {
|
||||||
*rc.context_mut() = updated_context;
|
*rc.context_mut() = updated_context;
|
||||||
try!(t.render(r, rc));
|
t.render(r, rc)?;
|
||||||
},
|
},
|
||||||
None => return Err(RenderError::new("Error with the handlebars template")),
|
None => return Err(RenderError::new("Error with the handlebars template")),
|
||||||
}
|
}
|
||||||
|
@ -115,9 +118,12 @@ pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), R
|
||||||
// get value from context data
|
// get value from context data
|
||||||
// rc.get_path() is current json parent path, you should always use it like this
|
// 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
|
// param is the key of value you want to display
|
||||||
let chapters = rc.context().navigate(rc.get_path(), &VecDeque::new(), "chapters").to_owned();
|
let chapters = rc.context()
|
||||||
|
.navigate(rc.get_path(), &VecDeque::new(), "chapters")
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
let current = rc.context().navigate(rc.get_path(), &VecDeque::new(), "path")
|
let current = rc.context()
|
||||||
|
.navigate(rc.get_path(), &VecDeque::new(), "path")
|
||||||
.to_string()
|
.to_string()
|
||||||
.replace("\"", "");
|
.replace("\"", "");
|
||||||
|
|
||||||
|
@ -181,7 +187,7 @@ pub fn next(_h: &Helper, r: &Handlebars, rc: &mut RenderContext) -> Result<(), R
|
||||||
match _h.template() {
|
match _h.template() {
|
||||||
Some(t) => {
|
Some(t) => {
|
||||||
*rc.context_mut() = updated_context;
|
*rc.context_mut() = updated_context;
|
||||||
try!(t.render(r, rc));
|
t.render(r, rc)?;
|
||||||
},
|
},
|
||||||
None => return Err(RenderError::new("Error with the handlebars template")),
|
None => return Err(RenderError::new("Error with the handlebars template")),
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,9 @@ use std::io::Read;
|
||||||
|
|
||||||
|
|
||||||
pub fn render_playpen(s: &str, path: &Path) -> String {
|
pub fn render_playpen(s: &str, path: &Path) -> String {
|
||||||
// When replacing one thing in a string by something with a different length, the indices
|
// When replacing one thing in a string by something with a different length,
|
||||||
// after that will not correspond, we therefore have to store the difference to correct this
|
// 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 previous_end_index = 0;
|
||||||
let mut replaced = String::new();
|
let mut replaced = String::new();
|
||||||
|
|
||||||
|
@ -35,13 +36,13 @@ pub fn render_playpen(s: &str, path: &Path) -> String {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let replacement = String::new() + "<pre><code class=\"language-rust\">" + &file_content +
|
let replacement = String::new() + "<pre><code class=\"language-rust\">" + &file_content + "</code></pre>";
|
||||||
"</code></pre>";
|
|
||||||
|
|
||||||
replaced.push_str(&s[previous_end_index..playpen.start_index]);
|
replaced.push_str(&s[previous_end_index..playpen.start_index]);
|
||||||
replaced.push_str(&replacement);
|
replaced.push_str(&replacement);
|
||||||
previous_end_index = playpen.end_index;
|
previous_end_index = playpen.end_index;
|
||||||
// println!("Playpen{{ {}, {}, {:?}, {} }}", playpen.start_index, playpen.end_index, playpen.rust_file,
|
// println!("Playpen{{ {}, {}, {:?}, {} }}", playpen.start_index,
|
||||||
|
// playpen.end_index, playpen.rust_file,
|
||||||
// playpen.editable);
|
// playpen.editable);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,12 +101,12 @@ fn find_playpens(s: &str, base_path: &Path) -> Vec<Playpen> {
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
playpens.push(Playpen {
|
playpens.push(Playpen {
|
||||||
start_index: i,
|
start_index: i,
|
||||||
end_index: end_i,
|
end_index: end_i,
|
||||||
rust_file: base_path.join(PathBuf::from(params[0])),
|
rust_file: base_path.join(PathBuf::from(params[0])),
|
||||||
editable: editable,
|
editable: editable,
|
||||||
escaped: escaped,
|
escaped: escaped,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
playpens
|
playpens
|
||||||
|
@ -189,7 +190,11 @@ fn test_find_playpens_escaped_playpen() {
|
||||||
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("")));
|
println!("\nOUTPUT: {:?}\n", find_playpens(s, Path::new("")));
|
||||||
|
|
||||||
assert!(find_playpens(s, Path::new("")) ==
|
assert!(find_playpens(s, Path::new("")) ==
|
||||||
vec![
|
vec![Playpen {
|
||||||
Playpen{start_index: 39, end_index: 68, rust_file: PathBuf::from("file.rs"), editable: true, escaped: true},
|
start_index: 39,
|
||||||
]);
|
end_index: 68,
|
||||||
|
rust_file: PathBuf::from("file.rs"),
|
||||||
|
editable: true,
|
||||||
|
escaped: true,
|
||||||
|
}]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,14 @@ impl HelperDef for RenderToc {
|
||||||
// get value from context data
|
// get value from context data
|
||||||
// rc.get_path() is current json parent path, you should always use it like this
|
// 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
|
// param is the key of value you want to display
|
||||||
let chapters = rc.context().navigate(rc.get_path(), &VecDeque::new(), "chapters").to_owned();
|
let chapters = rc.context()
|
||||||
let current = rc.context().navigate(rc.get_path(), &VecDeque::new(), "path").to_string().replace("\"", "");
|
.navigate(rc.get_path(), &VecDeque::new(), "chapters")
|
||||||
try!(rc.writer.write_all("<ul class=\"chapter\">".as_bytes()));
|
.to_owned();
|
||||||
|
let current = rc.context()
|
||||||
|
.navigate(rc.get_path(), &VecDeque::new(), "path")
|
||||||
|
.to_string()
|
||||||
|
.replace("\"", "");
|
||||||
|
rc.writer.write_all("<ul class=\"chapter\">".as_bytes())?;
|
||||||
|
|
||||||
// Decode json format
|
// Decode json format
|
||||||
let decoded: Vec<BTreeMap<String, String>> = serde_json::from_str(&chapters.to_string()).unwrap();
|
let decoded: Vec<BTreeMap<String, String>> = serde_json::from_str(&chapters.to_string()).unwrap();
|
||||||
|
@ -28,7 +33,8 @@ impl HelperDef for RenderToc {
|
||||||
|
|
||||||
// Spacer
|
// Spacer
|
||||||
if item.get("spacer").is_some() {
|
if item.get("spacer").is_some() {
|
||||||
try!(rc.writer.write_all("<li class=\"spacer\"></li>".as_bytes()));
|
rc.writer
|
||||||
|
.write_all("<li class=\"spacer\"></li>".as_bytes())?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,48 +46,47 @@ impl HelperDef for RenderToc {
|
||||||
|
|
||||||
if level > current_level {
|
if level > current_level {
|
||||||
while level > current_level {
|
while level > current_level {
|
||||||
try!(rc.writer.write_all("<li>".as_bytes()));
|
rc.writer.write_all("<li>".as_bytes())?;
|
||||||
try!(rc.writer.write_all("<ul class=\"section\">".as_bytes()));
|
rc.writer.write_all("<ul class=\"section\">".as_bytes())?;
|
||||||
current_level += 1;
|
current_level += 1;
|
||||||
}
|
}
|
||||||
try!(rc.writer.write_all("<li>".as_bytes()));
|
rc.writer.write_all("<li>".as_bytes())?;
|
||||||
} else if level < current_level {
|
} else if level < current_level {
|
||||||
while level < current_level {
|
while level < current_level {
|
||||||
try!(rc.writer.write_all("</ul>".as_bytes()));
|
rc.writer.write_all("</ul>".as_bytes())?;
|
||||||
try!(rc.writer.write_all("</li>".as_bytes()));
|
rc.writer.write_all("</li>".as_bytes())?;
|
||||||
current_level -= 1;
|
current_level -= 1;
|
||||||
}
|
}
|
||||||
try!(rc.writer.write_all("<li>".as_bytes()));
|
rc.writer.write_all("<li>".as_bytes())?;
|
||||||
} else {
|
} else {
|
||||||
try!(rc.writer.write_all("<li".as_bytes()));
|
rc.writer.write_all("<li".as_bytes())?;
|
||||||
if item.get("section").is_none() {
|
if item.get("section").is_none() {
|
||||||
try!(rc.writer.write_all(" class=\"affix\"".as_bytes()));
|
rc.writer.write_all(" class=\"affix\"".as_bytes())?;
|
||||||
}
|
}
|
||||||
try!(rc.writer.write_all(">".as_bytes()));
|
rc.writer.write_all(">".as_bytes())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Link
|
// Link
|
||||||
let path_exists = if let Some(path) = item.get("path") {
|
let path_exists = if let Some(path) = item.get("path") {
|
||||||
if !path.is_empty() {
|
if !path.is_empty() {
|
||||||
try!(rc.writer.write_all("<a href=\"".as_bytes()));
|
rc.writer.write_all("<a href=\"".as_bytes())?;
|
||||||
|
|
||||||
|
let tmp = 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("\\", "/");
|
||||||
|
|
||||||
// Add link
|
// Add link
|
||||||
try!(rc.writer.write_all(Path::new(item.get("path")
|
rc.writer.write_all(tmp.as_bytes())?;
|
||||||
.expect("Error: path should be Some(_)"))
|
rc.writer.write_all("\"".as_bytes())?;
|
||||||
.with_extension("html")
|
|
||||||
.to_str()
|
|
||||||
.unwrap()
|
|
||||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
|
||||||
.replace("\\", "/")
|
|
||||||
.as_bytes()));
|
|
||||||
|
|
||||||
try!(rc.writer.write_all("\"".as_bytes()));
|
|
||||||
|
|
||||||
if path == ¤t {
|
if path == ¤t {
|
||||||
try!(rc.writer.write_all(" class=\"active\"".as_bytes()));
|
rc.writer.write_all(" class=\"active\"".as_bytes())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
try!(rc.writer.write_all(">".as_bytes()));
|
rc.writer.write_all(">".as_bytes())?;
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -92,47 +97,45 @@ impl HelperDef for RenderToc {
|
||||||
|
|
||||||
// Section does not necessarily exist
|
// Section does not necessarily exist
|
||||||
if let Some(section) = item.get("section") {
|
if let Some(section) = item.get("section") {
|
||||||
try!(rc.writer.write_all("<strong>".as_bytes()));
|
rc.writer.write_all("<strong>".as_bytes())?;
|
||||||
try!(rc.writer.write_all(section.as_bytes()));
|
rc.writer.write_all(section.as_bytes())?;
|
||||||
try!(rc.writer.write_all("</strong> ".as_bytes()));
|
rc.writer.write_all("</strong> ".as_bytes())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = item.get("name") {
|
if let Some(name) = item.get("name") {
|
||||||
// Render only inline code blocks
|
// Render only inline code blocks
|
||||||
|
|
||||||
// filter all events that are not inline code blocks
|
// filter all events that are not inline code blocks
|
||||||
let parser = Parser::new(name).filter(|event| {
|
let parser = Parser::new(name).filter(|event| match *event {
|
||||||
match *event {
|
Event::Start(Tag::Code) |
|
||||||
Event::Start(Tag::Code) |
|
Event::End(Tag::Code) |
|
||||||
Event::End(Tag::Code) |
|
Event::InlineHtml(_) |
|
||||||
Event::InlineHtml(_) |
|
Event::Text(_) => true,
|
||||||
Event::Text(_) => true,
|
_ => false,
|
||||||
_ => false,
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// render markdown to html
|
// render markdown to html
|
||||||
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
|
let mut markdown_parsed_name = String::with_capacity(name.len() * 3 / 2);
|
||||||
html::push_html(&mut markdown_parsed_name, parser);
|
html::push_html(&mut markdown_parsed_name, parser);
|
||||||
|
|
||||||
// write to the handlebars template
|
// write to the handlebars template
|
||||||
try!(rc.writer.write_all(markdown_parsed_name.as_bytes()));
|
rc.writer.write_all(markdown_parsed_name.as_bytes())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if path_exists {
|
if path_exists {
|
||||||
try!(rc.writer.write_all("</a>".as_bytes()));
|
rc.writer.write_all("</a>".as_bytes())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
try!(rc.writer.write_all("</li>".as_bytes()));
|
rc.writer.write_all("</li>".as_bytes())?;
|
||||||
|
|
||||||
}
|
}
|
||||||
while current_level > 1 {
|
while current_level > 1 {
|
||||||
try!(rc.writer.write_all("</ul>".as_bytes()));
|
rc.writer.write_all("</ul>".as_bytes())?;
|
||||||
try!(rc.writer.write_all("</li>".as_bytes()));
|
rc.writer.write_all("</li>".as_bytes())?;
|
||||||
current_level -= 1;
|
current_level -= 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
try!(rc.writer.write_all("</ul>".as_bytes()));
|
rc.writer.write_all("</ul>".as_bytes())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
Based off of the Ayu theme
|
||||||
|
Original by Dempfi (https://github.com/dempfi/ayu)
|
||||||
|
*/
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: #191f26;
|
||||||
|
color: #e6e1cf;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote,
|
||||||
|
.hljs-meta {
|
||||||
|
color: #5c6773;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-selector-class {
|
||||||
|
color: #ff7733;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-builtin-name,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-params {
|
||||||
|
color: #ffee99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-bullet {
|
||||||
|
color: #b8cc52;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-section {
|
||||||
|
color: #ffb454;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-symbol {
|
||||||
|
color: #ff7733;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-name {
|
||||||
|
color: #36a3d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-tag {
|
||||||
|
color: #00568d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ body {
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
code {
|
code {
|
||||||
font-family: "Source Code Pro", "Menlo", "DejaVu Sans Mono", monospace;
|
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||||
font-size: 0.875em;
|
font-size: 0.875em;
|
||||||
}
|
}
|
||||||
.left {
|
.left {
|
||||||
|
@ -137,6 +137,7 @@ table thead td {
|
||||||
right: 0;
|
right: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
outline: 0;
|
||||||
padding-right: 15px;
|
padding-right: 15px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
@ -258,7 +259,6 @@ table thead td {
|
||||||
position: relative;
|
position: relative;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
-webkit-border-radius: 4px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.7em;
|
font-size: 0.7em;
|
||||||
}
|
}
|
||||||
|
@ -295,7 +295,6 @@ table thead td {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: 50px;
|
margin-bottom: 50px;
|
||||||
-webkit-border-radius: 5px;
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
.next {
|
.next {
|
||||||
|
@ -357,7 +356,8 @@ table thead td {
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
}
|
}
|
||||||
.light .content a:link,
|
.light .content a:link,
|
||||||
.light a:visited {
|
.light a:visited,
|
||||||
|
.light a > .hljs {
|
||||||
color: #4183c4;
|
color: #4183c4;
|
||||||
}
|
}
|
||||||
.light .theme-popup {
|
.light .theme-popup {
|
||||||
|
@ -398,14 +398,18 @@ table thead td {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding: 0.1em 0.3em;
|
padding: 0.1em 0.3em;
|
||||||
-webkit-border-radius: 3px;
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
color: #6e6b5e;
|
||||||
|
}
|
||||||
|
.light a:hover > .hljs {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
.light pre {
|
.light pre {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.light pre > .buttons {
|
.light pre > .buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
color: #364149;
|
color: #364149;
|
||||||
|
@ -472,7 +476,8 @@ table thead td {
|
||||||
background-color: #292c2f;
|
background-color: #292c2f;
|
||||||
}
|
}
|
||||||
.coal .content a:link,
|
.coal .content a:link,
|
||||||
.coal a:visited {
|
.coal a:visited,
|
||||||
|
.coal a > .hljs {
|
||||||
color: #2b79a2;
|
color: #2b79a2;
|
||||||
}
|
}
|
||||||
.coal .theme-popup {
|
.coal .theme-popup {
|
||||||
|
@ -513,14 +518,18 @@ table thead td {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding: 0.1em 0.3em;
|
padding: 0.1em 0.3em;
|
||||||
-webkit-border-radius: 3px;
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
color: #c5c8c6;
|
||||||
|
}
|
||||||
|
.coal a:hover > .hljs {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
.coal pre {
|
.coal pre {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.coal pre > .buttons {
|
.coal pre > .buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
color: #a1adb8;
|
color: #a1adb8;
|
||||||
|
@ -587,7 +596,8 @@ table thead td {
|
||||||
background-color: #282d3f;
|
background-color: #282d3f;
|
||||||
}
|
}
|
||||||
.navy .content a:link,
|
.navy .content a:link,
|
||||||
.navy a:visited {
|
.navy a:visited,
|
||||||
|
.navy a > .hljs {
|
||||||
color: #2b79a2;
|
color: #2b79a2;
|
||||||
}
|
}
|
||||||
.navy .theme-popup {
|
.navy .theme-popup {
|
||||||
|
@ -628,14 +638,18 @@ table thead td {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding: 0.1em 0.3em;
|
padding: 0.1em 0.3em;
|
||||||
-webkit-border-radius: 3px;
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
color: #c5c8c6;
|
||||||
|
}
|
||||||
|
.navy a:hover > .hljs {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
.navy pre {
|
.navy pre {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.navy pre > .buttons {
|
.navy pre > .buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
color: #c8c9db;
|
color: #c8c9db;
|
||||||
|
@ -702,7 +716,8 @@ table thead td {
|
||||||
background-color: #3b2e2a;
|
background-color: #3b2e2a;
|
||||||
}
|
}
|
||||||
.rust .content a:link,
|
.rust .content a:link,
|
||||||
.rust a:visited {
|
.rust a:visited,
|
||||||
|
.rust a > .hljs {
|
||||||
color: #2b79a2;
|
color: #2b79a2;
|
||||||
}
|
}
|
||||||
.rust .theme-popup {
|
.rust .theme-popup {
|
||||||
|
@ -743,14 +758,18 @@ table thead td {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding: 0.1em 0.3em;
|
padding: 0.1em 0.3em;
|
||||||
-webkit-border-radius: 3px;
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
color: #6e6b5e;
|
||||||
|
}
|
||||||
|
.rust a:hover > .hljs {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
.rust pre {
|
.rust pre {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.rust pre > .buttons {
|
.rust pre > .buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
color: #c8c9db;
|
color: #c8c9db;
|
||||||
|
@ -765,6 +784,126 @@ table thead td {
|
||||||
.rust pre > .result {
|
.rust pre > .result {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
.ayu {
|
||||||
|
color: #c5c5c5;
|
||||||
|
background-color: #0f1419;
|
||||||
|
/* Inline code */
|
||||||
|
}
|
||||||
|
.ayu .content .header:link,
|
||||||
|
.ayu .content .header:visited {
|
||||||
|
color: #c5c5c5;
|
||||||
|
pointer: cursor;
|
||||||
|
}
|
||||||
|
.ayu .content .header:link:hover,
|
||||||
|
.ayu .content .header:visited:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.ayu .sidebar {
|
||||||
|
background-color: #14191f;
|
||||||
|
color: #c8c9db;
|
||||||
|
}
|
||||||
|
.ayu .chapter li {
|
||||||
|
color: #5c6773;
|
||||||
|
}
|
||||||
|
.ayu .chapter li a {
|
||||||
|
color: #c8c9db;
|
||||||
|
}
|
||||||
|
.ayu .chapter li .active,
|
||||||
|
.ayu .chapter li a:hover {
|
||||||
|
/* Animate color change */
|
||||||
|
color: #ffb454;
|
||||||
|
}
|
||||||
|
.ayu .chapter .spacer {
|
||||||
|
background-color: #2d334f;
|
||||||
|
}
|
||||||
|
.ayu .menu-bar,
|
||||||
|
.ayu .menu-bar:visited,
|
||||||
|
.ayu .nav-chapters,
|
||||||
|
.ayu .nav-chapters:visited,
|
||||||
|
.ayu .mobile-nav-chapters,
|
||||||
|
.ayu .mobile-nav-chapters:visited {
|
||||||
|
color: #737480;
|
||||||
|
}
|
||||||
|
.ayu .menu-bar i:hover,
|
||||||
|
.ayu .nav-chapters:hover,
|
||||||
|
.ayu .mobile-nav-chapters i:hover {
|
||||||
|
color: #b7b9cc;
|
||||||
|
}
|
||||||
|
.ayu .mobile-nav-chapters i:hover {
|
||||||
|
color: #c8c9db;
|
||||||
|
}
|
||||||
|
.ayu .mobile-nav-chapters {
|
||||||
|
background-color: #14191f;
|
||||||
|
}
|
||||||
|
.ayu .content a:link,
|
||||||
|
.ayu a:visited,
|
||||||
|
.ayu a > .hljs {
|
||||||
|
color: #0096cf;
|
||||||
|
}
|
||||||
|
.ayu .theme-popup {
|
||||||
|
color: #c5c5c5;
|
||||||
|
background: #14191f;
|
||||||
|
border: 1px solid #5c6773;
|
||||||
|
}
|
||||||
|
.ayu .theme-popup .theme:hover {
|
||||||
|
background-color: #191f26;
|
||||||
|
}
|
||||||
|
.ayu .theme-popup .default {
|
||||||
|
color: #737480;
|
||||||
|
}
|
||||||
|
.ayu blockquote {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
color: #c5c5c5;
|
||||||
|
background-color: #262933;
|
||||||
|
border-top: 0.1em solid #2f333f;
|
||||||
|
border-bottom: 0.1em solid #2f333f;
|
||||||
|
}
|
||||||
|
.ayu table td {
|
||||||
|
border-color: #182028;
|
||||||
|
}
|
||||||
|
.ayu table tbody tr:nth-child(2n) {
|
||||||
|
background: #141b22;
|
||||||
|
}
|
||||||
|
.ayu table thead {
|
||||||
|
background: #324354;
|
||||||
|
}
|
||||||
|
.ayu table thead td {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.ayu table thead tr {
|
||||||
|
border: 1px #324354 solid;
|
||||||
|
}
|
||||||
|
.ayu :not(pre) > .hljs {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #ffb454;
|
||||||
|
}
|
||||||
|
.ayu a:hover > .hljs {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.ayu pre {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.ayu pre > .buttons {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
right: 5px;
|
||||||
|
top: 5px;
|
||||||
|
color: #c8c9db;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.ayu pre > .buttons :hover {
|
||||||
|
color: #ffb454;
|
||||||
|
}
|
||||||
|
.ayu pre > .buttons i {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.ayu pre > .result {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
@media only print {
|
@media only print {
|
||||||
#sidebar,
|
#sidebar,
|
||||||
#menu-bar,
|
#menu-bar,
|
||||||
|
@ -786,7 +925,6 @@ table thead td {
|
||||||
}
|
}
|
||||||
code {
|
code {
|
||||||
background-color: #666;
|
background-color: #666;
|
||||||
-webkit-border-radius: 5px;
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
/* Force background to be printed in Chrome */
|
/* Force background to be printed in Chrome */
|
||||||
-webkit-print-color-adjust: exact;
|
-webkit-print-color-adjust: exact;
|
||||||
|
@ -818,3 +956,25 @@ table thead td {
|
||||||
word-wrap: break-word /* Internet Explorer 5.5+ */;
|
word-wrap: break-word /* Internet Explorer 5.5+ */;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.tooltiptext {
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #333;
|
||||||
|
-webkit-transform: translateX(-50%);
|
||||||
|
-moz-transform: translateX(-50%);
|
||||||
|
-o-transform: translateX(-50%);
|
||||||
|
-ms-transform: translateX(-50%);
|
||||||
|
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
|
||||||
|
left: -8px; /* Half of the width of the icon */
|
||||||
|
top: -35px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
margin: 5px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.tooltipped .tooltiptext {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
|
@ -7,8 +7,8 @@ $( document ).ready(function() {
|
||||||
window.onunload = function(){};
|
window.onunload = function(){};
|
||||||
|
|
||||||
// Set theme
|
// Set theme
|
||||||
var theme = localStorage.getItem('theme');
|
var theme = store.get('theme');
|
||||||
if (theme === null) { theme = 'light'; }
|
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||||
|
|
||||||
set_theme(theme);
|
set_theme(theme);
|
||||||
|
|
||||||
|
@ -51,31 +51,22 @@ $( document ).ready(function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Interesting DOM Elements
|
// Interesting DOM Elements
|
||||||
var html = $("html");
|
|
||||||
var sidebar = $("#sidebar");
|
var sidebar = $("#sidebar");
|
||||||
var page_wrapper = $("#page-wrapper");
|
|
||||||
var content = $("#content");
|
// Help keyboard navigation by always focusing on page content
|
||||||
|
$(".page").focus();
|
||||||
|
|
||||||
// Toggle sidebar
|
// Toggle sidebar
|
||||||
$("#sidebar-toggle").click(function(event){
|
$("#sidebar-toggle").click(sidebarToggle);
|
||||||
if ( html.hasClass("sidebar-hidden") ) {
|
|
||||||
html.removeClass("sidebar-hidden").addClass("sidebar-visible");
|
// Hide sidebar on section link click if it occupies large space
|
||||||
localStorage.setItem('sidebar', 'visible');
|
// in relation to the whole screen (phone in portrait)
|
||||||
} else if ( html.hasClass("sidebar-visible") ) {
|
$("#sidebar a").click(function(event){
|
||||||
html.removeClass("sidebar-visible").addClass("sidebar-hidden");
|
if (sidebar.width() > window.screen.width * 0.4) {
|
||||||
localStorage.setItem('sidebar', 'hidden');
|
sidebarToggle();
|
||||||
} else {
|
|
||||||
if(sidebar.position().left === 0){
|
|
||||||
html.addClass("sidebar-hidden");
|
|
||||||
localStorage.setItem('sidebar', 'hidden');
|
|
||||||
} else {
|
|
||||||
html.addClass("sidebar-visible");
|
|
||||||
localStorage.setItem('sidebar', 'visible');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// Scroll sidebar to current active section
|
// Scroll sidebar to current active section
|
||||||
var activeSection = sidebar.find(".active");
|
var activeSection = sidebar.find(".active");
|
||||||
if(activeSection.length) {
|
if(activeSection.length) {
|
||||||
|
@ -102,7 +93,8 @@ $( document ).ready(function() {
|
||||||
.append($('<div class="theme" id="light">Light <span class="default">(default)</span><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="rust">Rust</div>'))
|
||||||
.append($('<div class="theme" id="coal">Coal</div>'))
|
.append($('<div class="theme" id="coal">Coal</div>'))
|
||||||
.append($('<div class="theme" id="navy">Navy</div>'));
|
.append($('<div class="theme" id="navy">Navy</div>'))
|
||||||
|
.append($('<div class="theme" id="ayu">Ayu</div>'));
|
||||||
|
|
||||||
|
|
||||||
popup.insertAfter(this);
|
popup.insertAfter(this);
|
||||||
|
@ -118,14 +110,20 @@ $( document ).ready(function() {
|
||||||
|
|
||||||
function set_theme(theme) {
|
function set_theme(theme) {
|
||||||
if (theme == 'coal' || theme == 'navy') {
|
if (theme == 'coal' || theme == 'navy') {
|
||||||
|
$("[href='ayu-highlight.css']").prop('disabled', true);
|
||||||
$("[href='tomorrow-night.css']").prop('disabled', false);
|
$("[href='tomorrow-night.css']").prop('disabled', false);
|
||||||
$("[href='highlight.css']").prop('disabled', true);
|
$("[href='highlight.css']").prop('disabled', true);
|
||||||
|
} else if (theme == 'ayu') {
|
||||||
|
$("[href='ayu-highlight.css']").prop('disabled', false);
|
||||||
|
$("[href='tomorrow-night.css']").prop('disabled', true);
|
||||||
|
$("[href='highlight.css']").prop('disabled', true);
|
||||||
} else {
|
} else {
|
||||||
|
$("[href='ayu-highlight.css']").prop('disabled', true);
|
||||||
$("[href='tomorrow-night.css']").prop('disabled', true);
|
$("[href='tomorrow-night.css']").prop('disabled', true);
|
||||||
$("[href='highlight.css']").prop('disabled', false);
|
$("[href='highlight.css']").prop('disabled', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem('theme', theme);
|
store.set('theme', theme);
|
||||||
|
|
||||||
$('body').removeClass().addClass(theme);
|
$('body').removeClass().addClass(theme);
|
||||||
}
|
}
|
||||||
|
@ -146,10 +144,10 @@ $( document ).ready(function() {
|
||||||
for(var n = 0; n < lines.length; n++){
|
for(var n = 0; n < lines.length; n++){
|
||||||
if($.trim(lines[n])[0] == hiding_character){
|
if($.trim(lines[n])[0] == hiding_character){
|
||||||
if(first_non_hidden_line){
|
if(first_non_hidden_line){
|
||||||
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)#/, "$1") + "</span>";
|
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)# ?/, "$1") + "</span>";
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
lines[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)#/, "$1") + "\n" + "</span>";
|
lines[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)# ?/, "$1") + "\n" + "</span>";
|
||||||
}
|
}
|
||||||
lines_hidden = true;
|
lines_hidden = true;
|
||||||
}
|
}
|
||||||
|
@ -191,15 +189,60 @@ $( document ).ready(function() {
|
||||||
buttons = pre_block.find(".buttons");
|
buttons = pre_block.find(".buttons");
|
||||||
}
|
}
|
||||||
buttons.prepend("<i class=\"fa fa-play play-button\"></i>");
|
buttons.prepend("<i class=\"fa fa-play play-button\"></i>");
|
||||||
|
buttons.prepend("<i class=\"fa fa-copy clip-button\"><i class=\"tooltiptext\"></i></i>");
|
||||||
|
|
||||||
buttons.find(".play-button").click(function(e){
|
buttons.find(".play-button").click(function(e){
|
||||||
run_rust_code(pre_block);
|
run_rust_code(pre_block);
|
||||||
});
|
});
|
||||||
|
buttons.find(".clip-button").mouseout(function(e){
|
||||||
|
hideTooltip(e.currentTarget);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var clipboardSnippets = new Clipboard('.clip-button', {
|
||||||
|
text: function(trigger) {
|
||||||
|
hideTooltip(trigger);
|
||||||
|
return $(trigger).parents(".playpen").find("code.language-rust.hljs")[0].textContent;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
clipboardSnippets.on('success', function(e) {
|
||||||
|
e.clearSelection();
|
||||||
|
showTooltip(e.trigger, "Copied!");
|
||||||
|
});
|
||||||
|
clipboardSnippets.on('error', function(e) {
|
||||||
|
showTooltip(e.trigger, "Clipboard error!");
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function hideTooltip(elem) {
|
||||||
|
elem.firstChild.innerText="";
|
||||||
|
elem.setAttribute('class', 'fa fa-copy clip-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTooltip(elem, msg) {
|
||||||
|
elem.firstChild.innerText=msg;
|
||||||
|
elem.setAttribute('class', 'fa fa-copy tooltipped');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sidebarToggle() {
|
||||||
|
var html = $("html");
|
||||||
|
if ( html.hasClass("sidebar-hidden") ) {
|
||||||
|
html.removeClass("sidebar-hidden").addClass("sidebar-visible");
|
||||||
|
store.set('sidebar', 'visible');
|
||||||
|
} else if ( html.hasClass("sidebar-visible") ) {
|
||||||
|
html.removeClass("sidebar-visible").addClass("sidebar-hidden");
|
||||||
|
store.set('sidebar', 'hidden');
|
||||||
|
} else {
|
||||||
|
if($("#sidebar").position().left === 0){
|
||||||
|
html.addClass("sidebar-hidden");
|
||||||
|
store.set('sidebar', 'hidden');
|
||||||
|
} else {
|
||||||
|
html.addClass("sidebar-visible");
|
||||||
|
store.set('sidebar', 'visible');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function run_rust_code(code_block) {
|
function run_rust_code(code_block) {
|
||||||
var result_block = code_block.find(".result");
|
var result_block = code_block.find(".result");
|
||||||
|
@ -208,15 +251,15 @@ function run_rust_code(code_block) {
|
||||||
result_block = code_block.find(".result");
|
result_block = code_block.find(".result");
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = code_block.find(".language-rust").text();
|
var text = code_block.find(".language-rust").text();
|
||||||
|
|
||||||
let params = {
|
var params = {
|
||||||
version: "stable",
|
version: "stable",
|
||||||
optimize: "0",
|
optimize: "0",
|
||||||
code: text,
|
code: text,
|
||||||
};
|
};
|
||||||
|
|
||||||
if(text.includes("#![feature")) {
|
if(text.indexOf("#![feature") !== -1) {
|
||||||
params.version = "nightly";
|
params.version = "nightly";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,15 +1,6 @@
|
||||||
/* Modified Base16 Atelier Dune Light - Theme
|
/* Base16 Atelier Dune Light - Theme */
|
||||||
/* Original by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */
|
/* by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) */
|
||||||
|
/* Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) */
|
||||||
.hljs {
|
|
||||||
display: block;
|
|
||||||
overflow-x: auto;
|
|
||||||
background: #f1f1f1;
|
|
||||||
color: #6e6b5e;
|
|
||||||
padding: 0.5em;
|
|
||||||
-webkit-text-size-adjust: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Atelier-Dune Comment */
|
/* Atelier-Dune Comment */
|
||||||
.hljs-comment,
|
.hljs-comment,
|
||||||
|
@ -61,6 +52,14 @@
|
||||||
color: #b854d4;
|
color: #b854d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: #f1f1f1;
|
||||||
|
color: #6e6b5e;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
.hljs-emphasis {
|
.hljs-emphasis {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -20,9 +20,23 @@
|
||||||
|
|
||||||
<link rel="stylesheet" href="highlight.css">
|
<link rel="stylesheet" href="highlight.css">
|
||||||
<link rel="stylesheet" href="tomorrow-night.css">
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme -->
|
||||||
|
{{#each additional_css}}
|
||||||
|
<link rel="stylesheet" href="{{this}}">
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
<!-- MathJax -->
|
<!-- MathJax -->
|
||||||
<script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||||
|
|
||||||
|
<!-- Fetch Clipboard.js from CDN but have a local fallback -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/clipboard.js/1.6.1/clipboard.min.js"></script>
|
||||||
|
<script>
|
||||||
|
if (typeof Clipboard == 'undefined') {
|
||||||
|
document.write(unescape("%3Cscript src='clipboard.min.js'%3E%3C/script%3E"));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Fetch JQuery from CDN but have a local fallback -->
|
<!-- Fetch JQuery from CDN but have a local fallback -->
|
||||||
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
|
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||||
|
@ -31,18 +45,27 @@
|
||||||
document.write(unescape("%3Cscript src='jquery.js'%3E%3C/script%3E"));
|
document.write(unescape("%3Cscript src='jquery.js'%3E%3C/script%3E"));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Fetch store.js from local - TODO add CDN when 2.x.x is available on cdnjs -->
|
||||||
|
<script src="store.js"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS script -->
|
||||||
|
{{#each additional_js}}
|
||||||
|
<script type="text/javascript" src="{{this}}"></script>
|
||||||
|
{{/each}}
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body class="light">
|
<body class="light">
|
||||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var theme = localStorage.getItem('theme');
|
var theme = store.get('theme');
|
||||||
if (theme == null) { theme = 'light'; }
|
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||||
$('body').removeClass().addClass(theme);
|
$('body').removeClass().addClass(theme);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Hide / unhide sidebar before it is displayed -->
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var sidebar = localStorage.getItem('sidebar');
|
var sidebar = store.get('sidebar');
|
||||||
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
|
if (sidebar === "hidden") { $("html").addClass("sidebar-hidden") }
|
||||||
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
|
else if (sidebar === "visible") { $("html").addClass("sidebar-visible") }
|
||||||
</script>
|
</script>
|
||||||
|
@ -53,7 +76,7 @@
|
||||||
|
|
||||||
<div id="page-wrapper" class="page-wrapper">
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
<div class="page">
|
<div class="page" tabindex="-1">
|
||||||
<div id="menu-bar" class="menu-bar">
|
<div id="menu-bar" class="menu-bar">
|
||||||
<div class="left-buttons">
|
<div class="left-buttons">
|
||||||
<i id="sidebar-toggle" class="fa fa-bars"></i>
|
<i id="sidebar-toggle" class="fa fa-bars"></i>
|
||||||
|
@ -73,13 +96,13 @@
|
||||||
|
|
||||||
<!-- Mobile navigation buttons -->
|
<!-- Mobile navigation buttons -->
|
||||||
{{#previous}}
|
{{#previous}}
|
||||||
<a href="{{link}}" class="mobile-nav-chapters previous">
|
<a rel="prev" href="{{link}}" class="mobile-nav-chapters previous">
|
||||||
<i class="fa fa-angle-left"></i>
|
<i class="fa fa-angle-left"></i>
|
||||||
</a>
|
</a>
|
||||||
{{/previous}}
|
{{/previous}}
|
||||||
|
|
||||||
{{#next}}
|
{{#next}}
|
||||||
<a href="{{link}}" class="mobile-nav-chapters next">
|
<a rel="next" href="{{link}}" class="mobile-nav-chapters next">
|
||||||
<i class="fa fa-angle-right"></i>
|
<i class="fa fa-angle-right"></i>
|
||||||
</a>
|
</a>
|
||||||
{{/next}}
|
{{/next}}
|
||||||
|
@ -111,6 +134,19 @@
|
||||||
<!-- Livereload script (if served using the cli tool) -->
|
<!-- Livereload script (if served using the cli tool) -->
|
||||||
{{{livereload}}}
|
{{{livereload}}}
|
||||||
|
|
||||||
|
{{#if google_analytics}}
|
||||||
|
<script>
|
||||||
|
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||||
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||||
|
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||||
|
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||||
|
|
||||||
|
ga('create', '{{google_analytics}}', 'auto');
|
||||||
|
ga('send', 'pageview');
|
||||||
|
</script>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
|
||||||
<script src="highlight.js"></script>
|
<script src="highlight.js"></script>
|
||||||
<script src="book.js"></script>
|
<script src="book.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use std::path::Path;
|
use std::path::PathBuf;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
|
||||||
|
@ -10,7 +10,10 @@ pub static JS: &'static [u8] = include_bytes!("book.js");
|
||||||
pub static HIGHLIGHT_JS: &'static [u8] = include_bytes!("highlight.js");
|
pub static HIGHLIGHT_JS: &'static [u8] = include_bytes!("highlight.js");
|
||||||
pub static TOMORROW_NIGHT_CSS: &'static [u8] = include_bytes!("tomorrow-night.css");
|
pub static TOMORROW_NIGHT_CSS: &'static [u8] = include_bytes!("tomorrow-night.css");
|
||||||
pub static HIGHLIGHT_CSS: &'static [u8] = include_bytes!("highlight.css");
|
pub static HIGHLIGHT_CSS: &'static [u8] = include_bytes!("highlight.css");
|
||||||
|
pub static AYU_HIGHLIGHT_CSS: &'static [u8] = include_bytes!("ayu-highlight.css");
|
||||||
pub static JQUERY: &'static [u8] = include_bytes!("jquery-2.1.4.min.js");
|
pub static JQUERY: &'static [u8] = include_bytes!("jquery-2.1.4.min.js");
|
||||||
|
pub static CLIPBOARD_JS: &'static [u8] = include_bytes!("clipboard.min.js");
|
||||||
|
pub static STORE_JS: &'static [u8] = include_bytes!("store.js");
|
||||||
pub static FONT_AWESOME: &'static [u8] = include_bytes!("_FontAwesome/css/font-awesome.min.css");
|
pub static FONT_AWESOME: &'static [u8] = include_bytes!("_FontAwesome/css/font-awesome.min.css");
|
||||||
pub static FONT_AWESOME_EOT: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.eot");
|
pub static FONT_AWESOME_EOT: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.eot");
|
||||||
pub static FONT_AWESOME_SVG: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.svg");
|
pub static FONT_AWESOME_SVG: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.svg");
|
||||||
|
@ -19,11 +22,14 @@ pub static FONT_AWESOME_WOFF: &'static [u8] = include_bytes!("_FontAwesome/fonts
|
||||||
pub static FONT_AWESOME_WOFF2: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff2");
|
pub static FONT_AWESOME_WOFF2: &'static [u8] = include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff2");
|
||||||
pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/FontAwesome.otf");
|
pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("_FontAwesome/fonts/FontAwesome.otf");
|
||||||
|
|
||||||
/// The `Theme` struct should be used instead of the static variables because the `new()` method
|
/// The `Theme` struct should be used instead of the static variables because
|
||||||
/// will look if the user has a theme directory in his source folder and use the users theme instead
|
/// the `new()` method
|
||||||
|
/// will look if the user has a theme directory in his source folder and use
|
||||||
|
/// the users theme instead
|
||||||
/// of the default.
|
/// of the default.
|
||||||
///
|
///
|
||||||
/// You should exceptionnaly use the static variables only if you need the default theme even if the
|
/// You should exceptionnaly use the static variables only if you need the
|
||||||
|
/// default theme even if the
|
||||||
/// user has specified another theme.
|
/// user has specified another theme.
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
pub index: Vec<u8>,
|
pub index: Vec<u8>,
|
||||||
|
@ -32,12 +38,15 @@ pub struct Theme {
|
||||||
pub js: Vec<u8>,
|
pub js: Vec<u8>,
|
||||||
pub highlight_css: Vec<u8>,
|
pub highlight_css: Vec<u8>,
|
||||||
pub tomorrow_night_css: Vec<u8>,
|
pub tomorrow_night_css: Vec<u8>,
|
||||||
|
pub ayu_highlight_css: Vec<u8>,
|
||||||
pub highlight_js: Vec<u8>,
|
pub highlight_js: Vec<u8>,
|
||||||
|
pub clipboard_js: Vec<u8>,
|
||||||
|
pub store_js: Vec<u8>,
|
||||||
pub jquery: Vec<u8>,
|
pub jquery: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Theme {
|
impl Theme {
|
||||||
pub fn new(src: &Path) -> Self {
|
pub fn new(src: Option<&PathBuf>) -> Self {
|
||||||
|
|
||||||
// Default theme
|
// Default theme
|
||||||
let mut theme = Theme {
|
let mut theme = Theme {
|
||||||
|
@ -47,15 +56,20 @@ impl Theme {
|
||||||
js: JS.to_owned(),
|
js: JS.to_owned(),
|
||||||
highlight_css: HIGHLIGHT_CSS.to_owned(),
|
highlight_css: HIGHLIGHT_CSS.to_owned(),
|
||||||
tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
|
tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
|
||||||
|
ayu_highlight_css: AYU_HIGHLIGHT_CSS.to_owned(),
|
||||||
highlight_js: HIGHLIGHT_JS.to_owned(),
|
highlight_js: HIGHLIGHT_JS.to_owned(),
|
||||||
|
clipboard_js: CLIPBOARD_JS.to_owned(),
|
||||||
|
store_js: STORE_JS.to_owned(),
|
||||||
jquery: JQUERY.to_owned(),
|
jquery: JQUERY.to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if the given path exists
|
// Check if the given path exists
|
||||||
if !src.exists() || !src.is_dir() {
|
if src.is_none() || !src.unwrap().exists() || !src.unwrap().is_dir() {
|
||||||
return theme;
|
return theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let src = src.unwrap();
|
||||||
|
|
||||||
// Check for individual files if they exist
|
// Check for individual files if they exist
|
||||||
|
|
||||||
// index.hbs
|
// index.hbs
|
||||||
|
@ -88,6 +102,18 @@ impl Theme {
|
||||||
let _ = f.read_to_end(&mut theme.highlight_js);
|
let _ = f.read_to_end(&mut theme.highlight_js);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clipboard.js
|
||||||
|
if let Ok(mut f) = File::open(&src.join("clipboard.min.js")) {
|
||||||
|
theme.clipboard_js.clear();
|
||||||
|
let _ = f.read_to_end(&mut theme.clipboard_js);
|
||||||
|
}
|
||||||
|
|
||||||
|
// store.js
|
||||||
|
if let Ok(mut f) = File::open(&src.join("store.js")) {
|
||||||
|
theme.store_js.clear();
|
||||||
|
let _ = f.read_to_end(&mut theme.store_js);
|
||||||
|
}
|
||||||
|
|
||||||
// highlight.css
|
// highlight.css
|
||||||
if let Ok(mut f) = File::open(&src.join("highlight.css")) {
|
if let Ok(mut f) = File::open(&src.join("highlight.css")) {
|
||||||
theme.highlight_css.clear();
|
theme.highlight_css.clear();
|
||||||
|
@ -100,6 +126,12 @@ impl Theme {
|
||||||
let _ = f.read_to_end(&mut theme.tomorrow_night_css);
|
let _ = f.read_to_end(&mut theme.tomorrow_night_css);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ayu-highlight.css
|
||||||
|
if let Ok(mut f) = File::open(&src.join("ayu-highlight.css")) {
|
||||||
|
theme.ayu_highlight_css.clear();
|
||||||
|
let _ = f.read_to_end(&mut theme.ayu_highlight_css);
|
||||||
|
}
|
||||||
|
|
||||||
theme
|
theme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -8,3 +8,4 @@
|
||||||
@import 'theme-popup'
|
@import 'theme-popup'
|
||||||
@import 'themes'
|
@import 'themes'
|
||||||
@import 'print'
|
@import 'print'
|
||||||
|
@import 'tooltip'
|
||||||
|
|
|
@ -4,7 +4,7 @@ html, body {
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: "Source Code Pro", "Menlo", "DejaVu Sans Mono", monospace;
|
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||||
font-size: 0.875em;
|
font-size: 0.875em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
right: 0
|
right: 0
|
||||||
left: 0
|
left: 0
|
||||||
bottom: 0
|
bottom: 0
|
||||||
|
outline: 0
|
||||||
|
|
||||||
padding-right: 15px
|
padding-right: 15px
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
$theme-name = 'ayu'
|
||||||
|
|
||||||
|
$bg = #0f1419
|
||||||
|
$fg = #c5c5c5
|
||||||
|
|
||||||
|
$sidebar-bg = #14191f
|
||||||
|
$sidebar-fg = #c8c9db
|
||||||
|
$sidebar-non-existant = #5c6773
|
||||||
|
$sidebar-active = #ffb454
|
||||||
|
$sidebar-spacer = #2d334f
|
||||||
|
|
||||||
|
$icons = #737480
|
||||||
|
$icons-hover = #b7b9cc
|
||||||
|
|
||||||
|
$links = #0096cf
|
||||||
|
|
||||||
|
$inline-code-color = #ffb454
|
||||||
|
|
||||||
|
$theme-popup-bg = #14191f
|
||||||
|
$theme-popup-border = #5c6773
|
||||||
|
$theme-hover = #191f26
|
||||||
|
|
||||||
|
$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'
|
|
@ -56,7 +56,7 @@
|
||||||
background-color: $sidebar-bg
|
background-color: $sidebar-bg
|
||||||
}
|
}
|
||||||
|
|
||||||
.content a:link, a:visited {
|
.content a:link, a:visited, a > .hljs {
|
||||||
color: $links
|
color: $links
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +105,11 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
padding: 0.1em 0.3em;
|
padding: 0.1em 0.3em;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
color: $inline-code-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover > .hljs {
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
|
@ -112,6 +117,7 @@
|
||||||
|
|
||||||
& > .buttons {
|
& > .buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ $icons-hover = #b3c0cc
|
||||||
|
|
||||||
$links = #2b79a2
|
$links = #2b79a2
|
||||||
|
|
||||||
|
$inline-code-color = #c5c8c6;
|
||||||
|
|
||||||
$theme-popup-bg = #141617
|
$theme-popup-bg = #141617
|
||||||
$theme-popup-border = #43484d
|
$theme-popup-border = #43484d
|
||||||
$theme-hover = #1f2124
|
$theme-hover = #1f2124
|
||||||
|
|
|
@ -2,3 +2,4 @@
|
||||||
@import 'coal'
|
@import 'coal'
|
||||||
@import 'navy'
|
@import 'navy'
|
||||||
@import 'rust'
|
@import 'rust'
|
||||||
|
@import 'ayu'
|
||||||
|
|
|
@ -14,6 +14,8 @@ $icons-hover = #333333
|
||||||
|
|
||||||
$links = #4183c4
|
$links = #4183c4
|
||||||
|
|
||||||
|
$inline-code-color = #6e6b5e;
|
||||||
|
|
||||||
$theme-popup-bg = #fafafa
|
$theme-popup-bg = #fafafa
|
||||||
$theme-popup-border = #cccccc
|
$theme-popup-border = #cccccc
|
||||||
$theme-hover = #e6e6e6
|
$theme-hover = #e6e6e6
|
||||||
|
|
|
@ -14,6 +14,8 @@ $icons-hover = #b7b9cc
|
||||||
|
|
||||||
$links = #2b79a2
|
$links = #2b79a2
|
||||||
|
|
||||||
|
$inline-code-color = #c5c8c6;
|
||||||
|
|
||||||
$theme-popup-bg = #161923
|
$theme-popup-bg = #161923
|
||||||
$theme-popup-border = #737480
|
$theme-popup-border = #737480
|
||||||
$theme-hover = #282e40
|
$theme-hover = #282e40
|
||||||
|
|
|
@ -14,6 +14,8 @@ $icons-hover = #262625
|
||||||
|
|
||||||
$links = #2b79a2
|
$links = #2b79a2
|
||||||
|
|
||||||
|
$inline-code-color = #6e6b5e;
|
||||||
|
|
||||||
$theme-popup-bg = #e1e1db
|
$theme-popup-bg = #e1e1db
|
||||||
$theme-popup-border = #b38f6b
|
$theme-popup-border = #b38f6b
|
||||||
$theme-hover = #99908a
|
$theme-hover = #99908a
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
.tooltiptext {
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #333;
|
||||||
|
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
|
||||||
|
left: -8px; /* Half of the width of the icon */
|
||||||
|
top: -35px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
margin: 5px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.tooltipped .tooltiptext {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
|
@ -24,10 +24,11 @@ pub fn file_to_string(path: &Path) -> Result<String, Box<Error>> {
|
||||||
Ok(content)
|
Ok(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Takes a path and returns a path containing just enough `../` to point to the root of the given path.
|
/// 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
|
/// This is mostly interesting for a relative path to point back to the
|
||||||
/// path starts.
|
/// directory from where the path starts.
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let mut path = Path::new("some/relative/path");
|
/// let mut path = Path::new("some/relative/path");
|
||||||
|
@ -41,9 +42,10 @@ pub fn file_to_string(path: &Path) -> Result<String, Box<Error>> {
|
||||||
/// "../../"
|
/// "../../"
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// **note:** it's not very fool-proof, if you find a situation where it doesn't return the correct
|
/// **note:** it's not very fool-proof, if you find a situation where
|
||||||
/// path. Consider [submitting a new issue](https://github.com/azerupi/mdBook/issues) or a
|
/// it doesn't return the correct path.
|
||||||
/// [pull-request](https://github.com/azerupi/mdBook/pulls) to improve it.
|
/// 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 {
|
pub fn path_to_root(path: &Path) -> String {
|
||||||
debug!("[fn]: path_to_root");
|
debug!("[fn]: path_to_root");
|
||||||
|
@ -66,8 +68,9 @@ pub fn path_to_root(path: &Path) -> String {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// This function creates a file and returns it. But before creating the file it checks every
|
/// This function creates a file and returns it. But before creating the file
|
||||||
/// directory in the path to see if it exists, and if it does not it will be created.
|
/// it checks every directory in the path to see if it exists,
|
||||||
|
/// and if it does not it will be created.
|
||||||
|
|
||||||
pub fn create_file(path: &Path) -> io::Result<File> {
|
pub fn create_file(path: &Path) -> io::Result<File> {
|
||||||
debug!("[fn]: create_file");
|
debug!("[fn]: create_file");
|
||||||
|
@ -76,7 +79,7 @@ pub fn create_file(path: &Path) -> io::Result<File> {
|
||||||
if let Some(p) = path.parent() {
|
if let Some(p) = path.parent() {
|
||||||
debug!("Parent directory is: {:?}", p);
|
debug!("Parent directory is: {:?}", p);
|
||||||
|
|
||||||
try!(fs::create_dir_all(p));
|
fs::create_dir_all(p)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("[*]: Create file: {:?}", path);
|
debug!("[*]: Create file: {:?}", path);
|
||||||
|
@ -86,13 +89,13 @@ pub fn create_file(path: &Path) -> io::Result<File> {
|
||||||
/// Removes all the content of a directory but not the directory itself
|
/// Removes all the content of a directory but not the directory itself
|
||||||
|
|
||||||
pub fn remove_dir_content(dir: &Path) -> Result<(), Box<Error>> {
|
pub fn remove_dir_content(dir: &Path) -> Result<(), Box<Error>> {
|
||||||
for item in try!(fs::read_dir(dir)) {
|
for item in fs::read_dir(dir)? {
|
||||||
if let Ok(item) = item {
|
if let Ok(item) = item {
|
||||||
let item = item.path();
|
let item = item.path();
|
||||||
if item.is_dir() {
|
if item.is_dir() {
|
||||||
try!(fs::remove_dir_all(item));
|
fs::remove_dir_all(item)?;
|
||||||
} else {
|
} else {
|
||||||
try!(fs::remove_file(item));
|
fs::remove_file(item)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -101,20 +104,21 @@ pub fn remove_dir_content(dir: &Path) -> Result<(), Box<Error>> {
|
||||||
|
|
||||||
///
|
///
|
||||||
///
|
///
|
||||||
/// Copies all files of a directory to another one except the files with the extensions given in the
|
/// Copies all files of a directory to another one except the files
|
||||||
/// `ext_blacklist` array
|
/// 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>> {
|
pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blacklist: &[&str])
|
||||||
|
-> Result<(), Box<Error>> {
|
||||||
debug!("[fn] copy_files_except_ext");
|
debug!("[fn] copy_files_except_ext");
|
||||||
// Check that from and to are different
|
// Check that from and to are different
|
||||||
if from == to {
|
if from == to {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
debug!("[*] Loop");
|
debug!("[*] Loop");
|
||||||
for entry in try!(fs::read_dir(from)) {
|
for entry in fs::read_dir(from)? {
|
||||||
let entry = try!(entry);
|
let entry = entry?;
|
||||||
debug!("[*] {:?}", entry.path());
|
debug!("[*] {:?}", entry.path());
|
||||||
let metadata = try!(entry.metadata());
|
let metadata = entry.metadata()?;
|
||||||
|
|
||||||
// If the entry is a dir and the recursive option is enabled, call itself
|
// If the entry is a dir and the recursive option is enabled, call itself
|
||||||
if metadata.is_dir() && recursive {
|
if metadata.is_dir() && recursive {
|
||||||
|
@ -125,13 +129,10 @@ pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blackl
|
||||||
|
|
||||||
// check if output dir already exists
|
// check if output dir already exists
|
||||||
if !to.join(entry.file_name()).exists() {
|
if !to.join(entry.file_name()).exists() {
|
||||||
try!(fs::create_dir(&to.join(entry.file_name())));
|
fs::create_dir(&to.join(entry.file_name()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
try!(copy_files_except_ext(&from.join(entry.file_name()),
|
copy_files_except_ext(&from.join(entry.file_name()), &to.join(entry.file_name()), true, ext_blacklist)?;
|
||||||
&to.join(entry.file_name()),
|
|
||||||
true,
|
|
||||||
ext_blacklist));
|
|
||||||
} else if metadata.is_file() {
|
} else if metadata.is_file() {
|
||||||
|
|
||||||
// Check if it is in the blacklist
|
// Check if it is in the blacklist
|
||||||
|
@ -141,13 +142,22 @@ pub fn copy_files_except_ext(from: &Path, to: &Path, recursive: bool, ext_blackl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
debug!("[*] creating path for file: {:?}",
|
debug!("[*] creating path for file: {:?}",
|
||||||
&to.join(entry.path().file_name().expect("a file should have a file name...")));
|
&to.join(entry
|
||||||
|
.path()
|
||||||
|
.file_name()
|
||||||
|
.expect("a file should have a file name...")));
|
||||||
|
|
||||||
info!("[*] Copying file: {:?}\n to {:?}",
|
info!("[*] Copying file: {:?}\n to {:?}",
|
||||||
entry.path(),
|
entry.path(),
|
||||||
&to.join(entry.path().file_name().expect("a file should have a file name...")));
|
&to.join(entry
|
||||||
try!(fs::copy(entry.path(),
|
.path()
|
||||||
&to.join(entry.path().file_name().expect("a file should have a file name..."))));
|
.file_name()
|
||||||
|
.expect("a file should have a file name...")));
|
||||||
|
fs::copy(entry.path(),
|
||||||
|
&to.join(entry
|
||||||
|
.path()
|
||||||
|
.file_name()
|
||||||
|
.expect("a file should have a file name...")))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
extern crate mdbook;
|
||||||
|
extern crate tempdir;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use mdbook::MDBook;
|
||||||
|
use tempdir::TempDir;
|
||||||
|
|
||||||
|
// Tests that config values unspecified in the configuration file do not overwrite
|
||||||
|
// values specified earlier.
|
||||||
|
#[test]
|
||||||
|
fn do_not_overwrite_unspecified_config_values() {
|
||||||
|
let dir = TempDir::new("mdbook").expect("Could not create a temp dir");
|
||||||
|
|
||||||
|
let book = MDBook::new(dir.path())
|
||||||
|
.with_source(Path::new("bar"))
|
||||||
|
.with_destination(Path::new("baz"));
|
||||||
|
|
||||||
|
assert_eq!(book.get_root(), dir.path());
|
||||||
|
assert_eq!(book.get_source(), dir.path().join("bar"));
|
||||||
|
assert_eq!(book.get_destination().unwrap(), dir.path().join("baz"));
|
||||||
|
|
||||||
|
// Test when trying to read a config file that does not exist
|
||||||
|
let book = book.read_config().expect("Error reading the config file");
|
||||||
|
|
||||||
|
assert_eq!(book.get_root(), dir.path());
|
||||||
|
assert_eq!(book.get_source(), dir.path().join("bar"));
|
||||||
|
assert_eq!(book.get_destination().unwrap(), dir.path().join("baz"));
|
||||||
|
|
||||||
|
// Try with a partial config file
|
||||||
|
let file_path = dir.path().join("book.toml");
|
||||||
|
let mut f = File::create(file_path).expect("Could not create config file");
|
||||||
|
f.write_all(br#"source = "barbaz""#).expect("Could not write to config file");
|
||||||
|
f.sync_all().expect("Could not sync the file");
|
||||||
|
|
||||||
|
let book = book.read_config().expect("Error reading the config file");
|
||||||
|
|
||||||
|
assert_eq!(book.get_root(), dir.path());
|
||||||
|
assert_eq!(book.get_source(), dir.path().join("barbaz"));
|
||||||
|
assert_eq!(book.get_destination().unwrap(), dir.path().join("baz"));
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
extern crate mdbook;
|
||||||
|
use mdbook::config::BookConfig;
|
||||||
|
use mdbook::config::jsonconfig::JsonConfig;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
// Tests that the `title` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_json_source() {
|
||||||
|
let json = r#"{
|
||||||
|
"src": "source"
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let parsed = JsonConfig::from_json(&json).expect("This should parse");
|
||||||
|
let config = BookConfig::from_jsonconfig("root", parsed);
|
||||||
|
|
||||||
|
assert_eq!(config.get_source(), PathBuf::from("root/source"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `title` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_json_title() {
|
||||||
|
let json = r#"{
|
||||||
|
"title": "Some title"
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let parsed = JsonConfig::from_json(&json).expect("This should parse");
|
||||||
|
let config = BookConfig::from_jsonconfig("root", parsed);
|
||||||
|
|
||||||
|
assert_eq!(config.get_title(), "Some title");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `description` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_json_description() {
|
||||||
|
let json = r#"{
|
||||||
|
"description": "This is a description"
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let parsed = JsonConfig::from_json(&json).expect("This should parse");
|
||||||
|
let config = BookConfig::from_jsonconfig("root", parsed);
|
||||||
|
|
||||||
|
assert_eq!(config.get_description(), "This is a description");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `author` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_json_author() {
|
||||||
|
let json = r#"{
|
||||||
|
"author": "John Doe"
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let parsed = JsonConfig::from_json(&json).expect("This should parse");
|
||||||
|
let config = BookConfig::from_jsonconfig("root", parsed);
|
||||||
|
|
||||||
|
assert_eq!(config.get_authors(), &[String::from("John Doe")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `output.html.destination` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_json_destination() {
|
||||||
|
let json = r#"{
|
||||||
|
"dest": "htmlbook"
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let parsed = JsonConfig::from_json(&json).expect("This should parse");
|
||||||
|
let config = BookConfig::from_jsonconfig("root", parsed);
|
||||||
|
|
||||||
|
let htmlconfig = config.get_html_config().expect("There should be an HtmlConfig");
|
||||||
|
|
||||||
|
assert_eq!(htmlconfig.get_destination(), PathBuf::from("root/htmlbook"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `output.html.theme` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_json_output_html_theme() {
|
||||||
|
let json = r#"{
|
||||||
|
"theme_path": "theme"
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
let parsed = JsonConfig::from_json(&json).expect("This should parse");
|
||||||
|
let config = BookConfig::from_jsonconfig("root", parsed);
|
||||||
|
|
||||||
|
let htmlconfig = config.get_html_config().expect("There should be an HtmlConfig");
|
||||||
|
|
||||||
|
assert_eq!(htmlconfig.get_theme().expect("the theme key was provided"), &PathBuf::from("root/theme"));
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
extern crate mdbook;
|
||||||
|
use mdbook::config::BookConfig;
|
||||||
|
use mdbook::config::tomlconfig::TomlConfig;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
// Tests that the `title` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_toml_source() {
|
||||||
|
let toml = r#"source = "source""#;
|
||||||
|
|
||||||
|
let parsed = TomlConfig::from_toml(&toml).expect("This should parse");
|
||||||
|
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||||
|
|
||||||
|
assert_eq!(config.get_source(), PathBuf::from("root/source"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `title` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_toml_title() {
|
||||||
|
let toml = r#"title = "Some title""#;
|
||||||
|
|
||||||
|
let parsed = TomlConfig::from_toml(&toml).expect("This should parse");
|
||||||
|
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||||
|
|
||||||
|
assert_eq!(config.get_title(), "Some title");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `description` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_toml_description() {
|
||||||
|
let toml = r#"description = "This is a description""#;
|
||||||
|
|
||||||
|
let parsed = TomlConfig::from_toml(&toml).expect("This should parse");
|
||||||
|
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||||
|
|
||||||
|
assert_eq!(config.get_description(), "This is a description");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `author` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_toml_author() {
|
||||||
|
let toml = r#"author = "John Doe""#;
|
||||||
|
|
||||||
|
let parsed = TomlConfig::from_toml(&toml).expect("This should parse");
|
||||||
|
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||||
|
|
||||||
|
assert_eq!(config.get_authors(), &[String::from("John Doe")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `authors` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_toml_authors() {
|
||||||
|
let toml = r#"authors = ["John Doe", "Jane Doe"]"#;
|
||||||
|
|
||||||
|
let parsed = TomlConfig::from_toml(&toml).expect("This should parse");
|
||||||
|
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||||
|
|
||||||
|
assert_eq!(config.get_authors(), &[String::from("John Doe"), String::from("Jane Doe")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `output.html.destination` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_toml_output_html_destination() {
|
||||||
|
let toml = r#"[output.html]
|
||||||
|
destination = "htmlbook""#;
|
||||||
|
|
||||||
|
let parsed = TomlConfig::from_toml(&toml).expect("This should parse");
|
||||||
|
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||||
|
|
||||||
|
let htmlconfig = config.get_html_config().expect("There should be an HtmlConfig");
|
||||||
|
|
||||||
|
assert_eq!(htmlconfig.get_destination(), PathBuf::from("root/htmlbook"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `output.html.theme` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_toml_output_html_theme() {
|
||||||
|
let toml = r#"[output.html]
|
||||||
|
theme = "theme""#;
|
||||||
|
|
||||||
|
let parsed = TomlConfig::from_toml(&toml).expect("This should parse");
|
||||||
|
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||||
|
|
||||||
|
let htmlconfig = config.get_html_config().expect("There should be an HtmlConfig");
|
||||||
|
|
||||||
|
assert_eq!(htmlconfig.get_theme().expect("the theme key was provided"), &PathBuf::from("root/theme"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `output.html.google-analytics` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_toml_output_html_google_analytics() {
|
||||||
|
let toml = r#"[output.html]
|
||||||
|
google-analytics = "123456""#;
|
||||||
|
|
||||||
|
let parsed = TomlConfig::from_toml(&toml).expect("This should parse");
|
||||||
|
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||||
|
|
||||||
|
let htmlconfig = config.get_html_config().expect("There should be an HtmlConfig");
|
||||||
|
|
||||||
|
assert_eq!(htmlconfig.get_google_analytics_id().expect("the google-analytics key was provided"), String::from("123456"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Tests that the `output.html.additional-css` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_toml_output_html_additional_stylesheet() {
|
||||||
|
let toml = r#"[output.html]
|
||||||
|
additional-css = ["custom.css", "two/custom.css"]"#;
|
||||||
|
|
||||||
|
let parsed = TomlConfig::from_toml(&toml).expect("This should parse");
|
||||||
|
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||||
|
|
||||||
|
let htmlconfig = config.get_html_config().expect("There should be an HtmlConfig");
|
||||||
|
|
||||||
|
assert_eq!(htmlconfig.get_additional_css(), &[PathBuf::from("root/custom.css"), PathBuf::from("root/two/custom.css")]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests that the `output.html.additional-js` key is correcly parsed in the TOML config
|
||||||
|
#[test]
|
||||||
|
fn from_toml_output_html_additional_scripts() {
|
||||||
|
let toml = r#"[output.html]
|
||||||
|
additional-js = ["custom.js", "two/custom.js"]"#;
|
||||||
|
|
||||||
|
let parsed = TomlConfig::from_toml(&toml).expect("This should parse");
|
||||||
|
let config = BookConfig::from_tomlconfig("root", parsed);
|
||||||
|
|
||||||
|
let htmlconfig = config.get_html_config().expect("There should be an HtmlConfig");
|
||||||
|
|
||||||
|
assert_eq!(htmlconfig.get_additional_js(), &[PathBuf::from("root/custom.js"), PathBuf::from("root/two/custom.js")]);
|
||||||
|
}
|
Loading…
Reference in New Issue