Compare commits

..

3 Commits

Author SHA1 Message Date
Michael Bryan
fe3f2ee4b1 Removed a deprecation warning 2018-04-07 06:27:58 +08:00
Michael Bryan
9784d7a23b Updated dependencies 2018-04-07 06:15:52 +08:00
Michael Bryan
38c06f3c39 Removed all the unnecessary CI jobs 2018-04-07 06:15:26 +08:00
105 changed files with 4081 additions and 3460 deletions

View File

@@ -1,8 +1,7 @@
language: rust
cache:
timeout: 360
cargo: true
- cargo
before_cache:
- chmod -R a+r $HOME/.cargo
@@ -21,6 +20,9 @@ script:
- cargo build --verbose
- cargo test --verbose
after_success:
- bash ci/github_pages.sh
before_deploy:
- sh ci/before_deploy.sh
@@ -37,8 +39,8 @@ deploy:
branches:
only:
- "/^v\\d+\\.\\d+\\.\\d+.*$/"
- master
- /^v\d+\.\d+\.\d+.*$/
notifications:
email:

View File

@@ -48,6 +48,30 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
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/rust-lang-nursery/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.

714
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,23 @@
[package]
name = "mdbook"
version = "0.2.0"
authors = [
"Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>",
"Matt Ickstadt <mattico8@gmail.com>"
]
version = "0.1.6"
authors = ["Mathieu David <mathieudavid@mathieudavid.org>", "Michael-F-Bryan <michaelfbryan@gmail.com>"]
description = "Create books from markdown files"
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
repository = "https://github.com/rust-lang-nursery/mdBook"
keywords = ["book", "gitbook", "rustbook", "markdown"]
license = "MPL-2.0"
readme = "README.md"
exclude = ["book-example/*"]
build = "build.rs"
exclude = [
"book-example/*",
"src/theme/stylus/**",
]
[package.metadata.release]
sign-commit = true
push-remote = "origin"
tag-prefix = "v"
[dependencies]
clap = "2.24"
@@ -20,23 +25,25 @@ chrono = "0.4"
handlebars = "0.32"
serde = "1.0"
serde_derive = "1.0"
error-chain = "0.12"
error-chain = "0.11"
serde_json = "1.0"
pulldown-cmark = "0.1.2"
lazy_static = "1.0"
log = "0.4"
env_logger = "0.5"
env_logger = "0.5.0-rc.1"
toml = "0.4"
memchr = "2.0"
open = "1.1"
regex = "1.0.0"
regex = "0.2.1"
tempfile = "3.0"
itertools = "0.7"
shlex = "0.1"
toml-query = "0.7"
toml-query = "0.6"
# Watch feature
notify = { version = "4.0", optional = true }
time = { version = "0.1.34", optional = true }
crossbeam = { version = "0.3", optional = true }
# Serve feature
iron = { version = "0.6", optional = true }
@@ -44,9 +51,12 @@ staticfile = { version = "0.5", optional = true }
ws = { version = "0.7", optional = true}
# Search feature
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
elasticlunr-rs = { version = "2.0", optional = true }
ammonia = { version = "1.1", optional = true }
[build-dependencies]
error-chain = "0.11"
[dev-dependencies]
select = "0.4"
pretty_assertions = "0.5"
@@ -57,10 +67,12 @@ pulldown-cmark-to-cmark = "1.1.0"
default = ["output", "watch", "serve", "search"]
debug = []
output = []
watch = ["notify"]
regenerate-css = []
watch = ["notify", "time", "crossbeam"]
serve = ["iron", "staticfile", "ws"]
search = ["elasticlunr-rs", "ammonia"]
[[bin]]
doc = false
name = "mdbook"
path = "src/bin/mdbook.rs"

View File

@@ -41,7 +41,7 @@ There are multiple ways to install mdBook.
2. **From Crates.io**
This requires at least [Rust] 1.20 and Cargo to be installed. Once you have installed
This requires [Rust] and Cargo to be installed. Once you have installed
Rust, type the following in the terminal:
```

View File

@@ -1,6 +1,7 @@
environment:
global:
PROJECT_NAME: mdBook
nodejs_version: "6"
matrix:
# Stable channel
- TARGET: i686-pc-windows-msvc
@@ -31,12 +32,17 @@ install:
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
- rustc -Vv
- cargo -V
- ps: Install-Product node $env:nodejs_version
- node --version
- npm --version
- npm install -g stylus nib
build: false
# Equivalent to Travis' `script` phase
test_script:
- cargo build --verbose
- cargo build --verbose --features=regenerate-css
- cargo test --verbose
before_deploy:
@@ -61,4 +67,3 @@ deploy:
branches:
only:
- master
- /^v\d+\.\d+\.\d+.*$/

View File

@@ -1,27 +1,24 @@
# Summary
- [mdBook](README.md)
- [Command Line Tool](cli/README.md)
- [Command Line Tool](cli/cli-tool.md)
- [init](cli/init.md)
- [build](cli/build.md)
- [watch](cli/watch.md)
- [serve](cli/serve.md)
- [test](cli/test.md)
- [clean](cli/clean.md)
- [Format](format/README.md)
- [Format](format/format.md)
- [SUMMARY.md](format/summary.md)
- [Configuration](format/config.md)
- [Theme](format/theme/README.md)
- [Theme](format/theme/theme.md)
- [index.hbs](format/theme/index-hbs.md)
- [Syntax highlighting](format/theme/syntax-highlighting.md)
- [Editor](format/theme/editor.md)
- [MathJax Support](format/mathjax.md)
- [mdBook specific features](format/mdbook.md)
- [Continuous Integration](continuous-integration.md)
- [For Developers](for_developers/README.md)
- [For Developers](for_developers/index.md)
- [Preprocessors](for_developers/preprocessors.md)
- [Alternate Backends](for_developers/backends.md)
-----------
[Contributors](misc/contributors.md)

View File

@@ -14,8 +14,8 @@ convenience. Large books will therefore remain structured when rendered.
#### Specify a directory
The `build` command can take a directory as an argument to use as the book's
root instead of the current working directory.
Like `init`, the `build` command can take a directory as an argument to use
instead of the current working directory.
```bash
mdbook build path/to/book
@@ -23,16 +23,13 @@ mdbook build path/to/book
#### --open
When you use the `--open` (`-o`) flag, mdbook will open the rendered book in
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
the book. If not specified it will default to the value of the
`build.build-dir` key in `book.toml`, or to `./book` relative to the book's
root directory.
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*

View File

@@ -7,21 +7,12 @@ artifacts.
mdbook clean
```
It will try to delete the built book. If a path is provided, it will be used.
#### Specify a directory
The `clean` command can take a directory as an argument to use as the book's
root instead of the current working directory.
```bash
mdbook clean path/to/book
```
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to override the book's output
directory, which will be deleted by this command. If not specified it
will default to the value of the `build.build-dir` key in `book.toml`, or to
`./book` relative to the book's root directory.
Like `init`, the `clean` command can take a directory as an argument to use
instead of the normal build directory.
```bash
mdbook clean --dest-dir=path/to/book

View File

@@ -1,7 +1,5 @@
# The init command
There is some minimal boilerplate that is the same for every new book. It's for this purpose
that mdBook includes an `init` command.
There is some minimal boilerplate that is the same for every new book. It's for this purpose that mdBook includes an `init` command.
The `init` command is used like this:
@@ -24,27 +22,23 @@ configuration files, etc.
- The `book` directory is where your book is rendered. All the output is ready to be uploaded
to a server to be seen by your audience.
- The `SUMMARY.md` file is the most important file, it's the skeleton of your book and is
discussed in more detail [in another chapter](../format/summary.md)
- 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: Generate chapters from SUMMARY.md
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.
#### 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.
#### Specify a directory
The `init` command can take a directory as an argument to use as the book's
root instead of the current working directory.
When using the `init` command, you can also specify a directory, instead of using the current working directory,
by appending a path to the command:
```bash
mdbook init path/to/book
```
#### --theme
## --theme
When you use the `--theme` flag, the default theme will be copied into a directory
When you use the `--theme` argument, the default theme will be copied into a directory
called `theme` in your source directory so that you can modify it.
The theme is selectively overwritten, this means that if you don't want to overwrite a

View File

@@ -1,50 +1,40 @@
# The serve command
The serve command is used to preview a book by serving it over HTTP at
`localhost:3000` by default. Additionally it watches the book's directory
for changes, rebuilding the book and refreshing clients for each change.
A websocket connection is used to trigger the client-side refresh.
The `serve` command is useful when you want to preview your book. It also does hot reloading of the webpage whenever a file changes.
It achieves this by serving the books content over `localhost:3000` (unless otherwise configured, see below) and runs a websocket server on `localhost:3001` which triggers the reloads.
This preferred by many for writing books with mdbook because it allows for you to see the result of your work instantly after every file change.
#### Specify a directory
The `serve` command can take a directory as an argument to use as the book's
root instead of the current working directory.
Like `watch`, `serve` can take a directory as an argument to use instead of
the current working directory.
```bash
mdbook serve path/to/book
```
#### Server options
`serve` has four options: the HTTP port, the WebSocket port, the HTTP hostname
to listen on, and the hostname for the browser to connect to for WebSockets.
`serve` has four options: the http port, the websocket port, the interface to serve on, and the public address of the server so that the browser may reach the websocket server.
For example: suppose you have an nginx server for SSL termination which has a
public address of 192.168.1.100 on port 80 and proxied that to 127.0.0.1 on
port 8000. To run use the nginx proxy do:
For example: suppose you had an nginx server for SSL termination which has a public address of 192.168.1.100 on port 80 and proxied that to 127.0.0.1 on port 8000. To run use the nginx proxy do:
```bash
mdbook serve path/to/book -p 8000 -n 127.0.0.1 --websocket-hostname 192.168.1.100
mdbook serve 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`) flag, mdbook will open the book in your
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
the book. If not specified it will default to the value of the
`build.build-dir` key in `book.toml`, or to `./book` relative to the book's
root directory.
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
-----
***Note:*** *The `serve` command is for testing, and is not intended
to be a complete HTTP server for a website.*
***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/rust-lang-nursery/mdBook/issues)*

View File

@@ -1,53 +1,19 @@
# The test command
When writing a book, you sometimes need to automate some tests. For example,
[The Rust Programming Book](https://doc.rust-lang.org/stable/book/) uses a lot
of code examples that could get outdated. Therefore it is very important for
them to be able to automatically test these code examples.
When writing a book, you sometimes need to automate some tests. For example, [The Rust Programming Book](https://doc.rust-lang.org/stable/book/) uses a lot of code examples that could get outdated.
Therefore it is very important for them to be able to automatically test these code examples.
mdBook supports a `test` command that will run all available tests in a book.
At the moment, only rustdoc tests are supported, but this may be expanded upon
in the future.
mdBook supports a `test` command that will run all available tests in mdBook. At the moment, only one test is available:
*"Test Rust code examples using Rustdoc"*, but I hope this will be expanded in the future to include more tests like:
#### Disable tests on a code block
- checking for broken links
- checking for unused files
- ...
rustdoc doesn't test code blocks which contain the `ignore` attribute:
```rust,ignore
fn main() {}
```
rustdoc also doesn't test code blocks which specify a language other than Rust:
```markdown
**Foo**: _bar_
```
rustdoc *does* test code blocks which have no language specified:
```
This is going to cause an error!
```
#### Specify a directory
The `test` command can take a directory as an argument to use as the book's
root instead of the current working directory.
In the future I would like the user to be able to enable / disable test from the `book.toml` configuration file and support custom tests.
**How to use it:**
```bash
mdbook test path/to/book
$ mdbook test
[*]: Testing file: "/mdBook/book-example/src/README.md”
```
#### --library-path
The `--library-path` (`-L`) option allows you to add directories to the library
search path used by `rustdoc` when it builds and tests the examples. Multiple
directories can be specified with multiple options (`-L foo -L bar`) or with a
comma-delimited list (`-L foo,bar`).
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for
the book. If not specified it will default to the value of the
`build.build-dir` key in `book.toml`, or to `./book` relative to the book's
root directory.

View File

@@ -1,14 +1,12 @@
# The watch command
The `watch` command is useful when you want your book to be rendered on every
file change. You could repeatedly issue `mdbook build` every time a file is
changed. But using `mdbook watch` once will watch your files and will trigger
a build automatically whenever you modify a file.
The `watch` command is useful when you want your book to be rendered on every file change.
You could repeatedly issue `mdbook build` every time a file is changed. But using `mdbook watch` once will watch your files and will trigger a build automatically whenever you modify a file.
#### Specify a directory
The `watch` command can take a directory as an argument to use as the book's
root instead of the current working directory.
Like `init` and `build`, `watch` can take a directory as an argument to use
instead of the current working directory.
```bash
mdbook watch path/to/book
@@ -21,7 +19,8 @@ your default web browser.
#### --dest-dir
The `--dest-dir` (`-d`) option allows you to change the output directory for
the book. If not specified it will default to the value of the
`build.build-dir` key in `book.toml`, or to `./book` relative to the book's
root directory.
The `--dest-dir` (`-d`) option allows you to change the output directory for your book.
-----
***note:*** *the `watch` 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/rust-lang-nursery/mdBook/issues)*

View File

@@ -1,52 +0,0 @@
# Running `mdbook` in Continuous Integration
While the following examples use Travis CI, their principles should
straightforwardly transfer to other continuous integration providers as well.
## Ensuring Your Book Builds and Tests Pass
Here is a sample Travis CI `.travis.yml` configuration that ensures `mdbook
build` and `mdbook test` run successfully. The key to fast CI turnaround times
is caching `mdbook` installs, so that you aren't compiling `mdbook` on every CI
run.
```yaml
language: rust
sudo: false
cache:
- cargo
rust:
- stable
before_script:
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.1" mdbook)
- cargo install-update -a
script:
- cd path/to/mybook && mdbook build && mdbook test
```
## Deploying Your Book to GitHub Pages
Following these instructions will result in your book being published to GitHub
pages after a successful CI run on your repository's `master` branch.
First, create a new GitHub "Personal Access Token" with the "public_repo" permissions (or "repo" for private repositories). Go to your repository's Travis CI settings page and add an environment variable named `GITHUB_TOKEN` that is marked secure and *not* shown in the logs.
Then, append this snippet to your `.travis.yml` and update the path to the `book` directory:
```yaml
deploy:
provider: pages
skip-cleanup: true
github-token: $GITHUB_TOKEN
local-dir: path/to/mybook/book
keep-history: false
on:
branch: master
```
That's it!

View File

@@ -11,8 +11,8 @@ The *For Developers* chapters are here to show you the more advanced usage of
The two main ways a developer can hook into the book's build process is via,
- [Preprocessors](preprocessors.md)
- [Alternate Backends](backends.md)
- [Preprocessors](for_developers/preprocessors.html)
- [Alternate Backends](for_developers/backends.html)
## The Build Process

View File

@@ -58,21 +58,12 @@ This controls the build process of your book.
will be created when the book is built (i.e. `create-missing = true`). If this
is `false` then the build process will instead exit with an error if any files
do not exist.
- **preprocess:** Specify which preprocessors to be applied. Default is `["links", "index"]`. To disable default preprocessors, pass an empty array `[]` in.
The following preprocessors are available and included by default:
- `links`: Expand the `{{# playpen}}` and `{{# include}}` handlebars helpers in a chapter.
- `index`: Convert all chapter files named `README.md` into `index.md`. That is to say, all `README.md` would be rendered to an index file `index.html` in the rendered book.
**book.toml**
```toml
[build]
build-dir = "build"
create-missing = false
preprocess = ["links", "index"]
```
### HTML renderer options
@@ -113,7 +104,6 @@ Available configuration options for the `[output.html.playpen]` table:
Available configuration options for the `[output.html.search]` table:
- **enable:** Enables the search feature. Defaults to `true`.
- **limit-results:** The maximum number of search results. Defaults to `30`.
- **teaser-word-count:** The number of words used for a search result teaser.
Defaults to `30`.
@@ -142,11 +132,6 @@ title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
[build]
build-dir = "book"
create-missing = true
preprocess = ["links", "index"]
[output.html]
theme = "my-theme"
curly-quotes = true
@@ -169,7 +154,6 @@ boost-hierarchy = 1
boost-paragraph = 1
expand = true
heading-split-level = 3
copy-js = true
```

View File

@@ -9,13 +9,9 @@ To enable MathJax, you need to add the `mathjax-support` key to your `book.toml`
mathjax-support = true
```
>**Note:**
>**Note:**
The usual delimiters MathJax uses are not yet supported. You can't currently use `$$ ... $$` as delimiters and the `\[ ... \]` delimiters need an extra backslash to work. Hopefully this limitation will be lifted soon.
>**Note:**
> When you use double backslashes in MathJax blocks (for example in commands such as `\begin{cases} \frac 1 2 \\ \frac 3 4 \end{cases}`) you need to add _two extra_ backslashes (e.g., `\begin{cases} \frac 1 2 \\\\ \frac 3 4 \end{cases}`).
### Inline equations
Inline equations are delimited by `\\(` and `\\)`. So for example, to render the following inline equation \\( \int x dx = \frac{x^2}{2} + C \\) you would write the following:
```

View File

@@ -37,7 +37,7 @@ Since the original directory structure is maintained, it is useful to prepend re
In addition to the properties you can access, there are some handlebars helpers at your disposal.
### 1. toc
1. ### toc
The toc helper is used like this
@@ -68,7 +68,7 @@ In addition to the properties you can access, there are some handlebars helpers
</script>
```
### 2. previous / next
2. ### previous / next
The previous and next helpers expose a `link` and `name` property to the previous and next chapters.
@@ -87,4 +87,5 @@ In addition to the properties you can access, there are some handlebars helpers
------
*If you would like other properties or helpers exposed, please [create a new issue](https://github.com/rust-lang-nursery/mdBook/issues)*
*If you would like me to expose other properties or helpers, please [create a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
and I will consider it.*

View File

@@ -16,4 +16,3 @@ If you have contributed to mdBook and I forgot to add you, don't hesitate to add
- [projektir](https://github.com/projektir)
- [Phaiax](https://github.com/Phaiax)
- [Matt Ickstadt](https://github.com/mattico)
- Weihang Lo ([@weihanglo](https://github.com/weihanglo))

98
build.rs Normal file
View File

@@ -0,0 +1,98 @@
// build.rs
use std::env;
use std::path::Path;
#[macro_use]
extern crate error_chain;
#[cfg(windows)]
mod execs {
use std::process::Command;
pub fn cmd(program: &str) -> Command {
let mut cmd = Command::new("cmd");
cmd.args(&["/c", program]);
cmd
}
}
#[cfg(not(windows))]
mod execs {
use std::process::Command;
pub fn cmd(program: &str) -> Command {
Command::new(program)
}
}
error_chain!{
foreign_links {
Io(std::io::Error);
}
}
fn program_exists(program: &str) -> Result<()> {
execs::cmd(program).arg("-v")
.output()
.chain_err(|| format!("Please install '{}'!", program))?;
Ok(())
}
fn npm_package_exists(package: &str) -> Result<()> {
let status = execs::cmd("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::{Package, Program};
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") {
// Check dependencies
Program("npm").exists()?;
Program("node").exists().or(Program("nodejs").exists())?;
Package("nib").exists()?;
Package("stylus").exists()?;
// Compile stylus stylesheet to css
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 stylus_dir = theme_dir.join("stylus/book.styl");
if !execs::cmd("stylus").arg(stylus_dir)
.arg("--out")
.arg(theme_dir)
.arg("--use")
.arg("nib")
.status()?
.success()
{
bail!("Stylus encountered an error");
}
}
Ok(())
}
quick_main!(run);

View File

@@ -15,12 +15,9 @@ main() {
;;
esac
# This will slow down the build, but is necessary to not run out of disk space
cargo clean
cargo rustc --bin mdbook --target $TARGET --release -- -C lto
cp target/$TARGET/release/mdbook $stage/
cp target/release/mdbook $stage/
cd $stage
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *

50
ci/github_pages.sh Normal file
View File

@@ -0,0 +1,50 @@
#!/bin/bash
# Deploys the `book-example` to GitHub Pages
set -ex
# Only run this on the master branch for stable
if [ "$TRAVIS_PULL_REQUEST" != "false" ] ||
[ "$TRAVIS_BRANCH" != "master" ] ||
[ "$TRAVIS_RUST_VERSION" != "stable" ] ||
[ "$TARGET" != "x86_64-unknown-linux-gnu" ]; then
exit 0
fi
# Make sure we have the css dependencies
npm install -g stylus nib
NC='\033[39m'
CYAN='\033[36m'
GREEN='\033[32m'
rev=$(git rev-parse --short HEAD)
echo -e "${CYAN}Running cargo doc${NC}"
cargo doc --features regenerate-css > /dev/null
echo -e "${CYAN}Running mdbook build${NC}"
cargo run -- build book-example/
echo -e "${CYAN}Copying book to target/doc${NC}"
cp -R book-example/book/* target/doc/
cd target/doc
echo -e "${CYAN}Initializing Git${NC}"
git init
git config user.name "Michael Bryan"
git config user.email "michaelfbryan@gmail.com"
git remote add upstream "https://$GH_TOKEN@github.com/rust-lang-nursery/mdBook.git"
git fetch upstream --quiet
git reset upstream/gh-pages --quiet
touch .
echo -e "${CYAN}Pushing changes to gh-pages${NC}"
git add -A .
git commit -m "rebuild pages at ${rev}" --quiet
git push -q upstream HEAD:gh-pages --quiet
echo -e "${GREEN}Deployed docs to GitHub Pages${NC}"

View File

@@ -3,15 +3,15 @@ extern crate mdbook;
extern crate pulldown_cmark;
extern crate pulldown_cmark_to_cmark;
use mdbook::book::{Book, BookItem, Chapter};
use mdbook::errors::{Error, Result};
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use mdbook::MDBook;
use mdbook::book::{Book, BookItem, Chapter};
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
use pulldown_cmark::{Event, Parser, Tag};
use pulldown_cmark_to_cmark::fmt::cmark;
use std::env::{args, args_os};
use std::ffi::OsString;
use std::env::{args, args_os};
use std::process;
struct Deemphasize;

View File

@@ -1,3 +0,0 @@
sign-commit = true
push-remote = "origin"
tag-prefix = "v"

7
rustfmt.toml Normal file
View File

@@ -0,0 +1,7 @@
array_layout = "Visual"
chain_indent = "Visual"
fn_args_layout = "Visual"
fn_call_style = "Visual"
format_strings = true
generics_indent = "Visual"

View File

@@ -1,21 +1,21 @@
use std::path::PathBuf;
use clap::{App, ArgMatches, SubCommand};
use mdbook::errors::Result;
use mdbook::MDBook;
use mdbook::errors::Result;
use {get_book_dir, open};
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("build")
.about("Builds a book from its markdown files")
.about("Build the book from the markdown files")
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
"-d, --dest-dir=[dest-dir] 'The output directory for your book{n}(Defaults to ./book \
when omitted)'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'",
)
.arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
}
// Build command implementation
@@ -24,7 +24,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let mut book = MDBook::load(&book_dir)?;
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into();
book.config.build.build_dir = PathBuf::from(dest_dir);
}
book.build()?;

View File

@@ -1,20 +1,17 @@
use clap::{App, ArgMatches, SubCommand};
use get_book_dir;
use mdbook::errors::*;
use mdbook::MDBook;
use std::fs;
use std::path::PathBuf;
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::*;
use get_book_dir;
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("clean")
.about("Deletes a built book")
.about("Delete built book")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
"-d, --dest-dir=[dest-dir] 'The directory of built book{n}(Defaults to ./book when \
omitted)'",
)
}
@@ -24,7 +21,7 @@ pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
let book = MDBook::load(&book_dir)?;
let dir_to_remove = match args.value_of("dest-dir") {
Some(dest_dir) => dest_dir.into(),
Some(dest_dir) => PathBuf::from(dest_dir),
None => book.root.join(&book.config.build.build_dir),
};
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;

View File

@@ -1,21 +1,22 @@
use clap::{App, ArgMatches, SubCommand};
use get_book_dir;
use mdbook::config;
use mdbook::errors::Result;
use mdbook::MDBook;
use std::io;
use std::io::Write;
use std::process::Command;
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::Result;
use mdbook::utils;
use mdbook::config;
use get_book_dir;
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("init")
.about("Creates the boilerplate structure and files for a new book")
.about("Create boilerplate structure and files in the directory")
// the {n} denotes a newline which will properly aligned in all help messages
.arg_from_usage("[dir] 'Directory to create the book in{n}\
(Defaults to the Current Directory when omitted)'")
.arg_from_usage("[dir] 'A directory for your book{n}(Defaults to Current Directory \
when omitted)'")
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
.arg_from_usage("--force 'Skips confirmation prompts'")
.arg_from_usage("--force 'skip confirmation prompts'")
}
// Init command implementation

View File

@@ -8,55 +8,61 @@ extern crate log;
extern crate mdbook;
extern crate open;
use chrono::Local;
use clap::{App, AppSettings, ArgMatches};
use env_logger::Builder;
use log::LevelFilter;
use mdbook::utils;
use std::env;
use std::ffi::OsStr;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::io::Write;
use clap::{App, AppSettings, ArgMatches};
use chrono::Local;
use log::LevelFilter;
use env_logger::Builder;
use mdbook::utils;
mod cmd;
pub mod build;
pub mod clean;
pub mod init;
pub mod test;
#[cfg(feature = "serve")]
pub mod serve;
#[cfg(feature = "watch")]
pub mod watch;
const NAME: &'static str = "mdBook";
const VERSION: &'static str = concat!("v", crate_version!());
const NAME: &'static str = "mdbook";
fn main() {
init_logger();
// Create a list of valid arguments and sub-commands
let app = App::new(NAME)
.about("Creates a book from markdown files")
.author("Mathieu David <mathieudavid@mathieudavid.org>")
.version(VERSION)
.setting(AppSettings::GlobalVersion)
.setting(AppSettings::ArgRequiredElseHelp)
.after_help(
"For more information about a specific command, try `mdbook <command> --help`\n\
The source code for mdBook is available at: https://github.com/rust-lang-nursery/mdBook",
)
.subcommand(cmd::init::make_subcommand())
.subcommand(cmd::build::make_subcommand())
.subcommand(cmd::test::make_subcommand())
.subcommand(cmd::clean::make_subcommand());
.about("Create a book in form of a static website from markdown files")
.author("Mathieu David <mathieudavid@mathieudavid.org>")
// Get the version from our Cargo.toml using clap's crate_version!() macro
.version(concat!("v",crate_version!()))
.setting(AppSettings::ArgRequiredElseHelp)
.after_help("For more information about a specific command, \
try `mdbook <command> --help`\n\
Source code for mdbook available \
at: https://github.com/rust-lang-nursery/mdBook")
.subcommand(init::make_subcommand())
.subcommand(build::make_subcommand())
.subcommand(test::make_subcommand())
.subcommand(clean::make_subcommand());
#[cfg(feature = "watch")]
let app = app.subcommand(cmd::watch::make_subcommand());
let app = app.subcommand(watch::make_subcommand());
#[cfg(feature = "serve")]
let app = app.subcommand(cmd::serve::make_subcommand());
let app = app.subcommand(serve::make_subcommand());
// Check which subcomamnd the user ran...
let res = match app.get_matches().subcommand() {
("init", Some(sub_matches)) => cmd::init::execute(sub_matches),
("build", Some(sub_matches)) => cmd::build::execute(sub_matches),
("clean", Some(sub_matches)) => cmd::clean::execute(sub_matches),
("init", Some(sub_matches)) => init::execute(sub_matches),
("build", Some(sub_matches)) => build::execute(sub_matches),
("clean", Some(sub_matches)) => clean::execute(sub_matches),
#[cfg(feature = "watch")]
("watch", Some(sub_matches)) => cmd::watch::execute(sub_matches),
("watch", Some(sub_matches)) => watch::execute(sub_matches),
#[cfg(feature = "serve")]
("serve", Some(sub_matches)) => cmd::serve::execute(sub_matches),
("test", Some(sub_matches)) => cmd::test::execute(sub_matches),
("serve", Some(sub_matches)) => serve::execute(sub_matches),
("test", Some(sub_matches)) => test::execute(sub_matches),
(_, _) => unreachable!(),
};

View File

@@ -2,69 +2,39 @@ extern crate iron;
extern crate staticfile;
extern crate ws;
use self::iron::{
status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set,
};
#[cfg(feature = "watch")]
use super::watch;
use clap::{App, Arg, ArgMatches, SubCommand};
use mdbook::errors::*;
use mdbook::utils;
use mdbook::MDBook;
use std;
use self::iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response,
Set};
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::utils;
use mdbook::errors::*;
use {get_book_dir, open};
#[cfg(feature = "watch")]
use watch;
struct ErrorRecover;
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("serve")
.about("Serves a book at http://localhost:3000, and rebuilds it on changes")
.about("Serve the book at http://localhost:3000. Rebuild and reload on change.")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'",
)
.arg_from_usage("-p, --port=[port] 'Use another port{n}(Defaults to 3000)'")
.arg_from_usage(
"-w, --websocket-port=[ws-port] 'Use another port for the websocket connection \
(livereload){n}(Defaults to 3001)'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
"-i, --interface=[interface] 'Interface to listen on{n}(Defaults to localhost)'",
)
.arg(
Arg::with_name("hostname")
.short("n")
.long("hostname")
.takes_value(true)
.default_value("localhost")
.empty_values(false)
.help("Hostname to listen on for HTTP connections"),
.arg_from_usage(
"-a, --address=[address] 'Address that the browser can reach the websocket server \
from{n}(Defaults to the interface address)'",
)
.arg(
Arg::with_name("port")
.short("p")
.long("port")
.takes_value(true)
.default_value("3000")
.empty_values(false)
.help("Port to use for HTTP connections"),
)
.arg(
Arg::with_name("websocket-hostname")
.long("websocket-hostname")
.takes_value(true)
.empty_values(false)
.help(
"Hostname to connect to for WebSockets connections (Defaults to the HTTP hostname)",
),
)
.arg(
Arg::with_name("websocket-port")
.short("w")
.long("websocket-port")
.takes_value(true)
.default_value("3001")
.empty_values(false)
.help("Port to use for WebSockets livereload connections"),
)
.arg_from_usage("-o, --open 'Opens the book server in a web browser'")
.arg_from_usage("-o, --open 'Open the book server in a web browser'")
}
// Watch command implementation
@@ -72,23 +42,19 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?;
let port = args.value_of("port").unwrap();
let ws_port = args.value_of("websocket-port").unwrap();
let hostname = args.value_of("hostname").unwrap();
let public_address = args.value_of("websocket-address").unwrap_or(hostname);
let port = args.value_of("port").unwrap_or("3000");
let ws_port = args.value_of("websocket-port").unwrap_or("3001");
let interface = args.value_of("interface").unwrap_or("localhost");
let public_address = args.value_of("address").unwrap_or(interface);
let open_browser = args.is_present("open");
let address = format!("{}:{}", hostname, port);
let ws_address = format!("{}:{}", hostname, ws_port);
let address = format!("{}:{}", interface, port);
let ws_address = format!("{}:{}", interface, ws_port);
let livereload_url = format!("ws://{}:{}", public_address, ws_port);
book.config
.set("output.html.livereload-url", &livereload_url)?;
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into();
}
book.build()?;
let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html")));
@@ -120,8 +86,10 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
// FIXME: This area is really ugly because we need to re-set livereload :(
let livereload_url = livereload_url.clone();
let result = MDBook::load(&book_dir)
.and_then(|mut b| {
.and_then(move |mut b| {
b.config.set("output.html.livereload-url", &livereload_url)?;
Ok(b)
})

26
src/bin/test.rs Normal file
View File

@@ -0,0 +1,26 @@
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::errors::Result;
use get_book_dir;
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("test")
.about("Test that code samples compile")
.arg_from_usage(
"-L, --library-path [DIR]... 'directory to add to crate search path'",
)
}
// test command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
let library_paths: Vec<&str> = args.values_of("library-path")
.map(|v| v.collect())
.unwrap_or_default();
let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?;
book.test(library_paths)?;
Ok(())
}

View File

@@ -1,28 +1,23 @@
extern crate notify;
use self::notify::Watcher;
use clap::{App, ArgMatches, SubCommand};
use mdbook::errors::Result;
use mdbook::utils;
use mdbook::MDBook;
use std::path::Path;
use std::sync::mpsc::channel;
use self::notify::Watcher;
use std::time::Duration;
use std::sync::mpsc::channel;
use clap::{App, ArgMatches, SubCommand};
use mdbook::MDBook;
use mdbook::utils;
use mdbook::errors::Result;
use {get_book_dir, open};
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("watch")
.about("Watches a book's files and rebuilds it on changes")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
)
.about("Watch the files for changes")
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
.arg_from_usage(
"[dir] 'A directory for your book{n}(Defaults to Current Directory when omitted)'",
)
}
// Watch command implementation
@@ -53,8 +48,8 @@ pub fn trigger_on_change<F>(book: &MDBook, closure: F)
where
F: Fn(&Path, &Path),
{
use self::notify::DebouncedEvent::*;
use self::notify::RecursiveMode::*;
use self::notify::DebouncedEvent::*;
// Create a channel to receive the events.
let (tx, rx) = channel();

View File

@@ -1,8 +1,8 @@
use std::collections::VecDeque;
use std::fmt::{self, Display, Formatter};
use std::path::{Path, PathBuf};
use std::collections::VecDeque;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
use config::BuildConfig;
@@ -297,8 +297,8 @@ impl Display for Chapter {
#[cfg(test)]
mod tests {
use super::*;
use tempfile::{TempDir, Builder as TempFileBuilder};
use std::io::Write;
use tempfile::{Builder as TempFileBuilder, TempDir};
const DUMMY_SRC: &'static str = "
# Dummy Chapter
@@ -404,12 +404,14 @@ And here is some \
..Default::default()
};
let should_be = Book {
sections: vec![BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
path: PathBuf::from("chapter_1.md"),
..Default::default()
})],
sections: vec![
BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
path: PathBuf::from("chapter_1.md"),
..Default::default()
}),
],
..Default::default()
};
@@ -533,11 +535,13 @@ And here is some \
fn cant_load_chapters_with_an_empty_path() {
let (_, temp) = dummy_link();
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("Empty"),
location: PathBuf::from(""),
..Default::default()
})],
numbered_chapters: vec![
SummaryItem::Link(Link {
name: String::from("Empty"),
location: PathBuf::from(""),
..Default::default()
}),
],
..Default::default()
};
@@ -552,11 +556,13 @@ And here is some \
fs::create_dir(&dir).unwrap();
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("nested"),
location: dir,
..Default::default()
})],
numbered_chapters: vec![
SummaryItem::Link(Link {
name: String::from("nested"),
location: dir,
..Default::default()
}),
],
..Default::default()
};

View File

@@ -1,12 +1,12 @@
use std::fs::{self, File};
use std::io::Write;
use std::path::PathBuf;
use std::io::Write;
use toml;
use super::MDBook;
use config::Config;
use errors::*;
use super::MDBook;
use theme;
use errors::*;
/// A helper for setting up a new book and its directory structure.
#[derive(Debug, Clone, PartialEq)]
@@ -127,20 +127,8 @@ impl BookBuilder {
let mut index = File::create(themedir.join("index.hbs"))?;
index.write_all(theme::INDEX)?;
let cssdir = themedir.join("css");
fs::create_dir(&cssdir)?;
let mut general_css = File::create(cssdir.join("general.css"))?;
general_css.write_all(theme::GENERAL_CSS)?;
let mut chrome_css = File::create(cssdir.join("chrome.css"))?;
chrome_css.write_all(theme::CHROME_CSS)?;
let mut print_css = File::create(cssdir.join("print.css"))?;
print_css.write_all(theme::PRINT_CSS)?;
let mut variables_css = File::create(cssdir.join("variables.css"))?;
variables_css.write_all(theme::VARIABLES_CSS)?;
let mut css = File::create(themedir.join("book.css"))?;
css.write_all(theme::CSS)?;
let mut favicon = File::create(themedir.join("favicon.png"))?;
favicon.write_all(theme::FAVICON)?;

View File

@@ -5,24 +5,24 @@
//!
//! [1]: ../index.html
mod summary;
mod book;
mod init;
mod summary;
pub use self::book::{load_book, Book, BookItem, BookItems, Chapter};
pub use self::init::BookBuilder;
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
pub use self::init::BookBuilder;
use std::io::Write;
use std::path::PathBuf;
use std::io::Write;
use std::process::Command;
use tempfile::Builder as TempFileBuilder;
use toml::Value;
use errors::*;
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext};
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
use utils;
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
use preprocess::{LinkPreprocessor, Preprocessor, PreprocessorContext};
use errors::*;
use config::Config;
@@ -213,13 +213,11 @@ impl MDBook {
.flat_map(|x| vec![x.0, x.1])
.collect();
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
let temp_dir = TempFileBuilder::new().prefix("mdbook").tempdir()?;
let preprocess_context = PreprocessorContext::new(self.root.clone(), self.config.clone());
LinkPreprocessor::new().run(&preprocess_context, &mut self.book)?;
// Index Preprocessor is disabled so that chapter paths continue to point to the
// actual markdown files.
for item in self.iter() {
if let BookItem::Chapter(ref ch) = *item {
@@ -324,19 +322,15 @@ fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
}
fn default_preprocessors() -> Vec<Box<Preprocessor>> {
vec![
Box::new(LinkPreprocessor::new()),
Box::new(IndexPreprocessor::new()),
]
vec![Box::new(LinkPreprocessor::new())]
}
/// Look at the `MDBook` and try to figure out what preprocessors to run.
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
let preprocess_list = match config.build.preprocess {
Some(ref p) => p,
// If no preprocessor field is set, default to the LinkPreprocessor and
// IndexPreprocessor. This allows you to disable default preprocessors
// by setting "preprocess" to an empty list.
// If no preprocessor field is set, default to the LinkPreprocessor. This allows you
// to disable the LinkPreprocessor by setting "preprocess" to an empty list.
None => return Ok(default_preprocessors()),
};
@@ -345,7 +339,6 @@ fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
for key in preprocess_list {
match key.as_ref() {
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
_ => bail!("{:?} is not a recognised preprocessor", key),
}
}
@@ -410,7 +403,7 @@ mod tests {
}
#[test]
fn config_defaults_to_link_and_index_preprocessor_if_not_set() {
fn config_defaults_to_link_preprocessor_if_not_set() {
let cfg = Config::default();
// make sure we haven't got anything in the `output` table
@@ -419,9 +412,8 @@ mod tests {
let got = determine_preprocessors(&cfg);
assert!(got.is_ok());
assert_eq!(got.as_ref().unwrap().len(), 2);
assert_eq!(got.as_ref().unwrap().len(), 1);
assert_eq!(got.as_ref().unwrap()[0].name(), "links");
assert_eq!(got.as_ref().unwrap()[1].name(), "index");
}
#[test]

View File

@@ -1,10 +1,10 @@
use errors::*;
use memchr::{self, Memchr};
use pulldown_cmark::{self, Event, Tag};
use std::fmt::{self, Display, Formatter};
use std::iter::FromIterator;
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
use memchr::{self, Memchr};
use pulldown_cmark::{self, Event, Tag};
use errors::*;
/// Parse the text from a `SUMMARY.md` file into a sort of "recipe" to be
/// used when loading a book from disk.
@@ -164,34 +164,33 @@ struct SummaryParser<'a> {
/// use pattern matching and you won't get errors because `take_while()`
/// moves `$stream` out of self.
macro_rules! collect_events {
($stream:expr,start $delimiter:pat) => {
($stream:expr, start $delimiter:pat) => {
collect_events!($stream, Event::Start($delimiter))
};
($stream:expr,end $delimiter:pat) => {
($stream:expr, end $delimiter:pat) => {
collect_events!($stream, Event::End($delimiter))
};
($stream:expr, $delimiter:pat) => {{
let mut events = Vec::new();
($stream:expr, $delimiter:pat) => {
{
let mut events = Vec::new();
loop {
let event = $stream.next();
trace!("Next event: {:?}", event);
loop {
let event = $stream.next();
trace!("Next event: {:?}", event);
match event {
Some($delimiter) => break,
Some(other) => events.push(other),
None => {
debug!(
"Reached end of stream without finding the closing pattern, {}",
stringify!($delimiter)
);
break;
match event {
Some($delimiter) => break,
Some(other) => events.push(other),
None => {
debug!("Reached end of stream without finding the closing pattern, {}", stringify!($delimiter));
break;
}
}
}
}
events
}};
events
}
}
}
impl<'a> SummaryParser<'a> {
@@ -660,12 +659,14 @@ mod tests {
name: String::from("First"),
location: PathBuf::from("./first.md"),
number: Some(SectionNumber(vec![1])),
nested_items: vec![SummaryItem::Link(Link {
name: String::from("Nested"),
location: PathBuf::from("./nested.md"),
number: Some(SectionNumber(vec![1, 1])),
nested_items: Vec::new(),
})],
nested_items: vec![
SummaryItem::Link(Link {
name: String::from("Nested"),
location: PathBuf::from("./nested.md"),
number: Some(SectionNumber(vec![1, 1])),
nested_items: Vec::new(),
}),
],
}),
SummaryItem::Link(Link {
name: String::from("Second"),

View File

@@ -1,10 +0,0 @@
//! Subcommand modules for the `mdbook` binary.
pub mod build;
pub mod clean;
pub mod init;
#[cfg(feature = "serve")]
pub mod serve;
pub mod test;
#[cfg(feature = "watch")]
pub mod watch;

View File

@@ -1,44 +0,0 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use get_book_dir;
use mdbook::errors::Result;
use mdbook::MDBook;
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
SubCommand::with_name("test")
.about("Tests that a book's Rust code samples compile")
.arg_from_usage(
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
)
.arg_from_usage(
"[dir] 'Root directory for the book{n}\
(Defaults to the Current Directory when omitted)'",
)
.arg(Arg::with_name("library-path")
.short("L")
.long("library-path")
.value_name("dir")
.takes_value(true)
.require_delimiter(true)
.multiple(true)
.empty_values(false)
.help("A comma-separated list of directories to add to {n}the crate search path when building tests"))
}
// test command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
let library_paths: Vec<&str> = args.values_of("library-path")
.map(|v| v.collect())
.unwrap_or_default();
let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?;
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into();
}
book.test(library_paths)?;
Ok(())
}

View File

@@ -50,17 +50,17 @@
#![deny(missing_docs)]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json;
use std::env;
use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use toml::value::Table;
use std::env;
use toml::{self, Value};
use toml_query::delete::TomlValueDeleteExt;
use toml_query::insert::TomlValueInsertExt;
use toml::value::Table;
use toml_query::read::TomlValueReadExt;
use toml_query::insert::TomlValueInsertExt;
use toml_query::delete::TomlValueDeleteExt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json;
use errors::*;
@@ -217,10 +217,9 @@ impl Config {
// figure out what try_into() deserializes to.
macro_rules! get_and_insert {
($table:expr, $key:expr => $out:expr) => {
let got = $table
.as_table_mut()
.and_then(|t| t.remove($key))
.and_then(|v| v.try_into().ok());
let got = $table.as_table_mut()
.and_then(|t| t.remove($key))
.and_then(|v| v.try_into().ok());
if let Some(value) = got {
$out = value;
}
@@ -464,11 +463,9 @@ impl Default for Playpen {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Search {
/// Enable the search feature. Default: `true`.
pub enable: bool,
/// Maximum number of visible results. Default: `30`.
pub limit_results: u32,
/// The number of words used for a search result teaser. Default: `30`.
/// The number of words used for a search result teaser. Default: `30`,
pub teaser_word_count: u32,
/// Define the logical link between multiple search words.
/// If true, all search words must appear in each result. Default: `true`.
@@ -497,7 +494,6 @@ impl Default for Search {
fn default() -> Search {
// Please update the documentation of `Search` when changing values!
Search {
enable: true,
limit_results: 30,
teaser_word_count: 30,
use_boolean_and: false,

View File

@@ -107,17 +107,17 @@ extern crate toml_query;
#[macro_use]
extern crate pretty_assertions;
pub mod preprocess;
pub mod book;
pub mod config;
pub mod preprocess;
pub mod renderer;
pub mod theme;
pub mod utils;
pub use book::BookItem;
pub use book::MDBook;
pub use config::Config;
pub use book::BookItem;
pub use renderer::Renderer;
pub use config::Config;
/// The error types used through out this crate.
pub mod errors {

View File

@@ -1,97 +0,0 @@
use regex::Regex;
use std::path::Path;
use errors::*;
use super::{Preprocessor, PreprocessorContext};
use book::{Book, BookItem};
/// A preprocessor for converting file name `README.md` to `index.md` since
/// `README.md` is the de facto index file in a markdown-based documentation.
pub struct IndexPreprocessor;
impl IndexPreprocessor {
/// Create a new `IndexPreprocessor`.
pub fn new() -> Self {
IndexPreprocessor
}
}
impl Preprocessor for IndexPreprocessor {
fn name(&self) -> &str {
"index"
}
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()> {
let source_dir = ctx.root.join(&ctx.config.book.src);
book.for_each_mut(|section: &mut BookItem| {
if let BookItem::Chapter(ref mut ch) = *section {
if is_readme_file(&ch.path) {
let index_md = source_dir.join(ch.path.with_file_name("index.md"));
if index_md.exists() {
warn_readme_name_conflict(&ch.path, &index_md);
}
ch.path.set_file_name("index.md");
}
}
});
Ok(())
}
}
fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
let file_name = readme_path.as_ref().file_name().unwrap_or_default();
let parent_dir = index_path.as_ref().parent().unwrap_or(index_path.as_ref());
warn!(
"It seems that there are both {:?} and index.md under \"{}\".",
file_name,
parent_dir.display()
);
warn!(
"mdbook converts {:?} into index.html by default. It may cause",
file_name
);
warn!("unexpected behavior if putting both files under the same directory.");
warn!("To solve the warning, try to rearrange the book structure or disable");
warn!("\"index\" preprocessor to stop the conversion.");
}
fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
lazy_static! {
static ref RE: Regex = Regex::new(r"(?i)^readme$").unwrap();
}
RE.is_match(
path.as_ref()
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn file_stem_exactly_matches_readme_case_insensitively() {
let path = "path/to/Readme.md";
assert!(is_readme_file(path));
let path = "path/to/README.md";
assert!(is_readme_file(path));
let path = "path/to/rEaDmE.md";
assert!(is_readme_file(path));
let path = "path/to/README.markdown";
assert!(is_readme_file(path));
let path = "path/to/README";
assert!(is_readme_file(path));
let path = "path/to/README-README.md";
assert!(!is_readme_file(path));
}
}

View File

@@ -1,15 +1,14 @@
use errors::*;
use regex::{CaptureMatches, Captures, Regex};
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
use std::path::{Path, PathBuf};
use regex::{CaptureMatches, Captures, Regex};
use utils::fs::file_to_string;
use utils::take_lines;
use errors::*;
use super::{Preprocessor, PreprocessorContext};
use book::{Book, BookItem};
const ESCAPE_CHAR: char = '\\';
const MAX_LINK_NESTED_DEPTH: usize = 10;
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
/// helpers in a chapter.
@@ -32,13 +31,12 @@ impl Preprocessor for LinkPreprocessor {
book.for_each_mut(|section: &mut BookItem| {
if let BookItem::Chapter(ref mut ch) = *section {
let base = ch
.path
let base = ch.path
.parent()
.map(|dir| src_dir.join(dir))
.expect("All book items have a parent");
let content = replace_all(&ch.content, base, &ch.path, 0);
let content = replace_all(&ch.content, base);
ch.content = content;
}
});
@@ -47,16 +45,11 @@ impl Preprocessor for LinkPreprocessor {
}
}
fn replace_all<P1, P2>(s: &str, path: P1, source: P2, depth: usize) -> String
where
P1: AsRef<Path>,
P2: AsRef<Path>,
{
fn replace_all<P: AsRef<Path>>(s: &str, path: P) -> String {
// When replacing one thing in a string by something with a different length,
// the indices after that will not correspond,
// we therefore have to store the difference to correct this
let path = path.as_ref();
let source = source.as_ref();
let mut previous_end_index = 0;
let mut replaced = String::new();
@@ -65,18 +58,7 @@ where
match playpen.render_with_path(&path) {
Ok(new_content) => {
if depth < MAX_LINK_NESTED_DEPTH {
if let Some(rel_path) = playpen.link.relative_path(path) {
replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1));
} else {
replaced.push_str(&new_content);
}
} else {
error!(
"Stack depth exceeded in {}. Check for cyclic includes",
source.display()
);
}
replaced.push_str(&new_content);
previous_end_index = playpen.end_index;
}
Err(e) => {
@@ -102,27 +84,6 @@ enum LinkType<'a> {
Playpen(PathBuf, Vec<&'a str>),
}
impl<'a> LinkType<'a> {
fn relative_path<P: AsRef<Path>>(self, base: P) -> Option<PathBuf> {
let base = base.as_ref();
match self {
LinkType::Escaped => None,
LinkType::IncludeRange(p, _) => Some(return_relative_path(base, &p)),
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)),
LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)),
LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)),
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
}
}
}
fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
base.as_ref()
.join(relative)
.parent()
.expect("Included file should not be /")
.to_path_buf()
}
fn parse_include_path(path: &str) -> LinkType<'static> {
let mut parts = path.split(':');
let path = parts.next().unwrap().into();
@@ -130,7 +91,7 @@ fn parse_include_path(path: &str) -> LinkType<'static> {
let start = parts
.next()
.and_then(|s| s.parse::<usize>().ok())
.map(|val| val.saturating_sub(1));
.map(|val| val.checked_sub(1).unwrap_or(0));
let end = parts.next();
let has_end = end.is_some();
let end = end.and_then(|s| s.parse::<usize>().ok());
@@ -249,16 +210,15 @@ fn find_links(contents: &str) -> LinkIter {
// lazily compute following regex
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
lazy_static! {
static ref RE: Regex = Regex::new(
r"(?x) # insignificant whitespace mode
\\\{\{\#.*\}\} # match escaped link
| # or
\{\{\s* # link opening parens and whitespace
\#([a-zA-Z0-9]+) # link type
\s+ # separating whitespace
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
\s*\}\} # whitespace and link closing parens"
).unwrap();
static ref RE: Regex = Regex::new(r"(?x) # insignificant whitespace mode
\\\{\{\#.*\}\} # match escaped link
| # or
\{\{\s* # link opening parens and whitespace
\#([a-zA-Z0-9]+) # link type
\s+ # separating whitespace
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
\s*\}\} # whitespace and link closing parens
").unwrap();
}
LinkIter(RE.captures_iter(contents))
}
@@ -267,21 +227,6 @@ fn find_links(contents: &str) -> LinkIter {
mod tests {
use super::*;
#[test]
fn test_replace_all_escaped() {
let start = r"
Some text over here.
```hbs
\{{#include file.rs}} << an escaped link!
```";
let end = r"
Some text over here.
```hbs
{{#include file.rs}} << an escaped link!
```";
assert_eq!(replace_all(start, "", "", 0), end);
}
#[test]
fn test_find_links_no_link() {
let s = "Some random text without link...";
@@ -343,12 +288,14 @@ mod tests {
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![Link {
start_index: 22,
end_index: 48,
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
link_text: "{{#include file.rs:10:20}}",
}]
vec![
Link {
start_index: 22,
end_index: 48,
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
link_text: "{{#include file.rs:10:20}}",
},
]
);
}
@@ -359,12 +306,14 @@ mod tests {
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![Link {
start_index: 22,
end_index: 45,
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
link_text: "{{#include file.rs:10}}",
}]
vec![
Link {
start_index: 22,
end_index: 45,
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
link_text: "{{#include file.rs:10}}",
},
]
);
}
@@ -375,12 +324,14 @@ mod tests {
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![Link {
start_index: 22,
end_index: 46,
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
link_text: "{{#include file.rs:10:}}",
}]
vec![
Link {
start_index: 22,
end_index: 46,
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
link_text: "{{#include file.rs:10:}}",
},
]
);
}
@@ -391,12 +342,14 @@ mod tests {
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![Link {
start_index: 22,
end_index: 46,
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
link_text: "{{#include file.rs::20}}",
}]
vec![
Link {
start_index: 22,
end_index: 46,
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
link_text: "{{#include file.rs::20}}",
},
]
);
}
@@ -407,12 +360,14 @@ mod tests {
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![Link {
start_index: 22,
end_index: 44,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_text: "{{#include file.rs::}}",
}]
vec![
Link {
start_index: 22,
end_index: 44,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_text: "{{#include file.rs::}}",
},
]
);
}
@@ -423,12 +378,14 @@ mod tests {
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![Link {
start_index: 22,
end_index: 42,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_text: "{{#include file.rs}}",
}]
vec![
Link {
start_index: 22,
end_index: 42,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_text: "{{#include file.rs}}",
},
]
);
}
@@ -441,12 +398,14 @@ mod tests {
assert_eq!(
res,
vec![Link {
start_index: 38,
end_index: 68,
link: LinkType::Escaped,
link_text: "\\{{#playpen file.rs editable}}",
}]
vec![
Link {
start_index: 38,
end_index: 68,
link: LinkType::Escaped,
link_text: "\\{{#playpen file.rs editable}}",
},
]
);
}

View File

@@ -1,9 +1,7 @@
//! Book preprocessing.
pub use self::index::IndexPreprocessor;
pub use self::links::LinkPreprocessor;
mod index;
mod links;
use book::Book;
@@ -12,7 +10,7 @@ use errors::*;
use std::path::PathBuf;
/// Extra information for a `Preprocessor` to give them more context when
/// Extra information for a `Preprocessor` to give them more context when
/// processing a book.
pub struct PreprocessorContext {
/// The location of the book directory on disk.
@@ -28,7 +26,7 @@ impl PreprocessorContext {
}
}
/// An operation which is run immediately after loading a book into memory and
/// An operation which is run immediately after loading a book into memory and
/// before it gets rendered.
pub trait Preprocessor {
/// Get the `Preprocessor`'s name.
@@ -37,4 +35,4 @@ pub trait Preprocessor {
/// Run this `Preprocessor`, allowing it to update the book before it is
/// given to a renderer.
fn run(&self, ctx: &PreprocessorContext, book: &mut Book) -> Result<()>;
}
}

View File

@@ -1,14 +1,15 @@
use book::{Book, BookItem};
use book::{Book, BookItem, Chapter};
use config::{Config, HtmlConfig, Playpen};
use errors::*;
use renderer::html_handlebars::helpers;
use renderer::{RenderContext, Renderer};
use theme::{self, playpen_editor, Theme};
use renderer::html_handlebars::helpers;
use theme::{self, Theme, playpen_editor};
use utils;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fs;
use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
use handlebars::Handlebars;
@@ -40,7 +41,11 @@ impl HtmlHandlebars {
let path = ch.path
.to_str()
.chain_err(|| "Could not convert path to str")?;
let filepath = Path::new(&ch.path).with_extension("html");
let filepath = Path::new(&ch.path)
.with_extension("html");
let filepathstr = filepath.to_str()
.chain_err(|| "Could not convert HTML path to str")?;
let filepathstr = utils::fs::normalize_path(filepathstr);
// "print.html" is used for the print page.
if ch.path == Path::new("print.md") {
@@ -51,9 +56,9 @@ impl HtmlHandlebars {
let title: String;
{
let book_title = ctx.data
.get("book_title")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
.get("book_title")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
title = ch.name.clone() + " - " + book_title;
}
@@ -61,33 +66,25 @@ impl HtmlHandlebars {
ctx.data.insert("content".to_owned(), json!(content));
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
ctx.data.insert("title".to_owned(), json!(title));
ctx.data.insert(
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(&ch.path)),
);
ctx.data.insert("path_to_root".to_owned(),
json!(utils::fs::path_to_root(&ch.path)));
// Render the handlebars template with the data
debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?;
let rendered = self.post_process(rendered, &ctx.html_config.playpen);
let rendered = self.post_process(
rendered,
&filepathstr,
&ctx.html_config.playpen,
);
// Write to file
debug!("Creating {}", filepath.display());
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
debug!("Creating {}", filepathstr);
utils::fs::write_file(&ctx.destination, &filepath, &rendered.into_bytes())?;
if ctx.is_index {
ctx.data.insert("path".to_owned(), json!("index.html"));
ctx.data.insert("path_to_root".to_owned(), json!(""));
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
let rendered_index =
self.post_process(rendered_index, &ctx.html_config.playpen);
debug!("Creating index.html from {}", path);
utils::fs::write_file(
&ctx.destination,
"index.html",
rendered_index.as_bytes(),
)?;
self.render_index(ch, &ctx.destination)?;
}
}
_ => {}
@@ -96,9 +93,41 @@ impl HtmlHandlebars {
Ok(())
}
/// Create an index.html from the first element in SUMMARY.md
fn render_index(&self, ch: &Chapter, destination: &Path) -> Result<()> {
debug!("index.html");
let mut content = String::new();
File::open(destination.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...
content = content.lines()
.filter(|line| !line.contains("<base href="))
.collect::<Vec<&str>>()
.join("\n");
utils::fs::write_file(destination, "index.html", content.as_bytes())?;
debug!(
"Creating index.html from {} ✓",
destination.join(&ch.path.with_extension("html")).display()
);
Ok(())
}
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String {
let rendered = build_header_links(&rendered);
fn post_process(&self,
rendered: String,
filepath: &str,
playpen_config: &Playpen)
-> String {
let rendered = build_header_links(&rendered, filepath);
let rendered = fix_anchor_links(&rendered, filepath);
let rendered = fix_code_blocks(&rendered);
let rendered = add_playpen_pre(&rendered, playpen_config);
@@ -113,17 +142,8 @@ impl HtmlHandlebars {
) -> Result<()> {
use utils::fs::write_file;
write_file(
destination,
".nojekyll",
b"This file makes sure that Github Pages doesn't process mdBook's output.",
)?;
write_file(destination, "book.js", &theme.js)?;
write_file(destination, "css/general.css", &theme.general_css)?;
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
write_file(destination, "css/print.css", &theme.print_css)?;
write_file(destination, "css/variables.css", &theme.variables_css)?;
write_file(destination, "book.css", &theme.css)?;
write_file(destination, "favicon.png", &theme.favicon)?;
write_file(destination, "highlight.css", &theme.highlight_css)?;
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
@@ -132,37 +152,37 @@ impl HtmlHandlebars {
write_file(destination, "clipboard.min.js", &theme.clipboard_js)?;
write_file(
destination,
"FontAwesome/css/font-awesome.css",
"_FontAwesome/css/font-awesome.css",
theme::FONT_AWESOME,
)?;
write_file(
destination,
"FontAwesome/fonts/fontawesome-webfont.eot",
"_FontAwesome/fonts/fontawesome-webfont.eot",
theme::FONT_AWESOME_EOT,
)?;
write_file(
destination,
"FontAwesome/fonts/fontawesome-webfont.svg",
"_FontAwesome/fonts/fontawesome-webfont.svg",
theme::FONT_AWESOME_SVG,
)?;
write_file(
destination,
"FontAwesome/fonts/fontawesome-webfont.ttf",
"_FontAwesome/fonts/fontawesome-webfont.ttf",
theme::FONT_AWESOME_TTF,
)?;
write_file(
destination,
"FontAwesome/fonts/fontawesome-webfont.woff",
"_FontAwesome/fonts/fontawesome-webfont.woff",
theme::FONT_AWESOME_WOFF,
)?;
write_file(
destination,
"FontAwesome/fonts/fontawesome-webfont.woff2",
"_FontAwesome/fonts/fontawesome-webfont.woff2",
theme::FONT_AWESOME_WOFF2,
)?;
write_file(
destination,
"FontAwesome/fonts/FontAwesome.ttf",
"_FontAwesome/fonts/FontAwesome.ttf",
theme::FONT_AWESOME_TTF,
)?;
@@ -175,8 +195,7 @@ impl HtmlHandlebars {
write_file(destination, "ace.js", playpen_editor::ACE_JS)?;
write_file(destination, "mode-rust.js", playpen_editor::MODE_RUST_JS)?;
write_file(destination, "theme-dawn.js", playpen_editor::THEME_DAWN_JS)?;
write_file(
destination,
write_file(destination,
"theme-tomorrow_night.js",
playpen_editor::THEME_TOMORROW_NIGHT_JS,
)?;
@@ -186,42 +205,28 @@ impl HtmlHandlebars {
}
/// Update the context with data for this file
fn configure_print_version(
&self,
data: &mut serde_json::Map<String, serde_json::Value>,
print_content: &str,
) {
fn configure_print_version(&self,
data: &mut serde_json::Map<String, serde_json::Value>,
print_content: &str) {
// Make sure that the Print chapter does not display the title from
// the last rendered chapter by removing it from its context
data.remove("title");
data.insert("is_print".to_owned(), json!(true));
data.insert("path".to_owned(), json!("print.md"));
data.insert("content".to_owned(), json!(print_content));
data.insert(
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(Path::new("print.md"))),
);
data.insert("path_to_root".to_owned(),
json!(utils::fs::path_to_root(Path::new("print.md"))));
}
fn register_hbs_helpers(&self, handlebars: &mut Handlebars, html_config: &HtmlConfig) {
handlebars.register_helper(
"toc",
Box::new(helpers::toc::RenderToc {
no_section_label: html_config.no_section_label,
}),
);
handlebars.register_helper("toc", Box::new(helpers::toc::RenderToc {no_section_label: html_config.no_section_label}));
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
handlebars.register_helper("next", Box::new(helpers::navigation::next));
}
/// Copy across any additional CSS and JavaScript files which the book
/// has been configured to use.
fn copy_additional_css_and_js(
&self,
html: &HtmlConfig,
root: &Path,
destination: &Path,
) -> Result<()> {
fn copy_additional_css_and_js(&self, html: &HtmlConfig, root: &Path, destination: &Path) -> Result<()> {
let custom_files = html.additional_css.iter().chain(html.additional_js.iter());
debug!("Copying additional CSS and JS");
@@ -252,25 +257,6 @@ impl HtmlHandlebars {
}
}
// TODO(mattico): Remove some time after the 0.1.8 release
fn maybe_wrong_theme_dir(dir: &Path) -> Result<bool> {
fn entry_is_maybe_book_file(entry: fs::DirEntry) -> Result<bool> {
Ok(entry.file_type()?.is_file()
&& entry.path().extension().map_or(false, |ext| ext == "md"))
}
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
if entry_is_maybe_book_file(entry?).unwrap_or(false) {
return Ok(false);
}
}
Ok(true)
} else {
Ok(false)
}
}
impl Renderer for HtmlHandlebars {
fn name(&self) -> &str {
"html"
@@ -287,19 +273,9 @@ impl Renderer for HtmlHandlebars {
let theme_dir = match html_config.theme {
Some(ref theme) => theme.to_path_buf(),
None => ctx.root.join("theme"),
None => src_dir.join("theme"),
};
if html_config.theme.is_none()
&& maybe_wrong_theme_dir(&src_dir.join("theme")).unwrap_or(false)
{
warn!(
"Previous versions of mdBook erroneously accepted `./src/theme` as an automatic \
theme directory"
);
warn!("Please move your theme files to `./theme` for them to continue being used");
}
let theme = theme::Theme::new(theme_dir);
debug!("Register the index handlebars template");
@@ -328,7 +304,9 @@ impl Renderer for HtmlHandlebars {
is_index: is_index,
html_config: html_config.clone(),
};
self.render_item(item, ctx, &mut print_content)?;
self.render_item(item,
ctx,
&mut print_content)?;
is_index = false;
}
@@ -342,9 +320,11 @@ impl Renderer for HtmlHandlebars {
debug!("Render template");
let rendered = handlebars.render("index", &data)?;
let rendered = self.post_process(rendered, &html_config.playpen);
let rendered = self.post_process(rendered,
"print.html",
&html_config.playpen);
utils::fs::write_file(&destination, "print.html", rendered.as_bytes())?;
utils::fs::write_file(&destination, "print.html", &rendered.into_bytes())?;
debug!("Creating print.html ✓");
debug!("Copy static files");
@@ -355,12 +335,7 @@ impl Renderer for HtmlHandlebars {
// Render search index
#[cfg(feature = "search")]
{
let search = html_config.search.unwrap_or_default();
if search.enable {
super::search::create_files(&search, &destination, &book)?;
}
}
super::search::create_files(&html_config.search.unwrap_or_default(), &destination, &book)?;
// Copy all remaining files
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
@@ -369,25 +344,14 @@ impl Renderer for HtmlHandlebars {
}
}
fn make_data(
root: &Path,
book: &Book,
config: &Config,
html_config: &HtmlConfig,
) -> Result<serde_json::Map<String, serde_json::Value>> {
fn make_data(root: &Path, book: &Book, config: &Config, html_config: &HtmlConfig) -> Result<serde_json::Map<String, serde_json::Value>> {
trace!("make_data");
let html = config.html_config().unwrap_or_default();
let mut data = serde_json::Map::new();
data.insert("language".to_owned(), json!("en"));
data.insert(
"book_title".to_owned(),
json!(config.book.title.clone().unwrap_or_default()),
);
data.insert(
"description".to_owned(),
json!(config.book.description.clone().unwrap_or_default()),
);
data.insert("book_title".to_owned(), json!(config.book.title.clone().unwrap_or_default()));
data.insert("description".to_owned(), json!(config.book.description.clone().unwrap_or_default()));
data.insert("favicon".to_owned(), json!("favicon.png"));
if let Some(ref livereload) = html_config.livereload_url {
data.insert("livereload".to_owned(), json!(livereload));
@@ -407,8 +371,13 @@ fn make_data(
let mut css = Vec::new();
for style in &html.additional_css {
match style.strip_prefix(root) {
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
Err(_) => css.push(style.to_str().expect("Could not convert to str")),
Ok(p) => {
css.push(p.to_str().expect("Could not convert to str"))
},
Err(_) => {
css.push(style.to_str()
.expect("Could not convert to str"))
}
}
}
data.insert("additional_css".to_owned(), json!(css));
@@ -420,13 +389,12 @@ fn make_data(
for script in &html.additional_js {
match script.strip_prefix(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"),
),
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));
@@ -438,20 +406,16 @@ fn make_data(
let search = html_config.search.clone();
if cfg!(feature = "search") {
let search = search.unwrap_or_default();
data.insert("search_enabled".to_owned(), json!(search.enable));
data.insert(
"search_js".to_owned(),
json!(search.enable && search.copy_js),
);
data.insert("search_enabled".to_owned(), json!(true));
if search.unwrap_or_default().copy_js {
data.insert("search_js".to_owned(), json!(true));
}
} else if search.is_some() {
warn!("mdBook compiled without search support, ignoring `output.html.search` table");
warn!(
"please reinstall with `cargo install mdbook --force --features search`to use the \
search feature"
)
warn!("please reinstall with `cargo install mdbook --force --features search`\
to use the search feature")
}
let mut chapters = vec![];
for item in book.iter() {
@@ -486,28 +450,26 @@ fn make_data(
/// Goes through the rendered HTML, making sure all header tags are wrapped in
/// an anchor so people can link to sections directly.
fn build_header_links(html: &str) -> String {
fn build_header_links(html: &str, filepath: &str) -> String {
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
let mut id_counter = HashMap::new();
regex
.replace_all(html, |caps: &Captures| {
let level = caps[1]
.parse()
.expect("Regex should ensure we only ever get numbers here");
regex.replace_all(html, |caps: &Captures| {
let level = caps[1].parse()
.expect("Regex should ensure we only ever get numbers here");
wrap_header_with_link(level, &caps[2], &mut id_counter)
})
.into_owned()
wrap_header_with_link(level, &caps[2], &mut id_counter, filepath)
})
.into_owned()
}
/// Wraps a single header tag with a link, making sure each tag gets its own
/// unique ID by appending an auto-incremented number (if necessary).
fn wrap_header_with_link(
level: usize,
content: &str,
id_counter: &mut HashMap<String, usize>,
) -> String {
fn wrap_header_with_link(level: usize,
content: &str,
id_counter: &mut HashMap<String, usize>,
filepath: &str)
-> String {
let raw_id = utils::id_from_content(content);
let id_count = id_counter.entry(raw_id.clone()).or_insert(0);
@@ -520,13 +482,34 @@ fn wrap_header_with_link(
*id_count += 1;
format!(
r##"<a class="header" href="#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
r##"<a class="header" href="{filepath}#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
level = level,
id = id,
text = content
text = content,
filepath = filepath
)
}
// anchors to the same page (href="#anchor") do not work because of
// <base href="../"> pointing to the root folder. This function *fixes*
// that in a very inelegant way
fn fix_anchor_links(html: &str, filepath: &str) -> String {
let regex = Regex::new(r##"<a([^>]+)href="#([^"]+)"([^>]*)>"##).unwrap();
regex.replace_all(html, |caps: &Captures| {
let before = &caps[1];
let anchor = &caps[2];
let after = &caps[3];
format!("<a{before}href=\"{filepath}#{anchor}\"{after}>",
before = before,
filepath = filepath,
anchor = anchor,
after = after)
})
.into_owned()
}
// The rust book uses annotations for rustdoc to test code snippets,
// like the following:
// ```rust,should_panic
@@ -537,57 +520,51 @@ fn wrap_header_with_link(
// This function replaces all commas by spaces in the code block classes
fn fix_code_blocks(html: &str) -> String {
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
regex
.replace_all(html, |caps: &Captures| {
let before = &caps[1];
let classes = &caps[2].replace(",", " ");
let after = &caps[3];
regex.replace_all(html, |caps: &Captures| {
let before = &caps[1];
let classes = &caps[2].replace(",", " ");
let after = &caps[3];
format!(
r#"<code{before}class="{classes}"{after}>"#,
format!(r#"<code{before}class="{classes}"{after}>"#,
before = before,
classes = classes,
after = after
)
})
.into_owned()
after = after)
}).into_owned()
}
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
regex
.replace_all(html, |caps: &Captures| {
let text = &caps[1];
let classes = &caps[2];
let code = &caps[3];
regex.replace_all(html, |caps: &Captures| {
let text = &caps[1];
let classes = &caps[2];
let code = &caps[3];
if (classes.contains("language-rust")
&& !classes.contains("ignore")
&& !classes.contains("noplaypen"))
|| classes.contains("mdbook-runnable")
if (classes.contains("language-rust") && !classes.contains("ignore")) ||
classes.contains("mdbook-runnable")
{
// wrap the contents in an external pre block
if playpen_config.editable && classes.contains("editable") ||
text.contains("fn main") || text.contains("quick_main!")
{
// wrap the contents in an external pre block
if playpen_config.editable && classes.contains("editable")
|| text.contains("fn main")
|| text.contains("quick_main!")
{
format!("<pre class=\"playpen\">{}</pre>", text)
} else {
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!(
"<pre class=\"playpen\"><code class=\"{}\">\n# \
#![allow(unused_variables)]\n{}#fn main() {{\n{}#}}</code></pre>",
classes, attrs, code
)
}
format!("<pre class=\"playpen\">{}</pre>", text)
} else {
// not language-rust, so no-op
text.to_owned()
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!("<pre class=\"playpen\"><code class=\"{}\">\n# \
#![allow(unused_variables)]\n\
{}#fn main() {{\n\
{}\
#}}</code></pre>",
classes,
attrs,
code)
}
})
.into_owned()
} else {
// not language-rust, so no-op
text.to_owned()
}
}).into_owned()
}
fn partition_source(s: &str) -> (String, String) {
@@ -628,32 +605,37 @@ mod tests {
let inputs = vec![
(
"blah blah <h1>Foo</h1>",
r##"blah blah <a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
r##"blah blah <a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
),
(
"<h1>Foo</h1>",
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a>"##,
),
(
"<h3>Foo^bar</h3>",
r##"<a class="header" href="#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
r##"<a class="header" href="./some_chapter/some_section.html#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
),
(
"<h4></h4>",
r##"<a class="header" href="#" id=""><h4></h4></a>"##,
r##"<a class="header" href="./some_chapter/some_section.html#" id=""><h4></h4></a>"##,
),
(
"<h4><em>Hï</em></h4>",
r##"<a class="header" href="#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
r##"<a class="header" href="./some_chapter/some_section.html#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
),
(
"<h1>Foo</h1><h3>Foo</h3>",
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a><a class="header" href="#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
r##"<a class="header" href="./some_chapter/some_section.html#foo" id="foo"><h1>Foo</h1></a><a class="header" href="./some_chapter/some_section.html#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
),
];
for (src, should_be) in inputs {
let got = build_header_links(&src);
let filepath = "./some_chapter/some_section.html";
let got = build_header_links(&src, filepath);
assert_eq!(got, should_be);
// This is redundant for most cases
let got = fix_anchor_links(&got, filepath);
assert_eq!(got, should_be);
}
}

View File

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

View File

@@ -1,10 +1,8 @@
use std::collections::BTreeMap;
use std::path::Path;
use std::collections::BTreeMap;
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
use serde_json;
use utils;
use handlebars::{Context, Handlebars, Helper, RenderContext, RenderError, Renderable};
type StringMap = BTreeMap<String, String>;
@@ -89,15 +87,6 @@ fn render(
trace!("Creating BTreeMap to inject in context");
let mut context = BTreeMap::new();
let base_path = rc.evaluate_absolute("path", false)?
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
context.insert(
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(&base_path)),
);
chapter
.get("name")

View File

@@ -1,11 +1,9 @@
use std::collections::BTreeMap;
use std::path::Path;
use std::collections::BTreeMap;
use utils;
use serde_json;
use handlebars::{Handlebars, Helper, HelperDef, RenderContext, RenderError};
use pulldown_cmark::{html, Event, Parser, Tag};
use serde_json;
// Handlebars helper to construct TOC
#[derive(Clone, Copy)]
@@ -79,8 +77,6 @@ impl HelperDef for RenderToc {
.replace("\\", "/");
// Add link
rc.writer
.write_all(&utils::fs::path_to_root(&current).as_bytes())?;
rc.writer.write_all(tmp.as_bytes())?;
rc.writer.write_all(b"\"")?;

View File

@@ -5,38 +5,29 @@ use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::path::Path;
use self::elasticlunr::Index;
use pulldown_cmark::*;
use serde_json;
use self::elasticlunr::Index;
use book::{Book, BookItem};
use config::Search;
use errors::*;
use theme::searcher;
use utils;
use theme::searcher;
/// Creates all files required for search.
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
let mut index = Index::new(&["title", "body", "breadcrumbs"]);
let mut doc_urls = Vec::with_capacity(book.sections.len());
for item in book.iter() {
render_item(&mut index, &search_config, &mut doc_urls, item)?;
render_item(&mut index, &search_config, item)?;
}
let index = write_to_json(index, &search_config, doc_urls)?;
let index = write_to_js(index, &search_config)?;
debug!("Writing search index ✓");
if index.len() > 10_000_000 {
warn!("searchindex.json is very large ({} bytes)", index.len());
}
if search_config.copy_js {
utils::fs::write_file(destination, "searchindex.json", index.as_bytes())?;
utils::fs::write_file(
destination,
"searchindex.js",
format!("window.search = {};", index).as_bytes(),
)?;
utils::fs::write_file(destination, "searchindex.js", index.as_bytes())?;
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
utils::fs::write_file(destination, "elasticlunr.min.js", searcher::ELASTICLUNR_JS)?;
@@ -47,22 +38,18 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
}
/// Uses the given arguments to construct a search document, then inserts it to the given index.
fn add_doc(
fn add_doc<'a>(
index: &mut Index,
doc_urls: &mut Vec<String>,
anchor_base: &str,
anchor_base: &'a str,
section_id: &Option<String>,
items: &[&str],
) {
let url = if let &Some(ref id) = section_id {
Cow::Owned(format!("{}#{}", anchor_base, id))
let doc_ref: Cow<'a, str> = if let &Some(ref id) = section_id {
format!("{}#{}", anchor_base, id).into()
} else {
Cow::Borrowed(anchor_base)
anchor_base.into()
};
let url = utils::collapse_whitespace(url.trim());
let doc_ref = doc_urls.len().to_string();
doc_urls.push(url.into());
let doc_ref = utils::collapse_whitespace(doc_ref.trim());
let items = items.iter().map(|&x| utils::collapse_whitespace(x.trim()));
index.add_doc(&doc_ref, items);
}
@@ -71,7 +58,6 @@ fn add_doc(
fn render_item(
index: &mut Index,
search_config: &Search,
doc_urls: &mut Vec<String>,
item: &BookItem,
) -> Result<()> {
let chapter = match item {
@@ -106,7 +92,6 @@ fn render_item(
// Write the data to the index, and clear it for the next section
add_doc(
index,
doc_urls,
&anchor_base,
&section_id,
&[&heading, &body, &breadcrumbs.join(" » ")],
@@ -159,7 +144,6 @@ fn render_item(
// Make sure the last section is added to the index
add_doc(
index,
doc_urls,
&anchor_base,
&section_id,
&[&heading, &body, &breadcrumbs.join(" » ")],
@@ -169,9 +153,12 @@ fn render_item(
Ok(())
}
fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
/// Exports the index and search options to a JS script which stores the index in `window.search`.
/// Using a JS script is a workaround for CORS in `file://` URIs. It also removes the need for
/// downloading/parsing JSON in JS.
fn write_to_js(index: Index, search_config: &Search) -> Result<String> {
use std::collections::BTreeMap;
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
#[derive(Serialize)]
struct ResultsOptions {
@@ -182,11 +169,9 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) ->
#[derive(Serialize)]
struct SearchindexJson {
/// The options used for displaying search results
results_options: ResultsOptions,
resultsoptions: ResultsOptions,
/// The searchoptions for elasticlunr.js
search_options: SearchOptions,
/// Used to lookup a document's URL from an integer document ref.
doc_urls: Vec<String>,
searchoptions: SearchOptions,
/// The index for elasticlunr.js
index: elasticlunr::Index,
}
@@ -200,7 +185,7 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) ->
opt.boost = Some(search_config.boost_hierarchy);
fields.insert("breadcrumbs".into(), opt);
let search_options = SearchOptions {
let searchoptions = SearchOptions {
bool: if search_config.use_boolean_and {
SearchBool::And
} else {
@@ -210,24 +195,19 @@ fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) ->
fields,
};
let results_options = ResultsOptions {
let resultsoptions = ResultsOptions {
limit_results: search_config.limit_results,
teaser_word_count: search_config.teaser_word_count,
};
let json_contents = SearchindexJson {
results_options,
search_options,
doc_urls,
resultsoptions,
searchoptions,
index,
};
// By converting to serde_json::Value as an intermediary, we use a
// BTreeMap internally and can force a stable ordering of map keys.
let json_contents = serde_json::to_value(&json_contents)?;
let json_contents = serde_json::to_string(&json_contents)?;
Ok(json_contents)
Ok(format!("window.search = {};", json_contents))
}
fn clean_html(html: &str) -> String {

View File

@@ -15,16 +15,16 @@ pub use self::html_handlebars::HtmlHandlebars;
mod html_handlebars;
use serde_json;
use shlex::Shlex;
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use serde_json;
use shlex::Shlex;
use book::Book;
use config::Config;
use errors::*;
use config::Config;
use book::Book;
const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -162,21 +162,17 @@ impl Renderer for CmdRenderer {
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.current_dir(&ctx.destination)
.spawn()
{
Ok(c) => c,
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
warn!(
"The command wasn't found, is the \"{}\" backend installed?",
self.name
);
warn!("\tCommand: {}", self.cmd);
return Ok(());
}
Err(e) => {
return Err(e).chain_err(|| "Unable to start the backend")?;
}
};
.spawn() {
Ok(c) => c,
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
warn!("The command wasn't found, is the \"{}\" backend installed?", self.name);
warn!("\tCommand: {}", self.cmd);
return Ok(());
}
Err(e) => {
return Err(e).chain_err(|| "Unable to start the backend")?;
}
};
{
let mut stdin = child.stdin.take().expect("Child has stdin");

View File

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 348 KiB

1454
src/theme/book.css Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,3 @@
"use strict";
// Fix back button cache problem
window.onunload = function () { };
@@ -18,30 +16,13 @@ function playpen_text(playpen) {
(function codeSnippets() {
// Hide Rust code lines prepended with a specific character
var hiding_character = "#";
function fetch_with_timeout(url, options, timeout = 6000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
]);
}
var playpens = Array.from(document.querySelectorAll(".playpen"));
if (playpens.length > 0) {
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
})
.then(response => response.json())
.then(response => {
// get list of crates available in the rust playground
let playground_crates = response.crates.map(item => item["id"]);
playpens.forEach(block => handle_crate_list_update(block, playground_crates));
});
}
var request = fetch("https://play.rust-lang.org/meta/crates", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
});
function handle_crate_list_update(playpen_block, playground_crates) {
// update the play buttons after receiving the response
@@ -74,7 +55,6 @@ function playpen_text(playpen) {
var txt = playpen_text(pre_block);
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
var snippet_crates = [];
var item;
while (item = re.exec(txt)) {
snippet_crates.push(item[1]);
}
@@ -103,28 +83,32 @@ function playpen_text(playpen) {
let text = playpen_text(code_block);
var params = {
version: "stable",
optimize: "0",
code: text
};
channel: "stable",
mode: "debug",
crateType: "bin",
tests: false,
code: text,
}
if (text.indexOf("#![feature") !== -1) {
params.version = "nightly";
params.channel = "nightly";
}
result_block.innerText = "Running...";
fetch_with_timeout("https://play.rust-lang.org/evaluate.json", {
var request = fetch("https://play.rust-lang.org/execute", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
body: JSON.stringify(params)
})
.then(response => response.json())
.then(response => result_block.innerText = response.result)
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
});
request
.then(function (response) { return response.json(); })
.then(function (response) { result_block.innerText = response.success ? response.stdout : response.stderr; })
.catch(function (error) { result_block.innerText = "Playground communication" + error.message; });
}
// Syntax highlighting Configuration
@@ -163,11 +147,9 @@ function playpen_text(playpen) {
var lines = code_block.innerHTML.split("\n");
var first_non_hidden_line = false;
var lines_hidden = false;
var trimmed_line = "";
for (var n = 0; n < lines.length; n++) {
trimmed_line = lines[n].trim();
if (trimmed_line[0] == hiding_character && trimmed_line[1] != hiding_character) {
if (lines[n].trim()[0] == hiding_character) {
if (first_non_hidden_line) {
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)# ?/, "$1") + "</span>";
}
@@ -182,9 +164,6 @@ function playpen_text(playpen) {
else {
first_non_hidden_line = true;
}
if (trimmed_line[0] == hiding_character && trimmed_line[1] == hiding_character) {
lines[n] = lines[n].replace("##", "#")
}
}
code_block.innerHTML = lines.join("");
@@ -196,7 +175,7 @@ function playpen_text(playpen) {
buttons.innerHTML = "<button class=\"fa fa-expand\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
// add expand button
pre_block.insertBefore(buttons, pre_block.firstChild);
pre_block.prepend(buttons);
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
if (e.target.classList.contains('fa-expand')) {
@@ -234,7 +213,7 @@ function playpen_text(playpen) {
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
pre_block.prepend(buttons);
}
var clipButton = document.createElement('button');
@@ -243,7 +222,7 @@ function playpen_text(playpen) {
clipButton.setAttribute('aria-label', clipButton.title);
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
buttons.insertBefore(clipButton, buttons.firstChild);
buttons.prepend(clipButton);
}
});
@@ -254,7 +233,7 @@ function playpen_text(playpen) {
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
pre_block.prepend(buttons);
}
var runCodeButton = document.createElement('button');
@@ -269,8 +248,8 @@ function playpen_text(playpen) {
copyCodeClipboardButton.title = 'Copy to clipboard';
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
buttons.insertBefore(runCodeButton, buttons.firstChild);
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
buttons.prepend(runCodeButton);
buttons.prepend(copyCodeClipboardButton);
runCodeButton.addEventListener('click', function (e) {
run_rust_code(pre_block);
@@ -283,7 +262,7 @@ function playpen_text(playpen) {
undoChangesButton.title = 'Undo changes';
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
buttons.insertBefore(undoChangesButton, buttons.firstChild);
buttons.prepend(undoChangesButton);
undoChangesButton.addEventListener('click', function () {
let editor = window.ace.edit(code_block);
@@ -292,6 +271,17 @@ function playpen_text(playpen) {
});
}
});
request
.then(function (response) { return response.json(); })
.then(function (response) {
// get list of crates available in the rust playground
let playground_crates = response.crates.map(function (item) { return item["id"]; });
Array.from(document.querySelectorAll(".playpen")).forEach(function (block) {
handle_crate_list_update(block, playground_crates);
});
});
})();
(function themes() {
@@ -300,9 +290,9 @@ function playpen_text(playpen) {
var themePopup = document.getElementById('theme-list');
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
var stylesheets = {
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
highlight: document.querySelector("[href$='highlight.css']"),
ayuHighlight: document.querySelector("[href='ayu-highlight.css']"),
tomorrowNight: document.querySelector("[href='tomorrow-night.css']"),
highlight: document.querySelector("[href='highlight.css']"),
};
function showThemes() {
@@ -383,7 +373,7 @@ function playpen_text(playpen) {
themePopup.addEventListener('focusout', function(e) {
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) {
if (!!e.relatedTarget && !themePopup.contains(e.relatedTarget)) {
hideThemes();
}
});

View File

@@ -1,417 +0,0 @@
/* CSS for UI elements (a.k.a. chrome) */
@import 'variables.css';
::-webkit-scrollbar {
background: var(--bg);
}
::-webkit-scrollbar-thumb {
background: var(--scrollbar);
}
#searchresults a,
.content a:link,
a:visited,
a > .hljs {
color: var(--links);
}
/* Menu Bar */
#menu-bar {
position: -webkit-sticky;
position: sticky;
top: 0;
z-index: 101;
margin: auto calc(0px - var(--page-padding));
}
#menu-bar > #menu-bar-sticky-container {
display: flex;
flex-wrap: wrap;
background-color: var(--bg);
border-bottom-color: var(--bg);
border-bottom-width: 1px;
border-bottom-style: solid;
}
.js #menu-bar > #menu-bar-sticky-container {
transition: transform 0.3s;
}
#menu-bar.bordered > #menu-bar-sticky-container {
border-bottom-color: var(--table-border-color);
}
#menu-bar i, #menu-bar .icon-button {
position: relative;
padding: 0 8px;
z-index: 10;
line-height: 50px;
cursor: pointer;
transition: color 0.5s;
}
@media only screen and (max-width: 420px) {
#menu-bar i, #menu-bar .icon-button {
padding: 0 5px;
}
}
.icon-button {
border: none;
background: none;
padding: 0;
color: inherit;
}
.icon-button i {
margin: 0;
}
#print-button {
margin: 0 15px;
}
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
transform: translateY(-60px);
}
.left-buttons {
display: flex;
margin: 0 5px;
}
.no-js .left-buttons {
display: none;
}
.menu-title {
display: inline-block;
font-weight: 200;
font-size: 20px;
line-height: 50px;
text-align: center;
margin: 0;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.js .menu-title {
cursor: pointer;
}
.menu-bar,
.menu-bar:visited,
.nav-chapters,
.nav-chapters:visited,
.mobile-nav-chapters,
.mobile-nav-chapters:visited,
.menu-bar .icon-button,
.menu-bar a i {
color: var(--icons);
}
.menu-bar i:hover,
.menu-bar .icon-button:hover,
.nav-chapters:hover,
.mobile-nav-chapters i:hover {
color: var(--icons-hover);
}
/* Nav Icons */
.nav-chapters {
font-size: 2.5em;
text-align: center;
text-decoration: none;
position: fixed;
top: 50px; /* Height of menu-bar */
bottom: 0;
margin: 0;
max-width: 150px;
min-width: 90px;
display: flex;
justify-content: center;
align-content: center;
flex-direction: column;
transition: color 0.5s;
}
.nav-chapters:hover { text-decoration: none; }
.nav-wrapper {
margin-top: 50px;
display: none;
}
.mobile-nav-chapters {
font-size: 2.5em;
text-align: center;
text-decoration: none;
width: 90px;
border-radius: 5px;
background-color: var(--sidebar-bg);
}
.previous {
float: left;
}
.next {
float: right;
right: var(--page-padding);
}
@media only screen and (max-width: 1080px) {
.nav-wide-wrapper { display: none; }
.nav-wrapper { display: block; }
}
@media only screen and (max-width: 1380px) {
.sidebar-visible .nav-wide-wrapper { display: none; }
.sidebar-visible .nav-wrapper { display: block; }
}
/* Inline code */
:not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
border-radius: 3px;
color: var(--inline-code-color);
}
a:hover > .hljs {
text-decoration: underline;
}
pre {
position: relative;
}
pre > .buttons {
position: absolute;
z-index: 100;
right: 5px;
top: 5px;
color: var(--sidebar-fg);
cursor: pointer;
}
pre > .buttons :hover {
color: var(--sidebar-active);
}
pre > .buttons i {
margin-left: 8px;
}
pre > .buttons button {
color: inherit;
background: transparent;
border: none;
cursor: inherit;
}
pre > .result {
margin-top: 10px;
}
/* Search */
#searchresults a {
text-decoration: none;
}
mark {
border-radius: 2px;
padding: 0 3px 1px 3px;
margin: 0 -3px -1px -3px;
background-color: var(--search-mark-bg);
transition: background-color 300ms linear;
cursor: pointer;
}
mark.fade-out {
background-color: rgba(0,0,0,0) !important;
cursor: auto;
}
.searchbar-outer {
margin-left: auto;
margin-right: auto;
max-width: var(--content-max-width);
}
#searchbar {
width: 100%;
margin: 5px auto 0px auto;
padding: 10px 16px;
transition: box-shadow 300ms ease-in-out;
border: 1px solid var(--searchbar-border-color);
border-radius: 3px;
background-color: var(--searchbar-bg);
color: var(--searchbar-fg);
}
#searchbar:focus,
#searchbar.active {
box-shadow: 0 0 3px var(--searchbar-shadow-color);
}
.searchresults-header {
font-weight: bold;
font-size: 1em;
padding: 18px 0 0 5px;
color: var(--searchresults-header-fg);
}
.searchresults-outer {
margin-left: auto;
margin-right: auto;
max-width: var(--content-max-width);
border-bottom: 1px dashed var(--searchresults-border-color);
}
ul#searchresults {
list-style: none;
padding-left: 20px;
}
ul#searchresults li {
margin: 10px 0px;
padding: 2px;
border-radius: 2px;
}
ul#searchresults li.focus {
background-color: var(--searchresults-li-bg);
}
ul#searchresults span.teaser {
display: block;
clear: both;
margin: 5px 0 0 20px;
font-size: 0.8em;
}
ul#searchresults span.teaser em {
font-weight: bold;
font-style: normal;
}
/* Sidebar */
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: var(--sidebar-width);
overflow-y: auto;
padding: 10px 10px;
font-size: 0.875em;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
overscroll-behavior-y: contain;
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
}
.js .sidebar {
transition: transform 0.3s; /* Animation: slide away */
}
.sidebar code {
line-height: 2em;
}
.sidebar-hidden .sidebar {
transform: translateX(calc(0px - var(--sidebar-width)));
}
.sidebar::-webkit-scrollbar {
background: var(--sidebar-bg);
}
.sidebar::-webkit-scrollbar-thumb {
background: var(--scrollbar);
}
.sidebar-visible .page-wrapper {
transform: translateX(var(--sidebar-width));
}
@media only screen and (min-width: 620px) {
.sidebar-visible .page-wrapper {
transform: none;
margin-left: var(--sidebar-width);
}
}
.chapter {
list-style: none outside none;
padding-left: 0;
line-height: 2.2em;
}
.chapter li {
color: var(--sidebar-non-existant);
}
.chapter li a {
color: var(--sidebar-fg);
display: block;
padding: 0;
text-decoration: none;
}
.chapter li a:hover { text-decoration: none }
.chapter li .active,
a:hover {
/* Animate color change */
color: var(--sidebar-active);
}
.spacer {
width: 100%;
height: 3px;
margin: 5px 0px;
}
.chapter .spacer {
background-color: var(--sidebar-spacer);
}
@media (-moz-touch-enabled: 1), (pointer: coarse) {
.chapter li a { padding: 5px 0; }
.spacer { margin: 10px 0; }
}
.section {
list-style: none outside none;
padding-left: 20px;
line-height: 1.9em;
}
/* Theme Menu Popup */
.theme-popup {
position: absolute;
left: 10px;
top: 50px;
z-index: 1000;
border-radius: 4px;
font-size: 0.7em;
color: var(--fg);
background: var(--theme-popup-bg);
border: 1px solid var(--theme-popup-border);
margin: 0;
padding: 0;
list-style: none;
display: none;
}
.theme-popup .default {
color: var(--icons);
}
.theme-popup .theme {
width: 100%;
border: 0;
margin: 0;
padding: 2px 10px;
line-height: 25px;
white-space: nowrap;
text-align: left;
cursor: pointer;
color: inherit;
background: inherit;
font-size: inherit;
}
.theme-popup .theme:hover {
background-color: var(--theme-hover);
}
.theme-popup .theme:hover:first-child,
.theme-popup .theme:hover:last-child {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}

