Compare commits
265 Commits
revert-200
...
master
Author | SHA1 | Date |
---|---|---|
Eric Huss | 2420919ca8 | |
Eric Huss | c671c2e904 | |
Janik H. | c9df8dd1f3 | |
Eric Huss | 8ae86d4310 | |
Johannes Gloeckle | c144c26dcf | |
Eric Huss | 481f6b1531 | |
dependabot[bot] | b267d56ba7 | |
Eric Huss | dd139f8228 | |
dependabot[bot] | be4756e4bf | |
Eric Huss | bd323fb930 | |
goodmost | aff1070f43 | |
Eric Huss | b6742e90b1 | |
Max Heller | 95b6ed7965 | |
Eric Huss | 5a35144d4f | |
Eric Huss | 5f5f9d6fd5 | |
Eric Huss | c602a2fcd6 | |
_ | 821d3c423c | |
Eric Huss | 6b89f5dad8 | |
Eric Huss | d28cf53009 | |
Eric Huss | 504900d7bd | |
Eric Huss | 0cc439eee3 | |
Eric Huss | e8b8f34f2b | |
Wil Wade | 58a23e06a1 | |
Eric Huss | 5a4ac03c0d | |
Eric Huss | c5a506e240 | |
Eric Huss | bc5cd13c16 | |
sspaeti | d406c7c09b | |
Eric Huss | 9cf3117636 | |
Eric Huss | 61786ddcdf | |
Eric Huss | f33281fae2 | |
Eric Huss | 93bd457a54 | |
Eric Huss | 600824bed2 | |
Eric Huss | 42e635bb9e | |
Eric Huss | d48810f045 | |
Eric Huss | 3387cf373d | |
Eric Huss | 7825bd6c5a | |
Jvst Me | ba14f4ad53 | |
Eric Huss | 02bbc3f777 | |
gibbz00 | 45a2d0b40e | |
Eric Huss | 53eccf7047 | |
Joe Taber | 63000bc122 | |
Eric Huss | 220cb4f0c8 | |
GeckoEidechse | 7ce3a41184 | |
Eric Huss | 51efaf2e81 | |
dependabot[bot] | f0d6d428dc | |
Eric Huss | 01778fc90a | |
dependabot[bot] | d9928ad3f9 | |
Eric Huss | 77b7876986 | |
klensy | 745f7c7313 | |
Eric Huss | 0a96d0e3fa | |
klensy | e3ad9d097e | |
klensy | 573b6522f9 | |
Eric Huss | 59d3717159 | |
Ning Sun | a42eafc316 | |
Eric Huss | 11f839b9e5 | |
Max Heller | 721274239a | |
Eric Huss | 090eba0db5 | |
klensy | 88be4ac417 | |
Dylan DPC | c1d622e56e | |
Jonathan Hult | 91af1c3b54 | |
Stephen Checkoway | 32687e64fe | |
Eric Huss | b7f46213c7 | |
Eric Huss | aa8982bdb4 | |
Eric Huss | 14826db606 | |
Eric Huss | 847a582022 | |
Eric Huss | 97cd00faeb | |
Eric Huss | 8d4193fb46 | |
leonzchang | 8d4ae388fa | |
leonzchang | 7082689866 | |
leonzchang | 40c034ed3f | |
Dylan DPC | 208d5ea7ab | |
DuckDuckWhale | ed51438c8b | |
Eric Huss | 49fce6673a | |
Eric Huss | a016ac0d2b | |
Eric Huss | ad55f5367e | |
Eric Huss | 660cbfa6ce | |
Joseph Perez | 982608246e | |
Dylan DPC | 6f6de2cf05 | |
Szymon Pilkowski | ae3e3f8269 | |
Eric Huss | dc21f1497b | |
Jacek Sieka | 5c8941ba16 | |
leonzchang | b0a001c6a4 | |
leonzchang | 722c55f85f | |
leonzchang | 3ab19f3295 | |
leonzchang | 621ffc46c0 | |
leonzchang | fbb629c02e | |
Evgeny Chaban | 80d3a86468 | |
Evgeny Chaban | 8e8fd2717e | |
Evgeny Chaban | f92d24e89c | |
Eric Huss | 94e0a44e15 | |
Eric Huss | f25181f68d | |
Eric Huss | cf19eb1386 | |
Eric Huss | 0583119698 | |
Dmitry Luschan | 3389f3db7f | |
Eric Huss | c642f5f8a3 | |
Michael Howell | ceb8b509e2 | |
Michael Howell | 65dae11e47 | |
Dylan DPC | d5b1676216 | |
Eric Huss | 09f222baf7 | |
Eric Huss | 802e7bffc3 | |
Eric Huss | fb272d1afa | |
Eric Huss | b871676def | |
Eric Huss | 869fe2f50d | |
Eric Huss | db877b1c9b | |
Eric Huss | 4749f9d97a | |
cN3rd | 8564a7fb51 | |
cN3rd | 6be98e0bbd | |
cN3rd | 5e0c68c45e | |
cN3rd | 7717b9dcf2 | |
cN3rd | 819a108f07 | |
Tim Crawford | 3a99899114 | |
Tim Crawford | 1088066c69 | |
Tim Crawford | 73d44503fd | |
Eric Huss | 25aaff0bd6 | |
cui fliter | 29691461c5 | |
Dylan DPC | a74e4dcec8 | |
Tshepang Mbambo | 0b0b548d7a | |
Dylan DPC | 02f3823e4c | |
qaqland | 36327efe9d | |
Aron Heinecke | 079f52a191 | |
Ivan Tham | c9f1d01346 | |
Eldred Habert | 9bc68bdd93 | |
Eric Huss | 56c225bd34 | |
Eric Huss | 55c017cad1 | |
Eric Huss | 7849d55b99 | |
Eric Huss | c903cc8827 | |
Eric Huss | 4a797b9565 | |
Eric Huss | 57b487eaa3 | |
Eric Huss | 891b7c06f2 | |
Eric Huss | f7e212ec9c | |
Pavel Roskin | 228538ea62 | |
Eric Huss | 347e7886e1 | |
Eric Huss | bfa5fb8844 | |
Eric Huss | a8fd6038f1 | |
Eric Huss | fbfe887084 | |
Eric Huss | aed991f75f | |
Eric Huss | ab2cb71c00 | |
Giorgio Reale | fcfde083e7 | |
Eric Huss | 4614a3637a | |
Eric Huss | d450544d6b | |
Eric Huss | 9340e6a78d | |
riverbl | e00b8835cc | |
Eric Huss | 429ca06289 | |
Eric Huss | 0fbfc90bea | |
Eric Huss | 581e5025a2 | |
Tshepang Mbambo | e57fce290b | |
Eric Huss | d5a3682de9 | |
Eric Huss | 75f5862218 | |
Eric Huss | aed518f945 | |
Eric Huss | e942d41c1d | |
Eric Huss | 38fcfd8732 | |
Eric Huss | 82ec68128d | |
Eric Huss | 9497354cfd | |
Eric Huss | baa936439d | |
Eric Huss | 394061d28d | |
Eric Huss | 0f25db67dc | |
Eric Huss | 49ba91961f | |
Eric Huss | 28ce772ae9 | |
Eric Huss | 424c2d9f6b | |
Eric Huss | 89797064b8 | |
Eric Huss | 7824aed878 | |
Eric Huss | 8236c43c90 | |
Eric Huss | 6df89fbe94 | |
Eric Huss | b423bf7ddd | |
Eric Huss | cdbdb8248c | |
Eric Huss | db45052d7e | |
Eric Huss | 804bbf6564 | |
Eric Huss | bd3b9bacf6 | |
Eric Huss | 5505d57066 | |
Eric Huss | cf88c4e720 | |
Eric Huss | 9911e86039 | |
zica | 9eba0f6ab2 | |
Antoine | 6d265c1cce | |
Eric Huss | 904aa530b5 | |
Eric Huss | fa316f3edc | |
Eric Huss | 41d19e7338 | |
Eric Huss | 4f15a3f85c | |
Eric Huss | 222166ca5a | |
Eric Huss | ab3eb81e52 | |
Eric Huss | f37486a74f | |
Eric Huss | a38b854338 | |
Eric Huss | e18113a746 | |
Dylan DPC | d4edbd1acf | |
Caleb Robson | 056e45a003 | |
Em Zhan | 72b3227824 | |
Eric Huss | a51f8a6b8e | |
Em Zhan | 1ef8d70ac4 | |
Eric Huss | a204946d39 | |
Tshepang Mbambo | 3c7795cf44 | |
Eric Huss | 9349204636 | |
Eric Huss | d2bcd04133 | |
Eric Huss | 61708ad0bd | |
Eric Huss | c9cfe22fd6 | |
Eric Huss | 5572d3d4de | |
Eric Huss | 1441fe0b91 | |
Jannik Obermann | 7df1d8c838 | |
Eric Huss | 3a51abfcad | |
Eric Huss | 870e9086dc | |
Eric Huss | 1db52ff531 | |
Eric Huss | e3be293420 | |
Eric Huss | bbc32dff82 | |
Eric Huss | 861197e61c | |
Eric Huss | 34e5ef22a0 | |
Eric Huss | b141297651 | |
Uriel | 0cb977e603 | |
ImUrX | c8a5adcee9 | |
ImUrX | ecdb411711 | |
ImUrX | a4e206168d | |
Dylan DPC | 4f1b5eae54 | |
zjj | 54f14e89cf | |
Eric Huss | 1b3922d466 | |
Eric Huss | 00a30a9984 | |
Eric Huss | db6699dae2 | |
Eric Huss | 4d229d7b94 | |
Eric Huss | d94c5f8380 | |
Eric Huss | 099217390e | |
Eric Huss | 4c4ab8a57d | |
Eric Huss | d746b23749 | |
Eric Huss | f77c597e01 | |
Eric Huss | 3c54a4d33b | |
Eric Huss | cf9de82c2a | |
Eric Huss | c3155e2642 | |
Eric Huss | d8f171a996 | |
Eric Huss | 0ef3bb1cc6 | |
Eric Huss | 54df8234ed | |
Eric Huss | dc08e37320 | |
Eric Huss | 45a8575b95 | |
Eric Huss | be966cfe1f | |
Eric Huss | f4507aeb9b | |
Eric Huss | 0985691fbd | |
Eric Huss | 01047846a9 | |
Eric Huss | 75a6d65e5a | |
Eric Huss | 71ea92bbec | |
liutailin | aac6de01de | |
Eric Huss | af036d9f45 | |
Tetsuya Morimoto | 04016f3be6 | |
Dylan DPC | 41567b0456 | |
Eric Huss | 9db3a601ca | |
Eric Huss | 35fdd00203 | |
expikr | 7a435be018 | |
Eric Huss | dec0e24275 | |
dependabot[bot] | c624fc078b | |
Sean Poulter | b9c6b326b7 | |
Sean Poulter | 0003072623 | |
Sean Poulter | bffdb0b03d | |
Eric Huss | b5ffc734a2 | |
Andreas Deininger | a2c88ae0f1 | |
Eric Huss | efb671aaf2 | |
Eric Huss | a4b4b8f649 | |
Eric Huss | 4c59405e5c | |
Eric Huss | 703a215ef8 | |
Skwodo | f5f96bc4f4 | |
Eric Huss | 1668ab7877 | |
Tshepang Mbambo | 26fc0da9a9 | |
Eric Huss | c15220d1a1 | |
Eric Huss | 7c4562a8b3 | |
Eric Huss | 6e3176f726 | |
Eric Huss | 958b456873 | |
Eric Huss | a43b5b69ab | |
Eric Huss | 1517435441 | |
Eric Huss | 7abb28cb2e | |
Eric Huss | 112fd4aac3 | |
Martin Geisler | c150529c7c | |
Roy Wellington Ⅳ | fa6aa2ced8 | |
Martin Geisler | b09aa0e65c |
|
@ -3,6 +3,13 @@ on:
|
|||
release:
|
||||
types: [created]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Deploy Release
|
||||
|
@ -28,17 +35,14 @@ jobs:
|
|||
os: 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 ${{ matrix.target }}
|
||||
shell: bash
|
||||
- name: Build and deploy artifacts
|
||||
- name: Build asset
|
||||
run: ci/make-release-asset.sh ${{ matrix.os }} ${{ matrix.target }}
|
||||
- name: Update release with new asset
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ci/make-release.sh ${{ matrix.os }} ${{ matrix.target }}
|
||||
shell: bash
|
||||
run: gh release upload $MDBOOK_TAG $MDBOOK_ASSET
|
||||
pages:
|
||||
name: GitHub Pages
|
||||
runs-on: ubuntu-latest
|
||||
|
@ -56,3 +60,14 @@ jobs:
|
|||
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
|
||||
cd guide/book
|
||||
/tmp/deploy
|
||||
publish:
|
||||
name: Publish to crates.io
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Install Rust (rustup)
|
||||
run: rustup update stable --no-self-update && rustup default stable
|
||||
- name: Publish
|
||||
env:
|
||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||
run: cargo publish --no-verify
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
name: CI
|
||||
on:
|
||||
# Only run when merging to master, or open/synchronize/reopen a PR.
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
merge_group:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
@ -32,13 +29,13 @@ jobs:
|
|||
- build: msrv
|
||||
os: ubuntu-20.04
|
||||
# sync MSRV with docs: guide/src/guide/installation.md and Cargo.toml
|
||||
rust: 1.60.0
|
||||
rust: 1.71.0
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
run: bash ci/install-rust.sh ${{ matrix.rust }}
|
||||
- name: Build and run tests
|
||||
run: cargo test
|
||||
run: cargo test --locked
|
||||
- name: Test no default
|
||||
run: cargo test --no-default-features
|
||||
|
||||
|
@ -46,7 +43,24 @@ jobs:
|
|||
name: Rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Rust
|
||||
run: rustup update stable && rustup default stable && rustup component add rustfmt
|
||||
- run: cargo fmt --check
|
||||
|
||||
# The success job is here to consolidate the total success/failure state of
|
||||
# all other jobs. This job is then included in the GitHub branch protection
|
||||
# rule which prevents merges unless all other jobs are passing. This makes
|
||||
# it easier to manage the list of jobs via this yml file and to prevent
|
||||
# accidentally adding new jobs without also updating the branch protections.
|
||||
success:
|
||||
name: Success gate
|
||||
if: always()
|
||||
needs:
|
||||
- test
|
||||
- rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}'
|
||||
- name: Done
|
||||
run: exit 0
|
||||
|
|
174
CHANGELOG.md
174
CHANGELOG.md
|
@ -1,8 +1,180 @@
|
|||
# Changelog
|
||||
|
||||
## mdBook 0.4.37
|
||||
[v0.4.36...v0.4.37](https://github.com/rust-lang/mdBook/compare/v0.4.36...v0.4.37)
|
||||
|
||||
### Changed
|
||||
- ❗️ Updated the markdown parser. This brings in many changes to more closely follow the CommonMark spec. This may cause some small rendering changes. It is recommended to compare the output of the old and new version to check for changes. See <https://github.com/raphlinus/pulldown-cmark/releases/tag/v0.10.0> for more information.
|
||||
[#2308](https://github.com/rust-lang/mdBook/pull/2308)
|
||||
- The warning about the legacy `src/theme` directory has been removed.
|
||||
[#2263](https://github.com/rust-lang/mdBook/pull/2263)
|
||||
- Updated dependencies. MSRV raised to 1.71.0.
|
||||
[#2283](https://github.com/rust-lang/mdBook/pull/2283)
|
||||
[#2293](https://github.com/rust-lang/mdBook/pull/2293)
|
||||
[#2297](https://github.com/rust-lang/mdBook/pull/2297)
|
||||
[#2310](https://github.com/rust-lang/mdBook/pull/2310)
|
||||
[#2309](https://github.com/rust-lang/mdBook/pull/2309)
|
||||
- Some internal performance/memory improvements.
|
||||
[#2273](https://github.com/rust-lang/mdBook/pull/2273)
|
||||
[#2290](https://github.com/rust-lang/mdBook/pull/2290)
|
||||
- Made the `pathdiff` dependency optional based on the `watch` feature.
|
||||
[#2291](https://github.com/rust-lang/mdBook/pull/2291)
|
||||
|
||||
### Fixed
|
||||
- The `s` shortcut key handler should not trigger when focus is in an HTML form.
|
||||
[#2311](https://github.com/rust-lang/mdBook/pull/2311)
|
||||
|
||||
## mdBook 0.4.36
|
||||
[v0.4.35...v0.4.36](https://github.com/rust-lang/mdBook/compare/v0.4.35...v0.4.36)
|
||||
|
||||
### Added
|
||||
- Added Nim to the default highlighted languages.
|
||||
[#2232](https://github.com/rust-lang/mdBook/pull/2232)
|
||||
- Added a small indicator for the sidebar resize handle.
|
||||
[#2209](https://github.com/rust-lang/mdBook/pull/2209)
|
||||
|
||||
### Changed
|
||||
- Updated dependencies. MSRV raised to 1.70.0.
|
||||
[#2173](https://github.com/rust-lang/mdBook/pull/2173)
|
||||
[#2250](https://github.com/rust-lang/mdBook/pull/2250)
|
||||
[#2252](https://github.com/rust-lang/mdBook/pull/2252)
|
||||
|
||||
### Fixed
|
||||
- Fixed blank column in print page when the sidebar was visible.
|
||||
[#2235](https://github.com/rust-lang/mdBook/pull/2235)
|
||||
- Fixed indentation of code blocks when Javascript is disabled.
|
||||
[#2162](https://github.com/rust-lang/mdBook/pull/2162)
|
||||
- Fixed a panic when `mdbook serve` or `mdbook watch` were given certain kinds of paths.
|
||||
[#2229](https://github.com/rust-lang/mdBook/pull/2229)
|
||||
|
||||
## mdBook 0.4.35
|
||||
[v0.4.34...v0.4.35](https://github.com/rust-lang/mdBook/compare/v0.4.34...v0.4.35)
|
||||
|
||||
### Added
|
||||
- Added the `book.text-direction` setting for explicit support for right-to-left languages.
|
||||
[#1641](https://github.com/rust-lang/mdBook/pull/1641)
|
||||
- Added `rel=prefetch` to the "next" links to potentially improve browser performance.
|
||||
[#2168](https://github.com/rust-lang/mdBook/pull/2168)
|
||||
- Added a `.warning` CSS class which is styled for displaying warning blocks.
|
||||
[#2187](https://github.com/rust-lang/mdBook/pull/2187)
|
||||
|
||||
### Changed
|
||||
- Better support of the sidebar when JavaScript is disabled.
|
||||
[#2175](https://github.com/rust-lang/mdBook/pull/2175)
|
||||
|
||||
## mdBook 0.4.34
|
||||
[v0.4.33...v0.4.34](https://github.com/rust-lang/mdBook/compare/v0.4.33...v0.4.34)
|
||||
|
||||
### Fixed
|
||||
- Fixed file change watcher failing on macOS with a large number of files.
|
||||
[#2157](https://github.com/rust-lang/mdBook/pull/2157)
|
||||
|
||||
## mdBook 0.4.33
|
||||
[v0.4.32...v0.4.33](https://github.com/rust-lang/mdBook/compare/v0.4.32...v0.4.33)
|
||||
|
||||
### Added
|
||||
- The `color-scheme` CSS property is now set based on the light/dark theme, which applies some slight color differences in browser elements like scroll bars on some browsers.
|
||||
[#2134](https://github.com/rust-lang/mdBook/pull/2134)
|
||||
|
||||
### Fixed
|
||||
- Fixed watching of extra-watch-dirs when not running in the book root directory.
|
||||
[#2146](https://github.com/rust-lang/mdBook/pull/2146)
|
||||
- Reverted the dependency update to the `toml` crate (again!). This was an unintentional breaking change in 0.4.32.
|
||||
[#2021](https://github.com/rust-lang/mdBook/pull/2021)
|
||||
- Changed macOS change notifications to use the kqueue implementation which should fix some issues with repeated rebuilds when a file changed.
|
||||
[#2152](https://github.com/rust-lang/mdBook/pull/2152)
|
||||
- Don't set a background color in the print page for code blocks in a header.
|
||||
[#2150](https://github.com/rust-lang/mdBook/pull/2150)
|
||||
|
||||
## mdBook 0.4.32
|
||||
[v0.4.31...v0.4.32](https://github.com/rust-lang/mdBook/compare/v0.4.31...v0.4.32)
|
||||
|
||||
### Fixed
|
||||
- Fixed theme-color meta tag not syncing with the theme.
|
||||
[#2118](https://github.com/rust-lang/mdBook/pull/2118)
|
||||
|
||||
### Changed
|
||||
- Updated all dependencies.
|
||||
[#2121](https://github.com/rust-lang/mdBook/pull/2121)
|
||||
[#2122](https://github.com/rust-lang/mdBook/pull/2122)
|
||||
[#2123](https://github.com/rust-lang/mdBook/pull/2123)
|
||||
[#2124](https://github.com/rust-lang/mdBook/pull/2124)
|
||||
[#2125](https://github.com/rust-lang/mdBook/pull/2125)
|
||||
[#2126](https://github.com/rust-lang/mdBook/pull/2126)
|
||||
|
||||
## mdBook 0.4.31
|
||||
[v0.4.30...v0.4.31](https://github.com/rust-lang/mdBook/compare/v0.4.30...v0.4.31)
|
||||
|
||||
### Fixed
|
||||
- Fixed menu border render flash during page navigation.
|
||||
[#2101](https://github.com/rust-lang/mdBook/pull/2101)
|
||||
- Fixed flicker setting sidebar scroll position.
|
||||
[#2104](https://github.com/rust-lang/mdBook/pull/2104)
|
||||
- Fixed compile error with proc-macro2 on latest Rust nightly.
|
||||
[#2109](https://github.com/rust-lang/mdBook/pull/2109)
|
||||
|
||||
## mdBook 0.4.30
|
||||
[v0.4.29...v0.4.30](https://github.com/rust-lang/mdBook/compare/v0.4.29...v0.4.30)
|
||||
|
||||
### Added
|
||||
- Added support for heading attributes.
|
||||
Attributes are specified in curly braces just after the heading text.
|
||||
An HTML ID can be specified with `#` and classes with `.`.
|
||||
For example: `## My heading {#custom-id .class1 .class2}`
|
||||
[#2013](https://github.com/rust-lang/mdBook/pull/2013)
|
||||
- Added support for hidden code lines for languages other than Rust.
|
||||
The `output.html.code.hidelines` table allows you to define the prefix character that will be used to hide code lines based on the language.
|
||||
[#2093](https://github.com/rust-lang/mdBook/pull/2093)
|
||||
|
||||
### Fixed
|
||||
- Fixed a few minor markdown rendering issues.
|
||||
[#2092](https://github.com/rust-lang/mdBook/pull/2092)
|
||||
|
||||
## mdBook 0.4.29
|
||||
[v0.4.28...v0.4.29](https://github.com/rust-lang/mdBook/compare/v0.4.28...v0.4.29)
|
||||
|
||||
### Changed
|
||||
- Built-in fonts are no longer copied when `fonts/fonts.css` is overridden in the theme directory.
|
||||
Additionally, the warning about `copy-fonts` has been removed if `fonts/fonts.css` is specified.
|
||||
[#2080](https://github.com/rust-lang/mdBook/pull/2080)
|
||||
- `mdbook init --force` now skips all interactive prompts as intended.
|
||||
[#2057](https://github.com/rust-lang/mdBook/pull/2057)
|
||||
- Updated dependencies
|
||||
[#2063](https://github.com/rust-lang/mdBook/pull/2063)
|
||||
[#2086](https://github.com/rust-lang/mdBook/pull/2086)
|
||||
[#2082](https://github.com/rust-lang/mdBook/pull/2082)
|
||||
[#2084](https://github.com/rust-lang/mdBook/pull/2084)
|
||||
[#2085](https://github.com/rust-lang/mdBook/pull/2085)
|
||||
|
||||
### Fixed
|
||||
- Switched from the `gitignore` library to `ignore`. This should bring some improvements with gitignore handling.
|
||||
[#2076](https://github.com/rust-lang/mdBook/pull/2076)
|
||||
|
||||
## mdBook 0.4.28
|
||||
[v0.4.27...v0.4.28](https://github.com/rust-lang/mdBook/compare/v0.4.27...v0.4.28)
|
||||
|
||||
### Changed
|
||||
- The sidebar is now shown on wide screens when localstorage is disabled.
|
||||
[#2017](https://github.com/rust-lang/mdBook/pull/2017)
|
||||
- Preprocessors are now run with `mdbook test`.
|
||||
[#1986](https://github.com/rust-lang/mdBook/pull/1986)
|
||||
|
||||
### Fixed
|
||||
- Fixed regression in 0.4.26 that prevented the title bar from scrolling properly on smaller screens.
|
||||
[#2039](https://github.com/rust-lang/mdBook/pull/2039)
|
||||
|
||||
## mdBook 0.4.27
|
||||
[v0.4.26...v0.4.27](https://github.com/rust-lang/mdBook/compare/v0.4.26...v0.4.27)
|
||||
|
||||
### Changed
|
||||
- Reverted the dependency update to the `toml` crate. This was an unintentional breaking change in 0.4.26.
|
||||
[#2021](https://github.com/rust-lang/mdBook/pull/2021)
|
||||
|
||||
## mdBook 0.4.26
|
||||
[v0.4.25...v0.4.26](https://github.com/rust-lang/mdBook/compare/v0.4.25...v0.4.26)
|
||||
|
||||
**The 0.4.26 release has been yanked due to an unintentional breaking change.**
|
||||
|
||||
### Changed
|
||||
- Removed custom scrollbars for webkit browsers
|
||||
[#1961](https://github.com/rust-lang/mdBook/pull/1961)
|
||||
|
@ -154,7 +326,7 @@
|
|||
[#1771](https://github.com/rust-lang/mdBook/pull/1771)
|
||||
- The 404 not-found page now includes the books title in the HTML title tag.
|
||||
[#1693](https://github.com/rust-lang/mdBook/pull/1693)
|
||||
- Migrated to clap 3.0 which which handles CLI option parsing.
|
||||
- Migrated to clap 3.0 which handles CLI option parsing.
|
||||
[#1731](https://github.com/rust-lang/mdBook/pull/1731)
|
||||
|
||||
### Fixed
|
||||
|
|
|
@ -148,8 +148,28 @@ The following are instructions for updating [highlight.js](https://highlightjs.o
|
|||
1. Clone the repository at <https://github.com/highlightjs/highlight.js>
|
||||
1. Check out a tagged release (like `10.1.1`).
|
||||
1. Run `npm install`
|
||||
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx properties r scala x86asm yaml`
|
||||
1. Run `node tools/build.js :common apache armasm coffeescript d handlebars haskell http julia nginx nim nix properties r scala x86asm yaml`
|
||||
1. Compare the language list that it spits out to the one in [`syntax-highlighting.md`](https://github.com/camelid/mdBook/blob/master/guide/src/format/theme/syntax-highlighting.md). If any are missing, add them to the list and rebuild (and update these docs). If any are added to the common set, add them to `syntax-highlighting.md`.
|
||||
1. Copy `build/highlight.min.js` to mdbook's directory [`highlight.js`](https://github.com/rust-lang/mdBook/blob/master/src/theme/highlight.js).
|
||||
1. Be sure to check the highlight.js [CHANGES](https://github.com/highlightjs/highlight.js/blob/main/CHANGES.md) for any breaking changes. Breaking changes that would affect users will need to wait until the next major release.
|
||||
1. Build mdbook with the new file and build some books with the new version and compare the output with a variety of languages to see if anything changes. The [test_book](https://github.com/rust-lang/mdBook/tree/master/test_book) contains a chapter with many languages to examine.
|
||||
|
||||
## Publishing new releases
|
||||
|
||||
Instructions for mdBook maintainers to publish a new release:
|
||||
|
||||
1. Create a PR to update the version and update the CHANGELOG:
|
||||
1. Update the version in `Cargo.toml`
|
||||
2. Run `cargo test` to verify that everything is passing, and to update `Cargo.lock`.
|
||||
3. Double-check for any SemVer breaking changes.
|
||||
Try [`cargo-semver-checks`](https://crates.io/crates/cargo-semver-checks), though beware that the current version of mdBook isn't properly adhering to SemVer due to the lack of `#[non_exhaustive]` and other issues. See https://github.com/rust-lang/mdBook/issues/1835.
|
||||
4. Update `CHANGELOG.md` with any changes that users may be interested in.
|
||||
5. Update `continuous-integration.md` to update the version number for the installation instructions.
|
||||
6. Commit the changes, and open a PR.
|
||||
2. After the PR has been merged, create a release in GitHub. This can either be done in the GitHub web UI, or on the command-line:
|
||||
```bash
|
||||
MDBOOK_VERS="`cargo read-manifest | jq -r .version`" ; \
|
||||
gh release create -R rust-lang/mdbook v$MDBOOK_VERS \
|
||||
--title v$MDBOOK_VERS \
|
||||
--notes "See https://github.com/rust-lang/mdBook/blob/master/CHANGELOG.md#mdbook-${MDBOOK_VERS//.} for a complete list of changes."
|
||||
```
|
||||
|
|
File diff suppressed because it is too large
Load Diff
71
Cargo.toml
71
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "mdbook"
|
||||
version = "0.4.26"
|
||||
version = "0.4.37"
|
||||
authors = [
|
||||
"Mathieu David <mathieudavid@mathieudavid.org>",
|
||||
"Michael-F-Bryan <michaelfbryan@gmail.com>",
|
||||
|
@ -14,55 +14,56 @@ license = "MPL-2.0"
|
|||
readme = "README.md"
|
||||
repository = "https://github.com/rust-lang/mdBook"
|
||||
description = "Creates a book from markdown files"
|
||||
rust-version = "1.60"
|
||||
rust-version = "1.71"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.28"
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.0.29", features = ["cargo", "wrap_help"] }
|
||||
clap_complete = "4.0.6"
|
||||
once_cell = "1"
|
||||
env_logger = "0.10.0"
|
||||
handlebars = "4.0"
|
||||
log = "0.4"
|
||||
memchr = "2.0"
|
||||
opener = "0.5"
|
||||
pulldown-cmark = { version = "0.9.1", default-features = false }
|
||||
regex = "1.5.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
shlex = "1"
|
||||
tempfile = "3.0"
|
||||
toml = "0.5.1"
|
||||
anyhow = "1.0.71"
|
||||
chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.3.12", features = ["cargo", "wrap_help"] }
|
||||
clap_complete = "4.3.2"
|
||||
once_cell = "1.17.1"
|
||||
env_logger = "0.11.1"
|
||||
handlebars = "5.0"
|
||||
log = "0.4.17"
|
||||
memchr = "2.5.0"
|
||||
opener = "0.6.1"
|
||||
pulldown-cmark = { version = "0.10.0", default-features = false, features = ["html"] }
|
||||
regex = "1.8.1"
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
shlex = "1.3.0"
|
||||
tempfile = "3.4.0"
|
||||
toml = "0.5.11" # Do not update, see https://github.com/rust-lang/mdBook/issues/2037
|
||||
topological-sort = "0.2.2"
|
||||
|
||||
# Watch feature
|
||||
notify = { version = "5.0.0", optional = true }
|
||||
notify-debouncer-mini = { version = "0.2.1", optional = true }
|
||||
gitignore = { version = "1.0", optional = true }
|
||||
notify = { version = "6.1.1", optional = true }
|
||||
notify-debouncer-mini = { version = "0.4.1", optional = true }
|
||||
ignore = { version = "0.4.20", optional = true }
|
||||
pathdiff = { version = "0.2.1", optional = true }
|
||||
|
||||
# Serve feature
|
||||
futures-util = { version = "0.3.4", optional = true }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
warp = { version = "0.3.2", default-features = false, features = ["websocket"], optional = true }
|
||||
futures-util = { version = "0.3.28", optional = true }
|
||||
tokio = { version = "1.28.1", features = ["macros", "rt-multi-thread"], optional = true }
|
||||
warp = { version = "0.3.6", default-features = false, features = ["websocket"], optional = true }
|
||||
|
||||
# Search feature
|
||||
elasticlunr-rs = { version = "3.0.0", optional = true }
|
||||
ammonia = { version = "3", optional = true }
|
||||
elasticlunr-rs = { version = "3.0.2", optional = true }
|
||||
ammonia = { version = "3.3.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "2.0.7"
|
||||
predicates = "2"
|
||||
assert_cmd = "2.0.11"
|
||||
predicates = "3.0.3"
|
||||
select = "0.6.0"
|
||||
semver = "1.0"
|
||||
pretty_assertions = "1.2.1"
|
||||
walkdir = "2.0"
|
||||
semver = "1.0.17"
|
||||
pretty_assertions = "1.3.0"
|
||||
walkdir = "2.3.3"
|
||||
|
||||
[features]
|
||||
default = ["watch", "serve", "search"]
|
||||
watch = ["notify", "notify-debouncer-mini", "gitignore"]
|
||||
serve = ["futures-util", "tokio", "warp"]
|
||||
search = ["elasticlunr-rs", "ammonia"]
|
||||
watch = ["dep:notify", "dep:notify-debouncer-mini", "dep:ignore", "dep:pathdiff"]
|
||||
serve = ["dep:futures-util", "dep:tokio", "dep:warp"]
|
||||
search = ["dep:elasticlunr-rs", "dep:ammonia"]
|
||||
|
||||
[[bin]]
|
||||
doc = false
|
||||
|
|
|
@ -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 "$PWD/hub/bin" >> $GITHUB_PATH
|
|
@ -17,7 +17,7 @@ then
|
|||
export "CARGO_TARGET_$(echo $target | tr a-z- A-Z_)_LINKER"=rust-lld
|
||||
fi
|
||||
export CARGO_PROFILE_RELEASE_LTO=true
|
||||
cargo build --bin mdbook --release --target $target
|
||||
cargo build --locked --bin mdbook --release --target $target
|
||||
cd target/$target/release
|
||||
case $1 in
|
||||
ubuntu*)
|
||||
|
@ -44,9 +44,10 @@ case $1 in
|
|||
esac
|
||||
cd ../..
|
||||
|
||||
if [[ -z "$GITHUB_TOKEN" ]]
|
||||
if [[ -z "$GITHUB_ENV" ]]
|
||||
then
|
||||
echo "$GITHUB_TOKEN not set, skipping deploy."
|
||||
echo "GITHUB_ENV not set, run: gh release upload $TAG target/$asset"
|
||||
else
|
||||
hub release edit -m "" --attach $asset $TAG
|
||||
echo "MDBOOK_TAG=$TAG" >> $GITHUB_ENV
|
||||
echo "MDBOOK_ASSET=target/$asset" >> $GITHUB_ENV
|
||||
fi
|
|
@ -17,6 +17,9 @@ edit-url-template = "https://github.com/rust-lang/mdBook/edit/master/guide/{path
|
|||
editable = true
|
||||
line-numbers = true
|
||||
|
||||
[output.html.code.hidelines]
|
||||
python = "~"
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 20
|
||||
use-boolean-and = true
|
||||
|
|
|
@ -7,8 +7,8 @@ mdbook build
|
|||
```
|
||||
|
||||
It will try to parse your `SUMMARY.md` file to understand the structure of your
|
||||
book and fetch the corresponding files. Note that files mentioned in `SUMMARY.md`
|
||||
but not present will be created.
|
||||
book and fetch the corresponding files. Note that this will also create files
|
||||
mentioned in `SUMMARY.md` which are not yet present.
|
||||
|
||||
The rendered output will maintain the same directory structure as the source for
|
||||
convenience. Large books will therefore remain structured when rendered.
|
||||
|
|
|
@ -6,7 +6,11 @@ This means when you type `mdbook` in your shell, you can then press your shell's
|
|||
The completions first need to be installed for your shell:
|
||||
|
||||
```bash
|
||||
# bash
|
||||
mdbook completions bash > ~/.local/share/bash-completion/completions/mdbook
|
||||
# oh-my-zsh
|
||||
mdbook completions zsh > ~/.oh-my-zsh/completions/_mdbook
|
||||
autoload -U compinit && compinit
|
||||
```
|
||||
|
||||
The command prints a completion script for the given shell.
|
||||
|
|
|
@ -67,4 +67,16 @@ mdbook init --title="my amazing book"
|
|||
Create a `.gitignore` file configured to ignore the `book` directory created when [building] a book.
|
||||
If not supplied, an interactive prompt will ask whether it should be created.
|
||||
|
||||
```bash
|
||||
mdbook init --ignore=none
|
||||
```
|
||||
|
||||
```bash
|
||||
mdbook init --ignore=git
|
||||
```
|
||||
|
||||
[building]: build.md
|
||||
|
||||
#### --force
|
||||
|
||||
Skip the prompts to create a `.gitignore` and for the title for the book.
|
||||
|
|
|
@ -6,8 +6,7 @@ of code examples that could get outdated. Therefore it is very important for
|
|||
them to be able to automatically test these code examples.
|
||||
|
||||
mdBook supports a `test` command that will run all available tests in a book. At
|
||||
the moment, only rustdoc tests are supported, but this may be expanded upon in
|
||||
the future.
|
||||
the moment, only Rust tests are supported.
|
||||
|
||||
#### Disable tests on a code block
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ A simple approach would be to use the popular `curl` CLI tool to download the ex
|
|||
|
||||
```sh
|
||||
mkdir bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.26/mdbook-v0.4.26-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.37/mdbook-v0.4.37-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=bin
|
||||
bin/mdbook build
|
||||
```
|
||||
|
||||
|
@ -83,7 +83,7 @@ Or if you have your own style checks, spell checker, or any other tests it might
|
|||
## Deploying
|
||||
|
||||
You may want to automatically deploy your book.
|
||||
Some may want to do this with every time a change is pushed, and others may want to only deploy when a specific release is tagged.
|
||||
Some may want to do this every time a change is pushed, and others may want to only deploy when a specific release is tagged.
|
||||
|
||||
You'll also need to understand the specifics on how to push a change to your web service.
|
||||
For example, [GitHub Pages] just requires committing the output onto a specific git branch.
|
||||
|
|
|
@ -287,7 +287,7 @@ like this:
|
|||
+ if cfg.deny_odds && num_words % 2 == 1 {
|
||||
+ eprintln!("{} has an odd number of words!", ch.name);
|
||||
+ process::exit(1);
|
||||
}
|
||||
+ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,9 @@ This is general information about your book.
|
|||
`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.
|
||||
This is also used to derive the direction of text (RTL, LTR) within the book.
|
||||
- **text-direction**: The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL). Possible values: `ltr`, `rtl`.
|
||||
When not specified, the text direction is derived from the book's `language` attribute.
|
||||
|
||||
**book.toml**
|
||||
```toml
|
||||
|
@ -55,6 +58,7 @@ 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"
|
||||
text-direction = "ltr"
|
||||
```
|
||||
|
||||
### Rust options
|
||||
|
@ -97,7 +101,7 @@ extra-watch-dirs = [] # directories to watch for triggering builds
|
|||
will be created when the book is built (i.e. `create-missing = true`). If this
|
||||
is `false` then the build process will instead exit with an error if any files
|
||||
do not exist.
|
||||
- **use-default-preprocessors:** Disable the default preprocessors of (`links` &
|
||||
- **use-default-preprocessors:** Disable the default preprocessors (of `links` &
|
||||
`index`) by setting this option to `false`.
|
||||
|
||||
If you have the same, and/or other preprocessors declared via their table
|
||||
|
|
|
@ -35,7 +35,7 @@ For example, if you have a preprocessor called `mdbook-example`, then you can in
|
|||
With this table, mdBook will execute the `mdbook-example` preprocessor.
|
||||
|
||||
This table can include additional key-value pairs that are specific to the preprocessor.
|
||||
For example, if our example prepocessor needed some extra configuration options:
|
||||
For example, if our example preprocessor needed some extra configuration options:
|
||||
|
||||
```toml
|
||||
[preprocessor.example]
|
||||
|
|
|
@ -97,7 +97,7 @@ description = "The example book covers examples."
|
|||
theme = "my-theme"
|
||||
default-theme = "light"
|
||||
preferred-dark-theme = "navy"
|
||||
curly-quotes = true
|
||||
smart-punctuation = true
|
||||
mathjax-support = false
|
||||
copy-fonts = true
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
|
@ -122,8 +122,10 @@ The following configuration options are available:
|
|||
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`.
|
||||
- **smart-punctuation:** Converts quotes to curly quotes, `...` to `…`, `--` to en-dash, and `---` to em-dash.
|
||||
See [Smart Punctuation].
|
||||
Defaults to `false`.
|
||||
- **curly-quotes:** Deprecated alias for `smart-punctuation`.
|
||||
- **mathjax-support:** Adds support for [MathJax](../mathjax.md). Defaults to
|
||||
`false`.
|
||||
- **copy-fonts:** (**Deprecated**) If `true` (the default), mdBook uses its built-in fonts which are copied to the output directory.
|
||||
|
@ -150,9 +152,9 @@ The following configuration options are available:
|
|||
- **edit-url-template:** Edit url template, when provided shows a
|
||||
"Suggest an edit" button (which looks like <i class="fa fa-edit"></i>) for directly jumping to editing the currently
|
||||
viewed page. For e.g. GitHub projects set this to
|
||||
`https://github.com/<owner>/<repo>/edit/master/{path}` or for
|
||||
`https://github.com/<owner>/<repo>/edit/<branch>/{path}` or for
|
||||
Bitbucket projects set it to
|
||||
`https://bitbucket.org/<owner>/<repo>/src/master/{path}?mode=edit`
|
||||
`https://bitbucket.org/<owner>/<repo>/src/<branch>/{path}?mode=edit`
|
||||
where {path} will be replaced with the full path of the file in the
|
||||
repository.
|
||||
- **input-404:** The name of the markdown file used for missing files.
|
||||
|
@ -182,7 +184,7 @@ page-break = true # insert page-break after each chapter
|
|||
|
||||
- **enable:** Enable print support. When `false`, all print support will not be
|
||||
rendered. Defaults to `true`.
|
||||
- **page-break** Insert page breaks between chapters. Defaults to `true`.
|
||||
- **page-break:** Insert page breaks between chapters. Defaults to `true`.
|
||||
|
||||
### `[output.html.fold]`
|
||||
|
||||
|
@ -218,11 +220,25 @@ runnable = true # displays a run button for rust code
|
|||
- **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`.
|
||||
- **runnable** Displays a run button for rust code snippets. Changing this to `false` will disable the run in playground feature globally. 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`.
|
||||
- **runnable:** Displays a run button for rust code snippets. Changing this to `false` will disable the run in playground feature globally. Defaults to `true`.
|
||||
|
||||
[Ace]: https://ace.c9.io/
|
||||
|
||||
### `[output.html.code]`
|
||||
|
||||
The `[output.html.code]` table provides options for controlling code blocks.
|
||||
|
||||
```toml
|
||||
[output.html.code]
|
||||
# A prefix string per language (one or more chars).
|
||||
# Any line starting with whitespace+prefix is hidden.
|
||||
hidelines = { python = "~" }
|
||||
```
|
||||
|
||||
- **hidelines:** A table that defines how [hidden code lines](../mdbook.md#hiding-code-lines) work for each language.
|
||||
The key is the language and the value is a string that will cause code lines starting with that prefix to be hidden.
|
||||
|
||||
### `[output.html.search]`
|
||||
|
||||
The `[output.html.search]` table provides options for controlling the built-in text [search].
|
||||
|
|
|
@ -73,14 +73,14 @@ Linking to a URL or local file is easy:
|
|||
```markdown
|
||||
Use [mdBook](https://github.com/rust-lang/mdBook).
|
||||
|
||||
Read about [mdBook](mdBook.md).
|
||||
Read about [mdBook](mdbook.md).
|
||||
|
||||
A bare url: <https://www.rust-lang.org>.
|
||||
```
|
||||
|
||||
Use [mdBook](https://github.com/rust-lang/mdBook).
|
||||
|
||||
Read about [mdBook](mdBook.md).
|
||||
Read about [mdBook](mdbook.md).
|
||||
|
||||
A bare url: <https://www.rust-lang.org>.
|
||||
|
||||
|
@ -124,7 +124,7 @@ mdBook has several extensions beyond the standard CommonMark specification.
|
|||
### Strikethrough
|
||||
|
||||
Text may be rendered with a horizontal line through the center by wrapping the
|
||||
text with two tilde characters on each side:
|
||||
text with one or two tilde characters on each side:
|
||||
|
||||
```text
|
||||
An example of ~~strikethrough text~~.
|
||||
|
@ -214,9 +214,22 @@ characters:
|
|||
So, no need to manually enter those Unicode characters!
|
||||
|
||||
This feature is disabled by default.
|
||||
To enable it, see the [`output.html.curly-quotes`] config option.
|
||||
To enable it, see the [`output.html.smart-punctuation`] config option.
|
||||
|
||||
[strikethrough]: https://github.github.com/gfm/#strikethrough-extension-
|
||||
[tables]: https://github.github.com/gfm/#tables-extension-
|
||||
[task list extension]: https://github.github.com/gfm/#task-list-items-extension-
|
||||
[`output.html.curly-quotes`]: configuration/renderers.md#html-renderer-options
|
||||
[`output.html.smart-punctuation`]: configuration/renderers.md#html-renderer-options
|
||||
|
||||
### Heading attributes
|
||||
|
||||
Headings can have a custom HTML ID and classes. This lets you maintain the same ID even if you change the heading's text, it also lets you add multiple classes in the heading.
|
||||
|
||||
Example:
|
||||
```md
|
||||
# Example heading { #first .class1 .class2 }
|
||||
```
|
||||
|
||||
This makes the level 1 heading with the content `Example heading`, ID `first`, and classes `class1` and `class2`. Note that the attributes should be space-separated.
|
||||
|
||||
More information can be found in the [heading attrs spec page](https://github.com/raphlinus/pulldown-cmark/blob/master/pulldown-cmark/specs/heading_attrs.txt).
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them
|
||||
with a `#` [like you would with Rustdoc][rustdoc-hide].
|
||||
This currently only works with Rust language code blocks.
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them with a specific prefix.
|
||||
|
||||
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/documentation-tests.html#hiding-portions-of-the-example
|
||||
For the Rust language, you can use the `#` character as a prefix which will hide lines [like you would with Rustdoc][rustdoc-hide].
|
||||
|
||||
[rustdoc-hide]: https://doc.rust-lang.org/stable/rustdoc/write-documentation/documentation-tests.html#hiding-portions-of-the-example
|
||||
|
||||
```bash
|
||||
# fn main() {
|
||||
|
@ -28,7 +28,47 @@ Will render as
|
|||
# }
|
||||
```
|
||||
|
||||
The code block has an eyeball icon (<i class="fa fa-eye"></i>) which will toggle the visibility of the hidden lines.
|
||||
When you tap or hover the mouse over the code block, there will be an eyeball icon (<i class="fa fa-eye"></i>) which will toggle the visibility of the hidden lines.
|
||||
|
||||
By default, this only works for code examples that are annotated with `rust`.
|
||||
However, you can define custom prefixes for other languages by adding a new line-hiding prefix in your `book.toml` with the language name and prefix character(s):
|
||||
|
||||
```toml
|
||||
[output.html.code.hidelines]
|
||||
python = "~"
|
||||
```
|
||||
|
||||
The prefix will hide any lines that begin with the given prefix. With the python prefix shown above, this:
|
||||
|
||||
```bash
|
||||
~hidden()
|
||||
nothidden():
|
||||
~ hidden()
|
||||
~hidden()
|
||||
nothidden()
|
||||
```
|
||||
|
||||
will render as
|
||||
|
||||
```python
|
||||
~hidden()
|
||||
nothidden():
|
||||
~ hidden()
|
||||
~hidden()
|
||||
nothidden()
|
||||
```
|
||||
|
||||
This behavior can be overridden locally with a different prefix. This has the same effect as above:
|
||||
|
||||
~~~markdown
|
||||
```python,hidelines=!!!
|
||||
!!!hidden()
|
||||
nothidden():
|
||||
!!! hidden()
|
||||
!!!hidden()
|
||||
nothidden()
|
||||
```
|
||||
~~~
|
||||
|
||||
## Rust Playground
|
||||
|
||||
|
@ -274,3 +314,51 @@ contents (sidebar) by including a `\{{#title ...}}` near the top of the page.
|
|||
```hbs
|
||||
\{{#title My Title}}
|
||||
```
|
||||
|
||||
## HTML classes provided by mdBook
|
||||
|
||||
<img class="right" src="images/rust-logo-blk.svg" alt="The Rust logo">
|
||||
|
||||
### `class="left"` and `"right"`
|
||||
|
||||
These classes are provided by default, for inline HTML to float images.
|
||||
|
||||
```html
|
||||
<img class="right" src="images/rust-logo-blk.svg" alt="The Rust logo">
|
||||
```
|
||||
|
||||
### `class="hidden"`
|
||||
|
||||
HTML tags with class `hidden` will not be shown.
|
||||
|
||||
```html
|
||||
<div class="hidden">This will not be seen.</div>
|
||||
```
|
||||
|
||||
<div class="hidden">This will not be seen.</div>
|
||||
|
||||
### `class="warning"`
|
||||
|
||||
To make a warning or similar note stand out, wrap it in a warning div.
|
||||
|
||||
```html
|
||||
<div class="warning">
|
||||
|
||||
This is a bad thing that you should pay attention to.
|
||||
|
||||
Warning blocks should be used sparingly in documentation, to avoid "warning
|
||||
fatigue," where people are trained to ignore them because they usually don't
|
||||
matter for what they're doing.
|
||||
|
||||
</div>
|
||||
```
|
||||
|
||||
<div class="warning">
|
||||
|
||||
This is a bad thing that you should pay attention to.
|
||||
|
||||
Warning blocks should be used sparingly in documentation, to avoid "warning
|
||||
fatigue," where people are trained to ignore them because they usually don't
|
||||
matter for what they're doing.
|
||||
|
||||
</div>
|
||||
|
|
|
@ -29,11 +29,12 @@ to be ignored at best, or may cause an error when attempting to build the book.
|
|||
- [First Chapter](relative/path/to/markdown2.md)
|
||||
```
|
||||
|
||||
1. ***Part Title*** - Headers can be used as a title for the following numbered
|
||||
chapters. This can be used to logically separate different sections
|
||||
of the book. The title is rendered as unclickable text.
|
||||
Titles are optional, and the numbered chapters can be broken into as many
|
||||
parts as desired.
|
||||
1. ***Part Title*** -
|
||||
Level 1 headers can be used as a title for the following numbered chapters.
|
||||
This can be used to logically separate different sections of the book.
|
||||
The title is rendered as unclickable text.
|
||||
Titles are optional, and the numbered chapters can be broken into as many parts as desired.
|
||||
Part titles must be h1 headers (one `#`), other heading levels are ignored.
|
||||
```markdown
|
||||
# My Part Title
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Theme
|
||||
|
||||
The default renderer uses a [handlebars](http://handlebarsjs.com/) template to
|
||||
The default renderer uses a [handlebars](https://handlebarsjs.com) template to
|
||||
render your markdown files and comes with a default theme included in the mdBook
|
||||
binary.
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ var chapters = {{chapters}};
|
|||
|
||||
### 2. previous / next
|
||||
|
||||
The previous and next helpers expose a `link` and `name` property to the
|
||||
The previous and next helpers expose a `link` and `title` property to the
|
||||
previous and next chapters.
|
||||
|
||||
They are used like this
|
||||
|
@ -87,7 +87,7 @@ They are used like this
|
|||
```handlebars
|
||||
{{#previous}}
|
||||
<a href="{{link}}" class="nav-chapters previous">
|
||||
<i class="fa fa-angle-left"></i>
|
||||
<i class="fa fa-angle-left"></i> {{title}}
|
||||
</a>
|
||||
{{/previous}}
|
||||
```
|
||||
|
|
|
@ -44,6 +44,8 @@ your own `highlight.js` file:
|
|||
- makefile
|
||||
- markdown
|
||||
- nginx
|
||||
- nim
|
||||
- nix
|
||||
- objectivec
|
||||
- perl
|
||||
- php
|
||||
|
@ -77,38 +79,6 @@ the `theme` folder of your book.
|
|||
|
||||
Now your theme will be used instead of the default theme.
|
||||
|
||||
## Hiding code lines
|
||||
|
||||
There is a feature in mdBook that lets you hide code lines by prepending them
|
||||
with a `#`.
|
||||
|
||||
|
||||
```bash
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 6;
|
||||
|
||||
println!("{}", x + y);
|
||||
# }
|
||||
```
|
||||
|
||||
Will render as
|
||||
|
||||
```rust
|
||||
# fn main() {
|
||||
let x = 5;
|
||||
let y = 7;
|
||||
|
||||
println!("{}", x + y);
|
||||
# }
|
||||
```
|
||||
|
||||
**At the moment, this only works for code examples that are annotated with
|
||||
`rust`. Because it would collide with semantics of some programming languages.
|
||||
In the future, we want to make this configurable through the `book.toml` so that
|
||||
everyone can benefit from it.**
|
||||
|
||||
|
||||
## Improve default theme
|
||||
|
||||
If you think the default theme doesn't look quite right for a specific language,
|
||||
|
|
|
@ -20,7 +20,7 @@ To make it easier to run, put the path to the binary into your `PATH`.
|
|||
|
||||
To build the `mdbook` executable from source, you will first need to install Rust and Cargo.
|
||||
Follow the instructions on the [Rust installation page].
|
||||
mdBook currently requires at least Rust version 1.60.
|
||||
mdBook currently requires at least Rust version 1.71.
|
||||
|
||||
Once you have installed Rust, the following command can be used to build and install mdBook:
|
||||
|
||||
|
|
|
@ -39,9 +39,7 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
|
|||
.chain(summary.suffix_chapters.iter())
|
||||
.collect();
|
||||
|
||||
while !items.is_empty() {
|
||||
let next = items.pop().expect("already checked");
|
||||
|
||||
while let Some(next) = items.pop() {
|
||||
if let SummaryItem::Link(ref link) = *next {
|
||||
if let Some(ref location) = link.location {
|
||||
let filename = src_dir.join(location);
|
||||
|
@ -162,8 +160,20 @@ pub struct Chapter {
|
|||
/// Nested items.
|
||||
pub sub_items: Vec<BookItem>,
|
||||
/// The chapter's location, relative to the `SUMMARY.md` file.
|
||||
///
|
||||
/// **Note**: After the index preprocessor runs, any README files will be
|
||||
/// modified to be `index.md`. If you need access to the actual filename
|
||||
/// on disk, use [`Chapter::source_path`] instead.
|
||||
///
|
||||
/// This is `None` for a draft chapter.
|
||||
pub path: Option<PathBuf>,
|
||||
/// The chapter's source file, relative to the `SUMMARY.md` file.
|
||||
///
|
||||
/// **Note**: Beware that README files will internally be treated as
|
||||
/// `index.md` via the [`Chapter::path`] field. The `source_path` field
|
||||
/// exists if you need access to the true file path.
|
||||
///
|
||||
/// This is `None` for a draft chapter.
|
||||
pub source_path: Option<PathBuf>,
|
||||
/// An ordered list of the names of each chapter above this one in the hierarchy.
|
||||
pub parent_names: Vec<String>,
|
||||
|
@ -277,7 +287,7 @@ fn load_chapter<P: AsRef<Path>>(
|
|||
}
|
||||
|
||||
let stripped = location
|
||||
.strip_prefix(&src_dir)
|
||||
.strip_prefix(src_dir)
|
||||
.expect("Chapters are always inside a book");
|
||||
|
||||
Chapter::new(&link.name, content, stripped, parent_names.clone())
|
||||
|
@ -317,7 +327,7 @@ impl<'a> Iterator for BookItems<'a> {
|
|||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let item = self.items.pop_front();
|
||||
|
||||
if let Some(&BookItem::Chapter(ref ch)) = item {
|
||||
if let Some(BookItem::Chapter(ch)) = item {
|
||||
// if we wanted a breadth-first iterator we'd `extend()` here
|
||||
for sub_item in ch.sub_items.iter().rev() {
|
||||
self.items.push_front(sub_item);
|
||||
|
@ -341,7 +351,6 @@ impl Display for Chapter {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::{Builder as TempFileBuilder, TempDir};
|
||||
|
||||
const DUMMY_SRC: &str = "
|
||||
|
|
|
@ -198,8 +198,7 @@ impl BookBuilder {
|
|||
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
|
||||
|
||||
let chapter_1 = src_dir.join("chapter_1.md");
|
||||
let mut f =
|
||||
File::create(&chapter_1).with_context(|| "Unable to create 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.");
|
||||
|
@ -212,10 +211,10 @@ impl BookBuilder {
|
|||
fs::create_dir_all(&self.root)?;
|
||||
|
||||
let src = self.root.join(&self.config.book.src);
|
||||
fs::create_dir_all(&src)?;
|
||||
fs::create_dir_all(src)?;
|
||||
|
||||
let build = self.root.join(&self.config.build.build_dir);
|
||||
fs::create_dir_all(&build)?;
|
||||
fs::create_dir_all(build)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
113
src/book/mod.rs
113
src/book/mod.rs
|
@ -15,10 +15,10 @@ pub use self::init::BookBuilder;
|
|||
pub use self::summary::{parse_summary, Link, SectionNumber, Summary, SummaryItem};
|
||||
|
||||
use log::{debug, error, info, log_enabled, trace, warn};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::ffi::OsString;
|
||||
use std::io::{IsTerminal, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::string::ToString;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
use toml::Value;
|
||||
use topological_sort::TopologicalSort;
|
||||
|
@ -71,18 +71,24 @@ impl MDBook {
|
|||
|
||||
config.update_from_env();
|
||||
|
||||
if config
|
||||
.html_config()
|
||||
.map_or(false, |html| html.google_analytics.is_some())
|
||||
{
|
||||
warn!(
|
||||
"The output.html.google-analytics field has been deprecated; \
|
||||
it will be removed in a future release.\n\
|
||||
Consider placing the appropriate site tag code into the \
|
||||
theme/head.hbs file instead.\n\
|
||||
The tracking code may be found in the Google Analytics Admin page.\n\
|
||||
"
|
||||
);
|
||||
if let Some(html_config) = config.html_config() {
|
||||
if html_config.google_analytics.is_some() {
|
||||
warn!(
|
||||
"The output.html.google-analytics field has been deprecated; \
|
||||
it will be removed in a future release.\n\
|
||||
Consider placing the appropriate site tag code into the \
|
||||
theme/head.hbs file instead.\n\
|
||||
The tracking code may be found in the Google Analytics Admin page.\n\
|
||||
"
|
||||
);
|
||||
}
|
||||
if html_config.curly_quotes {
|
||||
warn!(
|
||||
"The output.html.curly-quotes field has been renamed to \
|
||||
output.html.smart-punctuation.\n\
|
||||
Use the new name in book.toml to remove this warning."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if log_enabled!(log::Level::Trace) {
|
||||
|
@ -99,7 +105,7 @@ impl MDBook {
|
|||
let root = book_root.into();
|
||||
|
||||
let src_dir = root.join(&config.book.src);
|
||||
let book = book::load_book(&src_dir, &config.build)?;
|
||||
let book = book::load_book(src_dir, &config.build)?;
|
||||
|
||||
let renderers = determine_renderers(&config);
|
||||
let preprocessors = determine_preprocessors(&config)?;
|
||||
|
@ -122,7 +128,7 @@ impl 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 book = book::load_book_from_disk(&summary, src_dir)?;
|
||||
|
||||
let renderers = determine_renderers(&config);
|
||||
let preprocessors = determine_preprocessors(&config)?;
|
||||
|
@ -196,21 +202,26 @@ impl MDBook {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Run the entire build process for a particular [`Renderer`].
|
||||
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
||||
let mut preprocessed_book = self.book.clone();
|
||||
/// Run preprocessors and return the final book.
|
||||
pub fn preprocess_book(&self, renderer: &dyn Renderer) -> Result<(Book, PreprocessorContext)> {
|
||||
let preprocess_ctx = PreprocessorContext::new(
|
||||
self.root.clone(),
|
||||
self.config.clone(),
|
||||
renderer.name().to_string(),
|
||||
);
|
||||
|
||||
let mut preprocessed_book = self.book.clone();
|
||||
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)?;
|
||||
}
|
||||
}
|
||||
Ok((preprocessed_book, preprocess_ctx))
|
||||
}
|
||||
|
||||
/// Run the entire build process for a particular [`Renderer`].
|
||||
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
|
||||
let (preprocessed_book, preprocess_ctx) = self.preprocess_book(renderer)?;
|
||||
|
||||
let name = renderer.name();
|
||||
let build_dir = self.build_dir_for(name);
|
||||
|
@ -254,24 +265,45 @@ impl MDBook {
|
|||
/// Run `rustdoc` tests on a specific chapter of the book, linking against the provided libraries.
|
||||
/// If `chapter` is `None`, all tests will be run.
|
||||
pub fn test_chapter(&mut self, library_paths: Vec<&str>, chapter: Option<&str>) -> Result<()> {
|
||||
let library_args: Vec<&str> = (0..library_paths.len())
|
||||
.map(|_| "-L")
|
||||
.zip(library_paths.into_iter())
|
||||
.flat_map(|x| vec![x.0, x.1])
|
||||
let cwd = std::env::current_dir()?;
|
||||
let library_args: Vec<OsString> = library_paths
|
||||
.into_iter()
|
||||
.flat_map(|path| {
|
||||
let path = Path::new(path);
|
||||
let path = if path.is_relative() {
|
||||
cwd.join(path).into_os_string()
|
||||
} else {
|
||||
path.to_path_buf().into_os_string()
|
||||
};
|
||||
[OsString::from("-L"), path]
|
||||
})
|
||||
.collect();
|
||||
|
||||
let temp_dir = TempFileBuilder::new().prefix("mdbook-").tempdir()?;
|
||||
|
||||
let mut chapter_found = false;
|
||||
|
||||
// FIXME: Is "test" the proper renderer name to use here?
|
||||
let preprocess_context =
|
||||
PreprocessorContext::new(self.root.clone(), self.config.clone(), "test".to_string());
|
||||
struct TestRenderer;
|
||||
impl Renderer for TestRenderer {
|
||||
// FIXME: Is "test" the proper renderer name to use here?
|
||||
fn name(&self) -> &str {
|
||||
"test"
|
||||
}
|
||||
|
||||
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.
|
||||
fn render(&self, _: &RenderContext) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Index Preprocessor is disabled so that chapter paths
|
||||
// continue to point to the actual markdown files.
|
||||
self.preprocessors = determine_preprocessors(&self.config)?
|
||||
.into_iter()
|
||||
.filter(|pre| pre.name() != IndexPreprocessor::NAME)
|
||||
.collect();
|
||||
let (book, _) = self.preprocess_book(&TestRenderer)?;
|
||||
|
||||
let color_output = std::io::stderr().is_terminal();
|
||||
let mut failed = false;
|
||||
for item in book.iter() {
|
||||
if let BookItem::Chapter(ref ch) = *item {
|
||||
|
@ -292,27 +324,34 @@ impl MDBook {
|
|||
info!("Testing chapter '{}': {:?}", ch.name, chapter_path);
|
||||
|
||||
// write preprocessed file to tempdir
|
||||
let path = temp_dir.path().join(&chapter_path);
|
||||
let path = temp_dir.path().join(chapter_path);
|
||||
let mut tmpf = utils::fs::create_file(&path)?;
|
||||
tmpf.write_all(ch.content.as_bytes())?;
|
||||
|
||||
let mut cmd = Command::new("rustdoc");
|
||||
cmd.arg(&path).arg("--test").args(&library_args);
|
||||
cmd.current_dir(temp_dir.path())
|
||||
.arg(&chapter_path)
|
||||
.arg("--test")
|
||||
.args(&library_args);
|
||||
|
||||
if let Some(edition) = self.config.rust.edition {
|
||||
match edition {
|
||||
RustEdition::E2015 => {
|
||||
cmd.args(&["--edition", "2015"]);
|
||||
cmd.args(["--edition", "2015"]);
|
||||
}
|
||||
RustEdition::E2018 => {
|
||||
cmd.args(&["--edition", "2018"]);
|
||||
cmd.args(["--edition", "2018"]);
|
||||
}
|
||||
RustEdition::E2021 => {
|
||||
cmd.args(&["--edition", "2021"]);
|
||||
cmd.args(["--edition", "2021"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if color_output {
|
||||
cmd.args(&["--color", "always"]);
|
||||
}
|
||||
|
||||
debug!("running {:?}", cmd);
|
||||
let output = cmd.output()?;
|
||||
|
||||
|
@ -588,7 +627,7 @@ fn preprocessor_should_run(
|
|||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
use toml::value::{Table, Value};
|
||||
use toml::value::Table;
|
||||
|
||||
#[test]
|
||||
fn config_defaults_to_html_renderer_if_empty() {
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
use crate::errors::*;
|
||||
use log::{debug, trace, warn};
|
||||
use memchr::{self, Memchr};
|
||||
use pulldown_cmark::{self, Event, HeadingLevel, Tag};
|
||||
use memchr::Memchr;
|
||||
use pulldown_cmark::{DefaultBrokenLinkCallback, Event, HeadingLevel, Tag, TagEnd};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::iter::FromIterator;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
@ -163,7 +162,7 @@ impl From<Link> for SummaryItem {
|
|||
/// > match the following regex: "[^<>\n[]]+".
|
||||
struct SummaryParser<'a> {
|
||||
src: &'a str,
|
||||
stream: pulldown_cmark::OffsetIter<'a, 'a>,
|
||||
stream: pulldown_cmark::OffsetIter<'a, DefaultBrokenLinkCallback>,
|
||||
offset: usize,
|
||||
|
||||
/// We can't actually put an event back into the `OffsetIter` stream, so instead we store it
|
||||
|
@ -210,7 +209,7 @@ macro_rules! collect_events {
|
|||
}
|
||||
|
||||
impl<'a> SummaryParser<'a> {
|
||||
fn new(text: &str) -> SummaryParser<'_> {
|
||||
fn new(text: &'a str) -> SummaryParser<'a> {
|
||||
let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter();
|
||||
|
||||
SummaryParser {
|
||||
|
@ -265,7 +264,12 @@ impl<'a> SummaryParser<'a> {
|
|||
loop {
|
||||
match self.next_event() {
|
||||
Some(ev @ Event::Start(Tag::List(..)))
|
||||
| Some(ev @ Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
| Some(
|
||||
ev @ Event::Start(Tag::Heading {
|
||||
level: HeadingLevel::H1,
|
||||
..
|
||||
}),
|
||||
) => {
|
||||
if is_prefix {
|
||||
// we've finished prefix chapters and are at the start
|
||||
// of the numbered section.
|
||||
|
@ -275,8 +279,8 @@ impl<'a> SummaryParser<'a> {
|
|||
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 { dest_url, .. })) => {
|
||||
let link = self.parse_link(dest_url.to_string());
|
||||
items.push(SummaryItem::Link(link));
|
||||
}
|
||||
Some(Event::Rule) => items.push(SummaryItem::Separator),
|
||||
|
@ -304,10 +308,13 @@ impl<'a> SummaryParser<'a> {
|
|||
break;
|
||||
}
|
||||
|
||||
Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
Some(Event::Start(Tag::Heading {
|
||||
level: HeadingLevel::H1,
|
||||
..
|
||||
})) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..));
|
||||
let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
|
||||
Some(stringify_events(tags))
|
||||
}
|
||||
|
||||
|
@ -336,7 +343,7 @@ impl<'a> SummaryParser<'a> {
|
|||
/// 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", " ");
|
||||
let link_content = collect_events!(self.stream, end Tag::Link(..));
|
||||
let link_content = collect_events!(self.stream, end TagEnd::Link);
|
||||
let name = stringify_events(link_content);
|
||||
|
||||
let path = if href.is_empty() {
|
||||
|
@ -377,7 +384,12 @@ impl<'a> SummaryParser<'a> {
|
|||
}
|
||||
// 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(HeadingLevel::H1, ..))) => {
|
||||
Some(
|
||||
ev @ Event::Start(Tag::Heading {
|
||||
level: HeadingLevel::H1,
|
||||
..
|
||||
}),
|
||||
) => {
|
||||
// we're starting a new part
|
||||
self.back(ev);
|
||||
break;
|
||||
|
@ -398,7 +410,7 @@ impl<'a> SummaryParser<'a> {
|
|||
|
||||
// Skip over the contents of this tag
|
||||
while let Some(event) = self.next_event() {
|
||||
if event == Event::End(other_tag.clone()) {
|
||||
if event == Event::End(other_tag.clone().into()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -469,7 +481,7 @@ impl<'a> SummaryParser<'a> {
|
|||
|
||||
last_item.nested_items = sub_items;
|
||||
}
|
||||
Some(Event::End(Tag::List(..))) => break,
|
||||
Some(Event::End(TagEnd::List(..))) => break,
|
||||
Some(_) => {}
|
||||
None => break,
|
||||
}
|
||||
|
@ -486,8 +498,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 { dest_url, .. })) => {
|
||||
let mut link = self.parse_link(dest_url.to_string());
|
||||
|
||||
let mut number = parent.clone();
|
||||
number.0.push(num_existing_items as u32 + 1);
|
||||
|
@ -529,14 +541,18 @@ impl<'a> SummaryParser<'a> {
|
|||
fn parse_title(&mut self) -> Option<String> {
|
||||
loop {
|
||||
match self.next_event() {
|
||||
Some(Event::Start(Tag::Heading(HeadingLevel::H1, ..))) => {
|
||||
Some(Event::Start(Tag::Heading {
|
||||
level: HeadingLevel::H1,
|
||||
..
|
||||
})) => {
|
||||
debug!("Found a h1 in the SUMMARY");
|
||||
|
||||
let tags = collect_events!(self.stream, end Tag::Heading(HeadingLevel::H1, ..));
|
||||
let tags = collect_events!(self.stream, end TagEnd::Heading(HeadingLevel::H1));
|
||||
return Some(stringify_events(tags));
|
||||
}
|
||||
// Skip a HTML element such as a comment line.
|
||||
Some(Event::Html(_)) => {}
|
||||
Some(Event::Html(_) | Event::InlineHtml(_))
|
||||
| Some(Event::Start(Tag::HtmlBlock) | Event::End(TagEnd::HtmlBlock)) => {}
|
||||
// Otherwise, no title.
|
||||
Some(ev) => {
|
||||
self.back(ev);
|
||||
|
@ -744,7 +760,7 @@ mod tests {
|
|||
let _ = parser.stream.next(); // Discard opening paragraph
|
||||
|
||||
let href = match parser.stream.next() {
|
||||
Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(),
|
||||
Some((Event::Start(Tag::Link { dest_url, .. }), _range)) => dest_url.to_string(),
|
||||
other => panic!("Unreachable, {:?}", other),
|
||||
};
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ pub fn make_subcommand() -> Command {
|
|||
// Build command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
let mut book = MDBook::load(book_dir)?;
|
||||
|
||||
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.into();
|
||||
|
|
|
@ -16,7 +16,7 @@ pub fn make_subcommand() -> Command {
|
|||
// Clean command implementation
|
||||
pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let book = MDBook::load(&book_dir)?;
|
||||
let book = MDBook::load(book_dir)?;
|
||||
|
||||
let dir_to_remove = match args.get_one::<PathBuf>("dest-dir") {
|
||||
Some(dest_dir) => dest_dir.into(),
|
||||
|
|
|
@ -56,7 +56,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
"git" => builder.create_gitignore(true),
|
||||
_ => builder.create_gitignore(false),
|
||||
};
|
||||
} else {
|
||||
} else if !args.get_flag("force") {
|
||||
println!("\nDo you want a .gitignore to be created? (y/n)");
|
||||
if confirm() {
|
||||
builder.create_gitignore(true);
|
||||
|
@ -65,6 +65,8 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
|
||||
config.book.title = if args.contains_id("title") {
|
||||
args.get_one::<String>("title").map(String::from)
|
||||
} else if args.get_flag("force") {
|
||||
None
|
||||
} else {
|
||||
request_book_title()
|
||||
};
|
||||
|
@ -84,7 +86,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
/// Obtains author name from git config file by running the `git config` command.
|
||||
fn get_author_name() -> Option<String> {
|
||||
let output = Command::new("git")
|
||||
.args(&["config", "--get", "user.name"])
|
||||
.args(["config", "--get", "user.name"])
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
|
@ -114,5 +116,5 @@ fn confirm() -> bool {
|
|||
io::stdout().flush().unwrap();
|
||||
let mut s = String::new();
|
||||
io::stdin().read_line(&mut s).ok();
|
||||
matches!(&*s.trim(), "Y" | "y" | "yes" | "Yes")
|
||||
matches!(s.trim(), "Y" | "y" | "yes" | "Yes")
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ pub fn make_subcommand() -> Command {
|
|||
// Serve command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
let mut book = MDBook::load(book_dir)?;
|
||||
|
||||
let port = args.get_one::<String>("port").unwrap();
|
||||
let hostname = args.get_one::<String>("hostname").unwrap();
|
||||
|
@ -102,7 +102,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
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| {
|
||||
let result = MDBook::load(book_dir).and_then(|mut b| {
|
||||
update_config(&mut b);
|
||||
b.build()
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use super::command_prelude::*;
|
||||
use crate::get_book_dir;
|
||||
use clap::builder::NonEmptyStringValueParser;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use clap::ArgAction;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::MDBook;
|
||||
use std::path::PathBuf;
|
||||
|
@ -44,7 +44,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
let chapter: Option<&str> = args.get_one::<String>("chapter").map(|s| s.as_str());
|
||||
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
let mut book = MDBook::load(book_dir)?;
|
||||
|
||||
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||
book.config.build.build_dir = dest_dir.to_path_buf();
|
||||
|
|
105
src/cmd/watch.rs
105
src/cmd/watch.rs
|
@ -1,8 +1,10 @@
|
|||
use super::command_prelude::*;
|
||||
use crate::{get_book_dir, open};
|
||||
use ignore::gitignore::Gitignore;
|
||||
use mdbook::errors::Result;
|
||||
use mdbook::utils;
|
||||
use mdbook::MDBook;
|
||||
use pathdiff::diff_paths;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::channel;
|
||||
use std::thread::sleep;
|
||||
|
@ -20,7 +22,7 @@ pub fn make_subcommand() -> Command {
|
|||
// Watch command implementation
|
||||
pub fn execute(args: &ArgMatches) -> Result<()> {
|
||||
let book_dir = get_book_dir(args);
|
||||
let mut book = MDBook::load(&book_dir)?;
|
||||
let mut book = MDBook::load(book_dir)?;
|
||||
|
||||
let update_config = |book: &mut MDBook| {
|
||||
if let Some(dest_dir) = args.get_one::<PathBuf>("dest-dir") {
|
||||
|
@ -41,7 +43,7 @@ pub fn execute(args: &ArgMatches) -> Result<()> {
|
|||
|
||||
trigger_on_change(&book, |paths, book_dir| {
|
||||
info!("Files changed: {:?}\nBuilding book...\n", paths);
|
||||
let result = MDBook::load(&book_dir).and_then(|mut b| {
|
||||
let result = MDBook::load(book_dir).and_then(|mut b| {
|
||||
update_config(&mut b);
|
||||
b.build()
|
||||
});
|
||||
|
@ -62,14 +64,14 @@ fn remove_ignored_files(book_root: &Path, paths: &[PathBuf]) -> Vec<PathBuf> {
|
|||
|
||||
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()
|
||||
}
|
||||
let (ignore, err) = Gitignore::new(&gitignore_path);
|
||||
if let Some(err) = err {
|
||||
warn!(
|
||||
"error reading gitignore `{}`: {err}",
|
||||
gitignore_path.display()
|
||||
);
|
||||
}
|
||||
filter_ignored_files(ignore, paths)
|
||||
}
|
||||
None => {
|
||||
// There is no .gitignore file.
|
||||
|
@ -85,18 +87,22 @@ fn find_gitignore(book_root: &Path) -> Option<PathBuf> {
|
|||
.find(|p| p.exists())
|
||||
}
|
||||
|
||||
fn filter_ignored_files(exclusion_checker: gitignore::File, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
// Note: The usage of `canonicalize` may encounter occasional failures on the Windows platform, presenting a potential risk.
|
||||
// For more details, refer to [Pull Request #2229](https://github.com/rust-lang/mdBook/pull/2229#discussion_r1408665981).
|
||||
fn filter_ignored_files(ignore: Gitignore, paths: &[PathBuf]) -> Vec<PathBuf> {
|
||||
let ignore_root = ignore
|
||||
.path()
|
||||
.canonicalize()
|
||||
.expect("ignore root canonicalize error");
|
||||
|
||||
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
|
||||
}
|
||||
.filter(|path| {
|
||||
let relative_path =
|
||||
diff_paths(&path, &ignore_root).expect("One of the paths should be an absolute");
|
||||
!ignore
|
||||
.matched_path_or_any_parents(&relative_path, relative_path.is_dir())
|
||||
.is_ignore()
|
||||
})
|
||||
.map(|path| path.to_path_buf())
|
||||
.collect()
|
||||
|
@ -112,8 +118,7 @@ where
|
|||
// Create a channel to receive the events.
|
||||
let (tx, rx) = channel();
|
||||
|
||||
let mut debouncer = match notify_debouncer_mini::new_debouncer(Duration::from_secs(1), None, tx)
|
||||
{
|
||||
let mut debouncer = match notify_debouncer_mini::new_debouncer(Duration::from_secs(1), tx) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
error!("Error while trying to watch the files:\n\n\t{:?}", e);
|
||||
|
@ -134,11 +139,16 @@ where
|
|||
let _ = watcher.watch(&book.root.join("book.toml"), NonRecursive);
|
||||
|
||||
for dir in &book.config.build.extra_watch_dirs {
|
||||
let path = dir.canonicalize().unwrap();
|
||||
if let Err(e) = watcher.watch(&path, Recursive) {
|
||||
let path = book.root.join(dir);
|
||||
let canonical_path = path.canonicalize().unwrap_or_else(|e| {
|
||||
error!("Error while watching extra directory {path:?}:\n {e}");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
if let Err(e) = watcher.watch(&canonical_path, Recursive) {
|
||||
error!(
|
||||
"Error while watching extra directory {:?}:\n {:?}",
|
||||
path, e
|
||||
canonical_path, e
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
@ -156,10 +166,8 @@ where
|
|||
let paths: Vec<_> = all_events
|
||||
.filter_map(|event| match event {
|
||||
Ok(events) => Some(events),
|
||||
Err(errors) => {
|
||||
for error in errors {
|
||||
log::warn!("error while watching for changes: {error}");
|
||||
}
|
||||
Err(error) => {
|
||||
log::warn!("error while watching for changes: {error}");
|
||||
None
|
||||
}
|
||||
})
|
||||
|
@ -178,3 +186,44 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ignore::gitignore::GitignoreBuilder;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_filter_ignored_files() {
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
|
||||
let ignore = GitignoreBuilder::new(¤t_dir)
|
||||
.add_line(None, "*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
let should_remain = current_dir.join("record.text");
|
||||
let should_filter = current_dir.join("index.html");
|
||||
|
||||
let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]);
|
||||
assert_eq!(remain, vec![should_remain])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filter_ignored_files_should_handle_parent_dir() {
|
||||
let current_dir = env::current_dir().unwrap();
|
||||
|
||||
let ignore = GitignoreBuilder::new(¤t_dir)
|
||||
.add_line(None, "*.html")
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let parent_dir = current_dir.join("..");
|
||||
let should_remain = parent_dir.join("record.text");
|
||||
let should_filter = parent_dir.join("index.html");
|
||||
|
||||
let remain = filter_ignored_files(ignore, &[should_remain.clone(), should_filter]);
|
||||
assert_eq!(remain, vec![should_remain])
|
||||
}
|
||||
}
|
||||
|
|
186
src/config.rs
186
src/config.rs
|
@ -58,7 +58,7 @@ use std::io::Read;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use toml::value::Table;
|
||||
use toml::{self, Value};
|
||||
use toml::Value;
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::utils::{self, toml_ext::TomlExt};
|
||||
|
@ -308,7 +308,7 @@ impl<'de> serde::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: https://rust-lang.github.io/mdBook/format/config.html");
|
||||
return Ok(Config::from_legacy(raw));
|
||||
}
|
||||
|
||||
|
@ -411,6 +411,9 @@ pub struct BookConfig {
|
|||
pub multilingual: bool,
|
||||
/// The main language of the book.
|
||||
pub language: Option<String>,
|
||||
/// The direction of text in the book: Left-to-right (LTR) or Right-to-left (RTL).
|
||||
/// When not specified, the text direction is derived from [`BookConfig::language`].
|
||||
pub text_direction: Option<TextDirection>,
|
||||
}
|
||||
|
||||
impl Default for BookConfig {
|
||||
|
@ -422,6 +425,43 @@ impl Default for BookConfig {
|
|||
src: PathBuf::from("src"),
|
||||
multilingual: false,
|
||||
language: Some(String::from("en")),
|
||||
text_direction: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BookConfig {
|
||||
/// Gets the realized text direction, either from [`BookConfig::text_direction`]
|
||||
/// or derived from [`BookConfig::language`], to be used by templating engines.
|
||||
pub fn realized_text_direction(&self) -> TextDirection {
|
||||
if let Some(direction) = self.text_direction {
|
||||
direction
|
||||
} else {
|
||||
TextDirection::from_lang_code(self.language.as_deref().unwrap_or_default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Text direction to use for HTML output
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum TextDirection {
|
||||
/// Left to right.
|
||||
#[serde(rename = "ltr")]
|
||||
LeftToRight,
|
||||
/// Right to left
|
||||
#[serde(rename = "rtl")]
|
||||
RightToLeft,
|
||||
}
|
||||
|
||||
impl TextDirection {
|
||||
/// Gets the text direction from language code
|
||||
pub fn from_lang_code(code: &str) -> Self {
|
||||
match code {
|
||||
// list sourced from here: https://github.com/abarrak/rtl/blob/master/lib/rtl/core.rb#L16
|
||||
"ar" | "ara" | "arc" | "ae" | "ave" | "egy" | "he" | "heb" | "nqo" | "pal" | "phn"
|
||||
| "sam" | "syc" | "syr" | "fa" | "per" | "fas" | "ku" | "kur" | "ur" | "urd"
|
||||
| "pus" | "ps" | "yi" | "yid" => TextDirection::RightToLeft,
|
||||
_ => TextDirection::LeftToRight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -486,7 +526,9 @@ pub struct HtmlConfig {
|
|||
/// 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.
|
||||
/// Supports smart quotes, apostrophes, ellipsis, en-dash, and em-dash.
|
||||
pub smart_punctuation: bool,
|
||||
/// Deprecated alias for `smart_punctuation`.
|
||||
pub curly_quotes: bool,
|
||||
/// Should mathjax be enabled?
|
||||
pub mathjax_support: bool,
|
||||
|
@ -504,6 +546,8 @@ pub struct HtmlConfig {
|
|||
/// Playground settings.
|
||||
#[serde(alias = "playpen")]
|
||||
pub playground: Playground,
|
||||
/// Code settings.
|
||||
pub code: Code,
|
||||
/// Print settings.
|
||||
pub print: Print,
|
||||
/// Don't render section labels.
|
||||
|
@ -548,6 +592,7 @@ impl Default for HtmlConfig {
|
|||
theme: None,
|
||||
default_theme: None,
|
||||
preferred_dark_theme: None,
|
||||
smart_punctuation: false,
|
||||
curly_quotes: false,
|
||||
mathjax_support: false,
|
||||
copy_fonts: true,
|
||||
|
@ -556,6 +601,7 @@ impl Default for HtmlConfig {
|
|||
additional_js: Vec::new(),
|
||||
fold: Fold::default(),
|
||||
playground: Playground::default(),
|
||||
code: Code::default(),
|
||||
print: Print::default(),
|
||||
no_section_label: false,
|
||||
search: None,
|
||||
|
@ -580,6 +626,11 @@ impl HtmlConfig {
|
|||
None => root.join("theme"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if smart punctuation is enabled.
|
||||
pub fn smart_punctuation(&self) -> bool {
|
||||
self.smart_punctuation || self.curly_quotes
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for how to render the print icon, print.html, and print.css.
|
||||
|
@ -613,7 +664,7 @@ pub struct Fold {
|
|||
pub level: u8,
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the the HTML renderer handles the playground.
|
||||
/// Configuration for tweaking how the HTML renderer handles the playground.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Playground {
|
||||
|
@ -642,6 +693,22 @@ impl Default for Playground {
|
|||
}
|
||||
}
|
||||
|
||||
/// Configuration for tweaking how the HTML renderer handles code blocks.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
pub struct Code {
|
||||
/// A prefix string to hide lines per language (one or more chars).
|
||||
pub hidelines: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for Code {
|
||||
fn default() -> Code {
|
||||
Code {
|
||||
hidelines: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration of the search functionality of the HTML renderer.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
|
@ -703,7 +770,7 @@ trait Updateable<'de>: Serialize + Deserialize<'de> {
|
|||
let mut raw = Value::try_from(&self).expect("unreachable");
|
||||
|
||||
if let Ok(value) = Value::try_from(value) {
|
||||
let _ = raw.insert(key, value);
|
||||
raw.insert(key, value);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
@ -739,7 +806,7 @@ mod tests {
|
|||
[output.html]
|
||||
theme = "./themedir"
|
||||
default-theme = "rust"
|
||||
curly-quotes = true
|
||||
smart-punctuation = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["./foo/bar/baz.css"]
|
||||
git-repository-url = "https://foo.com/"
|
||||
|
@ -769,6 +836,7 @@ mod tests {
|
|||
multilingual: true,
|
||||
src: PathBuf::from("source"),
|
||||
language: Some(String::from("ja")),
|
||||
text_direction: None,
|
||||
};
|
||||
let build_should_be = BuildConfig {
|
||||
build_dir: PathBuf::from("outputs"),
|
||||
|
@ -785,7 +853,7 @@ mod tests {
|
|||
runnable: true,
|
||||
};
|
||||
let html_should_be = HtmlConfig {
|
||||
curly_quotes: true,
|
||||
smart_punctuation: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
|
||||
theme: Some(PathBuf::from("./themedir")),
|
||||
|
@ -965,7 +1033,7 @@ mod tests {
|
|||
[output.html]
|
||||
destination = "my-book" # the output files will be generated in `root/my-book` instead of `root/book`
|
||||
theme = "my-theme"
|
||||
curly-quotes = true
|
||||
smart-punctuation = true
|
||||
google-analytics = "123456"
|
||||
additional-css = ["custom.css", "custom2.css"]
|
||||
additional-js = ["custom.js"]
|
||||
|
@ -990,7 +1058,7 @@ mod tests {
|
|||
|
||||
let html_should_be = HtmlConfig {
|
||||
theme: Some(PathBuf::from("my-theme")),
|
||||
curly_quotes: true,
|
||||
smart_punctuation: true,
|
||||
google_analytics: Some(String::from("123456")),
|
||||
additional_css: vec![PathBuf::from("custom.css"), PathBuf::from("custom2.css")],
|
||||
additional_js: vec![PathBuf::from("custom.js")],
|
||||
|
@ -1121,6 +1189,73 @@ mod tests {
|
|||
assert_eq!(&get_404_output_file(&html_config.input_404), "missing.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_direction_ltr() {
|
||||
let src = r#"
|
||||
[book]
|
||||
text-direction = "ltr"
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.book.text_direction, Some(TextDirection::LeftToRight));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_direction_rtl() {
|
||||
let src = r#"
|
||||
[book]
|
||||
text-direction = "rtl"
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.book.text_direction, Some(TextDirection::RightToLeft));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_direction_none() {
|
||||
let src = r#"
|
||||
[book]
|
||||
"#;
|
||||
|
||||
let got = Config::from_str(src).unwrap();
|
||||
assert_eq!(got.book.text_direction, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_text_direction() {
|
||||
let mut cfg = BookConfig::default();
|
||||
|
||||
// test deriving the text direction from language codes
|
||||
cfg.language = Some("ar".into());
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
|
||||
|
||||
cfg.language = Some("he".into());
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
|
||||
|
||||
cfg.language = Some("en".into());
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
|
||||
|
||||
cfg.language = Some("ja".into());
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
|
||||
|
||||
// test forced direction
|
||||
cfg.language = Some("ar".into());
|
||||
cfg.text_direction = Some(TextDirection::LeftToRight);
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
|
||||
|
||||
cfg.language = Some("ar".into());
|
||||
cfg.text_direction = Some(TextDirection::RightToLeft);
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
|
||||
|
||||
cfg.language = Some("en".into());
|
||||
cfg.text_direction = Some(TextDirection::LeftToRight);
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::LeftToRight);
|
||||
|
||||
cfg.language = Some("en".into());
|
||||
cfg.text_direction = Some(TextDirection::RightToLeft);
|
||||
assert_eq!(cfg.realized_text_direction(), TextDirection::RightToLeft);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "Invalid configuration file")]
|
||||
fn invalid_language_type_error() {
|
||||
|
@ -1193,4 +1328,37 @@ mod tests {
|
|||
assert!(html_config.print.enable);
|
||||
assert!(!html_config.print.page_break);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curly_quotes_or_smart_punctuation() {
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
|
||||
[output.html]
|
||||
smart-punctuation = true
|
||||
"#;
|
||||
let config = Config::from_str(src).unwrap();
|
||||
assert_eq!(config.html_config().unwrap().smart_punctuation(), true);
|
||||
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
|
||||
[output.html]
|
||||
curly-quotes = true
|
||||
"#;
|
||||
let config = Config::from_str(src).unwrap();
|
||||
assert_eq!(config.html_config().unwrap().smart_punctuation(), true);
|
||||
|
||||
let src = r#"
|
||||
[book]
|
||||
title = "mdBook Documentation"
|
||||
"#;
|
||||
let config = Config::from_str(src).unwrap();
|
||||
assert_eq!(
|
||||
config.html_config().unwrap_or_default().smart_punctuation(),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ where
|
|||
for link in find_links(s) {
|
||||
replaced.push_str(&s[previous_end_index..link.start_index]);
|
||||
|
||||
match link.render_with_path(&path, chapter_title) {
|
||||
match link.render_with_path(path, chapter_title) {
|
||||
Ok(new_content) => {
|
||||
if depth < MAX_LINK_NESTED_DEPTH {
|
||||
if let Some(rel_path) = link.link_type.relative_path(path) {
|
||||
|
@ -327,7 +327,7 @@ impl<'a> Link<'a> {
|
|||
let base = base.as_ref();
|
||||
match self.link_type {
|
||||
// omit the escape char
|
||||
LinkType::Escaped => Ok((&self.link_text[1..]).to_owned()),
|
||||
LinkType::Escaped => Ok(self.link_text[1..].to_owned()),
|
||||
LinkType::Include(ref pat, ref range_or_anchor) => {
|
||||
let target = base.join(pat);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::book::{Book, BookItem};
|
||||
use crate::config::{BookConfig, Config, HtmlConfig, Playground, RustEdition};
|
||||
use crate::config::{BookConfig, Code, Config, HtmlConfig, Playground, RustEdition};
|
||||
use crate::errors::*;
|
||||
use crate::renderer::html_handlebars::helpers;
|
||||
use crate::renderer::{RenderContext, Renderer};
|
||||
|
@ -54,11 +54,13 @@ impl HtmlHandlebars {
|
|||
.insert("git_repository_edit_url".to_owned(), json!(edit_url));
|
||||
}
|
||||
|
||||
let content = ch.content.clone();
|
||||
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
|
||||
let content = utils::render_markdown(&ch.content, ctx.html_config.smart_punctuation());
|
||||
|
||||
let fixed_content =
|
||||
utils::render_markdown_with_path(&ch.content, ctx.html_config.curly_quotes, Some(path));
|
||||
let fixed_content = utils::render_markdown_with_path(
|
||||
&ch.content,
|
||||
ctx.html_config.smart_punctuation(),
|
||||
Some(path),
|
||||
);
|
||||
if !ctx.is_index && ctx.html_config.print.page_break {
|
||||
// Add page break between chapters
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/break-before and https://developer.mozilla.org/en-US/docs/Web/CSS/page-break-before
|
||||
|
@ -99,7 +101,7 @@ impl HtmlHandlebars {
|
|||
ctx.data.insert("title".to_owned(), json!(title));
|
||||
ctx.data.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&path)),
|
||||
json!(utils::fs::path_to_root(path)),
|
||||
);
|
||||
if let Some(ref section) = ch.number {
|
||||
ctx.data
|
||||
|
@ -110,7 +112,12 @@ impl HtmlHandlebars {
|
|||
debug!("Render template");
|
||||
let rendered = ctx.handlebars.render("index", &ctx.data)?;
|
||||
|
||||
let rendered = self.post_process(rendered, &ctx.html_config.playground, ctx.edition);
|
||||
let rendered = self.post_process(
|
||||
rendered,
|
||||
&ctx.html_config.playground,
|
||||
&ctx.html_config.code,
|
||||
ctx.edition,
|
||||
);
|
||||
|
||||
// Write to file
|
||||
debug!("Creating {}", filepath.display());
|
||||
|
@ -121,8 +128,12 @@ impl HtmlHandlebars {
|
|||
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);
|
||||
let rendered_index = self.post_process(
|
||||
rendered_index,
|
||||
&ctx.html_config.playground,
|
||||
&ctx.html_config.code,
|
||||
ctx.edition,
|
||||
);
|
||||
debug!("Creating index.html from {}", ctx_path);
|
||||
utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
|
||||
}
|
||||
|
@ -156,7 +167,8 @@ impl HtmlHandlebars {
|
|||
.to_string()
|
||||
}
|
||||
};
|
||||
let html_content_404 = utils::render_markdown(&content_404, html_config.curly_quotes);
|
||||
let html_content_404 =
|
||||
utils::render_markdown(&content_404, html_config.smart_punctuation());
|
||||
|
||||
let mut data_404 = data.clone();
|
||||
let base_url = if let Some(site_url) = &html_config.site_url {
|
||||
|
@ -182,8 +194,12 @@ impl HtmlHandlebars {
|
|||
data_404.insert("title".to_owned(), json!(title));
|
||||
let rendered = handlebars.render("index", &data_404)?;
|
||||
|
||||
let rendered =
|
||||
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
|
||||
let rendered = self.post_process(
|
||||
rendered,
|
||||
&html_config.playground,
|
||||
&html_config.code,
|
||||
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 ✓");
|
||||
|
@ -195,11 +211,13 @@ impl HtmlHandlebars {
|
|||
&self,
|
||||
rendered: String,
|
||||
playground_config: &Playground,
|
||||
code_config: &Code,
|
||||
edition: Option<RustEdition>,
|
||||
) -> String {
|
||||
let rendered = build_header_links(&rendered);
|
||||
let rendered = fix_code_blocks(&rendered);
|
||||
let rendered = add_playground_pre(&rendered, playground_config, edition);
|
||||
let rendered = hide_lines(&rendered, code_config);
|
||||
|
||||
rendered
|
||||
}
|
||||
|
@ -275,7 +293,8 @@ impl HtmlHandlebars {
|
|||
"FontAwesome/fonts/FontAwesome.ttf",
|
||||
theme::FONT_AWESOME_TTF,
|
||||
)?;
|
||||
if html_config.copy_fonts {
|
||||
// Don't copy the stock fonts if the user has specified their own fonts to use.
|
||||
if html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
|
||||
for (file_name, contents) in theme::fonts::LICENSES.iter() {
|
||||
write_file(destination, file_name, contents)?;
|
||||
|
@ -291,20 +310,13 @@ impl HtmlHandlebars {
|
|||
}
|
||||
if let Some(fonts_css) = &theme.fonts_css {
|
||||
if !fonts_css.is_empty() {
|
||||
if html_config.copy_fonts {
|
||||
warn!(
|
||||
"output.html.copy_fonts is deprecated.\n\
|
||||
Set copy_fonts=false and ensure the fonts you want are in \
|
||||
the `theme/fonts/` directory."
|
||||
);
|
||||
}
|
||||
write_file(destination, "fonts/fonts.css", &fonts_css)?;
|
||||
write_file(destination, "fonts/fonts.css", fonts_css)?;
|
||||
}
|
||||
}
|
||||
if !html_config.copy_fonts && theme.fonts_css.is_none() {
|
||||
warn!(
|
||||
"output.html.copy_fonts is deprecated.\n\
|
||||
This book appears to have copy_fonts=false without a fonts.css file.\n\
|
||||
"output.html.copy-fonts is deprecated.\n\
|
||||
This book appears to have copy-fonts=false in book.toml without a fonts.css file.\n\
|
||||
Add an empty `theme/fonts/fonts.css` file to squelch this warning."
|
||||
);
|
||||
}
|
||||
|
@ -469,25 +481,6 @@ impl HtmlHandlebars {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO(mattico): Remove some time after the 0.1.8 release
|
||||
fn maybe_wrong_theme_dir(dir: &Path) -> Result<bool> {
|
||||
fn entry_is_maybe_book_file(entry: fs::DirEntry) -> Result<bool> {
|
||||
Ok(entry.file_type()?.is_file()
|
||||
&& entry.path().extension().map_or(false, |ext| ext == "md"))
|
||||
}
|
||||
|
||||
if dir.is_dir() {
|
||||
for entry in fs::read_dir(dir)? {
|
||||
if entry_is_maybe_book_file(entry?).unwrap_or(false) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer for HtmlHandlebars {
|
||||
fn name(&self) -> &str {
|
||||
"html"
|
||||
|
@ -520,16 +513,6 @@ impl Renderer for HtmlHandlebars {
|
|||
None => ctx.root.join("theme"),
|
||||
};
|
||||
|
||||
if html_config.theme.is_none()
|
||||
&& maybe_wrong_theme_dir(&src_dir.join("theme")).unwrap_or(false)
|
||||
{
|
||||
warn!(
|
||||
"Previous versions of mdBook erroneously accepted `./src/theme` as an automatic \
|
||||
theme directory"
|
||||
);
|
||||
warn!("Please move your theme files to `./theme` for them to continue being used");
|
||||
}
|
||||
|
||||
let theme = theme::Theme::new(theme_dir);
|
||||
|
||||
debug!("Register the index handlebars template");
|
||||
|
@ -553,7 +536,7 @@ impl Renderer for HtmlHandlebars {
|
|||
// Print version
|
||||
let mut print_content = String::new();
|
||||
|
||||
fs::create_dir_all(&destination)
|
||||
fs::create_dir_all(destination)
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
let mut is_index = true;
|
||||
|
@ -589,8 +572,12 @@ 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.playground,
|
||||
&html_config.code,
|
||||
ctx.config.rust.edition,
|
||||
);
|
||||
|
||||
utils::fs::write_file(destination, "print.html", rendered.as_bytes())?;
|
||||
debug!("Creating print.html ✓");
|
||||
|
@ -635,6 +622,10 @@ fn make_data(
|
|||
"language".to_owned(),
|
||||
json!(config.book.language.clone().unwrap_or_default()),
|
||||
);
|
||||
data.insert(
|
||||
"text_direction".to_owned(),
|
||||
json!(config.book.realized_text_direction()),
|
||||
);
|
||||
data.insert(
|
||||
"book_title".to_owned(),
|
||||
json!(config.book.title.clone().unwrap_or_default()),
|
||||
|
@ -795,8 +786,10 @@ fn make_data(
|
|||
/// Goes through the rendered HTML, making sure all header tags have
|
||||
/// an anchor respectively so people can link to sections directly.
|
||||
fn build_header_links(html: &str) -> String {
|
||||
static BUILD_HEADER_LINKS: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"<h(\d)>(.*?)</h\d>").unwrap());
|
||||
static BUILD_HEADER_LINKS: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r#"<h(\d)(?: id="([^"]+)")?(?: class="([^"]+)")?>(.*?)</h\d>"#).unwrap()
|
||||
});
|
||||
static IGNORE_CLASS: &[&str] = &["menu-title"];
|
||||
|
||||
let mut id_counter = HashMap::new();
|
||||
|
||||
|
@ -806,7 +799,22 @@ fn build_header_links(html: &str) -> String {
|
|||
.parse()
|
||||
.expect("Regex should ensure we only ever get numbers here");
|
||||
|
||||
insert_link_into_header(level, &caps[2], &mut id_counter)
|
||||
// Ignore .menu-title because now it's getting detected by the regex.
|
||||
if let Some(classes) = caps.get(3) {
|
||||
for class in classes.as_str().split(" ") {
|
||||
if IGNORE_CLASS.contains(&class) {
|
||||
return caps[0].to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
insert_link_into_header(
|
||||
level,
|
||||
&caps[4],
|
||||
caps.get(2).map(|x| x.as_str().to_string()),
|
||||
caps.get(3).map(|x| x.as_str().to_string()),
|
||||
&mut id_counter,
|
||||
)
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
@ -816,15 +824,21 @@ fn build_header_links(html: &str) -> String {
|
|||
fn insert_link_into_header(
|
||||
level: usize,
|
||||
content: &str,
|
||||
id: Option<String>,
|
||||
classes: Option<String>,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
) -> String {
|
||||
let id = utils::unique_id_from_content(content, id_counter);
|
||||
let id = id.unwrap_or_else(|| utils::unique_id_from_content(content, id_counter));
|
||||
let classes = classes
|
||||
.map(|s| format!(" class=\"{s}\""))
|
||||
.unwrap_or_default();
|
||||
|
||||
format!(
|
||||
r##"<h{level} id="{id}"><a class="header" href="#{id}">{text}</a></h{level}>"##,
|
||||
r##"<h{level} id="{id}"{classes}><a class="header" href="#{id}">{text}</a></h{level}>"##,
|
||||
level = level,
|
||||
id = id,
|
||||
text = content
|
||||
text = content,
|
||||
classes = classes
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -856,67 +870,64 @@ fn fix_code_blocks(html: &str) -> String {
|
|||
.into_owned()
|
||||
}
|
||||
|
||||
static CODE_BLOCK_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
|
||||
|
||||
fn add_playground_pre(
|
||||
html: &str,
|
||||
playground_config: &Playground,
|
||||
edition: Option<RustEdition>,
|
||||
) -> String {
|
||||
static ADD_PLAYGROUND_PRE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap());
|
||||
|
||||
ADD_PLAYGROUND_PRE
|
||||
CODE_BLOCK_RE
|
||||
.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")
|
||||
if classes.contains("language-rust")
|
||||
&& ((!classes.contains("ignore")
|
||||
&& !classes.contains("noplayground")
|
||||
&& !classes.contains("noplaypen")
|
||||
&& playground_config.runnable)
|
||||
|| classes.contains("mdbook-runnable")
|
||||
{
|
||||
let contains_e2015 = classes.contains("edition2015");
|
||||
let contains_e2018 = classes.contains("edition2018");
|
||||
let contains_e2021 = classes.contains("edition2021");
|
||||
let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 {
|
||||
// the user forced edition, we should not overwrite it
|
||||
""
|
||||
} else {
|
||||
match edition {
|
||||
Some(RustEdition::E2015) => " edition2015",
|
||||
Some(RustEdition::E2018) => " edition2018",
|
||||
Some(RustEdition::E2021) => " edition2021",
|
||||
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!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
|
||||
.into()
|
||||
};
|
||||
hide_lines(&content)
|
||||
}
|
||||
)
|
||||
|| classes.contains("mdbook-runnable"))
|
||||
{
|
||||
let contains_e2015 = classes.contains("edition2015");
|
||||
let contains_e2018 = classes.contains("edition2018");
|
||||
let contains_e2021 = classes.contains("edition2021");
|
||||
let edition_class = if contains_e2015 || contains_e2018 || contains_e2021 {
|
||||
// the user forced edition, we should not overwrite it
|
||||
""
|
||||
} else {
|
||||
format!("<code class=\"{}\">{}</code>", classes, hide_lines(code))
|
||||
}
|
||||
match edition {
|
||||
Some(RustEdition::E2015) => " edition2015",
|
||||
Some(RustEdition::E2018) => " edition2018",
|
||||
Some(RustEdition::E2021) => " edition2021",
|
||||
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!("# #![allow(unused)]\n{}#fn main() {{\n{}#}}", attrs, code)
|
||||
.into()
|
||||
};
|
||||
content
|
||||
}
|
||||
)
|
||||
} else {
|
||||
// not language-rust, so no-op
|
||||
text.to_owned()
|
||||
|
@ -925,7 +936,51 @@ fn add_playground_pre(
|
|||
.into_owned()
|
||||
}
|
||||
|
||||
fn hide_lines(content: &str) -> String {
|
||||
/// Modifies all `<code>` blocks to convert "hidden" lines and to wrap them in
|
||||
/// a `<span class="boring">`.
|
||||
fn hide_lines(html: &str, code_config: &Code) -> String {
|
||||
static LANGUAGE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\blanguage-(\w+)\b").unwrap());
|
||||
static HIDELINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\bhidelines=(\S+)").unwrap());
|
||||
|
||||
CODE_BLOCK_RE
|
||||
.replace_all(html, |caps: &Captures<'_>| {
|
||||
let text = &caps[1];
|
||||
let classes = &caps[2];
|
||||
let code = &caps[3];
|
||||
|
||||
if classes.contains("language-rust") {
|
||||
format!(
|
||||
"<code class=\"{}\">{}</code>",
|
||||
classes,
|
||||
hide_lines_rust(code)
|
||||
)
|
||||
} else {
|
||||
// First try to get the prefix from the code block
|
||||
let hidelines_capture = HIDELINES_REGEX.captures(classes);
|
||||
let hidelines_prefix = match &hidelines_capture {
|
||||
Some(capture) => Some(&capture[1]),
|
||||
None => {
|
||||
// Then look up the prefix by language
|
||||
LANGUAGE_REGEX.captures(classes).and_then(|capture| {
|
||||
code_config.hidelines.get(&capture[1]).map(|p| p.as_str())
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
match hidelines_prefix {
|
||||
Some(prefix) => format!(
|
||||
"<code class=\"{}\">{}</code>",
|
||||
classes,
|
||||
hide_lines_with_prefix(code, prefix)
|
||||
),
|
||||
None => text.to_owned(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn hide_lines_rust(content: &str) -> String {
|
||||
static BORING_LINES_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(\s*)#(.?)(.*)$").unwrap());
|
||||
|
||||
let mut result = String::with_capacity(content.len());
|
||||
|
@ -958,6 +1013,26 @@ fn hide_lines(content: &str) -> String {
|
|||
result
|
||||
}
|
||||
|
||||
fn hide_lines_with_prefix(content: &str, prefix: &str) -> String {
|
||||
let mut result = String::with_capacity(content.len());
|
||||
for line in content.lines() {
|
||||
if line.trim_start().starts_with(prefix) {
|
||||
let pos = line.find(prefix).unwrap();
|
||||
let (ws, rest) = (&line[..pos], &line[pos + prefix.len()..]);
|
||||
|
||||
result += "<span class=\"boring\">";
|
||||
result += ws;
|
||||
result += rest;
|
||||
result += "\n";
|
||||
result += "</span>";
|
||||
continue;
|
||||
}
|
||||
result += line;
|
||||
result += "\n";
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn partition_source(s: &str) -> (String, String) {
|
||||
let mut after_header = false;
|
||||
let mut before = String::new();
|
||||
|
@ -992,7 +1067,10 @@ struct RenderItemContext<'a> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::config::TextDirection;
|
||||
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn original_build_header_links() {
|
||||
|
@ -1021,6 +1099,21 @@ mod tests {
|
|||
"<h1>Foo</h1><h3>Foo</h3>",
|
||||
r##"<h1 id="foo"><a class="header" href="#foo">Foo</a></h1><h3 id="foo-1"><a class="header" href="#foo-1">Foo</a></h3>"##,
|
||||
),
|
||||
// id only
|
||||
(
|
||||
r##"<h1 id="foobar">Foo</h1>"##,
|
||||
r##"<h1 id="foobar"><a class="header" href="#foobar">Foo</a></h1>"##,
|
||||
),
|
||||
// class only
|
||||
(
|
||||
r##"<h1 class="class1 class2">Foo</h1>"##,
|
||||
r##"<h1 id="foo" class="class1 class2"><a class="header" href="#foo">Foo</a></h1>"##,
|
||||
),
|
||||
// both id and class
|
||||
(
|
||||
r##"<h1 id="foobar" class="class1 class2">Foo</h1>"##,
|
||||
r##"<h1 id="foobar" class="class1 class2"><a class="header" href="#foobar">Foo</a></h1>"##,
|
||||
),
|
||||
];
|
||||
|
||||
for (src, should_be) in inputs {
|
||||
|
@ -1033,17 +1126,17 @@ mod tests {
|
|||
fn add_playground() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</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>\";</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\";</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\";</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>\";</code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n#\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>\";</code>"),
|
||||
"<code class=\"language-rust ignore\">let s = \"foo\n # bar\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]</code></pre>"),
|
||||
];
|
||||
|
@ -1063,7 +1156,7 @@ mod tests {
|
|||
fn add_playground_edition2015() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
|
@ -1087,7 +1180,7 @@ mod tests {
|
|||
fn add_playground_edition2018() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
|
@ -1111,7 +1204,7 @@ mod tests {
|
|||
fn add_playground_edition2021() {
|
||||
let inputs = [
|
||||
("<code class=\"language-rust\">x()</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\"><span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}</span></code></pre>"),
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\"># #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>"),
|
||||
("<code class=\"language-rust\">fn main() {}</code>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust edition2021\">fn main() {}</code></pre>"),
|
||||
("<code class=\"language-rust edition2015\">fn main() {}</code>",
|
||||
|
@ -1131,4 +1224,66 @@ mod tests {
|
|||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_lines_language_rust() {
|
||||
let inputs = [
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">\n# #![allow(unused)]\n#fn main() {\nx()\n#}</code></pre>",
|
||||
"<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\">}</span></code></pre>",),
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}</code></pre>",),
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";</code></pre>",),
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code></pre>",),
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";</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>\";</code>",),
|
||||
(
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>",
|
||||
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code></pre>",),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = hide_lines(src, &Code::default());
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_lines_language_other() {
|
||||
let inputs = [
|
||||
(
|
||||
"<code class=\"language-python\">~hidden()\nnothidden():\n~ hidden()\n ~hidden()\n nothidden()</code>",
|
||||
"<code class=\"language-python\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\"> hidden()\n</span><span class=\"boring\"> hidden()\n</span> nothidden()\n</code>",),
|
||||
(
|
||||
"<code class=\"language-python hidelines=!!!\">!!!hidden()\nnothidden():\n!!! hidden()\n !!!hidden()\n nothidden()</code>",
|
||||
"<code class=\"language-python hidelines=!!!\"><span class=\"boring\">hidden()\n</span>nothidden():\n<span class=\"boring\"> hidden()\n</span><span class=\"boring\"> hidden()\n</span> nothidden()\n</code>",),
|
||||
];
|
||||
for (src, should_be) in &inputs {
|
||||
let got = hide_lines(
|
||||
src,
|
||||
&Code {
|
||||
hidelines: {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("python".to_string(), "~".to_string());
|
||||
map
|
||||
},
|
||||
},
|
||||
);
|
||||
assert_eq!(&*got, *should_be);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_direction() {
|
||||
assert_eq!(json!(TextDirection::RightToLeft), json!("rtl"));
|
||||
assert_eq!(json!(TextDirection::LeftToRight), json!("ltr"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError, Renderable};
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason, Renderable,
|
||||
};
|
||||
|
||||
use crate::utils;
|
||||
use log::{debug, trace};
|
||||
|
@ -26,9 +28,9 @@ impl Target {
|
|||
) -> Result<Option<StringMap>, RenderError> {
|
||||
match *self {
|
||||
Target::Next => {
|
||||
let previous_path = previous_item
|
||||
.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))?;
|
||||
let previous_path = previous_item.get("path").ok_or_else(|| {
|
||||
RenderErrorReason::Other("No path found for chapter in JSON data".to_owned())
|
||||
})?;
|
||||
|
||||
if previous_path == base_path {
|
||||
return Ok(Some(current_item.clone()));
|
||||
|
@ -54,15 +56,18 @@ fn find_chapter(
|
|||
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())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
serde_json::value::from_value::<Vec<StringMap>>(c.as_json().clone()).map_err(|_| {
|
||||
RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into()
|
||||
})
|
||||
})?;
|
||||
|
||||
let base_path = rc
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
|
||||
})?
|
||||
.replace('\"', "");
|
||||
|
||||
if !rc.evaluate(ctx, "@root/is_index")?.is_missing() {
|
||||
|
@ -98,7 +103,7 @@ fn find_chapter(
|
|||
}
|
||||
}
|
||||
|
||||
previous = Some(item.clone());
|
||||
previous = Some(item);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
|
@ -108,7 +113,7 @@ fn find_chapter(
|
|||
}
|
||||
|
||||
fn render(
|
||||
_h: &Helper<'_, '_>,
|
||||
_h: &Helper<'_>,
|
||||
r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
|
@ -122,27 +127,35 @@ fn render(
|
|||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
|
||||
})?
|
||||
.replace('\"', "");
|
||||
|
||||
context.insert(
|
||||
"path_to_root".to_owned(),
|
||||
json!(utils::fs::path_to_root(&base_path)),
|
||||
json!(utils::fs::path_to_root(base_path)),
|
||||
);
|
||||
|
||||
chapter
|
||||
.get("name")
|
||||
.ok_or_else(|| RenderError::new("No title found for chapter in JSON data"))
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("No title found for chapter in JSON data".to_owned())
|
||||
})
|
||||
.map(|name| context.insert("title".to_owned(), json!(name)))?;
|
||||
|
||||
chapter
|
||||
.get("path")
|
||||
.ok_or_else(|| RenderError::new("No path found for chapter in JSON data"))
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("No path found for chapter in JSON data".to_owned())
|
||||
})
|
||||
.and_then(|p| {
|
||||
Path::new(p)
|
||||
.with_extension("html")
|
||||
.to_str()
|
||||
.ok_or_else(|| RenderError::new("Link could not be converted to str"))
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Link could not be converted to str".to_owned())
|
||||
})
|
||||
.map(|p| context.insert("link".to_owned(), json!(p.replace('\\', "/"))))
|
||||
})?;
|
||||
|
||||
|
@ -150,14 +163,14 @@ fn render(
|
|||
|
||||
let t = _h
|
||||
.template()
|
||||
.ok_or_else(|| RenderError::new("Error with the handlebars template"))?;
|
||||
.ok_or_else(|| RenderErrorReason::Other("Error with the handlebars template".to_owned()))?;
|
||||
let local_ctx = Context::wraps(&context)?;
|
||||
let mut local_rc = rc.clone();
|
||||
t.render(r, &local_ctx, &mut local_rc, out)
|
||||
}
|
||||
|
||||
pub fn previous(
|
||||
_h: &Helper<'_, '_>,
|
||||
_h: &Helper<'_>,
|
||||
r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
|
@ -173,7 +186,7 @@ pub fn previous(
|
|||
}
|
||||
|
||||
pub fn next(
|
||||
_h: &Helper<'_, '_>,
|
||||
_h: &Helper<'_>,
|
||||
r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError};
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, Output, RenderContext, RenderError, RenderErrorReason,
|
||||
};
|
||||
use log::trace;
|
||||
|
||||
pub fn theme_option(
|
||||
h: &Helper<'_, '_>,
|
||||
h: &Helper<'_>,
|
||||
_r: &Handlebars<'_>,
|
||||
ctx: &Context,
|
||||
rc: &mut RenderContext<'_, '_>,
|
||||
|
@ -11,14 +13,21 @@ pub fn theme_option(
|
|||
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.")
|
||||
RenderErrorReason::ParamTypeMismatchForName(
|
||||
"theme_option",
|
||||
"0".to_owned(),
|
||||
"string".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
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"))?;
|
||||
let default_theme_name = default_theme.as_json().as_str().ok_or_else(|| {
|
||||
RenderErrorReason::ParamTypeMismatchForName(
|
||||
"theme_option",
|
||||
"default_theme".to_owned(),
|
||||
"string".to_owned(),
|
||||
)
|
||||
})?;
|
||||
|
||||
out.write(param)?;
|
||||
if param.to_lowercase() == default_theme_name.to_lowercase() {
|
||||
|
|
|
@ -4,7 +4,9 @@ use std::{cmp::Ordering, collections::BTreeMap};
|
|||
use crate::utils;
|
||||
use crate::utils::bracket_escape;
|
||||
|
||||
use handlebars::{Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError};
|
||||
use handlebars::{
|
||||
Context, Handlebars, Helper, HelperDef, Output, RenderContext, RenderError, RenderErrorReason,
|
||||
};
|
||||
|
||||
// Handlebars helper to construct TOC
|
||||
#[derive(Clone, Copy)]
|
||||
|
@ -15,7 +17,7 @@ pub struct RenderToc {
|
|||
impl HelperDef for RenderToc {
|
||||
fn call<'reg: 'rc, 'rc>(
|
||||
&self,
|
||||
_h: &Helper<'reg, 'rc>,
|
||||
_h: &Helper<'rc>,
|
||||
_r: &'reg Handlebars<'_>,
|
||||
ctx: &'rc Context,
|
||||
rc: &mut RenderContext<'reg, 'rc>,
|
||||
|
@ -26,13 +28,17 @@ impl HelperDef for RenderToc {
|
|||
// 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())
|
||||
.map_err(|_| RenderError::new("Could not decode the JSON data"))
|
||||
.map_err(|_| {
|
||||
RenderErrorReason::Other("Could not decode the JSON data".to_owned()).into()
|
||||
})
|
||||
})?;
|
||||
let current_path = rc
|
||||
.evaluate(ctx, "@root/path")?
|
||||
.as_json()
|
||||
.as_str()
|
||||
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `path`, string expected".to_owned())
|
||||
})?
|
||||
.replace('\"', "");
|
||||
|
||||
let current_section = rc
|
||||
|
@ -46,13 +52,17 @@ impl HelperDef for RenderToc {
|
|||
.evaluate(ctx, "@root/fold_enable")?
|
||||
.as_json()
|
||||
.as_bool()
|
||||
.ok_or_else(|| RenderError::new("Type error for `fold_enable`, bool expected"))?;
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `fold_enable`, bool expected".to_owned())
|
||||
})?;
|
||||
|
||||
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"))?;
|
||||
.ok_or_else(|| {
|
||||
RenderErrorReason::Other("Type error for `fold_level`, u64 expected".to_owned())
|
||||
})?;
|
||||
|
||||
out.write("<ol class=\"chapter\">")?;
|
||||
|
||||
|
|
|
@ -66,10 +66,23 @@ fn add_doc(
|
|||
index: &mut Index,
|
||||
doc_urls: &mut Vec<String>,
|
||||
anchor_base: &str,
|
||||
section_id: &Option<String>,
|
||||
heading: &str,
|
||||
id_counter: &mut HashMap<String, usize>,
|
||||
section_id: &Option<CowStr<'_>>,
|
||||
items: &[&str],
|
||||
) {
|
||||
let url = if let Some(ref id) = *section_id {
|
||||
// Either use the explicit section id the user specified, or generate one
|
||||
// from the heading content.
|
||||
let section_id = section_id.as_ref().map(|id| id.to_string()).or_else(|| {
|
||||
if heading.is_empty() {
|
||||
// In the case where a chapter has no heading, don't set a section id.
|
||||
None
|
||||
} else {
|
||||
Some(utils::unique_id_from_content(heading, id_counter))
|
||||
}
|
||||
});
|
||||
|
||||
let url = if let Some(id) = section_id {
|
||||
Cow::Owned(format!("{}#{}", anchor_base, id))
|
||||
} else {
|
||||
Cow::Borrowed(anchor_base)
|
||||
|
@ -119,7 +132,7 @@ fn render_item(
|
|||
let mut id_counter = HashMap::new();
|
||||
while let Some(event) = p.next() {
|
||||
match event {
|
||||
Event::Start(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
|
||||
Event::Start(Tag::Heading { level, id, .. }) if level as u32 <= max_section_depth => {
|
||||
if !heading.is_empty() {
|
||||
// Section finished, the next heading is following now
|
||||
// Write the data to the index, and clear it for the next section
|
||||
|
@ -127,20 +140,21 @@ fn render_item(
|
|||
index,
|
||||
doc_urls,
|
||||
&anchor_base,
|
||||
&heading,
|
||||
&mut id_counter,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
section_id = None;
|
||||
heading.clear();
|
||||
body.clear();
|
||||
breadcrumbs.pop();
|
||||
}
|
||||
|
||||
section_id = id;
|
||||
in_heading = true;
|
||||
}
|
||||
Event::End(Tag::Heading(i, ..)) if i as u32 <= max_section_depth => {
|
||||
Event::End(TagEnd::Heading(level)) if level as u32 <= max_section_depth => {
|
||||
in_heading = false;
|
||||
section_id = Some(utils::unique_id_from_content(&heading, &mut id_counter));
|
||||
breadcrumbs.push(heading.clone());
|
||||
}
|
||||
Event::Start(Tag::FootnoteDefinition(name)) => {
|
||||
|
@ -157,9 +171,19 @@ fn render_item(
|
|||
html_block.push_str(html);
|
||||
p.next();
|
||||
}
|
||||
|
||||
body.push_str(&clean_html(&html_block));
|
||||
}
|
||||
Event::InlineHtml(html) => {
|
||||
// This is not capable of cleaning inline tags like
|
||||
// `foo <script>…</script>`. The `<script>` tags show up as
|
||||
// individual InlineHtml events, and the content inside is
|
||||
// just a regular Text event. There isn't a very good way to
|
||||
// know how to collect all the content in-between. I'm not
|
||||
// sure if this is easily fixable. It should be extremely
|
||||
// rare, since script and style tags should almost always be
|
||||
// blocks, and worse case you have some noise in the index.
|
||||
body.push_str(&clean_html(&html));
|
||||
}
|
||||
Event::Start(_) | Event::End(_) | Event::Rule | Event::SoftBreak | Event::HardBreak => {
|
||||
// Insert spaces where HTML output would usually separate text
|
||||
// to ensure words don't get merged together
|
||||
|
@ -186,18 +210,24 @@ fn render_item(
|
|||
}
|
||||
|
||||
if !body.is_empty() || !heading.is_empty() {
|
||||
if heading.is_empty() {
|
||||
let title = if heading.is_empty() {
|
||||
if let Some(chapter) = breadcrumbs.first() {
|
||||
heading = chapter.clone();
|
||||
chapter
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
&heading
|
||||
};
|
||||
// Make sure the last section is added to the index
|
||||
add_doc(
|
||||
index,
|
||||
doc_urls,
|
||||
&anchor_base,
|
||||
&heading,
|
||||
&mut id_counter,
|
||||
§ion_id,
|
||||
&[&heading, &body, &breadcrumbs.join(" » ")],
|
||||
&[title, &body, &breadcrumbs.join(" » ")],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -37,14 +37,14 @@ impl Renderer for MarkdownRenderer {
|
|||
if !ch.is_draft_chapter() {
|
||||
utils::fs::write_file(
|
||||
&ctx.destination,
|
||||
&ch.path.as_ref().expect("Checked path exists before"),
|
||||
ch.path.as_ref().expect("Checked path exists before"),
|
||||
ch.content.as_bytes(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_dir_all(&destination)
|
||||
fs::create_dir_all(destination)
|
||||
.with_context(|| "Unexpected error when constructing destination path")?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -68,7 +68,7 @@ function playground_text(playground, hidden = true) {
|
|||
}
|
||||
|
||||
// updates the visibility of play button based on `no_run` class and
|
||||
// used crates vs ones available on http://play.rust-lang.org
|
||||
// used crates vs ones available on https://play.rust-lang.org
|
||||
function update_play_button(pre_block, playground_crates) {
|
||||
var play_button = pre_block.querySelector(".play-button");
|
||||
|
||||
|
@ -179,7 +179,7 @@ function playground_text(playground, hidden = true) {
|
|||
// even if highlighting doesn't apply
|
||||
code_nodes.forEach(function (block) { block.classList.add('hljs'); });
|
||||
|
||||
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
|
||||
Array.from(document.querySelectorAll("code.hljs")).forEach(function (block) {
|
||||
|
||||
var lines = Array.from(block.querySelectorAll('.boring'));
|
||||
// If no lines were hidden, return
|
||||
|
@ -346,7 +346,7 @@ function playground_text(playground, hidden = true) {
|
|||
}
|
||||
|
||||
setTimeout(function () {
|
||||
themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor;
|
||||
themeColorMetaTag.content = getComputedStyle(document.documentElement).backgroundColor;
|
||||
}, 1);
|
||||
|
||||
if (window.ace && window.editors) {
|
||||
|
@ -441,7 +441,7 @@ function playground_text(playground, hidden = true) {
|
|||
})();
|
||||
|
||||
(function sidebar() {
|
||||
var html = document.querySelector("html");
|
||||
var body = document.querySelector("body");
|
||||
var sidebar = document.getElementById("sidebar");
|
||||
var sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||
var sidebarToggleButton = document.getElementById("sidebar-toggle");
|
||||
|
@ -449,8 +449,8 @@ function playground_text(playground, hidden = true) {
|
|||
var firstContact = null;
|
||||
|
||||
function showSidebar() {
|
||||
html.classList.remove('sidebar-hidden')
|
||||
html.classList.add('sidebar-visible');
|
||||
body.classList.remove('sidebar-hidden')
|
||||
body.classList.add('sidebar-visible');
|
||||
Array.from(sidebarLinks).forEach(function (link) {
|
||||
link.setAttribute('tabIndex', 0);
|
||||
});
|
||||
|
@ -471,8 +471,8 @@ function playground_text(playground, hidden = true) {
|
|||
});
|
||||
|
||||
function hideSidebar() {
|
||||
html.classList.remove('sidebar-visible')
|
||||
html.classList.add('sidebar-hidden');
|
||||
body.classList.remove('sidebar-visible')
|
||||
body.classList.add('sidebar-hidden');
|
||||
Array.from(sidebarLinks).forEach(function (link) {
|
||||
link.setAttribute('tabIndex', -1);
|
||||
});
|
||||
|
@ -483,14 +483,14 @@ function playground_text(playground, hidden = true) {
|
|||
|
||||
// Toggle sidebar
|
||||
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
|
||||
if (html.classList.contains("sidebar-hidden")) {
|
||||
if (body.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")) {
|
||||
} else if (body.classList.contains("sidebar-visible")) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (getComputedStyle(sidebar)['transform'] === 'none') {
|
||||
|
@ -506,14 +506,14 @@ function playground_text(playground, hidden = true) {
|
|||
function initResize(e) {
|
||||
window.addEventListener('mousemove', resize, false);
|
||||
window.addEventListener('mouseup', stopResize, false);
|
||||
html.classList.add('sidebar-resizing');
|
||||
body.classList.add('sidebar-resizing');
|
||||
}
|
||||
function resize(e) {
|
||||
var pos = (e.clientX - sidebar.offsetLeft);
|
||||
if (pos < 20) {
|
||||
hideSidebar();
|
||||
} else {
|
||||
if (html.classList.contains("sidebar-hidden")) {
|
||||
if (body.classList.contains("sidebar-hidden")) {
|
||||
showSidebar();
|
||||
}
|
||||
pos = Math.min(pos, window.innerWidth - 100);
|
||||
|
@ -522,7 +522,7 @@ function playground_text(playground, hidden = true) {
|
|||
}
|
||||
//on mouseup remove windows functions mousemove & mouseup
|
||||
function stopResize(e) {
|
||||
html.classList.remove('sidebar-resizing');
|
||||
body.classList.remove('sidebar-resizing');
|
||||
window.removeEventListener('mousemove', resize, false);
|
||||
window.removeEventListener('mouseup', stopResize, false);
|
||||
}
|
||||
|
@ -551,33 +551,41 @@ function playground_text(playground, hidden = true) {
|
|||
firstContact = null;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Scroll sidebar to current active section
|
||||
var activeSection = document.getElementById("sidebar").querySelector(".active");
|
||||
if (activeSection) {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
})();
|
||||
|
||||
(function chapterNavigation() {
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||
if (window.search && window.search.hasFocus()) { return; }
|
||||
var html = document.querySelector('html');
|
||||
|
||||
function next() {
|
||||
var nextButton = document.querySelector('.nav-chapters.next');
|
||||
if (nextButton) {
|
||||
window.location.href = nextButton.href;
|
||||
}
|
||||
}
|
||||
function prev() {
|
||||
var previousButton = document.querySelector('.nav-chapters.previous');
|
||||
if (previousButton) {
|
||||
window.location.href = previousButton.href;
|
||||
}
|
||||
}
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
var nextButton = document.querySelector('.nav-chapters.next');
|
||||
if (nextButton) {
|
||||
window.location.href = nextButton.href;
|
||||
if (html.dir == 'rtl') {
|
||||
prev();
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
var previousButton = document.querySelector('.nav-chapters.previous');
|
||||
if (previousButton) {
|
||||
window.location.href = previousButton.href;
|
||||
if (html.dir == 'rtl') {
|
||||
next();
|
||||
} else {
|
||||
prev();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -676,13 +684,14 @@ function playground_text(playground, hidden = true) {
|
|||
}, { passive: true });
|
||||
})();
|
||||
(function controllBorder() {
|
||||
menu.classList.remove('bordered');
|
||||
document.addEventListener('scroll', function () {
|
||||
function updateBorder() {
|
||||
if (menu.offsetTop === 0) {
|
||||
menu.classList.remove('bordered');
|
||||
} else {
|
||||
menu.classList.add('bordered');
|
||||
}
|
||||
}, { passive: true });
|
||||
}
|
||||
updateBorder();
|
||||
document.addEventListener('scroll', updateBorder, { passive: true });
|
||||
})();
|
||||
})();
|
||||
|
|
|
@ -22,7 +22,7 @@ a > .hljs {
|
|||
the screen on small screens. Without it, dragging on mobile Safari
|
||||
will want to reposition the viewport in a weird way.
|
||||
*/
|
||||
overflow-x: hidden;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
/* Menu Bar */
|
||||
|
@ -37,9 +37,9 @@ a > .hljs {
|
|||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background-color: var(--bg);
|
||||
border-bottom-color: var(--bg);
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-block-end-color: var(--bg);
|
||||
border-block-end-width: 1px;
|
||||
border-block-end-style: solid;
|
||||
}
|
||||
#menu-bar.sticky,
|
||||
.js #menu-bar-hover-placeholder:hover + #menu-bar,
|
||||
|
@ -56,7 +56,7 @@ a > .hljs {
|
|||
height: var(--menu-bar-height);
|
||||
}
|
||||
#menu-bar.bordered {
|
||||
border-bottom-color: var(--table-border-color);
|
||||
border-block-end-color: var(--table-border-color);
|
||||
}
|
||||
#menu-bar i, #menu-bar .icon-button {
|
||||
position: relative;
|
||||
|
@ -93,7 +93,7 @@ a > .hljs {
|
|||
display: flex;
|
||||
margin: 0 5px;
|
||||
}
|
||||
.no-js .left-buttons {
|
||||
.no-js .left-buttons button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -160,7 +160,7 @@ a > .hljs {
|
|||
}
|
||||
|
||||
.nav-wrapper {
|
||||
margin-top: 50px;
|
||||
margin-block-start: 50px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
@ -173,23 +173,34 @@ a > .hljs {
|
|||
background-color: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
.previous {
|
||||
float: left;
|
||||
}
|
||||
/* Only Firefox supports flow-relative values */
|
||||
.previous { float: left; }
|
||||
[dir=rtl] .previous { float: right; }
|
||||
|
||||
/* Only Firefox supports flow-relative values */
|
||||
.next {
|
||||
float: right;
|
||||
right: var(--page-padding);
|
||||
}
|
||||
[dir=rtl] .next {
|
||||
float: left;
|
||||
right: unset;
|
||||
left: var(--page-padding);
|
||||
}
|
||||
|
||||
/* Use the correct buttons for RTL layouts*/
|
||||
[dir=rtl] .previous i.fa-angle-left:before {content:"\f105";}
|
||||
[dir=rtl] .next i.fa-angle-right:before { content:"\f104"; }
|
||||
|
||||
@media only screen and (max-width: 1080px) {
|
||||
.nav-wide-wrapper { display: none; }
|
||||
.nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
/* sidebar-visible */
|
||||
@media only screen and (max-width: 1380px) {
|
||||
.sidebar-visible .nav-wide-wrapper { display: none; }
|
||||
.sidebar-visible .nav-wrapper { display: block; }
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wide-wrapper { display: none; }
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper .nav-wrapper { display: block; }
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
|
@ -236,7 +247,7 @@ pre > .buttons :hover {
|
|||
background-color: var(--theme-hover);
|
||||
}
|
||||
pre > .buttons i {
|
||||
margin-left: 8px;
|
||||
margin-inline-start: 8px;
|
||||
}
|
||||
pre > .buttons button {
|
||||
cursor: inherit;
|
||||
|
@ -258,8 +269,14 @@ pre > .buttons button {
|
|||
/* On mobile, make it easier to tap buttons. */
|
||||
padding: 0.3rem 1rem;
|
||||
}
|
||||
|
||||
.sidebar-resize-indicator {
|
||||
/* Hide resize indicator on devices with limited accuracy */
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
pre > code {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
@ -273,7 +290,7 @@ pre > code {
|
|||
}
|
||||
|
||||
pre > .result {
|
||||
margin-top: 10px;
|
||||
margin-block-start: 10px;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
|
@ -284,8 +301,14 @@ pre > .result {
|
|||
|
||||
mark {
|
||||
border-radius: 2px;
|
||||
padding: 0 3px 1px 3px;
|
||||
margin: 0 -3px -1px -3px;
|
||||
padding-block-start: 0;
|
||||
padding-block-end: 1px;
|
||||
padding-inline-start: 3px;
|
||||
padding-inline-end: 3px;
|
||||
margin-block-start: 0;
|
||||
margin-block-end: -1px;
|
||||
margin-inline-start: -3px;
|
||||
margin-inline-end: -3px;
|
||||
background-color: var(--search-mark-bg);
|
||||
transition: background-color 300ms linear;
|
||||
cursor: pointer;
|
||||
|
@ -297,14 +320,17 @@ mark.fade-out {
|
|||
}
|
||||
|
||||
.searchbar-outer {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
|
||||
#searchbar {
|
||||
width: 100%;
|
||||
margin: 5px auto 0px auto;
|
||||
margin-block-start: 5px;
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
padding: 10px 16px;
|
||||
transition: box-shadow 300ms ease-in-out;
|
||||
border: 1px solid var(--searchbar-border-color);
|
||||
|
@ -320,20 +346,23 @@ mark.fade-out {
|
|||
.searchresults-header {
|
||||
font-weight: bold;
|
||||
font-size: 1em;
|
||||
padding: 18px 0 0 5px;
|
||||
padding-block-start: 18px;
|
||||
padding-block-end: 0;
|
||||
padding-inline-start: 5px;
|
||||
padding-inline-end: 0;
|
||||
color: var(--searchresults-header-fg);
|
||||
}
|
||||
|
||||
.searchresults-outer {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
max-width: var(--content-max-width);
|
||||
border-bottom: 1px dashed var(--searchresults-border-color);
|
||||
border-block-end: 1px dashed var(--searchresults-border-color);
|
||||
}
|
||||
|
||||
ul#searchresults {
|
||||
list-style: none;
|
||||
padding-left: 20px;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
ul#searchresults li {
|
||||
margin: 10px 0px;
|
||||
|
@ -346,7 +375,10 @@ ul#searchresults li.focus {
|
|||
ul#searchresults span.teaser {
|
||||
display: block;
|
||||
clear: both;
|
||||
margin: 5px 0 0 20px;
|
||||
margin-block-start: 5px;
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: 20px;
|
||||
margin-inline-end: 0;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
ul#searchresults span.teaser em {
|
||||
|
@ -369,12 +401,14 @@ ul#searchresults span.teaser em {
|
|||
background-color: var(--sidebar-bg);
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
[dir=rtl] .sidebar { left: unset; right: 0; }
|
||||
.sidebar-resizing {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.no-js .sidebar,
|
||||
.js:not(.sidebar-resizing) .sidebar {
|
||||
transition: transform 0.3s; /* Animation: slide away */
|
||||
}
|
||||
|
@ -394,16 +428,35 @@ ul#searchresults span.teaser em {
|
|||
position: absolute;
|
||||
cursor: col-resize;
|
||||
width: 0;
|
||||
right: 0;
|
||||
right: calc(var(--sidebar-resize-indicator-width) * -1);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sidebar-resize-handle .sidebar-resize-indicator {
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
background-color: var(--icons);
|
||||
margin-inline-start: var(--sidebar-resize-indicator-space);
|
||||
}
|
||||
|
||||
[dir=rtl] .sidebar .sidebar-resize-handle {
|
||||
left: calc(var(--sidebar-resize-indicator-width) * -1);
|
||||
right: unset;
|
||||
}
|
||||
.js .sidebar .sidebar-resize-handle {
|
||||
cursor: col-resize;
|
||||
width: 5px;
|
||||
width: calc(var(--sidebar-resize-indicator-width) - var(--sidebar-resize-indicator-space));
|
||||
}
|
||||
.sidebar-hidden .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width)));
|
||||
/* sidebar-hidden */
|
||||
#sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
||||
z-index: -1;
|
||||
}
|
||||
[dir=rtl] #sidebar-toggle-anchor:not(:checked) ~ .sidebar {
|
||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
.sidebar::-webkit-scrollbar {
|
||||
background: var(--sidebar-bg);
|
||||
|
@ -412,19 +465,26 @@ ul#searchresults span.teaser em {
|
|||
background: var(--scrollbar);
|
||||
}
|
||||
|
||||
.sidebar-visible .page-wrapper {
|
||||
transform: translateX(var(--sidebar-width));
|
||||
/* sidebar-visible */
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: translateX(calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: translateX(calc(0px - var(--sidebar-width) - var(--sidebar-resize-indicator-width)));
|
||||
}
|
||||
@media only screen and (min-width: 620px) {
|
||||
.sidebar-visible .page-wrapper {
|
||||
#sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: none;
|
||||
margin-inline-start: calc(var(--sidebar-width) + var(--sidebar-resize-indicator-width));
|
||||
}
|
||||
[dir=rtl] #sidebar-toggle-anchor:checked ~ .page-wrapper {
|
||||
transform: none;
|
||||
margin-left: var(--sidebar-width);
|
||||
}
|
||||
}
|
||||
|
||||
.chapter {
|
||||
list-style: none outside none;
|
||||
padding-left: 0;
|
||||
padding-inline-start: 0;
|
||||
line-height: 2.2em;
|
||||
}
|
||||
|
||||
|
@ -454,7 +514,7 @@ ul#searchresults span.teaser em {
|
|||
.chapter li > a.toggle {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-inline-start: auto;
|
||||
padding: 0 10px;
|
||||
user-select: none;
|
||||
opacity: 0.68;
|
||||
|
@ -471,7 +531,7 @@ ul#searchresults span.teaser em {
|
|||
|
||||
.chapter li.chapter-item {
|
||||
line-height: 1.5em;
|
||||
margin-top: 0.6em;
|
||||
margin-block-start: 0.6em;
|
||||
}
|
||||
|
||||
.chapter li.expanded > a.toggle div {
|
||||
|
@ -494,7 +554,7 @@ ul#searchresults span.teaser em {
|
|||
|
||||
.section {
|
||||
list-style: none outside none;
|
||||
padding-left: 20px;
|
||||
padding-inline-start: 20px;
|
||||
line-height: 1.9em;
|
||||
}
|
||||
|
||||
|
@ -517,6 +577,7 @@ ul#searchresults span.teaser em {
|
|||
/* Don't let the children's background extend past the rounded corners. */
|
||||
overflow: hidden;
|
||||
}
|
||||
[dir=rtl] .theme-popup { left: unset; right: 10px; }
|
||||
.theme-popup .default {
|
||||
color: var(--icons);
|
||||
}
|
||||
|
@ -527,7 +588,7 @@ ul#searchresults span.teaser em {
|
|||
padding: 2px 20px;
|
||||
line-height: 25px;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
background: inherit;
|
||||
|
@ -540,6 +601,6 @@ ul#searchresults span.teaser em {
|
|||
.theme-selected::before {
|
||||
display: inline-block;
|
||||
content: "✓";
|
||||
margin-left: -14px;
|
||||
margin-inline-start: -14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
:root {
|
||||
/* Browser default font-size is 16px, this way 1 rem = 10px */
|
||||
font-size: 62.5%;
|
||||
color-scheme: var(--color-scheme);
|
||||
}
|
||||
|
||||
html {
|
||||
|
@ -24,6 +25,7 @@ body {
|
|||
code {
|
||||
font-family: var(--mono-font) !important;
|
||||
font-size: var(--code-font-size);
|
||||
direction: ltr !important;
|
||||
}
|
||||
|
||||
/* make long words/inline code not x overflow */
|
||||
|
@ -47,13 +49,13 @@ h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
|||
.hide-boring .boring { display: none; }
|
||||
.hidden { display: none !important; }
|
||||
|
||||
h2, h3 { margin-top: 2.5em; }
|
||||
h4, h5 { margin-top: 2em; }
|
||||
h2, h3 { margin-block-start: 2.5em; }
|
||||
h4, h5 { margin-block-start: 2em; }
|
||||
|
||||
.header + .header h3,
|
||||
.header + .header h4,
|
||||
.header + .header h5 {
|
||||
margin-top: 1em;
|
||||
margin-block-start: 1em;
|
||||
}
|
||||
|
||||
h1:target::before,
|
||||
|
@ -64,7 +66,7 @@ h5:target::before,
|
|||
h6:target::before {
|
||||
display: inline-block;
|
||||
content: "»";
|
||||
margin-left: -30px;
|
||||
margin-inline-start: -30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
|
@ -73,28 +75,34 @@ h6:target::before {
|
|||
https://bugs.webkit.org/show_bug.cgi?id=218076
|
||||
*/
|
||||
:target {
|
||||
/* Safari does not support logical properties */
|
||||
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 */
|
||||
margin-block-start: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
|
||||
}
|
||||
.page-wrapper {
|
||||
box-sizing: border-box;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
.no-js .page-wrapper,
|
||||
.js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
[dir=rtl] .js:not(.sidebar-resizing) .page-wrapper {
|
||||
transition: margin-right 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
padding: 0 5px 50px 5px;
|
||||
}
|
||||
.content main {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: auto;
|
||||
max-width: var(--content-max-width);
|
||||
}
|
||||
.content p { line-height: 1.45em; }
|
||||
|
@ -144,8 +152,31 @@ blockquote {
|
|||
padding: 0 20px;
|
||||
color: var(--fg);
|
||||
background-color: var(--quote-bg);
|
||||
border-top: .1em solid var(--quote-border);
|
||||
border-bottom: .1em solid var(--quote-border);
|
||||
border-block-start: .1em solid var(--quote-border);
|
||||
border-block-end: .1em solid var(--quote-border);
|
||||
}
|
||||
|
||||
.warning {
|
||||
margin: 20px;
|
||||
padding: 0 20px;
|
||||
border-inline-start: 2px solid var(--warning-border);
|
||||
}
|
||||
|
||||
.warning:before {
|
||||
position: absolute;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin-inline-start: calc(-1.5rem - 21px);
|
||||
content: "ⓘ";
|
||||
text-align: center;
|
||||
background-color: var(--bg);
|
||||
color: var(--warning-border);
|
||||
font-weight: bold;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
blockquote .warning:before {
|
||||
background-color: var(--quote-bg);
|
||||
}
|
||||
|
||||
kbd {
|
||||
|
@ -163,7 +194,7 @@ kbd {
|
|||
|
||||
:not(.footnote-definition) + .footnote-definition,
|
||||
.footnote-definition + :not(.footnote-definition) {
|
||||
margin-top: 2em;
|
||||
margin-block-start: 2em;
|
||||
}
|
||||
.footnote-definition {
|
||||
font-size: 0.9em;
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
}
|
||||
|
||||
#page-wrapper.page-wrapper {
|
||||
transform: none;
|
||||
margin-left: 0px;
|
||||
transform: none !important;
|
||||
margin-inline-start: 0px;
|
||||
overflow-y: initial;
|
||||
}
|
||||
|
||||
|
@ -23,11 +23,7 @@
|
|||
}
|
||||
|
||||
code {
|
||||
background-color: #666666;
|
||||
border-radius: 5px;
|
||||
|
||||
/* Force background to be printed in Chrome */
|
||||
-webkit-print-color-adjust: exact;
|
||||
direction: ltr !important;
|
||||
}
|
||||
|
||||
pre > .buttons {
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
:root {
|
||||
--sidebar-width: 300px;
|
||||
--sidebar-resize-indicator-width: 8px;
|
||||
--sidebar-resize-indicator-space: 2px;
|
||||
--page-padding: 15px;
|
||||
--content-max-width: 750px;
|
||||
--menu-bar-height: 50px;
|
||||
|
@ -38,6 +40,8 @@
|
|||
--quote-bg: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(210, 25%, 13%);
|
||||
--table-header-bg: hsl(210, 25%, 28%);
|
||||
--table-alternate-bg: hsl(210, 25%, 11%);
|
||||
|
@ -50,6 +54,8 @@
|
|||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #252932;
|
||||
--search-mark-bg: #e3b171;
|
||||
|
||||
--color-scheme: dark;
|
||||
}
|
||||
|
||||
.coal {
|
||||
|
@ -78,6 +84,8 @@
|
|||
--quote-bg: hsl(234, 21%, 18%);
|
||||
--quote-border: hsl(234, 21%, 23%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(200, 7%, 13%);
|
||||
--table-header-bg: hsl(200, 7%, 28%);
|
||||
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||
|
@ -90,6 +98,8 @@
|
|||
--searchresults-border-color: #98a3ad;
|
||||
--searchresults-li-bg: #2b2b2f;
|
||||
--search-mark-bg: #355c7d;
|
||||
|
||||
--color-scheme: dark;
|
||||
}
|
||||
|
||||
.light {
|
||||
|
@ -118,6 +128,8 @@
|
|||
--quote-bg: hsl(197, 37%, 96%);
|
||||
--quote-border: hsl(197, 37%, 91%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(0, 0%, 95%);
|
||||
--table-header-bg: hsl(0, 0%, 80%);
|
||||
--table-alternate-bg: hsl(0, 0%, 97%);
|
||||
|
@ -130,6 +142,8 @@
|
|||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #e4f2fe;
|
||||
--search-mark-bg: #a2cff5;
|
||||
|
||||
--color-scheme: light;
|
||||
}
|
||||
|
||||
.navy {
|
||||
|
@ -158,6 +172,8 @@
|
|||
--quote-bg: hsl(226, 15%, 17%);
|
||||
--quote-border: hsl(226, 15%, 22%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(226, 23%, 16%);
|
||||
--table-header-bg: hsl(226, 23%, 31%);
|
||||
--table-alternate-bg: hsl(226, 23%, 14%);
|
||||
|
@ -170,6 +186,8 @@
|
|||
--searchresults-border-color: #5c5c68;
|
||||
--searchresults-li-bg: #242430;
|
||||
--search-mark-bg: #a2cff5;
|
||||
|
||||
--color-scheme: dark;
|
||||
}
|
||||
|
||||
.rust {
|
||||
|
@ -198,6 +216,8 @@
|
|||
--quote-bg: hsl(60, 5%, 75%);
|
||||
--quote-border: hsl(60, 5%, 70%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(60, 9%, 82%);
|
||||
--table-header-bg: #b3a497;
|
||||
--table-alternate-bg: hsl(60, 9%, 84%);
|
||||
|
@ -210,6 +230,8 @@
|
|||
--searchresults-border-color: #888;
|
||||
--searchresults-li-bg: #dec2a2;
|
||||
--search-mark-bg: #e69f67;
|
||||
|
||||
--color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
@ -239,6 +261,8 @@
|
|||
--quote-bg: hsl(234, 21%, 18%);
|
||||
--quote-border: hsl(234, 21%, 23%);
|
||||
|
||||
--warning-border: #ff8e00;
|
||||
|
||||
--table-border-color: hsl(200, 7%, 13%);
|
||||
--table-header-bg: hsl(200, 7%, 28%);
|
||||
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,11 +1,11 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="{{ language }}" class="sidebar-visible no-js {{ default_theme }}">
|
||||
<html lang="{{ language }}" class="{{ default_theme }}" dir="{{ text_direction }}">
|
||||
<head>
|
||||
<!-- Book generated using mdBook -->
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ title }}</title>
|
||||
{{#if is_print }}
|
||||
<meta name="robots" content="noindex" />
|
||||
<meta name="robots" content="noindex">
|
||||
{{/if}}
|
||||
{{#if base_url}}
|
||||
<base href="{{ base_url }}">
|
||||
|
@ -17,7 +17,7 @@
|
|||
|
||||
<meta name="description" content="{{ description }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
{{#if favicon_svg}}
|
||||
<link rel="icon" href="{{ path_to_root }}favicon.svg">
|
||||
|
@ -53,7 +53,7 @@
|
|||
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
|
||||
{{/if}}
|
||||
</head>
|
||||
<body>
|
||||
<body class="sidebar-visible no-js">
|
||||
<div id="body-container">
|
||||
<!-- Provide site root to javascript -->
|
||||
<script>
|
||||
|
@ -83,41 +83,72 @@
|
|||
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');
|
||||
var body = document.querySelector('body');
|
||||
body.classList.remove('no-js')
|
||||
body.classList.add('js');
|
||||
</script>
|
||||
|
||||
<input type="checkbox" id="sidebar-toggle-anchor" class="hidden">
|
||||
|
||||
<!-- Hide / unhide sidebar before it is displayed -->
|
||||
<script>
|
||||
var html = document.querySelector('html');
|
||||
var sidebar = 'hidden';
|
||||
var body = document.querySelector('body');
|
||||
var sidebar = null;
|
||||
var sidebar_toggle = document.getElementById("sidebar-toggle-anchor");
|
||||
if (document.body.clientWidth >= 1080) {
|
||||
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||
sidebar = sidebar || 'visible';
|
||||
} else {
|
||||
sidebar = 'hidden';
|
||||
}
|
||||
html.classList.remove('sidebar-visible');
|
||||
html.classList.add("sidebar-" + sidebar);
|
||||
sidebar_toggle.checked = sidebar === 'visible';
|
||||
body.classList.remove('sidebar-visible');
|
||||
body.classList.add("sidebar-" + sidebar);
|
||||
</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>
|
||||
<div id="sidebar-resize-handle" class="sidebar-resize-handle">
|
||||
<div class="sidebar-resize-indicator"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Track and set sidebar scroll position -->
|
||||
<script>
|
||||
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
|
||||
sidebarScrollbox.addEventListener('click', function(e) {
|
||||
if (e.target.tagName === 'A') {
|
||||
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
|
||||
}
|
||||
}, { passive: true });
|
||||
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
|
||||
sessionStorage.removeItem('sidebar-scroll');
|
||||
if (sidebarScrollTop) {
|
||||
// preserve sidebar scroll position when navigating via links within sidebar
|
||||
sidebarScrollbox.scrollTop = sidebarScrollTop;
|
||||
} else {
|
||||
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
|
||||
var activeSection = document.querySelector('#sidebar .active');
|
||||
if (activeSection) {
|
||||
activeSection.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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 id="menu-bar" class="menu-bar sticky">
|
||||
<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">
|
||||
<label id="sidebar-toggle" class="icon-button" for="sidebar-toggle-anchor" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
</label>
|
||||
<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>
|
||||
|
@ -193,7 +224,7 @@
|
|||
{{/previous}}
|
||||
|
||||
{{#next}}
|
||||
<a rel="next" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<a rel="next prefetch" href="{{ path_to_root }}{{link}}" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||
<i class="fa fa-angle-right"></i>
|
||||
</a>
|
||||
{{/next}}
|
||||
|
@ -211,7 +242,7 @@
|
|||
{{/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 rel="next prefetch" 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}}
|
||||
|
|
|
@ -212,7 +212,6 @@ fn load_file_contents<P: AsRef<Path>>(filename: P, dest: &mut Vec<u8>) -> Result
|
|||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::Builder as TempFileBuilder;
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -316,7 +316,7 @@ window.search = window.search || {};
|
|||
|
||||
// Eventhandler for keyevents on `document`
|
||||
function globalKeyHandler(e) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; }
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text' || !hasFocus() && /^(?:input|select|textarea)$/i.test(e.target.nodeName)) { return; }
|
||||
|
||||
if (e.keyCode === ESCAPE_KEYCODE) {
|
||||
e.preventDefault();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* Tomorrow Night Theme */
|
||||
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||
/* https://github.com/jmblog/color-themes-for-highlightjs */
|
||||
/* Original theme - https://github.com/chriskempson/tomorrow-theme */
|
||||
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||
/* https://github.com/jmblog/color-themes-for-highlightjs */
|
||||
|
||||
/* Tomorrow Comment */
|
||||
.hljs-comment {
|
||||
|
|
164
src/utils/fs.rs
164
src/utils/fs.rs
|
@ -1,6 +1,5 @@
|
|||
use crate::errors::*;
|
||||
use log::{debug, trace};
|
||||
use std::convert::Into;
|
||||
use std::fs::{self, File};
|
||||
use std::io::Write;
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
@ -38,7 +37,6 @@ pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8])
|
|||
/// Consider [submitting a new issue](https://github.com/rust-lang/mdBook/issues)
|
||||
/// or a [pull-request](https://github.com/rust-lang/mdBook/pulls) to improve it.
|
||||
pub fn path_to_root<P: Into<PathBuf>>(path: P) -> String {
|
||||
debug!("path_to_root");
|
||||
// Remove filename and add "../" for every directory
|
||||
|
||||
path.into()
|
||||
|
@ -74,14 +72,12 @@ pub fn create_file(path: &Path) -> Result<File> {
|
|||
|
||||
/// Removes all the content of a directory but not the directory itself
|
||||
pub fn remove_dir_content(dir: &Path) -> Result<()> {
|
||||
for item in fs::read_dir(dir)? {
|
||||
if let Ok(item) = item {
|
||||
let item = item.path();
|
||||
if item.is_dir() {
|
||||
fs::remove_dir_all(item)?;
|
||||
} else {
|
||||
fs::remove_file(item)?;
|
||||
}
|
||||
for item in fs::read_dir(dir)?.flatten() {
|
||||
let item = item.path();
|
||||
if item.is_dir() {
|
||||
fs::remove_dir_all(item)?;
|
||||
} else {
|
||||
fs::remove_file(item)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -110,77 +106,102 @@ pub fn copy_files_except_ext(
|
|||
}
|
||||
|
||||
for entry in fs::read_dir(from)? {
|
||||
let entry = entry?;
|
||||
let entry = entry?.path();
|
||||
let metadata = entry
|
||||
.path()
|
||||
.metadata()
|
||||
.with_context(|| format!("Failed to read {:?}", entry.path()))?;
|
||||
.with_context(|| format!("Failed to read {entry:?}"))?;
|
||||
|
||||
let entry_file_name = entry.file_name().unwrap();
|
||||
let target_file_path = to.join(entry_file_name);
|
||||
|
||||
// If the entry is a dir and the recursive option is enabled, call itself
|
||||
if metadata.is_dir() && recursive {
|
||||
if entry.path() == to.to_path_buf() {
|
||||
if entry == to.as_os_str() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(avoid) = avoid_dir {
|
||||
if entry.path() == *avoid {
|
||||
if entry == *avoid {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// check if output dir already exists
|
||||
if !to.join(entry.file_name()).exists() {
|
||||
fs::create_dir(&to.join(entry.file_name()))?;
|
||||
if !target_file_path.exists() {
|
||||
fs::create_dir(&target_file_path)?;
|
||||
}
|
||||
|
||||
copy_files_except_ext(
|
||||
&from.join(entry.file_name()),
|
||||
&to.join(entry.file_name()),
|
||||
true,
|
||||
avoid_dir,
|
||||
ext_blacklist,
|
||||
)?;
|
||||
copy_files_except_ext(&entry, &target_file_path, true, avoid_dir, ext_blacklist)?;
|
||||
} else if metadata.is_file() {
|
||||
// Check if it is in the blacklist
|
||||
if let Some(ext) = entry.path().extension() {
|
||||
if let Some(ext) = entry.extension() {
|
||||
if ext_blacklist.contains(&ext.to_str().unwrap()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
debug!(
|
||||
"creating path for file: {:?}",
|
||||
&to.join(
|
||||
entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")
|
||||
)
|
||||
);
|
||||
|
||||
debug!(
|
||||
"Copying {:?} to {:?}",
|
||||
entry.path(),
|
||||
&to.join(
|
||||
entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name...")
|
||||
)
|
||||
);
|
||||
fs::copy(
|
||||
entry.path(),
|
||||
&to.join(
|
||||
entry
|
||||
.path()
|
||||
.file_name()
|
||||
.expect("a file should have a file name..."),
|
||||
),
|
||||
)?;
|
||||
debug!("Copying {entry:?} to {target_file_path:?}");
|
||||
copy(&entry, &target_file_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies a file.
|
||||
fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> Result<()> {
|
||||
let from = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
return copy_inner(from, to)
|
||||
.with_context(|| format!("failed to copy `{}` to `{}`", from.display(), to.display()));
|
||||
|
||||
// This is a workaround for an issue with the macOS file watcher.
|
||||
// Rust's `std::fs::copy` function uses `fclonefileat`, which creates
|
||||
// clones on APFS. Unfortunately fs events seem to trigger on both
|
||||
// sides of the clone, and there doesn't seem to be a way to differentiate
|
||||
// which side it is.
|
||||
// https://github.com/notify-rs/notify/issues/465#issuecomment-1657261035
|
||||
// contains more information.
|
||||
//
|
||||
// This is essentially a copy of the simple copy code path in Rust's
|
||||
// standard library.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn copy_inner(from: &Path, to: &Path) -> Result<()> {
|
||||
use std::fs::OpenOptions;
|
||||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||
|
||||
let mut reader = File::open(from)?;
|
||||
let metadata = reader.metadata()?;
|
||||
if !metadata.is_file() {
|
||||
anyhow::bail!(
|
||||
"expected a file, `{}` appears to be {:?}",
|
||||
from.display(),
|
||||
metadata.file_type()
|
||||
);
|
||||
}
|
||||
let perm = metadata.permissions();
|
||||
let mut writer = OpenOptions::new()
|
||||
.mode(perm.mode())
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(to)?;
|
||||
let writer_metadata = writer.metadata()?;
|
||||
if writer_metadata.is_file() {
|
||||
// Set the correct file permissions, in case the file already existed.
|
||||
// Don't set the permissions on already existing non-files like
|
||||
// pipes/FIFOs or device nodes.
|
||||
writer.set_permissions(perm)?;
|
||||
}
|
||||
std::io::copy(&mut reader, &mut writer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn copy_inner(from: &Path, to: &Path) -> Result<()> {
|
||||
fs::copy(from, to)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_404_output_file(input_404: &Option<String>) -> String {
|
||||
input_404
|
||||
.as_ref()
|
||||
|
@ -211,39 +232,36 @@ mod tests {
|
|||
};
|
||||
|
||||
// Create a couple of files
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("file.txt")) {
|
||||
if let Err(err) = fs::File::create(tmp.path().join("file.txt")) {
|
||||
panic!("Could not create file.txt: {}", err);
|
||||
}
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("file.md")) {
|
||||
if let Err(err) = fs::File::create(tmp.path().join("file.md")) {
|
||||
panic!("Could not create file.md: {}", err);
|
||||
}
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("file.png")) {
|
||||
if let Err(err) = fs::File::create(tmp.path().join("file.png")) {
|
||||
panic!("Could not create file.png: {}", err);
|
||||
}
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir")) {
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("sub_dir")) {
|
||||
panic!("Could not create sub_dir: {}", err);
|
||||
}
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("sub_dir/file.png")) {
|
||||
if let Err(err) = fs::File::create(tmp.path().join("sub_dir/file.png")) {
|
||||
panic!("Could not create sub_dir/file.png: {}", err);
|
||||
}
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("sub_dir_exists")) {
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("sub_dir_exists")) {
|
||||
panic!("Could not create sub_dir_exists: {}", err);
|
||||
}
|
||||
if let Err(err) = fs::File::create(&tmp.path().join("sub_dir_exists/file.txt")) {
|
||||
if let Err(err) = fs::File::create(tmp.path().join("sub_dir_exists/file.txt")) {
|
||||
panic!("Could not create sub_dir_exists/file.txt: {}", err);
|
||||
}
|
||||
if let Err(err) = symlink(
|
||||
&tmp.path().join("file.png"),
|
||||
&tmp.path().join("symlink.png"),
|
||||
) {
|
||||
if let Err(err) = symlink(tmp.path().join("file.png"), tmp.path().join("symlink.png")) {
|
||||
panic!("Could not symlink file.png: {}", err);
|
||||
}
|
||||
|
||||
// Create output dir
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("output")) {
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("output")) {
|
||||
panic!("Could not create output: {}", err);
|
||||
}
|
||||
if let Err(err) = fs::create_dir(&tmp.path().join("output/sub_dir_exists")) {
|
||||
if let Err(err) = fs::create_dir(tmp.path().join("output/sub_dir_exists")) {
|
||||
panic!("Could not create output/sub_dir_exists: {}", err);
|
||||
}
|
||||
|
||||
|
@ -254,22 +272,22 @@ mod tests {
|
|||
}
|
||||
|
||||
// Check if the correct files where created
|
||||
if !(&tmp.path().join("output/file.txt")).exists() {
|
||||
if !tmp.path().join("output/file.txt").exists() {
|
||||
panic!("output/file.txt should exist")
|
||||
}
|
||||
if (&tmp.path().join("output/file.md")).exists() {
|
||||
if tmp.path().join("output/file.md").exists() {
|
||||
panic!("output/file.md should not exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/file.png")).exists() {
|
||||
if !tmp.path().join("output/file.png").exists() {
|
||||
panic!("output/file.png should exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/sub_dir/file.png")).exists() {
|
||||
if !tmp.path().join("output/sub_dir/file.png").exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/sub_dir_exists/file.txt")).exists() {
|
||||
if !tmp.path().join("output/sub_dir_exists/file.txt").exists() {
|
||||
panic!("output/sub_dir/file.png should exist")
|
||||
}
|
||||
if !(&tmp.path().join("output/symlink.png")).exists() {
|
||||
if !tmp.path().join("output/symlink.png").exists() {
|
||||
panic!("output/symlink.png should exist")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ pub(crate) mod toml_ext;
|
|||
use crate::errors::Error;
|
||||
use log::error;
|
||||
use once_cell::sync::Lazy;
|
||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
|
||||
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag, TagEnd};
|
||||
use regex::Regex;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
@ -161,37 +161,59 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
|
|||
}
|
||||
|
||||
match event {
|
||||
Event::Start(Tag::Link(link_type, dest, title)) => {
|
||||
Event::Start(Tag::Link(link_type, fix(dest, path), title))
|
||||
}
|
||||
Event::Start(Tag::Image(link_type, dest, title)) => {
|
||||
Event::Start(Tag::Image(link_type, fix(dest, path), title))
|
||||
}
|
||||
Event::Start(Tag::Link {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
id,
|
||||
}) => Event::Start(Tag::Link {
|
||||
link_type,
|
||||
dest_url: fix(dest_url, path),
|
||||
title,
|
||||
id,
|
||||
}),
|
||||
Event::Start(Tag::Image {
|
||||
link_type,
|
||||
dest_url,
|
||||
title,
|
||||
id,
|
||||
}) => Event::Start(Tag::Image {
|
||||
link_type,
|
||||
dest_url: fix(dest_url, path),
|
||||
title,
|
||||
id,
|
||||
}),
|
||||
Event::Html(html) => Event::Html(fix_html(html, path)),
|
||||
Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)),
|
||||
_ => event,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around the pulldown-cmark parser for rendering markdown to HTML.
|
||||
pub fn render_markdown(text: &str, curly_quotes: bool) -> String {
|
||||
render_markdown_with_path(text, curly_quotes, None)
|
||||
pub fn render_markdown(text: &str, smart_punctuation: bool) -> String {
|
||||
render_markdown_with_path(text, smart_punctuation, None)
|
||||
}
|
||||
|
||||
pub fn new_cmark_parser(text: &str, curly_quotes: bool) -> Parser<'_, '_> {
|
||||
pub fn new_cmark_parser(text: &str, smart_punctuation: bool) -> Parser<'_> {
|
||||
let mut opts = Options::empty();
|
||||
opts.insert(Options::ENABLE_TABLES);
|
||||
opts.insert(Options::ENABLE_FOOTNOTES);
|
||||
opts.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
opts.insert(Options::ENABLE_TASKLISTS);
|
||||
if curly_quotes {
|
||||
opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
|
||||
if smart_punctuation {
|
||||
opts.insert(Options::ENABLE_SMART_PUNCTUATION);
|
||||
}
|
||||
Parser::new_ext(text, opts)
|
||||
}
|
||||
|
||||
pub fn render_markdown_with_path(text: &str, curly_quotes: bool, path: Option<&Path>) -> String {
|
||||
pub fn render_markdown_with_path(
|
||||
text: &str,
|
||||
smart_punctuation: bool,
|
||||
path: Option<&Path>,
|
||||
) -> String {
|
||||
let mut s = String::with_capacity(text.len() * 3 / 2);
|
||||
let p = new_cmark_parser(text, curly_quotes);
|
||||
let p = new_cmark_parser(text, smart_punctuation);
|
||||
let events = p
|
||||
.map(clean_codeblock_headers)
|
||||
.map(|event| adjust_links(event, path))
|
||||
|
@ -211,7 +233,7 @@ fn wrap_tables(event: Event<'_>) -> (Option<Event<'_>>, Option<Event<'_>>) {
|
|||
Some(Event::Html(r#"<div class="table-wrapper">"#.into())),
|
||||
Some(event),
|
||||
),
|
||||
Event::End(Tag::Table(_)) => (Some(event), Some(Event::Html(r#"</div>"#.into()))),
|
||||
Event::End(TagEnd::Table) => (Some(event), Some(Event::Html(r#"</div>"#.into()))),
|
||||
_ => (Some(event), None),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
- [Tables](individual/table.md)
|
||||
- [Tasks](individual/task.md)
|
||||
- [Strikethrough](individual/strikethrough.md)
|
||||
- [MathJax](individual/mathjax.md)
|
||||
- [Mixed](individual/mixed.md)
|
||||
- [Languages](languages/README.md)
|
||||
- [Syntax Highlight](languages/highlight.md)
|
||||
|
|
|
@ -10,7 +10,9 @@ This is a codeblock
|
|||
|
||||
---
|
||||
|
||||
This line contains `inline code`
|
||||
This line contains `inline code` mixed with some other stuff. (LTR)
|
||||
|
||||
ושורה זו מכילה `inline code` אבל עם טקסט בשפה שנכתבת מימין לשמאל. (RTL)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -13,3 +13,9 @@
|
|||
##### Really Small Heading
|
||||
|
||||
###### Is it even a heading anymore - heading
|
||||
|
||||
## Custom id {#example-id}
|
||||
|
||||
## Custom class {.class1 .class2}
|
||||
|
||||
## Both id and class {#example-id2 .class1 .class2}
|
||||
|
|
|
@ -4,19 +4,19 @@ For copyright and trademark information on these images, please check [rust-artw
|
|||
|
||||
## A 16x16 image
|
||||
|
||||
![16x16 rust-lang logo](http://rust-lang.org/logos/rust-logo-16x16.png)
|
||||
![16x16 rust-lang logo](https://rust-lang.org/logos/rust-logo-16x16.png)
|
||||
|
||||
## A 32x32 image
|
||||
|
||||
![32x32 rust-lang logo](http://rust-lang.org/logos/rust-logo-32x32-blk.png)
|
||||
![32x32 rust-lang logo](https://rust-lang.org/logos/rust-logo-32x32-blk.png)
|
||||
|
||||
## A 256x256 image
|
||||
|
||||
![256x256 rust-lang logo](http://rust-lang.org/logos/rust-logo-256x256.png)
|
||||
![256x256 rust-lang logo](https://rust-lang.org/logos/rust-logo-256x256.png)
|
||||
|
||||
## A 512x512 image
|
||||
|
||||
![512x512 rust-lang logo](http://rust-lang.org/logos/rust-logo-512x512-blk.png)
|
||||
![512x512 rust-lang logo](https://rust-lang.org/logos/rust-logo-512x512-blk.png)
|
||||
|
||||
## A large image
|
||||
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# MathJax
|
||||
|
||||
Fourier Transform
|
||||
|
||||
\\[
|
||||
\begin{aligned}
|
||||
f(x) &= \int_{-\infty}^{\infty}F(s)(-1)^{ 2xs}ds \\\\
|
||||
F(s) &= \int_{-\infty}^{\infty}f(x)(-1)^{-2xs}dx
|
||||
\end{aligned}
|
||||
\\]
|
||||
|
||||
The kernel can also be written as \\(e^{2i\pi xs}\\) which is more frequently used in literature.
|
||||
|
||||
> Proof that \\(e^{ix} = \cos x + i\sin x\\) a.k.a Euler's Formula:
|
||||
>
|
||||
> \\(
|
||||
\begin{aligned}
|
||||
e^x &= \sum_{n=0}^\infty \frac{x^n}{n!} \implies e^{ix} = \sum_{n=0}^\infty \frac{(ix)^n}{n!} \\\\
|
||||
\cos x &= \sum_{m=0}^\infty \frac{(-1)^m x^{2m}}{(2m)!} = \sum_{m=0}^\infty \frac{(ix)^{2m}}{(2m)!} \\\\
|
||||
\sin x &= \sum_{s=0}^\infty \frac{(-1)^s x^{2s+1}}{(2s+1)!} = \sum_{s=0}^\infty \frac{(ix)^{2s+1}}{i(2s+1)!} \\\\
|
||||
\cos x + i\sin x &= \sum_{l=0}^\infty \frac{(ix)^{2l}}{(2l)!} + \sum_{s=0}^\infty \frac{(ix)^{2s+1}}{(2s+1)!} = \sum_{n=0}^\infty \frac{(ix)^{n}}{n!} \\\\
|
||||
&= e^{ix}
|
||||
\end{aligned}
|
||||
\\)
|
||||
>
|
||||
|
||||
|
||||
Pauli Matrices
|
||||
|
||||
\\[
|
||||
\begin{aligned}
|
||||
\sigma_x &= \begin{pmatrix}
|
||||
1 & 0 \\\\ 0 & 1
|
||||
\end{pmatrix} \\\\
|
||||
\sigma_y &= \begin{pmatrix}
|
||||
0 & -i \\\\ i & 0
|
||||
\end{pmatrix} \\\\
|
||||
\sigma_z &= \begin{pmatrix}
|
||||
1 & 0 \\\\ 0 & -1
|
||||
\end{pmatrix}
|
||||
\end{aligned}
|
||||
\\]
|
|
@ -31,7 +31,7 @@ fn main(){
|
|||
|
||||
A random image sprinkled in between
|
||||
|
||||
![16x16 rust-lang logo](http://rust-lang.org/logos/rust-logo-16x16.png)
|
||||
![16x16 rust-lang logo](https://rust-lang.org/logos/rust-logo-16x16.png)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# Strikethrough
|
||||
|
||||
~Single strike~
|
||||
|
||||
~~This is Striked~~
|
||||
|
||||
~~This is **strong**, _italic_ , **_both_** and striked~~
|
||||
|
|
|
@ -27,6 +27,8 @@ This Currently contains following languages
|
|||
- makefile
|
||||
- markdown
|
||||
- nginx
|
||||
- nim
|
||||
- nix
|
||||
- objectivec
|
||||
- perl
|
||||
- php
|
||||
|
|
|
@ -57,7 +57,7 @@ _start:
|
|||
|
||||
## bash
|
||||
|
||||
```
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
###### CONFIG
|
||||
|
@ -529,6 +529,26 @@ http {
|
|||
}
|
||||
```
|
||||
|
||||
## nim
|
||||
|
||||
```nim
|
||||
from strutils import `%`
|
||||
|
||||
const numDoors = 100
|
||||
var doors {.compileTime.}: array[1..numDoors, bool]
|
||||
|
||||
proc calcDoors(): string =
|
||||
for pass in 1..numDoors:
|
||||
for door in countup(pass, numDoors, pass):
|
||||
doors[door] = not doors[door]
|
||||
for door in 1..numDoors:
|
||||
result.add("Door $1 is $2.\n" % [$door, if doors[door]: "open" else: "closed"])
|
||||
|
||||
const outputString: string = calcDoors()
|
||||
|
||||
echo outputString
|
||||
```
|
||||
|
||||
## objectivec
|
||||
|
||||
```objectivec
|
||||
|
@ -543,6 +563,15 @@ int main(int argc, const char * argv[]) {
|
|||
|
||||
```
|
||||
|
||||
## nix
|
||||
|
||||
```nix
|
||||
let
|
||||
world = "World!";
|
||||
in
|
||||
"Hello " + world
|
||||
```
|
||||
|
||||
## perl
|
||||
|
||||
```perl
|
||||
|
|
|
@ -39,29 +39,27 @@ fn alternate_backend_with_arguments() {
|
|||
md.build().unwrap();
|
||||
}
|
||||
|
||||
/// Get a command which will pipe `stdin` to the provided file.
|
||||
#[cfg(not(windows))]
|
||||
fn tee_command<P: AsRef<Path>>(out_file: P) -> String {
|
||||
let out_file = out_file.as_ref();
|
||||
|
||||
if cfg!(windows) {
|
||||
format!("cmd.exe /c \"type > {}\"", out_file.display())
|
||||
} else {
|
||||
format!("tee {}", out_file.display())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(windows))]
|
||||
fn backends_receive_render_context_via_stdin() {
|
||||
use mdbook::renderer::RenderContext;
|
||||
use std::fs::File;
|
||||
|
||||
let temp = TempFileBuilder::new().prefix("output").tempdir().unwrap();
|
||||
let out_file = temp.path().join("out.txt");
|
||||
let cmd = tee_command(&out_file);
|
||||
let (md, temp) = dummy_book_with_backend("cat-to-file", "renderers/myrenderer", false);
|
||||
|
||||
let (md, _temp) = dummy_book_with_backend("cat-to-file", &cmd, false);
|
||||
let renderers = temp.path().join("renderers");
|
||||
fs::create_dir(&renderers).unwrap();
|
||||
rust_exe(
|
||||
&renderers,
|
||||
"myrenderer",
|
||||
r#"fn main() {
|
||||
use std::io::Read;
|
||||
let mut s = String::new();
|
||||
std::io::stdin().read_to_string(&mut s).unwrap();
|
||||
std::fs::write("out.txt", s).unwrap();
|
||||
}"#,
|
||||
);
|
||||
|
||||
let out_file = temp.path().join("book/out.txt");
|
||||
|
||||
assert!(!out_file.exists());
|
||||
md.build().unwrap();
|
||||
|
@ -90,7 +88,7 @@ fn relative_command_path() {
|
|||
.set("output.html", toml::value::Table::new())
|
||||
.unwrap();
|
||||
config.set("output.myrenderer.command", cmd_path).unwrap();
|
||||
let md = MDBook::init(&temp.path())
|
||||
let md = MDBook::init(temp.path())
|
||||
.with_config(config)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
use crate::cli::cmd::mdbook_cmd;
|
||||
use crate::dummy_book::DummyBook;
|
||||
|
||||
use mdbook::config::Config;
|
||||
|
||||
/// Run `mdbook init` with `--force` to skip the confirmation prompts
|
||||
#[test]
|
||||
fn base_mdbook_init_can_skip_confirmation_prompts() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
|
||||
// doesn't exist before
|
||||
assert!(!temp.path().join("book").exists());
|
||||
|
||||
let mut cmd = mdbook_cmd();
|
||||
cmd.args(["init", "--force"]).current_dir(temp.path());
|
||||
cmd.assert()
|
||||
.success()
|
||||
.stdout(predicates::str::contains("\nAll done, no errors...\n"));
|
||||
|
||||
let config = Config::from_disk(temp.path().join("book.toml")).unwrap();
|
||||
assert_eq!(config.book.title, None);
|
||||
|
||||
assert!(!temp.path().join(".gitignore").exists());
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
mod build;
|
||||
mod cmd;
|
||||
mod init;
|
||||
mod test;
|
||||
|
|
|
@ -112,12 +112,12 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
|
|||
let from = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
|
||||
for entry in WalkDir::new(&from) {
|
||||
for entry in WalkDir::new(from) {
|
||||
let entry = entry.with_context(|| "Unable to inspect directory entry")?;
|
||||
|
||||
let original_location = entry.path();
|
||||
let relative = original_location
|
||||
.strip_prefix(&from)
|
||||
.strip_prefix(from)
|
||||
.expect("`original_location` is inside the `from` directory");
|
||||
let new_location = to.join(relative);
|
||||
|
||||
|
@ -126,7 +126,7 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
|
|||
fs::create_dir_all(parent).with_context(|| "Couldn't create directory")?;
|
||||
}
|
||||
|
||||
fs::copy(&original_location, &new_location)
|
||||
fs::copy(original_location, &new_location)
|
||||
.with_context(|| "Unable to copy file contents")?;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
- [Unicode](first/unicode.md)
|
||||
- [No Headers](first/no-headers.md)
|
||||
- [Duplicate Headers](first/duplicate-headers.md)
|
||||
- [Heading Attributes](first/heading-attributes.md)
|
||||
- [Second Chapter](second.md)
|
||||
- [Nested Chapter](second/nested.md)
|
||||
|
||||
|
|
|
@ -18,3 +18,7 @@ css looks, like this {
|
|||
}
|
||||
*/
|
||||
</style>
|
||||
|
||||
Sneaky inline event <script>alert("inline");</script>.
|
||||
|
||||
But regular <b>inline</b> is indexed.
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
# Heading Attributes {#attrs}
|
||||
|
||||
## Heading with classes {.class1 .class2}
|
||||
|
||||
## Heading with id and classes {#both .class1 .class2}
|
|
@ -35,6 +35,7 @@ const TOC_SECOND_LEVEL: &[&str] = &[
|
|||
"1.5. Unicode",
|
||||
"1.6. No Headers",
|
||||
"1.7. Duplicate Headers",
|
||||
"1.8. Heading Attributes",
|
||||
"2.1. Nested Chapter",
|
||||
];
|
||||
|
||||
|
@ -275,7 +276,7 @@ fn root_index_html() -> Result<Document> {
|
|||
.with_context(|| "Book building failed")?;
|
||||
|
||||
let index_page = temp.path().join("book").join("index.html");
|
||||
let html = fs::read_to_string(&index_page).with_context(|| "Unable to read index.html")?;
|
||||
let html = fs::read_to_string(index_page).with_context(|| "Unable to read index.html")?;
|
||||
|
||||
Ok(Document::from(html.as_str()))
|
||||
}
|
||||
|
@ -374,10 +375,7 @@ fn able_to_include_playground_files_in_chapters() {
|
|||
|
||||
let second = temp.path().join("book/second.html");
|
||||
|
||||
let playground_strings = &[
|
||||
r#"class="playground""#,
|
||||
r#"println!("Hello World!");"#,
|
||||
];
|
||||
let playground_strings = &[r#"class="playground""#, r#"println!("Hello World!");"#];
|
||||
|
||||
assert_contains_strings(&second, playground_strings);
|
||||
assert_doesnt_contain_strings(&second, &["{{#playground example.rs}}"]);
|
||||
|
@ -412,7 +410,7 @@ fn recursive_includes_are_capped() {
|
|||
let content = &["Around the world, around the world
|
||||
Around the world, around the world
|
||||
Around the world, around the world"];
|
||||
assert_contains_strings(&recursive, content);
|
||||
assert_contains_strings(recursive, content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -462,7 +460,7 @@ fn by_default_mdbook_use_index_preprocessor_to_convert_readme_to_index() {
|
|||
|
||||
let second_index = temp.path().join("book").join("second").join("index.html");
|
||||
let unexpected_strings = vec!["Second README"];
|
||||
assert_doesnt_contain_strings(&second_index, &unexpected_strings);
|
||||
assert_doesnt_contain_strings(second_index, &unexpected_strings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -628,10 +626,8 @@ fn edit_url_has_configured_src_dir_edit_url() {
|
|||
}
|
||||
|
||||
fn remove_absolute_components(path: &Path) -> impl Iterator<Item = Component> + '_ {
|
||||
path.components().skip_while(|c| match c {
|
||||
Component::Prefix(_) | Component::RootDir => true,
|
||||
_ => false,
|
||||
})
|
||||
path.components()
|
||||
.skip_while(|c| matches!(c, Component::Prefix(_) | Component::RootDir))
|
||||
}
|
||||
|
||||
/// Checks formatting of summary names with inline elements.
|
||||
|
@ -746,6 +742,7 @@ mod search {
|
|||
let index = read_book_index(temp.path());
|
||||
|
||||
let doc_urls = index["doc_urls"].as_array().unwrap();
|
||||
eprintln!("doc_urls={doc_urls:#?}",);
|
||||
let get_doc_ref =
|
||||
|url: &str| -> String { doc_urls.iter().position(|s| s == url).unwrap().to_string() };
|
||||
|
||||
|
@ -756,6 +753,7 @@ mod search {
|
|||
let no_headers = get_doc_ref("first/no-headers.html");
|
||||
let duplicate_headers_1 = get_doc_ref("first/duplicate-headers.html#header-text-1");
|
||||
let conclusion = get_doc_ref("conclusion.html#conclusion");
|
||||
let heading_attrs = get_doc_ref("first/heading-attributes.html#both");
|
||||
|
||||
let bodyidx = &index["index"]["index"]["body"]["root"];
|
||||
let textidx = &bodyidx["t"]["e"]["x"]["t"];
|
||||
|
@ -768,13 +766,16 @@ mod search {
|
|||
assert_eq!(docs[&some_section]["body"], "");
|
||||
assert_eq!(
|
||||
docs[&summary]["body"],
|
||||
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Duplicate Headers Second Chapter Nested Chapter Conclusion"
|
||||
"Dummy Book Introduction First Chapter Nested Chapter Includes Recursive Markdown Unicode No Headers Duplicate Headers Heading Attributes Second Chapter Nested Chapter Conclusion"
|
||||
);
|
||||
assert_eq!(
|
||||
docs[&summary]["breadcrumbs"],
|
||||
"First Chapter » Includes » Summary"
|
||||
);
|
||||
assert_eq!(docs[&conclusion]["body"], "I put <HTML> in here!");
|
||||
// See note about InlineHtml in search.rs. Ideally the `alert()` part
|
||||
// should not be in the index, but we don't have a way to scrub inline
|
||||
// html.
|
||||
assert_eq!(docs[&conclusion]["body"], "I put <HTML> in here! Sneaky inline event alert(\"inline\");. But regular inline is indexed.");
|
||||
assert_eq!(
|
||||
docs[&no_headers]["breadcrumbs"],
|
||||
"First Chapter » No Headers"
|
||||
|
@ -787,6 +788,10 @@ mod search {
|
|||
docs[&no_headers]["body"],
|
||||
"Capybara capybara capybara. Capybara capybara capybara. ThisLongWordIsIncludedSoWeCanCheckThatSufficientlyLongWordsAreOmittedFromTheSearchIndex."
|
||||
);
|
||||
assert_eq!(
|
||||
docs[&heading_attrs]["breadcrumbs"],
|
||||
"First Chapter » Heading Attributes » Heading with id and classes"
|
||||
);
|
||||
}
|
||||
|
||||
// Setting this to `true` may cause issues with `cargo watch`,
|
||||
|
@ -803,7 +808,7 @@ mod search {
|
|||
let src = read_book_index(temp.path());
|
||||
|
||||
let dest = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/searchindex_fixture.json");
|
||||
let dest = File::create(&dest).unwrap();
|
||||
let dest = File::create(dest).unwrap();
|
||||
serde_json::to_writer_pretty(dest, &src).unwrap();
|
||||
|
||||
src
|
||||
|
@ -891,8 +896,8 @@ fn custom_fonts() {
|
|||
assert_eq!(actual_files(&p.join("book/fonts")), &builtin_fonts);
|
||||
assert!(has_fonts_css(p));
|
||||
|
||||
// Mixed with copy_fonts=true
|
||||
// This should generate a deprecation warning.
|
||||
// Mixed with copy-fonts=true
|
||||
// Should ignore the copy-fonts setting since the user has provided their own fonts.css.
|
||||
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
|
||||
let p = temp.path();
|
||||
MDBook::init(p).build().unwrap();
|
||||
|
@ -900,10 +905,10 @@ fn custom_fonts() {
|
|||
write_file(&p.join("theme/fonts"), "myfont.woff", b"").unwrap();
|
||||
MDBook::load(p).unwrap().build().unwrap();
|
||||
assert!(has_fonts_css(p));
|
||||
let mut expected = Vec::from(builtin_fonts);
|
||||
expected.push("myfont.woff");
|
||||
expected.sort();
|
||||
assert_eq!(actual_files(&p.join("book/fonts")), expected.as_slice());
|
||||
assert_eq!(
|
||||
actual_files(&p.join("book/fonts")),
|
||||
["fonts.css", "myfont.woff"]
|
||||
);
|
||||
|
||||
// copy-fonts=false, no theme
|
||||
// This should generate a deprecation warning.
|
||||
|
@ -948,3 +953,19 @@ fn custom_fonts() {
|
|||
&["fonts.css", "myfont.woff"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_header_attributes() {
|
||||
let temp = DummyBook::new().build().unwrap();
|
||||
let md = MDBook::load(temp.path()).unwrap();
|
||||
md.build().unwrap();
|
||||
|
||||
let contents = temp.path().join("book/first/heading-attributes.html");
|
||||
|
||||
let summary_strings = &[
|
||||
r##"<h1 id="attrs"><a class="header" href="#attrs">Heading Attributes</a></h1>"##,
|
||||
r##"<h2 id="heading-with-classes" class="class1 class2"><a class="header" href="#heading-with-classes">Heading with classes</a></h2>"##,
|
||||
r##"<h2 id="both" class="class1 class2"><a class="header" href="#both">Heading with id and classes</a></h2>"##,
|
||||
];
|
||||
assert_contains_strings(&contents, summary_strings);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue