catch up with changes in master
This commit is contained in:
parent
ca718b3d78
commit
c2ff0c6b15
|
@ -28,11 +28,12 @@ glob = "0.2"
|
||||||
log = "0.3"
|
log = "0.3"
|
||||||
env_logger = "0.3"
|
env_logger = "0.3"
|
||||||
toml = { version = "0.2", features = ["serde"] }
|
toml = { version = "0.2", features = ["serde"] }
|
||||||
|
open = "1.1"
|
||||||
phf = "0.7"
|
phf = "0.7"
|
||||||
includedir = "0.2"
|
includedir = "0.2"
|
||||||
|
|
||||||
# Watch feature
|
# Watch feature
|
||||||
notify = { version = "2.5.5", optional = true }
|
notify = { version = "3.0", optional = true }
|
||||||
time = { version = "0.1.34", optional = true }
|
time = { version = "0.1.34", optional = true }
|
||||||
crossbeam = { version = "0.2.8", optional = true }
|
crossbeam = { version = "0.2.8", optional = true }
|
||||||
|
|
||||||
|
|
14
README.md
14
README.md
|
@ -21,7 +21,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
mdBook is a utility to create modern online books from markdown files.
|
mdBook is a utility to create modern online books from Markdown files.
|
||||||
|
|
||||||
**This project is still evolving.**
|
**This project is still evolving.**
|
||||||
See [#90](https://github.com/azerupi/mdBook/issues/90)
|
See [#90](https://github.com/azerupi/mdBook/issues/90)
|
||||||
|
@ -29,7 +29,7 @@ See [#90](https://github.com/azerupi/mdBook/issues/90)
|
||||||
|
|
||||||
## What does it look like?
|
## What does it look like?
|
||||||
|
|
||||||
The [**Documentation**](http://azerupi.github.io/mdBook/) for mdBook has been written in markdown and is using mdBook to generate the online book-like website you can read. The documentation uses the latest version on github and showcases the available features.
|
The [**Documentation**](http://azerupi.github.io/mdBook/) for mdBook has been written in Markdown and is using mdBook to generate the online book-like website you can read. The documentation uses the latest version on GitHub and showcases the available features.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -47,12 +47,12 @@ There are multiple ways to install mdBook.
|
||||||
This will download and compile mdBook for you, the only thing left to do is to add the Cargo bin directory to your `PATH`.
|
This will download and compile mdBook for you, the only thing left to do is to add the Cargo bin directory to your `PATH`.
|
||||||
|
|
||||||
3. **From Git**
|
3. **From Git**
|
||||||
The version published to Crates.io will ever so slightly be behind the version hosted here on Github. If you need the latest version you can build the git version of mdBook yourself. Cargo makes this ***super easy***!
|
The version published to crates.io will ever so slightly be behind the version hosted here on GitHub. If you need the latest version you can build the git version of mdBook yourself. Cargo makes this ***super easy***!
|
||||||
|
|
||||||
```
|
```
|
||||||
cargo install --git https://github.com/azerupi/mdBook.git
|
cargo install --git https://github.com/azerupi/mdBook.git
|
||||||
```
|
```
|
||||||
Again, make sure to add the Cargo bin directory to your `PATH`
|
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||||
|
|
||||||
4. **For Contributions**
|
4. **For Contributions**
|
||||||
If you want to contribute to mdBook you will have to clone the repository on your local machine:
|
If you want to contribute to mdBook you will have to clone the repository on your local machine:
|
||||||
|
@ -74,7 +74,7 @@ There are multiple ways to install mdBook.
|
||||||
|
|
||||||
mdBook will primaraly be used as a command line tool, even though it exposes all its functionality as a Rust crate for integration in other projects.
|
mdBook will primaraly be used as a command line tool, even though it exposes all its functionality as a Rust crate for integration in other projects.
|
||||||
|
|
||||||
Here are the main commands you will want to run, for a more exhaustive explanation, check out the [documentation](http://azerupi.github.io/mdBook/).
|
Here are the main commands you will want to run. For a more exhaustive explanation, check out the [documentation](http://azerupi.github.io/mdBook/).
|
||||||
|
|
||||||
- `mdbook init`
|
- `mdbook init`
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ Here are the main commands you will want to run, for a more exhaustive explanati
|
||||||
|
|
||||||
### As a library
|
### As a library
|
||||||
|
|
||||||
Aside from the command line interface, this crate can also be used as a library. This means that you could integrate it in an existing project, like a web-app for example. Since the command line interface is just a wrapper around the library functionality, when you use this crate as a library you have full access to all the functionality of the command line interface with and easy to use API and more!
|
Aside from the command line interface, this crate can also be used as a library. This means that you could integrate it in an existing project, like a web-app for example. Since the command line interface is just a wrapper around the library functionality, when you use this crate as a library you have full access to all the functionality of the command line interface with an easy to use API and more!
|
||||||
|
|
||||||
See the [Documentation](http://azerupi.github.io/mdBook/lib/lib.html) and the [API docs](http://azerupi.github.io/mdBook/mdbook/index.html) for more information.
|
See the [Documentation](http://azerupi.github.io/mdBook/lib/lib.html) and the [API docs](http://azerupi.github.io/mdBook/mdbook/index.html) for more information.
|
||||||
|
|
||||||
|
@ -123,4 +123,4 @@ You can pick any issue you want to work on. Usually it's a good idea to ask if s
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
All the code is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE](LICENSE) file
|
All the code is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE](LICENSE) file.
|
||||||
|
|
|
@ -21,6 +21,15 @@ current working directory.
|
||||||
mdbook build path/to/book
|
mdbook build path/to/book
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### --open
|
||||||
|
|
||||||
|
When you use the `--open` (`-o`) option, mdbook will open the rendered book in
|
||||||
|
your default web browser after building it.
|
||||||
|
|
||||||
|
#### --dest-dir
|
||||||
|
|
||||||
|
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
|
||||||
|
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
***note:*** *make sure to run the build command in the root directory and not in the source directory*
|
***note:*** *make sure to run the build command in the root directory and not in the source directory*
|
||||||
|
|
|
@ -22,7 +22,7 @@ configuration files, etc.
|
||||||
- The `book` directory is where your book is rendered. All the output is ready to be uploaded
|
- The `book` directory is where your book is rendered. All the output is ready to be uploaded
|
||||||
to a server to be seen by your audience.
|
to a server to be seen by your audience.
|
||||||
|
|
||||||
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is discussed in more detail in another [chapter](../format/summary.html).
|
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is discussed in more detail in another [chapter](format/summary.html).
|
||||||
|
|
||||||
#### Tip & Trick: Hidden Feature
|
#### Tip & Trick: Hidden Feature
|
||||||
When a `SUMMARY.md` file already exists, the `init` command will first parse it and generate the missing files according to the paths used in the `SUMMARY.md`. This allows you to think and create the whole structure of your book and then let mdBook generate it for you.
|
When a `SUMMARY.md` file already exists, the `init` command will first parse it and generate the missing files according to the paths used in the `SUMMARY.md`. This allows you to think and create the whole structure of your book and then let mdBook generate it for you.
|
||||||
|
|
|
@ -26,8 +26,15 @@ mdbook server path/to/book -p 8000 -i 127.0.0.1 -a 192.168.1.100
|
||||||
|
|
||||||
If you were to want live reloading for this you would need to proxy the websocket calls through nginx as well from `192.168.1.100:<WS_PORT>` to `127.0.0.1:<WS_PORT>`. The `-w` flag allows for the websocket port to be configured.
|
If you were to want live reloading for this you would need to proxy the websocket calls through nginx as well from `192.168.1.100:<WS_PORT>` to `127.0.0.1:<WS_PORT>`. The `-w` flag allows for the websocket port to be configured.
|
||||||
|
|
||||||
|
#### --open
|
||||||
|
|
||||||
|
When you use the `--open` (`-o`) option, mdbook will open the book in your
|
||||||
|
your default web browser after starting the server.
|
||||||
|
|
||||||
|
#### --dest-dir
|
||||||
|
|
||||||
|
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
***note:*** *the `serve` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/azerupi/mdBook/issues)*
|
***note:*** *the `serve` command has not gotten a lot of testing yet, there could be some rough edges. If you discover a problem, please report it [on Github](https://github.com/azerupi/mdBook/issues)*
|
||||||
|
|
||||||
***note***:
|
|
||||||
|
|
|
@ -12,6 +12,14 @@ current working directory.
|
||||||
mdbook watch path/to/book
|
mdbook watch path/to/book
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### --open
|
||||||
|
|
||||||
|
When you use the `--open` (`-o`) option, mdbook will open the rendered book in
|
||||||
|
your default web browser.
|
||||||
|
|
||||||
|
#### --dest-dir
|
||||||
|
|
||||||
|
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
|
||||||
|
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,9 @@ Here is a list of the properties that are exposed:
|
||||||
- ***language*** Language of the book in the form `en`. To use in <code class="language-html">\<html lang="{{ language }}"></code> for example.
|
- ***language*** Language of the book in the form `en`. To use in <code class="language-html">\<html lang="{{ language }}"></code> for example.
|
||||||
At the moment it is hardcoded.
|
At the moment it is hardcoded.
|
||||||
- ***title*** Title of the book, as specified in `book.toml`
|
- ***title*** Title of the book, as specified in `book.toml`
|
||||||
|
|
||||||
- ***path*** Relative path to the original markdown file from the source directory
|
- ***path*** Relative path to the original markdown file from the source directory
|
||||||
|
|
||||||
|
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
||||||
- ***content*** This is the rendered markdown.
|
- ***content*** This is the rendered markdown.
|
||||||
- ***path_to_root*** This is a path containing exclusively `../`'s that points to the root of the book from the current file.
|
- ***path_to_root*** This is a path containing exclusively `../`'s that points to the root of the book from the current file.
|
||||||
Since the original directory structure is maintained, it is useful to prepend relative links with this `path_to_root`.
|
Since the original directory structure is maintained, it is useful to prepend relative links with this `path_to_root`.
|
||||||
|
|
Before Width: | Height: | Size: 348 KiB After Width: | Height: | Size: 348 KiB |
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
@ -0,0 +1,2 @@
|
||||||
|
title = "An Empty Canvas"
|
||||||
|
author = "The Author"
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Title
|
||||||
|
|
||||||
|
[Introduction](introduction.md)
|
||||||
|
|
||||||
|
- [First Chapter](first-chapter.md)
|
||||||
|
- [Second Chapter]()
|
||||||
|
|
||||||
|
[Glossary](glossary.md)
|
|
@ -0,0 +1,8 @@
|
||||||
|
# First Chapter
|
||||||
|
|
||||||
|
> first cicada –
|
||||||
|
> for a quick song sighted
|
||||||
|
> on the post
|
||||||
|
>
|
||||||
|
> *Issa, 1816*
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Glossary
|
||||||
|
|
||||||
|
**ci·ca·da** (sĭ-kā′də, -kä′-) *n. pl.* ci·ca·das or ci·ca·dae (-dē′)
|
||||||
|
|
||||||
|
Any of various insects chiefly of the family Cicadidae, having a broad head,
|
||||||
|
membranous wings, and in the male a pair of resonating organs that produce a
|
||||||
|
characteristic high-pitched, droning sound.
|
|
@ -0,0 +1 @@
|
||||||
|
# Introduction
|
|
@ -5,6 +5,7 @@ extern crate clap;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate log;
|
extern crate log;
|
||||||
extern crate env_logger;
|
extern crate env_logger;
|
||||||
|
extern crate open;
|
||||||
|
|
||||||
// Dependencies for the Watch feature
|
// Dependencies for the Watch feature
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
|
@ -23,7 +24,9 @@ extern crate staticfile;
|
||||||
extern crate ws;
|
extern crate ws;
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
use std::ffi::OsStr;
|
||||||
use std::io::{self, Write, ErrorKind};
|
use std::io::{self, Write, ErrorKind};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
@ -34,6 +37,8 @@ use clap::{App, ArgMatches, SubCommand, AppSettings};
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
use notify::Watcher;
|
use notify::Watcher;
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
|
use std::time::Duration;
|
||||||
|
#[cfg(feature = "watch")]
|
||||||
use std::sync::mpsc::channel;
|
use std::sync::mpsc::channel;
|
||||||
|
|
||||||
use mdbook::MDBook;
|
use mdbook::MDBook;
|
||||||
|
@ -57,22 +62,28 @@ fn main() {
|
||||||
.subcommand(SubCommand::with_name("init")
|
.subcommand(SubCommand::with_name("init")
|
||||||
.about("Create boilerplate structure and files in the directory")
|
.about("Create boilerplate structure and files in the directory")
|
||||||
// the {n} denotes a newline which will properly aligned in all help messages
|
// the {n} denotes a newline which will properly aligned in all help messages
|
||||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'")
|
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")
|
||||||
.arg_from_usage("--copy-assets 'Copies the default assets (css, layout template, etc.) into your project folder'")
|
.arg_from_usage("--copy-assets 'Copies the default assets (css, layout template, etc.) into your project folder'")
|
||||||
.arg_from_usage("--force 'skip confirmation prompts'"))
|
.arg_from_usage("--force 'skip confirmation prompts'"))
|
||||||
.subcommand(SubCommand::with_name("build")
|
.subcommand(SubCommand::with_name("build")
|
||||||
.about("Build the book from the markdown files")
|
.about("Build the book from the markdown files")
|
||||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
|
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||||
|
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
|
||||||
|
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'"))
|
||||||
.subcommand(SubCommand::with_name("watch")
|
.subcommand(SubCommand::with_name("watch")
|
||||||
.about("Watch the files for changes")
|
.about("Watch the files for changes")
|
||||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'"))
|
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||||
|
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
|
||||||
|
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'"))
|
||||||
.subcommand(SubCommand::with_name("serve")
|
.subcommand(SubCommand::with_name("serve")
|
||||||
.about("Serve the book at http://localhost:3000. Rebuild and reload on change.")
|
.about("Serve the book at http://localhost:3000. Rebuild and reload on change.")
|
||||||
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when ommitted)'")
|
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'")
|
||||||
|
.arg_from_usage("-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book when omitted)'")
|
||||||
.arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
|
.arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
|
||||||
.arg_from_usage("-w, --websocket-port=[ws-port] 'Use another port for the websocket connection (livereload){n}(Defaults to 3001)'")
|
.arg_from_usage("-w, --websocket-port=[ws-port] 'Use another port for the websocket connection (livereload){n}(Defaults to 3001)'")
|
||||||
.arg_from_usage("-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'")
|
.arg_from_usage("-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'")
|
||||||
.arg_from_usage("-a, --address=[address] 'Address that the browser can reach the websocket server from{n}(Defaults to the interface addres)'"))
|
.arg_from_usage("-a, --address=[address] 'Address that the browser can reach the websocket server from{n}(Defaults to the interface addres)'")
|
||||||
|
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'"))
|
||||||
.subcommand(SubCommand::with_name("test")
|
.subcommand(SubCommand::with_name("test")
|
||||||
.about("Test that code samples compile"))
|
.about("Test that code samples compile"))
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
@ -106,10 +117,36 @@ fn confirm() -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init command implementation
|
/// Init command implementation
|
||||||
|
///
|
||||||
|
/// It creates some boilerplate files and directories to get you started with your book.
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// thebook/
|
||||||
|
/// ├── book.toml
|
||||||
|
/// └── src
|
||||||
|
/// ├── SUMMARY.md
|
||||||
|
/// ├── first-chapter.md
|
||||||
|
/// ├── glossary.md
|
||||||
|
/// └── introduction.md
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// It copies the embedded starter book as stored in data/book-init.
|
||||||
fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
|
debug!("[fn]: init");
|
||||||
|
|
||||||
let book_dir = get_book_dir(args);
|
let book_dir = get_book_dir(args);
|
||||||
|
|
||||||
|
if !book_dir.exists() {
|
||||||
|
fs::create_dir_all(&book_dir).unwrap();
|
||||||
|
info!("{:?} created", &book_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
try!(utils::fs::copy_data("data/book-init/*",
|
||||||
|
"data/book-init/",
|
||||||
|
vec![],
|
||||||
|
&book_dir));
|
||||||
|
|
||||||
let mut book_project = MDBook::new(&book_dir);
|
let mut book_project = MDBook::new(&book_dir);
|
||||||
|
|
||||||
book_project.read_config();
|
book_project.read_config();
|
||||||
|
@ -134,8 +171,8 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the assets
|
// Copy the assets
|
||||||
try!(utils::fs::copy_data("data/**/*",
|
try!(utils::fs::copy_data("data/assets/**/*",
|
||||||
"data/",
|
"data/assets/",
|
||||||
vec![],
|
vec![],
|
||||||
&book_project.get_project_root().join("assets")));
|
&book_project.get_project_root().join("assets")));
|
||||||
|
|
||||||
|
@ -154,6 +191,7 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
|
|
||||||
println!("\nAll done, no errors...");
|
println!("\nAll done, no errors...");
|
||||||
|
|
||||||
|
debug!("[*]: init done");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,9 +199,18 @@ fn init(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
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 mut dest_base: Option<PathBuf> = None;
|
||||||
|
if let Some(p) = args.value_of("dest-dir") {
|
||||||
|
dest_base = Some(PathBuf::from(p));
|
||||||
|
}
|
||||||
|
|
||||||
// TODO select render format intent when we acutally have different renderers
|
// TODO select render format intent when we acutally have different renderers
|
||||||
let renderer = HtmlHandlebars::new();
|
let renderer = HtmlHandlebars::new();
|
||||||
try!(renderer.build(&book_dir));
|
let book_project: MDBook = try!(renderer.build(&book_dir, &dest_base));
|
||||||
|
|
||||||
|
if args.is_present("open") {
|
||||||
|
open(book_project.get_dest_base().join("index.html"));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -172,22 +219,28 @@ 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 mut book = MDBook::new(&book_dir);
|
|
||||||
book.read_config();
|
|
||||||
|
|
||||||
// |event, book|
|
let mut dest_base: Option<PathBuf> = None;
|
||||||
trigger_on_change(&mut book, |event, _| {
|
if let Some(p) = args.value_of("dest-dir") {
|
||||||
if let Some(path) = event.path {
|
dest_base = Some(PathBuf::from(p));
|
||||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
}
|
||||||
|
|
||||||
// TODO select render format intent when we acutally have different renderers
|
let renderer = HtmlHandlebars::new();
|
||||||
let renderer = HtmlHandlebars::new();
|
let mut book_project: MDBook = try!(renderer.build(&book_dir, &dest_base));
|
||||||
match renderer.build(&book_dir) {
|
|
||||||
Err(e) => println!("Error while building: {:?}", e),
|
if args.is_present("open") {
|
||||||
_ => {},
|
open(book_project.get_dest_base().join("index.html"));
|
||||||
}
|
}
|
||||||
println!("");
|
|
||||||
|
trigger_on_change(&mut book_project, |path, _| {
|
||||||
|
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||||
|
// TODO select render format intent when we acutally have different renderers
|
||||||
|
let renderer = HtmlHandlebars::new();
|
||||||
|
match renderer.build(&book_dir, &dest_base) {
|
||||||
|
Err(e) => println!("Error while building: {:?}", e),
|
||||||
|
_ => {},
|
||||||
}
|
}
|
||||||
|
println!("");
|
||||||
});
|
});
|
||||||
|
|
||||||
println!("watch");
|
println!("watch");
|
||||||
|
@ -200,8 +253,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 mut dest_base: Option<PathBuf> = None;
|
||||||
|
if let Some(p) = args.value_of("dest-dir") {
|
||||||
|
dest_base = Some(PathBuf::from(p));
|
||||||
|
}
|
||||||
|
|
||||||
let mut book = MDBook::new(&book_dir);
|
let mut book = MDBook::new(&book_dir);
|
||||||
|
|
||||||
book.read_config();
|
book.read_config();
|
||||||
|
|
||||||
|
if let Some(p) = dest_base {
|
||||||
|
book.set_dest_base(&p);
|
||||||
|
}
|
||||||
|
|
||||||
book.parse_books();
|
book.parse_books();
|
||||||
book.link_translations();
|
book.link_translations();
|
||||||
|
|
||||||
|
@ -209,6 +274,7 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
let ws_port = args.value_of("ws-port").unwrap_or("3001");
|
let ws_port = args.value_of("ws-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 address = format!("{}:{}", interface, port);
|
let address = format!("{}:{}", interface, port);
|
||||||
let ws_address = format!("{}:{}", interface, ws_port);
|
let ws_address = format!("{}:{}", interface, ws_port);
|
||||||
|
@ -250,16 +316,18 @@ fn serve(args: &ArgMatches) -> Result<(), Box<Error>> {
|
||||||
|
|
||||||
println!("\nServing on {}", address);
|
println!("\nServing on {}", address);
|
||||||
|
|
||||||
trigger_on_change(&mut book, move |event, book| {
|
if open_browser {
|
||||||
if let Some(path) = event.path {
|
open(format!("http://{}", address));
|
||||||
println!("File changed: {:?}\nBuilding book...\n", path);
|
}
|
||||||
let renderer = HtmlHandlebars::new();
|
|
||||||
match renderer.render(&book) {
|
trigger_on_change(&mut book, move |path, book| {
|
||||||
Err(e) => println!("Error while building: {:?}", e),
|
println!("File changed: {:?}\nBuilding book...\n", path);
|
||||||
_ => broadcaster.send(RELOAD_COMMAND).unwrap(),
|
let renderer = HtmlHandlebars::new();
|
||||||
}
|
match renderer.render(&book) {
|
||||||
println!("");
|
Err(e) => println!("Error while building: {:?}", e),
|
||||||
|
_ => broadcaster.send(RELOAD_COMMAND).unwrap(),
|
||||||
}
|
}
|
||||||
|
println!("");
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -319,61 +387,64 @@ fn get_book_dir(args: &ArgMatches) -> PathBuf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open<P: AsRef<OsStr>>(path: P) {
|
||||||
|
if let Err(e) = open::that(path) {
|
||||||
|
println!("Error opening web browser: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calls the closure when a book source file is changed. This is blocking!
|
// Calls the closure when a book source file is changed. This is blocking!
|
||||||
#[cfg(feature = "watch")]
|
#[cfg(feature = "watch")]
|
||||||
fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
fn trigger_on_change<F>(book: &mut MDBook, closure: F) -> ()
|
||||||
where F: Fn(notify::Event, &mut MDBook) -> ()
|
where F: Fn(&Path, &mut MDBook) -> ()
|
||||||
{
|
{
|
||||||
|
use notify::RecursiveMode::*;
|
||||||
|
use notify::DebouncedEvent::*;
|
||||||
|
|
||||||
// Create a channel to receive the events.
|
// Create a channel to receive the events.
|
||||||
let (tx, rx) = channel();
|
let (tx, rx) = channel();
|
||||||
|
|
||||||
let w: Result<notify::RecommendedWatcher, notify::Error> = notify::Watcher::new(tx);
|
let mut watcher = match notify::watcher(tx, Duration::from_secs(1)) {
|
||||||
|
Ok(w) => w,
|
||||||
match w {
|
|
||||||
Ok(mut watcher) => {
|
|
||||||
// Add the source directory to the watcher
|
|
||||||
if let Err(e) = watcher.watch(book.get_src_base()) {
|
|
||||||
println!("Error while watching {:?}:\n {:?}", book.get_src_base(), e);
|
|
||||||
::std::process::exit(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add the book.toml or book.json file to the watcher if it exists,
|
|
||||||
// because it's not located in the source directory
|
|
||||||
|
|
||||||
if let Err(_) = watcher.watch(book.get_project_root().join("book.toml")) {
|
|
||||||
// do nothing if book.toml is not found
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(_) = watcher.watch(book.get_project_root().join("book.json")) {
|
|
||||||
// do nothing if book.json is not found
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut previous_time = time::get_time();
|
|
||||||
|
|
||||||
println!("\nListening for changes...\n");
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match rx.recv() {
|
|
||||||
Ok(event) => {
|
|
||||||
// Skip the event if an event has already been issued in the last second
|
|
||||||
let time = time::get_time();
|
|
||||||
if time - previous_time < time::Duration::seconds(1) {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
previous_time = time;
|
|
||||||
}
|
|
||||||
|
|
||||||
closure(event, book);
|
|
||||||
},
|
|
||||||
Err(e) => {
|
|
||||||
println!("An error occured: {:?}", e);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(e) => {
|
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(2);
|
||||||
},
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the source directory to the watcher
|
||||||
|
if let Err(e) = watcher.watch(book.get_src_base(), Recursive) {
|
||||||
|
println!("Error while watching {:?}:\n {:?}", book.get_src_base(), e);
|
||||||
|
::std::process::exit(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the book.{json,toml} file to the watcher if it exists, because it's not
|
||||||
|
// located in the source directory
|
||||||
|
if let Err(_) = watcher.watch(book.get_project_root().join("book.json"), NonRecursive) {
|
||||||
|
// do nothing if book.json is not found
|
||||||
|
}
|
||||||
|
if let Err(_) = watcher.watch(book.get_project_root().join("book.toml"), NonRecursive) {
|
||||||
|
// do nothing if book.toml is not found
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\nListening for changes...\n");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match rx.recv() {
|
||||||
|
Ok(event) => match event {
|
||||||
|
NoticeWrite(path) |
|
||||||
|
NoticeRemove(path) |
|
||||||
|
Create(path) |
|
||||||
|
Write(path) |
|
||||||
|
Remove(path) |
|
||||||
|
Rename(_, path) => {
|
||||||
|
closure(&path, book);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
println!("An error occured: {:?}", e);
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
use std::fs::File;
|
use std::path::PathBuf;
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
use book::bookconfig::BookConfig;
|
use book::bookconfig::BookConfig;
|
||||||
use book::toc::{TocItem, TocContent};
|
use book::toc::{TocItem, TocContent};
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
extern crate toml;
|
extern crate toml;
|
||||||
|
|
||||||
use std::process::exit;
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::str::FromStr;
|
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
use utils;
|
use utils;
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,9 @@ extern crate toml;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::error::Error;
|
use std::io::Read;
|
||||||
use std::io::{self, Read};
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use utils;
|
use utils;
|
||||||
|
@ -130,7 +129,7 @@ impl Chapter {
|
||||||
debug!("[*] Creating: {:?}", src_path);
|
debug!("[*] Creating: {:?}", src_path);
|
||||||
match create_with_str(src_path, &format!("# {}", self.title)) {
|
match create_with_str(src_path, &format!("# {}", self.title)) {
|
||||||
Ok(_) => { return Ok(self); },
|
Ok(_) => { return Ok(self); },
|
||||||
Err(e) => {
|
Err(_) => {
|
||||||
return Err(format!("Could not create: {:?}", src_path));
|
return Err(format!("Could not create: {:?}", src_path));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -140,8 +139,12 @@ impl Chapter {
|
||||||
match File::open(src_path) {
|
match File::open(src_path) {
|
||||||
Err(e) => { return Err(format!("Read error: {:?}", e)); },
|
Err(e) => { return Err(format!("Read error: {:?}", e)); },
|
||||||
Ok(mut f) => {
|
Ok(mut f) => {
|
||||||
// TODO try! here and return error
|
match f.read_to_string(&mut text) {
|
||||||
f.read_to_string(&mut text);
|
Ok(_) => {},
|
||||||
|
Err(e) => {
|
||||||
|
return Err(format!("Error: {:#?}", e));
|
||||||
|
},
|
||||||
|
}
|
||||||
self.content = Some(utils::strip_toml_header(&text));
|
self.content = Some(utils::strip_toml_header(&text));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,8 +9,6 @@ pub mod toc;
|
||||||
pub mod chapter;
|
pub mod chapter;
|
||||||
|
|
||||||
pub use self::book::Book;
|
pub use self::book::Book;
|
||||||
use renderer::{Renderer, HtmlHandlebars};
|
|
||||||
|
|
||||||
use self::chapter::TranslationLink;
|
use self::chapter::TranslationLink;
|
||||||
use self::toc::{TocItem, TocContent};
|
use self::toc::{TocItem, TocContent};
|
||||||
use utils;
|
use utils;
|
||||||
|
@ -19,9 +17,6 @@ use std::env;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::fs::{self, File};
|
|
||||||
use std::io::Read;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::collections::{HashMap, BTreeMap};
|
use std::collections::{HashMap, BTreeMap};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -142,34 +137,6 @@ impl MDBook {
|
||||||
MDBook::default().set_project_root(project_root).clone()
|
MDBook::default().set_project_root(project_root).clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `init()` creates some boilerplate files and directories to get you started with your book.
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// book-example/
|
|
||||||
/// ├── book
|
|
||||||
/// └── src
|
|
||||||
/// ├── chapter_1.md
|
|
||||||
/// └── SUMMARY.md
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// It uses the paths given as source and output directories and adds a `SUMMARY.md` and a
|
|
||||||
/// `chapter_1.md` to the source directory.
|
|
||||||
pub fn init(&mut self) -> Result<(), Box<Error>> {
|
|
||||||
|
|
||||||
debug!("[fn]: init");
|
|
||||||
|
|
||||||
if !self.project_root.exists() {
|
|
||||||
fs::create_dir_all(&self.project_root).unwrap();
|
|
||||||
info!("{:?} created", &self.project_root);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read book.toml if exists and populate .translations
|
|
||||||
self.read_config();
|
|
||||||
|
|
||||||
debug!("[*]: init done");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses the `book.toml` file (if it exists) to extract the configuration parameters.
|
/// Parses the `book.toml` file (if it exists) to extract the configuration parameters.
|
||||||
/// The `book.toml` file should be in the root directory of the book project.
|
/// The `book.toml` file should be in the root directory of the book project.
|
||||||
/// The project root directory is the one specified when creating a new `MDBook`
|
/// The project root directory is the one specified when creating a new `MDBook`
|
||||||
|
@ -186,7 +153,6 @@ impl MDBook {
|
||||||
/// In this example, `project_root_dir` will be the root directory of our book and is specified in function
|
/// In this example, `project_root_dir` will be the root directory of our book and is specified in function
|
||||||
/// of the current working directory by using a relative path instead of an absolute path.
|
/// of the current working directory by using a relative path instead of an absolute path.
|
||||||
pub fn read_config(&mut self) -> &mut Self {
|
pub fn read_config(&mut self) -> &mut Self {
|
||||||
|
|
||||||
debug!("[fn]: read_config");
|
debug!("[fn]: read_config");
|
||||||
|
|
||||||
// exit(2) is a clear indication for the user that something is wrong
|
// exit(2) is a clear indication for the user that something is wrong
|
||||||
|
@ -194,7 +160,7 @@ impl MDBook {
|
||||||
|
|
||||||
// Read book.toml or book.json if exists to a BTreeMap
|
// Read book.toml or book.json if exists to a BTreeMap
|
||||||
|
|
||||||
if Path::new(self.project_root.join("book.toml").as_os_str()).exists() {
|
if self.project_root.join("book.toml").exists() {
|
||||||
|
|
||||||
debug!("[*]: Reading config");
|
debug!("[*]: Reading config");
|
||||||
let text = match utils::fs::file_to_string(&self.project_root.join("book.toml")) {
|
let text = match utils::fs::file_to_string(&self.project_root.join("book.toml")) {
|
||||||
|
@ -213,7 +179,7 @@ impl MDBook {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if Path::new(self.project_root.join("book.json").as_os_str()).exists() {
|
} else if self.project_root.join("book.json").exists() {
|
||||||
|
|
||||||
debug!("[*]: Reading config");
|
debug!("[*]: Reading config");
|
||||||
let text = match utils::fs::file_to_string(&self.project_root.join("book.json")) {
|
let text = match utils::fs::file_to_string(&self.project_root.join("book.json")) {
|
||||||
|
@ -444,6 +410,8 @@ impl MDBook {
|
||||||
|
|
||||||
/// prepare a Vec of default links to point to the index.html of each translation
|
/// prepare a Vec of default links to point to the index.html of each translation
|
||||||
pub fn translation_index_links(&self) -> Option<Vec<TranslationLink>> {
|
pub fn translation_index_links(&self) -> Option<Vec<TranslationLink>> {
|
||||||
|
debug!("[fn] translation_index_links()");
|
||||||
|
|
||||||
let mut default_links: Vec<TranslationLink> = vec![];
|
let mut default_links: Vec<TranslationLink> = vec![];
|
||||||
|
|
||||||
let mut keys = self.translations.keys()
|
let mut keys = self.translations.keys()
|
||||||
|
@ -578,6 +546,22 @@ impl MDBook {
|
||||||
} else {
|
} else {
|
||||||
self.src_base = path.to_owned();
|
self.src_base = path.to_owned();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let a = self.translations.clone();
|
||||||
|
let keys = a.keys();
|
||||||
|
let is_multilang: bool = keys.clone().count() > 1;
|
||||||
|
|
||||||
|
for key in keys {
|
||||||
|
if let Some(mut book) = self.translations.get_mut(key) {
|
||||||
|
if is_multilang {
|
||||||
|
book.config.src = self.src_base.join(key);
|
||||||
|
book.config.is_multilang = is_multilang;
|
||||||
|
} else {
|
||||||
|
book.config.src = self.src_base.to_owned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -591,6 +575,22 @@ impl MDBook {
|
||||||
} else {
|
} else {
|
||||||
self.dest_base = path.to_owned();
|
self.dest_base = path.to_owned();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let a = self.translations.clone();
|
||||||
|
let keys = a.keys();
|
||||||
|
let is_multilang: bool = keys.clone().count() > 1;
|
||||||
|
|
||||||
|
for key in keys {
|
||||||
|
if let Some(mut book) = self.translations.get_mut(key) {
|
||||||
|
if is_multilang {
|
||||||
|
book.config.dest = self.dest_base.join(key);
|
||||||
|
book.config.is_multilang = is_multilang;
|
||||||
|
} else {
|
||||||
|
book.config.dest = self.dest_base.to_owned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
//! fn main() {
|
//! fn main() {
|
||||||
//! let path = PathBuf::from("my-book"); // Path to the book project's root
|
//! let path = PathBuf::from("my-book"); // Path to the book project's root
|
||||||
//! let renderer = HtmlHandlebars::new();
|
//! let renderer = HtmlHandlebars::new();
|
||||||
//! try!(renderer.build(&path)); // Build the book
|
//! try!(renderer.build(&path, &None)); // Build the book
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
|
|
|
@ -4,15 +4,12 @@ use book::{MDBook, Book};
|
||||||
use book::chapter::{Chapter, TranslationLink};
|
use book::chapter::{Chapter, TranslationLink};
|
||||||
use book::toc::{TocItem, TocContent};
|
use book::toc::{TocItem, TocContent};
|
||||||
use utils;
|
use utils;
|
||||||
use FILES;
|
|
||||||
|
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
use std::ffi::OsStr;
|
use std::fs;
|
||||||
use std::fs::{self, File};
|
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Write};
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use handlebars::Handlebars;
|
use handlebars::Handlebars;
|
||||||
|
|
||||||
|
@ -30,13 +27,17 @@ impl HtmlHandlebars {
|
||||||
impl Renderer for HtmlHandlebars {
|
impl Renderer for HtmlHandlebars {
|
||||||
|
|
||||||
/// Prepares the project and calls `render()`.
|
/// Prepares the project and calls `render()`.
|
||||||
fn build(&self, project_root: &PathBuf) -> Result<(), Box<Error>> {
|
fn build(&self, project_root: &PathBuf, dest_base: &Option<PathBuf>) -> Result<MDBook, Box<Error>> {
|
||||||
debug!("[fn]: build");
|
debug!("[fn]: build");
|
||||||
|
|
||||||
let mut book_project = MDBook::new(&project_root);
|
let mut book_project = MDBook::new(&project_root);
|
||||||
|
|
||||||
book_project.read_config();
|
book_project.read_config();
|
||||||
|
|
||||||
|
if let Some(p) = dest_base.clone() {
|
||||||
|
book_project.set_dest_base(&p);
|
||||||
|
}
|
||||||
|
|
||||||
if !book_project.get_src_base().exists() {
|
if !book_project.get_src_base().exists() {
|
||||||
println!("Source folder doesn't exist: {:?}", book_project.get_src_base());
|
println!("Source folder doesn't exist: {:?}", book_project.get_src_base());
|
||||||
exit(2);
|
exit(2);
|
||||||
|
@ -55,11 +56,12 @@ impl Renderer for HtmlHandlebars {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(book_project)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders the chapters and copies static assets.
|
/// Renders the chapters and copies static assets.
|
||||||
fn render(&self, book_project: &MDBook) -> Result<(), Box<Error>> {
|
fn render(&self, book_project: &MDBook) -> Result<(), Box<Error>> {
|
||||||
|
debug!("[fn]: render");
|
||||||
|
|
||||||
debug!("[*]: Check if book's base output folder exists");
|
debug!("[*]: Check if book's base output folder exists");
|
||||||
if let Err(_) = fs::create_dir_all(&book_project.get_dest_base()) {
|
if let Err(_) = fs::create_dir_all(&book_project.get_dest_base()) {
|
||||||
|
@ -82,11 +84,15 @@ impl Renderer for HtmlHandlebars {
|
||||||
let c = a.join("_*");
|
let c = a.join("_*");
|
||||||
let exclude_glob = c.to_str().unwrap();
|
let exclude_glob = c.to_str().unwrap();
|
||||||
|
|
||||||
// anyone wants to catch errors?
|
// Ignoring all errors. Should try to see which types are worth returning.
|
||||||
utils::fs::copy_files(include_glob,
|
|
||||||
base,
|
match utils::fs::copy_files(include_glob,
|
||||||
vec![exclude_glob],
|
base,
|
||||||
&book_project.get_dest_base());
|
vec![exclude_glob],
|
||||||
|
&book_project.get_dest_base()) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(_) => {},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy template's static assets
|
// Copy template's static assets
|
||||||
|
@ -118,20 +124,23 @@ impl Renderer for HtmlHandlebars {
|
||||||
// )
|
// )
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// anyone wants to catch errors?
|
// Ignoring all errors. Should try to see which types are worth returning.
|
||||||
utils::fs::copy_files(include_glob,
|
|
||||||
base,
|
|
||||||
vec![exclude_glob],
|
|
||||||
&book_project.get_dest_base());
|
|
||||||
|
|
||||||
|
match utils::fs::copy_files(include_glob,
|
||||||
|
base,
|
||||||
|
vec![exclude_glob],
|
||||||
|
&book_project.get_dest_base()) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(_) => {},
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
try!(utils::fs::copy_data("data/_html-template/**/*",
|
try!(utils::fs::copy_data("data/assets/_html-template/**/*",
|
||||||
"data/_html-template/",
|
"data/assets/_html-template/",
|
||||||
vec!["data/_html-template/_*"],
|
vec!["data/assets/_html-template/_*"],
|
||||||
&book_project.get_dest_base()));
|
&book_project.get_dest_base()));
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("[fn]: render");
|
debug!("[*]: start rendering");
|
||||||
let mut handlebars = Handlebars::new();
|
let mut handlebars = Handlebars::new();
|
||||||
|
|
||||||
let translation_indexes = book_project.translation_index_links();
|
let translation_indexes = book_project.translation_index_links();
|
||||||
|
@ -159,7 +168,7 @@ impl Renderer for HtmlHandlebars {
|
||||||
let s = if let Some(p) = first_path_that_exists(&search_paths) {
|
let s = if let Some(p) = first_path_that_exists(&search_paths) {
|
||||||
try!(utils::fs::file_to_string(&p))
|
try!(utils::fs::file_to_string(&p))
|
||||||
} else {
|
} else {
|
||||||
try!(utils::fs::get_data_file("data/_html-template/_layouts/page.hbs"))
|
try!(utils::fs::get_data_file("data/assets/_html-template/_layouts/page.hbs"))
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register template
|
// Register template
|
||||||
|
@ -194,7 +203,7 @@ impl Renderer for HtmlHandlebars {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the chapters of each book
|
// Render the chapters of each book
|
||||||
for (key, book) in &book_project.translations {
|
for (_, book) in &book_project.translations {
|
||||||
|
|
||||||
// Check if book's dest directory exists
|
// Check if book's dest directory exists
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
use std::path::Path;
|
use std::collections::VecDeque;
|
||||||
use std::collections::{VecDeque, BTreeMap};
|
|
||||||
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
|
use handlebars::{Handlebars, HelperDef, RenderError, RenderContext, Helper, Context};
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use std::path::Path;
|
|
||||||
use std::collections::{VecDeque, BTreeMap};
|
use std::collections::{VecDeque, BTreeMap};
|
||||||
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
|
@ -6,20 +6,27 @@ use book::MDBook;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
// TODO refactor dest_base out of the .build() call. It's only here b/c to
|
||||||
|
// influence the build output with the --dest-dir CLI arg. It is a good
|
||||||
|
// thing though that .build() encapsulates the steps to prepare the MDBook
|
||||||
|
// struct for .render(). Maybe give it the CLI args and process them within
|
||||||
|
// .build().
|
||||||
|
|
||||||
pub trait Renderer {
|
pub trait Renderer {
|
||||||
|
|
||||||
/// When the output format is determined (by a CLI argument for example),
|
/// When the output format is determined (by a CLI argument for example),
|
||||||
/// call `.build()` of the selected Renderer implementation.
|
/// call `.build()` of the selected Renderer implementation.
|
||||||
///
|
///
|
||||||
/// Constructs an `MDBook` struct given the path of the book project,
|
/// Constructs an `MDBook` struct given the path of the book project,
|
||||||
/// preparing the project and calling `render()`, doing what is necessary
|
/// optionally using a custom output folder (such as when given with
|
||||||
/// for the particular output format.
|
/// `--dest-dir` CLI argument). It prepares the project and calls
|
||||||
|
/// `render()`, doing what is necessary for the particular output format.
|
||||||
///
|
///
|
||||||
/// This involves parsing config options from `book.toml` and parsing the
|
/// This involves parsing config options from `book.toml` and parsing the
|
||||||
/// `SUMMARY.md` of each translation to a nested `Vec<TocItem>`.
|
/// `SUMMARY.md` of each translation to a nested `Vec<TocItem>`.
|
||||||
///
|
///
|
||||||
/// Finally it calls `render()` to process the chapters and static assets.
|
/// Finally it calls `render()` to process the chapters and static assets.
|
||||||
fn build(&self, project_root: &PathBuf) -> Result<(), Box<Error>>;
|
fn build(&self, project_root: &PathBuf, dest_base: &Option<PathBuf>) -> Result<MDBook, Box<Error>>;
|
||||||
|
|
||||||
/// Responsible for rendering the chapters and copying static assets.
|
/// Responsible for rendering the chapters and copying static assets.
|
||||||
fn render(&self, book_project: &MDBook) -> Result<(), Box<Error>>;
|
fn render(&self, book_project: &MDBook) -> Result<(), Box<Error>>;
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#[cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use book::bookconfig::BookConfig;
|
use book::bookconfig::BookConfig;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#[cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use book::bookconfig::Author;
|
use book::bookconfig::Author;
|
||||||
use book::chapter::Chapter;
|
use book::chapter::Chapter;
|
||||||
|
|
||||||
|
@ -11,7 +10,12 @@ fn it_parses_when_exists() {
|
||||||
let path = PathBuf::from("at-the-mountains-of-madness.md");
|
let path = PathBuf::from("at-the-mountains-of-madness.md");
|
||||||
|
|
||||||
let mut result = Chapter::new("Mountains".to_string(), path.clone());
|
let mut result = Chapter::new("Mountains".to_string(), path.clone());
|
||||||
result.parse_or_create_using(&src_path);
|
match result.parse_or_create_using(&src_path) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => {
|
||||||
|
println!("Error: {:#?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut expected = Chapter::new("Mountains".to_string(), path.clone());
|
let mut expected = Chapter::new("Mountains".to_string(), path.clone());
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
#[cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
extern crate tempdir;
|
extern crate tempdir;
|
||||||
|
|
||||||
use std;
|
use std;
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use utils;
|
use utils;
|
||||||
use utils::fs::copy_files_except_ext;
|
use utils::fs::copy_files_except_ext;
|
||||||
|
@ -15,18 +14,21 @@ fn it_copies_data_file() {
|
||||||
let dest_base = std::env::temp_dir().join("in tangles of old alleys");
|
let dest_base = std::env::temp_dir().join("in tangles of old alleys");
|
||||||
let dest_path = dest_base.join("book.css");
|
let dest_path = dest_base.join("book.css");
|
||||||
|
|
||||||
utils::fs::copy_data_file("data/_html-template/css/books.css", &dest_path);
|
match utils::fs::copy_data_file("data/assets/_html-template/css/books.css", &dest_path) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => { println!("{:#?}", e); }
|
||||||
|
}
|
||||||
|
|
||||||
let mut file = match File::open(&dest_path) {
|
let mut file = match File::open(&dest_path) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(e) => {
|
Err(_) => {
|
||||||
println!("Failed to open {:?}", dest_path);
|
println!("Failed to open {:?}", dest_path);
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut content = String::new();
|
let mut content = String::new();
|
||||||
if let Err(e) = file.read_to_string(&mut content) {
|
if let Err(_) = file.read_to_string(&mut content) {
|
||||||
println!("Failed to read {:?}", dest_path);
|
println!("Failed to read {:?}", dest_path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -34,7 +36,10 @@ fn it_copies_data_file() {
|
||||||
assert!(content.as_str().contains("Open Sans"));
|
assert!(content.as_str().contains("Open Sans"));
|
||||||
|
|
||||||
if dest_base.exists() {
|
if dest_base.exists() {
|
||||||
fs::remove_dir_all(dest_base);
|
match fs::remove_dir_all(dest_base) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => { println!("{:#?}", e); },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,9 +47,9 @@ fn it_copies_data_file() {
|
||||||
fn it_copies_data_by_pattern() {
|
fn it_copies_data_by_pattern() {
|
||||||
let dest_base = std::env::temp_dir().join("near the quays");
|
let dest_base = std::env::temp_dir().join("near the quays");
|
||||||
|
|
||||||
if let Err(e) = utils::fs::copy_data("data/_html-template/**/*",
|
if let Err(e) = utils::fs::copy_data("data/assets/_html-template/**/*",
|
||||||
"data/_html-template/",
|
"data/assets/_html-template/",
|
||||||
vec!["data/_html-template/_*"],
|
vec!["data/assets/_html-template/_*"],
|
||||||
&dest_base) {
|
&dest_base) {
|
||||||
println!("Error: {:#?}", e);
|
println!("Error: {:#?}", e);
|
||||||
return;
|
return;
|
||||||
|
@ -54,7 +59,10 @@ fn it_copies_data_by_pattern() {
|
||||||
assert!(!dest_base.join("_layouts").exists());
|
assert!(!dest_base.join("_layouts").exists());
|
||||||
|
|
||||||
if dest_base.exists() {
|
if dest_base.exists() {
|
||||||
fs::remove_dir_all(dest_base);
|
match fs::remove_dir_all(dest_base) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => { println!("{:#?}", e); },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,14 +80,20 @@ fn it_doesnt_delete_toplevel_dotfiles() {
|
||||||
Ok(_) => {},
|
Ok(_) => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
utils::fs::clean_output_dir(&dest_base);
|
match utils::fs::clean_output_dir(&dest_base) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => { println!("{:#?}", e); }
|
||||||
|
}
|
||||||
|
|
||||||
assert!(dest_base.exists());
|
assert!(dest_base.exists());
|
||||||
assert!(dest_base.join(".dotfile").exists());
|
assert!(dest_base.join(".dotfile").exists());
|
||||||
assert!(!dest_base.join("door.html").exists());
|
assert!(!dest_base.join("door.html").exists());
|
||||||
|
|
||||||
if dest_base.exists() {
|
if dest_base.exists() {
|
||||||
fs::remove_dir_all(dest_base);
|
match fs::remove_dir_all(dest_base) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(e) => { println!("{:#?}", e); },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#[cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ fn it_renders_multilanguage_book() {
|
||||||
let path = PathBuf::from(".").join("src").join("tests").join("book-wonderland-multilang");
|
let path = PathBuf::from(".").join("src").join("tests").join("book-wonderland-multilang");
|
||||||
|
|
||||||
let renderer = HtmlHandlebars::new();
|
let renderer = HtmlHandlebars::new();
|
||||||
if let Err(e) = renderer.build(&path) {
|
if let Err(e) = renderer.build(&path, &None) {
|
||||||
panic!("{:#?}", e);
|
panic!("{:#?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@ fn it_renders_multilanguage_book() {
|
||||||
proj.parse_books();
|
proj.parse_books();
|
||||||
|
|
||||||
let mut book_path: &Path = proj.translations.get("en").unwrap().config.get_dest();
|
let mut book_path: &Path = proj.translations.get("en").unwrap().config.get_dest();
|
||||||
let mut chapter_path: PathBuf = PathBuf::from("".to_string());
|
let mut chapter_path: PathBuf;
|
||||||
let mut s: String = String::new();
|
let mut s: String;
|
||||||
|
|
||||||
// Test if index.html in the project dest folder is the main book's first chapter
|
// Test if index.html in the project dest folder is the main book's first chapter
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#[cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ fn it_renders_html_from_minimal_book() {
|
||||||
let path = PathBuf::from(".").join("src").join("tests").join("book-minimal");
|
let path = PathBuf::from(".").join("src").join("tests").join("book-minimal");
|
||||||
|
|
||||||
let renderer = HtmlHandlebars::new();
|
let renderer = HtmlHandlebars::new();
|
||||||
if let Err(e) = renderer.build(&path) {
|
if let Err(e) = renderer.build(&path, &None) {
|
||||||
println!("{:#?}", e);
|
println!("{:#?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,8 +20,8 @@ fn it_renders_html_from_minimal_book() {
|
||||||
proj.parse_books();
|
proj.parse_books();
|
||||||
|
|
||||||
let book_path: &Path = proj.translations.get("en").unwrap().config.get_dest();
|
let book_path: &Path = proj.translations.get("en").unwrap().config.get_dest();
|
||||||
let mut chapter_path: PathBuf = PathBuf::from("".to_string());
|
let mut chapter_path: PathBuf;
|
||||||
let mut s: String = String::new();
|
let mut s: String;
|
||||||
|
|
||||||
// Test if "Library of Babel" was rendered
|
// Test if "Library of Babel" was rendered
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ fn it_copies_local_assets_when_found() {
|
||||||
let path = PathBuf::from(".").join("src").join("tests").join("book-minimal-with-assets");
|
let path = PathBuf::from(".").join("src").join("tests").join("book-minimal-with-assets");
|
||||||
|
|
||||||
let renderer = HtmlHandlebars::new();
|
let renderer = HtmlHandlebars::new();
|
||||||
if let Err(e) = renderer.build(&path) {
|
if let Err(e) = renderer.build(&path, &None) {
|
||||||
println!("{:#?}", e);
|
println!("{:#?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,13 +64,11 @@ fn it_copies_local_assets_when_found() {
|
||||||
|
|
||||||
|
|
||||||
let book_path: &Path = proj.translations.get("en").unwrap().config.get_dest();
|
let book_path: &Path = proj.translations.get("en").unwrap().config.get_dest();
|
||||||
let mut chapter_path: PathBuf = PathBuf::from("".to_string());
|
|
||||||
let mut s: String = String::new();
|
|
||||||
|
|
||||||
// Test if "Library of Babel" was rendered
|
// Test if "Library of Babel" was rendered
|
||||||
|
|
||||||
chapter_path = book_path.join("fictions").join("babel").with_extension("html");
|
let chapter_path = book_path.join("fictions").join("babel").with_extension("html");
|
||||||
s = utils::fs::file_to_string(&chapter_path).unwrap();
|
let s = utils::fs::file_to_string(&chapter_path).unwrap();
|
||||||
assert!(s.contains("The Library of Babel"));
|
assert!(s.contains("The Library of Babel"));
|
||||||
|
|
||||||
assert_eq!(book_path.join("css").join("book.css").exists(), true);
|
assert_eq!(book_path.join("css").join("book.css").exists(), true);
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
#[cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
extern crate toml;
|
extern crate toml;
|
||||||
|
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use serde_json;
|
|
||||||
|
|
||||||
use utils;
|
use utils;
|
||||||
use book::MDBook;
|
use book::MDBook;
|
||||||
use book::book::Book;
|
use book::book::Book;
|
||||||
use book::bookconfig::{BookConfig, Author, Language};
|
use book::bookconfig::{BookConfig, Author, Language};
|
||||||
use book::chapter::Chapter;
|
use book::chapter::Chapter;
|
||||||
use book::toc::{TocItem, TocContent};
|
use book::toc::TocItem;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_parses_simple_json_config() {
|
fn it_parses_simple_json_config() {
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
#[cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use parse::summary::parse_level;
|
use parse::summary::parse_level;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#[cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
use book::chapter::Chapter;
|
|
||||||
use book::toc::{TocItem, TocContent, flat_toc, toc_node_count_id};
|
use book::toc::{TocItem, TocContent, flat_toc, toc_node_count_id};
|
||||||
use parse::summary::parse_level;
|
use parse::summary::parse_level;
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
#[cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
||||||
use utils::*;
|
use utils::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -61,17 +61,17 @@ pub fn copy_data_file(src_path: &str, dest_path: &Path) -> Result<(), Box<Error>
|
||||||
/// `include_base` will be removed from the source path. This way the path
|
/// `include_base` will be removed from the source path. This way the path
|
||||||
/// relative to the `dest_path` can be controlled.
|
/// relative to the `dest_path` can be controlled.
|
||||||
///
|
///
|
||||||
/// The following will copy all files under "data/_html-template/", excluding
|
/// The following will copy all files under "data/assets/_html-template/", excluding
|
||||||
/// folders that start with "_", take the "data/_html-template/" part off the
|
/// folders that start with "_", take the "data/assets/_html-template/" part off the
|
||||||
/// source path, and write the entries to "assets" folder.
|
/// source path, and write the entries to "assets" folder.
|
||||||
///
|
///
|
||||||
/// I.e. "data/_html-template/css/book.css" will be written to
|
/// I.e. "data/assets/_html-template/css/book.css" will be written to
|
||||||
/// "assets/css/book.css".
|
/// "assets/css/book.css".
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// utils::fs::copy_data("data/_html-template/**/*",
|
/// utils::fs::copy_data("data/assets/_html-template/**/*",
|
||||||
/// "data/_html-template/",
|
/// "data/assets/_html-template/",
|
||||||
/// vec!["data/_html-template/_*"],
|
/// vec!["data/assets/_html-template/_*"],
|
||||||
/// &Path::new("assets"));
|
/// &Path::new("assets"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn copy_data(include_glob: &str,
|
pub fn copy_data(include_glob: &str,
|
||||||
|
@ -221,7 +221,7 @@ pub fn path_to_root(path: &Path) -> String {
|
||||||
|
|
||||||
/// This function creates a file and returns it. But before creating the file it checks every
|
/// This function creates a file and returns it. But before creating the file it checks every
|
||||||
/// directory in the path to see if it exists, and if it does not it will be created.
|
/// directory in the path to see if it exists, and if it does not it will be created.
|
||||||
pub fn create_file(path: &Path) -> Result<File, Box<Error>> {
|
pub fn create_file(path: &Path) -> io::Result<File> {
|
||||||
debug!("[fn]: create_file");
|
debug!("[fn]: create_file");
|
||||||
|
|
||||||
// Construct path
|
// Construct path
|
||||||
|
@ -232,15 +232,7 @@ pub fn create_file(path: &Path) -> Result<File, Box<Error>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("[*]: Create file: {:?}", path);
|
debug!("[*]: Create file: {:?}", path);
|
||||||
let f = match File::create(path) {
|
File::create(path)
|
||||||
Ok(f) => f,
|
|
||||||
Err(e) => {
|
|
||||||
debug!("File::create: {}", e);
|
|
||||||
return Err(Box::new(io::Error::new(io::ErrorKind::Other, format!("{}", e))));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(f)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A cleaning operation intended to be used on the output directory of a book
|
/// A cleaning operation intended to be used on the output directory of a book
|
||||||
|
|
|
@ -4,7 +4,6 @@ extern crate toml;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::error::Error;
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use serde_json;
|
use serde_json;
|
||||||
|
|
Loading…
Reference in New Issue