Compare commits

..

152 Commits

Author SHA1 Message Date
Eric Huss
138dc696b7 Merge pull request #1264 from ehuss/update-changelog
Update changelog.
2020-06-23 11:45:04 -07:00
Eric Huss
91b2fb86bf Update changelog. 2020-06-23 11:44:05 -07:00
Eric Huss
d4df7e7cdd Merge pull request #1230 from ehuss/favicon-svg
Add SVG favicon.
2020-06-23 11:34:03 -07:00
Eric Huss
4699269e49 Add SVG favicon. 2020-06-23 10:58:10 -07:00
Eric Huss
c1ed6ee108 Merge pull request #1228 from ehuss/fix-dest-dir
Fix dest-dir command-line flag.
2020-06-23 10:55:38 -07:00
Eric Huss
f59cfe7e2f Fix dest-dir command-line flag. 2020-06-23 10:55:01 -07:00
Eric Huss
9268884b17 Merge pull request #1221 from manuel-woelker/fb-539-not-found-page
Generate 404.html page (#539)
2020-06-23 10:48:08 -07:00
Eric Huss
4f435c62e6 Merge pull request #1249 from ThePuzzlemaker/thepuzzlemaker-stylepatch
Fix issue where Font Awesome overrides `.hidden`'s `display: none`
2020-06-22 12:28:09 -07:00
Eric Huss
9a97f0a096 Fix init creating empty [rust] table. (#1233) 2020-06-22 16:36:37 +02:00
Eric Huss
bc23d08fa5 Rename playpen to playground. (#1241)
looks good
2020-06-22 16:34:25 +02:00
ThePuzzlemaker
84d848f292 Fix issue where Font Awesome overrides .hidden's display: none 2020-06-10 12:55:22 -05:00
Manuel Woelker
d7df832cce fix test and formatting 2020-06-10 15:33:09 +02:00
Manuel Woelker
406b325c54 fix usage of newly stablized inner_deref/as_deref 2020-06-10 13:09:18 +02:00
Manuel Woelker
6d6e5407a3 document: config 404 config parameters output.html.site-url and output.html.site-url 2020-06-10 12:46:23 +02:00
Manuel Woelker
06efa7a675 additional changes to the 404 mechanism based on feedback:
- removed config output_404
 - ensure serve overrides the site url, and hosts the correct 404 file
 - refactor 404 rendering into separate fn
 - formatting
2020-06-10 12:46:23 +02:00
Manuel Woelker
bff36e7229 Add the config parameter output.html.site-url to set base url of the 404 page, making links and relative script/css loads behave correctly even in subdirectory paths 2020-06-10 12:46:23 +02:00
Manuel Woelker
cda28bb618 Generate 404.html page (#539) 2020-06-10 12:46:23 +02:00
Eric Huss
fe1ba71d45 Merge pull request #1199 from ehuss/preferred-dark-theme
Change default preferred-dark-theme to `navy`.
2020-06-08 12:38:06 -07:00
Eric Huss
23f5ffd6d6 Change default preferred-dark-theme to navy. 2020-06-08 11:40:38 -07:00
Eric Huss
484e5c0b8f Merge pull request #1244 from brycefisher/dont-open-search-bar-when-in-text-input
fix: don't jump away from form input to search bar
2020-06-07 13:19:03 -07:00
Eric Huss
a80febd318 Merge pull request #1245 from nihaals/improve-ga-doc
Improved Google Analytics example
2020-06-07 13:14:12 -07:00
Nihaal Sangha
16010ee28b Improved Google Analytics example 2020-06-06 18:05:32 +01:00
Bryce Fisher-Fleig
fb1476d1e3 fix: don't jump away from form input to search bar
Addresses #1020 with the suggestion from prior PR #1101
2020-06-04 14:35:49 -07:00
Eric Huss
b375f4e3d5 Merge pull request #1237 from Michael-F-Bryan/redirects
Implement URL redirecting
2020-05-30 10:30:09 -07:00
Eric Huss
25ec7ace1a Fix typo for redirect TOML. 2020-05-30 10:27:38 -07:00
Michael-F-Bryan
ebc01dbb71 Updated the test to follow our docs 2020-05-30 04:15:24 +08:00
Michael-F-Bryan
7b3e945a27 Updated the test to reflect how redirect keys should be absolute 2020-05-30 04:11:11 +08:00
Michael-F-Bryan
964a10ff29 Redid the wording in the docs because it was around the wrong way 2020-05-30 04:09:43 +08:00
Michael-F-Bryan
5907caa732 I should have used a more realistic example 2020-05-27 03:19:24 +08:00
Michael-F-Bryan
da55cf273f Changed redirect mapping to HashMap<String, String> and improved error handling 2020-05-27 03:12:57 +08:00
Michael-F-Bryan
a6ab4d8402 Explained how you can configure redirects 2020-05-27 02:52:59 +08:00
Michael-F-Bryan
4c2318922f Added integration tests to make sure files are redirected as intended 2020-05-27 02:43:01 +08:00
Michael-F-Bryan
b2d50392ea Emit redirects towards the end of the rendering process 2020-05-27 02:42:56 +08:00
Michael-F-Bryan
a5086a1e58 Added a redirect map to the HTML config 2020-05-27 02:38:04 +08:00
Eric Huss
6c4c3448e3 Update dependencies. (#1211)
* Removed the itertools dependency

* Removed an unused feature flag

* Stubbed out a toml_query replacement

* Update dependencies.

* Bump env_logger.

* Use warp instead of iron for http server.

Iron does not appear to be maintained anymore. warp/hyper seems to be
reasonably maintained. Unfortunately this takes a few seconds more
to compile, but shouldn't be too bad.

One benefit is that there is no longer a need for a separate websocket
port, which makes it easier to run multiple servers at once.

* Update pulldown-cmark to 0.7

* Switch from error-chain to anyhow.

* Bump MSRV to 1.39.

* Update elasticlunr-rs.

Co-authored-by: Michael Bryan <michaelfbryan@gmail.com>
2020-05-20 23:32:00 +02:00
Eric Huss
5d5c55e619 Merge pull request #1171 from mark-i-m/master
implement support for book parts
2020-05-20 12:17:45 -07:00
Eric Huss
e2023fd72d Tweak wording of documentation for part titles. 2020-05-20 12:17:17 -07:00
Eric Huss
e677b72eb8 Merge pull request #1188 from pheki/remove-google-surveillance
Remove google fonts by serving them locally
2020-05-20 11:01:07 -07:00
Aphek
7e090ca42f Also copy font licenses when copy-fonts is enabled 2020-05-19 03:17:01 -03:00
Aphek
122c988477 Rename config from no-copy-fonts to copy-fonts 2020-05-19 03:09:25 -03:00
mark
d0fe9bd41c make part titles another SummaryItem 2020-05-18 11:18:14 -05:00
mark
b1ccb30220 update docs 2020-05-17 17:00:03 -05:00
mark
91e3aa4b55 try to satisfy msrv? 2020-05-17 16:53:06 -05:00
mark
2d63286c63 make part titles bold 2020-05-17 16:53:06 -05:00
mark
5dd2a5bff4 implement support for book parts 2020-05-17 16:53:06 -05:00
Eric Huss
1b3b10d2ae Do not allow the sidebar to be dragged outside the window. (#1229) 2020-05-17 22:05:12 +02:00
Aphek
2c26c65f4d Remove google fonts by serving them locally
Co-authored-by: Aral Balkan <aral@ind.ie>
Co-authored-by: Collyn O'Kane <47607823+okaneco@users.noreply.github.com>
2020-05-15 02:48:28 -03:00
Eric Huss
e8d4bc52e1 Merge pull request #1215 from ehuss/block-cleanup
Remove some unnecessary blocks.
2020-05-10 09:35:46 -07:00
Eric Huss
6038af292f Remove some unnecessary blocks. 2020-05-10 09:23:40 -07:00
Eric Huss
578e4da5b6 Merge pull request #1153 from azerupi/draft-chapters
Bring back draft chapters
2020-05-10 09:18:02 -07:00
Mathieu David
43008ef2ef fix issues from code review 2020-05-10 09:04:02 -07:00
Mathieu David
d605938886 Bring back draft chapters 2020-05-10 09:04:02 -07:00
Eric Huss
7e11d37e49 Merge pull request #1206 from GillesRasigade/html-head-custom
HTML head customisation with `head.hbs`
2020-05-10 08:57:23 -07:00
Eric Huss
50bcf67f2b Consistent punctuation. 2020-05-10 08:42:53 -07:00
Gilles Rasigade
c2d58158da Revert HTML auto-formatting 2020-05-10 08:42:53 -07:00
Gilles Rasigade
1731779a8d Add header.hbs template documentation 2020-05-10 08:42:53 -07:00
Gilles Rasigade
f7e349d37f Add head.hbs template documentation 2020-05-10 08:42:53 -07:00
Gilles Rasigade
61c8413138 Allow to define own HTML <head> attributes
Create a `index.hbs` template file to render additional HTML <head> attributes
2020-05-10 08:42:53 -07:00
Eric Huss
8ee950e3de Merge pull request #1214 from ehuss/fix-clippy
Fix some clippy warnings.
2020-05-10 08:39:19 -07:00
Eric Huss
c44ef1b2f0 Fix some clippy warnings. 2020-05-10 08:29:50 -07:00
Eric Huss
07dfc4b89a Merge pull request #1207 from toyboot4e/fix-mdbook_book
Enable `MDBOOK_BOOK`  to overwrite `book.toml`
2020-05-08 06:51:21 -07:00
toyboot4e
282e55122e Update src/config.rs
Co-authored-by: Eric Huss <eric@huss.org>
2020-05-08 19:56:41 +09:00
Eric Huss
17210b058f Merge pull request #1208 from uint/links-preprocessor-support-pluses
#1098 Links preprocessor: support pluses
2020-05-07 15:36:42 -07:00
Tomasz Kurcz
b1cf3f117d Links preprocessor: support pluses in file paths 2020-05-03 14:50:03 +02:00
Tomasz Kurcz
d665732056 Links preprocessor: test links with special characters 2020-05-03 14:42:22 +02:00
toyboot4e
2f59dbf1ef Fix example of MDBOOK_BOOK (again) 2020-05-03 18:58:41 +09:00
toyboot4e
3a63276727 Not to use matches! 2020-05-03 18:16:44 +09:00
toyboot4e
4c64f23089 Fix example of MDBOOK_BOOK 2020-05-03 17:54:35 +09:00
toyboot4e
683d2b2240 Fix use of MDBOOK_BOOK 2020-05-03 17:54:17 +09:00
Eric Huss
11f95f76e6 Merge pull request #1122 from segfaultsourcery/zero-exit-code-when-plugin-not-found-893
Fixed zero exit code when plugin not found
2020-04-21 15:42:52 -07:00
Eric Huss
2732c5e8f7 Update docs and tweak error messages. 2020-04-21 15:34:59 -07:00
Kim Hermansson
6b550cb4bb Make missing backends optional. 2020-04-21 15:13:14 -07:00
Eric Huss
712362f9e7 Merge pull request #1163 from kngwyu/kpp-edition2018
Make new [rust] section to config and  place edition under it
2020-04-21 13:00:15 -07:00
Eric Huss
28ce8f5ac0 Some edition cleanup and fixes. 2020-04-21 12:26:48 -07:00
kngwyu
255756cfee Make new [rust] config and move edition config under it 2020-04-21 10:24:47 -07:00
Gabriel Majeri
53d821bf6d Add edition to hbs renderer 2020-04-21 10:24:47 -07:00
Gabriel Majeri
d39d4517aa Add support for Rust edition 2020-04-21 10:24:47 -07:00
Eric Huss
bd0f434225 Merge pull request #1164 from zdenek-crha/doc_gitignore_use
Mention that .gitignore is used for watch excludes (#1160)
2020-04-21 10:21:34 -07:00
Eric Huss
3806d7b6ea Merge pull request #1197 from ehuss/release-0.4.0
Start 0.4.0 release.
2020-04-20 09:58:13 -07:00
Eric Huss
d1b484ff35 Start 0.4.0 release. 2020-04-20 09:44:18 -07:00
Eric Huss
04c04dfc88 Merge pull request #1195 from englishm/me/suppress-more-warnings-for-small-listings
Broaden scope of suppressed warnings for listings without a main fn
2020-04-18 08:33:30 -07:00
Mike English
1d265fd143 Broaden scope of suppressed warnings for listings without a main fn
At present, code listings without a main function will be wrapped in one and
annotated with an allow lint check attribute provided by the following [code][]:

```
format!(
    "\n# #![allow(unused_variables)]\n{}#fn main() {{\n{}#}}",
    attrs, code
)
```

A broader lint check attribute such as `#![allow(unused)]` seems like it might
better fit the apparent intent of this code.

Addresses: https://github.com/rust-lang/mdBook/issues/1192

[code]: 769cc0a7c1/src/renderer/html_handlebars/hbs_renderer.rs (L635-L638)
2020-04-18 01:36:11 -04:00
Eric Huss
8e673c96c2 Release 0.3.7 2020-04-03 12:55:41 -07:00
Eric Huss
99ecd4f87c Merge pull request #1170 from ehuss/fix-theme-focus
Fix theme selector focus.
2020-04-03 12:45:33 -07:00
Eric Huss
e839ef0866 Fix sidebar line-height. (#1182) 2020-04-03 21:09:23 +02:00
Eric Huss
769cc0a7c1 Merge pull request #1026 from andrewdavidmackenzie/master
Avoid a possible recursive copy when destination (build_dir) is underneath source directory
2020-04-03 11:18:58 -07:00
Andrew Mackenzie
c2686a817a Address PR review comments 2020-04-03 10:39:15 -07:00
Andrew Mackenzie
bd14d0910a Avoid a possible recursive copy when destination (build_dir) is underneath source directory. Update docs to match. 2020-04-03 10:39:15 -07:00
Eric Huss
6eb597a556 Revert "Assure copy_files_except_ext(..) won't copy directories into itself (#1135)" (#1181)
This reverts commit d7a2b29f06.
2020-04-03 19:12:43 +02:00
Zdenek Crha
5c91041dad Mention that .gitignore is used for watch excludes (#1160)
The serve and watch commands use .gitignore file in book root directory
to read exclude patterns used when watching for changed files. A
.gitignore from parent directory or user home is not used though.

Add documentation of this behaviour to both commands.
2020-03-28 19:33:20 +01:00
Dylan DPC
59568208ff Revert "Support anchors in SUMMARY (#1173)" (#1175)
This reverts commit 2baed040c2.
2020-03-24 23:52:24 +01:00
Eric Huss
21a16c9b75 Scroll sidebar to middle instead of top. (#1161)
* Fix: Scroll sidebar to current active section (#1067)

* Clean: Some code related to PR #1052

* Change `scrollIntoViewIfNeeded` with `scrollIntoView`

* Don't use onload event for sidebar scroll to reduce flickering.

Co-authored-by: 李鸿章 <poodll@163.com>
2020-03-24 21:08:53 +01:00
Eric Huss
4e8e1e1408 Don't highlight code spans in headers. (#1162) 2020-03-24 20:43:20 +01:00
Martin Carton
2baed040c2 Support anchors in SUMMARY (#1173)
* Parse anchors in `SUMMARY.md`

* Support sub-links in SUMMARY with html renderer

* Add some tests for anchors
2020-03-24 19:15:28 +01:00
Mathieu David
101063093b Change CI badge to only push events (#1174)
I noticed that the CI badge was failing and it seems to be caused by the fact that it reflects the latest build including PRs. This change filters the events to only "push events on the master branch" so the badge stays green even if a PR fails.
2020-03-23 13:22:12 +01:00
Eric Huss
f7ffffbd1e Fix theme selector focus. 2020-03-20 11:12:42 -07:00
Eric Huss
760c9b0767 Bump quote and failure.
quote 1.0.2 was yanked, update failure to stay compatible.
2020-03-19 12:48:54 -07:00
Eric Huss
6016e12b90 Release 0.3.6. 2020-03-19 12:43:22 -07:00
Eric Huss
88684d843d Merge pull request #1167 from dalance/fix_summary_comment
Fix SUMMARY's parse_numbered with comment
2020-03-17 21:39:25 -07:00
dalance
b82562fe8a Fix SUMMARY's parse_numbered with comment 2020-03-18 12:28:27 +09:00
Eric Huss
44c3213f5d Merge pull request #1130 from sunng87/feature/handlebars-3.0
Update handlebars to 3.0
2020-03-08 18:54:39 -07:00
Dylan DPC
fd56a53e76 ui: improve menu folding (#989)
* ui: improve menu folding

Fold/unfold the menu bar just by the amount of scroll, not by its
full width

* refactor: use a variable for the menu bar height

* Fix menu scroll jittering, remove hover folding smoothness

Rewrite it to use `position:` `sticky` and `relative` instead
of continuous programmatic position changes

On-hover folding-unfolding transition removal is a side-effect
2020-03-06 01:11:37 +01:00
Dylan DPC
ca4b85b815 Increases line height for p, ul & ol (#1136) 2020-03-06 01:06:19 +01:00
Sebastian Thiel
d7a2b29f06 Assure copy_files_except_ext(..) won't copy directories into itself (#1135)
This prevents recursive copy-loops when the destination directory
is contained in the source directory.

Now it bails out with a descriptive error message.
2020-02-29 19:20:55 +01:00
Eric Huss
4039c72fd3 Merge pull request #1150 from EvanCarroll/master
Support rel=next and rel=previous
2020-02-18 07:55:40 -08:00
Evan Carroll
2bd8bdf798 Support rel=next and rel=previous
This resolves GH #1149
2020-02-18 09:20:17 -06:00
Sergey Pedan
0da7ba4abe Corrects inner/outer space in sidebar <li>s (#1137) 2020-02-17 22:04:59 +01:00
Eric Huss
d6cfa21fff Merge pull request #1145 from Rosto75/master
Add a case for an empty `book_title` parameter
2020-02-15 16:11:08 -08:00
Tomasz Różański
95fba3f357 Add a case for an empty book_title parameter
Html page title for every chapter in the book is created by the following code:
```rust
let title: String;
{
	let book_title = ctx
	.data
	.get("book_title")
	.and_then(serde_json::Value::as_str)
	.unwrap_or("");

	title = ch.name.clone() + " - " + book_title;
}
```

If the `book.toml` file is missing, and `book_title` parameter is empty, there's an awkward ` - ` string hanging at the end of each chapter's title.

This PR adds a `match` case to handle that kind of situation.
2020-02-15 23:13:37 +01:00
Eric Huss
d5999849d9 Merge pull request #1140 from willkg/patch-1
Fix typo in README.md
2020-02-06 08:47:57 -08:00
Will Kahn-Greene
8b2659e0f4 Fix typo in README.md 2020-02-06 11:39:37 -05:00
Eric Huss
c4a64ab599 Merge pull request #1133 from EverlastingBugstopper/avery/fix-derive-highlighting
fix: ayu theme meta highlighting
2020-01-31 18:59:22 -08:00
Eric Huss
6b4e3584b4 Merge pull request #1131 from ehuss/fix-rustup-ci
Fix CI: Don't update rustup.
2020-01-31 08:46:01 -08:00
Avery Harnish
b8fc7a1b2d fix: ayu theme meta highlighting 2020-01-30 09:57:45 -06:00
Eric Huss
2ee083dfbe Fix CI: Don't update rustup. 2020-01-27 13:46:42 -08:00
Ning Sun
1947f8ca65 Update handlebars to 3.0 2020-01-24 11:01:44 +08:00
Eric Huss
2f59943c04 rustfmt with 1.40
Some slight changes in formatting in 1.40.
2019-12-31 16:23:25 -08:00
Eric Huss
980f943179 Merge pull request #1123 from j127/patch-1
Fix typo in init.md
2019-12-31 15:49:29 -08:00
Josh
5e998788e9 Fix typo in init.md
"where" was spelled "were"
2019-12-31 15:36:12 -08:00
Arashmidos
6a94492238 fix scroll issue (#1108)
* fix sidebar scrolling

* fix query selector
2019-12-02 11:07:24 +01:00
Aleksey Kladov
e3717ad47b Add --features to CI recipe (#1103) 2019-11-30 01:10:11 +01:00
nickelc
49b7f08164 Fix doc comment of BuildConfig::create_missing (#1104) 2019-11-29 06:22:21 +01:00
Eric Huss
7def6d70e8 Merge pull request #1099 from Michael-F-Bryan/expose-execute-build-process
Exposed the MDBook::execute_build_process() method to 3rd parties
2019-11-23 10:44:58 -08:00
Eric Huss
554f29703f Merge pull request #1097 from dylanowen/cache
Prevent scrolling to the top of the page on websocket reload
2019-11-19 11:57:43 -08:00
Michael Bryan
730d7f8410 Exposed the execute_build_process() method to 3rd parties 2019-11-17 20:59:55 +08:00
Dylan Owen
b6603468d6 Stop scrolling on socket reload 2019-11-12 18:06:11 -08:00
Eric Huss
441a10bdd7 Release 0.3.5. 2019-11-11 13:36:13 -08:00
Eric Huss
efdb83266a Merge pull request #1021 from marcusklaas/pulldown0.6
Upgrade pulldown_cmark to 0.6
2019-11-11 13:27:19 -08:00
Eric Huss
ac9c12334a Update pulldown-cmark in Cargo.toml to 0.6.1. 2019-11-11 13:26:16 -08:00
Marcus Klaas de Vries
2a3088422a Upgrade pulldown_cmark to 0.6.1 2019-11-11 20:25:38 +01:00
Dylan DPC
1f505c2b2e Revert "Add support for Rust edition 2018 in playpens (#1086)" (#1093)
This reverts commit a7b3aa0444.
2019-11-11 13:24:13 +01:00
Gabriel Majeri
a7b3aa0444 Add support for Rust edition 2018 in playpens (#1086)
* Add support for Rust edition 2018 in playpens

* Add Rust edition support to rustdoc

* Run rustfmt

* Fix enum variant reference
2019-11-11 12:42:24 +01:00
Eric Huss
a9160acd64 Merge pull request #1088 from benediktwerner/master
Fix hiding of empty boring lines
2019-11-07 07:44:47 -08:00
Benedikt Werner
4c1bca1684 Don't hide macro lines 2019-11-07 02:42:02 +01:00
Benedikt Werner
8fffb2a704 Hide lines in ignored code blocks 2019-11-07 02:20:10 +01:00
Eric Huss
ba37cc8462 Merge pull request #1089 from Kampfkarren/patch-1
Change erroneous syntax highlighting definition
2019-11-06 12:45:01 -08:00
Ricky
3ea0f9b745 Lowercase matching the theme name (#1079)
* Using .to_lowercase() on the theme matching to avoid needed exact capitolization in the book.toml

* Changed the default-dark-theme from .to_string() to .to_lowercase() to match theme
2019-11-05 13:55:08 +01:00
boyned//Kampfkarren
29d8747e01 Change erroneous syntax highlighting definition
Doing what it says results in:

```
2019-11-04 16:13:15 [WARN] (mdbook::renderer::html_handlebars::hbs_renderer): Previous versions of mdBook erroneously accepted `./src/theme` as an automatic theme directory
2019-11-04 16:13:15 [WARN] (mdbook::renderer::html_handlebars::hbs_renderer): Please move your theme files to `./theme` for them to continue being used
```
2019-11-04 16:14:38 -08:00
Benedikt Werner
f5549f2267 Hide empty lines starting with '#' in playpens 2019-11-04 14:03:25 +01:00
Benedikt Werner
e2a8600712 Remove outdated unused var in theme js code 2019-11-04 14:03:24 +01:00
Eric Huss
f2cb601c11 Release 0.3.4. 2019-10-29 09:58:11 -07:00
Steve Klabnik
6e0d0facff Merge pull request #1083 from rust-lang/move-from-nursery
rust-lang-nursery -> rust-lang
2019-10-29 08:16:54 -05:00
Steve Klabnik
f79d5d4582 rust-lang-nursery -> rust-lang
Fixes #1080
2019-10-29 08:04:16 -05:00
Rostislav
820714a560 Use only relative font sizes (#894)
This replaces the only use of px for font-sizes by setting up a base
rem size on the root element in a way that is easy to calculate (1 rem =
10px) and scaling up according to browser settings.
2019-10-27 15:51:32 +01:00
Eric Huss
d5535d1226 Release 0.3.3. 2019-10-26 12:16:23 -07:00
Eric Huss
e5f77aaaf2 Merge pull request #1077 from mattheww/2019-10_scroll-margin
Add CSS `scroll-margin-top` to headings which contain link targets.
2019-10-26 11:34:57 -07:00
Matthew Woodcraft
86a368b726 Introduce a --menu-bar-height CSS variable 2019-10-26 13:21:26 +01:00
Matthew Woodcraft
1dc482b00d Add scroll-margin-top to headings which contain link targets.
This means when the link is followed, the page scrolls in such a way as to
leave space for the fixed menu bar.

Fixes #1040
2019-10-26 12:55:12 +01:00
Eric Huss
21d8f394ae Fix "next chapter" spacer handling. (#1075) 2019-10-25 17:33:21 +02:00
Benedikt Werner
c9dae170f3 Better automatic dark mode (#1069)
* Don't save default theme to localStorage

* Auto enable dark mode on no-js

* Fix light theme with no-js
2019-10-23 12:15:59 +02:00
Eric Huss
fcf2d7a03b Merge pull request #1073 from ehuss/fix-gh-pages
Fix gh-pages deploy.
2019-10-21 13:43:35 -07:00
Eric Huss
2498887dfc Fix gh-pages deploy. 2019-10-21 13:41:06 -07:00
92 changed files with 3988 additions and 2027 deletions

View File

@@ -10,30 +10,33 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@master
- name: Install hub
run: ci/install-hub.sh ${{ matrix.os }}
shell: bash
- name: Install Rustup
run: ci/install-rustup.sh stable
shell: bash
- name: Install Rust
run: ci/install-rust.sh stable
shell: bash
- name: Build and deploy artifacts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ci/make-release.sh ${{ matrix.os }}
shell: bash
pages:
name: GitHub Pages
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@master
- name: Install Rust (rustup)
run: rustup update stable --no-self-update && rustup default stable
- name: Deploy to GitHub Pages
run: ci/deploy-gh-pages.sh
- name: Build book
run: cargo run -- build book-example
- name: Deploy to GitHub
env:
GITHUB_DEPLOY_KEY: ${{ secrets.GITHUB_DEPLOY_KEY }}
run: |
touch book-example/book/.nojekyll
curl -LsSf https://raw.githubusercontent.com/rust-lang/simpleinfra/master/setup-deploy-keys/src/deploy.rs | rustc - -o /tmp/deploy
cd book-example/book
/tmp/deploy

View File

@@ -31,11 +31,9 @@ jobs:
rust: stable
- build: msrv
os: ubuntu-latest
rust: 1.35.0
rust: 1.39.0
steps:
- uses: actions/checkout@master
- name: Install Rustup
run: bash ci/install-rustup.sh ${{ matrix.rust }}
- name: Install Rust
run: bash ci/install-rust.sh ${{ matrix.rust }}
- name: Build and run tests

View File

@@ -1,200 +1,378 @@
# Changelog
## mdBook 0.4.0
[99ecd4f...d4df7e7](https://github.com/rust-lang/mdBook/compare/99ecd4f...d4df7e7)
### Breaking Changes
- Several of the changes in the release have altered the public API of the
mdbook library.
- Many dependencies have been updated or replaced.
This also removes the `--websocket-hostname` and `--websocket-port` from
the `serve` command.
[#1211](https://github.com/rust-lang/mdBook/pull/1211)
- A new "404" page is now automatically rendered. This requires knowledge of
the base URL of your site to work properly. If you decide to use this as
your 404 page, you should set the `site-url` setting in the book
configuration so mdbook can generate the links correctly. Alternatively you
can disable the 404 page generation, or set up your own 404 handling in your
web server.
[#1221](https://github.com/rust-lang/mdBook/pull/1221)
- If you are using customized themes, you may want to consider setting the
`preferred-dark-theme` config setting, as it now defaults to "navy".
[#1199](https://github.com/rust-lang/mdBook/pull/1199)
- "Playpen" has been renamed to "playground". This is generally backwards
compatible for users, but `{{#playpen}}` will now display warnings. This may
impact books that have modified the "playpen" elements in the theme.
[#1241](https://github.com/rust-lang/mdBook/pull/1241)
- If a renderer is not installed, it is now treated as an error. If you want
the old behavior of ignoring missing renderers, set the `optional` setting
for that renderer.
[#1122](https://github.com/rust-lang/mdBook/pull/1122)
### Added
- Added a new `[rust]` configuration section to `book.toml`, which allows
setting the default edition with `edition = "2018"`.
[#1163](https://github.com/rust-lang/mdBook/pull/1163)
- Renderers can now be marked as `optional`, so that they will be ignored if
the renderer is not installed.
[#1122](https://github.com/rust-lang/mdBook/pull/1122)
- Added `head.hbs` to allow adding content to the `<head>` section in HTML.
[#1206](https://github.com/rust-lang/mdBook/pull/1206)
- Added "draft chapters". These are chapters listed without a link to indicate
content yet to be written.
[#1153](https://github.com/rust-lang/mdBook/pull/1153)
- Added "parts" to split a book into different sections. Headers can be added
to `SUMMARY.md` to signify different sections.
[#1171](https://github.com/rust-lang/mdBook/pull/1171)
- Added generation of a "404" page for handling missing pages and broken links.
[#1221](https://github.com/rust-lang/mdBook/pull/1221)
- Added configuration section for specifying URL redirects.
[#1237](https://github.com/rust-lang/mdBook/pull/1237)
- Added an SVG favicon that works with light and dark colors schemes.
[#1230](https://github.com/rust-lang/mdBook/pull/1230)
### Changed
- Changed default Rust attribute of `allow(unused_variables)` to `allow(unused)`.
[#1195](https://github.com/rust-lang/mdBook/pull/1195)
- Fonts are now served locally instead of from the Google Fonts CDN. The
`copy-fonts` option was added to disable this if you want to supply your own
fonts.
[#1188](https://github.com/rust-lang/mdBook/pull/1188)
- Switched the built-in webserver for the `serve` command to a new
implementation. This results in some internal differences in how websockets
are handled, which removes the separate websocket options. This should also
make it easier to serve multiple books at once.
[#1211](https://github.com/rust-lang/mdBook/pull/1211)
- The default dark theme is now "navy".
[#1199](https://github.com/rust-lang/mdBook/pull/1199)
- "Playpen" has been renamed to "playground", matching the actual name of the
service which was renamed many years ago.
[#1241](https://github.com/rust-lang/mdBook/pull/1241)
### Fixed
- Links with the `+` symbol should now work.
[#1208](https://github.com/rust-lang/mdBook/pull/1208)
- The `MDBOOK_BOOK` environment variable now correctly allows overriding the
entire book configuration.
[#1207](https://github.com/rust-lang/mdBook/pull/1207)
- The sidebar can no longer be dragged outside of the window.
[#1229](https://github.com/rust-lang/mdBook/pull/1229)
- Hide the Rust Playground "play" button for `no_run` code samples.
[#1249](https://github.com/rust-lang/mdBook/pull/1249)
- Fixed the `--dest-dir` command-line option for the `serve` and `watch`
commands.
[#1228](https://github.com/rust-lang/mdBook/pull/1228)
- Hotkey handlers are now disabled in `text` input fields (for example, typing
`S` in a custom text input field).
[#1244](https://github.com/rust-lang/mdBook/pull/1244)
## mdBook 0.3.7
[88684d8...99ecd4f](https://github.com/rust-lang/mdBook/compare/88684d8...99ecd4f)
### Changed
- Code spans in headers are no longer highlighted as code.
[#1162](https://github.com/rust-lang/mdBook/pull/1162)
- The sidebar will now scroll the activate page to the middle instead of the top.
[#1161](https://github.com/rust-lang/mdBook/pull/1161)
- Reverted change to reject build output within the `src` directory, and
instead add a check that prevents infinite copies.
[#1181](https://github.com/rust-lang/mdBook/pull/1181)
[#1026](https://github.com/rust-lang/mdBook/pull/1026)
### Fixed
- Fixed sidebar line-height jumping for collapsed chapters.
[#1182](https://github.com/rust-lang/mdBook/pull/1182)
- Fixed theme selector focus.
[#1170](https://github.com/rust-lang/mdBook/pull/1170)
## mdBook 0.3.6
[efdb832...88684d8](https://github.com/rust-lang/mdBook/compare/efdb832...88684d8)
### Added
- `MDBook::execute_build_process` is now publicly accessible in the API so
that plugins can more easily initiate the build process.
[#1099](https://github.com/rust-lang/mdBook/pull/1099)
### Changed
- Use a different color for Ayu theme's highlighting for Rust attributes (uses
a bright color instead of the comment color).
[#1133](https://github.com/rust-lang/mdBook/pull/1133)
- Adjusted spacing of sidebar entries.
[#1137](https://github.com/rust-lang/mdBook/pull/1137)
- Slightly adjusted line-height of `<p>`, `<ul>`, and `<ol>`.
[#1136](https://github.com/rust-lang/mdBook/pull/1136)
- Handlebars updated to 3.0.
[#1130](https://github.com/rust-lang/mdBook/pull/1130)
### Fixed
- Fix an issue with sidebar scroll position on reload.
[#1108](https://github.com/rust-lang/mdBook/pull/1108)
- `mdbook serve` will retain the current scroll position when the page is reloaded.
[#1097](https://github.com/rust-lang/mdBook/pull/1097)
- Fixed the page name if the book didn't have a title to not be prefixed with ` - `.
[#1145](https://github.com/rust-lang/mdBook/pull/1145)
- HTML attributes `rel=next` and `rel=previous` are now supported in "wide"
mode (previously they were only set in narrow mode).
[#1150](https://github.com/rust-lang/mdBook/pull/1150)
- Prevent recursive copies when the destination directory is contained in the
source directory.
[#1135](https://github.com/rust-lang/mdBook/pull/1135)
- Adjusted the menu bar animation to not immediately obscure the top content.
[#989](https://github.com/rust-lang/mdBook/pull/989)
- Fix for comments in SUMMARY.md that appear between items.
[#1167](https://github.com/rust-lang/mdBook/pull/1167)
## mdBook 0.3.5
[6e0d0fa...efdb832](https://github.com/rust-lang/mdBook/compare/6e0d0fa...efdb832)
### Changed
- The `default-theme` config setting is now case-insensitive.
[#1079](https://github.com/rust-lang/mdBook/pull/1079)
### Fixed
- Fixed `#` hidden Rust code lines not rendering properly.
[#1088](https://github.com/rust-lang/mdBook/pull/1088)
- Updated pulldown-cmark to 0.6.1, fixing several issues.
[#1021](https://github.com/rust-lang/mdBook/pull/1021)
## mdBook 0.3.4
[e5f77aa...6e0d0fa](https://github.com/rust-lang/mdBook/compare/e5f77aa...6e0d0fa)
### Changed
- Switch to relative `rem` font sizes from `px`.
[#894](https://github.com/rust-lang/mdBook/pull/894)
- Migrated repository to https://github.com/rust-lang/mdBook/
[#1083](https://github.com/rust-lang/mdBook/pull/1083)
## mdBook 0.3.3
[2b649fe...e5f77aa](https://github.com/rust-lang/mdBook/compare/2b649fe...e5f77aa)
### Changed
- Improvements to the automatic dark theme selection.
[#1069](https://github.com/rust-lang/mdBook/pull/1069)
- Fragment links now prevent scrolling the header behind the menu bar.
[#1077](https://github.com/rust-lang/mdBook/pull/1077)
### Fixed
- Fixed error when building a book that has a spacer immediately after the
first chapter.
[#1075](https://github.com/rust-lang/mdBook/pull/1075)
## mdBook 0.3.2
[9cd47eb...2b649fe](https://github.com/rust-lang-nursery/mdBook/compare/9cd47eb...2b649fe)
[9cd47eb...2b649fe](https://github.com/rust-lang/mdBook/compare/9cd47eb...2b649fe)
### Added
- Added a markdown renderer, which is off by default. This may be useful for
debugging preprocessors.
[#1018](https://github.com/rust-lang-nursery/mdBook/pull/1018)
[#1018](https://github.com/rust-lang/mdBook/pull/1018)
- Code samples may now include line numbers with the
`output.html.playpen.line-numbers` configuration value.
[#1035](https://github.com/rust-lang-nursery/mdBook/pull/1035)
[#1035](https://github.com/rust-lang/mdBook/pull/1035)
- The `watch` and `serve` commands will now ignore files listed in
`.gitignore`.
[#1044](https://github.com/rust-lang-nursery/mdBook/pull/1044)
[#1044](https://github.com/rust-lang/mdBook/pull/1044)
- Added automatic dark-theme detection based on the CSS `prefers-color-scheme`
feature. This may be enabled by setting `output.html.preferred-dark-theme`
to your preferred dark theme.
[#1037](https://github.com/rust-lang-nursery/mdBook/pull/1037)
[#1037](https://github.com/rust-lang/mdBook/pull/1037)
- Added `rustdoc_include` preprocessor. This makes it easier to include
portions of an external Rust source file. The rest of the file is hidden,
but the user may expand it to see the entire file, and will continue to work
with `mdbook test`.
[#1003](https://github.com/rust-lang-nursery/mdBook/pull/1003)
[#1003](https://github.com/rust-lang/mdBook/pull/1003)
- Added Ctrl-Enter shortcut to the playpen editor to automatically run the
sample.
[#1066](https://github.com/rust-lang-nursery/mdBook/pull/1066)
[#1066](https://github.com/rust-lang/mdBook/pull/1066)
- Added `output.html.playpen.copyable` configuration option to disable
the copy button.
[#1050](https://github.com/rust-lang-nursery/mdBook/pull/1050)
[#1050](https://github.com/rust-lang/mdBook/pull/1050)
- Added ability to dynamically expand and fold sections within the sidebar.
See the `output.html.fold` configuration to enable this feature.
[#1027](https://github.com/rust-lang-nursery/mdBook/pull/1027)
[#1027](https://github.com/rust-lang/mdBook/pull/1027)
### Changed
- Use standard `scrollbar-color` CSS along with webkit extension
[#816](https://github.com/rust-lang-nursery/mdBook/pull/816)
[#816](https://github.com/rust-lang/mdBook/pull/816)
- The renderer build directory is no longer deleted before the renderer is
run. This allows a backend to cache results between runs.
[#985](https://github.com/rust-lang-nursery/mdBook/pull/985)
[#985](https://github.com/rust-lang/mdBook/pull/985)
- Next/prev links now highlight on hover to indicate it is clickable.
[#994](https://github.com/rust-lang-nursery/mdBook/pull/994)
[#994](https://github.com/rust-lang/mdBook/pull/994)
- Increase padding of table headers.
[#824](https://github.com/rust-lang-nursery/mdBook/pull/824)
[#824](https://github.com/rust-lang/mdBook/pull/824)
- Errors in `[output.html]` config are no longer ignored.
[#1033](https://github.com/rust-lang-nursery/mdBook/pull/1033)
[#1033](https://github.com/rust-lang/mdBook/pull/1033)
- Updated highlight.js for syntax highlighting updates (primarily to add
async/await to Rust highlighting).
[#1041](https://github.com/rust-lang-nursery/mdBook/pull/1041)
[#1041](https://github.com/rust-lang/mdBook/pull/1041)
- Raised minimum supported rust version to 1.35.
[#1003](https://github.com/rust-lang-nursery/mdBook/pull/1003)
[#1003](https://github.com/rust-lang/mdBook/pull/1003)
- Hidden code lines are no longer dynamically removed via JavaScript, but
instead managed with CSS.
[#846](https://github.com/rust-lang-nursery/mdBook/pull/846)
[#1065](https://github.com/rust-lang-nursery/mdBook/pull/1065)
[#846](https://github.com/rust-lang/mdBook/pull/846)
[#1065](https://github.com/rust-lang/mdBook/pull/1065)
- Changed the default font set for the ACE editor, giving preference to
"Source Code Pro".
[#1062](https://github.com/rust-lang-nursery/mdBook/pull/1062)
[#1062](https://github.com/rust-lang/mdBook/pull/1062)
- Windows 32-bit releases are no longer published.
[#1071](https://github.com/rust-lang-nursery/mdBook/pull/1071)
[#1071](https://github.com/rust-lang/mdBook/pull/1071)
### Fixed
- Fixed sidebar auto-scrolling.
[#1052](https://github.com/rust-lang-nursery/mdBook/pull/1052)
[#1052](https://github.com/rust-lang/mdBook/pull/1052)
- Fixed error message when running `clean` multiple times.
[#1055](https://github.com/rust-lang-nursery/mdBook/pull/1055)
[#1055](https://github.com/rust-lang/mdBook/pull/1055)
- Actually fix the "next" link on index.html. The previous fix didn't work.
[#1005](https://github.com/rust-lang-nursery/mdBook/pull/1005)
[#1005](https://github.com/rust-lang/mdBook/pull/1005)
- Stop using `inline-block` for `inline code`, fixing selection highlighting
and some rendering issues.
[#1058](https://github.com/rust-lang-nursery/mdBook/pull/1058)
[#1058](https://github.com/rust-lang/mdBook/pull/1058)
- Fix header auto-hide on browsers with momentum scrolling that allows
negative `scrollTop`.
[#1070](https://github.com/rust-lang-nursery/mdBook/pull/1070)
[#1070](https://github.com/rust-lang/mdBook/pull/1070)
## mdBook 0.3.1
[69a08ef...9cd47eb](https://github.com/rust-lang-nursery/mdBook/compare/69a08ef...9cd47eb)
[69a08ef...9cd47eb](https://github.com/rust-lang/mdBook/compare/69a08ef...9cd47eb)
### Added
- 🔥 Added ability to include files using anchor points instead of line numbers.
[#851](https://github.com/rust-lang-nursery/mdBook/pull/851)
[#851](https://github.com/rust-lang/mdBook/pull/851)
- Added `language` configuration value to set the language of the book, which
will affect things like the `<html lang="en">` tag.
[#941](https://github.com/rust-lang-nursery/mdBook/pull/941)
[#941](https://github.com/rust-lang/mdBook/pull/941)
### Changed
- Updated to handlebars 2.0.
[#977](https://github.com/rust-lang-nursery/mdBook/pull/977)
[#977](https://github.com/rust-lang/mdBook/pull/977)
### Fixed
- Fixed memory leak warning.
[#967](https://github.com/rust-lang-nursery/mdBook/pull/967)
[#967](https://github.com/rust-lang/mdBook/pull/967)
- Fix more print.html links.
[#963](https://github.com/rust-lang-nursery/mdBook/pull/963)
[#963](https://github.com/rust-lang/mdBook/pull/963)
- Fixed crash on some unicode input.
[#978](https://github.com/rust-lang-nursery/mdBook/pull/978)
[#978](https://github.com/rust-lang/mdBook/pull/978)
## mdBook 0.3.0
[6cbc41d...69a08ef](https://github.com/rust-lang-nursery/mdBook/compare/6cbc41d...69a08ef)
[6cbc41d...69a08ef](https://github.com/rust-lang/mdBook/compare/6cbc41d...69a08ef)
### Added
- Added ability to resize the sidebar.
[#849](https://github.com/rust-lang-nursery/mdBook/pull/849)
[#849](https://github.com/rust-lang/mdBook/pull/849)
- Added `load_with_config_and_summary` function to `MDBook` to be able to
build a book with a custom `Summary`.
[#883](https://github.com/rust-lang-nursery/mdBook/pull/883)
[#883](https://github.com/rust-lang/mdBook/pull/883)
- Set `noindex` on `print.html` page to prevent robots from indexing it.
[#844](https://github.com/rust-lang-nursery/mdBook/pull/844)
[#844](https://github.com/rust-lang/mdBook/pull/844)
- Added support for ~~strikethrough~~ and GitHub-style tasklists.
[#952](https://github.com/rust-lang-nursery/mdBook/pull/952)
[#952](https://github.com/rust-lang/mdBook/pull/952)
### Changed
- Command-line help output is now colored.
[#861](https://github.com/rust-lang-nursery/mdBook/pull/861)
[#861](https://github.com/rust-lang/mdBook/pull/861)
- The build directory is now deleted before rendering starts, instead of after
if finishes.
[#878](https://github.com/rust-lang-nursery/mdBook/pull/878)
[#878](https://github.com/rust-lang/mdBook/pull/878)
- Removed dependency on `same-file` crate.
[#903](https://github.com/rust-lang-nursery/mdBook/pull/903)
[#903](https://github.com/rust-lang/mdBook/pull/903)
- 💥 Renamed `with_preprecessor` to `with_preprocessor`.
[#906](https://github.com/rust-lang-nursery/mdBook/pull/906)
[#906](https://github.com/rust-lang/mdBook/pull/906)
- Updated ACE editor to 1.4.4, should remove a JavaScript console warning.
[#935](https://github.com/rust-lang-nursery/mdBook/pull/935)
[#935](https://github.com/rust-lang/mdBook/pull/935)
- Dependencies have been updated.
[#934](https://github.com/rust-lang-nursery/mdBook/pull/934)
[#945](https://github.com/rust-lang-nursery/mdBook/pull/945)
[#934](https://github.com/rust-lang/mdBook/pull/934)
[#945](https://github.com/rust-lang/mdBook/pull/945)
- Highlight.js has been updated. This fixes some TOML highlighting, and adds
Julia support.
[#942](https://github.com/rust-lang-nursery/mdBook/pull/942)
[#942](https://github.com/rust-lang/mdBook/pull/942)
- 🔥 Updated to pulldown-cmark 0.5. This may have significant changes to the
formatting of existing books, as the newer version has more accurate
interpretation of the CommonMark spec and a large number of bug fixes and
changes.
[#898](https://github.com/rust-lang-nursery/mdBook/pull/898)
[#898](https://github.com/rust-lang/mdBook/pull/898)
- The `diff` language should now highlight correctly.
[#943](https://github.com/rust-lang-nursery/mdBook/pull/943)
[#943](https://github.com/rust-lang/mdBook/pull/943)
- Make the blank region of a header not clickable.
[#948](https://github.com/rust-lang-nursery/mdBook/pull/948)
[#948](https://github.com/rust-lang/mdBook/pull/948)
- Rustdoc tests now use the preprocessed content instead of the raw,
unpreprocessed content.
[#891](https://github.com/rust-lang-nursery/mdBook/pull/891)
[#891](https://github.com/rust-lang/mdBook/pull/891)
### Fixed
- Fixed file change detection so that `mdbook serve` only reloads once when
multiple files are changed at once.
[#870](https://github.com/rust-lang-nursery/mdBook/pull/870)
[#870](https://github.com/rust-lang/mdBook/pull/870)
- Fixed on-hover color highlighting for links in sidebar.
[#834](https://github.com/rust-lang-nursery/mdBook/pull/834)
[#834](https://github.com/rust-lang/mdBook/pull/834)
- Fixed loss of focus when clicking the "Copy" button in code blocks.
[#867](https://github.com/rust-lang-nursery/mdBook/pull/867)
[#867](https://github.com/rust-lang/mdBook/pull/867)
- Fixed incorrectly stripping the path for `additional-js` files.
[#796](https://github.com/rust-lang-nursery/mdBook/pull/796)
[#796](https://github.com/rust-lang/mdBook/pull/796)
- Fixed color of `code spans` that are links.
[#905](https://github.com/rust-lang-nursery/mdBook/pull/905)
[#905](https://github.com/rust-lang/mdBook/pull/905)
- Fixed "next" navigation on index.html.
[#916](https://github.com/rust-lang-nursery/mdBook/pull/916)
[#916](https://github.com/rust-lang/mdBook/pull/916)
- Fixed keyboard chapter navigation for `file` urls.
[#915](https://github.com/rust-lang-nursery/mdBook/pull/915)
[#915](https://github.com/rust-lang/mdBook/pull/915)
- Fixed bad wrapping for inline code on some browsers.
[#818](https://github.com/rust-lang-nursery/mdBook/pull/818)
[#818](https://github.com/rust-lang/mdBook/pull/818)
- Properly load an existing `SUMMARY.md` in `mdbook init`.
[#841](https://github.com/rust-lang-nursery/mdBook/pull/841)
[#841](https://github.com/rust-lang/mdBook/pull/841)
- Fixed some broken links in `print.html`.
[#871](https://github.com/rust-lang-nursery/mdBook/pull/871)
[#871](https://github.com/rust-lang/mdBook/pull/871)
- The Rust Playground link now supports the 2018 edition.
[#946](https://github.com/rust-lang-nursery/mdBook/pull/946)
[#946](https://github.com/rust-lang/mdBook/pull/946)
## mdBook 0.2.3 (2018-01-18)
[2c20c99...6cbc41d](https://github.com/rust-lang-nursery/mdBook/compare/2c20c99...6cbc41d)
[2c20c99...6cbc41d](https://github.com/rust-lang/mdBook/compare/2c20c99...6cbc41d)
### Added
- Added an optional button to the top of the page which will link to a git
repository. Use the `git-repository-url` and `git-repository-icon` options
in the `[output.html]` section to enable it and set its appearance.
[#802](https://github.com/rust-lang-nursery/mdBook/pull/802)
[#802](https://github.com/rust-lang/mdBook/pull/802)
- Added a `default-theme` option to the `[output.html]` section.
[#804](https://github.com/rust-lang-nursery/mdBook/pull/804)
[#804](https://github.com/rust-lang/mdBook/pull/804)
### Changed
- 💥 Header ID anchors no longer add an arbitrary `a` character for headers
that start with a non-ascii-alphabetic character.
[#788](https://github.com/rust-lang-nursery/mdBook/pull/788)
[#788](https://github.com/rust-lang/mdBook/pull/788)
### Fixed
- Fix websocket hostname usage
[#865](https://github.com/rust-lang-nursery/mdBook/pull/865)
[#865](https://github.com/rust-lang/mdBook/pull/865)
- Fixing links in print.html
[#866](https://github.com/rust-lang-nursery/mdBook/pull/866)
[#866](https://github.com/rust-lang/mdBook/pull/866)
## mdBook 0.2.2 (2018-10-19)
[7e2e095...2c20c99](https://github.com/rust-lang-nursery/mdBook/compare/7e2e095...2c20c99)
[7e2e095...2c20c99](https://github.com/rust-lang/mdBook/compare/7e2e095...2c20c99)
### Added
- 🎉 Process-based custom preprocessors. See [the
docs](https://rust-lang-nursery.github.io/mdBook/for_developers/preprocessors.html)
docs](https://rust-lang.github.io/mdBook/for_developers/preprocessors.html)
for more.
[#792](https://github.com/rust-lang-nursery/mdBook/pull/792)
[#792](https://github.com/rust-lang/mdBook/pull/792)
- 🎉 Configurable preprocessors.
@@ -216,26 +394,26 @@
Added `PreprocessorContext::renderer` to indicate the renderer being used.
[#658](https://github.com/rust-lang-nursery/mdBook/pull/658)
[#787](https://github.com/rust-lang-nursery/mdBook/pull/787)
[#658](https://github.com/rust-lang/mdBook/pull/658)
[#787](https://github.com/rust-lang/mdBook/pull/787)
### Fixed
- Fix paths to additional CSS and JavaScript files
[#777](https://github.com/rust-lang-nursery/mdBook/pull/777)
[#777](https://github.com/rust-lang/mdBook/pull/777)
- Ensure section numbers are correctly incremented after a horizontal
separator
[#790](https://github.com/rust-lang-nursery/mdBook/pull/790)
[#790](https://github.com/rust-lang/mdBook/pull/790)
## mdBook 0.2.1 (2018-08-22)
[91ffca1...7e2e095](https://github.com/rust-lang-nursery/mdBook/compare/91ffca1...7e2e095)
[91ffca1...7e2e095](https://github.com/rust-lang/mdBook/compare/91ffca1...7e2e095)
### Changed
- Update to handlebars-rs 1.0
[#761](https://github.com/rust-lang-nursery/mdBook/pull/761)
[#761](https://github.com/rust-lang/mdBook/pull/761)
### Fixed
- Fix table colors, broken by Stylus -> CSS transition
[#765](https://github.com/rust-lang-nursery/mdBook/pull/765)
[#765](https://github.com/rust-lang/mdBook/pull/765)
## mdBook 0.2.0 (2018-08-02)
@@ -243,7 +421,7 @@
- 💥 This release changes how links are handled in mdBook. Previously, relative
links were interpreted relative to the book's root. In `0.2.0`+ links are
relative to the page they are in, and use the `.md` extension. This has [several
advantages](https://github.com/rust-lang-nursery/mdBook/pull/603#issue-166701447),
advantages](https://github.com/rust-lang/mdBook/pull/603#issue-166701447),
such as making links work in other markdown viewers like GitHub. You will
likely have to change links in your book to accommodate this change. For
example, a book with this layout:
@@ -275,11 +453,11 @@
help serve` for details.
- Embedded rust playpens now use the "stable" playground API.
[#754](https://github.com/rust-lang-nursery/mdBook/pull/754)
[#754](https://github.com/rust-lang/mdBook/pull/754)
### Fixed
- Escaped includes (`\{{#include file.rs}}`) will now render correctly.
[f30ce01](https://github.com/rust-lang-nursery/mdBook/commit/f30ce0184d71e342141145472bf816419d30a2c5)
[f30ce01](https://github.com/rust-lang/mdBook/commit/f30ce0184d71e342141145472bf816419d30a2c5)
- `index.html` will now render correctly when the book's first section is
inside a subdirectory.
[#756](https://github.com/rust-lang-nursery/mdBook/pull/756)
[#756](https://github.com/rust-lang/mdBook/pull/756)

View File

@@ -5,22 +5,22 @@ Welcome stranger!
If you have come here to learn how to contribute to mdBook, we have some tips for you!
First of all, don't hesitate to ask questions!
Use the [issue tracker](https://github.com/rust-lang-nursery/mdBook/issues), no question is too simple.
Use the [issue tracker](https://github.com/rust-lang/mdBook/issues), no question is too simple.
If we don't respond in a couple of days, ping us @Michael-F-Bryan, @budziq, @steveklabnik, @frewsxcv it might just be that we forgot. :wink:
### Issues to work on
Any issue is up for the grabbing, but if you are starting out, you might be interested in the
[E-Easy issues](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
[E-Easy issues](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy).
Those are issues that are considered more straightforward for beginners to Rust or the codebase itself.
These issues can be a good launching pad for more involved issues. Easy tasks for a first time contribution
include documentation improvements, new tests, examples, updating dependencies, etc.
If you come from a web development background, you might be interested in issues related to web technologies tagged
[A-JavaScript](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
[A-Style](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
[A-HTML](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
[A-Mobile](https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
[A-JavaScript](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-JavaScript),
[A-Style](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Style),
[A-HTML](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-HTML) or
[A-Mobile](https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AA-Mobile).
When you decide you want to work on a specific issue, ping us on that issue so that we can assign it to you.
Again, do not hesitate to ask questions. We will gladly mentor anyone that want to tackle an issue.
@@ -41,7 +41,7 @@ mdBook builds on stable Rust, if you want to build mdBook from source, here are
0. Clone this repository with git.
```
git clone https://github.com/rust-lang-nursery/mdBook.git
git clone https://github.com/rust-lang/mdBook.git
```
0. Navigate into the newly created `mdBook` directory
0. Run `cargo build`
@@ -57,7 +57,7 @@ We love code quality and Rust has some excellent tools to assist you with contri
Before you make your Pull Request to the project, please run it through the `rustfmt` utility.
This will ensure we have good quality source code that is better for us all to maintain.
[rustfmt](https://github.com/rust-lang-nursery/rustfmt) has a lot more information on the project.
[rustfmt](https://github.com/rust-lang/rustfmt) has a lot more information on the project.
The quick guide is
1. Install it
@@ -74,7 +74,7 @@ The quick guide is
```
When run through `cargo` it will format all bin and lib files in the current crate.
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang-nursery/rustfmt)
For more information, such as running it from your favourite editor, please see the `rustfmt` project. [rustfmt](https://github.com/rust-lang/rustfmt)
#### Finding Issues with Clippy
@@ -83,7 +83,7 @@ Clippy is a code analyser/linter detecting mistakes, and therfore helps to impro
Like formatting your code with `rustfmt`, running clippy regularly and before your Pull Request will
help us maintain awesome code.
The best documentation can be found over at [rust-clippy](https://github.com/rust-lang-nursery/rust-clippy)
The best documentation can be found over at [rust-clippy](https://github.com/rust-lang/rust-clippy)
1. To install
```
@@ -94,7 +94,7 @@ The best documentation can be found over at [rust-clippy](https://github.com/rus
cargo clippy
```
Clippy has an ever growing list of checks, that are managed in [lint files](https://rust-lang-nursery.github.io/rust-clippy/master/index.html).
Clippy has an ever growing list of checks, that are managed in [lint files](https://rust-lang.github.io/rust-clippy/master/index.html).
### Making a pull-request
@@ -102,7 +102,7 @@ When you feel comfortable that your changes could be integrated into mdBook, you
One of the core maintainers will then approve the changes or request some changes before it gets merged.
If you want to make your pull-request even better, you might want to run [Clippy](https://github.com/Manishearth/rust-clippy)
and [rustfmt](https://github.com/rust-lang-nursery/rustfmt) on the code first.
and [rustfmt](https://github.com/rust-lang/rustfmt) on the code first.
This is not a requirement though and will never block a pull-request from being merged.
That's it, happy contributions! :tada: :tada: :tada:

1945
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,31 @@
[package]
name = "mdbook"
version = "0.3.2"
version = "0.4.0"
authors = [
"Mathieu David <mathieudavid@mathieudavid.org>",
"Michael-F-Bryan <michaelfbryan@gmail.com>",
"Matt Ickstadt <mattico8@gmail.com>"
]
documentation = "http://rust-lang-nursery.github.io/mdBook/index.html"
documentation = "http://rust-lang.github.io/mdBook/index.html"
edition = "2018"
exclude = ["/book-example/*"]
keywords = ["book", "gitbook", "rustbook", "markdown"]
license = "MPL-2.0"
readme = "README.md"
repository = "https://github.com/rust-lang-nursery/mdBook"
repository = "https://github.com/rust-lang/mdBook"
description = "Creates a book from markdown files"
[dependencies]
anyhow = "1.0.28"
chrono = "0.4"
clap = "2.24"
env_logger = "0.6"
error-chain = "0.12"
handlebars = { version = "2.0", default-features = false, features = ["no_dir_source"] }
itertools = "0.8"
env_logger = "0.7.1"
handlebars = "3.0"
lazy_static = "1.0"
log = "0.4"
memchr = "2.0"
open = "1.1"
pulldown-cmark = "0.5"
pulldown-cmark = "0.7.0"
regex = "1.0.0"
serde = "1.0"
serde_derive = "1.0"
@@ -34,16 +33,15 @@ serde_json = "1.0"
shlex = "0.1"
tempfile = "3.0"
toml = "0.5.1"
toml-query = "0.9"
# Watch feature
notify = { version = "4.0", optional = true }
gitignore = { version = "1.0", optional = true }
# Serve feature
iron = { version = "0.6", optional = true }
staticfile = { version = "0.5", optional = true }
ws = { version = "0.9", optional = true}
futures-util = { version = "0.3.4", optional = true }
tokio = { version = "0.2.18", features = ["macros"], optional = true }
warp = { version = "0.2.2", default-features = false, features = ["websocket"], optional = true }
# Search feature
elasticlunr-rs = { version = "2.3", optional = true, default-features = false }
@@ -55,11 +53,9 @@ pretty_assertions = "0.6"
walkdir = "2.0"
[features]
default = ["output", "watch", "serve", "search"]
debug = []
output = []
default = ["watch", "serve", "search"]
watch = ["notify", "gitignore"]
serve = ["iron", "staticfile", "ws"]
serve = ["futures-util", "tokio", "warp"]
search = ["elasticlunr-rs", "ammonia"]
[[bin]]

View File

@@ -1,8 +1,8 @@
# mdBook
[![Build Status](https://github.com/rust-lang-nursery/mdBook/workflows/CI/badge.svg)](https://github.com/rust-lang-nursery/mdBook/actions?workflow=CI)
[![Build Status](https://github.com/rust-lang/mdBook/workflows/CI/badge.svg?event=push)](https://github.com/rust-lang/mdBook/actions?workflow=CI)
[![crates.io](https://img.shields.io/crates/v/mdbook.svg)](https://crates.io/crates/mdbook)
[![LICENSE](https://img.shields.io/github/license/rust-lang-nursery/mdBook.svg)](LICENSE)
[![LICENSE](https://img.shields.io/github/license/rust-lang/mdBook.svg)](LICENSE)
mdBook is a utility to create modern online books from Markdown files.
@@ -24,7 +24,7 @@ There are multiple ways to install mdBook.
2. **From Crates.io**
This requires at least [Rust] 1.35 and Cargo to be installed. Once you have installed
This requires at least [Rust] 1.39 and Cargo to be installed. Once you have installed
Rust, type the following in the terminal:
```
@@ -42,10 +42,14 @@ There are multiple ways to install mdBook.
This will constrain the server to install the latest **non-breaking**
version of mdBook and will prevent your books from failing to build because
we released a new version. For example:
we released a new version.
You can also disable default features to speed up compile time.
Example:
```
cargo install mdbook --vers "^0.1.0"
cargo install mdbook --no-default-features --features output --vers "^0.1.0"
```
3. **From Git**
@@ -55,7 +59,7 @@ There are multiple ways to install mdBook.
the git version of mdBook yourself. Cargo makes this ***super easy***!
```
cargo install --git https://github.com/rust-lang-nursery/mdBook.git mdbook
cargo install --git https://github.com/rust-lang/mdBook.git mdbook
```
Again, make sure to add the Cargo bin directory to your `PATH`.
@@ -66,7 +70,7 @@ There are multiple ways to install mdBook.
your local machine:
```
git clone https://github.com/rust-lang-nursery/mdBook.git
git clone https://github.com/rust-lang/mdBook.git
```
`cd` into `mdBook/` and run
@@ -144,7 +148,7 @@ preprocessors are:
all `README.md` chapters to `index.md` so `foo/README.md` can be accessed via
the url `foo/` when published to a browser
- `links` - a built-in preprocessor (enabled by default) for expanding the
`{{# playpen}}` and `{{# include}}` helpers in a chapter.
`{{# playground}}` and `{{# include}}` helpers in a chapter.
Renderers are given the final book so they can do something with it. This is
typically used for, as the name suggests, rendering the document in a particular
@@ -200,7 +204,7 @@ Contributions are highly appreciated and encouraged! Don't hesitate to
participate to discussions in the issues, propose new features and ask for
help.
If you are just starting out with Rust, there are a series of issus that are
If you are just starting out with Rust, there are a series of issues that are
tagged [E-Easy] and **we will gladly mentor you** so that you can successfully
go through the process of fixing a bug or adding a new feature! Let us know if
you need any help.
@@ -217,14 +221,14 @@ available, for those hacking on `master`.
All the code in this repository is released under the ***Mozilla Public License v2.0***, for more information take a look at the [LICENSE] file.
[User Guide]: https://rust-lang-nursery.github.io/mdBook/
[User Guide]: https://rust-lang.github.io/mdBook/
[API docs]: https://docs.rs/mdbook/*/mdbook/
[E-Easy]: https://github.com/rust-lang-nursery/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy
[contribution guide]: https://github.com/rust-lang-nursery/mdBook/blob/master/CONTRIBUTING.md
[LICENSE]: https://github.com/rust-lang-nursery/mdBook/blob/master/LICENSE
[releases]: https://github.com/rust-lang-nursery/mdBook/releases
[E-Easy]: https://github.com/rust-lang/mdBook/issues?q=is%3Aopen+is%3Aissue+label%3AE-Easy
[contribution guide]: https://github.com/rust-lang/mdBook/blob/master/CONTRIBUTING.md
[LICENSE]: https://github.com/rust-lang/mdBook/blob/master/LICENSE
[releases]: https://github.com/rust-lang/mdBook/releases
[Rust]: https://www.rust-lang.org/
[CLI docs]: http://rust-lang-nursery.github.io/mdBook/cli/init.html
[master-docs]: http://rust-lang-nursery.github.io/mdBook/
[CLI docs]: http://rust-lang.github.io/mdBook/cli/init.html
[master-docs]: http://rust-lang.github.io/mdBook/
[`linkcheck`]: https://crates.io/crates/mdbook-linkcheck
[`epub`]: https://crates.io/crates/mdbook-epub

View File

@@ -4,10 +4,14 @@ description = "Create book from markdown files. Like Gitbook but implemented in
authors = ["Mathieu David", "Michael-F-Bryan"]
language = "en"
[rust]
edition = "2018"
[output.html]
mathjax-support = true
site-url = "/mdBook/"
[output.html.playpen]
[output.html.playground]
editable = true
line-numbers = true

3
book-example/src/404.md Normal file
View File

@@ -0,0 +1,3 @@
# Document not found (404)
This URL is invalid, sorry. Try the search instead!

View File

@@ -8,9 +8,9 @@ What you are reading serves as an example of the output of mdBook and at the
same time as a high-level documentation.
mdBook is free and open source, you can find the source code on
[GitHub](https://github.com/rust-lang-nursery/mdBook). Issues and feature
[GitHub](https://github.com/rust-lang/mdBook). Issues and feature
requests can be posted on the [GitHub issue
tracker](https://github.com/rust-lang-nursery/mdBook/issues).
tracker](https://github.com/rust-lang/mdBook/issues).
## API docs

View File

@@ -10,6 +10,7 @@
- [clean](cli/clean.md)
- [Format](format/README.md)
- [SUMMARY.md](format/summary.md)
- [Draft chapter]()
- [Configuration](format/config.md)
- [Theme](format/theme/README.md)
- [index.hbs](format/theme/index-hbs.md)

View File

@@ -7,7 +7,7 @@ capabilities first.
## Install From Binaries
Precompiled binaries are provided for major platforms on a best-effort basis.
Visit [the releases page](https://github.com/rust-lang-nursery/mdBook/releases)
Visit [the releases page](https://github.com/rust-lang/mdBook/releases)
to download the appropriate version for your platform.
## Install From Source
@@ -39,14 +39,14 @@ have installed mdBook!
### Install Git version
The **[git version](https://github.com/rust-lang-nursery/mdBook)** contains all
The **[git version](https://github.com/rust-lang/mdBook)** contains all
the latest bug-fixes and features, that will be released in the next version on
**Crates.io**, if you can't wait until the next release. You can build the git
version yourself. Open your terminal and navigate to the directory of you
choice. We need to clone the git repository and then build it with Cargo.
```bash
git clone --depth=1 https://github.com/rust-lang-nursery/mdBook.git
git clone --depth=1 https://github.com/rust-lang/mdBook.git
cd mdBook
cargo build --release
```

View File

@@ -35,5 +35,5 @@ not specified it will default to the value of the `build.build-dir` key in
-------------------
***Note:*** *Make sure to run the build command in the root directory and not in
the source directory*
***Note:*** *The build command copies all files (excluding files with `.md` extension) from the source directory
into the build directory.*

View File

@@ -19,7 +19,7 @@ book-test/
└── SUMMARY.md
```
- The `src` directory is were you write your book in markdown. It contains all
- The `src` directory is where you write your book in markdown. It contains all
the source files, configuration files, etc.
- The `book` directory is where your book is rendered. All the output is ready

View File

@@ -46,3 +46,14 @@ The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. Relative paths are interpreted relative to the book's root directory. If
not specified it will default to the value of the `build.build-dir` key in
`book.toml`, or to `./book`.
#### Specify exclude patterns
The `serve` command will not automatically trigger a build for files listed in
the `.gitignore` file in the book root directory. The `.gitignore` file may
contain file patterns described in the [gitignore
documentation](https://git-scm.com/docs/gitignore). This can be useful for
ignoring temporary files created by some editors.
_Note: Only `.gitignore` from book root directory is used. Global
`$HOME/.gitignore` or `.gitignore` files in parent directories are not used._

View File

@@ -25,3 +25,15 @@ The `--dest-dir` (`-d`) option allows you to change the output directory for the
book. Relative paths are interpreted relative to the book's root directory. If
not specified it will default to the value of the `build.build-dir` key in
`book.toml`, or to `./book`.
#### Specify exclude patterns
The `watch` command will not automatically trigger a build for files listed in
the `.gitignore` file in the book root directory. The `.gitignore` file may
contain file patterns described in the [gitignore
documentation](https://git-scm.com/docs/gitignore). This can be useful for
ignoring temporary files created by some editors.
_Note: Only `.gitignore` from book root directory is used. Global
`$HOME/.gitignore` or `.gitignore` files in parent directories are not used._

View File

@@ -329,6 +329,36 @@ generation or a warning).
All environment variables are passed through to the backend, allowing you to use
the usual `RUST_LOG` to control logging verbosity.
## Handling missing backends
If you enable a backend that isn't installed, the default behavior is to throw an error:
```text
The command wasn't found, is the "wordcount" backend installed?
```
This behavior can be changed by marking the backend as optional.
```diff
[book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David", "Michael-F-Bryan"]
[output.html]
[output.wordcount]
command = "python /path/to/wordcount.py"
+ optional = true
```
This demotes the error to a warning, and it will instead look like this:
```text
The command was not found, but was marked as optional.
Command: wordcount
```
## Wrapping Up
@@ -352,4 +382,4 @@ the source code or ask questions.
[`Book`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html
[`Book::iter()`]: https://docs.rs/mdbook/*/mdbook/book/struct.Book.html#method.iter
[`Config`]: https://docs.rs/mdbook/*/mdbook/config/struct.Config.html
[issue tracker]: https://github.com/rust-lang-nursery/mdBook/issues
[issue tracker]: https://github.com/rust-lang/mdBook/issues

View File

@@ -109,7 +109,7 @@ For everything else, have a look [at the complete example][example].
[preprocessor-docs]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html
[pc]: https://crates.io/crates/pulldown-cmark
[pctc]: https://crates.io/crates/pulldown-cmark-to-cmark
[example]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/nop-preprocessor.rs
[an example no-op preprocessor]: https://github.com/rust-lang-nursery/mdBook/blob/master/examples/nop-preprocessor.rs
[example]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
[an example no-op preprocessor]: https://github.com/rust-lang/mdBook/blob/master/examples/nop-preprocessor.rs
[`CmdPreprocessor::parse_input()`]: https://docs.rs/mdbook/latest/mdbook/preprocess/trait.Preprocessor.html#method.parse_input
[`Book::for_each_mut()`]: https://docs.rs/mdbook/latest/mdbook/book/struct.Book.html#method.for_each_mut

View File

@@ -10,6 +10,9 @@ title = "Example book"
author = "John Doe"
description = "The example book covers examples."
[rust]
edition = "2018"
[build]
build-dir = "my-example-book"
create-missing = false
@@ -54,6 +57,22 @@ src = "my-src" # the source files will be found in `root/my-src` instead of `ro
language = "en"
```
### Rust options
Options for the Rust language, relevant to running tests and playground
integration.
- **edition**: Rust edition to use by default for the code snippets. Default
is "2015". Individual code blocks can be controlled with the `edition2015`
or `edition2018` annotations, such as:
~~~text
```rust,edition2015
// This only works in 2015.
let try = true;
```
~~~
### Build options
This controls the build process of your book.
@@ -81,7 +100,7 @@ This controls the build process of your book.
The following preprocessors are available and included by default:
- `links`: Expand the `{{ #playpen }}`, `{{ #include }}`, and `{{ #rustdoc_include }}` handlebars
- `links`: Expand the `{{ #playground }}`, `{{ #include }}`, and `{{ #rustdoc_include }}` handlebars
helpers in a chapter to include the contents of a file.
- `index`: Convert all chapter files named `README.md` into `index.md`. That is
to say, all `README.md` would be rendered to an index file `index.html` in the
@@ -153,11 +172,12 @@ The following configuration options are available:
- **preferred-dark-theme:** The default dark theme. This theme will be used if
the browser requests the dark version of the site via the
['prefers-color-scheme'](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme)
CSS media query. Defaults to the same theme as `default-theme`.
CSS media query. Defaults to `navy`.
- **curly-quotes:** Convert straight quotes to curly quotes, except for those
that occur in code blocks and code spans. Defaults to `false`.
- **mathjax-support:** Adds support for [MathJax](mathjax.md). Defaults to
`false`.
- **copy-fonts:** Copies fonts.css and respective font files to the output directory and use them in the default theme. Defaults to `true`.
- **google-analytics:** If you use Google Analytics, this option lets you enable
it by simply specifying your ID in the configuration file.
- **additional-css:** If you need to slightly change the appearance of your book
@@ -171,14 +191,26 @@ The following configuration options are available:
contents column. For example, "1.", "2.1". Set this option to true to disable
those labels. Defaults to `false`.
- **fold:** A subtable for configuring sidebar section-folding behavior.
- **playpen:** A subtable for configuring various playpen settings.
- **playground:** A subtable for configuring various playground settings.
- **search:** A subtable for configuring the in-browser search functionality.
mdBook must be compiled with the `search` feature enabled (on by default).
- **git-repository-url:** A url to the git repository for the book. If provided
an icon link will be output in the menu bar of the book.
- **git-repository-icon:** The FontAwesome icon class to use for the git
repository link. Defaults to `fa-github`.
- **redirect:** A subtable used for generating redirects when a page is moved.
The table contains key-value pairs where the key is where the redirect file
needs to be created, as an absolute path from the build directory, (e.g.
`/appendices/bibliography.html`). The value can be any valid URI the
browser should navigate to (e.g. `https://rust-lang.org/`,
`/overview.html`, or `../bibliography.html`).
- **input-404:** The name of the markdown file used for misssing files.
The corresponding output file will be the same, with the extension replaced with `html`.
Defaults to `404.md`.
- **site-url:** The url where the book will be hosted. This is required to ensure
navigation links and script/css imports in the 404 file work correctly, even when accessing
urls in subdirectories. Defaults to `/`.
Available configuration options for the `[output.html.fold]` table:
- **enable:** Enable section-folding. When off, all folds are open.
@@ -186,7 +218,7 @@ Available configuration options for the `[output.html.fold]` table:
- **level:** The higher the more folded regions are open. When level is 0, all
folds are closed. Defaults to `0`.
Available configuration options for the `[output.html.playpen]` table:
Available configuration options for the `[output.html.playground]` table:
- **editable:** Allow editing the source code. Defaults to `false`.
- **copyable:** Display the copy button on code snippets. Defaults to `true`.
@@ -233,18 +265,21 @@ default-theme = "light"
preferred-dark-theme = "navy"
curly-quotes = true
mathjax-support = false
google-analytics = "123456"
copy-fonts = true
google-analytics = "UA-123456-7"
additional-css = ["custom.css", "custom2.css"]
additional-js = ["custom.js"]
no-section-label = false
git-repository-url = "https://github.com/rust-lang-nursery/mdBook"
git-repository-url = "https://github.com/rust-lang/mdBook"
git-repository-icon = "fa-github"
site-url = "/example-book/"
input-404 = "not-found.md"
[output.html.fold]
enable = false
level = 0
[output.html.playpen]
[output.html.playground]
editable = false
copy-js = true
line-numbers = false
@@ -260,6 +295,10 @@ boost-paragraph = 1
expand = true
heading-split-level = 3
copy-js = true
[output.html.redirect]
"/appendices/bibliography.html" = "https://rustc-dev-guide.rust-lang.org/appendix/bibliography.html"
"/other-installation-methods.html" = "../infra/other-installation-methods.html"
```
### Markdown Renderer
@@ -270,7 +309,7 @@ conjunction with `mdbook test` to see the Markdown that `mdbook` is passing
to `rustdoc`.
The Markdown renderer is included with `mdbook` but disabled by default.
Enable it by adding an emtpy table to your `book.toml` as follows:
Enable it by adding an empty table to your `book.toml` as follows:
```toml
[output.markdown]
@@ -287,11 +326,17 @@ specify which preprocessors should run before the Markdown renderer.
A custom renderer can be enabled by adding a `[output.foo]` table to your
`book.toml`. Similar to [preprocessors](#configuring-preprocessors) this will
instruct `mdbook` to pass a representation of the book to `mdbook-foo` for
rendering.
rendering. See the [alternative backends] chapter for more detail.
Custom renderers will have access to all configuration within their table
(i.e. anything under `[output.foo]`), and the command to be invoked can be
manually specified with the `command` field.
The custom renderer has access to all the fields within its table (i.e.
anything under `[output.foo]`). mdBook checks for two common fields:
- **command:** The command to execute for this custom renderer. Defaults to
the name of the renderer with the `mdbook-` prefix (such as `mdbook-foo`).
- **optional:** If `true`, then the command will be ignored if it is not
installed, otherwise mdBook will fail with an error. Defaults to `false`.
[alternative backends]: ../for_developers/backends.md
## Environment Variables

View File

@@ -92,17 +92,17 @@ impl System for MySystem { ... }
Then in the book, all you have to do is:
````hbs
Here is a component:
```rust,no_run,noplaypen
```rust,no_run,noplayground
\{{#include file.rs:component}}
```
Here is a system:
```rust,no_run,noplaypen
```rust,no_run,noplayground
\{{#include file.rs:system}}
```
This is the full file.
```rust,no_run,noplaypen
```rust,no_run,noplayground
\{{#include file.rs:all}}
```
````
@@ -178,17 +178,17 @@ That is, it looks like this (click the "expand" icon to see the rest of the file
With the following syntax, you can insert runnable Rust files into your book:
```hbs
\{{#playpen file.rs}}
\{{#playground file.rs}}
```
The path to the Rust file has to be relative from the current source file.
When play is clicked, the code snippet will be sent to the [Rust Playpen] to be
When play is clicked, the code snippet will be sent to the [Rust Playground] to be
compiled and run. The result is sent back and displayed directly underneath the
code.
Here is what a rendered code snippet looks like:
{{#playpen example.rs}}
{{#playground example.rs}}
[Rust Playpen]: https://play.rust-lang.org/
[Rust Playground]: https://play.rust-lang.org/

View File

@@ -7,7 +7,7 @@ are. Without this file, there is no book.
Even though `SUMMARY.md` is a markdown file, the formatting is very strict to
allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
#### Allowed elements
#### Structure
1. ***Title*** It's common practice to begin with a title, generally <code
class="language-markdown"># Summary</code>. But it is not mandatory, the
@@ -22,17 +22,45 @@ allow for easy parsing. Let's see how you should format your `SUMMARY.md` file.
[Title of prefix element](relative/path/to/markdown.md)
```
3. ***Numbered Chapter*** Numbered chapters are the main content of the book,
3. ***Part Title:*** Headers can be used as a title for the following numbered
chapters. This can be used to logically separate different sections
of book. The title is rendered as unclickable text.
Titles are optional, and the numbered chapters can be broken into as many
parts as desired.
4. ***Numbered Chapter*** Numbered chapters are the main content of the book,
they will be numbered and can be nested, resulting in a nice hierarchy
(chapters, sub-chapters, etc.)
```markdown
# Title of Part
- [Title of the Chapter](relative/path/to/markdown.md)
# Title of Another Part
- [More Chapters](relative/path/to/markdown2.md)
```
You can either use `-` or `*` to indicate a numbered chapter.
4. ***Suffix Chapter*** After the numbered chapters you can add a couple of
5. ***Suffix Chapter*** After the numbered chapters you can add a couple of
non-numbered chapters. They are the same as prefix chapters but come after
the numbered chapters instead of before.
All other elements are unsupported and will be ignored at best or result in an
error.
#### Other elements
- ***Separators*** In between chapters you can add a separator. In the HTML renderer
this will result in a line being rendered in the table of contents. A separator is
a line containing exclusively dashes and at least three of them: `---`.
- ***Draft chapters*** Draft chapters are chapters without a file and thus content.
The purpose of a draft chapter is to signal future chapters still to be written.
Or when still laying out the structure of the book to avoid creating the files
while you are still changing the structure of the book a lot.
Draft chapters will be rendered in the HTML renderer as disabled links in the table
of contents, as you can see for the next chapter in the table of contents on the left.
Draft chapters are written like normal chapters but without writing the path to the file
```markdown
- [Draft chapter]()
```

View File

@@ -11,16 +11,19 @@ and now that file will be used instead of the default file.
Here are the files you can override:
- ***index.hbs*** is the handlebars template.
- ***book.css*** is the style used in the output. If you want to change the
- **_index.hbs_** is the handlebars template.
- **_head.hbs_** is appended to the HTML `<head>` section.
- **_header.hbs_** content is appended on top of every book page.
- **_book.css_** is the style used in the output. If you want to change the
design of your book, this is probably the file you want to modify. Sometimes
in conjunction with `index.hbs` when you want to radically change the layout.
- ***book.js*** is mostly used to add client side functionality, like hiding /
- **_book.js_** is mostly used to add client side functionality, like hiding /
un-hiding the sidebar, changing the theme, ...
- ***highlight.js*** is the JavaScript that is used to highlight code snippets,
you should not need to modify this.
- ***highlight.css*** is the theme used for the code highlighting
- ***favicon.png*** the favicon that will be used
- **_highlight.js_** is the JavaScript that is used to highlight code snippets,
you should not need to modify this.
- **_highlight.css_** is the theme used for the code highlighting.
- **_favicon.svg_** and **_favicon.png_** the favicon that will be used. The SVG
version is used by [newer browsers].
Generally, when you want to tweak the theme, you don't need to override all the
files. If you only need changes in the stylesheet, there is no point in
@@ -32,3 +35,10 @@ functionality. Therefore I recommend to use the file from the default theme as
template and only add / modify what you need. You can copy the default theme
into your source directory automatically by using `mdbook init --theme` just
remove the files you don't want to override.
If you completely replace all built-in themes, be sure to also set
[`output.html.preferred-dark-theme`] in the config, which defaults to the
built-in `navy` theme.
[`output.html.preferred-dark-theme`]: ../config.md#html-renderer-options
[newer browsers]: https://caniuse.com/#feat=link-icon-svg

View File

@@ -1,11 +1,11 @@
# Editor
In addition to providing runnable code playpens, mdBook optionally allows them
In addition to providing runnable code playgrounds, mdBook optionally allows them
to be editable. In order to enable editable code blocks, the following needs to
be added to the ***book.toml***:
```toml
[output.html.playpen]
[output.html.playground]
editable = true
```
@@ -19,7 +19,7 @@ fn main() {
}
```</code></pre>
The above will result in this editable playpen:
The above will result in this editable playground:
```rust,editable
fn main() {
@@ -28,7 +28,7 @@ fn main() {
}
```
Note the new `Undo Changes` button in the editable playpens.
Note the new `Undo Changes` button in the editable playgrounds.
## Customizing the Editor
@@ -36,7 +36,7 @@ By default, the editor is the [Ace](https://ace.c9.io/) editor, but, if desired,
the functionality may be overriden by providing a different folder:
```toml
[output.html.playpen]
[output.html.playground]
editable = true
editor = "/path/to/editor"
```

View File

@@ -97,4 +97,4 @@ Of course the inner html can be changed to your liking.
------
*If you would like other properties or helpers exposed, please [create a new
issue](https://github.com/rust-lang-nursery/mdBook/issues)*
issue](https://github.com/rust-lang/mdBook/issues)*

View File

@@ -22,7 +22,7 @@ overridden with your own.
If you want to use another theme for `highlight.js` download it from their
website, or make it yourself, rename it to `highlight.css` and put it in
`src/theme` (or the equivalent if you changed your source folder)
the `theme` folder of your book.
Now your theme will be used instead of the default theme.
@@ -62,7 +62,7 @@ everyone can benefit from it.**
If you think the default theme doesn't look quite right for a specific language,
or could be improved. Feel free to [submit a new
issue](https://github.com/rust-lang-nursery/mdBook/issues) explaining what you
issue](https://github.com/rust-lang/mdBook/issues) explaining what you
have in mind and I will take a look at it.
You could also create a pull-request with the proposed improvements.

View File

@@ -1,14 +0,0 @@
#!/usr/bin/env bash
# Updates gh-pages with latest docs.
set -ex
cargo run -- build book-example
cd book-example/book
touch .nojekyll
git init
git config --local user.email ""
git config --local user.name "GitHub Deployer"
git add .
git commit -m "Deploy to gh-pages"
remote="https://${GITHUB_ACTOR}:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git push "$remote" HEAD:gh-pages --force

View File

@@ -1,26 +0,0 @@
#!/usr/bin/env bash
# Install/update rustup.
# The first argument should be the toolchain to install.
#
# It is helpful to have this as a separate script due to some issues on
# Windows where immediately after `rustup self update`, rustup can fail with
# "Device or resource busy".
set -ex
if [ -z "$1" ]
then
echo "First parameter must be toolchain to install."
exit 1
fi
TOOLCHAIN="$1"
# Install/update rustup.
if command -v rustup
then
echo `command -v rustup` `rustup -V` already installed
rustup self update
else
# macOS currently does not have rust pre-installed.
curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $TOOLCHAIN --profile=minimal
echo "##[add-path]$HOME/.cargo/bin"
fi

View File

@@ -87,7 +87,7 @@ mod nop_lib {
// particular config value
if let Some(nop_cfg) = ctx.config.get_preprocessor(self.name()) {
if nop_cfg.contains_key("blow-up") {
return Err("Boom!!1!".into());
anyhow::bail!("Boom!!1!");
}
}

View File

@@ -15,13 +15,13 @@ pub fn load_book<P: AsRef<Path>>(src_dir: P, cfg: &BuildConfig) -> Result<Book>
let mut summary_content = String::new();
File::open(summary_md)
.chain_err(|| "Couldn't open SUMMARY.md")?
.with_context(|| "Couldn't open SUMMARY.md")?
.read_to_string(&mut summary_content)?;
let summary = parse_summary(&summary_content).chain_err(|| "Summary parsing failed")?;
let summary = parse_summary(&summary_content).with_context(|| "Summary parsing failed")?;
if cfg.create_missing {
create_missing(&src_dir, &summary).chain_err(|| "Unable to create missing chapters")?;
create_missing(&src_dir, &summary).with_context(|| "Unable to create missing chapters")?;
}
load_book_from_disk(&summary, src_dir)
@@ -39,17 +39,19 @@ fn create_missing(src_dir: &Path, summary: &Summary) -> Result<()> {
let next = items.pop().expect("already checked");
if let SummaryItem::Link(ref link) = *next {
let filename = src_dir.join(&link.location);
if !filename.exists() {
if let Some(parent) = filename.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
if let Some(ref location) = link.location {
let filename = src_dir.join(location);
if !filename.exists() {
if let Some(parent) = filename.parent() {
if !parent.exists() {
fs::create_dir_all(parent)?;
}
}
}
debug!("Creating missing file {}", filename.display());
debug!("Creating missing file {}", filename.display());
let mut f = File::create(&filename)?;
writeln!(f, "# {}", link.name)?;
let mut f = File::create(&filename)?;
writeln!(f, "# {}", link.name)?;
}
}
items.extend(&link.nested_items);
@@ -131,6 +133,8 @@ pub enum BookItem {
Chapter(Chapter),
/// A section separator.
Separator,
/// A part title.
PartTitle(String),
}
impl From<Chapter> for BookItem {
@@ -152,7 +156,7 @@ pub struct Chapter {
/// Nested items.
pub sub_items: Vec<BookItem>,
/// The chapter's location, relative to the `SUMMARY.md` file.
pub path: PathBuf,
pub path: Option<PathBuf>,
/// An ordered list of the names of each chapter above this one, in the hierarchy.
pub parent_names: Vec<String>,
}
@@ -168,11 +172,31 @@ impl Chapter {
Chapter {
name: name.to_string(),
content,
path: path.into(),
path: Some(path.into()),
parent_names,
..Default::default()
}
}
/// Create a new draft chapter that is not attached to a source markdown file and has
/// thus no content.
pub fn new_draft(name: &str, parent_names: Vec<String>) -> Self {
Chapter {
name: name.to_string(),
content: String::new(),
path: None,
parent_names,
..Default::default()
}
}
/// Check if the chapter is a draft chapter, meaning it has no path to a source markdown file
pub fn is_draft_chapter(&self) -> bool {
match self.path {
Some(_) => false,
None => true,
}
}
}
/// Use the provided `Summary` to load a `Book` from disk.
@@ -202,16 +226,17 @@ pub(crate) fn load_book_from_disk<P: AsRef<Path>>(summary: &Summary, src_dir: P)
})
}
fn load_summary_item<P: AsRef<Path>>(
fn load_summary_item<P: AsRef<Path> + Clone>(
item: &SummaryItem,
src_dir: P,
parent_names: Vec<String>,
) -> Result<BookItem> {
match *item {
match item {
SummaryItem::Separator => Ok(BookItem::Separator),
SummaryItem::Link(ref link) => {
load_chapter(link, src_dir, parent_names).map(BookItem::Chapter)
}
SummaryItem::PartTitle(title) => Ok(BookItem::PartTitle(title.clone())),
}
}
@@ -220,28 +245,36 @@ fn load_chapter<P: AsRef<Path>>(
src_dir: P,
parent_names: Vec<String>,
) -> Result<Chapter> {
debug!("Loading {} ({})", link.name, link.location.display());
let src_dir = src_dir.as_ref();
let location = if link.location.is_absolute() {
link.location.clone()
let mut ch = if let Some(ref link_location) = link.location {
debug!("Loading {} ({})", link.name, link_location.display());
let location = if link_location.is_absolute() {
link_location.clone()
} else {
src_dir.join(link_location)
};
let mut f = File::open(&location)
.with_context(|| format!("Chapter file not found, {}", link_location.display()))?;
let mut content = String::new();
f.read_to_string(&mut content).with_context(|| {
format!("Unable to read \"{}\" ({})", link.name, location.display())
})?;
let stripped = location
.strip_prefix(&src_dir)
.expect("Chapters are always inside a book");
Chapter::new(&link.name, content, stripped, parent_names.clone())
} else {
src_dir.join(&link.location)
Chapter::new_draft(&link.name, parent_names.clone())
};
let mut f = File::open(&location)
.chain_err(|| format!("Chapter file not found, {}", link.location.display()))?;
let mut content = String::new();
f.read_to_string(&mut content)
.chain_err(|| format!("Unable to read \"{}\" ({})", link.name, location.display()))?;
let stripped = location
.strip_prefix(&src_dir)
.expect("Chapters are always inside a book");
let mut sub_item_parents = parent_names.clone();
let mut ch = Chapter::new(&link.name, content, stripped, parent_names);
ch.number = link.number.clone();
sub_item_parents.push(link.name.clone());
@@ -376,7 +409,7 @@ And here is some \
name: String::from("Nested Chapter 1"),
content: String::from("Hello World!"),
number: Some(SectionNumber(vec![1, 2])),
path: PathBuf::from("second.md"),
path: Some(PathBuf::from("second.md")),
parent_names: vec![String::from("Chapter 1")],
sub_items: Vec::new(),
};
@@ -384,7 +417,7 @@ And here is some \
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
number: None,
path: PathBuf::from("chapter_1.md"),
path: Some(PathBuf::from("chapter_1.md")),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(nested.clone()),
@@ -408,7 +441,7 @@ And here is some \
sections: vec![BookItem::Chapter(Chapter {
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
path: PathBuf::from("chapter_1.md"),
path: Some(PathBuf::from("chapter_1.md")),
..Default::default()
})],
..Default::default()
@@ -448,7 +481,7 @@ And here is some \
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
number: None,
path: PathBuf::from("Chapter_1/index.md"),
path: Some(PathBuf::from("Chapter_1/index.md")),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(Chapter::new(
@@ -500,7 +533,7 @@ And here is some \
name: String::from("Chapter 1"),
content: String::from(DUMMY_SRC),
number: None,
path: PathBuf::from("Chapter_1/index.md"),
path: Some(PathBuf::from("Chapter_1/index.md")),
parent_names: Vec::new(),
sub_items: vec![
BookItem::Chapter(Chapter::new(
@@ -537,9 +570,10 @@ And here is some \
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("Empty"),
location: PathBuf::from(""),
location: Some(PathBuf::from("")),
..Default::default()
})],
..Default::default()
};
@@ -556,7 +590,7 @@ And here is some \
let summary = Summary {
numbered_chapters: vec![SummaryItem::Link(Link {
name: String::from("nested"),
location: dir,
location: Some(dir),
..Default::default()
})],
..Default::default()

View File

@@ -64,19 +64,19 @@ impl BookBuilder {
info!("Creating a new book with stub content");
self.create_directory_structure()
.chain_err(|| "Unable to create directory structure")?;
.with_context(|| "Unable to create directory structure")?;
self.create_stub_files()
.chain_err(|| "Unable to create stub files")?;
.with_context(|| "Unable to create stub files")?;
if self.create_gitignore {
self.build_gitignore()
.chain_err(|| "Unable to create .gitignore")?;
.with_context(|| "Unable to create .gitignore")?;
}
if self.copy_theme {
self.copy_across_theme()
.chain_err(|| "Unable to copy across the theme")?;
.with_context(|| "Unable to copy across the theme")?;
}
self.write_book_toml()?;
@@ -97,12 +97,12 @@ impl BookBuilder {
fn write_book_toml(&self) -> Result<()> {
debug!("Writing book.toml");
let book_toml = self.root.join("book.toml");
let cfg = toml::to_vec(&self.config).chain_err(|| "Unable to serialize the config")?;
let cfg = toml::to_vec(&self.config).with_context(|| "Unable to serialize the config")?;
File::create(book_toml)
.chain_err(|| "Couldn't create book.toml")?
.with_context(|| "Couldn't create book.toml")?
.write_all(&cfg)
.chain_err(|| "Unable to write config to book.toml")?;
.with_context(|| "Unable to write config to book.toml")?;
Ok(())
}
@@ -143,7 +143,10 @@ impl BookBuilder {
variables_css.write_all(theme::VARIABLES_CSS)?;
let mut favicon = File::create(themedir.join("favicon.png"))?;
favicon.write_all(theme::FAVICON)?;
favicon.write_all(theme::FAVICON_PNG)?;
let mut favicon = File::create(themedir.join("favicon.svg"))?;
favicon.write_all(theme::FAVICON_SVG)?;
let mut js = File::create(themedir.join("book.js"))?;
js.write_all(theme::JS)?;
@@ -174,13 +177,14 @@ impl BookBuilder {
let summary = src_dir.join("SUMMARY.md");
if !summary.exists() {
trace!("No summary found creating stub summary and chapter_1.md.");
let mut f = File::create(&summary).chain_err(|| "Unable to create SUMMARY.md")?;
let mut f = File::create(&summary).with_context(|| "Unable to create SUMMARY.md")?;
writeln!(f, "# Summary")?;
writeln!(f)?;
writeln!(f, "- [Chapter 1](./chapter_1.md)")?;
let chapter_1 = src_dir.join("chapter_1.md");
let mut f = File::create(&chapter_1).chain_err(|| "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.");

View File

@@ -5,6 +5,7 @@
//!
//! [1]: ../index.html
#[allow(clippy::module_inception)]
mod book;
mod init;
mod summary;
@@ -27,7 +28,7 @@ use crate::preprocess::{
use crate::renderer::{CmdRenderer, HtmlHandlebars, MarkdownRenderer, RenderContext, Renderer};
use crate::utils;
use crate::config::Config;
use crate::config::{Config, RustEdition};
/// The object used to manage and build a book.
pub struct MDBook {
@@ -56,7 +57,7 @@ impl MDBook {
warn!("This format is no longer used, so you should migrate to the");
warn!("book.toml format.");
warn!("Check the user guide for migration information:");
warn!("\thttps://rust-lang-nursery.github.io/mdBook/format/config.html");
warn!("\thttps://rust-lang.github.io/mdBook/format/config.html");
}
let mut config = if config_location.exists() {
@@ -126,13 +127,12 @@ impl MDBook {
/// ```no_run
/// # use mdbook::MDBook;
/// # use mdbook::book::BookItem;
/// # #[allow(unused_variables)]
/// # fn main() {
/// # let book = MDBook::load("mybook").unwrap();
/// for item in book.iter() {
/// match *item {
/// BookItem::Chapter(ref chapter) => {},
/// BookItem::Separator => {},
/// BookItem::PartTitle(ref title) => {}
/// }
/// }
///
@@ -143,7 +143,6 @@ impl MDBook {
/// // 2. Chapter 2
/// //
/// // etc.
/// # }
/// ```
pub fn iter(&self) -> BookItems<'_> {
self.book.iter()
@@ -182,7 +181,7 @@ impl MDBook {
}
/// Run the entire build process for a particular `Renderer`.
fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
pub fn execute_build_process(&self, renderer: &dyn Renderer) -> Result<()> {
let mut preprocessed_book = self.book.clone();
let preprocess_ctx = PreprocessorContext::new(
self.root.clone(),
@@ -216,7 +215,7 @@ impl MDBook {
renderer
.render(&render_context)
.chain_err(|| "Rendering failed")
.with_context(|| "Rendering failed")
}
/// You can change the default renderer to another one by using this method.
@@ -253,28 +252,43 @@ impl MDBook {
for item in book.iter() {
if let BookItem::Chapter(ref ch) = *item {
if !ch.path.as_os_str().is_empty() {
let path = self.source_dir().join(&ch.path);
info!("Testing file: {:?}", path);
let chapter_path = match ch.path {
Some(ref path) if !path.as_os_str().is_empty() => path,
_ => continue,
};
// write preprocessed file to tempdir
let path = temp_dir.path().join(&ch.path);
let mut tmpf = utils::fs::create_file(&path)?;
tmpf.write_all(ch.content.as_bytes())?;
let path = self.source_dir().join(&chapter_path);
info!("Testing file: {:?}", path);
let output = Command::new("rustdoc")
.arg(&path)
.arg("--test")
.args(&library_args)
.output()?;
// write preprocessed file to tempdir
let path = temp_dir.path().join(&chapter_path);
let mut tmpf = utils::fs::create_file(&path)?;
tmpf.write_all(ch.content.as_bytes())?;
if !output.status.success() {
bail!(ErrorKind::Subprocess(
"Rustdoc returned an error".to_string(),
output
));
let mut cmd = Command::new("rustdoc");
cmd.arg(&path).arg("--test").args(&library_args);
if let Some(edition) = self.config.rust.edition {
match edition {
RustEdition::E2015 => {
cmd.args(&["--edition", "2015"]);
}
RustEdition::E2018 => {
cmd.args(&["--edition", "2018"]);
}
}
}
let output = cmd.output()?;
if !output.status.success() {
bail!(
"rustdoc returned an error:\n\
\n--- stdout\n{}\n--- stderr\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
}
}
Ok(())
@@ -395,7 +409,7 @@ fn interpret_custom_preprocessor(key: &str, table: &Value) -> Box<CmdPreprocesso
.map(ToString::to_string)
.unwrap_or_else(|| format!("mdbook-{}", key));
Box::new(CmdPreprocessor::new(key.to_string(), command.to_string()))
Box::new(CmdPreprocessor::new(key.to_string(), command))
}
fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
@@ -408,7 +422,7 @@ fn interpret_custom_renderer(key: &str, table: &Value) -> Box<CmdRenderer> {
let command = table_dot_command.unwrap_or_else(|| format!("mdbook-{}", key));
Box::new(CmdRenderer::new(key.to_string(), command.to_string()))
Box::new(CmdRenderer::new(key.to_string(), command))
}
/// Check whether we should run a particular `Preprocessor` in combination

View File

@@ -25,12 +25,17 @@ use std::path::{Path, PathBuf};
/// [Title of prefix element](relative/path/to/markdown.md)
/// ```
///
/// **Part Title:** An optional title for the next collect of numbered chapters. The numbered
/// chapters can be broken into as many parts as desired.
///
/// **Numbered Chapter:** Numbered chapters are the main content of the book,
/// they
/// will be numbered and can be nested, resulting in a nice hierarchy (chapters,
/// sub-chapters, etc.)
///
/// ```markdown
/// # Title of Part
///
/// - [Title of the Chapter](relative/path/to/markdown.md)
/// ```
///
@@ -55,7 +60,7 @@ pub struct Summary {
pub title: Option<String>,
/// Chapters before the main text (e.g. an introduction).
pub prefix_chapters: Vec<SummaryItem>,
/// The main chapters in the document.
/// The main numbered chapters of the book, broken into one or more possibly named parts.
pub numbered_chapters: Vec<SummaryItem>,
/// Items which come after the main document (e.g. a conclusion).
pub suffix_chapters: Vec<SummaryItem>,
@@ -71,7 +76,7 @@ pub struct Link {
pub name: String,
/// The location of the chapter's source file, taking the book's `src`
/// directory as the root.
pub location: PathBuf,
pub location: Option<PathBuf>,
/// The section number, if this chapter is in the numbered section.
pub number: Option<SectionNumber>,
/// Any nested items this chapter may contain.
@@ -83,7 +88,7 @@ impl Link {
pub fn new<S: Into<String>, P: AsRef<Path>>(name: S, location: P) -> Link {
Link {
name: name.into(),
location: location.as_ref().to_path_buf(),
location: Some(location.as_ref().to_path_buf()),
number: None,
nested_items: Vec::new(),
}
@@ -94,7 +99,7 @@ impl Default for Link {
fn default() -> Self {
Link {
name: String::new(),
location: PathBuf::new(),
location: Some(PathBuf::new()),
number: None,
nested_items: Vec::new(),
}
@@ -108,6 +113,8 @@ pub enum SummaryItem {
Link(Link),
/// A separator (`---`).
Separator,
/// A part title.
PartTitle(String),
}
impl SummaryItem {
@@ -134,12 +141,13 @@ impl From<Link> for SummaryItem {
///
/// ```text
/// summary ::= title prefix_chapters numbered_chapters
/// suffix_chapters
/// suffix_chapters
/// title ::= "# " TEXT
/// | EPSILON
/// prefix_chapters ::= item*
/// suffix_chapters ::= item*
/// numbered_chapters ::= dotted_item+
/// numbered_chapters ::= part+
/// part ::= title dotted_item+
/// dotted_item ::= INDENT* DOT_POINT item
/// item ::= link
/// | separator
@@ -153,7 +161,12 @@ impl From<Link> for SummaryItem {
/// > match the following regex: "[^<>\n[]]+".
struct SummaryParser<'a> {
src: &'a str,
stream: pulldown_cmark::Parser<'a>,
stream: pulldown_cmark::OffsetIter<'a>,
offset: usize,
/// We can't actually put an event back into the `OffsetIter` stream, so instead we store it
/// here until somebody calls `next_event` again.
back: Option<Event<'a>>,
}
/// Reads `Events` from the provided stream until the corresponding
@@ -174,7 +187,7 @@ macro_rules! collect_events {
let mut events = Vec::new();
loop {
let event = $stream.next();
let event = $stream.next().map(|(ev, _range)| ev);
trace!("Next event: {:?}", event);
match event {
@@ -196,23 +209,23 @@ macro_rules! collect_events {
impl<'a> SummaryParser<'a> {
fn new(text: &str) -> SummaryParser<'_> {
let pulldown_parser = pulldown_cmark::Parser::new(text);
let pulldown_parser = pulldown_cmark::Parser::new(text).into_offset_iter();
SummaryParser {
src: text,
stream: pulldown_parser,
offset: 0,
back: None,
}
}
/// Get the current line and column to give the user more useful error
/// messages.
fn current_location(&self) -> (usize, usize) {
let byte_offset = self.stream.get_offset();
let previous_text = self.src[..byte_offset].as_bytes();
let previous_text = self.src[..self.offset].as_bytes();
let line = Memchr::new(b'\n', previous_text).count() + 1;
let start_of_line = memchr::memrchr(b'\n', previous_text).unwrap_or(0);
let col = self.src[start_of_line..byte_offset].chars().count();
let col = self.src[start_of_line..self.offset].chars().count();
(line, col)
}
@@ -223,13 +236,13 @@ impl<'a> SummaryParser<'a> {
let prefix_chapters = self
.parse_affix(true)
.chain_err(|| "There was an error parsing the prefix chapters")?;
.with_context(|| "There was an error parsing the prefix chapters")?;
let numbered_chapters = self
.parse_numbered()
.chain_err(|| "There was an error parsing the numbered chapters")?;
.parse_parts()
.with_context(|| "There was an error parsing the numbered chapters")?;
let suffix_chapters = self
.parse_affix(false)
.chain_err(|| "There was an error parsing the suffix chapters")?;
.with_context(|| "There was an error parsing the suffix chapters")?;
Ok(Summary {
title,
@@ -239,8 +252,7 @@ impl<'a> SummaryParser<'a> {
})
}
/// Parse the affix chapters. This expects the first event (start of
/// paragraph) to have already been consumed by the previous parser.
/// Parse the affix chapters.
fn parse_affix(&mut self, is_prefix: bool) -> Result<Vec<SummaryItem>> {
let mut items = Vec::new();
debug!(
@@ -250,20 +262,22 @@ impl<'a> SummaryParser<'a> {
loop {
match self.next_event() {
Some(Event::Start(Tag::List(..))) => {
Some(ev @ Event::Start(Tag::List(..)))
| Some(ev @ Event::Start(Tag::Heading(1))) => {
if is_prefix {
// we've finished prefix chapters and are at the start
// of the numbered section.
self.back(ev);
break;
} else {
bail!(self.parse_error("Suffix chapters cannot be followed by a list"));
}
}
Some(Event::Start(Tag::Link(_type, href, _title))) => {
let link = self.parse_link(href.to_string())?;
let link = self.parse_link(href.to_string());
items.push(SummaryItem::Link(link));
}
Some(Event::Start(Tag::Rule)) => items.push(SummaryItem::Separator),
Some(Event::Rule) => items.push(SummaryItem::Separator),
Some(_) => {}
None => break,
}
@@ -272,56 +286,111 @@ impl<'a> SummaryParser<'a> {
Ok(items)
}
fn parse_link(&mut self, href: String) -> Result<Link> {
fn parse_parts(&mut self) -> Result<Vec<SummaryItem>> {
let mut parts = vec![];
// We want the section numbers to be continues through all parts.
let mut root_number = SectionNumber::default();
let mut root_items = 0;
loop {
// Possibly match a title or the end of the "numbered chapters part".
let title = match self.next_event() {
Some(ev @ Event::Start(Tag::Paragraph)) => {
// we're starting the suffix chapters
self.back(ev);
break;
}
Some(Event::Start(Tag::Heading(1))) => {
debug!("Found a h1 in the SUMMARY");
let tags = collect_events!(self.stream, end Tag::Heading(1));
Some(stringify_events(tags))
}
Some(ev) => {
self.back(ev);
None
}
None => break, // EOF, bail...
};
// Parse the rest of the part.
let numbered_chapters = self
.parse_numbered(&mut root_items, &mut root_number)
.with_context(|| "There was an error parsing the numbered chapters")?;
if let Some(title) = title {
parts.push(SummaryItem::PartTitle(title));
}
parts.extend(numbered_chapters);
}
Ok(parts)
}
/// Finishes parsing a link once the `Event::Start(Tag::Link(..))` has been opened.
fn parse_link(&mut self, href: String) -> Link {
let link_content = collect_events!(self.stream, end Tag::Link(..));
let name = stringify_events(link_content);
if href.is_empty() {
Err(self.parse_error("You can't have an empty link."))
let path = if href.is_empty() {
None
} else {
Ok(Link {
name,
location: PathBuf::from(href.to_string()),
number: None,
nested_items: Vec::new(),
})
Some(PathBuf::from(href))
};
Link {
name,
location: path,
number: None,
nested_items: Vec::new(),
}
}
/// Parse the numbered chapters. This assumes the opening list tag has
/// already been consumed by a previous parser.
fn parse_numbered(&mut self) -> Result<Vec<SummaryItem>> {
/// Parse the numbered chapters.
fn parse_numbered(
&mut self,
root_items: &mut u32,
root_number: &mut SectionNumber,
) -> Result<Vec<SummaryItem>> {
let mut items = Vec::new();
let mut root_items = 0;
let root_number = SectionNumber::default();
// we need to do this funny loop-match-if-let dance because a rule will
// close off any currently running list. Therefore we try to read the
// list items before the rule, then if we encounter a rule we'll add a
// separator and try to resume parsing numbered chapters if we start a
// list immediately afterwards.
//
// If you can think of a better way to do this then please make a PR :)
// For the first iteration, we want to just skip any opening paragraph tags, as that just
// marks the start of the list. But after that, another opening paragraph indicates that we
// have started a new part or the suffix chapters.
let mut first = true;
loop {
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?;
// if we've resumed after something like a rule the root sections
// will be numbered from 1. We need to manually go back and update
// them
update_section_numbers(&mut bunch_of_items, 0, root_items);
root_items += bunch_of_items.len() as u32;
items.extend(bunch_of_items);
match self.next_event() {
Some(Event::Start(Tag::Paragraph)) => {
// we're starting the suffix chapters
Some(ev @ Event::Start(Tag::Paragraph)) => {
if !first {
// we're starting the suffix chapters
self.back(ev);
break;
}
}
// The expectation is that pulldown cmark will terminate a paragraph before a new
// heading, so we can always count on this to return without skipping headings.
Some(ev @ Event::Start(Tag::Heading(1))) => {
// we're starting a new part
self.back(ev);
break;
}
Some(ev @ Event::Start(Tag::List(..))) => {
self.back(ev);
let mut bunch_of_items = self.parse_nested_numbered(&root_number)?;
// if we've resumed after something like a rule the root sections
// will be numbered from 1. We need to manually go back and update
// them
update_section_numbers(&mut bunch_of_items, 0, *root_items);
*root_items += bunch_of_items.len() as u32;
items.extend(bunch_of_items);
}
Some(Event::Start(other_tag)) => {
if other_tag == Tag::Rule {
items.push(SummaryItem::Separator);
}
trace!("Skipping contents of {:?}", other_tag);
// Skip over the contents of this tag
@@ -330,29 +399,42 @@ impl<'a> SummaryParser<'a> {
break;
}
}
}
Some(Event::Rule) => {
items.push(SummaryItem::Separator);
}
if let Some(Event::Start(Tag::List(..))) = self.next_event() {
continue;
} else {
break;
}
}
Some(_) => {
// something else... ignore
continue;
}
// something else... ignore
Some(_) => {}
// EOF, bail...
None => {
// EOF, bail...
break;
}
}
// From now on, we cannot accept any new paragraph opening tags.
first = false;
}
Ok(items)
}
/// Push an event back to the tail of the stream.
fn back(&mut self, ev: Event<'a>) {
assert!(self.back.is_none());
trace!("Back: {:?}", ev);
self.back = Some(ev);
}
fn next_event(&mut self) -> Option<Event<'a>> {
let next = self.stream.next();
let next = self.back.take().or_else(|| {
self.stream.next().map(|(ev, range)| {
self.offset = range.start;
ev
})
});
trace!("Next event: {:?}", next);
next
@@ -369,6 +451,10 @@ impl<'a> SummaryParser<'a> {
items.push(item);
}
Some(Event::Start(Tag::List(..))) => {
// Skip this tag after comment bacause it is not nested.
if items.is_empty() {
continue;
}
// recurse to parse the nested list
let (_, last_item) = get_last_link(&mut items)?;
let last_item_number = last_item
@@ -398,7 +484,7 @@ impl<'a> SummaryParser<'a> {
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())?;
let mut link = self.parse_link(href.to_string());
let mut number = parent.clone();
number.0.push(num_existing_items as u32 + 1);
@@ -406,7 +492,10 @@ impl<'a> SummaryParser<'a> {
"Found chapter: {} {} ({})",
number,
link.name,
link.location.display()
link.location
.as_ref()
.map(|p| p.to_str().unwrap_or(""))
.unwrap_or("[draft]")
);
link.number = Some(number);
@@ -425,19 +514,24 @@ impl<'a> SummaryParser<'a> {
fn parse_error<D: Display>(&self, msg: D) -> Error {
let (line, col) = self.current_location();
ErrorKind::ParseError(line, col, msg.to_string()).into()
anyhow::anyhow!(
"failed to parse SUMMARY.md line {}, column {}: {}",
line,
col,
msg
)
}
/// Try to parse the title line.
fn parse_title(&mut self) -> Option<String> {
if let Some(Event::Start(Tag::Header(1))) = self.next_event() {
debug!("Found a h1 in the SUMMARY");
match self.next_event() {
Some(Event::Start(Tag::Heading(1))) => {
debug!("Found a h1 in the SUMMARY");
let tags = collect_events!(self.stream, end Tag::Header(1));
Some(stringify_events(tags))
} else {
None
let tags = collect_events!(self.stream, end Tag::Heading(1));
Some(stringify_events(tags))
}
_ => None,
}
}
}
@@ -463,10 +557,9 @@ fn get_last_link(links: &mut [SummaryItem]) -> Result<(usize, &mut Link)> {
.filter_map(|(i, item)| item.maybe_link_mut().map(|l| (i, l)))
.rev()
.next()
.ok_or_else(|| {
"Unable to get last link because the list of SummaryItems doesn't contain any Links"
.into()
})
.ok_or_else(||
anyhow::anyhow!("Unable to get last link because the list of SummaryItems doesn't contain any Links")
)
}
/// Removes the styling from a list of Markdown events and returns just the
@@ -577,17 +670,16 @@ mod tests {
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
location: Some(PathBuf::from("./first.md")),
..Default::default()
}),
SummaryItem::Link(Link {
name: String::from("Second"),
location: PathBuf::from("./second.md"),
location: Some(PathBuf::from("./second.md")),
..Default::default()
}),
];
let _ = parser.stream.next(); // step past first event
let got = parser.parse_affix(true).unwrap();
assert_eq!(got, should_be);
@@ -598,7 +690,6 @@ mod tests {
let src = "[First](./first.md)\n\n---\n\n[Second](./second.md)\n";
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // step past first event
let got = parser.parse_affix(true).unwrap();
assert_eq!(got.len(), 3);
@@ -610,7 +701,6 @@ mod tests {
let src = "[First](./first.md)\n- [Second](./second.md)\n";
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // step past first event
let got = parser.parse_affix(false);
assert!(got.is_err());
@@ -621,19 +711,19 @@ mod tests {
let src = "[First](./first.md)";
let should_be = Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
location: Some(PathBuf::from("./first.md")),
..Default::default()
};
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next(); // skip past start of paragraph
let _ = parser.stream.next(); // Discard opening paragraph
let href = match parser.stream.next() {
Some(Event::Start(Tag::Link(_type, href, _title))) => href.to_string(),
Some((Event::Start(Tag::Link(_type, href, _title)), _range)) => href.to_string(),
other => panic!("Unreachable, {:?}", other),
};
let got = parser.parse_link(href).unwrap();
let got = parser.parse_link(href);
assert_eq!(got, should_be);
}
@@ -642,16 +732,16 @@ mod tests {
let src = "- [First](./first.md)\n";
let link = Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])),
..Default::default()
};
let should_be = vec![SummaryItem::Link(link)];
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next();
let got = parser.parse_numbered().unwrap();
let got = parser
.parse_numbered(&mut 0, &mut SectionNumber::default())
.unwrap();
assert_eq!(got, should_be);
}
@@ -663,27 +753,92 @@ mod tests {
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])),
nested_items: vec![SummaryItem::Link(Link {
name: String::from("Nested"),
location: PathBuf::from("./nested.md"),
location: Some(PathBuf::from("./nested.md")),
number: Some(SectionNumber(vec![1, 1])),
nested_items: Vec::new(),
})],
}),
SummaryItem::Link(Link {
name: String::from("Second"),
location: PathBuf::from("./second.md"),
location: Some(PathBuf::from("./second.md")),
number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(),
}),
];
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next();
let got = parser
.parse_numbered(&mut 0, &mut SectionNumber::default())
.unwrap();
let got = parser.parse_numbered().unwrap();
assert_eq!(got, should_be);
}
#[test]
fn parse_numbered_chapters_separated_by_comment() {
let src = "- [First](./first.md)\n<!-- this is a comment -->\n- [Second](./second.md)";
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(),
}),
SummaryItem::Link(Link {
name: String::from("Second"),
location: Some(PathBuf::from("./second.md")),
number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(),
}),
];
let mut parser = SummaryParser::new(src);
let got = parser
.parse_numbered(&mut 0, &mut SectionNumber::default())
.unwrap();
assert_eq!(got, should_be);
}
#[test]
fn parse_titled_parts() {
let src = "- [First](./first.md)\n- [Second](./second.md)\n\
# Title 2\n- [Third](./third.md)\n\t- [Fourth](./fourth.md)";
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(),
}),
SummaryItem::Link(Link {
name: String::from("Second"),
location: Some(PathBuf::from("./second.md")),
number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(),
}),
SummaryItem::PartTitle(String::from("Title 2")),
SummaryItem::Link(Link {
name: String::from("Third"),
location: Some(PathBuf::from("./third.md")),
number: Some(SectionNumber(vec![3])),
nested_items: vec![SummaryItem::Link(Link {
name: String::from("Fourth"),
location: Some(PathBuf::from("./fourth.md")),
number: Some(SectionNumber(vec![3, 1])),
nested_items: Vec::new(),
})],
}),
];
let mut parser = SummaryParser::new(src);
let got = parser.parse_parts().unwrap();
assert_eq!(got, should_be);
}
@@ -698,37 +853,44 @@ mod tests {
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(),
}),
SummaryItem::Link(Link {
name: String::from("Second"),
location: PathBuf::from("./second.md"),
location: Some(PathBuf::from("./second.md")),
number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(),
}),
];
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next();
let got = parser.parse_numbered().unwrap();
let got = parser
.parse_numbered(&mut 0, &mut SectionNumber::default())
.unwrap();
assert_eq!(got, should_be);
}
#[test]
fn an_empty_link_location_is_an_error() {
fn an_empty_link_location_is_a_draft_chapter() {
let src = "- [Empty]()\n";
let mut parser = SummaryParser::new(src);
parser.stream.next();
let got = parser.parse_numbered();
assert!(got.is_err());
let got = parser.parse_numbered(&mut 0, &mut SectionNumber::default());
let should_be = vec![SummaryItem::Link(Link {
name: String::from("Empty"),
location: None,
number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(),
})];
assert!(got.is_ok());
assert_eq!(got.unwrap(), should_be);
}
/// Regression test for https://github.com/rust-lang-nursery/mdBook/issues/779
/// Regression test for https://github.com/rust-lang/mdBook/issues/779
/// Ensure section numbers are correctly incremented after a horizontal separator.
#[test]
fn keep_numbering_after_separator() {
@@ -737,30 +899,30 @@ mod tests {
let should_be = vec![
SummaryItem::Link(Link {
name: String::from("First"),
location: PathBuf::from("./first.md"),
location: Some(PathBuf::from("./first.md")),
number: Some(SectionNumber(vec![1])),
nested_items: Vec::new(),
}),
SummaryItem::Separator,
SummaryItem::Link(Link {
name: String::from("Second"),
location: PathBuf::from("./second.md"),
location: Some(PathBuf::from("./second.md")),
number: Some(SectionNumber(vec![2])),
nested_items: Vec::new(),
}),
SummaryItem::Separator,
SummaryItem::Link(Link {
name: String::from("Third"),
location: PathBuf::from("./third.md"),
location: Some(PathBuf::from("./third.md")),
number: Some(SectionNumber(vec![3])),
nested_items: Vec::new(),
}),
];
let mut parser = SummaryParser::new(src);
let _ = parser.stream.next();
let got = parser.parse_numbered().unwrap();
let got = parser
.parse_numbered(&mut 0, &mut SectionNumber::default())
.unwrap();
assert_eq!(got, should_be);
}

View File

@@ -1,6 +1,6 @@
use crate::get_book_dir;
use anyhow::Context;
use clap::{App, ArgMatches, SubCommand};
use mdbook::errors::*;
use mdbook::MDBook;
use std::fs;
@@ -31,7 +31,8 @@ pub fn execute(args: &ArgMatches) -> mdbook::errors::Result<()> {
};
if dir_to_remove.exists() {
fs::remove_dir_all(&dir_to_remove).chain_err(|| "Unable to remove the build directory")?;
fs::remove_dir_all(&dir_to_remove)
.with_context(|| "Unable to remove the build directory")?;
}
Ok(())

View File

@@ -2,12 +2,20 @@
use super::watch;
use crate::{get_book_dir, open};
use clap::{App, Arg, ArgMatches, SubCommand};
use iron::{status, AfterMiddleware, Chain, Iron, IronError, IronResult, Request, Response, Set};
use futures_util::sink::SinkExt;
use futures_util::StreamExt;
use mdbook::errors::*;
use mdbook::utils;
use mdbook::utils::fs::get_404_output_file;
use mdbook::MDBook;
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::PathBuf;
use tokio::sync::broadcast;
use warp::ws::Message;
use warp::Filter;
struct ErrorRecover;
/// The HTTP endpoint for the websocket used to trigger reloads when a file changes.
const LIVE_RELOAD_ENDPOINT: &str = "__livereload";
// Create clap subcommand arguments
pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
@@ -40,64 +48,53 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
.empty_values(false)
.help("Port to use for HTTP connections"),
)
.arg(
Arg::with_name("websocket-hostname")
.long("websocket-hostname")
.takes_value(true)
.empty_values(false)
.help(
"Hostname to connect to for WebSockets connections (Defaults to the HTTP hostname)",
),
)
.arg(
Arg::with_name("websocket-port")
.short("w")
.long("websocket-port")
.takes_value(true)
.default_value("3001")
.empty_values(false)
.help("Port to use for WebSockets livereload connections"),
)
.arg_from_usage("-o, --open 'Opens the book server in a web browser'")
}
// Watch command implementation
// Serve command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args);
let mut book = MDBook::load(&book_dir)?;
let port = args.value_of("port").unwrap();
let ws_port = args.value_of("websocket-port").unwrap();
let hostname = args.value_of("hostname").unwrap();
let public_address = args.value_of("websocket-hostname").unwrap_or(hostname);
let open_browser = args.is_present("open");
let address = format!("{}:{}", hostname, port);
let ws_address = format!("{}:{}", hostname, ws_port);
let livereload_url = format!("ws://{}:{}", public_address, ws_port);
book.config
.set("output.html.livereload-url", &livereload_url)?;
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into();
}
let livereload_url = format!("ws://{}/{}", address, LIVE_RELOAD_ENDPOINT);
let update_config = |book: &mut MDBook| {
book.config
.set("output.html.livereload-url", &livereload_url)
.expect("livereload-url update failed");
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into();
}
// Override site-url for local serving of the 404 file
book.config.set("output.html.site-url", "/").unwrap();
};
update_config(&mut book);
book.build()?;
let mut chain = Chain::new(staticfile::Static::new(book.build_dir_for("html")));
chain.link_after(ErrorRecover);
let _iron = Iron::new(chain)
.http(&*address)
.chain_err(|| "Unable to launch the server")?;
let sockaddr: SocketAddr = address
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("no address found for {}", address))?;
let build_dir = book.build_dir_for("html");
let input_404 = book
.config
.get("output.html.input-404")
.map(toml::Value::as_str)
.and_then(std::convert::identity) // flatten
.map(ToString::to_string);
let file_404 = get_404_output_file(&input_404);
let ws_server =
ws::WebSocket::new(|_| |_| Ok(())).chain_err(|| "Unable to start the websocket")?;
// A channel used to broadcast to any websockets to reload when a file changes.
let (tx, _rx) = tokio::sync::broadcast::channel::<Message>(100);
let broadcaster = ws_server.broadcaster();
std::thread::spawn(move || {
ws_server.listen(&*ws_address).unwrap();
let reload_tx = tx.clone();
let thread_handle = std::thread::spawn(move || {
serve(build_dir, sockaddr, reload_tx, &file_404);
});
let serving_url = format!("http://{}", address);
@@ -113,32 +110,56 @@ 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| {
b.config
.set("output.html.livereload-url", &livereload_url)?;
Ok(b)
})
.and_then(|b| b.build());
let result = MDBook::load(&book_dir).and_then(|mut b| {
update_config(&mut b);
b.build()
});
if let Err(e) = result {
error!("Unable to load the book");
utils::log_backtrace(&e);
} else {
let _ = broadcaster.send("reload");
let _ = tx.send(Message::text("reload"));
}
});
let _ = thread_handle.join();
Ok(())
}
impl AfterMiddleware for ErrorRecover {
fn catch(&self, _: &mut Request, err: IronError) -> IronResult<Response> {
match err.response.status {
// each error will result in 404 response
Some(_) => Ok(err.response.set(status::NotFound)),
_ => Err(err),
}
}
#[tokio::main]
async fn serve(
build_dir: PathBuf,
address: SocketAddr,
reload_tx: broadcast::Sender<Message>,
file_404: &str,
) {
// A warp Filter which captures `reload_tx` and provides an `rx` copy to
// receive reload messages.
let sender = warp::any().map(move || reload_tx.subscribe());
// A warp Filter to handle the livereload endpoint. This upgrades to a
// websocket, and then waits for any filesystem change notifications, and
// relays them over the websocket.
let livereload = warp::path(LIVE_RELOAD_ENDPOINT)
.and(warp::ws())
.and(sender)
.map(|ws: warp::ws::Ws, mut rx: broadcast::Receiver<Message>| {
ws.on_upgrade(move |ws| async move {
let (mut user_ws_tx, _user_ws_rx) = ws.split();
trace!("websocket got connection");
if let Ok(m) = rx.recv().await {
trace!("notify of reload");
let _ = user_ws_tx.send(m).await;
}
})
});
// A warp Filter that serves from the filesystem.
let book_route = warp::fs::dir(build_dir.clone());
// The fallback route for 404 errors
let fallback_route = warp::fs::file(build_dir.join(file_404))
.map(|reply| warp::reply::with_status(reply, warp::http::StatusCode::NOT_FOUND));
let routes = livereload.or(book_route).or(fallback_route);
warp::serve(routes).run(address).await;
}

View File

@@ -28,7 +28,14 @@ pub fn make_subcommand<'a, 'b>() -> App<'a, 'b> {
// Watch command implementation
pub fn execute(args: &ArgMatches) -> Result<()> {
let book_dir = get_book_dir(args);
let book = MDBook::load(&book_dir)?;
let mut book = MDBook::load(&book_dir)?;
let update_config = |book: &mut MDBook| {
if let Some(dest_dir) = args.value_of("dest-dir") {
book.config.build.build_dir = dest_dir.into();
}
};
update_config(&mut book);
if args.is_present("open") {
book.build()?;
@@ -37,7 +44,10 @@ 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(|b| b.build());
let result = MDBook::load(&book_dir).and_then(|mut b| {
update_config(&mut b);
b.build()
});
if let Err(e) = result {
error!("Unable to build the book");
@@ -59,7 +69,7 @@ fn remove_ignored_files(book_root: &PathBuf, paths: &[PathBuf]) -> Vec<PathBuf>
Ok(exclusion_checker) => filter_ignored_files(exclusion_checker, paths),
Err(_) => {
// We're unable to read the .gitignore file, so we'll silently allow everything.
// Please see discussion: https://github.com/rust-lang-nursery/mdBook/pull/1051
// Please see discussion: https://github.com/rust-lang/mdBook/pull/1051
paths.iter().map(|path| path.to_path_buf()).collect()
}
}

View File

@@ -44,12 +44,13 @@
//! assert_eq!(got, Some(PathBuf::from("./themes")));
//! # Ok(())
//! # }
//! # fn main() { run().unwrap() }
//! # run().unwrap()
//! ```
#![deny(missing_docs)]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
use std::env;
use std::fs::File;
use std::io::Read;
@@ -57,12 +58,9 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use toml::value::Table;
use toml::{self, Value};
use toml_query::delete::TomlValueDeleteExt;
use toml_query::insert::TomlValueInsertExt;
use toml_query::read::TomlValueReadExt;
use crate::errors::*;
use crate::utils;
use crate::utils::{self, toml_ext::TomlExt};
/// The overall configuration object for MDBook, essentially an in-memory
/// representation of `book.toml`.
@@ -72,6 +70,8 @@ pub struct Config {
pub book: BookConfig,
/// Information about the build environment.
pub build: BuildConfig,
/// Information about Rust language support.
pub rust: RustConfig,
rest: Value,
}
@@ -80,7 +80,7 @@ impl FromStr for Config {
/// Load a `Config` from some string.
fn from_str(src: &str) -> Result<Self> {
toml::from_str(src).chain_err(|| Error::from("Invalid configuration file"))
toml::from_str(src).with_context(|| "Invalid configuration file")
}
}
@@ -89,9 +89,9 @@ impl Config {
pub fn from_disk<P: AsRef<Path>>(config_file: P) -> Result<Config> {
let mut buffer = String::new();
File::open(config_file)
.chain_err(|| "Unable to open the configuration file")?
.with_context(|| "Unable to open the configuration file")?
.read_to_string(&mut buffer)
.chain_err(|| "Couldn't read the file")?;
.with_context(|| "Couldn't read the file")?;
Config::from_str(&buffer)
}
@@ -122,7 +122,7 @@ impl Config {
/// > when building the book with something like
/// >
/// > ```text
/// > $ export MDBOOK_BOOK="{'title': 'My Awesome Book', authors: ['Michael-F-Bryan']}"
/// > $ export MDBOOK_BOOK='{"title": "My Awesome Book", "authors": ["Michael-F-Bryan"]}'
/// > $ mdbook build
/// > ```
///
@@ -140,6 +140,17 @@ impl Config {
let parsed_value = serde_json::from_str(&value)
.unwrap_or_else(|_| serde_json::Value::String(value.to_string()));
if key == "book" || key == "build" {
if let serde_json::Value::Object(ref map) = parsed_value {
// To `set` each `key`, we wrap them as `prefix.key`
for (k, v) in map {
let full_key = format!("{}.{}", key, k);
self.set(&full_key, v).expect("unreachable");
}
return;
}
}
self.set(key, parsed_value).expect("unreachable");
}
}
@@ -147,18 +158,15 @@ impl Config {
/// Fetch an arbitrary item from the `Config` as a `toml::Value`.
///
/// You can use dotted indices to access nested items (e.g.
/// `output.html.playpen` will fetch the "playpen" out of the html output
/// `output.html.playground` will fetch the "playground" out of the html output
/// table).
pub fn get(&self, key: &str) -> Option<&Value> {
self.rest.read(key).unwrap_or(None)
self.rest.read(key)
}
/// Fetch a value from the `Config` so you can mutate it.
pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
match self.rest.read_mut(key) {
Ok(inner) => inner,
Err(_) => None,
}
self.rest.read_mut(key)
}
/// Convenience method for getting the html renderer's configuration.
@@ -169,11 +177,14 @@ impl Config {
/// HTML renderer is refactored to be less coupled to `mdbook` internals.
#[doc(hidden)]
pub fn html_config(&self) -> Option<HtmlConfig> {
match self.get_deserialized_opt("output.html") {
match self
.get_deserialized_opt("output.html")
.with_context(|| "Parsing configuration [output.html]")
{
Ok(Some(config)) => Some(config),
Ok(None) => None,
Err(e) => {
utils::log_backtrace(&e.chain_err(|| "Parsing configuration [output.html]"));
utils::log_backtrace(&e);
None
}
}
@@ -201,7 +212,7 @@ impl Config {
value
.clone()
.try_into()
.chain_err(|| "Couldn't deserialize the value")
.with_context(|| "Couldn't deserialize the value")
})
.transpose()
}
@@ -213,17 +224,15 @@ impl Config {
pub fn set<S: Serialize, I: AsRef<str>>(&mut self, index: I, value: S) -> Result<()> {
let index = index.as_ref();
let value =
Value::try_from(value).chain_err(|| "Unable to represent the item as a JSON Value")?;
let value = Value::try_from(value)
.with_context(|| "Unable to represent the item as a JSON Value")?;
if index.starts_with("book.") {
self.book.update_value(&index[5..], value);
} else if index.starts_with("build.") {
self.build.update_value(&index[6..], value);
} else {
self.rest
.insert(index, value)
.map_err(ErrorKind::TomlQueryError)?;
self.rest.insert(index, value);
}
Ok(())
@@ -264,7 +273,7 @@ impl Config {
get_and_insert!(table, "source" => cfg.book.src);
get_and_insert!(table, "description" => cfg.book.description);
if let Ok(Some(dest)) = table.delete("output.html.destination") {
if let Some(dest) = table.delete("output.html.destination") {
if let Ok(destination) = dest.try_into() {
cfg.build.build_dir = destination;
}
@@ -280,6 +289,7 @@ impl Default for Config {
Config {
book: BookConfig::default(),
build: BuildConfig::default(),
rust: RustConfig::default(),
rest: Value::Table(Table::default()),
}
}
@@ -296,7 +306,7 @@ impl<'de> Deserialize<'de> for Config {
warn!("`description` under a table called `[book]`, move the `destination` entry");
warn!("from `[output.html]`, renamed to `build-dir`, under a table called");
warn!("`[build]`, and it should all work.");
warn!("Documentation: http://rust-lang-nursery.github.io/mdBook/format/config.html");
warn!("Documentation: http://rust-lang.github.io/mdBook/format/config.html");
return Ok(Config::from_legacy(raw));
}
@@ -320,9 +330,15 @@ impl<'de> Deserialize<'de> for Config {
.and_then(|value| value.try_into().ok())
.unwrap_or_default();
let rust: RustConfig = table
.remove("rust")
.and_then(|value| value.try_into().ok())
.unwrap_or_default();
Ok(Config {
book,
build,
rust,
rest: Value::Table(table),
})
}
@@ -330,18 +346,17 @@ impl<'de> Deserialize<'de> for Config {
impl Serialize for Config {
fn serialize<S: Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
use serde::ser::Error;
// TODO: This should probably be removed and use a derive instead.
let mut table = self.rest.clone();
let book_config = match Value::try_from(self.book.clone()) {
Ok(cfg) => cfg,
Err(_) => {
return Err(S::Error::custom("Unable to serialize the BookConfig"));
}
};
let book_config = Value::try_from(&self.book).expect("should always be serializable");
table.insert("book", book_config);
if self.rust != RustConfig::default() {
let rust_config = Value::try_from(&self.rust).expect("should always be serializable");
table.insert("rust", rust_config);
}
table.insert("book", book_config).expect("unreachable");
table.serialize(s)
}
}
@@ -368,7 +383,7 @@ fn is_legacy_format(table: &Value) -> bool {
];
for item in &legacy_items {
if let Ok(Some(_)) = table.read(item) {
if table.read(item).is_some() {
return true;
}
}
@@ -414,7 +429,7 @@ impl Default for BookConfig {
pub struct BuildConfig {
/// Where to put built artefacts relative to the book's root directory.
pub build_dir: PathBuf,
/// Should non-existent markdown files specified in `SETTINGS.md` be created
/// Should non-existent markdown files specified in `SUMMARY.md` be created
/// if they don't exist?
pub create_missing: bool,
/// Should the default preprocessors always be used when they are
@@ -432,8 +447,27 @@ impl Default for BuildConfig {
}
}
/// Configuration for the Rust compiler(e.g., for playground)
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct RustConfig {
/// Rust edition used in playground
pub edition: Option<RustEdition>,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
/// Rust edition to use for the code.
pub enum RustEdition {
/// The 2018 edition of Rust
#[serde(rename = "2018")]
E2018,
/// The 2015 edition of Rust
#[serde(rename = "2015")]
E2015,
}
/// Configuration for the HTML renderer.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct HtmlConfig {
/// The theme directory, if specified.
@@ -441,12 +475,14 @@ pub struct HtmlConfig {
/// The default theme to use, defaults to 'light'
pub default_theme: Option<String>,
/// The theme to use if the browser requests the dark version of the site.
/// Defaults to the same as 'default_theme'
/// Defaults to 'navy'.
pub preferred_dark_theme: Option<String>,
/// Use "smart quotes" instead of the usual `"` character.
pub curly_quotes: bool,
/// Should mathjax be enabled?
pub mathjax_support: bool,
/// Whether to fonts.css and respective font files to the output directory.
pub copy_fonts: bool,
/// An optional google analytics code.
pub google_analytics: Option<String>,
/// Additional CSS stylesheets to include in the rendered page's `<head>`.
@@ -456,8 +492,9 @@ pub struct HtmlConfig {
pub additional_js: Vec<PathBuf>,
/// Fold settings.
pub fold: Fold,
/// Playpen settings.
pub playpen: Playpen,
/// Playground settings.
#[serde(alias = "playpen")]
pub playground: Playground,
/// Don't render section labels.
pub no_section_label: bool,
/// Search settings. If `None`, the default will be used.
@@ -467,6 +504,10 @@ pub struct HtmlConfig {
/// FontAwesome icon class to use for the Git repository link.
/// Defaults to `fa-github` if `None`.
pub git_repository_icon: Option<String>,
/// Input path for the 404 file, defaults to 404.md, set to "" to disable 404 file output
pub input_404: Option<String>,
/// Absolute url to site, used to emit correct paths for the 404 page, which might be accessed in a deeply nested directory
pub site_url: Option<String>,
/// This is used as a bit of a workaround for the `mdbook serve` command.
/// Basically, because you set the websocket port from the command line, the
/// `mdbook serve` command needs a way to let the HTML renderer know where
@@ -475,6 +516,35 @@ pub struct HtmlConfig {
/// This config item *should not be edited* by the end user.
#[doc(hidden)]
pub livereload_url: Option<String>,
/// The mapping from old pages to new pages/URLs to use when generating
/// redirects.
pub redirect: HashMap<String, String>,
}
impl Default for HtmlConfig {
fn default() -> HtmlConfig {
HtmlConfig {
theme: None,
default_theme: None,
preferred_dark_theme: None,
curly_quotes: false,
mathjax_support: false,
copy_fonts: true,
google_analytics: None,
additional_css: Vec::new(),
additional_js: Vec::new(),
fold: Fold::default(),
playground: Playground::default(),
no_section_label: false,
search: None,
git_repository_url: None,
git_repository_icon: None,
input_404: None,
site_url: None,
livereload_url: None,
redirect: HashMap::new(),
}
}
}
impl HtmlConfig {
@@ -500,24 +570,24 @@ pub struct Fold {
pub level: u8,
}
/// Configuration for tweaking how the the HTML renderer handles the playpen.
/// Configuration for tweaking how the the HTML renderer handles the playground.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case")]
pub struct Playpen {
/// Should playpen snippets be editable? Default: `false`.
pub struct Playground {
/// Should playground snippets be editable? Default: `false`.
pub editable: bool,
/// Display the copy button. Default: `true`.
pub copyable: bool,
/// Copy JavaScript files for the editor to the output directory?
/// Default: `true`.
pub copy_js: bool,
/// Display line numbers on playpen snippets. Default: `false`.
/// Display line numbers on playground snippets. Default: `false`.
pub line_numbers: bool,
}
impl Default for Playpen {
fn default() -> Playpen {
Playpen {
impl Default for Playground {
fn default() -> Playground {
Playground {
editable: false,
copyable: true,
copy_js: true,
@@ -603,6 +673,7 @@ impl<'de, T> Updateable<'de> for T where T: Serialize + Deserialize<'de> {}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::fs::get_404_output_file;
const COMPLEX_CONFIG: &str = r#"
[book]
@@ -627,10 +698,14 @@ mod tests {
git-repository-url = "https://foo.com/"
git-repository-icon = "fa-code-fork"
[output.html.playpen]
[output.html.playground]
editable = true
editor = "ace"
[output.html.redirect]
"index.html" = "overview.html"
"nexted/page.md" = "https://rust-lang.org/"
[preprocessor.first]
[preprocessor.second]
@@ -653,7 +728,8 @@ mod tests {
create_missing: false,
use_default_preprocessors: true,
};
let playpen_should_be = Playpen {
let rust_should_be = RustConfig { edition: None };
let playground_should_be = Playground {
editable: true,
copyable: true,
copy_js: true,
@@ -665,9 +741,18 @@ mod tests {
additional_css: vec![PathBuf::from("./foo/bar/baz.css")],
theme: Some(PathBuf::from("./themedir")),
default_theme: Some(String::from("rust")),
playpen: playpen_should_be,
playground: playground_should_be,
git_repository_url: Some(String::from("https://foo.com/")),
git_repository_icon: Some(String::from("fa-code-fork")),
redirect: vec![
(String::from("index.html"), String::from("overview.html")),
(
String::from("nexted/page.md"),
String::from("https://rust-lang.org/"),
),
]
.into_iter()
.collect(),
..Default::default()
};
@@ -675,9 +760,62 @@ mod tests {
assert_eq!(got.book, book_should_be);
assert_eq!(got.build, build_should_be);
assert_eq!(got.rust, rust_should_be);
assert_eq!(got.html_config().unwrap(), html_should_be);
}
#[test]
fn edition_2015() {
let src = r#"
[book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David"]
src = "./source"
[rust]
edition = "2015"
"#;
let book_should_be = BookConfig {
title: Some(String::from("mdBook Documentation")),
description: Some(String::from(
"Create book from markdown files. Like Gitbook but implemented in Rust",
)),
authors: vec![String::from("Mathieu David")],
src: PathBuf::from("./source"),
..Default::default()
};
let got = Config::from_str(src).unwrap();
assert_eq!(got.book, book_should_be);
let rust_should_be = RustConfig {
edition: Some(RustEdition::E2015),
};
let got = Config::from_str(src).unwrap();
assert_eq!(got.rust, rust_should_be);
}
#[test]
fn edition_2018() {
let src = r#"
[book]
title = "mdBook Documentation"
description = "Create book from markdown files. Like Gitbook but implemented in Rust"
authors = ["Mathieu David"]
src = "./source"
[rust]
edition = "2018"
"#;
let rust_should_be = RustConfig {
edition: Some(RustEdition::E2018),
};
let got = Config::from_str(src).unwrap();
assert_eq!(got.rust, rust_should_be);
}
#[test]
fn load_arbitrary_output_type() {
#[derive(Debug, Deserialize, PartialEq)]
@@ -720,7 +858,7 @@ mod tests {
// is happy...
let src = COMPLEX_CONFIG;
let mut config = Config::from_str(src).unwrap();
let key = "output.html.playpen.editable";
let key = "output.html.playground.editable";
assert_eq!(config.get(key).unwrap(), &Value::Boolean(true));
*config.get_mut(key).unwrap() = Value::Boolean(false);
@@ -870,4 +1008,31 @@ mod tests {
assert_eq!(cfg.book.title, Some(should_be));
}
#[test]
fn file_404_default() {
let src = r#"
[output.html]
destination = "my-book"
"#;
let got = Config::from_str(src).unwrap();
let html_config = got.html_config().unwrap();
assert_eq!(html_config.input_404, None);
assert_eq!(&get_404_output_file(&html_config.input_404), "404.html");
}
#[test]
fn file_404_custom() {
let src = r#"
[output.html]
input-404= "missing.md"
output-404= "missing.html"
"#;
let got = Config::from_str(src).unwrap();
let html_config = got.html_config().unwrap();
assert_eq!(html_config.input_404, Some("missing.md".to_string()));
assert_eq!(&get_404_output_file(&html_config.input_404), "missing.html");
}
}

View File

@@ -75,16 +75,15 @@
//! directly, making deserializing the `RenderContext` easy and giving you
//! access to the various methods for working with the [`Config`].
//!
//! [user guide]: https://rust-lang-nursery.github.io/mdBook/
//! [user guide]: https://rust-lang.github.io/mdBook/
//! [`RenderContext`]: renderer/struct.RenderContext.html
//! [relevant chapter]: https://rust-lang-nursery.github.io/mdBook/for_developers/backends.html
//! [relevant chapter]: https://rust-lang.github.io/mdBook/for_developers/backends.html
//! [`Config`]: config/struct.Config.html
#![deny(missing_docs)]
#![deny(rust_2018_idioms)]
#![allow(clippy::comparison_chain)]
#[macro_use]
extern crate error_chain;
#[macro_use]
extern crate lazy_static;
#[macro_use]
@@ -118,48 +117,6 @@ pub use crate::renderer::Renderer;
/// The error types used through out this crate.
pub mod errors {
use std::path::PathBuf;
error_chain! {
foreign_links {
Io(std::io::Error) #[doc = "A wrapper around `std::io::Error`"];
HandlebarsRender(handlebars::RenderError) #[doc = "Handlebars rendering failed"];
HandlebarsTemplate(Box<handlebars::TemplateError>) #[doc = "Unable to parse the template"];
Utf8(std::string::FromUtf8Error) #[doc = "Invalid UTF-8"];
SerdeJson(serde_json::Error) #[doc = "JSON conversion failed"];
}
errors {
/// A subprocess exited with an unsuccessful return code.
Subprocess(message: String, output: std::process::Output) {
description("A subprocess failed")
display("{}: {}", message, String::from_utf8_lossy(&output.stdout))
}
/// An error was encountered while parsing the `SUMMARY.md` file.
ParseError(line: usize, col: usize, message: String) {
description("A SUMMARY.md parsing error")
display("Error at line {}, column {}: {}", line, col, message)
}
/// The user tried to use a reserved filename.
ReservedFilenameError(filename: PathBuf) {
description("Reserved Filename")
display("{} is reserved for internal use", filename.display())
}
/// Error with a TOML file.
TomlQueryError(inner: toml_query::error::Error) {
description("toml_query error")
display("{}", inner)
}
}
}
// Box to halve the size of Error
impl From<handlebars::TemplateError> for Error {
fn from(e: handlebars::TemplateError) -> Error {
From::from(Box::new(e))
}
}
pub(crate) use anyhow::{bail, ensure, Context};
pub use anyhow::{Error, Result};
}

View File

@@ -30,7 +30,7 @@ fn main() {
.setting(AppSettings::ColoredHelp)
.after_help(
"For more information about a specific command, try `mdbook <command> --help`\n\
The source code for mdBook is available at: https://github.com/rust-lang-nursery/mdBook",
The source code for mdBook is available at: https://github.com/rust-lang/mdBook",
)
.subcommand(cmd::init::make_subcommand())
.subcommand(cmd::build::make_subcommand())

View File

@@ -43,7 +43,7 @@ impl CmdPreprocessor {
/// A convenience function custom preprocessors can use to parse the input
/// written to `stdin` by a `CmdRenderer`.
pub fn parse_input<R: Read>(reader: R) -> Result<(PreprocessorContext, Book)> {
serde_json::from_reader(reader).chain_err(|| "Unable to parse the input")
serde_json::from_reader(reader).with_context(|| "Unable to parse the input")
}
fn write_input_to_child(&self, child: &mut Child, book: &Book, ctx: &PreprocessorContext) {
@@ -100,7 +100,7 @@ impl Preprocessor for CmdPreprocessor {
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.chain_err(|| {
.with_context(|| {
format!(
"Unable to start the \"{}\" preprocessor. Is it installed?",
self.name()
@@ -111,7 +111,7 @@ impl Preprocessor for CmdPreprocessor {
let output = child
.wait_with_output()
.chain_err(|| "Error waiting for the preprocessor to complete")?;
.with_context(|| "Error waiting for the preprocessor to complete")?;
trace!("{} exited with output: {:?}", self.cmd, output);
ensure!(
@@ -119,7 +119,8 @@ impl Preprocessor for CmdPreprocessor {
"The preprocessor exited unsuccessfully"
);
serde_json::from_slice(&output.stdout).chain_err(|| "Unable to parse the preprocessed book")
serde_json::from_slice(&output.stdout)
.with_context(|| "Unable to parse the preprocessed book")
}
fn supports_renderer(&self, renderer: &str) -> bool {

View File

@@ -29,13 +29,15 @@ impl Preprocessor for IndexPreprocessor {
let source_dir = ctx.root.join(&ctx.config.book.src);
book.for_each_mut(|section: &mut BookItem| {
if let BookItem::Chapter(ref mut ch) = *section {
if is_readme_file(&ch.path) {
let index_md = source_dir.join(ch.path.with_file_name("index.md"));
if index_md.exists() {
warn_readme_name_conflict(&ch.path, &index_md);
}
if let Some(ref mut path) = ch.path {
if is_readme_file(&path) {
let mut index_md = source_dir.join(path.with_file_name("index.md"));
if index_md.exists() {
warn_readme_name_conflict(&path, &&mut index_md);
}
ch.path.set_file_name("index.md");
path.set_file_name("index.md");
}
}
}
});

View File

@@ -22,7 +22,7 @@ const MAX_LINK_NESTED_DEPTH: usize = 10;
///. specified or the lines between specified anchors, and include the rest of the file behind `#`.
/// This hides the lines from initial display but shows them when the reader expands the code
/// block and provides them to Rustdoc for testing.
/// - `{{# playpen}}` - Insert runnable Rust files
/// - `{{# playground}}` - Insert runnable Rust files
#[derive(Default)]
pub struct LinkPreprocessor;
@@ -45,14 +45,15 @@ impl Preprocessor for LinkPreprocessor {
book.for_each_mut(|section: &mut BookItem| {
if let BookItem::Chapter(ref mut ch) = *section {
let base = ch
.path
.parent()
.map(|dir| src_dir.join(dir))
.expect("All book items have a parent");
if let Some(ref chapter_path) = ch.path {
let base = chapter_path
.parent()
.map(|dir| src_dir.join(dir))
.expect("All book items have a parent");
let content = replace_all(&ch.content, base, &ch.path, 0);
ch.content = content;
let content = replace_all(&ch.content, base, chapter_path, 0);
ch.content = content;
}
}
});
@@ -94,7 +95,7 @@ where
}
Err(e) => {
error!("Error updating \"{}\", {}", link.link_text, e);
for cause in e.iter().skip(1) {
for cause in e.chain().skip(1) {
warn!("Caused By: {}", cause);
}
@@ -113,7 +114,7 @@ where
enum LinkType<'a> {
Escaped,
Include(PathBuf, RangeOrAnchor),
Playpen(PathBuf, Vec<&'a str>),
Playground(PathBuf, Vec<&'a str>),
RustdocInclude(PathBuf, RangeOrAnchor),
}
@@ -182,7 +183,7 @@ impl<'a> LinkType<'a> {
match self {
LinkType::Escaped => None,
LinkType::Include(p, _) => Some(return_relative_path(base, &p)),
LinkType::Playpen(p, _) => Some(return_relative_path(base, &p)),
LinkType::Playground(p, _) => Some(return_relative_path(base, &p)),
LinkType::RustdocInclude(p, _) => Some(return_relative_path(base, &p)),
}
}
@@ -261,7 +262,15 @@ impl<'a> Link<'a> {
match (typ.as_str(), file_arg) {
("include", Some(pth)) => Some(parse_include_path(pth)),
("playpen", Some(pth)) => Some(LinkType::Playpen(pth.into(), props)),
("playground", Some(pth)) => Some(LinkType::Playground(pth.into(), props)),
("playpen", Some(pth)) => {
warn!(
"the {{{{#playpen}}}} expression has been \
renamed to {{{{#playground}}}}, \
please update your book to use the new name"
);
Some(LinkType::Playground(pth.into(), props))
}
("rustdoc_include", Some(pth)) => Some(parse_rustdoc_include_path(pth)),
_ => None,
}
@@ -295,7 +304,7 @@ impl<'a> Link<'a> {
RangeOrAnchor::Range(range) => take_lines(&s, range.clone()),
RangeOrAnchor::Anchor(anchor) => take_anchored_lines(&s, anchor),
})
.chain_err(|| {
.with_context(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
@@ -315,7 +324,7 @@ impl<'a> Link<'a> {
take_rustdoc_include_anchored_lines(&s, anchor)
}
})
.chain_err(|| {
.with_context(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
@@ -323,10 +332,10 @@ impl<'a> Link<'a> {
)
})
}
LinkType::Playpen(ref pat, ref attrs) => {
LinkType::Playground(ref pat, ref attrs) => {
let target = base.join(pat);
let contents = fs::read_to_string(&target).chain_err(|| {
let contents = fs::read_to_string(&target).with_context(|| {
format!(
"Could not read file for link {} ({})",
self.link_text,
@@ -364,14 +373,14 @@ fn find_links(contents: &str) -> LinkIter<'_> {
// r"\\\{\{#.*\}\}|\{\{#([a-zA-Z0-9]+)\s*([a-zA-Z0-9_.\-:/\\\s]+)\}\}")?;
lazy_static! {
static ref RE: Regex = Regex::new(
r"(?x) # insignificant whitespace mode
\\\{\{\#.*\}\} # match escaped link
| # or
\{\{\s* # link opening parens and whitespace
\#([a-zA-Z0-9_]+) # link type
\s+ # separating whitespace
([a-zA-Z0-9\s_.\-:/\\]+) # link target path and space separated properties
\s*\}\} # whitespace and link closing parens"
r"(?x) # insignificant whitespace mode
\\\{\{\#.*\}\} # match escaped link
| # or
\{\{\s* # link opening parens and whitespace
\#([a-zA-Z0-9_]+) # link type
\s+ # separating whitespace
([a-zA-Z0-9\s_.\-:/\\\+]+) # link target path and space separated properties
\s*\}\} # whitespace and link closing parens"
)
.unwrap();
}
@@ -405,7 +414,7 @@ mod tests {
#[test]
fn test_find_links_partial_link() {
let s = "Some random text with {{#playpen...";
let s = "Some random text with {{#playground...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
let s = "Some random text with {{#include...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
@@ -415,19 +424,19 @@ mod tests {
#[test]
fn test_find_links_empty_link() {
let s = "Some random text with {{#playpen}} and {{#playpen }} {{}} {{#}}...";
let s = "Some random text with {{#playground}} and {{#playground }} {{}} {{#}}...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}
#[test]
fn test_find_links_unknown_link_type() {
let s = "Some random text with {{#playpenz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
let s = "Some random text with {{#playgroundz ar.rs}} and {{#incn}} {{baz}} {{#bar}}...";
assert!(find_links(s).collect::<Vec<_>>() == vec![]);
}
#[test]
fn test_find_links_simple_link() {
let s = "Some random text with {{#playpen file.rs}} and {{#playpen test.rs }}...";
let s = "Some random text with {{#playground file.rs}} and {{#playground test.rs }}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
@@ -437,20 +446,38 @@ mod tests {
vec![
Link {
start_index: 22,
end_index: 42,
link_type: LinkType::Playpen(PathBuf::from("file.rs"), vec![]),
link_text: "{{#playpen file.rs}}",
end_index: 45,
link_type: LinkType::Playground(PathBuf::from("file.rs"), vec![]),
link_text: "{{#playground file.rs}}",
},
Link {
start_index: 47,
end_index: 68,
link_type: LinkType::Playpen(PathBuf::from("test.rs"), vec![]),
link_text: "{{#playpen test.rs }}",
start_index: 50,
end_index: 74,
link_type: LinkType::Playground(PathBuf::from("test.rs"), vec![]),
link_text: "{{#playground test.rs }}",
},
]
);
}
#[test]
fn test_find_links_with_special_characters() {
let s = "Some random text with {{#playground foo-bar\\baz/_c++.rs}}...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
assert_eq!(
res,
vec![Link {
start_index: 22,
end_index: 57,
link_type: LinkType::Playground(PathBuf::from("foo-bar\\baz/_c++.rs"), vec![]),
link_text: "{{#playground foo-bar\\baz/_c++.rs}}",
},]
);
}
#[test]
fn test_find_links_with_range() {
let s = "Some random text with {{#include file.rs:10:20}}...";
@@ -586,7 +613,7 @@ mod tests {
#[test]
fn test_find_links_escaped_link() {
let s = "Some random text with escaped playpen \\{{#playpen file.rs editable}} ...";
let s = "Some random text with escaped playground \\{{#playground file.rs editable}} ...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
@@ -594,18 +621,19 @@ mod tests {
assert_eq!(
res,
vec![Link {
start_index: 38,
end_index: 68,
start_index: 41,
end_index: 74,
link_type: LinkType::Escaped,
link_text: "\\{{#playpen file.rs editable}}",
link_text: "\\{{#playground file.rs editable}}",
}]
);
}
#[test]
fn test_find_playpens_with_properties() {
let s = "Some random text with escaped playpen {{#playpen file.rs editable }} and some \
more\n text {{#playpen my.rs editable no_run should_panic}} ...";
fn test_find_playgrounds_with_properties() {
let s =
"Some random text with escaped playground {{#playground file.rs editable }} and some \
more\n text {{#playground my.rs editable no_run should_panic}} ...";
let res = find_links(s).collect::<Vec<_>>();
println!("\nOUTPUT: {:?}\n", res);
@@ -613,19 +641,19 @@ mod tests {
res,
vec![
Link {
start_index: 38,
end_index: 68,
link_type: LinkType::Playpen(PathBuf::from("file.rs"), vec!["editable"]),
link_text: "{{#playpen file.rs editable }}",
start_index: 41,
end_index: 74,
link_type: LinkType::Playground(PathBuf::from("file.rs"), vec!["editable"]),
link_text: "{{#playground file.rs editable }}",
},
Link {
start_index: 89,
end_index: 136,
link_type: LinkType::Playpen(
start_index: 95,
end_index: 145,
link_type: LinkType::Playground(
PathBuf::from("my.rs"),
vec!["editable", "no_run", "should_panic"],
),
link_text: "{{#playpen my.rs editable no_run should_panic}}",
link_text: "{{#playground my.rs editable no_run should_panic}}",
},
]
);
@@ -633,8 +661,9 @@ mod tests {
#[test]
fn test_find_all_link_types() {
let s = "Some random text with escaped playpen {{#include file.rs}} and \\{{#contents are \
insignifficant in escaped link}} some more\n text {{#playpen my.rs editable \
let s =
"Some random text with escaped playground {{#include file.rs}} and \\{{#contents are \
insignifficant in escaped link}} some more\n text {{#playground my.rs editable \
no_run should_panic}} ...";
let res = find_links(s).collect::<Vec<_>>();
@@ -643,8 +672,8 @@ mod tests {
assert_eq!(
res[0],
Link {
start_index: 38,
end_index: 58,
start_index: 41,
end_index: 61,
link_type: LinkType::Include(
PathBuf::from("file.rs"),
RangeOrAnchor::Range(LineRange::from(..))
@@ -655,8 +684,8 @@ mod tests {
assert_eq!(
res[1],
Link {
start_index: 63,
end_index: 112,
start_index: 66,
end_index: 115,
link_type: LinkType::Escaped,
link_text: "\\{{#contents are insignifficant in escaped link}}",
}
@@ -664,13 +693,13 @@ mod tests {
assert_eq!(
res[2],
Link {
start_index: 130,
end_index: 177,
link_type: LinkType::Playpen(
start_index: 133,
end_index: 183,
link_type: LinkType::Playground(
PathBuf::from("my.rs"),
vec!["editable", "no_run", "should_panic"]
),
link_text: "{{#playpen my.rs editable no_run should_panic}}",
link_text: "{{#playground my.rs editable no_run should_panic}}",
}
);
}

View File

@@ -1,17 +1,18 @@
use crate::book::{Book, BookItem};
use crate::config::{Config, HtmlConfig, Playpen};
use crate::config::{Config, HtmlConfig, Playground, RustEdition};
use crate::errors::*;
use crate::renderer::html_handlebars::helpers;
use crate::renderer::{RenderContext, Renderer};
use crate::theme::{self, playpen_editor, Theme};
use crate::theme::{self, playground_editor, Theme};
use crate::utils;
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::fs;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use crate::utils::fs::get_404_output_file;
use handlebars::Handlebars;
use regex::{Captures, Regex};
@@ -30,82 +31,144 @@ impl HtmlHandlebars {
print_content: &mut String,
) -> Result<()> {
// FIXME: This should be made DRY-er and rely less on mutable state
if let BookItem::Chapter(ref ch) = *item {
let content = ch.content.clone();
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
let fixed_content = utils::render_markdown_with_path(
&ch.content,
ctx.html_config.curly_quotes,
Some(&ch.path),
);
print_content.push_str(&fixed_content);
let (ch, path) = match item {
BookItem::Chapter(ch) if !ch.is_draft_chapter() => (ch, ch.path.as_ref().unwrap()),
_ => return Ok(()),
};
// Update the context with data for this file
let path = ch
.path
.to_str()
.chain_err(|| "Could not convert path to str")?;
let filepath = Path::new(&ch.path).with_extension("html");
let content = ch.content.clone();
let content = utils::render_markdown(&content, ctx.html_config.curly_quotes);
// "print.html" is used for the print page.
if ch.path == Path::new("print.md") {
bail!(ErrorKind::ReservedFilenameError(ch.path.clone()));
};
let fixed_content = utils::render_markdown_with_path(
&ch.content,
ctx.html_config.curly_quotes,
Some(&path),
);
print_content.push_str(&fixed_content);
// Non-lexical lifetimes needed :'(
let title: String;
{
let book_title = ctx
.data
.get("book_title")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
title = ch.name.clone() + " - " + book_title;
}
// Update the context with data for this file
let ctx_path = path
.to_str()
.with_context(|| "Could not convert path to str")?;
let filepath = Path::new(&ctx_path).with_extension("html");
ctx.data.insert("path".to_owned(), json!(path));
ctx.data.insert("content".to_owned(), json!(content));
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
ctx.data.insert("title".to_owned(), json!(title));
ctx.data.insert(
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(&ch.path)),
);
if let Some(ref section) = ch.number {
ctx.data
.insert("section".to_owned(), json!(section.to_string()));
}
// "print.html" is used for the print page.
if path == Path::new("print.md") {
bail!("{} is reserved for internal use", path.display());
};
// Render the handlebars template with the data
debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?;
let book_title = ctx
.data
.get("book_title")
.and_then(serde_json::Value::as_str)
.unwrap_or("");
let rendered = self.post_process(rendered, &ctx.html_config.playpen);
let title = match book_title {
"" => ch.name.clone(),
_ => ch.name.clone() + " - " + book_title,
};
// Write to file
debug!("Creating {}", filepath.display());
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
ctx.data.insert("path".to_owned(), json!(path));
ctx.data.insert("content".to_owned(), json!(content));
ctx.data.insert("chapter_title".to_owned(), json!(ch.name));
ctx.data.insert("title".to_owned(), json!(title));
ctx.data.insert(
"path_to_root".to_owned(),
json!(utils::fs::path_to_root(&path)),
);
if let Some(ref section) = ch.number {
ctx.data
.insert("section".to_owned(), json!(section.to_string()));
}
if ctx.is_index {
ctx.data.insert("path".to_owned(), json!("index.md"));
ctx.data.insert("path_to_root".to_owned(), json!(""));
ctx.data.insert("is_index".to_owned(), json!("true"));
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
let rendered_index = self.post_process(rendered_index, &ctx.html_config.playpen);
debug!("Creating index.html from {}", path);
utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
}
// Render the handlebars template with the data
debug!("Render template");
let rendered = ctx.handlebars.render("index", &ctx.data)?;
let rendered = self.post_process(rendered, &ctx.html_config.playground, ctx.edition);
// Write to file
debug!("Creating {}", filepath.display());
utils::fs::write_file(&ctx.destination, &filepath, rendered.as_bytes())?;
if ctx.is_index {
ctx.data.insert("path".to_owned(), json!("index.md"));
ctx.data.insert("path_to_root".to_owned(), json!(""));
ctx.data.insert("is_index".to_owned(), json!("true"));
let rendered_index = ctx.handlebars.render("index", &ctx.data)?;
let rendered_index =
self.post_process(rendered_index, &ctx.html_config.playground, ctx.edition);
debug!("Creating index.html from {}", ctx_path);
utils::fs::write_file(&ctx.destination, "index.html", rendered_index.as_bytes())?;
}
Ok(())
}
fn render_404(
&self,
ctx: &RenderContext,
html_config: &HtmlConfig,
src_dir: &PathBuf,
handlebars: &mut Handlebars<'_>,
data: &mut serde_json::Map<String, serde_json::Value>,
) -> Result<()> {
let destination = &ctx.destination;
let content_404 = if let Some(ref filename) = html_config.input_404 {
let path = src_dir.join(filename);
std::fs::read_to_string(&path)
.with_context(|| format!("unable to open 404 input file {:?}", path))?
} else {
// 404 input not explicitly configured try the default file 404.md
let default_404_location = src_dir.join("404.md");
if default_404_location.exists() {
std::fs::read_to_string(&default_404_location).with_context(|| {
format!("unable to open 404 input file {:?}", default_404_location)
})?
} else {
"# Document not found (404)\n\nThis URL is invalid, sorry. Please use the \
navigation bar or search to continue."
.to_string()
}
};
let html_content_404 = utils::render_markdown(&content_404, html_config.curly_quotes);
let mut data_404 = data.clone();
let base_url = if let Some(site_url) = &html_config.site_url {
site_url
} else {
debug!(
"HTML 'site-url' parameter not set, defaulting to '/'. Please configure \
this to ensure the 404 page work correctly, especially if your site is hosted in a \
subdirectory on the HTTP server."
);
"/"
};
data_404.insert("base_url".to_owned(), json!(base_url));
// Set a dummy path to ensure other paths (e.g. in the TOC) are generated correctly
data_404.insert("path".to_owned(), json!("404.md"));
data_404.insert("content".to_owned(), json!(html_content_404));
let rendered = handlebars.render("index", &data_404)?;
let rendered =
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
let output_file = get_404_output_file(&html_config.input_404);
utils::fs::write_file(&destination, output_file, rendered.as_bytes())?;
debug!("Creating 404.html ✓");
Ok(())
}
#[cfg_attr(feature = "cargo-clippy", allow(clippy::let_and_return))]
fn post_process(&self, rendered: String, playpen_config: &Playpen) -> String {
fn post_process(
&self,
rendered: String,
playground_config: &Playground,
edition: Option<RustEdition>,
) -> String {
let rendered = build_header_links(&rendered);
let rendered = fix_code_blocks(&rendered);
let rendered = add_playpen_pre(&rendered, playpen_config);
let rendered = add_playground_pre(&rendered, playground_config, edition);
rendered
}
@@ -129,7 +192,8 @@ impl HtmlHandlebars {
write_file(destination, "css/chrome.css", &theme.chrome_css)?;
write_file(destination, "css/print.css", &theme.print_css)?;
write_file(destination, "css/variables.css", &theme.variables_css)?;
write_file(destination, "favicon.png", &theme.favicon)?;
write_file(destination, "favicon.png", &theme.favicon_png)?;
write_file(destination, "favicon.svg", &theme.favicon_svg)?;
write_file(destination, "highlight.css", &theme.highlight_css)?;
write_file(destination, "tomorrow-night.css", &theme.tomorrow_night_css)?;
write_file(destination, "ayu-highlight.css", &theme.ayu_highlight_css)?;
@@ -170,20 +234,38 @@ impl HtmlHandlebars {
"FontAwesome/fonts/FontAwesome.ttf",
theme::FONT_AWESOME_TTF,
)?;
if html_config.copy_fonts {
write_file(destination, "fonts/fonts.css", theme::fonts::CSS)?;
for (file_name, contents) in theme::fonts::LICENSES.iter() {
write_file(destination, file_name, contents)?;
}
for (file_name, contents) in theme::fonts::OPEN_SANS.iter() {
write_file(destination, file_name, contents)?;
}
write_file(
destination,
theme::fonts::SOURCE_CODE_PRO.0,
theme::fonts::SOURCE_CODE_PRO.1,
)?;
}
let playpen_config = &html_config.playpen;
let playground_config = &html_config.playground;
// Ace is a very large dependency, so only load it when requested
if playpen_config.editable && playpen_config.copy_js {
if playground_config.editable && playground_config.copy_js {
// Load the editor
write_file(destination, "editor.js", playpen_editor::JS)?;
write_file(destination, "ace.js", playpen_editor::ACE_JS)?;
write_file(destination, "mode-rust.js", playpen_editor::MODE_RUST_JS)?;
write_file(destination, "theme-dawn.js", playpen_editor::THEME_DAWN_JS)?;
write_file(destination, "editor.js", playground_editor::JS)?;
write_file(destination, "ace.js", playground_editor::ACE_JS)?;
write_file(destination, "mode-rust.js", playground_editor::MODE_RUST_JS)?;
write_file(
destination,
"theme-dawn.js",
playground_editor::THEME_DAWN_JS,
)?;
write_file(
destination,
"theme-tomorrow_night.js",
playpen_editor::THEME_TOMORROW_NIGHT_JS,
playground_editor::THEME_TOMORROW_NIGHT_JS,
)?;
}
@@ -208,7 +290,7 @@ impl HtmlHandlebars {
);
}
fn register_hbs_helpers(&self, handlebars: &mut Handlebars, html_config: &HtmlConfig) {
fn register_hbs_helpers(&self, handlebars: &mut Handlebars<'_>, html_config: &HtmlConfig) {
handlebars.register_helper(
"toc",
Box::new(helpers::toc::RenderToc {
@@ -237,7 +319,7 @@ impl HtmlHandlebars {
let output_location = destination.join(custom_file);
if let Some(parent) = output_location.parent() {
fs::create_dir_all(parent)
.chain_err(|| format!("Unable to create {}", parent.display()))?;
.with_context(|| format!("Unable to create {}", parent.display()))?;
}
debug!(
"Copying {} -> {}",
@@ -245,7 +327,7 @@ impl HtmlHandlebars {
output_location.display()
);
fs::copy(&input_location, &output_location).chain_err(|| {
fs::copy(&input_location, &output_location).with_context(|| {
format!(
"Unable to copy {} to {}",
input_location.display(),
@@ -256,6 +338,68 @@ impl HtmlHandlebars {
Ok(())
}
fn emit_redirects(
&self,
root: &Path,
handlebars: &Handlebars<'_>,
redirects: &HashMap<String, String>,
) -> Result<()> {
if redirects.is_empty() {
return Ok(());
}
log::debug!("Emitting redirects");
for (original, new) in redirects {
log::debug!("Redirecting \"{}\"\"{}\"", original, new);
// Note: all paths are relative to the build directory, so the
// leading slash in an absolute path means nothing (and would mess
// up `root.join(original)`).
let original = original.trim_start_matches("/");
let filename = root.join(original);
self.emit_redirect(handlebars, &filename, new)?;
}
Ok(())
}
fn emit_redirect(
&self,
handlebars: &Handlebars<'_>,
original: &Path,
destination: &str,
) -> Result<()> {
if original.exists() {
// sanity check to avoid accidentally overwriting a real file.
let msg = format!(
"Not redirecting \"{}\" to \"{}\" because it already exists. Are you sure it needs to be redirected?",
original.display(),
destination,
);
return Err(Error::msg(msg));
}
if let Some(parent) = original.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Unable to ensure \"{}\" exists", parent.display()))?;
}
let ctx = json!({
"url": destination,
});
let f = File::create(original)?;
handlebars
.render_to_write("redirect", &ctx, f)
.with_context(|| {
format!(
"Unable to create a redirect file at \"{}\"",
original.display()
)
})?;
Ok(())
}
}
// TODO(mattico): Remove some time after the 0.1.8 release
@@ -287,10 +431,11 @@ impl Renderer for HtmlHandlebars {
let src_dir = ctx.root.join(&ctx.config.book.src);
let destination = &ctx.destination;
let book = &ctx.book;
let build_dir = ctx.root.join(&ctx.config.build.build_dir);
if destination.exists() {
utils::fs::remove_dir_content(destination)
.chain_err(|| "Unable to remove stale HTML output")?;
.with_context(|| "Unable to remove stale HTML output")?;
}
trace!("render");
@@ -316,6 +461,13 @@ impl Renderer for HtmlHandlebars {
debug!("Register the index handlebars template");
handlebars.register_template_string("index", String::from_utf8(theme.index.clone())?)?;
debug!("Register the head handlebars template");
handlebars.register_partial("head", String::from_utf8(theme.head.clone())?)?;
debug!("Register the redirect handlebars template");
handlebars
.register_template_string("redirect", String::from_utf8(theme.redirect.clone())?)?;
debug!("Register the header handlebars template");
handlebars.register_partial("header", String::from_utf8(theme.header.clone())?)?;
@@ -328,7 +480,7 @@ impl Renderer for HtmlHandlebars {
let mut print_content = String::new();
fs::create_dir_all(&destination)
.chain_err(|| "Unexpected error when constructing destination path")?;
.with_context(|| "Unexpected error when constructing destination path")?;
let mut is_index = true;
for item in book.iter() {
@@ -338,11 +490,17 @@ impl Renderer for HtmlHandlebars {
data: data.clone(),
is_index,
html_config: html_config.clone(),
edition: ctx.config.rust.edition,
};
self.render_item(item, ctx, &mut print_content)?;
is_index = false;
}
// Render 404 page
if html_config.input_404 != Some("".to_string()) {
self.render_404(ctx, &html_config, &src_dir, &mut handlebars, &mut data)?;
}
// Print version
self.configure_print_version(&mut data, &print_content);
if let Some(ref title) = ctx.config.book.title {
@@ -353,16 +511,17 @@ impl Renderer for HtmlHandlebars {
debug!("Render template");
let rendered = handlebars.render("index", &data)?;
let rendered = self.post_process(rendered, &html_config.playpen);
let rendered =
self.post_process(rendered, &html_config.playground, ctx.config.rust.edition);
utils::fs::write_file(&destination, "print.html", rendered.as_bytes())?;
debug!("Creating print.html ✓");
debug!("Copy static files");
self.copy_static_files(&destination, &theme, &html_config)
.chain_err(|| "Unable to copy across static files")?;
.with_context(|| "Unable to copy across static files")?;
self.copy_additional_css_and_js(&html_config, &ctx.root, &destination)
.chain_err(|| "Unable to copy across additional CSS and JS")?;
.with_context(|| "Unable to copy across additional CSS and JS")?;
// Render search index
#[cfg(feature = "search")]
@@ -373,8 +532,11 @@ impl Renderer for HtmlHandlebars {
}
}
// Copy all remaining files
utils::fs::copy_files_except_ext(&src_dir, &destination, true, &["md"])?;
self.emit_redirects(&ctx.destination, &handlebars, &html_config.redirect)
.context("Unable to emit redirects")?;
// Copy all remaining files, avoid a recursive copy from/to the book build dir
utils::fs::copy_files_except_ext(&src_dir, &destination, true, Some(&build_dir), &["md"])?;
Ok(())
}
@@ -401,20 +563,19 @@ fn make_data(
"description".to_owned(),
json!(config.book.description.clone().unwrap_or_default()),
);
data.insert("favicon".to_owned(), json!("favicon.png"));
if let Some(ref livereload) = html_config.livereload_url {
data.insert("livereload".to_owned(), json!(livereload));
}
let default_theme = match html_config.default_theme {
Some(ref theme) => theme,
None => "light",
Some(ref theme) => theme.to_lowercase(),
None => "light".to_string(),
};
data.insert("default_theme".to_owned(), json!(default_theme));
let preferred_dark_theme = match html_config.preferred_dark_theme {
Some(ref theme) => theme,
None => default_theme,
Some(ref theme) => theme.to_lowercase(),
None => "navy".to_string(),
};
data.insert(
"preferred_dark_theme".to_owned(),
@@ -430,6 +591,10 @@ fn make_data(
data.insert("mathjax_support".to_owned(), json!(true));
}
if html_config.copy_fonts {
data.insert("copy_fonts".to_owned(), json!(true));
}
// Add check to see if there is an additional style
if !html_config.additional_css.is_empty() {
let mut css = Vec::new();
@@ -454,14 +619,14 @@ fn make_data(
data.insert("additional_js".to_owned(), json!(js));
}
if html_config.playpen.editable && html_config.playpen.copy_js {
data.insert("playpen_js".to_owned(), json!(true));
if html_config.playpen.line_numbers {
data.insert("playpen_line_numbers".to_owned(), json!(true));
if html_config.playground.editable && html_config.playground.copy_js {
data.insert("playground_js".to_owned(), json!(true));
if html_config.playground.line_numbers {
data.insert("playground_line_numbers".to_owned(), json!(true));
}
}
if html_config.playpen.copyable {
data.insert("playpen_copyable".to_owned(), json!(true));
if html_config.playground.copyable {
data.insert("playground_copyable".to_owned(), json!(true));
}
data.insert("fold_enable".to_owned(), json!((html_config.fold.enable)));
@@ -500,6 +665,9 @@ fn make_data(
let mut chapter = BTreeMap::new();
match *item {
BookItem::PartTitle(ref title) => {
chapter.insert("part".to_owned(), json!(title));
}
BookItem::Chapter(ref ch) => {
if let Some(ref section) = ch.number {
chapter.insert("section".to_owned(), json!(section.to_string()));
@@ -511,11 +679,12 @@ fn make_data(
);
chapter.insert("name".to_owned(), json!(ch.name));
let path = ch
.path
.to_str()
.chain_err(|| "Could not convert path to str")?;
chapter.insert("path".to_owned(), json!(path));
if let Some(ref path) = ch.path {
let p = path
.to_str()
.with_context(|| "Could not convert path to str")?;
chapter.insert("path".to_owned(), json!(p));
}
}
BookItem::Separator => {
chapter.insert("spacer".to_owned(), json!("_spacer_"));
@@ -600,8 +769,11 @@ fn fix_code_blocks(html: &str) -> String {
.into_owned()
}
fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
let boring_line_regex = Regex::new(r"^(\s*)#(#|.)(.*)$").unwrap();
fn add_playground_pre(
html: &str,
playground_config: &Playground,
edition: Option<RustEdition>,
) -> String {
let regex = Regex::new(r##"((?s)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).unwrap();
regex
.replace_all(html, |caps: &Captures<'_>| {
@@ -609,57 +781,53 @@ fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
let classes = &caps[2];
let code = &caps[3];
if (classes.contains("language-rust")
&& !classes.contains("ignore")
&& !classes.contains("noplaypen"))
|| classes.contains("mdbook-runnable")
{
// wrap the contents in an external pre block
format!(
"<pre class=\"playpen\"><code class=\"{}\">{}</code></pre>",
classes,
{
let content: Cow<'_, str> = if playpen_config.editable
&& classes.contains("editable")
|| text.contains("fn main")
|| text.contains("quick_main!")
{
code.into()
} else {
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!(
"\n# #![allow(unused_variables)]\n{}#fn main() {{\n{}#}}",
attrs, code
)
.into()
};
let mut prev_line_hidden = false;
let mut result = String::with_capacity(content.len());
for line in content.lines() {
if let Some(caps) = boring_line_regex.captures(line) {
if !prev_line_hidden && &caps[2] != "#" {
result += "<span class=\"boring\">";
prev_line_hidden = true;
}
result += &caps[1];
if &caps[2] != " " {
result += &caps[2];
}
result += &caps[3];
} else {
if prev_line_hidden {
result += "</span>";
prev_line_hidden = false;
}
result += line;
}
result += "\n";
if classes.contains("language-rust") {
if (!classes.contains("ignore")
&& !classes.contains("noplayground")
&& !classes.contains("noplaypen"))
|| classes.contains("mdbook-runnable")
{
let contains_e2015 = classes.contains("edition2015");
let contains_e2018 = classes.contains("edition2018");
let edition_class = if contains_e2015 || contains_e2018 {
// the user forced edition, we should not overwrite it
""
} else {
match edition {
Some(RustEdition::E2015) => " edition2015",
Some(RustEdition::E2018) => " edition2018",
None => "",
}
result
}
)
};
// wrap the contents in an external pre block
format!(
"<pre class=\"playground\"><code class=\"{}{}\">{}</code></pre>",
classes,
edition_class,
{
let content: Cow<'_, str> = if playground_config.editable
&& classes.contains("editable")
|| text.contains("fn main")
|| text.contains("quick_main!")
{
code.into()
} else {
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!(
"\n# #![allow(unused)]\n{}#fn main() {{\n{}#}}",
attrs, code
)
.into()
};
hide_lines(&content)
}
)
} else {
format!("<code class=\"{}\">{}</code>", classes, hide_lines(code))
}
} else {
// not language-rust, so no-op
text.to_owned()
@@ -668,6 +836,38 @@ fn add_playpen_pre(html: &str, playpen_config: &Playpen) -> String {
.into_owned()
}
lazy_static! {
static ref BORING_LINES_REGEX: Regex = Regex::new(r"^(\s*)#(.?)(.*)$").unwrap();
}
fn hide_lines(content: &str) -> String {
let mut result = String::with_capacity(content.len());
for line in content.lines() {
if let Some(caps) = BORING_LINES_REGEX.captures(line) {
if &caps[2] == "#" {
result += &caps[1];
result += &caps[2];
result += &caps[3];
result += "\n";
continue;
} else if &caps[2] != "!" && &caps[2] != "[" {
result += "<span class=\"boring\">";
result += &caps[1];
if &caps[2] != " " {
result += &caps[2];
}
result += &caps[3];
result += "\n";
result += "</span>";
continue;
}
}
result += line;
result += "\n";
}
result
}
fn partition_source(s: &str) -> (String, String) {
let mut after_header = false;
let mut before = String::new();
@@ -690,11 +890,12 @@ fn partition_source(s: &str) -> (String, String) {
}
struct RenderItemContext<'a> {
handlebars: &'a Handlebars,
handlebars: &'a Handlebars<'a>,
destination: PathBuf,
data: serde_json::Map<String, serde_json::Value>,
is_index: bool,
html_config: HtmlConfig,
edition: Option<RustEdition>,
}
#[cfg(test)]
@@ -737,24 +938,79 @@ mod tests {
}
#[test]
fn add_playpen() {
fn add_playground() {
let inputs = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playpen\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused_variables)]\nfn main() {\n</span>x()\n<span class=\"boring\">}\n</code></pre>"),
"<pre class=\"playground\"><code class=\"language-rust\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playpen\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
"<pre class=\"playground\"><code class=\"language-rust\">fn main() {}\n</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n\";</code>",
"<pre class=\"playpen\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n ## bar\n\";</code>",
"<pre class=\"playpen\"><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 # bar\n\";\n</code></pre>"),
("<code class=\"language-rust editable\">let s = \"foo\n # bar\n#\n\";</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">let s = \"foo\n<span class=\"boring\"> bar\n</span><span class=\"boring\">\n</span>\";\n</code></pre>"),
("<code class=\"language-rust ignore\">let s = \"foo\n # bar\n\";</code>",
"<code class=\"language-rust ignore\">let s = \"foo\n<span class=\"boring\"> bar\n</span>\";\n</code>"),
("<code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]</code>",
"<pre class=\"playground\"><code class=\"language-rust editable\">#![no_std]\nlet s = \"foo\";\n #[some_attr]\n</code></pre>"),
];
for (src, should_be) in &inputs {
let got = add_playpen_pre(
let got = add_playground_pre(
src,
&Playpen {
&Playground {
editable: true,
..Playpen::default()
..Playground::default()
},
None,
);
assert_eq!(&*got, *should_be);
}
}
#[test]
fn add_playground_edition2015() {
let inputs = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
];
for (src, should_be) in &inputs {
let got = add_playground_pre(
src,
&Playground {
editable: true,
..Playground::default()
},
Some(RustEdition::E2015),
);
assert_eq!(&*got, *should_be);
}
}
#[test]
fn add_playground_edition2018() {
let inputs = [
("<code class=\"language-rust\">x()</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">\n<span class=\"boring\">#![allow(unused)]\n</span><span class=\"boring\">fn main() {\n</span>x()\n<span class=\"boring\">}\n</span></code></pre>"),
("<code class=\"language-rust\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
("<code class=\"language-rust edition2015\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2015\">fn main() {}\n</code></pre>"),
("<code class=\"language-rust edition2018\">fn main() {}</code>",
"<pre class=\"playground\"><code class=\"language-rust edition2018\">fn main() {}\n</code></pre>"),
];
for (src, should_be) in &inputs {
let got = add_playground_pre(
src,
&Playground {
editable: true,
..Playground::default()
},
Some(RustEdition::E2018),
);
assert_eq!(&*got, *should_be);
}

View File

@@ -46,7 +46,7 @@ impl Target {
fn find_chapter(
ctx: &Context,
rc: &mut RenderContext<'_>,
rc: &mut RenderContext<'_, '_>,
target: Target,
) -> Result<Option<StringMap>, RenderError> {
debug!("Get data from context");
@@ -69,7 +69,14 @@ fn find_chapter(
// "index.md" (unless there really is an index.md in SUMMARY.md).
match target {
Target::Previous => return Ok(None),
Target::Next => match chapters.iter().skip(1).next() {
Target::Next => match chapters
.iter()
.filter(|chapter| {
// Skip things like "spacer"
chapter.contains_key("path")
})
.nth(1)
{
Some(chapter) => return Ok(Some(chapter.clone())),
None => return Ok(None),
},
@@ -100,9 +107,9 @@ fn find_chapter(
fn render(
_h: &Helper<'_, '_>,
r: &Handlebars,
r: &Handlebars<'_>,
ctx: &Context,
rc: &mut RenderContext<'_>,
rc: &mut RenderContext<'_, '_>,
out: &mut dyn Output,
chapter: &StringMap,
) -> Result<(), RenderError> {
@@ -142,7 +149,7 @@ fn render(
_h.template()
.ok_or_else(|| RenderError::new("Error with the handlebars template"))
.and_then(|t| {
let mut local_rc = rc.new_for_block();
let mut local_rc = rc.clone();
let local_ctx = Context::wraps(&context)?;
t.render(r, &local_ctx, &mut local_rc, out)
})?;
@@ -152,9 +159,9 @@ fn render(
pub fn previous(
_h: &Helper<'_, '_>,
r: &Handlebars,
r: &Handlebars<'_>,
ctx: &Context,
rc: &mut RenderContext<'_>,
rc: &mut RenderContext<'_, '_>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
trace!("previous (handlebars helper)");
@@ -168,9 +175,9 @@ pub fn previous(
pub fn next(
_h: &Helper<'_, '_>,
r: &Handlebars,
r: &Handlebars<'_>,
ctx: &Context,
rc: &mut RenderContext<'_>,
rc: &mut RenderContext<'_, '_>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
trace!("next (handlebars helper)");

View File

@@ -2,9 +2,9 @@ use handlebars::{Context, Handlebars, Helper, Output, RenderContext, RenderError
pub fn theme_option(
h: &Helper<'_, '_>,
_r: &Handlebars,
_r: &Handlebars<'_>,
ctx: &Context,
rc: &mut RenderContext<'_>,
rc: &mut RenderContext<'_, '_>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
trace!("theme_option (handlebars helper)");

View File

@@ -16,9 +16,9 @@ impl HelperDef for RenderToc {
fn call<'reg: 'rc, 'rc>(
&self,
_h: &Helper<'reg, 'rc>,
_r: &'reg Handlebars,
_r: &'reg Handlebars<'_>,
ctx: &'rc Context,
rc: &mut RenderContext<'reg>,
rc: &mut RenderContext<'reg, 'rc>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
// get value from context data
@@ -32,7 +32,7 @@ impl HelperDef for RenderToc {
.evaluate(ctx, "@root/path")?
.as_json()
.as_str()
.ok_or(RenderError::new("Type error for `path`, string expected"))?
.ok_or_else(|| RenderError::new("Type error for `path`, string expected"))?
.replace("\"", "");
let current_section = rc
@@ -46,17 +46,13 @@ impl HelperDef for RenderToc {
.evaluate(ctx, "@root/fold_enable")?
.as_json()
.as_bool()
.ok_or(RenderError::new(
"Type error for `fold_enable`, bool expected",
))?;
.ok_or_else(|| RenderError::new("Type error for `fold_enable`, bool expected"))?;
let fold_level = rc
.evaluate(ctx, "@root/fold_level")?
.as_json()
.as_u64()
.ok_or(RenderError::new(
"Type error for `fold_level`, u64 expected",
))?;
.ok_or_else(|| RenderError::new("Type error for `fold_level`, u64 expected"))?;
out.write("<ol class=\"chapter\">")?;
@@ -75,18 +71,15 @@ impl HelperDef for RenderToc {
("", 1)
};
let is_expanded = {
if !fold_enable {
// Disable fold. Expand all chapters.
true
} else if !section.is_empty() && current_section.starts_with(section) {
// The section is ancestor or the current section itself.
let is_expanded =
if !fold_enable || (!section.is_empty() && current_section.starts_with(section)) {
// Expand if folding is disabled, or if the section is an
// ancestor or the current section itself.
true
} else {
// Levels that are larger than this would be folded.
level - 1 < fold_level as usize
}
};
};
if level > current_level {
while level > current_level {
@@ -106,6 +99,14 @@ impl HelperDef for RenderToc {
write_li_open_tag(out, is_expanded, item.get("section").is_none())?;
}
// Part title
if let Some(title) = item.get("part") {
out.write("<li class=\"part-title active\">")?;
out.write(title)?;
out.write("</li>")?;
continue;
}
// Link
let path_exists = if let Some(path) = item.get("path") {
if !path.is_empty() {
@@ -150,7 +151,7 @@ impl HelperDef for RenderToc {
// filter all events that are not inline code blocks
let parser = Parser::new(name).filter(|event| match *event {
Event::Code(_) | Event::InlineHtml(_) | Event::Text(_) => true,
Event::Code(_) | Event::Html(_) | Event::Text(_) => true,
_ => false,
});
@@ -191,7 +192,7 @@ fn write_li_open_tag(
is_expanded: bool,
is_affix: bool,
) -> Result<(), std::io::Error> {
let mut li = String::from("<li class=\"");
let mut li = String::from("<li class=\"chapter-item ");
if is_expanded {
li.push_str("expanded ");
}

View File

@@ -71,32 +71,35 @@ fn render_item(
item: &BookItem,
) -> Result<()> {
let chapter = match *item {
BookItem::Chapter(ref ch) => ch,
BookItem::Chapter(ref ch) if !ch.is_draft_chapter() => ch,
_ => return Ok(()),
};
let filepath = Path::new(&chapter.path).with_extension("html");
let chapter_path = chapter
.path
.as_ref()
.expect("Checked that path exists above");
let filepath = Path::new(&chapter_path).with_extension("html");
let filepath = filepath
.to_str()
.chain_err(|| "Could not convert HTML path to str")?;
.with_context(|| "Could not convert HTML path to str")?;
let anchor_base = utils::fs::normalize_path(filepath);
let p = utils::new_cmark_parser(&chapter.content);
let mut p = utils::new_cmark_parser(&chapter.content).peekable();
let mut in_header = false;
let max_section_depth = i32::from(search_config.heading_split_level);
let mut in_heading = false;
let max_section_depth = u32::from(search_config.heading_split_level);
let mut section_id = None;
let mut heading = String::new();
let mut body = String::new();
let mut html_block = String::new();
let mut breadcrumbs = chapter.parent_names.clone();
let mut footnote_numbers = HashMap::new();
for event in p {
while let Some(event) = p.next() {
match event {
Event::Start(Tag::Header(i)) if i <= max_section_depth => {
Event::Start(Tag::Heading(i)) if i <= max_section_depth => {
if !heading.is_empty() {
// Section finished, the next header is following now
// Section finished, the next heading is following now
// Write the data to the index, and clear it for the next section
add_doc(
index,
@@ -111,10 +114,10 @@ fn render_item(
breadcrumbs.pop();
}
in_header = true;
in_heading = true;
}
Event::End(Tag::Header(i)) if i <= max_section_depth => {
in_header = false;
Event::End(Tag::Heading(i)) if i <= max_section_depth => {
in_heading = false;
section_id = Some(utils::id_from_content(&heading));
breadcrumbs.push(heading.clone());
}
@@ -123,31 +126,34 @@ fn render_item(
footnote_numbers.entry(name).or_insert(number);
}
Event::Html(html) => {
html_block.push_str(&html);
}
Event::End(Tag::HtmlBlock) => {
let mut html_block = html.into_string();
// As of pulldown_cmark 0.6, html events are no longer contained
// in an HtmlBlock tag. We must collect consecutive Html events
// into a block ourselves.
while let Some(Event::Html(html)) = p.peek() {
html_block.push_str(&html);
p.next();
}
body.push_str(&clean_html(&html_block));
html_block.clear();
}
Event::Start(_) | Event::End(_) | Event::SoftBreak | Event::HardBreak => {
Event::Start(_) | Event::End(_) | Event::Rule | Event::SoftBreak | Event::HardBreak => {
// Insert spaces where HTML output would usually seperate text
// to ensure words don't get merged together
if in_header {
if in_heading {
heading.push(' ');
} else {
body.push(' ');
}
}
Event::Text(text) | Event::Code(text) => {
if in_header {
if in_heading {
heading.push_str(&text);
} else {
body.push_str(&text);
}
}
Event::InlineHtml(html) => {
body.push_str(&clean_html(&html));
}
Event::FootnoteReference(name) => {
let len = footnote_numbers.len() + 1;
let number = footnote_numbers.entry(name).or_insert(len);

View File

@@ -28,18 +28,24 @@ impl Renderer for MarkdownRenderer {
if destination.exists() {
utils::fs::remove_dir_content(destination)
.chain_err(|| "Unable to remove stale Markdown output")?;
.with_context(|| "Unable to remove stale Markdown output")?;
}
trace!("markdown render");
for item in book.iter() {
if let BookItem::Chapter(ref ch) = *item {
utils::fs::write_file(&ctx.destination, &ch.path, ch.content.as_bytes())?;
if !ch.is_draft_chapter() {
utils::fs::write_file(
&ctx.destination,
&ch.path.as_ref().expect("Checked path exists before"),
ch.content.as_bytes(),
)?;
}
}
}
fs::create_dir_all(&destination)
.chain_err(|| "Unexpected error when constructing destination path")?;
.with_context(|| "Unexpected error when constructing destination path")?;
Ok(())
}

View File

@@ -8,7 +8,7 @@
//!
//! The definition for [RenderContext] may be useful though.
//!
//! [For Developers]: https://rust-lang-nursery.github.io/mdBook/for_developers/index.html
//! [For Developers]: https://rust-lang.github.io/mdBook/for_developers/index.html
//! [RenderContext]: struct.RenderContext.html
pub use self::html_handlebars::HtmlHandlebars;
@@ -19,13 +19,14 @@ mod markdown_renderer;
use shlex::Shlex;
use std::fs;
use std::io::{self, Read};
use std::io::{self, ErrorKind, Read};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use crate::book::Book;
use crate::config::Config;
use crate::errors::*;
use toml::Value;
/// An arbitrary `mdbook` backend.
///
@@ -93,7 +94,7 @@ impl RenderContext {
/// Load a `RenderContext` from its JSON representation.
pub fn from_json<R: Read>(reader: R) -> Result<RenderContext> {
serde_json::from_reader(reader).chain_err(|| "Unable to deserialize the `RenderContext`")
serde_json::from_reader(reader).with_context(|| "Unable to deserialize the `RenderContext`")
}
}
@@ -149,6 +150,38 @@ impl CmdRenderer {
}
}
impl CmdRenderer {
fn handle_render_command_error(&self, ctx: &RenderContext, error: io::Error) -> Result<()> {
if let ErrorKind::NotFound = error.kind() {
// Look for "output.{self.name}.optional".
// If it exists and is true, treat this as a warning.
// Otherwise, fail the build.
let optional_key = format!("output.{}.optional", self.name);
let is_optional = match ctx.config.get(&optional_key) {
Some(Value::Boolean(value)) => *value,
_ => false,
};
if is_optional {
warn!(
"The command `{}` for backend `{}` was not found, \
but was marked as optional.",
self.cmd, self.name
);
return Ok(());
} else {
error!(
"The command `{}` wasn't found, is the `{}` backend installed?",
self.cmd, self.name
);
}
}
Err(error).with_context(|| "Unable to start the backend")?
}
}
impl Renderer for CmdRenderer {
fn name(&self) -> &str {
&self.name
@@ -168,34 +201,22 @@ impl Renderer for CmdRenderer {
.spawn()
{
Ok(c) => c,
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
warn!(
"The command wasn't found, is the \"{}\" backend installed?",
self.name
);
warn!("\tCommand: {}", self.cmd);
return Ok(());
}
Err(e) => {
return Err(e).chain_err(|| "Unable to start the backend")?;
}
Err(e) => return self.handle_render_command_error(ctx, e),
};
{
let mut stdin = child.stdin.take().expect("Child has stdin");
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
// Looks like the backend hung up before we could finish
// sending it the render context. Log the error and keep going
warn!("Error writing the RenderContext to the backend, {}", e);
}
// explicitly close the `stdin` file handle
drop(stdin);
let mut stdin = child.stdin.take().expect("Child has stdin");
if let Err(e) = serde_json::to_writer(&mut stdin, &ctx) {
// Looks like the backend hung up before we could finish
// sending it the render context. Log the error and keep going
warn!("Error writing the RenderContext to the backend, {}", e);
}
// explicitly close the `stdin` file handle
drop(stdin);
let status = child
.wait()
.chain_err(|| "Error waiting for the backend to complete")?;
.with_context(|| "Error waiting for the backend to complete")?;
trace!("{} exited with output: {:?}", self.cmd, status);

View File

@@ -12,8 +12,7 @@ Original by Dempfi (https://github.com/dempfi/ayu)
}
.hljs-comment,
.hljs-quote,
.hljs-meta {
.hljs-quote {
color: #5c6773;
font-style: italic;
}
@@ -30,6 +29,7 @@ Original by Dempfi (https://github.com/dempfi/ayu)
}
.hljs-number,
.hljs-meta,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,

View File

@@ -4,8 +4,8 @@
window.onunload = function () { };
// Global variable, shared between modules
function playpen_text(playpen) {
let code_block = playpen.querySelector("code");
function playground_text(playground) {
let code_block = playground.querySelector("code");
if (window.ace && code_block.classList.contains("editable")) {
let editor = window.ace.edit(code_block);
@@ -16,9 +16,6 @@ function playpen_text(playpen) {
}
(function codeSnippets() {
// Hide Rust code lines prepended with a specific character
var hiding_character = "#";
function fetch_with_timeout(url, options, timeout = 6000) {
return Promise.race([
fetch(url, options),
@@ -26,8 +23,8 @@ function playpen_text(playpen) {
]);
}
var playpens = Array.from(document.querySelectorAll(".playpen"));
if (playpens.length > 0) {
var playgrounds = Array.from(document.querySelectorAll(".playground"));
if (playgrounds.length > 0) {
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
headers: {
'Content-Type': "application/json",
@@ -39,21 +36,21 @@ function playpen_text(playpen) {
.then(response => {
// get list of crates available in the rust playground
let playground_crates = response.crates.map(item => item["id"]);
playpens.forEach(block => handle_crate_list_update(block, playground_crates));
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
});
}
function handle_crate_list_update(playpen_block, playground_crates) {
function handle_crate_list_update(playground_block, playground_crates) {
// update the play buttons after receiving the response
update_play_button(playpen_block, playground_crates);
update_play_button(playground_block, playground_crates);
// and install on change listener to dynamically update ACE editors
if (window.ace) {
let code_block = playpen_block.querySelector("code");
let code_block = playground_block.querySelector("code");
if (code_block.classList.contains("editable")) {
let editor = window.ace.edit(code_block);
editor.addEventListener("change", function (e) {
update_play_button(playpen_block, playground_crates);
update_play_button(playground_block, playground_crates);
});
// add Ctrl-Enter command to execute rust code
editor.commands.addCommand({
@@ -62,7 +59,7 @@ function playpen_text(playpen) {
win: "Ctrl-Enter",
mac: "Ctrl-Enter"
},
exec: _editor => run_rust_code(playpen_block)
exec: _editor => run_rust_code(playground_block)
});
}
}
@@ -80,7 +77,7 @@ function playpen_text(playpen) {
}
// get list of `extern crate`'s from snippet
var txt = playpen_text(pre_block);
var txt = playground_text(pre_block);
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
var snippet_crates = [];
var item;
@@ -109,7 +106,7 @@ function playpen_text(playpen) {
code_block.append(result_block);
}
let text = playpen_text(code_block);
let text = playground_text(code_block);
let classes = code_block.querySelector('code').classList;
let has_2018 = classes.contains("edition2018");
let edition = has_2018 ? "2018" : "2015";
@@ -146,6 +143,11 @@ function playpen_text(playpen) {
languages: [], // Languages used for auto-detection
});
let code_nodes = Array
.from(document.querySelectorAll('code'))
// Don't highlight `inline code` blocks in headers.
.filter(function (node) {return !node.parentElement.classList.contains("header"); });
if (window.ace) {
// language-rust class needs to be removed for editable
// blocks or highlightjs will capture events
@@ -157,16 +159,12 @@ function playpen_text(playpen) {
.from(document.querySelectorAll('code:not(.editable)'))
.forEach(function (block) { hljs.highlightBlock(block); });
} else {
Array
.from(document.querySelectorAll('code'))
.forEach(function (block) { hljs.highlightBlock(block); });
code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
}
// Adding the hljs class gives code blocks the color css
// even if highlighting doesn't apply
Array
.from(document.querySelectorAll('code'))
.forEach(function (block) { block.classList.add('hljs'); });
code_nodes.forEach(function (block) { block.classList.add('hljs'); });
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
@@ -202,10 +200,10 @@ function playpen_text(playpen) {
});
});
if (window.playpen_copyable) {
if (window.playground_copyable) {
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
var pre_block = block.parentNode;
if (!pre_block.classList.contains('playpen')) {
if (!pre_block.classList.contains('playground')) {
var buttons = pre_block.querySelector(".buttons");
if (!buttons) {
buttons = document.createElement('div');
@@ -224,8 +222,8 @@ function playpen_text(playpen) {
});
}
// Process playpen code blocks
Array.from(document.querySelectorAll(".playpen")).forEach(function (pre_block) {
// Process playground code blocks
Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) {
// Add play button
var buttons = pre_block.querySelector(".buttons");
if (!buttons) {
@@ -245,7 +243,7 @@ function playpen_text(playpen) {
run_rust_code(pre_block);
});
if (window.playpen_copyable) {
if (window.playground_copyable) {
var copyCodeClipboardButton = document.createElement('button');
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
@@ -287,7 +285,7 @@ function playpen_text(playpen) {
function showThemes() {
themePopup.style.display = 'block';
themeToggleButton.setAttribute('aria-expanded', true);
themePopup.querySelector("button#" + document.body.className).focus();
themePopup.querySelector("button#" + get_theme()).focus();
}
function hideThemes() {
@@ -296,7 +294,17 @@ function playpen_text(playpen) {
themeToggleButton.focus();
}
function set_theme(theme) {
function get_theme() {
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
if (theme === null || theme === undefined) {
return default_theme;
} else {
return theme;
}
}
function set_theme(theme, store = true) {
let ace_theme;
if (theme == 'coal' || theme == 'navy') {
@@ -327,23 +335,20 @@ function playpen_text(playpen) {
});
}
var previousTheme;
try { previousTheme = localStorage.getItem('mdbook-theme'); } catch (e) { }
if (previousTheme === null || previousTheme === undefined) { previousTheme = default_theme; }
var previousTheme = get_theme();
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
if (store) {
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
}
document.body.className = theme;
html.classList.remove(previousTheme);
html.classList.add(theme);
}
// Set theme
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var theme = get_theme();
set_theme(theme);
set_theme(theme, false);
themeToggleButton.addEventListener('click', function () {
if (themePopup.style.display === 'block') {
@@ -365,7 +370,7 @@ function playpen_text(playpen) {
}
});
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang-nursery/mdBook/issues/628
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628
document.addEventListener('click', function(e) {
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
hideThemes();
@@ -410,7 +415,6 @@ function playpen_text(playpen) {
(function sidebar() {
var html = document.querySelector("html");
var sidebar = document.getElementById("sidebar");
var sidebarScrollBox = document.getElementById("sidebar-scrollbox");
var sidebarLinks = document.querySelectorAll('#sidebar a');
var sidebarToggleButton = document.getElementById("sidebar-toggle");
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
@@ -452,6 +456,11 @@ function playpen_text(playpen) {
// Toggle sidebar
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
if (html.classList.contains("sidebar-hidden")) {
var current_width = parseInt(
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
if (current_width < 150) {
document.documentElement.style.setProperty('--sidebar-width', '150px');
}
showSidebar();
} else if (html.classList.contains("sidebar-visible")) {
hideSidebar();
@@ -472,7 +481,16 @@ function playpen_text(playpen) {
html.classList.add('sidebar-resizing');
}
function resize(e) {
document.documentElement.style.setProperty('--sidebar-width', (e.clientX - sidebar.offsetLeft) + 'px');
var pos = (e.clientX - sidebar.offsetLeft);
if (pos < 20) {
hideSidebar();
} else {
if (html.classList.contains("sidebar-hidden")) {
showSidebar();
}
pos = Math.min(pos, window.innerWidth - 100);
document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
}
}
//on mouseup remove windows functions mousemove & mouseup
function stopResize(e) {
@@ -507,9 +525,10 @@ function playpen_text(playpen) {
}, { passive: true });
// Scroll sidebar to current active section
var activeSection = sidebar.querySelector(".active");
var activeSection = document.getElementById("sidebar").querySelector(".active");
if (activeSection) {
sidebarScrollBox.scrollTop = activeSection.offsetTop;
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
activeSection.scrollIntoView({ block: 'center' });
}
})();
@@ -553,8 +572,8 @@ function playpen_text(playpen) {
var clipboardSnippets = new ClipboardJS('.clip-button', {
text: function (trigger) {
hideTooltip(trigger);
let playpen = trigger.closest("pre");
return playpen_text(playpen);
let playground = trigger.closest("pre");
return playground_text(playground);
}
});
@@ -582,26 +601,60 @@ function playpen_text(playpen) {
});
})();
(function autoHideMenu() {
(function controllMenu() {
var menu = document.getElementById('menu-bar');
var previousScrollTop = document.scrollingElement.scrollTop;
document.addEventListener('scroll', function () {
if (menu.classList.contains('folded') && document.scrollingElement.scrollTop < previousScrollTop) {
menu.classList.remove('folded');
} else if (!menu.classList.contains('folded') && document.scrollingElement.scrollTop > previousScrollTop) {
menu.classList.add('folded');
}
if (!menu.classList.contains('bordered') && document.scrollingElement.scrollTop > 0) {
menu.classList.add('bordered');
}
if (menu.classList.contains('bordered') && document.scrollingElement.scrollTop === 0) {
menu.classList.remove('bordered');
}
previousScrollTop = Math.max(document.scrollingElement.scrollTop, 0);
}, { passive: true });
(function controllPosition() {
var scrollTop = document.scrollingElement.scrollTop;
var prevScrollTop = scrollTop;
var minMenuY = -menu.clientHeight - 50;
// When the script loads, the page can be at any scroll (e.g. if you reforesh it).
menu.style.top = scrollTop + 'px';
// Same as parseInt(menu.style.top.slice(0, -2), but faster
var topCache = menu.style.top.slice(0, -2);
menu.classList.remove('sticky');
var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
document.addEventListener('scroll', function () {
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
// `null` means that it doesn't need to be updated
var nextSticky = null;
var nextTop = null;
var scrollDown = scrollTop > prevScrollTop;
var menuPosAbsoluteY = topCache - scrollTop;
if (scrollDown) {
nextSticky = false;
if (menuPosAbsoluteY > 0) {
nextTop = prevScrollTop;
}
} else {
if (menuPosAbsoluteY > 0) {
nextSticky = true;
} else if (menuPosAbsoluteY < minMenuY) {
nextTop = prevScrollTop + minMenuY;
}
}
if (nextSticky === true && stickyCache === false) {
menu.classList.add('sticky');
stickyCache = true;
} else if (nextSticky === false && stickyCache === true) {
menu.classList.remove('sticky');
stickyCache = false;
}
if (nextTop !== null) {
menu.style.top = nextTop + 'px';
topCache = nextTop;
}
prevScrollTop = scrollTop;
}, { passive: true });
})();
(function controllBorder() {
menu.classList.remove('bordered');
document.addEventListener('scroll', function () {
if (menu.offsetTop === 0) {
menu.classList.remove('bordered');
} else {
menu.classList.add('bordered');
}
}, { passive: true });
})();
})();

View File

@@ -20,14 +20,13 @@ a > .hljs {
/* Menu Bar */
#menu-bar {
position: -webkit-sticky;
position: sticky;
top: 0;
#menu-bar,
#menu-bar-hover-placeholder {
z-index: 101;
margin: auto calc(0px - var(--page-padding));
}
#menu-bar > #menu-bar-sticky-container {
#menu-bar {
position: relative;
display: flex;
flex-wrap: wrap;
background-color: var(--bg);
@@ -35,17 +34,28 @@ a > .hljs {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.js #menu-bar > #menu-bar-sticky-container {
transition: transform 0.3s;
#menu-bar.sticky,
.js #menu-bar-hover-placeholder:hover + #menu-bar,
.js #menu-bar:hover,
.js.sidebar-visible #menu-bar {
position: -webkit-sticky;
position: sticky;
top: 0 !important;
}
#menu-bar.bordered > #menu-bar-sticky-container {
#menu-bar-hover-placeholder {
position: sticky;
position: -webkit-sticky;
top: 0;
height: var(--menu-bar-height);
}
#menu-bar.bordered {
border-bottom-color: var(--table-border-color);
}
#menu-bar i, #menu-bar .icon-button {
position: relative;
padding: 0 8px;
z-index: 10;
line-height: 50px;
line-height: var(--menu-bar-height);
cursor: pointer;
transition: color 0.5s;
}
@@ -72,10 +82,6 @@ a > .hljs {
text-decoration: none;
}
html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-container {
transform: translateY(-60px);
}
.left-buttons {
display: flex;
margin: 0 5px;
@@ -87,8 +93,8 @@ html:not(.sidebar-visible) #menu-bar:not(:hover).folded > #menu-bar-sticky-conta
.menu-title {
display: inline-block;
font-weight: 200;
font-size: 20px;
line-height: 50px;
font-size: 2rem;
line-height: var(--menu-bar-height);
text-align: center;
margin: 0;
flex: 1;
@@ -417,6 +423,11 @@ ul#searchresults span.teaser em {
display: none;
}
.chapter li.chapter-item {
line-height: 1.5em;
margin-top: 0.6em;
}
.chapter li.expanded > a.toggle div {
transform: rotate(90deg);
}
@@ -446,7 +457,7 @@ ul#searchresults span.teaser em {
.theme-popup {
position: absolute;
left: 10px;
top: 50px;
top: var(--menu-bar-height);
z-index: 1000;
border-radius: 4px;
font-size: 0.7em;

View File

@@ -2,6 +2,11 @@
@import 'variables.css';
:root {
/* Browser default font-size is 16px, this way 1 rem = 10px */
font-size: 62.5%;
}
html {
font-family: "Open Sans", sans-serif;
color: var(--fg);
@@ -11,7 +16,7 @@ html {
body {
margin: 0;
font-size: 1rem;
font-size: 1.6rem;
overflow-x: hidden;
}
@@ -20,11 +25,16 @@ code {
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
}
/* Don't change font size in headers. */
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
font-size: unset;
}
.left { float: left; }
.right { float: right; }
.boring { opacity: 0.6; }
.hide-boring .boring { display: none; }
.hidden { display: none; }
.hidden { display: none !important; }
h2, h3 { margin-top: 2.5em; }
h4, h5 { margin-top: 2em; }
@@ -45,9 +55,17 @@ h4 a.header:target::before {
width: 30px;
}
h1 a.header:target,
h2 a.header:target,
h3 a.header:target,
h4 a.header:target {
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
}
.page {
outline: 0;
padding: 0 var(--page-padding);
margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
}
.page-wrapper {
box-sizing: border-box;
@@ -66,6 +84,9 @@ h4 a.header:target::before {
margin-right: auto;
max-width: var(--content-max-width);
}
.content p { line-height: 1.45em; }
.content ol { line-height: 1.45em; }
.content ul { line-height: 1.45em; }
.content a { text-decoration: none; }
.content a:hover { text-decoration: underline; }
.content img { max-width: 100%; }
@@ -145,3 +166,9 @@ blockquote {
.tooltipped .tooltiptext {
visibility: visible;
}
.chapter li.part-title {
color: var(--sidebar-fg);
margin: 5px 0px;
font-weight: bold;
}

View File

@@ -5,6 +5,7 @@
--sidebar-width: 300px;
--page-padding: 15px;
--content-max-width: 750px;
--menu-bar-height: 50px;
}
/* Themes */
@@ -208,3 +209,45 @@
--searchresults-li-bg: #dec2a2;
--search-mark-bg: #e69f67;
}
@media (prefers-color-scheme: dark) {
.light.no-js {
--bg: hsl(200, 7%, 8%);
--fg: #98a3ad;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
--sidebar-non-existant: #505254;
--sidebar-active: #3473ad;
--sidebar-spacer: #393939;
--scrollbar: var(--sidebar-fg);
--icons: #43484d;
--icons-hover: #b3c0cc;
--links: #2b79a2;
--inline-code-color: #c5c8c6;;
--theme-popup-bg: #141617;
--theme-popup-border: #43484d;
--theme-hover: #1f2124;
--quote-bg: hsl(234, 21%, 18%);
--quote-border: hsl(234, 21%, 23%);
--table-border-color: hsl(200, 7%, 13%);
--table-header-bg: hsl(200, 7%, 28%);
--table-alternate-bg: hsl(200, 7%, 11%);
--searchbar-border-color: #aaa;
--searchbar-bg: #b7b7b7;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #98a3ad;
--searchresults-li-bg: #2b2b2f;
--search-mark-bg: #355c7d;
}
}

22
src/theme/favicon.svg Executable file
View File

@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 199.7 184.2">
<style>
@media (prefers-color-scheme: dark) {
svg { fill: white; }
}
</style>
<path d="M189.5,36.8c0.2,2.8,0,5.1-0.6,6.8L153,162c-0.6,2.1-2,3.7-4.2,5c-2.2,1.2-4.4,1.9-6.7,1.9H31.4c-9.6,0-15.3-2.8-17.3-8.4
c-0.8-2.2-0.8-3.9,0.1-5.2c0.9-1.2,2.4-1.8,4.6-1.8H123c7.4,0,12.6-1.4,15.4-4.1s5.7-8.9,8.6-18.4l32.9-108.6
c1.8-5.9,1-11.1-2.2-15.6S169.9,0,164,0H72.7c-1,0-3.1,0.4-6.1,1.1l0.1-0.4C64.5,0.2,62.6,0,61,0.1s-3,0.5-4.3,1.4
c-1.3,0.9-2.4,1.8-3.2,2.8S52,6.5,51.2,8.1c-0.8,1.6-1.4,3-1.9,4.3s-1.1,2.7-1.8,4.2c-0.7,1.5-1.3,2.7-2,3.7c-0.5,0.6-1.2,1.5-2,2.5
s-1.6,2-2.2,2.8s-0.9,1.5-1.1,2.2c-0.2,0.7-0.1,1.8,0.2,3.2c0.3,1.4,0.4,2.4,0.4,3.1c-0.3,3-1.4,6.9-3.3,11.6
c-1.9,4.7-3.6,8.1-5.1,10.1c-0.3,0.4-1.2,1.3-2.6,2.7c-1.4,1.4-2.3,2.6-2.6,3.7c-0.3,0.4-0.3,1.5-0.1,3.4c0.3,1.8,0.4,3.1,0.3,3.8
c-0.3,2.7-1.3,6.3-3,10.8c-1.7,4.5-3.4,8.2-5,11c-0.2,0.5-0.9,1.4-2,2.8c-1.1,1.4-1.8,2.5-2,3.4c-0.2,0.6-0.1,1.8,0.1,3.4
c0.2,1.6,0.2,2.8-0.1,3.6c-0.6,3-1.8,6.7-3.6,11c-1.8,4.3-3.6,7.9-5.4,11c-0.5,0.8-1.1,1.7-2,2.8c-0.8,1.1-1.5,2-2,2.8
s-0.8,1.6-1,2.5c-0.1,0.5,0,1.3,0.4,2.3c0.3,1.1,0.4,1.9,0.4,2.6c-0.1,1.1-0.2,2.6-0.5,4.4c-0.2,1.8-0.4,2.9-0.4,3.2
c-1.8,4.8-1.7,9.9,0.2,15.2c2.2,6.2,6.2,11.5,11.9,15.8c5.7,4.3,11.7,6.4,17.8,6.4h110.7c5.2,0,10.1-1.7,14.7-5.2s7.7-7.8,9.2-12.9
l33-108.6c1.8-5.8,1-10.9-2.2-15.5C194.9,39.7,192.6,38,189.5,36.8z M59.6,122.8L73.8,80c0,0,7,0,10.8,0s28.8-1.7,25.4,17.5
c-3.4,19.2-18.8,25.2-36.8,25.4S59.6,122.8,59.6,122.8z M78.6,116.8c4.7-0.1,18.9-2.9,22.1-17.1S89.2,86.3,89.2,86.3l-8.9,0
l-10.2,30.5C70.2,116.9,74,116.9,78.6,116.8z M75.3,68.7L89,26.2h9.8l0.8,34l23.6-34h9.9l-13.6,42.5h-7.1l12.5-35.4l-24.5,35.4h-6.8
l-0.8-35L82,68.7H75.3z"/>
</svg>
<!-- Original image Copyright Dave Gandy — CC BY 4.0 License -->

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,93 @@
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

101
src/theme/fonts/fonts.css Normal file
View File

@@ -0,0 +1,101 @@
/* Open Sans is licensed under the Apache License, Version 2.0. See http://www.apache.org/licenses/LICENSE-2.0 */
/* Source Code Pro is under the Open Font License. See https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL */
/* open-sans-300 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 300;
src: local('Open Sans Light'), local('OpenSans-Light'),
url('open-sans-v17-all-charsets-300.woff2') format('woff2');
}
/* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 300;
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'),
url('open-sans-v17-all-charsets-300italic.woff2') format('woff2');
}
/* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 400;
src: local('Open Sans Regular'), local('OpenSans-Regular'),
url('open-sans-v17-all-charsets-regular.woff2') format('woff2');
}
/* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 400;
src: local('Open Sans Italic'), local('OpenSans-Italic'),
url('open-sans-v17-all-charsets-italic.woff2') format('woff2');
}
/* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 600;
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
url('open-sans-v17-all-charsets-600.woff2') format('woff2');
}
/* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 600;
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'),
url('open-sans-v17-all-charsets-600italic.woff2') format('woff2');
}
/* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 700;
src: local('Open Sans Bold'), local('OpenSans-Bold'),
url('open-sans-v17-all-charsets-700.woff2') format('woff2');
}
/* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 700;
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),
url('open-sans-v17-all-charsets-700italic.woff2') format('woff2');
}
/* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: normal;
font-weight: 800;
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'),
url('open-sans-v17-all-charsets-800.woff2') format('woff2');
}
/* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Open Sans';
font-style: italic;
font-weight: 800;
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'),
url('open-sans-v17-all-charsets-800italic.woff2') format('woff2');
}
/* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */
@font-face {
font-family: 'Source Code Pro';
font-style: normal;
font-weight: 500;
src: local('Source Code Pro Medium'), local('SourceCodePro-Medium'),
url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
}

61
src/theme/fonts/mod.rs Normal file
View File

@@ -0,0 +1,61 @@
pub static CSS: &[u8] = include_bytes!("fonts.css");
// An array of (file_name, file_contents) pairs
pub static LICENSES: [(&str, &[u8]); 2] = [
(
"fonts/OPEN-SANS-LICENSE.txt",
include_bytes!("OPEN-SANS-LICENSE.txt"),
),
(
"fonts/SOURCE-CODE-PRO-LICENSE.txt",
include_bytes!("SOURCE-CODE-PRO-LICENSE.txt"),
),
];
// An array of (file_name, file_contents) pairs
pub static OPEN_SANS: [(&str, &[u8]); 10] = [
(
"fonts/open-sans-v17-all-charsets-300.woff2",
include_bytes!("open-sans-v17-all-charsets-300.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-300italic.woff2",
include_bytes!("open-sans-v17-all-charsets-300italic.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-regular.woff2",
include_bytes!("open-sans-v17-all-charsets-regular.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-italic.woff2",
include_bytes!("open-sans-v17-all-charsets-italic.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-600.woff2",
include_bytes!("open-sans-v17-all-charsets-600.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-600italic.woff2",
include_bytes!("open-sans-v17-all-charsets-600italic.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-700.woff2",
include_bytes!("open-sans-v17-all-charsets-700.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-700italic.woff2",
include_bytes!("open-sans-v17-all-charsets-700italic.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-800.woff2",
include_bytes!("open-sans-v17-all-charsets-800.woff2"),
),
(
"fonts/open-sans-v17-all-charsets-800italic.woff2",
include_bytes!("open-sans-v17-all-charsets-800italic.woff2"),
),
];
// A (file_name, file_contents) pair
pub static SOURCE_CODE_PRO: (&str, &[u8]) = (
"fonts/source-code-pro-v11-all-charsets-500.woff2",
include_bytes!("source-code-pro-v11-all-charsets-500.woff2"),
);

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
src/theme/head.hbs Normal file
View File

@@ -0,0 +1 @@
{{!-- Put your head HTML text here --}}

View File

@@ -1,5 +1,5 @@
<!DOCTYPE HTML>
<html lang="{{ language }}" class="sidebar-visible no-js">
<html lang="{{ language }}" class="sidebar-visible no-js {{ default_theme }}">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
@@ -7,13 +7,21 @@
{{#if is_print }}
<meta name="robots" content="noindex" />
{{/if}}
{{#if base_url}}
<base href="{{ base_url }}">
{{/if}}
<!-- Custom HTML head -->
{{> head}}
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="shortcut icon" href="{{ path_to_root }}{{ favicon }}">
<link rel="icon" href="{{ path_to_root }}favicon.svg">
<link rel="shortcut icon" href="{{ path_to_root }}favicon.png">
<link rel="stylesheet" href="{{ path_to_root }}css/variables.css">
<link rel="stylesheet" href="{{ path_to_root }}css/general.css">
<link rel="stylesheet" href="{{ path_to_root }}css/chrome.css">
@@ -21,8 +29,9 @@
<!-- Fonts -->
<link rel="stylesheet" href="{{ path_to_root }}FontAwesome/css/font-awesome.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300italic,400italic,600italic,700italic,800italic,400,300,600,700,800" rel="stylesheet" type="text/css">
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro:500" rel="stylesheet" type="text/css">
{{#if copy_fonts}}
<link rel="stylesheet" href="{{ path_to_root }}fonts/fonts.css">
{{/if}}
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="{{ path_to_root }}highlight.css">
@@ -39,7 +48,7 @@
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}}
</head>
<body class="{{ default_theme }}">
<body>
<!-- Provide site root to javascript -->
<script type="text/javascript">
var path_to_root = "{{ path_to_root }}";
@@ -67,8 +76,11 @@
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
document.body.className = theme;
document.querySelector('html').className = theme + ' js';
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('{{ default_theme }}')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
@@ -84,7 +96,7 @@
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div id="sidebar-scrollbox" class="sidebar-scrollbox">
<div class="sidebar-scrollbox">
{{#toc}}{{/toc}}
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
@@ -94,41 +106,40 @@
<div class="page">
{{> header}}
<div id="menu-bar" class="menu-bar">
<div id="menu-bar-sticky-container">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">{{ theme_option "Light" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">{{ theme_option "Rust" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">{{ theme_option "Coal" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">{{ theme_option "Navy" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">{{ theme_option "Ayu" }}</button></li>
</ul>
{{#if search_enabled}}
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
{{/if}}
</div>
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">{{ theme_option "Light" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">{{ theme_option "Rust" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">{{ theme_option "Coal" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">{{ theme_option "Navy" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">{{ theme_option "Ayu" }}</button></li>
</ul>
{{#if search_enabled}}
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
{{/if}}
</div>
<h1 class="menu-title">{{ book_title }}</h1>
<h1 class="menu-title">{{ book_title }}</h1>
<div class="right-buttons">
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
{{#if git_repository_url}}
<a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
</a>
{{/if}}
</div>
<div class="right-buttons">
<a href="{{ path_to_root }}print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
{{#if git_repository_url}}
<a href="{{git_repository_url}}" title="Git repository" aria-label="Git repository">
<i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
</a>
{{/if}}
</div>
</div>
@@ -180,13 +191,13 @@
<nav class="nav-wide-wrapper" aria-label="Page navigation">
{{#previous}}
<a href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<a rel="prev" href="{{ path_to_root }}{{link}}" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
<i class="fa fa-angle-left"></i>
</a>
{{/previous}}
{{#next}}
<a href="{{ path_to_root }}{{link}}" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
<a rel="next" 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}}
@@ -201,7 +212,7 @@
socket.onmessage = function (event) {
if (event.data === "reload") {
socket.close();
location.reload(true); // force reload from server (not from cache)
location.reload();
}
};
@@ -230,19 +241,19 @@
</script>
{{/if}}
{{#if playpen_line_numbers}}
{{#if playground_line_numbers}}
<script type="text/javascript">
window.playpen_line_numbers = true;
</script>
{{/if}}
{{#if playpen_copyable}}
<script type="text/javascript">
window.playpen_copyable = true;
window.playground_line_numbers = true;
</script>
{{/if}}
{{#if playpen_js}}
{{#if playground_copyable}}
<script type="text/javascript">
window.playground_copyable = true;
</script>
{{/if}}
{{#if playground_js}}
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script>

View File

@@ -1,6 +1,8 @@
#![allow(missing_docs)]
pub mod playpen_editor;
pub mod playground_editor;
pub mod fonts;
#[cfg(feature = "search")]
pub mod searcher;
@@ -12,12 +14,15 @@ use std::path::Path;
use crate::errors::*;
pub static INDEX: &[u8] = include_bytes!("index.hbs");
pub static HEAD: &[u8] = include_bytes!("head.hbs");
pub static REDIRECT: &[u8] = include_bytes!("redirect.hbs");
pub static HEADER: &[u8] = include_bytes!("header.hbs");
pub static CHROME_CSS: &[u8] = include_bytes!("css/chrome.css");
pub static GENERAL_CSS: &[u8] = include_bytes!("css/general.css");
pub static PRINT_CSS: &[u8] = include_bytes!("css/print.css");
pub static VARIABLES_CSS: &[u8] = include_bytes!("css/variables.css");
pub static FAVICON: &[u8] = include_bytes!("favicon.png");
pub static FAVICON_PNG: &[u8] = include_bytes!("favicon.png");
pub static FAVICON_SVG: &[u8] = include_bytes!("favicon.svg");
pub static JS: &[u8] = include_bytes!("book.js");
pub static HIGHLIGHT_JS: &[u8] = include_bytes!("highlight.js");
pub static TOMORROW_NIGHT_CSS: &[u8] = include_bytes!("tomorrow-night.css");
@@ -42,12 +47,15 @@ pub static FONT_AWESOME_OTF: &[u8] = include_bytes!("FontAwesome/fonts/FontAweso
#[derive(Debug, PartialEq)]
pub struct Theme {
pub index: Vec<u8>,
pub head: Vec<u8>,
pub redirect: Vec<u8>,
pub header: Vec<u8>,
pub chrome_css: Vec<u8>,
pub general_css: Vec<u8>,
pub print_css: Vec<u8>,
pub variables_css: Vec<u8>,
pub favicon: Vec<u8>,
pub favicon_png: Vec<u8>,
pub favicon_svg: Vec<u8>,
pub js: Vec<u8>,
pub highlight_css: Vec<u8>,
pub tomorrow_night_css: Vec<u8>,
@@ -72,6 +80,8 @@ impl Theme {
{
let files = vec![
(theme_dir.join("index.hbs"), &mut theme.index),
(theme_dir.join("head.hbs"), &mut theme.head),
(theme_dir.join("redirect.hbs"), &mut theme.redirect),
(theme_dir.join("header.hbs"), &mut theme.header),
(theme_dir.join("book.js"), &mut theme.js),
(theme_dir.join("css/chrome.css"), &mut theme.chrome_css),
@@ -81,7 +91,8 @@ impl Theme {
theme_dir.join("css/variables.css"),
&mut theme.variables_css,
),
(theme_dir.join("favicon.png"), &mut theme.favicon),
(theme_dir.join("favicon.png"), &mut theme.favicon_png),
(theme_dir.join("favicon.svg"), &mut theme.favicon_svg),
(theme_dir.join("highlight.js"), &mut theme.highlight_js),
(theme_dir.join("clipboard.min.js"), &mut theme.clipboard_js),
(theme_dir.join("highlight.css"), &mut theme.highlight_css),
@@ -114,12 +125,15 @@ impl Default for Theme {
fn default() -> Theme {
Theme {
index: INDEX.to_owned(),
head: HEAD.to_owned(),
redirect: REDIRECT.to_owned(),
header: HEADER.to_owned(),
chrome_css: CHROME_CSS.to_owned(),
general_css: GENERAL_CSS.to_owned(),
print_css: PRINT_CSS.to_owned(),
variables_css: VARIABLES_CSS.to_owned(),
favicon: FAVICON.to_owned(),
favicon_png: FAVICON_PNG.to_owned(),
favicon_svg: FAVICON_SVG.to_owned(),
js: JS.to_owned(),
highlight_css: HIGHLIGHT_CSS.to_owned(),
tomorrow_night_css: TOMORROW_NIGHT_CSS.to_owned(),
@@ -168,9 +182,13 @@ mod tests {
fn theme_dir_overrides_defaults() {
let files = [
"index.hbs",
"head.hbs",
"redirect.hbs",
"header.hbs",
"favicon.png",
"favicon.svg",
"css/chrome.css",
"css/fonts.css",
"css/general.css",
"css/print.css",
"css/variables.css",
@@ -194,12 +212,15 @@ mod tests {
let empty = Theme {
index: Vec::new(),
head: Vec::new(),
redirect: Vec::new(),
header: Vec::new(),
chrome_css: Vec::new(),
general_css: Vec::new(),
print_css: Vec::new(),
variables_css: Vec::new(),
favicon: Vec::new(),
favicon_png: Vec::new(),
favicon_svg: Vec::new(),
js: Vec::new(),
highlight_css: Vec::new(),
tomorrow_night_css: Vec::new(),

View File

@@ -6,7 +6,7 @@ window.editors = [];
}
Array.from(document.querySelectorAll('.editable')).forEach(function(editable) {
let display_line_numbers = window.playpen_line_numbers || false;
let display_line_numbers = window.playground_line_numbers || false;
let editor = ace.edit(editable);
editor.setOptions({

View File

@@ -1,4 +1,4 @@
//! Theme dependencies for the playpen editor.
//! Theme dependencies for the playground editor.
pub static JS: &[u8] = include_bytes!("editor.js");
pub static ACE_JS: &[u8] = include_bytes!("ace.js");

12
src/theme/redirect.hbs Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Redirecting...</title>
<meta http-equiv="refresh" content="0;URL='{{url}}'">
<meta rel="canonical" href="{{url}}">
</head>
<body>
<p>Redirecting to... <a href="{{url}}">{{url}}</a>.</p>
</body>
</html>

View File

@@ -311,7 +311,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') { return; }
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; }
if (e.keyCode === ESCAPE_KEYCODE) {
e.preventDefault();

View File

@@ -28,17 +28,14 @@ pub fn write_file<P: AsRef<Path>>(build_dir: &Path, filename: P, content: &[u8])
/// ```rust
/// # use std::path::Path;
/// # use mdbook::utils::fs::path_to_root;
/// #
/// # fn main() {
/// let path = Path::new("some/relative/path");
/// assert_eq!(path_to_root(path), "../../");
/// # }
/// ```
///
/// **note:** it's not very fool-proof, if you find a situation where
/// it doesn't return the correct path.
/// Consider [submitting a new issue](https://github.com/rust-lang-nursery/mdBook/issues)
/// or a [pull-request](https://github.com/rust-lang-nursery/mdBook/pulls) to improve it.
/// 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
@@ -95,13 +92,15 @@ pub fn copy_files_except_ext(
from: &Path,
to: &Path,
recursive: bool,
avoid_dir: Option<&PathBuf>,
ext_blacklist: &[&str],
) -> Result<()> {
debug!(
"Copying all files from {} to {} (blacklist: {:?})",
"Copying all files from {} to {} (blacklist: {:?}), avoiding {:?}",
from.display(),
to.display(),
ext_blacklist
ext_blacklist,
avoid_dir
);
// Check that from and to are different
@@ -119,6 +118,12 @@ pub fn copy_files_except_ext(
continue;
}
if let Some(avoid) = avoid_dir {
if entry.path() == *avoid {
continue;
}
}
// check if output dir already exists
if !to.join(entry.file_name()).exists() {
fs::create_dir(&to.join(entry.file_name()))?;
@@ -128,6 +133,7 @@ pub fn copy_files_except_ext(
&from.join(entry.file_name()),
&to.join(entry.file_name()),
true,
avoid_dir,
ext_blacklist,
)?;
} else if metadata.is_file() {
@@ -171,6 +177,13 @@ pub fn copy_files_except_ext(
Ok(())
}
pub fn get_404_output_file(input_404: &Option<String>) -> String {
input_404
.as_ref()
.unwrap_or(&"404.md".to_string())
.replace(".md", ".html")
}
#[cfg(test)]
mod tests {
use super::copy_files_except_ext;
@@ -215,7 +228,7 @@ mod tests {
}
if let Err(e) =
copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, &["md"])
copy_files_except_ext(&tmp.path(), &tmp.path().join("output"), true, None, &["md"])
{
panic!("Error while executing the function:\n{:?}", e);
}

View File

@@ -2,10 +2,11 @@
pub mod fs;
mod string;
pub(crate) mod toml_ext;
use crate::errors::Error;
use regex::Regex;
use pulldown_cmark::{html, CowStr, Event, Options, Parser, Tag};
use pulldown_cmark::{html, CodeBlockKind, CowStr, Event, Options, Parser, Tag};
use std::borrow::Cow;
use std::fmt::Write;
@@ -158,7 +159,6 @@ fn adjust_links<'a>(event: Event<'a>, path: Option<&Path>) -> Event<'a> {
Event::Start(Tag::Image(link_type, fix(dest, path), title))
}
Event::Html(html) => Event::Html(fix_html(html, path)),
Event::InlineHtml(html) => Event::InlineHtml(fix_html(html, path)),
_ => event,
}
}
@@ -227,10 +227,10 @@ impl EventQuoteConverter {
fn clean_codeblock_headers(event: Event<'_>) -> Event<'_> {
match event {
Event::Start(Tag::CodeBlock(ref info)) => {
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info))) => {
let info: String = info.chars().filter(|ch| !ch.is_whitespace()).collect();
Event::Start(Tag::CodeBlock(CowStr::from(info)))
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(CowStr::from(info))))
}
_ => event,
}
@@ -272,7 +272,7 @@ fn convert_quotes_to_curly(original_text: &str) -> String {
pub fn log_backtrace(e: &Error) {
error!("Error: {}", e);
for cause in e.iter().skip(1) {
for cause in e.chain().skip(1) {
error!("\tCaused By: {}", cause);
}
}
@@ -359,8 +359,7 @@ more text with spaces
```
"#;
let expected =
r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre>
let expected = r#"<pre><code class="language-rust,no_run,should_panic,property_3"></code></pre>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);
@@ -373,8 +372,7 @@ more text with spaces
```
"#;
let expected =
r#"<pre><code class="language-rust,no_run,,,should_panic,,property_3"></code></pre>
let expected = r#"<pre><code class="language-rust,no_run,,,should_panic,,property_3"></code></pre>
"#;
assert_eq!(render_markdown(input, false), expected);
assert_eq!(render_markdown(input, true), expected);

View File

@@ -1,4 +1,3 @@
use itertools::Itertools;
use regex::Regex;
use std::ops::Bound::{Excluded, Included, Unbounded};
use std::ops::RangeBounds;
@@ -10,11 +9,17 @@ pub fn take_lines<R: RangeBounds<usize>>(s: &str, range: R) -> String {
Included(&n) => n,
Unbounded => 0,
};
let mut lines = s.lines().skip(start);
let lines = s.lines().skip(start);
match range.end_bound() {
Excluded(end) => lines.take(end.saturating_sub(start)).join("\n"),
Included(end) => lines.take((end + 1).saturating_sub(start)).join("\n"),
Unbounded => lines.join("\n"),
Excluded(end) => lines
.take(end.saturating_sub(start))
.collect::<Vec<_>>()
.join("\n"),
Included(end) => lines
.take((end + 1).saturating_sub(start))
.collect::<Vec<_>>()
.join("\n"),
Unbounded => lines.collect::<Vec<_>>().join("\n"),
}
}
@@ -43,11 +48,9 @@ pub fn take_anchored_lines(s: &str, anchor: &str) -> String {
}
}
}
} else {
if let Some(cap) = ANCHOR_START.captures(l) {
if &cap["anchor_name"] == anchor {
anchor_found = true;
}
} else if let Some(cap) = ANCHOR_START.captures(l) {
if &cap["anchor_name"] == anchor {
anchor_found = true;
}
}
}
@@ -96,16 +99,14 @@ pub fn take_rustdoc_include_anchored_lines(s: &str, anchor: &str) -> String {
}
}
}
} else {
if let Some(cap) = ANCHOR_START.captures(l) {
if &cap["anchor_name"] == anchor {
within_anchored_section = true;
}
} else if !ANCHOR_END.is_match(l) {
output.push_str("# ");
output.push_str(l);
output.push_str("\n");
} else if let Some(cap) = ANCHOR_START.captures(l) {
if &cap["anchor_name"] == anchor {
within_anchored_section = true;
}
} else if !ANCHOR_END.is_match(l) {
output.push_str("# ");
output.push_str(l);
output.push_str("\n");
}
}

130
src/utils/toml_ext.rs Normal file
View File

@@ -0,0 +1,130 @@
use toml::value::{Table, Value};
pub(crate) trait TomlExt {
fn read(&self, key: &str) -> Option<&Value>;
fn read_mut(&mut self, key: &str) -> Option<&mut Value>;
fn insert(&mut self, key: &str, value: Value);
fn delete(&mut self, key: &str) -> Option<Value>;
}
impl TomlExt for Value {
fn read(&self, key: &str) -> Option<&Value> {
if let Some((head, tail)) = split(key) {
self.get(head)?.read(tail)
} else {
self.get(key)
}
}
fn read_mut(&mut self, key: &str) -> Option<&mut Value> {
if let Some((head, tail)) = split(key) {
self.get_mut(head)?.read_mut(tail)
} else {
self.get_mut(key)
}
}
fn insert(&mut self, key: &str, value: Value) {
if !self.is_table() {
*self = Value::Table(Table::new());
}
let table = self.as_table_mut().expect("unreachable");
if let Some((head, tail)) = split(key) {
table
.entry(head)
.or_insert_with(|| Value::Table(Table::new()))
.insert(tail, value);
} else {
table.insert(key.to_string(), value);
}
}
fn delete(&mut self, key: &str) -> Option<Value> {
if let Some((head, tail)) = split(key) {
self.get_mut(head)?.delete(tail)
} else if let Some(table) = self.as_table_mut() {
table.remove(key)
} else {
None
}
}
}
fn split(key: &str) -> Option<(&str, &str)> {
let ix = key.find(".")?;
let (head, tail) = key.split_at(ix);
// splitting will leave the "."
let tail = &tail[1..];
Some((head, tail))
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn read_simple_table() {
let src = "[table]";
let value = Value::from_str(src).unwrap();
let got = value.read("table").unwrap();
assert!(got.is_table());
}
#[test]
fn read_nested_item() {
let src = "[table]\nnested=true";
let value = Value::from_str(src).unwrap();
let got = value.read("table.nested").unwrap();
assert_eq!(got, &Value::Boolean(true));
}
#[test]
fn insert_item_at_top_level() {
let mut value = Value::Table(Table::default());
let item = Value::Boolean(true);
value.insert("first", item.clone());
assert_eq!(value.get("first").unwrap(), &item);
}
#[test]
fn insert_nested_item() {
let mut value = Value::Table(Table::default());
let item = Value::Boolean(true);
value.insert("first.second", item.clone());
let inserted = value.read("first.second").unwrap();
assert_eq!(inserted, &item);
}
#[test]
fn delete_a_top_level_item() {
let src = "top = true";
let mut value = Value::from_str(src).unwrap();
let got = value.delete("top").unwrap();
assert_eq!(got, Value::Boolean(true));
}
#[test]
fn delete_a_nested_item() {
let src = "[table]\n nested = true";
let mut value = Value::from_str(src).unwrap();
let got = value.delete("table.nested").unwrap();
assert_eq!(got, Value::Boolean(true));
}
}

View File

@@ -8,28 +8,33 @@ use tempfile::{Builder as TempFileBuilder, TempDir};
#[test]
fn passing_alternate_backend() {
let (md, _temp) = dummy_book_with_backend("passing", success_cmd());
let (md, _temp) = dummy_book_with_backend("passing", success_cmd(), false);
md.build().unwrap();
}
#[test]
fn failing_alternate_backend() {
let (md, _temp) = dummy_book_with_backend("failing", fail_cmd());
let (md, _temp) = dummy_book_with_backend("failing", fail_cmd(), false);
md.build().unwrap_err();
}
#[test]
fn missing_backends_arent_fatal() {
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn");
fn missing_backends_are_fatal() {
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", false);
assert!(md.build().is_err());
}
#[test]
fn missing_optional_backends_are_not_fatal() {
let (md, _temp) = dummy_book_with_backend("missing", "trduyvbhijnorgevfuhn", true);
assert!(md.build().is_ok());
}
#[test]
fn alternate_backend_with_arguments() {
let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!");
let (md, _temp) = dummy_book_with_backend("arguments", "echo Hello World!", false);
md.build().unwrap();
}
@@ -56,7 +61,7 @@ fn backends_receive_render_context_via_stdin() {
let out_file = temp.path().join("out.txt");
let cmd = tee_command(&out_file);
let (md, _temp) = dummy_book_with_backend("cat-to-file", &cmd);
let (md, _temp) = dummy_book_with_backend("cat-to-file", &cmd, false);
assert!(!out_file.exists());
md.build().unwrap();
@@ -66,7 +71,11 @@ fn backends_receive_render_context_via_stdin() {
assert!(got.is_ok());
}
fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) {
fn dummy_book_with_backend(
name: &str,
command: &str,
backend_is_optional: bool,
) -> (MDBook, TempDir) {
let temp = TempFileBuilder::new().prefix("mdbook").tempdir().unwrap();
let mut config = Config::default();
@@ -74,6 +83,12 @@ fn dummy_book_with_backend(name: &str, command: &str) -> (MDBook, TempDir) {
.set(format!("output.{}.command", name), command)
.unwrap();
if backend_is_optional {
config
.set(format!("output.{}.optional", name), true)
.unwrap();
}
let md = MDBook::init(temp.path())
.with_config(config)
.build()

View File

@@ -4,12 +4,12 @@
// Not all features are used in all test crates, so...
#![allow(dead_code, unused_variables, unused_imports, unused_extern_crates)]
use anyhow::Context;
use mdbook::errors::*;
use mdbook::MDBook;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use mdbook::MDBook;
use tempfile::{Builder as TempFileBuilder, TempDir};
use walkdir::WalkDir;
@@ -43,10 +43,10 @@ impl DummyBook {
let temp = TempFileBuilder::new()
.prefix("dummy_book-")
.tempdir()
.chain_err(|| "Unable to create temp directory")?;
.with_context(|| "Unable to create temp directory")?;
let dummy_book_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/dummy_book");
recursive_copy(&dummy_book_root, temp.path()).chain_err(|| {
recursive_copy(&dummy_book_root, temp.path()).with_context(|| {
"Couldn't copy files into a \
temporary directory"
})?;
@@ -113,7 +113,7 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
let to = to.as_ref();
for entry in WalkDir::new(&from) {
let entry = entry.chain_err(|| "Unable to inspect directory entry")?;
let entry = entry.with_context(|| "Unable to inspect directory entry")?;
let original_location = entry.path();
let relative = original_location
@@ -123,11 +123,11 @@ fn recursive_copy<A: AsRef<Path>, B: AsRef<Path>>(from: A, to: B) -> Result<()>
if original_location.is_file() {
if let Some(parent) = new_location.parent() {
fs::create_dir_all(parent).chain_err(|| "Couldn't create directory")?;
fs::create_dir_all(parent).with_context(|| "Couldn't create directory")?;
}
fs::copy(&original_location, &new_location)
.chain_err(|| "Unable to copy file contents")?;
.with_context(|| "Unable to copy file contents")?;
}
}

View File

@@ -1,6 +1,9 @@
# Summary
[Dummy Book](README.md)
---
[Introduction](intro.md)
- [First Chapter](first/index.md)

View File

@@ -2,4 +2,4 @@
This makes sure you can insert runnable Rust files.
{{#playpen example.rs}}
{{#playground example.rs}}

View File

@@ -24,6 +24,12 @@ fn base_mdbook_init_should_create_default_content() {
println!("{}", target.display());
assert!(target.exists(), "{} doesn't exist", file);
}
let contents = fs::read_to_string(temp.path().join("book.toml")).unwrap();
assert_eq!(
contents,
"[book]\nauthors = []\nlanguage = \"en\"\nmultilingual = false\nsrc = \"src\"\n"
);
}
/// Run `mdbook init` in a directory containing a SUMMARY.md should create the

View File

@@ -28,11 +28,9 @@ macro_rules! summary_md_test {
.unwrap();
if let Err(e) = book::parse_summary(&content) {
use error_chain::ChainedError;
eprintln!("Error parsing {}", filename.display());
eprintln!();
eprintln!("{}", e.display_chain());
eprintln!("{:?}", e);
panic!();
}
}

View File

@@ -5,16 +5,18 @@ mod dummy_book;
use crate::dummy_book::{assert_contains_strings, assert_doesnt_contain_strings, DummyBook};
use anyhow::Context;
use mdbook::config::Config;
use mdbook::errors::*;
use mdbook::utils::fs::write_file;
use mdbook::MDBook;
use select::document::Document;
use select::predicate::{Class, Name, Predicate};
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::path::{Component, Path, PathBuf};
use tempfile::Builder as TempFileBuilder;
use walkdir::{DirEntry, WalkDir};
@@ -133,18 +135,18 @@ fn check_correct_relative_links_in_print_page() {
}
#[test]
fn rendered_code_has_playpen_stuff() {
fn rendered_code_has_playground_stuff() {
let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap();
md.build().unwrap();
let nested = temp.path().join("book/first/nested.html");
let playpen_class = vec![r#"class="playpen""#];
let playground_class = vec![r#"class="playground""#];
assert_contains_strings(nested, &playpen_class);
assert_contains_strings(nested, &playground_class);
let book_js = temp.path().join("book/book.js");
assert_contains_strings(book_js, &[".playpen"]);
assert_contains_strings(book_js, &[".playground"]);
}
#[test]
@@ -247,13 +249,13 @@ fn entry_ends_with(entry: &DirEntry, ending: &str) -> bool {
fn root_index_html() -> Result<Document> {
let temp = DummyBook::new()
.build()
.chain_err(|| "Couldn't create the dummy book")?;
.with_context(|| "Couldn't create the dummy book")?;
MDBook::load(temp.path())?
.build()
.chain_err(|| "Book building failed")?;
.with_context(|| "Book building failed")?;
let index_page = temp.path().join("book").join("index.html");
let html = fs::read_to_string(&index_page).chain_err(|| "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()))
}
@@ -306,7 +308,7 @@ fn check_first_toc_level() {
#[test]
fn check_spacers() {
let doc = root_index_html().unwrap();
let should_be = 1;
let should_be = 2;
let num_spacers = doc
.find(Class("chapter").descendant(Name("li").and(Class("spacer"))))
@@ -342,23 +344,23 @@ fn create_missing_file_with_config() {
assert!(temp.path().join("src").join("intro.md").exists());
}
/// This makes sure you can include a Rust file with `{{#playpen example.rs}}`.
/// This makes sure you can include a Rust file with `{{#playground example.rs}}`.
/// Specification is in `book-example/src/format/rust.md`
#[test]
fn able_to_include_playpen_files_in_chapters() {
fn able_to_include_playground_files_in_chapters() {
let temp = DummyBook::new().build().unwrap();
let md = MDBook::load(temp.path()).unwrap();
md.build().unwrap();
let second = temp.path().join("book/second.html");
let playpen_strings = &[
r#"class="playpen""#,
let playground_strings = &[
r#"class="playground""#,
r#"println!(&quot;Hello World!&quot;);"#,
];
assert_contains_strings(&second, playpen_strings);
assert_doesnt_contain_strings(&second, &["{{#playpen example.rs}}"]);
assert_contains_strings(&second, playground_strings);
assert_doesnt_contain_strings(&second, &["{{#playground example.rs}}"]);
}
/// This makes sure you can include a Rust file with `{{#include ../SUMMARY.md}}`.
@@ -490,12 +492,15 @@ fn markdown_options() {
"<td>bim</td>",
],
);
assert_contains_strings(&path, &[
r##"<sup class="footnote-reference"><a href="#1">1</a></sup>"##,
r##"<sup class="footnote-reference"><a href="#word">2</a></sup>"##,
r##"<div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>"##,
r##"<div class="footnote-definition" id="word"><sup class="footnote-definition-label">2</sup>"##,
]);
assert_contains_strings(
&path,
&[
r##"<sup class="footnote-reference"><a href="#1">1</a></sup>"##,
r##"<sup class="footnote-reference"><a href="#word">2</a></sup>"##,
r##"<div class="footnote-definition" id="1"><sup class="footnote-definition-label">1</sup>"##,
r##"<div class="footnote-definition" id="word"><sup class="footnote-definition-label">2</sup>"##,
],
);
assert_contains_strings(&path, &["<del>strikethrough example</del>"]);
assert_contains_strings(
&path,
@@ -507,6 +512,42 @@ fn markdown_options() {
);
}
#[test]
fn redirects_are_emitted_correctly() {
let temp = DummyBook::new().build().unwrap();
let mut md = MDBook::load(temp.path()).unwrap();
// override the "outputs.html.redirect" table
let redirects: HashMap<PathBuf, String> = vec![
(PathBuf::from("/overview.html"), String::from("index.html")),
(
PathBuf::from("/nexted/page.md"),
String::from("https://rust-lang.org/"),
),
]
.into_iter()
.collect();
md.config.set("output.html.redirect", &redirects).unwrap();
md.build().unwrap();
for (original, redirect) in &redirects {
let mut redirect_file = md.build_dir_for("html");
// append everything except the bits that make it absolute
// (e.g. "/" or "C:\")
redirect_file.extend(remove_absolute_components(&original));
let contents = fs::read_to_string(&redirect_file).unwrap();
assert!(contents.contains(redirect));
}
}
fn remove_absolute_components(path: &Path) -> impl Iterator<Item = Component> + '_ {
path.components().skip_while(|c| match c {
Component::Prefix(_) | Component::RootDir => true,
_ => false,
})
}
#[cfg(feature = "search")]
mod search {
use crate::dummy_book::DummyBook;

View File

@@ -6350,6 +6350,7 @@
}
}
},
"lang": "English",
"pipeline": [
"trimmer",
"stopWordFilter",