View File

@@ -1,144 +0,0 @@
/* Base styles and content styles */
@import 'variables.css';
html {
font-family: "Open Sans", sans-serif;
color: var(--fg);
background-color: var(--bg);
text-size-adjust: none;
}
body {
margin: 0;
font-size: 1rem;
overflow-x: hidden;
}
code {
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
}
.left { float: left; }
.right { float: right; }
.hidden { display: none; }
.play-button.hidden { display: none; }
h2, h3 { margin-top: 2.5em; }
h4, h5 { margin-top: 2em; }
.header + .header h3,
.header + .header h4,
.header + .header h5 {
margin-top: 1em;
}
a.header:target h1:before,
a.header:target h2:before,
a.header:target h3:before,
a.header:target h4:before {
display: inline-block;
content: "»";
margin-left: -30px;
width: 30px;
}
.page {
outline: 0;
padding: 0 var(--page-padding);
}
.page-wrapper {
box-sizing: border-box;
}
.js .page-wrapper {
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
}
.content {
overflow-y: auto;
padding: 0 15px;
padding-bottom: 50px;
}
.content main {
margin-left: auto;
margin-right: auto;
max-width: var(--content-max-width);
}
.content a { text-decoration: none; }
.content a:hover { text-decoration: underline; }
.content img { max-width: 100%; }
.content .header:link,
.content .header:visited {
color: var(--fg);
}
.content .header:link,
.content .header:visited:hover {
text-decoration: none;
}
table {
margin: 0 auto;
border-collapse: collapse;
}
table td {
padding: 3px 20px;
border: 1px var(--table-border-color) solid;
}
table thead {
background: var(--table-header-bg);
}
table thead td {
font-weight: 700;
border: none;
}
table thead tr {
border: 1px var(--table-header-bg) solid;
}
/* Alternate background colors for rows */
table tbody tr:nth-child(2n) {
background: var(--table-alternate-bg);
}
blockquote {
margin: 20px 0;
padding: 0 20px;
color: var(--fg);
background-color: var(--quote-bg);
border-top: .1em solid var(--quote-border);
border-bottom: .1em solid var(--quote-border);
}
:not(.footnote-definition) + .footnote-definition,
.footnote-definition + :not(.footnote-definition) {
margin-top: 2em;
}
.footnote-definition {
font-size: 0.9em;
margin: 0.5em 0;
}
.footnote-definition p {
display: inline;
}
.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;
}

