mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 13:51:10 -05:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f04d7b802d | ||
|
|
bfcddf2680 | ||
|
|
2b649fe94f | ||
|
|
fc4236eaa7 | ||
|
|
a592da33bb | ||
|
|
6af6219e5b | ||
|
|
e5f74b6c86 | ||
|
|
84a2ab0dba | ||
|
|
d63ef8330d | ||
|
|
01e50303a2 | ||
|
|
2b3304cb8b | ||
|
|
4448f3fc4b | ||
|
|
859659f197 | ||
|
|
4a93eddae2 | ||
|
|
0173451b67 | ||
|
|
ac1749ff2f | ||
|
|
8cdeb121c5 | ||
|
|
74313bb701 | ||
|
|
3c25dba9b4 | ||
|
|
2387942588 | ||
|
|
93c9ae5700 | ||
|
|
9efa9fd1c4 | ||
|
|
8a33407cc5 | ||
|
|
699844a5c3 | ||
|
|
9bdec5e7cc | ||
|
|
930f730361 | ||
|
|
09c738468f | ||
|
|
a3d1afdd1f | ||
|
|
8e8e53ae15 | ||
|
|
5fe801a7d1 | ||
|
|
a6f317e352 | ||
|
|
ed95252f05 | ||
|
|
a058da8b74 | ||
|
|
73be1292ab | ||
|
|
98ecd1178b | ||
|
|
996ac382c1 | ||
|
|
b88839cc25 | ||
|
|
1ef94c2a7e | ||
|
|
f0ac13e3e2 | ||
|
|
b0ae14a2c7 | ||
|
|
81ab2eb7db | ||
|
|
213171591a | ||
|
|
db13d8e561 | ||
|
|
b4bb44292d | ||
|
|
bb7a863d3e | ||
|
|
e62a9dba87 | ||
|
|
4a94b656cd | ||
|
|
a873d46871 | ||
|
|
ce0c5f1d07 | ||
|
|
33d7e86fb6 | ||
|
|
f9f9785839 | ||
|
|
0c37b912ba | ||
|
|
e880fb6339 | ||
|
|
a8d6337ac6 | ||
|
|
f37a89cd4c | ||
|
|
aaeb3e2852 | ||
|
|
8c4b292d58 | ||
|
|
40159362c0 | ||
|
|
aa67245743 | ||
|
|
d968443074 | ||
|
|
3716123e10 | ||
|
|
50a2ec3cf1 | ||
|
|
07459aef60 | ||
|
|
0f56c09d3a | ||
|
|
63ad3d9340 | ||
|
|
1c5dc1e310 | ||
|
|
77af889a2e | ||
|
|
e48fed74bf | ||
|
|
e512850c13 | ||
|
|
bb412edf53 | ||
|
|
5b0a23ebab | ||
|
|
e56c41a1c2 | ||
|
|
d1b5a8f982 | ||
|
|
f396623b63 | ||
|
|
9ec43b6c6d |
39
.github/workflows/deploy.yml
vendored
Normal file
39
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Deploy
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Deploy Release
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install hub
|
||||
run: ci/install-hub.sh ${{ matrix.os }}
|
||||
shell: bash
|
||||
- name: Install Rustup
|
||||
run: ci/install-rustup.sh stable
|
||||
shell: bash
|
||||
- name: Install Rust
|
||||
run: ci/install-rust.sh stable
|
||||
shell: bash
|
||||
- name: Build and deploy artifacts
|
||||
run: ci/make-release.sh ${{ matrix.os }}
|
||||
shell: bash
|
||||
pages:
|
||||
name: GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Deploy to GitHub Pages
|
||||
run: ci/deploy-gh-pages.sh
|
||||
53
.github/workflows/main.yml
vendored
Normal file
53
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: CI
|
||||
on:
|
||||
# Only run when merging to master, or open/synchronize/reopen a PR.
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
build: [stable, beta, nightly, macos, windows, msrv]
|
||||
include:
|
||||
- build: stable
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
- build: beta
|
||||
os: ubuntu-latest
|
||||
rust: beta
|
||||
- build: nightly
|
||||
os: ubuntu-latest
|
||||
rust: nightly
|
||||
- build: macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
- build: windows
|
||||
os: windows-latest
|
||||
rust: stable
|
||||
- build: msrv
|
||||
os: ubuntu-latest
|
||||
rust: 1.35.0
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rustup
|
||||
run: bash ci/install-rustup.sh ${{ matrix.rust }}
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh ${{ matrix.rust }}
|
||||
- name: Build and run tests
|
||||
run: cargo test
|
||||
- name: Test no default
|
||||
run: cargo test --no-default-features
|
||||
|
||||
rustfmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust
|
||||
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||
- run: cargo fmt -- --check
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,3 +9,5 @@ book-example/book
|
||||
.vscode
|
||||
tests/dummy_book/book/
|
||||
|
||||
# Ignore Jetbrains specific files.
|
||||
.idea/
|
||||
|
||||
82
.travis.yml
82
.travis.yml
@@ -1,82 +0,0 @@
|
||||
language: rust
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- "$HOME/.cargo"
|
||||
- "$HOME/.cache/sccache"
|
||||
|
||||
env:
|
||||
global:
|
||||
- CRATE_NAME=mdbook
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- rust: stable
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
- rust: beta
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
- rust: nightly
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
- rust: 1.34.0 # Minimum required Rust version
|
||||
env: TARGET=x86_64-unknown-linux-gnu
|
||||
|
||||
- rust: stable
|
||||
os: osx
|
||||
env: TARGET=x86_64-apple-darwin
|
||||
|
||||
before_install:
|
||||
- |
|
||||
export RUSTC_WRAPPER=sccache;
|
||||
cd "$(mktemp -d)";
|
||||
case "$TRAVIS_OS_NAME" in
|
||||
linux )
|
||||
travis_retry curl -sSL 'https://github.com/mozilla/sccache/releases/download/0.2.9/sccache-0.2.9-x86_64-unknown-linux-musl.tar.gz' | tar -xzf - --strip-components=1 &&
|
||||
sudo cp sccache /usr/local/bin/sccache;
|
||||
;;
|
||||
osx )
|
||||
travis_retry curl -sSL 'https://github.com/mozilla/sccache/releases/download/0.2.9/sccache-0.2.9-x86_64-apple-darwin.tar.gz' | tar -xzf - --strip-components=1 &&
|
||||
sudo cp sccache /usr/local/bin/sccache;
|
||||
;;
|
||||
* ) unset RUSTC_WRAPPER;;
|
||||
esac;
|
||||
cd "$TRAVIS_BUILD_DIR";
|
||||
|
||||
script:
|
||||
- cargo test --all
|
||||
- cargo test --all --no-default-features
|
||||
- |
|
||||
if [ "$TARGET" = x86_64-unknown-linux-gnu ] && [ "$TRAVIS_RUST_VERSION" = stable ]; then
|
||||
rustup component add rustfmt && \
|
||||
cargo fmt --all -- --check;
|
||||
fi
|
||||
|
||||
before_deploy:
|
||||
- cargo run -- build book-example
|
||||
- sh ci/before_deploy.sh
|
||||
|
||||
deploy:
|
||||
- provider: releases
|
||||
api_key: "$GITHUB_TOKEN"
|
||||
file_glob: true
|
||||
file: "$CRATE_NAME-$TRAVIS_TAG-$TARGET.*"
|
||||
on:
|
||||
condition: "$TRAVIS_RUST_VERSION = stable"
|
||||
tags: true
|
||||
skip_cleanup: true
|
||||
- provider: pages
|
||||
local_dir: book-example/book
|
||||
skip_cleanup: true
|
||||
github_token: "$GITHUB_TOKEN"
|
||||
keep_history: false
|
||||
on:
|
||||
condition: $TRAVIS_OS_NAME = "linux" && $TRAVIS_RUST_VERSION = "stable"
|
||||
tags: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
73
CHANGELOG.md
73
CHANGELOG.md
@@ -1,5 +1,78 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.3.2
|
||||
[9cd47eb...2b649fe](https://github.com/rust-lang-nursery/mdBook/compare/9cd47eb...2b649fe)
|
||||
|
||||
### Added
|
||||
- Added a markdown renderer, which is off by default. This may be useful for
|
||||
debugging preprocessors.
|
||||
[#1018](https://github.com/rust-lang-nursery/mdBook/pull/1018)
|
||||
- Code samples may now include line numbers with the
|
||||
`output.html.playpen.line-numbers` configuration value.
|
||||
[#1035](https://github.com/rust-lang-nursery/mdBook/pull/1035)
|
||||
- The `watch` and `serve` commands will now ignore files listed in
|
||||
`.gitignore`.
|
||||
[#1044](https://github.com/rust-lang-nursery/mdBook/pull/1044)
|
||||
- Added automatic dark-theme detection based on the CSS `prefers-color-scheme`
|
||||
feature. This may be enabled by setting `output.html.preferred-dark-theme`
|
||||
to your preferred dark theme.
|
||||
[#1037](https://github.com/rust-lang-nursery/mdBook/pull/1037)
|
||||
- Added `rustdoc_include` preprocessor. This makes it easier to include
|
||||
portions of an external Rust source file. The rest of the file is hidden,
|
||||
but the user may expand it to see the entire file, and will continue to work
|
||||
with `mdbook test`.
|
||||
[#1003](https://github.com/rust-lang-nursery/mdBook/pull/1003)
|
||||
- Added Ctrl-Enter shortcut to the playpen editor to automatically run the
|
||||
sample.
|
||||
[#1066](https://github.com/rust-lang-nursery/mdBook/pull/1066)
|
||||
- Added `output.html.playpen.copyable` configuration option to disable
|
||||
the copy button.
|
||||
[#1050](https://github.com/rust-lang-nursery/mdBook/pull/1050)
|
||||
- Added ability to dynamically expand and fold sections within the sidebar.
|
||||
See the `output.html.fold` configuration to enable this feature.
|
||||
[#1027](https://github.com/rust-lang-nursery/mdBook/pull/1027)
|
||||
|
||||
### Changed
|
||||
- Use standard `scrollbar-color` CSS along with webkit extension
|
||||
[#816](https://github.com/rust-lang-nursery/mdBook/pull/816)
|
||||
- The renderer build directory is no longer deleted before the renderer is
|
||||
run. This allows a backend to cache results between runs.
|
||||
[#985](https://github.com/rust-lang-nursery/mdBook/pull/985)
|
||||
- Next/prev links now highlight on hover to indicate it is clickable.
|
||||
[#994](https://github.com/rust-lang-nursery/mdBook/pull/994)
|
||||
- Increase padding of table headers.
|
||||
[#824](https://github.com/rust-lang-nursery/mdBook/pull/824)
|
||||
- Errors in `[output.html]` config are no longer ignored.
|
||||
[#1033](https://github.com/rust-lang-nursery/mdBook/pull/1033)
|
||||
- Updated highlight.js for syntax highlighting updates (primarily to add
|
||||
async/await to Rust highlighting).
|
||||
[#1041](https://github.com/rust-lang-nursery/mdBook/pull/1041)
|
||||
- Raised minimum supported rust version to 1.35.
|
||||
[#1003](https://github.com/rust-lang-nursery/mdBook/pull/1003)
|
||||
- Hidden code lines are no longer dynamically removed via JavaScript, but
|
||||
instead managed with CSS.
|
||||
[#846](https://github.com/rust-lang-nursery/mdBook/pull/846)
|
||||
[#1065](https://github.com/rust-lang-nursery/mdBook/pull/1065)
|
||||
- Changed the default font set for the ACE editor, giving preference to
|
||||
"Source Code Pro".
|
||||
[#1062](https://github.com/rust-lang-nursery/mdBook/pull/1062)
|
||||
- Windows 32-bit releases are no longer published.
|
||||
[#1071](https://github.com/rust-lang-nursery/mdBook/pull/1071)
|
||||
|
||||
### Fixed
|
||||
- Fixed sidebar auto-scrolling.
|
||||
[#1052](https://github.com/rust-lang-nursery/mdBook/pull/1052)
|
||||
- Fixed error message when running `clean` multiple times.
|
||||
[#1055](https://github.com/rust-lang-nursery/mdBook/pull/1055)
|
||||
- Actually fix the "next" link on index.html. The previous fix didn't work.
|
||||
[#1005](https://github.com/rust-lang-nursery/mdBook/pull/1005)
|
||||
- Stop using `inline-block` for `inline code`, fixing selection highlighting
|
||||
and some rendering issues.
|
||||
[#1058](https://github.com/rust-lang-nursery/mdBook/pull/1058)
|
||||
- Fix header auto-hide on browsers with momentum scrolling that allows
|
||||
negative `scrollTop`.
|
||||
[#1070](https://github.com/rust-lang-nursery/mdBook/pull/1070)
|
||||
|
||||
## mdBook 0.3.1
|
||||
[69a08ef...9cd47eb](https://github.com/rust-lang-nursery/mdBook/compare/69a08ef...9cd47eb)
|
||||
|
||||
|
||||
978
Cargo.lock
generated
978
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
@@ -38,15 +38,16 @@ toml-query = "0.9"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "4.0", optional = true }
|
||||
gitignore = { version = "1.0", optional = true }
|
||||
|
||||
# Serve feature
|
||||
iron = { version = "0.6", optional = true }
|
||||
staticfile = { version = "0.5", optional = true }
|
||||
ws = { version = "0.8", optional = true}
|
||||
ws = { version = "0.9", optional = true}
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
||||
ammonia = { version = "2.1.2", optional = true }
|
||||
ammonia = { version = "3", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
select = "0.4"
|
||||
@@ -57,7 +58,7 @@ walkdir = "2.0"
|
||||
default = ["output", "watch", "serve", "search"]
|
||||
debug = []
|
||||
output = []
|
||||
watch = ["notify"]
|
||||
watch = ["notify", "gitignore"]
|
||||
serve = ["iron", "staticfile", "ws"]
|
||||
search = ["elasticlunr-rs", "ammonia"]
|
||||
|
||||
|
||||
28
README.md
28
README.md
@@ -1,25 +1,8 @@
|
||||
# mdBook
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td><strong>Linux / OS X</strong></td>
|
||||
<td>
|
||||
<a href="https://travis-ci.com/rust-lang-nursery/mdBook"><img src="https://travis-ci.com/rust-lang-nursery/mdBook.svg?branch=master"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Windows</strong></td>
|
||||
<td>
|
||||
<a href="https://ci.appveyor.com/project/rust-lang-libs/mdbook"><img src="https://ci.appveyor.com/api/projects/status/ysyke2rvo85sni55?svg=true"></a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<a href="https://crates.io/crates/mdbook"><img src="https://img.shields.io/crates/v/mdbook.svg"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/rust-lang-nursery/mdBook.svg"></a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
[](https://github.com/rust-lang-nursery/mdBook/actions?workflow=CI)
|
||||
[](https://crates.io/crates/mdbook)
|
||||
[](LICENSE)
|
||||
|
||||
mdBook is a utility to create modern online books from Markdown files.
|
||||
|
||||
@@ -41,7 +24,7 @@ There are multiple ways to install mdBook.
|
||||
|
||||
2. **From Crates.io**
|
||||
|
||||
This requires at least [Rust] 1.34 and Cargo to be installed. Once you have installed
|
||||
This requires at least [Rust] 1.35 and Cargo to be installed. Once you have installed
|
||||
Rust, type the following in the terminal:
|
||||
|
||||
```
|
||||
@@ -169,6 +152,9 @@ format, however there's nothing stopping a renderer from doing static analysis
|
||||
of a book in order to validate links or run tests. Some existing renderers are:
|
||||
|
||||
- `html` - the built-in renderer which will generate a HTML version of the book
|
||||
- `markdown` - the built-in renderer (disabled by default) which will run
|
||||
preprocessors then output the resulting Markdown. Useful for debugging
|
||||
preprocessors.
|
||||
- [`linkcheck`] - a backend which will check that all links are valid
|
||||
- [`epub`] - an experimental EPUB generator
|
||||
|
||||
|
||||
59
appveyor.yml
59
appveyor.yml
@@ -1,59 +0,0 @@
|
||||
environment:
|
||||
global:
|
||||
PROJECT_NAME: mdBook
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: stable
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: stable
|
||||
# Nightly channel
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: nightly
|
||||
|
||||
# Install Rust and Cargo
|
||||
install:
|
||||
# Since rust-lang-libs is currently sharing 1 builder, only run 1 job when a
|
||||
# PR is opened. Merges to master or tags will run all jobs.
|
||||
- ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -gt 0 -and ($env:TARGET -ne "x86_64-pc-windows-msvc" -or $env:RUST_CHANNEL -ne "stable") ) {Exit-AppveyorBuild}
|
||||
- ps: >-
|
||||
If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\msys64\mingw64\bin'
|
||||
} ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') {
|
||||
$Env:PATH += ';C:\msys64\mingw32\bin'
|
||||
}
|
||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
|
||||
- rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_CHANNEL%
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -Vv
|
||||
- cargo -V
|
||||
|
||||
build: false
|
||||
|
||||
# Equivalent to Travis' `script` phase
|
||||
test_script:
|
||||
- cargo test --all
|
||||
- cargo test --all --no-default-features
|
||||
|
||||
before_deploy:
|
||||
# Generate artifacts for release
|
||||
- cargo rustc --bin mdbook --release -- -C lto
|
||||
- mkdir staging
|
||||
- copy target\release\mdbook.exe staging
|
||||
- cd staging
|
||||
- 7z a ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip *
|
||||
- appveyor PushArtifact ../%PROJECT_NAME%-%APPVEYOR_REPO_TAG_NAME%-%TARGET%.zip
|
||||
|
||||
deploy:
|
||||
description: 'Windows release'
|
||||
artifact: /.*\.zip/
|
||||
auth_token: $(GITHUB_TOKEN)
|
||||
provider: GitHub
|
||||
on:
|
||||
RUST_CHANNEL: stable
|
||||
appveyor_repo_tag: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
@@ -9,6 +9,7 @@ mathjax-support = true
|
||||
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
line-numbers = true
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 20
|
||||
|
||||
@@ -93,7 +93,7 @@ Now we've got the basics running, we want to actually use it. First, install the
|
||||
program.
|
||||
|
||||
```shell
|
||||
$ cargo install
|
||||
$ cargo install --path .
|
||||
```
|
||||
|
||||
Then `cd` to the particular book you'd like to count the words of and update its
|
||||
@@ -261,6 +261,10 @@ in [`RenderContext`].
|
||||
> **Note:** There is no guarantee that the destination directory exists or is
|
||||
> empty (`mdbook` may leave the previous contents to let backends do caching),
|
||||
> so it's always a good idea to create it with `fs::create_dir_all()`.
|
||||
>
|
||||
> If the destination directory already exists, don't assume it will be empty.
|
||||
> To allow backends to cache the results from previous runs, `mdbook` may leave
|
||||
> old content in the directory.
|
||||
|
||||
There's always the possibility that an error will occur while processing a book
|
||||
(just look at all the `unwrap()`'s we've written already), so `mdbook` will
|
||||
@@ -304,7 +308,7 @@ like this:
|
||||
Now, if we reinstall the backend and build a book,
|
||||
|
||||
```shell
|
||||
$ cargo install --force
|
||||
$ cargo install --path . --force
|
||||
$ mdbook build /path/to/book
|
||||
...
|
||||
2018-01-16 21:21:39 [INFO] (mdbook::renderer): Invoking the "wordcount" renderer
|
||||
|
||||
@@ -27,7 +27,7 @@ limit-results = 15
|
||||
|
||||
## Supported configuration options
|
||||
|
||||
It is important to note that **any** relative path specified in the in the
|
||||
It is important to note that **any** relative path specified in the
|
||||
configuration will always be taken relative from the root of the book where the
|
||||
configuration file is located.
|
||||
|
||||
@@ -81,7 +81,7 @@ This controls the build process of your book.
|
||||
|
||||
The following preprocessors are available and included by default:
|
||||
|
||||
- `links`: Expand the `{{ #playpen }}` and `{{ #include }}` handlebars
|
||||
- `links`: Expand the `{{ #playpen }}`, `{{ #include }}`, and `{{ #rustdoc_include }}` handlebars
|
||||
helpers in a chapter to include the contents of a file.
|
||||
- `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
|
||||
@@ -150,6 +150,10 @@ The following configuration options are available:
|
||||
files with the ones found in the specified folder.
|
||||
- **default-theme:** The theme color scheme to select by default in the
|
||||
'Change Theme' dropdown. Defaults to `light`.
|
||||
- **preferred-dark-theme:** The default dark theme. This theme will be used if
|
||||
the browser requests the dark version of the site via the
|
||||
['prefers-color-scheme'](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
|
||||
CSS media query. Defaults to the same theme as `default-theme`.
|
||||
- **curly-quotes:** Convert straight quotes to curly quotes, except for those
|
||||
that occur in code blocks and code spans. Defaults to `false`.
|
||||
- **mathjax-support:** Adds support for [MathJax](mathjax.md). Defaults to
|
||||
@@ -166,6 +170,7 @@ The following configuration options are available:
|
||||
- **no-section-label:** mdBook by defaults adds section label in table of
|
||||
contents column. For example, "1.", "2.1". Set this option to true to disable
|
||||
those labels. Defaults to `false`.
|
||||
- **fold:** A subtable for configuring sidebar section-folding behavior.
|
||||
- **playpen:** A subtable for configuring various playpen settings.
|
||||
- **search:** A subtable for configuring the in-browser search functionality.
|
||||
mdBook must be compiled with the `search` feature enabled (on by default).
|
||||
@@ -173,12 +178,21 @@ The following configuration options are available:
|
||||
an icon link will be output in the menu bar of the book.
|
||||
- **git-repository-icon:** The FontAwesome icon class to use for the git
|
||||
repository link. Defaults to `fa-github`.
|
||||
|
||||
Available configuration options for the `[output.html.fold]` table:
|
||||
|
||||
- **enable:** Enable section-folding. When off, all folds are open.
|
||||
Defaults to `false`.
|
||||
- **level:** The higher the more folded regions are open. When level is 0, all
|
||||
folds are closed. Defaults to `0`.
|
||||
|
||||
Available configuration options for the `[output.html.playpen]` table:
|
||||
|
||||
- **editable:** Allow editing the source code. Defaults to `false`.
|
||||
- **copyable:** Display the copy button on code snippets. Defaults to `true`.
|
||||
- **copy-js:** Copy JavaScript files for the editor to the output directory.
|
||||
Defaults to `true`.
|
||||
- **line-numbers** Display line numbers on editable sections of code. Requires both `editable` and `copy-js` to be `true`. Defaults to `false`.
|
||||
|
||||
[Ace]: https://ace.c9.io/
|
||||
|
||||
@@ -189,7 +203,7 @@ Available configuration options for the `[output.html.search]` table:
|
||||
- **teaser-word-count:** The number of words used for a search result teaser.
|
||||
Defaults to `30`.
|
||||
- **use-boolean-and:** Define the logical link between multiple search words. If
|
||||
true, all search words must appear in each result. Defaults to `true`.
|
||||
true, all search words must appear in each result. Defaults to `false`.
|
||||
- **boost-title:** Boost factor for the search result score if a search word
|
||||
appears in the header. Defaults to `2`.
|
||||
- **boost-hierarchy:** Boost factor for the search result score if a search word
|
||||
@@ -216,6 +230,7 @@ description = "The example book covers examples."
|
||||
[output.html]
|
||||
theme = "my-theme"
|
||||
default-theme = "light"
|
||||
preferred-dark-theme = "navy"
|
||||
curly-quotes = true
|
||||
mathjax-support = false
|
||||
google-analytics = "123456"
|
||||
@@ -225,9 +240,14 @@ no-section-label = false
|
||||
git-repository-url = "https://github.com/rust-lang-nursery/mdBook"
|
||||
git-repository-icon = "fa-github"
|
||||
|
||||
[output.html.fold]
|
||||
enable = false
|
||||
level = 0
|
||||
|
||||
[output.html.playpen]
|
||||
editable = false
|
||||
copy-js = true
|
||||
line-numbers = false
|
||||
|
||||
[output.html.search]
|
||||
enable = true
|
||||
@@ -242,6 +262,26 @@ heading-split-level = 3
|
||||
copy-js = true
|
||||
```
|
||||
|
||||
### Markdown Renderer
|
||||
|
||||
The Markdown renderer will run preprocessors and then output the resulting
|
||||
Markdown. This is mostly useful for debugging preprocessors, especially in
|
||||
conjunction with `mdbook test` to see the Markdown that `mdbook` is passing
|
||||
to `rustdoc`.
|
||||
|
||||
The Markdown renderer is included with `mdbook` but disabled by default.
|
||||
Enable it by adding an emtpy table to your `book.toml` as follows:
|
||||
|
||||
```toml
|
||||
[output.markdown]
|
||||
```
|
||||
|
||||
There are no configuration options for the Markdown renderer at this time;
|
||||
only whether it is enabled or disabled.
|
||||
|
||||
See [the preprocessors documentation](#configuring-preprocessors) for how to
|
||||
specify which preprocessors should run before the Markdown renderer.
|
||||
|
||||
### Custom Renderers
|
||||
|
||||
A custom renderer can be enabled by adding a `[output.foo]` table to your
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them
|
||||
with a `#`.
|
||||
with a `#` [in the same way that Rustdoc does][rustdoc-hide].
|
||||
|
||||
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example
|
||||
|
||||
```bash
|
||||
# fn main() {
|
||||
@@ -107,6 +109,70 @@ This is the full file.
|
||||
|
||||
Lines containing anchor patterns inside the included anchor are ignored.
|
||||
|
||||
## Including a file but initially hiding all except specified lines
|
||||
|
||||
The `rustdoc_include` helper is for including code from external Rust files that contain complete
|
||||
examples, but only initially showing particular lines specified with line numbers or anchors in the
|
||||
same way as with `include`.
|
||||
|
||||
The lines not in the line number range or between the anchors will still be included, but they will
|
||||
be prefaced with `#`. This way, a reader can expand the snippet to see the complete example, and
|
||||
Rustdoc will use the complete example when you run `mdbook test`.
|
||||
|
||||
For example, consider a file named `file.rs` that contains this Rust program:
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
let x = add_one(2);
|
||||
assert_eq!(x, 3);
|
||||
}
|
||||
|
||||
fn add_one(num: i32) -> i32 {
|
||||
num + 1
|
||||
}
|
||||
```
|
||||
|
||||
We can include a snippet that initially shows only line 2 by using this syntax:
|
||||
|
||||
````hbs
|
||||
To call the `add_one` function, we pass it an `i32` and bind the returned value to `x`:
|
||||
|
||||
```rust
|
||||
\{{#rustdoc_include file.rs:2}}
|
||||
```
|
||||
````
|
||||
|
||||
This would have the same effect as if we had manually inserted the code and hidden all but line 2
|
||||
using `#`:
|
||||
|
||||
````hbs
|
||||
To call the `add_one` function, we pass it an `i32` and bind the returned value to `x`:
|
||||
|
||||
```rust
|
||||
# fn main() {
|
||||
let x = add_one(2);
|
||||
# assert_eq!(x, 3);
|
||||
# }
|
||||
#
|
||||
# fn add_one(num: i32) -> i32 {
|
||||
# num + 1
|
||||
#}
|
||||
```
|
||||
````
|
||||
|
||||
That is, it looks like this (click the "expand" icon to see the rest of the file):
|
||||
|
||||
```rust
|
||||
# fn main() {
|
||||
let x = add_one(2);
|
||||
# assert_eq!(x, 3);
|
||||
# }
|
||||
#
|
||||
# fn add_one(num: i32) -> i32 {
|
||||
# num + 1
|
||||
#}
|
||||
```
|
||||
|
||||
## Inserting runnable Rust files
|
||||
|
||||
With the following syntax, you can insert runnable Rust files into your book:
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# This script takes care of building your crate and packaging it for release
|
||||
|
||||
set -ex
|
||||
|
||||
main() {
|
||||
local src=$(pwd) \
|
||||
stage=
|
||||
|
||||
case $TRAVIS_OS_NAME in
|
||||
linux)
|
||||
stage=$(mktemp -d)
|
||||
;;
|
||||
osx)
|
||||
stage=$(mktemp -d -t tmp)
|
||||
;;
|
||||
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/
|
||||
|
||||
cd $stage
|
||||
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
|
||||
cd $src
|
||||
|
||||
rm -rf $stage
|
||||
}
|
||||
|
||||
main
|
||||
14
ci/deploy-gh-pages.sh
Executable file
14
ci/deploy-gh-pages.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Updates gh-pages with latest docs.
|
||||
set -ex
|
||||
|
||||
cargo run -- build book-example
|
||||
cd book-example/book
|
||||
touch .nojekyll
|
||||
git init
|
||||
git config --local user.email ""
|
||||
git config --local user.name "GitHub Deployer"
|
||||
git add .
|
||||
git commit -m "Deploy to gh-pages"
|
||||
remote="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
|
||||
git push "$remote" HEAD:gh-pages --force
|
||||
24
ci/install-hub.sh
Executable file
24
ci/install-hub.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env bash
|
||||
# Installs the `hub` executable into hub/bin
|
||||
set -ex
|
||||
case $1 in
|
||||
ubuntu*)
|
||||
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-linux-amd64-2.12.8.tgz -o hub.tgz
|
||||
mkdir hub
|
||||
tar -xzvf hub.tgz --strip=1 -C hub
|
||||
;;
|
||||
macos*)
|
||||
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-darwin-amd64-2.12.8.tgz -o hub.tgz
|
||||
mkdir hub
|
||||
tar -xzvf hub.tgz --strip=1 -C hub
|
||||
;;
|
||||
windows*)
|
||||
curl -LsSf https://github.com/github/hub/releases/download/v2.12.8/hub-windows-amd64-2.12.8.zip -o hub.zip
|
||||
7z x hub.zip -ohub
|
||||
;;
|
||||
*)
|
||||
echo "OS should be first parameter, was: $1"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "##[add-path]$PWD/hub/bin"
|
||||
19
ci/install-rust.sh
Executable file
19
ci/install-rust.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install/update rust.
|
||||
# The first argument should be the toolchain to install.
|
||||
|
||||
set -ex
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "First parameter must be toolchain to install."
|
||||
exit 1
|
||||
fi
|
||||
TOOLCHAIN="$1"
|
||||
|
||||
rustup set profile minimal
|
||||
rustup component remove --toolchain=$TOOLCHAIN rust-docs || echo "already removed"
|
||||
rustup update $TOOLCHAIN
|
||||
rustup default $TOOLCHAIN
|
||||
rustup -V
|
||||
rustc -Vv
|
||||
cargo -V
|
||||
26
ci/install-rustup.sh
Executable file
26
ci/install-rustup.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install/update rustup.
|
||||
# The first argument should be the toolchain to install.
|
||||
#
|
||||
# It is helpful to have this as a separate script due to some issues on
|
||||
# Windows where immediately after `rustup self update`, rustup can fail with
|
||||
# "Device or resource busy".
|
||||
|
||||
set -ex
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "First parameter must be toolchain to install."
|
||||
exit 1
|
||||
fi
|
||||
TOOLCHAIN="$1"
|
||||
|
||||
# Install/update rustup.
|
||||
if command -v rustup
|
||||
then
|
||||
echo `command -v rustup` `rustup -V` already installed
|
||||
rustup self update
|
||||
else
|
||||
# macOS currently does not have rust pre-installed.
|
||||
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $TOOLCHAIN --profile=minimal
|
||||
echo "##[add-path]$HOME/.cargo/bin"
|
||||
fi
|
||||
36
ci/make-release.sh
Executable file
36
ci/make-release.sh
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the release and creates an archive and optionally deploys to GitHub.
|
||||
set -ex
|
||||
|
||||
if [[ -z "$GITHUB_REF" ]]
|
||||
then
|
||||
echo "GITHUB_REF must be set"
|
||||
exit 1
|
||||
fi
|
||||
# Strip mdbook-refs/tags/ from the start of the ref.
|
||||
TAG=${GITHUB_REF#*/tags/}
|
||||
|
||||
host=$(rustc -Vv | grep ^host: | sed -e "s/host: //g")
|
||||
cargo rustc --bin mdbook --release -- -C lto
|
||||
cd target/release
|
||||
case $1 in
|
||||
ubuntu* | macos*)
|
||||
asset="mdbook-$TAG-$host.tar.gz"
|
||||
tar czf ../../$asset mdbook
|
||||
;;
|
||||
windows*)
|
||||
asset="mdbook-$TAG-$host.zip"
|
||||
7z a ../../$asset mdbook.exe
|
||||
;;
|
||||
*)
|
||||
echo "OS should be first parameter, was: $1"
|
||||
;;
|
||||
esac
|
||||
cd ../..
|
||||
|
||||
if [[ -z "$GITHUB_TOKEN" ]]
|
||||
then
|
||||
echo "$GITHUB_TOKEN not set, skipping deploy."
|
||||
else
|
||||
hub release edit -m "" --attach $asset $TAG
|
||||
fi
|
||||
@@ -24,7 +24,7 @@ use crate::errors::*;
|
||||
use crate::preprocess::{
|
||||
CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
|
||||
};
|
||||
use crate::renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
||||
use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
|
||||
use crate::utils;
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -190,19 +190,6 @@ impl MDBook {
|
||||
renderer.name().to_string(),
|
||||
);
|
||||
|
||||
let name = renderer.name();
|
||||
let build_dir = self.build_dir_for(name);
|
||||
if build_dir.exists() {
|
||||
debug!(
|
||||
"Cleaning build dir for the \"{}\" renderer ({})",
|
||||
name,
|
||||
build_dir.display()
|
||||
);
|
||||
|
||||
utils::fs::remove_dir_content(&build_dir)
|
||||
.chain_err(|| "Unable to clear output directory")?;
|
||||
}
|
||||
|
||||
for preprocessor in &self.preprocessors {
|
||||
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
|
||||
debug!("Running the {} preprocessor.", preprocessor.name());
|
||||
@@ -349,6 +336,8 @@ fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
|
||||
renderers.extend(output_table.iter().map(|(key, table)| {
|
||||
if key == "html" {
|
||||
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
|
||||
} else if key == "markdown" {
|
||||
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
|
||||
} else {
|
||||
interpret_custom_renderer(key, table)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,10 @@ pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
|
||||
Some(dest_dir) => dest_dir.into(),
|
||||
None => book.root.join(&book.config.build.build_dir),
|
||||
};
|
||||
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;
|
||||
|
||||
if dir_to_remove.exists() {
|
||||
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -48,6 +48,53 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_ignored_files(book_root: &PathBuf, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
if paths.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
match find_gitignore(book_root) {
|
||||
Some(gitignore_path) => {
|
||||
match gitignore::File::new(gitignore_path.as_path()) {
|
||||
Ok(exclusion_checker) => filter_ignored_files(exclusion_checker, paths),
|
||||
Err(_) => {
|
||||
// We're unable to read the .gitignore file, so we'll silently allow everything.
|
||||
// Please see discussion: https://github.com/rust-lang-nursery/mdBook/pull/1051
|
||||
paths.iter().map(|path| path.to_path_buf()).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// There is no .gitignore file.
|
||||
paths.iter().map(|path| path.to_path_buf()).collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_gitignore(book_root: &PathBuf) -> Option<PathBuf> {
|
||||
book_root
|
||||
.ancestors()
|
||||
.map(|p| p.join(".gitignore"))
|
||||
.find(|p| p.exists())
|
||||
}
|
||||
|
||||
fn filter_ignored_files(exclusion_checker: gitignore::File, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
paths
|
||||
.iter()
|
||||
.filter(|path| match exclusion_checker.is_excluded(path) {
|
||||
Ok(exclude) => !exclude,
|
||||
Err(error) => {
|
||||
warn!(
|
||||
"Unable to determine if {:?} is excluded: {:?}. Including it.",
|
||||
&path, error
|
||||
);
|
||||
true
|
||||
}
|
||||
})
|
||||
.map(|path| path.to_path_buf())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Calls the closure when a book source file is changed, blocking indefinitely.
|
||||
pub fn trigger_on_change<F>(book: &MDBook, closure: F)
|
||||
where
|
||||
@@ -96,8 +143,12 @@ where
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
closure(paths, &book.root);
|
||||
let paths = remove_ignored_files(&book.root, &paths[..]);
|
||||
|
||||
if !paths.is_empty() {
|
||||
closure(paths, &book.root);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
107
src/config.rs
107
src/config.rs
@@ -40,8 +40,8 @@
|
||||
//! cfg.set("output.html.theme", "./themes");
|
||||
//!
|
||||
//! // then load it again, automatically deserializing to a `PathBuf`.
|
||||
//! let got: PathBuf = cfg.get_deserialized("output.html.theme")?;
|
||||
//! assert_eq!(got, PathBuf::from("./themes"));
|
||||
//! let got: Option<PathBuf> = cfg.get_deserialized_opt("output.html.theme")?;
|
||||
//! assert_eq!(got, Some(PathBuf::from("./themes")));
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # fn main() { run().unwrap() }
|
||||
@@ -62,6 +62,7 @@ use toml_query::insert::TomlValueInsertExt;
|
||||
use toml_query::read::TomlValueReadExt;
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::utils;
|
||||
|
||||
/// The overall configuration object for MDBook, essentially an in-memory
|
||||
/// representation of `book.toml`.
|
||||
@@ -168,22 +169,41 @@ impl Config {
|
||||
/// HTML renderer is refactored to be less coupled to `mdbook` internals.
|
||||
#[doc(hidden)]
|
||||
pub fn html_config(&self) -> Option<HtmlConfig> {
|
||||
self.get_deserialized("output.html").ok()
|
||||
match self.get_deserialized_opt("output.html") {
|
||||
Ok(Some(config)) => Some(config),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
utils::log_backtrace(&e.chain_err(|| "Parsing configuration [output.html]"));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deprecated, use get_deserialized_opt instead.
|
||||
#[deprecated = "use get_deserialized_opt instead"]
|
||||
pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
|
||||
let name = name.as_ref();
|
||||
match self.get_deserialized_opt(name)? {
|
||||
Some(value) => Ok(value),
|
||||
None => bail!("Key not found, {:?}", name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to fetch a value from the config and deserialize it
|
||||
/// into some arbitrary type.
|
||||
pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
|
||||
pub fn get_deserialized_opt<'de, T: Deserialize<'de>, S: AsRef<str>>(
|
||||
&self,
|
||||
name: S,
|
||||
) -> Result<Option<T>> {
|
||||
let name = name.as_ref();
|
||||
|
||||
if let Some(value) = self.get(name) {
|
||||
value
|
||||
.clone()
|
||||
.try_into()
|
||||
.chain_err(|| "Couldn't deserialize the value")
|
||||
} else {
|
||||
bail!("Key not found, {:?}", name)
|
||||
}
|
||||
self.get(name)
|
||||
.map(|value| {
|
||||
value
|
||||
.clone()
|
||||
.try_into()
|
||||
.chain_err(|| "Couldn't deserialize the value")
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// Set a config key, clobbering any existing values along the way.
|
||||
@@ -420,6 +440,9 @@ pub struct HtmlConfig {
|
||||
pub theme: Option<PathBuf>,
|
||||
/// The default theme to use, defaults to 'light'
|
||||
pub default_theme: Option<String>,
|
||||
/// The theme to use if the browser requests the dark version of the site.
|
||||
/// Defaults to the same as 'default_theme'
|
||||
pub preferred_dark_theme: Option<String>,
|
||||
/// Use "smart quotes" instead of the usual `"` character.
|
||||
pub curly_quotes: bool,
|
||||
/// Should mathjax be enabled?
|
||||
@@ -431,16 +454,10 @@ pub struct HtmlConfig {
|
||||
/// Additional JS scripts to include at the bottom of the rendered page's
|
||||
/// `<body>`.
|
||||
pub additional_js: Vec<PathBuf>,
|
||||
/// Fold settings.
|
||||
pub fold: Fold,
|
||||
/// Playpen settings.
|
||||
pub playpen: Playpen,
|
||||
/// This is used as a bit of a workaround for the `mdbook serve` command.
|
||||
/// Basically, because you set the websocket port from the command line, the
|
||||
/// `mdbook serve` command needs a way to let the HTML renderer know where
|
||||
/// to point livereloading at, if it has been enabled.
|
||||
///
|
||||
/// This config item *should not be edited* by the end user.
|
||||
#[doc(hidden)]
|
||||
pub livereload_url: Option<String>,
|
||||
/// Don't render section labels.
|
||||
pub no_section_label: bool,
|
||||
/// Search settings. If `None`, the default will be used.
|
||||
@@ -450,6 +467,14 @@ pub struct HtmlConfig {
|
||||
/// FontAwesome icon class to use for the Git repository link.
|
||||
/// Defaults to `fa-github` if `None`.
|
||||
pub git_repository_icon: Option<String>,
|
||||
/// This is used as a bit of a workaround for the `mdbook serve` command.
|
||||
/// Basically, because you set the websocket port from the command line, the
|
||||
/// `mdbook serve` command needs a way to let the HTML renderer know where
|
||||
/// to point livereloading at, if it has been enabled.
|
||||
///
|
||||
/// This config item *should not be edited* by the end user.
|
||||
#[doc(hidden)]
|
||||
pub livereload_url: Option<String>,
|
||||
}
|
||||
|
||||
impl HtmlConfig {
|
||||
@@ -463,22 +488,40 @@ impl HtmlConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for how to fold chapters of sidebar.
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Fold {
|
||||
/// When off, all folds are open. Default: `false`.
|
||||
pub enable: bool,
|
||||
/// The higher the more folded regions are open. When level is 0, all folds
|
||||
/// are closed.
|
||||
/// Default: `0`.
|
||||
pub level: u8,
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the the HTML renderer handles the playpen.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Playpen {
|
||||
/// Should playpen snippets be editable? Default: `false`.
|
||||
pub editable: bool,
|
||||
/// Display the copy button. Default: `true`.
|
||||
pub copyable: bool,
|
||||
/// Copy JavaScript files for the editor to the output directory?
|
||||
/// Default: `true`.
|
||||
pub copy_js: bool,
|
||||
/// Display line numbers on playpen snippets. Default: `false`.
|
||||
pub line_numbers: bool,
|
||||
}
|
||||
|
||||
impl Default for Playpen {
|
||||
fn default() -> Playpen {
|
||||
Playpen {
|
||||
editable: false,
|
||||
copyable: true,
|
||||
copy_js: true,
|
||||
line_numbers: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -494,7 +537,7 @@ pub struct Search {
|
||||
/// 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`.
|
||||
/// If true, all search words must appear in each result. Default: `false`.
|
||||
pub use_boolean_and: bool,
|
||||
/// Boost factor for the search result score if a search word appears in the header.
|
||||
/// Default: `2`.
|
||||
@@ -612,7 +655,9 @@ mod tests {
|
||||
};
|
||||
let playpen_should_be = Playpen {
|
||||
editable: true,
|
||||
copyable: true,
|
||||
copy_js: true,
|
||||
line_numbers: false,
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
@@ -656,11 +701,14 @@ mod tests {
|
||||
};
|
||||
|
||||
let cfg = Config::from_str(src).unwrap();
|
||||
let got: RandomOutput = cfg.get_deserialized("output.random").unwrap();
|
||||
let got: RandomOutput = cfg.get_deserialized_opt("output.random").unwrap().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
let got_baz: Vec<bool> = cfg.get_deserialized("output.random.baz").unwrap();
|
||||
let got_baz: Vec<bool> = cfg
|
||||
.get_deserialized_opt("output.random.baz")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let baz_should_be = vec![true, true, false];
|
||||
|
||||
assert_eq!(got_baz, baz_should_be);
|
||||
@@ -740,7 +788,7 @@ mod tests {
|
||||
assert!(cfg.get(key).is_none());
|
||||
cfg.set(key, value).unwrap();
|
||||
|
||||
let got: String = cfg.get_deserialized(key).unwrap();
|
||||
let got: String = cfg.get_deserialized_opt(key).unwrap().unwrap();
|
||||
assert_eq!(got, value);
|
||||
}
|
||||
|
||||
@@ -781,7 +829,10 @@ mod tests {
|
||||
|
||||
cfg.update_from_env();
|
||||
|
||||
assert_eq!(cfg.get_deserialized::<String, _>(key).unwrap(), value);
|
||||
assert_eq!(
|
||||
cfg.get_deserialized_opt::<String, _>(key).unwrap().unwrap(),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -800,7 +851,9 @@ mod tests {
|
||||
cfg.update_from_env();
|
||||
|
||||
assert_eq!(
|
||||
cfg.get_deserialized::<serde_json::Value, _>(key).unwrap(),
|
||||
cfg.get_deserialized_opt::<serde_json::Value, _>(key)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use crate::errors::*;
|
||||
use crate::utils::{take_anchored_lines, take_lines};
|
||||
use crate::utils::{
|
||||
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
|
||||
take_rustdoc_include_lines,
|
||||
};
|
||||
use regex::{CaptureMatches, Captures, Regex};
|
||||
use std::fs;
|
||||
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
|
||||
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
@@ -11,8 +14,15 @@ use crate::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.
|
||||
/// A preprocessor for expanding helpers in a chapter. Supported helpers are:
|
||||
///
|
||||
/// - `{{# include}}` - Insert an external file of any type. Include the whole file, only particular
|
||||
///. lines, or only between the specified anchors.
|
||||
/// - `{{# rustdoc_include}}` - Insert an external Rust file, showing the particular lines
|
||||
///. specified or the lines between specified anchors, and include the rest of the file behind `#`.
|
||||
/// This hides the lines from initial display but shows them when the reader expands the code
|
||||
/// block and provides them to Rustdoc for testing.
|
||||
/// - `{{# playpen}}` - Insert runnable Rust files
|
||||
#[derive(Default)]
|
||||
pub struct LinkPreprocessor;
|
||||
|
||||
@@ -102,12 +112,68 @@ where
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum LinkType<'a> {
|
||||
Escaped,
|
||||
IncludeRange(PathBuf, Range<usize>),
|
||||
IncludeRangeFrom(PathBuf, RangeFrom<usize>),
|
||||
IncludeRangeTo(PathBuf, RangeTo<usize>),
|
||||
IncludeRangeFull(PathBuf, RangeFull),
|
||||
IncludeAnchor(PathBuf, String),
|
||||
Include(PathBuf, RangeOrAnchor),
|
||||
Playpen(PathBuf, Vec<&'a str>),
|
||||
RustdocInclude(PathBuf, RangeOrAnchor),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum RangeOrAnchor {
|
||||
Range(LineRange),
|
||||
Anchor(String),
|
||||
}
|
||||
|
||||
// A range of lines specified with some include directive.
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum LineRange {
|
||||
Range(Range<usize>),
|
||||
RangeFrom(RangeFrom<usize>),
|
||||
RangeTo(RangeTo<usize>),
|
||||
RangeFull(RangeFull),
|
||||
}
|
||||
|
||||
impl RangeBounds<usize> for LineRange {
|
||||
fn start_bound(&self) -> Bound<&usize> {
|
||||
match self {
|
||||
LineRange::Range(r) => r.start_bound(),
|
||||
LineRange::RangeFrom(r) => r.start_bound(),
|
||||
LineRange::RangeTo(r) => r.start_bound(),
|
||||
LineRange::RangeFull(r) => r.start_bound(),
|
||||
}
|
||||
}
|
||||
|
||||
fn end_bound(&self) -> Bound<&usize> {
|
||||
match self {
|
||||
LineRange::Range(r) => r.end_bound(),
|
||||
LineRange::RangeFrom(r) => r.end_bound(),
|
||||
LineRange::RangeTo(r) => r.end_bound(),
|
||||
LineRange::RangeFull(r) => r.end_bound(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Range<usize>> for LineRange {
|
||||
fn from(r: Range<usize>) -> LineRange {
|
||||
LineRange::Range(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RangeFrom<usize>> for LineRange {
|
||||
fn from(r: RangeFrom<usize>) -> LineRange {
|
||||
LineRange::RangeFrom(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RangeTo<usize>> for LineRange {
|
||||
fn from(r: RangeTo<usize>) -> LineRange {
|
||||
LineRange::RangeTo(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RangeFull> for LineRange {
|
||||
fn from(r: RangeFull) -> LineRange {
|
||||
LineRange::RangeFull(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> LinkType<'a> {
|
||||
@@ -115,12 +181,9 @@ impl<'a> LinkType<'a> {
|
||||
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::IncludeAnchor(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -132,51 +195,54 @@ fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
fn parse_include_path(path: &str) -> LinkType<'static> {
|
||||
let mut parts = path.split(':');
|
||||
let path = parts.next().unwrap().into();
|
||||
fn parse_range_or_anchor(parts: Option<&str>) -> RangeOrAnchor {
|
||||
let mut parts = parts.unwrap_or("").splitn(3, ':').fuse();
|
||||
|
||||
let next_element = parts.next();
|
||||
let start = if let Some(value) = next_element.and_then(|s| s.parse::<usize>().ok()) {
|
||||
// subtract 1 since line numbers usually begin with 1
|
||||
Some(value.saturating_sub(1))
|
||||
} else if let Some("") = next_element {
|
||||
None
|
||||
} else if let Some(anchor) = next_element {
|
||||
if anchor == "" {
|
||||
None
|
||||
} else {
|
||||
return LinkType::IncludeAnchor(path, String::from(anchor));
|
||||
}
|
||||
return RangeOrAnchor::Anchor(String::from(anchor));
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let end = parts.next();
|
||||
let has_end = end.is_some();
|
||||
let end = end.and_then(|s| s.parse::<usize>().ok());
|
||||
match start {
|
||||
Some(start) => match end {
|
||||
Some(end) => LinkType::IncludeRange(path, Range { start, end }),
|
||||
None => {
|
||||
if has_end {
|
||||
LinkType::IncludeRangeFrom(path, RangeFrom { start })
|
||||
} else {
|
||||
LinkType::IncludeRange(
|
||||
path,
|
||||
Range {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
None => match end {
|
||||
Some(end) => LinkType::IncludeRangeTo(path, RangeTo { end }),
|
||||
None => LinkType::IncludeRangeFull(path, RangeFull),
|
||||
},
|
||||
// If `end` is empty string or any other value that can't be parsed as a usize, treat this
|
||||
// include as a range with only a start bound. However, if end isn't specified, include only
|
||||
// the single line specified by `start`.
|
||||
let end = end.map(|s| s.parse::<usize>());
|
||||
|
||||
match (start, end) {
|
||||
(Some(start), Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(start..end)),
|
||||
(Some(start), Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(start..)),
|
||||
(Some(start), None) => RangeOrAnchor::Range(LineRange::from(start..start + 1)),
|
||||
(None, Some(Ok(end))) => RangeOrAnchor::Range(LineRange::from(..end)),
|
||||
(None, None) | (None, Some(Err(_))) => RangeOrAnchor::Range(LineRange::from(RangeFull)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_include_path(path: &str) -> LinkType<'static> {
|
||||
let mut parts = path.splitn(2, ':');
|
||||
|
||||
let path = parts.next().unwrap().into();
|
||||
let range_or_anchor = parse_range_or_anchor(parts.next());
|
||||
|
||||
LinkType::Include(path, range_or_anchor)
|
||||
}
|
||||
|
||||
fn parse_rustdoc_include_path(path: &str) -> LinkType<'static> {
|
||||
let mut parts = path.splitn(2, ':');
|
||||
|
||||
let path = parts.next().unwrap().into();
|
||||
let range_or_anchor = parse_range_or_anchor(parts.next());
|
||||
|
||||
LinkType::RustdocInclude(path, range_or_anchor)
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
struct Link<'a> {
|
||||
start_index: usize,
|
||||
@@ -196,6 +262,7 @@ impl<'a> Link<'a> {
|
||||
match (typ.as_str(), file_arg) {
|
||||
("include", Some(pth)) => Some(parse_include_path(pth)),
|
||||
("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
|
||||
("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -220,11 +287,14 @@ impl<'a> Link<'a> {
|
||||
match self.link_type {
|
||||
// omit the escape char
|
||||
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
||||
LinkType::IncludeRange(ref pat, ref range) => {
|
||||
LinkType::Include(ref pat, ref range_or_anchor) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
fs::read_to_string(&target)
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.map(|s| match range_or_anchor {
|
||||
RangeOrAnchor::Range(range) => take_lines(&s, range.clone()),
|
||||
RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor),
|
||||
})
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
@@ -233,48 +303,18 @@ impl<'a> Link<'a> {
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeRangeFrom(ref pat, ref range) => {
|
||||
LinkType::RustdocInclude(ref pat, ref range_or_anchor) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
fs::read_to_string(&target)
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display(),
|
||||
)
|
||||
.map(|s| match range_or_anchor {
|
||||
RangeOrAnchor::Range(range) => {
|
||||
take_rustdoc_include_lines(&s, range.clone())
|
||||
}
|
||||
RangeOrAnchor::Anchor(anchor) => {
|
||||
take_rustdoc_include_anchored_lines(&s, anchor)
|
||||
}
|
||||
})
|
||||
}
|
||||
LinkType::IncludeRangeTo(ref pat, ref range) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
fs::read_to_string(&target)
|
||||
.map(|s| take_lines(&s, *range))
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeRangeFull(ref pat, _) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
fs::read_to_string(&target).chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::IncludeAnchor(ref pat, ref anchor) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
fs::read_to_string(&target)
|
||||
.map(|s| take_anchored_lines(&s, anchor))
|
||||
.chain_err(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
@@ -328,7 +368,7 @@ fn find_links(contents: &str) -> LinkIter<'_> {
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9]+) # link type
|
||||
\#([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"
|
||||
@@ -421,7 +461,10 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 48,
|
||||
link_type: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(9..20))
|
||||
),
|
||||
link_text: "{{#include file.rs:10:20}}",
|
||||
}]
|
||||
);
|
||||
@@ -437,7 +480,10 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 45,
|
||||
link_type: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(9..10))
|
||||
),
|
||||
link_text: "{{#include file.rs:10}}",
|
||||
}]
|
||||
);
|
||||
@@ -453,7 +499,10 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link_type: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(9..))
|
||||
),
|
||||
link_text: "{{#include file.rs:10:}}",
|
||||
}]
|
||||
);
|
||||
@@ -469,7 +518,10 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link_type: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..20))
|
||||
),
|
||||
link_text: "{{#include file.rs::20}}",
|
||||
}]
|
||||
);
|
||||
@@ -485,7 +537,10 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 44,
|
||||
link_type: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..))
|
||||
),
|
||||
link_text: "{{#include file.rs::}}",
|
||||
}]
|
||||
);
|
||||
@@ -501,7 +556,10 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 42,
|
||||
link_type: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..))
|
||||
),
|
||||
link_text: "{{#include file.rs}}",
|
||||
}]
|
||||
);
|
||||
@@ -517,9 +575,9 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 49,
|
||||
link_type: LinkType::IncludeAnchor(
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
String::from("anchor")
|
||||
RangeOrAnchor::Anchor(String::from("anchor"))
|
||||
),
|
||||
link_text: "{{#include file.rs:anchor}}",
|
||||
}]
|
||||
@@ -587,7 +645,10 @@ mod tests {
|
||||
Link {
|
||||
start_index: 38,
|
||||
end_index: 58,
|
||||
link_type: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..))
|
||||
),
|
||||
link_text: "{{#include file.rs}}",
|
||||
}
|
||||
);
|
||||
@@ -614,4 +675,183 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_without_colon_includes_all() {
|
||||
let link_type = parse_include_path("arbitrary");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_nothing_after_colon_includes_all() {
|
||||
let link_type = parse_include_path("arbitrary:");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_two_colons_includes_all() {
|
||||
let link_type = parse_include_path("arbitrary::");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_garbage_after_two_colons_includes_all() {
|
||||
let link_type = parse_include_path("arbitrary::NaN");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(RangeFull))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_one_number_after_colon_only_that_line() {
|
||||
let link_type = parse_include_path("arbitrary:5");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(4..5))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_one_based_start_becomes_zero_based() {
|
||||
let link_type = parse_include_path("arbitrary:1");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(0..1))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_zero_based_start_stays_zero_based_but_is_probably_an_error() {
|
||||
let link_type = parse_include_path("arbitrary:0");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(0..1))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_start_only_range() {
|
||||
let link_type = parse_include_path("arbitrary:5:");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(4..))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_start_with_garbage_interpreted_as_start_only_range() {
|
||||
let link_type = parse_include_path("arbitrary:5:NaN");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(4..))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_end_only_range() {
|
||||
let link_type = parse_include_path("arbitrary::5");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(..5))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_start_and_end_range() {
|
||||
let link_type = parse_include_path("arbitrary:5:10");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(4..10))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_negative_interpreted_as_anchor() {
|
||||
let link_type = parse_include_path("arbitrary:-5");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Anchor("-5".to_string())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_floating_point_interpreted_as_anchor() {
|
||||
let link_type = parse_include_path("arbitrary:-5.7");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Anchor("-5.7".to_string())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_anchor_followed_by_colon() {
|
||||
let link_type = parse_include_path("arbitrary:some-anchor:this-gets-ignored");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Anchor("some-anchor".to_string())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_more_than_three_colons_ignores_everything_after_third_colon() {
|
||||
let link_type = parse_include_path("arbitrary:5:10:17:anything:");
|
||||
assert_eq!(
|
||||
link_type,
|
||||
LinkType::Include(
|
||||
PathBuf::from("arbitrary"),
|
||||
RangeOrAnchor::Range(LineRange::from(4..10))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::theme::{self, playpen_editor, Theme};
|
||||
use crate::utils;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
@@ -71,6 +72,10 @@ impl HtmlHandlebars {
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&ch.path)),
|
||||
);
|
||||
if let Some(ref section) = ch.number {
|
||||
ctx.data
|
||||
.insert("section".to_owned(), json!(section.to_string()));
|
||||
}
|
||||
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
@@ -85,6 +90,7 @@ impl HtmlHandlebars {
|
||||
if ctx.is_index {
|
||||
ctx.data.insert("path".to_owned(), json!("index.md"));
|
||||
ctx.data.insert("path_to_root".to_owned(), json!(""));
|
||||
ctx.data.insert("is_index".to_owned(), json!("true"));
|
||||
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);
|
||||
@@ -282,6 +288,11 @@ impl Renderer for HtmlHandlebars {
|
||||
let destination = &ctx.destination;
|
||||
let book = &ctx.book;
|
||||
|
||||
if destination.exists() {
|
||||
utils::fs::remove_dir_content(destination)
|
||||
.chain_err(|| "Unable to remove stale HTML output")?;
|
||||
}
|
||||
|
||||
trace!("render");
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
@@ -376,7 +387,6 @@ fn make_data(
|
||||
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(
|
||||
@@ -402,19 +412,28 @@ fn make_data(
|
||||
};
|
||||
data.insert("default_theme".to_owned(), json!(default_theme));
|
||||
|
||||
let preferred_dark_theme = match html_config.preferred_dark_theme {
|
||||
Some(ref theme) => theme,
|
||||
None => default_theme,
|
||||
};
|
||||
data.insert(
|
||||
"preferred_dark_theme".to_owned(),
|
||||
json!(preferred_dark_theme),
|
||||
);
|
||||
|
||||
// Add google analytics tag
|
||||
if let Some(ref ga) = config.html_config().and_then(|html| html.google_analytics) {
|
||||
if let Some(ref ga) = html_config.google_analytics {
|
||||
data.insert("google_analytics".to_owned(), json!(ga));
|
||||
}
|
||||
|
||||
if html.mathjax_support {
|
||||
if html_config.mathjax_support {
|
||||
data.insert("mathjax_support".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional style
|
||||
if !html.additional_css.is_empty() {
|
||||
if !html_config.additional_css.is_empty() {
|
||||
let mut css = Vec::new();
|
||||
for style in &html.additional_css {
|
||||
for style in &html_config.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")),
|
||||
@@ -424,9 +443,9 @@ fn make_data(
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional script
|
||||
if !html.additional_js.is_empty() {
|
||||
if !html_config.additional_js.is_empty() {
|
||||
let mut js = Vec::new();
|
||||
for script in &html.additional_js {
|
||||
for script in &html_config.additional_js {
|
||||
match script.strip_prefix(root) {
|
||||
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => js.push(script.to_str().expect("Could not convert to str")),
|
||||
@@ -435,9 +454,18 @@ fn make_data(
|
||||
data.insert("additional_js".to_owned(), json!(js));
|
||||
}
|
||||
|
||||
if html.playpen.editable && html.playpen.copy_js {
|
||||
if html_config.playpen.editable && html_config.playpen.copy_js {
|
||||
data.insert("playpen_js".to_owned(), json!(true));
|
||||
if html_config.playpen.line_numbers {
|
||||
data.insert("playpen_line_numbers".to_owned(), json!(true));
|
||||
}
|
||||
}
|
||||
if html_config.playpen.copyable {
|
||||
data.insert("playpen_copyable".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
data.insert("fold_enable".to_owned(), json!((html_config.fold.enable)));
|
||||
data.insert("fold_level".to_owned(), json!((html_config.fold.level)));
|
||||
|
||||
let search = html_config.search.clone();
|
||||
if cfg!(feature = "search") {
|
||||
@@ -458,6 +486,7 @@ fn make_data(
|
||||
if let Some(ref git_repository_url) = html_config.git_repository_url {
|
||||
data.insert("git_repository_url".to_owned(), json!(git_repository_url));
|
||||
}
|
||||
|
||||
let git_repository_icon = match html_config.git_repository_icon {
|
||||
Some(ref git_repository_icon) => git_repository_icon,
|
||||
None => "fa-github",
|
||||
@@ -476,6 +505,11 @@ fn make_data(
|
||||
chapter.insert("section".to_owned(), json!(section.to_string()));
|
||||
}
|
||||
|
||||
chapter.insert(
|
||||
"has_sub_items".to_owned(),
|
||||
json!((!ch.sub_items.is_empty()).to_string()),
|
||||
);
|
||||
|
||||
chapter.insert("name".to_owned(), json!(ch.name));
|
||||
let path = ch
|
||||
.path
|
||||
@@ -567,6 +601,7 @@ fn fix_code_blocks(html: &str) -> String {
|
||||
}
|
||||
|
||||
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||
let boring_line_regex = Regex::new(r"^(\s*)#(#|.)(.*)$").unwrap();
|
||||
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
@@ -580,21 +615,51 @@ fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||
|| 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!")
|
||||
{
|
||||
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=\"{}\">{}</code></pre>",
|
||||
classes,
|
||||
{
|
||||
let content: Cow<'_, str> = if playpen_config.editable
|
||||
&& classes.contains("editable")
|
||||
|| text.contains("fn main")
|
||||
|| text.contains("quick_main!")
|
||||
{
|
||||
code.into()
|
||||
} 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!(
|
||||
"\n# #![allow(unused_variables)]\n{}#fn main() {{\n{}#}}",
|
||||
attrs, code
|
||||
)
|
||||
.into()
|
||||
};
|
||||
let mut prev_line_hidden = false;
|
||||
let mut result = String::with_capacity(content.len());
|
||||
for line in content.lines() {
|
||||
if let Some(caps) = boring_line_regex.captures(line) {
|
||||
if !prev_line_hidden && &caps[2] != "#" {
|
||||
result += "<span class=\"boring\">";
|
||||
prev_line_hidden = true;
|
||||
}
|
||||
result += &caps[1];
|
||||
if &caps[2] != " " {
|
||||
result += &caps[2];
|
||||
}
|
||||
result += &caps[3];
|
||||
} else {
|
||||
if prev_line_hidden {
|
||||
result += "</span>";
|
||||
prev_line_hidden = false;
|
||||
}
|
||||
result += line;
|
||||
}
|
||||
result += "\n";
|
||||
}
|
||||
result
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// not language-rust, so no-op
|
||||
text.to_owned()
|
||||
@@ -670,4 +735,28 @@ mod tests {
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_playpen() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playpen\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused_variables)]\nfn main() {\n</span>x()\n<span class=\"boring\">}\n</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playpen\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
|
||||
"<pre class=\"playpen\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
|
||||
"<pre class=\"playpen\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playpen_pre(
|
||||
src,
|
||||
&Playpen {
|
||||
editable: true,
|
||||
..Playpen::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,19 @@ fn find_chapter(
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
if !rc.evaluate(ctx, "@root/is_index")?.is_missing() {
|
||||
// Special case for index.md which may be a synthetic page.
|
||||
// Target::find won't match because there is no page with the path
|
||||
// "index.md" (unless there really is an index.md in SUMMARY.md).
|
||||
match target {
|
||||
Target::Previous => return Ok(None),
|
||||
Target::Next => match chapters.iter().skip(1).next() {
|
||||
Some(chapter) => return Ok(Some(chapter.clone())),
|
||||
None => return Ok(None),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let mut previous: Option<StringMap> = None;
|
||||
|
||||
debug!("Search for chapter");
|
||||
|
||||
@@ -28,13 +28,36 @@ impl HelperDef for RenderToc {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.as_json().clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
let current = rc
|
||||
let current_path = rc
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.ok_or(RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
|
||||
let current_section = rc
|
||||
.evaluate(ctx, "@root/section")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_default();
|
||||
|
||||
let fold_enable = rc
|
||||
.evaluate(ctx, "@root/fold_enable")?
|
||||
.as_json()
|
||||
.as_bool()
|
||||
.ok_or(RenderError::new(
|
||||
"Type error for `fold_enable`, bool expected",
|
||||
))?;
|
||||
|
||||
let fold_level = rc
|
||||
.evaluate(ctx, "@root/fold_level")?
|
||||
.as_json()
|
||||
.as_u64()
|
||||
.ok_or(RenderError::new(
|
||||
"Type error for `fold_level`, u64 expected",
|
||||
))?;
|
||||
|
||||
out.write("<ol class=\"chapter\">")?;
|
||||
|
||||
let mut current_level = 1;
|
||||
@@ -46,10 +69,23 @@ impl HelperDef for RenderToc {
|
||||
continue;
|
||||
}
|
||||
|
||||
let level = if let Some(s) = item.get("section") {
|
||||
s.matches('.').count()
|
||||
let (section, level) = if let Some(s) = item.get("section") {
|
||||
(s.as_str(), s.matches('.').count())
|
||||
} else {
|
||||
1
|
||||
("", 1)
|
||||
};
|
||||
|
||||
let is_expanded = {
|
||||
if !fold_enable {
|
||||
// Disable fold. Expand all chapters.
|
||||
true
|
||||
} else if !section.is_empty() && current_section.starts_with(section) {
|
||||
// The section is ancestor or the current section itself.
|
||||
true
|
||||
} else {
|
||||
// Levels that are larger than this would be folded.
|
||||
level - 1 < fold_level as usize
|
||||
}
|
||||
};
|
||||
|
||||
if level > current_level {
|
||||
@@ -58,20 +94,16 @@ impl HelperDef for RenderToc {
|
||||
out.write("<ol class=\"section\">")?;
|
||||
current_level += 1;
|
||||
}
|
||||
out.write("<li>")?;
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
} else if level < current_level {
|
||||
while level < current_level {
|
||||
out.write("</ol>")?;
|
||||
out.write("</li>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
out.write("<li>")?;
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
} else {
|
||||
out.write("<li")?;
|
||||
if item.get("section").is_none() {
|
||||
out.write(" class=\"affix\"")?;
|
||||
}
|
||||
out.write(">")?;
|
||||
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
|
||||
}
|
||||
|
||||
// Link
|
||||
@@ -87,11 +119,11 @@ impl HelperDef for RenderToc {
|
||||
.replace("\\", "/");
|
||||
|
||||
// Add link
|
||||
out.write(&utils::fs::path_to_root(¤t))?;
|
||||
out.write(&utils::fs::path_to_root(¤t_path))?;
|
||||
out.write(&tmp)?;
|
||||
out.write("\"")?;
|
||||
|
||||
if path == ¤t {
|
||||
if path == ¤t_path {
|
||||
out.write(" class=\"active\"")?;
|
||||
}
|
||||
|
||||
@@ -134,6 +166,13 @@ impl HelperDef for RenderToc {
|
||||
out.write("</a>")?;
|
||||
}
|
||||
|
||||
// Render expand/collapse toggle
|
||||
if let Some(flag) = item.get("has_sub_items") {
|
||||
let has_sub_items = flag.parse::<bool>().unwrap_or_default();
|
||||
if fold_enable && has_sub_items {
|
||||
out.write("<a class=\"toggle\"><div>❱</div></a>")?;
|
||||
}
|
||||
}
|
||||
out.write("</li>")?;
|
||||
}
|
||||
while current_level > 1 {
|
||||
@@ -146,3 +185,19 @@ impl HelperDef for RenderToc {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn write_li_open_tag(
|
||||
out: &mut dyn Output,
|
||||
is_expanded: bool,
|
||||
is_affix: bool,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let mut li = String::from("<li class=\"");
|
||||
if is_expanded {
|
||||
li.push_str("expanded ");
|
||||
}
|
||||
if is_affix {
|
||||
li.push_str("affix ");
|
||||
}
|
||||
li.push_str("\">");
|
||||
out.write(&li)
|
||||
}
|
||||
|
||||
46
src/renderer/markdown_renderer.rs
Normal file
46
src/renderer/markdown_renderer.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use crate::book::BookItem;
|
||||
use crate::errors::*;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::utils;
|
||||
|
||||
use std::fs;
|
||||
|
||||
#[derive(Default)]
|
||||
/// A renderer to output the Markdown after the preprocessors have run. Mostly useful
|
||||
/// when debugging preprocessors.
|
||||
pub struct MarkdownRenderer;
|
||||
|
||||
impl MarkdownRenderer {
|
||||
/// Create a new `MarkdownRenderer` instance.
|
||||
pub fn new() -> Self {
|
||||
MarkdownRenderer
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for MarkdownRenderer {
|
||||
fn name(&self) -> &str {
|
||||
"markdown"
|
||||
}
|
||||
|
||||
fn render(&self, ctx: &RenderContext) -> Result<()> {
|
||||
let destination = &ctx.destination;
|
||||
let book = &ctx.book;
|
||||
|
||||
if destination.exists() {
|
||||
utils::fs::remove_dir_content(destination)
|
||||
.chain_err(|| "Unable to remove stale Markdown output")?;
|
||||
}
|
||||
|
||||
trace!("markdown render");
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
utils::fs::write_file(&ctx.destination, &ch.path, ch.content.as_bytes())?;
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_dir_all(&destination)
|
||||
.chain_err(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,10 @@
|
||||
//! [RenderContext]: struct.RenderContext.html
|
||||
|
||||
pub use self::html_handlebars::HtmlHandlebars;
|
||||
pub use self::markdown_renderer::MarkdownRenderer;
|
||||
|
||||
mod html_handlebars;
|
||||
mod markdown_renderer;
|
||||
|
||||
use shlex::Shlex;
|
||||
use std::fs;
|
||||
|
||||
@@ -55,6 +55,15 @@ function playpen_text(playpen) {
|
||||
editor.addEventListener("change", function (e) {
|
||||
update_play_button(playpen_block, playground_crates);
|
||||
});
|
||||
// add Ctrl-Enter command to execute rust code
|
||||
editor.commands.addCommand({
|
||||
name: "run",
|
||||
bindKey: {
|
||||
win: "Ctrl-Enter",
|
||||
mac: "Ctrl-Enter"
|
||||
},
|
||||
exec: _editor => run_rust_code(playpen_block)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,95 +170,59 @@ function playpen_text(playpen) {
|
||||
|
||||
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
|
||||
|
||||
var code_block = block;
|
||||
var pre_block = block.parentNode;
|
||||
// hide lines
|
||||
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 (first_non_hidden_line) {
|
||||
lines[n] = "<span class=\"hidden\">" + "\n" + lines[n].replace(/(\s*)# ?/, "$1") + "</span>";
|
||||
}
|
||||
else {
|
||||
lines[n] = "<span class=\"hidden\">" + lines[n].replace(/(\s*)# ?/, "$1") + "\n" + "</span>";
|
||||
}
|
||||
lines_hidden = true;
|
||||
}
|
||||
else if (first_non_hidden_line) {
|
||||
lines[n] = "\n" + lines[n];
|
||||
}
|
||||
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("");
|
||||
|
||||
var lines = Array.from(block.querySelectorAll('.boring'));
|
||||
// If no lines were hidden, return
|
||||
if (!lines_hidden) { return; }
|
||||
if (!lines.length) { return; }
|
||||
block.classList.add("hide-boring");
|
||||
|
||||
var buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
buttons.innerHTML = "<button class=\"fa fa-expand\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
|
||||
|
||||
// add expand button
|
||||
var pre_block = block.parentNode;
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
|
||||
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
|
||||
if (e.target.classList.contains('fa-expand')) {
|
||||
var lines = pre_block.querySelectorAll('span.hidden');
|
||||
|
||||
e.target.classList.remove('fa-expand');
|
||||
e.target.classList.add('fa-compress');
|
||||
e.target.title = 'Hide lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
|
||||
Array.from(lines).forEach(function (line) {
|
||||
line.classList.remove('hidden');
|
||||
line.classList.add('unhidden');
|
||||
});
|
||||
block.classList.remove('hide-boring');
|
||||
} else if (e.target.classList.contains('fa-compress')) {
|
||||
var lines = pre_block.querySelectorAll('span.unhidden');
|
||||
|
||||
e.target.classList.remove('fa-compress');
|
||||
e.target.classList.add('fa-expand');
|
||||
e.target.title = 'Show hidden lines';
|
||||
e.target.setAttribute('aria-label', e.target.title);
|
||||
|
||||
Array.from(lines).forEach(function (line) {
|
||||
line.classList.remove('unhidden');
|
||||
line.classList.add('hidden');
|
||||
});
|
||||
block.classList.add('hide-boring');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
|
||||
var pre_block = block.parentNode;
|
||||
if (!pre_block.classList.contains('playpen')) {
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
if (window.playpen_copyable) {
|
||||
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
|
||||
var pre_block = block.parentNode;
|
||||
if (!pre_block.classList.contains('playpen')) {
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||
}
|
||||
|
||||
var clipButton = document.createElement('button');
|
||||
clipButton.className = 'fa fa-copy clip-button';
|
||||
clipButton.title = 'Copy to clipboard';
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
|
||||
|
||||
buttons.insertBefore(clipButton, buttons.firstChild);
|
||||
}
|
||||
|
||||
var clipButton = document.createElement('button');
|
||||
clipButton.className = 'fa fa-copy clip-button';
|
||||
clipButton.title = 'Copy to clipboard';
|
||||
clipButton.setAttribute('aria-label', clipButton.title);
|
||||
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
|
||||
|
||||
buttons.insertBefore(clipButton, buttons.firstChild);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Process playpen code blocks
|
||||
Array.from(document.querySelectorAll(".playpen")).forEach(function (pre_block) {
|
||||
@@ -267,19 +240,21 @@ function playpen_text(playpen) {
|
||||
runCodeButton.title = 'Run this code';
|
||||
runCodeButton.setAttribute('aria-label', runCodeButton.title);
|
||||
|
||||
var copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
|
||||
buttons.insertBefore(runCodeButton, buttons.firstChild);
|
||||
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
|
||||
|
||||
runCodeButton.addEventListener('click', function (e) {
|
||||
run_rust_code(pre_block);
|
||||
});
|
||||
|
||||
if (window.playpen_copyable) {
|
||||
var copyCodeClipboardButton = document.createElement('button');
|
||||
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
|
||||
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||
|
||||
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
|
||||
}
|
||||
|
||||
let code_block = pre_block.querySelector("code");
|
||||
if (window.ace && code_block.classList.contains("editable")) {
|
||||
var undoChangesButton = document.createElement('button');
|
||||
@@ -435,6 +410,7 @@ function playpen_text(playpen) {
|
||||
(function sidebar() {
|
||||
var html = document.querySelector("html");
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var sidebarScrollBox = document.getElementById("sidebar-scrollbox");
|
||||
var sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
var sidebarToggleButton = document.getElementById("sidebar-toggle");
|
||||
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
|
||||
@@ -451,6 +427,17 @@ function playpen_text(playpen) {
|
||||
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
|
||||
}
|
||||
|
||||
|
||||
var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
|
||||
|
||||
function toggleSection(ev) {
|
||||
ev.currentTarget.parentElement.classList.toggle('expanded');
|
||||
}
|
||||
|
||||
Array.from(sidebarAnchorToggles).forEach(function (el) {
|
||||
el.addEventListener('click', toggleSection);
|
||||
});
|
||||
|
||||
function hideSidebar() {
|
||||
html.classList.remove('sidebar-visible')
|
||||
html.classList.add('sidebar-hidden');
|
||||
@@ -522,7 +509,7 @@ function playpen_text(playpen) {
|
||||
// Scroll sidebar to current active section
|
||||
var activeSection = sidebar.querySelector(".active");
|
||||
if (activeSection) {
|
||||
sidebar.scrollTop = activeSection.offsetTop;
|
||||
sidebarScrollBox.scrollTop = activeSection.offsetTop;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -615,6 +602,6 @@ function playpen_text(playpen) {
|
||||
menu.classList.remove('bordered');
|
||||
}
|
||||
|
||||
previousScrollTop = document.scrollingElement.scrollTop;
|
||||
previousScrollTop = Math.max(document.scrollingElement.scrollTop, 0);
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar);
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-color: var(--scrollbar) var(--bg);
|
||||
}
|
||||
#searchresults a,
|
||||
.content a:link,
|
||||
a:visited,
|
||||
@@ -124,7 +126,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||
text-decoration: none;
|
||||
|
||||
position: fixed;
|
||||
top: 50px; /* Height of menu-bar */
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
max-width: 150px;
|
||||
@@ -135,10 +137,14 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||
align-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
transition: color 0.5s;
|
||||
transition: color 0.5s, background-color 0.5s;
|
||||
}
|
||||
|
||||
.nav-chapters:hover { text-decoration: none; }
|
||||
.nav-chapters:hover {
|
||||
text-decoration: none;
|
||||
background-color: var(--theme-hover);
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.nav-wrapper {
|
||||
margin-top: 50px;
|
||||
@@ -176,8 +182,7 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
|
||||
/* Inline code */
|
||||
|
||||
:not(pre) > .hljs {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
display: inline;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
@@ -370,7 +375,13 @@ ul#searchresults span.teaser em {
|
||||
padding-left: 0;
|
||||
line-height: 2.2em;
|
||||
}
|
||||
|
||||
.chapter ol {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chapter li {
|
||||
display: flex;
|
||||
color: var(--sidebar-non-existant);
|
||||
}
|
||||
.chapter li a {
|
||||
@@ -384,10 +395,32 @@ ul#searchresults span.teaser em {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.chapter li .active {
|
||||
.chapter li a.active {
|
||||
color: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.chapter li > a.toggle {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
padding: 0 10px;
|
||||
user-select: none;
|
||||
opacity: 0.68;
|
||||
}
|
||||
|
||||
.chapter li > a.toggle div {
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
/* collapse the section */
|
||||
.chapter li:not(.expanded) + li > ol {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.chapter li.expanded > a.toggle div {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
|
||||
@@ -16,14 +16,15 @@ body {
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important;
|
||||
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
|
||||
}
|
||||
|
||||
.left { float: left; }
|
||||
.right { float: right; }
|
||||
.boring { opacity: 0.6; }
|
||||
.hide-boring .boring { display: none; }
|
||||
.hidden { display: none; }
|
||||
.play-button.hidden { display: none; }
|
||||
|
||||
h2, h3 { margin-top: 2.5em; }
|
||||
h4, h5 { margin-top: 2em; }
|
||||
@@ -92,6 +93,9 @@ table thead td {
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
table thead th {
|
||||
padding: 3px 20px;
|
||||
}
|
||||
table thead tr {
|
||||
border: 1px var(--table-header-bg) solid;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -43,7 +43,7 @@
|
||||
<!-- Provide site root to javascript -->
|
||||
<script type="text/javascript">
|
||||
var path_to_root = "{{ path_to_root }}";
|
||||
var default_theme = "{{ default_theme }}";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
|
||||
</script>
|
||||
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
@@ -84,7 +84,7 @@
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<div class="sidebar-scrollbox">
|
||||
<div id="sidebar-scrollbox" class="sidebar-scrollbox">
|
||||
{{#toc}}{{/toc}}
|
||||
</div>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||
@@ -230,6 +230,18 @@
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playpen_line_numbers}}
|
||||
<script type="text/javascript">
|
||||
window.playpen_line_numbers = true;
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playpen_copyable}}
|
||||
<script type="text/javascript">
|
||||
window.playpen_copyable = true;
|
||||
</script>
|
||||
{{/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>
|
||||
|
||||
@@ -6,12 +6,14 @@ window.editors = [];
|
||||
}
|
||||
|
||||
Array.from(document.querySelectorAll('.editable')).forEach(function(editable) {
|
||||
let display_line_numbers = window.playpen_line_numbers || false;
|
||||
|
||||
let editor = ace.edit(editable);
|
||||
editor.setOptions({
|
||||
highlightActiveLine: false,
|
||||
showPrintMargin: false,
|
||||
showLineNumbers: false,
|
||||
showGutter: false,
|
||||
showLineNumbers: display_line_numbers,
|
||||
showGutter: display_line_numbers,
|
||||
maxLines: Infinity,
|
||||
fontSize: "0.875em" // please adjust the font size of the code in general.css
|
||||
});
|
||||
|
||||
@@ -11,7 +11,10 @@ use std::borrow::Cow;
|
||||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
|
||||
pub use self::string::{take_anchored_lines, take_lines};
|
||||
pub use self::string::{
|
||||
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
|
||||
take_rustdoc_include_lines,
|
||||
};
|
||||
|
||||
/// Replaces multiple consecutive whitespace characters with a single space character.
|
||||
pub fn collapse_whitespace(text: &str) -> Cow<'_, str> {
|
||||
@@ -448,18 +451,12 @@ 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]
|
||||
|
||||
@@ -18,33 +18,33 @@ pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref ANCHOR_START: Regex = Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap();
|
||||
static ref ANCHOR_END: Regex = Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap();
|
||||
}
|
||||
|
||||
/// Take anchored lines from a string.
|
||||
/// Lines containing anchor are ignored.
|
||||
pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
lazy_static! {
|
||||
static ref RE_START: Regex = Regex::new(r"ANCHOR:\s*(?P<anchor_name>[\w_-]+)").unwrap();
|
||||
static ref RE_END: Regex = Regex::new(r"ANCHOR_END:\s*(?P<anchor_name>[\w_-]+)").unwrap();
|
||||
}
|
||||
|
||||
let mut retained = Vec::<&str>::new();
|
||||
let mut anchor_found = false;
|
||||
|
||||
for l in s.lines() {
|
||||
if anchor_found {
|
||||
match RE_END.captures(l) {
|
||||
match ANCHOR_END.captures(l) {
|
||||
Some(cap) => {
|
||||
if &cap["anchor_name"] == anchor {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !RE_START.is_match(l) {
|
||||
if !ANCHOR_START.is_match(l) {
|
||||
retained.push(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(cap) = RE_START.captures(l) {
|
||||
if let Some(cap) = ANCHOR_START.captures(l) {
|
||||
if &cap["anchor_name"] == anchor {
|
||||
anchor_found = true;
|
||||
}
|
||||
@@ -55,9 +55,70 @@ pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
retained.join("\n")
|
||||
}
|
||||
|
||||
/// Keep lines contained within the range specified as-is.
|
||||
/// For any lines not in the range, include them but use `#` at the beginning. This will hide the
|
||||
/// lines from initial display but include them when expanding the code snippet or testing with
|
||||
/// rustdoc.
|
||||
pub fn take_rustdoc_include_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
|
||||
let mut output = String::with_capacity(s.len());
|
||||
|
||||
for (index, line) in s.lines().enumerate() {
|
||||
if !range.contains(&index) {
|
||||
output.push_str("# ");
|
||||
}
|
||||
output.push_str(line);
|
||||
output.push_str("\n");
|
||||
}
|
||||
output.pop();
|
||||
output
|
||||
}
|
||||
|
||||
/// Keep lines between the anchor comments specified as-is.
|
||||
/// For any lines not between the anchors, include them but use `#` at the beginning. This will
|
||||
/// hide the lines from initial display but include them when expanding the code snippet or testing
|
||||
/// with rustdoc.
|
||||
pub fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
|
||||
let mut output = String::with_capacity(s.len());
|
||||
let mut within_anchored_section = false;
|
||||
|
||||
for l in s.lines() {
|
||||
if within_anchored_section {
|
||||
match ANCHOR_END.captures(l) {
|
||||
Some(cap) => {
|
||||
if &cap["anchor_name"] == anchor {
|
||||
within_anchored_section = false;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if !ANCHOR_START.is_match(l) {
|
||||
output.push_str(l);
|
||||
output.push_str("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(cap) = ANCHOR_START.captures(l) {
|
||||
if &cap["anchor_name"] == anchor {
|
||||
within_anchored_section = true;
|
||||
}
|
||||
} else if !ANCHOR_END.is_match(l) {
|
||||
output.push_str("# ");
|
||||
output.push_str(l);
|
||||
output.push_str("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output.pop();
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{take_anchored_lines, take_lines};
|
||||
use super::{
|
||||
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
|
||||
take_rustdoc_include_lines,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn take_lines_test() {
|
||||
@@ -99,4 +160,93 @@ mod tests {
|
||||
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
|
||||
assert_eq!(take_anchored_lines(s, "something"), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_rustdoc_include_lines_test() {
|
||||
let s = "Lorem\nipsum\ndolor\nsit\namet";
|
||||
assert_eq!(
|
||||
take_rustdoc_include_lines(s, 1..3),
|
||||
"# Lorem\nipsum\ndolor\n# sit\n# amet"
|
||||
);
|
||||
assert_eq!(
|
||||
take_rustdoc_include_lines(s, 3..),
|
||||
"# Lorem\n# ipsum\n# dolor\nsit\namet"
|
||||
);
|
||||
assert_eq!(
|
||||
take_rustdoc_include_lines(s, ..3),
|
||||
"Lorem\nipsum\ndolor\n# sit\n# amet"
|
||||
);
|
||||
assert_eq!(take_rustdoc_include_lines(s, ..), s);
|
||||
// corner cases
|
||||
assert_eq!(
|
||||
take_rustdoc_include_lines(s, 4..3),
|
||||
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet"
|
||||
);
|
||||
assert_eq!(take_rustdoc_include_lines(s, ..100), s);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_rustdoc_include_anchored_lines_test() {
|
||||
let s = "Lorem\nipsum\ndolor\nsit\namet";
|
||||
assert_eq!(
|
||||
take_rustdoc_include_anchored_lines(s, "test"),
|
||||
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet"
|
||||
);
|
||||
|
||||
let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet";
|
||||
assert_eq!(
|
||||
take_rustdoc_include_anchored_lines(s, "test"),
|
||||
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet"
|
||||
);
|
||||
|
||||
let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet";
|
||||
assert_eq!(
|
||||
take_rustdoc_include_anchored_lines(s, "test"),
|
||||
"# Lorem\n# ipsum\ndolor\nsit\namet"
|
||||
);
|
||||
assert_eq!(
|
||||
take_rustdoc_include_anchored_lines(s, "something"),
|
||||
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet"
|
||||
);
|
||||
|
||||
let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
|
||||
assert_eq!(
|
||||
take_rustdoc_include_anchored_lines(s, "test"),
|
||||
"# Lorem\n# ipsum\ndolor\nsit\namet\n# lorem\n# ipsum"
|
||||
);
|
||||
assert_eq!(
|
||||
take_rustdoc_include_anchored_lines(s, "something"),
|
||||
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum"
|
||||
);
|
||||
|
||||
let s = "Lorem\nANCHOR: test\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
|
||||
assert_eq!(
|
||||
take_rustdoc_include_anchored_lines(s, "test"),
|
||||
"# Lorem\nipsum\ndolor\nsit\namet\n# lorem\n# ipsum"
|
||||
);
|
||||
assert_eq!(
|
||||
take_rustdoc_include_anchored_lines(s, "something"),
|
||||
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum"
|
||||
);
|
||||
|
||||
let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum";
|
||||
assert_eq!(
|
||||
take_rustdoc_include_anchored_lines(s, "test2"),
|
||||
"# Lorem\nipsum\ndolor\nsit\namet\nlorem\n# ipsum"
|
||||
);
|
||||
assert_eq!(
|
||||
take_rustdoc_include_anchored_lines(s, "test"),
|
||||
"# Lorem\n# ipsum\ndolor\nsit\namet\n# lorem\n# ipsum"
|
||||
);
|
||||
assert_eq!(
|
||||
take_rustdoc_include_anchored_lines(s, "something"),
|
||||
"# Lorem\n# ipsum\n# dolor\n# sit\n# amet\n# lorem\n# ipsum"
|
||||
);
|
||||
|
||||
let s = "Lorem\nANCHOR: test\nipsum\nANCHOR_END: test\ndolor\nANCHOR: test\nsit\nANCHOR_END: test\namet";
|
||||
assert_eq!(
|
||||
take_rustdoc_include_anchored_lines(s, "test"),
|
||||
"# Lorem\nipsum\n# dolor\nsit\n# amet"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,13 @@ impl DummyBook {
|
||||
})?;
|
||||
|
||||
let sub_pattern = if self.passing_test { "true" } else { "false" };
|
||||
let files_containing_tests = ["src/first/nested.md", "src/first/nested-test.rs"];
|
||||
let files_containing_tests = [
|
||||
"src/first/nested.md",
|
||||
"src/first/nested-test.rs",
|
||||
"src/first/nested-test-with-anchors.rs",
|
||||
"src/first/partially-included-test.rs",
|
||||
"src/first/partially-included-test-with-anchors.rs",
|
||||
];
|
||||
for file in &files_containing_tests {
|
||||
let path_containing_tests = temp.path().join(file);
|
||||
replace_pattern_in_file(&path_containing_tests, "$TEST_STATUS", sub_pattern)?;
|
||||
|
||||
11
tests/dummy_book/src/first/nested-test-with-anchors.rs
Normal file
11
tests/dummy_book/src/first/nested-test-with-anchors.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
// The next line will cause a `testing` test to fail if the anchor feature is broken in such a way
|
||||
// that the whole file gets mistakenly included.
|
||||
assert!(!$TEST_STATUS);
|
||||
|
||||
// ANCHOR: myanchor
|
||||
// ANCHOR: unendinganchor
|
||||
// The next line will cause a `rendered_output` test to fail if the anchor feature is broken in
|
||||
// such a way that the content between anchors isn't included.
|
||||
// unique-string-for-anchor-test
|
||||
assert!($TEST_STATUS);
|
||||
// ANCHOR_END: myanchor
|
||||
@@ -11,3 +11,21 @@ assert!($TEST_STATUS);
|
||||
```rust
|
||||
{{#include nested-test.rs}}
|
||||
```
|
||||
|
||||
## Anchors include the part of a file between special comments
|
||||
|
||||
```rust
|
||||
{{#include nested-test-with-anchors.rs:myanchor}}
|
||||
```
|
||||
|
||||
## Rustdoc include adds the rest of the file as hidden
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include partially-included-test.rs:5:7}}
|
||||
```
|
||||
|
||||
## Rustdoc include works with anchors too
|
||||
|
||||
```rust
|
||||
{{#rustdoc_include partially-included-test-with-anchors.rs:rustdoc-include-anchor}}
|
||||
```
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fn some_other_function() {
|
||||
// ANCHOR: unused-anchor-that-should-be-stripped
|
||||
assert!($TEST_STATUS);
|
||||
// ANCHOR_END: unused-anchor-that-should-be-stripped
|
||||
}
|
||||
|
||||
// ANCHOR: rustdoc-include-anchor
|
||||
fn main() {
|
||||
some_other_function();
|
||||
}
|
||||
// ANCHOR_END: rustdoc-include-anchor
|
||||
7
tests/dummy_book/src/first/partially-included-test.rs
Normal file
7
tests/dummy_book/src/first/partially-included-test.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
fn some_function() {
|
||||
assert!($TEST_STATUS);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
some_function();
|
||||
}
|
||||
@@ -147,6 +147,35 @@ fn rendered_code_has_playpen_stuff() {
|
||||
assert_contains_strings(book_js, &[".playpen"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anchors_include_text_between_but_not_anchor_comments() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let nested = temp.path().join("book/first/nested.html");
|
||||
let text_between_anchors = vec!["unique-string-for-anchor-test"];
|
||||
let anchor_text = vec!["ANCHOR"];
|
||||
|
||||
assert_contains_strings(nested.clone(), &text_between_anchors);
|
||||
assert_doesnt_contain_strings(nested, &anchor_text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rustdoc_include_hides_the_unspecified_part_of_the_file() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let nested = temp.path().join("book/first/nested.html");
|
||||
let text = vec![
|
||||
"<span class=\"boring\">fn some_function() {",
|
||||
"<span class=\"boring\">fn some_other_function() {",
|
||||
];
|
||||
|
||||
assert_contains_strings(nested, &text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chapter_content_appears_in_rendered_document() {
|
||||
let content = vec![
|
||||
@@ -235,7 +264,12 @@ fn check_second_toc_level() {
|
||||
let mut should_be = Vec::from(TOC_SECOND_LEVEL);
|
||||
should_be.sort();
|
||||
|
||||
let pred = descendants!(Class("chapter"), Name("li"), Name("li"), Name("a"));
|
||||
let pred = descendants!(
|
||||
Class("chapter"),
|
||||
Name("li"),
|
||||
Name("li"),
|
||||
Name("a").and(Class("toggle").not())
|
||||
);
|
||||
|
||||
let mut children_of_children: Vec<_> = doc
|
||||
.find(pred)
|
||||
@@ -254,7 +288,11 @@ fn check_first_toc_level() {
|
||||
should_be.extend(TOC_SECOND_LEVEL);
|
||||
should_be.sort();
|
||||
|
||||
let pred = descendants!(Class("chapter"), Name("li"), Name("a"));
|
||||
let pred = descendants!(
|
||||
Class("chapter"),
|
||||
Name("li"),
|
||||
Name("a").and(Class("toggle").not())
|
||||
);
|
||||
|
||||
let mut children: Vec<_> = doc
|
||||
.find(pred)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,12 @@ fn mdbook_can_correctly_test_a_passing_book() {
|
||||
let temp = DummyBook::new().with_passing_test(true).build().unwrap();
|
||||
let mut md = MDBook::load(temp.path()).unwrap();
|
||||
|
||||
assert!(md.test(vec![]).is_ok());
|
||||
let result = md.test(vec![]);
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Tests failed with {}",
|
||||
result.err().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user