mirror of
https://github.com/rust-lang/mdBook.git
synced 2025-12-28 12:41:16 -05:00
Compare commits
8 Commits
v0.4.3
...
smart-prep
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a00ccf8c72 | ||
|
|
18a36ec1fa | ||
|
|
40ede19103 | ||
|
|
4d7027f8a7 | ||
|
|
d0a6d7c87c | ||
|
|
995efc22d5 | ||
|
|
a0702037bd | ||
|
|
769fd94868 |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
* text=auto eol=lf
|
||||
*.rs rust
|
||||
*.woff binary
|
||||
*.ttf binary
|
||||
*.otf binary
|
||||
*.png binary
|
||||
*.woff -text
|
||||
*.ttf -text
|
||||
*.otf -text
|
||||
*.png -text
|
||||
|
||||
42
.github/workflows/deploy.yml
vendored
42
.github/workflows/deploy.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: Deploy
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Deploy Release
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install hub
|
||||
run: ci/install-hub.sh ${{ matrix.os }}
|
||||
shell: bash
|
||||
- name: Install Rust
|
||||
run: ci/install-rust.sh stable
|
||||
shell: bash
|
||||
- name: Build and deploy artifacts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ci/make-release.sh ${{ matrix.os }}
|
||||
shell: bash
|
||||
pages:
|
||||
name: GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Build book
|
||||
run: cargo run -- build book-example
|
||||
- name: Deploy to GitHub
|
||||
env:
|
||||
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
|
||||
run: |
|
||||
touch book-example/book/.nojekyll
|
||||
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
|
||||
cd book-example/book
|
||||
/tmp/deploy
|
||||
51
.github/workflows/main.yml
vendored
51
.github/workflows/main.yml
vendored
@@ -1,51 +0,0 @@
|
||||
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.39.0
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh ${{ matrix.rust }}
|
||||
- name: Build and run tests
|
||||
run: cargo test
|
||||
- name: Test no default
|
||||
run: cargo test --no-default-features
|
||||
|
||||
rustfmt:
|
||||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust
|
||||
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||
- run: cargo fmt -- --check
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,5 +9,3 @@ book-example/book
|
||||
.vscode
|
||||
tests/dummy_book/book/
|
||||
|
||||
# Ignore Jetbrains specific files.
|
||||
.idea/
|
||||
|
||||
48
.travis.yml
Normal file
48
.travis.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
language: rust
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
|
||||
os:
|
||||
- linux
|
||||
- osx
|
||||
|
||||
cache:
|
||||
timeout: 360
|
||||
cargo: true
|
||||
|
||||
before_cache:
|
||||
- chmod -R a+r $HOME/.cargo
|
||||
|
||||
env:
|
||||
global:
|
||||
- CRATE_NAME=mdbook
|
||||
|
||||
script:
|
||||
- cargo test --all
|
||||
- cargo test --all --no-default-features
|
||||
|
||||
before_deploy:
|
||||
- sh ci/before_deploy.sh
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
- secure: cURRWBr034iqBz/ifD7uOunBfNR30YxIXfgLX0osWz+iafkVbhDGYYz9sBmRraqO2P7L2koEXMADVb/md1kI2+ykiq/ml+l9zuEAZPVmvSGUN7ZD+7s+lu3l5OBPG5z175T+b2q2q2m8XVR7TW20ra4QbE0bq06KAoOyjSgQVBTSCYsL9uTsGwiVRMEqqJT/BmKhKJNkpGsTKyBSKkOXvfeAAbE260vXUDEN9TYdJ3fvteRrpwLX56ee64gIZUq0RjDc4SKIEqilM6iUtNMvurqaewYNGkiXKRruV6BPCHxEHo6NNT46kOJLBJTf7gZw//dWhSoWpg9P0gdAnPWm407kSa3F7aJ1eRShAFQ4BLyfz9efTqm+jP3fOp7Mm7igSh9w6caSRuOnSsUf5+raRQ8E5Y9HsWGzzpZQk24Fx9EGZ04EeDSdpZAFz+jcbMpHf8t2p4CEx0CCNwYvKx6EydMKbMF5QteQ8SQkXNLhv7Rz2OgtXWYZPRVCMfQfOplsi2InsLCrQxTgwh+6u654SqVSgaHG+IncEAxBrdWy4rHcg7qereUcKfcY3k96vaDxdn/T2c00Ig0aNFR91YnixGMd6J6tQgDcRK9jh6fUm1CCBE9hT+pNUmtgYKuWBoLZexUZFFnfuBed0WciBot1bGDDamndqKq0jJiAzg+GMHk=
|
||||
file_glob: true
|
||||
file: "$CRATE_NAME-$TRAVIS_TAG-$TARGET.*"
|
||||
on:
|
||||
condition: "$TRAVIS_RUST_VERSION = stable"
|
||||
tags: true
|
||||
skip_cleanup: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
|
||||
notifications:
|
||||
email:
|
||||
on_success: never
|
||||
523
CHANGELOG.md
523
CHANGELOG.md
@@ -1,523 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## mdBook 0.4.3
|
||||
[9278b83...9278b83](https://github.com/rust-lang/mdBook/compare/9278b83...4df9ec9)
|
||||
|
||||
### Added
|
||||
- Added `output.html.cname` option to emit a `CNAME` file which is used by
|
||||
GitHub Pages to know which domain is being used.
|
||||
[#1311](https://github.com/rust-lang/mdBook/pull/1311)
|
||||
|
||||
### Changed
|
||||
- `mdbook test` no longer stops on the first test failure, but instead will
|
||||
run all the tests.
|
||||
[#1313](https://github.com/rust-lang/mdBook/pull/1313)
|
||||
- Removed the `local` font source for Source Code Pro, as the locally
|
||||
installed font may not render properly on FireFox on macOS.
|
||||
[#1307](https://github.com/rust-lang/mdBook/pull/1307)
|
||||
|
||||
### Fixed
|
||||
- Added newline to end of `.nojekyll` file.
|
||||
[#1310](https://github.com/rust-lang/mdBook/pull/1310)
|
||||
- Fixed missing space before draft chapter titles.
|
||||
[#1309](https://github.com/rust-lang/mdBook/pull/1309)
|
||||
|
||||
## mdBook 0.4.2
|
||||
[649f355...9278b83](https://github.com/rust-lang/mdBook/compare/649f355...9278b83)
|
||||
|
||||
### Changed
|
||||
- The "show hidden lines" icon has changed from the "expand" icon to an "eye".
|
||||
[#1281](https://github.com/rust-lang/mdBook/pull/1281)
|
||||
- Updated highlight.js. This adds several languages: c, c-like (effectively
|
||||
cpp), csharp (replaces cs), kotlin, less, lua, php-template, plaintext,
|
||||
python-repl, r, scss, typescript.
|
||||
[#1277](https://github.com/rust-lang/mdBook/pull/1277)
|
||||
|
||||
### Fixed
|
||||
- Fixed SUMMARY links that contained newlines.
|
||||
[#1291](https://github.com/rust-lang/mdBook/pull/1291)
|
||||
- Fixed SUMMARY links that contain `%20` spaces.
|
||||
[#1293](https://github.com/rust-lang/mdBook/pull/1293)
|
||||
- Fixed favicon so that if only the png or svg is overridden, the other is not
|
||||
automatically included in the `<link>` tag.
|
||||
[#1272](https://github.com/rust-lang/mdBook/pull/1272)
|
||||
|
||||
## mdBook 0.4.1
|
||||
[d4df7e7...649f355](https://github.com/rust-lang/mdBook/compare/d4df7e7...649f355)
|
||||
|
||||
### Changed
|
||||
- Removed several outdated dev-dependencies.
|
||||
[#1267](https://github.com/rust-lang/mdBook/pull/1267)
|
||||
|
||||
### Fixed
|
||||
- Fixed sidebar scrolling if the book includes part titles.
|
||||
[#1265](https://github.com/rust-lang/mdBook/pull/1265)
|
||||
- Don't include the default favicon if only one of the PNG or SVG is overridden.
|
||||
[#1266](https://github.com/rust-lang/mdBook/pull/1266)
|
||||
|
||||
## mdBook 0.4.0
|
||||
[99ecd4f...d4df7e7](https://github.com/rust-lang/mdBook/compare/99ecd4f...d4df7e7)
|
||||
|
||||
### Breaking Changes
|
||||
- Several of the changes in the release have altered the public API of the
|
||||
mdbook library.
|
||||
- Many dependencies have been updated or replaced.
|
||||
This also removes the `--websocket-hostname` and `--websocket-port` from
|
||||
the `serve` command.
|
||||
[#1211](https://github.com/rust-lang/mdBook/pull/1211)
|
||||
- A new "404" page is now automatically rendered. This requires knowledge of
|
||||
the base URL of your site to work properly. If you decide to use this as
|
||||
your 404 page, you should set the `site-url` setting in the book
|
||||
configuration so mdbook can generate the links correctly. Alternatively you
|
||||
can disable the 404 page generation, or set up your own 404 handling in your
|
||||
web server.
|
||||
[#1221](https://github.com/rust-lang/mdBook/pull/1221)
|
||||
- The `debug` and `output` features have been removed as they were unused.
|
||||
[#1211](https://github.com/rust-lang/mdBook/pull/1211)
|
||||
- If you are using customized themes, you may want to consider setting the
|
||||
`preferred-dark-theme` config setting, as it now defaults to "navy".
|
||||
[#1199](https://github.com/rust-lang/mdBook/pull/1199)
|
||||
- "Playpen" has been renamed to "playground". This is generally backwards
|
||||
compatible for users, but `{{#playpen}}` will now display warnings. This may
|
||||
impact books that have modified the "playpen" elements in the theme.
|
||||
[#1241](https://github.com/rust-lang/mdBook/pull/1241)
|
||||
- If a renderer is not installed, it is now treated as an error. If you want
|
||||
the old behavior of ignoring missing renderers, set the `optional` setting
|
||||
for that renderer.
|
||||
[#1122](https://github.com/rust-lang/mdBook/pull/1122)
|
||||
- If you have a custom favicon, you may need to look into adding an SVG
|
||||
version, otherwise the default SVG icon will be displayed.
|
||||
[#1230](https://github.com/rust-lang/mdBook/pull/1230)
|
||||
|
||||
### Added
|
||||
- Added a new `[rust]` configuration section to `book.toml`, which allows
|
||||
setting the default edition with `edition = "2018"`.
|
||||
[#1163](https://github.com/rust-lang/mdBook/pull/1163)
|
||||
- Renderers can now be marked as `optional`, so that they will be ignored if
|
||||
the renderer is not installed.
|
||||
[#1122](https://github.com/rust-lang/mdBook/pull/1122)
|
||||
- Added `head.hbs` to allow adding content to the `<head>` section in HTML.
|
||||
[#1206](https://github.com/rust-lang/mdBook/pull/1206)
|
||||
- Added "draft chapters". These are chapters listed without a link to indicate
|
||||
content yet to be written.
|
||||
[#1153](https://github.com/rust-lang/mdBook/pull/1153)
|
||||
- Added "parts" to split a book into different sections. Headers can be added
|
||||
to `SUMMARY.md` to signify different sections.
|
||||
[#1171](https://github.com/rust-lang/mdBook/pull/1171)
|
||||
- Added generation of a "404" page for handling missing pages and broken links.
|
||||
[#1221](https://github.com/rust-lang/mdBook/pull/1221)
|
||||
- Added configuration section for specifying URL redirects.
|
||||
[#1237](https://github.com/rust-lang/mdBook/pull/1237)
|
||||
- Added an SVG favicon that works with light and dark colors schemes.
|
||||
[#1230](https://github.com/rust-lang/mdBook/pull/1230)
|
||||
|
||||
### Changed
|
||||
- Changed default Rust attribute of `allow(unused_variables)` to `allow(unused)`.
|
||||
[#1195](https://github.com/rust-lang/mdBook/pull/1195)
|
||||
- Fonts are now served locally instead of from the Google Fonts CDN. The
|
||||
`copy-fonts` option was added to disable this if you want to supply your own
|
||||
fonts.
|
||||
[#1188](https://github.com/rust-lang/mdBook/pull/1188)
|
||||
- Switched the built-in webserver for the `serve` command to a new
|
||||
implementation. This results in some internal differences in how websockets
|
||||
are handled, which removes the separate websocket options. This should also
|
||||
make it easier to serve multiple books at once.
|
||||
[#1211](https://github.com/rust-lang/mdBook/pull/1211)
|
||||
- The default dark theme is now "navy".
|
||||
[#1199](https://github.com/rust-lang/mdBook/pull/1199)
|
||||
- "Playpen" has been renamed to "playground", matching the actual name of the
|
||||
service which was renamed many years ago.
|
||||
[#1241](https://github.com/rust-lang/mdBook/pull/1241)
|
||||
|
||||
### Fixed
|
||||
- Links with the `+` symbol should now work.
|
||||
[#1208](https://github.com/rust-lang/mdBook/pull/1208)
|
||||
- The `MDBOOK_BOOK` environment variable now correctly allows overriding the
|
||||
entire book configuration.
|
||||
[#1207](https://github.com/rust-lang/mdBook/pull/1207)
|
||||
- The sidebar can no longer be dragged outside of the window.
|
||||
[#1229](https://github.com/rust-lang/mdBook/pull/1229)
|
||||
- Hide the Rust Playground "play" button for `no_run` code samples.
|
||||
[#1249](https://github.com/rust-lang/mdBook/pull/1249)
|
||||
- Fixed the `--dest-dir` command-line option for the `serve` and `watch`
|
||||
commands.
|
||||
[#1228](https://github.com/rust-lang/mdBook/pull/1228)
|
||||
- Hotkey handlers are now disabled in `text` input fields (for example, typing
|
||||
`S` in a custom text input field).
|
||||
[#1244](https://github.com/rust-lang/mdBook/pull/1244)
|
||||
|
||||
## mdBook 0.3.7
|
||||
[88684d8...99ecd4f](https://github.com/rust-lang/mdBook/compare/88684d8...99ecd4f)
|
||||
|
||||
### Changed
|
||||
- Code spans in headers are no longer highlighted as code.
|
||||
[#1162](https://github.com/rust-lang/mdBook/pull/1162)
|
||||
- The sidebar will now scroll the activate page to the middle instead of the top.
|
||||
[#1161](https://github.com/rust-lang/mdBook/pull/1161)
|
||||
- Reverted change to reject build output within the `src` directory, and
|
||||
instead add a check that prevents infinite copies.
|
||||
[#1181](https://github.com/rust-lang/mdBook/pull/1181)
|
||||
[#1026](https://github.com/rust-lang/mdBook/pull/1026)
|
||||
|
||||
### Fixed
|
||||
- Fixed sidebar line-height jumping for collapsed chapters.
|
||||
[#1182](https://github.com/rust-lang/mdBook/pull/1182)
|
||||
- Fixed theme selector focus.
|
||||
[#1170](https://github.com/rust-lang/mdBook/pull/1170)
|
||||
|
||||
## mdBook 0.3.6
|
||||
[efdb832...88684d8](https://github.com/rust-lang/mdBook/compare/efdb832...88684d8)
|
||||
|
||||
### Added
|
||||
- `MDBook::execute_build_process` is now publicly accessible in the API so
|
||||
that plugins can more easily initiate the build process.
|
||||
[#1099](https://github.com/rust-lang/mdBook/pull/1099)
|
||||
|
||||
### Changed
|
||||
- Use a different color for Ayu theme's highlighting for Rust attributes (uses
|
||||
a bright color instead of the comment color).
|
||||
[#1133](https://github.com/rust-lang/mdBook/pull/1133)
|
||||
- Adjusted spacing of sidebar entries.
|
||||
[#1137](https://github.com/rust-lang/mdBook/pull/1137)
|
||||
- Slightly adjusted line-height of `<p>`, `<ul>`, and `<ol>`.
|
||||
[#1136](https://github.com/rust-lang/mdBook/pull/1136)
|
||||
- Handlebars updated to 3.0.
|
||||
[#1130](https://github.com/rust-lang/mdBook/pull/1130)
|
||||
|
||||
### Fixed
|
||||
- Fix an issue with sidebar scroll position on reload.
|
||||
[#1108](https://github.com/rust-lang/mdBook/pull/1108)
|
||||
- `mdbook serve` will retain the current scroll position when the page is reloaded.
|
||||
[#1097](https://github.com/rust-lang/mdBook/pull/1097)
|
||||
- Fixed the page name if the book didn't have a title to not be prefixed with ` - `.
|
||||
[#1145](https://github.com/rust-lang/mdBook/pull/1145)
|
||||
- HTML attributes `rel=next` and `rel=previous` are now supported in "wide"
|
||||
mode (previously they were only set in narrow mode).
|
||||
[#1150](https://github.com/rust-lang/mdBook/pull/1150)
|
||||
- Prevent recursive copies when the destination directory is contained in the
|
||||
source directory.
|
||||
[#1135](https://github.com/rust-lang/mdBook/pull/1135)
|
||||
- Adjusted the menu bar animation to not immediately obscure the top content.
|
||||
[#989](https://github.com/rust-lang/mdBook/pull/989)
|
||||
- Fix for comments in SUMMARY.md that appear between items.
|
||||
[#1167](https://github.com/rust-lang/mdBook/pull/1167)
|
||||
|
||||
## mdBook 0.3.5
|
||||
[6e0d0fa...efdb832](https://github.com/rust-lang/mdBook/compare/6e0d0fa...efdb832)
|
||||
|
||||
### Changed
|
||||
- The `default-theme` config setting is now case-insensitive.
|
||||
[#1079](https://github.com/rust-lang/mdBook/pull/1079)
|
||||
|
||||
### Fixed
|
||||
- Fixed `#` hidden Rust code lines not rendering properly.
|
||||
[#1088](https://github.com/rust-lang/mdBook/pull/1088)
|
||||
- Updated pulldown-cmark to 0.6.1, fixing several issues.
|
||||
[#1021](https://github.com/rust-lang/mdBook/pull/1021)
|
||||
|
||||
## mdBook 0.3.4
|
||||
[e5f77aa...6e0d0fa](https://github.com/rust-lang/mdBook/compare/e5f77aa...6e0d0fa)
|
||||
|
||||
### Changed
|
||||
- Switch to relative `rem` font sizes from `px`.
|
||||
[#894](https://github.com/rust-lang/mdBook/pull/894)
|
||||
- Migrated repository to https://github.com/rust-lang/mdBook/
|
||||
[#1083](https://github.com/rust-lang/mdBook/pull/1083)
|
||||
|
||||
## mdBook 0.3.3
|
||||
[2b649fe...e5f77aa](https://github.com/rust-lang/mdBook/compare/2b649fe...e5f77aa)
|
||||
|
||||
### Changed
|
||||
- Improvements to the automatic dark theme selection.
|
||||
[#1069](https://github.com/rust-lang/mdBook/pull/1069)
|
||||
- Fragment links now prevent scrolling the header behind the menu bar.
|
||||
[#1077](https://github.com/rust-lang/mdBook/pull/1077)
|
||||
|
||||
### Fixed
|
||||
- Fixed error when building a book that has a spacer immediately after the
|
||||
first chapter.
|
||||
[#1075](https://github.com/rust-lang/mdBook/pull/1075)
|
||||
|
||||
## mdBook 0.3.2
|
||||
[9cd47eb...2b649fe](https://github.com/rust-lang/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/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/mdBook/pull/1035)
|
||||
- The `watch` and `serve` commands will now ignore files listed in
|
||||
`.gitignore`.
|
||||
[#1044](https://github.com/rust-lang/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/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/mdBook/pull/1003)
|
||||
- Added Ctrl-Enter shortcut to the playpen editor to automatically run the
|
||||
sample.
|
||||
[#1066](https://github.com/rust-lang/mdBook/pull/1066)
|
||||
- Added `output.html.playpen.copyable` configuration option to disable
|
||||
the copy button.
|
||||
[#1050](https://github.com/rust-lang/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/mdBook/pull/1027)
|
||||
|
||||
### Changed
|
||||
- Use standard `scrollbar-color` CSS along with webkit extension
|
||||
[#816](https://github.com/rust-lang/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/mdBook/pull/985)
|
||||
- Next/prev links now highlight on hover to indicate it is clickable.
|
||||
[#994](https://github.com/rust-lang/mdBook/pull/994)
|
||||
- Increase padding of table headers.
|
||||
[#824](https://github.com/rust-lang/mdBook/pull/824)
|
||||
- Errors in `[output.html]` config are no longer ignored.
|
||||
[#1033](https://github.com/rust-lang/mdBook/pull/1033)
|
||||
- Updated highlight.js for syntax highlighting updates (primarily to add
|
||||
async/await to Rust highlighting).
|
||||
[#1041](https://github.com/rust-lang/mdBook/pull/1041)
|
||||
- Raised minimum supported rust version to 1.35.
|
||||
[#1003](https://github.com/rust-lang/mdBook/pull/1003)
|
||||
- Hidden code lines are no longer dynamically removed via JavaScript, but
|
||||
instead managed with CSS.
|
||||
[#846](https://github.com/rust-lang/mdBook/pull/846)
|
||||
[#1065](https://github.com/rust-lang/mdBook/pull/1065)
|
||||
- Changed the default font set for the ACE editor, giving preference to
|
||||
"Source Code Pro".
|
||||
[#1062](https://github.com/rust-lang/mdBook/pull/1062)
|
||||
- Windows 32-bit releases are no longer published.
|
||||
[#1071](https://github.com/rust-lang/mdBook/pull/1071)
|
||||
|
||||
### Fixed
|
||||
- Fixed sidebar auto-scrolling.
|
||||
[#1052](https://github.com/rust-lang/mdBook/pull/1052)
|
||||
- Fixed error message when running `clean` multiple times.
|
||||
[#1055](https://github.com/rust-lang/mdBook/pull/1055)
|
||||
- Actually fix the "next" link on index.html. The previous fix didn't work.
|
||||
[#1005](https://github.com/rust-lang/mdBook/pull/1005)
|
||||
- Stop using `inline-block` for `inline code`, fixing selection highlighting
|
||||
and some rendering issues.
|
||||
[#1058](https://github.com/rust-lang/mdBook/pull/1058)
|
||||
- Fix header auto-hide on browsers with momentum scrolling that allows
|
||||
negative `scrollTop`.
|
||||
[#1070](https://github.com/rust-lang/mdBook/pull/1070)
|
||||
|
||||
## mdBook 0.3.1
|
||||
[69a08ef...9cd47eb](https://github.com/rust-lang/mdBook/compare/69a08ef...9cd47eb)
|
||||
|
||||
### Added
|
||||
- 🔥 Added ability to include files using anchor points instead of line numbers.
|
||||
[#851](https://github.com/rust-lang/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/mdBook/pull/941)
|
||||
|
||||
### Changed
|
||||
- Updated to handlebars 2.0.
|
||||
[#977](https://github.com/rust-lang/mdBook/pull/977)
|
||||
|
||||
### Fixed
|
||||
- Fixed memory leak warning.
|
||||
[#967](https://github.com/rust-lang/mdBook/pull/967)
|
||||
- Fix more print.html links.
|
||||
[#963](https://github.com/rust-lang/mdBook/pull/963)
|
||||
- Fixed crash on some unicode input.
|
||||
[#978](https://github.com/rust-lang/mdBook/pull/978)
|
||||
|
||||
## mdBook 0.3.0
|
||||
[6cbc41d...69a08ef](https://github.com/rust-lang/mdBook/compare/6cbc41d...69a08ef)
|
||||
|
||||
### Added
|
||||
- Added ability to resize the sidebar.
|
||||
[#849](https://github.com/rust-lang/mdBook/pull/849)
|
||||
- Added `load_with_config_and_summary` function to `MDBook` to be able to
|
||||
build a book with a custom `Summary`.
|
||||
[#883](https://github.com/rust-lang/mdBook/pull/883)
|
||||
- Set `noindex` on `print.html` page to prevent robots from indexing it.
|
||||
[#844](https://github.com/rust-lang/mdBook/pull/844)
|
||||
- Added support for ~~strikethrough~~ and GitHub-style tasklists.
|
||||
[#952](https://github.com/rust-lang/mdBook/pull/952)
|
||||
|
||||
### Changed
|
||||
- Command-line help output is now colored.
|
||||
[#861](https://github.com/rust-lang/mdBook/pull/861)
|
||||
- The build directory is now deleted before rendering starts, instead of after
|
||||
if finishes.
|
||||
[#878](https://github.com/rust-lang/mdBook/pull/878)
|
||||
- Removed dependency on `same-file` crate.
|
||||
[#903](https://github.com/rust-lang/mdBook/pull/903)
|
||||
- 💥 Renamed `with_preprecessor` to `with_preprocessor`.
|
||||
[#906](https://github.com/rust-lang/mdBook/pull/906)
|
||||
- Updated ACE editor to 1.4.4, should remove a JavaScript console warning.
|
||||
[#935](https://github.com/rust-lang/mdBook/pull/935)
|
||||
- Dependencies have been updated.
|
||||
[#934](https://github.com/rust-lang/mdBook/pull/934)
|
||||
[#945](https://github.com/rust-lang/mdBook/pull/945)
|
||||
- Highlight.js has been updated. This fixes some TOML highlighting, and adds
|
||||
Julia support.
|
||||
[#942](https://github.com/rust-lang/mdBook/pull/942)
|
||||
- 🔥 Updated to pulldown-cmark 0.5. This may have significant changes to the
|
||||
formatting of existing books, as the newer version has more accurate
|
||||
interpretation of the CommonMark spec and a large number of bug fixes and
|
||||
changes.
|
||||
[#898](https://github.com/rust-lang/mdBook/pull/898)
|
||||
- The `diff` language should now highlight correctly.
|
||||
[#943](https://github.com/rust-lang/mdBook/pull/943)
|
||||
- Make the blank region of a header not clickable.
|
||||
[#948](https://github.com/rust-lang/mdBook/pull/948)
|
||||
- Rustdoc tests now use the preprocessed content instead of the raw,
|
||||
unpreprocessed content.
|
||||
[#891](https://github.com/rust-lang/mdBook/pull/891)
|
||||
|
||||
### Fixed
|
||||
- Fixed file change detection so that `mdbook serve` only reloads once when
|
||||
multiple files are changed at once.
|
||||
[#870](https://github.com/rust-lang/mdBook/pull/870)
|
||||
- Fixed on-hover color highlighting for links in sidebar.
|
||||
[#834](https://github.com/rust-lang/mdBook/pull/834)
|
||||
- Fixed loss of focus when clicking the "Copy" button in code blocks.
|
||||
[#867](https://github.com/rust-lang/mdBook/pull/867)
|
||||
- Fixed incorrectly stripping the path for `additional-js` files.
|
||||
[#796](https://github.com/rust-lang/mdBook/pull/796)
|
||||
- Fixed color of `code spans` that are links.
|
||||
[#905](https://github.com/rust-lang/mdBook/pull/905)
|
||||
- Fixed "next" navigation on index.html.
|
||||
[#916](https://github.com/rust-lang/mdBook/pull/916)
|
||||
- Fixed keyboard chapter navigation for `file` urls.
|
||||
[#915](https://github.com/rust-lang/mdBook/pull/915)
|
||||
- Fixed bad wrapping for inline code on some browsers.
|
||||
[#818](https://github.com/rust-lang/mdBook/pull/818)
|
||||
- Properly load an existing `SUMMARY.md` in `mdbook init`.
|
||||
[#841](https://github.com/rust-lang/mdBook/pull/841)
|
||||
- Fixed some broken links in `print.html`.
|
||||
[#871](https://github.com/rust-lang/mdBook/pull/871)
|
||||
- The Rust Playground link now supports the 2018 edition.
|
||||
[#946](https://github.com/rust-lang/mdBook/pull/946)
|
||||
|
||||
## mdBook 0.2.3 (2018-01-18)
|
||||
[2c20c99...6cbc41d](https://github.com/rust-lang/mdBook/compare/2c20c99...6cbc41d)
|
||||
|
||||
### Added
|
||||
- Added an optional button to the top of the page which will link to a git
|
||||
repository. Use the `git-repository-url` and `git-repository-icon` options
|
||||
in the `[output.html]` section to enable it and set its appearance.
|
||||
[#802](https://github.com/rust-lang/mdBook/pull/802)
|
||||
- Added a `default-theme` option to the `[output.html]` section.
|
||||
[#804](https://github.com/rust-lang/mdBook/pull/804)
|
||||
|
||||
### Changed
|
||||
- 💥 Header ID anchors no longer add an arbitrary `a` character for headers
|
||||
that start with a non-ascii-alphabetic character.
|
||||
[#788](https://github.com/rust-lang/mdBook/pull/788)
|
||||
|
||||
### Fixed
|
||||
- Fix websocket hostname usage
|
||||
[#865](https://github.com/rust-lang/mdBook/pull/865)
|
||||
- Fixing links in print.html
|
||||
[#866](https://github.com/rust-lang/mdBook/pull/866)
|
||||
|
||||
## mdBook 0.2.2 (2018-10-19)
|
||||
[7e2e095...2c20c99](https://github.com/rust-lang/mdBook/compare/7e2e095...2c20c99)
|
||||
|
||||
### Added
|
||||
- 🎉 Process-based custom preprocessors. See [the
|
||||
docs](https://rust-lang.github.io/mdBook/for_developers/preprocessors.html)
|
||||
for more.
|
||||
[#792](https://github.com/rust-lang/mdBook/pull/792)
|
||||
|
||||
- 🎉 Configurable preprocessors.
|
||||
|
||||
Added `build.use-default-preprocessors` boolean TOML key to allow disabling
|
||||
the built-in `links` and `index` preprocessors.
|
||||
|
||||
Added `[preprocessor]` TOML tables to configure each preprocessor.
|
||||
|
||||
Specifying `[preprocessor.links]` or `[preprocessor.index]` will enable the
|
||||
respective built-in preprocessor if `build.use-default-preprocessors` is
|
||||
`false`.
|
||||
|
||||
Added `fn supports_renderer(&self, renderer: &str) -> bool` to the
|
||||
`Preprocessor` trait to specify if the preprocessor supports the given
|
||||
renderer. The default implementation always returns `true`.
|
||||
|
||||
`Preprocessor::run` now takes a book by value instead of a mutable
|
||||
reference. It should return a `Book` value with the intended modifications.
|
||||
|
||||
Added `PreprocessorContext::renderer` to indicate the renderer being used.
|
||||
|
||||
[#658](https://github.com/rust-lang/mdBook/pull/658)
|
||||
[#787](https://github.com/rust-lang/mdBook/pull/787)
|
||||
|
||||
### Fixed
|
||||
- Fix paths to additional CSS and JavaScript files
|
||||
[#777](https://github.com/rust-lang/mdBook/pull/777)
|
||||
- Ensure section numbers are correctly incremented after a horizontal
|
||||
separator
|
||||
[#790](https://github.com/rust-lang/mdBook/pull/790)
|
||||
|
||||
## mdBook 0.2.1 (2018-08-22)
|
||||
[91ffca1...7e2e095](https://github.com/rust-lang/mdBook/compare/91ffca1...7e2e095)
|
||||
|
||||
### Changed
|
||||
- Update to handlebars-rs 1.0
|
||||
[#761](https://github.com/rust-lang/mdBook/pull/761)
|
||||
|
||||
### Fixed
|
||||
- Fix table colors, broken by Stylus -> CSS transition
|
||||
[#765](https://github.com/rust-lang/mdBook/pull/765)
|
||||
|
||||
## mdBook 0.2.0 (2018-08-02)
|
||||
|
||||
### Changed
|
||||
- 💥 This release changes how links are handled in mdBook. Previously, relative
|
||||
links were interpreted relative to the book's root. In `0.2.0`+ links are
|
||||
relative to the page they are in, and use the `.md` extension. This has [several
|
||||
advantages](https://github.com/rust-lang/mdBook/pull/603#issue-166701447),
|
||||
such as making links work in other markdown viewers like GitHub. You will
|
||||
likely have to change links in your book to accommodate this change. For
|
||||
example, a book with this layout:
|
||||
|
||||
```
|
||||
chapter_1/
|
||||
section_1.md
|
||||
section_2.md
|
||||
SUMMARY.md
|
||||
```
|
||||
|
||||
Previously a link in `section_1.md` to `section_2.md` would look like this:
|
||||
```markdown
|
||||
[section_2](chapter_1/section_2.html)
|
||||
```
|
||||
|
||||
Now it must be changed to this:
|
||||
```markdown
|
||||
[section_2](section_2.md)
|
||||
```
|
||||
|
||||
- 💥 `mdbook test --library-path` now accepts a comma-delimited list of
|
||||
arguments rather than taking all following arguments. This makes it easier
|
||||
to handle the trailing book directory argument without always needing to put
|
||||
` -- ` before it. Multiple instances of the option continue to be accepted:
|
||||
`mdbook test -L foo -L bar`.
|
||||
|
||||
- 💥 `mdbook serve` has some of its options renamed for clarity. See `mdbook
|
||||
help serve` for details.
|
||||
|
||||
- Embedded rust playpens now use the "stable" playground API.
|
||||
[#754](https://github.com/rust-lang/mdBook/pull/754)
|
||||
|
||||
### Fixed
|
||||
- Escaped includes (`\{{#include file.rs}}`) will now render correctly.
|
||||
[f30ce01](https://github.com/rust-lang/mdBook/commit/f30ce0184d71e342141145472bf816419d30a2c5)
|
||||
- `index.html` will now render correctly when the book's first section is
|
||||
inside a subdirectory.
|
||||
[#756](https://github.com/rust-lang/mdBook/pull/756)
|
||||
@@ -5,22 +5,22 @@ Welcome stranger!
|
||||
If you have come here to learn how to contribute to mdBook, we have some tips for you!
|
||||
|
||||
First of all, don't hesitate to ask questions!
|
||||
Use the [issue tracker](https://github.com/rust-lang/mdBook/issues), no question is too simple.
|
||||
Use the [issue tracker](https://github.com/rust-lang-nursery/mdBook/issues), no question is too simple.
|
||||
If we don't respond in a couple of days, ping us @Michael-F-Bryan, @budziq, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
|
||||
|
||||
### Issues to work on
|
||||
|
||||
Any issue is up for the grabbing, but if you are starting out, you might be interested in the
|
||||
[E-Easy issues](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
||||
[E-Easy issues](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
|
||||
Those are issues that are considered more straightforward for beginners to Rust or the codebase itself.
|
||||
These issues can be a good launching pad for more involved issues. Easy tasks for a first time contribution
|
||||
include documentation improvements, new tests, examples, updating dependencies, etc.
|
||||
|
||||
If you come from a web development background, you might be interested in issues related to web technologies tagged
|
||||
[A-JavaScript](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||
[A-Style](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
|
||||
[A-HTML](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
||||
[A-Mobile](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
|
||||
[A-JavaScript](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
|
||||
[A-Style](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
|
||||
[A-HTML](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
|
||||
[A-Mobile](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
|
||||
|
||||
When you decide you want to work on a specific issue, ping us on that issue so that we can assign it to you.
|
||||
Again, do not hesitate to ask questions. We will gladly mentor anyone that want to tackle an issue.
|
||||
@@ -41,68 +41,20 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
|
||||
0. Clone this repository with git.
|
||||
|
||||
```
|
||||
git clone https://github.com/rust-lang/mdBook.git
|
||||
git clone https://github.com/rust-lang-nursery/mdBook.git
|
||||
```
|
||||
0. Navigate into the newly created `mdBook` directory
|
||||
0. Run `cargo build`
|
||||
|
||||
The resulting binary can be found in `mdBook/target/debug/` under the name `mdBook` or `mdBook.exe`.
|
||||
|
||||
### Code Quality
|
||||
|
||||
We love code quality and Rust has some excellent tools to assist you with contributions.
|
||||
|
||||
#### Formatting Code with rustfmt
|
||||
|
||||
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
|
||||
This will ensure we have good quality source code that is better for us all to maintain.
|
||||
|
||||
[rustfmt](https://github.com/rust-lang/rustfmt) has a lot more information on the project.
|
||||
The quick guide is
|
||||
|
||||
1. Install it
|
||||
```
|
||||
rustup component add rustfmt
|
||||
```
|
||||
1. You can now run `rustfmt` on a single file simply by...
|
||||
```
|
||||
rustfmt src/path/to/your/file.rs
|
||||
```
|
||||
... or you can format the entire project with
|
||||
```
|
||||
cargo fmt
|
||||
```
|
||||
When run through `cargo` it will format all bin and lib files in the current crate.
|
||||
|
||||
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang/rustfmt)
|
||||
|
||||
|
||||
#### Finding Issues with Clippy
|
||||
|
||||
Clippy is a code analyser/linter detecting mistakes, and therfore helps to improve your code.
|
||||
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will
|
||||
help us maintain awesome code.
|
||||
|
||||
The best documentation can be found over at [rust-clippy](https://github.com/rust-lang/rust-clippy)
|
||||
|
||||
1. To install
|
||||
```
|
||||
rustup component add clippy
|
||||
```
|
||||
2. Running clippy
|
||||
```
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
Clippy has an ever growing list of checks, that are managed in [lint files](https://rust-lang.github.io/rust-clippy/master/index.html).
|
||||
|
||||
### Making a pull-request
|
||||
|
||||
When you feel comfortable that your changes could be integrated into mdBook, you can create a pull-request on GitHub.
|
||||
One of the core maintainers will then approve the changes or request some changes before it gets merged.
|
||||
|
||||
If you want to make your pull-request even better, you might want to run [Clippy](https://github.com/Manishearth/rust-clippy)
|
||||
and [rustfmt](https://github.com/rust-lang/rustfmt) on the code first.
|
||||
and [rustfmt](https://github.com/rust-lang-nursery/rustfmt) on the code first.
|
||||
This is not a requirement though and will never block a pull-request from being merged.
|
||||
|
||||
That's it, happy contributions! :tada: :tada: :tada:
|
||||
|
||||
1976
Cargo.lock
generated
1976
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
61
Cargo.toml
61
Cargo.toml
@@ -1,61 +1,64 @@
|
||||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.4.3"
|
||||
version = "0.2.2-alpha.0"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
"Matt Ickstadt <mattico8@gmail.com>"
|
||||
]
|
||||
documentation = "http://rust-lang.github.io/mdBook/index.html"
|
||||
edition = "2018"
|
||||
exclude = ["/book-example/*"]
|
||||
description = "Create books from markdown files"
|
||||
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
|
||||
repository = "https://github.com/rust-lang-nursery/mdBook"
|
||||
keywords = ["book", "gitbook", "rustbook", "markdown"]
|
||||
license = "MPL-2.0"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/rust-lang/mdBook"
|
||||
description = "Creates a book from markdown files"
|
||||
exclude = ["book-example/*"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.28"
|
||||
chrono = "0.4"
|
||||
clap = "2.24"
|
||||
env_logger = "0.7.1"
|
||||
handlebars = "3.0"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4"
|
||||
memchr = "2.0"
|
||||
open = "1.1"
|
||||
pulldown-cmark = "0.7.0"
|
||||
regex = "1.0.0"
|
||||
chrono = "0.4"
|
||||
handlebars = "1.0"
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
error-chain = "0.12"
|
||||
serde_json = "1.0"
|
||||
shlex = "0.1"
|
||||
pulldown-cmark = "0.1.2"
|
||||
lazy_static = "1.0"
|
||||
log = "0.4"
|
||||
env_logger = "0.5"
|
||||
toml = "0.4"
|
||||
memchr = "2.0"
|
||||
open = "1.1"
|
||||
regex = "1.0.0"
|
||||
tempfile = "3.0"
|
||||
toml = "0.5.1"
|
||||
itertools = "0.7"
|
||||
shlex = "0.1"
|
||||
toml-query = "0.7"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "4.0", optional = true }
|
||||
gitignore = { version = "1.0", optional = true }
|
||||
|
||||
# Serve feature
|
||||
futures-util = { version = "0.3.4", optional = true }
|
||||
tokio = { version = "0.2.18", features = ["macros"], optional = true }
|
||||
warp = { version = "0.2.2", default-features = false, features = ["websocket"], optional = true }
|
||||
iron = { version = "0.6", optional = true }
|
||||
staticfile = { version = "0.5", optional = true }
|
||||
ws = { version = "0.7", optional = true}
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
|
||||
ammonia = { version = "3", optional = true }
|
||||
ammonia = { version = "1.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
select = "0.5"
|
||||
pretty_assertions = "0.6"
|
||||
select = "0.4"
|
||||
pretty_assertions = "0.5"
|
||||
walkdir = "2.0"
|
||||
pulldown-cmark-to-cmark = "1.1.0"
|
||||
|
||||
[features]
|
||||
default = ["watch", "serve", "search"]
|
||||
watch = ["notify", "gitignore"]
|
||||
serve = ["futures-util", "tokio", "warp"]
|
||||
default = ["output", "watch", "serve", "search"]
|
||||
debug = []
|
||||
output = []
|
||||
watch = ["notify"]
|
||||
serve = ["iron", "staticfile", "ws"]
|
||||
search = ["elasticlunr-rs", "ammonia"]
|
||||
|
||||
[[bin]]
|
||||
|
||||
117
README.md
117
README.md
@@ -1,8 +1,25 @@
|
||||
# mdBook
|
||||
|
||||
[](https://github.com/rust-lang/mdBook/actions?workflow=CI)
|
||||
[](https://crates.io/crates/mdbook)
|
||||
[](LICENSE)
|
||||
<table>
|
||||
<tr>
|
||||
<td><strong>Linux / OS X</strong></td>
|
||||
<td>
|
||||
<a href="https://travis-ci.org/rust-lang-nursery/mdBook"><img src="https://travis-ci.org/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>
|
||||
|
||||
mdBook is a utility to create modern online books from Markdown files.
|
||||
|
||||
@@ -24,7 +41,7 @@ There are multiple ways to install mdBook.
|
||||
|
||||
2. **From Crates.io**
|
||||
|
||||
This requires at least [Rust] 1.39 and Cargo to be installed. Once you have installed
|
||||
This requires at least [Rust] 1.20 and Cargo to be installed. Once you have installed
|
||||
Rust, type the following in the terminal:
|
||||
|
||||
```
|
||||
@@ -40,37 +57,33 @@ There are multiple ways to install mdBook.
|
||||
another CI server, we recommend that you specify a semver version range for
|
||||
mdBook when you install it through your script!
|
||||
|
||||
This will constrain the server to install the latest **non-breaking**
|
||||
This will constrain the server to install the latests **non-breaking**
|
||||
version of mdBook and will prevent your books from failing to build because
|
||||
we released a new version.
|
||||
|
||||
You can also disable default features to speed up compile time.
|
||||
|
||||
Example:
|
||||
we released a new version. For example:
|
||||
|
||||
```
|
||||
cargo install mdbook --no-default-features --features output --vers "^0.1.0"
|
||||
cargo install mdbook --vers "^0.1.0"
|
||||
```
|
||||
|
||||
3. **From Git**
|
||||
3. **From Git**
|
||||
|
||||
The version published to crates.io will ever so slightly be behind the
|
||||
version hosted here on GitHub. If you need the latest version you can build
|
||||
the git version of mdBook yourself. Cargo makes this ***super easy***!
|
||||
|
||||
```
|
||||
cargo install --git https://github.com/rust-lang/mdBook.git mdbook
|
||||
cargo install --git https://github.com/rust-lang-nursery/mdBook.git mdbook
|
||||
```
|
||||
|
||||
Again, make sure to add the Cargo bin directory to your `PATH`.
|
||||
|
||||
4. **For Contributions**
|
||||
4. **For Contributions**
|
||||
|
||||
If you want to contribute to mdBook you will have to clone the repository on
|
||||
your local machine:
|
||||
|
||||
```
|
||||
git clone https://github.com/rust-lang/mdBook.git
|
||||
git clone https://github.com/rust-lang-nursery/mdBook.git
|
||||
```
|
||||
|
||||
`cd` into `mdBook/` and run
|
||||
@@ -132,60 +145,6 @@ explanation, check out the [User Guide].
|
||||
|
||||
Delete directory in which generated book is located.
|
||||
|
||||
### 3rd Party Plugins
|
||||
|
||||
The way a book is loaded and rendered can be configured by the user via third
|
||||
party plugins. These plugins are just programs which will be invoked during the
|
||||
build process and are split into roughly two categories, *preprocessors* and
|
||||
*renderers*.
|
||||
|
||||
Preprocessors are used to transform a book before it is sent to a renderer.
|
||||
One example would be to replace all occurrences of
|
||||
`{{#include some_file.ext}}` with the contents of that file. Some existing
|
||||
preprocessors are:
|
||||
|
||||
- `index` - a built-in preprocessor (enabled by default) which will transform
|
||||
all `README.md` chapters to `index.md` so `foo/README.md` can be accessed via
|
||||
the url `foo/` when published to a browser
|
||||
- `links` - a built-in preprocessor (enabled by default) for expanding the
|
||||
`{{# playground}}` and `{{# include}}` helpers in a chapter.
|
||||
|
||||
Renderers are given the final book so they can do something with it. This is
|
||||
typically used for, as the name suggests, rendering the document in a particular
|
||||
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
|
||||
|
||||
> **Note for Developers:** Feel free to send us a PR if you've developed your
|
||||
> own plugin and want it mentioned here.
|
||||
|
||||
A preprocessor or renderer is enabled by installing the appropriate program and
|
||||
then mentioning it in the book's `book.toml` file.
|
||||
|
||||
```console
|
||||
$ cargo install mdbook-linkcheck
|
||||
$ edit book.toml && cat book.toml
|
||||
[book]
|
||||
title = "My Awesome Book"
|
||||
authors = ["Michael-F-Bryan"]
|
||||
|
||||
[output.html]
|
||||
|
||||
[output.linkcheck] # enable the "mdbook-linkcheck" renderer
|
||||
|
||||
$ mdbook build
|
||||
2018-10-20 13:57:51 [INFO] (mdbook::book): Book building has started
|
||||
2018-10-20 13:57:51 [INFO] (mdbook::book): Running the html backend
|
||||
2018-10-20 13:57:53 [INFO] (mdbook::book): Running the linkcheck backend
|
||||
```
|
||||
|
||||
For more information on the plugin system, consult the [User Guide].
|
||||
|
||||
### As a library
|
||||
|
||||
@@ -204,12 +163,12 @@ Contributions are highly appreciated and encouraged! Don't hesitate to
|
||||
participate to discussions in the issues, propose new features and ask for
|
||||
help.
|
||||
|
||||
If you are just starting out with Rust, there are a series of issues that are
|
||||
If you are just starting out with Rust, there are a series of issus that are
|
||||
tagged [E-Easy] and **we will gladly mentor you** so that you can successfully
|
||||
go through the process of fixing a bug or adding a new feature! Let us know if
|
||||
you need any help.
|
||||
|
||||
For more info about contributing, check out our [contribution guide] which helps
|
||||
For more info about contributing, check out our [contribution guide] who helps
|
||||
you go through the build and contribution process!
|
||||
|
||||
There is also a [rendered version][master-docs] of the latest API docs
|
||||
@@ -221,14 +180,12 @@ available, for those hacking on `master`.
|
||||
All the code in this repository is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE] file.
|
||||
|
||||
|
||||
[User Guide]: https://rust-lang.github.io/mdBook/
|
||||
[User Guide]: https://rust-lang-nursery.github.io/mdBook/
|
||||
[API docs]: https://docs.rs/mdbook/*/mdbook/
|
||||
[E-Easy]: https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy
|
||||
[contribution guide]: https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md
|
||||
[LICENSE]: https://github.com/rust-lang/mdBook/blob/master/LICENSE
|
||||
[releases]: https://github.com/rust-lang/mdBook/releases
|
||||
[E-Easy]: https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy
|
||||
[contribution guide]: https://github.com/rust-lang-nursery/mdBook/blob/master/CONTRIBUTING.md
|
||||
[LICENSE]: https://github.com/rust-lang-nursery/mdBook/blob/master/LICENSE
|
||||
[releases]: https://github.com/rust-lang-nursery/mdBook/releases
|
||||
[Rust]: https://www.rust-lang.org/
|
||||
[CLI docs]: http://rust-lang.github.io/mdBook/cli/init.html
|
||||
[master-docs]: http://rust-lang.github.io/mdBook/
|
||||
[`linkcheck`]: https://crates.io/crates/mdbook-linkcheck
|
||||
[`epub`]: https://crates.io/crates/mdbook-epub
|
||||
[CLI docs]: http://rust-lang-nursery.github.io/mdBook/cli/init.html
|
||||
[master-docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
|
||||
|
||||
64
appveyor.yml
Normal file
64
appveyor.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
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
|
||||
# Beta channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: beta
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
RUST_CHANNEL: beta
|
||||
# Nightly channel
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
RUST_CHANNEL: nightly
|
||||
- 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:
|
||||
secure: QQhjKVyz7mpjlyGhlXytbFQQfKFQWTahHkD+B0NzIUoEVqO7ZLWjnoWasvLqW4nE
|
||||
provider: GitHub
|
||||
on:
|
||||
RUST_CHANNEL: stable
|
||||
appveyor_repo_tag: true
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- /^v\d+\.\d+\.\d+.*$/
|
||||
@@ -2,18 +2,12 @@
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David", "Michael-F-Bryan"]
|
||||
language = "en"
|
||||
|
||||
[rust]
|
||||
edition = "2018"
|
||||
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
site-url = "/mdBook/"
|
||||
|
||||
[output.html.playground]
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
line-numbers = true
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 20
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Document not found (404)
|
||||
|
||||
This URL is invalid, sorry. Try the search instead!
|
||||
@@ -1,16 +1,16 @@
|
||||
# mdBook
|
||||
|
||||
**mdBook** is a command line tool and Rust crate to create books using Markdown
|
||||
(as by the [CommonMark](https://commonmark.org/) specification) files. It's very
|
||||
similar to Gitbook but written in [Rust](http://www.rust-lang.org).
|
||||
files. It's very similar to Gitbook but written in
|
||||
[Rust](http://www.rust-lang.org).
|
||||
|
||||
What you are reading serves as an example of the output of mdBook and at the
|
||||
same time as a high-level documentation.
|
||||
|
||||
mdBook is free and open source, you can find the source code on
|
||||
[GitHub](https://github.com/rust-lang/mdBook). Issues and feature
|
||||
[GitHub](https://github.com/rust-lang-nursery/mdBook). Issues and feature
|
||||
requests can be posted on the [GitHub issue
|
||||
tracker](https://github.com/rust-lang/mdBook/issues).
|
||||
tracker](https://github.com/rust-lang-nursery/mdBook/issues).
|
||||
|
||||
## API docs
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
- [clean](cli/clean.md)
|
||||
- [Format](format/README.md)
|
||||
- [SUMMARY.md](format/summary.md)
|
||||
- [Draft chapter]()
|
||||
- [Configuration](format/config.md)
|
||||
- [Theme](format/theme/README.md)
|
||||
- [index.hbs](format/theme/index-hbs.md)
|
||||
@@ -21,7 +20,7 @@
|
||||
- [Continuous Integration](continuous-integration.md)
|
||||
- [For Developers](for_developers/README.md)
|
||||
- [Preprocessors](for_developers/preprocessors.md)
|
||||
- [Alternative Backends](for_developers/backends.md)
|
||||
- [Alternate Backends](for_developers/backends.md)
|
||||
|
||||
-----------
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ capabilities first.
|
||||
## Install From Binaries
|
||||
|
||||
Precompiled binaries are provided for major platforms on a best-effort basis.
|
||||
Visit [the releases page](https://github.com/rust-lang/mdBook/releases)
|
||||
Visit [the releases page](https://github.com/rust-lang-nursery/mdBook/releases)
|
||||
to download the appropriate version for your platform.
|
||||
|
||||
## Install From Source
|
||||
@@ -18,7 +18,7 @@ mdBook can also be installed from source
|
||||
|
||||
mdBook is written in **[Rust](https://www.rust-lang.org/)** and therefore needs
|
||||
to be compiled with **Cargo**. If you haven't already installed Rust, please go
|
||||
ahead and [install it](https://www.rust-lang.org/tools/install) now.
|
||||
ahead and [install it](https://www.rust-lang.org/downloads.html) now.
|
||||
|
||||
### Install Crates.io version
|
||||
|
||||
@@ -39,14 +39,14 @@ have installed mdBook!
|
||||
|
||||
### Install Git version
|
||||
|
||||
The **[git version](https://github.com/rust-lang/mdBook)** contains all
|
||||
The **[git version](https://github.com/rust-lang-nursery/mdBook)** contains all
|
||||
the latest bug-fixes and features, that will be released in the next version on
|
||||
**Crates.io**, if you can't wait until the next release. You can build the git
|
||||
version yourself. Open your terminal and navigate to the directory of you
|
||||
choice. We need to clone the git repository and then build it with Cargo.
|
||||
|
||||
```bash
|
||||
git clone --depth=1 https://github.com/rust-lang/mdBook.git
|
||||
git clone --depth=1 https://github.com/rust-lang-nursery/mdBook.git
|
||||
cd mdBook
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
@@ -29,11 +29,10 @@ your default web browser after building it.
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
|
||||
-------------------
|
||||
|
||||
***Note:*** *The build command copies all files (excluding files with `.md` extension) from the source directory
|
||||
into the build directory.*
|
||||
***Note:*** *Make sure to run the build command in the root directory and not in
|
||||
the source directory*
|
||||
|
||||
@@ -19,9 +19,9 @@ mdbook clean path/to/book
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to override the book's output
|
||||
directory, which will be deleted by this command. Relative paths are interpreted
|
||||
relative to the book's root directory. If not specified it will default to the
|
||||
value of the `build.build-dir` key in `book.toml`, or to `./book`.
|
||||
directory, which will be deleted by this command. If not specified it will
|
||||
default to the value of the `build.build-dir` key in `book.toml`, or to `./book`
|
||||
relative to the book's root directory.
|
||||
|
||||
```bash
|
||||
mdbook clean --dest-dir=path/to/book
|
||||
|
||||
@@ -19,7 +19,7 @@ book-test/
|
||||
└── SUMMARY.md
|
||||
```
|
||||
|
||||
- The `src` directory is where you write your book in markdown. It contains all
|
||||
- The `src` directory is were you write your book in markdown. It contains all
|
||||
the source files, configuration files, etc.
|
||||
|
||||
- The `book` directory is where your book is rendered. All the output is ready
|
||||
|
||||
@@ -5,9 +5,6 @@ The serve command is used to preview a book by serving it over HTTP at
|
||||
changes, rebuilding the book and refreshing clients for each change. A websocket
|
||||
connection is used to trigger the client-side refresh.
|
||||
|
||||
***Note:*** *The `serve` command is for testing a book's HTML output, and is not
|
||||
intended to be a complete HTTP server for a website.*
|
||||
|
||||
#### Specify a directory
|
||||
|
||||
The `serve` command can take a directory as an argument to use as the book's
|
||||
@@ -37,23 +34,16 @@ configured.
|
||||
|
||||
#### --open
|
||||
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the book in your
|
||||
When you use the `--open` (`-o`) flag, mdbook will open the book in your your
|
||||
default web browser after starting the server.
|
||||
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
|
||||
#### Specify exclude patterns
|
||||
-----
|
||||
|
||||
The `serve` command will not automatically trigger a build for files listed in
|
||||
the `.gitignore` file in the book root directory. The `.gitignore` file may
|
||||
contain file patterns described in the [gitignore
|
||||
documentation](https://git-scm.com/docs/gitignore). This can be useful for
|
||||
ignoring temporary files created by some editors.
|
||||
|
||||
_Note: Only `.gitignore` from book root directory is used. Global
|
||||
`$HOME/.gitignore` or `.gitignore` files in parent directories are not used._
|
||||
***Note:*** *The `serve` command is for testing, and is not intended to be a
|
||||
complete HTTP server for a website.*
|
||||
|
||||
@@ -48,6 +48,5 @@ comma-delimited list (`-L foo,bar`).
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
|
||||
@@ -22,18 +22,5 @@ your default web browser.
|
||||
#### --dest-dir
|
||||
|
||||
The `--dest-dir` (`-d`) option allows you to change the output directory for the
|
||||
book. Relative paths are interpreted relative to the book's root directory. If
|
||||
not specified it will default to the value of the `build.build-dir` key in
|
||||
`book.toml`, or to `./book`.
|
||||
|
||||
|
||||
#### Specify exclude patterns
|
||||
|
||||
The `watch` command will not automatically trigger a build for files listed in
|
||||
the `.gitignore` file in the book root directory. The `.gitignore` file may
|
||||
contain file patterns described in the [gitignore
|
||||
documentation](https://git-scm.com/docs/gitignore). This can be useful for
|
||||
ignoring temporary files created by some editors.
|
||||
|
||||
_Note: Only `.gitignore` from book root directory is used. Global
|
||||
`$HOME/.gitignore` or `.gitignore` files in parent directories are not used._
|
||||
book. If not specified it will default to the value of the `build.build-dir` key
|
||||
in `book.toml`, or to `./book` relative to the book's root directory.
|
||||
|
||||
@@ -22,11 +22,11 @@ rust:
|
||||
|
||||
before_script:
|
||||
- (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update)
|
||||
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.3" mdbook)
|
||||
- (test -x $HOME/.cargo/bin/mdbook || cargo install --vers "^0.1" mdbook)
|
||||
- cargo install-update -a
|
||||
|
||||
script:
|
||||
- mdbook build path/to/mybook && mdbook test path/to/mybook
|
||||
- cd path/to/mybook && mdbook build && mdbook test
|
||||
```
|
||||
|
||||
## Deploying Your Book to GitHub Pages
|
||||
@@ -54,36 +54,3 @@ deploy:
|
||||
```
|
||||
|
||||
That's it!
|
||||
|
||||
### Deploying to GitHub Pages manually
|
||||
|
||||
If your CI doesn't support GitHub pages, or you're deploying somewhere else
|
||||
with integrations such as Github Pages:
|
||||
*note: you may want to use different tmp dirs*:
|
||||
|
||||
```console
|
||||
$> git worktree add /tmp/book gh-pages
|
||||
$> mdbook build
|
||||
$> rm -rf /tmp/book/* # this won't delete the .git directory
|
||||
$> cp -rp book/* /tmp/book/
|
||||
$> cd /tmp/book
|
||||
$> git add -A
|
||||
$> git commit 'new book message'
|
||||
$> git push origin gh-pages
|
||||
$> cd -
|
||||
```
|
||||
|
||||
Or put this into a Makefile rule:
|
||||
|
||||
```makefile
|
||||
.PHONY: deploy
|
||||
deploy: book
|
||||
@echo "====> deploying to github"
|
||||
git worktree add /tmp/book gh-pages
|
||||
rm -rf /tmp/book/*
|
||||
cp -rp book/* /tmp/book/
|
||||
cd /tmp/book && \
|
||||
git add -A && \
|
||||
git commit -m "deployed on $(shell date) by ${USER}" && \
|
||||
git push origin gh-pages
|
||||
```
|
||||
|
||||
@@ -12,14 +12,14 @@ The *For Developers* chapters are here to show you the more advanced usage of
|
||||
The two main ways a developer can hook into the book's build process is via,
|
||||
|
||||
- [Preprocessors](preprocessors.md)
|
||||
- [Alternative Backends](backends.md)
|
||||
- [Alternate Backends](backends.md)
|
||||
|
||||
|
||||
## The Build Process
|
||||
|
||||
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`]: https://docs.rs/mdbook/*/mdbook/book/struct.MDBook.html
|
||||
[API Docs]: https://docs.rs/mdbook/*/mdbook/
|
||||
[config]: https://docs.rs/mdbook/*/mdbook/config/index.html
|
||||
[`MDBook`]: http://rust-lang-nursery.github.io/mdBook/mdbook/book/struct.MDBook.html
|
||||
[API Docs]: http://rust-lang-nursery.github.io/mdBook/mdbook/
|
||||
[config]: file:///home/michael/Documents/forks/mdBook/target/doc/mdbook/config/index.html
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Alternative Backends
|
||||
# Alternate Backends
|
||||
|
||||
A "backend" is simply a program which `mdbook` will invoke during the book
|
||||
rendering process. This program is passed a JSON representation of the book and
|
||||
configuration information via `stdin`. Once the backend receives this
|
||||
information it is free to do whatever it wants.
|
||||
|
||||
There are already several alternative backends on GitHub which can be used as a
|
||||
There are already several alternate backends on GitHub which can be used as a
|
||||
rough example of how this is accomplished in practice.
|
||||
|
||||
- [mdbook-linkcheck] - a simple program for verifying the book doesn't contain
|
||||
@@ -14,7 +14,7 @@ rough example of how this is accomplished in practice.
|
||||
- [mdbook-test] - a program to run the book's contents through [rust-skeptic] to
|
||||
verify everything compiles and runs correctly (similar to `rustdoc --test`)
|
||||
|
||||
This page will step you through creating your own alternative backend in the form
|
||||
This page will step you through creating your own alternate backend in the form
|
||||
of a simple word counting program. Although it will be written in Rust, there's
|
||||
no reason why it couldn't be accomplished using something like Python or Ruby.
|
||||
|
||||
@@ -24,9 +24,9 @@ no reason why it couldn't be accomplished using something like Python or Ruby.
|
||||
First you'll want to create a new binary program and add `mdbook` as a
|
||||
dependency.
|
||||
|
||||
```shell
|
||||
```
|
||||
$ cargo new --bin mdbook-wordcount
|
||||
$ cd mdbook-wordcount
|
||||
$ cd mdbook-wordcount
|
||||
$ cargo add mdbook
|
||||
```
|
||||
|
||||
@@ -52,8 +52,8 @@ fn main() {
|
||||
> **Note:** The `RenderContext` contains a `version` field. This lets backends
|
||||
figure out whether they are compatible with the version of `mdbook` it's being
|
||||
called by. This `version` comes directly from the corresponding field in
|
||||
`mdbook`'s `Cargo.toml`.
|
||||
|
||||
`mdbook`'s `Cargo.toml`.
|
||||
|
||||
It is recommended that backends use the [`semver`] crate to inspect this field
|
||||
and emit a warning if there may be a compatibility issue.
|
||||
|
||||
@@ -92,12 +92,12 @@ fn count_words(ch: &Chapter) -> usize {
|
||||
Now we've got the basics running, we want to actually use it. First, install the
|
||||
program.
|
||||
|
||||
```shell
|
||||
$ cargo install --path .
|
||||
```
|
||||
$ cargo install
|
||||
```
|
||||
|
||||
Then `cd` to the particular book you'd like to count the words of and update its
|
||||
`book.toml` file.
|
||||
`book.toml` file.
|
||||
|
||||
```diff
|
||||
[book]
|
||||
@@ -112,7 +112,7 @@ Then `cd` to the particular book you'd like to count the words of and update its
|
||||
|
||||
When it loads a book into memory, `mdbook` will inspect your `book.toml` file to
|
||||
try and figure out which backends to use by looking for all `output.*` tables.
|
||||
If none are provided it'll fall back to using the default HTML renderer.
|
||||
If none are provided it'll fall back to using the default HTML renderer.
|
||||
|
||||
Notably, this means if you want to add your own custom backend you'll also need
|
||||
to make sure to add the HTML backend, even if its table just stays empty.
|
||||
@@ -120,7 +120,7 @@ to make sure to add the HTML backend, even if its table just stays empty.
|
||||
Now you just need to build your book like normal, and everything should *Just
|
||||
Work*.
|
||||
|
||||
```shell
|
||||
```
|
||||
$ mdbook build
|
||||
...
|
||||
2018-01-16 07:31:15 [INFO] (mdbook::renderer): Invoking the "mdbook-wordcount" renderer
|
||||
@@ -140,7 +140,7 @@ Syntax highlighting: 314
|
||||
MathJax Support: 153
|
||||
Rust code specific features: 148
|
||||
For Developers: 788
|
||||
Alternative Backends: 710
|
||||
Alternate Backends: 710
|
||||
Contributors: 85
|
||||
```
|
||||
|
||||
@@ -169,7 +169,7 @@ arguments or be an interpreted script), you can use the `command` field.
|
||||
Now imagine you don't want to count the number of words on a particular chapter
|
||||
(it might be generated text/code, etc). The canonical way to do this is via the
|
||||
usual `book.toml` configuration file by adding items to your `[output.foo]`
|
||||
table.
|
||||
table.
|
||||
|
||||
The `Config` can be treated roughly as a nested hashmap which lets you call
|
||||
methods like `get()` to access the config's contents, with a
|
||||
@@ -211,13 +211,13 @@ and then add a check to make sure we skip ignored chapters.
|
||||
+ let cfg: WordcountConfig = ctx.config
|
||||
+ .get_deserialized("output.wordcount")
|
||||
+ .unwrap_or_default();
|
||||
|
||||
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
+ if cfg.ignores.contains(&ch.name) {
|
||||
+ continue;
|
||||
+ }
|
||||
+
|
||||
+
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
}
|
||||
@@ -239,17 +239,17 @@ in [`RenderContext`].
|
||||
- use std::io;
|
||||
use mdbook::renderer::RenderContext;
|
||||
use mdbook::book::{BookItem, Chapter};
|
||||
|
||||
|
||||
fn main() {
|
||||
...
|
||||
|
||||
|
||||
+ let _ = fs::create_dir_all(&ctx.destination);
|
||||
+ let mut f = File::create(ctx.destination.join("wordcounts.txt")).unwrap();
|
||||
+
|
||||
+
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
...
|
||||
|
||||
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
+ writeln!(f, "{}: {}", ch.name, num_words).unwrap();
|
||||
@@ -261,10 +261,6 @@ 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
|
||||
@@ -280,11 +276,11 @@ like this:
|
||||
|
||||
fn main() {
|
||||
...
|
||||
|
||||
|
||||
for item in ctx.book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
...
|
||||
|
||||
|
||||
let num_words = count_words(ch);
|
||||
println!("{}: {}", ch.name, num_words);
|
||||
writeln!(f, "{}: {}", ch.name, num_words).unwrap();
|
||||
@@ -307,8 +303,8 @@ like this:
|
||||
|
||||
Now, if we reinstall the backend and build a book,
|
||||
|
||||
```shell
|
||||
$ cargo install --path . --force
|
||||
```
|
||||
$ cargo install --force
|
||||
$ mdbook build /path/to/book
|
||||
...
|
||||
2018-01-16 21:21:39 [INFO] (mdbook::renderer): Invoking the "wordcount" renderer
|
||||
@@ -329,43 +325,11 @@ generation or a warning).
|
||||
All environment variables are passed through to the backend, allowing you to use
|
||||
the usual `RUST_LOG` to control logging verbosity.
|
||||
|
||||
## Handling missing backends
|
||||
|
||||
If you enable a backend that isn't installed, the default behavior is to throw an error:
|
||||
|
||||
```text
|
||||
The command `mdbook-wordcount` wasn't found, is the "wordcount" backend installed?
|
||||
If you want to ignore this error when the "wordcount" backend is not installed,
|
||||
set `optional = true` in the `[output.wordcount]` section of the book.toml configuration file.
|
||||
```
|
||||
|
||||
This behavior can be changed by marking the backend as optional.
|
||||
|
||||
```diff
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David", "Michael-F-Bryan"]
|
||||
|
||||
[output.html]
|
||||
|
||||
[output.wordcount]
|
||||
command = "python /path/to/wordcount.py"
|
||||
+ optional = true
|
||||
```
|
||||
|
||||
This demotes the error to a warning, and it will instead look like this:
|
||||
|
||||
```text
|
||||
The command was not found, but was marked as optional.
|
||||
Command: wordcount
|
||||
```
|
||||
|
||||
|
||||
## Wrapping Up
|
||||
|
||||
Although contrived, hopefully this example was enough to show how you'd create
|
||||
an alternative backend for `mdbook`. If you feel it's missing something, don't
|
||||
an alternate backend for `mdbook`. If you feel it's missing something, don't
|
||||
hesitate to create an issue in the [issue tracker] so we can improve the user
|
||||
guide.
|
||||
|
||||
@@ -378,10 +342,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`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html
|
||||
[`RenderContext::from_json()`]: https://docs.rs/mdbook/*/mdbook/renderer/struct.RenderContext.html#method.from_json
|
||||
[`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
|
||||
[`semver`]: https://crates.io/crates/semver
|
||||
[`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/mdBook/issues
|
||||
[`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
|
||||
[issue tracker]: https://github.com/rust-lang-nursery/mdBook/issues
|
||||
|
||||
@@ -11,71 +11,68 @@ the book. Possible use cases are:
|
||||
mathjax equivalents
|
||||
|
||||
|
||||
## Hooking Into MDBook
|
||||
## Implementing a Preprocessor
|
||||
|
||||
MDBook uses a fairly simple mechanism for discovering third party plugins.
|
||||
A new table is added to `book.toml` (e.g. `preprocessor.foo` for the `foo`
|
||||
preprocessor) and then `mdbook` will try to invoke the `mdbook-foo` program as
|
||||
part of the build process.
|
||||
|
||||
While preprocessors can be hard-coded to specify which backend it should be run
|
||||
for (e.g. it doesn't make sense for MathJax to be used for non-HTML renderers)
|
||||
with the `preprocessor.foo.renderer` key.
|
||||
|
||||
```toml
|
||||
[book]
|
||||
title = "My Book"
|
||||
authors = ["Michael-F-Bryan"]
|
||||
|
||||
[preprocessor.foo]
|
||||
# The command can also be specified manually
|
||||
command = "python3 /path/to/foo.py"
|
||||
# Only run the `foo` preprocessor for the HTML and EPUB renderer
|
||||
renderer = ["html", "epub"]
|
||||
```
|
||||
|
||||
In typical unix style, all inputs to the plugin will be written to `stdin` as
|
||||
JSON and `mdbook` will read from `stdout` if it is expecting output.
|
||||
|
||||
The easiest way to get started is by creating your own implementation of the
|
||||
`Preprocessor` trait (e.g. in `lib.rs`) and then creating a shell binary which
|
||||
translates inputs to the correct `Preprocessor` method. For convenience, there
|
||||
is [an example no-op preprocessor] in the `examples/` directory which can easily
|
||||
be adapted for other preprocessors.
|
||||
|
||||
<details>
|
||||
<summary>Example no-op preprocessor</summary>
|
||||
A preprocessor is represented by the `Preprocessor` trait.
|
||||
|
||||
```rust
|
||||
// nop-preprocessors.rs
|
||||
|
||||
{{#include ../../../examples/nop-preprocessor.rs}}
|
||||
pub trait Preprocessor {
|
||||
fn name(&self) -> &str;
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book>;
|
||||
fn supports_renderer(&self, _renderer: &str) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
## Hints For Implementing A Preprocessor
|
||||
Where the `PreprocessorContext` is defined as
|
||||
|
||||
By pulling in `mdbook` as a library, preprocessors can have access to the
|
||||
existing infrastructure for dealing with books.
|
||||
```rust
|
||||
pub struct PreprocessorContext {
|
||||
pub root: PathBuf,
|
||||
pub config: Config,
|
||||
/// The `Renderer` this preprocessor is being used with.
|
||||
pub renderer: String,
|
||||
}
|
||||
```
|
||||
|
||||
For example, a custom preprocessor could use the
|
||||
[`CmdPreprocessor::parse_input()`] function to deserialize the JSON written to
|
||||
`stdin`. Then each chapter of the `Book` can be mutated in-place via
|
||||
[`Book::for_each_mut()`], and then written to `stdout` with the `serde_json`
|
||||
crate.
|
||||
The `renderer` value allows you react accordingly, for example, PDF or HTML.
|
||||
|
||||
Chapters can be accessed either directly (by recursively iterating over
|
||||
chapters) or via the `Book::for_each_mut()` convenience method.
|
||||
## A complete Example
|
||||
|
||||
The `chapter.content` is just a string which happens to be markdown. While it's
|
||||
entirely possible to use regular expressions or do a manual find & replace,
|
||||
you'll probably want to process the input into something more computer-friendly.
|
||||
The [`pulldown-cmark`][pc] crate implements a production-quality event-based
|
||||
Markdown parser, with the [`pulldown-cmark-to-cmark`][pctc] allowing you to
|
||||
translate events back into markdown text.
|
||||
The magic happens within the `run(...)` method of the
|
||||
[`Preprocessor`][preprocessor-docs] trait implementation.
|
||||
|
||||
The following code block shows how to remove all emphasis from markdown,
|
||||
without accidentally breaking the document.
|
||||
As direct access to the chapters is not possible, you will probably end up
|
||||
iterating them using `for_each_mut(...)`:
|
||||
|
||||
```rust
|
||||
book.for_each_mut(|item: &mut BookItem| {
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
eprintln!("{}: processing chapter '{}'", self.name(), chapter.name);
|
||||
res = Some(
|
||||
match Deemphasize::remove_emphasis(&mut num_removed_items, chapter) {
|
||||
Ok(md) => {
|
||||
chapter.content = md;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The `chapter.content` is just a markdown formatted string, and you will have to
|
||||
process it in some way. Even though it's entirely possible to implement some
|
||||
sort of manual find & replace operation, if that feels too unsafe you can use
|
||||
[`pulldown-cmark`][pc] to parse the string into events and work on them instead.
|
||||
|
||||
Finally you can use [`pulldown-cmark-to-cmark`][pctc] to transform these events
|
||||
back to a string.
|
||||
|
||||
The following code block shows how to remove all emphasis from markdown, and do
|
||||
so safely.
|
||||
|
||||
```rust
|
||||
fn remove_emphasis(
|
||||
@@ -109,7 +106,4 @@ For everything else, have a look [at the complete example][example].
|
||||
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
|
||||
[pc]: https://crates.io/crates/pulldown-cmark
|
||||
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
|
||||
[example]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||
[an example no-op preprocessor]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
|
||||
[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input
|
||||
[`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut
|
||||
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/de-emphasize.rs
|
||||
|
||||
@@ -10,16 +10,13 @@ title = "Example book"
|
||||
author = "John Doe"
|
||||
description = "The example book covers examples."
|
||||
|
||||
[rust]
|
||||
edition = "2018"
|
||||
|
||||
[build]
|
||||
build-dir = "my-example-book"
|
||||
create-missing = false
|
||||
|
||||
[preprocessor.index]
|
||||
[preprocess.index]
|
||||
|
||||
[preprocessor.links]
|
||||
[preprocess.links]
|
||||
|
||||
[output.html]
|
||||
additional-css = ["custom.css"]
|
||||
@@ -30,7 +27,7 @@ limit-results = 15
|
||||
|
||||
## Supported configuration options
|
||||
|
||||
It is important to note that **any** relative path specified in the
|
||||
It is important to note that **any** relative path specified in the in the
|
||||
configuration will always be taken relative from the root of the book where the
|
||||
configuration file is located.
|
||||
|
||||
@@ -45,7 +42,6 @@ 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
|
||||
@@ -54,25 +50,8 @@ 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"
|
||||
```
|
||||
|
||||
### Rust options
|
||||
|
||||
Options for the Rust language, relevant to running tests and playground
|
||||
integration.
|
||||
|
||||
- **edition**: Rust edition to use by default for the code snippets. Default
|
||||
is "2015". Individual code blocks can be controlled with the `edition2015`
|
||||
or `edition2018` annotations, such as:
|
||||
|
||||
~~~text
|
||||
```rust,edition2015
|
||||
// This only works in 2015.
|
||||
let try = true;
|
||||
```
|
||||
~~~
|
||||
|
||||
### Build options
|
||||
|
||||
This controls the build process of your book.
|
||||
@@ -89,19 +68,19 @@ This controls the build process of your book.
|
||||
If you have the same, and/or other preprocessors declared via their table
|
||||
of configuration, they will run instead.
|
||||
|
||||
- For clarity, with no preprocessor configuration, the default `links` and
|
||||
- For clarity, with no preprocessor configuration, the default `links` and
|
||||
`index` will run.
|
||||
- Setting `use-default-preprocessors = false` will disable these
|
||||
default preprocessors from running.
|
||||
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
|
||||
- Adding `[preprocessor.links]`, for example, will ensure, regardless of
|
||||
`use-default-preprocessors` that `links` it will run.
|
||||
|
||||
## Configuring Preprocessors
|
||||
|
||||
The following preprocessors are available and included by default:
|
||||
|
||||
- `links`: Expand the `{{ #playground }}`, `{{ #include }}`, and `{{ #rustdoc_include }}` handlebars
|
||||
helpers in a chapter to include the contents of a file.
|
||||
- `links`: Expand the `{{ #playpen }}` and `{{ #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
|
||||
rendered book.
|
||||
@@ -113,21 +92,20 @@ The following preprocessors are available and included by default:
|
||||
build-dir = "build"
|
||||
create-missing = false
|
||||
|
||||
[preprocessor.links]
|
||||
[preprocess.links]
|
||||
|
||||
[preprocessor.index]
|
||||
[preprocess.index]
|
||||
```
|
||||
|
||||
### Custom Preprocessor Configuration
|
||||
|
||||
Like renderers, preprocessor will need to be given its own table (e.g.
|
||||
`[preprocessor.mathjax]`). In the section, you may then pass extra
|
||||
configuration to the preprocessor by adding key-value pairs to the table.
|
||||
Like renderers, preprocessor will need to be given its own table (e.g. `[preprocessor.mathjax]`).
|
||||
In the section, you may then pass extra configuration to the preprocessor by adding key-value pairs to the table.
|
||||
|
||||
For example
|
||||
|
||||
```toml
|
||||
[preprocessor.links]
|
||||
```
|
||||
[preprocess.links]
|
||||
# set the renderers this preprocessor will run for
|
||||
renderers = ["html"]
|
||||
some_extra_feature = true
|
||||
@@ -135,26 +113,13 @@ some_extra_feature = true
|
||||
|
||||
#### Locking a Preprocessor dependency to a renderer
|
||||
|
||||
You can explicitly specify that a preprocessor should run for a renderer by
|
||||
binding the two together.
|
||||
You can explicitly specify that a preprocessor should run for a renderer by binding the two together.
|
||||
|
||||
```toml
|
||||
```
|
||||
[preprocessor.mathjax]
|
||||
renderers = ["html"] # mathjax only makes sense with the HTML renderer
|
||||
```
|
||||
|
||||
### Provide Your Own Command
|
||||
|
||||
By default when you add a `[preprocessor.foo]` table to your `book.toml` file,
|
||||
`mdbook` will try to invoke the `mdbook-foo` executable. If you want to use a
|
||||
different program name or pass in command-line arguments, this behaviour can
|
||||
be overridden by adding a `command` field.
|
||||
|
||||
```toml
|
||||
[preprocessor.random]
|
||||
command = "python random.py"
|
||||
```
|
||||
|
||||
## Configuring Renderers
|
||||
|
||||
### HTML renderer options
|
||||
@@ -167,17 +132,8 @@ The following configuration options are available:
|
||||
- **theme:** mdBook comes with a default theme and all the resource files needed
|
||||
for it. But if this option is set, mdBook will selectively overwrite the theme
|
||||
files with the ones found in the specified folder.
|
||||
- **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 `navy`.
|
||||
- **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
|
||||
`false`.
|
||||
- **copy-fonts:** Copies fonts.css and respective font files to the output directory and use them in the default theme. Defaults to `true`.
|
||||
- **google-analytics:** If you use Google Analytics, this option lets you enable
|
||||
it by simply specifying your ID in the configuration file.
|
||||
- **additional-css:** If you need to slightly change the appearance of your book
|
||||
@@ -190,47 +146,15 @@ 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.
|
||||
- **playground:** A subtable for configuring various playground settings.
|
||||
- **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).
|
||||
- **git-repository-url:** A url to the git repository for the book. If provided
|
||||
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`.
|
||||
- **redirect:** A subtable used for generating redirects when a page is moved.
|
||||
The table contains key-value pairs where the key is where the redirect file
|
||||
needs to be created, as an absolute path from the build directory, (e.g.
|
||||
`/appendices/bibliography.html`). The value can be any valid URI the
|
||||
browser should navigate to (e.g. `https://rust-lang.org/`,
|
||||
`/overview.html`, or `../bibliography.html`).
|
||||
- **input-404:** The name of the markdown file used for misssing files.
|
||||
The corresponding output file will be the same, with the extension replaced with `html`.
|
||||
Defaults to `404.md`.
|
||||
- **site-url:** The url where the book will be hosted. This is required to ensure
|
||||
navigation links and script/css imports in the 404 file work correctly, even when accessing
|
||||
urls in subdirectories. Defaults to `/`.
|
||||
- **cname:** The DNS subdomain or apex domain at which your book will be hosted.
|
||||
This string will be written to a file named CNAME in the root of your site, as
|
||||
required by GitHub Pages (see [*Managing a custom domain for your GitHub Pages
|
||||
site*][custom domain]).
|
||||
|
||||
[custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
||||
|
||||
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.playground]` table:
|
||||
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/
|
||||
|
||||
@@ -241,7 +165,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 `false`.
|
||||
true, all search words must appear in each result. Defaults to `true`.
|
||||
- **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
|
||||
@@ -257,42 +181,32 @@ Available configuration options for the `[output.html.search]` table:
|
||||
- **copy-js:** Copy JavaScript files for the search implementation to the output
|
||||
directory. Defaults to `true`.
|
||||
|
||||
This shows all available HTML output options in the **book.toml**:
|
||||
|
||||
This shows all available options in the **book.toml**:
|
||||
```toml
|
||||
[book]
|
||||
title = "Example book"
|
||||
authors = ["John Doe", "Jane Doe"]
|
||||
description = "The example book covers examples."
|
||||
|
||||
[build]
|
||||
build-dir = "book"
|
||||
create-missing = true
|
||||
preprocess = ["links", "index"]
|
||||
|
||||
[output.html]
|
||||
theme = "my-theme"
|
||||
default-theme = "light"
|
||||
preferred-dark-theme = "navy"
|
||||
curly-quotes = true
|
||||
mathjax-support = false
|
||||
copy-fonts = true
|
||||
google-analytics = "UA-123456-7"
|
||||
google-analytics = "123456"
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
no-section-label = false
|
||||
git-repository-url = "https://github.com/rust-lang/mdBook"
|
||||
git-repository-icon = "fa-github"
|
||||
site-url = "/example-book/"
|
||||
cname = "myproject.rs"
|
||||
input-404 = "not-found.md"
|
||||
|
||||
[output.html.fold]
|
||||
enable = false
|
||||
level = 0
|
||||
|
||||
[output.html.playground]
|
||||
[output.html.playpen]
|
||||
editor = "./path/to/editor"
|
||||
editable = false
|
||||
copy-js = true
|
||||
line-numbers = false
|
||||
|
||||
[output.html.search]
|
||||
enable = true
|
||||
searcher = "./path/to/searcher"
|
||||
limit-results = 30
|
||||
teaser-word-count = 30
|
||||
use-boolean-and = true
|
||||
@@ -302,48 +216,8 @@ boost-paragraph = 1
|
||||
expand = true
|
||||
heading-split-level = 3
|
||||
copy-js = true
|
||||
|
||||
[output.html.redirect]
|
||||
"/appendices/bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html"
|
||||
"/other-installation-methods.html" = "../infra/other-installation-methods.html"
|
||||
```
|
||||
|
||||
### 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 empty 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
|
||||
`book.toml`. Similar to [preprocessors](#configuring-preprocessors) this will
|
||||
instruct `mdbook` to pass a representation of the book to `mdbook-foo` for
|
||||
rendering. See the [alternative backends] chapter for more detail.
|
||||
|
||||
The custom renderer has access to all the fields within its table (i.e.
|
||||
anything under `[output.foo]`). mdBook checks for two common fields:
|
||||
|
||||
- **command:** The command to execute for this custom renderer. Defaults to
|
||||
the name of the renderer with the `mdbook-` prefix (such as `mdbook-foo`).
|
||||
- **optional:** If `true`, then the command will be ignored if it is not
|
||||
installed, otherwise mdBook will fail with an error. Defaults to `false`.
|
||||
|
||||
[alternative backends]: ../for_developers/backends.md
|
||||
|
||||
## Environment Variables
|
||||
|
||||
@@ -375,11 +249,11 @@ book's title without needing to touch your `book.toml`.
|
||||
> This means, if you so desired, you could override all book metadata when
|
||||
> building the book with something like
|
||||
>
|
||||
> ```shell
|
||||
> ```text
|
||||
> $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
|
||||
> $ mdbook build
|
||||
> ```
|
||||
|
||||
The latter case may be useful in situations where `mdbook` is invoked from a
|
||||
script or CI, where it sometimes isn't possible to update the `book.toml` before
|
||||
building.
|
||||
building.
|
||||
@@ -3,9 +3,7 @@
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them
|
||||
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
|
||||
with a `#`.
|
||||
|
||||
```bash
|
||||
# fn main() {
|
||||
@@ -37,20 +35,10 @@ With the following syntax, you can include files into your book:
|
||||
|
||||
The path to the file has to be relative from the current source file.
|
||||
|
||||
mdBook will interpret included files as markdown. Since the include command
|
||||
is usually used for inserting code snippets and examples, you will often
|
||||
wrap the command with ```` ``` ```` to display the file contents without
|
||||
interpretting them.
|
||||
|
||||
````hbs
|
||||
```
|
||||
\{{#include file.rs}}
|
||||
```
|
||||
````
|
||||
|
||||
## Including portions of a file
|
||||
Often you only need a specific part of the file e.g. relevant lines for an
|
||||
example. We support four different modes of partial includes:
|
||||
Usually, this command is used for including code snippets and examples. In this
|
||||
case, oftens one would include a specific part of the file e.g. which only
|
||||
contains the relevant lines for the example. We support four different modes of
|
||||
partial includes:
|
||||
|
||||
```hbs
|
||||
\{{#include file.rs:2}}
|
||||
@@ -65,130 +53,22 @@ 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,noplayground
|
||||
\{{#include file.rs:component}}
|
||||
```
|
||||
|
||||
Here is a system:
|
||||
```rust,no_run,noplayground
|
||||
\{{#include file.rs:system}}
|
||||
```
|
||||
|
||||
This is the full file.
|
||||
```rust,no_run,noplayground
|
||||
\{{#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:
|
||||
|
||||
```hbs
|
||||
\{{#playground file.rs}}
|
||||
\{{#playpen file.rs}}
|
||||
```
|
||||
|
||||
The path to the Rust file has to be relative from the current source file.
|
||||
|
||||
When play is clicked, the code snippet will be sent to the [Rust Playground] to be
|
||||
When play is clicked, the code snippet will be sent to the [Rust Playpen] to be
|
||||
compiled and run. The result is sent back and displayed directly underneath the
|
||||
code.
|
||||
|
||||
Here is what a rendered code snippet looks like:
|
||||
|
||||
{{#playground example.rs}}
|
||||
{{#playpen example.rs}}
|
||||
|
||||
[Rust Playground]: https://play.rust-lang.org/
|
||||
[Rust Playpen]: https://play.rust-lang.org/
|
||||
|
||||
@@ -7,7 +7,7 @@ are. Without this file, there is no book.
|
||||
Even though `SUMMARY.md` is a markdown file, the formatting is very strict to
|
||||
allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
|
||||
|
||||
#### Structure
|
||||
#### Allowed elements
|
||||
|
||||
1. ***Title*** It's common practice to begin with a title, generally <code
|
||||
class="language-markdown"># Summary</code>. But it is not mandatory, the
|
||||
@@ -22,45 +22,17 @@ allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
|
||||
[Title of prefix element](relative/path/to/markdown.md)
|
||||
```
|
||||
|
||||
3. ***Part Title:*** Headers can be used as a title for the following numbered
|
||||
chapters. This can be used to logically separate different sections
|
||||
of book. The title is rendered as unclickable text.
|
||||
Titles are optional, and the numbered chapters can be broken into as many
|
||||
parts as desired.
|
||||
|
||||
4. ***Numbered Chapter*** Numbered chapters are the main content of the book,
|
||||
3. ***Numbered Chapter*** Numbered chapters are the main content of the book,
|
||||
they will be numbered and can be nested, resulting in a nice hierarchy
|
||||
(chapters, sub-chapters, etc.)
|
||||
```markdown
|
||||
# Title of Part
|
||||
|
||||
- [Title of the Chapter](relative/path/to/markdown.md)
|
||||
|
||||
# Title of Another Part
|
||||
|
||||
- [More Chapters](relative/path/to/markdown2.md)
|
||||
```
|
||||
You can either use `-` or `*` to indicate a numbered chapter.
|
||||
|
||||
5. ***Suffix Chapter*** After the numbered chapters you can add a couple of
|
||||
4. ***Suffix Chapter*** After the numbered chapters you can add a couple of
|
||||
non-numbered chapters. They are the same as prefix chapters but come after
|
||||
the numbered chapters instead of before.
|
||||
|
||||
All other elements are unsupported and will be ignored at best or result in an
|
||||
error.
|
||||
|
||||
#### Other elements
|
||||
|
||||
- ***Separators*** In between chapters you can add a separator. In the HTML renderer
|
||||
this will result in a line being rendered in the table of contents. A separator is
|
||||
a line containing exclusively dashes and at least three of them: `---`.
|
||||
- ***Draft chapters*** Draft chapters are chapters without a file and thus content.
|
||||
The purpose of a draft chapter is to signal future chapters still to be written.
|
||||
Or when still laying out the structure of the book to avoid creating the files
|
||||
while you are still changing the structure of the book a lot.
|
||||
Draft chapters will be rendered in the HTML renderer as disabled links in the table
|
||||
of contents, as you can see for the next chapter in the table of contents on the left.
|
||||
Draft chapters are written like normal chapters but without writing the path to the file
|
||||
```markdown
|
||||
- [Draft chapter]()
|
||||
```
|
||||
|
||||
@@ -11,19 +11,16 @@ and now that file will be used instead of the default file.
|
||||
|
||||
Here are the files you can override:
|
||||
|
||||
- **_index.hbs_** is the handlebars template.
|
||||
- **_head.hbs_** is appended to the HTML `<head>` section.
|
||||
- **_header.hbs_** content is appended on top of every book page.
|
||||
- **_book.css_** is the style used in the output. If you want to change the
|
||||
- ***index.hbs*** is the handlebars template.
|
||||
- ***book.css*** is the style used in the output. If you want to change the
|
||||
design of your book, this is probably the file you want to modify. Sometimes
|
||||
in conjunction with `index.hbs` when you want to radically change the layout.
|
||||
- **_book.js_** is mostly used to add client side functionality, like hiding /
|
||||
- ***book.js*** is mostly used to add client side functionality, like hiding /
|
||||
un-hiding the sidebar, changing the theme, ...
|
||||
- **_highlight.js_** is the JavaScript that is used to highlight code snippets,
|
||||
you should not need to modify this.
|
||||
- **_highlight.css_** is the theme used for the code highlighting.
|
||||
- **_favicon.svg_** and **_favicon.png_** the favicon that will be used. The SVG
|
||||
version is used by [newer browsers].
|
||||
- ***highlight.js*** is the JavaScript that is used to highlight code snippets,
|
||||
you should not need to modify this.
|
||||
- ***highlight.css*** is the theme used for the code highlighting
|
||||
- ***favicon.png*** the favicon that will be used
|
||||
|
||||
Generally, when you want to tweak the theme, you don't need to override all the
|
||||
files. If you only need changes in the stylesheet, there is no point in
|
||||
@@ -35,10 +32,3 @@ functionality. Therefore I recommend to use the file from the default theme as
|
||||
template and only add / modify what you need. You can copy the default theme
|
||||
into your source directory automatically by using `mdbook init --theme` just
|
||||
remove the files you don't want to override.
|
||||
|
||||
If you completely replace all built-in themes, be sure to also set
|
||||
[`output.html.preferred-dark-theme`] in the config, which defaults to the
|
||||
built-in `navy` theme.
|
||||
|
||||
[`output.html.preferred-dark-theme`]: ../config.md#html-renderer-options
|
||||
[newer browsers]: https://caniuse.com/#feat=link-icon-svg
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Editor
|
||||
|
||||
In addition to providing runnable code playgrounds, mdBook optionally allows them
|
||||
In addition to providing runnable code playpens, mdBook optionally allows them
|
||||
to be editable. In order to enable editable code blocks, the following needs to
|
||||
be added to the ***book.toml***:
|
||||
|
||||
```toml
|
||||
[output.html.playground]
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
```
|
||||
|
||||
@@ -19,7 +19,7 @@ fn main() {
|
||||
}
|
||||
```</code></pre>
|
||||
|
||||
The above will result in this editable playground:
|
||||
The above will result in this editable playpen:
|
||||
|
||||
```rust,editable
|
||||
fn main() {
|
||||
@@ -28,7 +28,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
Note the new `Undo Changes` button in the editable playgrounds.
|
||||
Note the new `Undo Changes` button in the editable playpens.
|
||||
|
||||
## Customizing the Editor
|
||||
|
||||
@@ -36,7 +36,7 @@ By default, the editor is the [Ace](https://ace.c9.io/) editor, but, if desired,
|
||||
the functionality may be overriden by providing a different folder:
|
||||
|
||||
```toml
|
||||
[output.html.playground]
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
editor = "/path/to/editor"
|
||||
```
|
||||
|
||||
@@ -17,10 +17,10 @@ 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`, 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 used for the current page. This is identical to `{{ book_title }} - {{ chapter_title }}` unless `book_title` is not set in which case it just defaults to the `chapter_title`.
|
||||
- ***book_title*** Title of the book, as specified in `book.toml`
|
||||
- ***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.
|
||||
- ***title*** Title of the book, as specified in `book.toml`
|
||||
- ***chapter_title*** Title of the current chapter, as listed in `SUMMARY.md`
|
||||
|
||||
- ***path*** Relative path to the original markdown file from the source
|
||||
@@ -45,57 +45,53 @@ at your disposal.
|
||||
|
||||
### 1. toc
|
||||
|
||||
The toc helper is used like this
|
||||
The toc helper is used like this
|
||||
|
||||
```handlebars
|
||||
{{#toc}}{{/toc}}
|
||||
```
|
||||
```handlebars
|
||||
{{#toc}}{{/toc}}
|
||||
```
|
||||
|
||||
and outputs something that looks like this, depending on the structure of your
|
||||
book
|
||||
and outputs something that looks like this, depending on the structure of your book
|
||||
|
||||
```html
|
||||
<ul class="chapter">
|
||||
<li><a href="link/to/file.html">Some chapter</a></li>
|
||||
<li>
|
||||
<ul class="section">
|
||||
<li><a href="link/to/other_file.html">Some other Chapter</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
```html
|
||||
<ul class="chapter">
|
||||
<li><a href="link/to/file.html">Some chapter</a></li>
|
||||
<li>
|
||||
<ul class="section">
|
||||
<li><a href="link/to/other_file.html">Some other Chapter</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
If you would like to make a toc with another structure, you have access to the
|
||||
chapters property containing all the data. The only limitation at the moment
|
||||
is that you would have to do it with JavaScript instead of with a handlebars
|
||||
helper.
|
||||
If you would like to make a toc with another structure, you have access to the chapters property containing all the data.
|
||||
The only limitation at the moment is that you would have to do it with JavaScript instead of with a handlebars helper.
|
||||
|
||||
```html
|
||||
<script>
|
||||
var chapters = {{chapters}};
|
||||
// Processing here
|
||||
</script>
|
||||
```
|
||||
```html
|
||||
<script>
|
||||
var chapters = {{chapters}};
|
||||
// Processing here
|
||||
</script>
|
||||
```
|
||||
|
||||
### 2. previous / next
|
||||
|
||||
The previous and next helpers expose a `link` and `name` property to the
|
||||
previous and next chapters.
|
||||
The previous and next helpers expose a `link` and `name` property to the previous and next chapters.
|
||||
|
||||
They are used like this
|
||||
They are used like this
|
||||
|
||||
```handlebars
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="nav-chapters previous">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
```
|
||||
```handlebars
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="nav-chapters previous">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
```
|
||||
|
||||
The inner html will only be rendered if the previous / next chapter exists.
|
||||
Of course the inner html can be changed to your liking.
|
||||
The inner html will only be rendered if the previous / next chapter exists.
|
||||
Of course the inner html can be changed to your liking.
|
||||
|
||||
------
|
||||
|
||||
*If you would like other properties or helpers exposed, please [create a new
|
||||
issue](https://github.com/rust-lang/mdBook/issues)*
|
||||
issue](https://github.com/rust-lang-nursery/mdBook/issues)*
|
||||
|
||||
@@ -22,7 +22,7 @@ overridden with your own.
|
||||
|
||||
If you want to use another theme for `highlight.js` download it from their
|
||||
website, or make it yourself, rename it to `highlight.css` and put it in
|
||||
the `theme` folder of your book.
|
||||
`src/theme` (or the equivalent if you changed your source folder)
|
||||
|
||||
Now your theme will be used instead of the default theme.
|
||||
|
||||
@@ -62,7 +62,7 @@ everyone can benefit from it.**
|
||||
|
||||
If you think the default theme doesn't look quite right for a specific language,
|
||||
or could be improved. Feel free to [submit a new
|
||||
issue](https://github.com/rust-lang/mdBook/issues) explaining what you
|
||||
issue](https://github.com/rust-lang-nursery/mdBook/issues) explaining what you
|
||||
have in mind and I will take a look at it.
|
||||
|
||||
You could also create a pull-request with the proposed improvements.
|
||||
|
||||
32
ci/before_deploy.sh
Normal file
32
ci/before_deploy.sh
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/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 --no-self-update $TOOLCHAIN
|
||||
rustup default $TOOLCHAIN
|
||||
rustup -V
|
||||
rustc -Vv
|
||||
cargo -V
|
||||
@@ -1,47 +0,0 @@
|
||||
#!/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")
|
||||
export CARGO_PROFILE_RELEASE_LTO=true
|
||||
cargo build --bin mdbook --release
|
||||
cd target/release
|
||||
case $1 in
|
||||
ubuntu*)
|
||||
asset="mdbook-$TAG-$host.tar.gz"
|
||||
tar czf ../../$asset mdbook
|
||||
;;
|
||||
macos*)
|
||||
asset="mdbook-$TAG-$host.tar.gz"
|
||||
# There is a bug with BSD tar on macOS where the first 8MB of the file are
|
||||
# sometimes all NUL bytes. See https://github.com/actions/cache/issues/403
|
||||
# and https://github.com/rust-lang/cargo/issues/8603 for some more
|
||||
# information. An alternative solution here is to install GNU tar, but
|
||||
# flushing the disk cache seems to work, too.
|
||||
sudo /usr/sbin/purge
|
||||
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
|
||||
98
examples/de-emphasize.rs
Normal file
98
examples/de-emphasize.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
//! This program removes all forms of emphasis from the markdown of the book.
|
||||
extern crate mdbook;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate pulldown_cmark_to_cmark;
|
||||
|
||||
use mdbook::book::{Book, BookItem, Chapter};
|
||||
use mdbook::errors::{Error, Result};
|
||||
use mdbook::preprocess::{Preprocessor, PreprocessorContext};
|
||||
use mdbook::MDBook;
|
||||
use pulldown_cmark::{Event, Parser, Tag};
|
||||
use pulldown_cmark_to_cmark::fmt::cmark;
|
||||
|
||||
use std::env::{args, args_os};
|
||||
use std::ffi::OsString;
|
||||
use std::process;
|
||||
|
||||
const NAME: &str = "md-links-to-html-links";
|
||||
|
||||
fn do_it(book: OsString) -> Result<()> {
|
||||
let mut book = MDBook::load(book)?;
|
||||
book.with_preprecessor(Deemphasize);
|
||||
book.build()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if args_os().count() != 2 {
|
||||
eprintln!("USAGE: {} <book>", args().next().expect("executable"));
|
||||
return;
|
||||
}
|
||||
if let Err(e) = do_it(args_os().skip(1).next().expect("one argument")) {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
struct Deemphasize;
|
||||
|
||||
impl Preprocessor for Deemphasize {
|
||||
fn name(&self) -> &str {
|
||||
NAME
|
||||
}
|
||||
|
||||
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> {
|
||||
eprintln!("Running '{}' preprocessor", self.name());
|
||||
let mut num_removed_items = 0;
|
||||
|
||||
process(&mut book.sections, &mut num_removed_items)?;
|
||||
|
||||
eprintln!(
|
||||
"{}: removed {} events from markdown stream.",
|
||||
self.name(),
|
||||
num_removed_items
|
||||
);
|
||||
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
fn process<'a, I>(items: I, num_removed_items: &mut usize) -> Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = &'a mut BookItem> + 'a,
|
||||
{
|
||||
for item in items {
|
||||
if let BookItem::Chapter(ref mut chapter) = *item {
|
||||
eprintln!("{}: processing chapter '{}'", NAME, chapter.name);
|
||||
|
||||
let md = remove_emphasis(num_removed_items, chapter)?;
|
||||
chapter.content = md;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_emphasis(
|
||||
num_removed_items: &mut usize,
|
||||
chapter: &mut Chapter,
|
||||
) -> Result<String> {
|
||||
let mut buf = String::with_capacity(chapter.content.len());
|
||||
|
||||
let events = Parser::new(&chapter.content).filter(|e| {
|
||||
let should_keep = match *e {
|
||||
Event::Start(Tag::Emphasis)
|
||||
| Event::Start(Tag::Strong)
|
||||
| Event::End(Tag::Emphasis)
|
||||
| Event::End(Tag::Strong) => false,
|
||||
_ => true,
|
||||
};
|
||||
if !should_keep {
|
||||
*num_removed_items += 1;
|
||||
}
|
||||
should_keep
|
||||
});
|
||||
|
||||
cmark(events, &mut buf, None).map(|_| buf).map_err(|err| {
|
||||
Error::from(format!("Markdown serialization failed: {}", err))
|
||||
})
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
use crate::nop_lib::Nop;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use mdbook::book::Book;
|
||||
use mdbook::errors::Error;
|
||||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use std::io;
|
||||
use std::process;
|
||||
|
||||
pub fn make_app() -> App<'static, 'static> {
|
||||
App::new("nop-preprocessor")
|
||||
.about("A mdbook preprocessor which does precisely nothing")
|
||||
.subcommand(
|
||||
SubCommand::with_name("supports")
|
||||
.arg(Arg::with_name("renderer").required(true))
|
||||
.about("Check whether a renderer is supported by this preprocessor"),
|
||||
)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let matches = make_app().get_matches();
|
||||
|
||||
// Users will want to construct their own preprocessor here
|
||||
let preprocessor = Nop::new();
|
||||
|
||||
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
||||
handle_supports(&preprocessor, sub_args);
|
||||
} else if let Err(e) = handle_preprocessing(&preprocessor) {
|
||||
eprintln!("{}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_preprocessing(pre: &dyn Preprocessor) -> Result<(), Error> {
|
||||
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())?;
|
||||
|
||||
if ctx.mdbook_version != mdbook::MDBOOK_VERSION {
|
||||
// We should probably use the `semver` crate to check compatibility
|
||||
// here...
|
||||
eprintln!(
|
||||
"Warning: The {} plugin was built against version {} of mdbook, \
|
||||
but we're being called from version {}",
|
||||
pre.name(),
|
||||
mdbook::MDBOOK_VERSION,
|
||||
ctx.mdbook_version
|
||||
);
|
||||
}
|
||||
|
||||
let processed_book = pre.run(&ctx, book)?;
|
||||
serde_json::to_writer(io::stdout(), &processed_book)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_supports(pre: &dyn Preprocessor, sub_args: &ArgMatches) -> ! {
|
||||
let renderer = sub_args.value_of("renderer").expect("Required argument");
|
||||
let supported = pre.supports_renderer(&renderer);
|
||||
|
||||
// Signal whether the renderer is supported by exiting with 1 or 0.
|
||||
if supported {
|
||||
process::exit(0);
|
||||
} else {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// The actual implementation of the `Nop` preprocessor. This would usually go
|
||||
/// in your main `lib.rs` file.
|
||||
mod nop_lib {
|
||||
use super::*;
|
||||
|
||||
/// A no-op preprocessor.
|
||||
pub struct Nop;
|
||||
|
||||
impl Nop {
|
||||
pub fn new() -> Nop {
|
||||
Nop
|
||||
}
|
||||
}
|
||||
|
||||
impl Preprocessor for Nop {
|
||||
fn name(&self) -> &str {
|
||||
"nop-preprocessor"
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book, Error> {
|
||||
// In testing we want to tell the preprocessor to blow up by setting a
|
||||
// particular config value
|
||||
if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
|
||||
if nop_cfg.contains_key("blow-up") {
|
||||
anyhow::bail!("Boom!!1!");
|
||||
}
|
||||
}
|
||||
|
||||
// we *are* a no-op preprocessor after all
|
||||
Ok(book)
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
renderer != "not-supported"
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/book/book.rs
139
src/book/book.rs
@@ -5,8 +5,8 @@ use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use super::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
use crate::config::BuildConfig;
|
||||
use crate::errors::*;
|
||||
use config::BuildConfig;
|
||||
use errors::*;
|
||||
|
||||
/// Load a book into memory from its `src/` directory.
|
||||
pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book> {
|
||||
@@ -15,13 +15,13 @@ pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book>
|
||||
|
||||
let mut summary_content = String::new();
|
||||
File::open(summary_md)
|
||||
.with_context(|| "Couldn't open SUMMARY.md")?
|
||||
.chain_err(|| "Couldn't open SUMMARY.md")?
|
||||
.read_to_string(&mut summary_content)?;
|
||||
|
||||
let summary = parse_summary(&summary_content).with_context(|| "Summary parsing failed")?;
|
||||
let summary = parse_summary(&summary_content).chain_err(|| "Summary parsing failed")?;
|
||||
|
||||
if cfg.create_missing {
|
||||
create_missing(&src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
|
||||
create_missing(&src_dir, &summary).chain_err(|| "Unable to create missing chapters")?;
|
||||
}
|
||||
|
||||
load_book_from_disk(&summary, src_dir)
|
||||
@@ -39,19 +39,17 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
||||
let next = items.pop().expect("already checked");
|
||||
|
||||
if let SummaryItem::Link(ref link) = *next {
|
||||
if let Some(ref location) = link.location {
|
||||
let filename = src_dir.join(location);
|
||||
if !filename.exists() {
|
||||
if let Some(parent) = filename.parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let filename = src_dir.join(&link.location);
|
||||
if !filename.exists() {
|
||||
if let Some(parent) = filename.parent() {
|
||||
if !parent.exists() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
debug!("Creating missing file {}", filename.display());
|
||||
|
||||
let mut f = File::create(&filename)?;
|
||||
writeln!(f, "# {}", link.name)?;
|
||||
}
|
||||
debug!("Creating missing file {}", filename.display());
|
||||
|
||||
let mut f = File::create(&filename)?;
|
||||
writeln!(f, "# {}", link.name)?;
|
||||
}
|
||||
|
||||
items.extend(&link.nested_items);
|
||||
@@ -84,7 +82,7 @@ impl Book {
|
||||
}
|
||||
|
||||
/// Get a depth-first iterator over the items in the book.
|
||||
pub fn iter(&self) -> BookItems<'_> {
|
||||
pub fn iter(&self) -> BookItems {
|
||||
BookItems {
|
||||
items: self.sections.iter().collect(),
|
||||
}
|
||||
@@ -118,7 +116,7 @@ where
|
||||
I: IntoIterator<Item = &'a mut BookItem>,
|
||||
{
|
||||
for item in items {
|
||||
if let BookItem::Chapter(ch) = item {
|
||||
if let &mut BookItem::Chapter(ref mut ch) = item {
|
||||
for_each_mut(func, &mut ch.sub_items);
|
||||
}
|
||||
|
||||
@@ -133,8 +131,6 @@ pub enum BookItem {
|
||||
Chapter(Chapter),
|
||||
/// A section separator.
|
||||
Separator,
|
||||
/// A part title.
|
||||
PartTitle(String),
|
||||
}
|
||||
|
||||
impl From<Chapter> for BookItem {
|
||||
@@ -156,7 +152,7 @@ pub struct Chapter {
|
||||
/// Nested items.
|
||||
pub sub_items: Vec<BookItem>,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
pub path: Option<PathBuf>,
|
||||
pub path: PathBuf,
|
||||
/// An ordered list of the names of each chapter above this one, in the hierarchy.
|
||||
pub parent_names: Vec<String>,
|
||||
}
|
||||
@@ -171,39 +167,19 @@ impl Chapter {
|
||||
) -> Chapter {
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content,
|
||||
path: Some(path.into()),
|
||||
parent_names,
|
||||
content: content,
|
||||
path: path.into(),
|
||||
parent_names: parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new draft chapter that is not attached to a source markdown file and has
|
||||
/// thus no content.
|
||||
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
|
||||
Chapter {
|
||||
name: name.to_string(),
|
||||
content: String::new(),
|
||||
path: None,
|
||||
parent_names,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file
|
||||
pub fn is_draft_chapter(&self) -> bool {
|
||||
match self.path {
|
||||
Some(_) => false,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the provided `Summary` to load a `Book` from disk.
|
||||
///
|
||||
/// You need to pass in the book's source directory because all the links in
|
||||
/// `SUMMARY.md` give the chapter locations relative to it.
|
||||
pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
|
||||
fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P) -> Result<Book> {
|
||||
debug!("Loading the book from disk");
|
||||
let src_dir = src_dir.as_ref();
|
||||
|
||||
@@ -226,17 +202,16 @@ pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P)
|
||||
})
|
||||
}
|
||||
|
||||
fn load_summary_item<P: AsRef<Path> + Clone>(
|
||||
fn load_summary_item<P: AsRef<Path>>(
|
||||
item: &SummaryItem,
|
||||
src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Result<BookItem> {
|
||||
match item {
|
||||
match *item {
|
||||
SummaryItem::Separator => Ok(BookItem::Separator),
|
||||
SummaryItem::Link(ref link) => {
|
||||
load_chapter(link, src_dir, parent_names).map(BookItem::Chapter)
|
||||
load_chapter(link, src_dir, parent_names).map(|c| BookItem::Chapter(c))
|
||||
}
|
||||
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,36 +220,28 @@ fn load_chapter<P: AsRef<Path>>(
|
||||
src_dir: P,
|
||||
parent_names: Vec<String>,
|
||||
) -> Result<Chapter> {
|
||||
debug!("Loading {} ({})", link.name, link.location.display());
|
||||
let src_dir = src_dir.as_ref();
|
||||
|
||||
let mut ch = if let Some(ref link_location) = link.location {
|
||||
debug!("Loading {} ({})", link.name, link_location.display());
|
||||
|
||||
let location = if link_location.is_absolute() {
|
||||
link_location.clone()
|
||||
} else {
|
||||
src_dir.join(link_location)
|
||||
};
|
||||
|
||||
let mut f = File::open(&location)
|
||||
.with_context(|| format!("Chapter file not found, {}", link_location.display()))?;
|
||||
|
||||
let mut content = String::new();
|
||||
f.read_to_string(&mut content).with_context(|| {
|
||||
format!("Unable to read \"{}\" ({})", link.name, location.display())
|
||||
})?;
|
||||
|
||||
let stripped = location
|
||||
.strip_prefix(&src_dir)
|
||||
.expect("Chapters are always inside a book");
|
||||
|
||||
Chapter::new(&link.name, content, stripped, parent_names.clone())
|
||||
let location = if link.location.is_absolute() {
|
||||
link.location.clone()
|
||||
} else {
|
||||
Chapter::new_draft(&link.name, parent_names.clone())
|
||||
src_dir.join(&link.location)
|
||||
};
|
||||
|
||||
let mut sub_item_parents = parent_names.clone();
|
||||
let mut f = File::open(&location)
|
||||
.chain_err(|| format!("Chapter file not found, {}", link.location.display()))?;
|
||||
|
||||
let mut content = String::new();
|
||||
f.read_to_string(&mut content)
|
||||
.chain_err(|| format!("Unable to read \"{}\" ({})", link.name, location.display()))?;
|
||||
|
||||
let stripped = location
|
||||
.strip_prefix(&src_dir)
|
||||
.expect("Chapters are always inside a book");
|
||||
|
||||
let mut sub_item_parents = parent_names.clone();
|
||||
let mut ch = Chapter::new(&link.name, content, stripped, parent_names);
|
||||
ch.number = link.number.clone();
|
||||
|
||||
sub_item_parents.push(link.name.clone());
|
||||
@@ -319,7 +286,7 @@ impl<'a> Iterator for BookItems<'a> {
|
||||
}
|
||||
|
||||
impl Display for Chapter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
if let Some(ref section_number) = self.number {
|
||||
write!(f, "{} ", section_number)?;
|
||||
}
|
||||
@@ -334,7 +301,7 @@ mod tests {
|
||||
use std::io::Write;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
const DUMMY_SRC: &str = "
|
||||
const DUMMY_SRC: &'static str = "
|
||||
# Dummy Chapter
|
||||
|
||||
this is some dummy text.
|
||||
@@ -350,7 +317,7 @@ And here is some \
|
||||
let chapter_path = temp.path().join("chapter_1.md");
|
||||
File::create(&chapter_path)
|
||||
.unwrap()
|
||||
.write_all(DUMMY_SRC.as_bytes())
|
||||
.write(DUMMY_SRC.as_bytes())
|
||||
.unwrap();
|
||||
|
||||
let link = Link::new("Chapter 1", chapter_path);
|
||||
@@ -366,7 +333,7 @@ And here is some \
|
||||
|
||||
File::create(&second_path)
|
||||
.unwrap()
|
||||
.write_all(b"Hello World!")
|
||||
.write_all("Hello World!".as_bytes())
|
||||
.unwrap();
|
||||
|
||||
let mut second = Link::new("Nested Chapter 1", &second_path);
|
||||
@@ -409,7 +376,7 @@ And here is some \
|
||||
name: String::from("Nested Chapter 1"),
|
||||
content: String::from("Hello World!"),
|
||||
number: Some(SectionNumber(vec![1, 2])),
|
||||
path: Some(PathBuf::from("second.md")),
|
||||
path: PathBuf::from("second.md"),
|
||||
parent_names: vec![String::from("Chapter 1")],
|
||||
sub_items: Vec::new(),
|
||||
};
|
||||
@@ -417,7 +384,7 @@ And here is some \
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: Some(PathBuf::from("chapter_1.md")),
|
||||
path: PathBuf::from("chapter_1.md"),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(nested.clone()),
|
||||
@@ -441,7 +408,7 @@ And here is some \
|
||||
sections: vec![BookItem::Chapter(Chapter {
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
path: Some(PathBuf::from("chapter_1.md")),
|
||||
path: PathBuf::from("chapter_1.md"),
|
||||
..Default::default()
|
||||
})],
|
||||
..Default::default()
|
||||
@@ -481,7 +448,7 @@ And here is some \
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
path: PathBuf::from("Chapter_1/index.md"),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
@@ -514,8 +481,7 @@ And here is some \
|
||||
.filter_map(|i| match *i {
|
||||
BookItem::Chapter(ref ch) => Some(ch.name.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
}).collect();
|
||||
let should_be: Vec<_> = vec![
|
||||
String::from("Chapter 1"),
|
||||
String::from("Hello World"),
|
||||
@@ -533,7 +499,7 @@ And here is some \
|
||||
name: String::from("Chapter 1"),
|
||||
content: String::from(DUMMY_SRC),
|
||||
number: None,
|
||||
path: Some(PathBuf::from("Chapter_1/index.md")),
|
||||
path: PathBuf::from("Chapter_1/index.md"),
|
||||
parent_names: Vec::new(),
|
||||
sub_items: vec![
|
||||
BookItem::Chapter(Chapter::new(
|
||||
@@ -570,10 +536,9 @@ And here is some \
|
||||
let summary = Summary {
|
||||
numbered_chapters: vec![SummaryItem::Link(Link {
|
||||
name: String::from("Empty"),
|
||||
location: Some(PathBuf::from("")),
|
||||
location: PathBuf::from(""),
|
||||
..Default::default()
|
||||
})],
|
||||
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -590,7 +555,7 @@ And here is some \
|
||||
let summary = Summary {
|
||||
numbered_chapters: vec![SummaryItem::Link(Link {
|
||||
name: String::from("nested"),
|
||||
location: Some(dir),
|
||||
location: dir,
|
||||
..Default::default()
|
||||
})],
|
||||
..Default::default()
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use toml;
|
||||
|
||||
use super::MDBook;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use crate::theme;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
use theme;
|
||||
|
||||
/// A helper for setting up a new book and its directory structure.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -64,19 +65,19 @@ impl BookBuilder {
|
||||
info!("Creating a new book with stub content");
|
||||
|
||||
self.create_directory_structure()
|
||||
.with_context(|| "Unable to create directory structure")?;
|
||||
.chain_err(|| "Unable to create directory structure")?;
|
||||
|
||||
self.create_stub_files()
|
||||
.with_context(|| "Unable to create stub files")?;
|
||||
.chain_err(|| "Unable to create stub files")?;
|
||||
|
||||
if self.create_gitignore {
|
||||
self.build_gitignore()
|
||||
.with_context(|| "Unable to create .gitignore")?;
|
||||
.chain_err(|| "Unable to create .gitignore")?;
|
||||
}
|
||||
|
||||
if self.copy_theme {
|
||||
self.copy_across_theme()
|
||||
.with_context(|| "Unable to copy across the theme")?;
|
||||
.chain_err(|| "Unable to copy across the theme")?;
|
||||
}
|
||||
|
||||
self.write_book_toml()?;
|
||||
@@ -97,12 +98,12 @@ impl BookBuilder {
|
||||
fn write_book_toml(&self) -> Result<()> {
|
||||
debug!("Writing book.toml");
|
||||
let book_toml = self.root.join("book.toml");
|
||||
let cfg = toml::to_vec(&self.config).with_context(|| "Unable to serialize the config")?;
|
||||
let cfg = toml::to_vec(&self.config).chain_err(|| "Unable to serialize the config")?;
|
||||
|
||||
File::create(book_toml)
|
||||
.with_context(|| "Couldn't create book.toml")?
|
||||
.chain_err(|| "Couldn't create book.toml")?
|
||||
.write_all(&cfg)
|
||||
.with_context(|| "Unable to write config to book.toml")?;
|
||||
.chain_err(|| "Unable to write config to book.toml")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -143,10 +144,7 @@ impl BookBuilder {
|
||||
variables_css.write_all(theme::VARIABLES_CSS)?;
|
||||
|
||||
let mut favicon = File::create(themedir.join("favicon.png"))?;
|
||||
favicon.write_all(theme::FAVICON_PNG)?;
|
||||
|
||||
let mut favicon = File::create(themedir.join("favicon.svg"))?;
|
||||
favicon.write_all(theme::FAVICON_SVG)?;
|
||||
favicon.write_all(theme::FAVICON)?;
|
||||
|
||||
let mut js = File::create(themedir.join("book.js"))?;
|
||||
js.write_all(theme::JS)?;
|
||||
@@ -175,20 +173,15 @@ impl BookBuilder {
|
||||
let src_dir = self.root.join(&self.config.book.src);
|
||||
|
||||
let summary = src_dir.join("SUMMARY.md");
|
||||
if !summary.exists() {
|
||||
trace!("No summary found creating stub summary and chapter_1.md.");
|
||||
let mut f = File::create(&summary).with_context(|| "Unable to create SUMMARY.md")?;
|
||||
writeln!(f, "# Summary")?;
|
||||
writeln!(f)?;
|
||||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||
let mut f = File::create(&summary).chain_err(|| "Unable to create SUMMARY.md")?;
|
||||
writeln!(f, "# Summary")?;
|
||||
writeln!(f, "")?;
|
||||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||
|
||||
let chapter_1 = src_dir.join("chapter_1.md");
|
||||
let mut f = File::create(&chapter_1).chain_err(|| "Unable to create chapter_1.md")?;
|
||||
writeln!(f, "# Chapter 1")?;
|
||||
|
||||
let chapter_1 = src_dir.join("chapter_1.md");
|
||||
let mut f =
|
||||
File::create(&chapter_1).with_context(|| "Unable to create chapter_1.md")?;
|
||||
writeln!(f, "# Chapter 1")?;
|
||||
} else {
|
||||
trace!("Existing summary found, no need to create stub files.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
268
src/book/mod.rs
268
src/book/mod.rs
@@ -5,7 +5,6 @@
|
||||
//!
|
||||
//! [1]: ../index.html
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod book;
|
||||
mod init;
|
||||
mod summary;
|
||||
@@ -17,18 +16,15 @@ pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::string::ToString;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use toml::Value;
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::preprocess::{
|
||||
CmdPreprocessor, IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext,
|
||||
};
|
||||
use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
|
||||
use crate::utils;
|
||||
use errors::*;
|
||||
use preprocess::{IndexPreprocessor, LinkPreprocessor, Preprocessor, PreprocessorContext};
|
||||
use renderer::{CmdRenderer, HtmlHandlebars, RenderContext, Renderer};
|
||||
use utils;
|
||||
|
||||
use crate::config::{Config, RustEdition};
|
||||
use config::Config;
|
||||
|
||||
/// The object used to manage and build a book.
|
||||
pub struct MDBook {
|
||||
@@ -38,10 +34,10 @@ pub struct MDBook {
|
||||
pub config: Config,
|
||||
/// A representation of the book's contents in memory.
|
||||
pub book: Book,
|
||||
renderers: Vec<Box<dyn Renderer>>,
|
||||
renderers: Vec<Box<Renderer>>,
|
||||
|
||||
/// List of pre-processors to be run on the book
|
||||
preprocessors: Vec<Box<dyn Preprocessor>>,
|
||||
preprocessors: Vec<Box<Preprocessor>>,
|
||||
}
|
||||
|
||||
impl MDBook {
|
||||
@@ -57,7 +53,7 @@ impl MDBook {
|
||||
warn!("This format is no longer used, so you should migrate to the");
|
||||
warn!("book.toml format.");
|
||||
warn!("Check the user guide for migration information:");
|
||||
warn!("\thttps://rust-lang.github.io/mdBook/format/config.html");
|
||||
warn!("\thttps://rust-lang-nursery.github.io/mdBook/format/config.html");
|
||||
}
|
||||
|
||||
let mut config = if config_location.exists() {
|
||||
@@ -69,7 +65,7 @@ impl MDBook {
|
||||
|
||||
config.update_from_env();
|
||||
|
||||
if log_enabled!(log::Level::Trace) {
|
||||
if log_enabled!(::log::Level::Trace) {
|
||||
for line in format!("Config: {:#?}", config).lines() {
|
||||
trace!("{}", line);
|
||||
}
|
||||
@@ -97,42 +93,21 @@ impl MDBook {
|
||||
})
|
||||
}
|
||||
|
||||
/// Load a book from its root directory using a custom config and a custom summary.
|
||||
pub fn load_with_config_and_summary<P: Into<PathBuf>>(
|
||||
book_root: P,
|
||||
config: Config,
|
||||
summary: Summary,
|
||||
) -> Result<MDBook> {
|
||||
let root = book_root.into();
|
||||
|
||||
let src_dir = root.join(&config.book.src);
|
||||
let book = book::load_book_from_disk(&summary, &src_dir)?;
|
||||
|
||||
let renderers = determine_renderers(&config);
|
||||
let preprocessors = determine_preprocessors(&config)?;
|
||||
|
||||
Ok(MDBook {
|
||||
root,
|
||||
config,
|
||||
book,
|
||||
renderers,
|
||||
preprocessors,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a flat depth-first iterator over the elements of the book,
|
||||
/// it returns an [BookItem enum](bookitem.html):
|
||||
/// `(section: String, bookitem: &BookItem)`
|
||||
///
|
||||
/// ```no_run
|
||||
/// # extern crate mdbook;
|
||||
/// # use mdbook::MDBook;
|
||||
/// # use mdbook::book::BookItem;
|
||||
/// # #[allow(unused_variables)]
|
||||
/// # fn main() {
|
||||
/// # let book = MDBook::load("mybook").unwrap();
|
||||
/// for item in book.iter() {
|
||||
/// match *item {
|
||||
/// BookItem::Chapter(ref chapter) => {},
|
||||
/// BookItem::Separator => {},
|
||||
/// BookItem::PartTitle(ref title) => {}
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
@@ -143,8 +118,9 @@ impl MDBook {
|
||||
/// // 2. Chapter 2
|
||||
/// //
|
||||
/// // etc.
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn iter(&self) -> BookItems<'_> {
|
||||
pub fn iter(&self) -> BookItems {
|
||||
self.book.iter()
|
||||
}
|
||||
|
||||
@@ -181,18 +157,17 @@ impl MDBook {
|
||||
}
|
||||
|
||||
/// Run the entire build process for a particular `Renderer`.
|
||||
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
||||
fn execute_build_process(&self, renderer: &Renderer) -> Result<()> {
|
||||
let mut preprocessed_book = self.book.clone();
|
||||
let preprocess_ctx = PreprocessorContext::new(
|
||||
self.root.clone(),
|
||||
self.config.clone(),
|
||||
renderer.name().to_string(),
|
||||
);
|
||||
let preprocess_ctx = PreprocessorContext::new(self.root.clone(),
|
||||
self.config.clone(),
|
||||
renderer.name().to_string());
|
||||
|
||||
for preprocessor in &self.preprocessors {
|
||||
if preprocessor_should_run(&**preprocessor, renderer, &self.config) {
|
||||
debug!("Running the {} preprocessor.", preprocessor.name());
|
||||
preprocessed_book = preprocessor.run(&preprocess_ctx, preprocessed_book)?;
|
||||
preprocessed_book =
|
||||
preprocessor.run(&preprocess_ctx, preprocessed_book)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,9 +177,23 @@ impl MDBook {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(&self, preprocessed_book: &Book, renderer: &dyn Renderer) -> Result<()> {
|
||||
fn render(
|
||||
&self,
|
||||
preprocessed_book: &Book,
|
||||
renderer: &Renderer,
|
||||
) -> Result<()> {
|
||||
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")?;
|
||||
}
|
||||
|
||||
let render_context = RenderContext::new(
|
||||
self.root.clone(),
|
||||
@@ -215,7 +204,7 @@ impl MDBook {
|
||||
|
||||
renderer
|
||||
.render(&render_context)
|
||||
.with_context(|| "Rendering failed")
|
||||
.chain_err(|| "Rendering failed")
|
||||
}
|
||||
|
||||
/// You can change the default renderer to another one by using this method.
|
||||
@@ -227,7 +216,7 @@ impl MDBook {
|
||||
}
|
||||
|
||||
/// Register a [`Preprocessor`](../preprocess/trait.Preprocessor.html) to be used when rendering the book.
|
||||
pub fn with_preprocessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
||||
pub fn with_preprecessor<P: Preprocessor + 'static>(&mut self, preprocessor: P) -> &mut Self {
|
||||
self.preprocessors.push(Box::new(preprocessor));
|
||||
self
|
||||
}
|
||||
@@ -243,59 +232,41 @@ impl MDBook {
|
||||
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
||||
|
||||
// FIXME: Is "test" the proper renderer name to use here?
|
||||
let preprocess_context =
|
||||
PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
|
||||
let preprocess_context = PreprocessorContext::new(self.root.clone(),
|
||||
self.config.clone(),
|
||||
"test".to_string());
|
||||
|
||||
let book = LinkPreprocessor::new().run(&preprocess_context, self.book.clone())?;
|
||||
// Index Preprocessor is disabled so that chapter paths continue to point to the
|
||||
// actual markdown files.
|
||||
|
||||
let mut failed = false;
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
let chapter_path = match ch.path {
|
||||
Some(ref path) if !path.as_os_str().is_empty() => path,
|
||||
_ => continue,
|
||||
};
|
||||
if !ch.path.as_os_str().is_empty() {
|
||||
let path = self.source_dir().join(&ch.path);
|
||||
let content = utils::fs::file_to_string(&path)?;
|
||||
info!("Testing file: {:?}", path);
|
||||
|
||||
let path = self.source_dir().join(&chapter_path);
|
||||
info!("Testing file: {:?}", path);
|
||||
// write preprocessed file to tempdir
|
||||
let path = temp_dir.path().join(&ch.path);
|
||||
let mut tmpf = utils::fs::create_file(&path)?;
|
||||
tmpf.write_all(content.as_bytes())?;
|
||||
|
||||
// write preprocessed file to tempdir
|
||||
let path = temp_dir.path().join(&chapter_path);
|
||||
let mut tmpf = utils::fs::create_file(&path)?;
|
||||
tmpf.write_all(ch.content.as_bytes())?;
|
||||
let output = Command::new("rustdoc")
|
||||
.arg(&path)
|
||||
.arg("--test")
|
||||
.args(&library_args)
|
||||
.output()?;
|
||||
|
||||
let mut cmd = Command::new("rustdoc");
|
||||
cmd.arg(&path).arg("--test").args(&library_args);
|
||||
|
||||
if let Some(edition) = self.config.rust.edition {
|
||||
match edition {
|
||||
RustEdition::E2015 => {
|
||||
cmd.args(&["--edition", "2015"]);
|
||||
}
|
||||
RustEdition::E2018 => {
|
||||
cmd.args(&["--edition", "2018"]);
|
||||
}
|
||||
if !output.status.success() {
|
||||
bail!(ErrorKind::Subprocess(
|
||||
"Rustdoc returned an error".to_string(),
|
||||
output
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
failed = true;
|
||||
error!(
|
||||
"rustdoc returned an error:\n\
|
||||
\n--- stdout\n{}\n--- stderr\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
bail!("One or more tests failed");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -348,19 +319,19 @@ 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::new();
|
||||
fn determine_renderers(config: &Config) -> Vec<Box<Renderer>> {
|
||||
let mut renderers: Vec<Box<Renderer>> = Vec::new();
|
||||
|
||||
if let Some(output_table) = config.get("output").and_then(Value::as_table) {
|
||||
renderers.extend(output_table.iter().map(|(key, table)| {
|
||||
if let Some(output_table) = config.get("output").and_then(|o| o.as_table()) {
|
||||
for (key, table) in output_table.iter() {
|
||||
// the "html" backend has its own Renderer
|
||||
if key == "html" {
|
||||
Box::new(HtmlHandlebars::new()) as Box<dyn Renderer>
|
||||
} else if key == "markdown" {
|
||||
Box::new(MarkdownRenderer::new()) as Box<dyn Renderer>
|
||||
renderers.push(Box::new(HtmlHandlebars::new()));
|
||||
} else {
|
||||
interpret_custom_renderer(key, table)
|
||||
let renderer = interpret_custom_renderer(key, table);
|
||||
renderers.push(renderer);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// if we couldn't find anything, add the HTML renderer as a default
|
||||
@@ -371,63 +342,60 @@ fn determine_renderers(config: &Config) -> Vec<Box<dyn Renderer>> {
|
||||
renderers
|
||||
}
|
||||
|
||||
fn default_preprocessors() -> Vec<Box<dyn Preprocessor>> {
|
||||
fn default_preprocessors() -> Vec<Box<Preprocessor>> {
|
||||
vec![
|
||||
Box::new(LinkPreprocessor::new()),
|
||||
Box::new(IndexPreprocessor::new()),
|
||||
]
|
||||
}
|
||||
|
||||
fn is_default_preprocessor(pre: &dyn Preprocessor) -> bool {
|
||||
fn is_default_preprocessor(pre: &Preprocessor) -> bool {
|
||||
let name = pre.name();
|
||||
name == LinkPreprocessor::NAME || name == IndexPreprocessor::NAME
|
||||
}
|
||||
|
||||
/// Look at the `MDBook` and try to figure out what preprocessors to run.
|
||||
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<dyn Preprocessor>>> {
|
||||
let mut preprocessors = Vec::new();
|
||||
fn determine_preprocessors(config: &Config) -> Result<Vec<Box<Preprocessor>>> {
|
||||
let preprocessor_keys = config.get("preprocessor")
|
||||
.and_then(|value| value.as_table())
|
||||
.map(|table| table.keys());
|
||||
|
||||
if config.build.use_default_preprocessors {
|
||||
preprocessors.extend(default_preprocessors());
|
||||
}
|
||||
let mut preprocessors = if config.build.use_default_preprocessors {
|
||||
default_preprocessors()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
if let Some(preprocessor_table) = config.get("preprocessor").and_then(Value::as_table) {
|
||||
for key in preprocessor_table.keys() {
|
||||
match key.as_ref() {
|
||||
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
|
||||
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
|
||||
name => preprocessors.push(interpret_custom_preprocessor(
|
||||
name,
|
||||
&preprocessor_table[name],
|
||||
)),
|
||||
}
|
||||
let preprocessor_keys = match preprocessor_keys {
|
||||
Some(keys) => keys,
|
||||
// If no preprocessor field is set, default to the LinkPreprocessor and
|
||||
// IndexPreprocessor. This allows you to disable default preprocessors
|
||||
// by setting "preprocess" to an empty list.
|
||||
None => return Ok(preprocessors),
|
||||
};
|
||||
|
||||
for key in preprocessor_keys {
|
||||
match key.as_ref() {
|
||||
"links" => preprocessors.push(Box::new(LinkPreprocessor::new())),
|
||||
"index" => preprocessors.push(Box::new(IndexPreprocessor::new())),
|
||||
_ => bail!("{:?} is not a recognised preprocessor", key),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(preprocessors)
|
||||
}
|
||||
|
||||
fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocessor> {
|
||||
let command = table
|
||||
.get("command")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
|
||||
Box::new(CmdPreprocessor::new(key.to_string(), command))
|
||||
}
|
||||
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<Renderer> {
|
||||
// look for the `command` field, falling back to using the key
|
||||
// prepended by "mdbook-"
|
||||
let table_dot_command = table
|
||||
.get("command")
|
||||
.and_then(Value::as_str)
|
||||
.map(ToString::to_string);
|
||||
.and_then(|c| c.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
|
||||
|
||||
Box::new(CmdRenderer::new(key.to_string(), command))
|
||||
Box::new(CmdRenderer::new(key.to_string(), command.to_string()))
|
||||
}
|
||||
|
||||
/// Check whether we should run a particular `Preprocessor` in combination
|
||||
@@ -436,11 +404,7 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
|
||||
///
|
||||
/// The `build.use-default-preprocessors` config option can be used to ensure
|
||||
/// default preprocessors always run if they support the renderer.
|
||||
fn preprocessor_should_run(
|
||||
preprocessor: &dyn Preprocessor,
|
||||
renderer: &dyn Renderer,
|
||||
cfg: &Config,
|
||||
) -> bool {
|
||||
fn preprocessor_should_run(preprocessor: &Preprocessor, renderer: &Renderer, cfg: &Config) -> bool {
|
||||
// default preprocessors should be run by default (if supported)
|
||||
if cfg.build.use_default_preprocessors && is_default_preprocessor(preprocessor) {
|
||||
return preprocessor.supports_renderer(renderer.name());
|
||||
@@ -450,19 +414,18 @@ fn preprocessor_should_run(
|
||||
let renderer_name = renderer.name();
|
||||
|
||||
if let Some(Value::Array(ref explicit_renderers)) = cfg.get(&key) {
|
||||
return explicit_renderers
|
||||
.iter()
|
||||
.filter_map(Value::as_str)
|
||||
return explicit_renderers.into_iter()
|
||||
.filter_map(|val| val.as_str())
|
||||
.any(|name| name == renderer_name);
|
||||
}
|
||||
|
||||
preprocessor.supports_renderer(renderer_name)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use toml::value::{Table, Value};
|
||||
|
||||
#[test]
|
||||
@@ -529,8 +492,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_determine_third_party_preprocessors() {
|
||||
let cfg_str = r#"
|
||||
fn config_complains_if_unimplemented_preprocessor() {
|
||||
let cfg_str: &'static str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
|
||||
@@ -546,30 +509,14 @@ mod tests {
|
||||
// make sure the `preprocessor.random` table exists
|
||||
assert!(cfg.get_preprocessor("random").is_some());
|
||||
|
||||
let got = determine_preprocessors(&cfg).unwrap();
|
||||
let got = determine_preprocessors(&cfg);
|
||||
|
||||
assert!(got.into_iter().any(|p| p.name() == "random"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preprocessors_can_provide_their_own_commands() {
|
||||
let cfg_str = r#"
|
||||
[preprocessor.random]
|
||||
command = "python random.py"
|
||||
"#;
|
||||
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// make sure the `preprocessor.random` table exists
|
||||
let random = cfg.get_preprocessor("random").unwrap();
|
||||
let random = interpret_custom_preprocessor("random", &Value::Table(random.clone()));
|
||||
|
||||
assert_eq!(random.cmd(), "python random.py");
|
||||
assert!(got.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn config_respects_preprocessor_selection() {
|
||||
let cfg_str = r#"
|
||||
let cfg_str: &'static str = r#"
|
||||
[preprocessor.links]
|
||||
renderers = ["html"]
|
||||
"#;
|
||||
@@ -577,12 +524,11 @@ mod tests {
|
||||
let cfg = Config::from_str(cfg_str).unwrap();
|
||||
|
||||
// double-check that we can access preprocessor.links.renderers[0]
|
||||
let html = cfg
|
||||
.get_preprocessor("links")
|
||||
let html = cfg.get_preprocessor("links")
|
||||
.and_then(|links| links.get("renderers"))
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|renderers| renderers.as_array())
|
||||
.and_then(|renderers| renderers.get(0))
|
||||
.and_then(Value::as_str)
|
||||
.and_then(|renderer| renderer.as_str())
|
||||
.unwrap();
|
||||
assert_eq!(html, "html");
|
||||
let html_renderer = HtmlHandlebars::default();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::errors::*;
|
||||
use errors::*;
|
||||
use memchr::{self, Memchr};
|
||||
use pulldown_cmark::{self, Event, Tag};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
@@ -25,17 +25,12 @@ use std::path::{Path, PathBuf};
|
||||
/// [Title of prefix element](relative/path/to/markdown.md)
|
||||
/// ```
|
||||
///
|
||||
/// **Part Title:** An optional title for the next collect of numbered chapters. The numbered
|
||||
/// chapters can be broken into as many parts as desired.
|
||||
///
|
||||
/// **Numbered Chapter:** Numbered chapters are the main content of the book,
|
||||
/// they
|
||||
/// will be numbered and can be nested, resulting in a nice hierarchy (chapters,
|
||||
/// sub-chapters, etc.)
|
||||
///
|
||||
/// ```markdown
|
||||
/// # Title of Part
|
||||
///
|
||||
/// - [Title of the Chapter](relative/path/to/markdown.md)
|
||||
/// ```
|
||||
///
|
||||
@@ -60,7 +55,7 @@ pub struct Summary {
|
||||
pub title: Option<String>,
|
||||
/// Chapters before the main text (e.g. an introduction).
|
||||
pub prefix_chapters: Vec<SummaryItem>,
|
||||
/// The main numbered chapters of the book, broken into one or more possibly named parts.
|
||||
/// The main chapters in the document.
|
||||
pub numbered_chapters: Vec<SummaryItem>,
|
||||
/// Items which come after the main document (e.g. a conclusion).
|
||||
pub suffix_chapters: Vec<SummaryItem>,
|
||||
@@ -76,7 +71,7 @@ pub struct Link {
|
||||
pub name: String,
|
||||
/// The location of the chapter's source file, taking the book's `src`
|
||||
/// directory as the root.
|
||||
pub location: Option<PathBuf>,
|
||||
pub location: PathBuf,
|
||||
/// The section number, if this chapter is in the numbered section.
|
||||
pub number: Option<SectionNumber>,
|
||||
/// Any nested items this chapter may contain.
|
||||
@@ -88,7 +83,7 @@ impl Link {
|
||||
pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link {
|
||||
Link {
|
||||
name: name.into(),
|
||||
location: Some(location.as_ref().to_path_buf()),
|
||||
location: location.as_ref().to_path_buf(),
|
||||
number: None,
|
||||
nested_items: Vec::new(),
|
||||
}
|
||||
@@ -99,7 +94,7 @@ impl Default for Link {
|
||||
fn default() -> Self {
|
||||
Link {
|
||||
name: String::new(),
|
||||
location: Some(PathBuf::new()),
|
||||
location: PathBuf::new(),
|
||||
number: None,
|
||||
nested_items: Vec::new(),
|
||||
}
|
||||
@@ -113,8 +108,6 @@ pub enum SummaryItem {
|
||||
Link(Link),
|
||||
/// A separator (`---`).
|
||||
Separator,
|
||||
/// A part title.
|
||||
PartTitle(String),
|
||||
}
|
||||
|
||||
impl SummaryItem {
|
||||
@@ -141,13 +134,12 @@ impl From<Link> for SummaryItem {
|
||||
///
|
||||
/// ```text
|
||||
/// summary ::= title prefix_chapters numbered_chapters
|
||||
/// suffix_chapters
|
||||
/// suffix_chapters
|
||||
/// title ::= "# " TEXT
|
||||
/// | EPSILON
|
||||
/// prefix_chapters ::= item*
|
||||
/// suffix_chapters ::= item*
|
||||
/// numbered_chapters ::= part+
|
||||
/// part ::= title dotted_item+
|
||||
/// numbered_chapters ::= dotted_item+
|
||||
/// dotted_item ::= INDENT* DOT_POINT item
|
||||
/// item ::= link
|
||||
/// | separator
|
||||
@@ -161,12 +153,7 @@ impl From<Link> for SummaryItem {
|
||||
/// > match the following regex: "[^<>\n[]]+".
|
||||
struct SummaryParser<'a> {
|
||||
src: &'a str,
|
||||
stream: pulldown_cmark::OffsetIter<'a>,
|
||||
offset: usize,
|
||||
|
||||
/// We can't actually put an event back into the `OffsetIter` stream, so instead we store it
|
||||
/// here until somebody calls `next_event` again.
|
||||
back: Option<Event<'a>>,
|
||||
stream: pulldown_cmark::Parser<'a>,
|
||||
}
|
||||
|
||||
/// Reads `Events` from the provided stream until the corresponding
|
||||
@@ -187,7 +174,7 @@ macro_rules! collect_events {
|
||||
let mut events = Vec::new();
|
||||
|
||||
loop {
|
||||
let event = $stream.next().map(|(ev, _range)| ev);
|
||||
let event = $stream.next();
|
||||
trace!("Next event: {:?}", event);
|
||||
|
||||
match event {
|
||||
@@ -208,24 +195,24 @@ macro_rules! collect_events {
|
||||
}
|
||||
|
||||
impl<'a> SummaryParser<'a> {
|
||||
fn new(text: &str) -> SummaryParser<'_> {
|
||||
let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter();
|
||||
fn new(text: &str) -> SummaryParser {
|
||||
let pulldown_parser = pulldown_cmark::Parser::new(text);
|
||||
|
||||
SummaryParser {
|
||||
src: text,
|
||||
stream: pulldown_parser,
|
||||
offset: 0,
|
||||
back: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the current line and column to give the user more useful error
|
||||
/// messages.
|
||||
fn current_location(&self) -> (usize, usize) {
|
||||
let previous_text = self.src[..self.offset].as_bytes();
|
||||
let byte_offset = self.stream.get_offset();
|
||||
|
||||
let previous_text = self.src[..byte_offset].as_bytes();
|
||||
let line = Memchr::new(b'\n', previous_text).count() + 1;
|
||||
let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0);
|
||||
let col = self.src[start_of_line..self.offset].chars().count();
|
||||
let col = self.src[start_of_line..byte_offset].chars().count();
|
||||
|
||||
(line, col)
|
||||
}
|
||||
@@ -236,13 +223,13 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
let prefix_chapters = self
|
||||
.parse_affix(true)
|
||||
.with_context(|| "There was an error parsing the prefix chapters")?;
|
||||
.chain_err(|| "There was an error parsing the prefix chapters")?;
|
||||
let numbered_chapters = self
|
||||
.parse_parts()
|
||||
.with_context(|| "There was an error parsing the numbered chapters")?;
|
||||
.parse_numbered()
|
||||
.chain_err(|| "There was an error parsing the numbered chapters")?;
|
||||
let suffix_chapters = self
|
||||
.parse_affix(false)
|
||||
.with_context(|| "There was an error parsing the suffix chapters")?;
|
||||
.chain_err(|| "There was an error parsing the suffix chapters")?;
|
||||
|
||||
Ok(Summary {
|
||||
title,
|
||||
@@ -252,7 +239,8 @@ impl<'a> SummaryParser<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse the affix chapters.
|
||||
/// Parse the affix chapters. This expects the first event (start of
|
||||
/// paragraph) to have already been consumed by the previous parser.
|
||||
fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
|
||||
let mut items = Vec::new();
|
||||
debug!(
|
||||
@@ -262,22 +250,20 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(ev @ Event::Start(Tag::List(..)))
|
||||
| Some(ev @ Event::Start(Tag::Heading(1))) => {
|
||||
Some(Event::Start(Tag::List(..))) => {
|
||||
if is_prefix {
|
||||
// we've finished prefix chapters and are at the start
|
||||
// of the numbered section.
|
||||
self.back(ev);
|
||||
break;
|
||||
} else {
|
||||
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
|
||||
}
|
||||
}
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => {
|
||||
let link = self.parse_link(href.to_string());
|
||||
Some(Event::Start(Tag::Link(href, _))) => {
|
||||
let link = self.parse_link(href.to_string())?;
|
||||
items.push(SummaryItem::Link(link));
|
||||
}
|
||||
Some(Event::Rule) => items.push(SummaryItem::Separator),
|
||||
Some(Event::Start(Tag::Rule)) => items.push(SummaryItem::Separator),
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
@@ -286,112 +272,54 @@ impl<'a> SummaryParser<'a> {
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
fn parse_parts(&mut self) -> Result<Vec<SummaryItem>> {
|
||||
let mut parts = vec![];
|
||||
|
||||
// We want the section numbers to be continues through all parts.
|
||||
let mut root_number = SectionNumber::default();
|
||||
let mut root_items = 0;
|
||||
|
||||
loop {
|
||||
// Possibly match a title or the end of the "numbered chapters part".
|
||||
let title = match self.next_event() {
|
||||
Some(ev @ Event::Start(Tag::Paragraph)) => {
|
||||
// we're starting the suffix chapters
|
||||
self.back(ev);
|
||||
break;
|
||||
}
|
||||
|
||||
Some(Event::Start(Tag::Heading(1))) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(1));
|
||||
Some(stringify_events(tags))
|
||||
}
|
||||
|
||||
Some(ev) => {
|
||||
self.back(ev);
|
||||
None
|
||||
}
|
||||
|
||||
None => break, // EOF, bail...
|
||||
};
|
||||
|
||||
// Parse the rest of the part.
|
||||
let numbered_chapters = self
|
||||
.parse_numbered(&mut root_items, &mut root_number)
|
||||
.with_context(|| "There was an error parsing the numbered chapters")?;
|
||||
|
||||
if let Some(title) = title {
|
||||
parts.push(SummaryItem::PartTitle(title));
|
||||
}
|
||||
parts.extend(numbered_chapters);
|
||||
}
|
||||
|
||||
Ok(parts)
|
||||
}
|
||||
|
||||
/// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened.
|
||||
fn parse_link(&mut self, href: String) -> Link {
|
||||
let href = href.replace("%20", " ");
|
||||
fn parse_link(&mut self, href: String) -> Result<Link> {
|
||||
let link_content = collect_events!(self.stream, end Tag::Link(..));
|
||||
let name = stringify_events(link_content);
|
||||
|
||||
let path = if href.is_empty() {
|
||||
None
|
||||
if href.is_empty() {
|
||||
Err(self.parse_error("You can't have an empty link."))
|
||||
} else {
|
||||
Some(PathBuf::from(href))
|
||||
};
|
||||
|
||||
Link {
|
||||
name,
|
||||
location: path,
|
||||
number: None,
|
||||
nested_items: Vec::new(),
|
||||
Ok(Link {
|
||||
name: name,
|
||||
location: PathBuf::from(href.to_string()),
|
||||
number: None,
|
||||
nested_items: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the numbered chapters.
|
||||
fn parse_numbered(
|
||||
&mut self,
|
||||
root_items: &mut u32,
|
||||
root_number: &mut SectionNumber,
|
||||
) -> Result<Vec<SummaryItem>> {
|
||||
/// Parse the numbered chapters. This assumes the opening list tag has
|
||||
/// already been consumed by a previous parser.
|
||||
fn parse_numbered(&mut self) -> Result<Vec<SummaryItem>> {
|
||||
let mut items = Vec::new();
|
||||
let root_number = SectionNumber::default();
|
||||
|
||||
// For the first iteration, we want to just skip any opening paragraph tags, as that just
|
||||
// marks the start of the list. But after that, another opening paragraph indicates that we
|
||||
// have started a new part or the suffix chapters.
|
||||
let mut first = true;
|
||||
// we need to do this funny loop-match-if-let dance because a rule will
|
||||
// close off any currently running list. Therefore we try to read the
|
||||
// list items before the rule, then if we encounter a rule we'll add a
|
||||
// separator and try to resume parsing numbered chapters if we start a
|
||||
// list immediately afterwards.
|
||||
//
|
||||
// If you can think of a better way to do this then please make a PR :)
|
||||
|
||||
loop {
|
||||
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?;
|
||||
|
||||
// if we've resumed after something like a rule the root sections
|
||||
// will be numbered from 1. We need to manually go back and update
|
||||
// them
|
||||
update_section_numbers(&mut bunch_of_items, 0, items.len() as u32);
|
||||
items.extend(bunch_of_items);
|
||||
|
||||
match self.next_event() {
|
||||
Some(ev @ Event::Start(Tag::Paragraph)) => {
|
||||
if !first {
|
||||
// we're starting the suffix chapters
|
||||
self.back(ev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The expectation is that pulldown cmark will terminate a paragraph before a new
|
||||
// heading, so we can always count on this to return without skipping headings.
|
||||
Some(ev @ Event::Start(Tag::Heading(1))) => {
|
||||
// we're starting a new part
|
||||
self.back(ev);
|
||||
Some(Event::Start(Tag::Paragraph)) => {
|
||||
// we're starting the suffix chapters
|
||||
break;
|
||||
}
|
||||
Some(ev @ Event::Start(Tag::List(..))) => {
|
||||
self.back(ev);
|
||||
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?;
|
||||
|
||||
// if we've resumed after something like a rule the root sections
|
||||
// will be numbered from 1. We need to manually go back and update
|
||||
// them
|
||||
update_section_numbers(&mut bunch_of_items, 0, *root_items);
|
||||
*root_items += bunch_of_items.len() as u32;
|
||||
items.extend(bunch_of_items);
|
||||
}
|
||||
Some(Event::Start(other_tag)) => {
|
||||
if other_tag == Tag::Rule {
|
||||
items.push(SummaryItem::Separator);
|
||||
}
|
||||
trace!("Skipping contents of {:?}", other_tag);
|
||||
|
||||
// Skip over the contents of this tag
|
||||
@@ -400,42 +328,29 @@ impl<'a> SummaryParser<'a> {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Event::Rule) => {
|
||||
items.push(SummaryItem::Separator);
|
||||
}
|
||||
|
||||
// something else... ignore
|
||||
Some(_) => {}
|
||||
|
||||
// EOF, bail...
|
||||
if let Some(Event::Start(Tag::List(..))) = self.next_event() {
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
// something else... ignore
|
||||
continue;
|
||||
}
|
||||
None => {
|
||||
// EOF, bail...
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// From now on, we cannot accept any new paragraph opening tags.
|
||||
first = false;
|
||||
}
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
/// Push an event back to the tail of the stream.
|
||||
fn back(&mut self, ev: Event<'a>) {
|
||||
assert!(self.back.is_none());
|
||||
trace!("Back: {:?}", ev);
|
||||
self.back = Some(ev);
|
||||
}
|
||||
|
||||
fn next_event(&mut self) -> Option<Event<'a>> {
|
||||
let next = self.back.take().or_else(|| {
|
||||
self.stream.next().map(|(ev, range)| {
|
||||
self.offset = range.start;
|
||||
ev
|
||||
})
|
||||
});
|
||||
|
||||
let next = self.stream.next();
|
||||
trace!("Next event: {:?}", next);
|
||||
|
||||
next
|
||||
@@ -452,10 +367,6 @@ impl<'a> SummaryParser<'a> {
|
||||
items.push(item);
|
||||
}
|
||||
Some(Event::Start(Tag::List(..))) => {
|
||||
// Skip this tag after comment bacause it is not nested.
|
||||
if items.is_empty() {
|
||||
continue;
|
||||
}
|
||||
// recurse to parse the nested list
|
||||
let (_, last_item) = get_last_link(&mut items)?;
|
||||
let last_item_number = last_item
|
||||
@@ -484,8 +395,8 @@ impl<'a> SummaryParser<'a> {
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Paragraph)) => continue,
|
||||
Some(Event::Start(Tag::Link(_type, href, _title))) => {
|
||||
let mut link = self.parse_link(href.to_string());
|
||||
Some(Event::Start(Tag::Link(href, _))) => {
|
||||
let mut link = self.parse_link(href.to_string())?;
|
||||
|
||||
let mut number = parent.clone();
|
||||
number.0.push(num_existing_items as u32 + 1);
|
||||
@@ -493,10 +404,7 @@ impl<'a> SummaryParser<'a> {
|
||||
"Found chapter: {} {} ({})",
|
||||
number,
|
||||
link.name,
|
||||
link.location
|
||||
.as_ref()
|
||||
.map(|p| p.to_str().unwrap_or(""))
|
||||
.unwrap_or("[draft]")
|
||||
link.location.display()
|
||||
);
|
||||
|
||||
link.number = Some(number);
|
||||
@@ -515,24 +423,19 @@ impl<'a> SummaryParser<'a> {
|
||||
|
||||
fn parse_error<D: Display>(&self, msg: D) -> Error {
|
||||
let (line, col) = self.current_location();
|
||||
anyhow::anyhow!(
|
||||
"failed to parse SUMMARY.md line {}, column {}: {}",
|
||||
line,
|
||||
col,
|
||||
msg
|
||||
)
|
||||
|
||||
ErrorKind::ParseError(line, col, msg.to_string()).into()
|
||||
}
|
||||
|
||||
/// Try to parse the title line.
|
||||
fn parse_title(&mut self) -> Option<String> {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Heading(1))) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
if let Some(Event::Start(Tag::Header(1))) = self.next_event() {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(1));
|
||||
Some(stringify_events(tags))
|
||||
}
|
||||
_ => None,
|
||||
let tags = collect_events!(self.stream, end Tag::Header(1));
|
||||
Some(stringify_events(tags))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -558,22 +461,21 @@ fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
|
||||
.filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
|
||||
.rev()
|
||||
.next()
|
||||
.ok_or_else(||
|
||||
anyhow::anyhow!("Unable to get last link because the list of SummaryItems doesn't contain any Links")
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
"Unable to get last link because the list of SummaryItems doesn't contain any Links"
|
||||
.into()
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes the styling from a list of Markdown events and returns just the
|
||||
/// plain text.
|
||||
fn stringify_events(events: Vec<Event<'_>>) -> String {
|
||||
fn stringify_events(events: Vec<Event>) -> String {
|
||||
events
|
||||
.into_iter()
|
||||
.filter_map(|t| match t {
|
||||
Event::Text(text) | Event::Code(text) => Some(text.into_string()),
|
||||
Event::SoftBreak => Some(String::from(" ")),
|
||||
Event::Text(text) => Some(text.into_owned()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// A section number like "1.2.3", basically just a newtype'd `Vec<u32>` with
|
||||
@@ -582,7 +484,7 @@ fn stringify_events(events: Vec<Event<'_>>) -> String {
|
||||
pub struct SectionNumber(pub Vec<u32>);
|
||||
|
||||
impl Display for SectionNumber {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
if self.0.is_empty() {
|
||||
write!(f, "0")
|
||||
} else {
|
||||
@@ -672,16 +574,17 @@ mod tests {
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
location: PathBuf::from("./first.md"),
|
||||
..Default::default()
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: Some(PathBuf::from("./second.md")),
|
||||
location: PathBuf::from("./second.md"),
|
||||
..Default::default()
|
||||
}),
|
||||
];
|
||||
|
||||
let _ = parser.stream.next(); // step past first event
|
||||
let got = parser.parse_affix(true).unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
@@ -692,6 +595,7 @@ mod tests {
|
||||
let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n";
|
||||
let mut parser = SummaryParser::new(src);
|
||||
|
||||
let _ = parser.stream.next(); // step past first event
|
||||
let got = parser.parse_affix(true).unwrap();
|
||||
|
||||
assert_eq!(got.len(), 3);
|
||||
@@ -703,6 +607,7 @@ mod tests {
|
||||
let src = "[First](./first.md)\n- [Second](./second.md)\n";
|
||||
let mut parser = SummaryParser::new(src);
|
||||
|
||||
let _ = parser.stream.next(); // step past first event
|
||||
let got = parser.parse_affix(false);
|
||||
|
||||
assert!(got.is_err());
|
||||
@@ -713,19 +618,19 @@ mod tests {
|
||||
let src = "[First](./first.md)";
|
||||
let should_be = Link {
|
||||
name: String::from("First"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
location: PathBuf::from("./first.md"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let _ = parser.stream.next(); // Discard opening paragraph
|
||||
let _ = parser.stream.next(); // skip past start of paragraph
|
||||
|
||||
let href = match parser.stream.next() {
|
||||
Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(),
|
||||
Some(Event::Start(Tag::Link(href, _))) => href.to_string(),
|
||||
other => panic!("Unreachable, {:?}", other),
|
||||
};
|
||||
|
||||
let got = parser.parse_link(href);
|
||||
let got = parser.parse_link(href).unwrap();
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
@@ -734,16 +639,16 @@ mod tests {
|
||||
let src = "- [First](./first.md)\n";
|
||||
let link = Link {
|
||||
name: String::from("First"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
location: PathBuf::from("./first.md"),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
..Default::default()
|
||||
};
|
||||
let should_be = vec![SummaryItem::Link(link)];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
let _ = parser.stream.next();
|
||||
|
||||
let got = parser.parse_numbered().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
@@ -755,92 +660,27 @@ mod tests {
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
location: PathBuf::from("./first.md"),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: vec![SummaryItem::Link(Link {
|
||||
name: String::from("Nested"),
|
||||
location: Some(PathBuf::from("./nested.md")),
|
||||
location: PathBuf::from("./nested.md"),
|
||||
number: Some(SectionNumber(vec![1, 1])),
|
||||
nested_items: Vec::new(),
|
||||
})],
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: Some(PathBuf::from("./second.md")),
|
||||
location: PathBuf::from("./second.md"),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
let _ = parser.stream.next();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_numbered_chapters_separated_by_comment() {
|
||||
let src = "- [First](./first.md)\n<!-- this is a comment -->\n- [Second](./second.md)";
|
||||
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: Some(PathBuf::from("./second.md")),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_titled_parts() {
|
||||
let src = "- [First](./first.md)\n- [Second](./second.md)\n\
|
||||
# Title 2\n- [Third](./third.md)\n\t- [Fourth](./fourth.md)";
|
||||
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: Some(PathBuf::from("./second.md")),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::PartTitle(String::from("Title 2")),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Third"),
|
||||
location: Some(PathBuf::from("./third.md")),
|
||||
number: Some(SectionNumber(vec![3])),
|
||||
nested_items: vec![SummaryItem::Link(Link {
|
||||
name: String::from("Fourth"),
|
||||
location: Some(PathBuf::from("./fourth.md")),
|
||||
number: Some(SectionNumber(vec![3, 1])),
|
||||
nested_items: Vec::new(),
|
||||
})],
|
||||
}),
|
||||
];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser.parse_parts().unwrap();
|
||||
let got = parser.parse_numbered().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
@@ -855,122 +695,33 @@ mod tests {
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
location: PathBuf::from("./first.md"),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: Some(PathBuf::from("./second.md")),
|
||||
location: PathBuf::from("./second.md"),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
let _ = parser.stream.next();
|
||||
|
||||
let got = parser.parse_numbered().unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_link_location_is_a_draft_chapter() {
|
||||
fn an_empty_link_location_is_an_error() {
|
||||
let src = "- [Empty]()\n";
|
||||
let mut parser = SummaryParser::new(src);
|
||||
parser.stream.next();
|
||||
|
||||
let got = parser.parse_numbered(&mut 0, &mut SectionNumber::default());
|
||||
let should_be = vec![SummaryItem::Link(Link {
|
||||
name: String::from("Empty"),
|
||||
location: None,
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
})];
|
||||
|
||||
assert!(got.is_ok());
|
||||
assert_eq!(got.unwrap(), should_be);
|
||||
}
|
||||
|
||||
/// Regression test for https://github.com/rust-lang/mdBook/issues/779
|
||||
/// Ensure section numbers are correctly incremented after a horizontal separator.
|
||||
#[test]
|
||||
fn keep_numbering_after_separator() {
|
||||
let src =
|
||||
"- [First](./first.md)\n---\n- [Second](./second.md)\n---\n- [Third](./third.md)\n";
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("First"),
|
||||
location: Some(PathBuf::from("./first.md")),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Separator,
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Second"),
|
||||
location: Some(PathBuf::from("./second.md")),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Separator,
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("Third"),
|
||||
location: Some(PathBuf::from("./third.md")),
|
||||
number: Some(SectionNumber(vec![3])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
/// Regression test for https://github.com/rust-lang/mdBook/issues/1218
|
||||
/// Ensure chapter names spread across multiple lines have spaces between all the words.
|
||||
#[test]
|
||||
fn add_space_for_multi_line_chapter_names() {
|
||||
let src = "- [Chapter\ntitle](./chapter.md)";
|
||||
let should_be = vec![SummaryItem::Link(Link {
|
||||
name: String::from("Chapter title"),
|
||||
location: Some(PathBuf::from("./chapter.md")),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
})];
|
||||
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_space_in_link_destination() {
|
||||
let src = "- [test1](./test%20link1.md)\n- [test2](<./test link2.md>)";
|
||||
let should_be = vec![
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("test1"),
|
||||
location: Some(PathBuf::from("./test link1.md")),
|
||||
number: Some(SectionNumber(vec![1])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
SummaryItem::Link(Link {
|
||||
name: String::from("test2"),
|
||||
location: Some(PathBuf::from("./test link2.md")),
|
||||
number: Some(SectionNumber(vec![2])),
|
||||
nested_items: Vec::new(),
|
||||
}),
|
||||
];
|
||||
let mut parser = SummaryParser::new(src);
|
||||
let got = parser
|
||||
.parse_numbered(&mut 0, &mut SectionNumber::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
let got = parser.parse_numbered();
|
||||
assert!(got.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
use {get_book_dir, open};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
@@ -9,14 +9,11 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Builds a book from its markdown files")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
).arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
|
||||
).arg_from_usage("-o, --open 'Opens the compiled book in a web browser'")
|
||||
}
|
||||
|
||||
// Build command implementation
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::get_book_dir;
|
||||
use anyhow::Context;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::MDBook;
|
||||
use std::fs;
|
||||
|
||||
@@ -10,18 +10,15 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Deletes a built book")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
Running this command deletes this directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
).arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
}
|
||||
|
||||
// Clean command implementation
|
||||
pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
|
||||
pub fn execute(args: &ArgMatches) -> ::mdbook::errors::Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::load(&book_dir)?;
|
||||
|
||||
@@ -29,11 +26,7 @@ pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
|
||||
Some(dest_dir) => dest_dir.into(),
|
||||
None => book.root.join(&book.config.build.build_dir),
|
||||
};
|
||||
|
||||
if dir_to_remove.exists() {
|
||||
fs::remove_dir_all(&dir_to_remove)
|
||||
.with_context(|| "Unable to remove the build directory")?;
|
||||
}
|
||||
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::get_book_dir;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::config;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
@@ -12,10 +12,8 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
SubCommand::with_name("init")
|
||||
.about("Creates the boilerplate structure and files for a new book")
|
||||
// the {n} denotes a newline which will properly aligned in all help messages
|
||||
.arg_from_usage(
|
||||
"[dir] 'Directory to create the book in{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage("[dir] 'Directory to create the book in{n}\
|
||||
(Defaults to the Current Directory when omitted)'")
|
||||
.arg_from_usage("--theme 'Copies the default theme into your source folder'")
|
||||
.arg_from_usage("--force 'Skips confirmation prompts'")
|
||||
}
|
||||
|
||||
156
src/cmd/serve.rs
156
src/cmd/serve.rs
@@ -1,21 +1,20 @@
|
||||
extern crate iron;
|
||||
extern crate staticfile;
|
||||
extern crate ws;
|
||||
|
||||
use self::iron::{
|
||||
status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set,
|
||||
};
|
||||
#[cfg(feature = "watch")]
|
||||
use super::watch;
|
||||
use crate::{get_book_dir, open};
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use futures_util::sink::SinkExt;
|
||||
use futures_util::StreamExt;
|
||||
use mdbook::errors::*;
|
||||
use mdbook::utils;
|
||||
use mdbook::utils::fs::get_404_output_file;
|
||||
use mdbook::MDBook;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::broadcast;
|
||||
use warp::ws::Message;
|
||||
use warp::Filter;
|
||||
use std;
|
||||
use {get_book_dir, open};
|
||||
|
||||
/// The HTTP endpoint for the websocket used to trigger reloads when a file changes.
|
||||
const LIVE_RELOAD_ENDPOINT: &str = "__livereload";
|
||||
struct ErrorRecover;
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
@@ -23,8 +22,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Serves a book at http://localhost:3000, and rebuilds it on changes")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
@@ -48,53 +46,64 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.empty_values(false)
|
||||
.help("Port to use for HTTP connections"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("websocket-hostname")
|
||||
.long("websocket-hostname")
|
||||
.takes_value(true)
|
||||
.empty_values(false)
|
||||
.help(
|
||||
"Hostname to connect to for WebSockets connections (Defaults to the HTTP hostname)",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("websocket-port")
|
||||
.short("w")
|
||||
.long("websocket-port")
|
||||
.takes_value(true)
|
||||
.default_value("3001")
|
||||
.empty_values(false)
|
||||
.help("Port to use for WebSockets livereload connections"),
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Opens the book server in a web browser'")
|
||||
}
|
||||
|
||||
// Serve command implementation
|
||||
// Watch command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
let port = args.value_of("port").unwrap();
|
||||
let ws_port = args.value_of("websocket-port").unwrap();
|
||||
let hostname = args.value_of("hostname").unwrap();
|
||||
let public_address = args.value_of("websocket-address").unwrap_or(hostname);
|
||||
let open_browser = args.is_present("open");
|
||||
|
||||
let address = format!("{}:{}", hostname, port);
|
||||
let ws_address = format!("{}:{}", hostname, ws_port);
|
||||
|
||||
let livereload_url = format!("ws://{}:{}", public_address, ws_port);
|
||||
book.config
|
||||
.set("output.html.livereload-url", &livereload_url)?;
|
||||
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
|
||||
let livereload_url = format!("ws://{}/{}", address, LIVE_RELOAD_ENDPOINT);
|
||||
let update_config = |book: &mut MDBook| {
|
||||
book.config
|
||||
.set("output.html.livereload-url", &livereload_url)
|
||||
.expect("livereload-url update failed");
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
// Override site-url for local serving of the 404 file
|
||||
book.config.set("output.html.site-url", "/").unwrap();
|
||||
};
|
||||
update_config(&mut book);
|
||||
book.build()?;
|
||||
|
||||
let sockaddr: SocketAddr = address
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("no address found for {}", address))?;
|
||||
let build_dir = book.build_dir_for("html");
|
||||
let input_404 = book
|
||||
.config
|
||||
.get("output.html.input-404")
|
||||
.map(toml::Value::as_str)
|
||||
.and_then(std::convert::identity) // flatten
|
||||
.map(ToString::to_string);
|
||||
let file_404 = get_404_output_file(&input_404);
|
||||
let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html")));
|
||||
chain.link_after(ErrorRecover);
|
||||
let _iron = Iron::new(chain)
|
||||
.http(&*address)
|
||||
.chain_err(|| "Unable to launch the server")?;
|
||||
|
||||
// A channel used to broadcast to any websockets to reload when a file changes.
|
||||
let (tx, _rx) = tokio::sync::broadcast::channel::<Message>(100);
|
||||
let ws_server =
|
||||
ws::WebSocket::new(|_| |_| Ok(())).chain_err(|| "Unable to start the websocket")?;
|
||||
|
||||
let reload_tx = tx.clone();
|
||||
let thread_handle = std::thread::spawn(move || {
|
||||
serve(build_dir, sockaddr, reload_tx, &file_404);
|
||||
let broadcaster = ws_server.broadcaster();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
ws_server.listen(&*ws_address).unwrap();
|
||||
});
|
||||
|
||||
let serving_url = format!("http://{}", address);
|
||||
@@ -105,61 +114,36 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "watch")]
|
||||
watch::trigger_on_change(&book, move |paths, book_dir| {
|
||||
info!("Files changed: {:?}", paths);
|
||||
watch::trigger_on_change(&mut book, move |path, book_dir| {
|
||||
info!("File changed: {:?}", path);
|
||||
info!("Building book...");
|
||||
|
||||
// FIXME: This area is really ugly because we need to re-set livereload :(
|
||||
let result = MDBook::load(&book_dir).and_then(|mut b| {
|
||||
update_config(&mut b);
|
||||
b.build()
|
||||
});
|
||||
|
||||
let result = MDBook::load(&book_dir)
|
||||
.and_then(|mut b| {
|
||||
b.config
|
||||
.set("output.html.livereload-url", &livereload_url)?;
|
||||
Ok(b)
|
||||
}).and_then(|b| b.build());
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to load the book");
|
||||
utils::log_backtrace(&e);
|
||||
} else {
|
||||
let _ = tx.send(Message::text("reload"));
|
||||
let _ = broadcaster.send("reload");
|
||||
}
|
||||
});
|
||||
|
||||
let _ = thread_handle.join();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn serve(
|
||||
build_dir: PathBuf,
|
||||
address: SocketAddr,
|
||||
reload_tx: broadcast::Sender<Message>,
|
||||
file_404: &str,
|
||||
) {
|
||||
// A warp Filter which captures `reload_tx` and provides an `rx` copy to
|
||||
// receive reload messages.
|
||||
let sender = warp::any().map(move || reload_tx.subscribe());
|
||||
|
||||
// A warp Filter to handle the livereload endpoint. This upgrades to a
|
||||
// websocket, and then waits for any filesystem change notifications, and
|
||||
// relays them over the websocket.
|
||||
let livereload = warp::path(LIVE_RELOAD_ENDPOINT)
|
||||
.and(warp::ws())
|
||||
.and(sender)
|
||||
.map(|ws: warp::ws::Ws, mut rx: broadcast::Receiver<Message>| {
|
||||
ws.on_upgrade(move |ws| async move {
|
||||
let (mut user_ws_tx, _user_ws_rx) = ws.split();
|
||||
trace!("websocket got connection");
|
||||
if let Ok(m) = rx.recv().await {
|
||||
trace!("notify of reload");
|
||||
let _ = user_ws_tx.send(m).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
// A warp Filter that serves from the filesystem.
|
||||
let book_route = warp::fs::dir(build_dir.clone());
|
||||
// The fallback route for 404 errors
|
||||
let fallback_route = warp::fs::file(build_dir.join(file_404))
|
||||
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
|
||||
let routes = livereload.or(book_route).or(fallback_route);
|
||||
warp::serve(routes).run(address).await;
|
||||
impl AfterMiddleware for ErrorRecover {
|
||||
fn catch(&self, _: &mut Request, err: IronError) -> IronResult<Response> {
|
||||
match err.response.status {
|
||||
// each error will result in 404 response
|
||||
Some(_) => Ok(err.response.set(status::NotFound)),
|
||||
_ => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::get_book_dir;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use get_book_dir;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
|
||||
@@ -9,8 +9,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Tests that a book's Rust code samples compile")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
@@ -31,7 +30,7 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let library_paths: Vec<&str> = args
|
||||
.values_of("library-path")
|
||||
.map(std::iter::Iterator::collect)
|
||||
.map(|v| v.collect())
|
||||
.unwrap_or_default();
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
122
src/cmd/watch.rs
122
src/cmd/watch.rs
@@ -1,13 +1,14 @@
|
||||
use crate::{get_book_dir, open};
|
||||
extern crate notify;
|
||||
|
||||
use self::notify::Watcher;
|
||||
use clap::{App, ArgMatches, SubCommand};
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
use notify::Watcher;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
use {get_book_dir, open};
|
||||
|
||||
// Create clap subcommand arguments
|
||||
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
@@ -15,39 +16,26 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
|
||||
.about("Watches a book's files and rebuilds it on changes")
|
||||
.arg_from_usage(
|
||||
"-d, --dest-dir=[dest-dir] 'Output directory for the book{n}\
|
||||
Relative paths are interpreted relative to the book's root directory.{n}\
|
||||
If omitted, mdBook uses build.build-dir from book.toml or defaults to `./book`.'",
|
||||
)
|
||||
.arg_from_usage(
|
||||
(If omitted, uses build.build-dir from book.toml or defaults to ./book)'",
|
||||
).arg_from_usage(
|
||||
"[dir] 'Root directory for the book{n}\
|
||||
(Defaults to the Current Directory when omitted)'",
|
||||
)
|
||||
.arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
).arg_from_usage("-o, --open 'Open the compiled book in a web browser'")
|
||||
}
|
||||
|
||||
// Watch command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
|
||||
let update_config = |book: &mut MDBook| {
|
||||
if let Some(dest_dir) = args.value_of("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
}
|
||||
};
|
||||
update_config(&mut book);
|
||||
let book = MDBook::load(&book_dir)?;
|
||||
|
||||
if args.is_present("open") {
|
||||
book.build()?;
|
||||
open(book.build_dir_for("html").join("index.html"));
|
||||
}
|
||||
|
||||
trigger_on_change(&book, |paths, book_dir| {
|
||||
info!("Files changed: {:?}\nBuilding book...\n", paths);
|
||||
let result = MDBook::load(&book_dir).and_then(|mut b| {
|
||||
update_config(&mut b);
|
||||
b.build()
|
||||
});
|
||||
trigger_on_change(&book, |path, book_dir| {
|
||||
info!("File changed: {:?}\nBuilding book...\n", path);
|
||||
let result = MDBook::load(&book_dir).and_then(|b| b.build());
|
||||
|
||||
if let Err(e) = result {
|
||||
error!("Unable to build the book");
|
||||
@@ -58,60 +46,13 @@ 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/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
|
||||
F: Fn(Vec<PathBuf>, &Path),
|
||||
F: Fn(&Path, &Path),
|
||||
{
|
||||
use notify::DebouncedEvent::*;
|
||||
use notify::RecursiveMode::*;
|
||||
use self::notify::DebouncedEvent::*;
|
||||
use self::notify::RecursiveMode::*;
|
||||
|
||||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
@@ -120,14 +61,14 @@ where
|
||||
Ok(w) => w,
|
||||
Err(e) => {
|
||||
error!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
std::process::exit(1)
|
||||
::std::process::exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
// Add the source directory to the watcher
|
||||
if let Err(e) = watcher.watch(book.source_dir(), Recursive) {
|
||||
error!("Error while watching {:?}:\n {:?}", book.source_dir(), e);
|
||||
std::process::exit(1);
|
||||
::std::process::exit(1);
|
||||
};
|
||||
|
||||
let _ = watcher.watch(book.theme_dir(), Recursive);
|
||||
@@ -137,28 +78,13 @@ where
|
||||
|
||||
info!("Listening for changes...");
|
||||
|
||||
loop {
|
||||
let first_event = rx.recv().unwrap();
|
||||
sleep(Duration::from_millis(50));
|
||||
let other_events = rx.try_iter();
|
||||
|
||||
let all_events = std::iter::once(first_event).chain(other_events);
|
||||
|
||||
let paths = all_events
|
||||
.filter_map(|event| {
|
||||
debug!("Received filesystem event: {:?}", event);
|
||||
|
||||
match event {
|
||||
Create(path) | Write(path) | Remove(path) | Rename(_, path) => Some(path),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let paths = remove_ignored_files(&book.root, &paths[..]);
|
||||
|
||||
if !paths.is_empty() {
|
||||
closure(paths, &book.root);
|
||||
for event in rx.iter() {
|
||||
debug!("Received filesystem event: {:?}", event);
|
||||
match event {
|
||||
Create(path) | Write(path) | Remove(path) | Rename(_, path) => {
|
||||
closure(&path, &book.root);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
444
src/config.rs
444
src/config.rs
@@ -3,15 +3,16 @@
|
||||
//! The main entrypoint of the `config` module is the `Config` struct. This acts
|
||||
//! essentially as a bag of configuration information, with a couple
|
||||
//! pre-determined tables (`BookConfig` and `BuildConfig`) as well as support
|
||||
//! for arbitrary data which is exposed to plugins and alternative backends.
|
||||
//! for arbitrary data which is exposed to plugins and alternate backends.
|
||||
//!
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! # extern crate mdbook;
|
||||
//! # use mdbook::errors::*;
|
||||
//! # extern crate toml;
|
||||
//! use std::path::PathBuf;
|
||||
//! use std::str::FromStr;
|
||||
//! use mdbook::Config;
|
||||
//! use toml::Value;
|
||||
//!
|
||||
@@ -40,27 +41,28 @@
|
||||
//! cfg.set("output.html.theme", "./themes");
|
||||
//!
|
||||
//! // then load it again, automatically deserializing to a `PathBuf`.
|
||||
//! let got: Option<PathBuf> = cfg.get_deserialized_opt("output.html.theme")?;
|
||||
//! assert_eq!(got, Some(PathBuf::from("./themes")));
|
||||
//! let got: PathBuf = cfg.get_deserialized("output.html.theme")?;
|
||||
//! assert_eq!(got, PathBuf::from("./themes"));
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # run().unwrap()
|
||||
//! # fn main() { run().unwrap() }
|
||||
//! ```
|
||||
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::HashMap;
|
||||
use serde_json;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use toml::value::Table;
|
||||
use toml::{self, Value};
|
||||
use toml_query::delete::TomlValueDeleteExt;
|
||||
use toml_query::insert::TomlValueInsertExt;
|
||||
use toml_query::read::TomlValueReadExt;
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::utils::{self, toml_ext::TomlExt};
|
||||
use errors::*;
|
||||
|
||||
/// The overall configuration object for MDBook, essentially an in-memory
|
||||
/// representation of `book.toml`.
|
||||
@@ -70,28 +72,22 @@ pub struct Config {
|
||||
pub book: BookConfig,
|
||||
/// Information about the build environment.
|
||||
pub build: BuildConfig,
|
||||
/// Information about Rust language support.
|
||||
pub rust: RustConfig,
|
||||
rest: Value,
|
||||
}
|
||||
|
||||
impl FromStr for Config {
|
||||
type Err = Error;
|
||||
|
||||
/// Load a `Config` from some string.
|
||||
fn from_str(src: &str) -> Result<Self> {
|
||||
toml::from_str(src).with_context(|| "Invalid configuration file")
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load a `Config` from some string.
|
||||
pub fn from_str(src: &str) -> Result<Config> {
|
||||
toml::from_str(src).chain_err(|| Error::from("Invalid configuration file"))
|
||||
}
|
||||
|
||||
/// Load the configuration file from disk.
|
||||
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
|
||||
let mut buffer = String::new();
|
||||
File::open(config_file)
|
||||
.with_context(|| "Unable to open the configuration file")?
|
||||
.chain_err(|| "Unable to open the configuration file")?
|
||||
.read_to_string(&mut buffer)
|
||||
.with_context(|| "Couldn't read the file")?;
|
||||
.chain_err(|| "Couldn't read the file")?;
|
||||
|
||||
Config::from_str(&buffer)
|
||||
}
|
||||
@@ -122,7 +118,7 @@ impl Config {
|
||||
/// > when building the book with something like
|
||||
/// >
|
||||
/// > ```text
|
||||
/// > $ export MDBOOK_BOOK='{"title": "My Awesome Book", "authors": ["Michael-F-Bryan"]}'
|
||||
/// > $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
|
||||
/// > $ mdbook build
|
||||
/// > ```
|
||||
///
|
||||
@@ -132,25 +128,16 @@ impl Config {
|
||||
pub fn update_from_env(&mut self) {
|
||||
debug!("Updating the config from environment variables");
|
||||
|
||||
let overrides =
|
||||
env::vars().filter_map(|(key, value)| parse_env(&key).map(|index| (index, value)));
|
||||
let overrides = env::vars().filter_map(|(key, value)| match parse_env(&key) {
|
||||
Some(index) => Some((index, value)),
|
||||
None => None,
|
||||
});
|
||||
|
||||
for (key, value) in overrides {
|
||||
trace!("{} => {}", key, value);
|
||||
let parsed_value = serde_json::from_str(&value)
|
||||
.unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
|
||||
|
||||
if key == "book" || key == "build" {
|
||||
if let serde_json::Value::Object(ref map) = parsed_value {
|
||||
// To `set` each `key`, we wrap them as `prefix.key`
|
||||
for (k, v) in map {
|
||||
let full_key = format!("{}.{}", key, k);
|
||||
self.set(&full_key, v).expect("unreachable");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.set(key, parsed_value).expect("unreachable");
|
||||
}
|
||||
}
|
||||
@@ -158,15 +145,21 @@ impl Config {
|
||||
/// Fetch an arbitrary item from the `Config` as a `toml::Value`.
|
||||
///
|
||||
/// You can use dotted indices to access nested items (e.g.
|
||||
/// `output.html.playground` will fetch the "playground" out of the html output
|
||||
/// `output.html.playpen` will fetch the "playpen" out of the html output
|
||||
/// table).
|
||||
pub fn get(&self, key: &str) -> Option<&Value> {
|
||||
self.rest.read(key)
|
||||
match self.rest.read(key) {
|
||||
Ok(inner) => inner,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch a value from the `Config` so you can mutate it.
|
||||
pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
|
||||
self.rest.read_mut(key)
|
||||
pub fn get_mut<'a>(&'a mut self, key: &str) -> Option<&'a mut Value> {
|
||||
match self.rest.read_mut(key) {
|
||||
Ok(inner) => inner,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience method for getting the html renderer's configuration.
|
||||
@@ -177,44 +170,22 @@ impl Config {
|
||||
/// HTML renderer is refactored to be less coupled to `mdbook` internals.
|
||||
#[doc(hidden)]
|
||||
pub fn html_config(&self) -> Option<HtmlConfig> {
|
||||
match self
|
||||
.get_deserialized_opt("output.html")
|
||||
.with_context(|| "Parsing configuration [output.html]")
|
||||
{
|
||||
Ok(Some(config)) => Some(config),
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
utils::log_backtrace(&e);
|
||||
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),
|
||||
}
|
||||
self.get_deserialized("output.html").ok()
|
||||
}
|
||||
|
||||
/// Convenience function to fetch a value from the config and deserialize it
|
||||
/// into some arbitrary type.
|
||||
pub fn get_deserialized_opt<'de, T: Deserialize<'de>, S: AsRef<str>>(
|
||||
&self,
|
||||
name: S,
|
||||
) -> Result<Option<T>> {
|
||||
pub fn get_deserialized<'de, T: Deserialize<'de>, S: AsRef<str>>(&self, name: S) -> Result<T> {
|
||||
let name = name.as_ref();
|
||||
self.get(name)
|
||||
.map(|value| {
|
||||
value
|
||||
.clone()
|
||||
.try_into()
|
||||
.with_context(|| "Couldn't deserialize the value")
|
||||
})
|
||||
.transpose()
|
||||
|
||||
if let Some(value) = self.get(name) {
|
||||
value
|
||||
.clone()
|
||||
.try_into()
|
||||
.chain_err(|| "Couldn't deserialize the value")
|
||||
} else {
|
||||
bail!("Key not found, {:?}", name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a config key, clobbering any existing values along the way.
|
||||
@@ -224,15 +195,15 @@ impl Config {
|
||||
pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
|
||||
let index = index.as_ref();
|
||||
|
||||
let value = Value::try_from(value)
|
||||
.with_context(|| "Unable to represent the item as a JSON Value")?;
|
||||
let value =
|
||||
Value::try_from(value).chain_err(|| "Unable to represent the item as a JSON Value")?;
|
||||
|
||||
if index.starts_with("book.") {
|
||||
self.book.update_value(&index[5..], value);
|
||||
} else if index.starts_with("build.") {
|
||||
self.build.update_value(&index[6..], value);
|
||||
} else {
|
||||
self.rest.insert(index, value);
|
||||
self.rest.insert(index, value)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -241,13 +212,13 @@ impl Config {
|
||||
/// Get the table associated with a particular renderer.
|
||||
pub fn get_renderer<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
|
||||
let key = format!("output.{}", index.as_ref());
|
||||
self.get(&key).and_then(Value::as_table)
|
||||
self.get(&key).and_then(|v| v.as_table())
|
||||
}
|
||||
|
||||
/// Get the table associated with a particular preprocessor.
|
||||
pub fn get_preprocessor<I: AsRef<str>>(&self, index: I) -> Option<&Table> {
|
||||
let key = format!("preprocessor.{}", index.as_ref());
|
||||
self.get(&key).and_then(Value::as_table)
|
||||
self.get(&key).and_then(|v| v.as_table())
|
||||
}
|
||||
|
||||
fn from_legacy(mut table: Value) -> Config {
|
||||
@@ -273,7 +244,7 @@ impl Config {
|
||||
get_and_insert!(table, "source" => cfg.book.src);
|
||||
get_and_insert!(table, "description" => cfg.book.description);
|
||||
|
||||
if let Some(dest) = table.delete("output.html.destination") {
|
||||
if let Ok(Some(dest)) = table.delete("output.html.destination") {
|
||||
if let Ok(destination) = dest.try_into() {
|
||||
cfg.build.build_dir = destination;
|
||||
}
|
||||
@@ -289,13 +260,12 @@ impl Default for Config {
|
||||
Config {
|
||||
book: BookConfig::default(),
|
||||
build: BuildConfig::default(),
|
||||
rust: RustConfig::default(),
|
||||
rest: Value::Table(Table::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<'de> Deserialize<'de> for Config {
|
||||
fn deserialize<D: Deserializer<'de>>(de: D) -> std::result::Result<Self, D::Error> {
|
||||
fn deserialize<D: Deserializer<'de>>(de: D) -> ::std::result::Result<Self, D::Error> {
|
||||
let raw = Value::deserialize(de)?;
|
||||
|
||||
if is_legacy_format(&raw) {
|
||||
@@ -306,7 +276,7 @@ impl<'de> Deserialize<'de> for Config {
|
||||
warn!("`description` under a table called `[book]`, move the `destination` entry");
|
||||
warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
|
||||
warn!("`[build]`, and it should all work.");
|
||||
warn!("Documentation: http://rust-lang.github.io/mdBook/format/config.html");
|
||||
warn!("Documentation: http://rust-lang-nursery.github.io/mdBook/format/config.html");
|
||||
return Ok(Config::from_legacy(raw));
|
||||
}
|
||||
|
||||
@@ -330,33 +300,28 @@ impl<'de> Deserialize<'de> for Config {
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
let rust: RustConfig = table
|
||||
.remove("rust")
|
||||
.and_then(|value| value.try_into().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(Config {
|
||||
book,
|
||||
build,
|
||||
rust,
|
||||
book: book,
|
||||
build: build,
|
||||
rest: Value::Table(table),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Config {
|
||||
fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
|
||||
// TODO: This should probably be removed and use a derive instead.
|
||||
fn serialize<S: Serializer>(&self, s: S) -> ::std::result::Result<S::Ok, S::Error> {
|
||||
use serde::ser::Error;
|
||||
|
||||
let mut table = self.rest.clone();
|
||||
|
||||
let book_config = Value::try_from(&self.book).expect("should always be serializable");
|
||||
table.insert("book", book_config);
|
||||
|
||||
if self.rust != RustConfig::default() {
|
||||
let rust_config = Value::try_from(&self.rust).expect("should always be serializable");
|
||||
table.insert("rust", rust_config);
|
||||
}
|
||||
let book_config = match Value::try_from(self.book.clone()) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(_) => {
|
||||
return Err(S::Error::custom("Unable to serialize the BookConfig"));
|
||||
}
|
||||
};
|
||||
|
||||
table.insert("book", book_config).expect("unreachable");
|
||||
table.serialize(s)
|
||||
}
|
||||
}
|
||||
@@ -383,7 +348,7 @@ fn is_legacy_format(table: &Value) -> bool {
|
||||
];
|
||||
|
||||
for item in &legacy_items {
|
||||
if table.read(item).is_some() {
|
||||
if let Ok(Some(_)) = table.read(item) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -406,8 +371,6 @@ 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 {
|
||||
@@ -418,7 +381,6 @@ impl Default for BookConfig {
|
||||
description: None,
|
||||
src: PathBuf::from("src"),
|
||||
multilingual: false,
|
||||
language: Some(String::from("en")),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,7 +391,7 @@ impl Default for BookConfig {
|
||||
pub struct BuildConfig {
|
||||
/// Where to put built artefacts relative to the book's root directory.
|
||||
pub build_dir: PathBuf,
|
||||
/// Should non-existent markdown files specified in `SUMMARY.md` be created
|
||||
/// Should non-existent markdown files specified in `SETTINGS.md` be created
|
||||
/// if they don't exist?
|
||||
pub create_missing: bool,
|
||||
/// Should the default preprocessors always be used when they are
|
||||
@@ -447,42 +409,16 @@ impl Default for BuildConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for the Rust compiler(e.g., for playground)
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct RustConfig {
|
||||
/// Rust edition used in playground
|
||||
pub edition: Option<RustEdition>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
/// Rust edition to use for the code.
|
||||
pub enum RustEdition {
|
||||
/// The 2018 edition of Rust
|
||||
#[serde(rename = "2018")]
|
||||
E2018,
|
||||
/// The 2015 edition of Rust
|
||||
#[serde(rename = "2015")]
|
||||
E2015,
|
||||
}
|
||||
|
||||
/// Configuration for the HTML renderer.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct HtmlConfig {
|
||||
/// The theme directory, if specified.
|
||||
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 'navy'.
|
||||
pub preferred_dark_theme: Option<String>,
|
||||
/// Use "smart quotes" instead of the usual `"` character.
|
||||
pub curly_quotes: bool,
|
||||
/// Should mathjax be enabled?
|
||||
pub mathjax_support: bool,
|
||||
/// Whether to fonts.css and respective font files to the output directory.
|
||||
pub copy_fonts: bool,
|
||||
/// An optional google analytics code.
|
||||
pub google_analytics: Option<String>,
|
||||
/// Additional CSS stylesheets to include in the rendered page's `<head>`.
|
||||
@@ -490,31 +426,8 @@ 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,
|
||||
/// Playground settings.
|
||||
#[serde(alias = "playpen")]
|
||||
pub playground: Playground,
|
||||
/// Don't render section labels.
|
||||
pub no_section_label: bool,
|
||||
/// Search settings. If `None`, the default will be used.
|
||||
pub search: Option<Search>,
|
||||
/// Git repository url. If `None`, the git button will not be shown.
|
||||
pub git_repository_url: Option<String>,
|
||||
/// FontAwesome icon class to use for the Git repository link.
|
||||
/// Defaults to `fa-github` if `None`.
|
||||
pub git_repository_icon: Option<String>,
|
||||
/// Input path for the 404 file, defaults to 404.md, set to "" to disable 404 file output
|
||||
pub input_404: Option<String>,
|
||||
/// Absolute url to site, used to emit correct paths for the 404 page, which might be accessed in a deeply nested directory
|
||||
pub site_url: Option<String>,
|
||||
/// The DNS subdomain or apex domain at which your book will be hosted. This
|
||||
/// string will be written to a file named CNAME in the root of your site,
|
||||
/// as required by GitHub Pages (see [*Managing a custom domain for your
|
||||
/// GitHub Pages site*][custom domain]).
|
||||
///
|
||||
/// [custom domain]: https://docs.github.com/en/github/working-with-github-pages/managing-a-custom-domain-for-your-github-pages-site
|
||||
pub cname: Option<String>,
|
||||
/// 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
|
||||
@@ -523,36 +436,10 @@ pub struct HtmlConfig {
|
||||
/// This config item *should not be edited* by the end user.
|
||||
#[doc(hidden)]
|
||||
pub livereload_url: Option<String>,
|
||||
/// The mapping from old pages to new pages/URLs to use when generating
|
||||
/// redirects.
|
||||
pub redirect: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for HtmlConfig {
|
||||
fn default() -> HtmlConfig {
|
||||
HtmlConfig {
|
||||
theme: None,
|
||||
default_theme: None,
|
||||
preferred_dark_theme: None,
|
||||
curly_quotes: false,
|
||||
mathjax_support: false,
|
||||
copy_fonts: true,
|
||||
google_analytics: None,
|
||||
additional_css: Vec::new(),
|
||||
additional_js: Vec::new(),
|
||||
fold: Fold::default(),
|
||||
playground: Playground::default(),
|
||||
no_section_label: false,
|
||||
search: None,
|
||||
git_repository_url: None,
|
||||
git_repository_icon: None,
|
||||
input_404: None,
|
||||
site_url: None,
|
||||
cname: None,
|
||||
livereload_url: None,
|
||||
redirect: HashMap::new(),
|
||||
}
|
||||
}
|
||||
/// Should section labels be rendered?
|
||||
pub no_section_label: bool,
|
||||
/// Search settings. If `None`, the default will be used.
|
||||
pub search: Option<Search>,
|
||||
}
|
||||
|
||||
impl HtmlConfig {
|
||||
@@ -566,40 +453,22 @@ 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 playground.
|
||||
/// 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 Playground {
|
||||
/// Should playground snippets be editable? Default: `false`.
|
||||
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 playground snippets. Default: `false`.
|
||||
pub line_numbers: bool,
|
||||
}
|
||||
|
||||
impl Default for Playground {
|
||||
fn default() -> Playground {
|
||||
Playground {
|
||||
impl Default for Playpen {
|
||||
fn default() -> Playpen {
|
||||
Playpen {
|
||||
editable: false,
|
||||
copyable: true,
|
||||
copy_js: true,
|
||||
line_numbers: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -615,7 +484,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: `false`.
|
||||
/// If true, all search words must appear in each result. Default: `true`.
|
||||
pub use_boolean_and: bool,
|
||||
/// Boost factor for the search result score if a search word appears in the header.
|
||||
/// Default: `2`.
|
||||
@@ -664,10 +533,12 @@ 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() {
|
||||
@@ -681,16 +552,14 @@ impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::utils::fs::get_404_output_file;
|
||||
|
||||
const COMPLEX_CONFIG: &str = r#"
|
||||
const COMPLEX_CONFIG: &'static str = r#"
|
||||
[book]
|
||||
title = "Some Book"
|
||||
authors = ["Michael-F-Bryan <michaelfbryan@gmail.com>"]
|
||||
description = "A completely useless book"
|
||||
multilingual = true
|
||||
src = "source"
|
||||
language = "ja"
|
||||
|
||||
[build]
|
||||
build-dir = "outputs"
|
||||
@@ -699,24 +568,17 @@ mod tests {
|
||||
|
||||
[output.html]
|
||||
theme = "./themedir"
|
||||
default-theme = "rust"
|
||||
curly-quotes = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["./foo/bar/baz.css"]
|
||||
git-repository-url = "https://foo.com/"
|
||||
git-repository-icon = "fa-code-fork"
|
||||
|
||||
[output.html.playground]
|
||||
[output.html.playpen]
|
||||
editable = true
|
||||
editor = "ace"
|
||||
|
||||
[output.html.redirect]
|
||||
"index.html" = "overview.html"
|
||||
"nexted/page.md" = "https://rust-lang.org/"
|
||||
[preprocess.first]
|
||||
|
||||
[preprocessor.first]
|
||||
|
||||
[preprocessor.second]
|
||||
[preprocess.second]
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
@@ -729,38 +591,23 @@ mod tests {
|
||||
description: Some(String::from("A completely useless book")),
|
||||
multilingual: true,
|
||||
src: PathBuf::from("source"),
|
||||
language: Some(String::from("ja")),
|
||||
..Default::default()
|
||||
};
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("outputs"),
|
||||
create_missing: false,
|
||||
use_default_preprocessors: true,
|
||||
};
|
||||
let rust_should_be = RustConfig { edition: None };
|
||||
let playground_should_be = Playground {
|
||||
let playpen_should_be = Playpen {
|
||||
editable: true,
|
||||
copyable: true,
|
||||
copy_js: true,
|
||||
line_numbers: false,
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
|
||||
theme: Some(PathBuf::from("./themedir")),
|
||||
default_theme: Some(String::from("rust")),
|
||||
playground: playground_should_be,
|
||||
git_repository_url: Some(String::from("https://foo.com/")),
|
||||
git_repository_icon: Some(String::from("fa-code-fork")),
|
||||
redirect: vec![
|
||||
(String::from("index.html"), String::from("overview.html")),
|
||||
(
|
||||
String::from("nexted/page.md"),
|
||||
String::from("https://rust-lang.org/"),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
playpen: playpen_should_be,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -768,62 +615,9 @@ mod tests {
|
||||
|
||||
assert_eq!(got.book, book_should_be);
|
||||
assert_eq!(got.build, build_should_be);
|
||||
assert_eq!(got.rust, rust_should_be);
|
||||
assert_eq!(got.html_config().unwrap(), html_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edition_2015() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
src = "./source"
|
||||
[rust]
|
||||
edition = "2015"
|
||||
"#;
|
||||
|
||||
let book_should_be = BookConfig {
|
||||
title: Some(String::from("mdBook Documentation")),
|
||||
description: Some(String::from(
|
||||
"Create book from markdown files. Like Gitbook but implemented in Rust",
|
||||
)),
|
||||
authors: vec![String::from("Mathieu David")],
|
||||
src: PathBuf::from("./source"),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.book, book_should_be);
|
||||
|
||||
let rust_should_be = RustConfig {
|
||||
edition: Some(RustEdition::E2015),
|
||||
};
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.rust, rust_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edition_2018() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
|
||||
authors = ["Mathieu David"]
|
||||
src = "./source"
|
||||
[rust]
|
||||
edition = "2018"
|
||||
"#;
|
||||
|
||||
let rust_should_be = RustConfig {
|
||||
edition: Some(RustEdition::E2018),
|
||||
};
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.rust, rust_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_arbitrary_output_type() {
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
@@ -847,17 +641,14 @@ mod tests {
|
||||
};
|
||||
|
||||
let cfg = Config::from_str(src).unwrap();
|
||||
let got: RandomOutput = cfg.get_deserialized_opt("output.random").unwrap().unwrap();
|
||||
let got: RandomOutput = cfg.get_deserialized("output.random").unwrap();
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
|
||||
let got_baz: Vec<bool> = cfg
|
||||
.get_deserialized_opt("output.random.baz")
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let baz: Vec<bool> = cfg.get_deserialized("output.random.baz").unwrap();
|
||||
let baz_should_be = vec![true, true, false];
|
||||
|
||||
assert_eq!(got_baz, baz_should_be);
|
||||
assert_eq!(baz, baz_should_be);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -866,7 +657,7 @@ mod tests {
|
||||
// is happy...
|
||||
let src = COMPLEX_CONFIG;
|
||||
let mut config = Config::from_str(src).unwrap();
|
||||
let key = "output.html.playground.editable";
|
||||
let key = "output.html.playpen.editable";
|
||||
|
||||
assert_eq!(config.get(key).unwrap(), &Value::Boolean(true));
|
||||
*config.get_mut(key).unwrap() = Value::Boolean(false);
|
||||
@@ -934,7 +725,7 @@ mod tests {
|
||||
assert!(cfg.get(key).is_none());
|
||||
cfg.set(key, value).unwrap();
|
||||
|
||||
let got: String = cfg.get_deserialized_opt(key).unwrap().unwrap();
|
||||
let got: String = cfg.get_deserialized(key).unwrap();
|
||||
assert_eq!(got, value);
|
||||
}
|
||||
|
||||
@@ -949,7 +740,7 @@ mod tests {
|
||||
|
||||
for (src, should_be) in inputs {
|
||||
let got = parse_env(src);
|
||||
let should_be = should_be.map(ToString::to_string);
|
||||
let should_be = should_be.map(|s| s.to_string());
|
||||
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
@@ -975,14 +766,10 @@ mod tests {
|
||||
|
||||
cfg.update_from_env();
|
||||
|
||||
assert_eq!(
|
||||
cfg.get_deserialized_opt::<String, _>(key).unwrap().unwrap(),
|
||||
value
|
||||
);
|
||||
assert_eq!(cfg.get_deserialized::<String, _>(key).unwrap(), value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::approx_constant)]
|
||||
fn update_config_using_env_var_and_complex_value() {
|
||||
let mut cfg = Config::default();
|
||||
let key = "foo-bar.baz";
|
||||
@@ -997,9 +784,7 @@ mod tests {
|
||||
cfg.update_from_env();
|
||||
|
||||
assert_eq!(
|
||||
cfg.get_deserialized_opt::<serde_json::Value, _>(key)
|
||||
.unwrap()
|
||||
.unwrap(),
|
||||
cfg.get_deserialized::<serde_json::Value, _>(key).unwrap(),
|
||||
value
|
||||
);
|
||||
}
|
||||
@@ -1016,31 +801,4 @@ mod tests {
|
||||
|
||||
assert_eq!(cfg.book.title, Some(should_be));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_404_default() {
|
||||
let src = r#"
|
||||
[output.html]
|
||||
destination = "my-book"
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
let html_config = got.html_config().unwrap();
|
||||
assert_eq!(html_config.input_404, None);
|
||||
assert_eq!(&get_404_output_file(&html_config.input_404), "404.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_404_custom() {
|
||||
let src = r#"
|
||||
[output.html]
|
||||
input-404= "missing.md"
|
||||
output-404= "missing.html"
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
let html_config = got.html_config().unwrap();
|
||||
assert_eq!(html_config.input_404, Some("missing.md".to_string()));
|
||||
assert_eq!(&get_404_output_file(&html_config.input_404), "missing.html");
|
||||
}
|
||||
}
|
||||
|
||||
76
src/lib.rs
76
src/lib.rs
@@ -75,23 +75,33 @@
|
||||
//! directly, making deserializing the `RenderContext` easy and giving you
|
||||
//! access to the various methods for working with the [`Config`].
|
||||
//!
|
||||
//! [user guide]: https://rust-lang.github.io/mdBook/
|
||||
//! [user guide]: https://rust-lang-nursery.github.io/mdBook/
|
||||
//! [`RenderContext`]: renderer/struct.RenderContext.html
|
||||
//! [relevant chapter]: https://rust-lang.github.io/mdBook/for_developers/backends.html
|
||||
//! [relevant chapter]: https://rust-lang-nursery.github.io/mdBook/for_developers/backends.html
|
||||
//! [`Config`]: config/struct.Config.html
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![allow(clippy::comparison_chain)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate error_chain;
|
||||
extern crate handlebars;
|
||||
extern crate itertools;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate memchr;
|
||||
extern crate pulldown_cmark;
|
||||
extern crate regex;
|
||||
extern crate serde;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate serde_json;
|
||||
extern crate shlex;
|
||||
extern crate tempfile;
|
||||
extern crate toml;
|
||||
extern crate toml_query;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
@@ -104,19 +114,53 @@ pub mod renderer;
|
||||
pub mod theme;
|
||||
pub mod utils;
|
||||
|
||||
/// The current version of `mdbook`.
|
||||
///
|
||||
/// This is provided as a way for custom preprocessors and renderers to do
|
||||
/// compatibility checks.
|
||||
pub const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
pub use crate::book::BookItem;
|
||||
pub use crate::book::MDBook;
|
||||
pub use crate::config::Config;
|
||||
pub use crate::renderer::Renderer;
|
||||
pub use book::BookItem;
|
||||
pub use book::MDBook;
|
||||
pub use config::Config;
|
||||
pub use renderer::Renderer;
|
||||
|
||||
/// The error types used through out this crate.
|
||||
pub mod errors {
|
||||
pub(crate) use anyhow::{bail, ensure, Context};
|
||||
pub use anyhow::{Error, Result};
|
||||
use std::path::PathBuf;
|
||||
|
||||
error_chain!{
|
||||
foreign_links {
|
||||
Io(::std::io::Error) #[doc = "A wrapper around `std::io::Error`"];
|
||||
HandlebarsRender(::handlebars::RenderError) #[doc = "Handlebars rendering failed"];
|
||||
HandlebarsTemplate(Box<::handlebars::TemplateError>) #[doc = "Unable to parse the template"];
|
||||
Utf8(::std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
|
||||
SerdeJson(::serde_json::Error) #[doc = "JSON conversion failed"];
|
||||
}
|
||||
|
||||
links {
|
||||
TomlQuery(::toml_query::error::Error, ::toml_query::error::ErrorKind) #[doc = "A TomlQuery error"];
|
||||
}
|
||||
|
||||
errors {
|
||||
/// A subprocess exited with an unsuccessful return code.
|
||||
Subprocess(message: String, output: ::std::process::Output) {
|
||||
description("A subprocess failed")
|
||||
display("{}: {}", message, String::from_utf8_lossy(&output.stdout))
|
||||
}
|
||||
|
||||
/// An error was encountered while parsing the `SUMMARY.md` file.
|
||||
ParseError(line: usize, col: usize, message: String) {
|
||||
description("A SUMMARY.md parsing error")
|
||||
display("Error at line {}, column {}: {}", line, col, message)
|
||||
}
|
||||
|
||||
/// The user tried to use a reserved filename.
|
||||
ReservedFilenameError(filename: PathBuf) {
|
||||
description("Reserved Filename")
|
||||
display("{} is reserved for internal use", filename.display())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Box to halve the size of Error
|
||||
impl From<::handlebars::TemplateError> for Error {
|
||||
fn from(e: ::handlebars::TemplateError) -> Error {
|
||||
From::from(Box::new(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
19
src/main.rs
19
src/main.rs
@@ -1,7 +1,12 @@
|
||||
extern crate chrono;
|
||||
#[macro_use]
|
||||
extern crate clap;
|
||||
extern crate env_logger;
|
||||
extern crate error_chain;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate mdbook;
|
||||
extern crate open;
|
||||
|
||||
use chrono::Local;
|
||||
use clap::{App, AppSettings, ArgMatches};
|
||||
@@ -15,22 +20,22 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
mod cmd;
|
||||
|
||||
const VERSION: &str = concat!("v", crate_version!());
|
||||
const NAME: &'static str = "mdBook";
|
||||
const VERSION: &'static str = concat!("v", crate_version!());
|
||||
|
||||
fn main() {
|
||||
init_logger();
|
||||
|
||||
// Create a list of valid arguments and sub-commands
|
||||
let app = App::new(crate_name!())
|
||||
.about(crate_description!())
|
||||
let app = App::new(NAME)
|
||||
.about("Creates a book from markdown files")
|
||||
.author("Mathieu David <mathieudavid@mathieudavid.org>")
|
||||
.version(VERSION)
|
||||
.setting(AppSettings::GlobalVersion)
|
||||
.setting(AppSettings::ArgRequiredElseHelp)
|
||||
.setting(AppSettings::ColoredHelp)
|
||||
.after_help(
|
||||
"For more information about a specific command, try `mdbook <command> --help`\n\
|
||||
The source code for mdBook is available at: https://github.com/rust-lang/mdBook",
|
||||
The source code for mdBook is available at: https://github.com/rust-lang-nursery/mdBook",
|
||||
)
|
||||
.subcommand(cmd::init::make_subcommand())
|
||||
.subcommand(cmd::build::make_subcommand())
|
||||
@@ -58,7 +63,7 @@ fn main() {
|
||||
if let Err(e) = res {
|
||||
utils::log_backtrace(&e);
|
||||
|
||||
std::process::exit(101);
|
||||
::std::process::exit(101);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +82,7 @@ fn init_logger() {
|
||||
});
|
||||
|
||||
if let Ok(var) = env::var("RUST_LOG") {
|
||||
builder.parse_filters(&var);
|
||||
builder.parse(&var);
|
||||
} else {
|
||||
// if no RUST_LOG provided, default to logging at the Info level
|
||||
builder.filter(None, LevelFilter::Info);
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::Book;
|
||||
use crate::errors::*;
|
||||
use shlex::Shlex;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
|
||||
/// A custom preprocessor which will shell out to a 3rd-party program.
|
||||
///
|
||||
/// # Preprocessing Protocol
|
||||
///
|
||||
/// When the `supports_renderer()` method is executed, `CmdPreprocessor` will
|
||||
/// execute the shell command `$cmd supports $renderer`. If the renderer is
|
||||
/// supported, custom preprocessors should exit with a exit code of `0`,
|
||||
/// any other exit code be considered as unsupported.
|
||||
///
|
||||
/// The `run()` method is implemented by passing a `(PreprocessorContext, Book)`
|
||||
/// tuple to the spawned command (`$cmd`) as JSON via `stdin`. Preprocessors
|
||||
/// should then "return" a processed book by printing it to `stdout` as JSON.
|
||||
/// For convenience, the `CmdPreprocessor::parse_input()` function can be used
|
||||
/// to parse the input provided by `mdbook`.
|
||||
///
|
||||
/// Exiting with a non-zero exit code while preprocessing is considered an
|
||||
/// error. `stderr` is passed directly through to the user, so it can be used
|
||||
/// for logging or emitting warnings if desired.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// An example preprocessor is available in this project's `examples/`
|
||||
/// directory.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CmdPreprocessor {
|
||||
name: String,
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
impl CmdPreprocessor {
|
||||
/// Create a new `CmdPreprocessor`.
|
||||
pub fn new(name: String, cmd: String) -> CmdPreprocessor {
|
||||
CmdPreprocessor { name, cmd }
|
||||
}
|
||||
|
||||
/// A convenience function custom preprocessors can use to parse the input
|
||||
/// written to `stdin` by a `CmdRenderer`.
|
||||
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
|
||||
serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
|
||||
}
|
||||
|
||||
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
|
||||
let stdin = child.stdin.take().expect("Child has stdin");
|
||||
|
||||
if let Err(e) = self.write_input(stdin, &book, &ctx) {
|
||||
// Looks like the backend hung up before we could finish
|
||||
// sending it the render context. Log the error and keep going
|
||||
warn!("Error writing the RenderContext to the backend, {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn write_input<W: Write>(
|
||||
&self,
|
||||
writer: W,
|
||||
book: &Book,
|
||||
ctx: &PreprocessorContext,
|
||||
) -> Result<()> {
|
||||
serde_json::to_writer(writer, &(ctx, book)).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// The command this `Preprocessor` will invoke.
|
||||
pub fn cmd(&self) -> &str {
|
||||
&self.cmd
|
||||
}
|
||||
|
||||
fn command(&self) -> Result<Command> {
|
||||
let mut words = Shlex::new(&self.cmd);
|
||||
let executable = match words.next() {
|
||||
Some(e) => e,
|
||||
None => bail!("Command string was empty"),
|
||||
};
|
||||
|
||||
let mut cmd = Command::new(executable);
|
||||
|
||||
for arg in words {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
impl Preprocessor for CmdPreprocessor {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn run(&self, ctx: &PreprocessorContext, book: Book) -> Result<Book> {
|
||||
let mut cmd = self.command()?;
|
||||
|
||||
let mut child = cmd
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.spawn()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Unable to start the \"{}\" preprocessor. Is it installed?",
|
||||
self.name()
|
||||
)
|
||||
})?;
|
||||
|
||||
self.write_input_to_child(&mut child, &book, ctx);
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.with_context(|| "Error waiting for the preprocessor to complete")?;
|
||||
|
||||
trace!("{} exited with output: {:?}", self.cmd, output);
|
||||
ensure!(
|
||||
output.status.success(),
|
||||
"The preprocessor exited unsuccessfully"
|
||||
);
|
||||
|
||||
serde_json::from_slice(&output.stdout)
|
||||
.with_context(|| "Unable to parse the preprocessed book")
|
||||
}
|
||||
|
||||
fn supports_renderer(&self, renderer: &str) -> bool {
|
||||
debug!(
|
||||
"Checking if the \"{}\" preprocessor supports \"{}\"",
|
||||
self.name(),
|
||||
renderer
|
||||
);
|
||||
|
||||
let mut cmd = match self.command() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Unable to create the command for the \"{}\" preprocessor, {}",
|
||||
self.name(),
|
||||
e
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let outcome = cmd
|
||||
.arg("supports")
|
||||
.arg(renderer)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::inherit())
|
||||
.stderr(Stdio::inherit())
|
||||
.status()
|
||||
.map(|status| status.code() == Some(0));
|
||||
|
||||
if let Err(ref e) = outcome {
|
||||
if e.kind() == io::ErrorKind::NotFound {
|
||||
warn!(
|
||||
"The command wasn't found, is the \"{}\" preprocessor installed?",
|
||||
self.name
|
||||
);
|
||||
warn!("\tCommand: {}", self.cmd);
|
||||
}
|
||||
}
|
||||
|
||||
outcome.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::MDBook;
|
||||
use std::path::Path;
|
||||
|
||||
fn book_example() -> MDBook {
|
||||
let example = Path::new(env!("CARGO_MANIFEST_DIR")).join("book-example");
|
||||
MDBook::load(example).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_write_and_parse_input() {
|
||||
let cmd = CmdPreprocessor::new("test".to_string(), "test".to_string());
|
||||
let md = book_example();
|
||||
let ctx = PreprocessorContext::new(
|
||||
md.root.clone(),
|
||||
md.config.clone(),
|
||||
"some-renderer".to_string(),
|
||||
);
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
cmd.write_input(&mut buffer, &md.book, &ctx).unwrap();
|
||||
|
||||
let (got_ctx, got_book) = CmdPreprocessor::parse_input(buffer.as_slice()).unwrap();
|
||||
|
||||
assert_eq!(got_book, md.book);
|
||||
assert_eq!(got_ctx, ctx);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
use regex::Regex;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::errors::*;
|
||||
use errors::*;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::{Book, BookItem};
|
||||
use book::{Book, BookItem};
|
||||
|
||||
/// A preprocessor for converting file name `README.md` to `index.md` since
|
||||
/// `README.md` is the de facto index file in markdown-based documentation.
|
||||
#[derive(Default)]
|
||||
/// `README.md` is the de facto index file in a markdown-based documentation.
|
||||
pub struct IndexPreprocessor;
|
||||
|
||||
impl IndexPreprocessor {
|
||||
@@ -29,15 +28,13 @@ impl Preprocessor for IndexPreprocessor {
|
||||
let source_dir = ctx.root.join(&ctx.config.book.src);
|
||||
book.for_each_mut(|section: &mut BookItem| {
|
||||
if let BookItem::Chapter(ref mut ch) = *section {
|
||||
if let Some(ref mut path) = ch.path {
|
||||
if is_readme_file(&path) {
|
||||
let mut index_md = source_dir.join(path.with_file_name("index.md"));
|
||||
if index_md.exists() {
|
||||
warn_readme_name_conflict(&path, &&mut index_md);
|
||||
}
|
||||
|
||||
path.set_file_name("index.md");
|
||||
if is_readme_file(&ch.path) {
|
||||
let index_md = source_dir.join(ch.path.with_file_name("index.md"));
|
||||
if index_md.exists() {
|
||||
warn_readme_name_conflict(&ch.path, &index_md);
|
||||
}
|
||||
|
||||
ch.path.set_file_name("index.md");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -48,10 +45,7 @@ impl Preprocessor for IndexPreprocessor {
|
||||
|
||||
fn warn_readme_name_conflict<P: AsRef<Path>>(readme_path: P, index_path: P) {
|
||||
let file_name = readme_path.as_ref().file_name().unwrap_or_default();
|
||||
let parent_dir = index_path
|
||||
.as_ref()
|
||||
.parent()
|
||||
.unwrap_or_else(|| index_path.as_ref());
|
||||
let parent_dir = index_path.as_ref().parent().unwrap_or(index_path.as_ref());
|
||||
warn!(
|
||||
"It seems that there are both {:?} and index.md under \"{}\".",
|
||||
file_name,
|
||||
@@ -73,7 +67,7 @@ fn is_readme_file<P: AsRef<Path>>(path: P) -> bool {
|
||||
RE.is_match(
|
||||
path.as_ref()
|
||||
.file_stem()
|
||||
.and_then(std::ffi::OsStr::to_str)
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
use crate::errors::*;
|
||||
use crate::utils::{
|
||||
take_anchored_lines, take_lines, take_rustdoc_include_anchored_lines,
|
||||
take_rustdoc_include_lines,
|
||||
};
|
||||
use errors::*;
|
||||
use regex::{CaptureMatches, Captures, Regex};
|
||||
use std::fs;
|
||||
use std::ops::{Bound, Range, RangeBounds, RangeFrom, RangeFull, RangeTo};
|
||||
use std::ops::{Range, RangeFrom, RangeFull, RangeTo};
|
||||
use std::path::{Path, PathBuf};
|
||||
use utils::fs::file_to_string;
|
||||
use utils::take_lines;
|
||||
|
||||
use super::{Preprocessor, PreprocessorContext};
|
||||
use crate::book::{Book, BookItem};
|
||||
use book::{Book, BookItem};
|
||||
|
||||
const ESCAPE_CHAR: char = '\\';
|
||||
const MAX_LINK_NESTED_DEPTH: usize = 10;
|
||||
|
||||
/// 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.
|
||||
/// - `{{# playground}}` - Insert runnable Rust files
|
||||
#[derive(Default)]
|
||||
/// A preprocessor for expanding the `{{# playpen}}` and `{{# include}}`
|
||||
/// helpers in a chapter.
|
||||
pub struct LinkPreprocessor;
|
||||
|
||||
impl LinkPreprocessor {
|
||||
@@ -45,15 +34,14 @@ impl Preprocessor for LinkPreprocessor {
|
||||
|
||||
book.for_each_mut(|section: &mut BookItem| {
|
||||
if let BookItem::Chapter(ref mut ch) = *section {
|
||||
if let Some(ref chapter_path) = ch.path {
|
||||
let base = chapter_path
|
||||
.parent()
|
||||
.map(|dir| src_dir.join(dir))
|
||||
.expect("All book items have a parent");
|
||||
let base = ch
|
||||
.path
|
||||
.parent()
|
||||
.map(|dir| src_dir.join(dir))
|
||||
.expect("All book items have a parent");
|
||||
|
||||
let content = replace_all(&ch.content, base, chapter_path, 0);
|
||||
ch.content = content;
|
||||
}
|
||||
let content = replace_all(&ch.content, base, &ch.path, 0);
|
||||
ch.content = content;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -74,13 +62,13 @@ where
|
||||
let mut previous_end_index = 0;
|
||||
let mut replaced = String::new();
|
||||
|
||||
for link in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..link.start_index]);
|
||||
for playpen in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..playpen.start_index]);
|
||||
|
||||
match link.render_with_path(&path) {
|
||||
match playpen.render_with_path(&path) {
|
||||
Ok(new_content) => {
|
||||
if depth < MAX_LINK_NESTED_DEPTH {
|
||||
if let Some(rel_path) = link.link_type.relative_path(path) {
|
||||
if let Some(rel_path) = playpen.link.relative_path(path) {
|
||||
replaced.push_str(&replace_all(&new_content, rel_path, source, depth + 1));
|
||||
} else {
|
||||
replaced.push_str(&new_content);
|
||||
@@ -91,17 +79,13 @@ where
|
||||
source.display()
|
||||
);
|
||||
}
|
||||
previous_end_index = link.end_index;
|
||||
previous_end_index = playpen.end_index;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error updating \"{}\", {}", link.link_text, e);
|
||||
for cause in e.chain().skip(1) {
|
||||
warn!("Caused By: {}", cause);
|
||||
}
|
||||
|
||||
error!("Error updating \"{}\", {}", playpen.link_text, e);
|
||||
// This should make sure we include the raw `{{# ... }}` snippet
|
||||
// in the page content if there are any errors.
|
||||
previous_end_index = link.start_index;
|
||||
previous_end_index = playpen.start_index;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,68 +97,11 @@ where
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
enum LinkType<'a> {
|
||||
Escaped,
|
||||
Include(PathBuf, RangeOrAnchor),
|
||||
Playground(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)
|
||||
}
|
||||
IncludeRange(PathBuf, Range<usize>),
|
||||
IncludeRangeFrom(PathBuf, RangeFrom<usize>),
|
||||
IncludeRangeTo(PathBuf, RangeTo<usize>),
|
||||
IncludeRangeFull(PathBuf, RangeFull),
|
||||
Playpen(PathBuf, Vec<&'a str>),
|
||||
}
|
||||
|
||||
impl<'a> LinkType<'a> {
|
||||
@@ -182,9 +109,11 @@ impl<'a> LinkType<'a> {
|
||||
let base = base.as_ref();
|
||||
match self {
|
||||
LinkType::Escaped => None,
|
||||
LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Playground(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRange(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeFrom(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeTo(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::IncludeRangeFull(p, _) => Some(return_relative_path(base, &p)),
|
||||
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,59 +125,50 @@ fn return_relative_path<P: AsRef<Path>>(base: P, relative: P) -> PathBuf {
|
||||
.to_path_buf()
|
||||
}
|
||||
|
||||
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();
|
||||
// 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 mut parts = path.split(':');
|
||||
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)
|
||||
// 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));
|
||||
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: start,
|
||||
end: end,
|
||||
},
|
||||
),
|
||||
None => if has_end {
|
||||
LinkType::IncludeRangeFrom(path, RangeFrom { start: start })
|
||||
} else {
|
||||
LinkType::IncludeRange(
|
||||
path,
|
||||
Range {
|
||||
start: start,
|
||||
end: start + 1,
|
||||
},
|
||||
)
|
||||
},
|
||||
},
|
||||
None => match end {
|
||||
Some(end) => LinkType::IncludeRangeTo(path, RangeTo { end: end }),
|
||||
None => LinkType::IncludeRangeFull(path, RangeFull),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
struct Link<'a> {
|
||||
start_index: usize,
|
||||
end_index: usize,
|
||||
link_type: LinkType<'a>,
|
||||
link: LinkType<'a>,
|
||||
link_text: &'a str,
|
||||
}
|
||||
|
||||
@@ -262,16 +182,7 @@ impl<'a> Link<'a> {
|
||||
|
||||
match (typ.as_str(), file_arg) {
|
||||
("include", Some(pth)) => Some(parse_include_path(pth)),
|
||||
("playground", Some(pth)) => Some(LinkType::Playground(pth.into(), props)),
|
||||
("playpen", Some(pth)) => {
|
||||
warn!(
|
||||
"the {{{{#playpen}}}} expression has been \
|
||||
renamed to {{{{#playground}}}}, \
|
||||
please update your book to use the new name"
|
||||
);
|
||||
Some(LinkType::Playground(pth.into(), props))
|
||||
}
|
||||
("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)),
|
||||
("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -281,11 +192,11 @@ impl<'a> Link<'a> {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
link_type.and_then(|lnk_type| {
|
||||
link_type.and_then(|lnk| {
|
||||
cap.get(0).map(|mat| Link {
|
||||
start_index: mat.start(),
|
||||
end_index: mat.end(),
|
||||
link_type: lnk_type,
|
||||
link: lnk,
|
||||
link_text: mat.as_str(),
|
||||
})
|
||||
})
|
||||
@@ -293,55 +204,23 @@ impl<'a> Link<'a> {
|
||||
|
||||
fn render_with_path<P: AsRef<Path>>(&self, base: P) -> Result<String> {
|
||||
let base = base.as_ref();
|
||||
match self.link_type {
|
||||
match self.link {
|
||||
// omit the escape char
|
||||
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
||||
LinkType::Include(ref pat, ref range_or_anchor) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
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),
|
||||
})
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::RustdocInclude(ref pat, ref range_or_anchor) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display(),
|
||||
)
|
||||
})
|
||||
}
|
||||
LinkType::Playground(ref pat, ref attrs) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
let contents = fs::read_to_string(&target).with_context(|| {
|
||||
format!(
|
||||
"Could not read file for link {} ({})",
|
||||
self.link_text,
|
||||
target.display()
|
||||
)
|
||||
})?;
|
||||
LinkType::IncludeRange(ref pat, ref range) => file_to_string(base.join(pat))
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRangeFrom(ref pat, ref range) => file_to_string(base.join(pat))
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRangeTo(ref pat, ref range) => file_to_string(base.join(pat))
|
||||
.map(|s| take_lines(&s, range.clone()))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::IncludeRangeFull(ref pat, _) => file_to_string(base.join(pat))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text)),
|
||||
LinkType::Playpen(ref pat, ref attrs) => {
|
||||
let contents = file_to_string(base.join(pat))
|
||||
.chain_err(|| format!("Could not read file for link {}", self.link_text))?;
|
||||
let ftype = if !attrs.is_empty() { "rust," } else { "rust" };
|
||||
Ok(format!(
|
||||
"```{}{}\n{}\n```\n",
|
||||
@@ -368,21 +247,20 @@ impl<'a> Iterator for LinkIter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn find_links(contents: &str) -> LinkIter<'_> {
|
||||
fn find_links(contents: &str) -> LinkIter {
|
||||
// lazily compute following regex
|
||||
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new(
|
||||
r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9_]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([a-zA-Z0-9\s_.\-:/\\\+]+) # link target path and space separated properties
|
||||
\s*\}\} # whitespace and link closing parens"
|
||||
)
|
||||
.unwrap();
|
||||
r"(?x) # insignificant whitespace mode
|
||||
\\\{\{\#.*\}\} # match escaped link
|
||||
| # or
|
||||
\{\{\s* # link opening parens and whitespace
|
||||
\#([a-zA-Z0-9]+) # link type
|
||||
\s+ # separating whitespace
|
||||
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
|
||||
\s*\}\} # whitespace and link closing parens"
|
||||
).unwrap();
|
||||
}
|
||||
LinkIter(RE.captures_iter(contents))
|
||||
}
|
||||
@@ -414,7 +292,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_links_partial_link() {
|
||||
let s = "Some random text with {{#playground...";
|
||||
let s = "Some random text with {{#playpen...";
|
||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||
let s = "Some random text with {{#include...";
|
||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||
@@ -424,19 +302,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_links_empty_link() {
|
||||
let s = "Some random text with {{#playground}} and {{#playground }} {{}} {{#}}...";
|
||||
let s = "Some random text with {{#playpen}} and {{#playpen }} {{}} {{#}}...";
|
||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_unknown_link_type() {
|
||||
let s = "Some random text with {{#playgroundz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
|
||||
let s = "Some random text with {{#playpenz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
|
||||
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_simple_link() {
|
||||
let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
|
||||
let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
@@ -446,38 +324,20 @@ mod tests {
|
||||
vec![
|
||||
Link {
|
||||
start_index: 22,
|
||||
end_index: 45,
|
||||
link_type: LinkType::Playground(PathBuf::from("file.rs"), vec![]),
|
||||
link_text: "{{#playground file.rs}}",
|
||||
end_index: 42,
|
||||
link: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
|
||||
link_text: "{{#playpen file.rs}}",
|
||||
},
|
||||
Link {
|
||||
start_index: 50,
|
||||
end_index: 74,
|
||||
link_type: LinkType::Playground(PathBuf::from("test.rs"), vec![]),
|
||||
link_text: "{{#playground test.rs }}",
|
||||
start_index: 47,
|
||||
end_index: 68,
|
||||
link: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
|
||||
link_text: "{{#playpen test.rs }}",
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_with_special_characters() {
|
||||
let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 57,
|
||||
link_type: LinkType::Playground(PathBuf::from("foo-bar\\baz/_c++.rs"), vec![]),
|
||||
link_text: "{{#playground foo-bar\\baz/_c++.rs}}",
|
||||
},]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_links_with_range() {
|
||||
let s = "Some random text with {{#include file.rs:10:20}}...";
|
||||
@@ -488,10 +348,7 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 48,
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(9..20))
|
||||
),
|
||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..20),
|
||||
link_text: "{{#include file.rs:10:20}}",
|
||||
}]
|
||||
);
|
||||
@@ -507,10 +364,7 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 45,
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(9..10))
|
||||
),
|
||||
link: LinkType::IncludeRange(PathBuf::from("file.rs"), 9..10),
|
||||
link_text: "{{#include file.rs:10}}",
|
||||
}]
|
||||
);
|
||||
@@ -526,10 +380,7 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(9..))
|
||||
),
|
||||
link: LinkType::IncludeRangeFrom(PathBuf::from("file.rs"), 9..),
|
||||
link_text: "{{#include file.rs:10:}}",
|
||||
}]
|
||||
);
|
||||
@@ -545,10 +396,7 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 46,
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..20))
|
||||
),
|
||||
link: LinkType::IncludeRangeTo(PathBuf::from("file.rs"), ..20),
|
||||
link_text: "{{#include file.rs::20}}",
|
||||
}]
|
||||
);
|
||||
@@ -564,10 +412,7 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 44,
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..))
|
||||
),
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_text: "{{#include file.rs::}}",
|
||||
}]
|
||||
);
|
||||
@@ -583,37 +428,15 @@ mod tests {
|
||||
vec![Link {
|
||||
start_index: 22,
|
||||
end_index: 42,
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..))
|
||||
),
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
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 playground \\{{#playground file.rs editable}} ...";
|
||||
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
@@ -621,19 +444,18 @@ mod tests {
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![Link {
|
||||
start_index: 41,
|
||||
end_index: 74,
|
||||
link_type: LinkType::Escaped,
|
||||
link_text: "\\{{#playground file.rs editable}}",
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: LinkType::Escaped,
|
||||
link_text: "\\{{#playpen file.rs editable}}",
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_playgrounds_with_properties() {
|
||||
let s =
|
||||
"Some random text with escaped playground {{#playground file.rs editable }} and some \
|
||||
more\n text {{#playground my.rs editable no_run should_panic}} ...";
|
||||
fn test_find_playpens_with_properties() {
|
||||
let s = "Some random text with escaped playpen {{#playpen file.rs editable }} and some \
|
||||
more\n text {{#playpen my.rs editable no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
println!("\nOUTPUT: {:?}\n", res);
|
||||
@@ -641,19 +463,19 @@ mod tests {
|
||||
res,
|
||||
vec![
|
||||
Link {
|
||||
start_index: 41,
|
||||
end_index: 74,
|
||||
link_type: LinkType::Playground(PathBuf::from("file.rs"), vec!["editable"]),
|
||||
link_text: "{{#playground file.rs editable }}",
|
||||
start_index: 38,
|
||||
end_index: 68,
|
||||
link: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
|
||||
link_text: "{{#playpen file.rs editable }}",
|
||||
},
|
||||
Link {
|
||||
start_index: 95,
|
||||
end_index: 145,
|
||||
link_type: LinkType::Playground(
|
||||
start_index: 89,
|
||||
end_index: 136,
|
||||
link: LinkType::Playpen(
|
||||
PathBuf::from("my.rs"),
|
||||
vec!["editable", "no_run", "should_panic"],
|
||||
),
|
||||
link_text: "{{#playground my.rs editable no_run should_panic}}",
|
||||
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
||||
},
|
||||
]
|
||||
);
|
||||
@@ -661,9 +483,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_find_all_link_types() {
|
||||
let s =
|
||||
"Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
|
||||
insignifficant in escaped link}} some more\n text {{#playground my.rs editable \
|
||||
let s = "Some random text with escaped playpen {{#include file.rs}} and \\{{#contents are \
|
||||
insignifficant in escaped link}} some more\n text {{#playpen my.rs editable \
|
||||
no_run should_panic}} ...";
|
||||
|
||||
let res = find_links(s).collect::<Vec<_>>();
|
||||
@@ -672,215 +493,33 @@ mod tests {
|
||||
assert_eq!(
|
||||
res[0],
|
||||
Link {
|
||||
start_index: 41,
|
||||
end_index: 61,
|
||||
link_type: LinkType::Include(
|
||||
PathBuf::from("file.rs"),
|
||||
RangeOrAnchor::Range(LineRange::from(..))
|
||||
),
|
||||
start_index: 38,
|
||||
end_index: 58,
|
||||
link: LinkType::IncludeRangeFull(PathBuf::from("file.rs"), ..),
|
||||
link_text: "{{#include file.rs}}",
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
res[1],
|
||||
Link {
|
||||
start_index: 66,
|
||||
end_index: 115,
|
||||
link_type: LinkType::Escaped,
|
||||
start_index: 63,
|
||||
end_index: 112,
|
||||
link: LinkType::Escaped,
|
||||
link_text: "\\{{#contents are insignifficant in escaped link}}",
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
res[2],
|
||||
Link {
|
||||
start_index: 133,
|
||||
end_index: 183,
|
||||
link_type: LinkType::Playground(
|
||||
start_index: 130,
|
||||
end_index: 177,
|
||||
link: LinkType::Playpen(
|
||||
PathBuf::from("my.rs"),
|
||||
vec!["editable", "no_run", "should_panic"]
|
||||
),
|
||||
link_text: "{{#playground my.rs editable no_run should_panic}}",
|
||||
link_text: "{{#playpen my.rs editable no_run should_panic}}",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[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))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
//! Book preprocessing.
|
||||
|
||||
pub use self::cmd::CmdPreprocessor;
|
||||
pub use self::index::IndexPreprocessor;
|
||||
pub use self::links::LinkPreprocessor;
|
||||
|
||||
mod cmd;
|
||||
mod index;
|
||||
mod links;
|
||||
|
||||
use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use book::Book;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Extra information for a `Preprocessor` to give them more context when
|
||||
/// processing a book.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PreprocessorContext {
|
||||
/// The location of the book directory on disk.
|
||||
pub root: PathBuf,
|
||||
@@ -24,22 +21,12 @@ pub struct PreprocessorContext {
|
||||
pub config: Config,
|
||||
/// The `Renderer` this preprocessor is being used with.
|
||||
pub renderer: String,
|
||||
/// The calling `mdbook` version.
|
||||
pub mdbook_version: String,
|
||||
#[serde(skip)]
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
impl PreprocessorContext {
|
||||
/// Create a new `PreprocessorContext`.
|
||||
pub(crate) fn new(root: PathBuf, config: Config, renderer: String) -> Self {
|
||||
PreprocessorContext {
|
||||
root,
|
||||
config,
|
||||
renderer,
|
||||
mdbook_version: crate::MDBOOK_VERSION.to_string(),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
PreprocessorContext { root, config, renderer }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::config::{Config, HtmlConfig, Playground, RustEdition};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
use crate::theme::{self, playground_editor, Theme};
|
||||
use crate::utils;
|
||||
use book::{Book, BookItem};
|
||||
use config::{Config, HtmlConfig, Playpen};
|
||||
use errors::*;
|
||||
use renderer::html_handlebars::helpers;
|
||||
use renderer::{RenderContext, Renderer};
|
||||
use theme::{self, playpen_editor, Theme};
|
||||
use utils;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{self, File};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::utils::fs::get_404_output_file;
|
||||
use handlebars::Handlebars;
|
||||
use regex::{Captures, Regex};
|
||||
use serde_json;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HtmlHandlebars;
|
||||
@@ -27,148 +26,83 @@ impl HtmlHandlebars {
|
||||
fn render_item(
|
||||
&self,
|
||||
item: &BookItem,
|
||||
mut ctx: RenderItemContext<'_>,
|
||||
mut ctx: RenderItemContext,
|
||||
print_content: &mut String,
|
||||
) -> Result<()> {
|
||||
// FIXME: This should be made DRY-er and rely less on mutable state
|
||||
match *item {
|
||||
BookItem::Chapter(ref ch) => {
|
||||
let content = ch.content.clone();
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
print_content.push_str(&content);
|
||||
|
||||
let (ch, path) = match item {
|
||||
BookItem::Chapter(ch) if !ch.is_draft_chapter() => (ch, ch.path.as_ref().unwrap()),
|
||||
_ => return Ok(()),
|
||||
};
|
||||
// Update the context with data for this file
|
||||
let path = ch
|
||||
.path
|
||||
.to_str()
|
||||
.chain_err(|| "Could not convert path to str")?;
|
||||
let filepath = Path::new(&ch.path).with_extension("html");
|
||||
|
||||
let content = ch.content.clone();
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
// "print.html" is used for the print page.
|
||||
if ch.path == Path::new("print.md") {
|
||||
bail!(ErrorKind::ReservedFilenameError(ch.path.clone()));
|
||||
};
|
||||
|
||||
let fixed_content = utils::render_markdown_with_path(
|
||||
&ch.content,
|
||||
ctx.html_config.curly_quotes,
|
||||
Some(&path),
|
||||
);
|
||||
print_content.push_str(&fixed_content);
|
||||
// Non-lexical lifetimes needed :'(
|
||||
let title: String;
|
||||
{
|
||||
let book_title = ctx
|
||||
.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
title = ch.name.clone() + " - " + book_title;
|
||||
}
|
||||
|
||||
// Update the context with data for this file
|
||||
let ctx_path = path
|
||||
.to_str()
|
||||
.with_context(|| "Could not convert path to str")?;
|
||||
let filepath = Path::new(&ctx_path).with_extension("html");
|
||||
ctx.data.insert("path".to_owned(), json!(path));
|
||||
ctx.data.insert("content".to_owned(), json!(content));
|
||||
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
|
||||
ctx.data.insert("title".to_owned(), json!(title));
|
||||
ctx.data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&ch.path)),
|
||||
);
|
||||
|
||||
// "print.html" is used for the print page.
|
||||
if path == Path::new("print.md") {
|
||||
bail!("{} is reserved for internal use", path.display());
|
||||
};
|
||||
// Render the handlebars template with the data
|
||||
debug!("Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
|
||||
let book_title = ctx
|
||||
.data
|
||||
.get("book_title")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("");
|
||||
let rendered = self.post_process(rendered, &ctx.html_config.playpen);
|
||||
|
||||
let title = match book_title {
|
||||
"" => ch.name.clone(),
|
||||
_ => ch.name.clone() + " - " + book_title,
|
||||
};
|
||||
// Write to file
|
||||
debug!("Creating {}", filepath.display());
|
||||
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
|
||||
|
||||
ctx.data.insert("path".to_owned(), json!(path));
|
||||
ctx.data.insert("content".to_owned(), json!(content));
|
||||
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
|
||||
ctx.data.insert("title".to_owned(), json!(title));
|
||||
ctx.data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&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");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
|
||||
let rendered = self.post_process(rendered, &ctx.html_config.playground, ctx.edition);
|
||||
|
||||
// Write to file
|
||||
debug!("Creating {}", filepath.display());
|
||||
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
|
||||
|
||||
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.playground, ctx.edition);
|
||||
debug!("Creating index.html from {}", ctx_path);
|
||||
utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_404(
|
||||
&self,
|
||||
ctx: &RenderContext,
|
||||
html_config: &HtmlConfig,
|
||||
src_dir: &PathBuf,
|
||||
handlebars: &mut Handlebars<'_>,
|
||||
data: &mut serde_json::Map<String, serde_json::Value>,
|
||||
) -> Result<()> {
|
||||
let destination = &ctx.destination;
|
||||
let content_404 = if let Some(ref filename) = html_config.input_404 {
|
||||
let path = src_dir.join(filename);
|
||||
std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("unable to open 404 input file {:?}", path))?
|
||||
} else {
|
||||
// 404 input not explicitly configured try the default file 404.md
|
||||
let default_404_location = src_dir.join("404.md");
|
||||
if default_404_location.exists() {
|
||||
std::fs::read_to_string(&default_404_location).with_context(|| {
|
||||
format!("unable to open 404 input file {:?}", default_404_location)
|
||||
})?
|
||||
} else {
|
||||
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
|
||||
navigation bar or search to continue."
|
||||
.to_string()
|
||||
if ctx.is_index {
|
||||
ctx.data.insert("path".to_owned(), json!("index.html"));
|
||||
ctx.data.insert("path_to_root".to_owned(), json!(""));
|
||||
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
|
||||
let rendered_index =
|
||||
self.post_process(rendered_index, &ctx.html_config.playpen);
|
||||
debug!("Creating index.html from {}", path);
|
||||
utils::fs::write_file(
|
||||
&ctx.destination,
|
||||
"index.html",
|
||||
rendered_index.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
};
|
||||
let html_content_404 = utils::render_markdown(&content_404, html_config.curly_quotes);
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut data_404 = data.clone();
|
||||
let base_url = if let Some(site_url) = &html_config.site_url {
|
||||
site_url
|
||||
} else {
|
||||
debug!(
|
||||
"HTML 'site-url' parameter not set, defaulting to '/'. Please configure \
|
||||
this to ensure the 404 page work correctly, especially if your site is hosted in a \
|
||||
subdirectory on the HTTP server."
|
||||
);
|
||||
"/"
|
||||
};
|
||||
data_404.insert("base_url".to_owned(), json!(base_url));
|
||||
// Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
|
||||
data_404.insert("path".to_owned(), json!("404.md"));
|
||||
data_404.insert("content".to_owned(), json!(html_content_404));
|
||||
let rendered = handlebars.render("index", &data_404)?;
|
||||
|
||||
let rendered =
|
||||
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
|
||||
let output_file = get_404_output_file(&html_config.input_404);
|
||||
utils::fs::write_file(&destination, output_file, rendered.as_bytes())?;
|
||||
debug!("Creating 404.html ✓");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
|
||||
fn post_process(
|
||||
&self,
|
||||
rendered: String,
|
||||
playground_config: &Playground,
|
||||
edition: Option<RustEdition>,
|
||||
) -> String {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(let_and_return))]
|
||||
fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String {
|
||||
let rendered = build_header_links(&rendered);
|
||||
let rendered = fix_code_blocks(&rendered);
|
||||
let rendered = add_playground_pre(&rendered, playground_config, edition);
|
||||
let rendered = add_playpen_pre(&rendered, playpen_config);
|
||||
|
||||
rendered
|
||||
}
|
||||
@@ -179,29 +113,20 @@ impl HtmlHandlebars {
|
||||
theme: &Theme,
|
||||
html_config: &HtmlConfig,
|
||||
) -> Result<()> {
|
||||
use crate::utils::fs::write_file;
|
||||
use utils::fs::write_file;
|
||||
|
||||
write_file(
|
||||
destination,
|
||||
".nojekyll",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.\n",
|
||||
b"This file makes sure that Github Pages doesn't process mdBook's output.",
|
||||
)?;
|
||||
|
||||
if let Some(cname) = &html_config.cname {
|
||||
write_file(destination, "CNAME", format!("{}\n", cname).as_bytes())?;
|
||||
}
|
||||
|
||||
write_file(destination, "book.js", &theme.js)?;
|
||||
write_file(destination, "css/general.css", &theme.general_css)?;
|
||||
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
|
||||
write_file(destination, "css/print.css", &theme.print_css)?;
|
||||
write_file(destination, "css/variables.css", &theme.variables_css)?;
|
||||
if let Some(contents) = &theme.favicon_png {
|
||||
write_file(destination, "favicon.png", &contents)?;
|
||||
}
|
||||
if let Some(contents) = &theme.favicon_svg {
|
||||
write_file(destination, "favicon.svg", &contents)?;
|
||||
}
|
||||
write_file(destination, "favicon.png", &theme.favicon)?;
|
||||
write_file(destination, "highlight.css", &theme.highlight_css)?;
|
||||
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
|
||||
write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
|
||||
@@ -242,38 +167,20 @@ impl HtmlHandlebars {
|
||||
"FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
if html_config.copy_fonts {
|
||||
write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
|
||||
for (file_name, contents) in theme::fonts::LICENSES.iter() {
|
||||
write_file(destination, file_name, contents)?;
|
||||
}
|
||||
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
|
||||
write_file(destination, file_name, contents)?;
|
||||
}
|
||||
write_file(
|
||||
destination,
|
||||
theme::fonts::SOURCE_CODE_PRO.0,
|
||||
theme::fonts::SOURCE_CODE_PRO.1,
|
||||
)?;
|
||||
}
|
||||
|
||||
let playground_config = &html_config.playground;
|
||||
let playpen_config = &html_config.playpen;
|
||||
|
||||
// Ace is a very large dependency, so only load it when requested
|
||||
if playground_config.editable && playground_config.copy_js {
|
||||
if playpen_config.editable && playpen_config.copy_js {
|
||||
// Load the editor
|
||||
write_file(destination, "editor.js", playground_editor::JS)?;
|
||||
write_file(destination, "ace.js", playground_editor::ACE_JS)?;
|
||||
write_file(destination, "mode-rust.js", playground_editor::MODE_RUST_JS)?;
|
||||
write_file(
|
||||
destination,
|
||||
"theme-dawn.js",
|
||||
playground_editor::THEME_DAWN_JS,
|
||||
)?;
|
||||
write_file(destination, "editor.js", playpen_editor::JS)?;
|
||||
write_file(destination, "ace.js", playpen_editor::ACE_JS)?;
|
||||
write_file(destination, "mode-rust.js", playpen_editor::MODE_RUST_JS)?;
|
||||
write_file(destination, "theme-dawn.js", playpen_editor::THEME_DAWN_JS)?;
|
||||
write_file(
|
||||
destination,
|
||||
"theme-tomorrow_night.js",
|
||||
playground_editor::THEME_TOMORROW_NIGHT_JS,
|
||||
playpen_editor::THEME_TOMORROW_NIGHT_JS,
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -298,7 +205,7 @@ impl HtmlHandlebars {
|
||||
);
|
||||
}
|
||||
|
||||
fn register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig) {
|
||||
fn register_hbs_helpers(&self, handlebars: &mut Handlebars, html_config: &HtmlConfig) {
|
||||
handlebars.register_helper(
|
||||
"toc",
|
||||
Box::new(helpers::toc::RenderToc {
|
||||
@@ -307,7 +214,6 @@ impl HtmlHandlebars {
|
||||
);
|
||||
handlebars.register_helper("previous", Box::new(helpers::navigation::previous));
|
||||
handlebars.register_helper("next", Box::new(helpers::navigation::next));
|
||||
handlebars.register_helper("theme_option", Box::new(helpers::theme::theme_option));
|
||||
}
|
||||
|
||||
/// Copy across any additional CSS and JavaScript files which the book
|
||||
@@ -327,7 +233,7 @@ impl HtmlHandlebars {
|
||||
let output_location = destination.join(custom_file);
|
||||
if let Some(parent) = output_location.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Unable to create {}", parent.display()))?;
|
||||
.chain_err(|| format!("Unable to create {}", parent.display()))?;
|
||||
}
|
||||
debug!(
|
||||
"Copying {} -> {}",
|
||||
@@ -335,7 +241,7 @@ impl HtmlHandlebars {
|
||||
output_location.display()
|
||||
);
|
||||
|
||||
fs::copy(&input_location, &output_location).with_context(|| {
|
||||
fs::copy(&input_location, &output_location).chain_err(|| {
|
||||
format!(
|
||||
"Unable to copy {} to {}",
|
||||
input_location.display(),
|
||||
@@ -346,68 +252,6 @@ impl HtmlHandlebars {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_redirects(
|
||||
&self,
|
||||
root: &Path,
|
||||
handlebars: &Handlebars<'_>,
|
||||
redirects: &HashMap<String, String>,
|
||||
) -> Result<()> {
|
||||
if redirects.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::debug!("Emitting redirects");
|
||||
|
||||
for (original, new) in redirects {
|
||||
log::debug!("Redirecting \"{}\" → \"{}\"", original, new);
|
||||
// Note: all paths are relative to the build directory, so the
|
||||
// leading slash in an absolute path means nothing (and would mess
|
||||
// up `root.join(original)`).
|
||||
let original = original.trim_start_matches("/");
|
||||
let filename = root.join(original);
|
||||
self.emit_redirect(handlebars, &filename, new)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn emit_redirect(
|
||||
&self,
|
||||
handlebars: &Handlebars<'_>,
|
||||
original: &Path,
|
||||
destination: &str,
|
||||
) -> Result<()> {
|
||||
if original.exists() {
|
||||
// sanity check to avoid accidentally overwriting a real file.
|
||||
let msg = format!(
|
||||
"Not redirecting \"{}\" to \"{}\" because it already exists. Are you sure it needs to be redirected?",
|
||||
original.display(),
|
||||
destination,
|
||||
);
|
||||
return Err(Error::msg(msg));
|
||||
}
|
||||
|
||||
if let Some(parent) = original.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?;
|
||||
}
|
||||
|
||||
let ctx = json!({
|
||||
"url": destination,
|
||||
});
|
||||
let f = File::create(original)?;
|
||||
handlebars
|
||||
.render_to_write("redirect", &ctx, f)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Unable to create a redirect file at \"{}\"",
|
||||
original.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mattico): Remove some time after the 0.1.8 release
|
||||
@@ -439,12 +283,6 @@ impl Renderer for HtmlHandlebars {
|
||||
let src_dir = ctx.root.join(&ctx.config.book.src);
|
||||
let destination = &ctx.destination;
|
||||
let book = &ctx.book;
|
||||
let build_dir = ctx.root.join(&ctx.config.build.build_dir);
|
||||
|
||||
if destination.exists() {
|
||||
utils::fs::remove_dir_content(destination)
|
||||
.with_context(|| "Unable to remove stale HTML output")?;
|
||||
}
|
||||
|
||||
trace!("render");
|
||||
let mut handlebars = Handlebars::new();
|
||||
@@ -469,26 +307,19 @@ impl Renderer for HtmlHandlebars {
|
||||
debug!("Register the index handlebars template");
|
||||
handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
|
||||
|
||||
debug!("Register the head handlebars template");
|
||||
handlebars.register_partial("head", String::from_utf8(theme.head.clone())?)?;
|
||||
|
||||
debug!("Register the redirect handlebars template");
|
||||
handlebars
|
||||
.register_template_string("redirect", String::from_utf8(theme.redirect.clone())?)?;
|
||||
|
||||
debug!("Register the header handlebars template");
|
||||
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
|
||||
|
||||
debug!("Register handlebars helpers");
|
||||
self.register_hbs_helpers(&mut handlebars, &html_config);
|
||||
|
||||
let mut data = make_data(&ctx.root, &book, &ctx.config, &html_config, &theme)?;
|
||||
let mut data = make_data(&ctx.root, &book, &ctx.config, &html_config)?;
|
||||
|
||||
// Print version
|
||||
let mut print_content = String::new();
|
||||
|
||||
fs::create_dir_all(&destination)
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
.chain_err(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
let mut is_index = true;
|
||||
for item in book.iter() {
|
||||
@@ -496,19 +327,13 @@ impl Renderer for HtmlHandlebars {
|
||||
handlebars: &handlebars,
|
||||
destination: destination.to_path_buf(),
|
||||
data: data.clone(),
|
||||
is_index,
|
||||
is_index: is_index,
|
||||
html_config: html_config.clone(),
|
||||
edition: ctx.config.rust.edition,
|
||||
};
|
||||
self.render_item(item, ctx, &mut print_content)?;
|
||||
is_index = false;
|
||||
}
|
||||
|
||||
// Render 404 page
|
||||
if html_config.input_404 != Some("".to_string()) {
|
||||
self.render_404(ctx, &html_config, &src_dir, &mut handlebars, &mut data)?;
|
||||
}
|
||||
|
||||
// Print version
|
||||
self.configure_print_version(&mut data, &print_content);
|
||||
if let Some(ref title) = ctx.config.book.title {
|
||||
@@ -519,17 +344,16 @@ impl Renderer for HtmlHandlebars {
|
||||
debug!("Render template");
|
||||
let rendered = handlebars.render("index", &data)?;
|
||||
|
||||
let rendered =
|
||||
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
|
||||
let rendered = self.post_process(rendered, &html_config.playpen);
|
||||
|
||||
utils::fs::write_file(&destination, "print.html", rendered.as_bytes())?;
|
||||
debug!("Creating print.html ✓");
|
||||
|
||||
debug!("Copy static files");
|
||||
self.copy_static_files(&destination, &theme, &html_config)
|
||||
.with_context(|| "Unable to copy across static files")?;
|
||||
.chain_err(|| "Unable to copy across static files")?;
|
||||
self.copy_additional_css_and_js(&html_config, &ctx.root, &destination)
|
||||
.with_context(|| "Unable to copy across additional CSS and JS")?;
|
||||
.chain_err(|| "Unable to copy across additional CSS and JS")?;
|
||||
|
||||
// Render search index
|
||||
#[cfg(feature = "search")]
|
||||
@@ -540,11 +364,8 @@ impl Renderer for HtmlHandlebars {
|
||||
}
|
||||
}
|
||||
|
||||
self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
|
||||
.context("Unable to emit redirects")?;
|
||||
|
||||
// Copy all remaining files, avoid a recursive copy from/to the book build dir
|
||||
utils::fs::copy_files_except_ext(&src_dir, &destination, true, Some(&build_dir), &["md"])?;
|
||||
// Copy all remaining files
|
||||
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -555,15 +376,12 @@ fn make_data(
|
||||
book: &Book,
|
||||
config: &Config,
|
||||
html_config: &HtmlConfig,
|
||||
theme: &Theme,
|
||||
) -> 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!(config.book.language.clone().unwrap_or_default()),
|
||||
);
|
||||
data.insert("language".to_owned(), json!("en"));
|
||||
data.insert(
|
||||
"book_title".to_owned(),
|
||||
json!(config.book.title.clone().unwrap_or_default()),
|
||||
@@ -572,48 +390,24 @@ fn make_data(
|
||||
"description".to_owned(),
|
||||
json!(config.book.description.clone().unwrap_or_default()),
|
||||
);
|
||||
if theme.favicon_png.is_some() {
|
||||
data.insert("favicon_png".to_owned(), json!("favicon.png"));
|
||||
}
|
||||
if theme.favicon_svg.is_some() {
|
||||
data.insert("favicon_svg".to_owned(), json!("favicon.svg"));
|
||||
}
|
||||
data.insert("favicon".to_owned(), json!("favicon.png"));
|
||||
if let Some(ref livereload) = html_config.livereload_url {
|
||||
data.insert("livereload".to_owned(), json!(livereload));
|
||||
}
|
||||
|
||||
let default_theme = match html_config.default_theme {
|
||||
Some(ref theme) => theme.to_lowercase(),
|
||||
None => "light".to_string(),
|
||||
};
|
||||
data.insert("default_theme".to_owned(), json!(default_theme));
|
||||
|
||||
let preferred_dark_theme = match html_config.preferred_dark_theme {
|
||||
Some(ref theme) => theme.to_lowercase(),
|
||||
None => "navy".to_string(),
|
||||
};
|
||||
data.insert(
|
||||
"preferred_dark_theme".to_owned(),
|
||||
json!(preferred_dark_theme),
|
||||
);
|
||||
|
||||
// Add google analytics tag
|
||||
if let Some(ref ga) = html_config.google_analytics {
|
||||
if let Some(ref ga) = config.html_config().and_then(|html| html.google_analytics) {
|
||||
data.insert("google_analytics".to_owned(), json!(ga));
|
||||
}
|
||||
|
||||
if html_config.mathjax_support {
|
||||
if html.mathjax_support {
|
||||
data.insert("mathjax_support".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
if html_config.copy_fonts {
|
||||
data.insert("copy_fonts".to_owned(), json!(true));
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional style
|
||||
if !html_config.additional_css.is_empty() {
|
||||
if !html.additional_css.is_empty() {
|
||||
let mut css = Vec::new();
|
||||
for style in &html_config.additional_css {
|
||||
for style in &html.additional_css {
|
||||
match style.strip_prefix(root) {
|
||||
Ok(p) => css.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => css.push(style.to_str().expect("Could not convert to str")),
|
||||
@@ -623,29 +417,26 @@ fn make_data(
|
||||
}
|
||||
|
||||
// Add check to see if there is an additional script
|
||||
if !html_config.additional_js.is_empty() {
|
||||
if !html.additional_js.is_empty() {
|
||||
let mut js = Vec::new();
|
||||
for script in &html_config.additional_js {
|
||||
for script in &html.additional_js {
|
||||
match script.strip_prefix(root) {
|
||||
Ok(p) => js.push(p.to_str().expect("Could not convert to str")),
|
||||
Err(_) => js.push(script.to_str().expect("Could not convert to str")),
|
||||
Err(_) => js.push(
|
||||
script
|
||||
.file_name()
|
||||
.expect("File has a file name")
|
||||
.to_str()
|
||||
.expect("Could not convert to str"),
|
||||
),
|
||||
}
|
||||
}
|
||||
data.insert("additional_js".to_owned(), json!(js));
|
||||
}
|
||||
|
||||
if html_config.playground.editable && html_config.playground.copy_js {
|
||||
data.insert("playground_js".to_owned(), json!(true));
|
||||
if html_config.playground.line_numbers {
|
||||
data.insert("playground_line_numbers".to_owned(), json!(true));
|
||||
}
|
||||
if html.playpen.editable && html.playpen.copy_js {
|
||||
data.insert("playpen_js".to_owned(), json!(true));
|
||||
}
|
||||
if html_config.playground.copyable {
|
||||
data.insert("playground_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") {
|
||||
@@ -663,16 +454,6 @@ 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",
|
||||
};
|
||||
data.insert("git_repository_icon".to_owned(), json!(git_repository_icon));
|
||||
|
||||
let mut chapters = vec![];
|
||||
|
||||
for item in book.iter() {
|
||||
@@ -680,26 +461,17 @@ fn make_data(
|
||||
let mut chapter = BTreeMap::new();
|
||||
|
||||
match *item {
|
||||
BookItem::PartTitle(ref title) => {
|
||||
chapter.insert("part".to_owned(), json!(title));
|
||||
}
|
||||
BookItem::Chapter(ref ch) => {
|
||||
if let Some(ref section) = ch.number {
|
||||
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));
|
||||
if let Some(ref path) = ch.path {
|
||||
let p = path
|
||||
.to_str()
|
||||
.with_context(|| "Could not convert path to str")?;
|
||||
chapter.insert("path".to_owned(), json!(p));
|
||||
}
|
||||
let path = ch
|
||||
.path
|
||||
.to_str()
|
||||
.chain_err(|| "Could not convert path to str")?;
|
||||
chapter.insert("path".to_owned(), json!(path));
|
||||
}
|
||||
BookItem::Separator => {
|
||||
chapter.insert("spacer".to_owned(), json!("_spacer_"));
|
||||
@@ -715,26 +487,25 @@ fn make_data(
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Goes through the rendered HTML, making sure all header tags have
|
||||
/// an anchor respectively so people can link to sections directly.
|
||||
/// Goes through the rendered HTML, making sure all header tags are wrapped in
|
||||
/// an anchor so people can link to sections directly.
|
||||
fn build_header_links(html: &str) -> String {
|
||||
let regex = Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap();
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let level = caps[1]
|
||||
.parse()
|
||||
.expect("Regex should ensure we only ever get numbers here");
|
||||
|
||||
insert_link_into_header(level, &caps[2], &mut id_counter)
|
||||
})
|
||||
.into_owned()
|
||||
wrap_header_with_link(level, &caps[2], &mut id_counter)
|
||||
}).into_owned()
|
||||
}
|
||||
|
||||
/// Insert a sinle link into a header, making sure each link gets its own
|
||||
/// Wraps a single header tag with a link, making sure each tag gets its own
|
||||
/// unique ID by appending an auto-incremented number (if necessary).
|
||||
fn insert_link_into_header(
|
||||
fn wrap_header_with_link(
|
||||
level: usize,
|
||||
content: &str,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
@@ -751,7 +522,7 @@ fn insert_link_into_header(
|
||||
*id_count += 1;
|
||||
|
||||
format!(
|
||||
r##"<h{level}><a class="header" href="#{id}" id="{id}">{text}</a></h{level}>"##,
|
||||
r##"<a class="header" href="#{id}" id="{id}"><h{level}>{text}</h{level}></a>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content
|
||||
@@ -769,7 +540,7 @@ fn insert_link_into_header(
|
||||
fn fix_code_blocks(html: &str) -> String {
|
||||
let regex = Regex::new(r##"<code([^>]+)class="([^"]+)"([^>]*)>"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let before = &caps[1];
|
||||
let classes = &caps[2].replace(",", " ");
|
||||
let after = &caps[3];
|
||||
@@ -780,107 +551,43 @@ fn fix_code_blocks(html: &str) -> String {
|
||||
classes = classes,
|
||||
after = after
|
||||
)
|
||||
})
|
||||
.into_owned()
|
||||
}).into_owned()
|
||||
}
|
||||
|
||||
fn add_playground_pre(
|
||||
html: &str,
|
||||
playground_config: &Playground,
|
||||
edition: Option<RustEdition>,
|
||||
) -> String {
|
||||
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
|
||||
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
|
||||
regex
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
.replace_all(html, |caps: &Captures| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
|
||||
if classes.contains("language-rust") {
|
||||
if (!classes.contains("ignore")
|
||||
&& !classes.contains("noplayground")
|
||||
&& !classes.contains("noplaypen"))
|
||||
|| classes.contains("mdbook-runnable")
|
||||
if (classes.contains("language-rust")
|
||||
&& !classes.contains("ignore")
|
||||
&& !classes.contains("noplaypen"))
|
||||
|| 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!")
|
||||
{
|
||||
let contains_e2015 = classes.contains("edition2015");
|
||||
let contains_e2018 = classes.contains("edition2018");
|
||||
let edition_class = if contains_e2015 || contains_e2018 {
|
||||
// the user forced edition, we should not overwrite it
|
||||
""
|
||||
} else {
|
||||
match edition {
|
||||
Some(RustEdition::E2015) => " edition2015",
|
||||
Some(RustEdition::E2018) => " edition2018",
|
||||
None => "",
|
||||
}
|
||||
};
|
||||
|
||||
// wrap the contents in an external pre block
|
||||
format!(
|
||||
"<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
|
||||
classes,
|
||||
edition_class,
|
||||
{
|
||||
let content: Cow<'_, str> = if playground_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!(
|
||||
"\n# #![allow(unused)]\n{}#fn main() {{\n{}#}}",
|
||||
attrs, code
|
||||
)
|
||||
.into()
|
||||
};
|
||||
hide_lines(&content)
|
||||
}
|
||||
)
|
||||
format!("<pre class=\"playpen\">{}</pre>", text)
|
||||
} else {
|
||||
format!("<code class=\"{}\">{}</code>", classes, hide_lines(code))
|
||||
// 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
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// not language-rust, so no-op
|
||||
text.to_owned()
|
||||
}
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap();
|
||||
}
|
||||
|
||||
fn hide_lines(content: &str) -> String {
|
||||
let mut result = String::with_capacity(content.len());
|
||||
for line in content.lines() {
|
||||
if let Some(caps) = BORING_LINES_REGEX.captures(line) {
|
||||
if &caps[2] == "#" {
|
||||
result += &caps[1];
|
||||
result += &caps[2];
|
||||
result += &caps[3];
|
||||
result += "\n";
|
||||
continue;
|
||||
} else if &caps[2] != "!" && &caps[2] != "[" {
|
||||
result += "<span class=\"boring\">";
|
||||
result += &caps[1];
|
||||
if &caps[2] != " " {
|
||||
result += &caps[2];
|
||||
}
|
||||
result += &caps[3];
|
||||
result += "\n";
|
||||
result += "</span>";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result += line;
|
||||
result += "\n";
|
||||
}
|
||||
result
|
||||
}).into_owned()
|
||||
}
|
||||
|
||||
fn partition_source(s: &str) -> (String, String) {
|
||||
@@ -890,7 +597,7 @@ fn partition_source(s: &str) -> (String, String) {
|
||||
|
||||
for line in s.lines() {
|
||||
let trimline = line.trim();
|
||||
let header = trimline.chars().all(char::is_whitespace) || trimline.starts_with("#![");
|
||||
let header = trimline.chars().all(|c| c.is_whitespace()) || trimline.starts_with("#![");
|
||||
if !header || after_header {
|
||||
after_header = true;
|
||||
after.push_str(line);
|
||||
@@ -905,12 +612,11 @@ fn partition_source(s: &str) -> (String, String) {
|
||||
}
|
||||
|
||||
struct RenderItemContext<'a> {
|
||||
handlebars: &'a Handlebars<'a>,
|
||||
handlebars: &'a Handlebars,
|
||||
destination: PathBuf,
|
||||
data: serde_json::Map<String, serde_json::Value>,
|
||||
is_index: bool,
|
||||
html_config: HtmlConfig,
|
||||
edition: Option<RustEdition>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -922,27 +628,27 @@ mod tests {
|
||||
let inputs = vec![
|
||||
(
|
||||
"blah blah <h1>Foo</h1>",
|
||||
r##"blah blah <h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
|
||||
r##"blah blah <a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1>",
|
||||
r##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1>"##,
|
||||
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a>"##,
|
||||
),
|
||||
(
|
||||
"<h3>Foo^bar</h3>",
|
||||
r##"<h3><a class="header" href="#foobar" id="foobar">Foo^bar</a></h3>"##,
|
||||
r##"<a class="header" href="#foobar" id="foobar"><h3>Foo^bar</h3></a>"##,
|
||||
),
|
||||
(
|
||||
"<h4></h4>",
|
||||
r##"<h4><a class="header" href="#" id=""></a></h4>"##,
|
||||
r##"<a class="header" href="#" id=""><h4></h4></a>"##,
|
||||
),
|
||||
(
|
||||
"<h4><em>Hï</em></h4>",
|
||||
r##"<h4><a class="header" href="#hï" id="hï"><em>Hï</em></a></h4>"##,
|
||||
r##"<a class="header" href="#hï" id="hï"><h4><em>Hï</em></h4></a>"##,
|
||||
),
|
||||
(
|
||||
"<h1>Foo</h1><h3>Foo</h3>",
|
||||
r##"<h1><a class="header" href="#foo" id="foo">Foo</a></h1><h3><a class="header" href="#foo-1" id="foo-1">Foo</a></h3>"##,
|
||||
r##"<a class="header" href="#foo" id="foo"><h1>Foo</h1></a><a class="header" href="#foo-1" id="foo-1"><h3>Foo</h3></a>"##,
|
||||
),
|
||||
];
|
||||
|
||||
@@ -951,83 +657,4 @@ mod tests {
|
||||
assert_eq!(got, should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_playground() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
|
||||
"<pre class=\"playground\"><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=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";\n</code></pre>"),
|
||||
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";\n</code></pre>"),
|
||||
("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
|
||||
"<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code>"),
|
||||
("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]\n</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
src,
|
||||
&Playground {
|
||||
editable: true,
|
||||
..Playground::default()
|
||||
},
|
||||
None,
|
||||
);
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn add_playground_edition2015() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
src,
|
||||
&Playground {
|
||||
editable: true,
|
||||
..Playground::default()
|
||||
},
|
||||
Some(RustEdition::E2015),
|
||||
);
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn add_playground_edition2018() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
|
||||
("<code class=\"language-rust edition2018\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = add_playground_pre(
|
||||
src,
|
||||
&Playground {
|
||||
editable: true,
|
||||
..Playground::default()
|
||||
},
|
||||
Some(RustEdition::E2018),
|
||||
);
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
pub mod navigation;
|
||||
pub mod theme;
|
||||
pub mod toc;
|
||||
|
||||
@@ -2,8 +2,9 @@ use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
|
||||
use serde_json;
|
||||
|
||||
use crate::utils;
|
||||
use utils;
|
||||
|
||||
type StringMap = BTreeMap<String, String>;
|
||||
|
||||
@@ -17,13 +18,13 @@ impl Target {
|
||||
/// Returns target if found.
|
||||
fn find(
|
||||
&self,
|
||||
base_path: &str,
|
||||
current_path: &str,
|
||||
base_path: &String,
|
||||
current_path: &String,
|
||||
current_item: &StringMap,
|
||||
previous_item: &StringMap,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
match *self {
|
||||
Target::Next => {
|
||||
match self {
|
||||
&Target::Next => {
|
||||
let previous_path = previous_item
|
||||
.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
|
||||
@@ -33,7 +34,7 @@ impl Target {
|
||||
}
|
||||
}
|
||||
|
||||
Target::Previous => {
|
||||
&Target::Previous => {
|
||||
if current_path == base_path {
|
||||
return Ok(Some(previous_item.clone()));
|
||||
}
|
||||
@@ -46,43 +47,22 @@ impl Target {
|
||||
|
||||
fn find_chapter(
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
rc: &mut RenderContext,
|
||||
target: Target,
|
||||
) -> Result<Option<StringMap>, RenderError> {
|
||||
debug!("Get data from context");
|
||||
|
||||
let chapters = rc.evaluate(ctx, "@root/chapters").and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone())
|
||||
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<StringMap>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
|
||||
let base_path = rc
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.evaluate_absolute(ctx, "path", true)?
|
||||
.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()
|
||||
.filter(|chapter| {
|
||||
// Skip things like "spacer"
|
||||
chapter.contains_key("path")
|
||||
})
|
||||
.nth(1)
|
||||
{
|
||||
Some(chapter) => return Ok(Some(chapter.clone())),
|
||||
None => return Ok(None),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let mut previous: Option<StringMap> = None;
|
||||
|
||||
debug!("Search for chapter");
|
||||
@@ -106,19 +86,18 @@ fn find_chapter(
|
||||
}
|
||||
|
||||
fn render(
|
||||
_h: &Helper<'_, '_>,
|
||||
r: &Handlebars<'_>,
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
out: &mut dyn Output,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
chapter: &StringMap,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("Creating BTreeMap to inject in context");
|
||||
|
||||
let mut context = BTreeMap::new();
|
||||
let base_path = rc
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.evaluate_absolute(ctx, "path", false)?
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.replace("\"", "");
|
||||
@@ -149,7 +128,7 @@ fn render(
|
||||
_h.template()
|
||||
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
|
||||
.and_then(|t| {
|
||||
let mut local_rc = rc.clone();
|
||||
let mut local_rc = rc.new_for_block();
|
||||
let local_ctx = Context::wraps(&context)?;
|
||||
t.render(r, &local_ctx, &mut local_rc, out)
|
||||
})?;
|
||||
@@ -158,11 +137,11 @@ fn render(
|
||||
}
|
||||
|
||||
pub fn previous(
|
||||
_h: &Helper<'_, '_>,
|
||||
r: &Handlebars<'_>,
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
out: &mut dyn Output,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("previous (handlebars helper)");
|
||||
|
||||
@@ -174,11 +153,11 @@ pub fn previous(
|
||||
}
|
||||
|
||||
pub fn next(
|
||||
_h: &Helper<'_, '_>,
|
||||
r: &Handlebars<'_>,
|
||||
_h: &Helper,
|
||||
r: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
out: &mut dyn Output,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("next (handlebars helper)");
|
||||
|
||||
@@ -193,29 +172,29 @@ pub fn next(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
static TEMPLATE: &str =
|
||||
static TEMPLATE: &'static str =
|
||||
"{{#previous}}{{title}}: {{link}}{{/previous}}|{{#next}}{{title}}: {{link}}{{/next}}";
|
||||
|
||||
#[test]
|
||||
fn test_next_previous() {
|
||||
let data = json!({
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
@@ -230,23 +209,23 @@ mod tests {
|
||||
#[test]
|
||||
fn test_first() {
|
||||
let data = json!({
|
||||
"name": "one",
|
||||
"path": "one.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
"name": "one",
|
||||
"path": "one.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
@@ -260,23 +239,23 @@ mod tests {
|
||||
#[test]
|
||||
fn test_last() {
|
||||
let data = json!({
|
||||
"name": "three",
|
||||
"path": "three.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
"name": "three",
|
||||
"path": "three.path",
|
||||
"chapters": [
|
||||
{
|
||||
"name": "one",
|
||||
"path": "one.path"
|
||||
},
|
||||
{
|
||||
"name": "two",
|
||||
"path": "two.path",
|
||||
},
|
||||
{
|
||||
"name": "three",
|
||||
"path": "three.path"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let mut h = Handlebars::new();
|
||||
h.register_helper("previous", Box::new(previous));
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
|
||||
|
||||
pub fn theme_option(
|
||||
h: &Helper<'_, '_>,
|
||||
_r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
out: &mut dyn Output,
|
||||
) -> Result<(), RenderError> {
|
||||
trace!("theme_option (handlebars helper)");
|
||||
|
||||
let param = h.param(0).and_then(|v| v.value().as_str()).ok_or_else(|| {
|
||||
RenderError::new("Param 0 with String type is required for theme_option helper.")
|
||||
})?;
|
||||
|
||||
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() == default_theme_name.to_lowercase() {
|
||||
out.write(" (default)")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::utils;
|
||||
use utils;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
|
||||
use pulldown_cmark::{html, Event, Parser};
|
||||
use pulldown_cmark::{html, Event, Parser, Tag};
|
||||
use serde_json;
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -15,45 +16,25 @@ pub struct RenderToc {
|
||||
impl HelperDef for RenderToc {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
_h: &Helper<'reg, 'rc>,
|
||||
_r: &'reg Handlebars<'_>,
|
||||
ctx: &'rc Context,
|
||||
rc: &mut RenderContext<'reg, 'rc>,
|
||||
out: &mut dyn Output,
|
||||
_h: &Helper,
|
||||
_: &Handlebars,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext,
|
||||
out: &mut Output,
|
||||
) -> Result<(), RenderError> {
|
||||
// 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(ctx, "@root/chapters").and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.as_json().clone())
|
||||
let chapters = rc.evaluate_absolute(ctx, "chapters", true).and_then(|c| {
|
||||
serde_json::value::from_value::<Vec<BTreeMap<String, String>>>(c.clone())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
})?;
|
||||
let current_path = rc
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
let current = rc
|
||||
.evaluate_absolute(ctx, "path", true)?
|
||||
.as_str()
|
||||
.ok_or_else(|| 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_else(|| RenderError::new("Type error for `fold_enable`, bool expected"))?;
|
||||
|
||||
let fold_level = rc
|
||||
.evaluate(ctx, "@root/fold_level")?
|
||||
.as_json()
|
||||
.as_u64()
|
||||
.ok_or_else(|| RenderError::new("Type error for `fold_level`, u64 expected"))?;
|
||||
|
||||
out.write("<ol class=\"chapter\">")?;
|
||||
|
||||
let mut current_level = 1;
|
||||
@@ -65,75 +46,61 @@ impl HelperDef for RenderToc {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (section, level) = if let Some(s) = item.get("section") {
|
||||
(s.as_str(), s.matches('.').count())
|
||||
let level = if let Some(s) = item.get("section") {
|
||||
s.matches('.').count()
|
||||
} else {
|
||||
("", 1)
|
||||
1
|
||||
};
|
||||
|
||||
let is_expanded =
|
||||
if !fold_enable || (!section.is_empty() && current_section.starts_with(section)) {
|
||||
// Expand if folding is disabled, or if the section is an
|
||||
// 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 {
|
||||
while level > current_level {
|
||||
out.write("<li>")?;
|
||||
out.write("<ol class=\"section\">")?;
|
||||
current_level += 1;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
out.write("<li>")?;
|
||||
} else if level < current_level {
|
||||
while level < current_level {
|
||||
out.write("</ol>")?;
|
||||
out.write("</li>")?;
|
||||
current_level -= 1;
|
||||
}
|
||||
write_li_open_tag(out, is_expanded, false)?;
|
||||
out.write("<li>")?;
|
||||
} else {
|
||||
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
|
||||
}
|
||||
|
||||
// Part title
|
||||
if let Some(title) = item.get("part") {
|
||||
out.write("<li class=\"part-title\">")?;
|
||||
out.write(title)?;
|
||||
out.write("</li>")?;
|
||||
continue;
|
||||
out.write("<li")?;
|
||||
if item.get("section").is_none() {
|
||||
out.write(" class=\"affix\"")?;
|
||||
}
|
||||
out.write(">")?;
|
||||
}
|
||||
|
||||
// Link
|
||||
let path_exists = if let Some(path) =
|
||||
item.get("path")
|
||||
.and_then(|p| if p.is_empty() { None } else { Some(p) })
|
||||
{
|
||||
out.write("<a href=\"")?;
|
||||
let path_exists = if let Some(path) = item.get("path") {
|
||||
if !path.is_empty() {
|
||||
out.write("<a href=\"")?;
|
||||
|
||||
let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)"))
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
.replace("\\", "/");
|
||||
let tmp = Path::new(item.get("path").expect("Error: path should be Some(_)"))
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
// Hack for windows who tends to use `\` as separator instead of `/`
|
||||
.replace("\\", "/");
|
||||
|
||||
// Add link
|
||||
out.write(&utils::fs::path_to_root(¤t_path))?;
|
||||
out.write(&tmp)?;
|
||||
out.write("\"")?;
|
||||
// Add link
|
||||
out.write(&utils::fs::path_to_root(¤t))?;
|
||||
out.write(&tmp)?;
|
||||
out.write("\"")?;
|
||||
|
||||
if path == ¤t_path {
|
||||
out.write(" class=\"active\"")?;
|
||||
if path == ¤t {
|
||||
out.write(" class=\"active\"")?;
|
||||
}
|
||||
|
||||
out.write(">")?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
out.write(">")?;
|
||||
true
|
||||
} else {
|
||||
out.write("<div>")?;
|
||||
false
|
||||
};
|
||||
|
||||
@@ -151,7 +118,10 @@ impl HelperDef for RenderToc {
|
||||
|
||||
// filter all events that are not inline code blocks
|
||||
let parser = Parser::new(name).filter(|event| match *event {
|
||||
Event::Code(_) | Event::Html(_) | Event::Text(_) => true,
|
||||
Event::Start(Tag::Code)
|
||||
| Event::End(Tag::Code)
|
||||
| Event::InlineHtml(_)
|
||||
| Event::Text(_) => true,
|
||||
_ => false,
|
||||
});
|
||||
|
||||
@@ -165,17 +135,8 @@ impl HelperDef for RenderToc {
|
||||
|
||||
if path_exists {
|
||||
out.write("</a>")?;
|
||||
} else {
|
||||
out.write("</div>")?;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -188,19 +149,3 @@ 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=\"chapter-item ");
|
||||
if is_expanded {
|
||||
li.push_str("expanded ");
|
||||
}
|
||||
if is_affix {
|
||||
li.push_str("affix ");
|
||||
}
|
||||
li.push_str("\">");
|
||||
out.write(&li)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
extern crate ammonia;
|
||||
extern crate elasticlunr;
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
|
||||
use elasticlunr::Index;
|
||||
use self::elasticlunr::Index;
|
||||
use pulldown_cmark::*;
|
||||
use serde_json;
|
||||
|
||||
use crate::book::{Book, BookItem};
|
||||
use crate::config::Search;
|
||||
use crate::errors::*;
|
||||
use crate::theme::searcher;
|
||||
use crate::utils;
|
||||
use book::{Book, BookItem};
|
||||
use config::Search;
|
||||
use errors::*;
|
||||
use theme::searcher;
|
||||
use utils;
|
||||
|
||||
/// Creates all files required for search.
|
||||
pub fn create_files(search_config: &Search, destination: &Path, book: &Book) -> Result<()> {
|
||||
@@ -31,7 +35,7 @@ pub fn create_files(search_config: &Search, destination: &Path, book: &Book) ->
|
||||
utils::fs::write_file(
|
||||
destination,
|
||||
"searchindex.js",
|
||||
format!("Object.assign(window.search, {});", index).as_bytes(),
|
||||
format!("window.search = {};", index).as_bytes(),
|
||||
)?;
|
||||
utils::fs::write_file(destination, "searcher.js", searcher::JS)?;
|
||||
utils::fs::write_file(destination, "mark.min.js", searcher::MARK_JS)?;
|
||||
@@ -50,7 +54,7 @@ fn add_doc(
|
||||
section_id: &Option<String>,
|
||||
items: &[&str],
|
||||
) {
|
||||
let url = if let Some(ref id) = *section_id {
|
||||
let url = if let &Some(ref id) = section_id {
|
||||
Cow::Owned(format!("{}#{}", anchor_base, id))
|
||||
} else {
|
||||
Cow::Borrowed(anchor_base)
|
||||
@@ -70,36 +74,35 @@ fn render_item(
|
||||
doc_urls: &mut Vec<String>,
|
||||
item: &BookItem,
|
||||
) -> Result<()> {
|
||||
let chapter = match *item {
|
||||
BookItem::Chapter(ref ch) if !ch.is_draft_chapter() => ch,
|
||||
let chapter = match item {
|
||||
&BookItem::Chapter(ref ch) => ch,
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
let chapter_path = chapter
|
||||
.path
|
||||
.as_ref()
|
||||
.expect("Checked that path exists above");
|
||||
let filepath = Path::new(&chapter_path).with_extension("html");
|
||||
let filepath = Path::new(&chapter.path).with_extension("html");
|
||||
let filepath = filepath
|
||||
.to_str()
|
||||
.with_context(|| "Could not convert HTML path to str")?;
|
||||
.chain_err(|| "Could not convert HTML path to str")?;
|
||||
let anchor_base = utils::fs::normalize_path(filepath);
|
||||
|
||||
let mut p = utils::new_cmark_parser(&chapter.content).peekable();
|
||||
let mut opts = Options::empty();
|
||||
opts.insert(OPTION_ENABLE_TABLES);
|
||||
opts.insert(OPTION_ENABLE_FOOTNOTES);
|
||||
let p = Parser::new_ext(&chapter.content, opts);
|
||||
|
||||
let mut in_heading = false;
|
||||
let max_section_depth = u32::from(search_config.heading_split_level);
|
||||
let mut in_header = false;
|
||||
let max_section_depth = search_config.heading_split_level as i32;
|
||||
let mut section_id = None;
|
||||
let mut heading = String::new();
|
||||
let mut body = String::new();
|
||||
let mut breadcrumbs = chapter.parent_names.clone();
|
||||
let mut footnote_numbers = HashMap::new();
|
||||
|
||||
while let Some(event) = p.next() {
|
||||
for event in p {
|
||||
match event {
|
||||
Event::Start(Tag::Heading(i)) if i <= max_section_depth => {
|
||||
if !heading.is_empty() {
|
||||
// Section finished, the next heading is following now
|
||||
Event::Start(Tag::Header(i)) if i <= max_section_depth => {
|
||||
if heading.len() > 0 {
|
||||
// Section finished, the next header is following now
|
||||
// Write the data to the index, and clear it for the next section
|
||||
add_doc(
|
||||
index,
|
||||
@@ -114,10 +117,10 @@ fn render_item(
|
||||
breadcrumbs.pop();
|
||||
}
|
||||
|
||||
in_heading = true;
|
||||
in_header = true;
|
||||
}
|
||||
Event::End(Tag::Heading(i)) if i <= max_section_depth => {
|
||||
in_heading = false;
|
||||
Event::End(Tag::Header(i)) if i <= max_section_depth => {
|
||||
in_header = false;
|
||||
section_id = Some(utils::id_from_content(&heading));
|
||||
breadcrumbs.push(heading.clone());
|
||||
}
|
||||
@@ -125,45 +128,34 @@ fn render_item(
|
||||
let number = footnote_numbers.len() + 1;
|
||||
footnote_numbers.entry(name).or_insert(number);
|
||||
}
|
||||
Event::Html(html) => {
|
||||
let mut html_block = html.into_string();
|
||||
|
||||
// As of pulldown_cmark 0.6, html events are no longer contained
|
||||
// in an HtmlBlock tag. We must collect consecutive Html events
|
||||
// into a block ourselves.
|
||||
while let Some(Event::Html(html)) = p.peek() {
|
||||
html_block.push_str(&html);
|
||||
p.next();
|
||||
}
|
||||
|
||||
body.push_str(&clean_html(&html_block));
|
||||
}
|
||||
Event::Start(_) | Event::End(_) | Event::Rule | Event::SoftBreak | Event::HardBreak => {
|
||||
Event::Start(_) | Event::End(_) | Event::SoftBreak | Event::HardBreak => {
|
||||
// Insert spaces where HTML output would usually seperate text
|
||||
// to ensure words don't get merged together
|
||||
if in_heading {
|
||||
if in_header {
|
||||
heading.push(' ');
|
||||
} else {
|
||||
body.push(' ');
|
||||
}
|
||||
}
|
||||
Event::Text(text) | Event::Code(text) => {
|
||||
if in_heading {
|
||||
Event::Text(text) => {
|
||||
if in_header {
|
||||
heading.push_str(&text);
|
||||
} else {
|
||||
body.push_str(&text);
|
||||
}
|
||||
}
|
||||
Event::Html(html) | Event::InlineHtml(html) => {
|
||||
body.push_str(&clean_html(&html));
|
||||
}
|
||||
Event::FootnoteReference(name) => {
|
||||
let len = footnote_numbers.len() + 1;
|
||||
let number = footnote_numbers.entry(name).or_insert(len);
|
||||
body.push_str(&format!(" [{}] ", number));
|
||||
}
|
||||
Event::TaskListMarker(_checked) => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !heading.is_empty() {
|
||||
if heading.len() > 0 {
|
||||
// Make sure the last section is added to the index
|
||||
add_doc(
|
||||
index,
|
||||
@@ -178,7 +170,7 @@ fn render_item(
|
||||
}
|
||||
|
||||
fn write_to_json(index: Index, search_config: &Search, doc_urls: Vec<String>) -> Result<String> {
|
||||
use elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
|
||||
use self::elasticlunr::config::{SearchBool, SearchOptions, SearchOptionsField};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
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)
|
||||
.with_context(|| "Unable to remove stale Markdown output")?;
|
||||
}
|
||||
|
||||
trace!("markdown render");
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
if !ch.is_draft_chapter() {
|
||||
utils::fs::write_file(
|
||||
&ctx.destination,
|
||||
&ch.path.as_ref().expect("Checked path exists before"),
|
||||
ch.content.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_dir_all(&destination)
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -8,25 +8,25 @@
|
||||
//!
|
||||
//! The definition for [RenderContext] may be useful though.
|
||||
//!
|
||||
//! [For Developers]: https://rust-lang.github.io/mdBook/for_developers/index.html
|
||||
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/lib/index.html
|
||||
//! [RenderContext]: struct.RenderContext.html
|
||||
|
||||
pub use self::html_handlebars::HtmlHandlebars;
|
||||
pub use self::markdown_renderer::MarkdownRenderer;
|
||||
|
||||
mod html_handlebars;
|
||||
mod markdown_renderer;
|
||||
|
||||
use serde_json;
|
||||
use shlex::Shlex;
|
||||
use std::fs;
|
||||
use std::io::{self, ErrorKind, Read};
|
||||
use std::io::{self, Read};
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use crate::book::Book;
|
||||
use crate::config::Config;
|
||||
use crate::errors::*;
|
||||
use toml::Value;
|
||||
use book::Book;
|
||||
use config::Config;
|
||||
use errors::*;
|
||||
|
||||
const MDBOOK_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
/// An arbitrary `mdbook` backend.
|
||||
///
|
||||
@@ -66,8 +66,6 @@ pub struct RenderContext {
|
||||
/// renderers to cache intermediate results, this directory is not
|
||||
/// guaranteed to be empty or even exist.
|
||||
pub destination: PathBuf,
|
||||
#[serde(skip)]
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
|
||||
impl RenderContext {
|
||||
@@ -78,12 +76,11 @@ impl RenderContext {
|
||||
Q: Into<PathBuf>,
|
||||
{
|
||||
RenderContext {
|
||||
book,
|
||||
config,
|
||||
version: crate::MDBOOK_VERSION.to_string(),
|
||||
book: book,
|
||||
config: config,
|
||||
version: MDBOOK_VERSION.to_string(),
|
||||
root: root.into(),
|
||||
destination: destination.into(),
|
||||
__non_exhaustive: (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +91,7 @@ impl RenderContext {
|
||||
|
||||
/// Load a `RenderContext` from its JSON representation.
|
||||
pub fn from_json<R: Read>(reader: R) -> Result<RenderContext> {
|
||||
serde_json::from_reader(reader).with_context(|| "Unable to deserialize the `RenderContext`")
|
||||
serde_json::from_reader(reader).chain_err(|| "Unable to deserialize the `RenderContext`")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,40 +147,6 @@ impl CmdRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
impl CmdRenderer {
|
||||
fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> {
|
||||
if let ErrorKind::NotFound = error.kind() {
|
||||
// Look for "output.{self.name}.optional".
|
||||
// If it exists and is true, treat this as a warning.
|
||||
// Otherwise, fail the build.
|
||||
|
||||
let optional_key = format!("output.{}.optional", self.name);
|
||||
|
||||
let is_optional = match ctx.config.get(&optional_key) {
|
||||
Some(Value::Boolean(value)) => *value,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_optional {
|
||||
warn!(
|
||||
"The command `{}` for backend `{}` was not found, \
|
||||
but was marked as optional.",
|
||||
self.cmd, self.name
|
||||
);
|
||||
return Ok(());
|
||||
} else {
|
||||
error!(
|
||||
"The command `{0}` wasn't found, is the \"{1}\" backend installed? \
|
||||
If you want to ignore this error when the \"{1}\" backend is not installed, \
|
||||
set `optional = true` in the `[output.{1}]` section of the book.toml configuration file.",
|
||||
self.cmd, self.name
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error).with_context(|| "Unable to start the backend")?
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for CmdRenderer {
|
||||
fn name(&self) -> &str {
|
||||
&self.name
|
||||
@@ -203,22 +166,34 @@ impl Renderer for CmdRenderer {
|
||||
.spawn()
|
||||
{
|
||||
Ok(c) => c,
|
||||
Err(e) => return self.handle_render_command_error(ctx, e),
|
||||
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
warn!(
|
||||
"The command wasn't found, is the \"{}\" backend installed?",
|
||||
self.name
|
||||
);
|
||||
warn!("\tCommand: {}", self.cmd);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e).chain_err(|| "Unable to start the backend")?;
|
||||
}
|
||||
};
|
||||
|
||||
let mut stdin = child.stdin.take().expect("Child has stdin");
|
||||
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
|
||||
// Looks like the backend hung up before we could finish
|
||||
// sending it the render context. Log the error and keep going
|
||||
warn!("Error writing the RenderContext to the backend, {}", e);
|
||||
}
|
||||
{
|
||||
let mut stdin = child.stdin.take().expect("Child has stdin");
|
||||
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
|
||||
// Looks like the backend hung up before we could finish
|
||||
// sending it the render context. Log the error and keep going
|
||||
warn!("Error writing the RenderContext to the backend, {}", e);
|
||||
}
|
||||
|
||||
// explicitly close the `stdin` file handle
|
||||
drop(stdin);
|
||||
// explicitly close the `stdin` file handle
|
||||
drop(stdin);
|
||||
}
|
||||
|
||||
let status = child
|
||||
.wait()
|
||||
.with_context(|| "Error waiting for the backend to complete")?;
|
||||
.chain_err(|| "Error waiting for the backend to complete")?;
|
||||
|
||||
trace!("{} exited with output: {:?}", self.cmd, status);
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 434 KiB After Width: | Height: | Size: 348 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,7 +12,8 @@ Original by Dempfi (https://github.com/dempfi/ayu)
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
.hljs-quote,
|
||||
.hljs-meta {
|
||||
color: #5c6773;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -29,7 +30,6 @@ Original by Dempfi (https://github.com/dempfi/ayu)
|
||||
}
|
||||
|
||||
.hljs-number,
|
||||
.hljs-meta,
|
||||
.hljs-builtin-name,
|
||||
.hljs-literal,
|
||||
.hljs-type,
|
||||
@@ -69,11 +69,3 @@ Original by Dempfi (https://github.com/dempfi/ayu)
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #91b362;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #d96c75;
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
window.onunload = function () { };
|
||||
|
||||
// Global variable, shared between modules
|
||||
function playground_text(playground) {
|
||||
let code_block = playground.querySelector("code");
|
||||
function playpen_text(playpen) {
|
||||
let code_block = playpen.querySelector("code");
|
||||
|
||||
if (window.ace && code_block.classList.contains("editable")) {
|
||||
let editor = window.ace.edit(code_block);
|
||||
@@ -16,6 +16,9 @@ function playground_text(playground) {
|
||||
}
|
||||
|
||||
(function codeSnippets() {
|
||||
// Hide Rust code lines prepended with a specific character
|
||||
var hiding_character = "#";
|
||||
|
||||
function fetch_with_timeout(url, options, timeout = 6000) {
|
||||
return Promise.race([
|
||||
fetch(url, options),
|
||||
@@ -23,8 +26,8 @@ function playground_text(playground) {
|
||||
]);
|
||||
}
|
||||
|
||||
var playgrounds = Array.from(document.querySelectorAll(".playground"));
|
||||
if (playgrounds.length > 0) {
|
||||
var playpens = Array.from(document.querySelectorAll(".playpen"));
|
||||
if (playpens.length > 0) {
|
||||
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
|
||||
headers: {
|
||||
'Content-Type': "application/json",
|
||||
@@ -36,30 +39,21 @@ function playground_text(playground) {
|
||||
.then(response => {
|
||||
// get list of crates available in the rust playground
|
||||
let playground_crates = response.crates.map(item => item["id"]);
|
||||
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
|
||||
playpens.forEach(block => handle_crate_list_update(block, playground_crates));
|
||||
});
|
||||
}
|
||||
|
||||
function handle_crate_list_update(playground_block, playground_crates) {
|
||||
function handle_crate_list_update(playpen_block, playground_crates) {
|
||||
// update the play buttons after receiving the response
|
||||
update_play_button(playground_block, playground_crates);
|
||||
update_play_button(playpen_block, playground_crates);
|
||||
|
||||
// and install on change listener to dynamically update ACE editors
|
||||
if (window.ace) {
|
||||
let code_block = playground_block.querySelector("code");
|
||||
let code_block = playpen_block.querySelector("code");
|
||||
if (code_block.classList.contains("editable")) {
|
||||
let editor = window.ace.edit(code_block);
|
||||
editor.addEventListener("change", function (e) {
|
||||
update_play_button(playground_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(playground_block)
|
||||
update_play_button(playpen_block, playground_crates);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -77,7 +71,7 @@ function playground_text(playground) {
|
||||
}
|
||||
|
||||
// get list of `extern crate`'s from snippet
|
||||
var txt = playground_text(pre_block);
|
||||
var txt = playpen_text(pre_block);
|
||||
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
|
||||
var snippet_crates = [];
|
||||
var item;
|
||||
@@ -106,16 +100,12 @@ function playground_text(playground) {
|
||||
code_block.append(result_block);
|
||||
}
|
||||
|
||||
let text = playground_text(code_block);
|
||||
let classes = code_block.querySelector('code').classList;
|
||||
let has_2018 = classes.contains("edition2018");
|
||||
let edition = has_2018 ? "2018" : "2015";
|
||||
let text = playpen_text(code_block);
|
||||
|
||||
var params = {
|
||||
version: "stable",
|
||||
optimize: "0",
|
||||
code: text,
|
||||
edition: edition
|
||||
code: text
|
||||
};
|
||||
|
||||
if (text.indexOf("#![feature") !== -1) {
|
||||
@@ -143,11 +133,6 @@ function playground_text(playground) {
|
||||
languages: [], // Languages used for auto-detection
|
||||
});
|
||||
|
||||
let code_nodes = Array
|
||||
.from(document.querySelectorAll('code'))
|
||||
// Don't highlight `inline code` blocks in headers.
|
||||
.filter(function (node) {return !node.parentElement.classList.contains("header"); });
|
||||
|
||||
if (window.ace) {
|
||||
// language-rust class needs to be removed for editable
|
||||
// blocks or highlightjs will capture events
|
||||
@@ -159,71 +144,111 @@ function playground_text(playground) {
|
||||
.from(document.querySelectorAll('code:not(.editable)'))
|
||||
.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
} else {
|
||||
code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
Array
|
||||
.from(document.querySelectorAll('code'))
|
||||
.forEach(function (block) { hljs.highlightBlock(block); });
|
||||
}
|
||||
|
||||
// Adding the hljs class gives code blocks the color css
|
||||
// even if highlighting doesn't apply
|
||||
code_nodes.forEach(function (block) { block.classList.add('hljs'); });
|
||||
Array
|
||||
.from(document.querySelectorAll('code'))
|
||||
.forEach(function (block) { block.classList.add('hljs'); });
|
||||
|
||||
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
|
||||
|
||||
var lines = Array.from(block.querySelectorAll('.boring'));
|
||||
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("");
|
||||
|
||||
// If no lines were hidden, return
|
||||
if (!lines.length) { return; }
|
||||
block.classList.add("hide-boring");
|
||||
if (!lines_hidden) { return; }
|
||||
|
||||
var buttons = document.createElement('div');
|
||||
buttons.className = 'buttons';
|
||||
buttons.innerHTML = "<button class=\"fa fa-eye\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
|
||||
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-eye')) {
|
||||
e.target.classList.remove('fa-eye');
|
||||
e.target.classList.add('fa-eye-slash');
|
||||
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);
|
||||
|
||||
block.classList.remove('hide-boring');
|
||||
} else if (e.target.classList.contains('fa-eye-slash')) {
|
||||
e.target.classList.remove('fa-eye-slash');
|
||||
e.target.classList.add('fa-eye');
|
||||
Array.from(lines).forEach(function (line) {
|
||||
line.classList.remove('hidden');
|
||||
line.classList.add('unhidden');
|
||||
});
|
||||
} 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);
|
||||
|
||||
block.classList.add('hide-boring');
|
||||
Array.from(lines).forEach(function (line) {
|
||||
line.classList.remove('unhidden');
|
||||
line.classList.add('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (window.playground_copyable) {
|
||||
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
|
||||
var pre_block = block.parentNode;
|
||||
if (!pre_block.classList.contains('playground')) {
|
||||
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);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process playground code blocks
|
||||
Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) {
|
||||
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) {
|
||||
// Add play button
|
||||
var buttons = pre_block.querySelector(".buttons");
|
||||
if (!buttons) {
|
||||
@@ -238,21 +263,19 @@ function playground_text(playground) {
|
||||
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.playground_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');
|
||||
@@ -285,7 +308,7 @@ function playground_text(playground) {
|
||||
function showThemes() {
|
||||
themePopup.style.display = 'block';
|
||||
themeToggleButton.setAttribute('aria-expanded', true);
|
||||
themePopup.querySelector("button#" + get_theme()).focus();
|
||||
themePopup.querySelector("button#" + document.body.className).focus();
|
||||
}
|
||||
|
||||
function hideThemes() {
|
||||
@@ -294,17 +317,7 @@ function playground_text(playground) {
|
||||
themeToggleButton.focus();
|
||||
}
|
||||
|
||||
function get_theme() {
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
|
||||
if (theme === null || theme === undefined) {
|
||||
return default_theme;
|
||||
} else {
|
||||
return theme;
|
||||
}
|
||||
}
|
||||
|
||||
function set_theme(theme, store = true) {
|
||||
function set_theme(theme) {
|
||||
let ace_theme;
|
||||
|
||||
if (theme == 'coal' || theme == 'navy') {
|
||||
@@ -317,11 +330,13 @@ function playground_text(playground) {
|
||||
stylesheets.ayuHighlight.disabled = false;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = true;
|
||||
|
||||
ace_theme = "ace/theme/tomorrow_night";
|
||||
} else {
|
||||
stylesheets.ayuHighlight.disabled = true;
|
||||
stylesheets.tomorrowNight.disabled = true;
|
||||
stylesheets.highlight.disabled = false;
|
||||
|
||||
ace_theme = "ace/theme/dawn";
|
||||
}
|
||||
|
||||
@@ -335,20 +350,23 @@ function playground_text(playground) {
|
||||
});
|
||||
}
|
||||
|
||||
var previousTheme = get_theme();
|
||||
var previousTheme;
|
||||
try { previousTheme = localStorage.getItem('mdbook-theme'); } catch (e) { }
|
||||
if (previousTheme === null || previousTheme === undefined) { previousTheme = 'light'; }
|
||||
|
||||
if (store) {
|
||||
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
|
||||
}
|
||||
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
|
||||
|
||||
document.body.className = theme;
|
||||
html.classList.remove(previousTheme);
|
||||
html.classList.add(theme);
|
||||
}
|
||||
|
||||
// Set theme
|
||||
var theme = get_theme();
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||
|
||||
set_theme(theme, false);
|
||||
set_theme(theme);
|
||||
|
||||
themeToggleButton.addEventListener('click', function () {
|
||||
if (themePopup.style.display === 'block') {
|
||||
@@ -370,7 +388,7 @@ function playground_text(playground) {
|
||||
}
|
||||
});
|
||||
|
||||
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628
|
||||
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang-nursery/mdBook/issues/628
|
||||
document.addEventListener('click', function(e) {
|
||||
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
|
||||
hideThemes();
|
||||
@@ -417,7 +435,6 @@ function playground_text(playground) {
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
var sidebarToggleButton = document.getElementById("sidebar-toggle");
|
||||
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
|
||||
var firstContact = null;
|
||||
|
||||
function showSidebar() {
|
||||
@@ -431,17 +448,6 @@ function playground_text(playground) {
|
||||
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');
|
||||
@@ -456,11 +462,6 @@ function playground_text(playground) {
|
||||
// Toggle sidebar
|
||||
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
|
||||
if (html.classList.contains("sidebar-hidden")) {
|
||||
var current_width = parseInt(
|
||||
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
|
||||
if (current_width < 150) {
|
||||
document.documentElement.style.setProperty('--sidebar-width', '150px');
|
||||
}
|
||||
showSidebar();
|
||||
} else if (html.classList.contains("sidebar-visible")) {
|
||||
hideSidebar();
|
||||
@@ -473,32 +474,6 @@ function playground_text(playground) {
|
||||
}
|
||||
});
|
||||
|
||||
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
|
||||
|
||||
function initResize(e) {
|
||||
window.addEventListener('mousemove', resize, false);
|
||||
window.addEventListener('mouseup', stopResize, false);
|
||||
html.classList.add('sidebar-resizing');
|
||||
}
|
||||
function resize(e) {
|
||||
var pos = (e.clientX - sidebar.offsetLeft);
|
||||
if (pos < 20) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (html.classList.contains("sidebar-hidden")) {
|
||||
showSidebar();
|
||||
}
|
||||
pos = Math.min(pos, window.innerWidth - 100);
|
||||
document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
|
||||
}
|
||||
}
|
||||
//on mouseup remove windows functions mousemove & mouseup
|
||||
function stopResize(e) {
|
||||
html.classList.remove('sidebar-resizing');
|
||||
window.removeEventListener('mousemove', resize, false);
|
||||
window.removeEventListener('mouseup', stopResize, false);
|
||||
}
|
||||
|
||||
document.addEventListener('touchstart', function (e) {
|
||||
firstContact = {
|
||||
x: e.touches[0].clientX,
|
||||
@@ -525,10 +500,9 @@ function playground_text(playground) {
|
||||
}, { passive: true });
|
||||
|
||||
// Scroll sidebar to current active section
|
||||
var activeSection = document.getElementById("sidebar").querySelector(".active");
|
||||
var activeSection = sidebar.querySelector(".active");
|
||||
if (activeSection) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
sidebar.scrollTop = activeSection.offsetTop;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -569,11 +543,11 @@ function playground_text(playground) {
|
||||
elem.className = 'fa fa-copy tooltipped';
|
||||
}
|
||||
|
||||
var clipboardSnippets = new ClipboardJS('.clip-button', {
|
||||
var clipboardSnippets = new Clipboard('.clip-button', {
|
||||
text: function (trigger) {
|
||||
hideTooltip(trigger);
|
||||
let playground = trigger.closest("pre");
|
||||
return playground_text(playground);
|
||||
let playpen = trigger.closest("pre");
|
||||
return playpen_text(playpen);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -601,60 +575,26 @@ function playground_text(playground) {
|
||||
});
|
||||
})();
|
||||
|
||||
(function controllMenu() {
|
||||
(function autoHideMenu() {
|
||||
var menu = document.getElementById('menu-bar');
|
||||
|
||||
(function controllPosition() {
|
||||
var scrollTop = document.scrollingElement.scrollTop;
|
||||
var prevScrollTop = scrollTop;
|
||||
var minMenuY = -menu.clientHeight - 50;
|
||||
// When the script loads, the page can be at any scroll (e.g. if you reforesh it).
|
||||
menu.style.top = scrollTop + 'px';
|
||||
// Same as parseInt(menu.style.top.slice(0, -2), but faster
|
||||
var topCache = menu.style.top.slice(0, -2);
|
||||
menu.classList.remove('sticky');
|
||||
var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
|
||||
document.addEventListener('scroll', function () {
|
||||
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
|
||||
// `null` means that it doesn't need to be updated
|
||||
var nextSticky = null;
|
||||
var nextTop = null;
|
||||
var scrollDown = scrollTop > prevScrollTop;
|
||||
var menuPosAbsoluteY = topCache - scrollTop;
|
||||
if (scrollDown) {
|
||||
nextSticky = false;
|
||||
if (menuPosAbsoluteY > 0) {
|
||||
nextTop = prevScrollTop;
|
||||
}
|
||||
} else {
|
||||
if (menuPosAbsoluteY > 0) {
|
||||
nextSticky = true;
|
||||
} else if (menuPosAbsoluteY < minMenuY) {
|
||||
nextTop = prevScrollTop + minMenuY;
|
||||
}
|
||||
}
|
||||
if (nextSticky === true && stickyCache === false) {
|
||||
menu.classList.add('sticky');
|
||||
stickyCache = true;
|
||||
} else if (nextSticky === false && stickyCache === true) {
|
||||
menu.classList.remove('sticky');
|
||||
stickyCache = false;
|
||||
}
|
||||
if (nextTop !== null) {
|
||||
menu.style.top = nextTop + 'px';
|
||||
topCache = nextTop;
|
||||
}
|
||||
prevScrollTop = scrollTop;
|
||||
}, { passive: true });
|
||||
})();
|
||||
(function controllBorder() {
|
||||
menu.classList.remove('bordered');
|
||||
document.addEventListener('scroll', function () {
|
||||
if (menu.offsetTop === 0) {
|
||||
menu.classList.remove('bordered');
|
||||
} else {
|
||||
menu.classList.add('bordered');
|
||||
}
|
||||
}, { passive: true });
|
||||
})();
|
||||
var previousScrollTop = document.scrollingElement.scrollTop;
|
||||
|
||||
document.addEventListener('scroll', function () {
|
||||
if (menu.classList.contains('folded') && document.scrollingElement.scrollTop < previousScrollTop) {
|
||||
menu.classList.remove('folded');
|
||||
} else if (!menu.classList.contains('folded') && document.scrollingElement.scrollTop > previousScrollTop) {
|
||||
menu.classList.add('folded');
|
||||
}
|
||||
|
||||
if (!menu.classList.contains('bordered') && document.scrollingElement.scrollTop > 0) {
|
||||
menu.classList.add('bordered');
|
||||
}
|
||||
|
||||
if (menu.classList.contains('bordered') && document.scrollingElement.scrollTop === 0) {
|
||||
menu.classList.remove('bordered');
|
||||
}
|
||||
|
||||
previousScrollTop = document.scrollingElement.scrollTop;
|
||||
}, { passive: true });
|
||||
})();
|
||||
|
||||
6
src/theme/clipboard.min.js
vendored
6
src/theme/clipboard.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -8,9 +8,7 @@
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar);
|
||||
}
|
||||
html {
|
||||
scrollbar-color: var(--scrollbar) var(--bg);
|
||||
}
|
||||
|
||||
#searchresults a,
|
||||
.content a:link,
|
||||
a:visited,
|
||||
@@ -20,13 +18,14 @@ a > .hljs {
|
||||
|
||||
/* Menu Bar */
|
||||
|
||||
#menu-bar,
|
||||
#menu-bar-hover-placeholder {
|
||||
#menu-bar {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 101;
|
||||
margin: auto calc(0px - var(--page-padding));
|
||||
}
|
||||
#menu-bar {
|
||||
position: relative;
|
||||
#menu-bar > #menu-bar-sticky-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: var(--bg);
|
||||
@@ -34,28 +33,17 @@ a > .hljs {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
#menu-bar.sticky,
|
||||
.js #menu-bar-hover-placeholder:hover + #menu-bar,
|
||||
.js #menu-bar:hover,
|
||||
.js.sidebar-visible #menu-bar {
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 0 !important;
|
||||
.js #menu-bar > #menu-bar-sticky-container {
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
#menu-bar-hover-placeholder {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
height: var(--menu-bar-height);
|
||||
}
|
||||
#menu-bar.bordered {
|
||||
#menu-bar.bordered > #menu-bar-sticky-container {
|
||||
border-bottom-color: var(--table-border-color);
|
||||
}
|
||||
#menu-bar i, #menu-bar .icon-button {
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
z-index: 10;
|
||||
line-height: var(--menu-bar-height);
|
||||
line-height: 50px;
|
||||
cursor: pointer;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
@@ -75,26 +63,27 @@ a > .hljs {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.right-buttons {
|
||||
#print-button {
|
||||
margin: 0 15px;
|
||||
}
|
||||
.right-buttons a {
|
||||
text-decoration: none;
|
||||
|
||||
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
|
||||
transform: translateY(-60px);
|
||||
}
|
||||
|
||||
.left-buttons {
|
||||
display: flex;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.no-js .left-buttons {
|
||||
.no-js .left-buttons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
display: inline-block;
|
||||
font-weight: 200;
|
||||
font-size: 2rem;
|
||||
line-height: var(--menu-bar-height);
|
||||
font-size: 20px;
|
||||
line-height: 50px;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
@@ -132,7 +121,7 @@ a > .hljs {
|
||||
text-decoration: none;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
top: 50px; /* Height of menu-bar */
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
max-width: 150px;
|
||||
@@ -143,21 +132,17 @@ a > .hljs {
|
||||
align-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
transition: color 0.5s, background-color 0.5s;
|
||||
transition: color 0.5s;
|
||||
}
|
||||
|
||||
.nav-chapters:hover {
|
||||
text-decoration: none;
|
||||
background-color: var(--theme-hover);
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
}
|
||||
.nav-chapters:hover { text-decoration: none; }
|
||||
|
||||
.nav-wrapper {
|
||||
margin-top: 50px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-nav-chapters {
|
||||
.mobile-nav-chapters {
|
||||
font-size: 2.5em;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
@@ -188,14 +173,11 @@ a > .hljs {
|
||||
/* Inline code */
|
||||
|
||||
:not(pre) > .hljs {
|
||||
display: inline;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.1em 0.3em;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
:not(pre):not(a) > .hljs {
|
||||
color: var(--inline-code-color);
|
||||
overflow-x: initial;
|
||||
}
|
||||
|
||||
a:hover > .hljs {
|
||||
@@ -316,6 +298,8 @@ ul#searchresults span.teaser em {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--sidebar-width);
|
||||
overflow-y: auto;
|
||||
padding: 10px 10px;
|
||||
font-size: 0.875em;
|
||||
box-sizing: border-box;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@@ -323,39 +307,12 @@ ul#searchresults span.teaser em {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
.sidebar-resizing {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.js:not(.sidebar-resizing) .sidebar {
|
||||
.js .sidebar {
|
||||
transition: transform 0.3s; /* Animation: slide away */
|
||||
}
|
||||
.sidebar code {
|
||||
line-height: 2em;
|
||||
}
|
||||
.sidebar .sidebar-scrollbox {
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 10px 10px;
|
||||
}
|
||||
.sidebar .sidebar-resize-handle {
|
||||
position: absolute;
|
||||
cursor: col-resize;
|
||||
width: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
.js .sidebar .sidebar-resize-handle {
|
||||
cursor: col-resize;
|
||||
width: 5px;
|
||||
}
|
||||
.sidebar-hidden .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width)));
|
||||
}
|
||||
@@ -381,57 +338,22 @@ 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 {
|
||||
color: var(--sidebar-fg);
|
||||
display: block;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
|
||||
.chapter li a:hover {
|
||||
.chapter li a:hover { text-decoration: none }
|
||||
.chapter li .active,
|
||||
a:hover {
|
||||
/* Animate color change */
|
||||
color: var(--sidebar-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.chapter-item {
|
||||
line-height: 1.5em;
|
||||
margin-top: 0.6em;
|
||||
}
|
||||
|
||||
.chapter li.expanded > a.toggle div {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
@@ -441,7 +363,7 @@ ul#searchresults span.teaser em {
|
||||
background-color: var(--sidebar-spacer);
|
||||
}
|
||||
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||
.chapter li a { padding: 5px 0; }
|
||||
.spacer { margin: 10px 0; }
|
||||
}
|
||||
@@ -457,7 +379,7 @@ ul#searchresults span.teaser em {
|
||||
.theme-popup {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: var(--menu-bar-height);
|
||||
top: 50px;
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7em;
|
||||
|
||||
@@ -2,11 +2,6 @@
|
||||
|
||||
@import 'variables.css';
|
||||
|
||||
:root {
|
||||
/* Browser default font-size is 16px, this way 1 rem = 10px */
|
||||
font-size: 62.5%;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
color: var(--fg);
|
||||
@@ -16,61 +11,47 @@ html {
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-size: 1.6rem;
|
||||
font-size: 1rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important;
|
||||
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace;
|
||||
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
|
||||
}
|
||||
|
||||
/* Don't change font size in headers. */
|
||||
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
.left { float: left; }
|
||||
.right { float: right; }
|
||||
.boring { opacity: 0.6; }
|
||||
.hide-boring .boring { display: none; }
|
||||
.hidden { display: none !important; }
|
||||
.hidden { display: none; }
|
||||
.play-button.hidden { display: none; }
|
||||
|
||||
h2, h3 { margin-top: 2.5em; }
|
||||
h4, h5 { margin-top: 2em; }
|
||||
|
||||
.header + .header h3,
|
||||
.header + .header h4,
|
||||
.header + .header h5 {
|
||||
.header + .header h5 {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
h1 a.header:target::before,
|
||||
h2 a.header:target::before,
|
||||
h3 a.header:target::before,
|
||||
h4 a.header:target::before {
|
||||
a.header:target h1:before,
|
||||
a.header:target h2:before,
|
||||
a.header:target h3:before,
|
||||
a.header:target h4:before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-left: -30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
h1 a.header:target,
|
||||
h2 a.header:target,
|
||||
h3 a.header:target,
|
||||
h4 a.header:target {
|
||||
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
|
||||
}
|
||||
|
||||
.page {
|
||||
outline: 0;
|
||||
padding: 0 var(--page-padding);
|
||||
margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
|
||||
}
|
||||
.page-wrapper {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.js:not(.sidebar-resizing) .page-wrapper {
|
||||
.js .page-wrapper {
|
||||
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
|
||||
@@ -84,9 +65,6 @@ h4 a.header:target {
|
||||
margin-right: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
.content p { line-height: 1.45em; }
|
||||
.content ol { line-height: 1.45em; }
|
||||
.content ul { line-height: 1.45em; }
|
||||
.content a { text-decoration: none; }
|
||||
.content a:hover { text-decoration: underline; }
|
||||
.content img { max-width: 100%; }
|
||||
@@ -114,9 +92,6 @@ table thead td {
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
}
|
||||
table thead th {
|
||||
padding: 3px 20px;
|
||||
}
|
||||
table thead tr {
|
||||
border: 1px var(--table-header-bg) solid;
|
||||
}
|
||||
@@ -166,9 +141,4 @@ blockquote {
|
||||
.tooltipped .tooltiptext {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.chapter li.part-title {
|
||||
color: var(--sidebar-fg);
|
||||
margin: 5px 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
--sidebar-width: 300px;
|
||||
--page-padding: 15px;
|
||||
--content-max-width: 750px;
|
||||
--menu-bar-height: 50px;
|
||||
}
|
||||
|
||||
/* Themes */
|
||||
@@ -209,45 +208,3 @@
|
||||
--searchresults-li-bg: #dec2a2;
|
||||
--search-mark-bg: #e69f67;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.light.no-js {
|
||||
--bg: hsl(200, 7%, 8%);
|
||||
--fg: #98a3ad;
|
||||
|
||||
--sidebar-bg: #292c2f;
|
||||
--sidebar-fg: #a1adb8;
|
||||
--sidebar-non-existant: #505254;
|
||||
--sidebar-active: #3473ad;
|
||||
--sidebar-spacer: #393939;
|
||||
|
||||
--scrollbar: var(--sidebar-fg);
|
||||
|
||||
--icons: #43484d;
|
||||
--icons-hover: #b3c0cc;
|
||||
|
||||
--links: #2b79a2;
|
||||
|
||||
--inline-code-color: #c5c8c6;;
|
||||
|
||||
--theme-popup-bg: #141617;
|
||||
--theme-popup-border: #43484d;
|
||||
--theme-hover: #1f2124;
|
||||
|
||||
--quote-bg: hsl(234, 21%, 18%);
|
||||
--quote-border: hsl(234, 21%, 23%);
|
||||
|
||||
--table-border-color: hsl(200, 7%, 13%);
|
||||
--table-header-bg: hsl(200, 7%, 28%);
|
||||
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||
|
||||
--searchbar-border-color: #aaa;
|
||||
--searchbar-bg: #b7b7b7;
|
||||
--searchbar-fg: #000;
|
||||
--searchbar-shadow-color: #aaa;
|
||||
--searchresults-header-fg: #666;
|
||||
--searchresults-border-color: #98a3ad;
|
||||
--searchresults-li-bg: #2b2b2f;
|
||||
--search-mark-bg: #355c7d;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 199.7 184.2">
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
svg { fill: white; }
|
||||
}
|
||||
</style>
|
||||
<path d="M189.5,36.8c0.2,2.8,0,5.1-0.6,6.8L153,162c-0.6,2.1-2,3.7-4.2,5c-2.2,1.2-4.4,1.9-6.7,1.9H31.4c-9.6,0-15.3-2.8-17.3-8.4
|
||||
c-0.8-2.2-0.8-3.9,0.1-5.2c0.9-1.2,2.4-1.8,4.6-1.8H123c7.4,0,12.6-1.4,15.4-4.1s5.7-8.9,8.6-18.4l32.9-108.6
|
||||
c1.8-5.9,1-11.1-2.2-15.6S169.9,0,164,0H72.7c-1,0-3.1,0.4-6.1,1.1l0.1-0.4C64.5,0.2,62.6,0,61,0.1s-3,0.5-4.3,1.4
|
||||
c-1.3,0.9-2.4,1.8-3.2,2.8S52,6.5,51.2,8.1c-0.8,1.6-1.4,3-1.9,4.3s-1.1,2.7-1.8,4.2c-0.7,1.5-1.3,2.7-2,3.7c-0.5,0.6-1.2,1.5-2,2.5
|
||||
s-1.6,2-2.2,2.8s-0.9,1.5-1.1,2.2c-0.2,0.7-0.1,1.8,0.2,3.2c0.3,1.4,0.4,2.4,0.4,3.1c-0.3,3-1.4,6.9-3.3,11.6
|
||||
c-1.9,4.7-3.6,8.1-5.1,10.1c-0.3,0.4-1.2,1.3-2.6,2.7c-1.4,1.4-2.3,2.6-2.6,3.7c-0.3,0.4-0.3,1.5-0.1,3.4c0.3,1.8,0.4,3.1,0.3,3.8
|
||||
c-0.3,2.7-1.3,6.3-3,10.8c-1.7,4.5-3.4,8.2-5,11c-0.2,0.5-0.9,1.4-2,2.8c-1.1,1.4-1.8,2.5-2,3.4c-0.2,0.6-0.1,1.8,0.1,3.4
|
||||
c0.2,1.6,0.2,2.8-0.1,3.6c-0.6,3-1.8,6.7-3.6,11c-1.8,4.3-3.6,7.9-5.4,11c-0.5,0.8-1.1,1.7-2,2.8c-0.8,1.1-1.5,2-2,2.8
|
||||
s-0.8,1.6-1,2.5c-0.1,0.5,0,1.3,0.4,2.3c0.3,1.1,0.4,1.9,0.4,2.6c-0.1,1.1-0.2,2.6-0.5,4.4c-0.2,1.8-0.4,2.9-0.4,3.2
|
||||
c-1.8,4.8-1.7,9.9,0.2,15.2c2.2,6.2,6.2,11.5,11.9,15.8c5.7,4.3,11.7,6.4,17.8,6.4h110.7c5.2,0,10.1-1.7,14.7-5.2s7.7-7.8,9.2-12.9
|
||||
l33-108.6c1.8-5.8,1-10.9-2.2-15.5C194.9,39.7,192.6,38,189.5,36.8z M59.6,122.8L73.8,80c0,0,7,0,10.8,0s28.8-1.7,25.4,17.5
|
||||
c-3.4,19.2-18.8,25.2-36.8,25.4S59.6,122.8,59.6,122.8z M78.6,116.8c4.7-0.1,18.9-2.9,22.1-17.1S89.2,86.3,89.2,86.3l-8.9,0
|
||||
l-10.2,30.5C70.2,116.9,74,116.9,78.6,116.8z M75.3,68.7L89,26.2h9.8l0.8,34l23.6-34h9.9l-13.6,42.5h-7.1l12.5-35.4l-24.5,35.4h-6.8
|
||||
l-0.8-35L82,68.7H75.3z"/>
|
||||
</svg>
|
||||
<!-- Original image Copyright Dave Gandy — CC BY 4.0 License -->
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,93 +0,0 @@
|
||||
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -1,100 +0,0 @@
|
||||
/* Open Sans is licensed under the Apache License, Version 2.0. See http://www.apache.org/licenses/LICENSE-2.0 */
|
||||
/* Source Code Pro is under the Open Font License. See https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL */
|
||||
|
||||
/* open-sans-300 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light'), local('OpenSans-Light'),
|
||||
url('open-sans-v17-all-charsets-300.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'),
|
||||
url('open-sans-v17-all-charsets-300italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Regular'), local('OpenSans-Regular'),
|
||||
url('open-sans-v17-all-charsets-regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Open Sans Italic'), local('OpenSans-Italic'),
|
||||
url('open-sans-v17-all-charsets-italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
|
||||
url('open-sans-v17-all-charsets-600.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-600italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold'), local('OpenSans-Bold'),
|
||||
url('open-sans-v17-all-charsets-700.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),
|
||||
url('open-sans-v17-all-charsets-700italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'),
|
||||
url('open-sans-v17-all-charsets-800.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'),
|
||||
url('open-sans-v17-all-charsets-800italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */
|
||||
@font-face {
|
||||
font-family: 'Source Code Pro';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
pub static CSS: &[u8] = include_bytes!("fonts.css");
|
||||
// An array of (file_name, file_contents) pairs
|
||||
pub static LICENSES: [(&str, &[u8]); 2] = [
|
||||
(
|
||||
"fonts/OPEN-SANS-LICENSE.txt",
|
||||
include_bytes!("OPEN-SANS-LICENSE.txt"),
|
||||
),
|
||||
(
|
||||
"fonts/SOURCE-CODE-PRO-LICENSE.txt",
|
||||
include_bytes!("SOURCE-CODE-PRO-LICENSE.txt"),
|
||||
),
|
||||
];
|
||||
// An array of (file_name, file_contents) pairs
|
||||
pub static OPEN_SANS: [(&str, &[u8]); 10] = [
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-300.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-300.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-300italic.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-300italic.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-regular.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-regular.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-italic.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-italic.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-600.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-600.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-600italic.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-600italic.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-700.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-700.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-700italic.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-700italic.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-800.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-800.woff2"),
|
||||
),
|
||||
(
|
||||
"fonts/open-sans-v17-all-charsets-800italic.woff2",
|
||||
include_bytes!("open-sans-v17-all-charsets-800italic.woff2"),
|
||||
),
|
||||
];
|
||||
|
||||
// A (file_name, file_contents) pair
|
||||
pub static SOURCE_CODE_PRO: (&str, &[u8]) = (
|
||||
"fonts/source-code-pro-v11-all-charsets-500.woff2",
|
||||
include_bytes!("source-code-pro-v11-all-charsets-500.woff2"),
|
||||
);
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{{!-- Put your head HTML text here --}}
|
||||
@@ -67,13 +67,3 @@
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-addition {
|
||||
color: #22863a;
|
||||
background-color: #f0fff4;
|
||||
}
|
||||
|
||||
.hljs-deletion {
|
||||
color: #b31d28;
|
||||
background-color: #ffeef0;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,31 +1,15 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}" class="sidebar-visible no-js {{ default_theme }}">
|
||||
<html lang="{{ language }}" class="sidebar-visible no-js">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
{{#if is_print }}
|
||||
<meta name="robots" content="noindex" />
|
||||
{{/if}}
|
||||
{{#if base_url}}
|
||||
<base href="{{ base_url }}">
|
||||
{{/if}}
|
||||
|
||||
|
||||
<!-- Custom HTML head -->
|
||||
{{> head}}
|
||||
|
||||
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||
<meta name="description" content="{{ description }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
|
||||
{{#if favicon_svg}}
|
||||
<link rel="icon" href="{{ path_to_root }}favicon.svg">
|
||||
{{/if}}
|
||||
{{#if favicon_png}}
|
||||
<link rel="shortcut icon" href="{{ path_to_root }}favicon.png">
|
||||
{{/if}}
|
||||
<link rel="shortcut icon" href="{{ path_to_root }}{{ favicon }}">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
|
||||
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
|
||||
@@ -33,9 +17,8 @@
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
|
||||
{{#if copy_fonts}}
|
||||
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
|
||||
{{/if}}
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- Highlight.js Stylesheets -->
|
||||
<link rel="stylesheet" href="{{ path_to_root }}highlight.css">
|
||||
@@ -44,7 +27,7 @@
|
||||
|
||||
<!-- Custom theme stylesheets -->
|
||||
{{#each additional_css}}
|
||||
<link rel="stylesheet" href="{{ ../path_to_root }}{{ this }}">
|
||||
<link rel="stylesheet" href="{{ this }}">
|
||||
{{/each}}
|
||||
|
||||
{{#if mathjax_support}}
|
||||
@@ -52,12 +35,9 @@
|
||||
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{{/if}}
|
||||
</head>
|
||||
<body>
|
||||
<body class="light">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script type="text/javascript">
|
||||
var path_to_root = "{{ path_to_root }}";
|
||||
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
|
||||
</script>
|
||||
<script type="text/javascript">var path_to_root = "{{ path_to_root }}";</script>
|
||||
|
||||
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||
<script type="text/javascript">
|
||||
@@ -78,13 +58,10 @@
|
||||
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||
<script type="text/javascript">
|
||||
var theme;
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||
var html = document.querySelector('html');
|
||||
html.classList.remove('no-js')
|
||||
html.classList.remove('{{ default_theme }}')
|
||||
html.classList.add(theme);
|
||||
html.classList.add('js');
|
||||
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||
if (theme === null || theme === undefined) { theme = 'light'; }
|
||||
document.body.className = theme;
|
||||
document.querySelector('html').className = theme + ' js';
|
||||
</script>
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
@@ -100,50 +77,43 @@
|
||||
</script>
|
||||
|
||||
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||
<div class="sidebar-scrollbox">
|
||||
{{#toc}}{{/toc}}
|
||||
</div>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||
{{#toc}}{{/toc}}
|
||||
</nav>
|
||||
|
||||
<div id="page-wrapper" class="page-wrapper">
|
||||
|
||||
<div class="page">
|
||||
{{> header}}
|
||||
<div id="menu-bar-hover-placeholder"></div>
|
||||
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||
<div class="left-buttons">
|
||||
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
</button>
|
||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="light">{{ theme_option "Light" }}</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="rust">{{ theme_option "Rust" }}</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="coal">{{ theme_option "Coal" }}</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="navy">{{ theme_option "Navy" }}</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="ayu">{{ theme_option "Ayu" }}</button></li>
|
||||
</ul>
|
||||
{{#if search_enabled}}
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div id="menu-bar" class="menu-bar">
|
||||
<div id="menu-bar-sticky-container">
|
||||
<div class="left-buttons">
|
||||
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||
<i class="fa fa-paint-brush"></i>
|
||||
</button>
|
||||
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||
<li role="none"><button role="menuitem" class="theme" id="light">Light <span class="default">(default)</span></button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
|
||||
</ul>
|
||||
{{#if search_enabled}}
|
||||
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||
<i class="fa fa-search"></i>
|
||||
</button>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
<h1 class="menu-title">{{ book_title }}</h1>
|
||||
|
||||
<div class="right-buttons">
|
||||
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
|
||||
<i id="print-button" class="fa fa-print"></i>
|
||||
</a>
|
||||
{{#if git_repository_url}}
|
||||
<a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
|
||||
<i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
|
||||
</a>
|
||||
{{/if}}
|
||||
<div class="right-buttons">
|
||||
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
|
||||
<i id="print-button" class="fa fa-print"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -195,13 +165,13 @@
|
||||
|
||||
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||
{{#previous}}
|
||||
<a rel="prev" href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<a href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
</a>
|
||||
{{/previous}}
|
||||
|
||||
{{#next}}
|
||||
<a rel="next" href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<a href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
{{/next}}
|
||||
@@ -216,7 +186,7 @@
|
||||
socket.onmessage = function (event) {
|
||||
if (event.data === "reload") {
|
||||
socket.close();
|
||||
location.reload();
|
||||
location.reload(true); // force reload from server (not from cache)
|
||||
}
|
||||
};
|
||||
|
||||
@@ -245,19 +215,7 @@
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_line_numbers}}
|
||||
<script type="text/javascript">
|
||||
window.playground_line_numbers = true;
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_copyable}}
|
||||
<script type="text/javascript">
|
||||
window.playground_copyable = true;
|
||||
</script>
|
||||
{{/if}}
|
||||
|
||||
{{#if playground_js}}
|
||||
{{#if playpen_js}}
|
||||
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script>
|
||||
@@ -277,7 +235,7 @@
|
||||
|
||||
<!-- Custom JS scripts -->
|
||||
{{#each additional_js}}
|
||||
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script>
|
||||
<script type="text/javascript" src="{{ path_to_root }}{{this}}"></script>
|
||||
{{/each}}
|
||||
|
||||
{{#if is_print}}
|
||||
|
||||
123
src/theme/mod.rs
123
src/theme/mod.rs
@@ -1,8 +1,6 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
pub mod playground_editor;
|
||||
|
||||
pub mod fonts;
|
||||
pub mod playpen_editor;
|
||||
|
||||
#[cfg(feature = "search")]
|
||||
pub mod searcher;
|
||||
@@ -11,32 +9,33 @@ use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::errors::*;
|
||||
use errors::*;
|
||||
|
||||
pub static INDEX: &[u8] = include_bytes!("index.hbs");
|
||||
pub static HEAD: &[u8] = include_bytes!("head.hbs");
|
||||
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
|
||||
pub static HEADER: &[u8] = include_bytes!("header.hbs");
|
||||
pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
|
||||
pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
|
||||
pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
|
||||
pub static VARIABLES_CSS: &[u8] = include_bytes!("css/variables.css");
|
||||
pub static FAVICON_PNG: &[u8] = include_bytes!("favicon.png");
|
||||
pub static FAVICON_SVG: &[u8] = include_bytes!("favicon.svg");
|
||||
pub static JS: &[u8] = include_bytes!("book.js");
|
||||
pub static HIGHLIGHT_JS: &[u8] = include_bytes!("highlight.js");
|
||||
pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("tomorrow-night.css");
|
||||
pub static HIGHLIGHT_CSS: &[u8] = include_bytes!("highlight.css");
|
||||
pub static AYU_HIGHLIGHT_CSS: &[u8] = include_bytes!("ayu-highlight.css");
|
||||
pub static CLIPBOARD_JS: &[u8] = include_bytes!("clipboard.min.js");
|
||||
pub static FONT_AWESOME: &[u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
|
||||
pub static FONT_AWESOME_EOT: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
|
||||
pub static FONT_AWESOME_SVG: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &[u8] = include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
|
||||
pub static FONT_AWESOME_WOFF2: &[u8] =
|
||||
pub static INDEX: &'static [u8] = include_bytes!("index.hbs");
|
||||
pub static HEADER: &'static [u8] = include_bytes!("header.hbs");
|
||||
pub static CHROME_CSS: &'static [u8] = include_bytes!("css/chrome.css");
|
||||
pub static GENERAL_CSS: &'static [u8] = include_bytes!("css/general.css");
|
||||
pub static PRINT_CSS: &'static [u8] = include_bytes!("css/print.css");
|
||||
pub static VARIABLES_CSS: &'static [u8] = include_bytes!("css/variables.css");
|
||||
pub static FAVICON: &'static [u8] = include_bytes!("favicon.png");
|
||||
pub static JS: &'static [u8] = include_bytes!("book.js");
|
||||
pub static HIGHLIGHT_JS: &'static [u8] = include_bytes!("highlight.js");
|
||||
pub static TOMORROW_NIGHT_CSS: &'static [u8] = include_bytes!("tomorrow-night.css");
|
||||
pub static HIGHLIGHT_CSS: &'static [u8] = include_bytes!("highlight.css");
|
||||
pub static AYU_HIGHLIGHT_CSS: &'static [u8] = include_bytes!("ayu-highlight.css");
|
||||
pub static CLIPBOARD_JS: &'static [u8] = include_bytes!("clipboard.min.js");
|
||||
pub static FONT_AWESOME: &'static [u8] = include_bytes!("FontAwesome/css/font-awesome.min.css");
|
||||
pub static FONT_AWESOME_EOT: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.eot");
|
||||
pub static FONT_AWESOME_SVG: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.svg");
|
||||
pub static FONT_AWESOME_TTF: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.ttf");
|
||||
pub static FONT_AWESOME_WOFF: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff");
|
||||
pub static FONT_AWESOME_WOFF2: &'static [u8] =
|
||||
include_bytes!("FontAwesome/fonts/fontawesome-webfont.woff2");
|
||||
pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
|
||||
pub static FONT_AWESOME_OTF: &'static [u8] = include_bytes!("FontAwesome/fonts/FontAwesome.otf");
|
||||
|
||||
/// The `Theme` struct should be used instead of the static variables because
|
||||
/// the `new()` method will look if the user has a theme directory in their
|
||||
@@ -47,15 +46,12 @@ pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("FontAwesome/fonts/FontAweso
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Theme {
|
||||
pub index: Vec<u8>,
|
||||
pub head: Vec<u8>,
|
||||
pub redirect: Vec<u8>,
|
||||
pub header: Vec<u8>,
|
||||
pub chrome_css: Vec<u8>,
|
||||
pub general_css: Vec<u8>,
|
||||
pub print_css: Vec<u8>,
|
||||
pub variables_css: Vec<u8>,
|
||||
pub favicon_png: Option<Vec<u8>>,
|
||||
pub favicon_svg: Option<Vec<u8>>,
|
||||
pub favicon: Vec<u8>,
|
||||
pub js: Vec<u8>,
|
||||
pub highlight_css: Vec<u8>,
|
||||
pub tomorrow_night_css: Vec<u8>,
|
||||
@@ -80,8 +76,6 @@ impl Theme {
|
||||
{
|
||||
let files = vec![
|
||||
(theme_dir.join("index.hbs"), &mut theme.index),
|
||||
(theme_dir.join("head.hbs"), &mut theme.head),
|
||||
(theme_dir.join("redirect.hbs"), &mut theme.redirect),
|
||||
(theme_dir.join("header.hbs"), &mut theme.header),
|
||||
(theme_dir.join("book.js"), &mut theme.js),
|
||||
(theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
|
||||
@@ -91,6 +85,7 @@ impl Theme {
|
||||
theme_dir.join("css/variables.css"),
|
||||
&mut theme.variables_css,
|
||||
),
|
||||
(theme_dir.join("favicon.png"), &mut theme.favicon),
|
||||
(theme_dir.join("highlight.js"), &mut theme.highlight_js),
|
||||
(theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
|
||||
(theme_dir.join("highlight.css"), &mut theme.highlight_css),
|
||||
@@ -104,36 +99,13 @@ impl Theme {
|
||||
),
|
||||
];
|
||||
|
||||
let load_with_warn = |filename: &Path, dest| {
|
||||
if !filename.exists() {
|
||||
// Don't warn if the file doesn't exist.
|
||||
return false;
|
||||
}
|
||||
if let Err(e) = load_file_contents(filename, dest) {
|
||||
warn!("Couldn't load custom file, {}: {}", filename.display(), e);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
for (filename, dest) in files {
|
||||
load_with_warn(&filename, dest);
|
||||
}
|
||||
|
||||
// If the user overrides one favicon, but not the other, do not
|
||||
// copy the default for the other.
|
||||
let favicon_png = &mut theme.favicon_png.as_mut().unwrap();
|
||||
let png = load_with_warn(&theme_dir.join("favicon.png"), favicon_png);
|
||||
let favicon_svg = &mut theme.favicon_svg.as_mut().unwrap();
|
||||
let svg = load_with_warn(&theme_dir.join("favicon.svg"), favicon_svg);
|
||||
match (png, svg) {
|
||||
(true, true) | (false, false) => {}
|
||||
(true, false) => {
|
||||
theme.favicon_svg = None;
|
||||
if !filename.exists() {
|
||||
continue;
|
||||
}
|
||||
(false, true) => {
|
||||
theme.favicon_png = None;
|
||||
|
||||
if let Err(e) = load_file_contents(&filename, dest) {
|
||||
warn!("Couldn't load custom file, {}: {}", filename.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,15 +118,12 @@ impl Default for Theme {
|
||||
fn default() -> Theme {
|
||||
Theme {
|
||||
index: INDEX.to_owned(),
|
||||
head: HEAD.to_owned(),
|
||||
redirect: REDIRECT.to_owned(),
|
||||
header: HEADER.to_owned(),
|
||||
chrome_css: CHROME_CSS.to_owned(),
|
||||
general_css: GENERAL_CSS.to_owned(),
|
||||
print_css: PRINT_CSS.to_owned(),
|
||||
variables_css: VARIABLES_CSS.to_owned(),
|
||||
favicon_png: Some(FAVICON_PNG.to_owned()),
|
||||
favicon_svg: Some(FAVICON_SVG.to_owned()),
|
||||
favicon: FAVICON.to_owned(),
|
||||
js: JS.to_owned(),
|
||||
highlight_css: HIGHLIGHT_CSS.to_owned(),
|
||||
tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
|
||||
@@ -203,13 +172,9 @@ mod tests {
|
||||
fn theme_dir_overrides_defaults() {
|
||||
let files = [
|
||||
"index.hbs",
|
||||
"head.hbs",
|
||||
"redirect.hbs",
|
||||
"header.hbs",
|
||||
"favicon.png",
|
||||
"favicon.svg",
|
||||
"css/chrome.css",
|
||||
"css/fonts.css",
|
||||
"css/general.css",
|
||||
"css/print.css",
|
||||
"css/variables.css",
|
||||
@@ -233,15 +198,12 @@ mod tests {
|
||||
|
||||
let empty = Theme {
|
||||
index: Vec::new(),
|
||||
head: Vec::new(),
|
||||
redirect: Vec::new(),
|
||||
header: Vec::new(),
|
||||
chrome_css: Vec::new(),
|
||||
general_css: Vec::new(),
|
||||
print_css: Vec::new(),
|
||||
variables_css: Vec::new(),
|
||||
favicon_png: Some(Vec::new()),
|
||||
favicon_svg: Some(Vec::new()),
|
||||
favicon: Vec::new(),
|
||||
js: Vec::new(),
|
||||
highlight_css: Vec::new(),
|
||||
tomorrow_night_css: Vec::new(),
|
||||
@@ -252,19 +214,4 @@ mod tests {
|
||||
|
||||
assert_eq!(got, empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn favicon_override() {
|
||||
let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
|
||||
fs::write(temp.path().join("favicon.png"), "1234").unwrap();
|
||||
let got = Theme::new(temp.path());
|
||||
assert_eq!(got.favicon_png.as_ref().unwrap(), b"1234");
|
||||
assert_eq!(got.favicon_svg, None);
|
||||
|
||||
let temp = TempFileBuilder::new().prefix("mdbook-").tempdir().unwrap();
|
||||
fs::write(temp.path().join("favicon.svg"), "4567").unwrap();
|
||||
let got = Theme::new(temp.path());
|
||||
assert_eq!(got.favicon_png, None);
|
||||
assert_eq!(got.favicon_svg.as_ref().unwrap(), b"4567");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +0,0 @@
|
||||
//! Theme dependencies for the playground editor.
|
||||
|
||||
pub static JS: &[u8] = include_bytes!("editor.js");
|
||||
pub static ACE_JS: &[u8] = include_bytes!("ace.js");
|
||||
pub static MODE_RUST_JS: &[u8] = include_bytes!("mode-rust.js");
|
||||
pub static THEME_DAWN_JS: &[u8] = include_bytes!("theme-dawn.js");
|
||||
pub static THEME_TOMORROW_NIGHT_JS: &[u8] = include_bytes!("theme-tomorrow_night.js");
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user