View File

@@ -1,54 +0,0 @@
#sidebar,
#menu-bar,
.nav-chapters,
.mobile-nav-chapters {
display: none;
}
#page-wrapper.page-wrapper {
transform: none;
margin-left: 0px;
overflow-y: initial;
}
#content {
max-width: none;
margin: 0;
padding: 0;
}
.page {
overflow-y: initial;
}
code {
background-color: #666666;
border-radius: 5px;
/* Force background to be printed in Chrome */
-webkit-print-color-adjust: exact;
}
pre > .buttons {
z-index: 2;
}
a, a:visited, a:active, a:hover {
color: #4183c4;
text-decoration: none;
}
h1, h2, h3, h4, h5, h6 {
page-break-inside: avoid;
page-break-after: avoid;
}
pre, code {
page-break-inside: avoid;
white-space: pre-wrap;
}
.fa {
display: none !important;
}

View File

@@ -1,210 +0,0 @@
/* Globals */
:root {
--sidebar-width: 300px;
--page-padding: 15px;
--content-max-width: 750px;
}
/* Themes */
.ayu {
--bg: #0f1419;
--fg: #c5c5c5;
--sidebar-bg: #14191f;
--sidebar-fg: #c8c9db;
--sidebar-non-existant: #5c6773;
--sidebar-active: #ffb454;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--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(var(--quote-bg), 5%);
--table-border-color: lighten(var(--bg), 5%);
--table-header-bg: lighten(var(--bg), 20%);
--table-alternate-bg: lighten(var(--bg), 3%);
--searchbar-border-color: #848484;
--searchbar-bg: #424242;
--searchbar-fg: #fff;
--searchbar-shadow-color: #d4c89f;
--searchresults-header-fg: #666;
--searchresults-border-color: #888;
--searchresults-li-bg: #252932;
--search-mark-bg: #e3b171;
}
.coal {
--bg: #141617;
--fg: #98a3ad;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
--sidebar-non-existant: #505254;
--sidebar-active: #3473ad;
--sidebar-spacer: #393939;
--scrollbar: var(--sidebar-fg);
--icons: #43484d;
--icons-hover: #b3c0cc;
--links: #2b79a2;
--inline-code-color: #c5c8c6;;
--theme-popup-bg: #141617;
--theme-popup-border: #43484d;
--theme-hover: #1f2124;
--quote-bg: #242637;
--quote-border: lighten(var(--quote-bg), 5%);
--table-border-color: lighten(var(--bg), 5%);
--table-header-bg: lighten(var(--bg), 20%);
--table-alternate-bg: lighten(var(--bg), 3%);
--searchbar-border-color: #aaa;
--searchbar-bg: #b7b7b7;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #98a3ad;
--searchresults-li-bg: #2b2b2f;
--search-mark-bg: #355c7d;
}
.light {
--bg: #ffffff;
--fg: #333333;
--sidebar-bg: #fafafa;
--sidebar-fg: #364149;
--sidebar-non-existant: #aaaaaa;
--sidebar-active: #008cff;
--sidebar-spacer: #f4f4f4;
--scrollbar: #cccccc;
--icons: #cccccc;
--icons-hover: #333333;
--links: #4183c4;
--inline-code-color: #6e6b5e;
--theme-popup-bg: #fafafa;
--theme-popup-border: #cccccc;
--theme-hover: #e6e6e6;
--quote-bg: #f2f7f9;
--quote-border: darken(var(--quote-bg), 5%);
--table-border-color: darken(var(--bg), 5%);
--table-header-bg: darken(var(--bg), 20%);
--table-alternate-bg: darken(var(--bg), 3%);
--searchbar-border-color: #aaa;
--searchbar-bg: #fafafa;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #888;
--searchresults-li-bg: #e4f2fe;
--search-mark-bg: #a2cff5;
}
.navy {
--bg: #161923;
--fg: #bcbdd0;
--sidebar-bg: #282d3f;
--sidebar-fg: #c8c9db;
--sidebar-non-existant: #505274;
--sidebar-active: #2b79a2;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
--links: #2b79a2;
--inline-code-color: #c5c8c6;;
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: #282e40;
--quote-bg: #262933;
--quote-border: lighten(var(--quote-bg), 5%);
--table-border-color: lighten(var(--bg), 5%);
--table-header-bg: lighten(var(--bg), 20%);
--table-alternate-bg: lighten(var(--bg), 3%);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5;
}
.rust {
--bg: #e1e1db;
--fg: #262625;
--sidebar-bg: #3b2e2a;
--sidebar-fg: #c8c9db;
--sidebar-non-existant: #505254;
--sidebar-active: #e69f67;
--sidebar-spacer: #45373a;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #262625;
--links: #2b79a2;
--inline-code-color: #6e6b5e;
--theme-popup-bg: #e1e1db;
--theme-popup-border: #b38f6b;
--theme-hover: #99908a;
--quote-bg: #c1c1bb;
--quote-border: darken(var(--quote-bg), 5%);
--table-border-color: darken(var(--bg), 5%);
--table-header-bg: #b3a497;
--table-alternate-bg: darken(var(--bg), 3%);
--searchbar-border-color: #aaa;
--searchbar-bg: #fafafa;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #888;
--searchresults-li-bg: #dec2a2;
--search-mark-bg: #e69f67;
}

