Compare commits

...

112 Commits

Author SHA1 Message Date
Eric Huss
f04d7b802d Merge pull request #1072 from ehuss/release-0.3.2
Release 0.3.2.
2019-10-21 12:37:49 -07:00
Eric Huss
bfcddf2680 Release 0.3.2. 2019-10-21 11:41:39 -07:00
Eric Huss
2b649fe94f Merge pull request #1071 from ehuss/github-actions
Switch to GitHub Actions.
2019-10-21 10:49:18 -07:00
Eric Huss
fc4236eaa7 Switch to GitHub Actions. 2019-10-21 10:43:27 -07:00
rnitta
a592da33bb fix the behavior of sticky header (#1070) 2019-10-19 10:07:41 +02:00
Weihang Lo
6af6219e5b [Feature] expandable sidebar sections (ToC collapse) (#1027)
* render(toc): render expandable toc toggle

* ui(toc): js/css logic to toggle toc

* test: update rendered output css selector

* config: add `html.fold.[enable|level]`

* renderer: fold according to configs

* doc: add `output.html.fold`

* refactor: tidy fold config

- Derive default for `Fold`.
- Use `is_empty` instead of checking the length of chapters.
2019-10-19 09:56:08 +02:00
Andrew Pritchard
e5f74b6c86 Option to display copy buttons. (#1050)
* Option to display copy buttons.

- Added field to playpen data structure
- Communicate through window.playpen_copyable
- Javascript updated to check before displaying copy buttons.

* html -> html_config

Also:
- update description of copyable in source code.
- update description of line_numbers (my last PR to this repository)
2019-10-17 12:44:54 +02:00
Benedikt Werner
84a2ab0dba Reapply: Move hiding of boring lines into static content (#846) (#1065)
* Move hiding of boring lines into static content (#846)

* Fix test for hidden code
2019-10-16 11:27:14 +02:00
David Omar Flores Chávez
d63ef8330d Add !important to code {font-family} property (#1062)
If accepted, this will fix #1061
2019-10-11 14:21:13 +02:00
rnitta
01e50303a2 add a command to playpen (#1066) 2019-10-11 14:16:06 +02:00
Dylan DPC
2b3304cb8b Revert "Move hiding of boring lines into static content (#846)" (#1064)
This reverts commit 4448f3fc4b.
2019-10-10 14:31:55 +02:00
Adrian Heine né Lang
4448f3fc4b Move hiding of boring lines into static content (#846) 2019-10-10 13:55:29 +02:00
Chris Ladd
859659f197 Fix inline code display css (#1058) 2019-10-07 09:24:35 +02:00
Eric Huss
4a93eddae2 Fix "next" navigation on index.html (take 2). (#1005) 2019-10-06 17:55:36 +02:00
Eric Huss
0173451b67 Fix error message for missing output.html. (#1056) 2019-10-06 00:33:50 +02:00
Carol (Nichols || Goulding)
ac1749ff2f Implement a rustdoc_include preprocessor (#1003)
* Allow underscores in the link type name

* Add some tests for include anchors

* Include parts of Rust files and hide the rest

Fixes #618.

* Increase min supported Rust version to 1.35

* Add a test for a behavior of rustdoc_include I want to depend on

At first I thought this was a bug, but then I looked at some use cases
we have in TRPL and decided this was a feature that I'd like to use.
2019-10-06 00:27:03 +02:00
Eric Huss
8cdeb121c5 Merge pull request #1055 from amanjeev/amanjeev/clean-command
Fix (command:clean): removes error message 'dir not found' if 'clean' is run multiple times
2019-10-05 14:03:31 -07:00
Amanjeev Sethi
74313bb701 Fix (command:clean): removes error message 'dir not found' if 'clean' is run multiple times (uses existing path variable) 2019-10-05 15:59:34 -04:00
Amanjeev Sethi
3c25dba9b4 Revert "Fix (command:clean): removes error message 'dir not found' if 'clean' is run multiple times"
This reverts commit 2387942588.
2019-10-05 15:57:10 -04:00
Amanjeev Sethi
2387942588 Fix (command:clean): removes error message 'dir not found' if 'clean' is run multiple times 2019-10-05 15:01:01 -04:00
Eric Huss
93c9ae5700 Merge pull request #1037 from Flying-Toast/prefers-color-scheme
Automatically use a dark theme according to 'prefers-color-scheme'
2019-10-05 11:33:52 -07:00
Eric Huss
9efa9fd1c4 Merge pull request #1052 from morphologue/fix-sidebar-autoscroll
Fix #1029 sidebar not auto-scrolling
2019-10-05 10:19:29 -07:00
Eric Huss
8a33407cc5 Merge pull request #1051 from segfaultsourcery/fix-small-gitignore-bug
I fixed a small gitignore bug
2019-10-05 10:11:16 -07:00
morphologue
699844a5c3 Fix #1029 sidebar not auto-scrolling 2019-10-05 16:54:09 +10:00
Flying-Toast
9bdec5e7cc preferred-dark-theme defaults to default-theme 2019-10-04 19:32:03 -04:00
Kim Hermansson
930f730361 The .gitignore file is now searched for recursively.
Removed a warning if .gitignore is missing.
2019-10-04 22:56:56 +02:00
Eric Huss
09c738468f Merge pull request #1047 from rnitta/patch-1
Fix Search::use_boolean_and documents
2019-10-04 12:39:05 -07:00
Kim Hermansson
a3d1afdd1f This fixes a small bug where the gitignore location can be misinterpreted to be in the folder "above" where it actually is. 2019-10-04 19:44:36 +02:00
Kim Hå
8e8e53ae15 Added support for gitignore files. (#1044)
* Added support for gitignore files.
The watch command will now ignore files based on gitignore. This can be useful for when your editor creates cache or swap files.

* Ran cargo fmt.

* Made the code a bit tidier based on input from other Rust programmers.
Changed the type of the closure back to use PathBuf, not &PathBuf.
Reduced nesting.
2019-10-04 14:59:17 +02:00
rnitta
5fe801a7d1 fix Search::use_boolean_and documents 2019-10-03 11:35:42 +09:00
Eric Huss
a6f317e352 Update highlight.js (#1041) 2019-09-30 00:07:54 +02:00
Eric Huss
ed95252f05 Merge pull request #1039 from ehuss/fix-html-config
Fix merge conflict.
2019-09-26 11:49:57 -07:00
Eric Huss
a058da8b74 Fix merge conflict. 2019-09-26 11:03:51 -07:00
Eric Huss
73be1292ab Merge pull request #1035 from andymac-2/line-numbers
Added line numbers to editable sections of code.
2019-09-26 10:53:32 -07:00
Eric Huss
98ecd1178b Merge pull request #1033 from TjeuKayim/log-deserialization-error-html-config
Log deserialization errors for [html.config]
2019-09-26 10:28:18 -07:00
Eric Huss
996ac382c1 Merge pull request #1038 from ehuss/rustfmt-1.38
Rustfmt for 1.38.
2019-09-26 10:13:00 -07:00
Eric Huss
b88839cc25 Rustfmt for 1.38.
A minor change in the recent stable release.
2019-09-26 09:54:12 -07:00
Flying-Toast
1ef94c2a7e add preferred-dark-theme to book.toml example 2019-09-26 12:13:25 -04:00
Flying-Toast
f0ac13e3e2 Document preferred-dark-theme config option 2019-09-26 12:09:30 -04:00
Flying-Toast
b0ae14a2c7 Automatically use a dark theme according to 'prefers-color-scheme' 2019-09-25 19:11:28 -04:00
Andrew Pritchard
81ab2eb7db Added line numbers to editable sections of code.
- Added line numbers to config struct
- Added playpen_line_numbers field to hbs renderer.
- Added section to set `window.playpen_line_numbers = true` in page template
- Use line number global variable to show line numbers when required.
2019-09-24 21:27:02 +08:00
Tjeu Kayim
213171591a De-duplicate calling Config::html_config() 2019-09-22 21:48:49 +02:00
Tjeu Kayim
db13d8e561 Log HtmlConfig deserialization errors 2019-09-22 21:48:03 +02:00
Eric Huss
b4bb44292d Merge pull request #1030 from lzutao/rustup
Rustup
2019-09-21 09:03:34 -07:00
Lzu Tao
bb7a863d3e Bump compatible deps 2019-09-21 09:23:30 +00:00
Lzu Tao
e62a9dba87 Bump ws 2019-09-21 09:23:30 +00:00
Lzu Tao
4a94b656cd Bump ammonia 2019-09-21 09:23:30 +00:00
Carol (Nichols || Goulding)
a873d46871 Implement a markdown renderer (#1018)
Use case: when trying to `mdbook test` a file that has many `include`
directives, and a test fails, the line numbers in the `rustdoc` output
don't match the line numbers in the original markdown file.

Turning on the markdown renderer implemented here lets you see what is
being passed to `rustdoc` by saving the markdown after the preprocessors
have run.

This renderer could be helpful for debugging many preprocessors, but
it's probably not useful in the general case, so it's turned off by
default.
2019-08-30 12:20:53 +02:00
Carol (Nichols || Goulding)
ce0c5f1d07 Another refactoring in links.rs (#1001)
* Extract the concept of a link having a range or anchor specified

So that other kinds of links can use this concept too.

* Extract a function for parsing range or anchor
2019-08-13 11:19:42 +02:00
Eric Huss
33d7e86fb6 Merge pull request #999 from integer32llc/small-test-improvement
When this test fails, print out why to assist in debugging
2019-08-12 09:02:04 -07:00
Carol (Nichols || Goulding)
f9f9785839 When this test fails, print out why to assist in debugging 2019-08-12 09:50:54 -04:00
Eric Huss
0c37b912ba Merge pull request #824 from sdruskat/patch-1
Fix #823: Apply default padding to table headers
2019-08-09 09:49:56 -07:00
Stephan Druskat
e880fb6339 Fix #823: Apply default padding to table headers
This PR fixes #823 by applying the default padding for table cells (`padding: 3px 20px;`) to header cells.
2019-08-09 09:48:56 -07:00
Eric Huss
a8d6337ac6 Merge pull request #994 from WofWca/nav-chapters-style
ui: Improve next/prev chapter links' style
2019-08-07 11:27:20 -07:00
Eric Huss
f37a89cd4c Merge pull request #998 from integer32llc/links-improvements
Refactoring of some functionality in links.rs
2019-08-07 10:33:11 -07:00
Eric Huss
aaeb3e2852 Merge pull request #985 from Michael-F-Bryan/enable-caching
Allow backends to cache previous results
2019-08-07 10:26:06 -07:00
Carol (Nichols || Goulding)
8c4b292d58 Rework a match to possibly be more understandable 2019-08-06 22:14:17 -04:00
Carol (Nichols || Goulding)
40159362c0 Unnest another conditional 2019-08-06 22:14:16 -04:00
Carol (Nichols || Goulding)
aa67245743 Unnest a conditional 2019-08-06 22:14:16 -04:00
Carol (Nichols || Goulding)
d968443074 Don't bother splitting the path after the 3rd colon 2019-08-06 22:14:16 -04:00
Carol (Nichols || Goulding)
3716123e10 Increase test coverage of parse_include_path 2019-08-06 22:14:16 -04:00
Carol (Nichols || Goulding)
50a2ec3cf1 Ensure the iterator will always return None after the first None
I'm not sure in what cases this iterator might possibly return Some
again, but let's make absolutely sure.
2019-08-06 22:14:16 -04:00
Carol (Nichols || Goulding)
07459aef60 Factor out the use of different ranges from linktype
This eliminates some duplication and will enable different kinds of
LinkTypes to have line number ranges.

Implement `From` for the std `Range` types to enable easier
construction.

The new code reaaalllly makes me wish for a delegation mechanism though
:(
2019-08-06 22:14:16 -04:00
Eric Huss
0f56c09d3a Merge pull request #996 from lzutao/temporary-disable-sccache
build(CI): Temporary disable sccache
2019-08-05 18:00:35 -07:00
Lzu Tao
63ad3d9340 build(CI): Temporary disable sccache 2019-08-03 23:19:04 +07:00
WofWca
1c5dc1e310 ui: Improve next/prev chapter links' style 2019-08-03 22:56:25 +08:00
Steve Klabnik
77af889a2e Merge pull request #991 from lbeckman314/patch-1
Add `--path .` to cargo install commands.
2019-08-01 18:37:33 -05:00
Liam Beckman
e48fed74bf Add --path . to cargo install commands.
Changing `cargo install` to `cargo install --path .` prevents the following error:

<span style="color: red;">error:</span> Using `cargo install` to install the binaries for the package in current working directory is no longer supported, use `cargo install --path .` instead. Use `cargo build` if you want to simply build the package.
2019-07-31 14:49:25 -07:00
Sorin Davidoi
e512850c13 fix(css/chrome): Use standard property for scrollbar (#816)
This was recently standardized and is currently implemented in Firefox Nightly.
2019-07-28 21:01:33 +02:00
Michael Bryan
bb412edf53 Made sure the tests pass 2019-07-21 04:32:28 +08:00
Michael Bryan
5b0a23ebab Updated the documentation 2019-07-21 02:40:58 +08:00
Michael Bryan
e56c41a1c2 The HTML renderer now cleans its own build directory 2019-07-21 02:37:09 +08:00
Michael Bryan
d1b5a8f982 The MDBook::build() method no longer cleans the renderer's build directory 2019-07-21 02:35:18 +08:00
Eric Huss
f396623b63 Merge pull request #983 from meichstedt/me/fix-config-docs
Remove redundant occurrence of 'in the'
2019-07-17 09:56:01 -07:00
Matthias Eichstedt
9ec43b6c6d Remove redundant occurrence of 'in the' 2019-07-17 17:20:18 +02:00
Eric Huss
7c4d2070f7 Merge pull request #981 from ehuss/release-0.3.1
Release 0.3.1
2019-07-15 14:58:07 -07:00
Eric Huss
50d5917530 Release 0.3.1 2019-07-15 14:56:59 -07:00
Eric Huss
9cd47eb80f Merge pull request #979 from ehuss/fix-links
Fix some broken links.
2019-07-15 14:55:21 -07:00
Eric Huss
4932df2570 Merge pull request #980 from ehuss/appveyor-one-job
Only run 1 job on appveyor when a PR is opened.
2019-07-15 14:54:55 -07:00
Eric Huss
11d31c989c Only run 1 job on appveyor when a PR is opened. 2019-07-15 13:16:55 -07:00
Eric Huss
e5ace6d6a4 Fix some broken links. 2019-07-15 12:51:46 -07:00
Eric Huss
e7c3d02c61 Merge pull request #851 from CBenoit/master
Add include by anchor in preprocessor (partial include)
2019-07-15 12:31:59 -07:00
Benoît CORTIER
d8a68ba3f6 Document anchor-based partial include feature in the book 2019-07-14 21:55:51 -04:00
Benoît CORTIER
d29a79349c Add include by anchor in preprocessor. 2019-07-14 21:55:51 -04:00
Eric Huss
d6088c8a57 Merge pull request #978 from ehuss/bump-elasticlunr
Bump elasticlunr.
2019-07-12 21:59:39 -07:00
Eric Huss
b91e5c8807 Merge pull request #977 from sunng87/feature/handlebars-2.0
(feat) update handlebars to 2.0
2019-07-12 09:56:51 -07:00
Eric Huss
6199e4df79 Bump elasticlunr. 2019-07-12 09:53:11 -07:00
Ning Sun
2d11eb05fe (feat) update handlebars to 2.0 2019-07-13 00:11:05 +08:00
Carol (Nichols || Goulding)
3d45e40693 Small cleanups of variable/field names (#970)
* Rename a variable from playpen to link

Links can now be more than only playpen links

* Rename a field to match the enum type it holds

Also so that link.link.stuff doesn't happen when a variable link holds a
Link instance
2019-07-04 11:31:04 +02:00
Eric Huss
228e99ba11 Fix even more print page links. (#963) 2019-07-01 17:52:25 +02:00
Eric Huss
4b569edadd Fix memory leak and warning (#967) 2019-07-01 17:49:57 +02:00
Eric Huss
3e652b5bfc Merge pull request #965 from lzutao/missed-sccache
Fix broken cache by updating sccache version
2019-06-29 13:42:43 -07:00
Lzu Tao
ba41d73dc3 build: Fix stale builds 2019-06-28 23:14:26 +07:00
Lzu Tao
1ce1401263 travis: Include cargo registry when cache
This reduces the crates downloading time upto 30 seconds
on fast network.
2019-06-28 11:13:54 +07:00
Lzu Tao
00b3d9cf86 build: Fix broken cache by updating sccache 2019-06-28 11:13:54 +07:00
Eric Huss
bb3398bdbb Merge pull request #941 from rnitta/configurable-language
Change language attribute of the book to configurable
2019-06-24 08:56:22 -07:00
Eric Huss
19c26217c0 Don't keep gh-pages history. (#964) 2019-06-21 07:17:03 +02:00
Eric Huss
a2029f0a78 Merge pull request #959 from jeremystucki/refactoring
Minor Refactoring
2019-06-20 20:05:02 -07:00
Eric Huss
7c33ac800c Merge pull request #962 from integer32llc/rangebounds
Use stdlib RangeBounds
2019-06-20 20:01:52 -07:00
Eric Huss
d371001ab8 Merge pull request #961 from integer32llc/fs-read-to-string
Switch to the standard library's fs::read_to_string
2019-06-20 20:00:35 -07:00
Eric Huss
d73504eb23 Merge pull request #960 from integer32llc/update-min-rust-version
Update specified minimum Rust version and test it in travis
2019-06-20 19:59:15 -07:00
Carol (Nichols || Goulding)
abddd7c6f7 Use stdlib RangeBounds 2019-06-20 21:56:31 -04:00
Carol (Nichols || Goulding)
31e36f85e7 Update specified minimum Rust version and test it in travis 2019-06-20 15:04:55 -04:00
Jeremy Stucki
92a7b0cdcd Use iterator instead of for loop 2019-06-20 15:12:56 +02:00
Jeremy Stucki
592140db5b Remove redundant closure 2019-06-20 14:56:47 +02:00
Jeremy Stucki
3a0eeb4bbb Remove needless scope 2019-06-20 14:29:14 +02:00
Jeremy Stucki
a9dae326fa Use unwrap_or instead of match on Result 2019-06-20 14:27:57 +02:00
Jeremy Stucki
abba959add Remove needless lifetime 2019-06-20 14:18:31 +02:00
Jeremy Stucki
ea15e55829 Use map instead of match on Option 2019-06-20 14:18:17 +02:00
j143-bot
d07bd9fed4 Update the master-docs link to ../mdBook (#958) 2019-06-20 08:54:38 +02:00
Carol (Nichols || Goulding)
b83c55f7ef Switch to the standard library's fs::read_to_string 2019-06-19 22:49:18 -04:00
rnitta
4f7c299de7 update language attribute to configurable 2019-05-30 11:53:49 +09:00
52 changed files with 4998 additions and 1399 deletions

39
.github/workflows/deploy.yml vendored Normal file
View 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
View 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
View File

@@ -9,3 +9,5 @@ book-example/book
.vscode
tests/dummy_book/book/
# Ignore Jetbrains specific files.
.idea/

View File

@@ -1,82 +0,0 @@
language: rust
cache:
directories:
- "$HOME/.cargo"
- "$HOME/.cache/sccache"
before_cache:
- rm -rf "$HOME/.cargo/registry"
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: stable
os: osx
env: TARGET=x86_64-apple-darwin
before_install:
- export SCCACHE_VER=0.2.8 RUSTC_WRAPPER=sccache
- case "$TRAVIS_OS_NAME" in
linux )
cd /tmp
&& travis_retry curl -sSL "https://github.com/mozilla/sccache/releases/download/${SCCACHE_VER}/sccache-${SCCACHE_VER}-x86_64-unknown-linux-musl.tar.gz" | tar xzf -
&& sudo mv "sccache-${SCCACHE_VER}-x86_64-unknown-linux-musl/sccache" /usr/local/bin/sccache;
;;
osx )
cd "${TMPDIR}"
&& travis_retry curl -sSL "https://github.com/mozilla/sccache/releases/download/${SCCACHE_VER}/sccache-${SCCACHE_VER}-x86_64-apple-darwin.tar.gz" | tar xzf -
&& sudo mv "sccache-${SCCACHE_VER}-x86_64-apple-darwin/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;
rustfmt -vV;
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: true
on:
condition: $TRAVIS_OS_NAME = "linux" && $TRAVIS_RUST_VERSION = "stable"
tags: true
branches:
only:
- master
- /^v\d+\.\d+\.\d+.*$/
notifications:
email:
on_success: never

View File

@@ -1,7 +1,102 @@
# 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)
### Added
- 🔥 Added ability to include files using anchor points instead of line numbers.
[#851](https://github.com/rust-lang-nursery/mdBook/pull/851)
- Added `language` configuration value to set the language of the book, which
will affect things like the `<html lang="en">` tag.
[#941](https://github.com/rust-lang-nursery/mdBook/pull/941)
### Changed
- Updated to handlebars 2.0.
[#977](https://github.com/rust-lang-nursery/mdBook/pull/977)
### Fixed
- Fixed memory leak warning.
[#967](https://github.com/rust-lang-nursery/mdBook/pull/967)
- Fix more print.html links.
[#963](https://github.com/rust-lang-nursery/mdBook/pull/963)
- Fixed crash on some unicode input.
[#978](https://github.com/rust-lang-nursery/mdBook/pull/978)
## mdBook 0.3.0
[6cbc41d...84d4063](https://github.com/rust-lang-nursery/mdBook/compare/6cbc41d...84d4063)
[6cbc41d...69a08ef](https://github.com/rust-lang-nursery/mdBook/compare/6cbc41d...69a08ef)
### Added
- Added ability to resize the sidebar.

1006
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "mdbook"
version = "0.3.0"
version = "0.3.2"
authors = [
"Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>",
@@ -20,7 +20,7 @@ chrono = "0.4"
clap = "2.24"
env_logger = "0.6"
error-chain = "0.12"
handlebars = { version = "1.0", default-features = false, features = ["no_dir_source"] }
handlebars = { version = "2.0", default-features = false, features = ["no_dir_source"] }
itertools = "0.8"
lazy_static = "1.0"
log = "0.4"
@@ -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", 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"]

View File

@@ -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>
[![Build Status](https://github.com/rust-lang-nursery/mdBook/workflows/CI/badge.svg)](https://github.com/rust-lang-nursery/mdBook/actions?workflow=CI)
[![crates.io](https://img.shields.io/crates/v/mdbook.svg)](https://crates.io/crates/mdbook)
[![LICENSE](https://img.shields.io/github/license/rust-lang-nursery/mdBook.svg)](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.20 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
@@ -239,6 +225,6 @@ All the code in this repository is released under the ***Mozilla Public License
[releases]: https://github.com/rust-lang-nursery/mdBook/releases
[Rust]: https://www.rust-lang.org/
[CLI docs]: http://rust-lang-nursery.github.io/mdBook/cli/init.html
[master-docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
[master-docs]: http://rust-lang-nursery.github.io/mdBook/
[`linkcheck`]: https://crates.io/crates/mdbook-linkcheck
[`epub`]: https://crates.io/crates/mdbook-epub
[`epub`]: https://crates.io/crates/mdbook-epub

View File

@@ -1,56 +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:
- 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+.*$/

View File

@@ -2,12 +2,14 @@
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David", "Michael-F-Bryan"]
language = "en"
[output.html]
mathjax-support = true
[output.html.playpen]
editable = true
line-numbers = true
[output.html.search]
limit-results = 20

View File

@@ -19,7 +19,7 @@ The two main ways a developer can hook into the book's build process is via,
The process of rendering a book project goes through several steps.
1. Load the book
1. Load the book
- Parse the `book.toml`, falling back to the default `Config` if it doesn't
exist
- Load the book chapters into memory
@@ -41,6 +41,6 @@ The easiest way to find out how to use the `mdbook` crate is by looking at the
explanation on the configuration system.
[`MDBook`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.MDBook.html
[`MDBook`]: https://docs.rs/mdbook/*/mdbook/book/struct.MDBook.html
[API Docs]: https://docs.rs/mdbook/*/mdbook/
[config]: file:///home/michael/Documents/forks/mdBook/target/doc/mdbook/config/index.html
[config]: https://docs.rs/mdbook/*/mdbook/config/index.html

View File

@@ -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
@@ -342,10 +346,10 @@ the source code or ask questions.
[mdbook-epub]: https://github.com/Michael-F-Bryan/mdbook-epub
[mdbook-test]: https://github.com/Michael-F-Bryan/mdbook-test
[rust-skeptic]: https://github.com/budziq/rust-skeptic
[`RenderContext`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html
[`RenderContext::from_json()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/renderer/struct.RenderContext.html#method.from_json
[`RenderContext`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html
[`RenderContext::from_json()`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html#method.from_json
[`semver`]: https://crates.io/crates/semver
[`Book`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html
[`Book::iter()`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.Book.html#method.iter
[`Config`]: http://rust-lang-nursery.github.io/mdBook/mdbook/config/struct.Config.html
[`Book`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html
[`Book::iter()`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html#method.iter
[`Config`]: https://docs.rs/mdbook/*/mdbook/config/struct.Config.html
[issue tracker]: https://github.com/rust-lang-nursery/mdBook/issues

View File

@@ -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.
@@ -42,6 +42,7 @@ This is general information about your book.
- **src:** By default, the source directory is found in the directory named
`src` directly under the root folder. But this is configurable with the `src`
key in the configuration file.
- **language:** The main language of the book, which is used as a language attribute `<html lang="en">` for example.
**book.toml**
```toml
@@ -50,6 +51,7 @@ title = "Example book"
authors = ["John Doe", "Jane Doe"]
description = "The example book covers examples."
src = "my-src" # the source files will be found in `root/my-src` instead of `root/src`
language = "en"
```
### Build options
@@ -79,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
@@ -148,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
@@ -164,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).
@@ -171,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/
@@ -187,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
@@ -214,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"
@@ -223,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
@@ -240,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

View File

@@ -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() {
@@ -63,6 +65,114 @@ the file are omitted. The third command includes all lines from line 2, i.e. the
first line is omitted. The last command includes the excerpt of `file.rs`
consisting of lines 2 to 10.
To avoid breaking your book when modifying included files, you can also
include a specific section using anchors instead of line numbers.
An anchor is a pair of matching lines. The line beginning an anchor must
match the regex "ANCHOR:\s*[\w_-]+" and similarly the ending line must match
the regex "ANCHOR_END:\s*[\w_-]+". This allows you to put anchors in
any kind of commented line.
Consider the following file to include:
```rs
/* ANCHOR: all */
// ANCHOR: component
struct Paddle {
hello: f32,
}
// ANCHOR_END: component
////////// ANCHOR: system
impl System for MySystem { ... }
////////// ANCHOR_END: system
/* ANCHOR_END: all */
```
Then in the book, all you have to do is:
````hbs
Here is a component:
```rust,no_run,noplaypen
\{{#include file.rs:component}}
```
Here is a system:
```rust,no_run,noplaypen
\{{#include file.rs:system}}
```
This is the full file.
```rust,no_run,noplaypen
\{{#include file.rs:all}}
```
````
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:

View File

@@ -17,9 +17,8 @@ handlebars template you can access this information by using
Here is a list of the properties that are exposed:
- ***language*** Language of the book in the form `en`. To use in <code
class="language-html">\<html lang="{{ language }}"></code> for example. At the
moment it is hardcoded.
- ***language*** Language of the book in the form `en`, as specified in `book.toml` (if not specified, defaults to `en`). To use in <code
class="language-html">\<html lang="{{ language }}"></code> for example.
- ***title*** Title of the book, as specified in `book.toml`
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`

View File

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

View File

@@ -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());
@@ -343,18 +330,18 @@ impl MDBook {
/// Look at the `Config` and try to figure out what renderers to use.
fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
let mut renderers: Vec<Box<dyn Renderer>> = Vec::new();
let mut renderers = Vec::new();
if let Some(output_table) = config.get("output").and_then(Value::as_table) {
for (key, table) in output_table.iter() {
// the "html" backend has its own Renderer
renderers.extend(output_table.iter().map(|(key, table)| {
if key == "html" {
renderers.push(Box::new(HtmlHandlebars::new()));
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
} else if key == "markdown" {
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
} else {
let renderer = interpret_custom_renderer(key, table);
renderers.push(renderer);
interpret_custom_renderer(key, table)
}
}
}));
}
// if we couldn't find anything, add the HTML renderer as a default

View File

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

View File

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

View File

@@ -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`.
@@ -131,10 +132,8 @@ impl Config {
pub fn update_from_env(&mut self) {
debug!("Updating the config from environment variables");
let overrides = env::vars().filter_map(|(key, value)| match parse_env(&key) {
Some(index) => Some((index, value)),
None => None,
});
let overrides =
env::vars().filter_map(|(key, value)| parse_env(&key).map(|index| (index, value)));
for (key, value) in overrides {
trace!("{} => {}", key, value);
@@ -151,14 +150,11 @@ impl Config {
/// `output.html.playpen` will fetch the "playpen" out of the html output
/// table).
pub fn get(&self, key: &str) -> Option<&Value> {
match self.rest.read(key) {
Ok(inner) => inner,
Err(_) => None,
}
self.rest.read(key).unwrap_or(None)
}
/// Fetch a value from the `Config` so you can mutate it.
pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Value> {
pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
match self.rest.read_mut(key) {
Ok(inner) => inner,
Err(_) => None,
@@ -173,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.
@@ -208,7 +223,7 @@ impl Config {
} else {
self.rest
.insert(index, value)
.map_err(|e| ErrorKind::TomlQueryError(e))?;
.map_err(ErrorKind::TomlQueryError)?;
}
Ok(())
@@ -376,6 +391,8 @@ pub struct BookConfig {
pub src: PathBuf,
/// Does this book support more than one language?
pub multilingual: bool,
/// The main language of the book.
pub language: Option<String>,
}
impl Default for BookConfig {
@@ -386,6 +403,7 @@ impl Default for BookConfig {
description: None,
src: PathBuf::from("src"),
multilingual: false,
language: Some(String::from("en")),
}
}
}
@@ -422,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?
@@ -433,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.
@@ -452,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 {
@@ -465,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,
}
}
}
@@ -496,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`.
@@ -545,12 +586,10 @@ trait Updateable<'de>: Serialize + Deserialize<'de> {
fn update_value<S: Serialize>(&mut self, key: &str, value: S) {
let mut raw = Value::try_from(&self).expect("unreachable");
{
if let Ok(value) = Value::try_from(value) {
let _ = raw.insert(key, value);
} else {
return;
}
if let Ok(value) = Value::try_from(value) {
let _ = raw.insert(key, value);
} else {
return;
}
if let Ok(updated) = raw.try_into() {
@@ -572,6 +611,7 @@ mod tests {
description = "A completely useless book"
multilingual = true
src = "source"
language = "ja"
[build]
build-dir = "outputs"
@@ -606,6 +646,7 @@ mod tests {
description: Some(String::from("A completely useless book")),
multilingual: true,
src: PathBuf::from("source"),
language: Some(String::from("ja")),
};
let build_should_be = BuildConfig {
build_dir: PathBuf::from("outputs"),
@@ -614,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,
@@ -658,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);
@@ -742,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);
}
@@ -783,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]
@@ -802,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
);
}

View File

@@ -1,8 +1,11 @@
use crate::errors::*;
use crate::utils::fs::file_to_string;
use crate::utils::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::ops::{Range, RangeFrom, RangeFull, RangeTo};
use std::fs;
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;
@@ -63,13 +73,13 @@ where
let mut previous_end_index = 0;
let mut replaced = String::new();
for playpen in find_links(s) {
replaced.push_str(&s[previous_end_index..playpen.start_index]);
for link in find_links(s) {
replaced.push_str(&s[previous_end_index..link.start_index]);
match playpen.render_with_path(&path) {
match link.render_with_path(&path) {
Ok(new_content) => {
if depth < MAX_LINK_NESTED_DEPTH {
if let Some(rel_path) = playpen.link.relative_path(path) {
if let Some(rel_path) = link.link_type.relative_path(path) {
replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1));
} else {
replaced.push_str(&new_content);
@@ -80,17 +90,17 @@ where
source.display()
);
}
previous_end_index = playpen.end_index;
previous_end_index = link.end_index;
}
Err(e) => {
error!("Error updating \"{}\", {}", playpen.link_text, e);
error!("Error updating \"{}\", {}", link.link_text, e);
for cause in e.iter().skip(1) {
warn!("Caused By: {}", cause);
}
// This should make sure we include the raw `{{# ... }}` snippet
// in the page content if there are any errors.
previous_end_index = playpen.start_index;
previous_end_index = link.start_index;
}
}
}
@@ -102,11 +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),
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> {
@@ -114,11 +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::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)),
}
}
}
@@ -130,46 +195,59 @@ 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();
// subtract 1 since line numbers usually begin with 1
let start = parts
.next()
.and_then(|s| s.parse::<usize>().ok())
.map(|val| val.saturating_sub(1));
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 {
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,
end_index: usize,
link: LinkType<'a>,
link_type: LinkType<'a>,
link_text: &'a str,
}
@@ -184,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,
}
}
@@ -193,11 +272,11 @@ impl<'a> Link<'a> {
_ => None,
};
link_type.and_then(|lnk| {
link_type.and_then(|lnk_type| {
cap.get(0).map(|mat| Link {
start_index: mat.start(),
end_index: mat.end(),
link: lnk,
link_type: lnk_type,
link_text: mat.as_str(),
})
})
@@ -205,14 +284,17 @@ impl<'a> Link<'a> {
fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
let base = base.as_ref();
match self.link {
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);
file_to_string(&target)
.map(|s| take_lines(&s, range.clone()))
fs::read_to_string(&target)
.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 {} ({})",
@@ -221,11 +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);
file_to_string(&target)
.map(|s| take_lines(&s, range.clone()))
fs::read_to_string(&target)
.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)
}
})
.chain_err(|| {
format!(
"Could not read file for link {} ({})",
@@ -234,34 +323,10 @@ impl<'a> Link<'a> {
)
})
}
LinkType::IncludeRangeTo(ref pat, ref range) => {
let target = base.join(pat);
file_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);
file_to_string(&target).chain_err(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
target.display()
)
})
}
LinkType::Playpen(ref pat, ref attrs) => {
let target = base.join(pat);
let contents = file_to_string(&target).chain_err(|| {
let contents = fs::read_to_string(&target).chain_err(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
@@ -303,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"
@@ -373,13 +438,13 @@ mod tests {
Link {
start_index: 22,
end_index: 42,
link: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
link_type: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
link_text: "{{#playpen file.rs}}",
},
Link {
start_index: 47,
end_index: 68,
link: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
link_type: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
link_text: "{{#playpen test.rs }}",
},
]
@@ -396,7 +461,10 @@ mod tests {
vec![Link {
start_index: 22,
end_index: 48,
link: 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}}",
}]
);
@@ -412,7 +480,10 @@ mod tests {
vec![Link {
start_index: 22,
end_index: 45,
link: 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}}",
}]
);
@@ -428,7 +499,10 @@ mod tests {
vec![Link {
start_index: 22,
end_index: 46,
link: 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:}}",
}]
);
@@ -444,7 +518,10 @@ mod tests {
vec![Link {
start_index: 22,
end_index: 46,
link: 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}}",
}]
);
@@ -460,7 +537,10 @@ mod tests {
vec![Link {
start_index: 22,
end_index: 44,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Range(LineRange::from(..))
),
link_text: "{{#include file.rs::}}",
}]
);
@@ -476,12 +556,34 @@ mod tests {
vec![Link {
start_index: 22,
end_index: 42,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Range(LineRange::from(..))
),
link_text: "{{#include file.rs}}",
}]
);
}
#[test]
fn test_find_links_with_anchor() {
let s = "Some random text with {{#include file.rs:anchor}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![Link {
start_index: 22,
end_index: 49,
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Anchor(String::from("anchor"))
),
link_text: "{{#include file.rs:anchor}}",
}]
);
}
#[test]
fn test_find_links_escaped_link() {
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
@@ -494,7 +596,7 @@ mod tests {
vec![Link {
start_index: 38,
end_index: 68,
link: LinkType::Escaped,
link_type: LinkType::Escaped,
link_text: "\\{{#playpen file.rs editable}}",
}]
);
@@ -513,13 +615,13 @@ mod tests {
Link {
start_index: 38,
end_index: 68,
link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
link_type: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
link_text: "{{#playpen file.rs editable }}",
},
Link {
start_index: 89,
end_index: 136,
link: LinkType::Playpen(
link_type: LinkType::Playpen(
PathBuf::from("my.rs"),
vec!["editable", "no_run", "should_panic"],
),
@@ -543,7 +645,10 @@ mod tests {
Link {
start_index: 38,
end_index: 58,
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Range(LineRange::from(..))
),
link_text: "{{#include file.rs}}",
}
);
@@ -552,7 +657,7 @@ mod tests {
Link {
start_index: 63,
end_index: 112,
link: LinkType::Escaped,
link_type: LinkType::Escaped,
link_text: "\\{{#contents are insignifficant in escaped link}}",
}
);
@@ -561,7 +666,7 @@ mod tests {
Link {
start_index: 130,
end_index: 177,
link: LinkType::Playpen(
link_type: LinkType::Playpen(
PathBuf::from("my.rs"),
vec!["editable", "no_run", "should_panic"]
),
@@ -570,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))
)
);
}
}

View File

@@ -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;
@@ -33,12 +34,10 @@ impl HtmlHandlebars {
let content = ch.content.clone();
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
let string_path = ch.path.parent().unwrap().display().to_string();
let fixed_content = utils::render_markdown_with_base(
let fixed_content = utils::render_markdown_with_path(
&ch.content,
ctx.html_config.curly_quotes,
&string_path,
Some(&ch.path),
);
print_content.push_str(&fixed_content);
@@ -73,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");
@@ -87,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);
@@ -284,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();
@@ -378,10 +387,12 @@ 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("language".to_owned(), json!("en"));
data.insert(
"language".to_owned(),
json!(config.book.language.clone().unwrap_or_default()),
);
data.insert(
"book_title".to_owned(),
json!(config.book.title.clone().unwrap_or_default()),
@@ -401,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")),
@@ -423,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")),
@@ -434,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") {
@@ -457,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",
@@ -475,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
@@ -566,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<'_>| {
@@ -579,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()
@@ -669,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);
}
}
}

View File

@@ -51,17 +51,31 @@ fn find_chapter(
) -> Result<Option<StringMap>, RenderError> {
debug!("Get data from context");
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone())
.map_err(|_| RenderError::new("Could not decode the JSON data"))
})?;
let base_path = rc
.evaluate_absolute(ctx, "path", true)?
.evaluate(ctx, "@root/path")?
.as_json()
.as_str()
.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");
@@ -96,7 +110,8 @@ fn render(
let mut context = BTreeMap::new();
let base_path = rc
.evaluate_absolute(ctx, "path", false)?
.evaluate(ctx, "@root/path")?
.as_json()
.as_str()
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");

View File

@@ -13,13 +13,14 @@ pub fn theme_option(
RenderError::new("Param 0 with String type is required for theme_option helper.")
})?;
let theme_name = rc
.evaluate_absolute(ctx, "default_theme", true)?
let default_theme = rc.evaluate(ctx, "@root/default_theme")?;
let default_theme_name = default_theme
.as_json()
.as_str()
.ok_or_else(|| RenderError::new("Type error for `default_theme`, string expected"))?;
out.write(param)?;
if param.to_lowercase() == theme_name.to_lowercase() {
if param.to_lowercase() == default_theme_name.to_lowercase() {
out.write(" (default)")?;
}

View File

@@ -24,16 +24,40 @@ impl HelperDef for RenderToc {
// get value from context data
// rc.get_path() is current json parent path, you should always use it like this
// param is the key of value you want to display
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
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
.evaluate_absolute(ctx, "path", true)?
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;
@@ -45,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 {
@@ -57,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
@@ -86,11 +119,11 @@ impl HelperDef for RenderToc {
.replace("\\", "/");
// Add link
out.write(&utils::fs::path_to_root(&current))?;
out.write(&utils::fs::path_to_root(&current_path))?;
out.write(&tmp)?;
out.write("\"")?;
if path == &current {
if path == &current_path {
out.write(" class=\"active\"")?;
}
@@ -133,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 {
@@ -145,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)
}

View 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(())
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,22 +1,9 @@
use crate::errors::*;
use std::convert::Into;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::io::Write;
use std::path::{Component, Path, PathBuf};
/// Takes a path to a file and try to read the file into a String
pub fn file_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
let path = path.as_ref();
let mut content = String::new();
File::open(path)
.chain_err(|| "Unable to open the file")?
.read_to_string(&mut content)
.chain_err(|| "Unable to read the file")?;
Ok(content)
}
/// Naively replaces any path seperator with a forward-slash '/'
pub fn normalize_path(path: &str) -> String {
use std::path::is_separator;

View File

@@ -8,8 +8,13 @@ use regex::Regex;
use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag};
use std::borrow::Cow;
use std::fmt::Write;
use std::path::Path;
pub use self::string::{take_lines, RangeArgument};
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> {
@@ -65,20 +70,47 @@ pub fn id_from_content(content: &str) -> String {
normalize_id(trimmed)
}
fn adjust_links<'a>(event: Event<'a>, with_base: &str) -> Event<'a> {
/// Fix links to the correct location.
///
/// This adjusts links, such as turning `.md` extensions to `.html`.
///
/// `path` is the path to the page being rendered relative to the root of the
/// book. This is used for the `print.html` page so that links on the print
/// page go to the original location. Normal page rendering sets `path` to
/// None. Ideally, print page links would link to anchors on the print page,
/// but that is very difficult.
fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
lazy_static! {
static ref SCHEME_LINK: Regex = Regex::new(r"^[a-z][a-z0-9+.-]*:").unwrap();
static ref MD_LINK: Regex = Regex::new(r"(?P<link>.*)\.md(?P<anchor>#.*)?").unwrap();
}
fn fix<'a>(dest: CowStr<'a>, base: &str) -> CowStr<'a> {
fn fix<'a>(dest: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
if dest.starts_with('#') {
// Fragment-only link.
if let Some(path) = path {
let mut base = path.display().to_string();
if base.ends_with(".md") {
base.replace_range(base.len() - 3.., ".html");
}
return format!("{}{}", base, dest).into();
} else {
return dest;
}
}
// Don't modify links with schemes like `https`.
if !SCHEME_LINK.is_match(&dest) {
// This is a relative link, adjust it as necessary.
let mut fixed_link = String::new();
if !base.is_empty() {
fixed_link.push_str(base);
fixed_link.push_str("/");
if let Some(path) = path {
let base = path
.parent()
.expect("path can't be empty")
.to_str()
.expect("utf-8 paths only");
if !base.is_empty() {
write!(fixed_link, "{}/", base).unwrap();
}
}
if let Some(caps) = MD_LINK.captures(&dest) {
@@ -95,20 +127,45 @@ fn adjust_links<'a>(event: Event<'a>, with_base: &str) -> Event<'a> {
dest
}
fn fix_html<'a>(html: CowStr<'a>, path: Option<&Path>) -> CowStr<'a> {
// This is a terrible hack, but should be reasonably reliable. Nobody
// should ever parse a tag with a regex. However, there isn't anything
// in Rust that I know of that is suitable for handling partial html
// fragments like those generated by pulldown_cmark.
//
// There are dozens of HTML tags/attributes that contain paths, so
// feel free to add more tags if desired; these are the only ones I
// care about right now.
lazy_static! {
static ref HTML_LINK: Regex =
Regex::new(r#"(<(?:a|img) [^>]*?(?:src|href)=")([^"]+?)""#).unwrap();
}
HTML_LINK
.replace_all(&html, |caps: &regex::Captures<'_>| {
let fixed = fix(caps[2].into(), path);
format!("{}{}\"", &caps[1], fixed)
})
.into_owned()
.into()
}
match event {
Event::Start(Tag::Link(link_type, dest, title)) => {
Event::Start(Tag::Link(link_type, fix(dest, with_base), title))
Event::Start(Tag::Link(link_type, fix(dest, path), title))
}
Event::Start(Tag::Image(link_type, dest, title)) => {
Event::Start(Tag::Image(link_type, fix(dest, with_base), title))
Event::Start(Tag::Image(link_type, fix(dest, path), title))
}
Event::Html(html) => Event::Html(fix_html(html, path)),
Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)),
_ => event,
}
}
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
render_markdown_with_base(text, curly_quotes, "")
render_markdown_with_path(text, curly_quotes, None)
}
pub fn new_cmark_parser(text: &str) -> Parser<'_> {
@@ -120,13 +177,13 @@ pub fn new_cmark_parser(text: &str) -> Parser<'_> {
Parser::new_ext(text, opts)
}
pub fn render_markdown_with_base(text: &str, curly_quotes: bool, base: &str) -> String {
pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&Path>) -> String {
let mut s = String::with_capacity(text.len() * 3 / 2);
let p = new_cmark_parser(text);
let mut converter = EventQuoteConverter::new(curly_quotes);
let events = p
.map(clean_codeblock_headers)
.map(|event| adjust_links(event, base))
.map(|event| adjust_links(event, path))
.map(|event| converter.convert(event));
html::push_html(&mut s, events);
@@ -394,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]

View File

@@ -1,63 +1,124 @@
use itertools::Itertools;
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
// This trait is already contained in the standard lib, however it is unstable.
// TODO: Remove when the `collections_range` feature stabilises
// (https://github.com/rust-lang/rust/issues/30877)
pub trait RangeArgument<T: ?Sized> {
fn start(&self) -> Option<&T>;
fn end(&self) -> Option<&T>;
}
impl<T: ?Sized> RangeArgument<T> for RangeFull {
fn start(&self) -> Option<&T> {
None
}
fn end(&self) -> Option<&T> {
None
}
}
impl<T> RangeArgument<T> for RangeFrom<T> {
fn start(&self) -> Option<&T> {
Some(&self.start)
}
fn end(&self) -> Option<&T> {
None
}
}
impl<T> RangeArgument<T> for RangeTo<T> {
fn start(&self) -> Option<&T> {
None
}
fn end(&self) -> Option<&T> {
Some(&self.end)
}
}
impl<T> RangeArgument<T> for Range<T> {
fn start(&self) -> Option<&T> {
Some(&self.start)
}
fn end(&self) -> Option<&T> {
Some(&self.end)
}
}
use regex::Regex;
use std::ops::Bound::{Excluded, Included, Unbounded};
use std::ops::RangeBounds;
/// Take a range of lines from a string.
pub fn take_lines<R: RangeArgument<usize>>(s: &str, range: R) -> String {
let start = *range.start().unwrap_or(&0);
pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
let start = match range.start_bound() {
Excluded(&n) => n + 1,
Included(&n) => n,
Unbounded => 0,
};
let mut lines = s.lines().skip(start);
match range.end() {
Some(&end) => lines.take(end.saturating_sub(start)).join("\n"),
None => lines.join("\n"),
match range.end_bound() {
Excluded(end) => lines.take(end.saturating_sub(start)).join("\n"),
Included(end) => lines.take((end + 1).saturating_sub(start)).join("\n"),
Unbounded => lines.join("\n"),
}
}
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 {
let mut retained = Vec::<&str>::new();
let mut anchor_found = false;
for l in s.lines() {
if anchor_found {
match ANCHOR_END.captures(l) {
Some(cap) => {
if &cap["anchor_name"] == anchor {
break;
}
}
None => {
if !ANCHOR_START.is_match(l) {
retained.push(l);
}
}
}
} else {
if let Some(cap) = ANCHOR_START.captures(l) {
if &cap["anchor_name"] == anchor {
anchor_found = true;
}
}
}
}
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_lines;
use super::{
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
take_rustdoc_include_lines,
};
#[test]
fn take_lines_test() {
@@ -70,4 +131,122 @@ mod tests {
assert_eq!(take_lines(s, 4..3), "");
assert_eq!(take_lines(s, ..100), s);
}
#[test]
fn take_anchored_lines_test() {
let s = "Lorem\nipsum\ndolor\nsit\namet";
assert_eq!(take_anchored_lines(s, "test"), "");
let s = "Lorem\nipsum\ndolor\nANCHOR_END: test\nsit\namet";
assert_eq!(take_anchored_lines(s, "test"), "");
let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet";
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
assert_eq!(take_anchored_lines(s, "something"), "");
let s = "Lorem\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
assert_eq!(take_anchored_lines(s, "test"), "dolor\nsit\namet");
assert_eq!(take_anchored_lines(s, "something"), "");
let s = "Lorem\nANCHOR: test\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nipsum";
assert_eq!(take_anchored_lines(s, "test"), "ipsum\ndolor\nsit\namet");
assert_eq!(take_anchored_lines(s, "something"), "");
let s = "Lorem\nANCHOR: test2\nipsum\nANCHOR: test\ndolor\nsit\namet\nANCHOR_END: test\nlorem\nANCHOR_END:test2\nipsum";
assert_eq!(
take_anchored_lines(s, "test2"),
"ipsum\ndolor\nsit\namet\nlorem"
);
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"
);
}
}

View File

@@ -5,7 +5,6 @@
#![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
use mdbook::errors::*;
use mdbook::utils::fs::file_to_string;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
@@ -53,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)?;
@@ -64,7 +69,7 @@ impl DummyBook {
}
fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()> {
let contents = file_to_string(filename)?;
let contents = fs::read_to_string(filename)?;
File::create(filename)?.write_all(contents.replace(from, to).as_bytes())?;
Ok(())
@@ -74,7 +79,7 @@ fn replace_pattern_in_file(filename: &Path, from: &str, to: &str) -> Result<()>
/// the list of strings asserting that the file contains all of them.
pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
let filename = filename.as_ref();
let content = file_to_string(filename).expect("Couldn't read the file's contents");
let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(
@@ -89,7 +94,7 @@ pub fn assert_contains_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
pub fn assert_doesnt_contain_strings<P: AsRef<Path>>(filename: P, strings: &[&str]) {
let filename = filename.as_ref();
let content = file_to_string(filename).expect("Couldn't read the file's contents");
let content = fs::read_to_string(filename).expect("Couldn't read the file's contents");
for s in strings {
assert!(

View File

@@ -8,6 +8,7 @@
- [Includes](first/includes.md)
- [Recursive](first/recursive.md)
- [Markdown](first/markdown.md)
- [Unicode](first/unicode.md)
- [Second Chapter](second.md)
- [Nested Chapter](second/nested.md)

View 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

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
fn some_function() {
assert!($TEST_STATUS);
}
fn main() {
some_function();
}

View File

@@ -0,0 +1,21 @@
# Unicode stress tests
Please be careful editing, this contains carefully crafted characters.
Two byte character: spatiëring
Combining character: spatiëring
Three byte character: 书こんにちは
Four byte character: 𐌀‮𐌁‮𐌂‮𐌃‮𐌄‮𐌅‮𐌆‮𐌇‮𐌈‬
Right-to-left: مرحبا
Emoticons: 🔊 😍 💜 1
right-to-left mark: hello באמת!
Zalgo: ǫ̛̖̱̗̝͈̋͒͋̏ͥͫ̒̆ͩ̏͌̾͊͐ͪ̾̚

View File

@@ -3,6 +3,14 @@
When we link to [the first section](../first/nested.md), it should work on
both the print page and the non-print page.
A [fragment link](#some-section) should work.
Link [outside](../../std/foo/bar.html).
![Some image](../images/picture.png)
<a href="../first/markdown.md">HTML Link</a>
<img src="../images/picture.png" alt="raw html">
## Some section

View File

@@ -7,7 +7,7 @@ use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings,
use mdbook::config::Config;
use mdbook::errors::*;
use mdbook::utils::fs::{file_to_string, write_file};
use mdbook::utils::fs::write_file;
use mdbook::MDBook;
use select::document::Document;
use select::predicate::{Class, Name, Predicate};
@@ -31,6 +31,7 @@ const TOC_SECOND_LEVEL: &[&str] = &[
"1.2. Includes",
"1.3. Recursive",
"1.4. Markdown",
"1.5. Unicode",
"2.1. Nested Chapter",
];
@@ -124,6 +125,9 @@ fn check_correct_relative_links_in_print_page() {
r##"<a href="second/../first/nested.html">the first section</a>,"##,
r##"<a href="second/../../std/foo/bar.html">outside</a>"##,
r##"<img src="second/../images/picture.png" alt="Some image" />"##,
r##"<a href="second/nested.html#some-section">fragment link</a>"##,
r##"<a href="second/../first/markdown.html">HTML Link</a>"##,
r##"<img src="second/../images/picture.png" alt="raw html">"##,
],
);
}
@@ -143,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![
@@ -220,7 +253,7 @@ fn root_index_html() -> Result<Document> {
.chain_err(|| "Book building failed")?;
let index_page = temp.path().join("book").join("index.html");
let html = file_to_string(&index_page).chain_err(|| "Unable to read index.html")?;
let html = fs::read_to_string(&index_page).chain_err(|| "Unable to read index.html")?;
Ok(Document::from(html.as_str()))
}
@@ -231,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)
@@ -250,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)
@@ -468,14 +510,13 @@ fn markdown_options() {
#[cfg(feature = "search")]
mod search {
use crate::dummy_book::DummyBook;
use mdbook::utils::fs::file_to_string;
use mdbook::MDBook;
use std::fs::File;
use std::fs::{self, File};
use std::path::Path;
fn read_book_index(root: &Path) -> serde_json::Value {
let index = root.join("book/searchindex.js");
let index = file_to_string(index).unwrap();
let index = fs::read_to_string(index).unwrap();
let index = index.trim_start_matches("Object.assign(window.search, ");
let index = index.trim_end_matches(");");
serde_json::from_str(&index).unwrap()
@@ -511,7 +552,7 @@ mod search {
assert_eq!(docs[&some_section]["body"], "");
assert_eq!(
docs[&summary]["body"],
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Second Chapter Nested Chapter Conclusion"
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode Second Chapter Nested Chapter Conclusion"
);
assert_eq!(docs[&summary]["breadcrumbs"], "First Chapter » Summary");
assert_eq!(docs[&conclusion]["body"], "I put &lt;HTML&gt; in here!");

File diff suppressed because it is too large Load Diff

View File

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