View File

@@ -1,5 +1,5 @@
<!DOCTYPE HTML>
<html lang="{{ language }}" class="sidebar-visible no-js">
<html lang="{{ language }}" class="sidebar-visible">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
@@ -9,36 +9,49 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="shortcut icon" href="{{ path_to_root }}{{ favicon }}">
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
<link rel="stylesheet" href="{{ path_to_root }}css/print.css" media="print">
<base href="{{ path_to_root }}">
<!-- Fonts -->
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="book.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="{{ path_to_root }}highlight.css">
<link rel="stylesheet" href="{{ path_to_root }}tomorrow-night.css">
<link rel="stylesheet" href="{{ path_to_root }}ayu-highlight.css">
<link rel="shortcut icon" href="{{ favicon }}">
<!-- Custom theme stylesheets -->
<!-- Font Awesome -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<link rel="stylesheet" href="ayu-highlight.css">
<!-- Custom theme -->
{{#each additional_css}}
<link rel="stylesheet" href="{{ this }}">
<link rel="stylesheet" href="{{this}}">
{{/each}}
{{#if mathjax_support}}
<!-- MathJax -->
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/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>
{{/if}}
<!-- 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>
<noscript>
<style type="text/css">
.javascript-only {
display: none;
}
</style>
</noscript>
</head>
<body class="light">
<!-- Provide site root to javascript -->
<script type="text/javascript">var path_to_root = "{{ path_to_root }}";</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
try {
@@ -61,7 +74,7 @@
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = 'light'; }
document.body.className = theme;
document.querySelector('html').className = theme + ' js';
document.querySelector('html').className = theme;
</script>
<!-- Hide / unhide sidebar before it is displayed -->
@@ -86,7 +99,7 @@
{{> header}}
<div id="menu-bar" class="menu-bar">
<div id="menu-bar-sticky-container">
<div class="left-buttons">
<div class="left-buttons javascript-only">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
@@ -110,7 +123,7 @@
<h1 class="menu-title">{{ book_title }}</h1>
<div class="right-buttons">
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
<a href="print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
@@ -118,15 +131,13 @@
</div>
{{#if search_enabled}}
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
<div id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</div>
<div id="searchresults-outer" class="searchresults-outer">
<div class="searchresults-header" id="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
{{/if}}
@@ -147,13 +158,13 @@
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
{{#previous}}
<a rel="prev" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<a rel="prev" href="{{link}}" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<a rel="next" href="{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
@@ -165,13 +176,13 @@
<nav class="nav-wide-wrapper" aria-label="Page navigation">
{{#previous}}
<a href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<a href="{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<a href="{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<i class="fa fa-angle-right"></i>
</a>
{{/next}}
@@ -179,6 +190,18 @@
</div>
<!-- Local fallback for Font Awesome -->
<script>
if (getComputedStyle(document.querySelector(".fa")).fontFamily !== "FontAwesome") {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = '_FontAwesome/css/font-awesome.css';
document.head.insertBefore(link, document.head.firstChild)
}
</script>
{{#if livereload}}
<!-- Livereload script (if served using the cli tool) -->
<script type="text/javascript">
@@ -198,7 +221,7 @@
{{#if google_analytics}}
<!-- Google Analytics Tag -->
<script type="text/javascript">
<script>
var localAddrs = ["localhost", "127.0.0.1", ""];
// make sure we don't activate google analytics if the developer is
@@ -216,45 +239,37 @@
{{/if}}
{{#if playpen_js}}
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
<script src="ace.js" type="text/javascript" charset="utf-8"></script>
<script src="editor.js" type="text/javascript" charset="utf-8"></script>
<script src="mode-rust.js" type="text/javascript" charset="utf-8"></script>
<script src="theme-dawn.js" type="text/javascript" charset="utf-8"></script>
<script src="theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
{{#if search_enabled}}
<script src="searchindex.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
{{#if search_js}}
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
{{/if}}
<script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script>
<!-- Custom JS scripts -->
{{#each additional_js}}
<script type="text/javascript" src="{{ path_to_root }}{{this}}"></script>
{{/each}}
{{#if is_print}}
{{#if mathjax_support}}
<script type="text/javascript">
window.addEventListener('load', function() {
MathJax.Hub.Register.StartupHook('End', function() {
window.setTimeout(window.print, 100);
});
});
</script>
{{else}}
<script type="text/javascript">
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
<script>
document.addEventListener('DOMContentLoaded', function() {
window.print();
})
</script>
{{/if}}
{{/if}}
<script src="highlight.js"></script>
<script src="book.js"></script>
<!-- Custom JS script -->
{{#each additional_js}}
<script type="text/javascript" src="{{this}}"></script>
{{/each}}
</body>
</html>

View File

@@ -5,18 +5,15 @@ pub mod playpen_editor;
#[cfg(feature = "search")]
pub mod searcher;
use std::path::Path;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use errors::*;
pub static INDEX: &'static [u8] = include_bytes!("index.hbs");
pub static HEADER: &'static [u8] = include_bytes!("header.hbs");
pub static CHROME_CSS: &'static [u8] = include_bytes!("css/chrome.css");
pub static GENERAL_CSS: &'static [u8] = include_bytes!("css/general.css");
pub static PRINT_CSS: &'static [u8] = include_bytes!("css/print.css");
pub static VARIABLES_CSS: &'static [u8] = include_bytes!("css/variables.css");
pub static CSS: &'static [u8] = include_bytes!("book.css");
pub static FAVICON: &'static [u8] = include_bytes!("favicon.png");
pub static JS: &'static [u8] = include_bytes!("book.js");
pub static HIGHLIGHT_JS: &'static [u8] = include_bytes!("highlight.js");
@@ -24,21 +21,22 @@ pub static TOMORROW_NIGHT_CSS: &'static [u8] = include_bytes!("tomorrow-night.cs
pub static HIGHLIGHT_CSS: &'static [u8] = include_bytes!("highlight.css");
pub static AYU_HIGHLIGHT_CSS: &'static [u8] = include_bytes!("ayu-highlight.css");
pub static CLIPBOARD_JS: &'static [u8] = include_bytes!("clipboard.min.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");
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.eot");
pub static FONT_AWESOME_SVG: &'static [u8] =
include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.svg");
pub static FONT_AWESOME_TTF: &'static [u8] =
include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.ttf");
pub static FONT_AWESOME_WOFF: &'static [u8] =
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff");
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");
include_bytes!("_FontAwesome/fonts/fontawesome-webfont.woff2");
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 will look if the user has a theme directory in their
/// 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.
///
/// You should only ever use the static variables directly if you want to
@@ -47,10 +45,7 @@ pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("FontAwesome/fonts/F
pub struct Theme {
pub index: Vec<u8>,
pub header: Vec<u8>,
pub chrome_css: Vec<u8>,
pub general_css: Vec<u8>,
pub print_css: Vec<u8>,
pub variables_css: Vec<u8>,
pub css: Vec<u8>,
pub favicon: Vec<u8>,
pub js: Vec<u8>,
pub highlight_css: Vec<u8>,
@@ -78,25 +73,13 @@ impl Theme {
(theme_dir.join("index.hbs"), &mut theme.index),
(theme_dir.join("header.hbs"), &mut theme.header),
(theme_dir.join("book.js"), &mut theme.js),
(theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
(theme_dir.join("css/general.css"), &mut theme.general_css),
(theme_dir.join("css/print.css"), &mut theme.print_css),
(
theme_dir.join("css/variables.css"),
&mut theme.variables_css,
),
(theme_dir.join("book.css"), &mut theme.css),
(theme_dir.join("favicon.png"), &mut theme.favicon),
(theme_dir.join("highlight.js"), &mut theme.highlight_js),
(theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
(theme_dir.join("highlight.css"), &mut theme.highlight_css),
(
theme_dir.join("tomorrow-night.css"),
&mut theme.tomorrow_night_css,
),
(
theme_dir.join("ayu-highlight.css"),
&mut theme.ayu_highlight_css,
),
(theme_dir.join("tomorrow-night.css"), &mut theme.tomorrow_night_css),
(theme_dir.join("ayu-highlight.css"), &mut theme.ayu_highlight_css),
];
for (filename, dest) in files {
@@ -119,10 +102,7 @@ impl Default for Theme {
Theme {
index: INDEX.to_owned(),
header: HEADER.to_owned(),
chrome_css: CHROME_CSS.to_owned(),
general_css: GENERAL_CSS.to_owned(),
print_css: PRINT_CSS.to_owned(),
variables_css: VARIABLES_CSS.to_owned(),
css: CSS.to_owned(),
favicon: FAVICON.to_owned(),
js: JS.to_owned(),
highlight_css: HIGHLIGHT_CSS.to_owned(),
@@ -150,12 +130,12 @@ fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
use tempfile::Builder as TempFileBuilder;
use std::path::PathBuf;
#[test]
fn theme_uses_defaults_with_nonexistent_src_dir() {
@@ -170,28 +150,21 @@ mod tests {
#[test]
fn theme_dir_overrides_defaults() {
let files = [
"index.hbs",
"header.hbs",
"favicon.png",
"css/chrome.css",
"css/general.css",
"css/print.css",
"css/variables.css",
"book.js",
"highlight.js",
"tomorrow-night.css",
"highlight.css",
"ayu-highlight.css",
"clipboard.min.js",
];
// Get all the non-Rust files in the theme directory
let special_files = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("src/theme")
.read_dir()
.unwrap()
.filter_map(|f| f.ok())
.map(|f| f.path())
.filter(|p| p.is_file() && !p.ends_with(".rs"));
let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
fs::create_dir(temp.path().join("css")).unwrap();
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
// "touch" all of the special files so we have empty copies
for file in &files {
File::create(&temp.path().join(file)).unwrap();
for special_file in special_files {
let filename = temp.path().join(special_file.file_name().unwrap());
let _ = File::create(&filename);
}
let got = Theme::new(temp.path());
@@ -199,10 +172,7 @@ mod tests {
let empty = Theme {
index: Vec::new(),
header: Vec::new(),
chrome_css: Vec::new(),
general_css: Vec::new(),
print_css: Vec::new(),
variables_css: Vec::new(),
css: Vec::new(),
favicon: Vec::new(),
js: Vec::new(),
highlight_css: Vec::new(),

View File

@@ -1,4 +1,3 @@
"use strict";
window.editors = [];
(function(editors) {
if (typeof(ace) === 'undefined' || !ace) {
@@ -12,8 +11,7 @@ window.editors = [];
showPrintMargin: false,
showLineNumbers: false,
showGutter: false,
maxLines: Infinity,
fontSize: "0.875em" // please adjust the font size of the code in general.styl
maxLines: Infinity
});
editor.$blockScrolling = Infinity;

View File

@@ -1,24 +1,15 @@
"use strict";
window.search = window.search || {};
(function search(search) {
// Search functionality
//
// You can use !hasFocus() to prevent keyhandling in your key
// event handlers while the user is typing their search.
// event handlers while the user is typing his search.
if (!Mark || !elasticlunr) {
return;
}
//IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
if (!String.prototype.startsWith) {
String.prototype.startsWith = function(search, pos) {
return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
};
}
var search_wrap = document.getElementById('search-wrapper'),
searchbar = document.getElementById('searchbar'),
var searchbar = document.getElementById('searchbar'),
searchbar_outer = document.getElementById('searchbar-outer'),
searchresults = document.getElementById('searchresults'),
searchresults_outer = document.getElementById('searchresults-outer'),
@@ -27,12 +18,11 @@ window.search = window.search || {};
content = document.getElementById('content'),
searchindex = null,
doc_urls = [],
results_options = {
resultsoptions = {
teaser_word_count: 30,
limit_results: 30,
},
search_options = {
searchoptions = {
bool: "AND",
expand: true,
fields: {
@@ -140,12 +130,12 @@ window.search = window.search || {};
teaser_count++;
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
var url = doc_urls[result.ref].split("#");
var url = result.ref.split("#");
if (url.length == 1) { // no anchor found
url.push("");
}
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
return '<a href="' + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
+ teaser + '</span>';
@@ -197,7 +187,7 @@ window.search = window.search || {};
}
var window_weight = [];
var window_size = Math.min(weighted.length, results_options.teaser_word_count);
var window_size = Math.min(weighted.length, resultsoptions.teaser_word_count);
var cur_sum = 0;
for (var wordindex = 0; wordindex < window_size; wordindex++) {
@@ -247,21 +237,17 @@ window.search = window.search || {};
return teaser_split.join('');
}
function init(config) {
results_options = config.results_options;
search_options = config.search_options;
searchbar_outer = config.searchbar_outer;
doc_urls = config.doc_urls;
searchindex = elasticlunr.Index.load(config.index);
function init() {
resultsoptions = window.search.resultsoptions;
searchoptions = window.search.searchoptions;
searchindex = elasticlunr.Index.load(window.search.index);
// Set up events
searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false);
searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false);
document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false);
document.addEventListener('keydown', function (e) { globalKeyHandler(e); }, false);
// If the user uses the browser buttons, do the same as if a reload happened
window.onpopstate = function(e) { doSearchOrMarkFromUrl(); };
// Suppress "submit" events so the page doesn't reload when the user presses Enter
document.addEventListener('submit', function(e) { e.preventDefault(); }, false);
// If reloaded, do the search or mark again, depending on the current url parameters
doSearchOrMarkFromUrl();
@@ -311,84 +297,94 @@ window.search = window.search || {};
// Eventhandler for keyevents on `document`
function globalKeyHandler(e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea') { return; }
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (e.keyCode === ESCAPE_KEYCODE) {
if (e.keyCode == ESCAPE_KEYCODE) {
e.preventDefault();
searchbar.classList.remove("active");
setSearchUrlParameters("",
(searchbar.value.trim() !== "") ? "push" : "replace");
(searchbar.value.trim() != "") ? "push" : "replace");
if (hasFocus()) {
unfocusSearchbar();
}
showSearch(false);
marker.unmark();
} else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) {
return;
}
if (!hasFocus() && e.keyCode == SEARCH_HOTKEY_KEYCODE) {
e.preventDefault();
showSearch(true);
window.scrollTo(0, 0);
searchbar.select();
} else if (hasFocus() && e.keyCode === DOWN_KEYCODE) {
searchbar.focus();
return;
}
if (hasFocus() && e.keyCode == DOWN_KEYCODE) {
e.preventDefault();
unfocusSearchbar();
searchresults.firstElementChild.classList.add("focus");
} else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE
|| e.keyCode === UP_KEYCODE
|| e.keyCode === SELECT_KEYCODE)) {
searchresults.children('li').first().classList.add("focus");
return;
}
if (!hasFocus() && (e.keyCode == DOWN_KEYCODE
|| e.keyCode == UP_KEYCODE
|| e.keyCode == SELECT_KEYCODE)) {
// not `:focus` because browser does annoying scrolling
var focused = searchresults.querySelector("li.focus");
if (!focused) return;
var current_focus = search.searchresults.find("li.focus");
if (current_focus.length == 0) return;
e.preventDefault();
if (e.keyCode === DOWN_KEYCODE) {
var next = focused.nextElementSibling;
if (next) {
focused.classList.remove("focus");
if (e.keyCode == DOWN_KEYCODE) {
var next = current_focus.next()
if (next.length > 0) {
current_focus.classList.remove("focus");
next.classList.add("focus");
}
} else if (e.keyCode === UP_KEYCODE) {
focused.classList.remove("focus");
var prev = focused.previousElementSibling;
if (prev) {
prev.classList.add("focus");
} else if (e.keyCode == UP_KEYCODE) {
current_focus.classList.remove("focus");
var prev = current_focus.prev();
if (prev.length == 0) {
searchbar.focus();
} else {
searchbar.select();
prev.classList.add("focus");
}
} else { // SELECT_KEYCODE
window.location.assign(focused.querySelector('a'));
} else {
window.location = current_focus.children('a').attr('href');
}
}
}
function showSearch(yes) {
if (yes) {
search_wrap.classList.remove('hidden');
searchbar_outer.style.display = 'block';
content.style.display = 'none';
searchicon.setAttribute('aria-expanded', 'true');
} else {
search_wrap.classList.add('hidden');
content.style.display = 'block';
searchbar_outer.style.display = 'none';
searchresults_outer.style.display = 'none';
searchbar.value = '';
removeChildren(searchresults);
searchicon.setAttribute('aria-expanded', 'false');
var results = searchresults.children;
for (var i = 0; i < results.length; i++) {
results[i].classList.remove("focus");
}
}
}
function showResults(yes) {
if (yes) {
searchresults_outer.classList.remove('hidden');
searchbar_outer.style.display = 'block';
content.style.display = 'none';
searchresults_outer.style.display = 'block';
} else {
searchresults_outer.classList.add('hidden');
content.style.display = 'block';
searchresults_outer.style.display = 'none';
}
}
// Eventhandler for search icon
function searchIconClickHandler() {
if (search_wrap.classList.contains('hidden')) {
if (searchbar_outer.style.display === 'block') {
showSearch(false);
} else {
showSearch(true);
window.scrollTo(0, 0);
searchbar.select();
} else {
showSearch(false);
searchbar.focus();
}
}
@@ -443,8 +439,8 @@ window.search = window.search || {};
if (searchindex == null) { return; }
// Do the actual search
var results = searchindex.search(searchterm, search_options);
var resultcount = Math.min(results.length, results_options.limit_results);
var results = searchindex.search(searchterm, searchoptions);
var resultcount = Math.min(results.length, resultsoptions.limit_results);
// Display search metrics
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
@@ -462,16 +458,7 @@ window.search = window.search || {};
showResults(true);
}
fetch(path_to_root + 'searchindex.json')
.then(response => response.json())
.then(json => init(json))
.catch(error => { // Try to load searchindex.js if fetch failed
var script = document.createElement('script');
script.src = path_to_root + 'searchindex.js';
script.onload = () => init(window.search);
document.head.appendChild(script);
});
init();
// Exported functions
search.hasFocus = hasFocus;
})(window.search);

View File

@@ -0,0 +1,12 @@
@import "nib"
@import 'general'
@import 'sidebar'
@import 'page'
@import 'menu'
@import 'nav-icons'
@import 'theme-popup'
@import 'themes'
@import 'print'
@import 'tooltip'
@import 'searchbar'

View File

@@ -0,0 +1,72 @@
html {
font-family: "Open Sans", sans-serif
color: #333
}
body {
margin: 0;
font-size: 1rem;
overflow-x: hidden;
}
code {
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
font-size: 0.875em;
}
.left {
float: left
}
.right {
float: right
}
.hidden {
display: none;
}
.play-button.hidden {
display: none;
}
h2, h3 { margin-top: 2.5em }
h4, h5 { margin-top: 2em }
.header + .header h3, .header + .header h4, .header + .header h5 { margin-top: 1em }
a.header:target h1:before,
a.header:target h2:before,
a.header:target h3:before,
a.header:target h4:before {
display: inline-block;
content: "»";
margin-left: -30px;
width: 30px;
}
table {
margin: 0 auto;
border-collapse: collapse;
td {
padding: 3px 20px;
border: 1px solid;
}
thead {
td { font-weight: 700; }
}
}
:not(.footnote-definition) + .footnote-definition,
.footnote-definition + :not(.footnote-definition) {
margin-top: 2em;
}
.footnote-definition {
font-size: 0.9em;
margin: 0.5em 0;
p { display: inline; }
}

View File

@@ -0,0 +1,42 @@
#menu-bar {
position: -webkit-sticky
position: sticky
top: 0
z-index: 101
& > #menu-bar-sticky-container {
display: flex
flex-wrap: wrap
transition: transform 0.5s, border-bottom-color 0.5s
}
i, .icon-button {
position: relative
margin: 0 10px
z-index: 10
line-height: 50px
transition: color 0.5s
&:hover { cursor: pointer }
}
}
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
transform: translateY(-60px);
}
.menu-title {
display: inline-block
font-weight: 200
font-size: 20px
line-height: 50px
text-align: center
margin: 0
flex: 1
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
cursor: pointer;
}

View File

@@ -0,0 +1,55 @@
.nav-chapters {
font-size: 2.5em
text-align: center
text-decoration: none
position: fixed
top: 50px /* Height of menu-bar */
bottom: 0
margin: 0
max-width: 150px
min-width: 90px
display: flex
justify-content: center
align-content: center
flex-direction: column
transition: color 0.5s
}
.nav-chapters:hover { text-decoration: none }
.nav-wrapper {
margin-top: 50px
display: none
}
.mobile-nav-chapters {
font-size: 2.5em
text-align: center
text-decoration: none
width: 90px
border-radius: 5px
}
.previous {
float: left
}
.next {
float: right
right: $page-padding
}
@media only screen and (max-width: $page-plus-sidebar-width) {
.nav-wide-wrapper { display: none }
.nav-wrapper { display: block }
}
@media only screen and (max-width: $page-plus-sidebar-width + $sidebar-width) {
.sidebar-visible {
.nav-wide-wrapper { display: none }
.nav-wrapper { display: block }
}
}

View File

@@ -0,0 +1,51 @@
@require 'variables'
.page-wrapper {
box-sizing: border-box
left: 0
position: absolute
right: 0
top: 0
bottom: 0
// Animation: slide away
transition: padding-left 0.5s, margin-left 0.5s, left 0.5s
}
.sidebar-visible .page-wrapper {
left: $sidebar-width
}
.page {
outline: 0
padding: 0 $page-padding
}
.content {
position: relative
top: 0
bottom: 0
overflow-y: auto
right: 0
left: 0
padding: 0 15px
padding-bottom: 50px
main {
margin-left: auto
margin-right: auto
max-width: $content-max-width
}
a {
text-decoration: none;
&:hover { text-decoration: underline; }
}
img { max-width: 100%; }
}
.sidebar-visible .content {
position: absolute
top: 52px
}

View File

@@ -0,0 +1,60 @@
@media only print {
#sidebar,
#menu-bar,
.nav-chapters,
.mobile-nav-chapters {
display: none
}
#page-wrapper {
left: 0;
overflow-y: initial;
}
#page-wrapper.page-wrapper {
padding-left: 0px;
}
#content {
max-width: none;
margin: 0;
padding: 0;
}
.page {
overflow-y: initial;
}
code {
background-color: #666666
border-radius: 5px
/* Force background to be printed in Chrome */
-webkit-print-color-adjust: exact
}
pre > .buttons {
z-index: 2;
}
a, a:visited, a:active, a:hover {
color: #4183c4
text-decoration: none
}
h1, h2, h3, h4, h5, h6 {
page-break-inside: avoid
page-break-after: avoid
/*break-after: avoid*/
}
pre, code {
page-break-inside: avoid
white-space: pre-wrap /* CSS 3 */
white-space: -moz-pre-wrap /* Mozilla, since 1999 */
white-space: -pre-wrap /* Opera 4-6 */
white-space: -o-pre-wrap /* Opera 7 */
word-wrap: break-word /* Internet Explorer 5.5+ */
}
}

View File

@@ -0,0 +1,67 @@
@require 'variables'
#searchresults a {
text-decoration: none;
}
mark {
border-radius: 2px;
padding: 0 3px 1px 3px;
margin: 0 -3px -1px -3px;
transition: background-color 300ms linear;
}
.fade-out {
background-color: rgba(0,0,0,0) !important
}
.searchbar-outer {
display: none;
margin-left: auto;
margin-right: auto;
max-width: $content-max-width;
}
#searchbar {
display: block;
width: 100%;
margin: 5px auto 0px auto;
padding: 10px 16px;
transition: box-shadow 300ms ease-in-out;
}
.searchresults-header {
font-weight: bold;
font-size: 1em;
padding: 18px 0 0 5px;
}
.searchresults-outer {
display: none;
margin-left: auto;
margin-right: auto;
max-width: $content-max-width;
}
ul#searchresults {
list-style: none;
padding-left: 20px;
li {
margin: 10px 0px;
padding: 2px;
border-radius: 2px;
}
span.teaser {
display: block;
clear: both;
margin: 5px 0 0 20px;
font-size: 0.8em;
}
span.teaser em {
font-weight: bold;
font-style: normal;
}
}

View File

@@ -0,0 +1,60 @@
@require 'variables'
.sidebar {
position: fixed
left: 0
top: 0
bottom: 0
width: $sidebar-width
overflow-y: auto
padding: 10px 10px
font-size: 0.875em
box-sizing: border-box
-webkit-overflow-scrolling: touch
overscroll-behavior-y: contain;
// Animation: slide away
transition: transform 0.5s
code {
line-height: 2em;
}
}
.sidebar-hidden .sidebar {
transform: translateX(- $sidebar-width)
}
.chapter {
list-style: none outside none
padding-left: 0
line-height: 2.2em
li a {
display: block;
padding: 0
text-decoration: none
@media (-moz-touch-enabled: 1), (pointer: coarse) { padding: 5px 0; }
&:hover { text-decoration: none }
}
.spacer {
width: 100%
height: 3px
margin: 5px 0px
@media (-moz-touch-enabled: 1), (pointer: coarse) { margin: 10px 0; }
}
}
.section {
list-style: none outside none
padding-left: 20px
line-height: 1.9em
li {
text-overflow: ellipsis
overflow: hidden
white-space: nowrap
}
}

View File

@@ -0,0 +1,31 @@
.theme-popup {
position: absolute
left: 10px
z-index: 1000;
border-radius: 4px
font-size: 0.7em
.theme {
display: inline
border: 0
margin: 0
padding: 2px 10px
line-height: 25px
width: 100%
white-space: nowrap
text-align: left
cursor: pointer
color inherit
background: inherit;
font-size: inherit;
&:hover:first-child,
&:hover:last-child {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}
}
}

View File

@@ -0,0 +1,41 @@
$theme-name = 'ayu'
$bg = #0f1419
$fg = #c5c5c5
$sidebar-bg = #14191f
$sidebar-fg = #c8c9db
$sidebar-non-existant = #5c6773
$sidebar-active = #ffb454
$sidebar-spacer = #2d334f
$scrollbar = $sidebar-fg
$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%)
$searchbar-border-color = #848484
$searchbar-bg = #424242
$searchbar-fg = #fff
$searchbar-shadow-color = #d4c89f
$searchresults-header-fg = #666
$searchresults-border-color = #888
$searchresults-li-bg = #252932
$search-mark-bg = #e3b171
@import 'base'

View File

@@ -0,0 +1,222 @@
.{unquote($theme-name)} {
color: $fg
background-color: $bg
.content .header:link, .content .header:visited {
color: $fg;
pointer: cursor;
&:hover {
text-decoration: none;
}
}
.menu-bar {
margin: auto (- $page-padding);
& > #menu-bar-sticky-container {
background-color: $bg
border-bottom-color: $bg
border-bottom-width: 1px
border-bottom-style: solid
}
&.bordered > #menu-bar-sticky-container {
border-bottom-color: $table-border-color
}
}
$table-border-color
.sidebar {
background-color: $sidebar-bg
color: $sidebar-fg
&::-webkit-scrollbar {
background: $sidebar-bg;
}
&::-webkit-scrollbar-thumb {
background: $scrollbar;
}
}
.chapter li {
color: $sidebar-non-existant
a { color: $sidebar-fg }
.active,
a:hover, {
/* Animate color change */
color: $sidebar-active
}
}
.chapter .spacer {
background-color: $sidebar-spacer
}
.menu-bar,
.menu-bar:visited,
.nav-chapters,
.nav-chapters:visited,
.mobile-nav-chapters,
.mobile-nav-chapters:visited,
.menu-bar .icon-button,
.menu-bar a i {
color: $icons
}
.menu-bar i:hover,
.menu-bar .icon-button:hover,
.nav-chapters:hover,
.mobile-nav-chapters i:hover {
color: $icons-hover
}
.mobile-nav-chapters i:hover {
color: $sidebar-fg
}
.mobile-nav-chapters {
background-color: $sidebar-bg
}
#searchresults a,
.content a:link,
a:visited,
a > .hljs {
color: $links
}
.theme-popup {
color: $fg
background: $theme-popup-bg
border: 1px solid $theme-popup-border
margin: 0;
padding: 0;
list-style: none;
display: none;
.theme:hover { background-color: $theme-hover }
.default { color: $icons }
}
blockquote {
margin: 20px 0;
padding: 0 20px;
color: $fg;
background-color: $quote-bg;
border-top: .1em solid $quote-border;
border-bottom: .1em solid $quote-border;
}
table {
td {
border-color: $table-border-color;
}
// Alternate background colors for rows
tbody tr:nth-child(2n) {
background: $table-alternate-bg;
}
thead {
background: $table-header-bg;
td { border: none; }
tr { border: 1px $table-header-bg solid; }
}
}
/* Inline code */
:not(pre) > .hljs {
display: inline-block;
vertical-align: middle;
padding: 0.1em 0.3em;
border-radius: 3px;
color: $inline-code-color;
}
a:hover > .hljs {
text-decoration: underline;
}
pre {
position: relative;
& > .buttons {
position: absolute;
z-index: 100;
right: 5px;
top: 5px;
color: $sidebar-fg;
cursor: pointer;
:hover { color: $sidebar-active; }
i { margin-left: 8px; }
button {
color: inherit;
background: transparent;
border: none;
cursor: inherit;
}
}
& > .result { margin-top: 10px; }
}
.icon-button {
border: none;
background: none;
padding: 0;
color: inherit;
i {
margin: 0;
}
}
::-webkit-scrollbar {
background: $bg;
}
::-webkit-scrollbar-thumb {
background: $scrollbar;
}
/* Search */
#searchbar {
border: 1px solid $searchbar-border-color;
border-radius: 3px;
background-color: $searchbar-bg;
color: $searchbar-fg
&:focus, &.active {
box-shadow: 0 0 3px $searchbar-shadow-color;
}
}
.searchresults-header {
color: $searchresults-header-fg;
}
.searchresults-outer {
border-bottom: 1px dashed $searchresults-border-color;
}
ul#searchresults li.focus {
background-color: $searchresults-li-bg;
}
mark {
background-color: $search-mark-bg;
}
}

View File

@@ -0,0 +1,41 @@
$theme-name = 'coal'
$bg = #141617
$fg = #98a3ad
$sidebar-bg = #292c2f
$sidebar-fg = #a1adb8
$sidebar-non-existant = #505254
$sidebar-active = #3473ad
$sidebar-spacer = #393939
$scrollbar = $sidebar-fg
$icons = #43484d
$icons-hover = #b3c0cc
$links = #2b79a2
$inline-code-color = #c5c8c6;
$theme-popup-bg = #141617
$theme-popup-border = #43484d
$theme-hover = #1f2124
$quote-bg = #242637
$quote-border = lighten($quote-bg, 5%)
$table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
$searchbar-border-color = #aaa
$searchbar-bg = #b7b7b7
$searchbar-fg = #000
$searchbar-shadow-color = #aaa
$searchresults-header-fg = #666
$searchresults-border-color = #98a3ad
$searchresults-li-bg = #2b2b2f
$search-mark-bg = #355c7d
@import 'base'

View File

@@ -0,0 +1,5 @@
@import 'light'
@import 'coal'
@import 'navy'
@import 'rust'
@import 'ayu'

View File

@@ -0,0 +1,41 @@
$theme-name = 'light'
$bg = #ffffff
$fg = #333333
$sidebar-bg = #fafafa
$sidebar-fg = #364149
$sidebar-non-existant = #aaaaaa
$sidebar-active = #008cff
$sidebar-spacer = #f4f4f4
$scrollbar = #cccccc
$icons = #cccccc
$icons-hover = #333333
$links = #4183c4
$inline-code-color = #6e6b5e
$theme-popup-bg = #fafafa
$theme-popup-border = #cccccc
$theme-hover = #e6e6e6
$quote-bg = #f2f7f9
$quote-border = darken($quote-bg, 5%)
$table-border-color = darken($bg, 5%)
$table-header-bg = darken($bg, 20%)
$table-alternate-bg = darken($bg, 3%)
$searchbar-border-color = #aaa
$searchbar-bg = #fafafa
$searchbar-fg = #000
$searchbar-shadow-color = #aaa
$searchresults-header-fg = #666
$searchresults-border-color = #888
$searchresults-li-bg = #e4f2fe
$search-mark-bg = #a2cff5
@import 'base'

View File

@@ -0,0 +1,41 @@
$theme-name = 'navy'
$bg = #161923
$fg = #bcbdd0
$sidebar-bg = #282d3f
$sidebar-fg = #c8c9db
$sidebar-non-existant = #505274
$sidebar-active = #2b79a2
$sidebar-spacer = #2d334f
$scrollbar = $sidebar-fg
$icons = #737480
$icons-hover = #b7b9cc
$links = #2b79a2
$inline-code-color = #c5c8c6;
$theme-popup-bg = #161923
$theme-popup-border = #737480
$theme-hover = #282e40
$quote-bg = #262933
$quote-border = lighten($quote-bg, 5%)
$table-border-color = lighten($bg, 5%)
$table-header-bg = lighten($bg, 20%)
$table-alternate-bg = lighten($bg, 3%)
$searchbar-border-color = #aaa
$searchbar-bg = #aeaec6
$searchbar-fg = #000
$searchbar-shadow-color = #aaa
$searchresults-header-fg = #5f5f71
$searchresults-border-color = #5c5c68
$searchresults-li-bg = #242430
$search-mark-bg = #a2cff5
@import 'base'

View File

@@ -0,0 +1,41 @@
$theme-name = 'rust'
$bg = #e1e1db
$fg = #262625
$sidebar-bg = #3b2e2a
$sidebar-fg = #c8c9db
$sidebar-non-existant = #505254
$sidebar-active = #e69f67
$sidebar-spacer = #45373a
$scrollbar = $sidebar-fg
$icons = #737480
$icons-hover = #262625
$links = #2b79a2
$inline-code-color = #6e6b5e;
$theme-popup-bg = #e1e1db
$theme-popup-border = #b38f6b
$theme-hover = #99908a
$quote-bg = #c1c1bb
$quote-border = darken($quote-bg, 5%)
$table-border-color = darken($bg, 5%)
$table-header-bg = #b3a497
$table-alternate-bg = darken($bg, 3%)
$searchbar-border-color = #aaa
$searchbar-bg = #fafafa
$searchbar-fg = #000
$searchbar-shadow-color = #aaa
$searchresults-header-fg = #666
$searchresults-border-color = #888
$searchresults-li-bg = #dec2a2
$search-mark-bg = #e69f67
@import 'base'

View File

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

View File

@@ -0,0 +1,4 @@
$sidebar-width = 300px
$page-padding = 15px
$content-max-width = 750px
$page-plus-sidebar-width = $content-max-width + $sidebar-width + $page-padding * 2

View File

@@ -25,10 +25,16 @@ pub fn normalize_path(path: &str) -> String {
}
/// Write the given data to a file, creating it first if necessary
pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8]) -> Result<()> {
pub fn write_file<P: AsRef<Path>>(
build_dir: &Path,
filename: P,
content: &[u8],
) -> Result<()> {
let path = build_dir.join(filename);
create_file(&path)?.write_all(content).map_err(|e| e.into())
create_file(&path)?
.write_all(content)
.map_err(|e| e.into())
}
/// Takes a path and returns a path containing just enough `../` to point to

View File

@@ -5,13 +5,12 @@ mod string;
use errors::Error;
use regex::Regex;
use pulldown_cmark::{
html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES, OPTION_ENABLE_TABLES,
};
use pulldown_cmark::{html, Event, Options, Parser, Tag, OPTION_ENABLE_FOOTNOTES,
OPTION_ENABLE_TABLES};
use std::borrow::Cow;
pub use self::string::{take_lines, RangeArgument};
pub use self::string::{RangeArgument, take_lines};
/// Replaces multiple consecutive whitespace characters with a single space character.
pub fn collapse_whitespace<'a>(text: &'a str) -> Cow<'a, str> {
@@ -36,10 +35,7 @@ pub fn normalize_id(content: &str) -> String {
})
.collect::<String>();
// Ensure that the first character is [A-Za-z]
if ret.chars()
.next()
.map_or(false, |c| !c.is_ascii_alphabetic())
{
if ret.chars().next().map_or(false, |c| !c.is_ascii_alphabetic()) {
ret.insert(0, 'a');
}
ret
@@ -51,19 +47,17 @@ pub fn id_from_content(content: &str) -> String {
let mut content = content.to_string();
// Skip any tags or html-encoded stuff
const REPL_SUB: &[&str] = &[
"<em>",
"</em>",
"<code>",
"</code>",
"<strong>",
"</strong>",
"&lt;",
"&gt;",
"&amp;",
"&#39;",
"&quot;",
];
const REPL_SUB: &[&str] = &["<em>",
"</em>",
"<code>",
"</code>",
"<strong>",
"</strong>",
"&lt;",
"&gt;",
"&amp;",
"&#39;",
"&quot;"];
for sub in REPL_SUB {
content = content.replace(sub, "");
}
@@ -74,32 +68,6 @@ pub fn id_from_content(content: &str) -> String {
normalize_id(trimmed)
}
fn adjust_links(event: Event) -> Event {
lazy_static! {
static ref HTTP_LINK: Regex = Regex::new("^https?://").unwrap();
static ref MD_LINK: Regex = Regex::new("(?P<link>.*).md(?P<anchor>#.*)?").unwrap();
}
match event {
Event::Start(Tag::Link(dest, title)) => {
if !HTTP_LINK.is_match(&dest) {
if let Some(caps) = MD_LINK.captures(&dest) {
let mut html_link = [&caps["link"], ".html"].concat();
if let Some(anchor) = caps.name("anchor") {
html_link.push_str(anchor.as_str());
}
return Event::Start(Tag::Link(Cow::from(html_link), title));
}
}
Event::Start(Tag::Link(dest, title))
}
_ => event,
}
}
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2);
@@ -111,8 +79,7 @@ pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
let p = Parser::new_ext(text, opts);
let mut converter = EventQuoteConverter::new(curly_quotes);
let events = p.map(clean_codeblock_headers)
.map(adjust_links)
.map(|event| converter.convert(event));
.map(|event| converter.convert(event));
html::push_html(&mut s, events);
s
@@ -164,36 +131,36 @@ fn clean_codeblock_headers(event: Event) -> Event {
}
}
fn convert_quotes_to_curly(original_text: &str) -> String {
// We'll consider the start to be "whitespace".
let mut preceded_by_whitespace = true;
original_text
.chars()
.map(|original_char| {
let converted_char = match original_char {
'\'' => {
if preceded_by_whitespace {
''
} else {
''
}
original_text.chars()
.map(|original_char| {
let converted_char = match original_char {
'\'' => {
if preceded_by_whitespace {
''
} else {
''
}
'"' => {
if preceded_by_whitespace {
'“'
} else {
'”'
}
}
'"' => {
if preceded_by_whitespace {
'“'
} else {
'”'
}
_ => original_char,
};
}
_ => original_char,
};
preceded_by_whitespace = original_char.is_whitespace();
preceded_by_whitespace = original_char.is_whitespace();
converted_char
})
.collect()
converted_char
})
.collect()
}
/// Prints a "backtrace" of some `Error`.
@@ -210,26 +177,6 @@ mod tests {
mod render_markdown {
use super::super::render_markdown;
#[test]
fn preserves_external_links() {
assert_eq!(
render_markdown("[example](https://www.rust-lang.org/)", false),
"<p><a href=\"https://www.rust-lang.org/\">example</a></p>\n"
);
}
#[test]
fn it_can_adjust_markdown_links() {
assert_eq!(
render_markdown("[example](example.md)", false),
"<p><a href=\"example.html\">example</a></p>\n"
);
assert_eq!(
render_markdown("[example_anchor](example.md#anchor)", false),
"<p><a href=\"example.html#anchor\">example_anchor</a></p>\n"
);
}
#[test]
fn it_can_keep_quotes_straight() {
assert_eq!(render_markdown("'one'", false), "<p>'one'</p>\n");
@@ -328,26 +275,18 @@ more text with spaces
#[test]
fn it_generates_anchors() {
assert_eq!(
id_from_content("## `--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes"
);
assert_eq!(
id_from_content("## Method-call expressions"),
"method-call-expressions"
);
assert_eq!(id_from_content("## `--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes");
assert_eq!(id_from_content("## Method-call expressions"),
"method-call-expressions");
}
#[test]
fn it_normalizes_ids() {
assert_eq!(
normalize_id("`--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes"
);
assert_eq!(
normalize_id("Method-call 🐙 expressions \u{1f47c}"),
"method-call--expressions-"
);
assert_eq!(normalize_id("`--passes`: add more rustdoc passes"),
"a--passes-add-more-rustdoc-passes");
assert_eq!(normalize_id("Method-call 🐙 expressions \u{1f47c}"),
"method-call--expressions-");
assert_eq!(normalize_id("_-_12345"), "a_-_12345");
assert_eq!(normalize_id("12345"), "a12345");
assert_eq!(normalize_id(""), "");
@@ -359,18 +298,14 @@ more text with spaces
#[test]
fn it_converts_single_quotes() {
assert_eq!(
convert_quotes_to_curly("'one', 'two'"),
"one, two"
);
assert_eq!(convert_quotes_to_curly("'one', 'two'"),
"one, two");
}
#[test]
fn it_converts_double_quotes() {
assert_eq!(
convert_quotes_to_curly(r#""one", "two""#),
"“one”, “two”"
);
assert_eq!(convert_quotes_to_curly(r#""one", "two""#),
"one, two");
}
#[test]

View File

@@ -1,5 +1,5 @@
use itertools::Itertools;
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
use itertools::Itertools;
// This trait is already contained in the standard lib, however it is unstable.
// TODO: Remove when the `collections_range` feature stabilises
@@ -50,7 +50,7 @@ pub fn take_lines<R: RangeArgument<usize>>(s: &str, range: R) -> String {
let start = *range.start().unwrap_or(&0);
let mut lines = s.lines().skip(start);
match range.end() {
Some(&end) => lines.take(end.saturating_sub(start)).join("\n"),
Some(&end) => lines.take(end.checked_sub(start).unwrap_or(0)).join("\n"),
None => lines.join("\n"),
}
}

View File

@@ -3,11 +3,11 @@
extern crate mdbook;
extern crate tempfile;
use mdbook::config::Config;
use mdbook::MDBook;
#[cfg(not(windows))]
use std::path::Path;
use tempfile::{Builder as TempFileBuilder, TempDir};
use tempfile::{TempDir, Builder as TempFileBuilder};
use mdbook::config::Config;
use mdbook::MDBook;
#[test]
fn passing_alternate_backend() {
@@ -52,8 +52,8 @@ fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
#[test]
#[cfg(not(windows))]
fn backends_receive_render_context_via_stdin() {
use mdbook::renderer::RenderContext;
use std::fs::File;
use mdbook::renderer::RenderContext;
let temp = TempFileBuilder::new().prefix("output").tempdir().unwrap();
let out_file = temp.path().join("out.txt");

View File

@@ -7,18 +7,19 @@ extern crate mdbook;
extern crate tempfile;
extern crate walkdir;
use mdbook::errors::*;
use mdbook::utils::fs::file_to_string;
use std::path::Path;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use mdbook::errors::*;
use mdbook::utils::fs::file_to_string;
// The funny `self::` here is because we've got an `extern crate ...` and are
// in a submodule
use self::tempfile::{TempDir, Builder as TempFileBuilder};
use self::mdbook::MDBook;
use self::tempfile::{Builder as TempFileBuilder, TempDir};
use self::walkdir::WalkDir;
/// Create a dummy book in a temporary directory, using the contents of
/// `SUMMARY_MD` as a guide.
///
@@ -46,16 +47,13 @@ impl DummyBook {
/// Write a book to a temporary directory using the provided settings.
pub fn build(&self) -> Result<TempDir> {
let temp = TempFileBuilder::new()
.prefix("dummy_book-")
.tempdir()
.chain_err(|| "Unable to create temp directory")?;
let temp = TempFileBuilder::new().prefix("dummy_book").tempdir().chain_err(|| "Unable to create temp directory")?;
let dummy_book_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/dummy_book");
recursive_copy(&dummy_book_root, temp.path()).chain_err(|| {
"Couldn't copy files into a \
temporary directory"
})?;
"Couldn't copy files into a \
temporary directory"
})?;
let sub_pattern = if self.passing_test { "true" } else { "false" };
let file_containing_test = temp.path().join("src/first/nested.md");
@@ -79,13 +77,11 @@ pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
let content = file_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(
content.contains(s),
"Searching for {:?} in {}\n\n{}",
s,
filename.display(),
content
);
assert!(content.contains(s),
"Searching for {:?} in {}\n\n{}",
s,
filename.display(),
content);
}
}
@@ -94,16 +90,15 @@ pub fn assert_doesnt_contain_strings<P: AsRef<Path>>(filename: P, strings: &[&st
let content = file_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(
!content.contains(s),
"Found {:?} in {}\n\n{}",
s,
filename.display(),
content
);
assert!(!content.contains(s),
"Found {:?} in {}\n\n{}",
s,
filename.display(),
content);
}
}
/// Recursively copy an entire directory tree to somewhere else (a la `cp -r`).
fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()> {
let from = from.as_ref();
@@ -113,9 +108,9 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
let entry = entry.chain_err(|| "Unable to inspect directory entry")?;
let original_location = entry.path();
let relative = original_location
.strip_prefix(&from)
.expect("`original_location` is inside the `from` directory");
let relative = original_location.strip_prefix(&from)
.expect("`original_location` is inside the `from` \
directory");
let new_location = to.join(relative);
if original_location.is_file() {
@@ -123,8 +118,9 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
fs::create_dir_all(parent).chain_err(|| "Couldn't create directory")?;
}
fs::copy(&original_location, &new_location)
.chain_err(|| "Unable to copy file contents")?;
fs::copy(&original_location, &new_location).chain_err(|| {
"Unable to copy file contents"
})?;
}
}
@@ -133,7 +129,7 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
pub fn new_copy_of_example_book() -> Result<TempDir> {
let temp = TempFileBuilder::new().prefix("book-example").tempdir()?;
let book_example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
recursive_copy(book_example, temp.path())?;

View File

@@ -1,5 +0,0 @@
# Dummy Book
This file is just here to cause the index preprocessor to run.
Does a pretty good job, too.

View File

@@ -1,12 +1,10 @@
# Summary
[Dummy Book](README.md)
[Introduction](intro.md)
- [First Chapter](first/index.md)
- [Nested Chapter](first/nested.md)
- [Includes](first/includes.md)
- [Recursive](first/recursive.md)
- [Second Chapter](second.md)
---

View File

@@ -1,2 +0,0 @@
Around the world, around the world
{{#include recursive.md}}

View File

@@ -1 +0,0 @@
# Root README

View File

@@ -1,7 +0,0 @@
# This dummy book is for testing the conversion of README.md to index.html by IndexPreprocessor
[Root README](README.md)
- [1st README](first/README.md)
- [2nd README](second/README.md)
- [2nd index](second/index.md)

View File

@@ -1 +0,0 @@
# First README

View File

@@ -1 +0,0 @@
# Second README

View File

@@ -1 +0,0 @@
# Second index

Some files were not shown because too many files have changed in this diff Show More